MAJOR: Updates for Authenticated Web UI and CLI #30
| @ -13,6 +13,7 @@ var YAML = require('js-yaml'); | ||||
| var TOML = require('toml'); | ||||
| var TPLS = TOML.parse(fs.readFileSync(path.join(__dirname, "../lib/en-us.toml"), 'utf8')); | ||||
| var JWT = require('../lib/jwt.js'); | ||||
| var keypairs = require('keypairs'); | ||||
| 
 | ||||
| /* | ||||
| if ('function' !== typeof TOML.stringify) { | ||||
| @ -766,17 +767,18 @@ var keyname = 'telebit-remote'; | ||||
| state.keystore = keystore; | ||||
| state.keystoreSecure = !keystore.insecure; | ||||
| keystore.get(keyname).then(function (key) { | ||||
|   if (key && key.kty) { | ||||
|   if (key && key.kty && key.kid) { | ||||
|     state.key = key; | ||||
|     state.pub = keypairs.neuter({ jwk: key }); | ||||
|     fs.readFile(confpath, 'utf8', parseConfig); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   var keypairs = require('keypairs'); | ||||
|   return keypairs.generate().then(function (pair) { | ||||
|     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); | ||||
|         console.info("Generated new %s %s private key with thumbprint %s", jwk.kty, size, kid); | ||||
|         state.key = jwk; | ||||
|  | ||||
							
								
								
									
										149
									
								
								bin/telebitd.js
									
									
									
									
									
								
							
							
						
						
									
										149
									
								
								bin/telebitd.js
									
									
									
									
									
								
							| @ -11,7 +11,7 @@ try { | ||||
| 
 | ||||
| var pkg = require('../package.json'); | ||||
| 
 | ||||
| var url = require('url'); | ||||
| //var url = require('url');
 | ||||
| var path = require('path'); | ||||
| var os = require('os'); | ||||
| 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() { | ||||
|   var app = eggspress(); | ||||
| 
 | ||||
|   app.use('/', jwtEggspress); | ||||
|   app.use('/', jsonEggspress); | ||||
|   app.use('/', jwsEggspress); | ||||
|   app.use('/', function (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; | ||||
|       } | ||||
|     if (req.jwt) { | ||||
|       console.log('jwt', req.jwt); | ||||
|     } else if (req.jws) { | ||||
|       console.log('jws', req.jws); | ||||
|       console.log('body', req.body); | ||||
|     } | ||||
| 
 | ||||
|     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(); | ||||
|     }); | ||||
|     next(); | ||||
|   }); | ||||
| 
 | ||||
|   function listSuccess(req, res) { | ||||
|  | ||||
| @ -5,7 +5,11 @@ module.exports = function eggspress() { | ||||
|   var allPatterns = []; | ||||
|   var app = function (req, res) { | ||||
|     var patterns = allPatterns.slice(0).reverse(); | ||||
|     function next() { | ||||
|     function next(err) { | ||||
|       if (err) { | ||||
|         req.end(err.message); | ||||
|         return; | ||||
|       } | ||||
|       var todo = patterns.pop(); | ||||
|       if (!todo) { | ||||
|         console.log('[eggspress] Did not match any patterns', req.url); | ||||
|  | ||||
| @ -3,9 +3,11 @@ | ||||
| var os = require('os'); | ||||
| var path = require('path'); | ||||
| var http = require('http'); | ||||
| var keypairs = require('keypairs'); | ||||
| 
 | ||||
| var common = require('../cli-common.js'); | ||||
| 
 | ||||
| /* | ||||
| function packConfig(config) { | ||||
|   return Object.keys(config).map(function (key) { | ||||
|     var val = config[key]; | ||||
| @ -22,6 +24,7 @@ function packConfig(config) { | ||||
|     return key + ':' + val; // converts arrays to strings with ,
 | ||||
|   }); | ||||
| } | ||||
| */ | ||||
| 
 | ||||
| module.exports.create = function (state) { | ||||
|   common._init( | ||||
| @ -72,16 +75,20 @@ module.exports.create = function (state) { | ||||
|   RC.request = function request(opts, fn) { | ||||
|     if (!opts) { opts = {}; } | ||||
|     var service = opts.service || 'config'; | ||||
|     /* | ||||
|     var args = opts.data; | ||||
|     if (args && 'control' === service) { | ||||
|       args = packConfig(args); | ||||
|     } | ||||
|     var json = JSON.stringify(args); | ||||
|     var json = JSON.stringify(opts.data); | ||||
|     */ | ||||
|     var url = '/rpc/' + service; | ||||
|     /* | ||||
|     if (json) { | ||||
|       url += ('?_body=' + encodeURIComponent(json)); | ||||
|     } | ||||
|     var method = opts.method || (args && 'POST') || 'GET'; | ||||
|     */ | ||||
|     var method = opts.method || (opts.data && 'POST') || 'GET'; | ||||
|     var reqOpts = { | ||||
|       method: method | ||||
|     , path: url | ||||
| @ -124,11 +131,33 @@ module.exports.create = function (state) { | ||||
| 
 | ||||
|       fn(err); | ||||
|     }); | ||||
|     if ('POST' === method && opts.data) { | ||||
|       req.setHeader("content-type", 'application/json'); | ||||
|       req.write(json || opts.data); | ||||
| 
 | ||||
|     // Simple GET
 | ||||
|     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; | ||||
| }; | ||||
|  | ||||
							
								
								
									
										6
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -430,9 +430,9 @@ | ||||
|       "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" | ||||
|     }, | ||||
|     "keypairs": { | ||||
|       "version": "1.2.5", | ||||
|       "resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.5.tgz", | ||||
|       "integrity": "sha512-VKUxQ4iQB5LvVMtObOzNmZRfgXLTr5GMr+wg9A2BnILArBLrtg/DIuWRJQpDNRRfAGRQjHXxSVOW+7xpzIAY1Q==", | ||||
|       "version": "1.2.6", | ||||
|       "resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.6.tgz", | ||||
|       "integrity": "sha512-sJDaZvJqHWUawJjrOGKJvKGLfPh0eo2WV7td4RSL88w3BjPYCYI9PkqBn0hLqc6uw0HFSqZMikhGn/jgPpcWnQ==", | ||||
|       "requires": { | ||||
|         "eckles": "^1.4.1", | ||||
|         "rasha": "^1.2.4" | ||||
|  | ||||
| @ -57,7 +57,7 @@ | ||||
|     "finalhandler": "^1.1.1", | ||||
|     "greenlock": "^2.6.7", | ||||
|     "js-yaml": "^3.11.0", | ||||
|     "keypairs": "^1.2.5", | ||||
|     "keypairs": "^1.2.6", | ||||
|     "mkdirp": "^0.5.1", | ||||
|     "proxy-packer": "^2.0.2", | ||||
|     "ps-list": "^5.0.0", | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user