UNPKG

freedom-xhr

Version:

an XMLHttpRequest drop-in replacement using core.xhr or core.tcpsocket in freedom.js

1,082 lines (938 loc) 38.3 kB
var frontdomain = require('frontdomain'); var socketFactory = require('./socketfactory'); var ChunkReassembler = require('./chunkreassembler'); var TcpXhr = function () { Object.defineProperties(this, { options: { enumerable: false, writable: true, value: { uri: null, data: null, events: {}, method: null, inprogress: false, redirects: { max: 10, current: 0, last: null }, timer: { id: null, expired: false }, headers: { 'User-Agent': 'core.tcpsocket.xhr' }, response: { headers: {}, headersText: '', contentSegments: [], chunkReassembler: new ChunkReassembler() } } }, receiveListener: { enumerable: false, configurable: false, writable: true, value: null }, disconnectListener: { enumerable: false, configurable: false, writable: true, value: null }, props: { enumerable: false, configurable: false, value: { readyState: 0, } }, socket: { enumerable: false, configurable: false, writable: true, value: null }, /** * http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-unsent */ UNSENT: { enumerable: false, writable: true, value: 0 }, /** * http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-opened */ OPENED: { enumerable: false, writable: true, value: 1 }, /** * http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-headers_received * TODO: time in milliseconds. */ HEADERS_RECEIVED: { enumerable: false, writable: true, value: 2 }, /** * http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-loading * TODO: time in milliseconds. */ LOADING: { enumerable: false, writable: true, value: 3 }, /** * http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-done */ DONE: { enumerable: false, writable: true, value: 4 }, /** * http://www.w3.org/TR/XMLHttpRequest/#the-timeout-attribute * TODO: time in milliseconds. */ timeout: { enumerable: true, writable: true, value: 0 }, /** * http://www.w3.org/TR/XMLHttpRequest/#the-withcredentials-attribute */ withCredentials: { enumerable: true, writable: true, value: false }, /** * http://www.w3.org/TR/XMLHttpRequest/#the-upload-attribute */ upload: { enumerable: true, writable: true, value: null }, /** * http://www.w3.org/TR/XMLHttpRequest/#the-status-attribute */ status: { enumerable: true, writable: true, value: 0 }, /** * http://www.w3.org/TR/XMLHttpRequest/#the-statustext-attribute */ statusText: { enumerable: true, writable: true, value: null }, /** * http://www.w3.org/TR/XMLHttpRequest/#the-responsetype-attribute */ responseType: { enumerable: true, writable: true, value: 'text' }, /** * http://www.w3.org/TR/XMLHttpRequest/#the-response-attribute */ response: { enumerable: true, writable: true, value: null }, /** * http://www.w3.org/TR/XMLHttpRequest/#the-responsetext-attribute */ responseText: { enumerable: true, writable: true, value: '' }, /** * http://www.w3.org/TR/XMLHttpRequest/#the-responseurl-attribute */ responseURL: { enumerable: true, writable: true, value: '' }, /** * http://www.w3.org/TR/XMLHttpRequest/#the-responsexml-attribute */ responseXML: { enumerable: true, writable: true, value: null }, /** * http://www.w3.org/TR/XMLHttpRequest/#event-handlers */ onreadystatechange: { enumerable: true, writable: true, value: null }, ontimeout: { enumerable: true, writable: true, value: null }, onabort: { enumerable: true, writable: true, value: null }, onerror: { enumerable: true, writable: true, value: null }, onload: { enumerable: true, writable: true, value: null }, onloadstart: { enumerable: true, writable: true, value: null }, onloadend: { enumerable: true, writable: true, value: null }, onprogress: { enumerable: true, writable: true, value: null }, /** * custom event to match `chrome.webRequest.onBeforeRedirect` * http://developer.chrome.com/extensions/webRequest#event-onBeforeRedirect */ beforeredirect: { enumerable: true, writable: true, value: null }, readyState: { enumerable: true, get: function () { return this.props.readyState; }, set: function (value) { if (this.props.readyState === value) { return; } this.props.readyState = value; this.dispatchEvent('readystatechange'); } } }); }; /** * Regular Expression for URL validation * Modified: added capturing groups * * Author: Diego Perini * Updated: 2010/12/05 * License: MIT * * Copyright (c) 2010-2013 Diego Perini (http://www.iport.it) * * https://gist.github.com/dperini/729294 */ TcpXhr.prototype.regex = new RegExp( '^' + // protocol identifier '(?:(https?|ftp)://)' + // user:pass authentication '(?:\\S+(?::\\S*)?@)?' + '(' + // IP address exclusion // private & local networks '(?!(?:10|127)(?:\\.\\d{1,3}){3})' + '(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})' + '(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})' + // IP address dotted notation octets // excludes loopback network 0.0.0.0 // excludes reserved space >= 224.0.0.0 // excludes network & broacast addresses // (first & last IP address of each class) '(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])' + '(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}' + '(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))' + '|' + // host name '(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)' + // domain name '(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*' + // TLD identifier '(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))' + ')' + // port number '(?::(\\d{2,5}))?' + // resource path '(/[^\\s]*)?' + '$', 'i' ); /** * http://www.w3.org/TR/XMLHttpRequest/#the-open()-method */ TcpXhr.prototype.open = function (method, url) { this.options.method = method; this.options.uri = this.regex.exec(url); // check if the method is valid if (!this.options.method) { throw new TypeError('method is not a valid HTTP method'); } // check if the URI parsed properly if (this.options.uri === null) { throw new TypeError('url cannot be parsed'); } // catch no path specified error if (this.options.uri[4] === undefined) { this.options.uri[4] = '/'; } // set readyState to OPENED this.readyState = this.OPENED; var domain = this.options.uri[2]; if (frontdomain.isFront(domain)) { domain = frontdomain.demunge(domain).host; } this.setRequestHeader('Host', domain); }; /** * http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader()-method */ TcpXhr.prototype.setRequestHeader = function (header, value) { if (this.readyState !== this.OPENED || this.options.inprogress === true) { throw new TypeError('InvalidStateError'); } if (!header) { throw new TypeError('header is not a valid HTTP header field name'); } if (!value && value.length !== 0) { throw new TypeError('value is not a valid HTTP header field value.'); } // TODO: If header is in the headers list, append ",", followed by U+0020, followed by value. this.options.headers[header] = value; }; /** * http://www.w3.org/TR/XMLHttpRequest/#the-send()-method */ TcpXhr.prototype.send = function (data) { // If the state is not OPENED, throw an "InvalidStateError" exception. // If the send() flag is set, throw an "InvalidStateError" exception. if (this.readyState !== this.OPENED || this.options.inprogress === true) { throw new TypeError('InvalidStateError'); } // If the request method is GET or HEAD, set data to null if (['GET', 'HEADER'].indexOf(this.options.method.toUpperCase()) !== -1) { data = null; } // If data is null, do not include a request entity body and go to the next step. if (data !== null) { var encoding = null; var mimetype = null; if (data instanceof Uint8Array) { // Let the request entity body be the raw data represented by data. data = data.buffer; } else if (data instanceof Blob) { // if the object's type attribute is not the empty string let mime type be its value. // Let the request entity body be the raw data represented by data. var fileReader = new FileReader(); fileReader.onload = function() { this.send(fileReader.result); }.bind(this); fileReader.readAsArrayBuffer(data); return; } else if (typeof HTMLElement !== 'undefined' && data instanceof HTMLElement) { // Let encoding be "UTF-8". encoding = 'UTF-8'; // If data is an HTML document, let mime type be "text/html" // or let mime type be "application/xml" otherwise. mimetype = 'text/html'; // Then append ";charset=UTF-8" to mime type. mimetype += ';charset=UTF-8'; //Let the request entity body be data, serialized, converted to Unicode, and utf-8 encoded. Re-throw any exception serializing throws. throw new Error('HTMLElement is not yet supported'); } else if (data instanceof FormData) { // Let the request entity body be the result of running the multipart/form-data encoding algorithm with data as form data set and with utf-8 as the explicit character encoding. //Let mime type be the concatenation of "multipart/form-data;", a U+0020 SPACE character, "boundary=", and the multipart/form-data boundary string generated by the multipart/form-data encoding algorithm. throw new Error('FormData is not yet supported'); } else if (typeof(data) === 'string') { // Let encoding be "UTF-8". encoding = 'UTF-8'; // Let mime type be "text/plain;charset=UTF-8". mimetype = 'text/plain;charset=UTF-8'; // Let the request entity body be data, utf-8 encoded. var encoder = new TextEncoder('utf-8'); data = encoder.encode(data).buffer; } this.options.data = data; } // If a Content-Type header is in author request headers and its value is a valid MIME type that has a charset parameter whose value is not a case-insensitive match for encoding, and encoding is not null, set all the charset parameters of that Content-Type header to encoding. // If no Content-Type header is in author request headers and mime type is not null, append a Content-Type header with value mime type to author request headers. // Unset the error flag, upload complete flag and upload events flag. // If there is no request entity body or if it is empty, set the upload complete flag. // Set the send() flag. this.options.inprogress = true; // Fire a progress event named loadstart. this.dispatchProgressEvent('loadstart'); // Acquire a socket that is connected to the server. this.connect(); if (this.timeout > 0) { this.options.timer.id = setTimeout(this.expireTimer.bind(this), this.timeout); } }; /** * http://www.w3.org/TR/XMLHttpRequest/#the-abort()-method */ TcpXhr.prototype.abort = function () { this.disconnect(); }; /** * http://www.w3.org/TR/XMLHttpRequest/#the-getresponseheader()-method */ TcpXhr.prototype.getResponseHeader = function (header) { for (var responseHeader in this.options.response.headers) { if (responseHeader.toLowerCase() === header.toLowerCase()) { return this.options.response.headers[responseHeader]; } } }; /** * http://www.w3.org/TR/XMLHttpRequest/#the-getallresponseheaders()-method */ TcpXhr.prototype.getAllResponseHeaders = function () { return this.options.response.headersText; }; /** * http://www.w3.org/TR/XMLHttpRequest/#the-overridemimetype()-method */ TcpXhr.prototype.overrideMimeType = function (mimetype) { }; /** * event managers */ TcpXhr.prototype.addEventListener = function (name, callback) { if (this.options.events[name]) { this.options.events[name].push(callback); } else { this.options.events[name] = new Array(callback); } return this; }; TcpXhr.prototype.removeEventListener = function(name, callback) { if (this.options.events[name]) { var i = this.options.events[name].indexOf(callback); if (i > -1) { this.options.events[name].splice(i, 1); } else { return false; } return true; } else { return false; } }; TcpXhr.prototype.dispatchProgressEvent = function(name) { this.dispatchEvent(name, { lengthComputable: false, loaded: 0, total: 0 }); }; TcpXhr.prototype.dispatchEvent = function(name, e) { if (this.hasOwnProperty('on' + name)) { if (this['on' + name]) { this['on' + name].call(this, e); } } if (!this.options.events[name]) { return false; } this.options.events[name].forEach(function (event) { event.call(this, e); }.bind(this)); }; /** * Connect to the server using the socket factory. */ TcpXhr.prototype.connect = function () { if (!this.options.inprogress) { return; } var domain = this.options.uri[2]; if (frontdomain.isFront(domain)) { domain = frontdomain.demunge(domain).front; } var defaultPort = this.options.uri[1] === 'https' ? 443 : 80; var port = this.options.uri[3] ? parseInt(this.options.uri[3], null) : defaultPort; var tls = this.options.uri[1] === 'https'; socketFactory.getSocket(domain, port, tls).then(function(socket) { this.socket = socket; this.socket.connected.then(this.onConnect.bind(this, 0)). catch(this.onConnect.bind(this, -1)); }.bind(this)); }; TcpXhr.prototype.onConnect = function (result) { if (!this.options.inprogress) { return; } if (this.options.timer.expired) { return; } else if (result < 0) { this.error({ error: 'connect error', resultCode: result }); } else { this.receiveListener = this.onReceive.bind(this); this.socket.on('data', this.receiveListener); this.disconnectListener = this.onDisconnect.bind(this); this.socket.once('close', this.disconnectListener); // send message as ArrayBuffer var requestHeaderString = this.generateMessage(); var encoder = new TextEncoder('utf-8'); // Should be 'iso-8859-1' but Chrome doesn't support it var headersByteArray = encoder.encode(requestHeaderString); this.socket.write(headersByteArray.buffer).then(function() { if (this.options.data) { return this.socket.write(this.options.data); } }.bind(this)).then( this.onSend.bind(this, 0), this.onSend.bind(this, -1)); } }; TcpXhr.prototype.onSend = function (resultCode) { if (resultCode < 0) { this.error({ error: 'send error', resultCode: resultCode }); this.disconnect(); } }; TcpXhr.prototype.onReceiveError = function (info) { this.error({ error: 'receive error', resultCode: info.resultCode }); }; TcpXhr.prototype.onReceive = function (data) { if (!this.options.inprogress) { return; } this.parseResponse(data); }; TcpXhr.prototype.onDisconnect = function () { if (this.readyState == this.LOADING) { this.dispatchProgressEvent('error'); this.processResponse(false); // Indicate failure (incomplete response) } this.readyState = this.DONE; }; /** * internal methods */ TcpXhr.prototype.bytesReceived = function () { return this.options.response.contentSegments.reduce(function(bytes, segment) { return bytes + segment.byteLength; }, 0); }; TcpXhr.prototype.parseResponse = function (buffer) { if (this.readyState < this.HEADERS_RECEIVED) { var decoder = new TextDecoder('utf-8'); // Should be 'iso-8859-1' but Chrome doesn't support it var segment = decoder.decode(buffer); var headersLengthBefore = this.options.response.headersText.length; this.options.response.headersText += segment; // detect CRLFx2 position var headersEndMatch = this.options.response.headersText.match(/\r\n\r\n/); // headers are not yet complete. if (headersEndMatch === null) { return; } // Split the headers and preserve the segment of body. var bytesUsedFromBuffer = headersEndMatch.index + 4 - headersLengthBefore; buffer = buffer.slice(bytesUsedFromBuffer); this.options.response.headersText = this.options.response.headersText.slice(0, headersEndMatch.index); // parse headers, discarding the HTTP top-line var headerLines = this.options.response.headersText.split('\r\n'); var statusLine = headerLines.shift(); var statusLineMatch = statusLine.match(/(HTTP\/\d\.\d)\s+((\d+)\s+(.*))/); if (statusLineMatch) { this.status = parseInt(statusLineMatch[3], 0); this.statusText = statusLineMatch[4]; } headerLines.forEach(function (headerLine) { // detect CRLFx2 position var headerLineMatch = headerLine.match(/:/); // sanity check if (headerLineMatch) { // slice the header line at the colon and trim output var header = headerLine.slice(0, headerLineMatch.index).replace(/^\s+/g, '').replace(/\s+$/g, ''); var value = headerLine.slice(headerLineMatch.index + 1).replace(/^\s+/g, '').replace(/\s+$/g, ''); this.options.response.headers[header] = value; } }.bind(this)); // redirects or changes to HEADERS_RECEIVED state if (this.maybeRedirect()) { return; } this.responseURL = this.options.uri[0]; if (buffer.byteLength === 0) { // Needed to avoid a false zero-length segment termination trigger. return; } } if (this.readyState === this.HEADERS_RECEIVED) { this.readyState = this.LOADING; } // There are four required ways to encode the body of an HTTP response: // 1. Set a content-length header indicating the number of bytes // 2. Use chunked-transfer encoding to make the response self-delimiting // 3. Send a zero-length TCP segment to indicate termination (incorrect?) // 4. Close the socket from the server side (TCP FIN). var transferCoding = this.getResponseHeader('Transfer-Encoding'); if (transferCoding ) { // The response uses chunked transfer encoding. Use the decoder. if (transferCoding !== 'chunked') { this.error({resultCode: 330}); return; } var chunks; try { chunks = this.options.response.chunkReassembler.addSegment(buffer); } catch (e) { this.error({resultCode: 321}); return; } chunks.forEach(function(chunk) { this.options.response.contentSegments.push(chunk); }, this); if (this.options.response.chunkReassembler.isDone()) { this.processResponse(true); } else if (chunks.length > 0) { this.dispatchProgressEvent('progress'); } return; } this.options.response.contentSegments.push(buffer); var contentLength = Number(this.getResponseHeader('Content-length')) || 0; if (contentLength > 0) { // The response uses the content-length header. // TODO: Cache bytesReceived to avoid O(N^2) behavior if (this.bytesReceived() === contentLength) { // Indicate a successful load this.processResponse(true); } else if (buffer.byteLength > 0) { this.dispatchProgressEvent('progress'); } } else if (buffer.byteLength === 0) { // Receipt of a zero-length segment indicates loading has completed. this.processResponse(true); } }; TcpXhr.prototype.maybeRedirect = function () { // If the response has an HTTP status code of 301, 302, 303, 307, or 308 // TODO: implement infinite loop precautions if ([301, 302, 303, 307, 308].indexOf(this.status) !== -1) { // stop this.disconnect(); // set the new destination var redirectUrl = this.getResponseHeader('Location'); // notify this.dispatchEvent('beforeredirect', redirectUrl, this.options.response.headers, this.statusText); // enforece the redirect limit if (this.options.redirects.current === this.options.redirects.max) { this.error({ error: 'max redirects', resultCode: 310 }); return false; } // detect a loop if (this.options.redirects.last === redirectUrl) { this.error({ error: 'redirect loop' }); return false; } else { this.options.redirects.last = redirectUrl; } // count this.options.redirects.current++; // clear response headers this.options.response.headersText = ''; this.options.response.headers = []; // start a new call this.open(this.options.method, redirectUrl); this.send(this.options.data); return true; } // set readyState to HEADERS_RECEIVED this.readyState = this.HEADERS_RECEIVED; return false; }; TcpXhr.prototype.processResponse = function (success) { var segments = this.options.response.contentSegments; if (!this.responseType || this.responseType === 'text' || this.responseType === 'json' || this.responseType === 'document') { var text = ''; // TODO: Handle other encodings var decoder = new TextDecoder('utf-8'); segments.forEach(function(buffer) { text += decoder.decode(buffer, {stream: true}); }, this); text += decoder.decode(); // end of stream if (!this.responseType || this.responseType === 'text') { this.response = this.responseText = text; } else if (this.responseType === 'json') { this.response = JSON.parse(text); } else if (this.responseType === 'document') { this.response = text; // TODO: Support document mode when not in a worker } } else if (this.responseType === 'arraybuffer') { var length = this.bytesReceived(); this.response = new ArrayBuffer(length); var array = new Uint8Array(this.response); for (var i = 0, bytes = 0; i < segments.length; bytes += segments[i].byteLength, ++i) { array.set(new Uint8Array(segments[i]), bytes); } } else if (this.responseType === 'blob') { this.response = new Blob(segments); } this.options.response.contentSegments.length = 0; // Free memory. // set readyState to DONE this.readyState = this.DONE; this.dispatchProgressEvent('progress'); if (success) { this.dispatchProgressEvent('load'); } this.dispatchProgressEvent('loadend'); this.disconnect(); }; TcpXhr.prototype.generateMessage = function () { if (this.options.data) { // TODO: use setRequestHeader this.options.headers['Content-Length'] = this.options.data.byteLength; } var headers = []; // add missing parts to header headers.push(this.options.method + ' ' + this.options.uri[4] + ' HTTP/1.1'); // put the host header first headers.push('Host: ' + this.options.headers['Host']); for (var name in this.options.headers) { if (name === 'Host') { continue; } headers.push(name + ': ' + this.options.headers[name]); } return headers.join('\r\n') + '\r\n\r\n'; }; TcpXhr.prototype.error = function (error) { // list of network errors as defined in chromium source: // https://code.google.com/p/chromium/codesearch#chromium/src/net/base/net_error_list.h&sq=package:chromium // // Ranges: // 0- 99 System related errors // 100-199 Connection related errors // 200-299 Certificate errors // 300-399 HTTP errors // 800-899 DNS resolver errors var errorCodes = { 1: 'An asynchronous IO operation is not yet complete.', 2: 'A generic failure occurred.', 3: 'An operation was aborted (due to user action)', 4: 'An argument to the function is incorrect.', 5: 'The handle or file descriptor is invalid', 6: 'The file or directory cannot be found', 7: 'An operation timed out', 8: 'The file is too large', 9: 'An unexpected error. This may be caused by a programming mistake or an invalid assumption', 10: 'Permission to access a resource, other than the network, was denied', 11: 'The operation failed because of unimplemented functionality', 12: 'There were not enough resources to complete the operation', 13: 'Memory allocation failed', 14: 'The file upload failed because the file\'s modification time was different from the expectation', 15: 'The socket is not connected', 16: 'The file already exists', 17: 'The path or file name is too long', 18: 'Not enough room left on the disk', 19: 'The file has a virus', 20: 'The client chose to block the request', 21: 'The network changed', 22: 'The request was blocked by the URL blacklist configured by the domain administrator', 23: 'The socket is already connected', 100: 'A connection was closed (corresponding to a TCP FIN)', 101: 'A connection was reset (corresponding to a TCP RST)', 102: 'A connection attempt was refused', 103: 'A connection timed out as a result of not receiving an ACK for data sent. This can include a FIN packet that did not get ACK\'d', 104: 'A connection attempt failed', 105: 'The host name could not be resolved', 106: 'The Internet connection has been lost', 107: 'An SSL protocol error occurred', 108: 'The IP address or port number is invalid (e.g., cannot connect to the IP address 0 or the port 0)', 109: 'The IP address is unreachable. This usually means that there is no route to the specified host or network', 110: 'The server requested a client certificate for SSL client authentication', 111: 'A tunnel connection through the proxy could not be established', 112: 'No SSL protocol versions are enabled', 113: 'The client and server don\'t support a common SSL protocol version or cipher suite', 114: 'The server requested a renegotiation (rehandshake)', 115: 'The proxy requested authentication (for tunnel establishment) with an unsupported method', 116: 'During SSL renegotiation (rehandshake), the server sent a certificate with an error', 117: 'The SSL handshake failed because of a bad or missing client certificate', 118: 'A connection attempt timed out', 119: 'There are too many pending DNS resolves, so a request in the queue was aborted', 120: 'Failed establishing a connection to the SOCKS proxy server for a target host', 121: 'The SOCKS proxy server failed establishing connection to the target host because that host is unreachable', 122: 'The request to negotiate an alternate protocol failed', 123: 'The peer sent an SSL no_renegotiation alert message', 124: 'Winsock sometimes reports more data written than passed. This is probably due to a broken LSP', 125: 'An SSL peer sent us a fatal decompression_failure alert.', 126: 'An SSL peer sent us a fatal bad_record_mac alert', 127: 'The proxy requested authentication (for tunnel establishment)', 128: 'A known TLS strict server didn\'t offer the renegotiation extension', 129: 'The SSL server attempted to use a weak ephemeral Diffie-Hellman key', 130: 'Could not create a connection to the proxy server.', 131: 'A mandatory proxy configuration could not be used.', 133: 'We\'ve hit the max socket limit for the socket pool while preconnecting.', 134: 'The permission to use the SSL client certificate\'s private key was denied', 135: 'The SSL client certificate has no private key', 136: 'The certificate presented by the HTTPS Proxy was invalid', 137: 'An error occurred when trying to do a name resolution (DNS)', 138: 'Permission to access the network was denied.', 139: 'The request throttler module cancelled this request to avoid DDOS', 140: 'A request to create an SSL tunnel connection through the HTTPS proxy received a non-200 (OK) and non-407 (Proxy Auth) response.', 141: 'We were unable to sign the CertificateVerify data of an SSL client auth handshake with the client certificate\'s private key', 142: 'The message was too large for the transport', 143: 'A SPDY session already exists, and should be used instead of this connection', 145: 'Websocket protocol error.', 146: 'Connection was aborted for switching to another ptotocol.', 147: 'Returned when attempting to bind an address that is already in use', 148: 'An operation failed because the SSL handshake has not completed', 149: 'SSL peer\'s public key is invalid', 150: 'The certificate didn\'t match the built-in public key pins for the host name', 151: 'Server request for client certificate did not contain any types we support', 152: 'Server requested one type of cert, then requested a different type while the first was still being generated', 153: 'An SSL peer sent us a fatal decrypt_error alert. ', 154: 'There are too many pending WebSocketJob instances, so the new job was not pushed to the queue', 155: 'There are too many active SocketStream instances, so the new connect request was rejected', 156: 'The SSL server certificate changed in a renegotiation', 157: 'The SSL server indicated that an unnecessary TLS version fallback was performed', 158: 'Certificate Transparency: All Signed Certificate Timestamps failed to verify', 159: 'The SSL server sent us a fatal unrecognized_name alert', 300: 'The URL is invalid', 301: 'The scheme of the URL is disallowed', 302: 'The scheme of the URL is unknown', 310: 'Attempting to load an URL resulted in too many redirects', 311: 'Attempting to load an URL resulted in an unsafe redirect (e.g., a redirect to file: is considered unsafe)', 312: 'Attempting to load an URL with an unsafe port number.', 320: 'The server\'s response was invalid', 321: 'Error in chunked transfer encoding', 322: 'The server did not support the request method', 323: 'The response was 407 (Proxy Authentication Required), yet we did not send the request to a proxy', 324: 'The server closed the connection without sending any data', 325: 'The headers section of the response is too large', 326: 'The PAC requested by HTTP did not have a valid status code (non-200)', 327: 'The evaluation of the PAC script failed', 328: 'The response was 416 (Requested range not satisfiable) and the server cannot satisfy the range requested', 329: 'The identity used for authentication is invalid', 330: 'Content decoding of the response body failed', 331: 'An operation could not be completed because all network IO is suspended', 332: 'FLIP data received without receiving a SYN_REPLY on the stream', 333: 'Converting the response to target encoding failed', 334: 'The server sent an FTP directory listing in a format we do not understand', 335: 'Attempted use of an unknown SPDY stream id', 336: 'There are no supported proxies in the provided list', 337: 'There is a SPDY protocol error', 338: 'Credentials could not be established during HTTP Authentication', 339: 'An HTTP Authentication scheme was tried which is not supported on this machine', 340: 'Detecting the encoding of the response failed', 341: '(GSSAPI) No Kerberos credentials were available during HTTP Authentication', 342: 'An unexpected, but documented, SSPI or GSSAPI status code was returned', 343: 'The environment was not set up correctly for authentication', 344: 'An undocumented SSPI or GSSAPI status code was returned', 345: 'The HTTP response was too big to drain', 346: 'The HTTP response contained multiple distinct Content-Length headers', 347: 'SPDY Headers have been received, but not all of them - status or version headers are missing, so we\'re expecting additional frames to complete them', 348: 'No PAC URL configuration could be retrieved from DHCP.', 349: 'The HTTP response contained multiple Content-Disposition headers', 350: 'The HTTP response contained multiple Location headers', 351: 'SPDY server refused the stream. Client should retry. This should never be a user-visible error', 352: 'SPDY server didn\'t respond to the PING message', 353: 'The request couldn\'t be completed on an HTTP pipeline. Client should retry', 354: 'The HTTP response body transferred fewer bytes than were advertised by the Content-Length header when the connection is closed', 355: 'The HTTP response body is transferred with Chunked-Encoding, but the terminating zero-length chunk was never sent when the connection is closed', 356: 'There is a QUIC protocol error', 357: 'The HTTP headers were truncated by an EOF', 358: 'The QUIC crytpo handshake failed.', 359: 'An https resource was requested over an insecure QUIC connection', 501: 'The server\'s response was insecure (e.g. there was a cert error)', 502: 'The server responded to a <keygen> with a generated client cert that we don\'t have the matching private key for', 503: 'An error adding to the OS certificate database (e.g. OS X Keychain)', 800: 'DNS resolver received a malformed response', 801: 'DNS server requires TCP', 802: 'DNS server failed.', 803: 'DNS transaction timed out', 804: 'The entry was not found in cache, for cache-only lookups', 805: 'Suffix search list rules prevent resolution of the given host name', 806: 'Failed to sort addresses according to RFC3484' }; if (this.options.inprogress) { this.disconnect(); } if (error.resultCode) { error.resultCode = Math.abs(error.resultCode); error.message = errorCodes[Math.abs(error.resultCode)]; } this.dispatchEvent('error', error); }; TcpXhr.prototype.disconnect = function () { this.options.inprogress = false; if (this.socket) { if (this.receiveListener) { this.socket.removeListener('data', this.receiveListener); } if (this.disconnectListener) { this.socket.removeListener('close', this.disconnectListener); } socketFactory.release(this.socket); this.socket = null; } this.readyState = this.DONE; }; TcpXhr.prototype.expireTimer = function () { if (this.readyState === this.OPENED) { this.disconnect(); this.options.timer.expired = true; this.error({ error: 'timed out' }); this.dispatchProgressEvent('timeout'); } }; TcpXhr.prototype.setMaxRedirects = function (max) { this.options.redirects.max = max; }; module.exports = TcpXhr;