311 lines
10 KiB
JavaScript
311 lines
10 KiB
JavaScript
'use strict';
|
|
|
|
module.exports.debug = (-1 !== (process.env.NODE_DEBUG||'').split(/\s+/g).indexOf('telebit'));
|
|
var common = module.exports;
|
|
|
|
var path = require('path');
|
|
var url = require('url');
|
|
var fs = require('fs');
|
|
var mkdirp = require('mkdirp');
|
|
var os = require('os');
|
|
var homedir = os.homedir();
|
|
var urequest = require('@root/request');
|
|
|
|
common._NOTIFICATIONS = {
|
|
'newsletter': [ 'newsletter', 'communityMember' ]
|
|
, 'important': [ 'communityMember' ]
|
|
};
|
|
common.CONFIG_KEYS = [
|
|
'newsletter'
|
|
, 'communityMember'
|
|
, 'telemetry'
|
|
, 'sshAuto'
|
|
, 'email'
|
|
, 'agreeTos'
|
|
, 'relay'
|
|
, 'token'
|
|
, 'pretoken'
|
|
, 'secret'
|
|
];
|
|
//, '_servernames' // list instead of object
|
|
//, '_ports' // list instead of object
|
|
//, '_otp' // otp should not be saved
|
|
//, '_token' // temporary token
|
|
|
|
common.getPort = function (config, cb) {
|
|
var portfile = path.resolve(config.sock || common.DEFAULT_SOCK_PATH, '..', 'telebit.port');
|
|
if (cb) {
|
|
return fs.readFile(portfile, 'utf8', function (err, text) {
|
|
cb(err, parseInt((text||'').trim(), 10) || null);
|
|
});
|
|
} else {
|
|
try {
|
|
return parseInt(fs.readFileSync(portfile, 'utf8').trim(), 10) || null;
|
|
} catch(e) {
|
|
return null;
|
|
}
|
|
}
|
|
};
|
|
common.setPort = function (config, num, cb) {
|
|
var portfile = path.resolve(config.sock || common.DEFAULT_SOCK_PATH, '..', 'telebit.port');
|
|
var numstr = (num || '').toString();
|
|
if (cb) {
|
|
return fs.writeFile(portfile, numstr, 'utf8', function (err) {
|
|
cb(err);
|
|
});
|
|
} else {
|
|
try {
|
|
return fs.writeFileSync(portfile, numstr, 'utf8');
|
|
} catch(e) {
|
|
return null;
|
|
}
|
|
}
|
|
};
|
|
common.removePort = function (config, cb) {
|
|
var portfile = path.resolve(config.sock || common.DEFAULT_SOCK_PATH, '..', 'telebit.port');
|
|
if (cb) {
|
|
return fs.unlink(portfile, function (err, text) {
|
|
cb(err, (text||'').trim());
|
|
});
|
|
} else {
|
|
try {
|
|
return fs.unlinkSync(portfile);
|
|
} catch(e) {
|
|
return null;
|
|
}
|
|
}
|
|
};
|
|
common.pipename = function (config) {
|
|
var _ipc = {
|
|
path: (config.sock || common.DEFAULT_SOCK_PATH)
|
|
, comment: (/^win/i.test(os.platform()) ? 'windows pipe' : 'unix socket')
|
|
, type: (/^win/i.test(os.platform()) ? 'pipe' : 'socket')
|
|
};
|
|
if ('pipe' === _ipc.type) {
|
|
// https://docs.microsoft.com/en-us/windows/desktop/ipc/pipe-names
|
|
// Allows all characters accept backslash as part of the name
|
|
_ipc.path = '\\\\.\\pipe\\' + _ipc.path.replace(/\\/g, '/');
|
|
}
|
|
return _ipc;
|
|
};
|
|
common.DEFAULT_SOCK_PATH = path.join(homedir, '.local/share/telebit/var/run', 'telebit.sock');
|
|
common.DEFAULT_CONFIG_PATH = path.join(homedir, '.config/telebit', 'telebitd.yml');
|
|
|
|
common.parseUrl = function (hostname) {
|
|
var location = url.parse(hostname);
|
|
if (!location.protocol || /\./.test(location.protocol)) {
|
|
hostname = 'https://' + hostname;
|
|
location = url.parse(hostname);
|
|
}
|
|
hostname = location.hostname + (location.port ? ':' + location.port : '');
|
|
hostname = location.protocol.replace(/https?/, 'https') + '//' + hostname + location.pathname;
|
|
return hostname;
|
|
};
|
|
common.parseHostname = function (hostname) {
|
|
var location = url.parse(hostname);
|
|
if (!location.protocol || /\./.test(location.protocol)) {
|
|
hostname = 'https://' + hostname;
|
|
location = url.parse(hostname);
|
|
}
|
|
//hostname = location.hostname + (location.port ? ':' + location.port : '');
|
|
//hostname = location.protocol.replace(/https?/, 'https') + '//' + hostname + location.pathname;
|
|
return location.hostname;
|
|
};
|
|
|
|
common.apiDirectory = '_apis/telebit.cloud/index.json';
|
|
|
|
common.otp = function getOtp() {
|
|
return Math.round(Math.random() * 9999).toString().padStart(4, '0');
|
|
};
|
|
common.signToken = function (state) {
|
|
var JWT = require('./jwt.js');
|
|
var tokenData = {
|
|
domains: Object.keys(state.config.servernames || {}).filter(function (name) {
|
|
return /\./.test(name);
|
|
})
|
|
, ports: Object.keys(state.config.ports || {}).filter(function (port) {
|
|
port = parseInt(port, 10);
|
|
return port > 0 && port <= 65535;
|
|
})
|
|
, aud: state._relayUrl
|
|
, iss: Math.round(Date.now() / 1000)
|
|
};
|
|
|
|
return JWT.sign(tokenData, state.config.secret);
|
|
};
|
|
common.api = {};
|
|
common.api.directory = function (state, next) {
|
|
state._relayUrl = common.parseUrl(state.relay);
|
|
urequest({ url: state._relayUrl + common.apiDirectory, json: true }, function (err, resp, dir) {
|
|
if (!dir) { dir = { api_host: ':hostname', tunnel: { method: "wss", pathname: "" } }; }
|
|
state._apiDirectory = dir;
|
|
next(err, dir);
|
|
});
|
|
};
|
|
common.api._parseWss = function (state, dir) {
|
|
if (!dir || !dir.api_host) {
|
|
dir = { api_host: ':hostname', tunnel: { method: "wss", pathname: "" } };
|
|
}
|
|
state._relayHostname = common.parseHostname(state.relay);
|
|
return dir.tunnel.method + '://' + dir.api_host.replace(/:hostname/g, state._relayHostname) + dir.tunnel.pathname;
|
|
};
|
|
common.api.wss = function (state, cb) {
|
|
common.api.directory(state, function (err, dir) {
|
|
cb(err, common.api._parseWss(state, dir));
|
|
});
|
|
};
|
|
common.api.token = function (state, handlers) {
|
|
common.api.directory(state, function (err, dir) {
|
|
// directory, requested, connect, tunnelUrl, offer, granted, end
|
|
function afterDir() {
|
|
if (common.debug) { console.log('[debug] after dir'); }
|
|
state.wss = common.api._parseWss(state, dir);
|
|
|
|
handlers.tunnelUrl(state.wss, function () {
|
|
if (common.debug) { console.log('[debug] after tunnelUrl'); }
|
|
if (state.config.secret /* && !state.config.token */) {
|
|
state.config._token = common.signToken(state);
|
|
}
|
|
state.token = state.token || state.config.token || state.config._token;
|
|
if (state.token) {
|
|
if (common.debug) { console.log('[debug] token via token or secret'); }
|
|
// { token, pretoken }
|
|
handlers.connect(state.token, function () {
|
|
handlers.end(null, function () {});
|
|
});
|
|
return;
|
|
}
|
|
|
|
// backwards compat (TODO remove)
|
|
if (err || !dir || !dir.pair_request) {
|
|
if (common.debug) { console.log('[debug] no dir, connect'); }
|
|
handlers.error(new Error("No token found or generated, and no pair_request api found."));
|
|
return;
|
|
}
|
|
|
|
// TODO sign token with own private key, including public key and thumbprint
|
|
// (much like ACME JOSE account)
|
|
var otp = state.config._otp; // common.otp();
|
|
var authReq = {
|
|
subject: state.config.email
|
|
, subject_scheme: 'mailto'
|
|
// TODO create domains list earlier
|
|
, scope: (state.config._servernames || Object.keys(state.config.servernames || {}))
|
|
.concat(state.config._ports || Object.keys(state.config.ports || {})).join(',')
|
|
, otp: otp
|
|
, hostname: os.hostname()
|
|
// Used for User-Agent
|
|
, os_type: os.type()
|
|
, os_platform: os.platform()
|
|
, os_release: os.release()
|
|
, os_arch: os.arch()
|
|
};
|
|
var pairRequestUrl = url.resolve('https://' + dir.api_host.replace(/:hostname/g, state._relayHostname), dir.pair_request.pathname);
|
|
var req = {
|
|
url: pairRequestUrl
|
|
, method: dir.pair_request.method
|
|
, json: authReq
|
|
};
|
|
var firstReq = true;
|
|
var firstReady = true;
|
|
|
|
function gotoNext(req) {
|
|
if (common.debug) { console.log('[debug] gotoNext called'); }
|
|
if (common.debug) { console.log(req); }
|
|
urequest(req, function (err, resp, body) {
|
|
if (err) {
|
|
if (common.debug) { console.log('[debug] gotoNext error'); }
|
|
err._request = req;
|
|
err._hint = '[telebitd.js] pair request';
|
|
handlers.error(err, function () {});
|
|
return;
|
|
}
|
|
|
|
function checkLocation() {
|
|
if (common.debug) { console.log('[debug] checkLocation'); }
|
|
if (common.debug) { console.log(body); }
|
|
// pending, try again
|
|
if ('pending' === body.status && resp.headers.location) {
|
|
if (common.debug) { console.log('[debug] pending'); }
|
|
setTimeout(gotoNext, 2 * 1000, { url: resp.headers.location, json: true });
|
|
return;
|
|
}
|
|
|
|
if ('ready' === body.status) {
|
|
if (common.debug) { console.log('[debug] ready'); }
|
|
if (firstReady) {
|
|
if (common.debug) { console.log('[debug] first ready'); }
|
|
firstReady = false;
|
|
state.token = body.access_token;
|
|
state.config.token = state.token;
|
|
handlers.offer(body.access_token, function () {
|
|
/*ignore*/
|
|
});
|
|
}
|
|
setTimeout(gotoNext, 2 * 1000, req);
|
|
return;
|
|
}
|
|
|
|
if ('complete' === body.status) {
|
|
if (common.debug) { console.log('[debug] complete'); }
|
|
handlers.granted(null, function () {
|
|
handlers.end(null, function () {});
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (common.debug) { console.log('[debug] bad status'); }
|
|
var err = new Error("Bad State:" + body.status);
|
|
err._request = req;
|
|
handlers.error(err, function () {});
|
|
}
|
|
|
|
if (firstReq) {
|
|
if (common.debug) { console.log('[debug] first req'); }
|
|
handlers.requested(authReq, function () {
|
|
handlers.connect(body.access_token || body.jwt, function () {
|
|
var err;
|
|
if (!resp.headers.location) {
|
|
err = new Error("bad authentication request response");
|
|
err._resp = resp.toJSON();
|
|
handlers.error(err, function () {});
|
|
return;
|
|
}
|
|
setTimeout(gotoNext, 2 * 1000, { url: resp.headers.location, json: true });
|
|
});
|
|
});
|
|
firstReq = false;
|
|
return;
|
|
} else {
|
|
if (common.debug) { console.log('[debug] other req'); }
|
|
checkLocation();
|
|
}
|
|
});
|
|
}
|
|
|
|
gotoNext(req);
|
|
|
|
});
|
|
}
|
|
|
|
if (dir && dir.api_host) {
|
|
handlers.directory(dir, afterDir);
|
|
} else {
|
|
// backwards compat
|
|
dir = { api_host: ':hostname', tunnel: { method: "wss", pathname: "" } };
|
|
afterDir();
|
|
}
|
|
});
|
|
|
|
};
|
|
|
|
common._init = function (rootpath, confpath) {
|
|
try {
|
|
mkdirp.sync(path.join(rootpath, 'var', 'log'));
|
|
mkdirp.sync(path.join(rootpath, 'var', 'run'));
|
|
mkdirp.sync(path.join(confpath));
|
|
} catch(e) {
|
|
console.error(e);
|
|
}
|
|
};
|