diff --git a/.jshintrc b/.jshintrc index 26547e9..353f48b 100644 --- a/.jshintrc +++ b/.jshintrc @@ -14,6 +14,7 @@ "undef": true, "unused": true, "newcap": false, + "mocha": true, "globals": { "chrome": true, "URLSearchParams": true, @@ -21,6 +22,11 @@ "Cookies": true, "FetchDigestAuth": true, "FetchNtlmAuth": true, - "FetchBasicAuth": true + "FetchBasicAuth": true, + "Request": true, + "Headers": true, + "ArcEventSource": true, + "ArcRequest": true, + "ArcResponse": true } } diff --git a/app.event.js b/app.event.js index 50be1a8..607aebf 100644 --- a/app.event.js +++ b/app.event.js @@ -83,23 +83,30 @@ * Dispatches an Event at the specified EventTarget, invoking the affected EventListeners * in the appropriate order. * - * @type {Event} event An event to be dispatched. + * @param {Event} event An event to be dispatched. + * @return {Boolean} true if the event */ dispatchEvent(event) { var type = event.type; + var cancelable = event.cancelable; if (!type) { throw new TypeError('Argument is not a valid event.'); } if (!this.events.has(type)) { - return; + return false; } var set = this.events.get(type); for (let listener of set) { - let canceled = listener(event); - if (event.cancelable && canceled) { - break; + try { + listener(event); + if (cancelable && event.defaultPrevented) { + return true; + } + } catch (e) { + console.error(e); } } + return false; } } diff --git a/app.fetch.js b/app.fetch.js index 4fb2d52..a4e5f90 100644 --- a/app.fetch.js +++ b/app.fetch.js @@ -1,1454 +1,1453 @@ (function() { -'use strict'; -/** - * This object represents default options for SocketFetch class. - * This can be set before class initialize. - */ -const SocketFetchOptions = { + 'use strict'; /** - * Use this property to set up script import URL. - * This library uses web workders. Sometimes it is necessary to change import path of the - * library. - * By default the script will look in / path for web workers. However bower or combined scripts - * me have been placed in different location so this should be set to locate a file. - * - * Example: - * /path/to/file/%s - * - * Keep the %s. The script will replace it with corresponding file name. + * This object represents default options for SocketFetch class. + * This can be set before class initialize. */ - importUrl: null -}; -/******************************************************************************* - * Copyright 2016 Pawel Psztyc, The ARC team - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - ******************************************************************************/ -/* global chrome, Request, Headers, ArcEventSource, ArcRequest, ArcResponse */ -var URLSearchParams = URLSearchParams || {}; -/** - * A SocketFetch class is similar to fetch API but it uses chrome.socket as a transport. - * - * This library require Zlib to run. - * - * @example - * - * let request = SocketFetch('http://domain.com').fetch().then((response) => { - * if (response.ok) { - * return response.json(); - * } - * }); - * - * - * TODO: Add progress event. - */ -class SocketFetch extends ArcEventSource { - /** - * Partially based on - * https://github.com/ahmadnassri/chrome.sockets.tcp.xhr/ + const SocketFetchOptions = { + /** + * Use this property to set up script import URL. + * This library uses web workders. Sometimes it is necessary to change import path of the + * library. + * By default the script will look in / path for web workers. However bower or combined scripts + * me have been placed in different location so this should be set to locate a file. + * + * Example: + * /path/to/file/%s + * + * Keep the %s. The script will replace it with corresponding file name. + */ + importUrl: null + }; + /******************************************************************************* + * Copyright 2016 Pawel Psztyc, The ARC team + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at * + * http://www.apache.org/licenses/LICENSE-2.0 * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ + + var URLSearchParams = URLSearchParams || {}; + /** + * A SocketFetch class is similar to fetch API but it uses chrome.socket as a transport. * - * @constructor - * @property {String} url Defines the resource that you wish to fetch. This can either be: - * A USVString containing the direct URL of the resource you want to fetch. + * This library require Zlib to run. * - * @param {String|ArcRequest|Request} url An URL, {@link Request} or {@link ArcRequest} object. - * @property {Object} opts (Optional) An options object containing any custom settings that you - * want to apply to the request. The possible options are: - * - method: The request method, e.g., GET, POST. - * - headers: Any headers you want to add to your request, contained within a Headers object or - * an object literal with ByteString values. - * - body: Any body that you want to add to your request: this can be a Blob, BufferSource, - * FormData, URLSearchParams, or USVString object. Note that a request using the GET or HEAD - * method cannot have a body. - * - redirect: The redirect mode to use: follow or error. If follow is set the result will - * contain redairect information. + * ``` + * let request = SocketFetch('http://domain.com').fetch().then((response) => { + * if (response.ok) { + * return response.json(); + * } + * }); + * ``` */ - constructor(url, opts) { - super(); - this._logs = []; + class SocketFetch extends ArcEventSource { /** - * A original request object. - * This will contain data passed to the constructor. + * Partially based on + * https://github.com/ahmadnassri/chrome.sockets.tcp.xhr/ * - * @type {Request} - */ - this._request = this._createRequest(url, opts); - /** - * The Response interface of the Fetch API represents the response to a request. * - * @type {Response} - */ - this._response = undefined; - - if (typeof opts.debug !== 'undefined') { - this.debug = opts.debug; - } else { - this.debug = false; - } - /** - * True if the request has been aborted. - */ - this.aborted = false; - /** - * A boolean property state represents the socket read status. It can be either: - * STATUS (0) - expecting the message is contain a status line - * HEADERS (1) - expecting the message is containing headers part of the message (headers are - * optional) - * BODY (2) - expecting to read a message body - * DONE (3) - message has been fully read. This status can be set by readSocketError function - * when server closes the connection. - */ - this.state = 0; - /** - * A set of redirects. * - * @type {Set} - */ - this.redirects = undefined; - /** - * A reference to main promise. - */ - this._mainPromise = { - resolve: undefined, - reject: undefined - }; - /** - * Set of informations relevant to current socket connection. + * @constructor + * @property {String} url Defines the resource that you wish to fetch. This can either be: + * A USVString containing the direct URL of the resource you want to fetch. + * + * @param {String|ArcRequest|Request} url An URL, {@link Request} or {@link ArcRequest} object. + * @property {Object} opts (Optional) An options object containing any custom settings that you + * want to apply to the request. The possible options are: + * - method: The request method, e.g., GET, POST. + * - headers: Any headers you want to add to your request, contained within a Headers object or + * an object literal with ByteString values. + * - body: Any body that you want to add to your request: this can be a Blob, BufferSource, + * FormData, URLSearchParams, or USVString object. Note that a request using the GET or HEAD + * method cannot have a body. + * - redirect: The redirect mode to use: follow or error. If follow is set the result will + * contain redairect information. + * - requestId: Application innternall requests indetification system. It is set on each event + * detail object */ - this._connection = { - /** - * Set to true when the secured connection should be made. - */ - useSSL: false, - /** - * Socket ID the instance is operating on. - * - * @type {Number} - */ - socketId: undefined, - /** - * A connection can be made only once in one instance. It fthe flag state is true then - * the implementation will throw an error. - * - * @type {Boolean} - */ - started: false, - /** - * A host the socket is connecting to. - * - * @type {String} - */ - host: undefined, - /** - * A port the socket is connecting on. - * - * @type {Number} - * @default 80 - */ - port: 80, - /** - * A integer representing status code of the response. - * - * @type {Number} - */ - status: undefined, - /** - * An optional string representing response status message - * - * @type {String} - */ - statusMessage: undefined, + constructor(url, opts) { + super(); + opts = opts || {}; + this._logs = []; /** - * A read headers string. It may be incomplete if state equals HEADERS or STATUS. + * A original request object. + * This will contain data passed to the constructor. * - * @type {String} + * @type {Request} */ - headers: undefined, + this._request = this._createRequest(url, opts); + this.requestId = opts.requestId; /** - * A read response body. It may be incomplete if readyState does not equals DONE. + * The Response interface of the Fetch API represents the response to a request. * - * @type {Uint8Array} + * @type {Response} */ - body: undefined, + this._response = undefined; + + if (typeof opts.debug !== 'undefined') { + this.debug = opts.debug; + } else { + this.debug = false; + } /** - * As a shortcut for finding Content-Length header in a headers list. It can be either a - * getter function that is looking for a Content-Length header or a value set after headers - * are parsed. - * - * @type {Number} + * True if the request has been aborted. */ - contentLength: undefined, + this.aborted = false; /** - * A shortcut for finding Content-Length header in a headers list. It can be either a getter - * function that is looking for a Content-Length header or a value set after headers are - * parsed - * - * @type {Boolean} + * A boolean property state represents the socket read status. It can be either: + * STATUS (0) - expecting the message is contain a status line + * HEADERS (1) - expecting the message is containing headers part of the message (headers are + * optional) + * BODY (2) - expecting to read a message body + * DONE (3) - message has been fully read. This status can be set by readSocketError function + * when server closes the connection. */ - chunked: undefined, + this.state = 0; /** - * A flag determining that the response is chunked Transfer-Encoding. When Transfer-Encoding - * header is set to "chunked" then the response will be split in chunks. Every chunk starts - * with hex number of length in chunk followed by new line character (\r\n or CR or 13|10). - * Because message received by the socket most probably will have different buffer size, the - * `readSocketData()` function may contain more than one part of chunk or incomplete part of - * chunk. + * A set of redirects. * - * @type {Number} + * @type {Set} */ - chunkSize: undefined, + this.redirects = undefined; /** - * Message sent to the remote machine as a string of source message. - * If the request consisted of binnary data it will be presented as a string. - * - * @type {String} + * A reference to main promise. */ - messageSent: undefined, + this._mainPromise = { + resolve: undefined, + reject: undefined + }; /** - * Some stats about the connection + * Set of informations relevant to current socket connection. */ - stats: { + this._connection = { /** - * Timestamp of start. - * Set just before connection attempt. + * Set to true when the secured connection should be made. */ - startTime: undefined, + useSSL: false, /** - * Time required to create TCP connection. + * Socket ID the instance is operating on. + * + * @type {Number} */ - connect: undefined, + socketId: undefined, /** - * Time required to send HTTP request to the server. + * A connection can be made only once in one instance. It fthe flag state is true then + * the implementation will throw an error. + * + * @type {Boolean} */ - send: undefined, + started: false, /** - * Waiting for a response from the server. + * A host the socket is connecting to. + * + * @type {String} */ - wait: undefined, + host: undefined, /** - * Time required to read entire response from the server. + * A port the socket is connecting on. + * + * @type {Number} + * @default 80 */ - receive: undefined, + port: 80, /** - * Time required for SSL/TLS negotiation. + * A integer representing status code of the response. + * + * @type {Number} */ - ssl: undefined, - _firstReceived: undefined, - _lastReceived: undefined, - _messageSending: undefined, - _waitingStart: undefined - } - }; - /** - * Request timeout settings. - */ - this._timeout = { - /** - * True when connection is timed out. - * - * @type {Boolean} - */ - timedout: false, + status: undefined, + /** + * An optional string representing response status message + * + * @type {String} + */ + statusMessage: undefined, + /** + * A read headers string. It may be incomplete if state equals HEADERS or STATUS. + * + * @type {String} + */ + headers: undefined, + /** + * A read response body. It may be incomplete if readyState does not equals DONE. + * + * @type {Uint8Array} + */ + body: undefined, + /** + * As a shortcut for finding Content-Length header in a headers list. It can be either a + * getter function that is looking for a Content-Length header or a value set after headers + * are parsed. + * + * @type {Number} + */ + contentLength: undefined, + /** + * A shortcut for finding Content-Length header in a headers list. It can be either a getter + * function that is looking for a Content-Length header or a value set after headers are + * parsed + * + * @type {Boolean} + */ + chunked: undefined, + /** + * A flag determining that the response is chunked Transfer-Encoding. When Transfer-Encoding + * header is set to "chunked" then the response will be split in chunks. Every chunk starts + * with hex number of length in chunk followed by new line character (\r\n or CR or 13|10). + * Because message received by the socket most probably will have different buffer size, the + * `readSocketData()` function may contain more than one part of chunk or incomplete part of + * chunk. + * + * @type {Number} + */ + chunkSize: undefined, + /** + * Message sent to the remote machine as a string of source message. + * If the request consisted of binnary data it will be presented as a string. + * + * @type {String} + */ + messageSent: undefined, + /** + * Some stats about the connection + */ + stats: { + /** + * Timestamp of start. + * Set just before connection attempt. + */ + startTime: undefined, + /** + * Time required to create TCP connection. + */ + connect: undefined, + /** + * Time required to send HTTP request to the server. + */ + send: undefined, + /** + * Waiting for a response from the server. + */ + wait: undefined, + /** + * Time required to read entire response from the server. + */ + receive: undefined, + /** + * Time required for SSL/TLS negotiation. + */ + ssl: undefined, + _firstReceived: undefined, + _lastReceived: undefined, + _messageSending: undefined, + _waitingStart: undefined + } + }; /** - * User set timeout (in miliseconds). - * - * @type {Number} + * Request timeout settings. */ - timeout: this._request.timeout, + this._timeout = { + /** + * True when connection is timed out. + * + * @type {Boolean} + */ + timedout: false, + /** + * User set timeout (in miliseconds). + * + * @type {Number} + */ + timeout: this._request.timeout, + /** + * An id of timer function. + * + * @type {Number} + */ + timeoutId: undefined + }; /** - * An id of timer function. - * - * @type {Number} + * True if the request has FormData object as payload. + * In this case `socket-fetch` must extract generated boudary and update content type + * header to `multipart/form-data;boundary=[extracted boundary]` */ - timeoutId: undefined - }; + this._isMultipartRequest = false; + + this._setupUrlData(); + } /** - * True if the request has FileData object as payload. - * In this case `socket-fetch` must extract generated boudary and update content type - * header to `multipart/form-data;boundary=[extracted boundary]` + * Timeout set to the request. */ - this._hasFileData = false; - - this._setupUrlData(); - } - /** - * Timeout set to the request. - */ - get timeout() { - return this._timeout.timedout; - } - /** - * Get a Request object. - * - * @type {Request} - */ - get request() { - return this._request; - } - /** - * Get a Response object. - * - * @type {Response} - */ - get response() { - return this._response; - } - /** - * Replace popular shemas with port number - * - */ - get protocol2port() { - return { - 'http': 80, - 'https': 443, - 'ftp': 21 - }; - } - /** - * Status indicating thet expecting a ststus message. - * - * @type {Number} - * @default 0 - */ - static get STATUS() { - return 0; - } - /** - * Status indicating thet expecting headers. - * - * @type {Number} - * @default 1 - */ - static get HEADERS() { - return 1; - } - /** - * Status indicating thet expecting a body message. - * - * @type {Number} - * @default 2 - */ - static get BODY() { - return 2; - } - /** - * Status indicating thet the message has been read and connection is closing or closed. - * - * @type {Number} - * @default 0 - */ - static get DONE() { - return 3; - } - /** - * Returns new promise and perform a request. - * - * @return {Promise} Fulfilled promise will result with Resposne object. - */ - fetch() { - return new Promise((resolve, reject) => { - if (this.aborted) { - this._createResponse({includeRedirects: true}) - .then(() => { - resolve(this._response); - }) - .catch((e) => { - this._cancelTimer(); - reject({ - 'message': e.message + get timeout() { + return this._timeout.timedout; + } + /** + * Get a Request object. + * + * @type {Request} + */ + get request() { + return this._request; + } + /** + * Get a Response object. + * + * @type {Response} + */ + get response() { + return this._response; + } + /** + * Replace popular shemas with port number + * + */ + get protocol2port() { + return { + 'http': 80, + 'https': 443, + 'ftp': 21 + }; + } + /** + * Status indicating thet expecting a ststus message. + * + * @type {Number} + * @default 0 + */ + static get STATUS() { + return 0; + } + /** + * Status indicating thet expecting headers. + * + * @type {Number} + * @default 1 + */ + static get HEADERS() { + return 1; + } + /** + * Status indicating thet expecting a body message. + * + * @type {Number} + * @default 2 + */ + static get BODY() { + return 2; + } + /** + * Status indicating thet the message has been read and connection is closing or closed. + * + * @type {Number} + * @default 0 + */ + static get DONE() { + return 3; + } + /** + * Returns new promise and perform a request. + * + * @return {Promise} Fulfilled promise will result with Resposne object. + */ + fetch() { + return new Promise((resolve, reject) => { + if (this.aborted) { + this._createResponse({ + includeRedirects: true + }) + .then(() => { + resolve(this._response); + }) + .catch((e) => { + this._cancelTimer(); + reject({ + 'message': e.message + }); + this._cleanUp(); }); - this._cleanUp(); - }); - return; - } + return; + } - if (this._connection.started) { - reject(new Error('This connection has been made. Create a new class and use it instead.')); - return; - } + if (this._connection.started) { + reject(new Error('This connection has been made. Create a new class and use it instead.')); + return; + } - this._connection.started = true; - this._connection._readFn = this.readSocketData.bind(this); - this._connection._errorFn = this.readSocketError.bind(this); - this._mainPromise.reject = reject; - this._mainPromise.resolve = resolve; - chrome.sockets.tcp.onReceive.addListener(this._connection._readFn); - chrome.sockets.tcp.onReceiveError.addListener(this._connection._errorFn); + this._connection.started = true; + this._connection._readFn = this.readSocketData.bind(this); + this._connection._errorFn = this.readSocketError.bind(this); + this._mainPromise.reject = reject; + this._mainPromise.resolve = resolve; + chrome.sockets.tcp.onReceive.addListener(this._connection._readFn); + chrome.sockets.tcp.onReceiveError.addListener(this._connection._errorFn); - this._authRequest().then(() => this._createConnection()); - }); - } - // set auth methods. - _authRequest() { - if (!this._request.auth) { - return Promise.resolve(); - } - let auth = this._request.auth; - let obj = null; - switch (auth.method) { - case 'ntlm': - obj = new FetchNtlmAuth(auth); - obj.url = this.request.url; - obj.state = 0; - break; - case 'basic': - obj = new FetchBasicAuth(auth); - break; - case 'digest': - obj = new FetchDigestAuth(auth); - obj.url = this.request.url; - obj.httpMethod = this.request.method; - break; + this._authRequest().then(() => this._createConnection()); + }); } - if (!obj) { + // set auth methods. + _authRequest() { + if (!this._request.auth) { + return Promise.resolve(); + } + let auth = this._request.auth; + let obj = null; + switch (auth.method) { + case 'ntlm': + obj = new FetchNtlmAuth(auth); + obj.url = this.request.url; + obj.state = 0; + break; + case 'basic': + obj = new FetchBasicAuth(auth); + break; + case 'digest': + obj = new FetchDigestAuth(auth); + obj.url = this.request.url; + obj.httpMethod = this.request.method; + break; + } + if (!obj) { + return Promise.resolve(); + } + this.auth = obj; return Promise.resolve(); } - this.auth = obj; - return Promise.resolve(); - } - /** Called after socket has been created and connection yet to be made. */ - _createConnection() { - var socketProperties = { - name: 'arc' - }; - if (this._connection.socketId) { - this.log('Reusing last socket and connection: '); - this._onConnected(); - return; - } - chrome.sockets.tcp.create(socketProperties, (createInfo) => { - this.log('Created socket', createInfo.socketId); - this._connection.socketId = createInfo.socketId; - this.log('Connecting to %s:%d', this._connection.host, this._connection.port); - this._connection.stats.startTime = Date.now(); - let promise; - if (this._connection.useSSL) { - promise = this._connectSecure(createInfo.socketId, this._connection.host, - this._connection.port); - } else { - promise = this._connect(createInfo.socketId, this._connection.host, this._connection.port); - } - promise.then(() => { - this._runTimer(); - this.log('Connected to socked for host: ', this._connection.host, ' and port ', - this._connection.port); - this._readyState = 1; + /** Called after socket has been created and connection yet to be made. */ + _createConnection() { + var socketProperties = { + name: 'arc' + }; + if (this._connection.socketId) { + this.log('Reusing last socket and connection: '); this._onConnected(); - }) - .catch((cause) => { - if (this.redirects) { - // There were a redirects so it has something to display. - // Don't just throw an error, construct a response that is errored. - this._publishResponse({ - includeRedirects: true, - error: cause - }); - return; + return; + } + chrome.sockets.tcp.create(socketProperties, (createInfo) => { + this.log('Created socket', createInfo.socketId); + this._connection.socketId = createInfo.socketId; + this.log('Connecting to %s:%d', this._connection.host, this._connection.port); + this._connection.stats.startTime = Date.now(); + let promise; + if (this._connection.useSSL) { + promise = this._connectSecure(createInfo.socketId, this._connection.host, + this._connection.port); + } else { + promise = this._connect(createInfo.socketId, this._connection.host, this._connection.port); } - this._readyState = 0; - this._mainPromise.reject(cause); - this._cleanUp(); + promise.then(() => { + this._runTimer(); + this.log('Connected to socked for host: ', this._connection.host, ' and port ', + this._connection.port); + this._readyState = 1; + this._onConnected(); + }) + .catch((cause) => { + if (this.redirects) { + // There were a redirects so it has something to display. + // Don't just throw an error, construct a response that is errored. + this._publishResponse({ + includeRedirects: true, + error: cause + }); + return; + } + this._readyState = 0; + this._mainPromise.reject(cause); + this._cleanUp(); + }); }); - }); - } - /** - * Connect to a socket using secure connection. - * Note that ths function will result with paused socket. - * It must be unpaused after sending a data to remote host to receive a response. - * - * This method will throw an error when connection can't be made or was unable to secure the - * conection. - * - * @param {Number} socketId ID of the socket that the instance is operating on. - * @param {String} host A host name to connect to - * @param {Number} port A port number to connect to. - * - * @return {Promise} Fulfilled promise when connection has been made. Rejected promise will - * contain an Error object with description message. - */ - _connectSecure(socketId, host, port) { - return new Promise((resolve, reject) => { - chrome.sockets.tcp.setPaused(socketId, true, () => { - let connectionStart = performance.now(); - chrome.sockets.tcp.connect(socketId, host, port, (connectResult) => { - this._connection.stats.connect = performance.now() - connectionStart; - if (chrome.runtime.lastError) { - this.log(chrome.runtime.lastError); - reject(chrome.runtime.lastError); - return; - } - if (connectResult !== 0) { - reject('Connection to host ' + host + ' on port ' + port + ' unsuccessful'); - return; - } - let secureStart = performance.now(); - chrome.sockets.tcp.secure(socketId, (secureResult) => { + } + /** + * Connect to a socket using secure connection. + * Note that ths function will result with paused socket. + * It must be unpaused after sending a data to remote host to receive a response. + * + * This method will throw an error when connection can't be made or was unable to secure the + * conection. + * + * @param {Number} socketId ID of the socket that the instance is operating on. + * @param {String} host A host name to connect to + * @param {Number} port A port number to connect to. + * + * @return {Promise} Fulfilled promise when connection has been made. Rejected promise will + * contain an Error object with description message. + */ + _connectSecure(socketId, host, port) { + return new Promise((resolve, reject) => { + chrome.sockets.tcp.setPaused(socketId, true, () => { + let connectionStart = performance.now(); + chrome.sockets.tcp.connect(socketId, host, port, (connectResult) => { + this._connection.stats.connect = performance.now() - connectionStart; if (chrome.runtime.lastError) { this.log(chrome.runtime.lastError); reject(chrome.runtime.lastError); return; } - this._connection.stats.ssl = performance.now() - secureStart; - if (secureResult !== 0) { - reject('Unable to secure a connection to host ' + host + ' on port ' + port); + if (connectResult !== 0) { + reject('Connection to host ' + host + ' on port ' + port + ' unsuccessful'); return; } - resolve(); + let secureStart = performance.now(); + chrome.sockets.tcp.secure(socketId, (secureResult) => { + if (chrome.runtime.lastError) { + this.log(chrome.runtime.lastError); + reject(chrome.runtime.lastError); + return; + } + this._connection.stats.ssl = performance.now() - secureStart; + if (secureResult !== 0) { + reject('Unable to secure a connection to host ' + host + ' on port ' + port); + return; + } + resolve(); + }); }); }); }); - }); - } - /** - * Connect to a socket. To use a secure connection call `_connectSecure` method. - * Note that ths function will result with paused socket. - * It must be unpaused after sending a data to remote host to receive a response. - * - * @param {Number} socketId ID of the socket that the instance is operating on. - * @param {String} host A host name to connect to - * @param {Number} port A port number to connect to. - * - * @return {Promise} Fulfilled promise when connection has been made. Rejected promise will - * contain an Error object with description message. - */ - _connect(socketId, host, port) { - return new Promise((resolve, reject) => { - chrome.sockets.tcp.setPaused(socketId, true, () => { - let connectionStart = performance.now(); - chrome.sockets.tcp.connect(socketId, host, port, (connectResult) => { - this._connection.stats.connect = performance.now() - connectionStart; - if (chrome.runtime.lastError) { - this.log(chrome.runtime.lastError); - reject(chrome.runtime.lastError); - return; - } - if (connectResult !== 0) { - reject('Connection to host ' + host + ' on port ' + port + ' unsuccessful'); - return; - } - resolve(); + } + /** + * Connect to a socket. To use a secure connection call `_connectSecure` method. + * Note that ths function will result with paused socket. + * It must be unpaused after sending a data to remote host to receive a response. + * + * @param {Number} socketId ID of the socket that the instance is operating on. + * @param {String} host A host name to connect to + * @param {Number} port A port number to connect to. + * + * @return {Promise} Fulfilled promise when connection has been made. Rejected promise will + * contain an Error object with description message. + */ + _connect(socketId, host, port) { + return new Promise((resolve, reject) => { + chrome.sockets.tcp.setPaused(socketId, true, () => { + let connectionStart = performance.now(); + chrome.sockets.tcp.connect(socketId, host, port, (connectResult) => { + this._connection.stats.connect = performance.now() - connectionStart; + if (chrome.runtime.lastError) { + this.log(chrome.runtime.lastError); + reject(chrome.runtime.lastError); + return; + } + if (connectResult !== 0) { + reject('Connection to host ' + host + ' on port ' + port + ' unsuccessful'); + return; + } + resolve(); + }); }); }); - }); - } - /** - * Shortcut for dispatching a custom event on this. - * - * @param {String} name Name of the event - * @param {Object?} details Optional detail object. - */ - _dispatchCustomEvent(name, details) { - var opts = { - 'bubbles': true, - 'cancelable': false - }; - if (details) { - opts.detail = details; } - this.dispatchEvent(new CustomEvent(name, opts)); - } - /** Disconnect from the socket and release resources. */ - disconnect() { - this.log('Disconnect'); - if (!this._connection.socketId) { - return Promise.resolve(); + /** + * Shortcut for dispatching a custom event on this. + * + * @param {String} name Name of the event + * @param {?Object} details Optional detail object. + * @param {?} + */ + _dispatchCustomEvent(name, details, cancelable) { + cancelable = cancelable || false; + var opts = { + bubbles: true, + cancelable: cancelable + }; + if (details) { + opts.detail = details; + } + return this.dispatchEvent(new CustomEvent(name, opts)); } - return new Promise((resolve) => { - chrome.sockets.tcp.disconnect(this._connection.socketId, () => { - if (chrome.runtime.lastError) { - this.log(chrome.runtime.lastError, 'warn'); - } - this._readyState = 0; - chrome.sockets.tcp.close(this._connection.socketId, () => { + /** Disconnect from the socket and release resources. */ + disconnect() { + this.log('Disconnect'); + if (!this._connection.socketId) { + return Promise.resolve(); + } + return new Promise((resolve) => { + chrome.sockets.tcp.disconnect(this._connection.socketId, () => { if (chrome.runtime.lastError) { this.log(chrome.runtime.lastError, 'warn'); } - this._connection.socketId = undefined; - resolve(); + this._readyState = 0; + chrome.sockets.tcp.close(this._connection.socketId, () => { + if (chrome.runtime.lastError) { + this.log(chrome.runtime.lastError, 'warn'); + } + this._connection.socketId = undefined; + resolve(); + }); }); }); - }); - } - /** Set `debug: true` flag in init object to see debug messages */ - log(...entry) { - if (this.debug) { - console.log.apply(console, entry); } - this._logs.push(entry); - } - /** - * Calling abort function will immidietly result with Promise rejection. - * It will close the connection and clean up the resources. - */ - abort() { - this.aborted = true; - this._dispatchCustomEvent('abort'); - this.state = SocketFetch.DONE; - this._mainPromise.reject(new Error('Request aborted')); - this._cleanUp(); - } - /** - * Create a request object. - */ - _createRequest(url, opts) { - if (url instanceof Request || - url instanceof ArcRequest) { - return new ArcRequest(url); + /** Set `debug: true` flag in init object to see debug messages */ + log(...entry) { + if (this.debug) { + console.log.apply(console, entry); + } + this._logs.push(entry); } - if (opts.headers) { - opts.headers = new Headers(opts.headers); - } else { - opts.headers = new Headers(); + /** + * Calling abort function will immidietly result with Promise rejection. + * It will close the connection and clean up the resources. + */ + abort() { + this.aborted = true; + this._dispatchCustomEvent('abort'); + this.state = SocketFetch.DONE; + this._mainPromise.reject(new Error('Request aborted')); + this._cleanUp(); } - var defaults = { - 'method': 'GET', - 'redirect': 'follow' - }; - opts = Object.assign(defaults, opts); - if (['GET', 'HEADER'].indexOf(opts.method.toUpperCase()) !== -1) { - delete opts.body; + /** + * Create a request object. + */ + _createRequest(url, opts) { + opts = opts || {}; + if (url instanceof Request || + url instanceof ArcRequest) { + return new ArcRequest(url); + } + if (opts.headers) { + opts.headers = new Headers(opts.headers); + } else { + opts.headers = new Headers(); + } + var defaults = { + 'method': 'GET', + 'redirect': 'follow' + }; + opts = Object.assign(defaults, opts); + if (['GET', 'HEADER'].indexOf(opts.method.toUpperCase()) !== -1) { + delete opts.body; + } + return new ArcRequest(url, opts); } - return new ArcRequest(url, opts); - } - /** - * Create a response object. - * - * @param {Object} opts An options to construct a response object: - * - {Boolean} includeRedirects If true the response will have information about redirects. - * - {Error} error An error object when the response is errored. - * @return {ArcResponse} A response object. - */ - _createResponse(opts) { - return new Promise((resolve, reject) => { + /** + * Create a response object. + * + * @param {Object} opts An options to construct a response object: + * - {Boolean} includeRedirects If true the response will have information about redirects. + * - {Error} error An error object when the response is errored. + * @return {ArcResponse} A response object. + */ + _createResponse(opts) { if (opts.error) { - resolve(null); - return; + return Promise.resolve(); + } + + if (this.aborted) { + return Promise.resolve(); } + var status = this._connection.status; if (status < 100 || status > 599) { - reject(new Error(`The response status "${status}" is not allowed. - See HTTP spec for more details: https://tools.ietf.org/html/rfc2616#section-6.1.1`)); - return; + return Promise.reject(new Error(`The response status "${status}" is not allowed. + See HTTP spec for more details: https://tools.ietf.org/html/rfc2616#section-6.1.1`)); } else if (status === undefined) { - reject(new Error(`The response status is empty. - It means that the successful connection wasn't made. Check your request parameters.`)); - return; - } - if (this.aborted) { - return; + return Promise.reject(new Error(`The response status is empty. + It means that the successful connection wasn't made. Check your request parameters.`)); } + + var promise; if (this._connection.body) { - resolve(this.decompressData(this._connection.body)); + promise = this.decompressData(this._connection.body); } else { - resolve(this._connection.body); - } - }) - .then((body) => { - let stats = Object.assign({}, this._connection.stats); - delete stats._firstReceived; - delete stats._messageSending; - delete stats._waitingStart; - let options = { - status: this._connection.status, - statusText: this._connection.statusMessage, - headers: this._connection.headers, - stats: stats - }; - if (opts.error) { - options.error = opts.error; - } - if (opts.includeRedirects && this.redirects && this.redirects.size) { - options.redirects = this.redirects; + promise = Promise.resolve(this._connection.body); } - if (this._connection.status === 401) { - if (this.auth) { - options.auth = this.auth; - } else { - let auth = (this._connection.headers && this._connection.headers.has && - this._connection.headers.has('www-authenticate')) ? - this._connection.headers.get('www-authenticate') : undefined; - let aObj = { - 'method': 'unknown' - }; - if (auth) { - auth = auth.toLowerCase(); - if (auth.indexOf('ntlm') !== -1) { - aObj.method = 'ntlm'; - } else if (auth.indexOf('basic') !== -1) { - aObj.method = 'basic'; - } else if (auth.indexOf('digest') !== -1) { - aObj.method = 'digest'; + + return promise.then((body) => { + let stats = Object.assign({}, this._connection.stats); + delete stats._firstReceived; + delete stats._messageSending; + delete stats._waitingStart; + let options = { + status: this._connection.status, + statusText: this._connection.statusMessage, + headers: this._connection.headers, + stats: stats + }; + if (opts.error) { + options.error = opts.error; + } + if (opts.includeRedirects && this.redirects && this.redirects.size) { + options.redirects = this.redirects; + } + if (this._connection.status === 401) { + if (this.auth) { + options.auth = this.auth; + } else { + let auth = (this._connection.headers && this._connection.headers.has && + this._connection.headers.has('www-authenticate')) ? + this._connection.headers.get('www-authenticate') : undefined; + let aObj = { + 'method': 'unknown' + }; + if (auth) { + auth = auth.toLowerCase(); + if (auth.indexOf('ntlm') !== -1) { + aObj.method = 'ntlm'; + } else if (auth.indexOf('basic') !== -1) { + aObj.method = 'basic'; + } else if (auth.indexOf('digest') !== -1) { + aObj.method = 'digest'; + } } + options.auth = aObj; } - options.auth = aObj; } - } - this._response = new ArcResponse(body, options); - this._response.logs = this._logs; - this._logs = []; - }); - } - /** - * If timeout init option is set then when connection is established the - * program will start counter after when it fire the connection will be aborted - * (with abort flag and abort event) and `timeout` flag set to true. - * - * Note that timer function in JavaScript environment can't guarantee execution - * after exactly set amount of time. Instead it will fire event in next empty slot - * in event queue. - * - * Note that the timer run at the moment when connection was established. - */ - _runTimer() { - if (!this._timeout.timeout) { - return; + const resp = new ArcResponse(body, options); + this._response = resp; + this._response.logs = this._logs; + this._logs = []; + return resp; + }); } - this._cancelTimer(); - this._timeout.timeoutId = window.setTimeout(() => { - if (this.state !== SocketFetch.DONE && !this._timeout.timedout) { - this._timeout.timedout = true; - this.abort(); + /** + * If timeout init option is set then when connection is established the + * program will start counter after when it fire the connection will be aborted + * (with abort flag and abort event) and `timeout` flag set to true. + * + * Note that timer function in JavaScript environment can't guarantee execution + * after exactly set amount of time. Instead it will fire event in next empty slot + * in event queue. + * + * Note that the timer run at the moment when connection was established. + */ + _runTimer() { + if (!this._timeout.timeout) { + return; } - }, this._timeout.timeout); - } - /** - * Cancel any active timeout timer. - */ - _cancelTimer() { - if (!this._timeout.timeoutId) { - return; + this._cancelTimer(); + this._timeout.timeoutId = window.setTimeout(() => { + if (this.state !== SocketFetch.DONE && !this._timeout.timedout) { + this._timeout.timedout = true; + this.abort(); + } + }, this._timeout.timeout); } - window.clearTimeout(this._timeout.timeoutId); - this._timeout.timeoutId = undefined; - } - /** Called when the connection has been established. */ - _onConnected() { - if (this.aborted) { - return; + /** + * Cancel any active timeout timer. + */ + _cancelTimer() { + if (!this._timeout.timeoutId) { + return; + } + window.clearTimeout(this._timeout.timeoutId); + this._timeout.timeoutId = undefined; } - var promise; - if (this.auth && this.auth.method === 'ntlm') { - promise = this.generateNtlmMessage(); - } else if (this.auth && this.auth.method === 'basic') { - this.setupBasicAuth(); - this.auth = undefined; // Don't need it anymore - promise = this.generateMessage(); - } else if (this.auth && this.auth.method === 'digest') { - let auth = this.auth.getAuthHeader(); - if (auth) { - if (!this._request.headers) { - this._request.headers = new Headers(); + /** Called when the connection has been established. */ + _onConnected() { + if (this.aborted) { + return; + } + var isNtlm = false; + if (this.auth && this.auth.method) { + switch (this.auth.method) { + case 'ntlm': + isNtlm = true; + break; + case 'basic': + this.setupBasicAuth(); + this.auth = undefined; + break; + case 'digest': + let auth = this.auth.getAuthHeader(); + if (auth) { + if (!this._request.headers) { + this._request.headers = new Headers(); + } + this._request.headers.set( + 'Authorization', auth + ); + } + break; } - this._request.headers.set( - 'Authorization', auth - ); } - promise = this.generateMessage(); - } else { - promise = this.generateMessage(); + var promise = isNtlm ? this.generateNtlmMessage() : this.generateMessage(); + promise.then((buffer) => { + let message = this.arrayBufferToString(buffer); + if (this.debug) { + this.log('Generated message to send\n' + message); + } + this.log('Sending message.'); + this._connection.messageSent = message; + this._connection.stats._messageSending = performance.now(); + chrome.sockets.tcp.send(this._connection.socketId, buffer, this.onSend.bind(this)); + }); } - - promise.then((buffer) => { - let message = this.arrayBufferToString(buffer); - if (this.debug) { - this.log('Generated message to send\n' + message); - } - this.log('Sending message.'); - this._connection.messageSent = message; - this._connection.stats._messageSending = performance.now(); - chrome.sockets.tcp.send(this._connection.socketId, buffer, this.onSend.bind(this)); - }); - } - /** - * Generate a message for socket. - */ - generateMessage() { - return new Promise((resolve, reject) => { + /** + * Generate a message for socket. + */ + generateMessage() { if (this._request.body) { - this._createFileBuffer() - .then((fileBuffer) => { - if (this._hasFileData) { - //extract boundary and update content type header - let boundary = this._getBoundary(fileBuffer); - if (boundary) { - this._request.headers.set('content-type', 'multipart/form-data; boundary=' + - boundary); - } - } - return this._addContentLength(fileBuffer) - .then((buffer) => this._createMessageBuffer(buffer)); - }) - .then((messageBuffer) => { - resolve(messageBuffer); - }) - .catch(reject); + return this._createFileBuffer() + .then(buffer => this._updatePayloadRequestMeta(buffer)) + .then(buffer => this._createMessageBuffer(buffer)); } else { - this._addContentLength(null) - .then(() => this._createMessageBuffer()) - .then((messageBuffer) => { - resolve(messageBuffer); - }) - .catch(reject); - } - }); - } - - generateNtlmMessage() { - if (this.auth.state === 0) { - let orygHeaders = this._request.headers; - let msg = this.auth.createMessage1(this._connection.host); - this._request.headers = new Headers({ - 'Authorization': 'NTLM ' + msg.toBase64() - }); - return this._createMessageBuffer() - .then((buffer) => { - this._request.headers = orygHeaders; - return buffer; - }); + this._addContentLength(null); + return this._createMessageBuffer(); + } } - if (this.auth.state === 1) { - let msg = this.auth.createMessage3(this.auth.challenge, this._connection.host); - this.auth.state = 2; - this._request.headers.set('Authorization', 'NTLM ' + msg.toBase64()); - return this.generateMessage(); + /** + * Updates the request headers (content-type and content-length) when the + * request contains payload. + * + * @param {ArrayBuffer} buffer Message buffer. + * @return {[type]} [description] + */ + _updatePayloadRequestMeta(buffer) { + if (this._isMultipartRequest) { + let boundary = this._getBoundary(buffer); + if (boundary) { + this._request.headers.set('content-type', 'multipart/form-data; boundary=' + + boundary); + } + } + return this._addContentLength(buffer); } - return Promise.reject('Unknown auth state...'); - } - - setupBasicAuth() { - if (!this._request.headers) { - this._request.headers = new Headers(); + /** + * Different implementation of generating a message for the NTLM implementation. + * NTLM is based on a challenge and response that must be made on the same + * socket connection. + */ + generateNtlmMessage() { + if (this.auth.state === 0) { + let orygHeaders = this._request.headers; + let msg = this.auth.createMessage1(this._connection.host); + this._request.headers = new Headers({ + 'Authorization': 'NTLM ' + msg.toBase64() + }); + return this._createMessageBuffer() + .then((buffer) => { + this._request.headers = orygHeaders; + return buffer; + }); + } + if (this.auth.state === 1) { + let msg = this.auth.createMessage3(this.auth.challenge, this._connection.host); + this.auth.state = 2; + this._request.headers.set('Authorization', 'NTLM ' + msg.toBase64()); + return this.generateMessage(); + } + return Promise.reject('Unknown auth state...'); } - this._request.headers.set( - 'Authorization', this.auth.getHeader() - ); - } - /** - * Adds the content-length header if required. - * This function will do nothing if the request do not carry a payload or - * when the content length header is already set. - * - * @param {ArrayBuffer} buffer Generated message buffer. - */ - _addContentLength(buffer) { - if (this._request.method === 'GET') { - return Promise.resolve(buffer); - } - //HEAD must set content length header even if it's not carreing payload. - let cl = (this._request.headers && this._request.headers.get && - this._request.headers.get('Content-Length')); - if (!cl) { - if (buffer) { - this._request.headers.set('Content-Length', buffer.byteLength); - } else { - this._request.headers.set('Content-Length', 0); + setupBasicAuth() { + if (!this._request.headers) { + this._request.headers = new Headers(); } + this._request.headers.set( + 'Authorization', this.auth.getHeader() + ); } - return Promise.resolve(buffer); - } - /** - * Create a HTTP message to be send to the server. - * - * @return {Promise} Fullfiled promise with message ArrayBuffer. - */ - _createMessageBuffer(fileBuffer) { - var headers = []; - var path = this._request.uri.pathname; - var search = this._request.uri.search; - var hash = this._request.uri.hash; - if (search) { - path += search; - } - if (hash && path !== '#') { - path += hash; - } - headers.push(this._request.method + ' ' + path + ' HTTP/1.1'); - var port = this._connection.port; - var hostValue = this._connection.host; - var defaultPorts = [80, 443]; - if (defaultPorts.indexOf(port) === -1) { - hostValue += ':' + port; - } - headers.push('HOST: ' + hostValue); - if (this._request.headers) { - this._request.headers.forEach((value, key) => { - headers.push(key + ': ' + value); - }); + /** + * Adds the content-length header if required. + * This function will do nothing if the request do not carry a payload or + * when the content length header is already set. + * + * @param {ArrayBuffer} buffer Generated message buffer. + */ + _addContentLength(buffer) { + if (this._request.method === 'GET') { + return buffer; + } + //HEAD must set content length header even if it's not carreing payload. + let cl = (this._request.headers && this._request.headers.get && + this._request.headers.get('Content-Length')); + if (!cl) { + if (!this._request.headers) { + this._request.headers = new Headers(); + } + if (buffer) { + this._request.headers.set('Content-Length', buffer.byteLength); + } else { + this._request.headers.set('Content-Length', 0); + } + } + return buffer; } - var str = headers.join('\r\n'); - var buffer = this.stringToArrayBuffer(str); - var endBuffer = new Uint8Array([13, 10, 13, 10]); - return new Promise((resolve, reject) => { - let body; - if (fileBuffer) { - body = new Blob([buffer, endBuffer, fileBuffer]); - } else { - body = new Blob([buffer, endBuffer]); + + /** + * Create a HTTP message to be send to the server. + * + * @return {Promise} Fullfiled promise with message ArrayBuffer. + */ + _createMessageBuffer(fileBuffer) { + var headers = []; + var path = this._request.uri.pathname; + var search = this._request.uri.search; + var hash = this._request.uri.hash; + if (search) { + path += search; } - var reader = new FileReader(); - reader.addEventListener('loadend', (e) => { - resolve(e.target.result); - }); - reader.addEventListener('error', (e) => { - reject(e.message); - }); - reader.readAsArrayBuffer(body); - }); - } - /** - * Create an ArrayBuffer from payload data. - */ - _createFileBuffer() { - let ct = (this._request.headers && this._request.headers.has('content-type')) ? - this._request.headers.get('content-type') : undefined; - let blobOptions = {}; - if (ct) { - blobOptions.type = ct; + if (hash && path !== '#') { + path += hash; + } + headers.push(this._request.method + ' ' + path + ' HTTP/1.1'); + var port = this._connection.port; + var hostValue = this._connection.host; + var defaultPorts = [80, 443]; + if (defaultPorts.indexOf(port) === -1) { + hostValue += ':' + port; + } + headers.push('HOST: ' + hostValue); + if (this._request.headers) { + this._request.headers.forEach((value, key) => { + headers.push(key + ': ' + value); + }); + } + var buffer = this.stringToArrayBuffer(headers.join('\r\n')); + var endBuffer = new Uint8Array([13, 10, 13, 10]).buffer; + return this._concatArrayBuffers(buffer, endBuffer, fileBuffer); } - var body = this._request.body; - if (body instanceof FormData /*|| - body instanceof URLSearchParams*/) { - return this._transferAndCreateFileBuffer(); + /** + * Concatenates `ArrayBuffer`s into new Array buffer. + * It can also be used to make a copy of an `ArrayBuffer`. + * @param {...ArrayBuffer} buffers Buffers to concat. + * @return {ArrayBuffer} Concatenated ArrayBuffer. + */ + _concatArrayBuffers(...buffers) { + buffers = buffers.filter(item => !!item); + var size = buffers.reduce((accumulator, currentValue) => accumulator += currentValue.byteLength, 0); + var tmp = new Uint8Array(size); + var pointer = 0; + buffers.forEach(buffer => { + tmp.set(new Uint8Array(buffer), pointer); + pointer += buffer.byteLength; + }); + return tmp.buffer; } - return new Promise((resolve, reject) => { + + /** + * Create an ArrayBuffer from the payload data. + * In ARC the body can be only a String, File or FormData. + */ + _createFileBuffer() { + var body = this._request.body; + this._isMultipartRequest = body instanceof FormData; if (typeof body === 'string') { body = this._normalizeString(body); - body = new Blob([body], blobOptions); - } else if (body instanceof Blob) { - //nothing - } else if (body instanceof ArrayBuffer) { - body = new Blob([body], blobOptions); - } else { - reject(new Error('Unsupported payload.')); - return; } - var reader = new FileReader(); - reader.addEventListener('loadend', (e) => { - resolve(e.target.result); + let request = new Request(this._request.url, { + method: this._request.method, + headers: this._request.headers, + body: body }); - reader.addEventListener('error', (e) => { - reject(e.message); - }); - reader.readAsArrayBuffer(body); - }); - } - /** - * NormalizeLineEndingsToCRLF - * https://code.google.com/p/chromium/codesearch#chromium/src/third_party/WebKit/Source/ - * platform/text/LineEnding.cpp&rcl=1458041387&l=101 - * - * TODO: Check if using Uint8Array is faster. - */ - _normalizeString(string) { - var result = ''; - for (var i = 0; i < string.length; i++) { - let c = string[i]; - let p = string[i + 1]; - if (c === '\r') { - // Safe to look ahead because of trailing '\0'. - if (p && p !== '\n') { - // Turn CR into CRLF. + return request.arrayBuffer(); + } + /** + * NormalizeLineEndingsToCRLF + * https://code.google.com/p/chromium/codesearch#chromium/src/third_party/WebKit/Source/ + * platform/text/LineEnding.cpp&rcl=1458041387&l=101 + * + * TODO: Check if using Uint8Array is faster. + */ + _normalizeString(string) { + var result = ''; + for (var i = 0; i < string.length; i++) { + let c = string[i]; + let p = string[i + 1]; + if (c === '\r') { + // Safe to look ahead because of trailing '\0'. + if (p && p !== '\n') { + // Turn CR into CRLF. + result += '\r'; + result += '\n'; + } + } else if (c === '\n') { result += '\r'; result += '\n'; + } else { + // Leave other characters alone. + result += c; } - } else if (c === '\n') { - result += '\r'; - result += '\n'; - } else { - // Leave other characters alone. - result += c; } + return result; } - return result; - } - - _transferAndCreateFileBuffer() { - this._hasFileData = true; - var body = this._request.body; - let request = new Request(this._request.url, { - method: this._request.method, - headers: this._request.headers, - body: body - }); - return request.arrayBuffer(); - } - /** - * Read a generated by Chrome boundary. - * It will return a non empty string when FormData was passed as an `body` parameter. - */ - _getBoundary(buffer) { - var bufferView = new Uint8Array(buffer); - var startIndex = this.indexOfSubarray(bufferView, [45, 45]); - var endIndex = this.indexOfSubarray(bufferView, [13, 10]); - var boundary = bufferView.subarray(startIndex + 2, endIndex); // it starts with 2x '--' - var str = ''; - for (var i = 0, len = boundary.length; i < len; ++i) { - str += String.fromCharCode(boundary[i]); - } - return str; - } - /** - * Called when the message has been send to the remote host. - */ - onSend(sendInfo) { - this._connection.stats._waitingStart = performance.now(); - this._connection.stats.send = this._connection.stats._waitingStart - - this._connection.stats._messageSending; - if (this.aborted) { - return; - } - if (sendInfo.bytesSent < 0) { - this.log('Error writing to socket. Bytes sent: ' + sendInfo.bytesSent); - this._mainPromise.reject(new Error('Couldn\'t find host.')); - this._cleanUp(); - return; - } - chrome.sockets.tcp.setPaused(this._connection.socketId, false); - this.log('Written message. Bytes sent: ' + sendInfo.bytesSent); - this._dispatchCustomEvent('sendheaders', { - bytesWritten: sendInfo.bytesSent - }); - this._dispatchCustomEvent('loadstart'); - } - /** - * Handler for socket read event. - */ - readSocketData(readInfo) { - if (readInfo.socketId !== this._connection.socketId) { - return; - } - var now = performance.now(); - if (this.state === SocketFetch.STATUS) { - this._connection.stats._firstReceived = now; - this._connection.stats.wait = now - this._connection.stats._waitingStart; - this._dispatchCustomEvent('firstbyte'); - } else { - this._connection.stats.receive = now - this._connection.stats._firstReceived; - } - if (this.aborted) { - return; + /** + * Read a generated by Chrome boundary. + * It will return a non empty string when FormData was passed as an `body` parameter. + */ + _getBoundary(buffer) { + var bufferView = new Uint8Array(buffer); + var startIndex = this.indexOfSubarray(bufferView, [45, 45]); + var endIndex = this.indexOfSubarray(bufferView, [13, 10]); + var boundary = bufferView.subarray(startIndex + 2, endIndex); // it starts with 2x '--' + var str = ''; + for (var i = 0, len = boundary.length; i < len; ++i) { + str += String.fromCharCode(boundary[i]); + } + return str; } - if (readInfo) { - this.log('Has socket data'); - chrome.sockets.tcp.setPaused(this._connection.socketId, true); - try { - this._processSocketMessage(readInfo.data); - } catch (e) { - if (this.state === SocketFetch.STATUS || - this.state === SocketFetch.HEADERS) { - // The response is totally wrong! - this._errorRequest({ - 'message': e.message || 'Unknown error occurred' - }); - return; - } - console.error('Error occured reading part of the message', e); + /** + * Called when the message has been send to the remote host. + */ + onSend(sendInfo) { + this._connection.stats._waitingStart = performance.now(); + this._connection.stats.send = this._connection.stats._waitingStart - + this._connection.stats._messageSending; + if (this.aborted) { + return; + } + if (sendInfo.bytesSent < 0) { + this.log('Error writing to socket. Bytes sent: ' + sendInfo.bytesSent); + this._mainPromise.reject(new Error('Couldn\'t find host.')); + this._cleanUp(); + return; } chrome.sockets.tcp.setPaused(this._connection.socketId, false); - } - } - /** - * Handler for socker read error event. - */ - readSocketError(info) { - if (this.aborted) { - return; - } - if (info.socketId !== this._connection.socketId) { - return; - } - var code = Math.abs(info.resultCode); - this.log('Exit code:', code); - - if (code === 100) { - //connection has been closed by the remote server. - this.onResponseReady(); - return; - } - var message = '[chrome socket error]: ' + this.getCodeMessage(code); - this.log('readSocketError:', message, code); - if (this.state !== SocketFetch.DONE && this._mainPromise.reject) { - this._errorRequest({ - 'message': message || 'Unknown error occurred' + this.log('Written message. Bytes sent: ' + sendInfo.bytesSent); + this._dispatchCustomEvent('sendheaders', { + bytesWritten: sendInfo.bytesSent }); + this._dispatchCustomEvent('loadstart'); } - } - /** - * Process received message. - * - * @param {ArrayBuffer} data Received message. - */ - _processSocketMessage(data) { - if (this.aborted) { - return; - } - data = new Uint8Array(data); - //this.log('has message', data); - if (this.state === SocketFetch.DONE) { - return; - } - if (this.state === SocketFetch.STATUS) { - data = this._processStatus(data); - if (data === null) { + /** + * Handler for socket read event. + */ + readSocketData(readInfo) { + if (readInfo.socketId !== this._connection.socketId) { return; } - } - if (this.state === SocketFetch.HEADERS) { - data = this._processHeaders(data); - if (data === null) { + var now = performance.now(); + if (this.state === SocketFetch.STATUS) { + this._connection.stats._firstReceived = now; + this._connection.stats.wait = now - this._connection.stats._waitingStart; + this._dispatchCustomEvent('firstbyte'); + } else { + this._connection.stats.receive = now - this._connection.stats._firstReceived; + } + if (this.aborted) { return; } + if (readInfo) { + this.log('Has socket data'); + chrome.sockets.tcp.setPaused(this._connection.socketId, true); + try { + this._processSocketMessage(readInfo.data); + } catch (e) { + if (this.state === SocketFetch.STATUS || + this.state === SocketFetch.HEADERS) { + // The response is totally wrong! + this._errorRequest({ + 'message': e.message || 'Unknown error occurred' + }); + return; + } + console.error('Error occured reading part of the message', e); + } + chrome.sockets.tcp.setPaused(this._connection.socketId, false); + } } - if (this.state === SocketFetch.BODY) { - this._processBody(data); - return; - } - throw new Error('Unknown state: ' + this.state); - } - /** - * Read status line from the response. - * This function will set `status` and `statusMessage` fields - * and then will set `state` to HEADERS. - */ - _processStatus(data) { - if (this.aborted) { - return; - } - this.log('Processing status'); - var index = this.indexOfSubarray(data, [13, 10]); - var padding = 2; - if (index === -1) { - index = this.indexOfSubarray(data, [10]); - if (index === -1) { + /** + * Handler for socker read error event. + */ + readSocketError(info) { + if (this.aborted) { + return; + } + if (info.socketId !== this._connection.socketId) { + return; + } + var code = Math.abs(info.resultCode); + this.log('Exit code:', code); + + if (code === 100) { + //connection has been closed by the remote server. + this.onResponseReady(); + return; + } + var message = '[chrome socket error]: ' + this.getCodeMessage(code); + this.log('readSocketError:', message, code); + if (this.state !== SocketFetch.DONE && this._mainPromise.reject) { this._errorRequest({ - 'message': 'Unknown server response.' + 'message': message || 'Unknown error occurred' }); } - padding = 1; } - var statusArray = data.subarray(0, index); - data = data.subarray(index + padding); - var statusLine = this.arrayBufferToString(statusArray); - statusLine = statusLine.replace(/HTTP\/\d(\.\d)?\s/, ''); - var delimPos = statusLine.indexOf(' '); - var status; - var msg = ''; - if (delimPos === -1) { - status = statusLine; - } else { - status = statusLine.substr(0, delimPos); - msg = statusLine.substr(delimPos + 1); - } - status = Number(status); - if (status !== status) { - status = 0; - } - if (msg && msg.indexOf('\n') !== -1) { - msg = msg.split('\n')[0]; - } - this._connection.status = status; - this._connection.statusMessage = msg; - this.log('Received status', this._connection.status, this._connection.statusMessage); - this.state = SocketFetch.HEADERS; - return data; - } - /** - * Read headers from the received data. - */ - _processHeaders(data) { - if (this.aborted) { - return; - } - this.log('Processing headers'); - // Looking for end of headers section - var index = this.indexOfSubarray(data, [13, 10, 13, 10]); - var padding = 4; - if (index === -1) { - // It can also be 2x ASCII 10 - var _index = this.indexOfSubarray(data, [10, 10]); - if (_index !== -1) { - index = _index; - padding = 2; + /** + * Process received message. + * + * @param {ArrayBuffer} data Received message. + */ + _processSocketMessage(data) { + if (this.aborted) { + return; } + data = new Uint8Array(data); + //this.log('has message', data); + if (this.state === SocketFetch.DONE) { + return; + } + if (this.state === SocketFetch.STATUS) { + data = this._processStatus(data); + if (data === null) { + return; + } + } + if (this.state === SocketFetch.HEADERS) { + data = this._processHeaders(data); + if (data === null) { + return; + } + } + if (this.state === SocketFetch.BODY) { + this._processBody(data); + return; + } + throw new Error('Unknown state: ' + this.state); } - // https://github.com/jarrodek/socket-fetch/issues/3 - var enterIndex = this.indexOfSubarray(data, [13, 10]); - if (index === -1 && enterIndex !== 0) { - //end in next chunk - // this._connection.headers += this.arrayBufferToString(data); - if (!this._connection.headers) { - this._connection.headers = data; + /** + * Read status line from the response. + * This function will set `status` and `statusMessage` fields + * and then will set `state` to HEADERS. + */ + _processStatus(data) { + if (this.aborted) { + return; + } + this.log('Processing status'); + var index = this.indexOfSubarray(data, [13, 10]); + var padding = 2; + if (index === -1) { + index = this.indexOfSubarray(data, [10]); + if (index === -1) { + this._errorRequest({ + 'message': 'Unknown server response.' + }); + } + padding = 1; + } + var statusArray = data.subarray(0, index); + data = data.subarray(index + padding); + var statusLine = this.arrayBufferToString(statusArray); + statusLine = statusLine.replace(/HTTP\/\d(\.\d)?\s/, ''); + var delimPos = statusLine.indexOf(' '); + var status; + var msg = ''; + if (delimPos === -1) { + status = statusLine; } else { - let sum = new Int8Array(this._connection.headers.length + data.length); - sum.set(this._connection.headers); - sum.set(data, this._connection.headers.length); - this._connection.headers = sum; + status = statusLine.substr(0, delimPos); + msg = statusLine.substr(delimPos + 1); + } + status = Number(status); + if (status !== status) { + status = 0; + } + if (msg && msg.indexOf('\n') !== -1) { + msg = msg.split('\n')[0]; } - return null; + this._connection.status = status; + this._connection.statusMessage = msg; + this.log('Received status', this._connection.status, this._connection.statusMessage); + this.state = SocketFetch.HEADERS; + return data; } + /** + * Read headers from the received data. + */ + _processHeaders(data) { + if (this.aborted) { + return; + } + this.log('Processing headers'); + // Looking for end of headers section + var index = this.indexOfSubarray(data, [13, 10, 13, 10]); + var padding = 4; + if (index === -1) { + // It can also be 2x ASCII 10 + var _index = this.indexOfSubarray(data, [10, 10]); + if (_index !== -1) { + index = _index; + padding = 2; + } + } + // https://github.com/jarrodek/socket-fetch/issues/3 + var enterIndex = this.indexOfSubarray(data, [13, 10]); + if (index === -1 && enterIndex !== 0) { + // end in next chunk + // this._connection.headers += this.arrayBufferToString(data); + if (!this._connection.headers) { + this._connection.headers = data; + } else { + let sum = new Int8Array(this._connection.headers.length + data.length); + sum.set(this._connection.headers); + sum.set(data, this._connection.headers.length); + this._connection.headers = sum; + } + return null; + } - if (enterIndex !== 0) { - let headersArray = data.subarray(0, index); - if (!this._connection.headers) { - this._connection.headers = headersArray; - } else { - let sum = new Int8Array(this._connection.headers.length + headersArray.length); - sum.set(this._connection.headers); - sum.set(headersArray, this._connection.headers.length); - this._connection.headers = sum; + if (enterIndex !== 0) { + let headersArray = data.subarray(0, index); + if (!this._connection.headers) { + this._connection.headers = headersArray; + } else { + let sum = new Int8Array(this._connection.headers.length + headersArray.length); + sum.set(this._connection.headers); + sum.set(headersArray, this._connection.headers.length); + this._connection.headers = sum; + } + // this._connection.headers += this.arrayBufferToString(headersArray); } - // this._connection.headers += this.arrayBufferToString(headersArray); - } - this._connection.headers = this.arrayBufferToString(this._connection.headers); - this._parseHeaders(); - this.state = SocketFetch.BODY; - var start = index === -1 ? 0 : index; - var move = (enterIndex === 0) ? 2 : padding; - data = data.subarray(start + move); + this._connection.headers = this.arrayBufferToString(this._connection.headers); + this._parseHeaders(); + if (this.aborted) { + return; + } + this.state = SocketFetch.BODY; + var start = index === -1 ? 0 : index; + var move = (enterIndex === 0) ? 2 : padding; + data = data.subarray(start + move); - return this._postHeaders(data); - } - // Check the response headers and end the request if nescesary. - _postHeaders(data) { - if (this._request.method === 'HEAD') { - // there will be no payload anyway. (spec defined) - window.setTimeout(() => { - this.onResponseReady(); - }, 0); - return null; + return this._postHeaders(data); } + // Check the response headers and end the request if nescesary. + _postHeaders(data) { + if (this._request.method === 'HEAD') { + // there will be no payload anyway. (spec defined) + window.setTimeout(() => { + this.onResponseReady(); + }, 0); + return null; + } - if (data.length === 0) { - if (this._connection.headers && this._connection.headers.has && - this._connection.headers.has('Content-Length')) { - // If the server do not close connection and clearly indicate that there are no - // further data to receive the app can close the connection and prepare the response. - let length = Number(this._connection.headers.get('Content-Length')); - // NaN never equals NaN. This is faster. - if (length === length && length === 0) { + if (data.length === 0) { + if (this._connection.headers && this._connection.headers.has && + this._connection.headers.has('Content-Length')) { + // If the server do not close connection and clearly indicate that there are no + // further data to receive the app can close the connection and prepare the response. + let length = Number(this._connection.headers.get('Content-Length')); + // NaN never equals NaN. This is faster. + if (length === length && length === 0) { + window.setTimeout(() => { + this.onResponseReady(); + }, 0); + } + } else if (!this._connection.headers.has('Transfer-Encoding') || + !this._connection.headers.get('Transfer-Encoding')) { + // Fix for https://github.com/jarrodek/socket-fetch/issues/6 + // There is no body in the response. window.setTimeout(() => { this.onResponseReady(); }, 0); } - } else if (!this._connection.headers.has('Transfer-Encoding') || - !this._connection.headers.get('Transfer-Encoding')) { - // Fix for https://github.com/jarrodek/socket-fetch/issues/6 - // There is no body in the response. - window.setTimeout(() => { - this.onResponseReady(); - }, 0); + return null; } - return null; + return data; } - return data; - } - /** - * Process data. - * - * @param {Uint8Array} data A data to process - */ - _processBody(data) { - if (this.aborted) { - return; - } - this.log('Processing body'); - if (this._connection.chunked) { - while (true) { - if (this._connection.chunkSize === 0 && - this.indexOfSubarray(data, [13, 10, 13, 10]) === 0) { - this.onResponseReady(); - return; - } - if (!this._connection.chunkSize) { - data = this.readChunkSize(data); - this.log('Chunk size: ', this._connection.chunkSize); - if (this._connection.chunkSize === null) { - // It may happen that chrome's buffer cuts the data - // just before the chunk size. - // It should proceed it in next portion of the data. - this.log('The chunk size was null!', 'warn'); + /** + * Process data. + * + * @param {Uint8Array} data A data to process + */ + _processBody(data) { + if (this.aborted) { + return; + } + this.log('Processing body'); + if (this._connection.chunked) { + while (true) { + if (this._connection.chunkSize === 0 && + this.indexOfSubarray(data, [13, 10, 13, 10]) === 0) { + this.onResponseReady(); return; } if (!this._connection.chunkSize) { - this.onResponseReady(); + data = this.readChunkSize(data); + this.log('Chunk size: ', this._connection.chunkSize); + if (this._connection.chunkSize === null) { + // It may happen that chrome's buffer cuts the data + // just before the chunk size. + // It should proceed it in next portion of the data. + this.log('The chunk size was null!', 'warn'); + return; + } + if (!this._connection.chunkSize) { + this.onResponseReady(); + return; + } + } + let size = Math.min(this._connection.chunkSize, data.length); + this.log('Part size: ', size); + if (!this._connection.body) { + this.log('Creating new body'); + this._connection.body = new Uint8Array(data.subarray(0, size)); + } else { + this.log('Appending to the body'); + let bodySize = size + this._connection.body.length; + let body = new Uint8Array(bodySize); + body.set(this._connection.body); + body.set(data.subarray(0, size), this._connection.body.length); + this._connection.body = body; + } + this._connection.chunkSize -= size; + this.log('Body size is: ', this._connection.body.length, ' and chunk size is left is: ', + this._connection.chunkSize); + // debugger; + if (data.length === 0) { + this.log('Next chunk will start with CRLF!', 'warn'); + } + data = data.subarray(size + 2); // + CR + if (data.length === 0) { + this.log('No more data here. Waiting for new chunk'); return; } } - let size = Math.min(this._connection.chunkSize, data.length); - this.log('Part size: ', size); + } else { if (!this._connection.body) { this.log('Creating new body'); - this._connection.body = new Uint8Array(data.subarray(0, size)); + this._connection.body = new Uint8Array(data.length); + this._connection.body.set(data); + if (this._connection.body.length >= this._connection.contentLength) { + this.log('Response ready. Calling it.'); + this.onResponseReady(); + } } else { - this.log('Appending to the body'); - let bodySize = size + this._connection.body.length; - let body = new Uint8Array(bodySize); - body.set(this._connection.body); - body.set(data.subarray(0, size), this._connection.body.length); - this._connection.body = body; - } - this._connection.chunkSize -= size; - this.log('Body size is: ', this._connection.body.length, ' and chunk size is left is: ', - this._connection.chunkSize); - // debugger; - if (data.length === 0) { - this.log('Next chunk will start with CRLF!', 'warn'); - } - data = data.subarray(size + 2); // + CR - if (data.length === 0) { - this.log('No more data here. Waiting for new chunk'); - return; - } - } - } else { - if (!this._connection.body) { - this.log('Creating new body'); - this._connection.body = new Uint8Array(data.length); - this._connection.body.set(data); - if (this._connection.body.length >= this._connection.contentLength) { - this.log('Response ready. Calling it.'); - this.onResponseReady(); - } - } else { - let len = this._connection.body.length; - let sumLength = len + data.length; - let newArray = new Uint8Array(sumLength); - newArray.set(this._connection.body); - newArray.set(data, len); - this._connection.body = newArray; - this.log('Appended data to body.'); - if (newArray.length >= this._connection.contentLength) { - this.log('Response ready. Calling it.'); - this.onResponseReady(); + let len = this._connection.body.length; + let sumLength = len + data.length; + let newArray = new Uint8Array(sumLength); + newArray.set(this._connection.body); + newArray.set(data, len); + this._connection.body = newArray; + this.log('Appended data to body.'); + if (newArray.length >= this._connection.contentLength) { + this.log('Response ready. Calling it.'); + this.onResponseReady(); + } } } } - } - /** - * This method is called when the response is ready to serve. - * It the response contain an information about the redirection - * it will connect again to redirect URL if initial option `redirect` is set to `follow` - * (default) or it with throw an error if it is set to `error`. - */ - onResponseReady() { - if (this.aborted) { - return; - } - if (this.state === SocketFetch.DONE) { - return; - } - this._connection.stats._lastReceived = performance.now(); - this._connection.stats.receive = this._connection.stats._lastReceived - - this._connection.stats._firstReceived; - this.state = SocketFetch.DONE; - - var status = this._connection.status; - if (status >= 300 && status < 400) { //redirect - - if (this.redirect === 'error') { - this._mainPromise.reject({ - 'message': 'Redirects are not allowed', - 'redirect': true - }); - this._cleanUp(); + /** + * This method is called when the response is ready to serve. + * It the response contain an information about the redirection + * it will connect again to redirect URL if initial option `redirect` is set to `follow` + * (default) or it with throw an error if it is set to `error`. + */ + onResponseReady() { + if (this.aborted) { + return; + } + if (this.state === SocketFetch.DONE) { return; } + this._connection.stats._lastReceived = performance.now(); + this._connection.stats.receive = this._connection.stats._lastReceived - + this._connection.stats._firstReceived; + this.state = SocketFetch.DONE; - // See https://github.com/jarrodek/socket-fetch/issues/13 - let redirect = false; - let redirectOptions = {}; + var status = this._connection.status; + if (status >= 300 && status < 400) { //redirect - switch (status) { - case 300: - case 304: - case 305: - // do nothing; - break; - case 301: - case 302: - case 307: - if (['GET','HEAD'].indexOf(this._request.method) !== -1) { + if (this.redirect === 'error') { + this._mainPromise.reject({ + 'message': 'Redirects are not allowed', + 'redirect': true + }); + this._cleanUp(); + return; + } + + // See https://github.com/jarrodek/socket-fetch/issues/13 + let redirect = false; + let redirectOptions = {}; + + switch (status) { + case 300: + case 304: + case 305: + // do nothing; + break; + case 301: + case 302: + case 307: + if (['GET', 'HEAD'].indexOf(this._request.method) !== -1) { + redirect = true; + } + break; + case 303: redirect = true; + redirectOptions.forceGet = true; + break; + } + + if (redirect) { + // Redirect only when you know where to redirect the request. + if (this._connection.headers && this._connection.headers.has && + this._connection.headers.has('Location')) { + redirectOptions.location = this._connection.headers.get('Location'); + this._redirectRequest(redirectOptions); + return; } - break; - case 303: - redirect = true; - redirectOptions.forceGet = true; - break; - } + } - if (redirect) { - // Redirect only when you know where to redirect the request. + } else if (status === 401 && this.auth) { + switch (this.auth.method) { + case 'ntlm': + this.handleNtlmResponse(); + return; + } + } else if (status === 401) { if (this._connection.headers && this._connection.headers.has && - this._connection.headers.has('Location')) { - redirectOptions.location = this._connection.headers.get('Location'); - this._redirectRequest(redirectOptions); - return; + this._connection.headers.has('www-authenticate')) { + let authHeader = this._connection.headers.get('www-authenticate'); + if (authHeader.toLowerCase().indexOf('digest') !== -1) { + this.handleDigestResponse(authHeader); + } } } - } else if (status === 401 && this.auth) { - switch (this.auth.method) { - case 'ntlm': - this.handleNtlmResponse(); - return; - } - } else if (status === 401) { - if (this._connection.headers && this._connection.headers.has && - this._connection.headers.has('www-authenticate')) { - let authHeader = this._connection.headers.get('www-authenticate'); - if (authHeader.toLowerCase().indexOf('digest') !== -1) { - this.handleDigestResponse(authHeader); - } - } + this._cancelTimer(); + this._dispatchCustomEvent('loadend'); + this._publishResponse({ + includeRedirects: true + }); } - this._cancelTimer(); - this._dispatchCustomEvent('loadend'); - this._publishResponse({includeRedirects: true}); - } - - handleNtlmResponse() { - if (this.auth.state === 0) { - if (this._connection.headers && this._connection.headers.has && - this._connection.headers.has('www-authenticate')) { - try { - this.auth.challenge = - this.auth.getChallenge(this._connection.headers.get('www-authenticate')); - this.auth.state = 1; - this._cancelTimer(); - this._cleanUpRedirect({ - keepConnection: true - }) - .then(() => { - this._setupUrlData(); - this._createConnection(); - }) - .catch((e) => { + handleNtlmResponse() { + if (this.auth.state === 0) { + if (this._connection.headers && this._connection.headers.has && + this._connection.headers.has('www-authenticate')) { + try { + this.auth.challenge = + this.auth.getChallenge(this._connection.headers.get('www-authenticate')); + this.auth.state = 1; + this._cancelTimer(); + this._cleanUpRedirect({ + keepConnection: true + }) + .then(() => { + this._setupUrlData(); + this._createConnection(); + }) + .catch((e) => { + this._errorRequest({ + 'message': e && e.message || 'Unknown error occurred' + }); + }); + } catch (e) { + this.auth = undefined; this._errorRequest({ - 'message': e && e.message || 'Unknown error occurred' + 'message': e && e.message || 'Unknown error occurred in NTLM auth.' }); - }); - } catch (e) { - this.auth = undefined; - this._errorRequest({ - 'message': e && e.message || 'Unknown error occurred in NTLM auth.' + } + } else { + this.auth = {}; + this.auth.method = 'ntlm'; + this._cancelTimer(); + this._dispatchCustomEvent('loadend'); + this._publishResponse({ + includeRedirects: true }); } } else { @@ -1456,698 +1455,692 @@ class SocketFetch extends ArcEventSource { this.auth.method = 'ntlm'; this._cancelTimer(); this._dispatchCustomEvent('loadend'); - this._publishResponse({includeRedirects: true}); + this._publishResponse({ + includeRedirects: true + }); } - } else { - this.auth = {}; - this.auth.method = 'ntlm'; - this._cancelTimer(); - this._dispatchCustomEvent('loadend'); - this._publishResponse({includeRedirects: true}); } - } - handleDigestResponse(digestHeaders) { - digestHeaders = digestHeaders.slice(digestHeaders.indexOf(':') + 1, -1); - digestHeaders = digestHeaders.split(','); - this.auth = new FetchDigestAuth({}); - this.auth.httpMethod = this.request.method; - this.auth.scheme = digestHeaders[0].split(/\s/)[1]; - for (var i = 0; i < digestHeaders.length; i++) { - let equalIndex = digestHeaders[i].indexOf('='); - let key = digestHeaders[i].substring(0, equalIndex); - let val = digestHeaders[i].substring(equalIndex + 1); - val = val.replace(/['"]+/g, ''); - // find realm - if (key.match(/realm/i) !== null) { - this.auth.realm = val; - } - // find nonce - if (key.match(/nonce/i) !== null) { - this.auth.nonce = val; - } - // find opaque - if (key.match(/opaque/i) !== null) { - this.auth.opaque = val; - } - // find QOP - if (key.match(/qop/i) !== null) { - this.auth.qop = val; + handleDigestResponse(digestHeaders) { + digestHeaders = digestHeaders.slice(digestHeaders.indexOf(':') + 1, -1); + digestHeaders = digestHeaders.split(','); + this.auth = new FetchDigestAuth({}); + this.auth.httpMethod = this.request.method; + this.auth.scheme = digestHeaders[0].split(/\s/)[1]; + for (var i = 0; i < digestHeaders.length; i++) { + let equalIndex = digestHeaders[i].indexOf('='); + let key = digestHeaders[i].substring(0, equalIndex); + let val = digestHeaders[i].substring(equalIndex + 1); + val = val.replace(/['"]+/g, ''); + // find realm + if (key.match(/realm/i) !== null) { + this.auth.realm = val; + } + // find nonce + if (key.match(/nonce/i) !== null) { + this.auth.nonce = val; + } + // find opaque + if (key.match(/opaque/i) !== null) { + this.auth.opaque = val; + } + // find QOP + if (key.match(/qop/i) !== null) { + this.auth.qop = val; + } } + // client generated keys + this.auth.generateCnonce(); + if (!this.auth.nc) { + this.auth.nc = 1; + } + this.auth.nc++; } - // client generated keys - this.auth.generateCnonce(); - if (!this.auth.nc) { - this.auth.nc = 1; - } - this.auth.nc++; - } - /** - * Generate response object and publish it to the listeners. - * - * @param {Object} opts See #_createResponse for more info. - */ - _publishResponse(opts) { - this._request.messageSent = this._connection.messageSent; - delete this._request._uri; - this._createResponse(opts) - .then(() => { - this._dispatchCustomEvent('load', { - response: this._response - }); - this._mainPromise.resolve(this._response); - this._cleanUp(); - }) - .catch((e) => { - this._errorRequest({ - 'message': e && e.message || 'Unknown error occurred' + /** + * Generate response object and publish it to the listeners. + * + * @param {Object} opts See #_createResponse for more info. + */ + _publishResponse(opts) { + this._request.messageSent = this._connection.messageSent; + delete this._request._uri; + this._createResponse(opts) + .then(() => { + this._dispatchCustomEvent('load', { + response: this._response, + request: this._request + }); + this._mainPromise.resolve(this._response); + this._cleanUp(); + }) + .catch((e) => { + this._errorRequest({ + 'message': e && e.message || 'Unknown error occurred' + }); }); - }); - } - // Finishes the response with error message. - _errorRequest(opts) { - this.aborted = true; - this.state = SocketFetch.DONE; - var message; - if (opts.code && !opts.message) { - message = this.getCodeMessage(opts.code); - } else if (opts.message) { - message = opts.message; } - message = message || 'Unknown error occurred'; - let error = new Error(message); - if (this._mainPromise.reject) { - this._mainPromise.reject(error); + // Finishes the response with error message. + _errorRequest(opts) { + this.aborted = true; + this.state = SocketFetch.DONE; + var message; + if (opts.code && !opts.message) { + message = this.getCodeMessage(opts.code); + } else if (opts.message) { + message = opts.message; + } + message = message || 'Unknown error occurred'; + let error = new Error(message); + if (this._mainPromise.reject) { + this._mainPromise.reject(error); + } + this._dispatchCustomEvent('error', { + error: error + }); + this._cancelTimer(); + this._cleanUp(); } - this._dispatchCustomEvent('error', { - error: error - }); - this._cancelTimer(); - this._cleanUp(); - } - /** - * Creates a response and adds it to the redirects list and redirects the request to the - * new location. - * - * @param {Object} options A redirection options: - * forceGet {Boolean} - If true the redirected request will be GET request - * location {String} - location of the resource (redirect uri) - */ - _redirectRequest(options) { - var location = options.location; - // https://github.com/jarrodek/socket-fetch/issues/5 - try { - let u = new URL(location); - let protocol = u.protocol; - if (protocol === '') { - let path = u.pathname; - if (path && path[0] !== '/') { - path = '/' + path; + /** + * Creates a response and adds it to the redirects list and redirects the request to the + * new location. + * + * @param {Object} options A redirection options: + * forceGet {Boolean} - If true the redirected request will be GET request + * location {String} - location of the resource (redirect uri) + */ + _redirectRequest(options) { + var location = options.location; + // https://github.com/jarrodek/socket-fetch/issues/5 + try { + let u = new URL(location); + let protocol = u.protocol; + if (protocol === '') { + let path = u.pathname; + if (path && path[0] !== '/') { + path = '/' + path; + } } + } catch (e) { + // It must be relative location + let origin = this._request.uri.origin; + if (origin[origin.length - 1] === '/') { + origin = origin.substr(0, origin.length - 1); + } + if (location[0] !== '/') { + location = origin + this._request.uri.pathname + location; + } else { + location = origin + location; + } + } + // check if this is infinite loop + if (this.redirects) { + let loop = false; + this.redirects.forEach((item) => { + if (item.requestUrl === location) { + loop = true; + } + }); + if (loop) { + this._errorRequest({ + code: 310 + }); + return; + } } - } catch (e) { - // It must be relative location - let origin = this._request.uri.origin; - if (origin[origin.length - 1] === '/') { - origin = origin.substr(0, origin.length - 1); + // this is a redirect; + var canceled = this._dispatchCustomEvent('beforeredirect', { + location: location + }, true); + if (canceled) { + this.abort(); + return; } - if (location[0] !== '/') { - location = origin + this._request.uri.pathname + location; - } else { - location = origin + location; + if (!this.redirects) { + this.redirects = new Set(); } - - } - - // check if this is infinite loop - if (this.redirects) { - let loop = false; - this.redirects.forEach((item) => { - if (item.requestUrl === location) { - loop = true; + var responseCookies = null; + if (this._connection.headers && this._connection.headers.has && + this._connection.headers.has('set-cookie')) { + responseCookies = this._connection.headers.get('set-cookie'); + } + this._createResponse({ + includeRedirects: false + }) + .then(() => { + this._cancelTimer(); + this._response.requestUrl = this._request.url; + this._response.messageSent = this._connection.messageSent; + this.redirects.add(this._response); + return this._cleanUpRedirect({ + keepConnection: false + }); + }) + .then(() => { + if (!responseCookies) { + return; } - }); - if (loop) { + var newParser = new Cookies(responseCookies, location); + newParser.filter(); + let expired = newParser.clearExpired(); + if (this._request.headers.has('Cookie')) { + var oldCookies = this._request.headers.get('Cookie'); + var oldParser = new Cookies(oldCookies, location); + oldParser.filter(); + oldParser.clearExpired(); + oldParser.merge(newParser); + newParser = oldParser; + // remove expired from the new response. + newParser.cookies = newParser.cookies.filter((c) => { + for (let i = 0, len = expired.length; i < len; i++) { + if (expired[i].name === c.name) { + return false; + } + } + return true; + }); + } + var str = newParser.toString(true); + if (str) { + this._request.headers.set('Cookie', str); + } else { + this._request.headers.delete('Cookie'); + } + }) + .then(() => { + this._request.url = location; + if (options.forceGet) { + this._request.method = 'GET'; + } + this._setupUrlData(); + // No idea why but without setTimeout the program loses it's scope after calling + // the function. + window.setTimeout(() => { + this._createConnection(); + }, 0); + }) + .catch((e) => { this._errorRequest({ - code: 310 + 'message': e && e.message || 'Unknown error occurred' }); - return; - } - } - // this is a redirect; - this._dispatchCustomEvent('beforeredirect', { - location: location - }); - if (!this.redirects) { - this.redirects = new Set(); - } - var responseCookies = null; - if (this._connection.headers && this._connection.headers.has && - this._connection.headers.has('set-cookie')) { - responseCookies = this._connection.headers.get('set-cookie'); + }); } - this._createResponse({includeRedirects: false}) - .then(() => { - this._cancelTimer(); - this._response.requestUrl = this._request.url; - this._response.messageSent = this._connection.messageSent; - this.redirects.add(this._response); - return this._cleanUpRedirect({ + + /** + * After the connection is closed an result returned this method will release resources. + */ + _cleanUp() { + this._cleanUpRedirect({ keepConnection: false }); - }) - .then(() => { - if (!responseCookies) { - return; - } - var newParser = new Cookies(responseCookies, location); - newParser.filter(); - let expired = newParser.clearExpired(); - if (this._request.headers.has('Cookie')) { - var oldCookies = this._request.headers.get('Cookie'); - var oldParser = new Cookies(oldCookies, location); - oldParser.filter(); - oldParser.clearExpired(); - oldParser.merge(newParser); - newParser = oldParser; - // remove expired from the new response. - newParser.cookies = newParser.cookies.filter((c) => { - for (let i = 0, len = expired.length; i < len; i++) { - if (expired[i].name === c.name) { - return false; - } - } - return true; - }); - } - var str = newParser.toString(true); - if (str) { - this._request.headers.set('Cookie', str); + this._mainPromise.reject = undefined; + this._mainPromise.resolve = undefined; + chrome.sockets.tcp.onReceive.removeListener(this._connection._readFn); + chrome.sockets.tcp.onReceiveError.removeListener(this._connection._errorFn); + this._connection._readFn = undefined; + this._connection._errorFn = undefined; + this.redirects = undefined; + this._logs = []; + this._cancelTimer(); + } + /** Clean up for redirect */ + _cleanUpRedirect(opts) { + var promise; + if (opts.keepConnection) { + promise = Promise.resolve(); } else { - this._request.headers.delete('Cookie'); - } - }) - .then(() => { - this._request.url = location; - if (options.forceGet) { - this._request.method = 'GET'; - //this._request.body = undefined; + promise = this.disconnect(); } - this._setupUrlData(); - // No idea why but without setTimeout the program loses it's scope after calling - // the function. - window.setTimeout(() => { - this._createConnection(); - }, 0); - }) - .catch((e) => { - this._errorRequest({ - 'message': e && e.message || 'Unknown error occurred' + return promise.then(() => { + this._connection.body = undefined; + this._connection.headers = undefined; + this._connection.chunkSize = undefined; + this._connection.chunked = undefined; + this._connection.contentLength = undefined; + this._connection.statusMessage = undefined; + this._connection.status = undefined; + this.state = SocketFetch.STATUS; + this._connection.host = undefined; + this._connection.port = undefined; + this._response = undefined; + this._connection.messageSent = undefined; + this._connection.stats.startTime = undefined; + this._connection.stats.connect = undefined; + this._connection.stats.send = undefined; + this._connection.stats.wait = undefined; + this._connection.stats.receive = undefined; + this._connection.stats.ssl = undefined; + this._connection.stats._firstReceived = undefined; + this._connection.stats._messageSending = undefined; + this._connection.stats._waitingStart = undefined; + this._connection.stats._lastReceived = undefined; }); - }); - } - - /** - * After the connection is closed an result returned this method will release resources. - */ - _cleanUp() { - this._cleanUpRedirect({ - keepConnection: false - }); - this._mainPromise.reject = undefined; - this._mainPromise.resolve = undefined; - chrome.sockets.tcp.onReceive.removeListener(this._connection._readFn); - chrome.sockets.tcp.onReceiveError.removeListener(this._connection._errorFn); - this._connection._readFn = undefined; - this._connection._errorFn = undefined; - this.redirects = undefined; - this._logs = []; - this._cancelTimer(); - } - /** Clean up for redirect */ - _cleanUpRedirect(opts) { - var promise; - if (opts.keepConnection) { - promise = Promise.resolve(); - } else { - promise = this.disconnect(); - } - return promise.then(() => { - this._connection.body = undefined; - this._connection.headers = undefined; - this._connection.chunkSize = undefined; - this._connection.chunked = undefined; - this._connection.contentLength = undefined; - this._connection.statusMessage = undefined; - this._connection.status = undefined; - this.state = SocketFetch.STATUS; - this._connection.host = undefined; - this._connection.port = undefined; - this._response = undefined; - this._connection.messageSent = undefined; - this._connection.stats.startTime = undefined; - this._connection.stats.connect = undefined; - this._connection.stats.send = undefined; - this._connection.stats.wait = undefined; - this._connection.stats.receive = undefined; - this._connection.stats.ssl = undefined; - this._connection.stats._firstReceived = undefined; - this._connection.stats._messageSending = undefined; - this._connection.stats._waitingStart = undefined; - this._connection.stats._lastReceived = undefined; - }); - } - /** - * If response's Transfer-Encoding is 'chunked' read until next CR. Everything before it is a - * chunk size. - * This method will set {@link #chunkSize} to read value. - * - * @param {Uint8Array} array - * @returns {Uint8Array} Truncated response without chunk size line - */ - readChunkSize(array) { - if (this.aborted) { - return; - } - // this.log('Attemping to read chunk size from the array. ', array.length, array); - var index = this.indexOfSubarray(array, [13, 10]); - if (index === -1) { - //not found in this portion of data. - return array; } - if (index === 0) { - // Chrome's buffer cut CRLF after the end of chunk data, without last CLCR, here's to fix it. - // debugger; - // It can be either new line from the last chunk or end of the message where - // the rest of the array is [13, 10, 48, 13, 10, 13, 10] - if (this.indexOfSubarray(array, [13, 10, 13, 10]) === 0) { - this._connection.chunkSize = 0; - return new Uint8Array(); - } else { - array = array.subarray(index + 2); - index = this.indexOfSubarray(array, [13, 10]); + /** + * If response's Transfer-Encoding is 'chunked' read until next CR. Everything before it is a + * chunk size. + * This method will set {@link #chunkSize} to read value. + * + * @param {Uint8Array} array + * @returns {Uint8Array} Truncated response without chunk size line + */ + readChunkSize(array) { + if (this.aborted) { + return; } - } - // this.log('Size index: ', index); - var sizeArray = array.subarray(0, index); - var sizeHex = this.arrayBufferToString(sizeArray); - if (!sizeHex || sizeHex === '') { - this._connection.chunkSize = null; + // this.log('Attemping to read chunk size from the array. ', array.length, array); + var index = this.indexOfSubarray(array, [13, 10]); + if (index === -1) { + //not found in this portion of data. + return array; + } + if (index === 0) { + // Chrome's buffer cut CRLF after the end of chunk data, without last CLCR, here's to fix it. + // debugger; + // It can be either new line from the last chunk or end of the message where + // the rest of the array is [13, 10, 48, 13, 10, 13, 10] + if (this.indexOfSubarray(array, [13, 10, 13, 10]) === 0) { + this._connection.chunkSize = 0; + return new Uint8Array(); + } else { + array = array.subarray(index + 2); + index = this.indexOfSubarray(array, [13, 10]); + } + } + // this.log('Size index: ', index); + var sizeArray = array.subarray(0, index); + var sizeHex = this.arrayBufferToString(sizeArray); + if (!sizeHex || sizeHex === '') { + this._connection.chunkSize = null; + return array.subarray(index + 2); + } + this._connection.chunkSize = parseInt(sizeHex, 16); return array.subarray(index + 2); } - this._connection.chunkSize = parseInt(sizeHex, 16); - return array.subarray(index + 2); - } - /** - * This function assumes that all the headers has been read and it's just before changing - * the ststaus to BODY. - */ - _parseHeaders() { - if (this.aborted) { - return; - } - var list = this.headersToObject(this._connection.headers); - this.log('Received headers list', this._connection.headers, list); - this._connection.headers = new Headers(list); - if (this._connection.headers.has('Content-Length')) { - this._connection.contentLength = this._connection.headers.get('Content-Length'); - } - if (this._connection.headers.has('Transfer-Encoding')) { - let tr = this._connection.headers.get('Transfer-Encoding'); - if (tr === 'chunked') { - this._connection.chunked = true; + /** + * This function assumes that all the headers has been read and it's just before changing + * the ststaus to BODY. + */ + _parseHeaders() { + if (this.aborted) { + return; + } + var list = this.headersToObject(this._connection.headers); + this.log('Received headers list', this._connection.headers, list); + this._connection.headers = new Headers(list); + if (this._connection.headers.has('Content-Length')) { + this._connection.contentLength = this._connection.headers.get('Content-Length'); + } + if (this._connection.headers.has('Transfer-Encoding')) { + let tr = this._connection.headers.get('Transfer-Encoding'); + if (tr === 'chunked') { + this._connection.chunked = true; + } + } + var canceled = this._dispatchCustomEvent('headersreceived', { + value: this._connection.headers + }, true); + if (canceled) { + this.abort(); + return; } } - } - /** - * After the message is received check if the response has been compressed. - * If so, decompress the data. - * - * @param {Uint8Array} data Received data - * @return {Uint8Array} Decompressed data. - */ - decompressData(data) { - if (this.aborted) { + /** + * After the message is received check if the response has been compressed. + * If so, decompress the data. + * + * @param {Uint8Array} data Received data + * @return {Uint8Array} Decompressed data. + */ + decompressData(data) { + if (this.aborted) { + return Promise.resolve(data); + } + if (!this._connection.headers || !this._connection.headers.has || + !this._connection.headers.has('Content-Encoding')) { + return Promise.resolve(data); + } + var ce = this._connection.headers.get('Content-Encoding'); + if (ce.indexOf('gzip') !== -1 || ce.indexOf('deflate') !== -1) { + return new Promise((resolve, reject) => { + let workerUrl = 'decompress-worker.js'; + if (SocketFetchOptions.importUrl) { + workerUrl = SocketFetchOptions.importUrl.replace('%s', workerUrl); + } else if (location.pathname === '/components/tasks/demo/index.html') { + // demo, test + workerUrl = '../decompress-worker.js'; + } + + let worker = new Worker(workerUrl); + worker.onmessage = (e) => { + resolve(e.data); + }; + worker.onerror = (e) => { + var data = e.data; + if (!data) { + data = new Error('Data decompression worker not found.'); + } + reject(e.data); + }; + worker.postMessage({ + 'buffer': this._connection.body, + 'compression': ce + }); + }); + } return data; } - if (!this._connection.headers || !this._connection.headers.has || - !this._connection.headers.has('Content-Encoding')) { - return data; + /** + * Parse headers string and receive an object. + * + * @param {String} headersString A headers string as defined in the spec + * @return {Object} And object of key-value pairs where key is a + */ + headersToObject(headersString) { + if (this.aborted) { + return []; + } + if (headersString === null || headersString.trim() === '') { + return []; + } + if (typeof headersString !== 'string') { + throw new Error('Headers must be a String.'); + } + const result = {}; + const headers = headersString.split(/\n/gim); + + for (let i = 0, len = headers.length; i < len; i++) { + let line = headers[i].trim(); + if (line === '') { + continue; + } + let sepPosition = line.indexOf(':'); + if (sepPosition === -1) { + result[line] = ''; + continue; + } + let name = line.substr(0, sepPosition); + let value = line.substr(sepPosition + 1).trim(); + if (name in result) { + result[name] += '; ' + value; + } else { + result[name] = value; + } + } + return result; } - var ce = this._connection.headers.get('Content-Encoding'); - if (ce.indexOf('gzip') !== -1 || ce.indexOf('deflate') !== -1) { - return new Promise((resolve, reject) => { - let workerUrl = 'decompress-worker.js'; - if (SocketFetchOptions.importUrl) { - workerUrl = SocketFetchOptions.importUrl.replace('%s', workerUrl); - } else if (location.pathname === '/components/tasks/demo/index.html') { - // demo, test - workerUrl = '../decompress-worker.js'; + /** + * @return Returns an index of first occurance of subArray sequence in inputArray or -1 if not + * found. + */ + indexOfSubarray(inputArray, subArray) { + if (this.aborted) { + return -1; + } + var result = -1; + var len = inputArray.length; + var subLen = subArray.length; + for (let i = 0; i < len; ++i) { + if (result !== -1) { + return result; } - - let worker = new Worker(workerUrl); - worker.onmessage = (e) => { - resolve(e.data); - }; - worker.onerror = (e) => { - var data = e.data; - if (!data) { - data = new Error('Data decompression worker not found.'); + if (inputArray[i] !== subArray[0]) { + continue; + } + result = i; + for (let j = 1; j < subLen; j++) { + if (inputArray[i + j] === subArray[j]) { + result = i; + } else { + result = -1; + break; } - reject(e.data); - }; - worker.postMessage({ - 'buffer': this._connection.body, - 'compression': ce - }); - }); - } - return data; - } - /** - * Parse headers string and receive an object. - * - * @param {String} headersString A headers string as defined in the spec - * @return {Object} And object of key-value pairs where key is a - */ - headersToObject(headersString) { - if (this.aborted) { - return []; - } - if (headersString === null || headersString.trim() === '') { - return []; - } - if (typeof headersString !== 'string') { - throw new Error('Headers must be a String.'); + } + } + return result; } - const result = {}; - const headers = headersString.split(/\n/gim); - - for (let i = 0, len = headers.length; i < len; i++) { - let line = headers[i].trim(); - if (line === '') { - continue; - } - let sepPosition = line.indexOf(':'); - if (sepPosition === -1) { - result[line] = ''; - continue; - } - let name = line.substr(0, sepPosition); - let value = line.substr(sepPosition + 1).trim(); - if (name in result) { - result[name] += '; ' + value; - } else { - result[name] = value; + /** + * Convert ArrayBuffer to readable form + * @param {ArrayBuffer} buff + * @returns {String} Converted string + */ + arrayBufferToString(buff) { + if (this.aborted) { + return ''; + } + if (!!buff.buffer) { + // Not a ArrayBuffer, need and instance of AB + // It can't just get buff.buffer because it will use original buffer if the buff is a slice + // of it. + let b = buff.slice(0); + buff = b.buffer; } + var decoder = new TextDecoder('utf-8'); + var view = new DataView(buff); + return decoder.decode(view); } - return result; - } - /** - * @return Returns an index of first occurance of subArray sequence in inputArray or -1 if not - * found. - */ - indexOfSubarray(inputArray, subArray) { - if (this.aborted) { - return -1; + /** + * Convert a string to an ArrayBuffer. + * @param {string} string The string to convert. + * @return {ArrayBuffer} An array buffer whose bytes correspond to the string. + * @returns {ArrayBuffer} + */ + stringToArrayBuffer(string) { + if (this.aborted) { + return new ArrayBuffer(); + } + var encoder = new TextEncoder(); + var encoded = encoder.encode(string); + return encoded.buffer; } - var result = -1; - var len = inputArray.length; - var subLen = subArray.length; - for (let i = 0; i < len; ++i) { - if (result !== -1) { - return result; - } - if (inputArray[i] !== subArray[0]) { - continue; - } - result = i; - for (let j = 1; j < subLen; j++) { - if (inputArray[i + j] === subArray[j]) { - result = i; + /** + * Set up URL data relevant during making a connection. + */ + _setupUrlData() { + var port = this._request.uri.port; + var protocol = this._request.uri.protocol; + if (protocol) { + protocol = protocol.replace(':', ''); + } + if (!port) { + if (protocol in this.protocol2port) { + port = this.protocol2port[protocol]; } else { - result = -1; - break; + port = 80; } + this._request.uri.port = port; } - } - return result; - } - /** - * Convert ArrayBuffer to readable form - * @param {ArrayBuffer} buff - * @returns {String} Converted string - */ - arrayBufferToString(buff) { - if (this.aborted) { - return ''; - } - if (!!buff.buffer) { - // Not a ArrayBuffer, need and instance of AB - // It can't just get buff.buffer because it will use original buffer if the buff is a slice - // of it. - let b = buff.slice(0); - buff = b.buffer; - } - var decoder = new TextDecoder('utf-8'); - var view = new DataView(buff); - return decoder.decode(view); - // var array = new Uint8Array(buff); - // var str = ''; - // for (var i = 0; i < array.length; ++i) { - // str += String.fromCharCode(array[i]); - // } - // return str; - } - /** - * Convert a string to an ArrayBuffer. - * @param {string} string The string to convert. - * @return {ArrayBuffer} An array buffer whose bytes correspond to the string. - * @returns {ArrayBuffer} - */ - stringToArrayBuffer(string) { - if (this.aborted) { - return new ArrayBuffer(); - } - var encoder = new TextEncoder(); - var encoded = encoder.encode(string); - return encoded.buffer; - // var buffer = new ArrayBuffer(string.length); - // var bufferView = new Uint8Array(buffer); - // for (var i = 0; i < string.length; i++) { - // bufferView[i] = string.charCodeAt(i); - // } - // return buffer; - } - /** - * Set up URL data relevant during making a connection. - */ - _setupUrlData() { - var port = this._request.uri.port; - var protocol = this._request.uri.protocol; - if (protocol) { - protocol = protocol.replace(':', ''); - } - if (!port) { - if (protocol in this.protocol2port) { - port = this.protocol2port[protocol]; + this._connection.port = Number(port); + this._connection.host = this._request.uri.hostname; + if (protocol === 'https' || this._connection.port === 443) { + this._connection.useSSL = true; } else { - port = 80; + this._connection.useSSL = false; } - this._request.uri.port = port; - } - this._connection.port = Number(port); - this._connection.host = this._request.uri.hostname; - if (protocol === 'https' || this._connection.port === 443) { - this._connection.useSSL = true; - } else { - this._connection.useSSL = false; - } - // Check if URL contains username and password for basic auth. - var uid = this._request.uri.username; - var passwd = this._request.uri.password; - if (uid && passwd) { - let auth = { - 'uid': uid, - 'passwd': passwd, - 'method': 'basic' - }; - this.auth = new FetchBasicAuth(auth); + // Check if URL contains username and password for basic auth. + var uid = this._request.uri.username; + var passwd = this._request.uri.password; + if (uid && passwd) { + let auth = { + 'uid': uid, + 'passwd': passwd, + 'method': 'basic' + }; + this.auth = new FetchBasicAuth(auth); + } } - } - getCodeMessage(code) { - 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 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 (code in errorCodes) { - return errorCodes[code]; - } else { - return 'Unknown error'; + getCodeMessage(code) { + 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 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 (code in errorCodes) { + return errorCodes[code]; + } else { + return 'Unknown error'; + } } } -} -window.SocketFetch = SocketFetch; -window.SocketFetchOptions = SocketFetchOptions; + window.SocketFetch = SocketFetch; + window.SocketFetchOptions = SocketFetchOptions; })(); diff --git a/app.response.js b/app.response.js index f27ab2d..a1031aa 100644 --- a/app.response.js +++ b/app.response.js @@ -37,7 +37,6 @@ class ArcResponse { // Expecting a init.error to be an Error object. this.error = init.error; } else { - // not sure why Response object do not accept 1xx status codes... if (init.status >= 100 && init.status < 200 || init.status === 0) { init.status = 200; } else if (init.status === undefined) { @@ -66,7 +65,7 @@ class ArcResponse { // To be set only if this response is a redirect. It contains the URL of the request for // this response. this.requestUrl = undefined; - // To be set only if this response is a redirect. Originally message sent to server. + // To be set only if this response is a redirect. Original message sent to server. this.messageSent = undefined; // Object filled in with auth method data when reporting 401 or 407. // It will contain a `method` property with (lowercase) auth method name diff --git a/bower.json b/bower.json index 090cc57..e4f8ae4 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "socket-fetch", - "version": "1.0.76", + "version": "2.0.0-rc1", "license": "LICENSE.txt", "description": "A HTTP transport based on chrome.socket.tcp API.", "authors": [ diff --git a/chrome-app/main.js b/chrome-app/main.js deleted file mode 100644 index 635782b..0000000 --- a/chrome-app/main.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Listens for the app launching then creates the window - */ -chrome.app.runtime.onLaunched.addListener(function() { - chrome.app.window.create('components/_element_/_type_/index.html', { - bounds: { - width: 1024, - height: 800 - } - }); -}); diff --git a/chrome-app/manifest.json b/chrome-app/manifest.json deleted file mode 100644 index 02c66a5..0000000 --- a/chrome-app/manifest.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "manifest_version": 2, - "name": "_element_ _type_", - "version": "0.0.1", - "minimum_chrome_version": "34", - "app": { - "background": { - "scripts": ["main.js"] - } - }, - "sockets": { - "tcp": { - "connect": "*" - } - } -} diff --git a/demo/index.html b/demo/index.html deleted file mode 100644 index e910c7d..0000000 --- a/demo/index.html +++ /dev/null @@ -1,254 +0,0 @@ - - - - - - chrome-storage - - - - - - - - - - - - - -
- -
- - - - diff --git a/dependencyci.yml b/dependencyci.yml deleted file mode 100644 index ef1131e..0000000 --- a/dependencyci.yml +++ /dev/null @@ -1,4 +0,0 @@ -type: - development: - tests: - unlicensed: skip diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index 63dfd98..0000000 --- a/gulpfile.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -require('./tasks/lint-task.js'); -require('./tasks/chrome-app.js'); -require('./tasks/release.js'); diff --git a/package.json b/package.json index 0903c4c..35ba771 100644 --- a/package.json +++ b/package.json @@ -1,47 +1,11 @@ { "name": "socket-fetch", - "version": "1.0.76", + "version": "2.0.0-rc1", "license": "LICENSE", "description": "A HTTP transport based on chrome.socket.tcp API.", "repository": { "type": "git", "url": "git://github.com/advanced-rest-client/socket-fetch.git" }, - "scripts": { - "lint": "gulp lint", - "live:test": "gulp live:test", - "live:demo": "gulp live:demo", - "build:test": "gulp build:test", - "build:demo": "build:demo" - }, - "devDependencies": { - "conventional-github-releaser": "^1.1.1", - "crisper": "git+https://git@github.com/scarygami/crisper.git", - "del": "^1.1.1", - "gulp": "^3.8.5", - "gulp-batch": "^1.0.5", - "gulp-bump": "^2.1.0", - "gulp-conventional-changelog": "^1.1.0", - "gulp-crisper": "^0.0.5", - "gulp-flatten": "0.0.4", - "gulp-git": "^1.7.1", - "gulp-html-extract": "^0.0.3", - "gulp-if": "^2.0.0", - "gulp-insert-lines": "0.0.4", - "gulp-jscs": "^3.0.0", - "gulp-jscs-stylish": "^1.1.2", - "gulp-jshint": "^1.6.3", - "gulp-load-plugins": "^1.1.0", - "gulp-replace": "^0.5.3", - "gulp-size": "^1.0.0", - "gulp-util": "^3.0.7", - "jshint-stylish": "^2.0.0", - "merge-stream": "^0.1.8", - "run-sequence": "^1.1.5", - "tiny-lr": "^0.1.6" - }, - "engines": { - "node": ">=0.10.0" - }, - "private": false + "private": true } diff --git a/tasks/chrome-app.js b/tasks/chrome-app.js deleted file mode 100644 index 74fe9af..0000000 --- a/tasks/chrome-app.js +++ /dev/null @@ -1,109 +0,0 @@ -'use strict'; - -/* global __dirname */ - -var gulp = require('gulp'); -var $ = require('gulp-load-plugins')(); -var del = require('del'); -var runSequence = require('run-sequence'); -var path = require('path'); -var tinylr = require('tiny-lr'); -var copyTask = require('./copy-task.js'); - -var element = __dirname.split(path.sep).pop(); - -// copy and prepare dependencies -var bowerTask = function(destDir) { - return gulp.src([ - 'bower_components/**/*' - ]).pipe($.if('*.html', $.crisper())) - .pipe(gulp.dest(path.join(destDir, 'components'))); -}; - -// Create Chrome App to test/demo the custom element -var createAppTask = function(type, destDir) { - return gulp.src([ - './chrome-app/**' - ]).pipe($.replace('_element_', element)) - .pipe($.replace('_type_', type)) - .pipe(gulp.dest(destDir)); -}; - -// Setup tiny-lr and watch for changes to rebuild the apps -var watchTask = function(type, destDir) { - var lr = tinylr(); - lr.listen(35729); - - gulp.watch(['./*', './' + type + '/**'], ['copy-live:' + type]); - gulp.watch(['./chrome-app/**'], ['app:' + type]); - gulp.watch(['bower_components/**'], ['bower:' + type]); - - gulp.watch([destDir + '/**'], $.batch({ - timeout: 500 - }, function(events, cb) { - var paths = []; - events.on('data', function(evt) { - paths.push(evt.path); - }).on('end', function() { - lr.changed({ - body: { - files: paths - } - }); - cb(); - }); - })); -}; - -gulp.task('copy:test', copyTask.bind(null, 'test', 'test-app', false, element)); -gulp.task('copy:demo', copyTask.bind(null, 'demo', 'demo-app', false, element)); -gulp.task('copy-live:test', copyTask.bind(null, 'test', 'test-app', true, element)); -gulp.task('copy-live:demo', copyTask.bind(null, 'demo', 'demo-app', true, element)); - -gulp.task('bower:test', bowerTask.bind(null, 'test-app')); -gulp.task('bower:demo', bowerTask.bind(null, 'demo-app')); - -gulp.task('app:test', createAppTask.bind(null, 'test', 'test-app')); -gulp.task('app:demo', createAppTask.bind(null, 'demo', 'demo-app')); - -gulp.task('watch:test', watchTask.bind(null, 'test', 'test-app')); -gulp.task('watch:demo', watchTask.bind(null, 'demo', 'demo-app')); - -// Clean Output Directory -gulp.task('clean:test', del.bind(null, ['test-app'])); -gulp.task('clean:demo', del.bind(null, ['demo-app'])); - -// Main Gulp tasks -gulp.task('build:test', ['clean:test'], function(cb) { - runSequence( - ['copy:test', 'bower:test'], - 'app:test', - cb - ); -}); - -gulp.task('build:demo', ['clean:demo'], function(cb) { - runSequence( - ['copy:demo', 'bower:demo'], - 'app:demo', - cb - ); -}); - -gulp.task('live:test', ['clean:test'], function(cb) { - runSequence( - ['copy-live:test', 'bower:test'], - 'app:test', - 'watch:test', - cb - ); -}); - -gulp.task('live:demo', ['clean:demo'], function(cb) { - runSequence( - ['copy-live:demo', 'bower:demo'], - 'app:demo', - 'watch:demo', - cb - ); -}); diff --git a/tasks/ci.js b/tasks/ci.js deleted file mode 100644 index eeacd0e..0000000 --- a/tasks/ci.js +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -process.title = 'arc-ci'; - -const http = require('http'); -const options = { - port: 5243, - hostname: '104.199.30.184', - method: 'POST', - path: '/travis-build', - headers: { - 'x-travis-ci-event': 'build-stage', - 'content-type': 'application/json' - } -}; - -const payload = { - 'branch': process.env.TRAVIS_BRANCH || 'stage', - 'buildNumber': process.env.TRAVIS_BUILD_NUMBER || 'dev', - 'commit': process.env.TRAVIS_COMMIT || 'test', - 'jobNumber': process.env.TRAVIS_JOB_NUMBER || 'dev', - 'pullRequest': process.env.TRAVIS_PULL_REQUEST || false, - 'pullRequestSha': process.env.TRAVIS_PULL_REQUEST_SHA || 'none', - 'slug': process.env.TRAVIS_REPO_SLUG || 'unknown' -}; - -var req = http.request(options, (res) => { - res.on('end', () => { - process.exit(); - }); -}); -req.on('end', () => { - process.exit(); -}); -req.on('error', (e) => { - console.log(`problem with request: ${e.message}`); - process.exit(1); -}); -req.write(JSON.stringify(payload)); -req.end(); diff --git a/tasks/copy-task.js b/tasks/copy-task.js deleted file mode 100644 index 6451573..0000000 --- a/tasks/copy-task.js +++ /dev/null @@ -1,74 +0,0 @@ -'use strict'; - -var gulp = require('gulp'); -var $ = require('gulp-load-plugins')(); -var path = require('path'); - -// Copy and prepare custom element -function copyTask(type, destDir, live, element) { - // All files necessary for the element + test/demo files - // This will have to be extended, e.g. if your element uses images - let copy = gulp.src([ - './*.html', - './*.js', - './*.css', - './' + type + '/**', - '!./gulpfile.js', - '!./index.html', - ], { - base: './' - }); - - // Run all html files through crisper for CSP - copy = copy.pipe($.if('*.html', $.crisper())); - - if (live) { - // Insert live-reload script into main demo/test files - copy = copy.pipe( - $.if( - '**/index.html', - $.insertLines({ - before: /<\/head>/, - lineBefore: '' - }) - ) - ); - } - - if (type === 'test') { - // Scripts necessary for WCT that need to be loaded explicitely for Chrome Apps - let wctScripts = [ - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '' - ].join('\n'); - - // Insert WCT Scripts in test files before WCT is loaded - copy = copy.pipe( - $.if('*.html', $.insertLines({ - before: / { - return { - createDemo: () => { - - }, - createTests: () => { - - }, - createApp: (permissions) => { - fs.writeFileSync('./chrome-app/main.js', chromeAppTpl.main); - fs.writeFileSync('./chrome-app/manifest.json', chromeAppTpl.manifest(permissions)); - } - } -} diff --git a/tasks/lint-task.js b/tasks/lint-task.js deleted file mode 100644 index c35af08..0000000 --- a/tasks/lint-task.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -var gulp = require('gulp'); -var $ = require('gulp-load-plugins')(); - -// Lint JavaScript files -function lint() { - return gulp.src([ - './**/*.js', - './**/*.html', - '!./node_modules/**/*.*' - ]) - // JSCS has not yet a extract option - .pipe($.if('*.html', $.htmlExtract({ - strip: true - }))) - .pipe($.jshint()) - .pipe($.jscs()) - .pipe($.jscsStylish.combineWithHintResults()) - .pipe($.jshint.reporter('jshint-stylish')) - .pipe($.jshint.reporter('fail')); -} -gulp.task('lint', lint); - -//module.exports = lint; diff --git a/tasks/release.js b/tasks/release.js deleted file mode 100644 index 77b2a31..0000000 --- a/tasks/release.js +++ /dev/null @@ -1,87 +0,0 @@ -var gulp = require('gulp'); -var conventionalGithubReleaser = require('conventional-github-releaser'); -var conventionalChangelog = require('gulp-conventional-changelog'); -var bump = require('gulp-bump'); -var gutil = require('gulp-util'); -var git = require('gulp-git'); -var fs = require('fs'); -var runSequence = require('run-sequence'); - -gulp.task('changelog', function() { - return gulp.src('CHANGELOG.md', { - buffer: false - }) - .pipe(conventionalChangelog({ - preset: 'eslint' // Or to any other commit message convention you use. - })) - .pipe(gulp.dest('./')); -}); - -gulp.task('github-release', function(done) { - conventionalGithubReleaser({ - type: 'oauth', - // change this to your own GitHub token or use an environment variable - token: process.env.GITHUB_TOKEN - }, { - preset: 'angular' // Or to any other commit message convention you use. - }, done); -}); -gulp.task('bump-version', function() { - // We hardcode the version change type to 'patch' but it may be a good idea to - // use minimist (https://www.npmjs.com/package/minimist) to determine with a - // command argument whether you are doing a 'major', 'minor' or a 'patch' change. - return gulp.src(['./bower.json', './package.json']) - .pipe(bump({ - type: 'patch' - }).on('error', gutil.log)) - .pipe(gulp.dest('./')); -}); -gulp.task('commit-changes', function() { - return gulp.src('.') - .pipe(git.add()) - .pipe(git.commit('[Prerelease] Bumped version number')); -}); - -gulp.task('push-changes', function(cb) { - git.push('origin', 'master', cb); -}); - -function getPackageJsonVersion() { - // We parse the json file instead of using require because require caches - // multiple calls so the version number won't be updated - return JSON.parse(fs.readFileSync('./package.json', 'utf8')).version; -} - -gulp.task('create-new-tag', function(cb) { - var version = getPackageJsonVersion(); - git.tag(version, 'Created Tag for version: ' + version, {signed: true}, function(error) { - if (error) { - return cb(error); - } - git.push('origin', 'master', { - args: '--tags' - }, cb); - }); -}); - -gulp.task('release', function(callback) { - if (!process.env.GITHUB_TOKEN) { - callback(new Error('The GITHUB_TOKEN environment variable is not set.')); - return; - } - runSequence( - 'bump-version', - 'changelog', - 'commit-changes', - 'push-changes', - 'create-new-tag', - 'github-release', - function(error) { - if (error) { - console.log(error.message); - } else { - console.log('RELEASE FINISHED SUCCESSFULLY'); - } - callback(error); - }); -}); diff --git a/tasks/test-srv.js b/tasks/test-srv.js deleted file mode 100644 index 7427365..0000000 --- a/tasks/test-srv.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict'; - -const http = require('http'); -const url = require('url'); - -http.createServer(function(request, response) { - response.setHeader('Content-Type', 'text/html; charset=UTF-8'); - response.setHeader('Transfer-Encoding', 'chunked'); - - var query = url.parse(request.url, true).query; - var chunk = 1024; - var numbytes = 1024; - if ('chunk' in query) { - chunk = Number(query.chunk); - if (chunk !== chunk) { - chunk = 1024; - } - } - if ('size' in query) { - numbytes = Number(query.size); - if (numbytes !== numbytes) { - numbytes = 1024; - } - } - var sent = 0; - var letters = 'abcdefghijklmnopqrstuwxyz'.split(''); - var lettersLength = letters.length; - var lastLetter = -1; - function gen() { - - let str = ''; - let len = 0; - let end = false; - if (sent + chunk > numbytes) { - len = numbytes - sent; - end = true; - } else { - len = chunk; - } - - for (let i = 0; i < len; i++) { - lastLetter++; - if (lastLetter >= lettersLength) { - lastLetter = 0; - } - str += letters[lastLetter]; - sent++; - } - - if (end) { - response.end(str); - } else { - response.write(str); - setTimeout(gen, 20); - } - } - - gen(); -}).listen(process.env.VMC_APP_PORT || 1337, null); diff --git a/test/.jshintrc b/test/.jshintrc deleted file mode 100644 index 3f26cee..0000000 --- a/test/.jshintrc +++ /dev/null @@ -1,20 +0,0 @@ -{ - "node": true, - "browser": true, - "esnext": true, - "bitwise": true, - "curly": true, - "eqeqeq": true, - "immed": true, - "indent": 2, - "latedef": true, - "noarg": true, - "quotmark": "single", - "undef": true, - "unused": true, - "mocha": true, - "globals": { - "chrome": false, - "assert": false - } -} \ No newline at end of file diff --git a/test/buffers.js b/test/buffers.js new file mode 100644 index 0000000..62f1b9f --- /dev/null +++ b/test/buffers.js @@ -0,0 +1,6 @@ +window.TestBuffers = {}; +window.TestBuffers.stringToBuffer = function(string) { + var encoder = new TextEncoder(); + var encoded = encoder.encode(string); + return encoded.buffer; +}; \ No newline at end of file diff --git a/test/concat-buffers-test.html b/test/concat-buffers-test.html new file mode 100644 index 0000000..cc7937b --- /dev/null +++ b/test/concat-buffers-test.html @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + diff --git a/test/file-test.html b/test/file-test.html new file mode 100644 index 0000000..1c0ef4c --- /dev/null +++ b/test/file-test.html @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + diff --git a/test/index.html b/test/index.html index 634f276..074f412 100644 --- a/test/index.html +++ b/test/index.html @@ -9,71 +9,25 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt --> + + + - - - - - - - - - - -

Test passed for n=

-

- - - + + + + + + diff --git a/test/multipart-test.html b/test/multipart-test.html new file mode 100644 index 0000000..755ff41 --- /dev/null +++ b/test/multipart-test.html @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + diff --git a/test/prepare_wct.js b/test/prepare_wct.js deleted file mode 100644 index af1fab0..0000000 --- a/test/prepare_wct.js +++ /dev/null @@ -1,3 +0,0 @@ -WCT = { - environmentScripts: [] -}; diff --git a/test/process-headers.html b/test/process-headers.html new file mode 100644 index 0000000..bd551ad --- /dev/null +++ b/test/process-headers.html @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + diff --git a/test/process-status-test.html b/test/process-status-test.html new file mode 100644 index 0000000..f98f58a --- /dev/null +++ b/test/process-status-test.html @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + diff --git a/test/redirects-test.html b/test/redirects-test.html new file mode 100644 index 0000000..bd0b808 --- /dev/null +++ b/test/redirects-test.html @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + +