diff --git a/package-lock.json b/package-lock.json index 0e4bb70cd..9fc66548a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,12 +13,11 @@ "@types/node": "^20.6.0", "ajv": "^6.12.6", "async": "2.6.4", + "axios": "^1.7.4", "crypto-js": "^4.0.0", "memfs": "^4.11.1", "net": "^1.0.2", "promise-throttle": "^1.1.2", - "request": "^2.88.0", - "request-promise": "^4.2.6", "serialize-javascript": "^3.1.0", "text-encoding": "^0.7.0", "tls": "0.0.1", @@ -3031,13 +3030,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asn1": { - "version": "0.2.6", - "license": "MIT", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, "node_modules/asn1.js": { "version": "5.4.1", "dev": true, @@ -3066,13 +3058,6 @@ "util": "^0.12.5" } }, - "node_modules/assert-plus": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, "node_modules/ast-types-flow": { "version": "0.0.7", "dev": true, @@ -3087,7 +3072,8 @@ }, "node_modules/asynckit": { "version": "0.4.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/available-typed-arrays": { "version": "1.0.5", @@ -3100,17 +3086,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.12.0", - "license": "MIT" - }, "node_modules/axe-core": { "version": "4.8.2", "dev": true, @@ -3119,6 +3094,16 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "3.2.1", "dev": true, @@ -3308,13 +3293,6 @@ ], "license": "MIT" }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "license": "BSD-3-Clause", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "dev": true, @@ -3323,10 +3301,6 @@ "node": ">=8" } }, - "node_modules/bluebird": { - "version": "3.7.2", - "license": "MIT" - }, "node_modules/bn.js": { "version": "5.2.1", "dev": true, @@ -3552,10 +3526,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/caseless": { - "version": "0.12.0", - "license": "Apache-2.0" - }, "node_modules/chalk": { "version": "2.4.2", "dev": true, @@ -3655,7 +3625,8 @@ }, "node_modules/combined-stream": { "version": "1.0.8", - "license": "MIT", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -3713,10 +3684,6 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/core-util-is": { - "version": "1.0.2", - "license": "MIT" - }, "node_modules/create-ecdh": { "version": "4.0.4", "dev": true, @@ -3805,16 +3772,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/dashdash": { - "version": "1.14.1", - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/debug": { "version": "4.3.4", "dev": true, @@ -3878,7 +3835,8 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "engines": { "node": ">=0.4.0" } @@ -3945,14 +3903,6 @@ "node": ">=6.0.0" } }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "license": "MIT", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, "node_modules/electron-to-chromium": { "version": "1.4.537", "dev": true, @@ -4586,17 +4536,6 @@ "safe-buffer": "^5.1.1" } }, - "node_modules/extend": { - "version": "3.0.2", - "license": "MIT" - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "license": "MIT" @@ -4717,6 +4656,25 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "dev": true, @@ -4725,23 +4683,17 @@ "is-callable": "^1.1.3" } }, - "node_modules/forever-agent": { - "version": "0.6.1", - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, "node_modules/form-data": { - "version": "2.3.3", - "license": "MIT", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "dependencies": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", + "combined-stream": "^1.0.8", "mime-types": "^2.1.12" }, "engines": { - "node": ">= 0.12" + "node": ">= 6" } }, "node_modules/fs-readdir-recursive": { @@ -4852,13 +4804,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/getpass": { - "version": "0.1.7", - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, "node_modules/glob": { "version": "7.2.3", "dev": true, @@ -4977,24 +4922,6 @@ "node": ">=4.x" } }, - "node_modules/har-schema": { - "version": "2.0.0", - "license": "ISC", - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "license": "MIT", - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/has": { "version": "1.0.3", "dev": true, @@ -5120,19 +5047,6 @@ "node": ">=0.10.0" } }, - "node_modules/http-signature": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } - }, "node_modules/https-browserify": { "version": "1.0.0", "dev": true, @@ -5593,10 +5507,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "license": "MIT" - }, "node_modules/is-unicode-supported": { "version": "0.1.0", "dev": true, @@ -5637,10 +5547,6 @@ "node": ">=0.10.0" } }, - "node_modules/isstream": { - "version": "0.1.2", - "license": "MIT" - }, "node_modules/jest-worker": { "version": "27.5.1", "dev": true, @@ -5692,10 +5598,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "0.1.1", - "license": "MIT" - }, "node_modules/jsesc": { "version": "2.5.2", "dev": true, @@ -5717,10 +5619,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-schema": { - "version": "0.4.0", - "license": "(AFL-2.1 OR BSD-3-Clause)" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "license": "MIT" @@ -5730,10 +5628,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "license": "ISC" - }, "node_modules/json5": { "version": "2.2.3", "dev": true, @@ -5750,19 +5644,6 @@ "dev": true, "license": "MIT" }, - "node_modules/jsprim": { - "version": "1.4.2", - "license": "MIT", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "dev": true, @@ -6313,13 +6194,6 @@ "node": ">=0.10.0" } }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, "node_modules/object-inspect": { "version": "1.12.3", "dev": true, @@ -6600,10 +6474,6 @@ "node": ">=0.12" } }, - "node_modules/performance-now": { - "version": "2.1.0", - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.0.0", "dev": true, @@ -6723,9 +6593,10 @@ "version": "1.1.2", "license": "MIT" }, - "node_modules/psl": { - "version": "1.9.0", - "license": "MIT" + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/public-encrypt": { "version": "4.0.3", @@ -6752,13 +6623,6 @@ "node": ">=6" } }, - "node_modules/qs": { - "version": "6.5.3", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.6" - } - }, "node_modules/querystring-es3": { "version": "0.2.1", "dev": true, @@ -6925,64 +6789,6 @@ "jsesc": "bin/jsesc" } }, - "node_modules/request": { - "version": "2.88.2", - "license": "Apache-2.0", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/request-promise": { - "version": "4.2.6", - "license": "ISC", - "dependencies": { - "bluebird": "^3.5.0", - "request-promise-core": "1.1.4", - "stealthy-require": "^1.1.1", - "tough-cookie": "^2.3.3" - }, - "engines": { - "node": ">=0.10.0" - }, - "peerDependencies": { - "request": "^2.34" - } - }, - "node_modules/request-promise-core": { - "version": "1.1.4", - "license": "ISC", - "dependencies": { - "lodash": "^4.17.19" - }, - "engines": { - "node": ">=0.10.0" - }, - "peerDependencies": { - "request": "^2.34" - } - }, "node_modules/require-directory": { "version": "2.1.1", "dev": true, @@ -7154,6 +6960,7 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", + "dev": true, "license": "MIT" }, "node_modules/schema-utils": { @@ -7378,36 +7185,6 @@ "source-map": "^0.6.0" } }, - "node_modules/sshpk": { - "version": "1.17.0", - "license": "MIT", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stealthy-require": { - "version": "1.1.1", - "license": "ISC", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/stream-browserify": { "version": "3.0.0", "dev": true, @@ -7678,17 +7455,6 @@ "node": ">=8.0" } }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "license": "BSD-3-Clause", - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/tree-dump": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz", @@ -7917,20 +7683,6 @@ "dev": true, "license": "0BSD" }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "license": "Unlicense" - }, "node_modules/type-check": { "version": "0.4.0", "dev": true, @@ -8222,18 +7974,6 @@ "node": ">= 0.10" } }, - "node_modules/verror": { - "version": "1.10.0", - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, "node_modules/vscode-oniguruma": { "version": "1.7.0", "dev": true, diff --git a/package.json b/package.json index 0d292e550..8569c2b85 100644 --- a/package.json +++ b/package.json @@ -33,12 +33,11 @@ "@types/node": "^20.6.0", "ajv": "^6.12.6", "async": "2.6.4", + "axios": "^1.7.4", "crypto-js": "^4.0.0", "memfs": "^4.11.1", "net": "^1.0.2", "promise-throttle": "^1.1.2", - "request": "^2.88.0", - "request-promise": "^4.2.6", "serialize-javascript": "^3.1.0", "text-encoding": "^0.7.0", "tls": "0.0.1", diff --git a/src/main/cpp/http_client_wasm.cpp b/src/main/cpp/http_client_wasm.cpp index be19a9353..5448dec6a 100644 --- a/src/main/cpp/http_client_wasm.cpp +++ b/src/main/cpp/http_client_wasm.cpp @@ -30,7 +30,6 @@ EM_JS(const char*, js_send_json_request, (const char* uri, const char* username, body: UTF8ToString(body), resolveWithFullResponse: true, rejectUnauthorized: LibraryUtils.isRejectUnauthorized(UTF8ToString(reject_unauthorized_fn_id)), - requestApi: GenUtils.isFirefox() ? "xhr" : "fetch" // firefox issue: https://bugzilla.mozilla.org/show_bug.cgi?id=1491010 }).then(resp => { // build response container @@ -96,7 +95,6 @@ EM_JS(const char*, js_send_binary_request, (const char* uri, const char* usernam body: view, resolveWithFullResponse: true, rejectUnauthorized: LibraryUtils.isRejectUnauthorized(UTF8ToString(reject_unauthorized_fn_id)), - requestApi: GenUtils.isFirefox() ? "xhr" : "fetch" // firefox issue: https://bugzilla.mozilla.org/show_bug.cgi?id=1491010 }).then(resp => { // write binary body to heap to pass back pointer diff --git a/src/main/ts/common/HttpClient.ts b/src/main/ts/common/HttpClient.ts index 4b3000692..6c9c0b69f 100644 --- a/src/main/ts/common/HttpClient.ts +++ b/src/main/ts/common/HttpClient.ts @@ -1,11 +1,10 @@ import GenUtils from "./GenUtils"; import LibraryUtils from "./LibraryUtils"; -import MoneroUtils from "./MoneroUtils"; import ThreadPool from "./ThreadPool"; import PromiseThrottle from "promise-throttle"; -import Request from "request-promise"; import http from "http"; import https from "https"; +import axios, { AxiosError } from "axios"; /** * Handle HTTP requests with a uniform interface. @@ -17,7 +16,6 @@ export default class HttpClient { // default request config protected static DEFAULT_REQUEST = { method: "GET", - requestApi: "fetch", resolveWithFullResponse: false, rejectUnauthorized: true } @@ -30,7 +28,7 @@ export default class HttpClient { protected static HTTP_AGENT: any; protected static HTTPS_AGENT: any; - + /** *
Make a HTTP request.
* @@ -41,7 +39,6 @@ export default class HttpClient { * @param {string} [request.username] - username to authenticate the request (optional) * @param {string} [request.password] - password to authenticate the request (optional) * @param {object} [request.headers] - headers to add to the request (optional) - * @param {string} [request.requestApi] - one of "fetch" or "xhr" (default "fetch") * @param {boolean} [request.resolveWithFullResponse] - return full response if true, else body only (default false) * @param {boolean} [request.rejectUnauthorized] - whether or not to reject self-signed certificates (default true) * @param {number} request.timeout - maximum time allowed in milliseconds @@ -53,7 +50,6 @@ export default class HttpClient { * @return {object} response.headers - the response headers */ static async request(request) { - // proxy to worker if configured if (request.proxyToWorker) { try { @@ -67,20 +63,20 @@ export default class HttpClient { throw err; } } - + // assign defaults request = Object.assign({}, HttpClient.DEFAULT_REQUEST, request); - + // validate request try { request.host = new URL(request.uri).host; } // hostname:port catch (err) { throw new Error("Invalid request URL: " + request.uri); } if (request.body && !(typeof request.body === "string" || typeof request.body === "object")) { throw new Error("Request body type is not string or object"); } - + // initialize one task queue per host if (!HttpClient.TASK_QUEUES[request.host]) HttpClient.TASK_QUEUES[request.host] = new ThreadPool(1); - + // initialize one promise throttle per host if (!HttpClient.PROMISE_THROTTLES[request.host]) { HttpClient.PROMISE_THROTTLES[request.host] = new PromiseThrottle({ @@ -88,96 +84,19 @@ export default class HttpClient { promiseImplementation: Promise }); } - + // request using fetch or xhr with timeout let timeout = request.timeout === undefined ? HttpClient.DEFAULT_TIMEOUT : request.timeout === 0 ? HttpClient.MAX_TIMEOUT : request.timeout; - let requestPromise = request.requestApi === "fetch" ? HttpClient.requestFetch(request) : HttpClient.requestXhr(request); + let requestPromise = HttpClient.requestAxios(request); return GenUtils.executeWithTimeout(requestPromise, timeout); } - + // ----------------------------- PRIVATE HELPERS ---------------------------- - - protected static async requestFetch(req) { - - // build request options - let opts: any = { - method: req.method, - uri: req.uri, - body: req.body, - agent: req.uri.startsWith("https") ? HttpClient.getHttpsAgent() : HttpClient.getHttpAgent(), - rejectUnauthorized: req.rejectUnauthorized, - resolveWithFullResponse: req.resolveWithFullResponse, - requestCert: true // TODO: part of config? - }; - if (req.username) { - opts.forever = true; - opts.auth = { - user: req.username, - pass: req.password, - sendImmediately: false - } - } - if (req.body instanceof Uint8Array) opts.encoding = null; - - // queue and throttle request to execute in serial and rate limited - let host = req.host; - let resp = await HttpClient.TASK_QUEUES[host].submit(async function() { - return HttpClient.PROMISE_THROTTLES[host].add(function(opts) { return Request(opts); }.bind(this, opts)); - }); - - // normalize response - let normalizedResponse: any = {}; - if (req.resolveWithFullResponse) { - normalizedResponse.statusCode = resp.statusCode; - normalizedResponse.statusText = resp.statusMessage; - normalizedResponse.headers = resp.headers; - normalizedResponse.body = resp.body; - } else { - normalizedResponse.body = resp; - } - return normalizedResponse; - } - - protected static async requestXhr(req) { - if (req.headers) throw new Error("Custom headers not implemented in XHR request"); // TODO - - // collect params from request which change on await - let method = req.method; - let uri = req.uri; - let host = req.host; - let username = req.username; - let password = req.password; - let body = req.body; - let isBinary = body instanceof Uint8Array; - - // queue and throttle requests to execute in serial and rate limited per host - let resp = await HttpClient.TASK_QUEUES[host].submit(async function() { - return HttpClient.PROMISE_THROTTLES[host].add(function() { - return new Promise(function(resolve, reject) { - let digestAuthRequest = new HttpClient.digestAuthRequest(method, uri, username, password); - digestAuthRequest.request(function(resp) { - resolve(resp); - }, function(resp) { - if (resp.status) resolve(resp); - else reject(new Error("Request failed without response: " + method + " " + uri)); - }, body); - }); - }.bind(this)); - }); - - // normalize response - let normalizedResponse: any = {}; - normalizedResponse.statusCode = resp.status; - normalizedResponse.statusText = resp.statusText; - normalizedResponse.headers = HttpClient.parseXhrResponseHeaders(resp.getAllResponseHeaders()); - normalizedResponse.body = isBinary ? new Uint8Array(resp.response) : resp.response; - if (normalizedResponse.body instanceof ArrayBuffer) normalizedResponse.body = new Uint8Array(normalizedResponse.body); // handle empty binary request - return normalizedResponse; - } - + + /** * Get a singleton instance of an HTTP client to share. - * + * * @return {http.Agent} a shared agent for network requests among library instances */ protected static getHttpAgent() { @@ -187,10 +106,10 @@ export default class HttpClient { }); return HttpClient.HTTP_AGENT; } - + /** * Get a singleton instance of an HTTPS client to share. - * + * * @return {https.Agent} a shared agent for network requests among library instances */ protected static getHttpsAgent() { @@ -200,293 +119,125 @@ export default class HttpClient { }); return HttpClient.HTTPS_AGENT; } - - protected static parseXhrResponseHeaders(headersStr) { - let headerMap = {}; - let headers = headersStr.trim().split(/[\r\n]+/); - for (let header of headers) { - let headerVals = header.split(": "); - headerMap[headerVals[0]] = headerVals[1]; - } - return headerMap; - } - /** - * Modification of digest auth request by @inorganik. - * - * Dependent on CryptoJS MD5 hashing: http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/md5.js - * - * MIT licensed. - */ - protected static digestAuthRequest = function(method, url, username, password) { - var self = this; + protected static async requestAxios(req) { + if (req.headers) throw new Error("Custom headers not implemented in XHR request"); // TODO - if (typeof CryptoJS === 'undefined' && typeof require === 'function') { - var CryptoJS = require('crypto-js'); - } + // collect params from request which change on await + const method = req.method; + const uri = req.uri; + const host = req.host; + const username = req.username; + const password = req.password; + const body = req.body; + const isBinary = body instanceof Uint8Array; - this.scheme = null; // we just echo the scheme, to allow for 'Digest', 'X-Digest', 'JDigest' etc - this.nonce = null; // server issued nonce - this.realm = null; // server issued realm - this.qop = null; // "quality of protection" - '' or 'auth' or 'auth-int' - this.response = null; // hashed response to server challenge - this.opaque = null; // hashed response to server challenge - this.nc = 1; // nonce count - increments with each request used with the same nonce - this.cnonce = null; // client nonce + // queue and throttle requests to execute in serial and rate limited per host + const resp = await HttpClient.TASK_QUEUES[host].submit(async function() { + return HttpClient.PROMISE_THROTTLES[host].add(function() { + return new Promise(function(resolve, reject) { + HttpClient.axiosDigestAuthRequest(method, uri, username, password, body).then(function(resp) { + resolve(resp); + }).catch(function(error: AxiosError) { + if (error.response?.status) resolve(error.response); + reject(new Error("Request failed without response: " + method + " " + uri)); + }); + }); - // settings - this.timeout = 60000; // timeout - this.loggingOn = false; // toggle console logging + }.bind(this)); + }); - // determine if a post, so that request will send data - this.post = false; - if (method.toLowerCase() === 'post' || method.toLowerCase() === 'put') { - this.post = true; - } + // normalize response + let normalizedResponse: any = {}; + normalizedResponse.statusCode = resp.status; + normalizedResponse.statusText = resp.statusText; + normalizedResponse.headers = {...resp.headers}; + normalizedResponse.body = isBinary ? new Uint8Array(resp.data) : resp.data; + if (normalizedResponse.body instanceof ArrayBuffer) normalizedResponse.body = new Uint8Array(normalizedResponse.body); // handle empty binary request + return normalizedResponse; + } - // start here - // successFn - will be passed JSON data - // errorFn - will be passed the failed authenticatedRequest - // data - optional, for POSTS - this.request = function(successFn, errorFn, data) { - - // stringify json - if (data) { - try { - self.data = data instanceof Uint8Array || typeof data === "string" ? data : JSON.stringify(data); - } catch (err) { - console.error(err); - throw err; - } - } - self.successFn = successFn; - self.errorFn = errorFn; + protected static axiosDigestAuthRequest = async function(method, url, username, password, body) { + if (typeof CryptoJS === 'undefined' && typeof require === 'function') { + var CryptoJS = require('crypto-js'); + } - if (!self.nonce) { - self.makeUnauthenticatedRequest(self.data); - } else { - self.makeAuthenticatedRequest(); + const generateCnonce = function(): string { + const characters = 'abcdef0123456789'; + let token = ''; + for (let i = 0; i < 16; i++) { + const randNum = Math.round(Math.random() * characters.length); + token += characters.slice(randNum, randNum+1); } + return token; } - this.makeUnauthenticatedRequest = function(data) { - self.firstRequest = new XMLHttpRequest(); - self.firstRequest.open(method, url, true); - self.firstRequest.timeout = self.timeout; - // if we are posting, add appropriate headers - if (self.post && data) { - if (typeof data === "string") { - self.firstRequest.setRequestHeader('Content-type', 'text/plain'); - } else { - self.firstRequest.responseType = "arraybuffer"; - } - } - - self.firstRequest.onreadystatechange = function() { - - // 2: received headers, 3: loading, 4: done - if (self.firstRequest.readyState === 2) { - var responseHeaders = self.firstRequest.getAllResponseHeaders(); - responseHeaders = responseHeaders.split('\n'); - // get authenticate header - var digestHeaders; - for(var i = 0; i < responseHeaders.length; i++) { - if (responseHeaders[i].match(/www-authenticate/i) != null) { - digestHeaders = responseHeaders[i]; - } - } - - if (digestHeaders != null) { - // parse auth header and get digest auth keys - digestHeaders = digestHeaders.slice(digestHeaders.indexOf(':') + 1, -1); - digestHeaders = digestHeaders.split(','); - self.scheme = digestHeaders[0].split(/\s/)[1]; - for (var i = 0; i < digestHeaders.length; i++) { - var equalIndex = digestHeaders[i].indexOf('='), - key = digestHeaders[i].substring(0, equalIndex), - val = digestHeaders[i].substring(equalIndex + 1); - val = val.replace(/['"]+/g, ''); - // find realm - if (key.match(/realm/i) != null) { - self.realm = val; - } - // find nonce - if (key.match(/nonce/i) != null) { - self.nonce = val; - } - // find opaque - if (key.match(/opaque/i) != null) { - self.opaque = val; - } - // find QOP - if (key.match(/qop/i) != null) { - self.qop = val; - } - } - // client generated keys - self.cnonce = self.generateCnonce(); - self.nc++; - // if logging, show headers received: - self.log('received headers:'); - self.log(' realm: '+self.realm); - self.log(' nonce: '+self.nonce); - self.log(' opaque: '+self.opaque); - self.log(' qop: '+self.qop); - // now we can make an authenticated request - self.makeAuthenticatedRequest(); - } - } - if (self.firstRequest.readyState === 4) { - if (self.firstRequest.status === 200) { - self.log('Authentication not required for '+url); - if (data instanceof Uint8Array) { - self.successFn(self.firstRequest); - } else { - if (self.firstRequest.responseText !== 'undefined') { - if (self.firstRequest.responseText.length > 0) { - // If JSON, parse and return object - if (self.isJson(self.firstRequest.responseText)) { // TODO: redundant - self.successFn(self.firstRequest); - } else { - self.successFn(self.firstRequest); - } - } - } else { - self.successFn(); - } - } - } + let count = 0; + return axios.request({ + url: url, + method: method, + timeout: this.timeout, + headers: { + 'Content-Type': 'application/json' + }, + responseType: body instanceof Uint8Array ? 'arraybuffer' : undefined, + httpAgent: url.startsWith("https") ? undefined : HttpClient.getHttpAgent(), + httpsAgent: url.startsWith("https") ? HttpClient.getHttpsAgent() : undefined, + data: body, + transformResponse: res => res, + }).catch(async (err) => { + if (err.response?.status === 401) { + let authHeader = err.response.headers['www-authenticate'].replace(/,\sDigest.*/, ""); + if (!authHeader) { + throw err; } - } - // send - if (self.post) { - // in case digest auth not required - self.firstRequest.send(self.data); - } else { - self.firstRequest.send(); - } - self.log('Unauthenticated request to '+url); - // handle error - self.firstRequest.onerror = function() { - if (self.firstRequest.status !== 401) { - self.log('Error ('+self.firstRequest.status+') on unauthenticated request to '+url); - self.errorFn(self.firstRequest); - } - } - } - this.makeAuthenticatedRequest= function() { + // Digest qop="auth",algorithm=MD5,realm="monero-rpc",nonce="hBZ2rZIxElv4lqCRrUylXA==",stale=false + const authHeaderMap = authHeader.replace("Digest ", "").replaceAll('"', "").split(",").reduce((prev, curr) => ({...prev, [curr.split("=")[0]]: curr.split("=").slice(1).join('=')}), {}) + + ++count; + + const cnonce = generateCnonce(); + const HA1 = CryptoJS.MD5(username+':'+authHeaderMap.realm+':'+password).toString(); + const HA2 = CryptoJS.MD5(method+':'+url).toString(); + + const response = CryptoJS.MD5(HA1+':'+ + authHeaderMap.nonce+':'+ + ('00000000' + count).slice(-8)+':'+ + cnonce+':'+ + authHeaderMap.qop+':'+ + HA2).toString(); + const digestAuthHeader = 'Digest'+' '+ + 'username="'+username+'", '+ + 'realm="'+authHeaderMap.realm+'", '+ + 'nonce="'+authHeaderMap.nonce+'", '+ + 'uri="'+url+'", '+ + 'response="'+response+'", '+ + 'opaque="'+(authHeaderMap.opaque ?? null)+'", '+ + 'qop='+authHeaderMap.qop+', '+ + 'nc='+('00000000' + count).slice(-8)+', '+ + 'cnonce="'+cnonce+'"'; + + const finalResponse = await axios.request({ + url: url, + method: method, + timeout: this.timeout, + headers: { + 'Authorization': digestAuthHeader, + 'Content-Type': 'application/json' + }, + responseType: body instanceof Uint8Array ? 'arraybuffer' : undefined, + httpAgent: url.startsWith("https") ? undefined : HttpClient.getHttpAgent(), + httpsAgent: url.startsWith("https") ? HttpClient.getHttpsAgent() : undefined, + data: body, + transformResponse: res => res, + }); - self.response = self.formulateResponse(); - self.authenticatedRequest = new XMLHttpRequest(); - self.authenticatedRequest.open(method, url, true); - self.authenticatedRequest.timeout = self.timeout; - var digestAuthHeader = self.scheme+' '+ - 'username="'+username+'", '+ - 'realm="'+self.realm+'", '+ - 'nonce="'+self.nonce+'", '+ - 'uri="'+url+'", '+ - 'response="'+self.response+'", '+ - 'opaque="'+self.opaque+'", '+ - 'qop='+self.qop+', '+ - 'nc='+('00000000' + self.nc).slice(-8)+', '+ - 'cnonce="'+self.cnonce+'"'; - self.authenticatedRequest.setRequestHeader('Authorization', digestAuthHeader); - self.log('digest auth header response to be sent:'); - self.log(digestAuthHeader); - // if we are posting, add appropriate headers - if (self.post && self.data) { - if (typeof self.data === "string") { - self.authenticatedRequest.setRequestHeader('Content-type', 'text/plain'); - } else { - self.authenticatedRequest.responseType = "arraybuffer"; - } + return finalResponse; } - self.authenticatedRequest.onload = function() { - // success - if (self.authenticatedRequest.status >= 200 && self.authenticatedRequest.status < 400) { - // increment nonce count - self.nc++; - // return data - if (self.data instanceof Uint8Array) { - self.successFn(self.authenticatedRequest); - } else { - if (self.authenticatedRequest.responseText !== 'undefined' && self.authenticatedRequest.responseText.length > 0 ) { - // If JSON, parse and return object - if (self.isJson(self.authenticatedRequest.responseText)) { // TODO: redundant from not parsing - self.successFn(self.authenticatedRequest); - } else { - self.successFn(self.authenticatedRequest); - } - } else { - self.successFn(); - } - } - } - // failure - else { - self.nonce = null; - self.errorFn(self.authenticatedRequest); - } - } - // handle errors - self.authenticatedRequest.onerror = function() { - self.log('Error ('+self.authenticatedRequest.status+') on authenticated request to '+url); - self.nonce = null; - self.errorFn(self.authenticatedRequest); - }; - // send - if (self.post) { - self.authenticatedRequest.send(self.data); - } else { - self.authenticatedRequest.send(); - } - self.log('Authenticated request to '+url); - } - // hash response based on server challenge - this.formulateResponse = function() { - var HA1 = CryptoJS.MD5(username+':'+self.realm+':'+password).toString(); - var HA2 = CryptoJS.MD5(method+':'+url).toString(); - var response = CryptoJS.MD5(HA1+':'+ - self.nonce+':'+ - ('00000000' + self.nc).slice(-8)+':'+ - self.cnonce+':'+ - self.qop+':'+ - HA2).toString(); - return response; - } - // generate 16 char client nonce - this.generateCnonce = function() { - var characters = 'abcdef0123456789'; - var token = ''; - for (var i = 0; i < 16; i++) { - var randNum = Math.round(Math.random() * characters.length); - token += characters.substr(randNum, 1); - } - return token; - } - this.abort = function() { - self.log('[digestAuthRequest] Aborted request to '+url); - if (self.firstRequest != null) { - if (self.firstRequest.readyState != 4) self.firstRequest.abort(); - } - if (self.authenticatedRequest != null) { - if (self.authenticatedRequest.readyState != 4) self.authenticatedRequest.abort(); - } - } - this.isJson = function(str) { - try { - JSON.parse(str); - } catch (err) { - return false; - } - return true; - } - this.log = function(str) { - if (self.loggingOn) { - console.log('[digestAuthRequest] '+str); - } - } - this.version = function() { return '0.8.0' } + throw err; + }).catch(err => { + throw err; + }); } } diff --git a/src/main/ts/common/MoneroRpcConnection.ts b/src/main/ts/common/MoneroRpcConnection.ts index c17a6ce07..95fbaaeca 100644 --- a/src/main/ts/common/MoneroRpcConnection.ts +++ b/src/main/ts/common/MoneroRpcConnection.ts @@ -272,7 +272,6 @@ export default class MoneroRpcConnection { body: body, timeout: timeoutMs === undefined ? this.timeoutMs : timeoutMs, rejectUnauthorized: this.rejectUnauthorized, - requestApi: GenUtils.isFirefox() ? "xhr" : "fetch", // firefox issue: https://bugzilla.mozilla.org/show_bug.cgi?id=1491010 proxyToWorker: this.proxyToWorker }); @@ -322,7 +321,6 @@ export default class MoneroRpcConnection { body: JSON.stringify(params), // body is stringified so text/plain is returned so bigints are preserved timeout: timeoutMs === undefined ? this.timeoutMs : timeoutMs, rejectUnauthorized: this.rejectUnauthorized, - requestApi: GenUtils.isFirefox() ? "xhr" : "fetch", proxyToWorker: this.proxyToWorker }); @@ -374,7 +372,6 @@ export default class MoneroRpcConnection { body: paramsBin, timeout: timeoutMs === undefined ? this.timeoutMs : timeoutMs, rejectUnauthorized: this.rejectUnauthorized, - requestApi: GenUtils.isFirefox() ? "xhr" : "fetch", proxyToWorker: this.proxyToWorker });