Skip to content
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

Direct Access to parse-server #2316

Merged
merged 3 commits into from
Sep 9, 2016
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
119 changes: 119 additions & 0 deletions spec/ParseServerRESTController.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
const ParseServerRESTController = require('../src/ParseServerRESTController').ParseServerRESTController;
const ParseServer = require('../src/ParseServer').default;
let RESTController;

describe('ParseServerRESTController', () => {

beforeEach(() => {
RESTController = ParseServerRESTController(Parse.applicationId, ParseServer.promiseRouter({appId: Parse.applicationId}));
})

it('should handle a get request', (done) => {
RESTController.request("GET", "/classes/MyObject").then((res) => {
expect(res.results.length).toBe(0);
done();
}, (err) => {
console.log(err);
jfail(err);
done();
});
});

it('should handle a get request with full serverURL mount path', (done) => {
RESTController.request("GET", "/1/classes/MyObject").then((res) => {
expect(res.results.length).toBe(0);
done();
}, (err) => {
jfail(err);
done();
});
});

it('should handle a POST batch', (done) => {
RESTController.request("POST", "batch", {
requests: [
{
method: 'GET',
path: '/classes/MyObject'
},
{
method: 'POST',
path: '/classes/MyObject',
body: {"key": "value"}
},
{
method: 'GET',
path: '/classes/MyObject'
}
]
}).then((res) => {
expect(res.length).toBe(3);
done();
}, (err) => {
jfail(err);
done();
});
});

it('should handle a POST request', (done) => {
RESTController.request("POST", "/classes/MyObject", {"key": "value"}).then((res) => {
return RESTController.request("GET", "/classes/MyObject");
}).then((res) => {
expect(res.results.length).toBe(1);
expect(res.results[0].key).toEqual("value");
done();
}).fail((err) => {
console.log(err);
jfail(err);
done();
});
});

it('ensures sessionTokens are properly handled', (done) => {
let userId;
Parse.User.signUp('user', 'pass').then((user) => {
userId = user.id;
let sessionToken = user.getSessionToken();
return RESTController.request("GET", "/users/me", undefined, {sessionToken});
}).then((res) => {
// Result is in JSON format
expect(res.objectId).toEqual(userId);
done();
}).fail((err) => {
console.log(err);
jfail(err);
done();
});
});

it('ensures masterKey is properly handled', (done) => {
let userId;
Parse.User.signUp('user', 'pass').then((user) => {
userId = user.id;
let sessionToken = user.getSessionToken();
return Parse.User.logOut().then(() => {
return RESTController.request("GET", "/classes/_User", undefined, {useMasterKey: true});
});
}).then((res) => {
expect(res.results.length).toBe(1);
expect(res.results[0].objectId).toEqual(userId);
done();
}, (err) => {
jfail(err);
done();
});
});

it('ensures no session token is created on creating users', (done) => {
RESTController.request("POST", "/classes/_User", {username: "hello", password: "world"}).then(() => {
let query = new Parse.Query('_Session');
return query.find({useMasterKey: true});
}).then(sessions => {
expect(sessions.length).toBe(0);
done();
}, (err) => {
jfail(err);
done();
});
});
});
43 changes: 26 additions & 17 deletions src/ParseServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ import DatabaseController from './Controllers/DatabaseController';
import SchemaCache from './Controllers/SchemaCache';
import ParsePushAdapter from 'parse-server-push-adapter';
import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter';

import { ParseServerRESTController } from './ParseServerRESTController';
// Mutate the Parse object to add the Cloud Code handlers
addParseCloud();

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

let appRouter = ParseServer.promiseRouter({ appId });
api.use(appRouter.expressRouter());

api.use(middlewares.handleParseErrors);

//This causes tests to spew some useless warnings, so disable in test
if (!process.env.TESTING) {
process.on('uncaughtException', (err) => {
if ( err.code === "EADDRINUSE" ) { // user-friendly message for this common error
console.error(`Unable to listen on port ${err.port}. The port is already in use.`);
process.exit(0);
} else {
throw err;
}
});
}
if (process.env.PARSE_SERVER_ENABLE_EXPERIMENTAL_DIRECT_ACCESS === '1') {
Parse.CoreManager.setRESTController(ParseServerRESTController(appId, appRouter));
}
return api;
}

