diff --git a/package-lock.json b/package-lock.json index aacdba8746..6fdb5bd8d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3042,6 +3042,28 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, + "axios": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", + "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, "babel-eslint": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", @@ -13522,6 +13544,11 @@ "ipaddr.js": "1.9.1" } }, + "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==" + }, "ps-node": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/ps-node/-/ps-node-0.1.6.tgz", diff --git a/package.json b/package.json index 1810510d87..3e6dc54a82 100644 --- a/package.json +++ b/package.json @@ -19,12 +19,13 @@ ], "license": "BSD-3-Clause", "dependencies": { - "@graphql-yoga/node": "2.6.0", - "@graphql-tools/utils": "8.12.0", "@graphql-tools/merge": "8.3.6", "@graphql-tools/schema": "9.0.4", + "@graphql-tools/utils": "8.12.0", + "@graphql-yoga/node": "2.6.0", "@parse/fs-files-adapter": "1.2.2", "@parse/push-adapter": "4.1.2", + "axios": "1.1.3", "bcryptjs": "2.4.3", "body-parser": "1.20.1", "commander": "5.1.0", @@ -34,8 +35,8 @@ "follow-redirects": "1.15.2", "graphql": "16.6.0", "graphql-list-fields": "2.0.2", - "graphql-tag": "2.12.6", "graphql-relay": "0.10.0", + "graphql-tag": "2.12.6", "intersect": "1.0.1", "jsonwebtoken": "8.5.1", "jwks-rsa": "2.1.5", diff --git a/spec/HTTPRequest.spec.js b/spec/HTTPRequest.spec.js index f218ff3c91..54c85f60c9 100644 --- a/spec/HTTPRequest.spec.js +++ b/spec/HTTPRequest.spec.js @@ -83,18 +83,17 @@ describe('httpRequest', () => { }); it('should fail on 404', async () => { - await expectAsync( - httpRequest({ + try { + await httpRequest({ url: `${httpRequestServer}/404`, - }) - ).toBeRejectedWith( - jasmine.objectContaining({ - status: 404, - buffer: Buffer.from('NO'), - text: 'NO', - data: undefined, - }) - ); + }); + fail('should have failed'); + } catch (e) { + expect(e.status).toBe(404); + expect(e.buffer).toEqual(Buffer.from('NO')); + expect(e.text).toBe('NO'); + expect(e.data).toBeUndefined(); + } }); it('should post on echo', async () => { @@ -178,7 +177,6 @@ describe('httpRequest', () => { url: `${httpRequestServer}/qs`, params: 'foo=bar&foo2=bar2', }); - expect(httpResponse.status).toBe(200); expect(httpResponse.data).toEqual({ foo: 'bar', foo2: 'bar2' }); }); diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index e50144f1fe..aea3c1fa95 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -116,7 +116,7 @@ describe('Pages Router', () => { followRedirects: false, }).catch(e => e); expect(res.status).toBe(200); - expect(res.text).toEqual('"Password successfully reset"'); + expect(res.text).toEqual('Password successfully reset'); }); it('request_password_reset: responds with AJAX error on missing password', async () => { @@ -970,10 +970,12 @@ describe('Pages Router', () => { // Do not compose this URL with `new URL(...)` because that would normalize // the URL and remove path patterns; the path patterns must reach the router const url = `${config.publicServerURL}/apps/../.gitignore`; - const response = await request({ - url: url, - followRedirects: false, - }).catch(e => e); + const response = await request + .legacy({ + url: url, + followRedirects: false, + }) + .catch(e => e); expect(response.status).toBe(404); expect(response.text).toBe('Not found.'); }); diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 92301316e4..1354f38f2c 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -2459,7 +2459,7 @@ describe('Parse.User testing', () => { 'X-Parse-REST-API-Key': 'rest', }, url: 'http://localhost:8378/1/sessions/' + b.objectId, - body: JSON.stringify({ foo: 'bar' }), + body: { foo: 'bar' }, }).then(() => { done(); }); diff --git a/spec/RegexVulnerabilities.spec.js b/spec/RegexVulnerabilities.spec.js index 62af4f04e2..ccd31fc597 100644 --- a/spec/RegexVulnerabilities.spec.js +++ b/spec/RegexVulnerabilities.spec.js @@ -137,11 +137,11 @@ describe('Regex Vulnerabilities', function () { await request({ url: `${serverURL}/apps/test/request_password_reset`, method: 'POST', - body: { + body: JSON.stringify({ token: { $regex: '' }, username: 'someemail@somedomain.com', new_password: 'newpassword', - }, + }), }); try { await Parse.User.logIn('someemail@somedomain.com', 'newpassword'); @@ -174,7 +174,7 @@ describe('Regex Vulnerabilities', function () { expect(passwordResetResponse.headers.location).toMatch( `\\/choose\\_password\\?token\\=${token}\\&` ); - await request({ + await request.legacy({ url: `${serverURL}/apps/test/request_password_reset`, method: 'POST', body: { diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index d7cc72c7b0..13a37cbcac 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -690,20 +690,22 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }, publicServerURL: 'http://localhost:8378/1', }).then(() => { - request({ - url: 'http://localhost:8378/1/apps/test/resend_verification_email', - method: 'POST', - followRedirects: false, - body: { - username: 'sadfasga', - }, - }).then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/link_send_fail.html' - ); - done(); - }); + request + .legacy({ + url: 'http://localhost:8378/1/apps/test/resend_verification_email', + method: 'POST', + followRedirects: false, + body: { + username: 'sadfasga', + }, + }) + .then(response => { + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/link_send_fail.html' + ); + done(); + }); }); }); @@ -975,7 +977,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { followRedirects: false, }); expect(resetResponse.status).toEqual(200); - expect(resetResponse.text).toEqual('"Password successfully reset"'); + expect(resetResponse.text).toEqual('Password successfully reset'); await Parse.User.logIn('zxcv', 'hello'); const config = Config.get('test'); diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 1ee02fdb60..bb3f5eb45a 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -713,14 +713,8 @@ ParseCloud.useMasterKey = () => { ); }; -const request = require('./httpRequest'); -ParseCloud.httpRequest = opts => { - Deprecator.logRuntimeDeprecation({ - usage: 'Parse.Cloud.httpRequest', - solution: 'Use a http request library instead.', - }); - return request(opts); -}; +import axios from 'axios'; +ParseCloud.httpRequest = axios; module.exports = ParseCloud; diff --git a/src/cloud-code/httpRequest.js b/src/cloud-code/httpRequest.js index 0f70a8d6a6..e09730c348 100644 --- a/src/cloud-code/httpRequest.js +++ b/src/cloud-code/httpRequest.js @@ -82,10 +82,63 @@ const encodeBody = function ({ body, headers = {} }) { * * @method httpRequest * @name Parse.Cloud.httpRequest - * @param {Parse.Cloud.HTTPOptions} options The Parse.Cloud.HTTPOptions object that makes the request. - * @return {Promise} A promise that will be resolved with a {@link Parse.Cloud.HTTPResponse} object when the request completes. + * @param {Object} options axios object for options + * @return {Promise} axios response object */ -module.exports = function httpRequest(options) { +import axios from 'axios'; +import { parse as qs } from 'querystring'; +module.exports = async options => { + if (options.method) { + options.method = options.method.toLowerCase(); + } + if (options.body) { + options.data = options.body; + delete options.body; + } + if (typeof options.params === 'object') { + options.qs = options.params; + } else if (typeof options.params === 'string') { + options.qs = qs(options.params); + } + if (options.qs) { + options.params = options.qs; + delete options.qs; + } + if (!options.followRedirects) { + options.maxRedirects = 0; + delete options.followRedirects; + } + try { + const response = await axios(options); + const data = response.data; + if (Object.prototype.toString.call(data) === '[object Object]') { + response.text = JSON.stringify(data); + response.data = data; + } else { + response.text = data; + } + response.buffer = Buffer.from(response.text); + return response; + } catch (e) { + e.status = e.response && e.response.status; + const data = e.response && e.response.data; + if (Object.prototype.toString.call(data) === '[object Object]') { + e.text = JSON.stringify(data); + e.data = data; + } else { + e.text = data; + } + e.buffer = Buffer.from(e.text); + if (e.response && e.response.headers) { + e.headers = e.response.headers; + } + if (e.status === 301 || e.status === 302 || e.status === 303) { + return e; + } + throw e; + } +}; +module.exports.legacy = function httpRequest(options) { let url; try { url = parse(options.url); @@ -143,16 +196,4 @@ module.exports = function httpRequest(options) { }); }; -/** - * @typedef Parse.Cloud.HTTPOptions - * @property {String|Object} body The body of the request. If it is a JSON object, then the Content-Type set in the headers must be application/x-www-form-urlencoded or application/json. You can also set this to a {@link Buffer} object to send raw bytes. If you use a Buffer, you should also set the Content-Type header explicitly to describe what these bytes represent. - * @property {function} error The function that is called when the request fails. It will be passed a Parse.Cloud.HTTPResponse object. - * @property {Boolean} followRedirects Whether to follow redirects caused by HTTP 3xx responses. Defaults to false. - * @property {Object} headers The headers for the request. - * @property {String} method The method of the request. GET, POST, PUT, DELETE, HEAD, and OPTIONS are supported. Will default to GET if not specified. - * @property {String|Object} params The query portion of the url. You can pass a JSON object of key value pairs like params: {q : 'Sean Plott'} or a raw string like params:q=Sean Plott. - * @property {function} success The function that is called when the request successfully completes. It will be passed a Parse.Cloud.HTTPResponse object. - * @property {string} url The url to send the request to. - */ - module.exports.encodeBody = encodeBody;