Skip to content

Commit cb7b549

Browse files
authored
Direct Access to parse-server (#2316)
* Adds ParseServerRESTController experimental support * Adds basic tests * Do not create sessionToken when requests come from cloudCode #1495
1 parent ccf2b14 commit cb7b549

6 files changed

+292
-56
lines changed
+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
const ParseServerRESTController = require('../src/ParseServerRESTController').ParseServerRESTController;
2+
const ParseServer = require('../src/ParseServer').default;
3+
let RESTController;
4+
5+
describe('ParseServerRESTController', () => {
6+
7+
beforeEach(() => {
8+
RESTController = ParseServerRESTController(Parse.applicationId, ParseServer.promiseRouter({appId: Parse.applicationId}));
9+
})
10+
11+
it('should handle a get request', (done) => {
12+
RESTController.request("GET", "/classes/MyObject").then((res) => {
13+
expect(res.results.length).toBe(0);
14+
done();
15+
}, (err) => {
16+
console.log(err);
17+
jfail(err);
18+
done();
19+
});
20+
});
21+
22+
it('should handle a get request with full serverURL mount path', (done) => {
23+
RESTController.request("GET", "/1/classes/MyObject").then((res) => {
24+
expect(res.results.length).toBe(0);
25+
done();
26+
}, (err) => {
27+
jfail(err);
28+
done();
29+
});
30+
});
31+
32+
it('should handle a POST batch', (done) => {
33+
RESTController.request("POST", "batch", {
34+
requests: [
35+
{
36+
method: 'GET',
37+
path: '/classes/MyObject'
38+
},
39+
{
40+
method: 'POST',
41+
path: '/classes/MyObject',
42+
body: {"key": "value"}
43+
},
44+
{
45+
method: 'GET',
46+
path: '/classes/MyObject'
47+
}
48+
]
49+
}).then((res) => {
50+
expect(res.length).toBe(3);
51+
done();
52+
}, (err) => {
53+
jfail(err);
54+
done();
55+
});
56+
});
57+
58+
it('should handle a POST request', (done) => {
59+
RESTController.request("POST", "/classes/MyObject", {"key": "value"}).then((res) => {
60+
return RESTController.request("GET", "/classes/MyObject");
61+
}).then((res) => {
62+
expect(res.results.length).toBe(1);
63+
expect(res.results[0].key).toEqual("value");
64+
done();
65+
}).fail((err) => {
66+
console.log(err);
67+
jfail(err);
68+
done();
69+
});
70+
});
71+
72+
it('ensures sessionTokens are properly handled', (done) => {
73+
let userId;
74+
Parse.User.signUp('user', 'pass').then((user) => {
75+
userId = user.id;
76+
let sessionToken = user.getSessionToken();
77+
return RESTController.request("GET", "/users/me", undefined, {sessionToken});
78+
}).then((res) => {
79+
// Result is in JSON format
80+
expect(res.objectId).toEqual(userId);
81+
done();
82+
}).fail((err) => {
83+
console.log(err);
84+
jfail(err);
85+
done();
86+
});
87+
});
88+
89+
it('ensures masterKey is properly handled', (done) => {
90+
let userId;
91+
Parse.User.signUp('user', 'pass').then((user) => {
92+
userId = user.id;
93+
let sessionToken = user.getSessionToken();
94+
return Parse.User.logOut().then(() => {
95+
return RESTController.request("GET", "/classes/_User", undefined, {useMasterKey: true});
96+
});
97+
}).then((res) => {
98+
expect(res.results.length).toBe(1);
99+
expect(res.results[0].objectId).toEqual(userId);
100+
done();
101+
}, (err) => {
102+
jfail(err);
103+
done();
104+
});
105+
});
106+
107+
it('ensures no session token is created on creating users', (done) => {
108+
RESTController.request("POST", "/classes/_User", {username: "hello", password: "world"}).then(() => {
109+
let query = new Parse.Query('_Session');
110+
return query.find({useMasterKey: true});
111+
}).then(sessions => {
112+
expect(sessions.length).toBe(0);
113+
done();
114+
}, (err) => {
115+
jfail(err);
116+
done();
117+
});
118+
});
119+
});

src/ParseServer.js

+26-17
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ import DatabaseController from './Controllers/DatabaseController';
5858
import SchemaCache from './Controllers/SchemaCache';
5959
import ParsePushAdapter from 'parse-server-push-adapter';
6060
import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter';
61+
62+
import { ParseServerRESTController } from './ParseServerRESTController';
6163
// Mutate the Parse object to add the Cloud Code handlers
6264
addParseCloud();
6365

@@ -273,6 +275,29 @@ class ParseServer {
273275
api.use(bodyParser.json({ 'type': '*/*' , limit: maxUploadSize }));
274276
api.use(middlewares.allowMethodOverride);
275277

278+
let appRouter = ParseServer.promiseRouter({ appId });
279+
api.use(appRouter.expressRouter());
280+
281+
api.use(middlewares.handleParseErrors);
282+
283+
//This causes tests to spew some useless warnings, so disable in test
284+
if (!process.env.TESTING) {
285+
process.on('uncaughtException', (err) => {
286+
if ( err.code === "EADDRINUSE" ) { // user-friendly message for this common error
287+
console.error(`Unable to listen on port ${err.port}. The port is already in use.`);
288+
process.exit(0);
289+
} else {
290+
throw err;
291+
}
292+
});
293+
}
294+
if (process.env.PARSE_SERVER_ENABLE_EXPERIMENTAL_DIRECT_ACCESS === '1') {
295+
Parse.CoreManager.setRESTController(ParseServerRESTController(appId, appRouter));
296+
}
297+
return api;
298+
}
299+
300+
static promiseRouter({appId}) {
276301
let routers = [
277302
new ClassesRouter(),
278303
new UsersRouter(),
@@ -301,23 +326,7 @@ class ParseServer {
301326
appRouter.use(middlewares.handleParseHeaders);
302327

303328
batch.mountOnto(appRouter);
304-
305-
api.use(appRouter.expressRouter());
306-
307-
api.use(middlewares.handleParseErrors);
308-
309-
//This causes tests to spew some useless warnings, so disable in test
310-
if (!process.env.TESTING) {
311-
process.on('uncaughtException', (err) => {
312-
if ( err.code === "EADDRINUSE" ) { // user-friendly message for this common error
313-
console.error(`Unable to listen on port ${err.port}. The port is already in use.`);
314-
process.exit(0);
315-
} else {
316-
throw err;
317-
}
318-
});
319-
}
320-
return api;
329+
return appRouter;
321330
}
322331

323332
static createLiveQueryServer(httpServer, config) {

src/ParseServerRESTController.js

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
const Config = require('./Config');
2+
const Auth = require('./Auth');
3+
const RESTController = require('parse/lib/node/RESTController');
4+
const URL = require('url');
5+
const Parse = require('parse/node');
6+
7+
function getSessionToken(options) {
8+
if (options && typeof options.sessionToken === 'string') {
9+
return Parse.Promise.as(options.sessionToken);
10+
}
11+
return Parse.Promise.as(null);
12+
}
13+
14+
function getAuth(options, config) {
15+
if (options.useMasterKey) {
16+
return Parse.Promise.as(new Auth.Auth({config, isMaster: true, installationId: 'cloud' }));
17+
}
18+
return getSessionToken(options).then((sessionToken) => {
19+
if (sessionToken) {
20+
options.sessionToken = sessionToken;
21+
return Auth.getAuthForSessionToken({
22+
config,
23+
sessionToken: sessionToken,
24+
installationId: 'cloud'
25+
});
26+
} else {
27+
return Parse.Promise.as(new Auth.Auth({ config, installationId: 'cloud' }));
28+
}
29+
})
30+
}
31+
32+
function ParseServerRESTController(applicationId, router) {
33+
function handleRequest(method, path, data = {}, options = {}) {
34+
// Store the arguments, for later use if internal fails
35+
let args = arguments;
36+
37+
let config = new Config(applicationId);
38+
let serverURL = URL.parse(config.serverURL);
39+
if (path.indexOf(serverURL.path) === 0) {
40+
path = path.slice(serverURL.path.length, path.length);
41+
}
42+
43+
if (path[0] !== "/") {
44+
path = "/" + path;
45+
}
46+
47+
if (path === '/batch') {
48+
let promises = data.requests.map((request) => {
49+
return handleRequest(request.method, request.path, request.body, options).then((response) => {
50+
return Parse.Promise.as({success: response});
51+
}, (error) => {
52+
return Parse.Promise.as({error: {code: error.code, error: error.message}});
53+
});
54+
});
55+
return Parse.Promise.all(promises);
56+
}
57+
58+
let query;
59+
if (method === 'GET') {
60+
query = data;
61+
}
62+
63+
return new Parse.Promise((resolve, reject) => {
64+
getAuth(options, config).then((auth) => {
65+
let request = {
66+
body: data,
67+
config,
68+
auth,
69+
info: {
70+
applicationId: applicationId,
71+
sessionToken: options.sessionToken
72+
},
73+
query
74+
};
75+
return Promise.resolve().then(() => {
76+
return router.tryRouteRequest(method, path, request);
77+
}).then((response) => {
78+
resolve(response.response, response.status, response);
79+
}, (err) => {
80+
if (err instanceof Parse.Error &&
81+
err.code == Parse.Error.INVALID_JSON &&
82+
err.message == `cannot route ${method} ${path}`) {
83+
RESTController.request.apply(null, args).then(resolve, reject);
84+
} else {
85+
reject(err);
86+
}
87+
});
88+
}, reject);
89+
});
90+
};
91+
92+
return {
93+
request: handleRequest,
94+
ajax: RESTController.ajax
95+
};
96+
};
97+
98+
export default ParseServerRESTController;
99+
export { ParseServerRESTController };

src/PromiseRouter.js

+39-24
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,22 @@ import express from 'express';
1010
import url from 'url';
1111
import log from './logger';
1212
import {inspect} from 'util';
13+
const Layer = require('express/lib/router/layer');
14+
15+
function validateParameter(key, value) {
16+
if (key == 'className') {
17+
if (value.match(/_?[A-Za-z][A-Za-z_0-9]*/)) {
18+
return value;
19+
}
20+
} else if (key == 'objectId') {
21+
if (value.match(/[A-Za-z0-9]+/)) {
22+
return value;
23+
}
24+
} else {
25+
return value;
26+
}
27+
}
28+
1329

1430
export default class PromiseRouter {
1531
// Each entry should be an object with:
@@ -70,7 +86,8 @@ export default class PromiseRouter {
7086
this.routes.push({
7187
path: path,
7288
method: method,
73-
handler: handler
89+
handler: handler,
90+
layer: new Layer(path, null, handler)
7491
});
7592
};
7693

@@ -83,30 +100,15 @@ export default class PromiseRouter {
83100
if (route.method != method) {
84101
continue;
85102
}
86-
// NOTE: we can only route the specific wildcards :className and
87-
// :objectId, and in that order.
88-
// This is pretty hacky but I don't want to rebuild the entire
89-
// express route matcher. Maybe there's a way to reuse its logic.
90-
var pattern = '^' + route.path + '$';
91-
92-
pattern = pattern.replace(':className',
93-
'(_?[A-Za-z][A-Za-z_0-9]*)');
94-
pattern = pattern.replace(':objectId',
95-
'([A-Za-z0-9]+)');
96-
var re = new RegExp(pattern);
97-
var m = path.match(re);
98-
if (!m) {
99-
continue;
100-
}
101-
var params = {};
102-
if (m[1]) {
103-
params.className = m[1];
104-
}
105-
if (m[2]) {
106-
params.objectId = m[2];
103+
let layer = route.layer || new Layer(route.path, null, route.handler);
104+
let match = layer.match(path);
105+
if (match) {
106+
let params = layer.params;
107+
Object.keys(params).forEach((key) => {
108+
params[key] = validateParameter(key, params[key]);
109+
});
110+
return {params: params, handler: route.handler};
107111
}
108-
109-
return {params: params, handler: route.handler};
110112
}
111113
};
112114

@@ -124,6 +126,19 @@ export default class PromiseRouter {
124126
expressRouter() {
125127
return this.mountOnto(express.Router());
126128
}
129+
130+
tryRouteRequest(method, path, request) {
131+
var match = this.match(method, path);
132+
if (!match) {
133+
throw new Parse.Error(
134+
Parse.Error.INVALID_JSON,
135+
'cannot route ' + method + ' ' + path);
136+
}
137+
request.params = match.params;
138+
return new Promise((resolve, reject) => {
139+
match.handler(request).then(resolve, reject);
140+
});
141+
}
127142
}
128143

129144
// A helper function to make an express handler out of a a promise

src/RestWrite.js

+5
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,11 @@ RestWrite.prototype.createSessionTokenIfNeeded = function() {
436436
}
437437

438438
RestWrite.prototype.createSessionToken = function() {
439+
// cloud installationId from Cloud Code,
440+
// never create session tokens from there.
441+
if (this.auth.installationId && this.auth.installationId === 'cloud') {
442+
return;
443+
}
439444
var token = 'r:' + cryptoUtils.newToken();
440445

441446
var expiresAt = this.config.generateSessionExpiresAt();

0 commit comments

Comments
 (0)