remove runtime dependency on request (#5076)

This commit is contained in:
Florent Vilmart
2018-09-23 12:31:08 -04:00
committed by GitHub
parent 4dc4a3a56d
commit 93a0017b25
13 changed files with 2706 additions and 2779 deletions

4809
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -28,6 +28,7 @@
"commander": "2.18.0", "commander": "2.18.0",
"deepcopy": "1.0.0", "deepcopy": "1.0.0",
"express": "4.16.2", "express": "4.16.2",
"follow-redirects": "1.5.8",
"intersect": "1.0.1", "intersect": "1.0.1",
"lodash": "4.17.11", "lodash": "4.17.11",
"lru-cache": "4.1.3", "lru-cache": "4.1.3",
@@ -36,7 +37,6 @@
"parse": "2.1.0", "parse": "2.1.0",
"pg-promise": "8.4.6", "pg-promise": "8.4.6",
"redis": "2.8.0", "redis": "2.8.0",
"request": "2.88.0",
"semver": "5.5.1", "semver": "5.5.1",
"tv4": "1.3.0", "tv4": "1.3.0",
"uuid": "^3.1.0", "uuid": "^3.1.0",
@@ -69,6 +69,7 @@
"nodemon": "1.18.4", "nodemon": "1.18.4",
"nyc": "^12.0.2", "nyc": "^12.0.2",
"prettier": "1.14.3", "prettier": "1.14.3",
"request": "2.88.0",
"request-promise": "4.2.2", "request-promise": "4.2.2",
"supports-color": "^5.4.0" "supports-color": "^5.4.0"
}, },

View File

