WIP merging walnut, serve-https, and stunnel.js
This commit is contained in:
		
							parent
							
								
									4267955286
								
							
						
					
					
						commit
						67aa28aece
					
				| @ -1,10 +1,32 @@ | ||||
| #!/usr/bin/env node
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| var cluster = require('cluster'); | ||||
| 
 | ||||
| if (!cluster.isMaster) { | ||||
|   require('../lib/worker.js'); | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| function run(config) { | ||||
|   // TODO spin up multiple workers
 | ||||
|   // TODO use greenlock-cluster
 | ||||
|   function work() { | ||||
|     var worker = cluster.fork(); | ||||
|     worker.on('exit', work).on('online', function () { | ||||
|       console.log('[worker]', worker.id, 'online'); | ||||
|       // Worker is listening
 | ||||
|       worker.send(config); | ||||
|     }); | ||||
|   } | ||||
|   console.log('config.tcp.ports', config.tcp.ports); | ||||
|   work(); | ||||
| } | ||||
| 
 | ||||
| function readConfigAndRun(args) { | ||||
|   var fs = require('fs'); | ||||
|   var path = require('path'); | ||||
|   var cwd = args.cwd || process.cwd(); | ||||
|   var cwd = args.cwd; | ||||
|   var text; | ||||
|   var filename; | ||||
|   var config; | ||||
| @ -13,13 +35,13 @@ function readConfigAndRun(args) { | ||||
|     text = fs.readFileSync(path.join(cwd, args.config), 'utf8'); | ||||
|   } | ||||
|   else { | ||||
|     filename = path.join(cwd, 'Goldilocks.yml'); | ||||
|     filename = path.join(cwd, 'goldilocks.yml'); | ||||
| 
 | ||||
|     if (fs.existsSync(filename)) { | ||||
|       text = fs.readFileSync(filename, 'utf8'); | ||||
|     } | ||||
|     else { | ||||
|       filename = path.join(cwd, 'Goldilocks.json'); | ||||
|       filename = path.join(cwd, 'goldilocks.json'); | ||||
|       if (fs.existsSync(filename)) { | ||||
|         text = fs.readFileSync(filename, 'utf8'); | ||||
|       } else { | ||||
| @ -39,16 +61,75 @@ function readConfigAndRun(args) { | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|   if (!config.tcp) { | ||||
|     config.tcp = {}; | ||||
|   } | ||||
|   if (!config.http) { | ||||
|     config.http = {}; | ||||
|   } | ||||
|   if (!config.tls) { | ||||
|     config.tls = { | ||||
|       agreeTos: args.agreeTos || args.agree || args['agree-tos'] | ||||
|     , servernames: (args.servernames||'').split(',').filter(Boolean).map(function (str) { return str.toLowerCase(); }) | ||||
|     }; | ||||
|   } | ||||
|   if (args.email) { | ||||
|     config.email = args.email; | ||||
|     config.tls.email = args.email; | ||||
|   } | ||||
| 
 | ||||
|   require('../lib/goldilocks.js').create(config); | ||||
|   // maybe this should not go in config... but be ephemeral in some way?
 | ||||
|   if (args.cwd) { | ||||
|     config.cwd = args.cwd; | ||||
|   } | ||||
|   if (!config.cwd) { | ||||
|     config.cwd = process.cwd(); | ||||
|   } | ||||
| 
 | ||||
|   if (config.tcp.ports) { | ||||
|     run(config); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   require('../lib/check-ports.js').checkPorts(config, function (failed, bound) { | ||||
|     config.tcp.ports = Object.keys(bound); | ||||
| 
 | ||||
|     if (!config.tcp.ports.length) { | ||||
|       console.warn("could not bind to the desired ports"); | ||||
|       Object.keys(failed).forEach(function (key) { | ||||
|         console.log('[error bind]', key, failed[key].code); | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     run(config); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function readEnv(args) { | ||||
|   // TODO
 | ||||
|   var env = { | ||||
|     tunnel: process.env.GOLDILOCKS_TUNNEL_TOKEN || process.env.GOLDILOCKS_TUNNEL && true | ||||
|   , email: process.env.GOLDILOCKS_EMAIL | ||||
|   , cwd: process.env.GOLDILOCKS_HOME | ||||
|   , debug: process.env.GOLDILOCKS_DEBUG && true | ||||
|   }; | ||||
|   args.cwd = args.cwd || env.cwd; | ||||
|   Object.keys(env).forEach(function (key) { | ||||
|     if ('undefined' === typeof args[key]) { | ||||
|       args[key] = env[key]; | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   readConfigAndRun(args); | ||||
| } | ||||
| 
 | ||||
| if (process.argv.length === 2) { | ||||
|   readConfigAndRun({}); | ||||
|   readEnv({ cwd: process.cwd() }); | ||||
| } | ||||
| else if (process.argv.length === 4) { | ||||
|   if ('-c' === process.argv[3] || '--config' === process.argv[3]) { | ||||
|     readConfigAndRun({ config: process.argv[4] }); | ||||
|     readEnv({ config: process.argv[4] }); | ||||
|   } | ||||
| } | ||||
| else if (process.argv.length > 2) { | ||||
| @ -56,11 +137,15 @@ else if (process.argv.length > 2) { | ||||
| 
 | ||||
|   program | ||||
|     .version(require('package.json').version) | ||||
|     .option('--agree-tos [url1,url2]', "agree to all Terms of Service for Daplie, Let's Encrypt, etc (or specific URLs only)") | ||||
|     .option('--config', 'Path to config file (Goldilocks.json or Goldilocks.yml) example: --config /etc/goldilocks/Goldilocks.json') | ||||
|     .option('--tunnel [token]', 'Turn tunnel on. This will enter interactive mode for login if no token is specified.') | ||||
|     .option('--email <email>', "(Re)set default email to use for Daplie, Let's Encrypt, ACME, etc.") | ||||
|     .option('--debug', "Enable debug output") | ||||
|     .parse(process.argv); | ||||
| 
 | ||||
|   readConfigAndRun(program); | ||||
|   program.cwd = process.cwd(); | ||||
|   readEnv(program); | ||||
| } | ||||
| else { | ||||
|   throw new Error("impossible number of arguments: " + process.argv.length); | ||||
|  | ||||
							
								
								
									
										72
									
								
								lib/app.js
									
									
									
									
									
								
							
							
						
						
									
										72
									
								
								lib/app.js
									
									
									
									
									
								
							| @ -1,6 +1,6 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| module.exports = function (opts) { | ||||
| module.exports = function (deps, conf) { | ||||
|   var express = require('express'); | ||||
|   //var finalhandler = require('finalhandler');
 | ||||
|   var serveStatic = require('serve-static'); | ||||
| @ -11,7 +11,7 @@ module.exports = function (opts) { | ||||
| 
 | ||||
|   var serveStaticMap = {}; | ||||
|   var serveIndexMap = {}; | ||||
|   var content = opts.content; | ||||
|   var content = conf.content; | ||||
|   //var server;
 | ||||
|   var serveInit; | ||||
|   var app; | ||||
| @ -106,7 +106,7 @@ module.exports = function (opts) { | ||||
|       } | ||||
|     , recase: require('recase').create({}) | ||||
|     , request: request | ||||
|     , options: opts | ||||
|     , options: conf | ||||
|     , api: { | ||||
|         // TODO move loopback to oauth3.api('tunnel:loopback')
 | ||||
|         loopback: function (deps, session, opts2) { | ||||
| @ -184,7 +184,7 @@ module.exports = function (opts) { | ||||
|                 , stunneld: result.tunnelUrl | ||||
|                   // we'll provide faux networking and pipe as we please
 | ||||
|                 , services: { https: { '*': 443 }, http: { '*': 80 }, smtp: { '*': 25}, smtps: { '*': 587 /*also 465/starttls*/ } /*, ssh: { '*': 22 }*/ } | ||||
|                 , net: opts.net | ||||
|                 , net: conf.net | ||||
|                 }; | ||||
| 
 | ||||
|                 if (tun) { | ||||
| @ -199,7 +199,7 @@ module.exports = function (opts) { | ||||
| 
 | ||||
|                 if (!tun) { | ||||
|                   tun = stunnel.connect(opts3); | ||||
|                   opts.tun = true; | ||||
|                   conf.tun = true; | ||||
|                 } | ||||
|               }); | ||||
|             /* | ||||
| @ -214,37 +214,44 @@ module.exports = function (opts) { | ||||
| 
 | ||||
|   app = express(); | ||||
| 
 | ||||
|   var Sites = { | ||||
|     add: function (sitesMap, site) { | ||||
|       if (!sitesMap[site.$id]) { | ||||
|         sitesMap[site.$id] = site; | ||||
|       } | ||||
| 
 | ||||
|       if (!site.paths) { | ||||
|         site.paths = []; | ||||
|       } | ||||
|       if (!site.paths._map) { | ||||
|         site.paths._map = {}; | ||||
|       } | ||||
|       site.paths.forEach(function (path) { | ||||
| 
 | ||||
|         site.paths._map[path.$id] = path; | ||||
| 
 | ||||
|         if (!path.modules) { | ||||
|           path.modules = []; | ||||
|         } | ||||
|         if (!path.modules._map) { | ||||
|           path.modules._map = {}; | ||||
|         } | ||||
|         path.modules.forEach(function (module) { | ||||
| 
 | ||||
|           path.modules._map[module.$id] = module; | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   var opts = conf.http; | ||||
|   if (!opts.sites) { | ||||
|     opts.sites = []; | ||||
|   } | ||||
|   opts.sites._map = {}; | ||||
|   opts.sites.forEach(function (site) { | ||||
| 
 | ||||
|     if (!opts.sites._map[site.$id]) { | ||||
|       opts.sites._map[site.$id] = site; | ||||
|     } | ||||
| 
 | ||||
|     if (!site.paths) { | ||||
|       site.paths = []; | ||||
|     } | ||||
|     if (!site.paths._map) { | ||||
|       site.paths._map = {}; | ||||
|     } | ||||
|     site.paths.forEach(function (path) { | ||||
| 
 | ||||
|       site.paths._map[path.$id] = path; | ||||
| 
 | ||||
|       if (!path.modules) { | ||||
|         path.modules = []; | ||||
|       } | ||||
|       if (!path.modules._map) { | ||||
|         path.modules._map = {}; | ||||
|       } | ||||
|       path.modules.forEach(function (module) { | ||||
| 
 | ||||
|         path.modules._map[module.$id] = module; | ||||
|       }); | ||||
|     }); | ||||
|     Sites.add(opts.sites._map, site); | ||||
|   }); | ||||
| 
 | ||||
|   function mapMap(el, i, arr) { | ||||
| @ -277,6 +284,7 @@ module.exports = function (opts) { | ||||
|     path.modules._map = {}; | ||||
|     path.modules.forEach(mapMap); | ||||
|   }); | ||||
| 
 | ||||
|   return app.use('/', function (req, res, next) { | ||||
|     if (!req.headers.host) { | ||||
|       next(new Error('missing HTTP Host header')); | ||||
| @ -331,7 +339,7 @@ module.exports = function (opts) { | ||||
|           } | ||||
| 
 | ||||
|           console.log('[serve]', req.url, hostname, pathname, dirname); | ||||
|           dirname = path.resolve(opts.cwd, dirname.replace(/:hostname/, hostname)); | ||||
|           dirname = path.resolve(conf.cwd, dirname.replace(/:hostname/, hostname)); | ||||
|           if (!serveStaticMap[dirname]) { | ||||
|             serveStaticMap[dirname] = serveStatic(dirname); | ||||
|           } | ||||
| @ -355,7 +363,7 @@ module.exports = function (opts) { | ||||
|           } | ||||
| 
 | ||||
|           console.log('[indexes]', req.url, hostname, pathname, dirname); | ||||
|           dirname = path.resolve(opts.cwd, dirname.replace(/:hostname/, hostname)); | ||||
|           dirname = path.resolve(conf.cwd, dirname.replace(/:hostname/, hostname)); | ||||
|           if (!serveStaticMap[dirname]) { | ||||
|             serveIndexMap[dirname] = serveIndex(dirname); | ||||
|           } | ||||
|  | ||||
							
								
								
									
										55
									
								
								lib/check-ports.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								lib/check-ports.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,55 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| function bindTcpAndRelease(port, cb) { | ||||
|   var server = require('net').createServer(); | ||||
|   server.on('error', function (e) { | ||||
|     cb(e); | ||||
|   }); | ||||
|   server.listen(port, function () { | ||||
|     server.close(); | ||||
|     cb(); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function checkPorts(config, cb) { | ||||
|   var bound = {}; | ||||
|   var failed = {}; | ||||
| 
 | ||||
|   bindTcpAndRelease(80, function (e) { | ||||
|     if (e) { | ||||
|       failed[80] = e; | ||||
|       //console.log(e.code);
 | ||||
|       //console.log(e.message);
 | ||||
|     } else { | ||||
|       bound['80'] = true; | ||||
|     } | ||||
| 
 | ||||
|     bindTcpAndRelease(443, function (e) { | ||||
|       if (e) { | ||||
|         failed[443] = e; | ||||
|       } else { | ||||
|         bound['443'] = true; | ||||
|       } | ||||
| 
 | ||||
|       if (bound['80'] && bound['443']) { | ||||
|         //config.tcp.ports = [ 80, 443 ];
 | ||||
|         cb(null, bound); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       console.warn("default ports 80 and 443 are not available, trying 8443"); | ||||
| 
 | ||||
|       bindTcpAndRelease(8443, function (e) { | ||||
|         if (e) { | ||||
|           failed[8443] = e; | ||||
|         } else { | ||||
|           bound['8443'] = true; | ||||
|         } | ||||
| 
 | ||||
|         cb(failed, bound); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| module.exports.checkPorts = checkPorts; | ||||
| @ -1,688 +1,309 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| module.exports.create = function (config) { | ||||
| module.exports.create = function (deps, config) { | ||||
|   console.log('config', config); | ||||
| 
 | ||||
|   //var PromiseA = global.Promise;
 | ||||
|   var PromiseA = require('bluebird'); | ||||
|   var tls = require('tls'); | ||||
|   var https = require('httpolyglot'); | ||||
|   var http = require('http'); | ||||
|   var path = require('path'); | ||||
|   var httpPort = 80; | ||||
|   var httpsPort = 443; | ||||
|   var lrPort = 35729; | ||||
|   var portFallback = 8443; | ||||
|   var insecurePortFallback = 4080; | ||||
|   var greenlock = require('greenlock'); | ||||
|   var listeners = require('./servers').listeners; | ||||
| 	var parseSni = require('sni'); | ||||
| 	var modules = { }; | ||||
| 	var program = { | ||||
| 		tlsOptions: require('localhost.daplie.me-certificates').merge({}) | ||||
| 	, acmeDirectoryUrl: 'https://acme-v01.api.letsencrypt.org/directory' | ||||
| 	, challengeType: 'tls-sni-01' | ||||
| 	}; | ||||
|   var secureContexts = {}; | ||||
| 	var tunnelAdminTlsOpts = {}; | ||||
| 	var tls = require('tls'); | ||||
| 
 | ||||
|   function showError(err, port) { | ||||
|     if ('EACCES' === err.code) { | ||||
|       console.error(err); | ||||
|       console.warn("You do not have permission to use '" + port + "'."); | ||||
|       console.warn("You can probably fix that by running as Administrator or root."); | ||||
|     } | ||||
|     else if ('EADDRINUSE' === err.code) { | ||||
|       console.warn("Another server is already running on '" + port + "'."); | ||||
|       console.warn("You can probably fix that by rebooting your computer (or stopping it if you know what it is)."); | ||||
| 	var tcpRouter = { | ||||
| 		_map: { } | ||||
| 	, _create: function (address, port) { | ||||
| 			// port provides hinting for http, smtp, etc
 | ||||
| 			return function (conn, firstChunk) { | ||||
| 				console.log('[tcpRouter] ' + address + ':' + port + ' servername'); | ||||
| 
 | ||||
| 				// At this point we cannot necessarily trace which port or address the socket came from
 | ||||
| 				// (because node's netowrking layer == 💩 )
 | ||||
| 				var m; | ||||
| 				var str; | ||||
|         var servername; | ||||
| 				// TODO test per-module
 | ||||
| 				// Maybe HTTP
 | ||||
| 				if (firstChunk[0] > 32 && firstChunk[0] < 127) { | ||||
| 					str = firstChunk.toString(); | ||||
| 					m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im); | ||||
|           servername = (m && m[1].toLowerCase() || '').split(':')[0]; | ||||
| 					//conn.__servername = servername;
 | ||||
| 					console.log('[tcpRouter] hostname', servername); | ||||
| 					if (/HTTP\//i.test(str)) { | ||||
| 						//conn.__service = 'http';
 | ||||
| 					} | ||||
| 				} | ||||
|         console.log('1010'); | ||||
| 
 | ||||
| 				if (!servername) { | ||||
| 					// TODO allow tcp tunneling
 | ||||
| 					// TODO we need some way of tagging tcp as either terminated tls or insecure
 | ||||
| 					conn.write( | ||||
| 						"HTTP/1.1 404 Not Found\r\n" | ||||
| 					+ "Date: Fri, 31 Dec 1999 23:59:59 GMT\r\n" | ||||
| 					+ "Content-Type: text/html\r\n" | ||||
| 					+ "Content-Length: " + 9 + "\r\n" | ||||
| 					+ "\r\n" | ||||
| 					+ "Not Found" | ||||
| 					); | ||||
|           conn.end(); | ||||
| 					return; | ||||
| 				} | ||||
| 
 | ||||
|         console.log('1020'); | ||||
| 				if (/\blocalhost\.admin\./.test(servername) || /\badmin\.localhost\./.test(servername) | ||||
|             || /\blocalhost\.alpha\./.test(servername) || /\balpha\.localhost\./.test(servername)) { | ||||
|           console.log('1050'); | ||||
| 					if (!modules.admin) { | ||||
| 						modules.admin = require('./modules/admin.js').create(deps, config); | ||||
| 					} | ||||
|           console.log('1100'); | ||||
| 					modules.admin.emit('connection', conn); | ||||
|           console.log('1500'); | ||||
| 					return; | ||||
| 				} | ||||
| 
 | ||||
| 				if (!modules.http) { | ||||
| 					if (!modules.http) { | ||||
| 						modules.http = require('./modules/http.js').create(deps, config); | ||||
| 					} | ||||
| 					modules.http.emit('connection', conn); | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
| 	, get: function getTcpRouter(address, port) { | ||||
| 			address = address || '0.0.0.0'; | ||||
| 
 | ||||
| 			var id = address + ':' + port; | ||||
| 			if (!tcpRouter._map[id]) { | ||||
| 				tcpRouter._map[id] = tcpRouter._create(address, port); | ||||
| 			} | ||||
| 
 | ||||
| 			return tcpRouter._map[id]; | ||||
| 		} | ||||
| 	}; | ||||
| 	var tlsRouter = { | ||||
| 		_map: { } | ||||
| 	, _create: function (address, port) { | ||||
| 			// port provides hinting for https, smtps, etc
 | ||||
| 			return function (socket, servername) { | ||||
| 				//program.tlsTunnelServer.emit('connection', socket);
 | ||||
|         //return;
 | ||||
| 				console.log('[tlsRouter] ' + address + ':' + port + ' servername', servername); | ||||
| 
 | ||||
| 				var packerStream = require('tunnel-packer').Stream; | ||||
| 				var myDuplex = packerStream.create(socket); | ||||
| 
 | ||||
| 				// needs to wind up in one of 3 states:
 | ||||
| 				// 1. Proxied / Tunneled (we don't even need to put it through the tlsSocket)
 | ||||
| 				// 2. Admin (skips normal processing)
 | ||||
| 				// 3. Terminated (goes on to a particular module or route)
 | ||||
| 				//myDuplex.__tlsTerminated = true;
 | ||||
| 				program.tlsTunnelServer.emit('connection', myDuplex); | ||||
| 
 | ||||
| 				socket.on('data', function (chunk) { | ||||
| 					console.log('[' + Date.now() + '] tls socket data', chunk.byteLength); | ||||
| 					myDuplex.push(chunk); | ||||
| 				}); | ||||
| 				socket.on('error', function (err) { | ||||
| 					console.error('[error] httpsTunnel (Admin) TODO close'); | ||||
| 					console.error(err); | ||||
| 					myDuplex.emit('error', err); | ||||
| 				}); | ||||
| 				socket.on('close', function () { | ||||
| 					myDuplex.close(); | ||||
| 				}); | ||||
| 			}; | ||||
| 		} | ||||
| 	, get: function getTcpRouter(address, port) { | ||||
| 			address = address || '0.0.0.0'; | ||||
| 
 | ||||
| 			var id = address + ':' + port; | ||||
| 			if (!tlsRouter._map[id]) { | ||||
| 				tlsRouter._map[id] = tlsRouter._create(address, port); | ||||
| 			} | ||||
| 
 | ||||
| 			return tlsRouter._map[id]; | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 
 | ||||
|   function handler(conn, opts) { | ||||
|     opts = opts || {}; | ||||
|     console.log('[handler]', conn.localAddres, conn.localPort, opts.secure); | ||||
| 
 | ||||
|     // TODO inspect SNI and HTTP Host
 | ||||
|     conn.once('data', function (firstChunk) { | ||||
|       var servername; | ||||
| 
 | ||||
| 			process.nextTick(function () { | ||||
| 			  conn.unshift(firstChunk); | ||||
| 			}); | ||||
| 			// copying stuff over to firstChunk because the network abstraction goes too deep to find these again
 | ||||
| 			//firstChunk.__port = conn.__port;
 | ||||
| 
 | ||||
| 			// TLS
 | ||||
| 			if (22 === firstChunk[0]) { | ||||
|         servername = (parseSni(firstChunk)||'').toLowerCase() || 'localhost.invalid'; | ||||
| 				//conn.__servername = servername;
 | ||||
|         //conn.__tls = true;
 | ||||
|         //conn.__tlsTerminated = false;
 | ||||
| 				//firstChunk.__servername = conn.__servername;
 | ||||
| 				//firstChunk.__tls = true;
 | ||||
| 				//firstChunk.__tlsTerminated = false;
 | ||||
|         console.log('tryTls'); | ||||
| 				tlsRouter.get(conn.localAddress, conn.localPort)(conn, servername); | ||||
| 			} | ||||
| 			else { | ||||
| 				// TODO how to tag as insecure?
 | ||||
|         console.log('tryTcp'); | ||||
| 				tcpRouter.get(conn.localAddress, conn.localPort)(conn, firstChunk, { secure: opts.secure || false }); | ||||
| 			} | ||||
|     }); | ||||
| 
 | ||||
| /* | ||||
|     if ('http' === config.tcp.default || !config.tcp.default) { | ||||
|       console.log('deal with as http'); | ||||
|     } | ||||
| */ | ||||
|   } | ||||
| 
 | ||||
|   function createInsecureServer(port, _delete_me_, opts) { | ||||
|     return new PromiseA(function (realResolve) { | ||||
|       var server = http.createServer(); | ||||
| 	function approveDomains(opts, certs, cb) { | ||||
| 		// This is where you check your database and associated
 | ||||
| 		// email addresses with domains and agreements and such
 | ||||
| 
 | ||||
|       function resolve() { | ||||
|         realResolve(server); | ||||
| 		// The domains being approved for the first time are listed in opts.domains
 | ||||
| 		// Certs being renewed are listed in certs.altnames
 | ||||
| 
 | ||||
| 		function complete(err, stuff) { | ||||
| 			opts.email = stuff.email; | ||||
| 			opts.agreeTos = stuff.agreeTos; | ||||
| 			opts.server = stuff.server; | ||||
| 			opts.challengeType = stuff.challengeType; | ||||
| 
 | ||||
| 			cb(null, { options: opts, certs: certs }); | ||||
| 		} | ||||
| 
 | ||||
| 		if (certs) { | ||||
| 			// TODO make sure the same options are used for renewal as for registration?
 | ||||
| 			opts.domains = certs.altnames; | ||||
| 
 | ||||
| 			cb(null, { options: opts, certs: certs }); | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		// check config for domain name
 | ||||
| 		if (-1 !== config.tls.servernames.indexOf(opts.domain)) { | ||||
| 			// TODO how to handle SANs?
 | ||||
| 			// TODO fetch domain-specific email
 | ||||
| 			// TODO fetch domain-specific acmeDirectory
 | ||||
| 			// NOTE: you can also change other options such as `challengeType` and `challenge`
 | ||||
| 			// opts.challengeType = 'http-01';
 | ||||
| 			// opts.challenge = require('le-challenge-fs').create({}); // TODO this doesn't actually work yet
 | ||||
| 			complete(null, { | ||||
| 				email: config.tls.email, agreeTos: true, server: program.acmeDirectoryUrl, challengeType: program.challengeType }); | ||||
| 			return; | ||||
| 		} | ||||
| 		// TODO ask http module about the default path (/srv/www/:hostname)
 | ||||
| 		// (if it exists, we can allow and add to config)
 | ||||
| 		if (!modules.http) { | ||||
| 			modules.http = require('./modules/http.js').create(config); | ||||
| 		} | ||||
| 		modules.http.checkServername(opts.domain).then(function (stuff) { | ||||
| 			if (!stuff.domains) { | ||||
| 				// TODO once precheck is implemented we can just let it pass if it passes, yknow?
 | ||||
| 				cb(new Error('domain is not allowed')); | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			complete(null, { | ||||
| 				domain: stuff.domain || stuff.domains[0] | ||||
| 			, domains: stuff.domains | ||||
| 			, email: program.email | ||||
| 			, server: program.acmeDirectoryUrl | ||||
| 			, challengeType: program.challengeType | ||||
| 			}); | ||||
| 			return; | ||||
| 		}, cb); | ||||
| 	} | ||||
| 
 | ||||
|   function getAcme() { | ||||
|     return greenlock.create({ | ||||
| 
 | ||||
|       //server: 'staging'
 | ||||
|       server: 'https://acme-v01.api.letsencrypt.org/directory' | ||||
| 
 | ||||
|     , challenges: { | ||||
|         // TODO dns-01
 | ||||
|         'http-01': require('le-challenge-fs').create({ webrootPath: '/tmp/acme-challenges', debug: config.debug }) | ||||
|       , 'tls-sni-01': require('le-challenge-sni').create({ debug: config.debug }) | ||||
|       //, 'dns-01': require('le-challenge-ddns').create()
 | ||||
|       } | ||||
| 
 | ||||
|       server.on('error', function (err) { | ||||
|         if (opts.errorInsecurePort || opts.manualInsecurePort) { | ||||
|           showError(err, port); | ||||
|           process.exit(1); | ||||
|           return; | ||||
|         } | ||||
|     , store: require('le-store-certbot').create({ webrootPath: '/tmp/acme-challenges' }) | ||||
| 
 | ||||
|         opts.errorInsecurePort = err.toString(); | ||||
|     //, email: program.email
 | ||||
| 
 | ||||
|         return createInsecureServer(insecurePortFallback, null, opts).then(resolve); | ||||
|       }); | ||||
|     //, agreeTos: program.agreeTos
 | ||||
| 
 | ||||
|       server.on('request', opts.redirectApp); | ||||
|     , approveDomains: approveDomains | ||||
| 
 | ||||
|     //, approvedDomains: program.servernames
 | ||||
| 
 | ||||
|       server.listen(port, function () { | ||||
|         opts.insecurePort = port; | ||||
|         resolve(); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function createServer(port, _delete_me_, content, opts) { | ||||
|     function approveDomains(params, certs, cb) { | ||||
|       // This is where you check your database and associated
 | ||||
|       // email addresses with domains and agreements and such
 | ||||
|       var domains = params.domains; | ||||
|       //var p;
 | ||||
|       console.log('approveDomains'); | ||||
|       console.log(domains); | ||||
|   Object.keys(program.tlsOptions).forEach(function (key) { | ||||
|     tunnelAdminTlsOpts[key] = program.tlsOptions[key]; | ||||
|   }); | ||||
|   tunnelAdminTlsOpts.SNICallback = function (sni, cb) { | ||||
|     console.log("[tlsOptions.SNICallback] SNI: '" + sni + "'"); | ||||
| 
 | ||||
|     var tlsOptions; | ||||
| 
 | ||||
|       // The domains being approved for the first time are listed in opts.domains
 | ||||
|       // Certs being renewed are listed in certs.altnames
 | ||||
|       if (certs) { | ||||
|         params.domains = certs.altnames; | ||||
|         //p = PromiseA.resolve();
 | ||||
|     // Static Certs
 | ||||
|     if (/.*localhost.*\.daplie\.me/.test(sni.toLowerCase())) { | ||||
|       // TODO implement
 | ||||
|       if (!secureContexts[sni]) { | ||||
|         tlsOptions = require('localhost.daplie.me-certificates').mergeTlsOptions(sni, {}); | ||||
|       } | ||||
|       else { | ||||
|         //params.email = opts.email;
 | ||||
|         if (!opts.agreeTos) { | ||||
|           console.error("You have not previously registered '" + domains + "' so you must specify --agree-tos to agree to both the Let's Encrypt and Daplie DNS terms of service."); | ||||
|           process.exit(1); | ||||
|           return; | ||||
|         } | ||||
|         params.agreeTos = opts.agreeTos; | ||||
|       if (tlsOptions) { | ||||
|         secureContexts[sni] = tls.createSecureContext(tlsOptions); | ||||
|       } | ||||
| 
 | ||||
|       // ddns.token(params.email, domains[0])
 | ||||
|       params.email = opts.email; | ||||
|       params.refreshToken = opts.refreshToken; | ||||
|       params.challengeType = 'dns-01'; | ||||
|       params.cli = opts.argv; | ||||
| 
 | ||||
|       cb(null, { options: params, certs: certs }); | ||||
|     } | ||||
| 
 | ||||
|     return new PromiseA(function (realResolve) { | ||||
|       var app = require('../lib/app.js'); | ||||
|       var ipaddr = require('ipaddr.js'); | ||||
|       var addresses = []; | ||||
| 
 | ||||
|       Object.keys(opts.ifaces).forEach(function (ifacename) { | ||||
|         var iface = opts.ifaces[ifacename]; | ||||
|         iface.ipv4.forEach(function (ip) { | ||||
|           addresses.push(ip); | ||||
|         }); | ||||
|         iface.ipv6.forEach(function (ip) { | ||||
|           addresses.push(ip); | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       addresses.sort(function (a, b) { | ||||
|         if (a.family !== b.family) { | ||||
|           return 'IPv4' === a.family ? 1 : -1; | ||||
|         } | ||||
| 
 | ||||
|         return a.address > b.address ? 1 : -1; | ||||
|       }); | ||||
| 
 | ||||
|       addresses.forEach(function (addr) { | ||||
|         addr.range = ipaddr.parse(addr.address).range(); | ||||
|       }); | ||||
| 
 | ||||
|       var Oauth3 = require('oauth3-cli'); | ||||
|       var oauth3 = Oauth3.create({ device: { hostname: opts.device } }); | ||||
|       return Oauth3.Devices.one(oauth3).then(function (device) { | ||||
|         return Oauth3.Devices.all(oauth3).then(function (devices) { | ||||
|           return { devices: devices, device: device.device || device }; | ||||
|         }); | ||||
|       }).then(function (devices) { | ||||
|         devices.device.secret = undefined; | ||||
|         console.log('devices'); | ||||
|         console.log(devices); | ||||
|         var directive = { | ||||
|           global: opts.global | ||||
|         , sites: opts.sites | ||||
|         , defaults: opts.defaults | ||||
|         , cwd: process.cwd() | ||||
|         , ifaces: opts.ifaces | ||||
|         , addresses: addresses | ||||
|         , devices: devices.devices | ||||
|         , device: devices.device | ||||
|         , net: { | ||||
|             createConnection: function (opts, cb) { | ||||
|               // opts = { host, port, data
 | ||||
|               //        , /*proprietary to tunneler*/ servername, remoteAddress, remoteFamily, remotePort
 | ||||
|               //        , secure (tls already terminated by a proxy) }
 | ||||
|               //        // http://stackoverflow.com/questions/10348906/how-to-know-if-a-request-is-http-or-https-in-node-js
 | ||||
|               // var packerStream = require('tunnel-packer').Stream;
 | ||||
|               // TODO here we will have the tls termination (or re-forward)
 | ||||
|               return require('net').createConnection(opts, cb); | ||||
|             } | ||||
|           } | ||||
|         }; | ||||
|         var server; | ||||
|         var insecureServer; | ||||
| 
 | ||||
|         function resolve() { | ||||
|           realResolve({ | ||||
|             plainServer: insecureServer | ||||
|           , server: server | ||||
|           }); | ||||
|         } | ||||
| 
 | ||||
|         // returns an instance of node-letsencrypt with additional helper methods
 | ||||
|         var webrootPath = require('os').tmpdir(); | ||||
|         var leChallengeFs = require('le-challenge-fs').create({ webrootPath: webrootPath }); | ||||
|         //var leChallengeSni = require('le-challenge-sni').create({ webrootPath: webrootPath });
 | ||||
|         var leChallengeDdns = require('le-challenge-ddns').create({ ttl: 1 }); | ||||
|         var lex = require('greenlock-express').create({ | ||||
|           // set to https://acme-v01.api.letsencrypt.org/directory in production
 | ||||
|           server: opts.debug ? 'staging' : 'https://acme-v01.api.letsencrypt.org/directory' | ||||
| 
 | ||||
|         // If you wish to replace the default plugins, you may do so here
 | ||||
|         //
 | ||||
|         , challenges: { | ||||
|             'http-01': leChallengeFs | ||||
|           , 'tls-sni-01': leChallengeFs // leChallengeSni
 | ||||
|           , 'dns-01': leChallengeDdns | ||||
|           } | ||||
|         , challengeType: (opts.tunnel ? 'http-01' : 'dns-01') | ||||
|         , store: require('le-store-certbot').create({ | ||||
|             webrootPath: webrootPath | ||||
|           , configDir: path.join((opts.homedir || '~'), 'letsencrypt', 'etc') | ||||
|           , homedir: opts.homedir | ||||
|           }) | ||||
|         , webrootPath: webrootPath | ||||
| 
 | ||||
|         // You probably wouldn't need to replace the default sni handler
 | ||||
|         // See https://git.daplie.com/Daplie/le-sni-auto if you think you do
 | ||||
|         //, sni: require('le-sni-auto').create({})
 | ||||
| 
 | ||||
|         , approveDomains: approveDomains | ||||
|         }); | ||||
| 
 | ||||
|         var secureContexts = { | ||||
|           'localhost.daplie.me': null | ||||
|         }; | ||||
|         opts.httpsOptions.SNICallback = function (sni, cb ) { | ||||
|           var tlsOptions; | ||||
|           console.log('[https] sni', sni); | ||||
| 
 | ||||
|           // Static Certs
 | ||||
|           if (/.*localhost.*\.daplie\.me/.test(sni.toLowerCase())) { | ||||
|             // TODO implement
 | ||||
|             if (!secureContexts[sni]) { | ||||
|               tlsOptions = require('localhost.daplie.me-certificates').mergeTlsOptions(sni, {}); | ||||
|             } | ||||
|             if (tlsOptions) { | ||||
|               secureContexts[sni] = tls.createSecureContext(tlsOptions); | ||||
|             } | ||||
|             cb(null, secureContexts[sni]); | ||||
|             return; | ||||
|           } | ||||
| 
 | ||||
|           // Dynamic Certs
 | ||||
|           lex.httpsOptions.SNICallback(sni, cb); | ||||
|         }; | ||||
|         server = https.createServer(opts.httpsOptions); | ||||
| 
 | ||||
|         server.on('error', function (err) { | ||||
|           if (opts.errorPort || opts.manualPort) { | ||||
|             showError(err, port); | ||||
|             process.exit(1); | ||||
|             return; | ||||
|           } | ||||
| 
 | ||||
|           opts.errorPort = err.toString(); | ||||
| 
 | ||||
|           return createServer(portFallback, null, content, opts).then(resolve); | ||||
|         }); | ||||
| 
 | ||||
|         server.listen(port, function () { | ||||
|           opts.port = port; | ||||
|           opts.redirectOptions.port = port; | ||||
| 
 | ||||
|           if (opts.livereload) { | ||||
|             opts.lrPort = opts.lrPort || lrPort; | ||||
|             var livereload = require('livereload'); | ||||
|             var server2 = livereload.createServer({ | ||||
|               https: opts.httpsOptions | ||||
|             , port: opts.lrPort | ||||
|             , exclusions: [ 'node_modules' ] | ||||
|             }); | ||||
| 
 | ||||
|             console.info("[livereload] watching " + opts.pubdir); | ||||
|             console.warn("WARNING: If CPU usage spikes to 100% it's because too many files are being watched"); | ||||
|             // TODO create map of directories to watch from opts.sites and iterate over it
 | ||||
|             server2.watch(opts.pubdir); | ||||
|           } | ||||
| 
 | ||||
|           // if we haven't disabled insecure port
 | ||||
|           if ('false' !== opts.insecurePort) { | ||||
|             // and both ports are the default
 | ||||
|             if ((httpsPort === opts.port && httpPort === opts.insecurePort) | ||||
|               // or other case
 | ||||
|               || (httpPort !== opts.insecurePort && opts.port !== opts.insecurePort) | ||||
|             ) { | ||||
|               return createInsecureServer(opts.insecurePort, null, opts).then(function (_server) { | ||||
|                 insecureServer = _server; | ||||
|                 resolve(); | ||||
|               }); | ||||
|             } | ||||
|           } | ||||
| 
 | ||||
|           opts.insecurePort = opts.port; | ||||
|           resolve(); | ||||
|           return; | ||||
|         }); | ||||
| 
 | ||||
|         if ('function' === typeof app) { | ||||
|           app = app(directive); | ||||
|         } else if ('function' === typeof app.create) { | ||||
|           app = app.create(directive); | ||||
|         } | ||||
| 
 | ||||
|         server.on('request', function (req, res) { | ||||
|           console.log('[' + req.method + '] ' + req.url); | ||||
|           if (!req.socket.encrypted && !/\/\.well-known\/acme-challenge\//.test(req.url)) { | ||||
|             opts.redirectApp(req, res); | ||||
|             return; | ||||
|           } | ||||
| 
 | ||||
|           if ('function' === typeof app) { | ||||
|             app(req, res); | ||||
|             return; | ||||
|           } | ||||
| 
 | ||||
|           res.end('not ready'); | ||||
|         }); | ||||
| 
 | ||||
|         return PromiseA.resolve(app).then(function (_app) { | ||||
|           app = _app; | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   module.exports.createServer = createServer; | ||||
| 
 | ||||
|   function run() { | ||||
|     var defaultServername = 'localhost.daplie.me'; | ||||
|     var minimist = require('minimist'); | ||||
|     var argv = minimist(process.argv.slice(2)); | ||||
|     var port = parseInt(argv.p || argv.port || argv._[0], 10) || httpsPort; | ||||
|     var livereload = argv.livereload; | ||||
|     var defaultWebRoot = path.normalize(argv['default-web-root'] || argv.d || argv._[1] || '.'); | ||||
|     var assetsPath = path.join(__dirname, '..', 'packages', 'assets'); | ||||
|     var content = argv.c; | ||||
|     var letsencryptHost = argv['letsencrypt-certs']; | ||||
|     var yaml = require('js-yaml'); | ||||
|     var fs = PromiseA.promisifyAll(require('fs')); | ||||
|     var configFile = argv.c || argv.conf || argv.config; | ||||
|     var config; | ||||
|     var DDNS; | ||||
|     console.log('defaultWebRoot', defaultWebRoot); | ||||
| 
 | ||||
|     try { | ||||
|       config = fs.readFileSync(configFile || 'Goldilocks.yml'); | ||||
|     } catch(e) { | ||||
|       if (configFile) { | ||||
|         console.error('Failed to read config:', e); | ||||
|         process.exit(1); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (config) { | ||||
|       try { | ||||
|         config = yaml.safeLoad(config); | ||||
|       } catch(e) { | ||||
|         console.error('Failed to parse config:', e); | ||||
|         process.exit(1); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (argv.V || argv.version || argv.v) { | ||||
|       if (argv.v) { | ||||
|         console.warn("flag -v is reserved for future use. Use -V or --version for version information."); | ||||
|       } | ||||
|       console.info('v' + require('../package.json').version); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     argv.sites = argv.sites; | ||||
| 
 | ||||
|     // letsencrypt
 | ||||
|     var httpsOptions = require('localhost.daplie.me-certificates').merge({}); | ||||
|     var secureContext; | ||||
| 
 | ||||
|     var opts = { | ||||
|       agreeTos: argv.agreeTos || argv['agree-tos'] | ||||
|     , debug: argv.debug | ||||
|     , device: argv.device | ||||
|     , provider: (argv.provider && 'false' !== argv.provider) ? argv.provider : 'oauth3.org' | ||||
|     , email: argv.email | ||||
|     , httpsOptions: { | ||||
|         key: httpsOptions.key | ||||
|       , cert: httpsOptions.cert | ||||
|       //, ca: httpsOptions.ca
 | ||||
|       } | ||||
|     , homedir: argv.homedir | ||||
|     , argv: argv | ||||
|     }; | ||||
|     var peerCa; | ||||
|     var p; | ||||
| 
 | ||||
|     opts.PromiseA = PromiseA; | ||||
|     opts.httpsOptions.SNICallback = function (sni, cb) { | ||||
|       if (!secureContext) { | ||||
|         secureContext = tls.createSecureContext(opts.httpsOptions); | ||||
|       } | ||||
|       cb(null, secureContext); | ||||
|       return; | ||||
|     }; | ||||
| 
 | ||||
|     if (letsencryptHost) { | ||||
|       // TODO remove in v3.x (aka goldilocks)
 | ||||
|       argv.key = argv.key || '/etc/letsencrypt/live/' + letsencryptHost + '/privkey.pem'; | ||||
|       argv.cert = argv.cert || '/etc/letsencrypt/live/' + letsencryptHost + '/fullchain.pem'; | ||||
|       argv.root = argv.root || argv.chain || ''; | ||||
|       argv.sites = argv.sites || letsencryptHost; | ||||
|       argv['serve-root'] = argv['serve-root'] || argv['serve-chain']; | ||||
|       // argv[express-app]
 | ||||
|     } | ||||
| 
 | ||||
|     if (argv['serve-root'] && !argv.root) { | ||||
|       console.error("You must specify bath --root to use --serve-root"); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (argv.key || argv.cert || argv.root) { | ||||
|       if (!argv.key || !argv.cert) { | ||||
|         console.error("You must specify bath --key and --cert, and optionally --root (required with serve-root)"); | ||||
|       if (secureContexts[sni]) { | ||||
|         console.log('Got static secure context:', sni, secureContexts[sni]); | ||||
|         cb(null, secureContexts[sni]); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       if (!Array.isArray(argv.root)) { | ||||
|         argv.root = [argv.root]; | ||||
|       } | ||||
| 
 | ||||
|       opts.httpsOptions.key = fs.readFileSync(argv.key); | ||||
|       opts.httpsOptions.cert = fs.readFileSync(argv.cert); | ||||
| 
 | ||||
|       // turn multiple-cert pemfile into array of cert strings
 | ||||
|       peerCa = argv.root.reduce(function (roots, fullpath) { | ||||
|         if (!fs.existsSync(fullpath)) { | ||||
|           return roots; | ||||
|         } | ||||
| 
 | ||||
|         return roots.concat(fs.readFileSync(fullpath, 'ascii') | ||||
|         .split('-----END CERTIFICATE-----') | ||||
|         .filter(function (ca) { | ||||
|           return ca.trim(); | ||||
|         }).map(function (ca) { | ||||
|           return (ca + '-----END CERTIFICATE-----').trim(); | ||||
|         })); | ||||
|       }, []); | ||||
| 
 | ||||
|       // TODO * `--verify /path/to/root.pem` require peers to present certificates from said authority
 | ||||
|       if (argv.verify) { | ||||
|         opts.httpsOptions.ca = peerCa; | ||||
|         opts.httpsOptions.requestCert = true; | ||||
|         opts.httpsOptions.rejectUnauthorized = true; | ||||
|       } | ||||
| 
 | ||||
|       if (argv['serve-root']) { | ||||
|         content = peerCa.join('\r\n'); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     opts.cwd = process.cwd(); | ||||
|     opts.sites = []; | ||||
|     opts.sites._map = {}; | ||||
| 
 | ||||
|     if (argv.sites) { | ||||
|       opts._externalHost = false; | ||||
|       argv.sites.split(',').map(function (name) { | ||||
|         var nameparts = name.split('|'); | ||||
|         var servername = nameparts.shift(); | ||||
|         var modules; | ||||
| 
 | ||||
|         opts._externalHost = opts._externalHost || !/(^|\.)localhost\./.test(servername); | ||||
|         // TODO allow reverse proxy
 | ||||
|         if (!opts.sites._map[servername]) { | ||||
|           opts.sites._map[servername] =  { $id: servername, paths: [] }; | ||||
|           opts.sites._map[servername].paths._map = {}; | ||||
|           opts.sites.push(opts.sites._map[servername]); | ||||
|         } | ||||
| 
 | ||||
|         if (!nameparts.length) { | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         if (!opts.sites._map[servername].paths._map['/']) { | ||||
|           opts.sites._map[servername].paths._map['/'] = { $id: '/', modules: [] }; | ||||
|           opts.sites._map[servername].paths.push(opts.sites._map[servername].paths._map['/']); | ||||
|         } | ||||
| 
 | ||||
|         modules = opts.sites._map[servername].paths._map['/'].modules; | ||||
|         modules.push({ | ||||
|           $id: 'serve' | ||||
|         , paths: nameparts | ||||
|         }); | ||||
|         modules.push({ | ||||
|           $id: 'indexes' | ||||
|         , paths: nameparts | ||||
|         }); | ||||
|       }); | ||||
|     if (!program.greenlock) { | ||||
|       program.greenlock = getAcme(); | ||||
|     } | ||||
|     (program.greenlock.tlsOptions||program.greenlock.httpsOptions).SNICallback(servername, cb); | ||||
|   }; | ||||
| 
 | ||||
|     opts.groups = []; | ||||
|   program.tlsTunnelServer = tls.createServer(tunnelAdminTlsOpts, function (tlsSocket) { | ||||
|     console.log('(pre-terminated) tls connection'); | ||||
|     // things get a little messed up here
 | ||||
|     //tlsSocket.on('data', function (chunk) {
 | ||||
|     //  console.log('terminated data:', chunk.toString());
 | ||||
|     //});
 | ||||
|     //(program.httpTunnelServer || program.httpServer).emit('connection', tlsSocket);
 | ||||
| 		//tcpRouter.get(conn.localAddress, conn.localPort)(conn, firstChunk, { secure: false });
 | ||||
|     handler(tlsSocket, { secure: true }); | ||||
|   }); | ||||
| 
 | ||||
|     // 'packages', 'assets', 'com.daplie.caddy'
 | ||||
|     opts.global = { | ||||
|       modules: [ // TODO uh-oh we've got a mixed bag of modules (various types), a true map
 | ||||
|         { $id: 'greenlock', email: opts.email, tos: opts.tos } | ||||
|       , { $id: 'rvpn', email: opts.email, tos: opts.tos } | ||||
|       , { $id: 'content', content: content } | ||||
|       , { $id: 'livereload', on: opts.livereload } | ||||
|       , { $id: 'app', path: opts.expressApp } | ||||
|       ] | ||||
|     , paths: [ | ||||
|         { $id: '/assets/', modules: [ { $id: 'serve', paths: [ assetsPath ] } ] } | ||||
|         // TODO figure this b out
 | ||||
|       , { $id: '/.well-known/', modules: [ | ||||
|           { $id: 'serve', paths: [ path.join(assetsPath, 'well-known') ] } | ||||
|         ] } | ||||
|       ] | ||||
|     }; | ||||
|     opts.defaults = { | ||||
|       modules: [] | ||||
|     , paths: [ | ||||
|         { $id: '/', modules: [ | ||||
|           { $id: 'serve', paths: [ defaultWebRoot ] } | ||||
|         , { $id: 'indexes', paths: [ defaultWebRoot ] } | ||||
|         ] } | ||||
|       ] | ||||
|     }; | ||||
|     opts.sites.push({ | ||||
|       // greenlock: {}
 | ||||
|       $id: 'localhost.alpha.daplie.me' | ||||
|     , paths: [ | ||||
|         { $id: '/', modules: [ | ||||
|           { $id: 'serve', paths: [ path.resolve(__dirname, '..', 'admin', 'public') ] } | ||||
|         ] } | ||||
|       , { $id: '/api/', modules: [ | ||||
|           { $id: 'app', path: path.join(__dirname, 'admin') } | ||||
|         ] } | ||||
|       ] | ||||
|     }); | ||||
|     opts.sites.push({ | ||||
|       $id: 'localhost.daplie.invalid' | ||||
|     , paths: [ | ||||
|         { $id: '/', modules: [ { $id: 'serve', paths: [ path.resolve(__dirname, '..', 'admin', 'public') ] } ] } | ||||
|       , { $id: '/api/', modules: [ { $id: 'app', path: path.join(__dirname, 'admin') } ] } | ||||
|       ] | ||||
|     }); | ||||
| 
 | ||||
|     // ifaces
 | ||||
|     opts.ifaces = require('../lib/local-ip.js').find(); | ||||
| 
 | ||||
|     // TODO use arrays in all things
 | ||||
|     opts._old_server_name = opts.sites[0].$id; | ||||
|     opts.pubdir = defaultWebRoot.replace(/(:hostname|:servername).*/, ''); | ||||
| 
 | ||||
|     if (argv.p || argv.port || argv._[0]) { | ||||
|       opts.manualPort = true; | ||||
|     } | ||||
|     if (argv.t || argv.tunnel) { | ||||
|       opts.tunnel = true; | ||||
|     } | ||||
|     if (argv.i || argv['insecure-port']) { | ||||
|       opts.manualInsecurePort = true; | ||||
|     } | ||||
|     opts.insecurePort = parseInt(argv.i || argv['insecure-port'], 10) | ||||
|       || argv.i || argv['insecure-port'] | ||||
|       || httpPort | ||||
|       ; | ||||
|     opts.livereload = livereload; | ||||
| 
 | ||||
|     if (argv['express-app']) { | ||||
|       opts.expressApp = require(argv['express-app']); | ||||
|     } | ||||
| 
 | ||||
|     if (opts.email || opts._externalHost) { | ||||
|       if (!opts.agreeTos) { | ||||
|         console.warn("You may need to specify --agree-tos to agree to both the Let's Encrypt and Daplie DNS terms of service."); | ||||
|       } | ||||
|       if (!opts.email) { | ||||
|         // TODO store email in .ddnsrc.json
 | ||||
|         console.warn("You may need to specify --email to register with both the Let's Encrypt and Daplie DNS."); | ||||
|       } | ||||
|       DDNS = require('ddns-cli'); | ||||
|       p = DDNS.refreshToken({ | ||||
|         email: opts.email | ||||
|       , providerUrl: opts.provider | ||||
|       , silent: true | ||||
|       , homedir: opts.homedir | ||||
|       }, { | ||||
|         debug: false | ||||
|       , email: opts.argv.email | ||||
|       }).then(function (refreshToken) { | ||||
|         opts.refreshToken = refreshToken; | ||||
|       }); | ||||
|     } | ||||
|     else { | ||||
|       p = PromiseA.resolve(); | ||||
|     } | ||||
| 
 | ||||
|     return p.then(function () { | ||||
| 
 | ||||
|     // can be changed to tunnel external port
 | ||||
|     opts.redirectOptions = { | ||||
|       port: opts.port | ||||
|     }; | ||||
|     opts.redirectApp = require('redirect-https')(opts.redirectOptions); | ||||
| 
 | ||||
|     return createServer(port, null, content, opts).then(function (servers) { | ||||
|       var p; | ||||
|       var httpsUrl; | ||||
|       var httpUrl; | ||||
|       var promise; | ||||
| 
 | ||||
|       // TODO show all sites
 | ||||
|       console.info(''); | ||||
|       console.info('Serving ' + opts.pubdir + ' at '); | ||||
|       console.info(''); | ||||
| 
 | ||||
|       // Port
 | ||||
|       httpsUrl = 'https://' + opts._old_server_name; | ||||
|       p = opts.port; | ||||
|       if (httpsPort !== p) { | ||||
|         httpsUrl += ':' + p; | ||||
|       } | ||||
|       console.info('\t' + httpsUrl); | ||||
| 
 | ||||
|       // Insecure Port
 | ||||
|       httpUrl = 'http://' + opts._old_server_name; | ||||
|       p = opts.insecurePort; | ||||
|       if (httpPort !== p) { | ||||
|         httpUrl += ':' + p; | ||||
|       } | ||||
|       console.info('\t' + httpUrl + ' (redirecting to https)'); | ||||
|       console.info(''); | ||||
| 
 | ||||
|       if (!(argv.sites && (defaultServername !== argv.sites) && !(argv.key && argv.cert))) { | ||||
|         // TODO what is this condition actually intending to test again?
 | ||||
|         // (I think it can be replaced with if (!opts._externalHost) { ... }
 | ||||
| 
 | ||||
|         promise = PromiseA.resolve(); | ||||
|       } else { | ||||
|         console.info("Attempting to resolve external connection for '" + opts._old_server_name + "'"); | ||||
|         try { | ||||
|           promise = require('../lib/match-ips.js').match(opts._old_server_name, opts); | ||||
|         } catch(e) { | ||||
|           console.warn("Upgrade to version 2.x to use automatic certificate issuance for '" + opts._old_server_name + "'"); | ||||
|           promise = PromiseA.resolve(); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       return promise.then(function (matchingIps) { | ||||
|         if (matchingIps) { | ||||
|           if (!matchingIps.length) { | ||||
|             console.info("Neither the attached nor external interfaces match '" + opts._old_server_name + "'"); | ||||
|           } | ||||
|         } | ||||
|         opts.matchingIps = matchingIps || []; | ||||
| 
 | ||||
|         if (opts.matchingIps.length) { | ||||
|           console.info(''); | ||||
|           console.info('External IPs:'); | ||||
|           console.info(''); | ||||
|           opts.matchingIps.forEach(function (ip) { | ||||
|             if ('IPv4' === ip.family) { | ||||
|               httpsUrl = 'https://' + ip.address; | ||||
|               if (httpsPort !== opts.port) { | ||||
|                 httpsUrl += ':' + opts.port; | ||||
|               } | ||||
|               console.info('\t' + httpsUrl); | ||||
|             } | ||||
|             else { | ||||
|               httpsUrl = 'https://[' + ip.address + ']'; | ||||
|               if (httpsPort !== opts.port) { | ||||
|                 httpsUrl += ':' + opts.port; | ||||
|               } | ||||
|               console.info('\t' + httpsUrl); | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|         else if (!opts.tunnel) { | ||||
|           console.info("External IP address does not match local IP address."); | ||||
|           console.info("Use --tunnel to allow the people of the Internet to access your server."); | ||||
|         } | ||||
| 
 | ||||
|         if (opts.tunnel) { | ||||
|           require('../lib/tunnel.js').create(opts, servers); | ||||
|         } | ||||
|         else if (opts.ddns) { | ||||
|           require('../lib/ddns.js').create(opts, servers); | ||||
|         } | ||||
| 
 | ||||
|         Object.keys(opts.ifaces).forEach(function (iname) { | ||||
|           var iface = opts.ifaces[iname]; | ||||
| 
 | ||||
|           if (iface.ipv4.length) { | ||||
|             console.info(''); | ||||
|             console.info(iname + ':'); | ||||
| 
 | ||||
|             httpsUrl = 'https://' + iface.ipv4[0].address; | ||||
|             if (httpsPort !== opts.port) { | ||||
|               httpsUrl += ':' + opts.port; | ||||
|             } | ||||
|             console.info('\t' + httpsUrl); | ||||
| 
 | ||||
|             if (iface.ipv6.length) { | ||||
|               httpsUrl = 'https://[' + iface.ipv6[0].address + ']'; | ||||
|               if (httpsPort !== opts.port) { | ||||
|                 httpsUrl += ':' + opts.port; | ||||
|               } | ||||
|               console.info('\t' + httpsUrl); | ||||
|             } | ||||
|           } | ||||
|         }); | ||||
| 
 | ||||
|         console.info(''); | ||||
|       }); | ||||
|     }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   run(); | ||||
|   PromiseA.all(config.tcp.ports.map(function (port) { | ||||
|     return listeners.tcp.add(port, handler); | ||||
|   })); | ||||
| }; | ||||
|  | ||||
							
								
								
									
										66
									
								
								lib/modules/admin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								lib/modules/admin.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,66 @@ | ||||
| module.exports.create = function (deps, conf) { | ||||
|   'use strict'; | ||||
| 
 | ||||
|   var path = require('path'); | ||||
|   //var defaultServername = 'localhost.daplie.me';
 | ||||
|   var defaultWebRoot = '.'; | ||||
|   var assetsPath = path.join(__dirname, '..', '..', 'packages', 'assets'); | ||||
|   var opts = /*conf.http ||*/ {}; | ||||
| 
 | ||||
|   opts.sites = []; | ||||
|   opts.sites._map = {}; | ||||
| 
 | ||||
|   // argv.sites
 | ||||
| 
 | ||||
|   opts.groups = []; | ||||
| 
 | ||||
|   // 'packages', 'assets', 'com.daplie.caddy'
 | ||||
|   opts.global = { | ||||
|     modules: [ // TODO uh-oh we've got a mixed bag of modules (various types), a true map
 | ||||
|       { $id: 'greenlock', email: opts.email, tos: opts.tos } | ||||
|     , { $id: 'rvpn', email: opts.email, tos: opts.tos } | ||||
|     //, { $id: 'content', content: content }
 | ||||
|     , { $id: 'livereload', on: opts.livereload } | ||||
|     , { $id: 'app', path: opts.expressApp } | ||||
|     ] | ||||
|   , paths: [ | ||||
|       { $id: '/assets/', modules: [ { $id: 'serve', paths: [ assetsPath ] } ] } | ||||
|       // TODO figure this b out
 | ||||
|     , { $id: '/.well-known/', modules: [ | ||||
|         { $id: 'serve', paths: [ path.join(assetsPath, 'well-known') ] } | ||||
|       ] } | ||||
|     ] | ||||
|   }; | ||||
|   opts.defaults = { | ||||
|     modules: [] | ||||
|   , paths: [ | ||||
|       { $id: '/', modules: [ | ||||
|         { $id: 'serve', paths: [ defaultWebRoot ] } | ||||
|       , { $id: 'indexes', paths: [ defaultWebRoot ] } | ||||
|       ] } | ||||
|     ] | ||||
|   }; | ||||
|   opts.sites.push({ | ||||
|     // greenlock: {}
 | ||||
|     $id: 'localhost.alpha.daplie.me' | ||||
|   , paths: [ | ||||
|       { $id: '/', modules: [ | ||||
|         { $id: 'serve', paths: [ path.resolve(__dirname, '..', 'admin', 'public') ] } | ||||
|       ] } | ||||
|     , { $id: '/api/', modules: [ | ||||
|         { $id: 'app', path: path.join(__dirname, 'admin') } | ||||
|       ] } | ||||
|     ] | ||||
|   }); | ||||
|   opts.sites.push({ | ||||
|     $id: 'localhost.daplie.invalid' | ||||
|   , paths: [ | ||||
|       { $id: '/', modules: [ { $id: 'serve', paths: [ path.resolve(__dirname, '..', 'admin', 'public') ] } ] } | ||||
|     , { $id: '/api/', modules: [ { $id: 'app', path: path.join(__dirname, 'admin') } ] } | ||||
|     ] | ||||
|   }); | ||||
| 
 | ||||
|   var app = require('../app.js')(deps, { cwd: conf.cwd, http: opts }); | ||||
|   var http = require('http'); | ||||
|   return http.createServer(app); | ||||
| }; | ||||
							
								
								
									
										392
									
								
								lib/modules/http.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										392
									
								
								lib/modules/http.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,392 @@ | ||||
|   function run() { | ||||
|     var defaultServername = 'localhost.daplie.me'; | ||||
|     var minimist = require('minimist'); | ||||
|     var argv = minimist(process.argv.slice(2)); | ||||
|     var port = parseInt(argv.p || argv.port || argv._[0], 10) || httpsPort; | ||||
|     var livereload = argv.livereload; | ||||
|     var defaultWebRoot = path.normalize(argv['default-web-root'] || argv.d || argv._[1] || '.'); | ||||
|     var assetsPath = path.join(__dirname, '..', 'packages', 'assets'); | ||||
|     var content = argv.c; | ||||
|     var letsencryptHost = argv['letsencrypt-certs']; | ||||
|     var yaml = require('js-yaml'); | ||||
|     var fs = PromiseA.promisifyAll(require('fs')); | ||||
|     var configFile = argv.c || argv.conf || argv.config; | ||||
|     var config; | ||||
|     var DDNS; | ||||
|     console.log('defaultWebRoot', defaultWebRoot); | ||||
| 
 | ||||
|     try { | ||||
|       config = fs.readFileSync(configFile || 'goldilocks.yml'); | ||||
|     } catch(e) { | ||||
|       if (configFile) { | ||||
|         console.error('Failed to read config:', e); | ||||
|         process.exit(1); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (config) { | ||||
|       try { | ||||
|         config = yaml.safeLoad(config); | ||||
|       } catch(e) { | ||||
|         console.error('Failed to parse config:', e); | ||||
|         process.exit(1); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (argv.V || argv.version || argv.v) { | ||||
|       if (argv.v) { | ||||
|         console.warn("flag -v is reserved for future use. Use -V or --version for version information."); | ||||
|       } | ||||
|       console.info('v' + require('../package.json').version); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     argv.sites = argv.sites; | ||||
| 
 | ||||
|     // letsencrypt
 | ||||
|     var httpsOptions = require('localhost.daplie.me-certificates').merge({}); | ||||
|     var secureContext; | ||||
| 
 | ||||
|     var opts = { | ||||
|       agreeTos: argv.agreeTos || argv['agree-tos'] | ||||
|     , debug: argv.debug | ||||
|     , device: argv.device | ||||
|     , provider: (argv.provider && 'false' !== argv.provider) ? argv.provider : 'oauth3.org' | ||||
|     , email: argv.email | ||||
|     , httpsOptions: { | ||||
|         key: httpsOptions.key | ||||
|       , cert: httpsOptions.cert | ||||
|       //, ca: httpsOptions.ca
 | ||||
|       } | ||||
|     , homedir: argv.homedir | ||||
|     , argv: argv | ||||
|     }; | ||||
|     var peerCa; | ||||
|     var p; | ||||
| 
 | ||||
|     opts.PromiseA = PromiseA; | ||||
|     opts.httpsOptions.SNICallback = function (sni, cb) { | ||||
|       if (!secureContext) { | ||||
|         secureContext = tls.createSecureContext(opts.httpsOptions); | ||||
|       } | ||||
|       cb(null, secureContext); | ||||
|       return; | ||||
|     }; | ||||
| 
 | ||||
|     if (letsencryptHost) { | ||||
|       // TODO remove in v3.x (aka goldilocks)
 | ||||
|       argv.key = argv.key || '/etc/letsencrypt/live/' + letsencryptHost + '/privkey.pem'; | ||||
|       argv.cert = argv.cert || '/etc/letsencrypt/live/' + letsencryptHost + '/fullchain.pem'; | ||||
|       argv.root = argv.root || argv.chain || ''; | ||||
|       argv.sites = argv.sites || letsencryptHost; | ||||
|       argv['serve-root'] = argv['serve-root'] || argv['serve-chain']; | ||||
|       // argv[express-app]
 | ||||
|     } | ||||
| 
 | ||||
|     if (argv['serve-root'] && !argv.root) { | ||||
|       console.error("You must specify bath --root to use --serve-root"); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (argv.key || argv.cert || argv.root) { | ||||
|       if (!argv.key || !argv.cert) { | ||||
|         console.error("You must specify bath --key and --cert, and optionally --root (required with serve-root)"); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       if (!Array.isArray(argv.root)) { | ||||
|         argv.root = [argv.root]; | ||||
|       } | ||||
| 
 | ||||
|       opts.httpsOptions.key = fs.readFileSync(argv.key); | ||||
|       opts.httpsOptions.cert = fs.readFileSync(argv.cert); | ||||
| 
 | ||||
|       // turn multiple-cert pemfile into array of cert strings
 | ||||
|       peerCa = argv.root.reduce(function (roots, fullpath) { | ||||
|         if (!fs.existsSync(fullpath)) { | ||||
|           return roots; | ||||
|         } | ||||
| 
 | ||||
|         return roots.concat(fs.readFileSync(fullpath, 'ascii') | ||||
|         .split('-----END CERTIFICATE-----') | ||||
|         .filter(function (ca) { | ||||
|           return ca.trim(); | ||||
|         }).map(function (ca) { | ||||
|           return (ca + '-----END CERTIFICATE-----').trim(); | ||||
|         })); | ||||
|       }, []); | ||||
| 
 | ||||
|       // TODO * `--verify /path/to/root.pem` require peers to present certificates from said authority
 | ||||
|       if (argv.verify) { | ||||
|         opts.httpsOptions.ca = peerCa; | ||||
|         opts.httpsOptions.requestCert = true; | ||||
|         opts.httpsOptions.rejectUnauthorized = true; | ||||
|       } | ||||
| 
 | ||||
|       if (argv['serve-root']) { | ||||
|         content = peerCa.join('\r\n'); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     opts.cwd = process.cwd(); | ||||
|     opts.sites = []; | ||||
|     opts.sites._map = {}; | ||||
| 
 | ||||
|     if (argv.sites) { | ||||
|       opts._externalHost = false; | ||||
|       argv.sites.split(',').map(function (name) { | ||||
|         var nameparts = name.split('|'); | ||||
|         var servername = nameparts.shift(); | ||||
|         var modules; | ||||
| 
 | ||||
|         opts._externalHost = opts._externalHost || !/(^|\.)localhost\./.test(servername); | ||||
|         // TODO allow reverse proxy
 | ||||
|         if (!opts.sites._map[servername]) { | ||||
|           opts.sites._map[servername] =  { $id: servername, paths: [] }; | ||||
|           opts.sites._map[servername].paths._map = {}; | ||||
|           opts.sites.push(opts.sites._map[servername]); | ||||
|         } | ||||
| 
 | ||||
|         if (!nameparts.length) { | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         if (!opts.sites._map[servername].paths._map['/']) { | ||||
|           opts.sites._map[servername].paths._map['/'] = { $id: '/', modules: [] }; | ||||
|           opts.sites._map[servername].paths.push(opts.sites._map[servername].paths._map['/']); | ||||
|         } | ||||
| 
 | ||||
|         modules = opts.sites._map[servername].paths._map['/'].modules; | ||||
|         modules.push({ | ||||
|           $id: 'serve' | ||||
|         , paths: nameparts | ||||
|         }); | ||||
|         modules.push({ | ||||
|           $id: 'indexes' | ||||
|         , paths: nameparts | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     opts.groups = []; | ||||
| 
 | ||||
|     // 'packages', 'assets', 'com.daplie.caddy'
 | ||||
|     opts.global = { | ||||
|       modules: [ // TODO uh-oh we've got a mixed bag of modules (various types), a true map
 | ||||
|         { $id: 'greenlock', email: opts.email, tos: opts.tos } | ||||
|       , { $id: 'rvpn', email: opts.email, tos: opts.tos } | ||||
|       , { $id: 'content', content: content } | ||||
|       , { $id: 'livereload', on: opts.livereload } | ||||
|       , { $id: 'app', path: opts.expressApp } | ||||
|       ] | ||||
|     , paths: [ | ||||
|         { $id: '/assets/', modules: [ { $id: 'serve', paths: [ assetsPath ] } ] } | ||||
|         // TODO figure this b out
 | ||||
|       , { $id: '/.well-known/', modules: [ | ||||
|           { $id: 'serve', paths: [ path.join(assetsPath, 'well-known') ] } | ||||
|         ] } | ||||
|       ] | ||||
|     }; | ||||
|     opts.defaults = { | ||||
|       modules: [] | ||||
|     , paths: [ | ||||
|         { $id: '/', modules: [ | ||||
|           { $id: 'serve', paths: [ defaultWebRoot ] } | ||||
|         , { $id: 'indexes', paths: [ defaultWebRoot ] } | ||||
|         ] } | ||||
|       ] | ||||
|     }; | ||||
|     opts.sites.push({ | ||||
|       // greenlock: {}
 | ||||
|       $id: 'localhost.alpha.daplie.me' | ||||
|     , paths: [ | ||||
|         { $id: '/', modules: [ | ||||
|           { $id: 'serve', paths: [ path.resolve(__dirname, '..', 'admin', 'public') ] } | ||||
|         ] } | ||||
|       , { $id: '/api/', modules: [ | ||||
|           { $id: 'app', path: path.join(__dirname, 'admin') } | ||||
|         ] } | ||||
|       ] | ||||
|     }); | ||||
|     opts.sites.push({ | ||||
|       $id: 'localhost.daplie.invalid' | ||||
|     , paths: [ | ||||
|         { $id: '/', modules: [ { $id: 'serve', paths: [ path.resolve(__dirname, '..', 'admin', 'public') ] } ] } | ||||
|       , { $id: '/api/', modules: [ { $id: 'app', path: path.join(__dirname, 'admin') } ] } | ||||
|       ] | ||||
|     }); | ||||
| 
 | ||||
|     // ifaces
 | ||||
|     opts.ifaces = require('../lib/local-ip.js').find(); | ||||
| 
 | ||||
|     // TODO use arrays in all things
 | ||||
|     opts._old_server_name = opts.sites[0].$id; | ||||
|     opts.pubdir = defaultWebRoot.replace(/(:hostname|:servername).*/, ''); | ||||
| 
 | ||||
|     if (argv.p || argv.port || argv._[0]) { | ||||
|       opts.manualPort = true; | ||||
|     } | ||||
|     if (argv.t || argv.tunnel) { | ||||
|       opts.tunnel = true; | ||||
|     } | ||||
|     if (argv.i || argv['insecure-port']) { | ||||
|       opts.manualInsecurePort = true; | ||||
|     } | ||||
|     opts.insecurePort = parseInt(argv.i || argv['insecure-port'], 10) | ||||
|       || argv.i || argv['insecure-port'] | ||||
|       || httpPort | ||||
|       ; | ||||
|     opts.livereload = livereload; | ||||
| 
 | ||||
|     if (argv['express-app']) { | ||||
|       opts.expressApp = require(argv['express-app']); | ||||
|     } | ||||
| 
 | ||||
|     if (opts.email || opts._externalHost) { | ||||
|       if (!opts.agreeTos) { | ||||
|         console.warn("You may need to specify --agree-tos to agree to both the Let's Encrypt and Daplie DNS terms of service."); | ||||
|       } | ||||
|       if (!opts.email) { | ||||
|         // TODO store email in .ddnsrc.json
 | ||||
|         console.warn("You may need to specify --email to register with both the Let's Encrypt and Daplie DNS."); | ||||
|       } | ||||
|       DDNS = require('ddns-cli'); | ||||
|       p = DDNS.refreshToken({ | ||||
|         email: opts.email | ||||
|       , providerUrl: opts.provider | ||||
|       , silent: true | ||||
|       , homedir: opts.homedir | ||||
|       }, { | ||||
|         debug: false | ||||
|       , email: opts.argv.email | ||||
|       }).then(function (refreshToken) { | ||||
|         opts.refreshToken = refreshToken; | ||||
|       }); | ||||
|     } | ||||
|     else { | ||||
|       p = PromiseA.resolve(); | ||||
|     } | ||||
| 
 | ||||
|     return p.then(function () { | ||||
| 
 | ||||
|     // can be changed to tunnel external port
 | ||||
|     opts.redirectOptions = { | ||||
|       port: opts.port | ||||
|     }; | ||||
|     opts.redirectApp = require('redirect-https')(opts.redirectOptions); | ||||
| 
 | ||||
|     return createServer(port, null, content, opts).then(function (servers) { | ||||
|       var p; | ||||
|       var httpsUrl; | ||||
|       var httpUrl; | ||||
|       var promise; | ||||
| 
 | ||||
|       // TODO show all sites
 | ||||
|       console.info(''); | ||||
|       console.info('Serving ' + opts.pubdir + ' at '); | ||||
|       console.info(''); | ||||
| 
 | ||||
|       // Port
 | ||||
|       httpsUrl = 'https://' + opts._old_server_name; | ||||
|       p = opts.port; | ||||
|       if (httpsPort !== p) { | ||||
|         httpsUrl += ':' + p; | ||||
|       } | ||||
|       console.info('\t' + httpsUrl); | ||||
| 
 | ||||
|       // Insecure Port
 | ||||
|       httpUrl = 'http://' + opts._old_server_name; | ||||
|       p = opts.insecurePort; | ||||
|       if (httpPort !== p) { | ||||
|         httpUrl += ':' + p; | ||||
|       } | ||||
|       console.info('\t' + httpUrl + ' (redirecting to https)'); | ||||
|       console.info(''); | ||||
| 
 | ||||
|       if (!(argv.sites && (defaultServername !== argv.sites) && !(argv.key && argv.cert))) { | ||||
|         // TODO what is this condition actually intending to test again?
 | ||||
|         // (I think it can be replaced with if (!opts._externalHost) { ... }
 | ||||
| 
 | ||||
|         promise = PromiseA.resolve(); | ||||
|       } else { | ||||
|         console.info("Attempting to resolve external connection for '" + opts._old_server_name + "'"); | ||||
|         try { | ||||
|           promise = require('../lib/match-ips.js').match(opts._old_server_name, opts); | ||||
|         } catch(e) { | ||||
|           console.warn("Upgrade to version 2.x to use automatic certificate issuance for '" + opts._old_server_name + "'"); | ||||
|           promise = PromiseA.resolve(); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       return promise.then(function (matchingIps) { | ||||
|         if (matchingIps) { | ||||
|           if (!matchingIps.length) { | ||||
|             console.info("Neither the attached nor external interfaces match '" + opts._old_server_name + "'"); | ||||
|           } | ||||
|         } | ||||
|         opts.matchingIps = matchingIps || []; | ||||
| 
 | ||||
|         if (opts.matchingIps.length) { | ||||
|           console.info(''); | ||||
|           console.info('External IPs:'); | ||||
|           console.info(''); | ||||
|           opts.matchingIps.forEach(function (ip) { | ||||
|             if ('IPv4' === ip.family) { | ||||
|               httpsUrl = 'https://' + ip.address; | ||||
|               if (httpsPort !== opts.port) { | ||||
|                 httpsUrl += ':' + opts.port; | ||||
|               } | ||||
|               console.info('\t' + httpsUrl); | ||||
|             } | ||||
|             else { | ||||
|               httpsUrl = 'https://[' + ip.address + ']'; | ||||
|               if (httpsPort !== opts.port) { | ||||
|                 httpsUrl += ':' + opts.port; | ||||
|               } | ||||
|               console.info('\t' + httpsUrl); | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|         else if (!opts.tunnel) { | ||||
|           console.info("External IP address does not match local IP address."); | ||||
|           console.info("Use --tunnel to allow the people of the Internet to access your server."); | ||||
|         } | ||||
| 
 | ||||
|         if (opts.tunnel) { | ||||
|           require('../lib/tunnel.js').create(opts, servers); | ||||
|         } | ||||
|         else if (opts.ddns) { | ||||
|           require('../lib/ddns.js').create(opts, servers); | ||||
|         } | ||||
| 
 | ||||
|         Object.keys(opts.ifaces).forEach(function (iname) { | ||||
|           var iface = opts.ifaces[iname]; | ||||
| 
 | ||||
|           if (iface.ipv4.length) { | ||||
|             console.info(''); | ||||
|             console.info(iname + ':'); | ||||
| 
 | ||||
|             httpsUrl = 'https://' + iface.ipv4[0].address; | ||||
|             if (httpsPort !== opts.port) { | ||||
|               httpsUrl += ':' + opts.port; | ||||
|             } | ||||
|             console.info('\t' + httpsUrl); | ||||
| 
 | ||||
|             if (iface.ipv6.length) { | ||||
|               httpsUrl = 'https://[' + iface.ipv6[0].address + ']'; | ||||
|               if (httpsPort !== opts.port) { | ||||
|                 httpsUrl += ':' + opts.port; | ||||
|               } | ||||
|               console.info('\t' + httpsUrl); | ||||
|             } | ||||
|           } | ||||
|         }); | ||||
| 
 | ||||
|         console.info(''); | ||||
|       }); | ||||
|     }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   run(); | ||||
							
								
								
									
										107
									
								
								lib/servers.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								lib/servers.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,107 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| var serversMap = module.exports._serversMap = {}; | ||||
| 
 | ||||
| module.exports.addTcpListener = function (port, handler) { | ||||
|   var PromiseA = require('bluebird'); | ||||
| 
 | ||||
|   return new PromiseA(function (resolve, reject) { | ||||
|     var stat = serversMap[port] || serversMap[port]; | ||||
| 
 | ||||
|     if (stat) { | ||||
|       if (stat._closing) { | ||||
|         module.exports.destroyTcpListener(port); | ||||
|       } | ||||
|       else if (handler !== stat.handler) { | ||||
| 
 | ||||
|         // we'll replace the current listener
 | ||||
|         stat.handler = handler; | ||||
|         resolve(); | ||||
|         return; | ||||
|       } | ||||
|       else { | ||||
|         // this exact listener is already open
 | ||||
|         resolve(); | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     var enableDestroy = require('server-destroy'); | ||||
|     var net = require('net'); | ||||
|     var resolved; | ||||
|     var server = net.createServer(); | ||||
| 
 | ||||
|     stat = serversMap[port] = { | ||||
|       server: server | ||||
|     , handler: handler | ||||
|     , _closing: false | ||||
|     }; | ||||
| 
 | ||||
|     server.on('connection', function (conn) { | ||||
|       conn.__port = port; | ||||
|       conn.__proto = 'tcp'; | ||||
|       stat.handler(conn); | ||||
|     }); | ||||
|     server.on('error', function (e) { | ||||
|       delete serversMap[port]; | ||||
| 
 | ||||
|       if (!resolved) { | ||||
|         reject(e); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       if (handler.onError) { | ||||
|         handler.onError(e); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       throw e; | ||||
|     }); | ||||
| 
 | ||||
|     server.listen(port, function () { | ||||
|       resolved = true; | ||||
|       resolve(); | ||||
|     }); | ||||
| 
 | ||||
|     enableDestroy(server); // adds .destroy
 | ||||
|   }); | ||||
| }; | ||||
| module.exports.closeTcpListener = function (port) { | ||||
|   var PromiseA = require('bluebird'); | ||||
| 
 | ||||
|   return new PromiseA(function (resolve) { | ||||
|     var stat = serversMap[port]; | ||||
|     if (!stat) { | ||||
|       return; | ||||
|     } | ||||
|     stat.server.on('close', function () { | ||||
|       // once the clients close too
 | ||||
|       delete serversMap[port]; | ||||
|       if (stat._closing) { | ||||
|         stat._closing(); // resolve
 | ||||
|         stat._closing = null; | ||||
|       } | ||||
|       stat = null; | ||||
|     }); | ||||
|     stat._closing = resolve; | ||||
|     stat.server.close(); | ||||
|   }); | ||||
| }; | ||||
| module.exports.destroyTcpListener = function (port) { | ||||
|   var stat = serversMap[port]; | ||||
|   delete serversMap[port]; | ||||
|   stat.server.destroy(); | ||||
|   if (stat._closing) { | ||||
|     stat._closing(); | ||||
|     stat._closing = null; | ||||
|   } | ||||
|   stat = null; | ||||
| }; | ||||
| 
 | ||||
| module.exports.listeners = { | ||||
|   tcp: { | ||||
|     add: module.exports.addTcpListener | ||||
|   , close: module.exports.closeTcpListener | ||||
|   , destroy: module.exports.destroyTcpListener | ||||
|   } | ||||
| }; | ||||
							
								
								
									
										9
									
								
								lib/worker.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								lib/worker.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| // TODO needs some sort of config-sync
 | ||||
| process.on('message', function (conf) { | ||||
|   var deps = { | ||||
|     messenger: process | ||||
|   }; | ||||
|   require('./goldilocks.js').create(deps, conf); | ||||
| }); | ||||
| @ -63,6 +63,7 @@ | ||||
|     "scmp": "git+https://github.com/freewil/scmp.git#1.x", | ||||
|     "serve-index": "^1.7.0", | ||||
|     "serve-static": "^1.10.0", | ||||
|     "server-destroy": "^1.0.1", | ||||
|     "stunnel": "git+https://git.daplie.com/Daplie/node-tunnel-client.git#v1" | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| module.exports.dependencies = [ 'OAUTH3', 'storage.owners', 'options.device' ]; | ||||
| module.exports.create = function (deps) { | ||||
| module.exports.create = function (deps, conf) { | ||||
|   var scmp = require('scmp'); | ||||
|   var crypto = require('crypto'); | ||||
|   var jwt = require('jsonwebtoken'); | ||||
| @ -69,7 +69,7 @@ module.exports.create = function (deps) { | ||||
| 
 | ||||
|         if (req.body.ip_url) { | ||||
|           // TODO set options / GunDB
 | ||||
|           deps.options.ip_url = req.body.ip_url; | ||||
|           conf.ip_url = req.body.ip_url; | ||||
|         } | ||||
| 
 | ||||
|         return deps.storage.owners.all().then(function (results) { | ||||
| @ -139,7 +139,7 @@ module.exports.create = function (deps) { | ||||
|       isAuthorized(req, res, function () { | ||||
|         if ('POST' !== req.method) { | ||||
|           res.setHeader('Content-Type', 'application/json;'); | ||||
|           res.end(JSON.stringify(deps.recase.snakeCopy(deps.options))); | ||||
|           res.end(JSON.stringify(deps.recase.snakeCopy(conf.snake_copy))); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user