rebrand to goldilocks
This commit is contained in:
commit
8df415c91d
|
@ -30,6 +30,7 @@ build/Release
|
||||||
# Dependency directories
|
# Dependency directories
|
||||||
node_modules
|
node_modules
|
||||||
jspm_packages
|
jspm_packages
|
||||||
|
bower_components
|
||||||
|
|
||||||
# Optional npm cache directory
|
# Optional npm cache directory
|
||||||
.npm
|
.npm
|
||||||
|
@ -42,3 +43,7 @@ jspm_packages
|
||||||
|
|
||||||
# Output of 'npm pack'
|
# Output of 'npm pack'
|
||||||
*.tgz
|
*.tgz
|
||||||
|
|
||||||
|
# Dependency directory
|
||||||
|
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||||
|
node_modules
|
||||||
|
|
|
@ -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.
|
||||||
|
|
159
README.md
159
README.md
|
@ -1,4 +1,161 @@
|
||||||
|
<!-- BANNER_TPL_BEGIN -->
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
<a href="mailto:jobs@daplie.com">jobs@daplie.com</a> | [Invest in Daplie on Wefunder](https://daplie.com/invest/) | [Pre-order Cloud](https://daplie.com/preorder/), The World's First Home Server for Everyone
|
||||||
|
|
||||||
|
<!-- BANNER_TPL_END -->
|
||||||
|
|
||||||
Goldilocks
|
Goldilocks
|
||||||
==========
|
==========
|
||||||
|
|
||||||
The webserver that's just right.
|
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 <port>` - i.e. `sudo goldilocks -p 443` (defaults to 80+443 or 8443)
|
||||||
|
* `-d <dirpath>` - 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 <content>` - i.e. `server-https -c 'Hello, World! '` (defaults to directory index)
|
||||||
|
* `--express-app <path>` - 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 `<dirpath>` has thousands of files it will spike your CPU usage to 100%
|
||||||
|
|
||||||
|
* `--email <email>` - email to use for Let's Encrypt, Daplie DNS, Daplie Tunnel
|
||||||
|
* `--agree-tos` - agree to terms for Let's Encrypt, Daplie DNS
|
||||||
|
* `--sites <domain.tld>` 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 <http://localhost.daplie.me:3443> in a browser,
|
||||||
|
it would redirect to <https://localhost.daplie.me:3443> (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 <https://freedns.afraid.org>.
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
|
@ -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 = '<script src="//'
|
||||||
|
+ (host || opts.sites[0].name).split(':')[0]
|
||||||
|
+ ':35729/livereload.js?snipver=1"></script>';
|
||||||
|
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);
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
|
@ -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('');
|
||||||
|
*/
|
||||||
|
};
|
|
@ -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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//*/
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
60
package.json
60
package.json
|
@ -1,21 +1,55 @@
|
||||||
{
|
{
|
||||||
"name": "goldilocks",
|
"name": "goldilocks",
|
||||||
"version": "1.0.0-placeholder",
|
"version": "2.2.0",
|
||||||
"description": "The webserver that's just right.",
|
"description": "The node.js webserver that's just right, Greenlock (HTTPS/TLS/SSL via ACME/Let's Encrypt) and tunneling (RVPN) included.",
|
||||||
"keywords": [
|
"main": "bin/goldilocks.js",
|
||||||
"greenlock"
|
|
||||||
],
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git@git.daplie.com:Daplie/goldilocks.git"
|
"url": "git@git.daplie.com:Daplie/goldilocks.js.git"
|
||||||
},
|
},
|
||||||
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
"author": "AJ ONeal <aj@daplie.com> (https://daplie.com/)",
|
||||||
"license": "(MIT OR Apache-2.0)",
|
"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": {
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
});
|
|
@ -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
|
Loading…
Reference in New Issue