UNPKG

scriptremote

Version:

Message server providing secure remote access to scripted applications from a browser UI

567 lines (542 loc) 23.3 kB
'use strict' // // Additional middleware for '../projects/..' routes. // Request info is collected in res.locals.reqParams. // // The middleware chain is: // copy route params // authenticate - session-based for browsers, basic header for scripts // validate params - from route params look up DB instances // parse query/form/multipartform // handle route // handle server errors // // var Multiparty = require('multiparty'), q = require('q'), User = require('../../../app/models/user.server.model.js'), Project = require('../../../app/models/project.server.model.js'), Job = require('../../../app/models/job.server.model.js'), Location = require('../../../app/models/location.server.model.js'), Message = require('../../../app/models/message.server.model.js'), AuthUtil = require('./auth.server.js'), Utils = require('../../../app/utils.js'); // Sanitize a string that could be used in mongo query. // The injection threat is supposed to be inherently small // compared to sql to begin with, so not much needs to be done. var sanitizeQString = function(s) { var start = 0; var len = s.length; if (len > 255) { len = 255; } if (s.charAt(0) == '$') { start = 1; } return s.slice(start, start + len); } exports.sanitizeQString = sanitizeQString; exports.isSanitizedQString = function(s) { return (s == sanitizeQString(s)) } // Copy the request operation and route params. exports.copyParams = function(req, res, next, access, op, paramNames) { if (!res.locals.reqParams) { res.locals.reqParams = {}; } res.locals.reqParams['access'] = access; res.locals.reqParams['op'] = op; if (req.params.base == 'brsapi') { res.locals.reqParams['base'] = 'brsapi'; } else if (req.params.base == 'api') { res.locals.reqParams['base'] = 'api'; } for (var i = 0; i < paramNames.length; i++) { var p = paramNames[i]; switch(p) { case 'project': res.locals.reqParams['project_name'] = sanitizeQString(req.params.project); break; case 'job': res.locals.reqParams['job_id'] = sanitizeQString(req.params.job); break; case 'location': res.locals.reqParams['location_name'] = sanitizeQString(req.params.location); break; case 'msg': res.locals.reqParams['msg_id'] = sanitizeQString(req.params.msg); break; case 'file_key': res.locals.reqParams['file_key'] = sanitizeQString(req.params.file_key); break; case 'name': res.locals.reqParams['name'] = sanitizeQString(req.params.name); break; case 'owner': res.locals.reqParams['owner'] = sanitizeQString(req.params.owner); break; default: var err = new Error('Fatal error in middle.js/copyParams'); return next(err); } } next(); } // Middleware to validate :location/:msg route params // previously saved in res.locals.reqParams and also to add their // corresponding model objects exports.validateParams = function(req, res, next) { var reqParams = res.locals.reqParams; var user = reqParams['user']; var project = reqParams['project']; var jobId = reqParams['job_id']; var locName = reqParams['location_name']; var msgId = reqParams['msg_id']; if (!jobId || !locName) { return next(); } else { var job = reqParams['job']; Location.findOne( { locName: locName, jobDBId: job.Id() }, function(err, loc) { if (err) { return next(err); } else if (!loc) { if (reqParams['op'] != 'msg') { // The location name can be new only if POSTing a message res.status(404); res.send( Utils.makeRes(Utils.statusTag, 'Invalid location name') ); } else { return next(); } } else { reqParams['location'] = loc; if (!msgId) { return next(); } else { Message.findOne( { msgId: msgId, locDBId: loc.Id() }, function(err, msg) { if (err) { return next(err); } else if (!msg) { res.status(404); res.send( Utils.makeRes(Utils.statusTag, 'Invalid message id') ); } else { reqParams['msg'] = msg; reqParams['parentId'] = msg.Id(); return next(); } }) } } }) } } // Validate :job route param and if doing api route also // check token/job authorization - returns a promise. // Response is sent immediately for user errors, not // for server errors. var authJobPrm = function(res) { var deferred = q.defer(); var reqParams = res.locals.reqParams; var user = reqParams['user']; var project = reqParams['project']; var token = reqParams['token']; var jobId = reqParams['job_id']; var p = null; if (jobId) { // Request is for specific job Job.findOne( { jobId: jobId, projectDBId: project.Id() }, function(err, job) { if (err) { err.type = 'server'; deferred.reject(err); } else if (!job) { res.status(404); res.send( Utils.makeRes(Utils.statusTag, 'Invalid jobid') ); var err1 = new Error(); err1.type = 'user'; deferred.reject(err1); } else { reqParams['job'] = job; if (reqParams['base'] == 'api') { // url is 'api - check Basic Auth token vs job or user var promises = []; // Does request token match current job token? p = AuthUtil.verifyToken(token, job.apiToken); promises.push(p); if (reqParams['owned']) { // 'user' is the project owner and requesting user. // Does request token match the owner's current token from the database? p = AuthUtil.verifyToken(token, user.token); promises.push(p); } else if (reqParams['shareUser']) { // 'shareUser' is the requesting user, not project owner. // Does request token match the requestor's current token from the database? p = AuthUtil.verifyToken(token, reqParams['shareUser'].token); promises.push(p); } q.all(promises) .then(function(verified) { if (verified[0] || verified[1]) { // Matches job token so valid deferred.resolve(res); } else { res.status(401); res.set('WWW-Authenticate', 'Basic realm="scriptremote/api"'); res.send( Utils.makeRes(Utils.statusTag, 'Invalid user token') ); var err1 = new Error(); err1.type = 'user'; deferred.reject(err1); } }, function(err) { deferred.reject(err) } ) } else { // Current url is not 'api' so nothing more to check deferred.resolve(res); } } }) } else { // Request is not specific to job if (reqParams['base'] == 'api') { if (reqParams['owned']) { // 'user' is the project owner and requesting user. // Does auth token match the owner's current token from the database? p = AuthUtil.verifyToken(token, user.token); } else if (reqParams['shareUser']) { // 'shareUser' is the requesting user, not project owner. // Does request token match the requestor's current token from the database? p = AuthUtil.verifyToken(token, reqParams['shareUser'].token); } p.then( function(verified) { if (verified) { deferred.resolve(res); } else { res.status(401); res.set('WWW-Authenticate', 'Basic realm="scriptremote/api"'); res.send( Utils.makeRes(Utils.statusTag, 'Invalid user token') ); var err1 = new Error(); err1.type = 'user'; deferred.reject(err1); } }, function(err) { deferred.reject(err) } ) } else { // Current url is not 'api' so nothing more to check deferred.resolve(res); } } return deferred.promise; } // Check project authorization - returns a promise. // Response is sent immediately for user errors, not // for server errors. var authProjectPrm = function(res) { var deferred = q.defer(); var reqParams = res.locals.reqParams; var user = res.locals.reqParams['user']; var err; var err1; if (reqParams['op'] == 'projects') { // Getting projects owned by requesting user reqParams['owned'] = true; deferred.resolve(res); } else if (reqParams['op'] == 'projects-share') { // Getting projects shared by another user var email = reqParams['owner']; User.findOne( { 'email': email }, function(err, user1) { if (err) { err.type = 'server'; deferred.reject(err); } else if (!user1) { res.status(404); res.send( Utils.makeRes(Utils.statusTag, 'Sharing user not found') ); err1 = new Error(); err1.type = 'user'; deferred.reject(err1); } else { reqParams['shareUser'] = user; // reqParams['user'] wlll represent the owner instead of // the requestor for following middleware reqParams['user'] = user1; // and remember that requstor does not own the project reqParams['owned'] = false; deferred.resolve(res); } }) } else if (reqParams['op'] == 'project-post') { // Creating new project owned by requesting user reqParams['owned'] = true; deferred.resolve(res); } else { // Request is for existing project var projName = reqParams['project_name']; Project.findOne( { 'projectName': projName, 'ownerUid': user.uid }, function(err, project) { if (err) { err.type = 'server'; deferred.reject(err); } else if (project) { // It's an existing project owned by requesting user reqParams['project'] = project; reqParams['owned'] = true; deferred.resolve(res); } else { // Check for a project with different owner var sp = projName.split("~"); if (sp.length != 2) { res.status(404); res.send( Utils.makeRes(Utils.statusTag, 'Invalid project') ); err1 = new Error(); err1.type = 'user'; deferred.reject(err1); } else { // Look for the owner User.findOne( { 'email': sp[1] }, function(err, user1) { if (err) { err.type = 'server'; deferred.reject(err); } else if (!user1) { res.status(404); res.send( Utils.makeRes(Utils.statusTag, 'Invalid project') ); err1 = new Error(); err1.type = 'user'; deferred.reject(err1); } else { // Look for the project Project.findOne( { 'projectName': sp[0], 'ownerUid': user1.uid }, function(err, project) { if (err) { err.type = 'server'; deferred.reject(err); } else if (!project) { res.status(404); res.send( Utils.makeRes(Utils.statusTag, 'Invalid project') ); err1 = new Error(); err1.type = 'user'; deferred.reject(err1); } else { // Check if requesting user has the needed permission var access = project.chkAuthorizedTo(user.Id()); if (!access) { if (reqParams['base'] == 'api') { res.set('WWW-Authenticate', 'Basic realm="scriptremote/api"'); } res.status(401); res.send( Utils.makeRes(Utils.statusTag, 'Project not authorized for user') ); err1 = new Error(); err1.type = 'user'; deferred.reject(err1); } else if ((reqParams['access'] == 'owner')) { if (reqParams['base'] == 'api') { res.set('WWW-Authenticate', 'Basic realm="scriptremote/api"'); } res.status(401); res.send( Utils.makeRes(Utils.statusTag, 'Project not owned by user') ); err1 = new Error(); err1.type = 'user'; deferred.reject(err1); } else if ((reqParams['access'] == 'write') && (access != 'write')) { if (reqParams['base'] == 'api') { res.set('WWW-Authenticate', 'Basic realm="scriptremote/api"'); } res.status(401); res.send( Utils.makeRes(Utils.statusTag, 'Project write not authorized for user') ); err1 = new Error(); err1.type = 'user'; deferred.reject(err1); } else if ((reqParams['access'] == 'reply') && (access == 'read')) { if (reqParams['base'] == 'api') { res.set('WWW-Authenticate', 'Basic realm="scriptremote/api"'); } res.status(401); res.send( Utils.makeRes(Utils.statusTag, 'Project reply not authorized for user') ); err1 = new Error(); err1.type = 'user'; deferred.reject(err1); } else if (reqParams['op'] == 'delete') { if (reqParams['base'] == 'api') { res.set('WWW-Authenticate', 'Basic realm="scriptremote/api"'); } res.status(401); res.send( Utils.makeRes(Utils.statusTag, 'Delete not authorized for user') ); err1 = new Error(); err1.type = 'user'; deferred.reject(err1); } else { // user has permission reqParams['project'] = project; reqParams['shareUser'] = user; // reqParams['user'] wlll represent the owner instead of // the requestor for following middleware reqParams['user'] = user1; // and remember that requstor does not own the project reqParams['owned'] = false; reqParams['access'] = access; deferred.resolve(res); } } }) } }) } } }) } return deferred.promise; } // Middleware to check auth for rest project routes. // Auth is based on login sessions for /brsapi/projects/.. uri's and // http Basic for /api/projects/.. uri's. For Basic the credentials // are the user id and api token. user and project objects are added // to res.locals.reqParams. Projects can be shared so auth can // require checking permissions granted by a different owner. // // If a specific job is being addressed by an 'api' uri // then the token can either match the value stored in the job // or the current value in the user record. If not specific to a // job then the token must match the current value for the user. // exports.authRest = function(req, res, next) { var reqParams = res.locals.reqParams; var token = null; var result; // Checking requires some db access's in sequence. // Response is sent immediately for user errors, responses // for server errors are handled at the end of the middleware // chain. if (reqParams['base'] == 'brsapi') { // Using login session result = AuthUtil.sessionCheckPrm(req, res) .then(authProjectPrm) .then(authJobPrm) .then( function(res) { return next() }, function(err) { if (err.type == 'server') return next(err); else return; } ) } else if (reqParams['base'] == 'api') { // Using basic auth result = AuthUtil.basicUserCheckPrm(req, res) .then(authProjectPrm) .then(authJobPrm) .then( function(res) { return next() }, function(err) { if (err.type == 'server') return next(err); else return; } ) } else { var err = new Error('Fatal error in middle.js/authRest'); return next(err); } } // Middleware to check auth for browser access to shared // dynamic template data. // exports.authTmpl = function(req, res, next) { var reqParams = res.locals.reqParams; var token = null; // Checking requires some db access's in sequence. // Response is sent immediately for user errors, responses // for server errors are handled at the end of the middleware // chain. var result = AuthUtil.sessionCheckPrm(req, res) .then(authProjectPrm) .then(authJobPrm) .then( function(res) { return next() }, function(err) { if (err.type == 'server') return next(err); else return; } ) } // Middleware to parse request query/form/multi-part-form // and add to res.locals.reqParams exports.parse = function(req, res, next) { if (req.headers['content-type'] && (req.headers['content-type'].search('multipart/form-data') >= 0)) { // Request has multi-part form body var form = new Multiparty.Form(); form.parse(req, function(err, fields, files) { if (err) { res.status(400); res.send( Utils.makeRes(Utils.statusTag, 'Invalid multipart form') ); } else if (files.length > 0) { // Parsing for file uploads is handled separately res.status(400); res.send( Utils.makeRes(Utils.statusTag, 'Form data contains unexpected files') ); } else { Object.keys(fields).forEach(function(name) { res.locals.reqParams[name] = fields[name][0]; }); next(); } }) } else { // Express middleware handles other types - just copy var gotData = false; Object.keys(req.body).forEach(function(name) { if (req.body.hasOwnProperty(name)) { var val = req.body[name]; res.locals.reqParams[name] = val; gotData = true; } }); if (gotData) { // Require json for browser API requests if (res.locals.reqParams['base'] == 'brsapi') { if (!req.headers['content-type'] || !req.is('json')) { res.status(400); res.send( Utils.makeRes(Utils.statusTag, 'Request content type must be json') ); return; } } } next(); } } // Middleware for unhandled errors of matched routes. // These should mainly be from failed db requests exports.errHandler = function(err, req, res, next) { res.status(500); res.send(err.message); }