Skip to content

feat: add allowHeaders to Options #6044

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Sep 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion spec/Middlewares.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
});
});
23 changes: 20 additions & 3 deletions src/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export class Config {
masterKeyIps,
masterKey,
readOnlyMasterKey,
allowHeaders,
}) {
if (masterKey === readOnlyMasterKey) {
throw new Error('masterKey and readOnlyMasterKey should be different');
Expand Down Expand Up @@ -110,6 +111,8 @@ export class Config {
this.validateMasterKeyIps(masterKeyIps);

this.validateMaxLimit(maxLimit);

this.validateAllowHeaders(allowHeaders);
}

static validateAccountLockoutPolicy(accountLockout) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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`;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what happened here - maybe the pre-commit ran a formatter through it!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it’s prettier

}

get passwordResetSuccessURL() {
Expand Down
5 changes: 5 additions & 0 deletions src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/Options/docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/Options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
2 changes: 1 addition & 1 deletion src/ParseServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
'/',
Expand Down
51 changes: 31 additions & 20 deletions src/middlewares.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,23 @@ 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.
// Adds info to the request:
// 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'),
Expand Down Expand Up @@ -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) {
Expand Down