Skip to content

Commit eef530b

Browse files
omairvaiyanidavimacedo
authored andcommitted
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
1 parent 1361bb3 commit eef530b

File tree

7 files changed

+113
-25
lines changed

7 files changed

+113
-25
lines changed

spec/Middlewares.spec.js

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,10 +298,62 @@ describe('middlewares', () => {
298298
headers[key] = value;
299299
},
300300
};
301-
middlewares.allowCrossDomain({}, res, () => {});
301+
const allowCrossDomain = middlewares.allowCrossDomain(
302+
fakeReq.body._ApplicationId
303+
);
304+
allowCrossDomain(fakeReq, res, () => {});
302305
expect(Object.keys(headers).length).toBe(4);
303306
expect(headers['Access-Control-Expose-Headers']).toBe(
304307
'X-Parse-Job-Status-Id, X-Parse-Push-Status-Id'
305308
);
306309
});
310+
311+
it('should set default Access-Control-Allow-Headers if allowHeaders are empty', () => {
312+
AppCache.put(fakeReq.body._ApplicationId, {
313+
allowHeaders: undefined,
314+
});
315+
const headers = {};
316+
const res = {
317+
header: (key, value) => {
318+
headers[key] = value;
319+
},
320+
};
321+
const allowCrossDomain = middlewares.allowCrossDomain(
322+
fakeReq.body._ApplicationId
323+
);
324+
allowCrossDomain(fakeReq, res, () => {});
325+
expect(headers['Access-Control-Allow-Headers']).toContain(
326+
middlewares.DEFAULT_ALLOWED_HEADERS
327+
);
328+
329+
AppCache.put(fakeReq.body._ApplicationId, {
330+
allowHeaders: [],
331+
});
332+
allowCrossDomain(fakeReq, res, () => {});
333+
expect(headers['Access-Control-Allow-Headers']).toContain(
334+
middlewares.DEFAULT_ALLOWED_HEADERS
335+
);
336+
});
337+
338+
it('should append custom headers to Access-Control-Allow-Headers if allowHeaders provided', () => {
339+
AppCache.put(fakeReq.body._ApplicationId, {
340+
allowHeaders: ['Header-1', 'Header-2'],
341+
});
342+
const headers = {};
343+
const res = {
344+
header: (key, value) => {
345+
headers[key] = value;
346+
},
347+
};
348+
const allowCrossDomain = middlewares.allowCrossDomain(
349+
fakeReq.body._ApplicationId
350+
);
351+
allowCrossDomain(fakeReq, res, () => {});
352+
expect(headers['Access-Control-Allow-Headers']).toContain(
353+
'Header-1, Header-2'
354+
);
355+
expect(headers['Access-Control-Allow-Headers']).toContain(
356+
middlewares.DEFAULT_ALLOWED_HEADERS
357+
);
358+
});
307359
});

src/Config.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export class Config {
7373
masterKeyIps,
7474
masterKey,
7575
readOnlyMasterKey,
76+
allowHeaders,
7677
}) {
7778
if (masterKey === readOnlyMasterKey) {
7879
throw new Error('masterKey and readOnlyMasterKey should be different');
@@ -110,6 +111,8 @@ export class Config {
110111
this.validateMasterKeyIps(masterKeyIps);
111112

112113
this.validateMaxLimit(maxLimit);
114+
115+
this.validateAllowHeaders(allowHeaders);
113116
}
114117

115118
static validateAccountLockoutPolicy(accountLockout) {
@@ -254,6 +257,22 @@ export class Config {
254257
}
255258
}
256259

260+
static validateAllowHeaders(allowHeaders) {
261+
if (![null, undefined].includes(allowHeaders)) {
262+
if (Array.isArray(allowHeaders)) {
263+
allowHeaders.forEach(header => {
264+
if (typeof header !== 'string') {
265+
throw 'Allow headers must only contain strings';
266+
} else if (!header.trim().length) {
267+
throw 'Allow headers must not contain empty strings';
268+
}
269+
});
270+
} else {
271+
throw 'Allow headers must be an array';
272+
}
273+
}
274+
}
275+
257276
generateEmailVerifyTokenExpiresAt() {
258277
if (!this.verifyUserEmails || !this.emailVerifyTokenValidityDuration) {
259278
return undefined;
@@ -328,9 +347,7 @@ export class Config {
328347
}
329348

330349
get requestResetPasswordURL() {
331-
return `${this.publicServerURL}/apps/${
332-
this.applicationId
333-
}/request_password_reset`;
350+
return `${this.publicServerURL}/apps/${this.applicationId}/request_password_reset`;
334351
}
335352

336353
get passwordResetSuccessURL() {

src/Options/Definitions.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ module.exports.ParseServerOptions = {
1717
action: parsers.booleanParser,
1818
default: true,
1919
},
20+
allowHeaders: {
21+
env: 'PARSE_SERVER_ALLOW_HEADERS',
22+
help: 'Add headers to Access-Control-Allow-Headers',
23+
action: parsers.arrayParser,
24+
},
2025
analyticsAdapter: {
2126
env: 'PARSE_SERVER_ANALYTICS_ADAPTER',
2227
help: 'Adapter module for the analytics',

src/Options/docs.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* @interface ParseServerOptions
33
* @property {Any} accountLockout account lockout policy for failed login attempts
44
* @property {Boolean} allowClientClassCreation Enable (or disable) client class creation, defaults to true
5+
* @property {String[]} allowHeaders Add headers to Access-Control-Allow-Headers
56
* @property {Adapter<AnalyticsAdapter>} analyticsAdapter Adapter module for the analytics
67
* @property {String} appId Your Parse Application ID
78
* @property {String} appName Sets the app name

src/Options/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export interface ParseServerOptions {
2626
masterKeyIps: ?(string[]);
2727
/* Sets the app name */
2828
appName: ?string;
29+
/* Add headers to Access-Control-Allow-Headers */
30+
allowHeaders: ?(string[]);
2931
/* Adapter module for the analytics */
3032
analyticsAdapter: ?Adapter<AnalyticsAdapter>;
3133
/* Adapter module for the files sub-system */

src/ParseServer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ class ParseServer {
145145
// It's the equivalent of https://api.parse.com/1 in the hosted Parse API.
146146
var api = express();
147147
//api.use("/apps", express.static(__dirname + "/public"));
148-
api.use(middlewares.allowCrossDomain);
148+
api.use(middlewares.allowCrossDomain(appId));
149149
// File handling needs to be before default middlewares are applied
150150
api.use(
151151
'/',

src/middlewares.js

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,23 @@ import Config from './Config';
55
import ClientSDK from './ClientSDK';
66
import defaultLogger from './logger';
77

8+
export const DEFAULT_ALLOWED_HEADERS =
9+
'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';
10+
11+
const getMountForRequest = function(req) {
12+
const mountPathLength = req.originalUrl.length - req.url.length;
13+
const mountPath = req.originalUrl.slice(0, mountPathLength);
14+
return req.protocol + '://' + req.get('host') + mountPath;
15+
};
16+
817
// Checks that the request is authorized for this app and checks user
918
// auth too.
1019
// The bodyparser should run before this middleware.
1120
// Adds info to the request:
1221
// req.config - the Config for this app
1322
// req.auth - the Auth for this request
1423
export function handleParseHeaders(req, res, next) {
15-
var mountPathLength = req.originalUrl.length - req.url.length;
16-
var mountPath = req.originalUrl.slice(0, mountPathLength);
17-
var mount = req.protocol + '://' + req.get('host') + mountPath;
24+
var mount = getMountForRequest(req);
1825

1926
var info = {
2027
appId: req.get('X-Parse-Application-Id'),
@@ -279,23 +286,27 @@ function decodeBase64(str) {
279286
return Buffer.from(str, 'base64').toString();
280287
}
281288

282-
export function allowCrossDomain(req, res, next) {
283-
res.header('Access-Control-Allow-Origin', '*');
284-
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
285-
res.header(
286-
'Access-Control-Allow-Headers',
287-
'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'
288-
);
289-
res.header(
290-
'Access-Control-Expose-Headers',
291-
'X-Parse-Job-Status-Id, X-Parse-Push-Status-Id'
292-
);
293-
// intercept OPTIONS method
294-
if ('OPTIONS' == req.method) {
295-
res.sendStatus(200);
296-
} else {
297-
next();
298-
}
289+
export function allowCrossDomain(appId) {
290+
return (req, res, next) => {
291+
const config = Config.get(appId, getMountForRequest(req));
292+
let allowHeaders = DEFAULT_ALLOWED_HEADERS;
293+
if (config && config.allowHeaders) {
294+
allowHeaders += `, ${config.allowHeaders.join(', ')}`;
295+
}
296+
res.header('Access-Control-Allow-Origin', '*');
297+
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
298+
res.header('Access-Control-Allow-Headers', allowHeaders);
299+
res.header(
300+
'Access-Control-Expose-Headers',
301+
'X-Parse-Job-Status-Id, X-Parse-Push-Status-Id'
302+
);
303+
// intercept OPTIONS method
304+
if ('OPTIONS' == req.method) {
305+
res.sendStatus(200);
306+
} else {
307+
next();
308+
}
309+
};
299310
}
300311

301312
export function allowMethodOverride(req, res, next) {

0 commit comments

Comments
 (0)