'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; };