diff --git a/bin/goldilocks.js b/bin/goldilocks.js index b19d5e0..70526cd 100755 --- a/bin/goldilocks.js +++ b/bin/goldilocks.js @@ -69,26 +69,34 @@ function readConfigAndRun(args) { var recase = require('recase').create({}); config = recase.camelCopy(config); + config.debug = config.debug || args.debug; if (!config.dns) { - config.dns = { modules: { name: 'proxy', port: 3053 } }; + config.dns = { modules: [{ name: 'proxy', port: 3053 }] }; } + // Use Object.assign to add any properties needed but not defined in the mdns config. + // It will first copy the defaults into an empty object, then copy any real config over that. + var mdnsDefaults = { port: 5353, broadcast: '224.0.0.251', ttl: 300 }; + config.mdns = Object.assign({}, mdnsDefaults, config.mdns || {}); + if (!config.tcp) { config.tcp = {}; } if (!config.http) { - config.http = { modules: { name: 'proxy', port: 3000 } }; + config.http = { modules: [{ name: 'proxy', domains: ['*'], port: 3000 }] }; } if (!config.tls) { - console.log("TODO: tls: { modules: { name: 'acme', email: 'foo@bar.com', domains: [ '*' ] } }"); - config.tls = { - agreeTos: args.agreeTos || args.agree || args['agree-tos'] - , servernames: (args.servernames||'').split(',').filter(Boolean).map(function (str) { return str.toLowerCase(); }) - }; + config.tls = {}; + } + if (!config.tls.acme && (args.email || args.agreeTos)) { + config.tls.acme = {}; + } + if (typeof args.agreeTos === 'string') { + config.tls.acme.approvedDomains = args.agreeTos.split(','); } if (args.email) { config.email = args.email; - config.tls.email = args.email; + config.tls.acme.email = args.email; } // maybe this should not go in config... but be ephemeral in some way? @@ -190,20 +198,20 @@ function readConfigAndRun(args) { function readEnv(args) { // TODO + try { + if (process.env.GOLDILOCKS_HOME) { + process.chdir(process.env.GOLDILOCKS_HOME); + } + } catch (err) {} + var env = { tunnel: process.env.GOLDILOCKS_TUNNEL_TOKEN || process.env.GOLDILOCKS_TUNNEL && true , email: process.env.GOLDILOCKS_EMAIL - , cwd: process.env.GOLDILOCKS_HOME + , cwd: process.env.GOLDILOCKS_HOME || process.cwd() , 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); + readConfigAndRun(Object.assign({}, env, args)); } var program = require('commander'); @@ -217,5 +225,4 @@ program .option('--debug', "Enable debug output") .parse(process.argv); -program.cwd = process.cwd(); readEnv(program); diff --git a/etc/goldilocks/goldilocks.example.yml b/etc/goldilocks/goldilocks.example.yml index 80f1283..c593118 100644 --- a/etc/goldilocks/goldilocks.example.yml +++ b/etc/goldilocks/goldilocks.example.yml @@ -10,17 +10,55 @@ tcp: address: '127.0.0.1:8022' tls: + acme: + email: 'joe.shmoe@example.com' + server: 'https://acme-staging.api.letsencrypt.org/directory' + challenge_type: 'http-01' + approved_domains: + - localhost.baz.daplie.me + - localhost.beta.daplie.me + domains: + - names: + - localhost.gamma.daplie.me + modules: + - name: proxy + address: '127.0.0.1:6443' + - names: + - beta.localhost.daplie.me + - baz.localhost.daplie.me + modules: + - name: acme + email: 'owner@example.com' + challenge_type: 'tls-sni-01' + # default server is 'https://acme-v01.api.letsencrypt.org/directory' modules: - name: proxy domains: - localhost.bar.daplie.me - localhost.foo.daplie.me address: '127.0.0.1:5443' + - name: acme + email: 'guest@example.com' + challenge_type: 'http-01' + domains: + - foo.localhost.daplie.me + - gamma.localhost.daplie.me + http: trust_proxy: true allow_insecure: false primary_domain: localhost.foo.daplie.me + domains: + - names: + - localhost.baz.daplie.me + modules: + - name: redirect + from: /nowhere/in/particular + to: /just/an/example + - name: proxy + port: 3001 + modules: - name: redirect domains: @@ -31,8 +69,15 @@ http: - name: proxy domains: - localhost.daplie.me - address: '127.0.0.1:4000' + host: locahost + port: 4000 - name: static domains: - '*.localhost.daplie.me' root: '/srv/www/:hostname' + +mdns: + disabled: false + port: 5353 + broadcast: '224.0.0.251' + ttl: 300 diff --git a/lib/goldilocks.js b/lib/goldilocks.js index 709ade1..ae6c552 100644 --- a/lib/goldilocks.js +++ b/lib/goldilocks.js @@ -74,46 +74,36 @@ module.exports.create = function (deps, config) { } function dnsListener(msg) { - var dgram = require('dgram'); - var socket = dgram.createSocket('udp4'); - socket.send(msg, config.dns.proxy.port, config.dns.proxy.address || '127.0.0.1'); + if (!Array.isArray(config.dns.modules)) { + return; + } + var socket = require('dgram').createSocket('udp4'); + config.dns.modules.forEach(function (mod) { + if (mod.name !== 'proxy') { + console.warn('found bad DNS module', mod); + return; + } + var dest = require('./domain-utils').separatePort(mod.address || ''); + dest.port = dest.port || mod.port; + dest.host = dest.host || mod.host || 'localhost'; + socket.send(msg, dest.port, dest.host); + }); } function createTcpForwarder(mod) { - var destination = mod.address.split(':'); - var connected = false; + var dest = require('./domain-utils').separatePort(mod.address || ''); + dest.port = dest.port || mod.port; + dest.host = dest.host || mod.host || 'localhost'; return function (conn) { - var newConn = deps.net.createConnection({ - port: destination[1] - , host: destination[0] || '127.0.0.1' - - , remoteFamily: conn.remoteFamily - , remoteAddress: conn.remoteAddress - , remotePort: conn.remotePort - }, function () { - connected = true; - - newConn.pipe(conn); - conn.pipe(newConn); - }); - - // Not sure how to effectively report this to the user or client, but we need to listen - // for the event to prevent it from crashing us. - newConn.on('error', function (err) { - if (connected) { - console.error('TCP forward remote error', err); - conn.end(); - } else { - console.log('TCP forward connection error', err); - require('./proxy-err-resp').sendBadGateway(conn, err, config.debug); - } - }); - conn.on('error', function (err) { - console.error('TCP forward client error', err); - newConn.end(); + var newConnOpts = {}; + ['remote', 'local'].forEach(function (end) { + ['Family', 'Address', 'Port'].forEach(function (name) { + newConnOpts[end+name] = conn[end+name]; + }); }); + deps.proxy(conn, Object.assign({}, dest, newConnOpts)); }; } @@ -249,5 +239,9 @@ module.exports.create = function (deps, config) { } } + if (!config.mdns.disabled) { + require('./mdns').start(deps, config); + } + return PromiseA.all(listenPromises); }; diff --git a/lib/mdns.js b/lib/mdns.js new file mode 100644 index 0000000..cc08ccd --- /dev/null +++ b/lib/mdns.js @@ -0,0 +1,144 @@ +'use strict'; + +var PromiseA = require('bluebird'); +var fs = PromiseA.promisifyAll(require('fs')); +var idFilename = require('path').join(__dirname, '..', 'var', 'mdns-id'); +var queryName = '_cloud._tcp.local'; + +var randomId = { + get: function () { + return fs.readFileAsync(idFilename) + .catch(function (err) { + if (err.code !== 'ENOENT') { + return PromiseA.reject(err); + } + var id = require('crypto').randomBytes(5).toString('hex'); + return randomId.set(id); + }); + } + +, set: function (value) { + return fs.writeFileAsync(idFilename, value) + .then(function () { + return value; + }); + } +}; + +function createResponse(name, packet, ttl) { + var rpacket = { + header: { + id: packet.header.id + , qr: 1 + , opcode: 0 + , aa: 1 + , tc: 0 + , rd: 0 + , ra: 0 + , res1: 0 + , res2: 0 + , res3: 0 + , rcode: 0 + , } + , question: packet.question + , answer: [] + , authority: [] + , additional: [] + , edns_options: [] + }; + + rpacket.answer.push({ + name: queryName + , typeName: 'PTR' + , ttl: ttl + , className: 'IN' + , data: name + '.' + queryName + }); + + var ifaces = require('./local-ip').find(); + Object.keys(ifaces).forEach(function (iname) { + var iface = ifaces[iname]; + + iface.ipv4.forEach(function (addr) { + rpacket.additional.push({ + name: name + '.local' + , typeName: 'A' + , ttl: ttl + , className: 'IN' + , address: addr.address + }); + }); + + iface.ipv6.forEach(function (addr) { + rpacket.additional.push({ + name: name + '.local' + , typeName: 'AAAA' + , ttl: ttl + , className: 'IN' + , address: addr.address + }); + }); + }); + + rpacket.additional.push({ + name: name + '.' + queryName + , typeName: 'SRV' + , ttl: ttl + , className: 'IN' + , priority: 1 + , weight: 0 + , port: 443 + , target: name + ".local" + }); + rpacket.additional.push({ + name: name + '._device-info._tcp.local' + , typeName: 'TXT' + , ttl: ttl + , className: 'IN' + , data: ["model=CloudHome1,1", "dappsvers=1"] + }); + + return require('dns-suite').DNSPacket.write(rpacket); +} + +module.exports.start = function (deps, config) { + var socket = require('dgram').createSocket({ type: 'udp4', reuseAddr: true }); + var dns = require('dns-suite'); + + socket.on('message', function (message, rinfo) { + // console.log('Received %d bytes from %s:%d', message.length, rinfo.address, rinfo.port); + + var packet; + try { + packet = dns.DNSPacket.parse(message); + } + catch (er) { + // `dns-suite` actually errors on a lot of the packets floating around in our network, + // so don't bother logging any errors. (We still use `dns-suite` because unlike `dns-js` + // it can successfully craft the one packet we want to send.) + return; + } + + // Only respond to queries. + if (packet.header.qr !== 0) { + return; + } + // Only respond if they were asking for cloud devices. + if (packet.question.length !== 1 || packet.question[0].name !== queryName) { + return; + } + + randomId.get().then(function (name) { + var resp = createResponse(name, packet, config.mdns.ttl); + socket.send(resp, config.mdns.port, config.mdns.broadcast); + }); + }); + + socket.bind(config.mdns.port, function () { + var addr = this.address(); + console.log('bound on UDP %s:%d for mDNS', addr.address, addr.port); + + socket.setBroadcast(true); + socket.addMembership(config.mdns.broadcast); + }); +}; diff --git a/lib/modules/admin.js b/lib/modules/admin.js index 7f5a81a..691fc4f 100644 --- a/lib/modules/admin.js +++ b/lib/modules/admin.js @@ -59,6 +59,6 @@ module.exports.create = function (deps, conf) { ] }); - /* device, addresses, cwd, http */ - return require('../app.js')(deps, conf, opts); + var app = require('../app.js')(deps, conf, opts); + return require('http').createServer(app); }; diff --git a/lib/modules/http.js b/lib/modules/http.js index 8a4da01..6d78a58 100644 --- a/lib/modules/http.js +++ b/lib/modules/http.js @@ -1,12 +1,10 @@ 'use strict'; module.exports.create = function (deps, conf, greenlockMiddleware) { - var express = require('express'); - var app = express(); - var adminApp = require('./admin').create(deps, conf); + var PromiseA = require('bluebird'); + var statAsync = PromiseA.promisify(require('fs').stat); var domainMatches = require('../domain-utils').match; var separatePort = require('../domain-utils').separatePort; - var proxyRoutes = []; var adminDomains = [ /\blocalhost\.admin\./ @@ -15,232 +13,387 @@ module.exports.create = function (deps, conf, greenlockMiddleware) { , /\balpha\.localhost\./ ]; - function moduleMatchesHost(req, mod) { - var host = separatePort(req.headers.host).host; + function parseHeaders(conn, opts) { + // There should already be a `firstChunk` on the opts, but because we might sometimes + // need more than that to get all the headers it's easier to always read the data off + // the connection and put it back later if we need to. + opts.firstChunk = Buffer.alloc(0); - return mod.domains.some(function (pattern) { + // First we make sure we have all of the headers. + return new PromiseA(function (resolve, reject) { + if (opts.firstChunk.includes('\r\n\r\n')) { + resolve(opts.firstChunk.toString()); + return; + } + + var errored = false; + function handleErr(err) { + errored = true; + reject(err); + } + conn.once('error', handleErr); + + function handleChunk(chunk) { + if (!errored) { + opts.firstChunk = Buffer.concat([opts.firstChunk, chunk]); + if (opts.firstChunk.includes('\r\n\r\n')) { + resolve(opts.firstChunk.toString()); + conn.removeListener('error', handleErr); + } else { + conn.once('data', handleChunk); + } + } + } + conn.once('data', handleChunk); + }).then(function (firstStr) { + var headerSection = firstStr.split('\r\n\r\n')[0]; + var lines = headerSection.split('\r\n'); + var result = {}; + + lines.slice(1).forEach(function (line) { + var match = /(.*)\s*:\s*(.*)/.exec(line); + if (match) { + result[match[1].toLowerCase()] = match[2]; + } else { + console.error('HTTP header line does not match pattern', line); + } + }); + + var match = /^([a-zA-Z]+)\s+(\S+)\s+HTTP/.exec(lines[0]); + if (!match) { + throw new Error('first line of "HTTP" does not match pattern: '+lines[0]); + } + result.method = match[1].toUpperCase(); + result.url = match[2]; + + return result; + }); + } + + function hostMatchesDomains(req, domains) { + var host = separatePort((req.headers || req).host).host; + + return domains.some(function (pattern) { return domainMatches(pattern, host); }); } - function verifyHost(fullHost) { - var host = separatePort(fullHost).host; - - if (host === 'localhost') { - return fullHost.replace(host, 'localhost.daplie.me'); - } - - // Test for IPv4 and IPv6 addresses. These patterns will match some invalid addresses, - // but since those still won't be valid domains that won't really be a problem. - if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host) || /^\[[0-9a-fA-F:]+\]$/.test(host)) { - if (!conf.http.primaryDomain) { - (conf.http.modules || []).some(function (mod) { - return mod.domains.some(function (domain) { - if (domain[0] !== '*') { - conf.http.primaryDomain = domain; - return true; - } - }); + function determinePrimaryHost() { + var result; + if (Array.isArray(conf.http.domains)) { + conf.http.domains.some(function (dom) { + return dom.names.some(function (domain) { + if (domain[0] !== '*') { + result = domain; + return true; + } }); - } - return fullHost.replace(host, conf.http.primaryDomain || host); + }); + } + if (result) { + return result; } - return fullHost; + if (Array.isArray(conf.http.modules)) { + conf.http.modules.some(function (mod) { + return mod.domains.some(function (domain) { + if (domain[0] !== '*') { + result = domain; + return true; + } + }); + }); + } + return result; } // We handle both HTTPS and HTTP traffic on the same ports, and we want to redirect // any unencrypted requests to the same port they came from unless it came in on // the default HTTP port, in which case there wont be a port specified in the host. var redirecters = {}; - function redirectHttps(req, res, next) { - if (conf.http.allowInsecure) { - next(); - return; - } + var ipv4Re = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; + var ipv6Re = /^\[[0-9a-fA-F:]+\]$/; + function redirectHttps(req, res) { + var host = separatePort(req.headers.host); - var port = separatePort(req.headers.host).port; - if (!redirecters[port]) { - redirecters[port] = require('redirect-https')({ - port: port - , trustProxy: conf.http.trustProxy - }); + if (!redirecters[host.port]) { + redirecters[host.port] = require('redirect-https')({ port: host.port }); } // localhost and IP addresses cannot have real SSL certs (and don't contain any useful // info for redirection either), so we direct some hosts to either localhost.daplie.me // or the "primary domain" ie the first manually specified domain. - req.headers.host = verifyHost(req.headers.host); + if (host.host === 'localhost') { + req.headers.host = 'localhost.daplie.me' + (host.port ? ':'+host.port : ''); + } + // Test for IPv4 and IPv6 addresses. These patterns will match some invalid addresses, + // but since those still won't be valid domains that won't really be a problem. + if (ipv4Re.test(host.host) || ipv6Re.test(host.host)) { + var dest; + if (conf.http.primaryDomain) { + dest = conf.http.primaryDomain; + } else { + dest = determinePrimaryHost(); + } + if (dest) { + req.headers.host = dest + (host.port ? ':'+host.port : ''); + } + } - redirecters[port](req, res, next); + redirecters[host.port](req, res); } - function handleAdmin(req, res, next) { + function emitConnection(server, conn, opts) { + server.emit('connection', conn); + + // We need to put back whatever data we read off to determine the connection was HTTP + // and to parse the headers. Must be done after data handlers added but before any new + // data comes in. + process.nextTick(function () { + conn.unshift(opts.firstChunk); + }); + + // Convenience return for all the check* functions. + return true; + } + + var acmeServer; + function checkACME(conn, opts, headers) { + if (headers.url.indexOf('/.well-known/acme-challenge/') !== 0) { + return false; + } + + if (!acmeServer) { + acmeServer = require('http').createServer(greenlockMiddleware); + } + return emitConnection(acmeServer, conn, opts); + } + + var httpsRedirectServer; + function checkHttps(conn, opts, headers) { + if (conf.http.allowInsecure || conn.encrypted) { + return false; + } + if (conf.http.trustProxy && 'https' === headers['x-forwarded-proto']) { + return false; + } + + if (!httpsRedirectServer) { + httpsRedirectServer = require('http').createServer(redirectHttps); + } + return emitConnection(httpsRedirectServer, conn, opts); + } + + var adminServer; + function checkAdmin(conn, opts, headers) { var admin = adminDomains.some(function (re) { - return re.test(req.headers.host); + return re.test(headers.host); }); if (admin) { - adminApp(req, res); + if (!adminServer) { + adminServer = require('./admin').create(deps, conf); + } + return emitConnection(adminServer, conn, opts); + } + return false; + } + + function checkProxy(mod, conn, opts, headers) { + var index = opts.firstChunk.indexOf('\r\n\r\n'); + var body = opts.firstChunk.slice(index); + + var head = opts.firstChunk.slice(0, index).toString(); + var headLines = head.split('\r\n'); + // First strip any existing `X-Forwarded-*` headers (for security purposes?) + headLines = headLines.filter(function (line) { + return !/^x-forwarded/i.test(line); + }); + // Then add our own `X-Forwarded` headers at the end. + if (conf.http.trustProxy && headers['x-forwarded-proto']) { + headLines.push('X-Forwarded-Proto: ' + headers['x-forwarded-proto']); } else { - next(); + headLines.push('X-Forwarded-Proto: ' + conn.encrypted ? 'https' : 'http'); } + var proxyChain = (headers['x-forwarded-for'] || '').split(/ *, */).filter(Boolean); + proxyChain.push(opts.remoteAddress || opts.address || conn.remoteAddress); + headLines.push('X-Forwarded-For: ' + proxyChain.join(', ')); + headLines.push('X-Forwarded-Host: ' + headers.host); + // Then convert all of the head lines back into a header buffer. + head = Buffer.from(headLines.join('\r\n')); + + opts.firstChunk = Buffer.concat([head, body]); + + var newConnOpts = separatePort(mod.address || ''); + newConnOpts.port = newConnOpts.port || mod.port; + newConnOpts.host = newConnOpts.host || mod.host || 'localhost'; + newConnOpts.servername = separatePort(headers.host).host; + newConnOpts.data = opts.firstChunk; + + newConnOpts.remoteFamily = opts.family || conn.remoteFamily; + newConnOpts.remoteAddress = opts.address || conn.remoteAddress; + newConnOpts.remotePort = opts.port || conn.remotePort; + + deps.proxy(conn, newConnOpts, opts.firstChunk); + return true; } - function respond404(req, res) { - res.writeHead(404); - res.end('Not Found'); - } - - function createProxyRoute(mod) { - // This is the easiest way to override the createConnections function the proxy - // module uses, but take note the since we don't have control over where this is - // called the extra options availabled will be different. - var agent = new require('http').Agent({}); - agent.createConnection = deps.net.createConnection; - - var proxy = require('http-proxy').createProxyServer({ - agent: agent - , target: 'http://' + mod.address - , xfwd: true - , toProxy: true - }); - - // We want to override the default value for some headers with the extra information we - // have available to us in the opts object attached to the connection. - proxy.on('proxyReq', function (proxyReq, req) { - var conn = req.connection; - var opts = conn.__opts; - proxyReq.setHeader('X-Forwarded-For', opts.remoteAddress || conn.remoteAddress); - }); - - proxy.on('error', function (err, req, res) { - console.log(err); - res.statusCode = 502; - res.setHeader('Content-Type', 'text/html'); - res.setHeader('Connection', 'close'); - res.end(require('../proxy-err-resp').getRespBody(err, conf.debug)); - }); - - return { - web: function (req, res, next) { - if (moduleMatchesHost(req, mod)) { - proxy.web(req, res); - } else { - next(); - } - } - , ws: function (req, socket, head, next) { - if (moduleMatchesHost(req, mod)) { - proxy.ws(req, socket, head); - } else { - next(); - } - } - }; - } - - function createRedirectRoute(mod) { - // Escape any characters that (can) have special meaning in regular expression - // but that aren't the special characters we have interest in. - var from = mod.from.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&'); - // Then modify the characters we are interested in so they do what we want in - // the regular expression after being compiled. - from = from.replace(/\*/g, '(.*)'); - var fromRe = new RegExp('^' + from + '/?$'); - - return function (req, res, next) { - if (!moduleMatchesHost(req, mod)) { - next(); - return; - } - - var match = fromRe.exec(req.url); - if (!match) { - next(); - return; - } - - var to = mod.to; - match.slice(1).forEach(function (globMatch, index) { - to = to.replace(':'+(index+1), globMatch); - }); - res.writeHead(mod.status || 301, { 'Location': to }); - res.end(); - }; - } - - function createStaticRoute(mod) { - var getStaticApp, staticApp; - if (/:hostname/.test(mod.root)) { - staticApp = {}; - getStaticApp = function (hostname) { - if (!staticApp[hostname]) { - staticApp[hostname] = express.static(mod.root.replace(':hostname', hostname)); - } - return staticApp[hostname]; - }; - } - else { - staticApp = express.static(mod.root); - getStaticApp = function () { - return staticApp; - }; + function checkRedirect(mod, conn, opts, headers) { + if (!mod.fromRe || mod.fromRe.origSrc !== mod.from) { + // Escape any characters that (can) have special meaning in regular expression + // but that aren't the special characters we have interest in. + var from = mod.from.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&'); + // Then modify the characters we are interested in so they do what we want in + // the regular expression after being compiled. + from = from.replace(/\*/g, '(.*)'); + var fromRe = new RegExp('^' + from + '/?$'); + fromRe.origSrc = mod.from; + // We don't want this property showing up in the actual config file or the API, + // so we define it this way so it's not enumberable. + Object.defineProperty(mod, 'fromRe', {value: fromRe, configurable: true}); } - return function (req, res, next) { - if (moduleMatchesHost(req, mod)) { - getStaticApp(separatePort(req.headers.host).host)(req, res, next); + var match = mod.fromRe.exec(headers.url); + if (!match) { + return false; + } + + var to = mod.to; + match.slice(1).forEach(function (globMatch, index) { + to = to.replace(':'+(index+1), globMatch); + }); + var status = mod.status || 301; + var code = require('http').STATUS_CODES[status] || 'Unknown'; + + conn.end([ + 'HTTP/1.1 ' + status + ' ' + code + , 'Date: ' + (new Date()).toUTCString() + , 'Location: ' + to + , 'Connection: close' + , 'Content-Length: 0' + , '' + , '' + ].join('\r\n')); + return true; + } + + var staticServer; + var staticHandlers = {}; + function serveStatic(req, res) { + var rootDir = req.connection.rootDir; + + if (!staticHandlers[rootDir]) { + staticHandlers[rootDir] = require('express').static(rootDir, { fallthrough: false }); + } + + staticHandlers[rootDir](req, res, function (err) { + if (err) { + res.statusCode = err.statusCode; } else { - next(); + res.statusCode = 404; } - }; + res.setHeader('Content-Type', 'text/html'); + + if (res.statusCode === 404) { + res.end('File Not Found'); + } else { + res.end(require('http').STATUS_CODES[res.statusCode]); + } + }); + } + function checkStatic(mod, conn, opts, headers) { + var rootDir = mod.root.replace(':hostname', separatePort(headers.host).host); + return statAsync(rootDir) + .then(function (stats) { + if (!stats || !stats.isDirectory()) { + return false; + } + + if (!staticServer) { + staticServer = require('http').createServer(serveStatic); + } + conn.rootDir = rootDir; + return emitConnection(staticServer, conn, opts); + }) + .catch(function (err) { + if (err.code !== 'ENOENT') { + console.warn('errored stating', rootDir, 'for serving static files', err); + } + return false; + }) + ; } - app.use(greenlockMiddleware); - app.use(redirectHttps); - app.use(handleAdmin); + var moduleChecks = { + proxy: checkProxy + , redirect: checkRedirect + , static: checkStatic + }; - (conf.http.modules || []).forEach(function (mod) { - if (mod.name === 'proxy') { - var proxyRoute = createProxyRoute(mod); - proxyRoutes.push(proxyRoute); - app.use(proxyRoute.web); - } - else if (mod.name === 'redirect') { - app.use(createRedirectRoute(mod)); - } - else if (mod.name === 'static') { - app.use(createStaticRoute(mod)); - } - else { - console.warn('unknown HTTP module', mod); - } - }); + function handleConnection(conn) { + var opts = conn.__opts; + parseHeaders(conn, opts) + .then(function (headers) { + if (checkACME(conn, opts, headers)) { return; } + if (checkHttps(conn, opts, headers)) { return; } + if (checkAdmin(conn, opts, headers)) { return; } - app.use(respond404); + var prom = PromiseA.resolve(false); + (conf.http.domains || []).forEach(function (dom) { + prom = prom.then(function (handled) { + if (handled) { + return handled; + } + if (!hostMatchesDomains(headers, dom.names)) { + return false; + } - var server = require('http').createServer(function (req, res) { - app(req, res) - }); + return dom.modules.some(function (mod) { + if (moduleChecks[mod.name]) { + return moduleChecks[mod.name](mod, conn, opts, headers); + } + console.warn('unknown HTTP module under domains', dom.names.join(','), mod); + }); + }); + }); + (conf.http.modules || []).forEach(function (mod) { + prom = prom.then(function (handled) { + if (handled) { + return handled; + } + if (!hostMatchesDomains(headers, mod.domains)) { + return false; + } - server.on('upgrade', function (req, socket, head) { - if (!proxyRoutes.length) { - socket.end(); - } + if (moduleChecks[mod.name]) { + return moduleChecks[mod.name](mod, conn, opts, headers); + } + console.warn('unknown HTTP module found', mod); + }); + }); - var prs = proxyRoutes.slice(); - function proxyWs() { - var proxyRoute = prs.shift(); - if (!proxyRoute) { - socket.end(); - return; + prom.then(function (handled) { + if (!handled) { + conn.end([ + 'HTTP/1.1 404 Not Found' + , 'Date: ' + (new Date()).toUTCString() + , 'Content-Type: text/html' + , 'Content-Length: 9' + , 'Connection: close' + , '' + , 'Not Found' + ].join('\r\n')); + } + }); + }) + ; + } + + return { + emit: function (type, value) { + if (type === 'connection') { + handleConnection(value); } - proxyRoute.ws(req, socket, head, proxyWs); } - - proxyWs(); - }); - - return server; + }; }; diff --git a/lib/modules/tls.js b/lib/modules/tls.js index a292c93..a18ab2d 100644 --- a/lib/modules/tls.js +++ b/lib/modules/tls.js @@ -9,52 +9,81 @@ module.exports.create = function (deps, config, netHandler) { function extractSocketProp(socket, propName) { // remoteAddress, remotePort... ugh... https://github.com/nodejs/node/issues/8854 - return socket[propName] - || socket['_' + propName] - || socket._handle._parent.owner.stream[propName] - ; + var value = socket[propName] || socket['_' + propName]; + try { + value = value || socket._handle._parent.owner.stream[propName]; + } catch (e) {} + + try { + value = value || socket._handle._parentWrap[propName]; + value = value || socket._handle._parentWrap._handle.owner.stream[propName]; + } catch (e) {} + + return value || ''; } + function nameMatchesDomains(name, domains) { + return domains.some(function (pattern) { + return domainMatches(pattern, name); + }); + } + + var addressNames = [ + 'remoteAddress' + , 'remotePort' + , 'remoteFamily' + , 'localAddress' + , 'localPort' + ]; function wrapSocket(socket, opts) { - var myDuplex = require('tunnel-packer').Stream.create(socket); - myDuplex.remoteFamily = opts.remoteFamily || myDuplex.remoteFamily; - myDuplex.remoteAddress = opts.remoteAddress || myDuplex.remoteAddress; - myDuplex.remotePort = opts.remotePort || myDuplex.remotePort; + var reader = require('socket-pair').create(function (err, writer) { + if (err) { + reader.emit('error', err); + return; + } - 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.end(); + process.nextTick(function () { + socket.unshift(opts.firstChunk); + }); + + socket.pipe(writer); + writer.pipe(socket); + + socket.on('error', function (err) { + console.log('wrapped TLS socket error', err); + reader.emit('error', err); + }); + writer.on('error', function (err) { + console.error('socket-pair writer error', err); + // If the writer had an error the reader probably did too, and I don't think we'll + // get much out of emitting this on the original socket, so logging is enough. + }); }); - process.nextTick(function () { - // this must happen after the socket is emitted to the next in the chain, - // but before any more data comes in via the network - socket.unshift(opts.firstChunk); + // We can't set these properties the normal way because there is a getter without a setter, + // but we can use defineProperty. We reuse the descriptor even though we will be manipulating + // it because we will only ever set the value and we set it every time. + var descriptor = {enumerable: true, configurable: true, writable: true}; + addressNames.forEach(function (name) { + descriptor.value = opts[name] || extractSocketProp(socket, name); + Object.defineProperty(reader, name, descriptor); }); - return myDuplex; + return reader; } var le = greenlock.create({ - // server: 'staging' server: 'https://acme-v01.api.letsencrypt.org/directory' , challenges: { - 'http-01': require('le-challenge-fs').create({ webrootPath: '/tmp/acme-challenges', debug: config.debug }) + 'http-01': require('le-challenge-fs').create({ debug: config.debug }) , 'tls-sni-01': require('le-challenge-sni').create({ debug: config.debug }) // TODO dns-01 - //, 'dns-01': require('le-challenge-ddns').create() + //, 'dns-01': require('le-challenge-ddns').create({ debug: config.debug }) } + , challengeType: 'http-01' - , store: require('le-store-certbot').create({ webrootPath: '/tmp/acme-challenges' }) + , store: require('le-store-certbot').create({ debug: config.debug }) , approveDomains: function (opts, certs, cb) { // This is where you check your database and associated @@ -65,60 +94,87 @@ module.exports.create = function (deps, config, netHandler) { 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; } - function complete(optsOverride) { - Object.keys(optsOverride).forEach(function (key) { - opts[key] = optsOverride[key]; - }); + function complete(optsOverride, domains) { + if (!cb) { + console.warn('tried to complete domain approval multiple times'); + return; + } + // We can't request certificates for wildcard domains, so filter any of those + // out of this list and put the domain that triggered this in the list if needed. + domains = (domains || []).filter(function (dom) { return dom[0] !== '*'; }); + if (domains.indexOf(opts.domain) < 0) { + domains.push(opts.domain); + } + // TODO: allow user to specify options for challenges or storage. + + Object.assign(opts, optsOverride, { domains: domains, agreeTos: true }); cb(null, { options: opts, certs: certs }); + cb = null; } + var handled = false; + if (Array.isArray(config.tls.domains)) { + handled = config.tls.domains.some(function (dom) { + if (!nameMatchesDomains(opts.domain, dom.names)) { + return false; + } - // 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({ + return dom.modules.some(function (mod) { + if (mod.name !== 'acme') { + return false; + } + complete(mod, dom.names); + return true; + }); + }); + } + if (handled) { + return; + } + + if (Array.isArray(config.tls.modules)) { + handled = config.tls.modules.some(function (mod) { + if (mod.name !== 'acme') { + return false; + } + if (!nameMatchesDomains(opts.domain, mod.domains)) { + return false; + } + + complete(mod, mod.domains); + return true; + }); + } + if (handled) { + return; + } + + var defAcmeConf; + if (config.tls.acme) { + defAcmeConf = config.tls.acme; + } else { + defAcmeConf = { email: config.tls.email - , agreeTos: true , server: config.tls.acmeDirectoryUrl || le.server - , challengeType: config.tls.challengeType || 'http-01' - }); + , challengeType: config.tls.challengeType || le.challengeType + , approvedDomains: config.tls.servernames + }; + } + + // Check config for domain name + // TODO: if `approvedDomains` isn't defined check all other modules to see if they can + // handle this domain (and what other domains it's grouped with). + if (-1 !== (defAcmeConf.approvedDomains || []).indexOf(opts.domain)) { + complete(defAcmeConf, defAcmeConf.approvedDomains); return; } - // TODO ask http module (and potentially all other modules) about what domains it can - // handle. We can allow any domains that other modules will handle after we terminate TLS. cb(new Error('domain is not allowed')); - // if (!modules.http) { - // modules.http = require('./modules/http.js').create(deps, config); - // } - // modules.http.checkServername(opts.domain).then(function (stuff) { - // if (!stuff || !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({ - // domain: stuff.domain || stuff.domains[0] - // , domains: stuff.domains - // , email: stuff.email || program.email - // , server: stuff.acmeDirectoryUrl || program.acmeDirectoryUrl - // , challengeType: stuff.challengeType || program.challengeType - // , challenge: stuff.challenge - // }); - // return; - // }, cb); } }); le.tlsOptions = le.tlsOptions || le.httpsOptions; @@ -163,49 +219,27 @@ module.exports.create = function (deps, config, netHandler) { }); function proxy(socket, opts, mod) { - var destination = mod.address.split(':'); - var connected = false; + var newConnOpts = require('../domain-utils').separatePort(mod.address || ''); + newConnOpts.port = newConnOpts.port || mod.port; + newConnOpts.host = newConnOpts.host || mod.host || 'localhost'; + newConnOpts.servername = opts.servername; + newConnOpts.data = opts.firstChunk; - var newConn = deps.net.createConnection({ - port: destination[1] - , host: destination[0] || '127.0.0.1' + newConnOpts.remoteFamily = opts.family || extractSocketProp(socket, 'remoteFamily'); + newConnOpts.remoteAddress = opts.address || extractSocketProp(socket, 'remoteAddress'); + newConnOpts.remotePort = opts.port || extractSocketProp(socket, 'remotePort'); - , servername: opts.servername - , data: opts.firstChunk - , remoteFamily: opts.family || extractSocketProp(socket, 'remoteFamily') - , remoteAddress: opts.address || extractSocketProp(socket, 'remoteAddress') - , remotePort: opts.port || extractSocketProp(socket, 'remotePort') - }, function () { - connected = true; - if (!opts.hyperPeek) { - newConn.write(opts.firstChunk); - } - newConn.pipe(socket); - socket.pipe(newConn); - }); - - // Not sure how to effectively report this to the user or client, but we need to listen - // for the event to prevent it from crashing us. - newConn.on('error', function (err) { - if (connected) { - console.error('TLS proxy remote error', err); - socket.end(); + deps.proxy(socket, newConnOpts, opts.firstChunk, function () { + // This function is called in the event of a connection error and should decrypt + // the socket so the proxy module can send a 502 HTTP response. + var tlsOpts = localhostCerts.mergeTlsOptions('localhost.daplie.me', {isServer: true}); + if (opts.hyperPeek) { + return new tls.TLSSocket(socket, tlsOpts); } else { - console.log('TLS proxy connection error', err); - var tlsOpts = localhostCerts.mergeTlsOptions('localhost.daplie.me', {isServer: true}); - var decrypted; - if (opts.hyperPeek) { - decrypted = new tls.TLSSocket(socket, tlsOpts); - } else { - decrypted = new tls.TLSSocket(wrapSocket(socket, opts), tlsOpts); - } - require('../proxy-err-resp').sendBadGateway(decrypted, err, config.debug); + return new tls.TLSSocket(wrapSocket(socket, opts), tlsOpts); } }); - socket.on('error', function (err) { - console.error('TLS proxy client error', err); - newConn.end(); - }); + return true; } function terminate(socket, opts) { @@ -246,30 +280,39 @@ module.exports.create = function (deps, config, netHandler) { return; } - var handled = (config.tls.modules || []).some(function (mod) { - var relevant = mod.domains.some(function (pattern) { - return domainMatches(pattern, opts.servername); - }); - if (!relevant) { - return false; - } - + function checkModule(mod) { if (mod.name === 'proxy') { - proxy(socket, opts, mod); + return proxy(socket, opts, mod); } - else { + if (mod.name !== 'acme') { console.error('saw unknown TLS module', mod); + } + } + + var handled = (config.tls.domains || []).some(function (dom) { + if (!nameMatchesDomains(opts.servername, dom.names)) { return false; } - return true; + return dom.modules.some(checkModule); }); + if (handled) { + return; + } + + handled = (config.tls.modules || []).some(function (mod) { + if (!nameMatchesDomains(opts.servername, mod.domains)) { + return false; + } + return checkModule(mod); + }); + if (handled) { + return; + } // TODO: figure out all of the domains that the other modules intend to handle, and only // terminate those ones, closing connections for all others. - if (!handled) { - terminate(socket, opts); - } + terminate(socket, opts); } return { diff --git a/lib/proxy-conn.js b/lib/proxy-conn.js new file mode 100644 index 0000000..b9a704c --- /dev/null +++ b/lib/proxy-conn.js @@ -0,0 +1,76 @@ +'use strict'; + +function getRespBody(err, debug) { + if (debug) { + return err.toString(); + } + + if (err.code === 'ECONNREFUSED') { + return 'The connection was refused. Most likely the service being connected to ' + + 'has stopped running or the configuration is wrong.'; + } + + return 'Bad Gateway: ' + err.code; +} + +function sendBadGateway(conn, err, debug) { + var body = getRespBody(err, debug); + + conn.write([ + 'HTTP/1.1 502 Bad Gateway' + , 'Date: ' + (new Date()).toUTCString() + , 'Connection: close' + , 'Content-Type: text/html' + , 'Content-Length: ' + body.length + , '' + , body + ].join('\r\n')); + conn.end(); +} + +module.exports.getRespBody = getRespBody; +module.exports.sendBadGateway = sendBadGateway; + +module.exports.create = function (deps, config) { + return function proxy(conn, newConnOpts, firstChunk, decrypt) { + var connected = false; + var newConn = deps.net.createConnection(newConnOpts, function () { + connected = true; + + if (firstChunk) { + newConn.write(firstChunk); + } + + newConn.pipe(conn); + conn.pipe(newConn); + }); + + // Listening for this largely to prevent uncaught exceptions. + conn.on('error', function (err) { + console.log('proxy client error', err); + }); + newConn.on('error', function (err) { + if (connected) { + // Not sure how to report this to a user or a client. We can assume that some data + // has already been exchanged, so we can't really be sure what we can send in addition + // that wouldn't result in a parse error. + console.log('proxy remote error', err); + } else { + console.log('proxy connection error', err); + if (decrypt) { + sendBadGateway(decrypt(conn), err, config.debug); + } else { + sendBadGateway(conn, err, config.debug); + } + } + }); + + // Make sure that once one side closes, no I/O activity will happen on the other side. + conn.on('close', function () { + newConn.destroy(); + }); + newConn.on('close', function () { + conn.destroy(); + }); + }; +}; diff --git a/lib/proxy-err-resp.js b/lib/proxy-err-resp.js deleted file mode 100644 index c2a35a8..0000000 --- a/lib/proxy-err-resp.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict'; - -function getRespBody(err, debug) { - if (debug) { - return err.toString(); - } - - if (err.code === 'ECONNREFUSED') { - return 'The connection was refused. Most likely the service being connected to ' - + 'has stopped running or the configuration is wrong.'; - } - - return 'Bad Gateway: ' + err.code; -} - -function sendBadGateway(conn, err, debug) { - var body = getRespBody(err, debug); - - conn.write([ - 'HTTP/1.1 502 Bad Gateway' - , 'Date: ' + (new Date()).toUTCString() - , 'Connection: close' - , 'Content-Type: text/html' - , 'Content-Length: ' + body.length - , '' - , body - ].join('\r\n')); - conn.end(); -} - -module.exports.getRespBody = getRespBody; -module.exports.sendBadGateway = sendBadGateway; diff --git a/lib/tunnel.js b/lib/tunnel.js deleted file mode 100644 index a4ea58c..0000000 --- a/lib/tunnel.js +++ /dev/null @@ -1,144 +0,0 @@ -'use strict'; - -module.exports.create = function (opts, servers) { - // servers = { plainserver, server } - var Oauth3 = require('oauth3-cli'); - var Tunnel = require('daplie-tunnel').create({ - Oauth3: Oauth3 - , PromiseA: opts.PromiseA - , CLI: { - init: function (rs, ws/*, state, options*/) { - // noop - return ws; - } - } - }).Tunnel; - var stunnel = require('stunnel'); - var killcount = 0; - - /* - var Dup = { - write: function (chunk, encoding, cb) { - this.__my_socket.push(chunk, encoding); - cb(); - } - , read: function (size) { - var x = this.__my_socket.read(size); - if (x) { this.push(x); } - } - , setTimeout: function () { - console.log('TODO implement setTimeout on Duplex'); - } - }; - - var httpServer = require('http').createServer(function (req, res) { - console.log('req.socket.encrypted', req.socket.encrypted); - res.end('Hello, tunneled World!'); - }); - - var tlsServer = require('tls').createServer(opts.httpsOptions, function (tlsSocket) { - console.log('tls connection'); - // things get a little messed up here - httpServer.emit('connection', tlsSocket); - - // try again - //servers.server.emit('connection', tlsSocket); - }); - */ - - process.on('SIGINT', function () { - killcount += 1; - console.log('[quit] closing http and https servers'); - if (killcount >= 3) { - process.exit(1); - } - if (servers.server) { - servers.server.close(); - } - if (servers.insecureServer) { - servers.insecureServer.close(); - } - }); - - return Tunnel.token({ - refreshToken: opts.refreshToken - , email: opts.email - , domains: opts.sites.map(function (site) { - return site.name; - }) - , device: { hostname: opts.devicename || opts.device } - }).then(function (result) { - // { jwt, tunnelUrl } - var locals = []; - opts.sites.map(function (site) { - locals.push({ - protocol: 'https' - , hostname: site.name - , port: opts.port - }); - locals.push({ - protocol: 'http' - , hostname: site.name - , port: opts.insecurePort || opts.port - }); - }); - return stunnel.connect({ - token: result.jwt - , stunneld: result.tunnelUrl - // XXX TODO BUG // this is just for testing - , insecure: /*opts.insecure*/ true - , locals: locals - // a simple passthru is proving to not be so simple - , net: require('net') /* - { - createConnection: function (info, cb) { - // data is the hello packet / first chunk - // info = { data, servername, port, host, remoteAddress: { family, address, port } } - - var myDuplex = new (require('stream').Duplex)(); - var myDuplex2 = new (require('stream').Duplex)(); - // duplex = { write, push, end, events: [ 'readable', 'data', 'error', 'end' ] }; - - myDuplex2.__my_socket = myDuplex; - myDuplex.__my_socket = myDuplex2; - - myDuplex2._write = Dup.write; - myDuplex2._read = Dup.read; - - myDuplex._write = Dup.write; - myDuplex._read = Dup.read; - - myDuplex.remoteFamily = info.remoteFamily; - myDuplex.remoteAddress = info.remoteAddress; - myDuplex.remotePort = info.remotePort; - - // socket.local{Family,Address,Port} - myDuplex.localFamily = 'IPv4'; - myDuplex.localAddress = '127.0.01'; - myDuplex.localPort = info.port; - - myDuplex.setTimeout = Dup.setTimeout; - - // this doesn't seem to work so well - //servers.server.emit('connection', myDuplex); - - // try a little more manual wrapping / unwrapping - var firstByte = info.data[0]; - if (firstByte < 32 || firstByte >= 127) { - tlsServer.emit('connection', myDuplex); - } - else { - httpServer.emit('connection', myDuplex); - } - - if (cb) { - process.nextTick(cb); - } - - return myDuplex2; - } - } - //*/ - }); - }); -}; diff --git a/lib/worker.js b/lib/worker.js index 79db594..23724cb 100644 --- a/lib/worker.js +++ b/lib/worker.js @@ -9,5 +9,7 @@ process.on('message', function (conf) { // HTTP proxying connection creation is not something we currently control. , net: require('net') }; + deps.proxy = require('./proxy-conn').create(deps, conf); + require('./goldilocks.js').create(deps, conf); }); diff --git a/package.json b/package.json index e425fc7..0f2629a 100644 --- a/package.json +++ b/package.json @@ -41,30 +41,27 @@ "bluebird": "^3.4.6", "body-parser": "git+https://github.com/expressjs/body-parser.git#1.16.1", "commander": "^2.9.0", - "daplie-tunnel": "git+https://git.daplie.com/Daplie/daplie-cli-tunnel.git#master", - "ddns-cli": "git+https://git.daplie.com/Daplie/node-ddns-client.git#master", + "dns-suite": "git+https://git@git.daplie.com:Daplie/dns-suite#v1", "express": "git+https://github.com/expressjs/express.git#4.x", "finalhandler": "^0.4.0", "greenlock": "git+https://git.daplie.com/Daplie/node-greenlock.git#master", - "greenlock-express": "git+https://git.daplie.com/Daplie/greenlock-express.git#master", - "http-proxy": "^1.16.2", - "httpolyglot": "^0.1.1", "ipaddr.js": "git+https://github.com/whitequark/ipaddr.js.git#v1.3.0", "ipify": "^1.1.0", "js-yaml": "^3.8.3", + "jsonwebtoken": "^7.4.0", "le-challenge-ddns": "git+https://git.daplie.com/Daplie/le-challenge-ddns.git#master", "le-challenge-fs": "git+https://git.daplie.com/Daplie/le-challenge-webroot.git#master", "le-challenge-sni": "^2.0.1", - "livereload": "^0.6.0", + "le-store-certbot": "git+https://git.daplie.com/Daplie/le-store-certbot.git#master", "localhost.daplie.me-certificates": "^1.3.0", - "minimist": "^1.1.1", - "oauth3-cli": "git+https://git.daplie.com/OAuth3/oauth3-cli.git#master", "recase": "git+https://git.daplie.com/coolaj86/recase-js.git#v1.0.4", "redirect-https": "^1.1.0", + "request": "^2.81.0", "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", + "sni": "^1.0.0", "socket-pair": "^1.0.0", "stream-pair": "^1.0.3", "stunnel": "git+https://git.daplie.com/Daplie/node-tunnel-client.git#v1" diff --git a/stages/01-serve.js b/stages/01-serve.js deleted file mode 100644 index 8f92791..0000000 --- a/stages/01-serve.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; - -var https = require('httpolyglot'); -var httpsOptions = require('localhost.daplie.me-certificates').merge({}); -var httpsPort = 8443; -var redirectApp = require('redirect-https')({ - port: httpsPort -}); - -var server = https.createServer(httpsOptions); - -server.on('request', function (req, res) { - if (!req.socket.encrypted) { - redirectApp(req, res); - return; - } - - res.end("Hello, Encrypted World!"); -}); - -server.listen(httpsPort, function () { - console.log('https://' + 'localhost.daplie.me' + (443 === httpsPort ? ':' : ':' + httpsPort)); -});