goldilocks.js/lib/ddns/loopback.js

117 lines
3.5 KiB
JavaScript

'use strict';
module.exports.create = function (deps, conf) {
var pending = {};
async function _checkPublicAddr(host) {
var result = await deps.request({
method: 'GET'
, url: deps.OAUTH3.url.normalize(host)+'/api/org.oauth3.tunnel/checkip'
, json: true
});
if (!result.body) {
throw new Error('No response body in request for public address');
}
if (result.body.error) {
// Note that the error on the body will probably have a message that overwrites the default
throw Object.assign(new Error('error in check IP response'), result.body.error);
}
if (!result.body.address) {
throw new Error("public address resonse doesn't contain address: "+JSON.stringify(result.body));
}
return result.body.address;
}
async function checkPublicAddr(provider) {
var directives = await deps.OAUTH3.discover(provider);
return _checkPublicAddr(directives.api);
}
async function checkSinglePort(host, address, port) {
var crypto = require('crypto');
var token = crypto.randomBytes(8).toString('hex');
var keyAuth = crypto.randomBytes(32).toString('hex');
pending[token] = keyAuth;
var reqObj = {
method: 'POST'
, url: deps.OAUTH3.url.normalize(host)+'/api/org.oauth3.tunnel/loopback'
, timeout: 20*1000
, json: {
address: address
, port: port
, token: token
, keyAuthorization: keyAuth
, iat: Date.now()
, timeout: 18*1000
}
};
var result;
try {
result = await deps.request(reqObj);
} catch (err) {
delete pending[token];
if (conf.debug) {
console.log('error making loopback request for port ' + port + ' loopback', err.message);
}
return false;
}
delete pending[token];
if (!result.body) {
if (conf.debug) {
console.log('No response body in loopback request for port '+port);
}
return false;
}
// If the loopback requests don't go to us then there are all kinds of ways it could
// error, but none of them really provide much extra information so we don't do
// anything that will break the PromiseA.all out and mask the other results.
if (conf.debug && result.body.error) {
console.log('error on remote side of port '+port+' loopback', result.body.error);
}
return !!result.body.success;
}
async function loopback(provider) {
var directives = await deps.OAUTH3.discover(provider);
var address = await _checkPublicAddr(directives.api);
if (conf.debug) {
console.log('checking to see if', address, 'gets back to us');
}
var ports = require('../servers').listeners.tcp.list();
var values = await deps.PromiseA.all(ports.map(function (port) {
return checkSinglePort(directives.api, address, port);
}));
if (conf.debug && Object.keys(pending).length) {
console.log('remaining loopback tokens', pending);
}
return {
address: address
, ports: ports.reduce(function (obj, port, ind) {
obj[port] = values[ind];
return obj;
}, {})
};
}
loopback.checkPublicAddr = checkPublicAddr;
loopback.server = require('http').createServer(function (req, res) {
var parsed = require('url').parse(req.url);
var token = parsed.pathname.replace('/.well-known/cloud-challenge/', '');
if (pending[token]) {
res.setHeader('Content-Type', 'text/plain');
res.end(pending[token]);
} else {
res.statusCode = 404;
res.end();
}
});
return loopback;
};