feat: add allowHeaders to Options (#6044)
* feat: add allowHeaders to Options This allows developers to use custom headers in their API requests, and they will be accepted by their mounted app. * refactor: convert allowCrossDomain to generator to add appId in scope This is necessary as the middleware may run in OPTIONS request that do not contain the appId within the header. * chore: update Definitions and docs * fix: update test to use new allowCrossDomain params * chore: add tests for allowCustomDomain middleware re: allowHeadrs
This commit is contained in:
committed by
Antonio Davi Macedo Coelho de Castro
parent
1361bb3020
commit
eef530b1e3
@@ -298,10 +298,62 @@ describe('middlewares', () => {
|
||||
headers[key] = value;
|
||||
},
|
||||
};
|
||||
middlewares.allowCrossDomain({}, res, () => {});
|
||||
const allowCrossDomain = middlewares.allowCrossDomain(
|
||||
fakeReq.body._ApplicationId
|
||||
);
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
expect(Object.keys(headers).length).toBe(4);
|
||||
expect(headers['Access-Control-Expose-Headers']).toBe(
|
||||
'X-Parse-Job-Status-Id, X-Parse-Push-Status-Id'
|
||||
);
|
||||
});
|
||||
|
||||
it('should set default Access-Control-Allow-Headers if allowHeaders are empty', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
allowHeaders: undefined,
|
||||
});
|
||||
const headers = {};
|
||||
const res = {
|
||||
header: (key, value) => {
|
||||
headers[key] = value;
|
||||
},
|
||||
};
|
||||
const allowCrossDomain = middlewares.allowCrossDomain(
|
||||
fakeReq.body._ApplicationId
|
||||
);
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
expect(headers['Access-Control-Allow-Headers']).toContain(
|
||||
middlewares.DEFAULT_ALLOWED_HEADERS
|
||||
);
|
||||
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
allowHeaders: [],
|
||||
});
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
expect(headers['Access-Control-Allow-Headers']).toContain(
|
||||
middlewares.DEFAULT_ALLOWED_HEADERS
|
||||
);
|
||||
});
|
||||
|
||||
it('should append custom headers to Access-Control-Allow-Headers if allowHeaders provided', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
allowHeaders: ['Header-1', 'Header-2'],
|
||||
});
|
||||
const headers = {};
|
||||
const res = {
|
||||
header: (key, value) => {
|
||||
headers[key] = value;
|
||||
},
|
||||
};
|
||||
const allowCrossDomain = middlewares.allowCrossDomain(
|
||||
fakeReq.body._ApplicationId
|
||||
);
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
expect(headers['Access-Control-Allow-Headers']).toContain(
|
||||
'Header-1, Header-2'
|
||||
);
|
||||
expect(headers['Access-Control-Allow-Headers']).toContain(
|
||||
middlewares.DEFAULT_ALLOWED_HEADERS
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,6 +73,7 @@ export class Config {
|
||||
masterKeyIps,
|
||||
masterKey,
|
||||
readOnlyMasterKey,
|
||||
allowHeaders,
|
||||
}) {
|
||||
if (masterKey === readOnlyMasterKey) {
|
||||
throw new Error('masterKey and readOnlyMasterKey should be different');
|
||||
@@ -110,6 +111,8 @@ export class Config {
|
||||
this.validateMasterKeyIps(masterKeyIps);
|
||||
|
||||
this.validateMaxLimit(maxLimit);
|
||||
|
||||
this.validateAllowHeaders(allowHeaders);
|
||||
}
|
||||
|
||||
static validateAccountLockoutPolicy(accountLockout) {
|
||||
@@ -254,6 +257,22 @@ export class Config {
|
||||
}
|
||||
}
|
||||
|
||||
static validateAllowHeaders(allowHeaders) {
|
||||
if (![null, undefined].includes(allowHeaders)) {
|
||||
if (Array.isArray(allowHeaders)) {
|
||||
allowHeaders.forEach(header => {
|
||||
if (typeof header !== 'string') {
|
||||
throw 'Allow headers must only contain strings';
|
||||
} else if (!header.trim().length) {
|
||||
throw 'Allow headers must not contain empty strings';
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw 'Allow headers must be an array';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generateEmailVerifyTokenExpiresAt() {
|
||||
if (!this.verifyUserEmails || !this.emailVerifyTokenValidityDuration) {
|
||||
return undefined;
|
||||
@@ -328,9 +347,7 @@ export class Config {
|
||||
}
|
||||
|
||||
get requestResetPasswordURL() {
|
||||
return `${this.publicServerURL}/apps/${
|
||||
this.applicationId
|
||||
}/request_password_reset`;
|
||||
return `${this.publicServerURL}/apps/${this.applicationId}/request_password_reset`;
|
||||
}
|
||||
|
||||
get passwordResetSuccessURL() {
|
||||
|
||||
@@ -17,6 +17,11 @@ module.exports.ParseServerOptions = {
|
||||
action: parsers.booleanParser,
|
||||
default: true,
|
||||
},
|
||||
allowHeaders: {
|
||||
env: 'PARSE_SERVER_ALLOW_HEADERS',
|
||||
help: 'Add headers to Access-Control-Allow-Headers',
|
||||
action: parsers.arrayParser,
|
||||
},
|
||||
analyticsAdapter: {
|
||||
env: 'PARSE_SERVER_ANALYTICS_ADAPTER',
|
||||
help: 'Adapter module for the analytics',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* @interface ParseServerOptions
|
||||
* @property {Any} accountLockout account lockout policy for failed login attempts
|
||||
* @property {Boolean} allowClientClassCreation Enable (or disable) client class creation, defaults to true
|
||||
* @property {String[]} allowHeaders Add headers to Access-Control-Allow-Headers
|
||||
* @property {Adapter<AnalyticsAdapter>} analyticsAdapter Adapter module for the analytics
|
||||
* @property {String} appId Your Parse Application ID
|
||||
* @property {String} appName Sets the app name
|
||||
|
||||
@@ -26,6 +26,8 @@ export interface ParseServerOptions {
|
||||
masterKeyIps: ?(string[]);
|
||||
/* Sets the app name */
|
||||
appName: ?string;
|
||||
/* Add headers to Access-Control-Allow-Headers */
|
||||
allowHeaders: ?(string[]);
|
||||
/* Adapter module for the analytics */
|
||||
analyticsAdapter: ?Adapter<AnalyticsAdapter>;
|
||||
/* Adapter module for the files sub-system */
|
||||
|
||||
@@ -145,7 +145,7 @@ class ParseServer {
|
||||
// It's the equivalent of https://api.parse.com/1 in the hosted Parse API.
|
||||
var api = express();
|
||||
//api.use("/apps", express.static(__dirname + "/public"));
|
||||
api.use(middlewares.allowCrossDomain);
|
||||
api.use(middlewares.allowCrossDomain(appId));
|
||||
// File handling needs to be before default middlewares are applied
|
||||
api.use(
|
||||
'/',
|
||||
|
||||
@@ -5,6 +5,15 @@ import Config from './Config';
|
||||
import ClientSDK from './ClientSDK';
|
||||
import defaultLogger from './logger';
|
||||
|
||||
export const DEFAULT_ALLOWED_HEADERS =
|
||||
'X-Parse-Master-Key, X-Parse-REST-API-Key, X-Parse-Javascript-Key, X-Parse-Application-Id, X-Parse-Client-Version, X-Parse-Session-Token, X-Requested-With, X-Parse-Revocable-Session, Content-Type, Pragma, Cache-Control';
|
||||
|
||||
const getMountForRequest = function(req) {
|
||||
const mountPathLength = req.originalUrl.length - req.url.length;
|
||||
const mountPath = req.originalUrl.slice(0, mountPathLength);
|
||||
return req.protocol + '://' + req.get('host') + mountPath;
|
||||
};
|
||||
|
||||
// Checks that the request is authorized for this app and checks user
|
||||
// auth too.
|
||||
// The bodyparser should run before this middleware.
|
||||
@@ -12,9 +21,7 @@ import defaultLogger from './logger';
|
||||
// req.config - the Config for this app
|
||||
// req.auth - the Auth for this request
|
||||
export function handleParseHeaders(req, res, next) {
|
||||
var mountPathLength = req.originalUrl.length - req.url.length;
|
||||
var mountPath = req.originalUrl.slice(0, mountPathLength);
|
||||
var mount = req.protocol + '://' + req.get('host') + mountPath;
|
||||
var mount = getMountForRequest(req);
|
||||
|
||||
var info = {
|
||||
appId: req.get('X-Parse-Application-Id'),
|
||||
@@ -279,23 +286,27 @@ function decodeBase64(str) {
|
||||
return Buffer.from(str, 'base64').toString();
|
||||
}
|
||||
|
||||
export function allowCrossDomain(req, res, next) {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
|
||||
res.header(
|
||||
'Access-Control-Allow-Headers',
|
||||
'X-Parse-Master-Key, X-Parse-REST-API-Key, X-Parse-Javascript-Key, X-Parse-Application-Id, X-Parse-Client-Version, X-Parse-Session-Token, X-Requested-With, X-Parse-Revocable-Session, Content-Type, Pragma, Cache-Control'
|
||||
);
|
||||
res.header(
|
||||
'Access-Control-Expose-Headers',
|
||||
'X-Parse-Job-Status-Id, X-Parse-Push-Status-Id'
|
||||
);
|
||||
// intercept OPTIONS method
|
||||
if ('OPTIONS' == req.method) {
|
||||
res.sendStatus(200);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
export function allowCrossDomain(appId) {
|
||||
return (req, res, next) => {
|
||||
const config = Config.get(appId, getMountForRequest(req));
|
||||
let allowHeaders = DEFAULT_ALLOWED_HEADERS;
|
||||
if (config && config.allowHeaders) {
|
||||
allowHeaders += `, ${config.allowHeaders.join(', ')}`;
|
||||
}
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
|
||||
res.header('Access-Control-Allow-Headers', allowHeaders);
|
||||
res.header(
|
||||
'Access-Control-Expose-Headers',
|
||||
'X-Parse-Job-Status-Id, X-Parse-Push-Status-Id'
|
||||
);
|
||||
// intercept OPTIONS method
|
||||
if ('OPTIONS' == req.method) {
|
||||
res.sendStatus(200);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function allowMethodOverride(req, res, next) {
|
||||
|
||||
Reference in New Issue
Block a user