'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'); 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 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; }); }) ; } return { 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 } })); }); }); } , tunnel: function (req, res) { if (handleCors(req, res)) { return; } isAuthorized(req, res, function () { if ('POST' !== req.method) { res.setHeader('Content-Type', 'application/json'); return deps.tunnelClients.get(req.userId).then(function (result) { res.end(JSON.stringify(result)); }, function (err) { res.statusCode = 500; res.end(JSON.stringify({ error: { message: err.message, code: err.code, uri: err.uri } })); }); } return deps.storage.owners.get(req.userId).then(function (session) { return deps.tunnelClients.start(session).then(function () { res.setHeader('Content-Type', 'application/json;'); res.end(JSON.stringify({ success: true })); }, function (err) { res.setHeader('Content-Type', 'application/json;'); res.statusCode = 500; res.end(JSON.stringify({ error: { message: err.message, code: err.code, uri: err.uri } })); }); }); }); } , config: function (req, res) { if (handleCors(req, res)) { return; } isAuthorized(req, res, function () { if ('POST' !== req.method) { res.setHeader('Content-Type', 'application/json;'); res.end(JSON.stringify(deps.recase.snakeCopy(conf))); return; } jsonParser(req, res, function () { console.log('config POST body', req.body); // Since we are sending the changes to another process we don't really // have a good way of seeing if it worked, so always report success deps.storage.config.save(req.body); res.setHeader('Content-Type', 'application/json;'); res.end('{"success":true}'); }); }); } , 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); }); }); }); } , loopback: function (req, res) { if (handleCors(req, res, 'GET')) { return; } isAuthorized(req, res, function () { var prom; var query = require('querystring').parse(require('url').parse(req.url).query); if (query.provider) { prom = deps.loopback(query.provider); } else { prom = deps.storage.owners.get(req.userId).then(function (session) { return deps.loopback(session.token.aud); }); } res.setHeader('Content-Type', 'application/json'); prom.then(function (result) { res.end(JSON.stringify(result)); }, function (err) { res.end(JSON.stringify({error: {message: err.message, code: err.code}})); }); }); } , 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}})); }); }); } }; };