From 40ee72742667106581eeec4e4390703369e3cf09 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 3 Nov 2022 14:27:50 +1100 Subject: [PATCH 1/5] fix: Replace existing HTTP request module --- DEPRECATIONS.md | 1 - package-lock.json | 27 +++++++++++++++++ package.json | 7 +++-- spec/CloudCode.spec.js | 19 ------------ spec/HTTPRequest.spec.js | 22 +++++++------- spec/PagesRouter.spec.js | 12 ++++---- spec/PurchaseValidation.spec.js | 1 + src/cloud-code/Parse.Cloud.js | 10 ++----- src/cloud-code/httpRequest.js | 52 ++++++++++++++++++++++++++++++++- 9 files changed, 102 insertions(+), 49 deletions(-) diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md index 82aa8144a0..a601a0b3d6 100644 --- a/DEPRECATIONS.md +++ b/DEPRECATIONS.md @@ -7,7 +7,6 @@ The following is a list of deprecations, according to the [Deprecation Policy](h | DEPPS1 | Native MongoDB syntax in aggregation pipeline | [#7338](https://github.com/parse-community/parse-server/issues/7338) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - | | DEPPS2 | Config option `directAccess` defaults to `true` | [#6636](https://github.com/parse-community/parse-server/pull/6636) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - | | DEPPS3 | Config option `enforcePrivateUsers` defaults to `true` | [#7319](https://github.com/parse-community/parse-server/pull/7319) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - | -| DEPPS4 | Remove convenience method for http request `Parse.Cloud.httpRequest` | [#7589](https://github.com/parse-community/parse-server/pull/7589) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - | | DEPPS5 | Config option `allowClientClassCreation` defaults to `false` | [#7925](https://github.com/parse-community/parse-server/pull/7925) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - | | DEPPS6 | Auth providers disabled by default | [#7953](https://github.com/parse-community/parse-server/pull/7953) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - | | DEPPS7 | Remove file trigger syntax `Parse.Cloud.beforeSaveFile((request) => {})` | [#7966](https://github.com/parse-community/parse-server/pull/7966) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - | 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/CloudCode.spec.js b/spec/CloudCode.spec.js index c522af0b5d..3cec9ac468 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1686,25 +1686,6 @@ describe('Cloud Code', () => { obj.save().then(done, done.fail); }); - it('can deprecate Parse.Cloud.httpRequest', async () => { - const logger = require('../lib/logger').logger; - spyOn(logger, 'warn').and.callFake(() => {}); - Parse.Cloud.define('hello', () => { - return 'Hello world!'; - }); - await Parse.Cloud.httpRequest({ - method: 'POST', - url: 'http://localhost:8378/1/functions/hello', - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest', - }, - }); - expect(logger.warn).toHaveBeenCalledWith( - 'DeprecationWarning: Parse.Cloud.httpRequest is deprecated and will be removed in a future version. Use a http request library instead.' - ); - }); - describe('cloud jobs', () => { it('should define a job', done => { expect(() => { 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/PurchaseValidation.spec.js b/spec/PurchaseValidation.spec.js index f139b1fd04..1666815bb5 100644 --- a/spec/PurchaseValidation.spec.js +++ b/spec/PurchaseValidation.spec.js @@ -59,6 +59,7 @@ describe('test validate_receipt endpoint', () => { const otherResponse = await request({ url: url, }); + console.log(otherResponse.data); expect(otherResponse.text).toBe('download_file'); } }); 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..54215c5c39 100644 --- a/src/cloud-code/httpRequest.js +++ b/src/cloud-code/httpRequest.js @@ -85,7 +85,57 @@ const encodeBody = function ({ body, headers = {} }) { * @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. */ -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.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); From c5d9387922b6d3259e7b72b13d11bf48952f7541 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 3 Nov 2022 14:31:22 +1100 Subject: [PATCH 2/5] Update PurchaseValidation.spec.js --- spec/PurchaseValidation.spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/PurchaseValidation.spec.js b/spec/PurchaseValidation.spec.js index 1666815bb5..f139b1fd04 100644 --- a/spec/PurchaseValidation.spec.js +++ b/spec/PurchaseValidation.spec.js @@ -59,7 +59,6 @@ describe('test validate_receipt endpoint', () => { const otherResponse = await request({ url: url, }); - console.log(otherResponse.data); expect(otherResponse.text).toBe('download_file'); } }); From b39c138cbe1737f3e1c053081033b2bb5540c25e Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 3 Nov 2022 15:58:37 +1100 Subject: [PATCH 3/5] fix tests --- spec/ParseUser.spec.js | 2 +- spec/RegexVulnerabilities.spec.js | 6 ++--- spec/ValidationAndPasswordsReset.spec.js | 32 +++++++++++++----------- src/cloud-code/httpRequest.js | 3 +++ 4 files changed, 24 insertions(+), 19 deletions(-) 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/httpRequest.js b/src/cloud-code/httpRequest.js index 54215c5c39..f9c1185d9d 100644 --- a/src/cloud-code/httpRequest.js +++ b/src/cloud-code/httpRequest.js @@ -129,6 +129,9 @@ module.exports = async options => { 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; } From 17387b676ee48f6779b0f23918d0eb2a7129d2a2 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 3 Nov 2022 16:00:17 +1100 Subject: [PATCH 4/5] Update httpRequest.js --- src/cloud-code/httpRequest.js | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/cloud-code/httpRequest.js b/src/cloud-code/httpRequest.js index f9c1185d9d..e09730c348 100644 --- a/src/cloud-code/httpRequest.js +++ b/src/cloud-code/httpRequest.js @@ -82,8 +82,8 @@ 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 */ import axios from 'axios'; import { parse as qs } from 'querystring'; @@ -196,16 +196,4 @@ module.exports.legacy = 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; From 1e185c79cf6548d8102bdd6e2a7e3d361eedfabe Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 4 Nov 2022 11:19:51 +1100 Subject: [PATCH 5/5] revert removal --- DEPRECATIONS.md | 1 + spec/CloudCode.spec.js | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md index a601a0b3d6..82aa8144a0 100644 --- a/DEPRECATIONS.md +++ b/DEPRECATIONS.md @@ -7,6 +7,7 @@ The following is a list of deprecations, according to the [Deprecation Policy](h | DEPPS1 | Native MongoDB syntax in aggregation pipeline | [#7338](https://github.com/parse-community/parse-server/issues/7338) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - | | DEPPS2 | Config option `directAccess` defaults to `true` | [#6636](https://github.com/parse-community/parse-server/pull/6636) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - | | DEPPS3 | Config option `enforcePrivateUsers` defaults to `true` | [#7319](https://github.com/parse-community/parse-server/pull/7319) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - | +| DEPPS4 | Remove convenience method for http request `Parse.Cloud.httpRequest` | [#7589](https://github.com/parse-community/parse-server/pull/7589) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - | | DEPPS5 | Config option `allowClientClassCreation` defaults to `false` | [#7925](https://github.com/parse-community/parse-server/pull/7925) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - | | DEPPS6 | Auth providers disabled by default | [#7953](https://github.com/parse-community/parse-server/pull/7953) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - | | DEPPS7 | Remove file trigger syntax `Parse.Cloud.beforeSaveFile((request) => {})` | [#7966](https://github.com/parse-community/parse-server/pull/7966) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - | diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 3cec9ac468..c522af0b5d 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1686,6 +1686,25 @@ describe('Cloud Code', () => { obj.save().then(done, done.fail); }); + it('can deprecate Parse.Cloud.httpRequest', async () => { + const logger = require('../lib/logger').logger; + spyOn(logger, 'warn').and.callFake(() => {}); + Parse.Cloud.define('hello', () => { + return 'Hello world!'; + }); + await Parse.Cloud.httpRequest({ + method: 'POST', + url: 'http://localhost:8378/1/functions/hello', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + }); + expect(logger.warn).toHaveBeenCalledWith( + 'DeprecationWarning: Parse.Cloud.httpRequest is deprecated and will be removed in a future version. Use a http request library instead.' + ); + }); + describe('cloud jobs', () => { it('should define a job', done => { expect(() => {