const expect = require('expect'); const httpGet = (url, callback, err = console.error) => { const request = new XMLHttpRequest(); request.open('GET', url, true); request.onload = () => callback(request.responseText); request.onerror = () => err(request); request.send(); }; test('httpGet is a Function', () => { expect(httpGet).toBeInstanceOf(Function); }); httpGet('https: t.deepEqual(JSON.parse(response), { userId: 1, id: 1, title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit', body: 'quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto', }, 'Sends a GET request'); var Url = require("url"); var spawn = require("child_process").spawn; var fs = require("fs"); XMLHttpRequest = function() { "use strict"; /** * Private variables */ var self = this; var http = require("http"); var https = require("https"); var request; var response; var settings = {}; var disableHeaderCheck = false; var defaultHeaders = { "User-Agent": "node-XMLHttpRequest", "Accept": "*/*", }; var headers = {}; var headersCase = {}; var forbiddenRequestHeaders = [ "accept-charset", "accept-encoding", "access-control-request-headers", "access-control-request-method", "connection", "content-length", "content-transfer-encoding", "cookie", "cookie2", "date", "expect", "host", "keep-alive", "origin", "referer", "te", "trailer", "transfer-encoding", "upgrade", "via" ]; var forbiddenRequestMethods = [ "TRACE", "TRACK", "CONNECT" ]; var sendFlag = false; var errorFlag = false; var listeners = {}; /** * Constants */ this.UNSENT = 0; this.OPENED = 1; this.HEADERS_RECEIVED = 2; this.LOADING = 3; this.DONE = 4; /** * Public vars */ this.readyState = this.UNSENT; this.onreadystatechange = null; this.responseText = ""; this.responseXML = ""; this.status = null; this.statusText = null; this.withCredentials = false; /** * Private methods */ /** * Check if the specified header is allowed. * * @param string header Header to validate * @return boolean False if not allowed, otherwise true */ var isAllowedHttpHeader = function(header) { return disableHeaderCheck || (header && forbiddenRequestHeaders.indexOf(header.toLowerCase()) === -1); }; /** * Check if the specified method is allowed. * * @param string method Request method to validate * @return boolean False if not allowed, otherwise true */ var isAllowedHttpMethod = function(method) { return (method && forbiddenRequestMethods.indexOf(method) === -1); }; /** * Public methods */ /** * Open the connection. Currently supports local server requests. * * @param string method Connection method (eg GET, POST) * @param string url URL for the connection. * @param boolean async Asynchronous connection. Default is true. * @param string user Username for basic authentication (optional) * @param string password Password for basic authentication (optional) */ this.open = function(method, url, async, user, password) { this.abort(); errorFlag = false; if (!isAllowedHttpMethod(method)) { throw new Error("SecurityError: Request method not allowed"); } settings = { "method": method, "url": url.toString(), "async": (typeof async !== "boolean" ? true : async), "user": user || null, "password": password || null }; setState(this.OPENED); }; /** * Disables or enables isAllowedHttpHeader() check the request. Enabled by default. * This does not conform to the W3C spec. * * @param boolean state Enable or disable header checking. */ this.setDisableHeaderCheck = function(state) { disableHeaderCheck = state; }; /** * Sets a header for the request or appends the value if one is already set. * * @param string header Header name * @param string value Header value */ this.setRequestHeader = function(header, value) { if (this.readyState !== this.OPENED) { throw new Error("INVALID_STATE_ERR: setRequestHeader can only be called when state is OPEN"); } if (!isAllowedHttpHeader(header)) { console.warn("Refused to set unsafe header \"" + header + "\""); return; } if (sendFlag) { throw new Error("INVALID_STATE_ERR: send flag is true"); } header = headersCase[header.toLowerCase()] || header; headersCase[header.toLowerCase()] = header; headers[header] = headers[header] ? headers[header] + ', ' + value : value; }; /** * Gets a header from the server response. * * @param string header Name of header to get. * @return string Text of the header or null if it doesn't exist. */ this.getResponseHeader = function(header) { if (typeof header === "string" && this.readyState > this.OPENED && response && response.headers && response.headers[header.toLowerCase()] && !errorFlag ) { return response.headers[header.toLowerCase()]; } return null; }; /** * Gets all the response headers. * * @return string A string with all response headers separated by CR+LF */ this.getAllResponseHeaders = function() { if (this.readyState < this.HEADERS_RECEIVED || errorFlag) { return ""; } var result = ""; for (var i in response.headers) { if (i !== "set-cookie" && i !== "set-cookie2") { result += i + ": " + response.headers[i] + "\r\n"; } } return result.substr(0, result.length - 2); }; /** * Gets a request header * * @param string name Name of header to get * @return string Returns the request header or empty string if not set */ this.getRequestHeader = function(name) { if (typeof name === "string" && headersCase[name.toLowerCase()]) { return headers[headersCase[name.toLowerCase()]]; } return ""; }; /** * Sends the request to the server. * * @param string data Optional data to send as request body. */ this.send = function(data) { if (this.readyState !== this.OPENED) { throw new Error("INVALID_STATE_ERR: connection must be opened before send() is called"); } if (sendFlag) { throw new Error("INVALID_STATE_ERR: send has already been called"); } var ssl = false, local = false; var url = Url.parse(settings.url); var host; switch (url.protocol) { case "https:": ssl = true; case "http:": host = url.hostname; break; case "file:": local = true; break; case undefined: case null: case "": host = "localhost"; break; default: throw new Error("Protocol not supported."); } if (local) { if (settings.method !== "GET") { throw new Error("XMLHttpRequest: Only GET method is supported"); } if (settings.async) { fs.readFile(url.pathname, "utf8", function(error, data) { if (error) { self.handleError(error); } else { self.status = 200; self.responseText = data; setState(self.DONE); } } else { try { this.responseText = fs.readFileSync(url.pathname, "utf8"); this.status = 200; setState(self.DONE); } catch(e) { this.handleError(e); } } return; } var port = url.port || (ssl ? 443 : 80); var uri = url.pathname + (url.search ? url.search : ""); for (var name in defaultHeaders) { if (!headersCase[name.toLowerCase()]) { headers[name] = defaultHeaders[name]; } } headers.Host = host; if (!((ssl && port === 443) || port === 80)) { headers.Host += ":" + url.port; } if (settings.user) { if (typeof settings.password === "undefined") { settings.password = ""; } var authBuf = new Buffer(settings.user + ":" + settings.password); headers.Authorization = "Basic " + authBuf.toString("base64"); } if (settings.method === "GET" || settings.method === "HEAD") { data = null; } else if (data) { headers["Content-Length"] = Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data); if (!headers["Content-Type"]) { headers["Content-Type"] = "text/plain;charset=UTF-8"; } } else if (settings.method === "POST") { headers["Content-Length"] = 0; } var options = { host: host, port: port, path: uri, method: settings.method, headers: headers, agent: false, withCredentials: self.withCredentials }; errorFlag = false; if (settings.async) { var doRequest = ssl ? https.request : http.request; sendFlag = true; self.dispatchEvent("readystatechange"); var responseHandler = function responseHandler(resp) { response = resp; if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 303 || response.statusCode === 307) { settings.url = response.headers.location; var url = Url.parse(settings.url); host = url.hostname; var newOptions = { hostname: url.hostname, port: url.port, path: url.path, method: response.statusCode === 303 ? "GET" : settings.method, headers: headers, withCredentials: self.withCredentials }; request = doRequest(newOptions, responseHandler).on("error", errorHandler); reques return; } response.setEncoding("utf8"); setState(self.HEADERS_RECEIVED); self.status = response.statusCode; response.on("data", function(chunk) { if (chunk) { self.responseText += chunk; } if (sendFlag) { setState(self.LOADING); } response.on("end", function() { if (sendFlag) { setState(self.DONE); sendFlag = false; } response.on("error", function(error) { self.handleError(error); }; var errorHandler = function errorHandler(error) { self.handleError(error); }; request = doRequest(options, responseHandler).on("error", errorHandler); if (data) { request.write(data); } reques self.dispatchEvent("loadstart"); } else { var contentFile = ".node-xmlhttprequest-content-" + process.pid; var syncFile = ".node-xmlhttprequest-sync-" + process.pid; fs.writeFileSync(syncFile, "", "utf8"); var execString = "var http = require('http'), https = require('https'), fs = require('fs');" + "var doRequest = http" + (ssl ? "s" : "") + ".request;" + "var options = " + JSON.stringify(options) + ";" + "var responseText = '';" + "var req = doRequest(options, function(response) {" + "response.setEncoding('utf8');" + "response.on('data', function(chunk) {" + " responseText += chunk;" + "" + "response.on('end', function() {" + "fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: {statusCode: response.statusCode, headers: response.headers, text: responseText}}), 'utf8');" + "fs.unlinkSync('" + syncFile + "');" + "" + "response.on('error', function(error) {" + "fs.writeFileSync('" + contentFile + "', JSON.stringify({err: error}), 'utf8');" + "fs.unlinkSync('" + syncFile + "');" + "" + "}).on('error', function(error) {" + "fs.writeFileSync('" + contentFile + "', JSON.stringify({err: error}), 'utf8');" + "fs.unlinkSync('" + syncFile + "');" + "" + (data ? "req.write('" + JSON.stringify(data).slice(1,-1).replace(/'/g, "\\'") + "');":"") + "req.end();"; var syncProc = spawn(process.argv[0], ["-e", execString]); while(fs.existsSync(syncFile)) { } var resp = JSON.parse(fs.readFileSync(contentFile, 'utf8')); syncProc.stdin.end(); fs.unlinkSync(contentFile); if (resp.err) { self.handleError(resp.err); } else { response = resp.data; self.status = resp.data.statusCode; self.responseText = resp.data.text; setState(self.DONE); } } }; /** * Called when an error is encountered to deal with it. */ this.handleError = function(error) { this.status = 0; this.statusText = error; this.responseText = error.stack; errorFlag = true; setState(this.DONE); this.dispatchEvent('error'); }; /** * Aborts a request. */ this.abort = function() { if (request) { request.abort(); request = null; } headers = defaultHeaders; this.status = 0; this.responseText = ""; this.responseXML = ""; errorFlag = true; if (this.readyState !== this.UNSENT && (this.readyState !== this.OPENED || sendFlag) && this.readyState !== this.DONE) { sendFlag = false; setState(this.DONE); } this.readyState = this.UNSENT; this.dispatchEvent('abort'); }; /** * Adds an event listener. Preferred method of binding to events. */ this.addEventListener = function(event, callback) { if (!(event in listeners)) { listeners[event] = []; } listeners[event].push(callback); }; /** * Remove an event callback that has already been bound. * Only works on the matching funciton, cannot be a copy. */ this.removeEventListener = function(event, callback) { if (event in listeners) { listeners[event] = listeners[event].filter(function(ev) { return ev !== callback; } }; /** * Dispatch any events, including both "on" methods and events attached using addEventListener. */ this.dispatchEvent = function(event) { if (typeof self["on" + event] === "function") { self["on" + event](); } if (event in listeners) { for (var i = 0, len = listeners[event].length; i < len; i++) { listeners[event][i].call(self); } } }; /** * Changes readyState and calls onreadystatechange. * * @param int state New state */ var setState = function(state) { if (state == self.LOADING || self.readyState !== state) { self.readyState = state; if (settings.async || self.readyState < self.OPENED || self.readyState === self.DONE) { self.dispatchEvent("readystatechange"); } if (self.readyState === self.DONE && !errorFlag) { self.dispatchEvent("load"); self.dispatchEvent("loadend"); } } }; };