From 276e088f847575bcedd382fab6530596b022a9b6 Mon Sep 17 00:00:00 2001 From: bhpsngum Date: Sun, 1 Dec 2024 08:52:39 +0900 Subject: [PATCH 01/15] Data URI support, redirect checks and more --- lib/XMLHttpRequest.js | 322 ++++++++++++++++++++++++++++++++---------- 1 file changed, 247 insertions(+), 75 deletions(-) diff --git a/lib/XMLHttpRequest.js b/lib/XMLHttpRequest.js index 372d3c5..d65db9b 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,33 @@ XMLHttpRequest.XMLHttpRequest = XMLHttpRequest; function XMLHttpRequest(opts) { "use strict"; - opts = opts || {}; + // defines a list of default options to prevent parameters pollution + var default_options = { + pfx: void 0, + key: void 0, + passphrase: void 0, + cert: void 0, + ca: void 0, + ciphers: void 0, + rejectUnauthorized: true, + autoUnref: false, + agent: void 0, + max_redirects: 20, + allowFileSystemSources: true, + maxRedirects: 20, // Chrome standard + } + + 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 +71,12 @@ function XMLHttpRequest(opts) { var http = require('http'); var https = require('https'); + var max_redirects = opts.maxRedirects; + if (max_redirects == null || 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 +161,7 @@ function XMLHttpRequest(opts) { // Result & response this.responseText = ""; this.responseXML = ""; + this.responseURL = ""; this.response = Buffer.alloc(0); this.status = null; this.statusText = null; @@ -256,7 +288,7 @@ function XMLHttpRequest(opts) { } settings = { - "method": method, + "method": method.toUpperCase(), "url": url.toString(), "async": (typeof async !== "boolean" ? true : async), "user": user || null, @@ -305,10 +337,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 +357,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 = ""; @@ -365,8 +401,15 @@ 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 ssl = false, local = false, datauri = false; + var url; + try { + url = new URL(settings.url); + } + catch (e) { + self.handleError(new Error("ERR_INVALID_URL")); + return; + } var host; // Determine the server switch (url.protocol) { @@ -377,6 +420,9 @@ function XMLHttpRequest(opts) { host = url.hostname; break; + case 'data:': + datauri = true; + case 'file:': local = true; break; @@ -391,7 +437,61 @@ function XMLHttpRequest(opts) { } // Load files off the local filesystem (file://) + // or data from Data URI (data:) if (local) { + if (datauri) try { + // Triming from original url string since + var data = url.href.substr(5, settings.url.length); + + // separator between header and actual data + var parts = data.split(",", 2); + + if (parts.length < 2) throw "Invalid URL"; + + var base64 = false; + // check if header part has base64 + var data_headers = parts[0].split(";"); + + // ignore first element since it's MIME type + for (var i = 1; i < data_headers.length; ++i) { + if (data_headers[i].toLowerCase() === "base64") { + base64 = true; + break; + } + } + + 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.substr(0, inputData.length - padding.length); + responseData = Buffer.from(inputData, "base64"); + if (responseData.toString("base64").replace(/=+$/, "") !== inputData) throw "malformed base64 encoding"; + } + else { + responseData = Buffer.from(inputData); + } + + self.status = 200; + self.responseURL = settings.url; + self.createFileOrSyncResponse(responseData); + setState(self.DONE); + return; + } + catch (e) { + self.handleError(new Error("ERR_INVALID_URL")); + return; + } + + if (!opts.allowFileSystemSources) { + 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 +502,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 +513,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) { @@ -426,7 +528,7 @@ function XMLHttpRequest(opts) { // to use http://localhost:port/path var port = url.port || (ssl ? 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; @@ -459,26 +561,15 @@ 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 @@ -500,35 +591,56 @@ function XMLHttpRequest(opts) { // 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 ( + response.statusCode === 301 || + response.statusCode === 302 || + response.statusCode === 303 || + response.statusCode === 307 || + response.statusCode === 308 + ) { + ++redirect_count; + // end the response + response.destroy(); + if (redirect_count > max_redirects) { + self.handleError(new Error("ERR_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(response.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("ERR_UNSAFE_REDIRECT")); + return; + } + // change request options again to match with new redirect protocol + ssl = url.protocol === "https:"; + doRequest = ssl ? https.request : http.request; + + // Set host and port var in case it's used later host = url.hostname; + port = url.port || (ssl ? 443 : 80); + + headers["Host"] = host; + if (!((ssl && port === 443) || port === 80)) { + headers["Host"] += ':' + url.port; + } + // Options for the new request var newOptions = { hostname: url.hostname, - port: url.port, - path: url.path, + port: port, + path: url.pathname + (url.search || ''), method: response.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; } @@ -573,6 +685,8 @@ function XMLHttpRequest(opts) { sendFlag = false; // Create the correct response for responseType. self.createResponse(buffers); + self.statusText = response.statusMessage; + self.responseURL = settings.url; // Discard the 'end' event if the connection has been aborted setState(self.DONE); } @@ -592,21 +706,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 (ssl) Object.assign(opt, sslOptions); - if (opts.autoUnref) { - request.on('socket', (socket) => { - socket.unref(); - }); - } + request = doRequest(opt, responseHandler).on('error', errorHandler); + + if (opts.autoUnref) { + request.on('socket', (socket) => { + socket.unref(); + }); + } + + // Node 0.4 and later won't accept empty data. Make sure it's needed. + if (data) { + request.write(data); + } - // 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 @@ -631,28 +752,69 @@ function XMLHttpRequest(opts) { + " return result;" + "};" + "var doRequest = http" + (ssl ? "s" : "") + ".request;" + + "var ssl = " + !!ssl + ";" + "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 (ssl) 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: ERR_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: ERR_UNSAFE_REDIRECT', 'utf8');" + + " fs.unlinkSync('" + syncFile + "');" + + " return;" + + " };" + + " ssl = url.protocol === 'https:';" + + " doRequest = ssl ? 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 (!((ssl && 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 +825,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); @@ -691,12 +860,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 +922,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) } } }; @@ -868,4 +1040,4 @@ function XMLHttpRequest(opts) { self.dispatchEvent("loadend"); } }; -}; +}; \ No newline at end of file From 9e0d5458d57b740bdf5d0d3df5701043a810dc2c Mon Sep 17 00:00:00 2001 From: bhpsngum Date: Mon, 2 Dec 2024 06:54:40 +0900 Subject: [PATCH 02/15] add tests --- lib/XMLHttpRequest.js | 37 +++--- tests/server.js | 22 +++- tests/test-allowFileSystemSources-flag.js | 36 ++++++ tests/test-data-uri.js | 131 ++++++++++++++++++++++ tests/test-max-redirects.js | 47 ++++++++ tests/test-perf.js | 14 ++- tests/test-redirect-301.js | 41 +++++++ tests/test-redirect-308.js | 41 +++++++ tests/test-sync-response.js | 2 +- tests/test-unsafe-redirect.js | 41 +++++++ 10 files changed, 385 insertions(+), 27 deletions(-) create mode 100644 tests/test-allowFileSystemSources-flag.js create mode 100644 tests/test-data-uri.js create mode 100644 tests/test-max-redirects.js create mode 100644 tests/test-redirect-301.js create mode 100644 tests/test-redirect-308.js create mode 100644 tests/test-unsafe-redirect.js diff --git a/lib/XMLHttpRequest.js b/lib/XMLHttpRequest.js index d65db9b..75fb9a5 100644 --- a/lib/XMLHttpRequest.js +++ b/lib/XMLHttpRequest.js @@ -585,22 +585,17 @@ 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 if ( - response.statusCode === 301 || - response.statusCode === 302 || - response.statusCode === 303 || - response.statusCode === 307 || - response.statusCode === 308 + resp.statusCode === 301 || + resp.statusCode === 302 || + resp.statusCode === 303 || + resp.statusCode === 307 || + resp.statusCode === 308 ) { ++redirect_count; // end the response - response.destroy(); + resp.destroy(); if (redirect_count > max_redirects) { self.handleError(new Error("ERR_TOO_MANY_REDIRECTS")); return; @@ -608,7 +603,7 @@ function XMLHttpRequest(opts) { // Change URL to the redirect location var url; try { - url = new URL(response.headers.location, settings.url); + 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; @@ -635,7 +630,7 @@ function XMLHttpRequest(opts) { hostname: url.hostname, port: port, path: url.pathname + (url.search || ''), - method: response.statusCode === 303 ? 'GET' : settings.method, + method: resp.statusCode === 303 ? 'GET' : settings.method, headers: headers }; @@ -645,6 +640,12 @@ function XMLHttpRequest(opts) { 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. @@ -676,7 +677,7 @@ function XMLHttpRequest(opts) { if (sendFlag) { setState(self.LOADING); } - }); + }.bind(response)); response.on('end', function() { if (sendFlag) { @@ -685,16 +686,16 @@ function XMLHttpRequest(opts) { sendFlag = false; // Create the correct response for responseType. self.createResponse(buffers); - self.statusText = response.statusMessage; + 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 @@ -860,7 +861,7 @@ function XMLHttpRequest(opts) { */ this.handleError = function(error, status) { this.status = status || 0; - this.statusText = error.message; + this.statusText = error.message || ""; this.responseText = ""; this.responseXML = ""; this.responseURL = ""; 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-allowFileSystemSources-flag.js b/tests/test-allowFileSystemSources-flag.js new file mode 100644 index 0000000..b647d01 --- /dev/null +++ b/tests/test-allowFileSystemSources-flag.js @@ -0,0 +1,36 @@ +var sys = require("util") + , assert = require("assert") + , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest + , xhr; + +xhr = new XMLHttpRequest({ allowFileSystemSources: 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({ allowFileSystemSources: 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-data-uri.js b/tests/test-data-uri.js new file mode 100644 index 0000000..8320df3 --- /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:text/plain;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: "ERR_INVALID_URL" + }, + { + name: "Test normal bass64-encoded data URI with invalid characters (url-encoded)", + data: "data:text;base64,SGV%26%26%26%26sbG8gV%7B29ybGQ%3D", + error: "ERR_INVALID_URL" + }, + { + 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: "ERR_INVALID_URL" + } +]; + +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(); \ No newline at end of file diff --git a/tests/test-max-redirects.js b/tests/test-max-redirects.js new file mode 100644 index 0000000..ccb43b6 --- /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, 'ERR_TOO_MANY_REDIRECTS'); + assert.equal(xhr.status, 0); + console.log("excessive redirects count: done"); + } + }; + xhr.send(); + } catch(e) { + + } +} + +setTimeout(function () { + try { + runTest(); + } finally { + serverProcess.kill('SIGINT'); + } +}, 100); \ No newline at end of file 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..0135c84 --- /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-308.js b/tests/test-redirect-308.js new file mode 100644 index 0000000..3d160de --- /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..03c10ff --- /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, "ERR_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); +} From d902038fd0036c50f85f6c90658c8fc3f9201f7c Mon Sep 17 00:00:00 2001 From: bhpsngum Date: Mon, 2 Dec 2024 06:58:00 +0900 Subject: [PATCH 03/15] Fix one of the tests --- tests/test-data-uri.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test-data-uri.js b/tests/test-data-uri.js index 8320df3..7160339 100644 --- a/tests/test-data-uri.js +++ b/tests/test-data-uri.js @@ -49,7 +49,7 @@ var tests = [ }, { name: "Test base64-encoded data with no paddings", - data: "data:text;base64,SGVsbG8gV29ybGQ=", + data: "data:text;base64,SGVsbG8gV29ybGQ", output: "Hello World" }, { From 2e240ff873bb47aa5db3ea6e814c17b74cc1b644 Mon Sep 17 00:00:00 2001 From: bhpsngum Date: Mon, 2 Dec 2024 11:11:46 +0900 Subject: [PATCH 04/15] allowFileSystemSources --> allowFileSystemResources --- lib/XMLHttpRequest.js | 4 ++-- ...ileSystemSources-flag.js => test-disallow-fs-resources.js} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename tests/{test-allowFileSystemSources-flag.js => test-disallow-fs-resources.js} (86%) diff --git a/lib/XMLHttpRequest.js b/lib/XMLHttpRequest.js index 75fb9a5..1c29d33 100644 --- a/lib/XMLHttpRequest.js +++ b/lib/XMLHttpRequest.js @@ -48,7 +48,7 @@ function XMLHttpRequest(opts) { autoUnref: false, agent: void 0, max_redirects: 20, - allowFileSystemSources: true, + allowFileSystemResources: true, maxRedirects: 20, // Chrome standard } @@ -487,7 +487,7 @@ function XMLHttpRequest(opts) { return; } - if (!opts.allowFileSystemSources) { + if (!opts.allowFileSystemResources) { self.handleError(new Error("Not allowed to load local resource: " + url.href)); return; } diff --git a/tests/test-allowFileSystemSources-flag.js b/tests/test-disallow-fs-resources.js similarity index 86% rename from tests/test-allowFileSystemSources-flag.js rename to tests/test-disallow-fs-resources.js index b647d01..51c541b 100644 --- a/tests/test-allowFileSystemSources-flag.js +++ b/tests/test-disallow-fs-resources.js @@ -3,7 +3,7 @@ var sys = require("util") , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest , xhr; -xhr = new XMLHttpRequest({ allowFileSystemSources: false }); +xhr = new XMLHttpRequest({ allowFileSystemResources: false }); xhr.onreadystatechange = function() { if (this.readyState == 4) { @@ -22,7 +22,7 @@ xhr.send(); // Sync var runSync = function() { - xhr = new XMLHttpRequest({ allowFileSystemSources: false }); + xhr = new XMLHttpRequest({ allowFileSystemResources: false }); xhr.onreadystatechange = function() { if (this.readyState == 4) { From 87e3f5ae16bb8ddad78b917db590e19cec208ba3 Mon Sep 17 00:00:00 2001 From: bhpsngum Date: Tue, 3 Dec 2024 14:53:58 +0900 Subject: [PATCH 05/15] Add origin options --- lib/XMLHttpRequest.js | 19 ++++++++++++++-- tests/test-url-origin.js | 47 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 tests/test-url-origin.js diff --git a/lib/XMLHttpRequest.js b/lib/XMLHttpRequest.js index 1c29d33..27d59ee 100644 --- a/lib/XMLHttpRequest.js +++ b/lib/XMLHttpRequest.js @@ -50,6 +50,7 @@ function XMLHttpRequest(opts) { max_redirects: 20, allowFileSystemResources: true, maxRedirects: 20, // Chrome standard + origin: void 0 } opts = Object.assign(default_options, opts); @@ -289,12 +290,20 @@ function XMLHttpRequest(opts) { settings = { "method": method.toUpperCase(), - "url": url.toString(), + "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); }; @@ -404,7 +413,13 @@ function XMLHttpRequest(opts) { var ssl = false, local = false, datauri = false; var url; try { - url = new URL(settings.url); + if (settings.origin != null) { + url = new URL(settings.url, settings.origin); + } + else { + url = new URL(settings.url); + } + settings.url = url.href; } catch (e) { self.handleError(new Error("ERR_INVALID_URL")); diff --git a/tests/test-url-origin.js b/tests/test-url-origin.js new file mode 100644 index 0000000..920f055 --- /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) { + + } +} + +setTimeout(function () { + try { + runTest(); + } finally { + serverProcess.kill('SIGINT'); + } +}, 100); \ No newline at end of file From f7daba39837d67d91c594ea6ac4e14906f6804f3 Mon Sep 17 00:00:00 2001 From: bhpsngum Date: Tue, 3 Dec 2024 17:59:10 +0900 Subject: [PATCH 06/15] resolve some issues --- lib/XMLHttpRequest.js | 144 ++++++++++++++++++++++-------------------- 1 file changed, 76 insertions(+), 68 deletions(-) diff --git a/lib/XMLHttpRequest.js b/lib/XMLHttpRequest.js index 27d59ee..853f770 100644 --- a/lib/XMLHttpRequest.js +++ b/lib/XMLHttpRequest.js @@ -38,19 +38,19 @@ function XMLHttpRequest(opts) { // defines a list of default options to prevent parameters pollution var default_options = { - pfx: void 0, - key: void 0, - passphrase: void 0, - cert: void 0, - ca: void 0, - ciphers: void 0, + pfx: undefined, + key: undefined, + passphrase: undefined, + cert: undefined, + ca: undefined, + ciphers: undefined, rejectUnauthorized: true, autoUnref: false, - agent: void 0, + agent: undefined, max_redirects: 20, allowFileSystemResources: true, maxRedirects: 20, // Chrome standard - origin: void 0 + origin: undefined } opts = Object.assign(default_options, opts); @@ -378,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); }; /** @@ -396,6 +396,51 @@ function XMLHttpRequest(opts) { return ""; }; + /** + * Convert from Data URI to Buffer + * @param {String} 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"; + + var base64 = false; + // check if header part has base64 + var dataHeaders = parts[0].split(";"); + + // ignore first element since it's MIME type + for (var i = 1; i < dataHeaders.length; ++i) { + if (dataHeaders[i].toLowerCase() === "base64") { + base64 = true; + break; + } + } + + 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. * @@ -410,7 +455,7 @@ function XMLHttpRequest(opts) { throw new Error("INVALID_STATE_ERR: send has already been called"); } - var ssl = false, local = false, datauri = false; + var isSsl = false, isLocal = false, isDataUri = false; var url; try { if (settings.origin != null) { @@ -429,17 +474,17 @@ function XMLHttpRequest(opts) { // 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:': - datauri = true; + isDataUri = true; case 'file:': - local = true; + isLocal = true; break; case undefined: @@ -453,47 +498,11 @@ function XMLHttpRequest(opts) { // Load files off the local filesystem (file://) // or data from Data URI (data:) - if (local) { - if (datauri) try { - // Triming from original url string since - var data = url.href.substr(5, settings.url.length); - - // separator between header and actual data - var parts = data.split(",", 2); - - if (parts.length < 2) throw "Invalid URL"; - - var base64 = false; - // check if header part has base64 - var data_headers = parts[0].split(";"); - - // ignore first element since it's MIME type - for (var i = 1; i < data_headers.length; ++i) { - if (data_headers[i].toLowerCase() === "base64") { - base64 = true; - break; - } - } - - 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.substr(0, inputData.length - padding.length); - responseData = Buffer.from(inputData, "base64"); - if (responseData.toString("base64").replace(/=+$/, "") !== inputData) throw "malformed base64 encoding"; - } - else { - responseData = Buffer.from(inputData); - } - + if (isLocal) { + if (isDataUri) try { self.status = 200; self.responseURL = settings.url; - self.createFileOrSyncResponse(responseData); + self.createFileOrSyncResponse(bufferFromDataUri(url)); setState(self.DONE); return; } @@ -541,13 +550,13 @@ 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 || ''); // 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; } @@ -590,7 +599,7 @@ function XMLHttpRequest(opts) { // 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; @@ -628,15 +637,15 @@ function XMLHttpRequest(opts) { return; } // change request options again to match with new redirect protocol - ssl = url.protocol === "https:"; - doRequest = ssl ? https.request : http.request; + 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 || (ssl ? 443 : 80); + port = url.port || (isSsl ? 443 : 80); headers["Host"] = host; - if (!((ssl && port === 443) || port === 80)) { + if (!((isSsl && port === 443) || port === 80)) { headers["Host"] += ':' + url.port; } @@ -724,12 +733,12 @@ function XMLHttpRequest(opts) { var createRequest = function (opt) { opt = Object.assign({}, opt); - if (ssl) Object.assign(opt, sslOptions); + if (isSsl) Object.assign(opt, sslOptions); request = doRequest(opt, responseHandler).on('error', errorHandler); if (opts.autoUnref) { - request.on('socket', (socket) => { + request.on('socket', function (socket) { socket.unref(); }); } @@ -767,8 +776,8 @@ function XMLHttpRequest(opts) { + " }" + " return result;" + "};" - + "var doRequest = http" + (ssl ? "s" : "") + ".request;" - + "var ssl = " + !!ssl + ";" + + "var doRequest = http" + (isSsl ? "s" : "") + ".request;" + + "var isSsl = " + !!isSsl + ";" + "var options = " + JSON.stringify(options) + ";" + "var sslOptions = " + JSON.stringify(sslOptions) + ";" + "var responseData = Buffer.alloc(0);" @@ -777,7 +786,7 @@ function XMLHttpRequest(opts) { + "var max_redirects = " + max_redirects + ", redirects_count = 0;" + "var makeRequest = function () {" + " var opt = Object.assign({}, options);" - + " if (ssl) Object.assign(opt, sslOptions);" + + " 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();" @@ -796,8 +805,8 @@ function XMLHttpRequest(opts) { + " fs.unlinkSync('" + syncFile + "');" + " return;" + " };" - + " ssl = url.protocol === 'https:';" - + " doRequest = ssl ? https.request : http.request;" + + " isSsl = url.protocol === 'https:';" + + " doRequest = isSsl ? https.request : http.request;" + " var port = url.port;" + " options = {" + " hostname: url.hostname," @@ -807,7 +816,7 @@ function XMLHttpRequest(opts) { + " headers: options.headers" + " };" + " options.headers['Host'] = url.host;" - + " if (!((ssl && port === 443) || port === 80)) options.headers['Host'] += ':' + port;" + + " if (!((isSsl && port === 443) || port === 80)) options.headers['Host'] += ':' + port;" + " makeRequest();" + " return;" + " }" @@ -866,7 +875,6 @@ function XMLHttpRequest(opts) { }; setState(self.DONE); } - } }; From 02b66c1447d4788acabe427191a0e4d3d0eb4ee8 Mon Sep 17 00:00:00 2001 From: bhpsngum Date: Tue, 3 Dec 2024 18:05:15 +0900 Subject: [PATCH 07/15] little docfix for params --- lib/XMLHttpRequest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/XMLHttpRequest.js b/lib/XMLHttpRequest.js index 853f770..23393b6 100644 --- a/lib/XMLHttpRequest.js +++ b/lib/XMLHttpRequest.js @@ -398,7 +398,7 @@ function XMLHttpRequest(opts) { /** * Convert from Data URI to Buffer - * @param {String} url URI to parse + * @param {URL} url URI to parse * @returns {Buffer} buffer */ From d8deec653f5c940e473e4c49b82411d78b7e56e5 Mon Sep 17 00:00:00 2001 From: bhpsngum Date: Sat, 7 Dec 2024 18:13:39 +0900 Subject: [PATCH 08/15] Updates to reflect new reviews --- lib/XMLHttpRequest.js | 22 ++++++++-------------- tests/test-data-uri.js | 8 ++++---- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/lib/XMLHttpRequest.js b/lib/XMLHttpRequest.js index 23393b6..9d56c37 100644 --- a/lib/XMLHttpRequest.js +++ b/lib/XMLHttpRequest.js @@ -411,17 +411,10 @@ function XMLHttpRequest(opts) { if (parts.length < 2) throw "Invalid URL"; - var base64 = false; - // check if header part has base64 - var dataHeaders = parts[0].split(";"); - - // ignore first element since it's MIME type - for (var i = 1; i < dataHeaders.length; ++i) { - if (dataHeaders[i].toLowerCase() === "base64") { - base64 = true; - break; - } - } + // 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]); @@ -458,7 +451,7 @@ function XMLHttpRequest(opts) { var isSsl = false, isLocal = false, isDataUri = false; var url; try { - if (settings.origin != null) { + if (settings.origin) { url = new URL(settings.url, settings.origin); } else { @@ -467,7 +460,8 @@ function XMLHttpRequest(opts) { settings.url = url.href; } catch (e) { - self.handleError(new Error("ERR_INVALID_URL")); + // URL parsing throws TypeError, here we only want to take its message + self.handleError(new Error(e.message)); return; } var host; @@ -507,7 +501,7 @@ function XMLHttpRequest(opts) { return; } catch (e) { - self.handleError(new Error("ERR_INVALID_URL")); + self.handleError(new Error("Invalid data URI")); return; } diff --git a/tests/test-data-uri.js b/tests/test-data-uri.js index 7160339..452783d 100644 --- a/tests/test-data-uri.js +++ b/tests/test-data-uri.js @@ -19,7 +19,7 @@ var tests = [ }, { name: "Test plain URI Data with data URI headers", - data: "data:text/plain;example=1;args=2,Hello%20World", + data: "data:base64;example=1;args=2,Hello%20World", output: "Hello World" }, { @@ -40,12 +40,12 @@ var tests = [ { name: "Test normal bass64-encoded data URI with invalid characters", data: "data:text;base64,SGV&&&&sbG8gV{29ybGQ=", - error: "ERR_INVALID_URL" + 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: "ERR_INVALID_URL" + error: "Invalid data URI" }, { name: "Test base64-encoded data with no paddings", @@ -55,7 +55,7 @@ var tests = [ { name: "Test base64-encoded data with excessive paddings", data: "data:text;base64,SGVsbG8gV29ybGQ==", - error: "ERR_INVALID_URL" + error: "Invalid data URI" } ]; From 56d4a0acffdcb5a9eb8585973fa69043d993000a Mon Sep 17 00:00:00 2001 From: Frederick Lanford Date: Sat, 7 Dec 2024 18:34:31 +0900 Subject: [PATCH 09/15] Update lib/XMLHttpRequest.js Co-authored-by: Michael de Wit --- lib/XMLHttpRequest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/XMLHttpRequest.js b/lib/XMLHttpRequest.js index 9d56c37..a506269 100644 --- a/lib/XMLHttpRequest.js +++ b/lib/XMLHttpRequest.js @@ -51,7 +51,7 @@ function XMLHttpRequest(opts) { allowFileSystemResources: true, maxRedirects: 20, // Chrome standard origin: undefined - } + }; opts = Object.assign(default_options, opts); From fc80f84a322c49c7767b12e19fe08ee715d3c08f Mon Sep 17 00:00:00 2001 From: Frederick Lanford Date: Sat, 7 Dec 2024 18:36:30 +0900 Subject: [PATCH 10/15] Update lib/XMLHttpRequest.js Co-authored-by: Michael de Wit --- lib/XMLHttpRequest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/XMLHttpRequest.js b/lib/XMLHttpRequest.js index a506269..41b19fe 100644 --- a/lib/XMLHttpRequest.js +++ b/lib/XMLHttpRequest.js @@ -73,7 +73,7 @@ function XMLHttpRequest(opts) { var https = require('https'); var max_redirects = opts.maxRedirects; - if (max_redirects == null || isNaN(max_redirects)) max_redirects = 20; + if (typeof max_redirects !== 'number' || Number.isNaN(max_redirects)) max_redirects = 20; else max_redirects = Math.max(max_redirects, 0); var redirect_count = 0; From 8c4296b55dcfb7970d0eeeda8e8ddfe2c7921f9a Mon Sep 17 00:00:00 2001 From: bhpsngum Date: Sat, 7 Dec 2024 18:45:45 +0900 Subject: [PATCH 11/15] Change from error code to error messages + newlines --- lib/XMLHttpRequest.js | 14 +++++++------- tests/test-data-uri.js | 2 +- tests/test-disallow-fs-resources.js | 4 ++-- tests/test-max-redirects.js | 10 +++++----- tests/test-unsafe-redirect.js | 4 ++-- tests/test-url-origin.js | 4 ++-- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/XMLHttpRequest.js b/lib/XMLHttpRequest.js index 41b19fe..73eb896 100644 --- a/lib/XMLHttpRequest.js +++ b/lib/XMLHttpRequest.js @@ -556,10 +556,10 @@ function XMLHttpRequest(opts) { // 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"); } @@ -615,7 +615,7 @@ function XMLHttpRequest(opts) { // end the response resp.destroy(); if (redirect_count > max_redirects) { - self.handleError(new Error("ERR_TOO_MANY_REDIRECTS")); + self.handleError(new Error("Too many redirects")); return; } // Change URL to the redirect location @@ -627,7 +627,7 @@ function XMLHttpRequest(opts) { settings.url = url.href; } catch (e) { - self.handleError(new Error("ERR_UNSAFE_REDIRECT")); + self.handleError(new Error("Unsafe redirect")); return; } // change request options again to match with new redirect protocol @@ -786,7 +786,7 @@ function XMLHttpRequest(opts) { + " response.destroy();" + " ++redirects_count;" + " if (redirects_count > max_redirects) {" - + " fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR-REDIRECT: ERR_TOO_MANY_REDIRECTS', 'utf8');" + + " fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR-REDIRECT: Too many redirects', 'utf8');" + " fs.unlinkSync('" + syncFile + "');" + " return;" + " }" @@ -795,7 +795,7 @@ function XMLHttpRequest(opts) { + " if (url.protocol !== 'https:' && url.protocol !== 'http:') throw 'bad protocol';" + " }" + " catch (e) {" - + " fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR-REDIRECT: ERR_UNSAFE_REDIRECT', 'utf8');" + + " fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR-REDIRECT: Unsafe redirect', 'utf8');" + " fs.unlinkSync('" + syncFile + "');" + " return;" + " };" @@ -1058,4 +1058,4 @@ function XMLHttpRequest(opts) { self.dispatchEvent("loadend"); } }; -}; \ No newline at end of file +}; diff --git a/tests/test-data-uri.js b/tests/test-data-uri.js index 452783d..3344008 100644 --- a/tests/test-data-uri.js +++ b/tests/test-data-uri.js @@ -128,4 +128,4 @@ var startTest = function () { }, 500); } -startTest(); \ No newline at end of file +startTest(); diff --git a/tests/test-disallow-fs-resources.js b/tests/test-disallow-fs-resources.js index 51c541b..47a8696 100644 --- a/tests/test-disallow-fs-resources.js +++ b/tests/test-disallow-fs-resources.js @@ -6,7 +6,7 @@ var sys = require("util") xhr = new XMLHttpRequest({ allowFileSystemResources: false }); xhr.onreadystatechange = function() { - if (this.readyState == 4) { + if (this.readyState === 4) { assert.equal(this.statusText, "Not allowed to load local resource: " + url); assert.equal(this.status, 0); try { runSync(); } catch (e) { @@ -25,7 +25,7 @@ var runSync = function() { xhr = new XMLHttpRequest({ allowFileSystemResources: false }); xhr.onreadystatechange = function() { - if (this.readyState == 4) { + if (this.readyState === 4) { assert.equal(this.statusText, "Not allowed to load local resource: " + url); assert.equal(this.status, 0); console.log("done"); diff --git a/tests/test-max-redirects.js b/tests/test-max-redirects.js index ccb43b6..d221001 100644 --- a/tests/test-max-redirects.js +++ b/tests/test-max-redirects.js @@ -11,7 +11,7 @@ var runTest = function () { let xhr = new XMLHttpRequest({ maxRedirects: 10 }); xhr.open("GET", "http://localhost:8888/redirectingResource/10", false); xhr.onreadystatechange = function() { - if (xhr.readyState == 4) { + if (xhr.readyState === 4) { assert.equal(xhr.getRequestHeader('Location'), ''); assert.equal(xhr.responseText, "Hello World"); console.log("safe redirects count: done"); @@ -26,15 +26,15 @@ var runTest = function () { 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, 'ERR_TOO_MANY_REDIRECTS'); + 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); } } @@ -44,4 +44,4 @@ setTimeout(function () { } finally { serverProcess.kill('SIGINT'); } -}, 100); \ No newline at end of file +}, 100); diff --git a/tests/test-unsafe-redirect.js b/tests/test-unsafe-redirect.js index 03c10ff..54fff68 100644 --- a/tests/test-unsafe-redirect.js +++ b/tests/test-unsafe-redirect.js @@ -26,8 +26,8 @@ var server = http.createServer(function (req, res) { }).listen(8000); xhr.onreadystatechange = function() { - if (this.readyState == 4) { - assert.equal(xhr.statusText, "ERR_UNSAFE_REDIRECT"); + if (this.readyState === 4) { + assert.equal(xhr.statusText, "Unsafe redirect"); assert.equal(xhr.status, 0); console.log("done"); } diff --git a/tests/test-url-origin.js b/tests/test-url-origin.js index 920f055..23eafd2 100644 --- a/tests/test-url-origin.js +++ b/tests/test-url-origin.js @@ -34,7 +34,7 @@ var runTest = function () { }; xhr.send(); } catch(e) { - + console.log("ERROR: Exception raised", e); } } @@ -44,4 +44,4 @@ setTimeout(function () { } finally { serverProcess.kill('SIGINT'); } -}, 100); \ No newline at end of file +}, 100); From 050b5bf11a1100ac2ce12205b741dee8a4b253de Mon Sep 17 00:00:00 2001 From: bhpsngum Date: Sat, 7 Dec 2024 18:46:38 +0900 Subject: [PATCH 12/15] Missing strict equality somewhere --- tests/test-url-origin.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test-url-origin.js b/tests/test-url-origin.js index 23eafd2..c86abb7 100644 --- a/tests/test-url-origin.js +++ b/tests/test-url-origin.js @@ -11,7 +11,7 @@ var runTest = function () { let xhr = new XMLHttpRequest({ origin: "http://localhost:8888" }); xhr.open("GET", "text", false); xhr.onreadystatechange = function() { - if (xhr.readyState == 4) { + if (xhr.readyState === 4) { assert.equal(xhr.getResponseHeader('Content-Type'), 'text/plain'); assert.equal(xhr.responseText, "Hello world!"); console.log("origin test 1: done"); @@ -26,7 +26,7 @@ var runTest = function () { let xhr = new XMLHttpRequest({ origin: "http://localhost:8888/text" }); xhr.open("GET", "", false); xhr.onreadystatechange = function() { - if (xhr.readyState == 4) { + if (xhr.readyState === 4) { assert.equal(xhr.getResponseHeader('Content-Type'), 'text/plain'); assert.equal(xhr.responseText, "Hello world!"); console.log("origin test 2: done"); From 9a36aa98a26e762e6b564f525fa7fb81010785d3 Mon Sep 17 00:00:00 2001 From: bhpsngum Date: Sat, 7 Dec 2024 19:26:09 +0900 Subject: [PATCH 13/15] Create automated test script --- package.json | 2 +- tests/run-test.js | 50 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 tests/run-test.js 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..abbf751 --- /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."); \ No newline at end of file From 900dc9d1a98b5eb878b43ee5fc0acc81dcad90fa Mon Sep 17 00:00:00 2001 From: bhpsngum Date: Sat, 7 Dec 2024 19:27:26 +0900 Subject: [PATCH 14/15] change identation of test runner --- tests/run-test.js | 56 +++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/run-test.js b/tests/run-test.js index abbf751..1e00421 100644 --- a/tests/run-test.js +++ b/tests/run-test.js @@ -1,6 +1,6 @@ var ignored_files = [ - "run-test.js", // this file - "server.js" + "run-test.js", // this file + "server.js" ]; var spawnSync = require("child_process").spawnSync; @@ -12,34 +12,34 @@ 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()); - } + 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); - } + 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 @@ -47,4 +47,4 @@ check_dir("./"); if (fail_path) throw new Error("Test failed at file: " + fail_path); -console.log("ALL TESTS PASSED."); \ No newline at end of file +console.log("ALL TESTS PASSED."); From 4066610f9d3d6520ddcaa38517c49d4be3509a8a Mon Sep 17 00:00:00 2001 From: bhpsngum Date: Sat, 7 Dec 2024 20:38:16 +0900 Subject: [PATCH 15/15] add strict equality in redirect code checks --- tests/test-redirect-301.js | 2 +- tests/test-redirect-302.js | 2 +- tests/test-redirect-303.js | 2 +- tests/test-redirect-307.js | 2 +- tests/test-redirect-308.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test-redirect-301.js b/tests/test-redirect-301.js index 0135c84..0b3efe3 100644 --- a/tests/test-redirect-301.js +++ b/tests/test-redirect-301.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-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 index 3d160de..71543e0 100644 --- a/tests/test-redirect-308.js +++ b/tests/test-redirect-308.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");