diff --git a/spec/FilesController.spec.js b/spec/FilesController.spec.js new file mode 100644 index 0000000000..b0c6f56808 --- /dev/null +++ b/spec/FilesController.spec.js @@ -0,0 +1,27 @@ +var FilesController = require('../src/Controllers/FilesController').FilesController; +var Config = require("../src/Config"); + +// Small additional tests to improve overall coverage +describe("FilesController",()=>{ + + it("should properly expand objects", (done) => { + var config = new Config(Parse.applicationId); + var filesController = new FilesController(); + var result = filesController.expandFilesInObject(config, function(){}); + + expect(result).toBeUndefined(); + + var fullFile = { + type: '__type', + url: "http://an.url" + } + + var anObject = { + aFile: fullFile + } + filesController.expandFilesInObject(config, anObject); + expect(anObject.aFile.url).toEqual("http://an.url"); + + done(); + }) +}) \ No newline at end of file diff --git a/spec/LoggerController.spec.js b/spec/LoggerController.spec.js index f23004ab64..3475495e4f 100644 --- a/spec/LoggerController.spec.js +++ b/spec/LoggerController.spec.js @@ -2,53 +2,85 @@ var LoggerController = require('../src/Controllers/LoggerController').LoggerCont var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter; describe('LoggerController', () => { - it('can check valid master key of request', (done) => { + it('can check process a query witout throwing', (done) => { // Make mock request - var request = { - auth: { - isMaster: true - }, - query: {} - }; + var query = {}; var loggerController = new LoggerController(new FileLoggerAdapter()); expect(() => { - loggerController.handleGET(request); + loggerController.getLogs(query).then(function(res) { + expect(res.length).toBe(0); + done(); + }) }).not.toThrow(); + }); + + it('properly validates dateTimes', (done) => { + expect(LoggerController.validDateTime()).toBe(null); + expect(LoggerController.validDateTime("String")).toBe(null); + expect(LoggerController.validDateTime(123456).getTime()).toBe(123456); + expect(LoggerController.validDateTime("2016-01-01Z00:00:00").getTime()).toBe(1451606400000); done(); }); - - it('can check invalid construction of controller', (done) => { + + it('can set the proper default values', (done) => { + // Make mock request + var result = LoggerController.parseOptions(); + expect(result.size).toEqual(10); + expect(result.order).toEqual('desc'); + expect(result.level).toEqual('info'); + + done(); + }); + + it('can process a query witout throwing', (done) => { // Make mock request - var request = { - auth: { - isMaster: true - }, - query: {} + var query = { + from: "2016-01-01Z00:00:00", + until: "2016-01-01Z00:00:00", + size: 5, + order: 'asc', + level: 'error' }; - var loggerController = new LoggerController(); + var result = LoggerController.parseOptions(query); - expect(() => { - loggerController.handleGET(request); - }).toThrow(); + expect(result.from.getTime()).toEqual(1451606400000); + expect(result.until.getTime()).toEqual(1451606400000); + expect(result.size).toEqual(5); + expect(result.order).toEqual('asc'); + expect(result.level).toEqual('error'); + done(); }); - - it('can check invalid master key of request', (done) => { + + it('can check process a query witout throwing', (done) => { // Make mock request - var request = { - auth: { - isMaster: false - }, - query: {} + var query = { + from: "2015-01-01", + until: "2016-01-01", + size: 5, + order: 'desc', + level: 'error' }; var loggerController = new LoggerController(new FileLoggerAdapter()); expect(() => { - loggerController.handleGET(request); + loggerController.getLogs(query).then(function(res) { + expect(res.length).toBe(0); + done(); + }) + }).not.toThrow(); + }); + + it('should throw without an adapter', (done) => { + + var loggerController = new LoggerController(); + + expect(() => { + loggerController.getLogs(); }).toThrow(); done(); }); diff --git a/spec/LogsRouter.spec.js b/spec/LogsRouter.spec.js new file mode 100644 index 0000000000..a8ef8b25d1 --- /dev/null +++ b/spec/LogsRouter.spec.js @@ -0,0 +1,67 @@ +var LogsRouter = require('../src/Routers/LogsRouter').LogsRouter; +var LoggerController = require('../src/Controllers/LoggerController').LoggerController; +var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter; + +const loggerController = new LoggerController(new FileLoggerAdapter()); + +describe('LogsRouter', () => { + it('can check valid master key of request', (done) => { + // Make mock request + var request = { + auth: { + isMaster: true + }, + query: {}, + config: { + loggerController: loggerController + } + }; + + var router = new LogsRouter(); + + expect(() => { + router.handleGET(request); + }).not.toThrow(); + done(); + }); + + it('can check invalid construction of controller', (done) => { + // Make mock request + var request = { + auth: { + isMaster: true + }, + query: {}, + config: { + loggerController: undefined // missing controller + } + }; + + var router = new LogsRouter(); + + expect(() => { + router.handleGET(request); + }).toThrow(); + done(); + }); + + it('can check invalid master key of request', (done) => { + // Make mock request + var request = { + auth: { + isMaster: false + }, + query: {}, + config: { + loggerController: loggerController + } + }; + + var router = new LogsRouter(); + + expect(() => { + router.handleGET(request); + }).toThrow(); + done(); + }); +}); diff --git a/spec/PushController.spec.js b/spec/PushController.spec.js index 5414eca2bd..6c86b011a1 100644 --- a/spec/PushController.spec.js +++ b/spec/PushController.spec.js @@ -3,103 +3,28 @@ var PushController = require('../src/Controllers/PushController').PushController describe('PushController', () => { it('can check valid master key of request', (done) => { // Make mock request - var request = { - info: { - masterKey: 'masterKey' - }, - config: { - masterKey: 'masterKey' - } + var auth = { + isMaster: true } expect(() => { - PushController.validateMasterKey(request); + PushController.validateMasterKey(auth); }).not.toThrow(); done(); }); it('can check invalid master key of request', (done) => { // Make mock request - var request = { - info: { - masterKey: 'masterKey' - }, - config: { - masterKey: 'masterKeyAgain' - } + var auth = { + isMaster: false } expect(() => { - PushController.validateMasterKey(request); + PushController.validateMasterKey(auth); }).toThrow(); done(); }); - it('can get query condition when channels is set', (done) => { - // Make mock request - var request = { - body: { - channels: ['Giants', 'Mets'] - } - } - - var where = PushController.getQueryCondition(request); - expect(where).toEqual({ - 'channels': { - '$in': ['Giants', 'Mets'] - } - }); - done(); - }); - - it('can get query condition when where is set', (done) => { - // Make mock request - var request = { - body: { - 'where': { - 'injuryReports': true - } - } - } - - var where = PushController.getQueryCondition(request); - expect(where).toEqual({ - 'injuryReports': true - }); - done(); - }); - - it('can get query condition when nothing is set', (done) => { - // Make mock request - var request = { - body: { - } - } - - expect(function() { - PushController.getQueryCondition(request); - }).toThrow(); - done(); - }); - - it('can throw on getQueryCondition when channels and where are set', (done) => { - // Make mock request - var request = { - body: { - 'channels': { - '$in': ['Giants', 'Mets'] - }, - 'where': { - 'injuryReports': true - } - } - } - - expect(function() { - PushController.getQueryCondition(request); - }).toThrow(); - done(); - }); it('can validate device type when no device type is set', (done) => { // Make query condition @@ -170,13 +95,11 @@ describe('PushController', () => { it('can get expiration time in string format', (done) => { // Make mock request var timeStr = '2015-03-19T22:05:08Z'; - var request = { - body: { + var body = { 'expiration_time': timeStr - } - } + } - var time = PushController.getExpirationTime(request); + var time = PushController.getExpirationTime(body); expect(time).toEqual(new Date(timeStr).valueOf()); done(); }); @@ -184,28 +107,25 @@ describe('PushController', () => { it('can get expiration time in number format', (done) => { // Make mock request var timeNumber = 1426802708; - var request = { - body: { - 'expiration_time': timeNumber - } + var body = { + 'expiration_time': timeNumber } - var time = PushController.getExpirationTime(request); + var time = PushController.getExpirationTime(body); expect(time).toEqual(timeNumber * 1000); done(); }); it('can throw on getExpirationTime in invalid format', (done) => { // Make mock request - var request = { - body: { - 'expiration_time': 'abcd' - } + var body = { + 'expiration_time': 'abcd' } expect(function(){ - PushController.getExpirationTime(request); + PushController.getExpirationTime(body); }).toThrow(); done(); }); + }); diff --git a/spec/PushRouter.spec.js b/spec/PushRouter.spec.js new file mode 100644 index 0000000000..e7273dd508 --- /dev/null +++ b/spec/PushRouter.spec.js @@ -0,0 +1,123 @@ +var PushRouter = require('../src/Routers/PushRouter').PushRouter; +var request = require('request'); + +describe('PushRouter', () => { + it('can check valid master key of request', (done) => { + // Make mock request + var request = { + info: { + masterKey: 'masterKey' + }, + config: { + masterKey: 'masterKey' + } + } + + expect(() => { + PushRouter.validateMasterKey(request); + }).not.toThrow(); + done(); + }); + + it('can check invalid master key of request', (done) => { + // Make mock request + var request = { + info: { + masterKey: 'masterKey' + }, + config: { + masterKey: 'masterKeyAgain' + } + } + + expect(() => { + PushRouter.validateMasterKey(request); + }).toThrow(); + done(); + }); + + it('can get query condition when channels is set', (done) => { + // Make mock request + var request = { + body: { + channels: ['Giants', 'Mets'] + } + } + + var where = PushRouter.getQueryCondition(request); + expect(where).toEqual({ + 'channels': { + '$in': ['Giants', 'Mets'] + } + }); + done(); + }); + + it('can get query condition when where is set', (done) => { + // Make mock request + var request = { + body: { + 'where': { + 'injuryReports': true + } + } + } + + var where = PushRouter.getQueryCondition(request); + expect(where).toEqual({ + 'injuryReports': true + }); + done(); + }); + + it('can get query condition when nothing is set', (done) => { + // Make mock request + var request = { + body: { + } + } + + expect(function() { + PushRouter.getQueryCondition(request); + }).toThrow(); + done(); + }); + + it('can throw on getQueryCondition when channels and where are set', (done) => { + // Make mock request + var request = { + body: { + 'channels': { + '$in': ['Giants', 'Mets'] + }, + 'where': { + 'injuryReports': true + } + } + } + + expect(function() { + PushRouter.getQueryCondition(request); + }).toThrow(); + done(); + }); + + it('sends a push through REST', (done) => { + request.post({ + url: Parse.serverURL+"/push", + json: true, + body: { + 'channels': { + '$in': ['Giants', 'Mets'] + } + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey + } + }, function(err, res, body){ + expect(body.result).toBe(true); + done(); + }); + }); +}); \ No newline at end of file diff --git a/spec/helper.js b/spec/helper.js index c89e2dbe9e..7fbb0ae230 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -26,6 +26,14 @@ var defaultConfiguration = { masterKey: 'test', collectionPrefix: 'test_', fileKey: 'test', + push: { + 'ios': { + cert: 'prodCert.pem', + key: 'prodKey.pem', + production: true, + bundleId: 'bundleId' + } + }, oauth: { // Override the facebook provider facebook: mockFacebook(), myoauth: { diff --git a/src/Config.js b/src/Config.js index 3b9188a483..4859c09c85 100644 --- a/src/Config.js +++ b/src/Config.js @@ -1,34 +1,38 @@ // A Config object provides information about how a specific app is // configured. // mount is the URL for the root of the API; includes http, domain, etc. -function Config(applicationId, mount) { - var cache = require('./cache'); - var DatabaseAdapter = require('./DatabaseAdapter'); +export class Config { - var cacheInfo = cache.apps[applicationId]; - this.valid = !!cacheInfo; - if (!this.valid) { - return; - } + constructor(applicationId, mount) { + var cache = require('./cache'); + var DatabaseAdapter = require('./DatabaseAdapter'); - this.applicationId = applicationId; - this.collectionPrefix = cacheInfo.collectionPrefix || ''; - this.masterKey = cacheInfo.masterKey; - this.clientKey = cacheInfo.clientKey; - this.javascriptKey = cacheInfo.javascriptKey; - this.dotNetKey = cacheInfo.dotNetKey; - this.restAPIKey = cacheInfo.restAPIKey; - this.fileKey = cacheInfo.fileKey; - this.facebookAppIds = cacheInfo.facebookAppIds; - this.enableAnonymousUsers = cacheInfo.enableAnonymousUsers; + var cacheInfo = cache.apps[applicationId]; + this.valid = !!cacheInfo; + if (!this.valid) { + return; + } - this.database = DatabaseAdapter.getDatabaseConnection(applicationId); - this.filesController = cacheInfo.filesController; - this.pushController = cacheInfo.pushController; - this.oauth = cacheInfo.oauth; + this.applicationId = applicationId; + this.collectionPrefix = cacheInfo.collectionPrefix || ''; + this.masterKey = cacheInfo.masterKey; + this.clientKey = cacheInfo.clientKey; + this.javascriptKey = cacheInfo.javascriptKey; + this.dotNetKey = cacheInfo.dotNetKey; + this.restAPIKey = cacheInfo.restAPIKey; + this.fileKey = cacheInfo.fileKey; + this.facebookAppIds = cacheInfo.facebookAppIds; + this.enableAnonymousUsers = cacheInfo.enableAnonymousUsers; - this.mount = mount; -} + this.database = DatabaseAdapter.getDatabaseConnection(applicationId); + this.filesController = cacheInfo.filesController; + this.pushController = cacheInfo.pushController; + this.loggerController = cacheInfo.loggerController; + this.oauth = cacheInfo.oauth; + this.mount = mount; + } +}; +export default Config; module.exports = Config; diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index f1238694aa..e7d01763ec 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -1,11 +1,5 @@ // FilesController.js - -import express from 'express'; -import mime from 'mime'; import { Parse } from 'parse/node'; -import BodyParser from 'body-parser'; -import * as Middlewares from '../middlewares'; -import Config from '../Config'; import { randomHexString } from '../cryptoUtils'; export class FilesController { @@ -13,98 +7,23 @@ export class FilesController { this._filesAdapter = filesAdapter; } - static getHandler() { - return (req, res) => { - let config = new Config(req.params.appId); - return config.filesController.getHandler()(req, res); - } - } - - getHandler() { - return (req, res) => { - let config = new Config(req.params.appId); - let filename = req.params.filename; - this._filesAdapter.getFileData(config, filename).then((data) => { - res.status(200); - var contentType = mime.lookup(filename); - res.set('Content-type', contentType); - res.end(data); - }).catch((error) => { - res.status(404); - res.set('Content-type', 'text/plain'); - res.end('File not found.'); - }); - }; + getFileData(config, filename) { + return this._filesAdapter.getFileData(config, filename); } - static createHandler() { - return (req, res, next) => { - let config = req.config; - return config.filesController.createHandler()(req, res, next); - } - } - - createHandler() { - return (req, res, next) => { - if (!req.body || !req.body.length) { - next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, - 'Invalid file upload.')); - return; - } - - if (req.params.filename.length > 128) { - next(new Parse.Error(Parse.Error.INVALID_FILE_NAME, - 'Filename too long.')); - return; - } - - if (!req.params.filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) { - next(new Parse.Error(Parse.Error.INVALID_FILE_NAME, - 'Filename contains invalid characters.')); - return; - } - - const filesController = req.config.filesController; - // If a content-type is included, we'll add an extension so we can - // return the same content-type. - let extension = ''; - let hasExtension = req.params.filename.indexOf('.') > 0; - let contentType = req.get('Content-type'); - if (!hasExtension && contentType && mime.extension(contentType)) { - extension = '.' + mime.extension(contentType); - } - - let filename = randomHexString(32) + '_' + req.params.filename + extension; - filesController._filesAdapter.createFile(req.config, filename, req.body).then(() => { - res.status(201); - var location = filesController._filesAdapter.getFileLocation(req.config, filename); - res.set('Location', location); - res.json({ url: location, name: filename }); - }).catch((error) => { - next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, - 'Could not store file.')); + createFile(config, filename, data) { + filename = randomHexString(32) + '_' + filename; + var location = this._filesAdapter.getFileLocation(config, filename); + return this._filesAdapter.createFile(config, filename, data).then(() => { + return Promise.resolve({ + url: location, + name: filename }); - }; - } - - static deleteHandler() { - return (req, res, next) => { - let config = req.config; - return config.filesController.deleteHandler()(req, res, next); - } - } + }); + } - deleteHandler() { - return (req, res, next) => { - this._filesAdapter.deleteFile(req.config, req.params.filename).then(() => { - res.status(200); - // TODO: return useful JSON here? - res.end(); - }).catch((error) => { - next(new Parse.Error(Parse.Error.FILE_DELETE_ERROR, - 'Could not delete file.')); - }); - }; + deleteFile(config, filename) { + return this._filesAdapter.deleteFile(config, filename); } /** @@ -135,32 +54,6 @@ export class FilesController { } } } - - static getExpressRouter() { - let router = express.Router(); - router.get('/files/:appId/:filename', FilesController.getHandler()); - - router.post('/files', function(req, res, next) { - next(new Parse.Error(Parse.Error.INVALID_FILE_NAME, - 'Filename not provided.')); - }); - - router.post('/files/:filename', - Middlewares.allowCrossDomain, - BodyParser.raw({type: '*/*', limit: '20mb'}), - Middlewares.handleParseHeaders, - FilesController.createHandler() - ); - - router.delete('/files/:filename', - Middlewares.allowCrossDomain, - Middlewares.handleParseHeaders, - Middlewares.enforceMasterKeyAccess, - FilesController.deleteHandler() - ); - - return router; - } } export default FilesController; diff --git a/src/Controllers/LoggerController.js b/src/Controllers/LoggerController.js index d0b8bb28cf..fe89446c73 100644 --- a/src/Controllers/LoggerController.js +++ b/src/Controllers/LoggerController.js @@ -1,35 +1,55 @@ import { Parse } from 'parse/node'; import PromiseRouter from '../PromiseRouter'; -import rest from '../rest'; const Promise = Parse.Promise; -const INFO = 'info'; -const ERROR = 'error'; const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000; -// only allow request with master key -let enforceSecurity = (auth) => { - if (!auth || !auth.isMaster) { - throw new Parse.Error( - Parse.Error.OPERATION_FORBIDDEN, - 'Clients aren\'t allowed to perform the ' + - 'get' + ' operation on logs.' - ); - } +export const LogLevel = { + INFO: 'info', + ERROR: 'error' } -// check that date input is valid -let isValidDateTime = (date) => { - if (!date || isNaN(Number(date))) { - return false; - } +export const LogOrder = { + DESCENDING: 'desc', + ASCENDING: 'asc' } export class LoggerController { - - constructor(loggerAdapter) { + + constructor(loggerAdapter, loggerOptions) { this._loggerAdapter = loggerAdapter; } + + // check that date input is valid + static validDateTime(date) { + if (!date) { + return null; + } + date = new Date(date); + + if (!isNaN(date.getTime())) { + return date; + } + + return null; + } + + static parseOptions(options = {}) { + let from = LoggerController.validDateTime(options.from) || + new Date(Date.now() - 7 * MILLISECONDS_IN_A_DAY); + let until = LoggerController.validDateTime(options.until) || new Date(); + let size = Number(options.size) || 10; + let order = options.order || LogOrder.DESCENDING; + let level = options.level || LogLevel.INFO; + + return { + from, + until, + size, + order, + level, + }; + } // Returns a promise for a {response} object. // query params: @@ -38,41 +58,21 @@ export class LoggerController { // until (optional) End time for the search. Defaults to current time. // order (optional) Direction of results returned, either “asc” or “desc”. Defaults to “desc”. // size (optional) Number of rows returned by search. Defaults to 10 - handleGET(req) { + getLogs(options= {}) { if (!this._loggerAdapter) { throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'Logger adapter is not availabe'); } let promise = new Parse.Promise(); - let from = (isValidDateTime(req.query.from) && new Date(req.query.from)) || - new Date(Date.now() - 7 * MILLISECONDS_IN_A_DAY); - let until = (isValidDateTime(req.query.until) && new Date(req.query.until)) || new Date(); - let size = Number(req.query.size) || 10; - let order = req.query.order || 'desc'; - let level = req.query.level || INFO; - enforceSecurity(req.auth); - this._loggerAdapter.query({ - from, - until, - size, - order, - level, - }, (result) => { - promise.resolve({ - response: result - }); + + options = LoggerController.parseOptions(options); + + this._loggerAdapter.query(options, (result) => { + promise.resolve(result); }); return promise; } - - getExpressRouter() { - let router = new PromiseRouter(); - router.route('GET','/logs', (req) => { - return this.handleGET(req); - }); - return router; - } } export default LoggerController; diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js index e87e6f764c..3b73f16b91 100644 --- a/src/Controllers/PushController.js +++ b/src/Controllers/PushController.js @@ -6,138 +6,85 @@ export class PushController { constructor(pushAdapter) { this._pushAdapter = pushAdapter; - } + }; - handlePOST(req) { - if (!this._pushAdapter) { + /** + * Check whether the deviceType parameter in qury condition is valid or not. + * @param {Object} where A query condition + * @param {Array} validPushTypes An array of valid push types(string) + */ + static validatePushType(where = {}, validPushTypes = []) { + var deviceTypeField = where.deviceType || {}; + var deviceTypes = []; + if (typeof deviceTypeField === 'string') { + deviceTypes.push(deviceTypeField); + } else if (typeof deviceTypeField['$in'] === 'array') { + deviceTypes.concat(deviceTypeField['$in']); + } + for (var i = 0; i < deviceTypes.length; i++) { + var deviceType = deviceTypes[i]; + if (validPushTypes.indexOf(deviceType) < 0) { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, + deviceType + ' is not supported push type.'); + } + } + }; + + /** + * Check whether the api call has master key or not. + * @param {Object} request A request object + */ + static validateMasterKey(auth = {}) { + if (!auth.isMaster) { throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - 'Push adapter is not availabe'); + 'Master key is invalid, you should only use master key to send push'); } + } - validateMasterKey(req); - var where = getQueryCondition(req); + sendPush(body = {}, where = {}, config, auth) { var pushAdapter = this._pushAdapter; - validatePushType(where, pushAdapter.getValidPushTypes()); + if (!pushAdapter) { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, + 'Push adapter is not available'); + } + PushController.validateMasterKey(auth); + + PushController.validatePushType(where, pushAdapter.getValidPushTypes()); // Replace the expiration_time with a valid Unix epoch milliseconds time - req.body['expiration_time'] = getExpirationTime(req); + body['expiration_time'] = PushController.getExpirationTime(body); // TODO: If the req can pass the checking, we return immediately instead of waiting // pushes to be sent. We probably change this behaviour in the future. - rest.find(req.config, req.auth, '_Installation', where).then(function(response) { - return pushAdapter.send(req.body, response.results); + rest.find(config, auth, '_Installation', where).then(function(response) { + return pushAdapter.send(body, response.results); }); - return Parse.Promise.as({ - response: { - 'result': true - } - }); - } - - static getExpressRouter() { - var router = new PromiseRouter(); - router.route('POST','/push', (req) => { - return req.config.pushController.handlePOST(req); - }); - return router; - } -} - -/** - * Check whether the deviceType parameter in qury condition is valid or not. - * @param {Object} where A query condition - * @param {Array} validPushTypes An array of valid push types(string) - */ -function validatePushType(where, validPushTypes) { - var where = where || {}; - var deviceTypeField = where.deviceType || {}; - var deviceTypes = []; - if (typeof deviceTypeField === 'string') { - deviceTypes.push(deviceTypeField); - } else if (typeof deviceTypeField['$in'] === 'array') { - deviceTypes.concat(deviceTypeField['$in']); - } - for (var i = 0; i < deviceTypes.length; i++) { - var deviceType = deviceTypes[i]; - if (validPushTypes.indexOf(deviceType) < 0) { + }; + /** + * Get expiration time from the request body. + * @param {Object} request A request object + * @returns {Number|undefined} The expiration time if it exists in the request + */ + static getExpirationTime(body = {}) { + var hasExpirationTime = !!body['expiration_time']; + if (!hasExpirationTime) { + return; + } + var expirationTimeParam = body['expiration_time']; + var expirationTime; + if (typeof expirationTimeParam === 'number') { + expirationTime = new Date(expirationTimeParam * 1000); + } else if (typeof expirationTimeParam === 'string') { + expirationTime = new Date(expirationTimeParam); + } else { throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - deviceType + ' is not supported push type.'); + body['expiration_time'] + ' is not valid time.'); } - } -} - -/** - * Get expiration time from the request body. - * @param {Object} request A request object - * @returns {Number|undefined} The expiration time if it exists in the request - */ -function getExpirationTime(req) { - var body = req.body || {}; - var hasExpirationTime = !!body['expiration_time']; - if (!hasExpirationTime) { - return; - } - var expirationTimeParam = body['expiration_time']; - var expirationTime; - if (typeof expirationTimeParam === 'number') { - expirationTime = new Date(expirationTimeParam * 1000); - } else if (typeof expirationTimeParam === 'string') { - expirationTime = new Date(expirationTimeParam); - } else { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - body['expiration_time'] + ' is not valid time.'); - } - // Check expirationTime is valid or not, if it is not valid, expirationTime is NaN - if (!isFinite(expirationTime)) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - body['expiration_time'] + ' is not valid time.'); - } - return expirationTime.valueOf(); -} - -/** - * Get query condition from the request body. - * @param {Object} request A request object - * @returns {Object} The query condition, the where field in a query api call - */ -function getQueryCondition(req) { - var body = req.body || {}; - var hasWhere = typeof body.where !== 'undefined'; - var hasChannels = typeof body.channels !== 'undefined'; - - var where; - if (hasWhere && hasChannels) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - 'Channels and query can not be set at the same time.'); - } else if (hasWhere) { - where = body.where; - } else if (hasChannels) { - where = { - "channels": { - "$in": body.channels - } + // Check expirationTime is valid or not, if it is not valid, expirationTime is NaN + if (!isFinite(expirationTime)) { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, + body['expiration_time'] + ' is not valid time.'); } - } else { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - 'Channels and query should be set at least one.'); - } - return where; -} - -/** - * Check whether the api call has master key or not. - * @param {Object} request A request object - */ -function validateMasterKey(req) { - if (req.info.masterKey !== req.config.masterKey) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - 'Master key is invalid, you should only use master key to send push'); - } -} - -if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') { - PushController.getQueryCondition = getQueryCondition; - PushController.validateMasterKey = validateMasterKey; - PushController.getExpirationTime = getExpirationTime; - PushController.validatePushType = validatePushType; -} + return expirationTime.valueOf(); + }; +}; export default PushController; diff --git a/src/Routers/AnalyticsRouter.js b/src/Routers/AnalyticsRouter.js index 9e26e5f980..76be8d8718 100644 --- a/src/Routers/AnalyticsRouter.js +++ b/src/Routers/AnalyticsRouter.js @@ -1,7 +1,4 @@ // AnalyticsRouter.js - -var Parse = require('parse/node').Parse; - import PromiseRouter from '../PromiseRouter'; // Returns a promise that resolves to an empty object response diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js new file mode 100644 index 0000000000..68bdd1e42f --- /dev/null +++ b/src/Routers/FilesRouter.js @@ -0,0 +1,104 @@ +import PromiseRouter from '../PromiseRouter'; +import express from 'express'; +import BodyParser from 'body-parser'; +import * as Middlewares from '../middlewares'; +import { randomHexString } from '../cryptoUtils'; +import mime from 'mime'; +import Config from '../Config'; + +export class FilesRouter { + + getExpressRouter() { + var router = express.Router(); + router.get('/files/:appId/:filename', this.getHandler); + + router.post('/files', function(req, res, next) { + next(new Parse.Error(Parse.Error.INVALID_FILE_NAME, + 'Filename not provided.')); + }); + + router.post('/files/:filename', + Middlewares.allowCrossDomain, + BodyParser.raw({type: '*/*', limit: '20mb'}), + Middlewares.handleParseHeaders, + this.createHandler + ); + + router.delete('/files/:filename', + Middlewares.allowCrossDomain, + Middlewares.handleParseHeaders, + Middlewares.enforceMasterKeyAccess, + this.deleteHandler + ); + return router; + } + + getHandler(req, res, next) { + const config = new Config(req.params.appId); + const filesController = config.filesController; + const filename = req.params.filename; + filesController.getFileData(config, filename).then((data) => { + res.status(200); + var contentType = mime.lookup(filename); + res.set('Content-type', contentType); + res.end(data); + }).catch((error) => { + res.status(404); + res.set('Content-type', 'text/plain'); + res.end('File not found.'); + }); + } + + createHandler(req, res, next) { + if (!req.body || !req.body.length) { + next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, + 'Invalid file upload.')); + return; + } + + if (req.params.filename.length > 128) { + next(new Parse.Error(Parse.Error.INVALID_FILE_NAME, + 'Filename too long.')); + return; + } + + if (!req.params.filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) { + next(new Parse.Error(Parse.Error.INVALID_FILE_NAME, + 'Filename contains invalid characters.')); + return; + } + let extension = ''; + + // Not very safe there. + const hasExtension = req.params.filename.indexOf('.') > 0; + const contentType = req.get('Content-type'); + if (!hasExtension && contentType && mime.extension(contentType)) { + extension = '.' + mime.extension(contentType); + } + + const filename = req.params.filename + extension; + const config = req.config; + const filesController = config.filesController; + + filesController.createFile(config, filename, req.body).then((result) => { + res.status(201); + res.set('Location', result.url); + res.json(result); + }).catch((err) => { + next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, + 'Could not store file.')); + }); + } + + deleteHandler(req, res, next) { + const filesController = req.config.filesController; + filesController.deleteFile(req.config, req.params.filename).then(() => { + res.status(200); + // TODO: return useful JSON here? + res.end(); + }).catch((error) => { + next(new Parse.Error(Parse.Error.FILE_DELETE_ERROR, + 'Could not delete file.')); + }); + } +} \ No newline at end of file diff --git a/src/Routers/LogsRouter.js b/src/Routers/LogsRouter.js new file mode 100644 index 0000000000..abd57944da --- /dev/null +++ b/src/Routers/LogsRouter.js @@ -0,0 +1,60 @@ +import { Parse } from 'parse/node'; +import PromiseRouter from '../PromiseRouter'; + +// only allow request with master key +let enforceSecurity = (auth) => { + if (!auth || !auth.isMaster) { + throw new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + 'Clients aren\'t allowed to perform the ' + + 'get' + ' operation on logs.' + ); + } +} + +export class LogsRouter extends PromiseRouter { + + mountRoutes() { + this.route('GET','/logs', (req) => { + return this.handleGET(req); + }); + } + + // Returns a promise for a {response} object. + // query params: + // level (optional) Level of logging you want to query for (info || error) + // from (optional) Start time for the search. Defaults to 1 week ago. + // until (optional) End time for the search. Defaults to current time. + // order (optional) Direction of results returned, either “asc” or “desc”. Defaults to “desc”. + // size (optional) Number of rows returned by search. Defaults to 10 + handleGET(req) { + if (!req.config || !req.config.loggerController) { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, + 'Logger adapter is not availabe'); + } + + let promise = new Parse.Promise(); + let from = req.query.from; + let until = req.query.until; + let size = req.query.size; + let order = req.query.order + let level = req.query.level; + enforceSecurity(req.auth); + + const options = { + from, + until, + size, + order, + level, + } + + return req.config.loggerController.getLogs(options).then((result) => { + return Promise.resolve({ + response: result + }); + }) + } +} + +export default LogsRouter; diff --git a/src/Routers/PushRouter.js b/src/Routers/PushRouter.js new file mode 100644 index 0000000000..f75d99985b --- /dev/null +++ b/src/Routers/PushRouter.js @@ -0,0 +1,72 @@ +import PushController from '../Controllers/PushController' +import PromiseRouter from '../PromiseRouter'; + +export class PushRouter extends PromiseRouter { + + mountRoutes() { + this.route("POST", "/push", req => { return this.handlePOST(req); }); + } + + /** + * Check whether the api call has master key or not. + * @param {Object} request A request object + */ + static validateMasterKey(req) { + if (req.info.masterKey !== req.config.masterKey) { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, + 'Master key is invalid, you should only use master key to send push'); + } + } + + handlePOST(req) { + // TODO: move to middlewares when support for Promise middlewares + PushRouter.validateMasterKey(req); + + const pushController = req.config.pushController; + if (!pushController) { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, + 'Push controller is not set'); + } + + var where = PushRouter.getQueryCondition(req); + + pushController.sendPush(req.body, where, req.config, req.auth); + return Promise.resolve({ + response: { + 'result': true + } + }); + } + + /** + * Get query condition from the request body. + * @param {Object} request A request object + * @returns {Object} The query condition, the where field in a query api call + */ + static getQueryCondition(req) { + var body = req.body || {}; + var hasWhere = typeof body.where !== 'undefined'; + var hasChannels = typeof body.channels !== 'undefined'; + + var where; + if (hasWhere && hasChannels) { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, + 'Channels and query can not be set at the same time.'); + } else if (hasWhere) { + where = body.where; + } else if (hasChannels) { + where = { + "channels": { + "$in": body.channels + } + } + } else { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, + 'Channels and query should be set at least one.'); + } + return where; + } + +} + +export default PushRouter; diff --git a/src/index.js b/src/index.js index de63f9bb06..6ff5bab74c 100644 --- a/src/index.js +++ b/src/index.js @@ -18,6 +18,7 @@ import { FilesController } from './Controllers/FilesController'; import ParsePushAdapter from './Adapters/Push/ParsePushAdapter'; import { PushController } from './Controllers/PushController'; + import { ClassesRouter } from './Routers/ClassesRouter'; import { InstallationsRouter } from './Routers/InstallationsRouter'; import { UsersRouter } from './Routers/UsersRouter'; @@ -27,7 +28,9 @@ import { AnalyticsRouter } from './Routers/AnalyticsRouter'; import { FunctionsRouter } from './Routers/FunctionsRouter'; import { SchemasRouter } from './Routers/SchemasRouter'; import { IAPValidationRouter } from './Routers/IAPValidationRouter'; - +import { PushRouter } from './Routers/PushRouter'; +import { FilesRouter } from './Routers/FilesRouter'; +import { LogsRouter } from './Routers/LogsRouter'; import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter'; import { LoggerController } from './Controllers/LoggerController'; @@ -110,7 +113,9 @@ function ParseServer({ } } - let filesController = new FilesController(filesAdapter); + const filesController = new FilesController(filesAdapter); + const pushController = new PushController(pushAdapter); + const loggerController = new LoggerController(loggerAdapter); cache.apps[appId] = { masterKey: masterKey, @@ -122,6 +127,8 @@ function ParseServer({ fileKey: fileKey, facebookAppIds: facebookAppIds, filesController: filesController, + pushController: pushController, + loggerController: loggerController, enableAnonymousUsers: enableAnonymousUsers, oauth: oauth, }; @@ -140,7 +147,7 @@ function ParseServer({ var api = express(); // File handling needs to be before default middlewares are applied - api.use('/', FilesController.getExpressRouter()); + api.use('/', new FilesRouter().getExpressRouter()); // TODO: separate this from the regular ParseServer object if (process.env.TESTING == 1) { @@ -161,8 +168,8 @@ function ParseServer({ new InstallationsRouter(), new FunctionsRouter(), new SchemasRouter(), - PushController.getExpressRouter(), - new LoggerController(loggerAdapter).getExpressRouter(), + new PushRouter(), + new LogsRouter(), new IAPValidationRouter() ];