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; | ||||||
| }; | }; | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user