@@ -50,91 +50,48 @@ describe('httpRequest', () => {
it('should do /hello', done => { it('should do /hello', done => {
httpRequest({ httpRequest({
url: httpRequestServer + '/hello', url: httpRequestServer + '/hello',
}).then( }).then(function(httpResponse) {
function(httpResponse) { expect(httpResponse.status).toBe(200);
expect(httpResponse.status).toBe(200); expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}'));
expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}')); expect(httpResponse.text).toEqual('{"response":"OK"}');
expect(httpResponse.text).toEqual('{"response":"OK"}'); expect(httpResponse.data.response).toEqual('OK');
expect(httpResponse.data.response).toEqual('OK'); done();
done(); }, done.fail);
},
function() {
fail('should not fail');
done();
}
);
});
it('should do /hello with callback and promises', done => {
let calls = 0;
httpRequest({
url: httpRequestServer + '/hello',
success: function() {
calls++;
},
error: function() {
calls++;
},
}).then(
function(httpResponse) {
expect(calls).toBe(1);
expect(httpResponse.status).toBe(200);
expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}'));
expect(httpResponse.text).toEqual('{"response":"OK"}');
expect(httpResponse.data.response).toEqual('OK');
done();
},
function() {
fail('should not fail');
done();
}
);
}); });
it('should do not follow redirects by default', done => { it('should do not follow redirects by default', done => {
httpRequest({ httpRequest({
url: httpRequestServer + '/301', url: httpRequestServer + '/301',
}).then( }).then(function(httpResponse) {
function(httpResponse) { expect(httpResponse.status).toBe(301);
expect(httpResponse.status).toBe(301); done();
done(); }, done.fail);
},
function() {
fail('should not fail');
done();
}
);
}); });
it('should follow redirects when set', done => { it('should follow redirects when set', done => {
httpRequest({ httpRequest({
url: httpRequestServer + '/301', url: httpRequestServer + '/301',
followRedirects: true, followRedirects: true,
}).then( }).then(function(httpResponse) {
function(httpResponse) { expect(httpResponse.status).toBe(200);
expect(httpResponse.status).toBe(200); expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}'));
expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}')); expect(httpResponse.text).toEqual('{"response":"OK"}');
expect(httpResponse.text).toEqual('{"response":"OK"}'); expect(httpResponse.data.response).toEqual('OK');
expect(httpResponse.data.response).toEqual('OK'); done();
done(); }, done.fail);
},
function() {
fail('should not fail');
done();
}
);
}); });
it('should fail on 404', done => { it('should fail on 404', done => {
let calls = 0; let calls = 0;
httpRequest({ httpRequest({
url: httpRequestServer + '/404', url: httpRequestServer + '/404',
success: function() { }).then(
function() {
calls++; calls++;
fail('should not succeed'); fail('should not succeed');
done(); done();
}, },
error: function(httpResponse) { function(httpResponse) {
calls++; calls++;
expect(calls).toBe(1); expect(calls).toBe(1);
expect(httpResponse.status).toBe(404); expect(httpResponse.status).toBe(404);
@@ -142,12 +99,11 @@ describe('httpRequest', () => {
expect(httpResponse.text).toEqual('NO'); expect(httpResponse.text).toEqual('NO');
expect(httpResponse.data).toBe(undefined); expect(httpResponse.data).toBe(undefined);
done(); done();
}, }
}); );
}); });
it('should post on echo', done => { it('should post on echo', done => {
let calls = 0;
httpRequest({ httpRequest({
method: 'POST', method: 'POST',
url: httpRequestServer + '/echo', url: httpRequestServer + '/echo',
@@ -157,15 +113,8 @@ describe('httpRequest', () => {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
success: function() {
calls++;
},
error: function() {
calls++;
},
}).then( }).then(
function(httpResponse) { function(httpResponse) {
expect(calls).toBe(1);
expect(httpResponse.status).toBe(200); expect(httpResponse.status).toBe(200);
expect(httpResponse.data).toEqual({ foo: 'bar' }); expect(httpResponse.data).toEqual({ foo: 'bar' });
done(); done();
@@ -220,15 +169,10 @@ describe('httpRequest', () => {
it('should fail gracefully', done => { it('should fail gracefully', done => {
httpRequest({ httpRequest({
url: 'http://not a good url', url: 'http://not a good url',
success: function() { }).then(done.fail, function(error) {
fail('should not succeed'); expect(error).not.toBeUndefined();
done(); expect(error).not.toBeNull();
}, done();
error: function(error) {
expect(error).not.toBeUndefined();
expect(error).not.toBeNull();
done();
},
}); });
}); });

View File

@@ -1,4 +1,4 @@
const request = require('request'); const request = require('../lib/request');
function createProduct() { function createProduct() {
const file = new Parse.File( const file = new Parse.File(
@@ -26,165 +26,136 @@ describe('test validate_receipt endpoint', () => {
beforeEach(done => { beforeEach(done => {
createProduct() createProduct()
.then(done) .then(done)
.catch(function() { .catch(function(err) {
console.error({ err });
done(); done();
}); });
}); });
it('should bypass appstore validation', done => { it('should bypass appstore validation', async () => {
request.post( const httpResponse = await request({
{ headers: {
headers: { 'X-Parse-Application-Id': 'test',
'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest',
'X-Parse-REST-API-Key': 'rest', 'Content-Type': 'application/json',
},
url: 'http://localhost:8378/1/validate_purchase',
json: true,
body: {
productIdentifier: 'a-product',
receipt: {
__type: 'Bytes',
base64: new Buffer('receipt', 'utf-8').toString('base64'),
},
bypassAppStoreValidation: true,
},
}, },
function(err, res, body) { method: 'POST',
if (typeof body != 'object') { url: 'http://localhost:8378/1/validate_purchase',
fail('Body is not an object'); body: {
done(); productIdentifier: 'a-product',
} else { receipt: {
expect(body.__type).toEqual('File'); __type: 'Bytes',
const url = body.url; base64: new Buffer('receipt', 'utf-8').toString('base64'),
request.get( },
{ bypassAppStoreValidation: true,
url: url, },
}, });
function(err, res, body) { const body = httpResponse.data;
expect(body).toEqual('download_file'); if (typeof body != 'object') {
done(); fail('Body is not an object');
} } else {
); console.log(body);
} expect(body.__type).toEqual('File');
} const url = body.url;
); const otherResponse = await request({
url: url,
});
expect(otherResponse.text).toBe('download_file');
}
}); });
it('should fail for missing receipt', done => { it('should fail for missing receipt', async () => {
request.post( const response = await request({
{ headers: {
headers: { 'X-Parse-Application-Id': 'test',
'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest',
'X-Parse-REST-API-Key': 'rest', 'Content-Type': 'application/json',
},
url: 'http://localhost:8378/1/validate_purchase',
json: true,
body: {
productIdentifier: 'a-product',
bypassAppStoreValidation: true,
},
}, },
function(err, res, body) { url: 'http://localhost:8378/1/validate_purchase',
if (typeof body != 'object') { method: 'POST',
fail('Body is not an object'); body: {
done(); productIdentifier: 'a-product',
} else { bypassAppStoreValidation: true,
expect(body.code).toEqual(Parse.Error.INVALID_JSON); },
done(); }).then(fail, res => res);
} const body = response.data;
} expect(body.code).toEqual(Parse.Error.INVALID_JSON);
);
}); });
it('should fail for missing product identifier', done => { it('should fail for missing product identifier', async () => {
request.post( const response = await request({
{ headers: {
headers: { 'X-Parse-Application-Id': 'test',
'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest',
'X-Parse-REST-API-Key': 'rest', 'Content-Type': 'application/json',
},
url: 'http://localhost:8378/1/validate_purchase',
json: true,
body: {
receipt: {
__type: 'Bytes',
base64: new Buffer('receipt', 'utf-8').toString('base64'),
},
bypassAppStoreValidation: true,
},
}, },
function(err, res, body) { url: 'http://localhost:8378/1/validate_purchase',
if (typeof body != 'object') { method: 'POST',
fail('Body is not an object'); body: {
done(); receipt: {
} else { __type: 'Bytes',
expect(body.code).toEqual(Parse.Error.INVALID_JSON); base64: new Buffer('receipt', 'utf-8').toString('base64'),
done(); },
} bypassAppStoreValidation: true,
} },
); }).then(fail, res => res);
const body = response.data;
expect(body.code).toEqual(Parse.Error.INVALID_JSON);
}); });
it('should bypass appstore validation and not find product', done => { it('should bypass appstore validation and not find product', async () => {
request.post( const response = await request({
{ headers: {
headers: { 'X-Parse-Application-Id': 'test',
'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest',
'X-Parse-REST-API-Key': 'rest', 'Content-Type': 'application/json',
},
url: 'http://localhost:8378/1/validate_purchase',
json: true,
body: {
productIdentifier: 'another-product',
receipt: {
__type: 'Bytes',
base64: new Buffer('receipt', 'utf-8').toString('base64'),
},
bypassAppStoreValidation: true,
},
}, },
function(err, res, body) { url: 'http://localhost:8378/1/validate_purchase',
if (typeof body != 'object') { method: 'POST',
fail('Body is not an object'); body: {
done(); productIdentifier: 'another-product',
} else { receipt: {
expect(body.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); __type: 'Bytes',
expect(body.error).toEqual('Object not found.'); base64: new Buffer('receipt', 'utf8').toString('base64'),
done(); },
} bypassAppStoreValidation: true,
} },
); }).catch(error => error);
const body = response.data;
if (typeof body != 'object') {
fail('Body is not an object');
} else {
expect(body.code).toEqual(Parse.Error.OBJECT_NOT_FOUND);
expect(body.error).toEqual('Object not found.');
}
}); });
it('should fail at appstore validation', done => { it('should fail at appstore validation', async () => {
request.post( const response = await request({
{ headers: {
headers: { 'X-Parse-Application-Id': 'test',
'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest',
'X-Parse-REST-API-Key': 'rest', 'Content-Type': 'application/json',
}, },
url: 'http://localhost:8378/1/validate_purchase', url: 'http://localhost:8378/1/validate_purchase',
json: true, method: 'POST',
body: { body: {
productIdentifier: 'a-product', productIdentifier: 'a-product',
receipt: { receipt: {
__type: 'Bytes', __type: 'Bytes',
base64: new Buffer('receipt', 'utf-8').toString('base64'), base64: new Buffer('receipt', 'utf-8').toString('base64'),
},
}, },
}, },
function(err, res, body) { });
if (typeof body != 'object') { const body = response.data;
fail('Body is not an object'); if (typeof body != 'object') {
} else { fail('Body is not an object');
expect(body.status).toBe(21002); } else {
expect(body.error).toBe( expect(body.status).toBe(21002);
'The data in the receipt-data property was malformed or missing.' expect(body.error).toBe(
); 'The data in the receipt-data property was malformed or missing.'
} );
done(); }
}
);
}); });
it('should not create a _Product', done => { it('should not create a _Product', done => {

View File

@@ -1,9 +1,10 @@
{ {
"apps": [ "apps": [
{ {
"arg1": "my_app", "arg1": "my_app",
"arg2": 8888, "arg2": 8888,
"arg3": "hello", "arg3": "hello",
"arg4": "/1" "arg4": "/1"
}] }
]
} }

View File

@@ -1,16 +1,16 @@
{ {
"apps": [ "apps": [
{ {
"arg1": "my_app", "arg1": "my_app",
"arg2": "99999", "arg2": "99999",
"arg3": "hello", "arg3": "hello",
"arg4": "/1" "arg4": "/1"
}, },
{ {
"arg1": "my_app2", "arg1": "my_app2",
"arg2": "9999", "arg2": "9999",
"arg3": "hello", "arg3": "hello",
"arg4": "/1" "arg4": "/1"
} }
] ]
} }

View File

@@ -1,10 +1,6 @@
{ {
"spec_dir": "spec", "spec_dir": "spec",
"spec_files": [ "spec_files": ["*spec.js"],
"*spec.js" "helpers": ["helper.js"],
],
"helpers": [
"helper.js"
],
"random": false "random": false
} }

View File

@@ -4,7 +4,7 @@ import * as triggers from '../triggers';
// @flow-disable-next // @flow-disable-next
import * as Parse from 'parse/node'; import * as Parse from 'parse/node';
// @flow-disable-next // @flow-disable-next
import * as request from 'request'; import request from '../request';
import { logger } from '../logger'; import { logger } from '../logger';
import http from 'http'; import http from 'http';
import https from 'https'; import https from 'https';
@@ -225,10 +225,12 @@ function wrapToHTTPRequest(hook, key) {
jsonBody.original.className = req.original.className; jsonBody.original.className = req.original.className;
} }
const jsonRequest: any = { const jsonRequest: any = {
url: hook.url,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(jsonBody), body: jsonBody,
method: 'POST',
}; };
const agent = hook.url.startsWith('https') const agent = hook.url.startsWith('https')
@@ -243,39 +245,38 @@ function wrapToHTTPRequest(hook, key) {
'Making outgoing webhook request without webhookKey being set!' 'Making outgoing webhook request without webhookKey being set!'
); );
} }
return request(jsonRequest).then(response => {
return new Promise((resolve, reject) => { let err;
request.post(hook.url, jsonRequest, function(err, httpResponse, body) { let result;
var result; let body = response.data;
if (body) { if (body) {
if (typeof body === 'string') { if (typeof body === 'string') {
try { try {
body = JSON.parse(body); body = JSON.parse(body);
} catch (e) { } catch (e) {
err = { err = {
error: 'Malformed response', error: 'Malformed response',
code: -1, code: -1,
partialResponse: body.substring(0, 100), partialResponse: body.substring(0, 100),
}; };
}
}
if (!err) {
result = body.success;
err = body.error;
} }
} }
if (err) { if (!err) {
return reject(err); result = body.success;
} else if (hook.triggerName === 'beforeSave') { err = body.error;
if (typeof result === 'object') {
delete result.createdAt;
delete result.updatedAt;
}
return resolve({ object: result });
} else {
return resolve(result);
} }
}); }
if (err) {
throw err;
} else if (hook.triggerName === 'beforeSave') {
if (typeof result === 'object') {
delete result.createdAt;
delete result.updatedAt;
}
return { object: result };
} else {
return result;
}
}); });
}; };
} }

View File

@@ -297,39 +297,31 @@ class ParseServer {
static verifyServerUrl(callback) { static verifyServerUrl(callback) {
// perform a health check on the serverURL value // perform a health check on the serverURL value
if (Parse.serverURL) { if (Parse.serverURL) {
const request = require('request'); const request = require('./request');
request(Parse.serverURL.replace(/\/$/, '') + '/health', function( request({ url: Parse.serverURL.replace(/\/$/, '') + '/health' })
error, .catch(response => response)
response, .then(response => {
body const json = response.data || null;
) { if (
let json; response.status !== 200 ||
try { !json ||
json = JSON.parse(body); (json && json.status !== 'ok')
} catch (e) { ) {
json = null; /* eslint-disable no-console */
} console.warn(
if ( `\nWARNING, Unable to connect to '${Parse.serverURL}'.` +
error || ` Cloud code and push notifications may be unavailable!\n`
response.statusCode !== 200 || );
!json || /* eslint-enable no-console */
(json && json.status !== 'ok') if (callback) {
) { callback(false);
/* eslint-disable no-console */ }
console.warn( } else {
`\nWARNING, Unable to connect to '${Parse.serverURL}'.` + if (callback) {
` Cloud code and push notifications may be unavailable!\n` callback(true);
); }
/* eslint-enable no-console */
if (callback) {
callback(false);
} }
} else { });
if (callback) {
callback(true);
}
}
});
} }
} }
} }

View File

@@ -1,6 +1,6 @@
import PromiseRouter from '../PromiseRouter'; import PromiseRouter from '../PromiseRouter';
var request = require('request'); const request = require('../request');
var rest = require('../rest'); const rest = require('../rest');
import Parse from 'parse/node'; import Parse from 'parse/node';
// TODO move validation logic in IAPValidationController // TODO move validation logic in IAPValidationController
@@ -25,23 +25,21 @@ function appStoreError(status) {
} }
function validateWithAppStore(url, receipt) { function validateWithAppStore(url, receipt) {
return new Promise(function(fulfill, reject) { return request({
request.post( url: url,
{ method: 'POST',
url: url, body: { 'receipt-data': receipt },
body: { 'receipt-data': receipt }, headers: {
json: true, 'Content-Type': 'application/json',
}, },
function(err, res, body) { }).then(httpResponse => {
var status = body.status; const body = httpResponse.data;
if (status == 0) { if (body && body.status === 0) {
// No need to pass anything, status is OK // No need to pass anything, status is OK
return fulfill(); return;
} }
// receipt is from test and should go to test // receipt is from test and should go to test
return reject(body); throw body;
}
);
}); });
} }

View File

@@ -1,9 +1,36 @@
import request from 'request';
import HTTPResponse from './HTTPResponse'; import HTTPResponse from './HTTPResponse';
import querystring from 'querystring'; import querystring from 'querystring';
import log from '../logger'; import log from '../logger';
import { http, https } from 'follow-redirects';
import { URL } from 'url';
var encodeBody = function({ body, headers = {} }) { const clients = {
'http:': http,
'https:': https,
};
function makeCallback(resolve, reject) {
return function(response) {
const chunks = [];
response.on('data', chunk => {
chunks.push(chunk);
});
response.on('end', () => {
const body = Buffer.concat(chunks);
const httpResponse = new HTTPResponse(response, body);
// Consider <200 && >= 400 as errors
if (httpResponse.status < 200 || httpResponse.status >= 400) {
return reject(httpResponse);
} else {
return resolve(httpResponse);
}
});
response.on('error', reject);
};
}
const encodeBody = function({ body, headers = {} }) {
if (typeof body !== 'object') { if (typeof body !== 'object') {
return { body, headers }; return { body, headers };
} }
@@ -63,48 +90,48 @@ var encodeBody = function({ body, headers = {} }) {
* @param {Parse.Cloud.HTTPOptions} options The Parse.Cloud.HTTPOptions object that makes the request. * @param {Parse.Cloud.HTTPOptions} options The Parse.Cloud.HTTPOptions object that makes the request.
* @return {Promise<Parse.Cloud.HTTPResponse>} A promise that will be resolved with a {@link Parse.Cloud.HTTPResponse} object when the request completes. * @return {Promise<Parse.Cloud.HTTPResponse>} A promise that will be resolved with a {@link Parse.Cloud.HTTPResponse} object when the request completes.
*/ */
module.exports = function(options) { module.exports = function httpRequest(options) {
var callbacks = { let url;
success: options.success, try {
error: options.error, url = new URL(options.url);
}; } catch (e) {
delete options.success; return Promise.reject(e);
delete options.error; }
delete options.uri; // not supported
options = Object.assign(options, encodeBody(options)); options = Object.assign(options, encodeBody(options));
// set follow redirects to false by default
options.followRedirect = options.followRedirects == true;
// support params options // support params options
if (typeof options.params === 'object') { if (typeof options.params === 'object') {
options.qs = options.params; options.qs = options.params;
} else if (typeof options.params === 'string') { } else if (typeof options.params === 'string') {
options.qs = querystring.parse(options.params); options.qs = querystring.parse(options.params);
} }
// force the response as a buffer const client = clients[url.protocol];
options.encoding = null; if (!client) {
return Promise.reject(`Unsupported protocol ${url.protocol}`);
}
const requestOptions = {
method: options.method,
port: Number(url.port),
path: url.pathname,
hostname: url.hostname,
headers: options.headers,
encoding: null,
followRedirects: options.followRedirects === true,
};
if (options.qs) {
requestOptions.path += `?${querystring.stringify(options.qs)}`;
}
if (options.agent) {
requestOptions.agent = options.agent;
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request(options, (error, response, body) => { const req = client.request(
if (error) { requestOptions,
if (callbacks.error) { makeCallback(resolve, reject, options)
callbacks.error(error); );
} if (options.body) {
return reject(error); req.write(options.body);
} }
const httpResponse = new HTTPResponse(response, body); req.end();
// Consider <200 && >= 400 as errors
if (httpResponse.status < 200 || httpResponse.status >= 400) {
if (callbacks.error) {
callbacks.error(httpResponse);
}
return reject(httpResponse);
} else {
if (callbacks.success) {
callbacks.success(httpResponse);
}
return resolve(httpResponse);
}
});
}); });
}; };

1
src/request.js Normal file
View File

@@ -0,0 +1 @@
module.exports = require('./cloud-code/httpRequest');

View File

@@ -1,8 +1,8 @@
# mongoUrl # mongoUrl
A fork of node's `url` module, with the modification that commas and colons are A fork of node's `url` module, with the modification that commas and colons are
allowed in hostnames. While this results in a slightly incorrect parsed result, allowed in hostnames. While this results in a slightly incorrect parsed result,
as the hostname field for a mongodb should be an array of replica sets, it's as the hostname field for a mongodb should be an array of replica sets, it's
good enough to let us pull out and escape the auth portion of the URL. good enough to let us pull out and escape the auth portion of the URL.
https://github.com/parse-community/parse-server/pull/986 https://github.com/parse-community/parse-server/pull/986