diff --git a/lib/XMLHttpRequest.js b/lib/XMLHttpRequest.js index 372d3c5..73eb896 100644 --- a/lib/XMLHttpRequest.js +++ b/lib/XMLHttpRequest.js @@ -12,7 +12,6 @@ */ var fs = require('fs'); -var Url = require('url'); var spawn = require('child_process').spawn; /** @@ -37,7 +36,34 @@ XMLHttpRequest.XMLHttpRequest = XMLHttpRequest; function XMLHttpRequest(opts) { "use strict"; - opts = opts || {}; + // defines a list of default options to prevent parameters pollution + var default_options = { + pfx: undefined, + key: undefined, + passphrase: undefined, + cert: undefined, + ca: undefined, + ciphers: undefined, + rejectUnauthorized: true, + autoUnref: false, + agent: undefined, + max_redirects: 20, + allowFileSystemResources: true, + maxRedirects: 20, // Chrome standard + origin: undefined + }; + + opts = Object.assign(default_options, opts); + + var sslOptions = { + pfx: opts.pfx, + key: opts.key, + passphrase: opts.passphrase, + cert: opts.cert, + ca: opts.ca, + ciphers: opts.ciphers, + rejectUnauthorized: opts.rejectUnauthorized !== false + }; /** * Private variables @@ -46,6 +72,12 @@ function XMLHttpRequest(opts) { var http = require('http'); var https = require('https'); + var max_redirects = opts.maxRedirects; + if (typeof max_redirects !== 'number' || Number.isNaN(max_redirects)) max_redirects = 20; + else max_redirects = Math.max(max_redirects, 0); + + var redirect_count = 0; + // Holds http.js objects var request; var response; @@ -130,6 +162,7 @@ function XMLHttpRequest(opts) { // Result & response this.responseText = ""; this.responseXML = ""; + this.responseURL = ""; this.response = Buffer.alloc(0); this.status = null; this.statusText = null; @@ -256,13 +289,21 @@ function XMLHttpRequest(opts) { } settings = { - "method": method, - "url": url.toString(), + "method": method.toUpperCase(), + "url": url, "async": (typeof async !== "boolean" ? true : async), "user": user || null, "password": password || null }; + // parse origin + try { + settings.origin = new URL(opts.origin); + } + catch (e) { + settings.origin = null; + } + setState(this.OPENED); }; @@ -305,10 +346,13 @@ function XMLHttpRequest(opts) { * @return string Text of the header or null if it doesn't exist. */ this.getResponseHeader = function(header) { + // in case of local request, headers are not present if (typeof header === "string" && this.readyState > this.OPENED && response.headers[header.toLowerCase()] && !errorFlag + && response + && response.headers ) { return response.headers[header.toLowerCase()]; } @@ -322,7 +366,8 @@ function XMLHttpRequest(opts) { * @return string A string with all response headers separated by CR+LF */ this.getAllResponseHeaders = function() { - if (this.readyState < this.HEADERS_RECEIVED || errorFlag) { + // in case of local request, headers are not present + if (this.readyState < this.HEADERS_RECEIVED || errorFlag || !response || !response.headers) { return ""; } var result = ""; @@ -333,7 +378,7 @@ function XMLHttpRequest(opts) { result += i + ": " + response.headers[i] + "\r\n"; } } - return result.substr(0, result.length - 2); + return result.slice(0, -2); }; /** @@ -351,6 +396,44 @@ function XMLHttpRequest(opts) { return ""; }; + /** + * Convert from Data URI to Buffer + * @param {URL} url URI to parse + * @returns {Buffer} buffer + */ + + var bufferFromDataUri = function (url) { + // Triming from original url object for more consistency + var data = url.href.slice(5); + + // separator between header and actual data + var parts = data.split(",", 2); + + if (parts.length < 2) throw "Invalid URL"; + + // check if header part has base64 (from 2nd header onwards) + var base64 = parts[0].split(";").some(function (dataHeader, index) { + return index > 0 && dataHeader.toLowerCase() === "base64"; + }); + + var responseData, inputData = decodeURIComponent(parts[1]); + + if (base64) { + // remove any ASCII whitespaces + inputData = inputData.replace(/(\s|\t|\r|\n|\v|\f)+/g, ""); + // check padding amount + let padding = inputData.match(/=*$/)[0]; + if (padding.length + (inputData.length - padding.length) % 4 > 4) throw "invalid padding"; + inputData = inputData.slice(0, inputData.length - padding.length); + responseData = Buffer.from(inputData, "base64"); + if (responseData.toString("base64").replace(/=+$/, "") !== inputData) throw "malformed base64 encoding"; + return responseData; + } + else { + return Buffer.from(inputData); + } + } + /** * Sends the request to the server. * @@ -365,20 +448,37 @@ function XMLHttpRequest(opts) { throw new Error("INVALID_STATE_ERR: send has already been called"); } - var ssl = false, local = false; - var url = Url.parse(settings.url); + var isSsl = false, isLocal = false, isDataUri = false; + var url; + try { + if (settings.origin) { + url = new URL(settings.url, settings.origin); + } + else { + url = new URL(settings.url); + } + settings.url = url.href; + } + catch (e) { + // URL parsing throws TypeError, here we only want to take its message + self.handleError(new Error(e.message)); + return; + } var host; // Determine the server switch (url.protocol) { case 'https:': - ssl = true; + isSsl = true; // SSL & non-SSL both need host, no break here. case 'http:': host = url.hostname; break; + case 'data:': + isDataUri = true; + case 'file:': - local = true; + isLocal = true; break; case undefined: @@ -391,7 +491,25 @@ function XMLHttpRequest(opts) { } // Load files off the local filesystem (file://) - if (local) { + // or data from Data URI (data:) + if (isLocal) { + if (isDataUri) try { + self.status = 200; + self.responseURL = settings.url; + self.createFileOrSyncResponse(bufferFromDataUri(url)); + setState(self.DONE); + return; + } + catch (e) { + self.handleError(new Error("Invalid data URI")); + return; + } + + if (!opts.allowFileSystemResources) { + self.handleError(new Error("Not allowed to load local resource: " + url.href)); + return; + } + if (settings.method !== "GET") { throw new Error("XMLHttpRequest: Only GET method is supported"); } @@ -402,6 +520,7 @@ function XMLHttpRequest(opts) { self.handleError(error, error.errno || -1); } else { self.status = 200; + self.responseURL = settings.url; // Use self.responseType to create the correct self.responseType, self.response. self.createFileOrSyncResponse(data); setState(self.DONE); @@ -412,6 +531,7 @@ function XMLHttpRequest(opts) { this.status = 200; const syncData = fs.readFileSync(unescape(url.pathname)); // Use self.responseType to create the correct self.responseType, self.response. + this.responseURL = settings.url; this.createFileOrSyncResponse(syncData); setState(self.DONE); } catch(e) { @@ -424,22 +544,22 @@ function XMLHttpRequest(opts) { // Default to port 80. If accessing localhost on another port be sure // to use http://localhost:port/path - var port = url.port || (ssl ? 443 : 80); + var port = url.port || (isSsl ? 443 : 80); // Add query string if one is used - var uri = url.pathname + (url.search ? url.search : ''); + var uri = url.pathname + (url.search || ''); // Set the Host header or the server may reject the request headers["Host"] = host; - if (!((ssl && port === 443) || port === 80)) { + if (!((isSsl && port === 443) || port === 80)) { headers["Host"] += ':' + url.port; } // Set Basic Auth if necessary if (settings.user) { - if (typeof settings.password == "undefined") { + if (typeof settings.password === "undefined") { settings.password = ""; } - var authBuf = new Buffer(settings.user + ":" + settings.password); + var authBuf = Buffer.from(settings.user + ":" + settings.password); headers["Authorization"] = "Basic " + authBuf.toString("base64"); } @@ -459,32 +579,21 @@ function XMLHttpRequest(opts) { headers["Content-Length"] = 0; } - var agent = opts.agent || false; var options = { host: host, port: port, path: uri, method: settings.method, headers: headers, - agent: agent + agent: opts.agent || false }; - if (ssl) { - options.pfx = opts.pfx; - options.key = opts.key; - options.passphrase = opts.passphrase; - options.cert = opts.cert; - options.ca = opts.ca; - options.ciphers = opts.ciphers; - options.rejectUnauthorized = opts.rejectUnauthorized === false ? false : true; - } - // Reset error flag errorFlag = false; // Handle async requests if (settings.async) { // Use the proper protocol - var doRequest = ssl ? https.request : http.request; + var doRequest = isSsl ? https.request : http.request; // Request is being sent, set send flag sendFlag = true; @@ -494,45 +603,67 @@ function XMLHttpRequest(opts) { // Handler for the response var responseHandler = function(resp) { - // Set response var to the response we got back - // This is so it remains accessable outside this scope - response = resp; - // Collect buffers and concatenate once. - const buffers = []; // Check for redirect - // @TODO Prevent looped redirects - if (response.statusCode === 302 || response.statusCode === 303 || response.statusCode === 307) { + if ( + resp.statusCode === 301 || + resp.statusCode === 302 || + resp.statusCode === 303 || + resp.statusCode === 307 || + resp.statusCode === 308 + ) { + ++redirect_count; + // end the response + resp.destroy(); + if (redirect_count > max_redirects) { + self.handleError(new Error("Too many redirects")); + return; + } // Change URL to the redirect location - settings.url = response.headers.location; - var url = Url.parse(settings.url); - // Set host var in case it's used later + var url; + try { + url = new URL(resp.headers.location, settings.url); + // reject redirects to any protocols other than http and https + if (url.protocol !== "https:" && url.protocol !== "http:") throw "bad protocol"; + settings.url = url.href; + } + catch (e) { + self.handleError(new Error("Unsafe redirect")); + return; + } + // change request options again to match with new redirect protocol + isSsl = url.protocol === "https:"; + doRequest = isSsl ? https.request : http.request; + + // Set host and port var in case it's used later host = url.hostname; + port = url.port || (isSsl ? 443 : 80); + + headers["Host"] = host; + if (!((isSsl && port === 443) || port === 80)) { + headers["Host"] += ':' + url.port; + } + // Options for the new request var newOptions = { hostname: url.hostname, - port: url.port, - path: url.path, - method: response.statusCode === 303 ? 'GET' : settings.method, + port: port, + path: url.pathname + (url.search || ''), + method: resp.statusCode === 303 ? 'GET' : settings.method, headers: headers }; - if (ssl) { - newOptions.pfx = opts.pfx; - newOptions.key = opts.key; - newOptions.passphrase = opts.passphrase; - newOptions.cert = opts.cert; - newOptions.ca = opts.ca; - newOptions.ciphers = opts.ciphers; - newOptions.rejectUnauthorized = opts.rejectUnauthorized === false ? false : true; - } - // Issue the new request - request = doRequest(newOptions, responseHandler).on('error', errorHandler); - request.end(); + createRequest(newOptions); // @TODO Check if an XHR event needs to be fired here return; } + // Set response var to the response we got back + // This is so it remains accessable outside this scope + response = resp; + // Collect buffers and concatenate once. + const buffers = []; + setState(self.HEADERS_RECEIVED); // When responseType is 'text' or '', self.responseText will be utf8 decoded text. @@ -564,7 +695,7 @@ function XMLHttpRequest(opts) { if (sendFlag) { setState(self.LOADING); } - }); + }.bind(response)); response.on('end', function() { if (sendFlag) { @@ -573,14 +704,16 @@ function XMLHttpRequest(opts) { sendFlag = false; // Create the correct response for responseType. self.createResponse(buffers); + self.statusText = this.statusMessage; + self.responseURL = settings.url; // Discard the 'end' event if the connection has been aborted setState(self.DONE); } - }); + }.bind(response)); response.on('error', function(error) { self.handleError(error); - }); + }.bind(response)); } // Error handler for the request @@ -592,21 +725,28 @@ function XMLHttpRequest(opts) { self.handleError(error); } - // Create the request - request = doRequest(options, responseHandler).on('error', errorHandler); + var createRequest = function (opt) { + opt = Object.assign({}, opt); + if (isSsl) Object.assign(opt, sslOptions); - if (opts.autoUnref) { - request.on('socket', (socket) => { - socket.unref(); - }); - } + request = doRequest(opt, responseHandler).on('error', errorHandler); - // Node 0.4 and later won't accept empty data. Make sure it's needed. - if (data) { - request.write(data); + if (opts.autoUnref) { + request.on('socket', function (socket) { + socket.unref(); + }); + } + + // Node 0.4 and later won't accept empty data. Make sure it's needed. + if (data) { + request.write(data); + } + + request.end(); } - request.end(); + // Create the request + createRequest(options); self.dispatchEvent("loadstart"); } else { // Synchronous @@ -630,29 +770,70 @@ function XMLHttpRequest(opts) { + " }" + " return result;" + "};" - + "var doRequest = http" + (ssl ? "s" : "") + ".request;" + + "var doRequest = http" + (isSsl ? "s" : "") + ".request;" + + "var isSsl = " + !!isSsl + ";" + "var options = " + JSON.stringify(options) + ";" + + "var sslOptions = " + JSON.stringify(sslOptions) + ";" + "var responseData = Buffer.alloc(0);" + "var buffers = [];" - + "var req = doRequest(options, function(response) {" - + " response.on('data', function(chunk) {" - + " buffers.push(chunk);" - + " });" - + " response.on('end', function() {" - + " responseData = concat(buffers);" - + " fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: {statusCode: response.statusCode, headers: response.headers, data: responseData.toString('utf8')}}), 'utf8');" - + " fs.unlinkSync('" + syncFile + "');" - + " });" - + " response.on('error', function(error) {" + + "var url = new URL(" + JSON.stringify(settings.url) + ");" + + "var max_redirects = " + max_redirects + ", redirects_count = 0;" + + "var makeRequest = function () {" + + " var opt = Object.assign({}, options);" + + " if (isSsl) Object.assign(opt, sslOptions);" + + " var req = doRequest(opt, function(response) {" + + " if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 303 || response.statusCode === 307 || response.statusCode === 308) {" + + " response.destroy();" + + " ++redirects_count;" + + " if (redirects_count > max_redirects) {" + + " fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR-REDIRECT: Too many redirects', 'utf8');" + + " fs.unlinkSync('" + syncFile + "');" + + " return;" + + " }" + + " try {" + + " url = new URL(response.headers.location, url);" + + " if (url.protocol !== 'https:' && url.protocol !== 'http:') throw 'bad protocol';" + + " }" + + " catch (e) {" + + " fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR-REDIRECT: Unsafe redirect', 'utf8');" + + " fs.unlinkSync('" + syncFile + "');" + + " return;" + + " };" + + " isSsl = url.protocol === 'https:';" + + " doRequest = isSsl ? https.request : http.request;" + + " var port = url.port;" + + " options = {" + + " hostname: url.hostname," + + " port: port," + + " path: url.pathname + (url.search || '')," + + " method: response.statusCode === 303 ? 'GET' : options.method," + + " headers: options.headers" + + " };" + + " options.headers['Host'] = url.host;" + + " if (!((isSsl && port === 443) || port === 80)) options.headers['Host'] += ':' + port;" + + " makeRequest();" + + " return;" + + " }" + + " response.on('data', function(chunk) {" + + " buffers.push(chunk);" + + " });" + + " response.on('end', function() {" + + " responseData = concat(buffers);" + + " fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: {url: url.href, statusCode: response.statusCode, statusText: response.statusMessage, headers: response.headers, data: responseData.toString('utf8')}}), 'utf8');" + + " fs.unlinkSync('" + syncFile + "');" + + " });" + + " response.on('error', function(error) {" + + " fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');" + + " fs.unlinkSync('" + syncFile + "');" + + " });" + + " }).on('error', function(error) {" + " fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');" + " fs.unlinkSync('" + syncFile + "');" + " });" - + "}).on('error', function(error) {" - + " fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');" - + " fs.unlinkSync('" + syncFile + "');" - + "});" - + (data ? "req.write('" + JSON.stringify(data).slice(1,-1).replace(/'/g, "\\'") + "');":"") - + "req.end();"; + + " " + (data ? "req.write('" + JSON.stringify(data).slice(1,-1).replace(/'/g, "\\'") + "');":"") + + " req.end();" + + "};" + + "makeRequest();" // Start the other Node Process, executing this string var syncProc = spawn(process.argv[0], ["-e", execString]); while(fs.existsSync(syncFile)) { @@ -663,14 +844,21 @@ function XMLHttpRequest(opts) { syncProc.stdin.end(); // Remove the temporary file fs.unlinkSync(contentFile); - if (self.responseText.match(/^NODE-XMLHTTPREQUEST-ERROR:/)) { + if (self.responseText.match(/^NODE-XMLHTTPREQUEST-ERROR(-REDIRECT){0,1}:/)) { // If the file returned an error, handle it - var errorObj = JSON.parse(self.responseText.replace(/^NODE-XMLHTTPREQUEST-ERROR:/, "")); - self.handleError(errorObj, 503); + if (self.responseText.startsWith('NODE-XMLHTTPREQUEST-ERROR-REDIRECT')) { + self.handleError(new Error(self.responseText.replace(/^NODE-XMLHTTPREQUEST-ERROR-REDIRECT: /, ""))); + } + else { + var errorObj = JSON.parse(self.responseText.replace(/^NODE-XMLHTTPREQUEST-ERROR:/, "")); + self.handleError(errorObj, 503); + } } else { // If the file returned okay, parse its data and move to the DONE state const resp = JSON.parse(self.responseText); self.status = resp.data.statusCode; + self.statusText = resp.data.statusText; + self.responseURL = resp.data.url; self.response = stringToBuffer(resp.data.data); // Use self.responseType to create the correct self.responseType, self.response, self.responseXML. self.createFileOrSyncResponse(self.response); @@ -681,7 +869,6 @@ function XMLHttpRequest(opts) { }; setState(self.DONE); } - } }; @@ -691,12 +878,14 @@ function XMLHttpRequest(opts) { */ this.handleError = function(error, status) { this.status = status || 0; - this.statusText = error; - this.responseText = error.stack; + this.statusText = error.message || ""; + this.responseText = ""; this.responseXML = ""; + this.responseURL = ""; this.response = Buffer.alloc(0); errorFlag = true; setState(this.DONE); + if (!settings.async) throw error; }; /** @@ -751,18 +940,19 @@ function XMLHttpRequest(opts) { * Dispatch any events, including both "on" methods and events attached using addEventListener. */ this.dispatchEvent = function (event) { + let argument = { type: event }; if (typeof self["on" + event] === "function") { if (this.readyState === this.DONE && settings.async) - setTimeout(function() { self["on" + event]() }, 0) + setTimeout(function() { self["on" + event](argument) }, 0) else - self["on" + event]() + self["on" + event](argument) } if (event in listeners) { for (let i = 0, len = listeners[event].length; i < len; i++) { if (this.readyState === this.DONE) - setTimeout(function() { listeners[event][i].call(self) }, 0) + setTimeout(function() { listeners[event][i].call(self, argument) }, 0) else - listeners[event][i].call(self) + listeners[event][i].call(self, argument) } } }; diff --git a/package.json b/package.json index 4bb493a..2b796fd 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "node": ">=12.0.0" }, "scripts": { - "test": "cd ./tests && node test-constants.js && node test-events.js && node test-exceptions.js && node test-headers.js && node test-redirect-302.js && node test-redirect-303.js && node test-redirect-307.js && node test-request-methods.js && node test-request-protocols-txt-data.js && node test-request-protocols-binary-data.js && node test-sync-response.js && node test-utf8-tearing.js && node test-keepalive.js" + "test": "cd ./tests && node run-test.js" }, "directories": { "lib": "./lib", diff --git a/tests/run-test.js b/tests/run-test.js new file mode 100644 index 0000000..1e00421 --- /dev/null +++ b/tests/run-test.js @@ -0,0 +1,50 @@ +var ignored_files = [ + "run-test.js", // this file + "server.js" +]; + +var spawnSync = require("child_process").spawnSync; +var fs = require("fs"); +var path = require("path"); + +// global flag to check if some of test fails, and will store location of failed test file +var fail_path = false; + +// function to read and conduct test case +var run_test = function (file) { + if (fail_path) return; + // logging + console.log("Running:", file); + + // spawn a nodejs process + var proc = spawnSync("node", [file]); + + if (proc.status === 0) { + console.log(proc.stdout.toString()); + console.log("--> PASSED"); + } + else { + fail_path = file; + console.log("--> TEST FAILED - CAUGHT ERROR:", proc.stderr.toString()); + } +} + +var check_dir = function (dirPath) { + if (fail_path) return; + var files = fs.readdirSync(dirPath); + + for (var file of files) { + // return early in case something fails + if (fail_path) return; + var full_path = path.join(dirPath, file); + if (fs.statSync(full_path).isDirectory()) check_dir(full_path); + else if (path.extname(file) === ".js" && !ignored_files.includes(full_path)) run_test(full_path); + } +} + +// start test +check_dir("./"); + +if (fail_path) throw new Error("Test failed at file: " + fail_path); + +console.log("ALL TESTS PASSED."); diff --git a/tests/server.js b/tests/server.js index 4649091..e76752b 100644 --- a/tests/server.js +++ b/tests/server.js @@ -3,6 +3,17 @@ var http = require("http"); var server = http.createServer(function (req, res) { switch (req.url) { + case "/": { + var body = "Hello World"; + res.writeHead(200, { + "Content-Type": "text/plain", + "Content-Length": Buffer.byteLength(body), + "Date": "Thu, 30 Aug 2012 18:17:53 GMT", + "Connection": "close" + }); + res.end(body); + return; + } case "/text": res.writeHead(200, {"Content-Type": "text/plain"}) res.end("Hello world!"); @@ -27,8 +38,15 @@ var server = http.createServer(function (req, res) { res.end(str); return; default: - res.writeHead(404, {"Content-Type": "text/plain"}) - res.end("Not found"); + if (req.url.startsWith('/redirectingResource/')) { + let remaining = req.url.replace(/^\/redirectingResource\/*/, "") - 1; + res.writeHead(301, {'Location': remaining ? ('http://localhost:8888/redirectingResource/' + remaining) : 'http://localhost:8888/'}); + res.end(); + } + else { + res.writeHead(404, {"Content-Type": "text/plain"}) + res.end("Not found"); + } } }).listen(8888); diff --git a/tests/test-data-uri.js b/tests/test-data-uri.js new file mode 100644 index 0000000..3344008 --- /dev/null +++ b/tests/test-data-uri.js @@ -0,0 +1,131 @@ +var sys = require("util") + , assert = require("assert") + , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest + , xhr; + +xhr = new XMLHttpRequest(); + +// define test data +var tests = [ + { + name: "Test plain URI Data", + data: "data:,Hello%20World", + output: "Hello World" + }, + { + name: "Test plain URI Data with spaces", + data: "data:, Hello World", + output: " Hello World" + }, + { + name: "Test plain URI Data with data URI headers", + data: "data:base64;example=1;args=2,Hello%20World", + output: "Hello World" + }, + { + name: "Test normal bass64-encoded data URI", + data: "data:text;base64,SGVsbG8gV29ybGQ=", + output: "Hello World" + }, + { + name: "Test normal bass64-encoded data URI with mixed space characters", + data: "data:text;base64,SGV sbG8gV\n29ybGQ=", + output: "Hello World" + }, + { + name: "Test normal bass64-encoded data URI with mixed space characters (url-encoded)", + data: "data:text;base64,SGV%20sbG8gV%0a29ybGQ=", + output: "Hello World" + }, + { + name: "Test normal bass64-encoded data URI with invalid characters", + data: "data:text;base64,SGV&&&&sbG8gV{29ybGQ=", + error: "Invalid data URI" + }, + { + name: "Test normal bass64-encoded data URI with invalid characters (url-encoded)", + data: "data:text;base64,SGV%26%26%26%26sbG8gV%7B29ybGQ%3D", + error: "Invalid data URI" + }, + { + name: "Test base64-encoded data with no paddings", + data: "data:text;base64,SGVsbG8gV29ybGQ", + output: "Hello World" + }, + { + name: "Test base64-encoded data with excessive paddings", + data: "data:text;base64,SGVsbG8gV29ybGQ==", + error: "Invalid data URI" + } +]; + +var tests_passed = 0; + +var runAsyncTest = function (test) { + console.log(" ASYNC"); + + xhr = new XMLHttpRequest; + xhr.open("get", test.data); + xhr.onreadystatechange = function () { + if (this.readyState === 4) { + if (test.error) { + assert.equal(xhr.status, 0); + assert.equal(xhr.statusText, test.error); + } + else { + assert.equal(xhr.status, 200); + assert.equal(xhr.responseText, test.output); + } + console.log(" --> SUCESS"); + ++tests_passed; + } + } + xhr.send(); +} + +var runSyncTest = function (test) { + console.log(" SYNC"); + + xhr = new XMLHttpRequest; + xhr.open("get", test.data, false); + try { + xhr.send(); + if (test.error) throw "Expected to fail, Success with " + e.responseText; + assert.equal(xhr.status, 200); + assert.equal(xhr.responseText, test.output); + } + catch (e) { + if (!test.error) throw "Expected to success, Caught error: " + e.toString() + assert.equal(xhr.status, 0); + assert.equal(e.message, test.error); + } + console.log(" --> SUCESS"); + ++tests_passed; +} + +var i = 0; + +var startTest = function () { + let test = tests[i]; + + if (!test) { + console.log("Done:", tests_passed === tests.length * 2 ? "PASS" : "FAILED"); + return; + } + + console.log(test.name); + + runAsyncTest(test); + + setTimeout(function () { + try { + runSyncTest(test); + } + catch (e) { console.error(e) }; + console.log(""); + ++i; + startTest(); + }, 500); +} + +startTest(); diff --git a/tests/test-disallow-fs-resources.js b/tests/test-disallow-fs-resources.js new file mode 100644 index 0000000..47a8696 --- /dev/null +++ b/tests/test-disallow-fs-resources.js @@ -0,0 +1,36 @@ +var sys = require("util") + , assert = require("assert") + , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest + , xhr; + +xhr = new XMLHttpRequest({ allowFileSystemResources: false }); + +xhr.onreadystatechange = function() { + if (this.readyState === 4) { + assert.equal(this.statusText, "Not allowed to load local resource: " + url); + assert.equal(this.status, 0); + try { runSync(); } catch (e) { + if (e instanceof assert.AssertionError) console.error(e); + } + } +}; + +// Async +var url = "file://" + __dirname + "/testdata.txt"; +xhr.open("GET", url); +xhr.send(); + +// Sync +var runSync = function() { + xhr = new XMLHttpRequest({ allowFileSystemResources: false }); + + xhr.onreadystatechange = function() { + if (this.readyState === 4) { + assert.equal(this.statusText, "Not allowed to load local resource: " + url); + assert.equal(this.status, 0); + console.log("done"); + } + }; + xhr.open("GET", url, false); + xhr.send(); +} diff --git a/tests/test-max-redirects.js b/tests/test-max-redirects.js new file mode 100644 index 0000000..d221001 --- /dev/null +++ b/tests/test-max-redirects.js @@ -0,0 +1,47 @@ +var sys = require("util") + , assert = require("assert") + , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest + , spawn = require('child_process').spawn; + +// Test server +var serverProcess = spawn(process.argv[0], [__dirname + "/server.js"], { stdio: 'inherit' }); + +var runTest = function () { + try { + let xhr = new XMLHttpRequest({ maxRedirects: 10 }); + xhr.open("GET", "http://localhost:8888/redirectingResource/10", false); + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + assert.equal(xhr.getRequestHeader('Location'), ''); + assert.equal(xhr.responseText, "Hello World"); + console.log("safe redirects count: done"); + } + }; + xhr.send(); + } catch(e) { + console.log("ERROR: Exception raised", e); + } + + try { + let xhr = new XMLHttpRequest({ maxRedirects: 10 }); + xhr.open("GET", "http://localhost:8888/redirectingResource/20", false); + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + assert.equal(xhr.statusText, 'Too many redirects'); + assert.equal(xhr.status, 0); + console.log("excessive redirects count: done"); + } + }; + xhr.send(); + } catch(e) { + if (e.message !== 'Too many redirects') console.log("ERROR: Exception raised", e); + } +} + +setTimeout(function () { + try { + runTest(); + } finally { + serverProcess.kill('SIGINT'); + } +}, 100); diff --git a/tests/test-perf.js b/tests/test-perf.js index 2bf9634..488b12a 100644 --- a/tests/test-perf.js +++ b/tests/test-perf.js @@ -12,7 +12,7 @@ const XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest; const supressConsoleOutput = false; function log (_) { if ( !supressConsoleOutput) - console.log(arguments); + console.log.apply(console, arguments); } var serverProcess; @@ -94,12 +94,14 @@ function createServer() { res.end(`success:len ${u8.length}`); }); } else { - if (!storage[req.url]) + if (!storage[req.url]) { res.writeHead(404, {"Content-Type": "text/plain; charset=utf8"}) res.end("Not in storage"); - - res.writeHead(200, {"Content-Type": "application/octet-stream"}) - res.end(storage[req.url]); + } + else { + res.writeHead(200, {"Content-Type": "application/octet-stream"}) + res.end(storage[req.url]); + } } }).listen(8888); process.on("SIGINT", function () { @@ -216,7 +218,7 @@ function afterUpload(r) { function afterDownload(ab) { clearTimeout(handle); console.log(`Download elapsed time:, ${Date.now() - _t0}ms`, ab.byteLength); - console.info(['...waiting to see elapsed time of download...']) + console.info('...waiting to see elapsed time of download...'); if (!success) throw new Error("Download has taken far too long!"); } diff --git a/tests/test-redirect-301.js b/tests/test-redirect-301.js new file mode 100644 index 0000000..0b3efe3 --- /dev/null +++ b/tests/test-redirect-301.js @@ -0,0 +1,41 @@ +var sys = require("util") + , assert = require("assert") + , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest + , xhr = new XMLHttpRequest() + , http = require("http"); + +// Test server +var server = http.createServer(function (req, res) { + if (req.url === '/redirectingResource') { + res.writeHead(301, {'Location': 'http://localhost:8000/'}); + res.end(); + return; + } + + var body = "Hello World"; + res.writeHead(200, { + "Content-Type": "text/plain", + "Content-Length": Buffer.byteLength(body), + "Date": "Thu, 30 Aug 2012 18:17:53 GMT", + "Connection": "close" + }); + res.write("Hello World"); + res.end(); + + this.close(); +}).listen(8000); + +xhr.onreadystatechange = function() { + if (this.readyState === 4) { + assert.equal(xhr.getRequestHeader('Location'), ''); + assert.equal(xhr.responseText, "Hello World"); + console.log("done"); + } +}; + +try { + xhr.open("GET", "http://localhost:8000/redirectingResource"); + xhr.send(); +} catch(e) { + console.log("ERROR: Exception raised", e); +} diff --git a/tests/test-redirect-302.js b/tests/test-redirect-302.js index 0b87192..ca69e0f 100644 --- a/tests/test-redirect-302.js +++ b/tests/test-redirect-302.js @@ -26,7 +26,7 @@ var server = http.createServer(function (req, res) { }).listen(8000); xhr.onreadystatechange = function() { - if (this.readyState == 4) { + if (this.readyState === 4) { assert.equal(xhr.getRequestHeader('Location'), ''); assert.equal(xhr.responseText, "Hello World"); console.log("done"); diff --git a/tests/test-redirect-303.js b/tests/test-redirect-303.js index aa85b2d..c6ce0cf 100644 --- a/tests/test-redirect-303.js +++ b/tests/test-redirect-303.js @@ -26,7 +26,7 @@ var server = http.createServer(function (req, res) { }).listen(8000); xhr.onreadystatechange = function() { - if (this.readyState == 4) { + if (this.readyState === 4) { assert.equal(xhr.getRequestHeader('Location'), ''); assert.equal(xhr.responseText, "Hello World"); console.log("done"); diff --git a/tests/test-redirect-307.js b/tests/test-redirect-307.js index a73819f..4eb4a2f 100644 --- a/tests/test-redirect-307.js +++ b/tests/test-redirect-307.js @@ -28,7 +28,7 @@ var server = http.createServer(function (req, res) { }).listen(8000); xhr.onreadystatechange = function() { - if (this.readyState == 4) { + if (this.readyState === 4) { assert.equal(xhr.getRequestHeader('Location'), ''); assert.equal(xhr.responseText, "Hello World"); console.log("done"); diff --git a/tests/test-redirect-308.js b/tests/test-redirect-308.js new file mode 100644 index 0000000..71543e0 --- /dev/null +++ b/tests/test-redirect-308.js @@ -0,0 +1,41 @@ +var sys = require("util") + , assert = require("assert") + , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest + , xhr = new XMLHttpRequest() + , http = require("http"); + +// Test server +var server = http.createServer(function (req, res) { + if (req.url === '/redirectingResource') { + res.writeHead(308, {'Location': 'http://localhost:8000/'}); + res.end(); + return; + } + + var body = "Hello World"; + res.writeHead(200, { + "Content-Type": "text/plain", + "Content-Length": Buffer.byteLength(body), + "Date": "Thu, 30 Aug 2012 18:17:53 GMT", + "Connection": "close" + }); + res.write("Hello World"); + res.end(); + + this.close(); +}).listen(8000); + +xhr.onreadystatechange = function() { + if (this.readyState === 4) { + assert.equal(xhr.getRequestHeader('Location'), ''); + assert.equal(xhr.responseText, "Hello World"); + console.log("done"); + } +}; + +try { + xhr.open("GET", "http://localhost:8000/redirectingResource"); + xhr.send(); +} catch(e) { + console.log("ERROR: Exception raised", e); +} diff --git a/tests/test-sync-response.js b/tests/test-sync-response.js index 316027e..734fe01 100644 --- a/tests/test-sync-response.js +++ b/tests/test-sync-response.js @@ -12,7 +12,7 @@ var assert = require("assert") const supressConsoleOutput = true; function log (_) { if ( !supressConsoleOutput) - console.log(arguments); + console.log.apply(console, arguments); } // Running a sync XHR and a webserver within the same process will cause a deadlock diff --git a/tests/test-unsafe-redirect.js b/tests/test-unsafe-redirect.js new file mode 100644 index 0000000..54fff68 --- /dev/null +++ b/tests/test-unsafe-redirect.js @@ -0,0 +1,41 @@ +var sys = require("util") + , assert = require("assert") + , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest + , xhr = new XMLHttpRequest() + , http = require("http"); + +// Test server +var server = http.createServer(function (req, res) { + if (req.url === '/redirectingResource') { + res.writeHead(301, {'Location': 'file:///etc/passwd'}); + res.end(); + } + else { + var body = "Hello World"; + res.writeHead(200, { + "Content-Type": "text/plain", + "Content-Length": Buffer.byteLength(body), + "Date": "Thu, 30 Aug 2012 18:17:53 GMT", + "Connection": "close" + }); + res.write("Hello World"); + res.end(); + } + + this.close(); +}).listen(8000); + +xhr.onreadystatechange = function() { + if (this.readyState === 4) { + assert.equal(xhr.statusText, "Unsafe redirect"); + assert.equal(xhr.status, 0); + console.log("done"); + } +}; + +try { + xhr.open("GET", "http://localhost:8000/redirectingResource"); + xhr.send(); +} catch(e) { + console.log("ERROR: Exception raised", e); +} diff --git a/tests/test-url-origin.js b/tests/test-url-origin.js new file mode 100644 index 0000000..c86abb7 --- /dev/null +++ b/tests/test-url-origin.js @@ -0,0 +1,47 @@ +var sys = require("util") + , assert = require("assert") + , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest + , spawn = require('child_process').spawn; + +// Test server +var serverProcess = spawn(process.argv[0], [__dirname + "/server.js"], { stdio: 'inherit' }); + +var runTest = function () { + try { + let xhr = new XMLHttpRequest({ origin: "http://localhost:8888" }); + xhr.open("GET", "text", false); + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + assert.equal(xhr.getResponseHeader('Content-Type'), 'text/plain'); + assert.equal(xhr.responseText, "Hello world!"); + console.log("origin test 1: done"); + } + }; + xhr.send(); + } catch(e) { + console.log("ERROR: Exception raised", e); + } + + try { + let xhr = new XMLHttpRequest({ origin: "http://localhost:8888/text" }); + xhr.open("GET", "", false); + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + assert.equal(xhr.getResponseHeader('Content-Type'), 'text/plain'); + assert.equal(xhr.responseText, "Hello world!"); + console.log("origin test 2: done"); + } + }; + xhr.send(); + } catch(e) { + console.log("ERROR: Exception raised", e); + } +} + +setTimeout(function () { + try { + runTest(); + } finally { + serverProcess.kill('SIGINT'); + } +}, 100);