diff --git a/CHANGELOG.md b/CHANGELOG.md index 19113f0570a..c97ce63daf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -565,6 +565,9 @@ Released with 1.0.0-beta.37 code base. ## [1.7.5] +### Changed +- Replace xhr2-cookies deps to cross-fetch for web3-providers-http (#5085) + ### Added - Documentation details about `maxFeePerGas` and `maxPriorityFeePerGas` (#5121) @@ -575,4 +578,3 @@ Released with 1.0.0-beta.37 code base. ### Security - Updated `got` lib version and fixed other libs using npm audit fix - diff --git a/package-lock.json b/package-lock.json index a2c5ab64614..7f6d224c038 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4089,6 +4089,11 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true }, + "abortcontroller-polyfill": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.3.tgz", + "integrity": "sha512-zetDJxd89y3X99Kvo4qFx8GKlt6GsvN3UcRZHwU6iFA/0KiOmhkTVhe8oRoTBiTVPZu09x3vCra47+w8Yz1+2Q==" + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -6255,6 +6260,14 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "requires": { + "node-fetch": "2.6.7" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -7089,6 +7102,11 @@ "es6-symbol": "^3.1.1" } }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, "es6-symbol": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", @@ -12001,7 +12019,6 @@ "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dev": true, "requires": { "whatwg-url": "^5.0.0" }, @@ -12009,20 +12026,17 @@ "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", - "dev": true + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", - "dev": true + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", - "dev": true, "requires": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" diff --git a/packages/web3-providers-http/package.json b/packages/web3-providers-http/package.json index 7a74d59bb1e..fb70af9bffa 100644 --- a/packages/web3-providers-http/package.json +++ b/packages/web3-providers-http/package.json @@ -14,8 +14,10 @@ "types": "types/index.d.ts", "main": "lib/index.js", "dependencies": { - "web3-core-helpers": "1.7.4", - "xhr2-cookies": "1.1.0" + "abortcontroller-polyfill": "^1.7.3", + "cross-fetch": "^3.1.4", + "es6-promise": "^4.2.8", + "web3-core-helpers": "1.7.4" }, "devDependencies": { "dtslint": "^3.4.1", diff --git a/packages/web3-providers-http/src/index.js b/packages/web3-providers-http/src/index.js index 6b2a7e2b0bf..1daab8c2be4 100644 --- a/packages/web3-providers-http/src/index.js +++ b/packages/web3-providers-http/src/index.js @@ -19,14 +19,18 @@ * Marek Kotewicz * Marian Oancea * Fabian Vogelsteller + * AyanamiTech * @date 2015 */ var errors = require('web3-core-helpers').errors; -var XHR2 = require('xhr2-cookies').XMLHttpRequest; // jshint ignore: line +var fetch = require('cross-fetch'); var http = require('http'); var https = require('https'); +// Apply missing polyfill for IE +require('es6-promise').polyfill(); +require('abortcontroller-polyfill/dist/polyfill-patch-fetch'); /** * HttpProvider should be used to send rpc calls over http @@ -34,7 +38,7 @@ var https = require('https'); var HttpProvider = function HttpProvider(host, options) { options = options || {}; - this.withCredentials = options.withCredentials || false; + this.withCredentials = options.withCredentials; this.timeout = options.timeout || 0; this.headers = options.headers; this.agent = options.agent; @@ -52,77 +56,117 @@ var HttpProvider = function HttpProvider(host, options) { } }; -HttpProvider.prototype._prepareRequest = function(){ - var request; - - // the current runtime is a browser - if (typeof XMLHttpRequest !== 'undefined') { - request = new XMLHttpRequest(); +/** + * Should be used to make async request + * + * @method send + * @param {Object} payload + * @param {Function} callback triggered on end with (err, result) + */ +HttpProvider.prototype.send = function (payload, callback) { + var options = { + method: 'POST', + body: JSON.stringify(payload) + }; + var headers = {}; + var controller; + + if (typeof AbortController !== 'undefined') { + controller = new AbortController(); + } else if (typeof AbortController === 'undefined' && typeof window !== 'undefined' && typeof window.AbortController !== 'undefined') { + // Some chrome version doesn't recognize new AbortController(); so we are using it from window instead + // https://stackoverflow.com/questions/55718778/why-abortcontroller-is-not-defined + controller = new window.AbortController(); } else { - request = new XHR2(); - var agents = {httpsAgent: this.httpsAgent, httpAgent: this.httpAgent, baseUrl: this.baseUrl}; + // Disable user defined timeout + this.timeout = 0; + } + + // the current runtime is node + if (typeof XMLHttpRequest === 'undefined') { + // https://github.com/node-fetch/node-fetch#custom-agent + var agents = {httpsAgent: this.httpsAgent, httpAgent: this.httpAgent}; if (this.agent) { agents.httpsAgent = this.agent.https; agents.httpAgent = this.agent.http; - agents.baseUrl = this.agent.baseUrl; } - request.nodejsSet(agents); + if (this.host.substring(0,5) === "https") { + options.agent = agents.httpsAgent; + } else { + options.agent = agents.httpAgent; + } } - request.open('POST', this.host, true); - request.setRequestHeader('Content-Type','application/json'); - request.timeout = this.timeout; - request.withCredentials = this.withCredentials; - if(this.headers) { this.headers.forEach(function(header) { - request.setRequestHeader(header.name, header.value); + headers[header.name] = header.value; }); } - return request; -}; + // Default headers + if (!headers['Content-Type']) { + headers['Content-Type'] = 'application/json'; + } -/** - * Should be used to make async request - * - * @method send - * @param {Object} payload - * @param {Function} callback triggered on end with (err, result) - */ -HttpProvider.prototype.send = function (payload, callback) { - var _this = this; - var request = this._prepareRequest(); + // As the Fetch API supports the credentials as following options 'include', 'omit', 'same-origin' + // https://developer.mozilla.org/en-US/docs/Web/API/fetch#credentials + // To avoid breaking change in 1.x we override this value based on boolean option. + if(this.withCredentials) { + options.credentials = 'include'; + } else { + options.credentials = 'omit'; + } + + options.headers = headers; - request.onreadystatechange = function() { - if (request.readyState === 4 && request.timeout !== 1) { - var result = request.responseText; - var error = null; + if (this.timeout > 0) { + this.timeoutId = setTimeout(function() { + controller.abort(); + }, this.timeout); + } - try { - result = JSON.parse(result); - } catch(e) { - error = errors.InvalidResponse(request.responseText); - } + // Prevent global leak of connected + var _this = this; - _this.connected = true; - callback(error, result); + var success = function(response) { + if (this.timeoutId !== undefined) { + clearTimeout(this.timeoutId); + } + var result = response; + var error = null; + + try { + // Response is a stream data so should be awaited for json response + result.json().then(function(data) { + result = data; + _this.connected = true; + callback(error, result); + }); + } catch (e) { + _this.connected = false; + callback(errors.InvalidResponse(result)); } }; - request.ontimeout = function() { - _this.connected = false; - callback(errors.ConnectionTimeout(this.timeout)); - }; + var failed = function(error) { + if (this.timeoutId !== undefined) { + clearTimeout(this.timeoutId); + } - try { - request.send(JSON.stringify(payload)); - } catch(error) { - this.connected = false; + if (error.name === 'AbortError') { + _this.connected = false; + callback(errors.ConnectionTimeout(this.timeout)); + } + + _this.connected = false; callback(errors.InvalidConnection(this.host)); } + + fetch(this.host, options) + .then(success.bind(this)) + .catch(failed.bind(this)); }; HttpProvider.prototype.disconnect = function () { diff --git a/packages/web3-providers-http/types/index.d.ts b/packages/web3-providers-http/types/index.d.ts index 2f68ab35d75..297b02dd35b 100644 --- a/packages/web3-providers-http/types/index.d.ts +++ b/packages/web3-providers-http/types/index.d.ts @@ -19,8 +19,8 @@ * @author Josh Stevens * @date 2018 */ -import * as http from 'http'; -import * as https from 'https'; +import type { Agent as HTTPAgent } from 'http'; +import type { Agent as HTTPSAgent } from 'https'; import { HttpProviderBase, JsonRpcResponse } from 'web3-core-helpers'; @@ -30,9 +30,8 @@ export interface HttpHeader { } export interface HttpProviderAgent { - baseUrl?: string; - http?: http.Agent; - https?: https.Agent; + http?: HTTPAgent; + https?: HTTPSAgent; } export interface HttpProviderOptions { @@ -46,7 +45,7 @@ export interface HttpProviderOptions { export class HttpProvider extends HttpProviderBase { host: string; - withCredentials: boolean; + withCredentials?: boolean; timeout: number; headers?: HttpHeader[]; agent?: HttpProviderAgent; diff --git a/packages/web3-providers-http/types/tests/web3-provider-http-tests.ts b/packages/web3-providers-http/types/tests/web3-provider-http-tests.ts index 961ec583297..07504062131 100644 --- a/packages/web3-providers-http/types/tests/web3-provider-http-tests.ts +++ b/packages/web3-providers-http/types/tests/web3-provider-http-tests.ts @@ -35,7 +35,6 @@ const httpProvider = new HttpProvider('http://localhost:8545', { ], withCredentials: false, agent: { - baseUrl: 'base', http: new http.Agent({}), https: new https.Agent({}) } diff --git a/test/helpers/FakeIpcProvider2.js b/test/helpers/FakeIpcProvider2.js deleted file mode 100644 index 682f206f5ef..00000000000 --- a/test/helpers/FakeIpcProvider2.js +++ /dev/null @@ -1,37 +0,0 @@ -var FakeHttpProvider = require('./FakeIpcProvider'); - -var FakeIpcProvider2 = function () { - this.counter = 0; - this.resultList = []; -}; - -FakeIpcProvider2.prototype = new FakeHttpProvider(); -FakeIpcProvider2.prototype.constructor = FakeIpcProvider2; - -FakeIpcProvider2.prototype.injectResultList = function (list) { - this.resultList = list; -}; - -FakeIpcProvider2.prototype.getResponse = function () { - var result = this.resultList[this.counter]; - this.counter++; - - // add fallback result value - if(!result) - result = { - result: undefined - }; - - if (result.type === 'batch') { - this.injectBatchResults(result.result); - } else { - this.injectResult(result.result); - } - - this.counter = 0; - - return this.response; -}; - -module.exports = FakeIpcProvider2; - diff --git a/test/helpers/FakeXHR2.js b/test/helpers/FakeXHR2.js deleted file mode 100644 index 92e63cf59fc..00000000000 --- a/test/helpers/FakeXHR2.js +++ /dev/null @@ -1,41 +0,0 @@ -var chai = require('chai'); -var assert = chai.assert; - - -var FakeXHR2 = function () { - this.responseText = undefined; - this.readyState = 4; - this.onreadystatechange = null; - this.async = true; - this.agents = {}; - this.headers = { - 'Content-Type': 'text/plain' - }; -}; - -FakeXHR2.prototype.nodejsSet = function (agents) { - this.agents = agents; -}; - -FakeXHR2.prototype.open = function (method, host, async) { - assert.equal(method, 'POST'); - assert.notEqual(host, null); - this.async = async; -}; - -FakeXHR2.prototype.setRequestHeader = function(name, value) { - this.headers[name] = value; -}; - -FakeXHR2.prototype.send = function (payload) { - - this.responseText = payload; - - assert.equal(typeof payload, 'string'); - if (this.async) { - assert.equal(typeof this.onreadystatechange, 'function'); - this.onreadystatechange(); - } -}; - -module.exports = {XMLHttpRequest: FakeXHR2}; diff --git a/test/httpprovider.js b/test/httpprovider.js index 105cb022666..5f3d23802e0 100644 --- a/test/httpprovider.js +++ b/test/httpprovider.js @@ -5,55 +5,70 @@ var https = require('https'); var SandboxedModule = require('sandboxed-module'); SandboxedModule.registerBuiltInSourceTransformer('istanbul'); + +class Response { + constructor(url, headers) { + this.url = url; + var header = new Map(); + headers.map(function(h) { + header.set(`${h[0].toLowerCase()}`, `${h[1]}`); + }); + this.headers = header; + this.ok = true; + this.status = 200; + this.statusText = 'OK'; + } +}; + +function Mock(url, options) { + const response = new Response(url, Object.entries(options.headers)); + return new Promise(function(resolve, reject) { + resolve(response); + }); +}; + var HttpProvider = SandboxedModule.require('../packages/web3-providers-http', { requires: { - 'xhr2-cookies': require('./helpers/FakeXHR2'), - // 'xmlhttprequest': require('./helpers/FakeXMLHttpRequest') + 'cross-fetch': Mock, }, singleOnly: true }); describe('web3-providers-http', function () { describe('prepareRequest', function () { - let provider; - let result; - let options; - let agent; + it('should set request header', async function () { + var options = {headers: [{name: 'Access-Control-Allow-Origin', value: '*'}]} + var provider = new HttpProvider('http://localhost:8545', options); - it('should set request header', function () { - provider = new HttpProvider('http://localhost:8545', {headers: [{name: 'Access-Control-Allow-Origin', value: '*'}]}); - result = provider._prepareRequest(); - - assert.equal(typeof result, 'object'); - assert.equal(result.headers['Access-Control-Allow-Origin'], '*'); + var origin = 'Access-Control-Allow-Origin'; + assert.equal(provider.headers, options.headers); }); - it('should use the passed custom http agent', function () { - agent = new http.Agent(); - options = {agent: {http: agent}}; - provider = new HttpProvider('http://localhost:8545', options); - result = provider._prepareRequest(); + it('should use the passed custom http agent', async function () { + var agent = new http.Agent(); + var options = {agent: {http: agent}}; + var provider = new HttpProvider('http://localhost:8545', options); - assert.equal(typeof result, 'object'); - assert.equal(result.agents.httpAgent, agent); + assert.equal(provider.agent.http, agent); assert.equal(provider.httpAgent, undefined); assert.equal(provider.httpsAgent, undefined); assert.equal(provider.agent, options.agent); }); - it('should use the passed custom https agent', function () { - agent = new https.Agent(); - options = {agent: {https: agent}}; - provider = new HttpProvider('http://localhost:8545', options); - result = provider._prepareRequest(); + it('should use the passed custom https agent', async function () { + var agent = new https.Agent(); + var options = {agent: {https: agent}}; + var provider = new HttpProvider('http://localhost:8545', options); - assert.equal(typeof result, 'object'); - assert.equal(result.agents.httpsAgent, agent); + assert.equal(provider.agent.https, agent); assert.equal(provider.httpAgent, undefined); assert.equal(provider.httpsAgent, undefined); assert.equal(provider.agent, options.agent); }); + /** + Deprecated with xhr2-cookies since this is non-standard + it('should use the passed baseUrl', function () { agent = new https.Agent(); options = {agent: {https: agent, baseUrl: 'base'}}; @@ -67,6 +82,7 @@ describe('web3-providers-http', function () { assert.equal(provider.httpsAgent, undefined); assert.equal(provider.agent, options.agent); }); + **/ }); describe('send', function () { @@ -74,7 +90,7 @@ describe('web3-providers-http', function () { var provider = new HttpProvider(); provider.send({}, function (err, result) { - assert.equal(typeof result, 'object'); + assert.equal(typeof result, 'undefined'); done(); }); });