static promiseRouter({appId}) {
let routers = [
new ClassesRouter(),
new UsersRouter(),
Expand Down Expand Up @@ -301,23 +326,7 @@ class ParseServer {
appRouter.use(middlewares.handleParseHeaders);

batch.mountOnto(appRouter);

api.use(appRouter.expressRouter());

api.use(middlewares.handleParseErrors);

//This causes tests to spew some useless warnings, so disable in test
if (!process.env.TESTING) {
process.on('uncaughtException', (err) => {
if ( err.code === "EADDRINUSE" ) { // user-friendly message for this common error
console.error(`Unable to listen on port ${err.port}. The port is already in use.`);
process.exit(0);
} else {
throw err;
}
});
}
return api;
return appRouter;
}

static createLiveQueryServer(httpServer, config) {
Expand Down
99 changes: 99 additions & 0 deletions src/ParseServerRESTController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
const Config = require('./Config');
const Auth = require('./Auth');
const RESTController = require('parse/lib/node/RESTController');
const URL = require('url');
const Parse = require('parse/node');

function getSessionToken(options) {
if (options && typeof options.sessionToken === 'string') {
return Parse.Promise.as(options.sessionToken);
}
return Parse.Promise.as(null);
}

function getAuth(options, config) {
if (options.useMasterKey) {
return Parse.Promise.as(new Auth.Auth({config, isMaster: true, installationId: 'cloud' }));
}
return getSessionToken(options).then((sessionToken) => {
if (sessionToken) {
options.sessionToken = sessionToken;
return Auth.getAuthForSessionToken({
config,
sessionToken: sessionToken,
installationId: 'cloud'
});
} else {
return Parse.Promise.as(new Auth.Auth({ config, installationId: 'cloud' }));
}
})
}

function ParseServerRESTController(applicationId, router) {
function handleRequest(method, path, data = {}, options = {}) {
// Store the arguments, for later use if internal fails
let args = arguments;

let config = new Config(applicationId);
let serverURL = URL.parse(config.serverURL);
if (path.indexOf(serverURL.path) === 0) {
path = path.slice(serverURL.path.length, path.length);
}

if (path[0] !== "/") {
path = "/" + path;
}

if (path === '/batch') {
let promises = data.requests.map((request) => {
return handleRequest(request.method, request.path, request.body, options).then((response) => {
return Parse.Promise.as({success: response});
}, (error) => {
return Parse.Promise.as({error: {code: error.code, error: error.message}});
});
});
return Parse.Promise.all(promises);
}

let query;
if (method === 'GET') {
query = data;
}

return new Parse.Promise((resolve, reject) => {
getAuth(options, config).then((auth) => {
let request = {
body: data,
config,
auth,
info: {
applicationId: applicationId,
sessionToken: options.sessionToken
},
query
};
return Promise.resolve().then(() => {
return router.tryRouteRequest(method, path, request);
}).then((response) => {
resolve(response.response, response.status, response);
}, (err) => {
if (err instanceof Parse.Error &&
err.code == Parse.Error.INVALID_JSON &&
err.message == `cannot route ${method} ${path}`) {
RESTController.request.apply(null, args).then(resolve, reject);
} else {
reject(err);
}
});
}, reject);
});
};

return {
request: handleRequest,
ajax: RESTController.ajax
};
};

export default ParseServerRESTController;
export { ParseServerRESTController };
63 changes: 39 additions & 24 deletions src/PromiseRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@ import express from 'express';
import url from 'url';
import log from './logger';
import {inspect} from 'util';
const Layer = require('express/lib/router/layer');

function validateParameter(key, value) {
if (key == 'className') {
if (value.match(/_?[A-Za-z][A-Za-z_0-9]*/)) {
return value;
}
} else if (key == 'objectId') {
if (value.match(/[A-Za-z0-9]+/)) {
return value;
}
} else {
return value;
}
}


export default class PromiseRouter {
// Each entry should be an object with:
Expand Down Expand Up @@ -70,7 +86,8 @@ export default class PromiseRouter {
this.routes.push({
path: path,
method: method,
handler: handler
handler: handler,
layer: new Layer(path, null, handler)
});
};

Expand All @@ -83,30 +100,15 @@ export default class PromiseRouter {
if (route.method != method) {
continue;
}
// NOTE: we can only route the specific wildcards :className and
// :objectId, and in that order.
// This is pretty hacky but I don't want to rebuild the entire
// express route matcher. Maybe there's a way to reuse its logic.
var pattern = '^' + route.path + '$';

pattern = pattern.replace(':className',
'(_?[A-Za-z][A-Za-z_0-9]*)');
pattern = pattern.replace(':objectId',
'([A-Za-z0-9]+)');
var re = new RegExp(pattern);
var m = path.match(re);
if (!m) {
continue;
}
var params = {};
if (m[1]) {
params.className = m[1];
}
if (m[2]) {
params.objectId = m[2];
let layer = route.layer || new Layer(route.path, null, route.handler);
let match = layer.match(path);
if (match) {
let params = layer.params;
Object.keys(params).forEach((key) => {
params[key] = validateParameter(key, params[key]);
});
return {params: params, handler: route.handler};
}

return {params: params, handler: route.handler};
}
};

Expand All @@ -124,6 +126,19 @@ export default class PromiseRouter {
expressRouter() {
return this.mountOnto(express.Router());
}

tryRouteRequest(method, path, request) {
var match = this.match(method, path);
if (!match) {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
'cannot route ' + method + ' ' + path);
}
request.params = match.params;
return new Promise((resolve, reject) => {
match.handler(request).then(resolve, reject);
});
}
}

// A helper function to make an express handler out of a a promise
Expand Down
5 changes: 5 additions & 0 deletions src/RestWrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,11 @@ RestWrite.prototype.createSessionTokenIfNeeded = function() {
}

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

var expiresAt = this.config.generateSessionExpiresAt();
Expand Down
Loading