MAJOR: Updates for Authenticated Web UI and CLI #30
|
@ -13,6 +13,7 @@ var YAML = require('js-yaml');
|
||||||
var TOML = require('toml');
|
var TOML = require('toml');
|
||||||
var TPLS = TOML.parse(fs.readFileSync(path.join(__dirname, "../lib/en-us.toml"), 'utf8'));
|
var TPLS = TOML.parse(fs.readFileSync(path.join(__dirname, "../lib/en-us.toml"), 'utf8'));
|
||||||
var JWT = require('../lib/jwt.js');
|
var JWT = require('../lib/jwt.js');
|
||||||
|
var keypairs = require('keypairs');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
if ('function' !== typeof TOML.stringify) {
|
if ('function' !== typeof TOML.stringify) {
|
||||||
|
@ -766,17 +767,18 @@ var keyname = 'telebit-remote';
|
||||||
state.keystore = keystore;
|
state.keystore = keystore;
|
||||||
state.keystoreSecure = !keystore.insecure;
|
state.keystoreSecure = !keystore.insecure;
|
||||||
keystore.get(keyname).then(function (key) {
|
keystore.get(keyname).then(function (key) {
|
||||||
if (key && key.kty) {
|
if (key && key.kty && key.kid) {
|
||||||
state.key = key;
|
state.key = key;
|
||||||
|
state.pub = keypairs.neuter({ jwk: key });
|
||||||
fs.readFile(confpath, 'utf8', parseConfig);
|
fs.readFile(confpath, 'utf8', parseConfig);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var keypairs = require('keypairs');
|
|
||||||
return keypairs.generate().then(function (pair) {
|
return keypairs.generate().then(function (pair) {
|
||||||
var jwk = pair.private;
|
var jwk = pair.private;
|
||||||
return keystore.set(keyname, jwk).then(function () {
|
return keypairs.thumbprint({ jwk: pair.public }).then(function (kid) {
|
||||||
return keypairs.thumbprint({ jwk: pair.public }).then(function (kid) {
|
jwk.kid = kid;
|
||||||
|
return keystore.set(keyname, jwk).then(function () {
|
||||||
var size = (jwk.crv || Buffer.from(jwk.n, 'base64').byteLength * 8);
|
var size = (jwk.crv || Buffer.from(jwk.n, 'base64').byteLength * 8);
|
||||||
console.info("Generated new %s %s private key with thumbprint %s", jwk.kty, size, kid);
|
console.info("Generated new %s %s private key with thumbprint %s", jwk.kty, size, kid);
|
||||||
state.key = jwk;
|
state.key = jwk;
|
||||||
|
|
149
bin/telebitd.js
149
bin/telebitd.js
|
@ -11,7 +11,7 @@ try {
|
||||||
|
|
||||||
var pkg = require('../package.json');
|
var pkg = require('../package.json');
|
||||||
|
|
||||||
var url = require('url');
|
//var url = require('url');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var os = require('os');
|
var os = require('os');
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
|
@ -374,46 +374,123 @@ controllers.relay = function (req, res) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function jsonEggspress(req, res, next) {
|
||||||
|
/*
|
||||||
|
var opts = url.parse(req.url, true);
|
||||||
|
if (false && opts.query._body) {
|
||||||
|
try {
|
||||||
|
req.body = JSON.parse(decodeURIComponent(opts.query._body, true));
|
||||||
|
} catch(e) {
|
||||||
|
res.statusCode = 500;
|
||||||
|
res.end('{"error":{"message":"?_body={{bad_format}}"}}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
var hasLength = req.headers['content-length'] > 0;
|
||||||
|
if (!hasLength && !req.headers['content-type']) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = '';
|
||||||
|
req.on('readable', function () {
|
||||||
|
var data;
|
||||||
|
while (true) {
|
||||||
|
data = req.read();
|
||||||
|
if (!data) { break; }
|
||||||
|
body += data.toString();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
req.on('end', function () {
|
||||||
|
try {
|
||||||
|
req.body = JSON.parse(body);
|
||||||
|
} catch(e) {
|
||||||
|
res.statusCode = 400;
|
||||||
|
res.end('{"error":{"message":"POST body is not valid json"}}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeJwt(jwt) {
|
||||||
|
var parts = jwt.split('.');
|
||||||
|
var jws = {
|
||||||
|
protected: parts[0]
|
||||||
|
, payload: parts[0]
|
||||||
|
, signature: parts[2] //Buffer.from(parts[2], 'base64')
|
||||||
|
};
|
||||||
|
jws.header = JSON.parse(Buffer.from(jws.protected, 'base64'));
|
||||||
|
jws.claims = JSON.parse(Buffer.from(jws.payload, 'base64'));
|
||||||
|
return jws;
|
||||||
|
}
|
||||||
|
function jwtEggspress(req, res, next) {
|
||||||
|
var jwt = (req.headers.authorization||'').replace(/Bearer /i, '');
|
||||||
|
if (!jwt) { next(); return; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
req.jwt = decodeJwt(jwt);
|
||||||
|
} catch(e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO verify if possible
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyJws(jwk, jws) {
|
||||||
|
return require('keypairs').export({ jwk: jwk }).then(function (pem) {
|
||||||
|
var alg = 'RSA-SHA' + jws.header.alg.replace(/[^\d]+/i, '');
|
||||||
|
// XXX
|
||||||
|
// TODO check for public key in keytar
|
||||||
|
// XXX
|
||||||
|
return require('crypto')
|
||||||
|
.createVerify(alg)
|
||||||
|
.update(jws.protected + '.' + jws.payload)
|
||||||
|
.verify(pem, jws.signature, 'base64');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function jwsEggspress(req, res, next) {
|
||||||
|
// TODO check header application/jose+json ??
|
||||||
|
if (!req.body || !(req.body.protected && req.body.payload && req.body.signature)) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
req.jws = req.body;
|
||||||
|
req.jws.header = JSON.parse(Buffer.from(req.jws.protected, 'base64'));
|
||||||
|
req.body = Buffer.from(req.jws.payload, 'base64');
|
||||||
|
if ('{'.charCodeAt(0) === req.body[0] || '['.charCodeAt(0) === req.body[0]) {
|
||||||
|
req.body = JSON.parse(req.body);
|
||||||
|
}
|
||||||
|
if (req.jws.header.jwk) {
|
||||||
|
verifyJws(req.jws.header.jwk, req.jws).then(function (verified) {
|
||||||
|
req.jws.selfVerified = verified;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO verify if possible
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
function handleApi() {
|
function handleApi() {
|
||||||
var app = eggspress();
|
var app = eggspress();
|
||||||
|
|
||||||
|
app.use('/', jwtEggspress);
|
||||||
|
app.use('/', jsonEggspress);
|
||||||
|
app.use('/', jwsEggspress);
|
||||||
app.use('/', function (req, res, next) {
|
app.use('/', function (req, res, next) {
|
||||||
var opts = url.parse(req.url, true);
|
if (req.jwt) {
|
||||||
if (false && opts.query._body) {
|
console.log('jwt', req.jwt);
|
||||||
try {
|
} else if (req.jws) {
|
||||||
req.body = JSON.parse(decodeURIComponent(opts.query._body, true));
|
console.log('jws', req.jws);
|
||||||
} catch(e) {
|
console.log('body', req.body);
|
||||||
res.statusCode = 500;
|
|
||||||
res.end('{"error":{"message":"?_body={{bad_format}}"}}');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
next();
|
||||||
var hasLength = req.headers['content-length'] > 0;
|
|
||||||
if (!hasLength && !req.headers['content-type']) {
|
|
||||||
next();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var body = '';
|
|
||||||
req.on('readable', function () {
|
|
||||||
var data;
|
|
||||||
while (true) {
|
|
||||||
data = req.read();
|
|
||||||
if (!data) { break; }
|
|
||||||
body += data.toString();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
req.on('end', function () {
|
|
||||||
try {
|
|
||||||
req.body = JSON.parse(body);
|
|
||||||
} catch(e) {
|
|
||||||
res.statusCode = 400;
|
|
||||||
res.end('{"error":{"message":"POST body is not valid json"}}');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function listSuccess(req, res) {
|
function listSuccess(req, res) {
|
||||||
|
|
|
@ -5,7 +5,11 @@ module.exports = function eggspress() {
|
||||||
var allPatterns = [];
|
var allPatterns = [];
|
||||||
var app = function (req, res) {
|
var app = function (req, res) {
|
||||||
var patterns = allPatterns.slice(0).reverse();
|
var patterns = allPatterns.slice(0).reverse();
|
||||||
function next() {
|
function next(err) {
|
||||||
|
if (err) {
|
||||||
|
req.end(err.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
var todo = patterns.pop();
|
var todo = patterns.pop();
|
||||||
if (!todo) {
|
if (!todo) {
|
||||||
console.log('[eggspress] Did not match any patterns', req.url);
|
console.log('[eggspress] Did not match any patterns', req.url);
|
||||||
|
|
|
@ -3,9 +3,11 @@
|
||||||
var os = require('os');
|
var os = require('os');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var http = require('http');
|
var http = require('http');
|
||||||
|
var keypairs = require('keypairs');
|
||||||
|
|
||||||
var common = require('../cli-common.js');
|
var common = require('../cli-common.js');
|
||||||
|
|
||||||
|
/*
|
||||||
function packConfig(config) {
|
function packConfig(config) {
|
||||||
return Object.keys(config).map(function (key) {
|
return Object.keys(config).map(function (key) {
|
||||||
var val = config[key];
|
var val = config[key];
|
||||||
|
@ -22,6 +24,7 @@ function packConfig(config) {
|
||||||
return key + ':' + val; // converts arrays to strings with ,
|
return key + ':' + val; // converts arrays to strings with ,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
module.exports.create = function (state) {
|
module.exports.create = function (state) {
|
||||||
common._init(
|
common._init(
|
||||||
|
@ -72,16 +75,20 @@ module.exports.create = function (state) {
|
||||||
RC.request = function request(opts, fn) {
|
RC.request = function request(opts, fn) {
|
||||||
if (!opts) { opts = {}; }
|
if (!opts) { opts = {}; }
|
||||||
var service = opts.service || 'config';
|
var service = opts.service || 'config';
|
||||||
|
/*
|
||||||
var args = opts.data;
|
var args = opts.data;
|
||||||
if (args && 'control' === service) {
|
if (args && 'control' === service) {
|
||||||
args = packConfig(args);
|
args = packConfig(args);
|
||||||
}
|
}
|
||||||
var json = JSON.stringify(args);
|
var json = JSON.stringify(opts.data);
|
||||||
|
*/
|
||||||
var url = '/rpc/' + service;
|
var url = '/rpc/' + service;
|
||||||
|
/*
|
||||||
if (json) {
|
if (json) {
|
||||||
url += ('?_body=' + encodeURIComponent(json));
|
url += ('?_body=' + encodeURIComponent(json));
|
||||||
}
|
}
|
||||||
var method = opts.method || (args && 'POST') || 'GET';
|
*/
|
||||||
|
var method = opts.method || (opts.data && 'POST') || 'GET';
|
||||||
var reqOpts = {
|
var reqOpts = {
|
||||||
method: method
|
method: method
|
||||||
, path: url
|
, path: url
|
||||||
|
@ -124,11 +131,33 @@ module.exports.create = function (state) {
|
||||||
|
|
||||||
fn(err);
|
fn(err);
|
||||||
});
|
});
|
||||||
if ('POST' === method && opts.data) {
|
|
||||||
req.setHeader("content-type", 'application/json');
|
// Simple GET
|
||||||
req.write(json || opts.data);
|
if ('POST' !== method || !opts.data) {
|
||||||
|
return keypairs.signJwt({
|
||||||
|
jwk: state.key
|
||||||
|
, claims: { iss: false, exp: Math.round(Date.now()/1000) + (15 * 60) }
|
||||||
|
//TODO , exp: '15m'
|
||||||
|
}).then(function (jwt) {
|
||||||
|
req.setHeader("authorization", 'bearer ' + jwt);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
req.end();
|
|
||||||
|
return keypairs.signJws({
|
||||||
|
jwk: state.key
|
||||||
|
, protected: {
|
||||||
|
// alg will be filled out automatically
|
||||||
|
jwk: state.pub
|
||||||
|
, nonce: require('crypto').randomBytes(16).toString('hex') // TODO get from server
|
||||||
|
, url: 'https://' + reqOpts.host + reqOpts.path
|
||||||
|
}
|
||||||
|
, payload: JSON.stringify(opts.data)
|
||||||
|
}).then(function (jws) {
|
||||||
|
req.setHeader("content-type", 'application/json');
|
||||||
|
req.write(JSON.stringify(jws));
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
return RC;
|
return RC;
|
||||||
};
|
};
|
||||||
|
|
|
@ -430,9 +430,9 @@
|
||||||
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
|
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
|
||||||
},
|
},
|
||||||
"keypairs": {
|
"keypairs": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.6.tgz",
|
||||||
"integrity": "sha512-VKUxQ4iQB5LvVMtObOzNmZRfgXLTr5GMr+wg9A2BnILArBLrtg/DIuWRJQpDNRRfAGRQjHXxSVOW+7xpzIAY1Q==",
|
"integrity": "sha512-sJDaZvJqHWUawJjrOGKJvKGLfPh0eo2WV7td4RSL88w3BjPYCYI9PkqBn0hLqc6uw0HFSqZMikhGn/jgPpcWnQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"eckles": "^1.4.1",
|
"eckles": "^1.4.1",
|
||||||
"rasha": "^1.2.4"
|
"rasha": "^1.2.4"
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
"finalhandler": "^1.1.1",
|
"finalhandler": "^1.1.1",
|
||||||
"greenlock": "^2.6.7",
|
"greenlock": "^2.6.7",
|
||||||
"js-yaml": "^3.11.0",
|
"js-yaml": "^3.11.0",
|
||||||
"keypairs": "^1.2.5",
|
"keypairs": "^1.2.6",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
"proxy-packer": "^2.0.2",
|
"proxy-packer": "^2.0.2",
|
||||||
"ps-list": "^5.0.0",
|
"ps-list": "^5.0.0",
|
||||||
|
|
Loading…
Reference in New Issue