forked from coolaj86/goldilocks.js
		
	
		
			
				
	
	
		
			586 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			586 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| module.exports.dependencies = [ 'OAUTH3', 'storage.owners', 'options.device' ];
 | |
| module.exports.create = function (deps, conf) {
 | |
|   var scmp = require('scmp');
 | |
|   var crypto = require('crypto');
 | |
|   var jwt = require('jsonwebtoken');
 | |
|   var bodyParser = require('body-parser');
 | |
|   var jsonParser = bodyParser.json({
 | |
|     inflate: true, limit: '100kb', reviver: null, strict: true /* type, verify */
 | |
|   });
 | |
| 
 | |
|   function handleCors(req, res, methods) {
 | |
|     if (!methods) {
 | |
|       methods = ['GET', 'POST'];
 | |
|     }
 | |
|     if (!Array.isArray(methods)) {
 | |
|       methods = [ methods ];
 | |
|     }
 | |
| 
 | |
|     res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*');
 | |
|     res.setHeader('Access-Control-Allow-Methods', methods.join(', '));
 | |
|     res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
 | |
|     res.setHeader('Access-Control-Allow-Credentials', 'true');
 | |
| 
 | |
|     if (req.method.toUpperCase() === 'OPTIONS') {
 | |
|       res.setHeader('Allow', methods.join(', '));
 | |
|       res.end();
 | |
|       return true;
 | |
|     }
 | |
| 
 | |
|     if (methods.indexOf('*') >= 0) {
 | |
|       return false;
 | |
|     }
 | |
|     if (methods.indexOf(req.method.toUpperCase()) < 0) {
 | |
|       res.statusCode = 405;
 | |
|       res.setHeader('Content-Type', 'application/json');
 | |
|       res.end(JSON.stringify({ error: { message: 'method '+req.method+' not allowed', code: 'EBADMETHOD'}}));
 | |
|       return true;
 | |
|     }
 | |
|   }
 | |
|   function makeCorsHandler(methods) {
 | |
|     return function corsHandler(req, res, next) {
 | |
|       if (!handleCors(req, res, methods)) {
 | |
|         next();
 | |
|       }
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   function handlePromise(req, res, prom) {
 | |
|     prom.then(function (result) {
 | |
|       res.send(deps.recase.snakeCopy(result));
 | |
|     }).catch(function (err) {
 | |
|       if (conf.debug) {
 | |
|         console.log(err);
 | |
|       }
 | |
|       res.statusCode = err.statusCode || 500;
 | |
|       err.message = err.message || err.toString();
 | |
|       res.end(JSON.stringify({error: {message: err.message, code: err.code}}));
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   function isAuthorized(req, res, fn) {
 | |
|     var auth = jwt.decode((req.headers.authorization||'').replace(/^bearer\s+/i, ''));
 | |
|     if (!auth) {
 | |
|       res.statusCode = 401;
 | |
|       res.setHeader('Content-Type', 'application/json;');
 | |
|       res.end(JSON.stringify({ error: { message: "no token", code: 'E_NO_TOKEN', uri: undefined } }));
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     var id = crypto.createHash('sha256').update(auth.sub).digest('hex');
 | |
|     return deps.storage.owners.exists(id).then(function (exists) {
 | |
|       if (!exists) {
 | |
|         res.statusCode = 401;
 | |
|         res.setHeader('Content-Type', 'application/json;');
 | |
|         res.end(JSON.stringify({ error: { message: "not authorized", code: 'E_NO_AUTHZ', uri: undefined } }));
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       req.userId = id;
 | |
|       fn();
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   function checkPaywall() {
 | |
|     var url = require('url');
 | |
|     var PromiseA = require('bluebird');
 | |
|     var testDomains = [
 | |
|       'daplie.com'
 | |
|     , 'duckduckgo.com'
 | |
|     , 'google.com'
 | |
|     , 'amazon.com'
 | |
|     , 'facebook.com'
 | |
|     , 'msn.com'
 | |
|     , 'yahoo.com'
 | |
|     ];
 | |
| 
 | |
|     // While this is not being developed behind a paywall the current idea is that
 | |
|     // a paywall will either manipulate DNS queries to point to the paywall gate,
 | |
|     // or redirect HTTP requests to the paywall gate. So we check for both and
 | |
|     // hope we can detect most hotel/ISP paywalls out there in the world.
 | |
|     //
 | |
|     // It is also possible that the paywall will prevent any unknown traffic from
 | |
|     // leaving the network, so the DNS queries could fail if the unit is set to
 | |
|     // use nameservers other than the paywall router.
 | |
|     return PromiseA.resolve()
 | |
|     .then(function () {
 | |
|       var dns = PromiseA.promisifyAll(require('dns'));
 | |
|       var proms = testDomains.map(function (dom) {
 | |
|         return dns.resolve6Async(dom)
 | |
|           .catch(function () {
 | |
|             return dns.resolve4Async(dom);
 | |
|           })
 | |
|           .then(function (result) {
 | |
|             return result[0];
 | |
|           }, function () {
 | |
|             return null;
 | |
|           });
 | |
|       });
 | |
| 
 | |
|       return PromiseA.all(proms).then(function (addrs) {
 | |
|         var unique = addrs.filter(function (value, ind, self) {
 | |
|           return value && self.indexOf(value) === ind;
 | |
|         });
 | |
|         // It is possible some walls might have exceptions that leave some of the domains
 | |
|         // we test alone, so we might have more than one unique address even behind an
 | |
|         // active paywall.
 | |
|         return unique.length < addrs.length;
 | |
|       });
 | |
|     })
 | |
|     .then(function (paywall) {
 | |
|       if (paywall) {
 | |
|         return paywall;
 | |
|       }
 | |
|       var request = deps.request.defaults({
 | |
|         followRedirect: false
 | |
|       , headers: {
 | |
|           connection: 'close'
 | |
|         }
 | |
|       });
 | |
| 
 | |
|       var proms = testDomains.map(function (dom) {
 | |
|         return request('http://'+dom).then(function (resp) {
 | |
|           if (resp.statusCode >= 300 && resp.statusCode < 400) {
 | |
|             return url.parse(resp.headers.location).hostname;
 | |
|           } else {
 | |
|             return dom;
 | |
|           }
 | |
|         });
 | |
|       });
 | |
| 
 | |
|       return PromiseA.all(proms).then(function (urls) {
 | |
|         var unique = urls.filter(function (value, ind, self) {
 | |
|           return value && self.indexOf(value) === ind;
 | |
|         });
 | |
|         return unique.length < urls.length;
 | |
|       });
 | |
|     })
 | |
|     ;
 | |
|   }
 | |
| 
 | |
|   // This object contains all of the API endpoints written before we changed how
 | |
|   // the API routing is handled. Eventually it will hopefully disappear, but for
 | |
|   // now we're focusing on the things that need changing more.
 | |
|   var oldEndPoints = {
 | |
|     init: function (req, res) {
 | |
|       if (handleCors(req, res, ['GET', 'POST'])) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       if ('POST' !== req.method) {
 | |
|         // It should be safe to give the list of owner IDs to an un-authenticated
 | |
|         // request because the ID is the sha256 of the PPID and shouldn't be reversible
 | |
|         return deps.storage.owners.all().then(function (results) {
 | |
|           var ids = results.map(function (owner) {
 | |
|             return owner.id;
 | |
|           });
 | |
|           res.setHeader('Content-Type', 'application/json');
 | |
|           res.end(JSON.stringify(ids));
 | |
|         });
 | |
|       }
 | |
| 
 | |
|       jsonParser(req, res, function () {
 | |
| 
 | |
|       return deps.PromiseA.resolve().then(function () {
 | |
|         console.log('init POST body', req.body);
 | |
| 
 | |
|         var auth = jwt.decode((req.headers.authorization||'').replace(/^bearer\s+/i, ''));
 | |
|         var token = jwt.decode(req.body.access_token);
 | |
|         var refresh = jwt.decode(req.body.refresh_token);
 | |
|         auth.sub = auth.sub || auth.acx.id;
 | |
|         token.sub = token.sub || token.acx.id;
 | |
|         refresh.sub = refresh.sub || refresh.acx.id;
 | |
| 
 | |
|         // TODO validate token with issuer, but as-is the sub is already a secret
 | |
|         var id = crypto.createHash('sha256').update(auth.sub).digest('hex');
 | |
|         var tid = crypto.createHash('sha256').update(token.sub).digest('hex');
 | |
|         var rid = crypto.createHash('sha256').update(refresh.sub).digest('hex');
 | |
|         var session = {
 | |
|           access_token: req.body.access_token
 | |
|         , token: token
 | |
|         , refresh_token: req.body.refresh_token
 | |
|         , refresh: refresh
 | |
|         };
 | |
| 
 | |
|         console.log('ids', id, tid, rid);
 | |
| 
 | |
|         if (req.body.ip_url) {
 | |
|           // TODO set options / GunDB
 | |
|           conf.ip_url = req.body.ip_url;
 | |
|         }
 | |
| 
 | |
|         return deps.storage.owners.all().then(function (results) {
 | |
|           console.log('results', results);
 | |
|           var err;
 | |
| 
 | |
|           // There is no owner yet. First come, first serve.
 | |
|           if (!results || !results.length) {
 | |
|             if (tid !== id || rid !== id) {
 | |
|               err = new Error(
 | |
|                 "When creating an owner the Authorization Bearer and Token and Refresh must all match"
 | |
|               );
 | |
|               err.statusCode = 400;
 | |
|               return deps.PromiseA.reject(err);
 | |
|             }
 | |
|             console.log('no owner, creating');
 | |
|             return deps.storage.owners.set(id, session);
 | |
|           }
 | |
|           console.log('has results');
 | |
| 
 | |
|           // There are onwers. Is this one of them?
 | |
|           if (!results.some(function (token) {
 | |
|             return scmp(id, token.id);
 | |
|           })) {
 | |
|             err = new Error("Authorization token does not belong to an existing owner.");
 | |
|             err.statusCode = 401;
 | |
|             return deps.PromiseA.reject(err);
 | |
|           }
 | |
|           console.log('has correct owner');
 | |
| 
 | |
|           // We're adding an owner, unless it already exists
 | |
|           if (!results.some(function (token) {
 | |
|             return scmp(tid, token.id);
 | |
|           })) {
 | |
|             console.log('adds new owner with existing owner');
 | |
|             return deps.storage.owners.set(tid, session);
 | |
|           }
 | |
|         }).then(function () {
 | |
|           res.setHeader('Content-Type', 'application/json;');
 | |
|           res.end(JSON.stringify({ success: true }));
 | |
|         });
 | |
|       })
 | |
|       .catch(function (err) {
 | |
|         res.setHeader('Content-Type', 'application/json;');
 | |
|         res.statusCode = err.statusCode || 500;
 | |
|         res.end(JSON.stringify({ error: { message: err.message, code: err.code, uri: err.uri } }));
 | |
|       });
 | |
| 
 | |
|       });
 | |
|     }
 | |
|   , request: function (req, res) {
 | |
|       if (handleCors(req, res, '*')) {
 | |
|         return;
 | |
|       }
 | |
|       isAuthorized(req, res, function () {
 | |
|       jsonParser(req, res, function () {
 | |
| 
 | |
|         deps.request({
 | |
|           method: req.body.method || 'GET'
 | |
|         , url: req.body.url
 | |
|         , headers: req.body.headers
 | |
|         , body: req.body.data
 | |
|         }).then(function (resp) {
 | |
|           if (resp.body instanceof Buffer || 'string' === typeof resp.body) {
 | |
|             resp.body = JSON.parse(resp.body);
 | |
|           }
 | |
| 
 | |
|           return {
 | |
|             statusCode: resp.statusCode
 | |
|           , status: resp.status
 | |
|           , headers: resp.headers
 | |
|           , body: resp.body
 | |
|           , data: resp.data
 | |
|           };
 | |
|         }).then(function (result) {
 | |
|           res.send(result);
 | |
|         });
 | |
| 
 | |
|       });
 | |
|       });
 | |
|     }
 | |
|   , paywall_check: function (req, res) {
 | |
|       if (handleCors(req, res, 'GET')) {
 | |
|         return;
 | |
|       }
 | |
|       isAuthorized(req, res, function () {
 | |
|         res.setHeader('Content-Type', 'application/json;');
 | |
| 
 | |
|         checkPaywall().then(function (paywall) {
 | |
|           res.end(JSON.stringify({paywall: paywall}));
 | |
|         }, function (err) {
 | |
|           err.message = err.message || err.toString();
 | |
|           res.statusCode = 500;
 | |
|           res.end(JSON.stringify({error: {message: err.message, code: err.code}}));
 | |
|         });
 | |
|       });
 | |
|     }
 | |
|   , socks5: function (req, res) {
 | |
|       if (handleCors(req, res, ['GET', 'POST', 'DELETE'])) {
 | |
|         return;
 | |
|       }
 | |
|       isAuthorized(req, res, function () {
 | |
|         var method = req.method.toUpperCase();
 | |
|         var prom;
 | |
| 
 | |
|         if (method === 'POST') {
 | |
|           prom = deps.socks5.start();
 | |
|         } else if (method === 'DELETE') {
 | |
|           prom = deps.socks5.stop();
 | |
|         } else {
 | |
|           prom = deps.socks5.curState();
 | |
|         }
 | |
| 
 | |
|         res.setHeader('Content-Type', 'application/json;');
 | |
|         prom.then(function (result) {
 | |
|           res.end(JSON.stringify(result));
 | |
|         }, function (err) {
 | |
|           err.message = err.message || err.toString();
 | |
|           res.statusCode = 500;
 | |
|           res.end(JSON.stringify({error: {message: err.message, code: err.code}}));
 | |
|         });
 | |
|       });
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   function handleOldApis(req, res, next) {
 | |
|     if (typeof oldEndPoints[req.params.name] === 'function') {
 | |
|       oldEndPoints[req.params.name](req, res);
 | |
|     } else {
 | |
|       next();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   var config = { restful: {} };
 | |
|   config.restful.readConfig = function (req, res, next) {
 | |
|     var part = new (require('./config').ConfigChanger)(conf);
 | |
|     if (req.params.group) {
 | |
|       part = part[req.params.group];
 | |
|     }
 | |
|     if (part && req.params.domId) {
 | |
|       part = part.domains.findId(req.params.domId);
 | |
|     }
 | |
|     if (part && req.params.mod) {
 | |
|       part = part[req.params.mod];
 | |
|     }
 | |
|     if (part && req.params.modGrp) {
 | |
|       part = part[req.params.modGrp];
 | |
|     }
 | |
|     if (part && req.params.modId) {
 | |
|       part = part.findId(req.params.modId);
 | |
|     }
 | |
| 
 | |
|     if (part) {
 | |
|       res.send(deps.recase.snakeCopy(part));
 | |
|     } else {
 | |
|       next();
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   config.save = function (changer) {
 | |
|     var errors = changer.validate();
 | |
|     if (errors.length) {
 | |
|       throw Object.assign(new Error(), errors[0], {statusCode: 400});
 | |
|     }
 | |
| 
 | |
|     return deps.storage.config.save(changer);
 | |
|   };
 | |
|   config.restful.saveBaseConfig = function (req, res, next) {
 | |
|     console.log('config POST body', JSON.stringify(req.body));
 | |
|     if (req.params.group === 'domains') {
 | |
|       next();
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     var promise = deps.PromiseA.resolve().then(function () {
 | |
|       var update;
 | |
|       if (req.params.group) {
 | |
|         update = {};
 | |
|         update[req.params.group] = req.body;
 | |
|       } else {
 | |
|         update = req.body;
 | |
|       }
 | |
| 
 | |
|       var changer = new (require('./config').ConfigChanger)(conf);
 | |
|       changer.update(update);
 | |
|       return config.save(changer);
 | |
|     }).then(function (newConf) {
 | |
|       if (req.params.group) {
 | |
|         return newConf[req.params.group];
 | |
|       }
 | |
|       return newConf;
 | |
|     });
 | |
|     handlePromise(req, res, promise);
 | |
|   };
 | |
| 
 | |
|   config.extractModList = function (changer, params) {
 | |
|     var err;
 | |
|     if (params.domId) {
 | |
|       var dom = changer.domains.find(function (dom) {
 | |
|         return dom.id === params.domId;
 | |
|       });
 | |
| 
 | |
|       if (!dom) {
 | |
|         err = new Error("no domain with ID '"+params.domId+"'");
 | |
|       } else if (!dom.modules[params.group]) {
 | |
|         err = new Error("domains don't contain '"+params.group+"' modules");
 | |
|       } else {
 | |
|         return dom.modules[params.group];
 | |
|       }
 | |
|     } else {
 | |
|       if (!changer[params.group] || !changer[params.group].modules) {
 | |
|         err = new Error("'"+params.group+"' is not a valid settings group or doesn't support modules");
 | |
|       } else {
 | |
|         return changer[params.group].modules;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     err.statusCode = 404;
 | |
|     throw err;
 | |
|   };
 | |
|   config.restful.createModule = function (req, res, next) {
 | |
|     if (req.params.group === 'domains') {
 | |
|       next();
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     var promise = deps.PromiseA.resolve().then(function () {
 | |
|       var changer = new (require('./config').ConfigChanger)(conf);
 | |
|       var modList = config.extractModList(changer, req.params);
 | |
| 
 | |
|       var update = req.body;
 | |
|       if (!Array.isArray(update)) {
 | |
|         update = [ update ];
 | |
|       }
 | |
|       update.forEach(modList.add, modList);
 | |
| 
 | |
|       return config.save(changer);
 | |
|     }).then(function (newConf) {
 | |
|       return config.extractModList(newConf, req.params);
 | |
|     });
 | |
|     handlePromise(req, res, promise);
 | |
|   };
 | |
|   config.restful.updateModule = function (req, res, next) {
 | |
|     if (req.params.group === 'domains') {
 | |
|       next();
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     var promise = deps.PromiseA.resolve().then(function () {
 | |
|       var changer = new (require('./config').ConfigChanger)(conf);
 | |
|       var modList = config.extractModList(changer, req.params);
 | |
|       modList.update(req.params.modId, req.body);
 | |
|       return config.save(changer);
 | |
|     }).then(function (newConf) {
 | |
|       return config.extractModule(newConf, req.params).find(function (mod) {
 | |
|         return mod.id === req.params.modId;
 | |
|       });
 | |
|     });
 | |
|     handlePromise(req, res, promise);
 | |
|   };
 | |
|   config.restful.removeModule = function (req, res, next) {
 | |
|     if (req.params.group === 'domains') {
 | |
|       next();
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     var promise = deps.PromiseA.resolve().then(function () {
 | |
|       var changer = new (require('./config').ConfigChanger)(conf);
 | |
|       var modList = config.extractModList(changer, req.params);
 | |
|       modList.remove(req.params.modId);
 | |
|       return config.save(changer);
 | |
|     }).then(function (newConf) {
 | |
|       return config.extractModList(newConf, req.params);
 | |
|     });
 | |
|     handlePromise(req, res, promise);
 | |
|   };
 | |
| 
 | |
|   config.restful.createDomain = function (req, res) {
 | |
|     var promise = deps.PromiseA.resolve().then(function () {
 | |
|       var changer = new (require('./config').ConfigChanger)(conf);
 | |
| 
 | |
|       var update = req.body;
 | |
|       if (!Array.isArray(update)) {
 | |
|         update = [ update ];
 | |
|       }
 | |
|       update.forEach(changer.domains.add, changer.domains);
 | |
|       return config.save(changer);
 | |
|     }).then(function (newConf) {
 | |
|       return newConf.domains;
 | |
|     });
 | |
|     handlePromise(req, res, promise);
 | |
|   };
 | |
|   config.restful.updateDomain = function (req, res) {
 | |
|     var promise = deps.PromiseA.resolve().then(function () {
 | |
|       if (req.body.modules) {
 | |
|         throw Object.assign(new Error('do not add modules with this route'), {statusCode: 400});
 | |
|       }
 | |
| 
 | |
|       var changer = new (require('./config').ConfigChanger)(conf);
 | |
|       changer.domains.update(req.params.domId, req.body);
 | |
|       return config.save(changer);
 | |
|     }).then(function (newConf) {
 | |
|       return newConf.domains.find(function (dom) {
 | |
|         return dom.id === req.params.domId;
 | |
|       });
 | |
|     });
 | |
|     handlePromise(req, res, promise);
 | |
|   };
 | |
|   config.restful.removeDomain = function (req, res) {
 | |
|     var promise = deps.PromiseA.resolve().then(function () {
 | |
|       var changer = new (require('./config').ConfigChanger)(conf);
 | |
|       changer.domains.remove(req.params.domId);
 | |
|       return config.save(changer);
 | |
|     }).then(function (newConf) {
 | |
|       return newConf.domains;
 | |
|     });
 | |
|     handlePromise(req, res, promise);
 | |
|   };
 | |
| 
 | |
|   var tokens = { restful: {} };
 | |
|   tokens.restful.getAll = function (req, res) {
 | |
|     handlePromise(req, res, deps.storage.tokens.all());
 | |
|   };
 | |
|   tokens.restful.getOne = function (req, res) {
 | |
|     handlePromise(req, res, deps.storage.tokens.get(req.params.id));
 | |
|   };
 | |
|   tokens.restful.save = function (req, res) {
 | |
|     handlePromise(req, res, deps.storage.tokens.save(req.body));
 | |
|   };
 | |
|   tokens.restful.revoke = function (req, res) {
 | |
|     var promise = deps.storage.tokens.remove(req.params.id).then(function (success) {
 | |
|       return {success: success};
 | |
|     });
 | |
|     handlePromise(req, res, promise);
 | |
|   };
 | |
| 
 | |
| 
 | |
|   var app = require('express')();
 | |
| 
 | |
|   // Handle all of the API endpoints using the old definition style, and then we can
 | |
|   // add middleware without worrying too much about the consequences to older code.
 | |
|   app.use('/:name', handleOldApis);
 | |
| 
 | |
|   // Not all routes support all of these methods, but not worth making this more specific
 | |
|   app.use('/', makeCorsHandler(['GET', 'POST', 'PUT', 'DELETE']), isAuthorized, jsonParser);
 | |
| 
 | |
|   app.get(   '/config',                                                 config.restful.readConfig);
 | |
|   app.get(   '/config/:group',                                          config.restful.readConfig);
 | |
|   app.get(   '/config/:group/:mod(modules)/:modId?',                    config.restful.readConfig);
 | |
|   app.get(   '/config/domains/:domId/:mod(modules)?',                   config.restful.readConfig);
 | |
|   app.get(   '/config/domains/:domId/:mod(modules)/:modGrp/:modId?',    config.restful.readConfig);
 | |
| 
 | |
|   app.post(  '/config',                                       config.restful.saveBaseConfig);
 | |
|   app.post(  '/config/:group',                                config.restful.saveBaseConfig);
 | |
| 
 | |
|   app.post(  '/config/:group/modules',                        config.restful.createModule);
 | |
|   app.put(   '/config/:group/modules/:modId',                 config.restful.updateModule);
 | |
|   app.delete('/config/:group/modules/:modId',                 config.restful.removeModule);
 | |
| 
 | |
|   app.post(  '/config/domains/:domId/modules/:group',         config.restful.createModule);
 | |
|   app.put(   '/config/domains/:domId/modules/:group/:modId',  config.restful.updateModule);
 | |
|   app.delete('/config/domains/:domId/modules/:group/:modId',  config.restful.removeModule);
 | |
| 
 | |
|   app.post(  '/config/domains',                               config.restful.createDomain);
 | |
|   app.put(   '/config/domains/:domId',                        config.restful.updateDomain);
 | |
|   app.delete('/config/domains/:domId',                        config.restful.removeDomain);
 | |
| 
 | |
|   app.get(   '/tokens',         tokens.restful.getAll);
 | |
|   app.get(   '/tokens/:id',     tokens.restful.getOne);
 | |
|   app.post(  '/tokens',         tokens.restful.save);
 | |
|   app.delete('/tokens/:id',     tokens.restful.revoke);
 | |
| 
 | |
|   return app;
 | |
| };
 |