Direct Access to parse-server (#2316)
* Adds ParseServerRESTController experimental support * Adds basic tests * Do not create sessionToken when requests come from cloudCode #1495
This commit is contained in:
119
spec/ParseServerRESTController.spec.js
Normal file
119
spec/ParseServerRESTController.spec.js
Normal file
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -58,6 +58,8 @@ import DatabaseController from './Controllers/DatabaseController';
|
|||||||
import SchemaCache from './Controllers/SchemaCache';
|
import SchemaCache from './Controllers/SchemaCache';
|
||||||
import ParsePushAdapter from 'parse-server-push-adapter';
|
import ParsePushAdapter from 'parse-server-push-adapter';
|
||||||
import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter';
|
import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter';
|
||||||
|
|
||||||
|
import { ParseServerRESTController } from './ParseServerRESTController';
|
||||||
// Mutate the Parse object to add the Cloud Code handlers
|
// Mutate the Parse object to add the Cloud Code handlers
|
||||||
addParseCloud();
|
addParseCloud();
|
||||||
|
|
||||||
@@ -273,6 +275,29 @@ class ParseServer {
|
|||||||
api.use(bodyParser.json({ 'type': '*/*' , limit: maxUploadSize }));
|
api.use(bodyParser.json({ 'type': '*/*' , limit: maxUploadSize }));
|
||||||
api.use(middlewares.allowMethodOverride);
|
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 = [
|
let routers = [
|
||||||
new ClassesRouter(),
|
new ClassesRouter(),
|
||||||
new UsersRouter(),
|
new UsersRouter(),
|
||||||
@@ -301,23 +326,7 @@ class ParseServer {
|
|||||||
appRouter.use(middlewares.handleParseHeaders);
|
appRouter.use(middlewares.handleParseHeaders);
|
||||||
|
|
||||||
batch.mountOnto(appRouter);
|
batch.mountOnto(appRouter);
|
||||||
|
return 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static createLiveQueryServer(httpServer, config) {
|
static createLiveQueryServer(httpServer, config) {
|
||||||
|
|||||||
99
src/ParseServerRESTController.js
Normal file
99
src/ParseServerRESTController.js
Normal file
@@ -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 };
|
||||||
@@ -10,6 +10,22 @@ import express from 'express';
|
|||||||
import url from 'url';
|
import url from 'url';
|
||||||
import log from './logger';
|
import log from './logger';
|
||||||
import {inspect} from 'util';
|
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 {
|
export default class PromiseRouter {
|
||||||
// Each entry should be an object with:
|
// Each entry should be an object with:
|
||||||
@@ -70,7 +86,8 @@ export default class PromiseRouter {
|
|||||||
this.routes.push({
|
this.routes.push({
|
||||||
path: path,
|
path: path,
|
||||||
method: method,
|
method: method,
|
||||||
handler: handler
|
handler: handler,
|
||||||
|
layer: new Layer(path, null, handler)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,31 +100,16 @@ export default class PromiseRouter {
|
|||||||
if (route.method != method) {
|
if (route.method != method) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// NOTE: we can only route the specific wildcards :className and
|
let layer = route.layer || new Layer(route.path, null, route.handler);
|
||||||
// :objectId, and in that order.
|
let match = layer.match(path);
|
||||||
// This is pretty hacky but I don't want to rebuild the entire
|
if (match) {
|
||||||
// express route matcher. Maybe there's a way to reuse its logic.
|
let params = layer.params;
|
||||||
var pattern = '^' + route.path + '$';
|
Object.keys(params).forEach((key) => {
|
||||||
|
params[key] = validateParameter(key, params[key]);
|
||||||
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];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {params: params, handler: route.handler};
|
return {params: params, handler: route.handler};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mount the routes on this router onto an express app (or express router)
|
// Mount the routes on this router onto an express app (or express router)
|
||||||
@@ -124,6 +126,19 @@ export default class PromiseRouter {
|
|||||||
expressRouter() {
|
expressRouter() {
|
||||||
return this.mountOnto(express.Router());
|
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
|
// A helper function to make an express handler out of a a promise
|
||||||
|
|||||||
@@ -436,6 +436,11 @@ RestWrite.prototype.createSessionTokenIfNeeded = function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RestWrite.prototype.createSessionToken = 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 token = 'r:' + cryptoUtils.newToken();
|
||||||
|
|
||||||
var expiresAt = this.config.generateSessionExpiresAt();
|
var expiresAt = this.config.generateSessionExpiresAt();
|
||||||
|
|||||||
19
src/batch.js
19
src/batch.js
@@ -29,8 +29,7 @@ function handleBatch(router, req) {
|
|||||||
var apiPrefixLength = req.originalUrl.length - batchPath.length;
|
var apiPrefixLength = req.originalUrl.length - batchPath.length;
|
||||||
var apiPrefix = req.originalUrl.slice(0, apiPrefixLength);
|
var apiPrefix = req.originalUrl.slice(0, apiPrefixLength);
|
||||||
|
|
||||||
var promises = [];
|
const promises = req.body.requests.map((restRequest) => {
|
||||||
for (var restRequest of req.body.requests) {
|
|
||||||
// The routablePath is the path minus the api prefix
|
// The routablePath is the path minus the api prefix
|
||||||
if (restRequest.path.slice(0, apiPrefixLength) != apiPrefix) {
|
if (restRequest.path.slice(0, apiPrefixLength) != apiPrefix) {
|
||||||
throw new Parse.Error(
|
throw new Parse.Error(
|
||||||
@@ -38,30 +37,20 @@ function handleBatch(router, req) {
|
|||||||
'cannot route batch path ' + restRequest.path);
|
'cannot route batch path ' + restRequest.path);
|
||||||
}
|
}
|
||||||
var routablePath = restRequest.path.slice(apiPrefixLength);
|
var routablePath = restRequest.path.slice(apiPrefixLength);
|
||||||
|
|
||||||
// Use the router to figure out what handler to use
|
|
||||||
var match = router.match(restRequest.method, routablePath);
|
|
||||||
if (!match) {
|
|
||||||
throw new Parse.Error(
|
|
||||||
Parse.Error.INVALID_JSON,
|
|
||||||
'cannot route ' + restRequest.method + ' ' + routablePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct a request that we can send to a handler
|
// Construct a request that we can send to a handler
|
||||||
var request = {
|
var request = {
|
||||||
body: restRequest.body,
|
body: restRequest.body,
|
||||||
params: match.params,
|
|
||||||
config: req.config,
|
config: req.config,
|
||||||
auth: req.auth,
|
auth: req.auth,
|
||||||
info: req.info
|
info: req.info
|
||||||
};
|
};
|
||||||
|
|
||||||
promises.push(match.handler(request).then((response) => {
|
return router.tryRouteRequest(restRequest.method, routablePath, request).then((response) => {
|
||||||
return {success: response.response};
|
return {success: response.response};
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
return {error: {code: error.code, error: error.message}};
|
return {error: {code: error.code, error: error.message}};
|
||||||
}));
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
return Promise.all(promises).then((results) => {
|
return Promise.all(promises).then((results) => {
|
||||||
return {response: results};
|
return {response: results};
|
||||||
|
|||||||
Reference in New Issue
Block a user