diff --git a/.gitignore b/.gitignore
index bc7fc55..9b5d38b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,6 +30,7 @@ build/Release
# Dependency directories
node_modules
jspm_packages
+bower_components
# Optional npm cache directory
.npm
@@ -42,3 +43,7 @@ jspm_packages
# Output of 'npm pack'
*.tgz
+
+# Dependency directory
+# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
+node_modules
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8f71f43
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/README.md b/README.md
index 799cfd6..74313de 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,161 @@
+
+
+About Daplie: We're taking back the Internet!
+--------------
+
+Down with Google, Apple, and Facebook!
+
+We're re-decentralizing the web and making it read-write again - one home cloud system at a time.
+
+Tired of serving the Empire? Come join the Rebel Alliance:
+
+jobs@daplie.com | [Invest in Daplie on Wefunder](https://daplie.com/invest/) | [Pre-order Cloud](https://daplie.com/preorder/), The World's First Home Server for Everyone
+
+
+
Goldilocks
==========
-The webserver that's just right.
\ No newline at end of file
+The node.js webserver that's just right.
+
+
+A simple HTTPS static file server with valid TLS (SSL) certs.
+
+Comes bundled a valid certificate for localhost.daplie.me,
+which is great for testing and development, and you can specify your own.
+
+Also great for testing ACME certs from letsencrypt.org.
+
+Install
+-------
+
+```bash
+# v2 in npm
+npm install -g goldilocks
+
+# master in git (via ssh)
+npm install -g git+ssh://git@git.daplie.com:Daplie/goldilocks.js
+
+# master in git (unauthenticated)
+npm install -g git+https://git@git.daplie.com:Daplie/goldilocks.js
+```
+
+```bash
+goldilocks
+```
+
+```bash
+Serving /Users/foo/ at https://localhost.daplie.me:8443
+```
+
+Usage
+-----
+
+Examples:
+
+```
+# Install
+npm install -g git+https://git@git.daplie.com:Daplie/goldilocks.js
+
+# Use tunnel
+goldilocks --sites jane.daplie.me --agree-tos --email jane@example.com --tunnel
+
+# BEFORE you access in a browser for the first time, use curl
+# (because there's a concurrency bug in the greenlock setup)
+curl https://jane.daplie.me
+```
+
+Options:
+
+* `-p ` - i.e. `sudo goldilocks -p 443` (defaults to 80+443 or 8443)
+* `-d ` - i.e. `goldilocks -d /tmp/` (defaults to `pwd`)
+ * you can use `:hostname` as a template for multiple directories
+ * Example A: `goldilocks -d /srv/www/:hostname --sites localhost.foo.daplie.me,localhost.bar.daplie.me`
+ * Example B: `goldilocks -d ./:hostname/public/ --sites localhost.foo.daplie.me,localhost.bar.daplie.me`
+* `-c ` - i.e. `server-https -c 'Hello, World! '` (defaults to directory index)
+* `--express-app ` - path to a file the exports an express-style app (`function (req, res, next) { ... }`)
+* `--livereload` - inject livereload into all html pages (see also: [fswatch](http://stackoverflow.com/a/13807906/151312)), but be careful if `` has thousands of files it will spike your CPU usage to 100%
+
+* `--email ` - email to use for Let's Encrypt, Daplie DNS, Daplie Tunnel
+* `--agree-tos` - agree to terms for Let's Encrypt, Daplie DNS
+* `--sites ` comma-separated list of domains to respond to (default is `localhost.daplie.me`)
+ * optionally you may include the path to serve with `|` such as `example.com|/tmp,example.net/srv/www`
+* `--tunnel` - make world-visible (must use `--sites`)
+
+Specifying a custom HTTPS certificate:
+
+* `--key /path/to/privkey.pem` specifies the server private key
+* `--cert /path/to/fullchain.pem` specifies the bundle of server certificate and all intermediate certificates
+* `--root /path/to/root.pem` specifies the certificate authority(ies)
+
+Note: `--root` may specify single cert or a bundle, and may be used multiple times like so:
+
+```
+--root /path/to/primary-root.pem --root /path/to/cross-root.pem
+```
+
+Other options:
+
+* `--serve-root true` alias for `-c` with the contents of root.pem
+* `--sites example.com` changes the servername logged to the console
+* `--letsencrypt-certs example.com` sets and key, fullchain, and root to standard letsencrypt locations
+
+Examples
+--------
+
+```bash
+goldilocks -p 1443 -c 'Hello from 1443' &
+goldilocks -p 2443 -c 'Hello from 2443' &
+goldilocks -p 3443 -d /tmp &
+
+curl https://localhost.daplie.me:1443
+> Hello from 1443
+
+curl --insecure https://localhost:2443
+> Hello from 2443
+
+curl https://localhost.daplie.me:3443
+> [html index listing of /tmp]
+```
+
+And if you tested in a browser,
+it would redirect to (on the same port).
+
+(in curl it would just show an error message)
+
+### Testing ACME Let's Encrypt certs
+
+In case you didn't know, you can get free https certificates from
+[letsencrypt.org](https://letsencrypt.org)
+(ACME letsencrypt)
+and even a free subdomain from .
+
+If you want to quickly test the certificates you installed,
+you can do so like this:
+
+```bash
+goldilocks -p 8443 \
+ --letsencrypt-certs test.mooo.com \
+ --serve-root true
+```
+
+which is equilavent to
+
+```bash
+goldilocks -p 8443 \
+ --sites test.mooo.com
+ --key /etc/letsencrypt/live/test.mooo.com/privkey.pem \
+ --cert /etc/letsencrypt/live/test.mooo.com/fullchain.pem \
+ --root /etc/letsencrypt/live/test.mooo.com/root.pem \
+ -c "$(cat 'sudo /etc/letsencrypt/live/test.mooo.com/root.pem')"
+```
+
+and can be tested like so
+
+```bash
+curl --insecure https://test.mooo.com:8443 > ./root.pem
+curl https://test.mooo.com:8843 --cacert ./root.pem
+```
+
+* [QuickStart Guide for Let's Encrypt](https://coolaj86.com/articles/lets-encrypt-on-raspberry-pi/)
+* [QuickStart Guide for FreeDNS](https://coolaj86.com/articles/free-dns-hosting-with-freedns-afraid-org.html)
diff --git a/bin/goldilocks.js b/bin/goldilocks.js
new file mode 100755
index 0000000..b5adef5
--- /dev/null
+++ b/bin/goldilocks.js
@@ -0,0 +1,543 @@
+#!/usr/bin/env node
+'use strict';
+
+//var PromiseA = global.Promise;
+var PromiseA = require('bluebird');
+var tls = require('tls');
+var https = require('httpolyglot');
+var http = require('http');
+var fs = require('fs');
+var path = require('path');
+var DDNS = require('ddns-cli');
+var httpPort = 80;
+var httpsPort = 443;
+var lrPort = 35729;
+var portFallback = 8443;
+var insecurePortFallback = 4080;
+
+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).");
+ }
+}
+
+function createInsecureServer(port, _delete_me_, opts) {
+ return new PromiseA(function (realResolve) {
+ var server = http.createServer();
+
+ function resolve() {
+ realResolve(server);
+ }
+
+ server.on('error', function (err) {
+ if (opts.errorInsecurePort || opts.manualInsecurePort) {
+ showError(err, port);
+ process.exit(1);
+ return;
+ }
+
+ opts.errorInsecurePort = err.toString();
+
+ return createInsecureServer(insecurePortFallback, null, opts).then(resolve);
+ });
+
+ server.on('request', opts.redirectApp);
+
+ 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);
+
+
+ // 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();
+ }
+ 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;
+ }
+
+ // 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 directive = { content: content, livereload: opts.livereload
+ , sites: opts.sites
+ , expressApp: opts.expressApp };
+ 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);
+ };
+ var 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.resolve(argv['default-web-root'] || argv.d || argv._[1] || process.cwd());
+ var content = argv.c;
+ var letsencryptHost = argv['letsencrypt-certs'];
+
+ 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;
+ }
+ if (argv.servername && argv.sites) {
+ throw new Error('specify only --sites, not --servername');
+ }
+ argv.sites = argv.sites || argv.servername;
+
+ // 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.sites = [ { name: defaultServername , path: '.' } ];
+ if (argv.sites) {
+ opts._externalHost = false;
+ opts.sites = argv.sites.split(',').map(function (name) {
+ var nameparts = name.split('|');
+ var servername = nameparts.shift();
+ opts._externalHost = opts._externalHost || !/(^|\.)localhost\./.test(servername);
+ // TODO allow reverse proxy
+ return {
+ name: servername
+ // there should always be a path
+ , paths: nameparts.length && nameparts || [
+ defaultWebRoot.replace(/(:hostname|:servername)/g, servername)
+ ]
+ // TODO check for existing custom path before issuing with greenlock
+ , _hasCustomPath: !!nameparts.length
+ };
+ });
+ }
+ // TODO use arrays in all things
+ opts._old_server_name = opts.sites[0].name;
+ 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(path.resolve(process.cwd(), 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.");
+ }
+ 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) { ... }
+
+ // ifaces
+ opts.ifaces = require('../lib/local-ip.js').find();
+ 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('');
+ });
+ });
+ });
+}
+
+if (require.main === module) {
+ run();
+}
diff --git a/lib/app.js b/lib/app.js
new file mode 100644
index 0000000..9169c95
--- /dev/null
+++ b/lib/app.js
@@ -0,0 +1,108 @@
+'use strict';
+
+module.exports = function (opts) {
+ var finalhandler = require('finalhandler');
+ var serveStatic = require('serve-static');
+ var serveIndex = require('serve-index');
+
+ var hostsMap = {};
+ var pathsMap = {};
+ var content = opts.content;
+ var server;
+
+ function addServer(hostname) {
+
+ if (hostsMap[hostname]) {
+ return hostsMap[hostname];
+ }
+
+ opts.sites.forEach(function (site) {
+ if (hostname !== site.name) {
+ return;
+ }
+
+ // path should exist before it gets to this point
+ site.path = site.path || site.paths[0];
+
+ if (!pathsMap[site.path]) {
+ pathsMap[site.path] = {
+ serve: serveStatic(site.path)
+ // TODO option for dotfiles
+ , index: serveIndex(site.path)
+ };
+ }
+
+ hostsMap[hostname] = {
+ serve: pathsMap[site.path].serve
+ , index: pathsMap[site.path].index
+ , app: site.app
+ };
+
+ });
+
+ }
+
+ function _reloadWrite(data, enc, cb) {
+ /*jshint validthis: true */
+ if (this.headersSent) {
+ this.__write(data, enc, cb);
+ return;
+ }
+
+ if (!/html/i.test(this.getHeader('Content-Type'))) {
+ this.__write(data, enc, cb);
+ return;
+ }
+
+ if (this.getHeader('Content-Length')) {
+ this.setHeader('Content-Length', this.getHeader('Content-Length') + this.__my_addLen);
+ }
+
+ this.__write(this.__my_livereload);
+ this.__write(data, enc, cb);
+ }
+
+
+ addServer(opts.sites[0].name);
+
+ return function (req, res) {
+ if (content && '/' === req.url) {
+ // res.setHeader('Content-Type', 'application/octet-stream');
+ res.end(content);
+ return;
+ }
+ var done = finalhandler(req, res);
+ var host = req.headers.host;
+ var hostname = (host||'').split(':')[0] || opts.sites[0].name;
+
+ function serveStatic(server) {
+ if (server.expressApp) {
+ server.expressApp(req, res, serveStatic);
+ return;
+ }
+
+ server.serve(req, res, function (err) {
+ if (err) { return done(err); }
+ server.index(req, res, done);
+ });
+ }
+
+ if (opts.livereload) {
+ res.__my_livereload = '';
+ res.__my_addLen = res.__my_livereload.length;
+
+ // TODO modify prototype instead of each instance?
+ res.__write = res.write;
+ res.write = _reloadWrite;
+ }
+
+ console.log('hostname:', hostname);
+
+ addServer(hostname);
+ server = hostsMap[hostname] || hostsMap[opts.sites[0].name];
+ serveStatic(server);
+
+ };
+};
diff --git a/lib/ddns.js b/lib/ddns.js
new file mode 100644
index 0000000..2ed1cca
--- /dev/null
+++ b/lib/ddns.js
@@ -0,0 +1,88 @@
+'use strict';
+
+module.exports.create = function (opts/*, servers*/) {
+ var PromiseA = opts.PromiseA;
+ var dns = PromiseA.promisifyAll(require('dns'));
+
+ return PromiseA.all([
+ dns.resolve4Async(opts._old_server_name).then(function (results) {
+ return results;
+ }, function () {})
+ , dns.resolve6Async(opts._old_server_name).then(function (results) {
+ return results;
+ }, function () {})
+ ]).then(function (results) {
+ var ipv4 = results[0] || [];
+ var ipv6 = results[1] || [];
+ var record;
+
+ opts.dnsRecords = {
+ A: ipv4
+ , AAAA: ipv6
+ };
+
+ Object.keys(opts.ifaces).some(function (ifacename) {
+ var iface = opts.ifaces[ifacename];
+
+ return iface.ipv4.some(function (localIp) {
+ return ipv4.some(function (remoteIp) {
+ if (localIp.address === remoteIp) {
+ record = localIp;
+ return record;
+ }
+ });
+ }) || iface.ipv6.some(function (localIp) {
+ return ipv6.forEach(function (remoteIp) {
+ if (localIp.address === remoteIp) {
+ record = localIp;
+ return record;
+ }
+ });
+ });
+ });
+
+ if (!record) {
+ console.info("DNS Record '" + ipv4.concat(ipv6).join(',') + "' does not match any local IP address.");
+ console.info("Use --ddns to allow the people of the Internet to access your server.");
+ }
+
+ opts.externalIps.ipv4.some(function (localIp) {
+ return ipv4.some(function (remoteIp) {
+ if (localIp.address === remoteIp) {
+ record = localIp;
+ return record;
+ }
+ });
+ });
+
+ opts.externalIps.ipv6.some(function (localIp) {
+ return ipv6.some(function (remoteIp) {
+ if (localIp.address === remoteIp) {
+ record = localIp;
+ return record;
+ }
+ });
+ });
+
+ if (!record) {
+ console.info("DNS Record '" + ipv4.concat(ipv6).join(',') + "' does not match any local IP address.");
+ console.info("Use --ddns to allow the people of the Internet to access your server.");
+ }
+ });
+};
+
+if (require.main === module) {
+ var opts = {
+ _old_server_name: 'aj.daplie.me'
+ , PromiseA: require('bluebird')
+ };
+ // ifaces
+ opts.ifaces = require('./local-ip.js').find();
+ console.log('opts.ifaces');
+ console.log(opts.ifaces);
+ require('./match-ips.js').match(opts._old_server_name, opts).then(function (ips) {
+ opts.matchingIps = ips.matchingIps || [];
+ opts.externalIps = ips.externalIps;
+ module.exports.create(opts);
+ });
+}
diff --git a/lib/local-ip.js b/lib/local-ip.js
new file mode 100644
index 0000000..6b19a47
--- /dev/null
+++ b/lib/local-ip.js
@@ -0,0 +1,53 @@
+'use strict';
+
+var os = require('os');
+
+module.exports.find = function (opts) {
+ opts = opts || {};
+ opts.externals = opts.externals || [];
+
+ var ifaceMap = os.networkInterfaces();
+ var newMap = {};
+
+ Object.keys(ifaceMap).forEach(function (iname) {
+ var ifaces = ifaceMap[iname];
+
+ ifaces = ifaces.filter(function (iface) {
+ return opts.externals.some(function (ip) {
+ if (ip.address === iface.address) {
+ ip.external = true;
+ return true;
+ }
+ }) || (!iface.internal && !/^fe80/.test(iface.address) && !/^[0:]+$/.test(iface.mac));
+ });
+
+ if (!ifaces.length) {
+ return;
+ }
+
+ newMap[iname] = newMap[iname] || { ipv4: [], ipv6: [] };
+
+ ifaces.forEach(function (addr) {
+ addr.iface = iname;
+ if ('IPv4' === addr.family) {
+ newMap[iname].ipv4.push(addr);
+ }
+ else if ('IPv6' === addr.family) {
+ newMap[iname].ipv6.push(addr);
+ }
+ });
+ });
+
+ return newMap;
+
+ /*
+https://[2601:681:300:92c0:2477:d58a:d69e:51a0]:8443
+
+ console.log('');
+
+ console.log('');
+ console.log(iname);
+ console.log(ifaces);
+ console.log('');
+ */
+};
diff --git a/lib/match-ips.js b/lib/match-ips.js
new file mode 100644
index 0000000..dbb3ff1
--- /dev/null
+++ b/lib/match-ips.js
@@ -0,0 +1,117 @@
+'use strict';
+
+var PromiseA = require('bluebird');
+
+module.exports.match = function (servername, opts) {
+ return PromiseA.promisify(require('ipify'))().then(function (externalIp) {
+ var dns = PromiseA.promisifyAll(require('dns'));
+
+ opts.externalIps = [ { address: externalIp, family: 'IPv4' } ];
+ opts.ifaces = require('./local-ip.js').find({ externals: opts.externalIps });
+ opts.externalIfaces = Object.keys(opts.ifaces).reduce(function (all, iname) {
+ var iface = opts.ifaces[iname];
+
+ iface.ipv4.forEach(function (addr) {
+ if (addr.external) {
+ addr.iface = iname;
+ all.push(addr);
+ }
+ });
+ iface.ipv6.forEach(function (addr) {
+ if (addr.external) {
+ addr.iface = iname;
+ all.push(addr);
+ }
+ });
+
+ return all;
+ }, []).filter(Boolean);
+
+ function resolveIps(hostname) {
+ var allIps = [];
+
+ return PromiseA.all([
+ dns.resolve4Async(hostname).then(function (records) {
+ records.forEach(function (ip) {
+ allIps.push({
+ address: ip
+ , family: 'IPv4'
+ });
+ });
+ }, function () {})
+ , dns.resolve6Async(hostname).then(function (records) {
+ records.forEach(function (ip) {
+ allIps.push({
+ address: ip
+ , family: 'IPv6'
+ });
+ });
+ }, function () {})
+ ]).then(function () {
+ return allIps;
+ });
+ }
+
+ function resolveIpsAndCnames(hostname) {
+ return PromiseA.all([
+ resolveIps(hostname)
+ , dns.resolveCnameAsync(hostname).then(function (records) {
+ return PromiseA.all(records.map(function (hostname) {
+ return resolveIps(hostname);
+ })).then(function (allIps) {
+ return allIps.reduce(function (all, ips) {
+ return all.concat(ips);
+ }, []);
+ });
+ }, function () {
+ return [];
+ })
+ ]).then(function (ips) {
+ return ips.reduce(function (all, set) {
+ return all.concat(set);
+ }, []);
+ });
+ }
+
+ return resolveIpsAndCnames(servername).then(function (allIps) {
+ var matchingIps = [];
+
+ if (!allIps.length) {
+ console.warn("Could not resolve '" + servername + "'");
+ }
+
+ // { address, family }
+ allIps.some(function (ip) {
+ function match(addr) {
+ if (ip.address === addr.address) {
+ matchingIps.push(addr);
+ }
+ }
+
+ opts.externalIps.forEach(match);
+ // opts.externalIfaces.forEach(match);
+
+ Object.keys(opts.ifaces).forEach(function (iname) {
+ var iface = opts.ifaces[iname];
+
+ iface.ipv4.forEach(match);
+ iface.ipv6.forEach(match);
+ });
+
+ return matchingIps.length;
+ });
+
+ matchingIps.externalIps = {
+ ipv4: [
+ { address: externalIp
+ , family: 'IPv4'
+ }
+ ]
+ , ipv6: [
+ ]
+ };
+ matchingIps.matchingIps = matchingIps;
+ return matchingIps;
+ });
+ });
+};
diff --git a/lib/tunnel.js b/lib/tunnel.js
new file mode 100644
index 0000000..a4ea58c
--- /dev/null
+++ b/lib/tunnel.js
@@ -0,0 +1,144 @@
+'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/package.json b/package.json
index ef272df..d60e3a8 100644
--- a/package.json
+++ b/package.json
@@ -1,21 +1,55 @@
{
"name": "goldilocks",
- "version": "1.0.0-placeholder",
- "description": "The webserver that's just right.",
- "keywords": [
- "greenlock"
- ],
- "main": "index.js",
- "scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
- },
+ "version": "2.2.0",
+ "description": "The node.js webserver that's just right, Greenlock (HTTPS/TLS/SSL via ACME/Let's Encrypt) and tunneling (RVPN) included.",
+ "main": "bin/goldilocks.js",
"repository": {
"type": "git",
- "url": "git@git.daplie.com:Daplie/goldilocks.git"
+ "url": "git@git.daplie.com:Daplie/goldilocks.js.git"
},
- "author": "AJ ONeal (https://coolaj86.com/)",
- "license": "(MIT OR Apache-2.0)",
+ "author": "AJ ONeal (https://daplie.com/)",
+ "license": "SEE LICENSE IN LICENSE.txt",
+ "scripts": { "test": "node bin/goldilocks.js -p 8443 -d /tmp/" },
+ "bin": { "goldilocks": "./bin/goldilocks.js" },
+ "keywords": [
+ "https",
+ "local",
+ "localhost",
+ "development",
+ "dev",
+ "tls",
+ "ssl",
+ "cert",
+ "certs",
+ "certificate",
+ "certificates",
+ "http",
+ "express",
+ "connect",
+ "serve",
+ "server"
+ ],
+ "bugs": { "url": "https://git.daplie.com/Daplie/server-https/issues" },
+ "homepage": "https://git.daplie.com/Daplie/goldilocks.js#readme",
"dependencies": {
- "greenlock": "^2.1.11"
+ "bluebird": "^3.4.6",
+ "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",
+ "finalhandler": "^0.4.0",
+ "httpolyglot": "^0.1.1",
+ "ipify": "^1.1.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",
+ "greenlock-express": "git+https://git.daplie.com/Daplie/greenlock-express.git#master",
+ "greenlock": "git+https://git.daplie.com/Daplie/node-greenlock.git#master",
+ "livereload": "^0.6.0",
+ "localhost.daplie.me-certificates": "^1.3.0",
+ "minimist": "^1.1.1",
+ "oauth3-cli": "git+https://git.daplie.com/OAuth3/oauth3-cli.git#master",
+ "redirect-https": "^1.1.0",
+ "serve-index": "^1.7.0",
+ "serve-static": "^1.10.0",
+ "stunnel": "git+https://git.daplie.com/Daplie/node-tunnel-client.git#master"
}
}
diff --git a/stages/01-serve.js b/stages/01-serve.js
new file mode 100644
index 0000000..8f92791
--- /dev/null
+++ b/stages/01-serve.js
@@ -0,0 +1,23 @@
+'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));
+});
diff --git a/test-chain.sh b/test-chain.sh
new file mode 100755
index 0000000..396d286
--- /dev/null
+++ b/test-chain.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+node serve.js \
+ --port 8443 \
+ --key node_modules/localhost.daplie.me-certificates/privkey.pem \
+ --cert node_modules/localhost.daplie.me-certificates/fullchain.pem \
+ --root node_modules/localhost.daplie.me-certificates/root.pem \
+ -c "$(cat node_modules/localhost.daplie.me-certificates/root.pem)" &
+
+PID=$!
+
+sleep 1
+curl -s --insecure http://localhost.daplie.me:8443 > ./root.pem
+curl -s https://localhost.daplie.me:8443 --cacert ./root.pem
+
+rm ./root.pem
+kill $PID 2>/dev/null