Compare commits

...

No commits in common. "master" and "v1.x" have entirely different histories.
master ... v1.x

49 changed files with 17904 additions and 2668 deletions

15
.gitignore vendored
View File

@ -1,8 +1,6 @@
# Logs # Logs
logs logs
*.log *.log
npm-debug.log*
.vscode
# Runtime data # Runtime data
pids pids
@ -15,9 +13,6 @@ lib-cov
# Coverage directory used by tools like istanbul # Coverage directory used by tools like istanbul
coverage coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt .grunt
@ -27,12 +22,6 @@ coverage
# Compiled binary addons (http://nodejs.org/api/addons.html) # Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release build/Release
# Dependency directories # Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history

View File

@ -1,7 +0,0 @@
{
"bracketSpacing": true,
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "none",
"useTabs": true
}

575
LICENSE
View File

@ -1,375 +1,202 @@
Copyright 2015-2019 AJ ONeal 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.
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

718
README.md
View File

@ -1,376 +1,478 @@
# New Documentation & [v2/v3 Migration Guide](https://git.rootprojects.org/root/greenlock.js/src/branch/v3/MIGRATION_GUIDE_V2_V3.md) [![Join the chat at https://gitter.im/Daplie/letsencrypt-express](https://badges.gitter.im/Daplie/letsencrypt-express.svg)](https://gitter.im/Daplie/letsencrypt-express?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Greenlock v3 just came out of private beta **today** (Nov 1st, 2019). | [letsencrypt (library)](https://github.com/Daplie/node-letsencrypt)
| [letsencrypt-cli](https://github.com/Daplie/letsencrypt-cli)
| **letsencrypt-express**
| [letsencrypt-koa](https://github.com/Daplie/letsencrypt-koa)
| [letsencrypt-hapi](https://github.com/Daplie/letsencrypt-hapi)
|
The code is complete and we're working on great documentation. # HELP WANTED
Many **examples** and **full API** documentation are still coming. There are a number of easy to fix bugs (the most important of which is basically requires tracing some functions, doing some console.log-ing and returning the right type of object).
# [Greenlock Express](https://git.rootprojects.org/root/greenlock-express.js) is Let's Encrypt for Node If you've got some free cycles to help, I can guide you through the process, I'm just still too busy to fix them right now and the workarounds work mentioned in the comments work.
![Greenlock Logo](https://git.rootprojects.org/root/greenlock.js/raw/branch/master/logo/greenlock-1063x250.png "Greenlock Logo") Email me coolaj86@gmail.com if you want to help.
| Built by [Root](https://therootcompany.com) for [Hub](https://rootprojects.org/hub/) # LetsEncrypt Express
Free SSL, Automated HTTPS / HTTP2, served with Node via Express, Koa, hapi, etc. [![Join the chat at https://gitter.im/Daplie/letsencrypt-express](https://badges.gitter.im/Daplie/letsencrypt-express.svg)](https://gitter.im/Daplie/letsencrypt-express?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
### Let's Encrypt for Node, Express, etc Free SSL and managed or automatic HTTPS for node.js with Express, Koa, Connect, Hapi, and all other middleware systems.
Greenlock Express is a **Web Server** with **Fully Automated HTTPS** and renewals. * Automatic Registration via SNI (`httpsOptions.SNICallback`)
* **registrations** require an **approval callback** in *production*
* Automatic Renewal (around 80 days)
* **renewals** are *fully automatic* and happen in the *background*, with **no downtime**
* Automatic vhost / virtual hosting
```js All you have to do is start the webserver and then visit it at it's domain name.
"use strict";
function httpsWorker(glx) { ## Install
// Serves on 80 and 443
// Get's SSL certificates magically!
glx.serveApp(function(req, res) { ```
res.end("Hello, Encrypted World!"); npm install --save letsencrypt-express
});
}
var pkg = require("./package.json");
require("greenlock-express")
.init(function getConfig() {
// Greenlock Config
return {
package: { name: pkg.name, version: pkg.version },
maintainerEmail: pkg.author,
cluster: false
};
})
.serve(httpsWorker);
``` ```
Manage via API or the config file: ## Usage
`~/.config/greenlock/manage.json`: (default filesystem config) * standalone
* express
* http / https
* http / http2 / spdy
* koa
```json ### Setup (same for all examples)
{
"subscriberEmail": "letsencrypt-test@therootcompany.com", ```javascript
"agreeToTerms": true, 'use strict';
"sites": {
"example.com": { /* Note: using staging server url, remove .testing() for production
"subject": "example.com", Using .testing() will overwrite the debug flag with true */
"altnames": ["example.com", "www.example.com"] var LEX = require('letsencrypt-express').testing();
}
} // Change these two lines!
} var DOMAIN = 'myservice.example.com';
var EMAIL = 'user@example.com';
var lex = LEX.create({
configDir: require('os').homedir() + '/letsencrypt/etc'
, approveRegistration: function (hostname, approve) { // leave `null` to disable automatic registration
if (hostname === DOMAIN) { // Or check a database or list of allowed domains
approve(null, {
domains: [DOMAIN]
, email: EMAIL
, agreeTos: true
});
}
}
});
``` ```
# Let's Encrypt for... WARNING: If you don't do any checks and simply complete `approveRegistration` callback, an attacker will spoof SNI packets with bad hostnames and that will cause you to be rate-limited and or blocked from the ACME server. Alternatively, You can run registration *manually*:
- IoT
- Enterprise On-Prem
- Local Development
- Home Servers
- Quitting Heroku
# Features
- [x] Let's Encrypt v2 (November 2019)
- [x] ACME Protocol (RFC 8555)
- [x] HTTP Validation (HTTP-01)
- [x] DNS Validation (DNS-01)
- [ ] ALPN Validation (TLS-ALPN-01)
- Need ALPN validation? [contact us](mailto:greenlock-support@therootcompany.com)
- [x] Automated HTTPS
- [x] Fully Automatic Renewals every 45 days
- [x] Free SSL
- [x] **Wildcard** SSL
- [x] **Localhost** certificates
- [x] HTTPS-enabled Secure **WebSockets** (`wss://`)
- [x] Fully customizable
- [x] **Reasonable defaults**
- [x] Domain Management
- [x] Key and Certificate Management
- [x] ACME Challenge Plugins
# QuickStart Guide
Easy as 1, 2, 3... 4
<details>
<summary>1. Create a node project</summary>
## 1. Create a node project
Create an empty node project.
Be sure to fill out the package name, version, and an author email.
```bash ```bash
mkdir ~/my-project npm install -g letsencrypt-cli
pushd ~/my-project
npm init letsencrypt certonly --standalone \
--config-dir ~/letsencrypt/etc \
--agree-tos --domains example.com --email user@example.com
# Note: the '--webrootPath' option is also available if you don't want to shut down your webserver to get the cert.
``` ```
</details> ### Standalone
<details> ```javascript
<summary>2. Create an http app (i.e. express)</summary> lex.onRequest = function (req, res) {
res.end('Hello, World!');
};
## 2. Create an http app (i.e. express) lex.listen([80], [443, 5001], function () {
console.log("ENCRYPT __ALL__ THE DOMAINS!");
});
This example is shown with Express, but any node app will do. Greenlock // NOTE:
works with everything. // `~/letsencrypt/etc` is the default `configDir`
(or any node-style http app) // ports 80, 443, and 5001 are the default ports to listen on.
```
`my-express-app.js`: ## Express
```js ```bash
"use strict"; npm install --save spdy
```
// A plain, node-style app ```javascript
// A happy little express app
function myPlainNodeHttpApp(req, res) { var express = require('express');
res.end("Hello, Encrypted World!");
}
// Wrap that plain app in express,
// because that's what you're used to
var express = require("express");
var app = express(); var app = express();
app.get("/", myPlainNodeHttpApp);
// export the app normally app.use(function (req, res) {
// do not .listen() res.send({ success: true });
});
module.exports = app; lex.onRequest = app;
lex.listen([80], [443, 5001], function () {
var protocol = ('requestCert' in this) ? 'https': 'http';
console.log("Listening at " + protocol + '://localhost:' + this.address().port);
});
``` ```
</details> ### Use with raw http / https modules
<details> Let's say you want to redirect all http to https.
<summary>3. Serve with Greenlock Express</summary>
## 3. Serve with Greenlock Express ```javascript
var http = require('http');
var https = require('spdy');
// NOTE: you could use the old https module if for some reason you don't want to support modern browsers
Greenlock Express is designed with these goals in mind: function redirectHttp() {
http.createServer(LEX.createAcmeResponder(lex, function redirectHttps(req, res) {
- Simplicity and ease-of-use res.setHeader('Location', 'https://' + req.headers.host + req.url);
- Performance and scalability res.statusCode = 302; // use 307 if you want to redirect requests with POST, DELETE or PUT action.
- Configurability and control res.end('<!-- Hello Developer Person! Please use HTTPS instead -->');
})).listen(80);
You can start with **near-zero configuration** and
slowly add options for greater performance and customization
later, if you need them.
`server.js`:
```js
require("greenlock-express")
.init(getConfig)
.serve(worker);
function getConfig() {
return {
// uses name and version as part of the ACME client user-agent
// uses author as the contact for support notices
package: require("./package.json")
};
} }
function worker(server) { function serveHttps() {
// Works with any Node app (Express, etc) var app = require('express')();
var app = require("my-express-app.js");
server.serveApp(app); app.use('/', function (req, res) {
res.end('Hello!');
});
https.createServer(lex.httpsOptions, LEX.createAcmeResponder(lex, app)).listen(443);
}
redirectHttp();
serveHttps();
```
### Let's Encrypt with Koa
```javascript
var http = require('http');
var https = require('spdy'); // Note: some have reported trouble with `http2` and success with `spdy`
var koa = require('koa');
var app = koa();
var redirectHttps = koa().use(require('koa-sslify')()).callback();
app.use(function *() {
this.body = 'Hello World';
});
var server = https.createServer(lex.httpsOptions, LEX.createAcmeResponder(lex, app.callback()));
var redirectServer = http.createServer(LEX.createAcmeResponder(lex, redirectHttps)));
server.listen(443, function () {
console.log('Listening at https://localhost:' + this.address().port);
});
redirectServer.listen(80, function () {
console.log('Redirecting insecure traffic from http://localhost:' + this.address().port + ' to https');
});
```
### WebSockets with Let's Encrypt
Note: you don't need to create websockets for the plain ports.
```javascript
var WebSocketServer = require('ws').Server;
var https = require('spdy');
var server = https.createServer(lex.httpsOptions, LEX.createAcmeResponder(lex, app));
var wss = new WebSocketServer({ server: server });
wss.on('connection', onConnection);
server.listen(443);
function onConnection(ws) {
var location = url.parse(ws.upgradeReq.url, true);
// you might use location.query.access_token to authenticate or share sessions
// or ws.upgradeReq.headers.cookie (see http://stackoverflow.com/a/16395220/151312)
ws.on('message', function incoming(message) {
console.log('received: %s', message);
});
ws.send('something');
} }
``` ```
And start your server: ## API
```bash ```
# Allow non-root node to use ports 80 (HTTP) and 443 (HTTPS) // checks options and sets up defaults. returns object with `listen`
sudo setcap 'cap_net_bind_service=+ep' $(which node) LEX.create(options) // (it was really just done this way to appeal to what people are used to seeing)
lex.listen(plain, tls, fn) // actually creates the servers and causes them to listen
// receives an instance of letsencrypt, returns an SNICallback handler for https.createServer()
LEX.createSniCallback(opts) // this will call letsencrypt.renew and letsencrypt.register as appropriate
// it will randomly stagger renewals such that they don't all happen at once on boot
// or at any other time. registrations will be handled as per `handleRegistration`
opts = {
letsencrypt: <obj> // letsencrypt instance
, memorizeFor: <1 day> // how long to wait before checking the disk for updated certificates
, renewWithin: <3 days> // the first possible moment the certificate staggering should begin
, failedWait: <5 minutes> // how long to wait before trying again if the certificate registration failed
// registrations are NOT approved automatically by default due to security concerns
, approveRegistration: func // (someone can spoof servername indication to your server and cause you to be rate-limited)
// but you can implement handling of them if you wish
// (note that you should probably call the callback immediately with a tlsContext)
//
// default function (hostname, cb) { cb(null, null); }
//
// example function (hostname, cb) {
// cb(null, { domains: [hostname], agreeTos: true, email: 'user@example.com' });
// }
, handleRenewFailure: func // renewals are automatic, but sometimes they may fail. If that happens, you should handle it
// (note that renewals happen in the background)
//
// default function (err, letsencrypt, hostname, certInfo) {}
}
// uses `opts.webrootPath` to read from the filesystem
LEX.getChallenge(opts, hostname, key cb)
LEX.createAcmeResponder(opts, fn) // this will return the necessary request handler for /.well-known/acme-challenges
// which then calls `fn` (such as express app) to complete the request
//
// opts lex instance created with LEX.create(opts)
// more generally, any object with a compatible `getChallenge` will work:
// `lex.getChallenge(opts, domain, key, function (err, val) {})`
//
// fn function (req, res) {
// console.log(req.method, req.url);
//
// res.end('Hello!');
// }
``` ```
```bash ## Options
# `npm start` will call `node ./server.js` by default
npm start If any of these values are `undefined` or `null` the will assume use reasonable defaults.
Partially defined values will be merged with the defaults.
Setting the value to `false` will, in many cases (as documented), disable the defaults.
```
configDir: string // string the letsencrypt configuration path (de facto /etc/letsencrypt)
//
// default os.homedir() + '/letsencrypt/etc'
webrootPath: string // string a path to a folder where temporary challenge files will be stored and read
//
// default os.tmpdir() + '/acme-challenge'
getChallenge: func | false // false do not handle getChallenge
//
// func Example:
//
// default function (defaults, hostname, key, cb) {
// var filename = path.join(defaults.webrootPath.replace(':hostname', hostname), key);
// fs.readFile(filename, 'ascii', function (cb, text) {
// cb(null, text);
// })
// }
httpsOptions: object // object will be merged with internal defaults and passed to https.createServer()
// { pfx, key, cert, passphrase, ca, ciphers, rejectUnauthorized, secureProtocol }
// See https://nodejs.org/api/https.html
// Note: if SNICallback is specified, it will be run *before*
// the internal SNICallback that manages automated certificates
//
// default uses a localhost cert and key to prevent https.createServer() from throwing an error
// and also uses our SNICallback, which manages certificates
sniCallback: func // func replace the default sniCallback handler (which manages certificates) with your own
letsencrypt: object // object configure the letsencrypt object yourself and pass it in directly
//
// default we create the letsencrypt object using parameters you specify
server: url // url use letsencrypt.productionServerUrl (i.e. https://acme-v01.api.letsencrypt.org/directory)
// or letsencrypt.stagingServerUrl (i.e. https://acme-staging.api.letsencrypt.org/directory)
//
// default production
``` ```
```txt ## More Examples
Greenlock v3.0.0
Greenlock Manager Config File: ~/.config/greenlock/manager.json
Greenlock Storage Directory: ~/.config/greenlock/
Listening on 0.0.0.0:80 for ACME challenges and HTTPS redirects ### < 140 Characters
Listening on 0.0.0.0:443 for secure traffic
Let's Encrypt in 128 characters, with spaces!
```
node -e 'require("letsencrypt-express").testing().create( require("express")().use(function (_, r) { r.end("Hi!") }) ).listen()'
``` ```
</details> ### More realistic
<details> ```javascript
<summary>4. Manage SSL Certificates and Domains</summary> 'use strict';
## 4. Manage domains // Note: using staging server url, remove .testing() for production
var LEX = require('letsencrypt-express').testing();
var express = require('express');
var app = express();
The management API is built to work with Databases, S3, etc. app.use('/', function (req, res) {
res.send({ success: true });
});
HOWEVER, by default it starts with a simple config file. LEX.create({
configDir: './letsencrypt.config' // ~/letsencrypt, /etc/letsencrypt, whatever you want
<!-- , onRequest: app // your express app (or plain node http app)
This will update the config file (assuming the default fs-based management plugin):
-->
`~/.config/greenlock/manager.json`: , letsencrypt: null // you can provide you own instance of letsencrypt
// if you need to configure it (with an agreeToTerms
// callback, for example)
```json , approveRegistration: function (hostname, cb) { // PRODUCTION MODE needs this function, but only if you want
{ // automatic registration (usually not necessary)
"subscriberEmail": "letsencrypt-test@therootcompany.com", // renewals for registered domains will still be automatic
"agreeToTerms": true, cb(null, {
"sites": { domains: [hostname]
"example.com": { , email: 'user@example.com'
"subject": "example.com", , agreeTos: true // you
"altnames": ["example.com", "www.example.com"] });
} }
} }).listen([80], [443, 5001], function () {
} console.log("ENCRYPT __ALL__ THE DOMAINS!");
});
``` ```
COMING SOON ### More Options Exposed
Management can be done via the **CLI** or the JavaScript [**API**](https://git.rootprojects.org/root/greenlock.js/). ```javascript
Since this is the QuickStart, we'll demo the **CLI**: 'use strict';
You need to create a Let's Encrypt _subscriber account_, which can be done globally, or per-site. var lex = require('letsencrypt-express');
All individuals, and most businesses, should set this globally: var express = require('express');
var app = express();
```bash app.use('/', function (req, res) {
# COMING SOON res.send({ success: true });
# (this command should be here by Nov 5th) });
# (edit the config by hand for now)
# var results = lex.create({
# Set a global subscriber account configDir: '/etc/letsencrypt'
npx greenlock config --subscriber-email 'mycompany@example.com' --agree-to-terms true , onRequest: app
, server: require('letsencrypt').productionServerUrl
}).listen(
// you can give just the port, or expand out to the full options
[80, { port: 8080, address: 'localhost', onListening: function () { console.log('http://localhost'); } }]
// you can give just the port, or expand out to the full options
, [443, 5001, { port: 8443, address: 'localhost' }]
// this is pretty much the default onListening handler
, function onListening() {
var server = this;
var protocol = ('requestCert' in server) ? 'https': 'http';
console.log("Listening at " + protocol + '://localhost:' + this.address().port);
}
);
// In case you need access to the raw servers (i.e. using websockets)
console.log(results.plainServers);
console.log(results.tlsServers);
``` ```
<!-- todo print where the key was saved -->
A Let's Encrypt SSL certificate has a "Subject" (Primary Domain) and up to 100 "Alternative Names" ### All Options Exposed
(of which the first _must_ be the subject).
```bash Here's absolutely every option and function exposed
# COMING SOON
# (this command should be here by Nov 5th) ```javascript
# (edit the config by hand for now) var http = require('http');
# var https = require('spdy');
# Add a certificate with specific domains var LEX = require('letsencrypt-express');
npx greenlock add --subject example.com --altnames example.com,www.example.com var LE = require('letsencrypt');
var lex;
lex = LEX.create({
webrootPath: '/tmp/.well-known/acme-challenge'
, lifetime: 90 * 24 * 60 * 60 * 1000 // expect certificates to last 90 days
, failedWait: 5 * 60 * 1000 // if registering fails wait 5 minutes before trying again
, renewWithin: 3 * 24 * 60 * 60 * 1000 // renew at least 3 days before expiration
, memorizeFor: 1 * 24 * 60 * 60 * 1000 // keep certificates in memory for 1 day
, approveRegistration: function (hostname, cb) {
cb(null, {
domains: [hostname]
, email: 'user@example.com'
, agreeTos: true
});
}
, handleRenewFailure: function (err, hostname, certInfo) {
console.error("ERROR: Failed to renew domain '", hostname, "':");
if (err) {
console.error(err.stack || err);
}
if (certInfo) {
console.error(certInfo);
}
}
, letsencrypt: LE.create(
// options
{ configDir: './letsencrypt.config'
, manual: true
, server: LE.productionServerUrl
, privkeyPath: LE.privkeyPath
, fullchainPath: LE.fullchainPath
, certPath: LE.certPath
, chainPath: LE.chainPath
, renewalPath: LE.renewalPath
, accountsDir: LE.accountsDir
, debug: false
}
// handlers
, { setChallenge: LEX.setChallenge
, removeChallenge: LEX.removeChallenge
}
)
, debug: false
});
http.createServer(LEX.createAcmeResponder(lex, function (req, res) {
res.setHeader('Location', 'https://' + req.headers.host + req.url);
res.end('<!-- Hello Mr Developer! Please use HTTPS instead -->');
}));
https.createServer(lex.httpsOptions, LEX.createAcmeResponder(lex, function (req, res) {
res.end('Hello!');
}));
``` ```
<!-- todo print where the cert was saved --> ## Heroku?
Note: **Localhost**, **Wildcard**, and Certificates for Private Networks require This doesn't work on heroku because heroku uses a proxy with built-in https
[**DNS validation**](https://git.rootprojects.org/root/greenlock-exp). (which is a smart thing to do) and besides, they want you to pay big bucks
for https. (hopefully not for long?...)
- DNS Validation
- [**Wildcards**](https://git.rootprojects.org/root/greenlock-express.js/src/branch/master/examples/wildcards/) (coming soon)
- [**Localhost**](https://git.rootprojects.org/root/greenlock-express.js/src/branch/master/examples/localhost/) (coming soon)
- [**CI/CD**](https://git.rootprojects.org/root/greenlock-express.js/src/branch/master/examples/ci-cd/) (coming soon)
</details>
# Plenty of Examples
**These are in-progress** Check back tomorrow (Nov 2nd, 2019).
- [greenlock-express.js/examples/](https://git.rootprojects.org/root/greenlock-express.js/src/branch/master/examples)
- [Express](https://git.rootprojects.org/root/greenlock-express.js/src/branch/master/examples/express/)
- [Node's **http2**](https://git.rootprojects.org/root/greenlock-express.js/src/branch/master/examples/http2/)
- [Node's https](https://git.rootprojects.org/root/greenlock-express.js/src/branch/master/examples/https/)
- [**WebSockets**](https://git.rootprojects.org/root/greenlock-express.js/src/branch/master/examples/websockets/)
- [Socket.IO](https://git.rootprojects.org/root/greenlock-express.js/src/branch/master/examples/socket-io/)
- [Cluster](https://git.rootprojects.org/root/greenlock-express.js/src/branch/master/examples/cluster/)
- [**Wildcards**](https://git.rootprojects.org/root/greenlock-express.js/src/branch/master/examples/wildcards/) (coming soon)
- [**Localhost**](https://git.rootprojects.org/root/greenlock-express.js/src/branch/master/examples/localhost/) (coming soon)
- [**CI/CD**](https://git.rootprojects.org/root/greenlock-express.js/src/branch/master/examples/ci-cd/) (coming soon)
- [HTTP Proxy](https://git.rootprojects.org/root/greenlock-express.js/src/branch/master/examples/http-proxy/)
# Easy to Customize
<!-- greenlock-manager-test => greenlock-manager-custom -->
<!--
- [greenlock.js/examples/](https://git.rootprojects.org/root/greenlock.js/src/branch/master/examples)
-->
- [Custom Domain Management](https://git.rootprojects.org/root/greenlock-manager-test.js)
- [Custom Key & Cert Storage](https://git.rootprojects.org/root/greenlock-store-test.js)
- [Custom ACME HTTP-01 Challenges](https://git.rootprojects.org/root/acme-http-01-test.js)
- [Custom ACME DNS-01 Challenges](https://git.rootprojects.org/root/acme-dns-01-test.js)
# Ready-made Integrations
Greenlock Express integrates between Let's Encrypt's ACME Challenges and many popular services.
| Type | Service | Plugin |
| ----------- | ----------------------------------------------------------------------------------- | ------------------------ |
| dns-01 | CloudFlare | acme-dns-01-cloudflare |
| dns-01 | [Digital Ocean](https://git.rootprojects.org/root/acme-dns-01-digitalocean.js) | acme-dns-01-digitalocean |
| dns-01 | [DNSimple](https://git.rootprojects.org/root/acme-dns-01-dnsimple.js) | acme-dns-01-dnsimple |
| dns-01 | [DuckDNS](https://git.rootprojects.org/root/acme-dns-01-duckdns.js) | acme-dns-01-duckdns |
| http-01 | File System / [Web Root](https://git.rootprojects.org/root/acme-http-01-webroot.js) | acme-http-01-webroot |
| dns-01 | [GoDaddy](https://git.rootprojects.org/root/acme-dns-01-godaddy.js) | acme-dns-01-godaddy |
| dns-01 | [Gandi](https://git.rootprojects.org/root/acme-dns-01-gandi.js) | acme-dns-01-gandi |
| dns-01 | [NameCheap](https://git.rootprojects.org/root/acme-dns-01-namecheap.js) | acme-dns-01-namecheap |
| dns-01 | [Name&#46;com](https://git.rootprojects.org/root/acme-dns-01-namedotcom.js) | acme-dns-01-namedotcom |
| dns-01 | Route53 (AWS) | acme-dns-01-route53 |
| http-01 | S3 (AWS, Digital Ocean, Scaleway) | acme-http-01-s3 |
| dns-01 | [Vultr](https://git.rootprojects.org/root/acme-dns-01-vultr.js) | acme-dns-01-vultr |
| dns-01 | [Build your own](https://git.rootprojects.org/root/acme-dns-01-test.js) | acme-dns-01-test |
| http-01 | [Build your own](https://git.rootprojects.org/root/acme-http-01-test.js) | acme-http-01-test |
| tls-alpn-01 | [Contact us](mailto:support@therootcompany.com) | - |
Search `acme-http-01-` or `acme-dns-01-` on npm to find more.
# Full Documentation
<!--
- Greenlock CLI
- Greenlock JavaScript API
-->
Most of the documentation is done by use-case examples, as shown up at the top of the README.
We're working on more comprehensive documentation for this newly released version.
**Please open an issue** with questions in the meantime.
# Commercial Support
Do you need...
- training?
- specific features?
- different integrations?
- bugfixes, on _your_ timeline?
- custom code, built by experts?
- commercial support and licensing?
You're welcome to [contact us](mailto:aj@therootcompany.com) in regards to IoT, On-Prem,
Enterprise, and Internal installations, integrations, and deployments.
We have both commercial support and commercial licensing available.
We also offer consulting for all-things-ACME and Let's Encrypt.
# Legal &amp; Rules of the Road
Greenlock&trade; is a [trademark](https://rootprojects.org/legal/#trademark) of AJ ONeal
The rule of thumb is "attribute, but don't confuse". For example:
> Built with [Greenlock Express](https://git.rootprojects.org/root/greenlock.js) (a [Root](https://rootprojects.org) project).
Please [contact us](mailto:aj@therootcompany.com) if you have any questions in regards to our trademark,
attribution, and/or visible source policies. We want to build great software and a great community.
[Greenlock&trade;](https://git.rootprojects.org/root/greenlock.js) |
MPL-2.0 |
[Terms of Use](https://therootcompany.com/legal/#terms) |
[Privacy Policy](https://therootcompany.com/legal/#privacy)

226
bin/lex.js Executable file
View File

@ -0,0 +1,226 @@
#!/usr/bin/env node
'use strict';
var fs = require('fs');
var path = require('path');
var mkdirp = require('fs');
var cli = require('cli');
var mkdirp = require('mkdirp');
var homedir = require('os').homedir();
var configDir = path.join(homedir, 'letsencrypt');
var desktop = path.join(homedir, 'Desktop');
var vhostDir = path.join(fs.existsSync(desktop) ? desktop : configDir, 'www');
var welcomeHtml = fs.readFileSync(path.join(__dirname, '..', 'lib', 'public', 'welcome.html'), 'utf8');
var express = require('express');
cli.parse({
'agree-tos': [ false, " Agree to the Let's Encrypt Subscriber Agreement", 'boolean', false ]
, email: [ false, " Email used for registration and recovery contact. (default: null)", 'email' ]
, domains: [ false, " Domain names to apply. To include the www domain with your main domain your can enter both with a comma. Ex --domains example.com,www.example.com (default: [])", 'string' ]
, debug: [ false, " show traces and logs", 'boolean', false ]
, server: [ false, " ACME Directory Resource URI.", 'string', 'https://acme-v01.api.letsencrypt.org/directory)' ]
});
// ignore certonly and extraneous arguments
cli.main(function(_, options) {
console.log('');
var args = {};
Object.keys(options).forEach(function (key) {
var val = options[key];
if ('string' === typeof val) {
val = val.replace(/^~/, homedir);
}
key = key.replace(/\-([a-z0-9A-Z])/g, function (c) { return c[1].toUpperCase(); });
args[key] = val;
});
if (args.domains) {
args.domains = args.domains.split(',');
}
makeDirectories();
function makeDirectories() {
mkdirp(configDir, function (err) {
if (err) {
console.error("Could not create config directory '" + configDir + "':", err.code);
console.error(err.stack);
return;
}
mkdirp(vhostDir, function (err) {
if (err) {
console.error("Could not create vhost directory '" + vhostDir + "':", err.code);
console.error(err.stack);
return;
}
startServers();
});
});
}
function configure(le, args, cb) {
var vhost;
var pubDir;
var index;
if (!(args.email && args.agreeTos && args.server && args.domains)) {
cb({ error : { message: "missing one or more of agreeTos,domains,email,server" } });
return;
}
vhost = args.domains[0];
pubDir = path.join(vhostDir, vhost);
index = path.join(pubDir, 'index.html');
makeLandingPage();
function makeLandingPage() {
mkdirp(pubDir, function (err) {
if (err) {
cb(err);
return;
}
fs.exists(index, function (exists) {
if (exists) {
configureForHttps();
return;
}
fs.writeFile(path.join(pubDir, 'index.html'), welcomeHtml.replace(/:hostname/g, vhost), 'utf8', function (err) {
if (err) {
cb(err);
return;
}
configureForHttps();
});
});
});
}
function configureForHttps() {
if (args.debug) {
console.log('[LEX] configureForHttps');
console.log(args);
}
le.setConfig(args, cb);
}
}
function createConfigurator(le) {
var app = express();
app.use('/', express.static(path.join(__dirname, '..', 'lib', 'configurator')));
app.use(require('body-parser').json());
app.get('/api/com.daplie.lex/sites', function (req, res, next) {
le.getConfigs({ configDir: configDir }, function (err, configs) {
if (err) {
next(err);
return;
}
res.send(configs);
});
});
app.post('/api/com.daplie.lex/sites', function (req, res, next) {
var data = req.body;
configure(le, data, function (err, configs) {
if (err) {
console.error("[LEX/bin] configure");
console.error(err.stack);
next(err);
return;
}
res.send(configs);
});
});
return app;
}
function startServers() {
// Note: using staging server url, remove .testing() for production
var LE = require('letsencrypt');
var LEX = require('../');
var le = LE.create({
configDir: configDir
, manual: true
, privkeyPath: LE.privkeyPath
, fullchainPath: LE.fullchainPath
, certPath: LE.certPath
, chainPath: LE.chainPath
, renewalPath: LE.renewalPath
, accountsDir: LE.accountsDir
}, {
setChallenge: LEX.setChallenge
, removeChallenge: LEX.removeChallenge
});
var app = express();
var vhosts = {};
vhosts['localhost.daplie.com'] = createConfigurator(le, vhosts);
app.use('/', function (req, res, next) {
var hostname = (req.hostname||req.headers.host||'').replace(/^www\./, '');
var pubDir = path.join(vhostDir, hostname);
if (vhosts[hostname]) {
vhosts[hostname](req, res, next);
return;
}
fs.exists(pubDir, function (exists) {
if (exists) {
vhosts[hostname] = express().use('/', express.static(pubDir));
vhosts[hostname](req, res, next);
} else {
vhosts['localhost.daplie.com'](req, res, next);
}
});
});
app.use('/', express.static(path.join(__dirname, '..', 'lib', 'public')));
LEX.create({
onRequest: app
, configDir: configDir
, letsencrypt: le
, approveRegistration: function (domain, cb) {
le.getConfig({ domains: [domain] }, function (err, config) {
if (!(config && config.checkpoints >= 0)) {
cb(null, null);
return;
}
cb(null, {
email: config.email
// can't remember which it is, but the pyconf is different that the regular variable
, agreeTos: config.tos || config.agree || config.agreeTos
, server: config.server || LE.productionServerUrl
, domains: config.domains || [domain]
});
});
}
}).listen([80], [443, 5001]);
}
/*
// should get back account, path to certs, pems, etc?
console.log('\nCertificates installed at:');
console.log(Object.keys(results).filter(function (key) {
return /Path/.test(key);
}).map(function (key) {
return results[key];
}).join('\n'));
*/
});

View File

@ -1,20 +0,0 @@
"use strict";
var path = require("path");
module.exports = {
email: "jon.doe@example.com",
configDir: path.join(__dirname, "acme"),
srv: "/srv/www/",
api: "/srv/api/",
proxy: {
"example.com": "http://localhost:4080",
"*.example.com": "http://localhost:4080"
},
// DNS-01 challenges only
challenges: {
"*.example.com": require("acme-dns-01-YOUR_DNS_HOST").create({
token: "xxxx"
})
}
};

35
demo.js
View File

@ -1,35 +0,0 @@
"use strict";
require("./")
.init(initialize)
.serve(worker)
.master(function() {
console.log("Hello from master");
});
function initialize() {
var pkg = require("./package.json");
var config = {
package: {
name: "Greenlock_Express_Demo",
version: pkg.version,
author: pkg.author
},
staging: true,
cluster: true,
notify: function(ev, params) {
console.info(ev, params);
}
};
return config;
}
function worker(glx) {
console.info();
console.info("Hello from worker #" + glx.id());
glx.serveApp(function(req, res) {
res.end("Hello, Encrypted World!");
});
}

View File

@ -1,53 +0,0 @@
# sudo systemctl daemon-reload
# sudo systemctl restart greenlock-express
# sudo journalctl -xefu greenlock-express
[Unit]
Description=Greenlock Static Server
Documentation=https://git.coolaj86.com/coolaj86/greenlock-express.js/
After=network.target
Wants=network.target systemd-networkd-wait-online.service
[Service]
# Restart on crash (bad signal), 'clean' failure (error exit code), everything
# Allow up to 3 restarts within 10 seconds
# (it's unlikely that a user or properly-running script will do this)
Restart=always
StartLimitInterval=10
StartLimitBurst=3
# User and group the process will run as
# (git is the de facto standard on most systems)
User=ubuntu
Group=ubuntu
WorkingDirectory=/srv/www
# custom directory cannot be set and will be the place where gitea exists, not the working directory
ExecStart=/opt/node/bin/node /opt/greenlock-express.js/server.js /opt/greenlock-express.js/config.js
# Limit the number of file descriptors and processes; see `man systemd.exec` for more limit settings.
# greenlock is not expected to use more than this.
LimitNOFILE=1048576
LimitNPROC=64
# Use private /tmp and /var/tmp, which are discarded after gitea stops.
PrivateTmp=true
# Use a minimal /dev
PrivateDevices=true
# Hide /home, /root, and /run/user. Nobody will steal your SSH-keys.
ProtectHome=true
# Make /usr, /boot, /etc and possibly some more folders read-only.
ProtectSystem=full
# ... except /opt/greenlock-express.js/acme because we want a place for the database
# and /opt/greenlock-express.js/var because we want a place where logs can go.
# This merely retains r/w access rights, it does not add any new.
# Must still be writable on the host!
ReadWriteDirectories=/srv/www /opt/greenlock-express.js
# Note: in v231 and above ReadWritePaths has been renamed to ReadWriteDirectories
; ReadWritePaths=/opt/gitea /var/log/gitea
# The following additional security directives only work with systemd v229 or later.
# They further retrict privileges that can be gained by gitea.
# Note that you may have to add capabilities required by any plugins in use.
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE

9
examples/116.js Normal file
View File

@ -0,0 +1,9 @@
'use strict';
// Don't try this at home kids, it's just for fun
//
// require('letsencrypt-express')
require('../').testing().create(require('express')().use(function (_, r) {
r.end('Hi!');
})).listen();

View File

@ -1,39 +0,0 @@
"use strict";
var pkg = require("../../package.json");
//require("greenlock-express")
require("../../")
.init(function getConfig() {
// Greenlock Config
return {
package: { name: "websocket-example", version: pkg.version },
maintainerEmail: "jon@example.com",
// When you're ready to go full cloud scale, you just change this to true:
// Note: in cluster you CANNOT use in-memory state (see below)
cluster: true,
// This will default to the number of workers being equal to
// n-1 cpus, with a minimum of 2
workers: 4
};
})
.serve(httpsWorker);
function httpsWorker(glx) {
// WRONG
// This won't work like you
// think because EACH worker
// has ITS OWN `count`.
var count = 0;
var app = function(req, res) {
res.end("Hello... how many times now? Oh, " + count + " times");
count += 1;
};
// Serves on 80 and 443... for each worker
// Get's SSL certificates magically!
glx.serveApp(app);
}

50
examples/express.js Normal file
View File

@ -0,0 +1,50 @@
'use strict';
// allow node.js to bind to port 80 and 443 without root:
//
// sudo setcap 'cap_net_bind_service=+ep' `which node`
/* Note: using staging server url, remove .testing() for production
Using .testing() will overwrite the debug flag with true */
var LEX = require('letsencrypt-express').testing();
var http = require('http');
// NOTE: you could use the old https module if for some reason
// you don't want to support modern browsers
var https = require('spdy');
var leConfDir = require('os').homedir() + '/letsencrypt/etc';
throw new Error(
"You must edit the example to change the email address (and remove this error)."
+ " Also, you'll need to remove .testing() and rm -rf '" + leConfDir + "'"
+ " to get actual, trusted production certificates.");
var lex = LEX.create({
configDir: leConfDir
, approveRegistration: function (hostname, cb) { // leave `null` to disable automatic registration
// Note: this is the place to check your database to get the user associated with this domain
cb(null, {
domains: [hostname]
, email: 'CHANGE_ME' // user@example.com
, agreeTos: true
});
}
});
function redirectHttp() {
http.createServer(LEX.createAcmeResponder(lex, function redirectHttps(req, res) {
res.setHeader('Location', 'https://' + req.headers.host + req.url);
res.statusCode = 302;
res.end('<!-- Hello Developer Person! Please use HTTPS instead -->');
})).listen(80);
}
function serveHttps() {
var app = require('express')();
app.use('/', function (req, res) {
res.end('Hello!');
});
https.createServer(lex.httpsOptions, LEX.createAcmeResponder(lex, app)).listen(443);
}
redirectHttp();
serveHttps();

View File

@ -1,17 +0,0 @@
"use strict";
var express = require("express");
var app = express();
app.use("/", function(req, res) {
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end("Hello, World!\n\n💚 🔒.js");
});
// DO NOT DO app.listen() unless we're testing this directly
if (require.main === module) {
app.listen(3000);
}
// Instead do export the app:
module.exports = app;

View File

@ -1,27 +0,0 @@
"use strict";
function httpsWorker(glx) {
var app = require("./my-express-app.js");
app.get("/hello", function(req, res) {
res.end("Hello, Encrypted World!");
});
// Serves on 80 and 443
// Get's SSL certificates magically!
glx.serveApp(app);
}
var pkg = require("../../package.json");
//require("greenlock-express")
require("../../")
.init(function getConfig() {
// Greenlock Config
return {
package: { name: "http2-example", version: pkg.version },
maintainerEmail: "jon@example.com",
cluster: false
};
})
.serve(httpsWorker);

View File

@ -1,44 +0,0 @@
"use strict";
function httpsWorker(glx) {
// we need the raw https server
var server = glx.httpsServer();
var proxy = require("http-proxy").createProxyServer({ xfwd: true });
// catches error events during proxying
proxy.on("error", function(err, req, res) {
console.error(err);
res.statusCode = 500;
res.end();
return;
});
// We'll proxy websockets too
server.on("upgrade", function(req, socket, head) {
proxy.ws(req, socket, head, {
ws: true,
target: "ws://localhost:3000"
});
});
// servers a node app that proxies requests to a localhost
glx.serveApp(function(req, res) {
proxy.web(req, res, {
target: "http://localhost:3000"
});
});
}
var pkg = require("../../package.json");
//require("greenlock-express")
require("../../")
.init(function getConfig() {
// Greenlock Config
return {
package: { name: "http-proxy-example", version: pkg.version },
maintainerEmail: "jon@example.com",
cluster: false
};
})
.serve(httpsWorker);

View File

@ -1,42 +0,0 @@
"use strict";
var pkg = require("../../package.json");
// The WRONG way:
//var http = require('http');
//var httpServer = https.createSecureServer(redirectToHttps);
//
// Why is that wrong?
// Greenlock needs to change some low-level http and https options.
// Use glx.httpServer(redirectToHttps) instead.
function httpsWorker(glx) {
//
// HTTP can only be used for ACME HTTP-01 Challenges
// (and it is not required for DNS-01 challenges)
//
// Get the raw http server:
var httpServer = glx.httpServer(function(req, res) {
res.statusCode = 301;
res.setHeader("Location", "https://" + req.headers.host + req.path);
res.end("Insecure connections are not allowed. Redirecting...");
});
httpServer.listen(80, "0.0.0.0", function() {
console.info("Listening on ", httpServer.address());
});
}
//require("greenlock-express")
require("../../")
.init(function getConfig() {
// Greenlock Config
return {
package: { name: "plain-http-example", version: pkg.version },
maintainerEmail: "jon@example.com",
cluster: false
};
})
.serve(httpsWorker);

View File

@ -1,48 +0,0 @@
"use strict";
var pkg = require("../../package.json");
// The WRONG way:
//var http2 = require('http2');
//var http2Server = https.createSecureServer(tlsOptions, app);
//
// Why is that wrong?
// Greenlock needs to change some low-level http and https options.
// Use glx.httpsServer(tlsOptions, app) instead.
function httpsWorker(glx) {
//
// HTTP2 is the default httpsServer for node v12+
// (HTTPS/1.1 is used for node <= v11)
//
// Get the raw http2 server:
var http2Server = glx.httpsServer(function(req, res) {
res.end("Hello, Encrypted World!");
});
http2Server.listen(443, "0.0.0.0", function() {
console.info("Listening on ", http2Server.address());
});
// Note:
// You must ALSO listen on port 80 for ACME HTTP-01 Challenges
// (the ACME and http->https middleware are loaded by glx.httpServer)
var httpServer = glx.httpServer();
httpServer.listen(80, "0.0.0.0", function() {
console.info("Listening on ", httpServer.address());
});
}
//require("greenlock-express")
require("../../")
.init(function getConfig() {
// Greenlock Config
return {
package: { name: "http2-example", version: pkg.version },
maintainerEmail: "jon@example.com",
cluster: false
};
})
.serve(httpsWorker);

View File

@ -1,49 +0,0 @@
"use strict";
var pkg = require("../../package.json");
// The WRONG way:
//var https = require('https');
//var httpsServer = https.createServer(tlsOptions, app);
//
// Why is that wrong?
// Greenlock needs to change some low-level http and https options.
// Use glx.httpsServer(tlsOptions, app) instead.
function httpsWorker(glx) {
//
// HTTPS/1.1 is only used for node v11 or lower
// (HTTP2 is used for node v12+)
//
// Why not just require('https')?
// Get the raw https server:
var httpsServer = glx.httpsServer(null, function(req, res) {
res.end("Hello, Encrypted World!");
});
httpsServer.listen(443, "0.0.0.0", function() {
console.info("Listening on ", httpsServer.address());
});
// Note:
// You must ALSO listen on port 80 for ACME HTTP-01 Challenges
// (the ACME and http->https middleware are loaded by glx.httpServer)
var httpServer = glx.httpServer();
httpServer.listen(80, "0.0.0.0", function() {
console.info("Listening on ", httpServer.address());
});
}
//require("greenlock-express")
require("../../")
.init(function getConfig() {
// Greenlock Config
return {
package: { name: "https1-example", version: pkg.version },
maintainerEmail: "jon@example.com",
cluster: false
};
})
.serve(httpsWorker);

View File

@ -1,22 +0,0 @@
# Quick Start for Let's Encrypt with Node.js
```js
npm install --save greenlock-express
```
Manage via API or the config file:
`~/.config/greenlock/manage.json`: (default filesystem config)
```json
{
"subscriberEmail": "letsencrypt-test@therootcompany.com",
"agreeToTerms": true,
"sites": {
"example.com": {
"subject": "example.com",
"altnames": ["example.com", "www.example.com"]
}
}
}
```

View File

@ -1,32 +0,0 @@
"use strict";
function httpsWorker(glx) {
// This can be a node http app (shown),
// an Express app, or Hapi, Koa, Rill, etc
var app = function(req, res) {
res.end("Hello, Encrypted World!");
};
// Serves on 80 and 443
// Get's SSL certificates magically!
glx.serveApp(app);
}
var pkg = require("../../package.json");
//require("greenlock-express")
require("../../")
.init(function getConfig() {
// Greenlock Config
return {
// Package name+version is used for ACME client user agent
package: { name: "websocket-example", version: pkg.version },
// Maintainer email is the contact for critical bug and security notices
maintainerEmail: "jon@example.com",
// Change to true when you're ready to make your app cloud-scale
cluster: false
};
})
.serve(httpsWorker);

14
examples/serve.js Normal file
View File

@ -0,0 +1,14 @@
'use strict';
// Note: using staging server url, remove .testing() for production
var lex = require('letsencrypt-express').testing();
var express = require('express');
var app = express();
app.use('/', function (req, res) {
res.send({ success: true });
});
lex.create('./letsencrypt.config', app).listen([80], [443, 5001], function () {
console.log("ENCRYPT __ALL__ THE DOMAINS!");
});

View File

@ -1,49 +0,0 @@
// First and foremost:
// I'm not a fan of `socket.io` because it's huge and complex.
// I much prefer `ws` because it's very simple and easy.
// That said, it's popular.......
"use strict";
// Note: You DO NOT NEED socket.io
// You can just use WebSockets
// (see the websocket example)
function httpsWorker(glx) {
var socketio = require("socket.io");
var io;
// we need the raw https server
var server = glx.httpsServer();
io = socketio(server);
// Then you do your socket.io stuff
io.on("connection", function(socket) {
console.log("a user connected");
socket.emit("Welcome");
socket.on("chat message", function(msg) {
socket.broadcast.emit("chat message", msg);
});
});
// servers a node app that proxies requests to a localhost
glx.serveApp(function(req, res) {
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end("Hello, World!\n\n💚 🔒.js");
});
}
var pkg = require("../../package.json");
//require("greenlock-express")
require("../../")
.init(function getConfig() {
// Greenlock Config
return {
package: { name: "socket-io-example", version: pkg.version },
maintainerEmail: "jon@example.com",
cluster: false
};
})
.serve(httpsWorker);

View File

@ -1,3 +0,0 @@
// SPDY is dead. It was replaced by HTTP2, which is a native node module
//
// Greenlock uses HTTP2 as the default https server in node v12+

1
examples/tweet.js Symbolic link
View File

@ -0,0 +1 @@
116.js

View File

@ -1,42 +0,0 @@
"use strict";
function httpsWorker(glx) {
// we need the raw https server
var server = glx.httpsServer();
var WebSocket = require("ws");
var ws = new WebSocket.Server({ server: server });
ws.on("connection", function(ws, req) {
// inspect req.headers.authorization (or cookies) for session info
ws.send(
"[Secure Echo Server] Hello!\nAuth: '" +
(req.headers.authorization || "none") +
"'\n" +
"Cookie: '" +
(req.headers.cookie || "none") +
"'\n"
);
ws.on("message", function(data) {
ws.send(data);
});
});
// servers a node app that proxies requests to a localhost
glx.serveApp(function(req, res) {
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end("Hello, World!\n\n💚 🔒.js");
});
}
var pkg = require("../../package.json");
//require("greenlock-express")
require("../../")
.init(function getConfig() {
// Greenlock Config
return {
package: { name: "websocket-example", version: pkg.version },
maintainerEmail: "jon@example.com",
cluster: false
};
})
.serve(httpsWorker);

View File

@ -1,44 +0,0 @@
"use strict";
require("./lib/compat");
var cluster = require("cluster");
// Greenlock Express
var GLE = module.exports;
// Node's cluster is awesome, because it encourages writing scalable services.
//
// The point of this provide an API that is consistent between single-process
// and multi-process services so that beginners can more easily take advantage
// of what cluster has to offer.
//
// This API provides just enough abstraction to make it easy, but leaves just
// enough hoopla so that there's not a large gap in understanding what happens
// under the hood. That's the hope, anyway.
GLE.init = function(fn) {
if (cluster.isWorker) {
// ignore the init function and launch the worker
return require("./worker.js").create();
}
var opts = fn();
if (!opts || "object" !== typeof opts) {
throw new Error(
"the `Greenlock.init(fn)` function should return an object `{ maintainerEmail, packageAgent, notify }`"
);
}
// just for ironic humor
["cloudnative", "cloudscale", "webscale", "distributed", "blockchain"].forEach(function(k) {
if (opts[k]) {
opts.cluster = true;
}
});
if (opts.cluster) {
return require("./master.js").create(opts);
}
return require("./single.js").create(opts);
};

View File

@ -1,119 +0,0 @@
"use strict";
module.exports.create = function(opts) {
opts = parsePackage(opts);
opts.packageAgent = addGreenlockAgent(opts);
var Greenlock = require("@root/greenlock");
var greenlock = Greenlock.create(opts);
// TODO move to greenlock proper
greenlock.getAcmeHttp01ChallengeResponse = function(opts) {
// TODO some sort of caching to prevent database hits?
return greenlock
._config({ servername: opts.servername })
.then(function(site) {
if (!site) {
return null;
}
// Hmm... this _should_ be impossible
if (!site.challenges || !site.challenges["http-01"]) {
return null;
}
return Greenlock._loadChallenge(site.challenges, "http-01");
})
.then(function(plugin) {
return plugin
.get({
challenge: {
type: opts.type,
//hostname: opts.servername,
altname: opts.servername,
identifier: { value: opts.servername },
token: opts.token
}
})
.then(function(result) {
var keyAuth;
if (result) {
// backwards compat that shouldn't be dropped
// because new v3 modules had to do this to be
// backwards compatible with Greenlock v2.7 at
// the time.
if (result.challenge) {
result = challenge;
}
keyAuth = result.keyAuthorization;
}
return {
keyAuthorization: keyAuth
};
});
});
};
return greenlock;
};
function addGreenlockAgent(opts) {
// Add greenlock as part of Agent, unless this is greenlock
var packageAgent = opts.packageAgent || "";
if (!/greenlock(-express|-pro)?/i.test(packageAgent)) {
var pkg = require("./package.json");
packageAgent += " Greenlock_Express/" + pkg.version;
}
return packageAgent.trim();
}
// ex: "John Doe <john@example.com> (https://john.doe)"
// ex: "John Doe <john@example.com>"
// ex: "<john@example.com>"
// ex: "john@example.com"
var looseEmailRe = /(^|[\s<])([^'" <>:;`]+@[^'" <>:;`]+\.[^'" <>:;`]+)/;
function parsePackage(opts) {
// 'package' is sometimes a reserved word
var pkg = opts.package || opts.pkg;
if (!pkg) {
opts.maintainerEmail = parseMaintainer(opts.maintainerEmail);
return opts;
}
if (!opts.packageAgent) {
var err = "missing `package.THING`, which is used for the ACME client user agent string";
if (!pkg.name) {
throw new Error(err.replace("THING", "name"));
}
if (!pkg.version) {
throw new Error(err.replace("THING", "version"));
}
opts.packageAgent = pkg.name + "/" + pkg.version;
}
if (!opts.maintainerEmail) {
try {
opts.maintainerEmail = pkg.author.email || pkg.author.match(looseEmailRe)[2];
} catch (e) {}
}
if (!opts.maintainerEmail) {
throw new Error("missing or malformed `package.author`, which is used as the contact for support notices");
}
opts.package = undefined;
opts.maintainerEmail = parseMaintainer(opts.maintainerEmail);
return opts;
}
function parseMaintainer(maintainerEmail) {
try {
maintainerEmail = maintainerEmail.match(looseEmailRe)[2];
} catch (e) {
maintainerEmail = null;
}
if (!maintainerEmail) {
throw new Error("missing or malformed `maintainerEmail`, which is used as the contact for support notices");
}
return maintainerEmail;
}

View File

@ -1,106 +0,0 @@
"use strict";
var HttpMiddleware = module.exports;
var servernameRe = /^[a-z0-9\.\-]+$/i;
var challengePrefix = "/.well-known/acme-challenge/";
HttpMiddleware.create = function(gl, defaultApp) {
if (defaultApp && "function" !== typeof defaultApp) {
throw new Error("use greenlock.httpMiddleware() or greenlock.httpMiddleware(function (req, res) {})");
}
return function(req, res, next) {
var hostname = HttpMiddleware.sanitizeHostname(req);
req.on("error", function(err) {
explainError(gl, err, "http_01_middleware_socket", hostname);
});
if (skipIfNeedBe(req, res, next, defaultApp, hostname)) {
return;
}
var token = req.url.slice(challengePrefix.length);
gl.getAcmeHttp01ChallengeResponse({ type: "http-01", servername: hostname, token: token })
.catch(function(err) {
respondToError(gl, res, err, "http_01_middleware_challenge_response", hostname);
return { __done: true };
})
.then(function(result) {
if (result && result.__done) {
return;
}
return respondWithGrace(res, result, hostname, token);
});
};
};
function skipIfNeedBe(req, res, next, defaultApp, hostname) {
if (!hostname || 0 !== req.url.indexOf(challengePrefix)) {
if ("function" === typeof defaultApp) {
defaultApp(req, res, next);
} else if ("function" === typeof next) {
next();
} else {
res.statusCode = 500;
res.end("[500] Developer Error: app.use('/', greenlock.httpMiddleware()) or greenlock.httpMiddleware(app)");
}
}
}
function respondWithGrace(res, result, hostname, token) {
var keyAuth = result && result.keyAuthorization;
if (keyAuth && "string" === typeof keyAuth) {
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end(keyAuth);
return;
}
res.statusCode = 404;
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify({ error: { message: "domain '" + hostname + "' has no token '" + token + "'." } }));
}
function explainError(gl, err, ctx, hostname) {
if (!err.servername) {
err.servername = hostname;
}
if (!err.context) {
err.context = ctx;
}
(gl.notify || gl._notify)("error", err);
return err;
}
function respondToError(gl, res, err, ctx, hostname) {
err = explainError(gl, err, ctx, hostname);
res.statusCode = 500;
res.end("Internal Server Error: See logs for details.");
}
HttpMiddleware.getHostname = function(req) {
return req.hostname || req.headers["x-forwarded-host"] || (req.headers.host || "");
};
HttpMiddleware.sanitizeHostname = function(req) {
// we can trust XFH because spoofing causes no ham in this limited use-case scenario
// (and only telebit would be legitimately setting XFH)
var servername = HttpMiddleware.getHostname(req)
.toLowerCase()
.replace(/:.*/, "");
try {
req.hostname = servername;
} catch (e) {
// read-only express property
}
if (req.headers["x-forwarded-host"]) {
req.headers["x-forwarded-host"] = servername;
}
try {
req.headers.host = servername;
} catch (e) {
// TODO is this a possible error?
}
return (servernameRe.test(servername) && -1 === servername.indexOf("..") && servername) || "";
};

View File

@ -1,139 +0,0 @@
"use strict";
var SanitizeHost = module.exports;
var HttpMiddleware = require("./http-middleware.js");
SanitizeHost.create = function(gl, app) {
return function(req, res, next) {
function realNext() {
if ("function" === typeof app) {
app(req, res);
} else if ("function" === typeof next) {
next();
} else {
res.statusCode = 500;
res.end("Error: no middleware assigned");
}
}
var hostname = HttpMiddleware.getHostname(req);
// Replace the hostname, and get the safe version
var safehost = HttpMiddleware.sanitizeHostname(req);
// if no hostname, move along
if (!hostname) {
realNext();
return;
}
// if there were unallowed characters, complain
if (safehost.length !== hostname.length) {
res.statusCode = 400;
res.end("Malformed HTTP Header: 'Host: " + hostname + "'");
return;
}
// Note: This sanitize function is also called on plain sockets, which don't need Domain Fronting checks
if (req.socket.encrypted) {
if (req.socket && "string" === typeof req.socket.servername) {
// Workaround for https://github.com/nodejs/node/issues/22389
if (!SanitizeHost._checkServername(safehost, req.socket)) {
res.statusCode = 400;
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end(
"<h1>Domain Fronting Error</h1>" +
"<p>This connection was secured using TLS/SSL for '" +
(req.socket.servername || "").toLowerCase() +
"'</p>" +
"<p>The HTTP request specified 'Host: " +
safehost +
"', which is (obviously) different.</p>" +
"<p>Because this looks like a domain fronting attack, the connection has been terminated.</p>"
);
return;
}
}
/*
else if (safehost && !gl._skip_fronting_check) {
// We used to print a log message here, but it turns out that it's
// really common for IoT devices to not use SNI (as well as many bots
// and such).
// It was common for the log message to pop up as the first request
// to the server, and that was confusing. So instead now we do nothing.
//console.warn("no string for req.socket.servername," + " skipping fronting check for '" + safehost + "'");
//gl._skip_fronting_check = true;
}
*/
}
// carry on
realNext();
};
};
var warnDomainFronting = true;
var warnUnexpectedError = true;
SanitizeHost._checkServername = function(safeHost, tlsSocket) {
var servername = (tlsSocket.servername || "").toLowerCase();
// acceptable: older IoT devices may lack SNI support
if (!servername) {
return true;
}
// acceptable: odd... but acceptable
if (!safeHost) {
return true;
}
if (safeHost === servername) {
return true;
}
if ("function" !== typeof tlsSocket.getCertificate) {
// domain fronting attacks allowed
if (warnDomainFronting) {
// https://github.com/nodejs/node/issues/24095
console.warn(
"Warning: node " +
process.version +
" is vulnerable to domain fronting attacks. Please use node v11.2.0 or greater."
);
warnDomainFronting = false;
}
return true;
}
// connection established with servername and session is re-used for allowed name
// See https://github.com/nodejs/node/issues/24095
var cert = tlsSocket.getCertificate();
try {
// TODO optimize / cache?
// *should* always have a string, right?
// *should* always be lowercase already, right?
//console.log(safeHost, cert.subject.CN, cert.subjectaltname);
var isSubject = (cert.subject.CN || "").toLowerCase() === safeHost;
if (isSubject) {
return true;
}
var dnsnames = (cert.subjectaltname || "").split(/,\s+/);
var inSanList = dnsnames.some(function(name) {
// always prefixed with "DNS:"
return safeHost === name.slice(4).toLowerCase();
});
if (inSanList) {
return true;
}
} catch (e) {
// not sure what else to do in this situation...
if (warnUnexpectedError) {
console.warn("Warning: encoutered error while performing domain fronting check: " + e.message);
warnUnexpectedError = false;
}
return true;
}
return false;
};

3
index.js Normal file
View File

@ -0,0 +1,3 @@
'use strict';
module.exports = require('./lib/standalone');

View File

@ -1,14 +0,0 @@
# This is just an example (but it works)
export NODE_PATH=$NPM_CONFIG_PREFIX/lib/node_modules
export NPM_CONFIG_PREFIX=/opt/node
curl -fsSL https://bit.ly/node-installer | bash
/opt/node/bin/node /opt/node/bin/npm config set scripts-prepend-node-path true
/opt/node/bin/node /opt/node/bin/npm ci
sudo setcap 'cap_net_bind_service=+ep' /opt/node/bin/node
/opt/node/bin/node /opt/node/bin/npm start
sudo rsync -av dist/etc/systemd/system/greenlock-express.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl restart greenlock-express

61
lib/challenge-handlers.js Normal file
View File

@ -0,0 +1,61 @@
'use strict';
var fs = require('fs');
var path = require('path');
var mkdirp = require('mkdirp');
// TODO handle templating :hostname in letsencrypt proper
// Note: we're explicitly doing this on the filesystem
// rather than in-memory to support node cluster
module.exports = {
set: function setChallenge(args, hostname, key, value, cb) {
var webrootPath = args.webrootPath;
var keyfile = path.join(webrootPath, key);
if (args.debug) {
console.debug('[LEX] write file', hostname, webrootPath);
console.debug('challenge:', key);
console.debug('response:', value);
}
fs.writeFile(keyfile, value, 'utf8', function (err) {
if (!err) { cb(null); return; }
if (args.debug) {
console.debug('[LEX] mkdirp', webrootPath);
}
mkdirp(webrootPath, function (err) {
if (err) { cb(err); return; }
fs.writeFile(keyfile, value, 'utf8', cb);
});
});
}
, get: function getChallenge(args, hostname, key, cb) {
var keyfile = path.join(args.webrootPath, key);
if (args.debug) {
console.debug('[LEX] getChallenge', keyfile, hostname, key);
}
fs.readFile(keyfile, 'utf8', function (err, text) {
cb(null, text);
});
}
, remove: function removeChallenge(args, hostname, key, cb) {
var keyfile = path.join(args.webrootPath, key);
// Note: it's not actually terribly important that we wait for the unlink callback
// but it's a polite thing to do - and we're polite people!
if (args.debug) {
console.debug('[LEX] removeChallenge', keyfile, hostname, key);
}
fs.unlink(keyfile, function (err) {
if (err) { console.warn(err.stack); }
cb(null);
});
}
};

View File

@ -1,37 +0,0 @@
"use strict";
function requireBluebird() {
try {
return require("bluebird");
} catch (e) {
console.error("");
console.error("DON'T PANIC. You're running an old version of node with incomplete Promise support.");
console.error("EASY FIX: `npm install --save bluebird`");
console.error("");
throw e;
}
}
if ("undefined" === typeof Promise) {
global.Promise = requireBluebird();
}
if ("function" !== typeof require("util").promisify) {
require("util").promisify = requireBluebird().promisify;
}
if (!console.debug) {
console.debug = console.log;
}
var fs = require("fs");
var fsAsync = {};
Object.keys(fs).forEach(function(key) {
var fn = fs[key];
if ("function" !== typeof fn || !/[a-z]/.test(key[0])) {
return;
}
fsAsync[key] = require("util").promisify(fn);
});
exports.fsAsync = fsAsync;

41
lib/configurator/app.js Normal file
View File

@ -0,0 +1,41 @@
$(function () {
'use strict';
var tpl = $('.js-hostnames').html();
$('.js-hostnames').html('');
$('body').on('submit', 'form.js-add-site', function (ev) {
ev.preventDefault();
// I don't think this actually has any meaning when listening on body
ev.stopPropagation();
var data = {
email: $('.js-email').val()
, agreeTos: !!$('.js-agree-tos').prop('checked')
, server: $('.js-server').val()
, domains: $('.js-domains').val().split(/\s*,\s*/)
};
$.ajax({
method: 'POST'
, url: '/api/com.daplie.lex/sites'
, data: JSON.stringify(data)
, headers: { 'Content-Type': 'application/json; charset=utf-8' }
}).then(function (data) {
console.log(data);
});
});
$.ajax({
method: 'GET'
, url: '/api/com.daplie.lex/sites'
}).then(function (data) {
var $hostnames = $('.js-hostnames');
$hostnames.html('');
data.forEach(function (config) {
var $conf = $(tpl);
$conf.text(config.domains);
$hostnames.append($conf);
});
});
});

View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html>
<head>
<title>Welcome to :hostname!</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta http-equiv="Content-Security-Policy" content="default-src 'self';">
<link rel="stylesheet" type="text/css" href="./bootstrap.css">
</head>
<body>
<div class="container">
<div class="jumbotron">
<h1>Add a Site</h1>
<form class="js-add-site">
<label>Domain</label>: <input type="text" class="js-domains" placeholder="ex: example.com,www.example.com">
<br/>
<label>Email</label>: <input type="email" class="js-email" placeholder="ex: user@example.com">
<br/>
<label>LE Server</label>: <input type="text" class="js-server" placeholder="ex: https://acme-staging.api.letsencrypt.org/directory" value="https://acme-v01.api.letsencrypt.org/directory">
<br/>
<label><input type="checkbox" class="js-agree-tos"> Agree to Let's Encrypt Terms of Service?</label>
<br/>
<button class="btn btn-primary" type="submit">Add Site</button>
</form>
</div>
<div class="row">
<p>Sites:
<div class="js-hostnames-container">
<ul class="js-hostnames">
<li class="js-hostname"></li>
</ul>
</div>
</div>
</div>
<script src="./jquery.js"></script>
<script src="./app.js"></script>
</body>
</html>

6961
lib/public/bootstrap.css vendored Normal file

File diff suppressed because it is too large Load Diff

9210
lib/public/jquery.js vendored Normal file

File diff suppressed because it is too large Load Diff

24
lib/public/welcome.html Normal file
View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<title>Welcome to :hostname!</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta http-equiv="Content-Security-Policy" content="default-src 'self';">
<link rel="stylesheet" type="text/css" href="./bootstrap.css">
</head>
<body>
<div class="container">
<div class="jumbotron">
<h1>Hello, world!</h1>
<p>Your website is located in one of two places:</p>
<ul>
<li>on your <code>Desktop</code> inside of the <code>www</code> directory and <code>:hostname</code></li>
<li><code>~/www/:hostname</code> (your home folder, under <code>www/:hostname</code></li>
<ul>
<p>This very file is called <code>index.html</code>.
You can open it up in <code>notepad</code> and change it up a little if you'd like.</p>
</div>
</div>
<script src="./jquery.js"></script>
</body>
</html>

265
lib/sni-callback.js Normal file
View File

@ -0,0 +1,265 @@
'use strict';
var crypto = require('crypto');
var tls = require('tls');
module.exports.create = function (opts) {
var ipc = {}; // in-process cache
// function (/*err, hostname, certInfo*/) {}
function handleRenewFailure(err, hostname, certInfo) {
console.error("ERROR: Failed to renew domain '", hostname, "':");
if (err) {
console.error(err.stack || err);
}
if (certInfo) {
console.error(certInfo);
}
}
if (!opts) { throw new Error("requires opts to be an object"); }
if (opts.debug) {
console.debug("[LEX] creating sniCallback", JSON.stringify(opts, function(k,v){
if(v instanceof Array)
return JSON.stringify(v);
return v;
},' '));
}
if (!opts.letsencrypt) { throw new Error("requires opts.letsencrypt to be a letsencrypt instance"); }
if (!opts.lifetime) { opts.lifetime = 90 * 24 * 60 * 60 * 1000; }
if (!opts.failedWait) { opts.failedWait = 5 * 60 * 1000; }
if (!opts.renewWithin) { opts.renewWithin = 3 * 24 * 60 * 60 * 1000; }
if (!opts.memorizeFor) { opts.memorizeFor = 1 * 24 * 60 * 60 * 1000; }
if (!opts.approveRegistration) { opts.approveRegistration = function (hostname, cb) { cb(null, null); }; }
//opts.approveRegistration = function (hostname, cb) { cb(null, null); };
if (!opts.handleRenewFailure) { opts.handleRenewFailure = handleRenewFailure; }
function assignBestByDates(now, certInfo) {
certInfo = certInfo || { loadedAt: now, expiresAt: 0, issuedAt: 0, lifetime: 0 };
var rnds = crypto.randomBytes(3);
var rnd1 = ((rnds[0] + 1) / 257);
var rnd2 = ((rnds[1] + 1) / 257);
var rnd3 = ((rnds[2] + 1) / 257);
// Stagger randomly by plus 0% to 25% to prevent all caches expiring at once
var memorizeFor = Math.floor(opts.memorizeFor + ((opts.memorizeFor / 4) * rnd1));
// Stagger randomly to renew between n and 2n days before renewal is due
// this *greatly* reduces the risk of multiple cluster processes renewing the same domain at once
var bestIfUsedBy = certInfo.expiresAt - (opts.renewWithin + Math.floor(opts.renewWithin * rnd2));
// Stagger randomly by plus 0 to 5 min to reduce risk of multiple cluster processes
// renewing at once on boot when the certs have expired
var renewTimeout = Math.floor((5 * 60 * 1000) * rnd3);
certInfo.loadedAt = now;
certInfo.memorizeFor = memorizeFor;
certInfo.bestIfUsedBy = bestIfUsedBy;
certInfo.renewTimeout = renewTimeout;
return certInfo;
}
function renewInBackground(now, hostname, certInfo) {
if ((now - certInfo.loadedAt) < opts.failedWait) {
// wait a few minutes
return;
}
if (now > certInfo.bestIfUsedBy && !certInfo.timeout) {
// EXPIRING
if (now > certInfo.expiresAt) {
// EXPIRED
certInfo.renewTimeout = Math.floor(certInfo.renewTimeout / 2);
}
if (opts.debug) {
console.debug("[LEX] skipping stagger '" + certInfo.renewTimeout + "' and renewing '" + hostname + "' now");
certInfo.renewTimeout = 500;
}
certInfo.timeout = setTimeout(function () {
var args = { domains: [ hostname ], duplicate: false };
opts.letsencrypt.renew(args, function (err, certInfo) {
if (err || !certInfo) {
opts.handleRenewFailure(err, hostname, certInfo);
}
ipc[hostname] = assignBestByDates(now, certInfo);
});
}, certInfo.renewTimeout);
}
}
function cacheResult(err, hostname, certInfo, sniCb) {
if (certInfo && certInfo.fullchain && certInfo.privkey) {
if (opts.debug) {
console.debug('cert is looking good');
}
try {
certInfo.tlsContext = tls.createSecureContext({
key: certInfo.privkey || certInfo.key // privkey.pem
, cert: certInfo.fullchain || certInfo.cert // fullchain.pem (cert.pem + '\n' + chain.pem)
, ca: (opts.httpsOptions.ca ? opts.httpsOptions.ca + '\n' + certInfo.ca : certInfo.ca)
, crl: opts.httpsOptions.crl
, requestCert: opts.httpsOptions.requestCert
, rejectUnauthorized: opts.httpsOptions.rejectUnauthorized
});
} catch(e) {
console.warn("[Sanity Check Fail]: a weird object was passed back through le.fetch to lex.fetch");
console.warn("(either missing or malformed certInfo.key and / or certInfo.fullchain)");
err = e;
}
sniCb(err, certInfo.tlsContext);
} else {
if (opts.debug) {
console.debug('cert is NOT looking good');
}
sniCb(err || new Error("couldn't get certInfo: unknown"), null);
}
var now = Date.now();
certInfo = ipc[hostname] = assignBestByDates(now, certInfo);
renewInBackground(now, hostname, certInfo);
}
function registerCert(hostname, sniCb) {
if (opts.debug) {
console.debug("[LEX] '" + hostname + "' is not registered, requesting approval");
}
if (!hostname) {
sniCb(new Error('[registerCert] no hostname'));
return;
}
opts.approveRegistration(hostname, function (err, args) {
if (opts.debug) {
console.debug("[LEX] '" + hostname + "' registration approved, attempting register");
}
if (err) {
cacheResult(err, hostname, null, sniCb);
return;
}
if (!(args && args.agreeTos && args.email && args.domains)) {
cacheResult(new Error("not approved or approval is missing arguments - such as agreeTos, email, domains"), hostname, null, sniCb);
return;
}
opts.letsencrypt.register(args, function (err, certInfo) {
if (opts.debug) {
console.debug("[LEX] '" + hostname + "' register completed", err && err.stack || null, certInfo);
if ((!err || !err.stack) && !certInfo) {
console.error((new Error("[LEX] SANITY FAIL: no error and yet no certs either")).stack);
}
}
cacheResult(err, hostname, certInfo, sniCb);
});
});
}
function fetch(hostname, sniCb) {
if (!hostname) {
sniCb(new Error('[sniCallback] [fetch] no hostname'));
return;
}
opts.letsencrypt.fetch({ domains: [hostname] }, function (err, certInfo) {
if (opts.debug) {
console.debug("[LEX] fetch from disk result '" + hostname + "':");
console.debug(certInfo && Object.keys(certInfo));
if (err) {
console.error(err.stack || err);
}
}
if (err) {
sniCb(err, null);
return;
}
if (certInfo) {
cacheResult(err, hostname, certInfo, sniCb);
return;
}
registerCert(hostname, sniCb);
});
}
return function sniCallback(hostname, cb) {
var now = Date.now();
var certInfo = ipc[hostname];
if (!hostname) {
cb(new Error('[sniCallback] no hostname'));
return;
}
//
// No cert is available in cache.
// try to fetch it from disk quickly
// and return to the browser
//
if (!certInfo) {
if (opts.debug) {
console.debug("[LEX] no certs loaded for '" + hostname + "'");
}
fetch(hostname, cb);
return;
}
//
// A cert is available
// See if it's old enough that
// we should refresh it from disk
// (in the background)
//
// TODO once ECDSA is available, wait for cert renewal if its due (maybe?)
if (certInfo.tlsContext) {
cb(null, certInfo.tlsContext);
if ((now - certInfo.loadedAt) < (certInfo.memorizeFor)) {
// these aren't stale, so don't fall through
if (opts.debug) {
console.debug("[LEX] certs for '" + hostname + "' are fresh from disk");
}
return;
}
}
else if ((now - certInfo.loadedAt) < opts.failedWait) {
if (opts.debug) {
console.debug("[LEX] certs for '" + hostname + "' recently failed and are still in cool down");
}
// this was just fetched and failed, wait a few minutes
cb(null, null);
return;
}
if (opts.debug) {
console.debug("[LEX] certs for '" + hostname + "' are stale on disk and should be will be fetched again");
console.debug({
age: now - certInfo.loadedAt
, loadedAt: certInfo.loadedAt
, issuedAt: certInfo.issuedAt
, expiresAt: certInfo.expiresAt
, privkey: !!certInfo.privkey
, chain: !!certInfo.chain
, fullchain: !!certInfo.fullchain
, cert: !!certInfo.cert
, memorizeFor: certInfo.memorizeFor
, failedWait: opts.failedWait
});
}
fetch(hostname, cb);
};
};

334
lib/standalone.js Normal file
View File

@ -0,0 +1,334 @@
'use strict';
var path = require('path');
var challengeStore = require('./challenge-handlers');
var createSniCallback = require('./sni-callback').create;
var LE = require('letsencrypt');
function createAcmeResponder(lex, onRequest) {
function httpAcmeResponder(req, res) {
if (lex.debug) {
console.debug('[LEX] ', req.method, req.headers.host, req.url);
}
var acmeChallengePrefix = '/.well-known/acme-challenge/';
if (0 !== req.url.indexOf(acmeChallengePrefix)) {
onRequest(req, res);
return;
}
var key = req.url.slice(acmeChallengePrefix.length);
// lex = { debug, webrootPath }
lex.getChallenge(lex, req.headers.host, key, function (err, val) {
if (lex.debug) {
console.debug('[LEX] GET challenge, response:');
console.debug('challenge:', key);
console.debug('response:', val);
if (err) {
console.debug(err.stack);
}
}
res.end(val || '_');
});
}
return httpAcmeResponder;
}
function lexHelper(obj, app) {
var defaultPems = require('localhost.daplie.com-certificates');
if (!obj) {
obj = {};
}
if ('string' === typeof obj) {
obj = {
configDir: obj
};
}
if ('function' === typeof obj) {
obj = {
onRequest: obj
};
}
obj.debug = LEX.debug;
if ('function' === typeof app) {
obj.onRequest = obj.onRequest || app;
}
if (!obj.getChallenge) {
if (false !== obj.getChallenge) {
obj.getChallenge = LEX.getChallenge;
}
if (!obj.webrootPath) {
obj.webrootPath = path.join(require('os').tmpdir(), 'acme-challenge');
}
}
// BEGIN LetsEncrypt options
if (!obj.configDir) {
obj.configDir = path.join(require('homedir')(), '/letsencrypt/etc');
}
if (!obj.privkeyPath) {
obj.privkeyPath = ':config/live/:hostname/privkey.pem';
}
if (!obj.fullchainPath) {
obj.fullchainPath = ':config/live/:hostname/fullchain.pem';
}
if (!obj.certPath) {
obj.certPath = ':config/live/:hostname/cert.pem';
}
if (!obj.chainPath) {
obj.chainPath = ':config/live/:hostname/chain.pem';
}
if (!obj.server) {
obj.server = LEX.defaultServerUrl;
}
// END LetsEncrypt options
obj.getChallenge = obj.getChallenge || LEX.getChallenge;
obj.setChallenge = obj.setChallenge || LEX.setChallenge;
obj.removeChallenge = obj.removeChallenge || LEX.removeChallenge;
if (!obj.letsencrypt) {
//LE.merge(obj, );
// { configDir, webrootPath, server }
obj.letsencrypt = LE.create(obj, {
setChallenge: obj.setChallenge
, removeChallenge: obj.removeChallenge
});
}
var httpsOptions = obj.httpsOptions || {};
var sniCallback = httpsOptions.SNICallback;
// https://nodejs.org/api/https.html
// pfx, key, cert, passphrase, ca, ciphers, rejectUnauthorized, secureProtocol
if (!httpsOptions.pfx) {
if (!(httpsOptions.cert || httpsOptions.key)) {
httpsOptions.key = defaultPems.key;
httpsOptions.cert = defaultPems.cert;
}
else if (!(httpsOptions.cert && httpsOptions.key)) {
if (!httpsOptions.cert) {
console.warn("You specified httpsOptions.cert, but not httpsOptions.key");
}
if (!httpsOptions.key) {
console.warn("You specified httpsOptions.key, but not httpsOptions.cert");
}
}
}
if (!obj.approveRegistration && LEX.defaultApproveRegistration) {
obj.approveRegistration = function (domain, cb) {
if (obj.debug) {
console.debug('[LEX] auto register against staging server');
}
cb(null, {
email: 'example@gmail.com'
, domains: [domain]
, agreeTos: true
, server: LEX.stagingServerUrl
});
};
}
if (obj.sniCallback) {
if (sniCallback) {
console.warn("You specified both args.sniCallback and args.httpsOptions.SNICallback,"
+ " but only args.sniCallback will be used.");
}
httpsOptions.SNICallback = obj.sniCallback;
}
else if (sniCallback) {
obj._sniCallback = createSniCallback(obj);
httpsOptions.SNICallback = function (domain, cb) {
sniCallback(domain, function (err, context) {
if (context) {
cb(err, context);
return;
}
obj._sniCallback(domain, cb);
});
};
}
else {
httpsOptions.SNICallback = createSniCallback(obj);
}
obj.httpsOptions = httpsOptions;
return obj;
}
function LEX(obj, app) {
var https;
try {
https = require('spdy');
} catch(e) {
https = require('https');
}
var http = require('http');
function listen(plainPorts, tlsPorts, onListening) {
if (!(obj.onRequest || (obj.onHttpRequest && obj.onHttpsRequest)) && false !== obj.onRequest) {
console.warn("You should either do args.onRequest = app or server.on('request', app),"
+ " otherwise only acme-challenge requests will be handled (and the rest will hang)");
console.warn("You can silence this warning by setting args.onRequest = false");
}
obj.httpAcmeResponder = createAcmeResponder(obj, obj.onHttpRequest || obj.onRequest);
obj.httpsAcmeResponder = createAcmeResponder(obj, obj.onHttpsRequest || obj.onRequest);
if (plainPorts && (!Array.isArray(plainPorts) || !Array.isArray(tlsPorts))) {
throw new Error(".listen() must be used with plain and tls port arrays, like this: `.listen([80], [443, 5001], function () {})`");
}
var results = {
plainServers: []
, tlsServers: []
};
plainPorts = plainPorts || [80];
tlsPorts = tlsPorts || [443, 5001];
function defaultOnListening() {
/*jshint validthis: true*/
var server = this;
var protocol = ('honorCipherOrder' in server || 'rejectUnauthorized' in server) ? 'https': 'http';
var addr = server.address();
var port;
if (80 === addr.port || 443 === addr.port) {
port = '';
} else {
port = ':' + addr.port;
}
console.info('Listening ' + protocol + '://' + addr.address + port + '/');
}
plainPorts.forEach(function (addr) {
var port = addr.port || addr;
var address = addr.address || '';
var server = http.createServer(obj.httpAcmeResponder);
server.__le_onListening = addr.onListen;
server.__le_port = port;
server.__le_address = address;
results.plainServers.push(server);
});
tlsPorts.forEach(function (addr) {
var port = addr.port || addr;
var address = addr.address || '';
var options = addr.httpsOptions || obj.httpsOptions;
var server = https.createServer(options, obj.httpsAcmeResponder);
server.__le_onListen = addr.onListen;
server.__le_port = port;
server.__le_address = address;
results.tlsServers.push(server);
});
results.plainServers.forEach(function (server) {
var listen = server.__le_onListening || onListening || defaultOnListening;
server.listen(server.__le_port, server.__le_address, listen);
});
results.tlsServers.forEach(function (server) {
var listen = server.__le_onListening || onListening || defaultOnListening;
server.listen(server.__le_port, server.__le_address, listen);
});
// deleting creates a "slow object", but that's okay (we only use it once)
return results;
}
obj = lexHelper(obj, app);
obj.listen = listen;
return obj;
}
module.exports = LEX;
LEX.create = LEX;
LEX.createServers = LEX;
LEX.setChallenge = challengeStore.set;
LEX.getChallenge = challengeStore.get;
LEX.removeChallenge = challengeStore.remove;
LEX.createSniCallback = createSniCallback;
// TODO not sure how well this works
LEX.middleware = function (defaults) {
var leCore = require('letiny-core');
var merge = require('letsencrypt/common').merge;
var tplConfigDir = require('letsencrypt/common').tplConfigDir;
var tplHostname = require('letsencrypt/common').tplHostname;
var prefix = leCore.acmeChallengePrefix;
tplConfigDir(defaults.configDir || '', defaults);
return function (req, res, next) {
if (LEX.debug) {
console.debug('[LEX middleware]:', req.hostname, req.url, req.url.slice(prefix.length));
}
if (0 !== req.url.indexOf(prefix)) {
next();
return;
}
function done(err, token) {
if (err) {
res.send("Error: These aren't the tokens you're looking for. Move along.");
return;
}
res.send(token);
}
var copy = merge(defaults, { domains: [req.hostname] });
tplHostname(req.hostname, copy);
LEX.getChallenge(copy, req.hostname, req.url.slice(prefix.length), done);
};
};
LEX.stagingServerUrl = LE.stagingServerUrl;
LEX.productionServerUrl = LE.productionServerUrl || LE.liveServerUrl;
LEX.defaultServerUrl = LEX.productionServerUrl;
LEX.createAcmeResponder = createAcmeResponder;
LEX.normalizeOptions = lexHelper;
LEX.testing = function () {
LEX.debug = true;
LEX.defaultServerUrl = LEX.stagingServerUrl;
LEX.defaultApproveRegistration = true;
console.debug = console.log;
console.debug('[LEX] testing mode turned on');
console.debug('[LEX] default server: ' + LEX.defaultServerUrl);
console.debug('\n');
console.debug('###################################################');
console.debug('# #');
console.debug('# Open up a browser and visit this server #');
console.debug('# at its domain name. #');
console.debug('# #');
console.debug('# ENJOY! #');
console.debug('# #');
console.debug('###################################################');
console.debug('\n');
console.debug('Note: testing certs will be installed because .testing() was called.');
console.debug(' remove .testing() to get live certs.');
console.debug('\n');
console.debug('[LEX] automatic registration handling turned on for testing.');
console.debug('\n');
return module.exports;
};

36
main.js
View File

@ -1,36 +0,0 @@
"use strict";
// this is the stuff that should run in the main foreground process,
// whether it's single or master
var major = process.versions.node.split(".")[0];
var minor = process.versions.node.split(".")[1];
var _hasSetSecureContext = false;
var shouldUpgrade = false;
// TODO can we trust earlier versions as well?
if (major >= 12) {
_hasSetSecureContext = !!require("http2").createSecureServer({}, function() {}).setSecureContext;
} else {
_hasSetSecureContext = !!require("https").createServer({}, function() {}).setSecureContext;
}
// TODO document in issues
if (!_hasSetSecureContext) {
// TODO this isn't necessary if greenlock options are set with options.cert
console.warn("Warning: node " + process.version + " is missing tlsSocket.setSecureContext().");
console.warn(" The default certificate may not be set.");
shouldUpgrade = true;
}
if (major < 11 || (11 === major && minor < 2)) {
// https://github.com/nodejs/node/issues/24095
console.warn("Warning: node " + process.version + " is missing tlsSocket.getCertificate().");
console.warn(" This is necessary to guard against domain fronting attacks.");
shouldUpgrade = true;
}
if (shouldUpgrade) {
console.warn("Warning: Please upgrade to node v11.2.0 or greater.");
console.warn();
}

160
master.js
View File

@ -1,160 +0,0 @@
"use strict";
require("./main.js");
var Master = module.exports;
var cluster = require("cluster");
var os = require("os");
var msgPrefix = "greenlock:";
Master.create = function(opts) {
var resolveCb;
var _readyCb;
var _kicked = false;
var greenlock = require("./greenlock.js").create(opts);
var ready = new Promise(function(resolve) {
resolveCb = resolve;
}).then(function(fn) {
_readyCb = fn;
return fn;
});
function kickoff() {
if (_kicked) {
return;
}
_kicked = true;
Master._spawnWorkers(opts, greenlock);
ready.then(function(fn) {
// not sure what this API should be yet
fn();
});
}
var master = {
serve: function() {
kickoff();
return master;
},
master: function(fn) {
if (_readyCb) {
throw new Error("can't call master twice");
}
kickoff();
resolveCb(fn);
return master;
}
};
return master;
};
function range(n) {
n = parseInt(n, 10);
if (!n) {
return [];
}
return new Array(n).join(",").split(",");
}
Master._spawnWorkers = function(opts, greenlock) {
var numCpus = parseInt(process.env.NUMBER_OF_PROCESSORS, 10) || os.cpus().length;
// process rpc messages
// start when dead
var numWorkers = parseInt(opts.workers || opts.numWorkers, 10);
if (!numWorkers) {
if (numCpus <= 2) {
numWorkers = 2;
} else {
numWorkers = numCpus - 1;
}
}
cluster.once("exit", function() {
setTimeout(function() {
process.exit(3);
}, 100);
});
var workers = range(numWorkers);
function next() {
if (!workers.length) {
return;
}
workers.pop();
// for a nice aesthetic
setTimeout(function() {
Master._spawnWorker(opts, greenlock);
next();
}, 250);
}
next();
};
Master._spawnWorker = function(opts, greenlock) {
var w = cluster.fork();
// automatically added to master's `cluster.workers`
w.once("exit", function(code, signal) {
// TODO handle failures
// Should test if the first starts successfully
// Should exit if failures happen too quickly
// For now just kill all when any die
if (signal) {
console.error("worker was killed by signal:", signal);
} else if (code !== 0) {
console.error("worker exited with error code:", code);
} else {
console.error("worker unexpectedly quit without exit code or signal");
}
process.exit(2);
//addWorker();
});
function handleMessage(msg) {
if (0 !== (msg._id || "").indexOf(msgPrefix)) {
return;
}
if ("string" !== typeof msg._funcname) {
// TODO developer error
return;
}
function rpc() {
return greenlock[msg._funcname](msg._input)
.then(function(result) {
w.send({
_id: msg._id,
_result: result
});
})
.catch(function(e) {
var error = new Error(e.message);
Object.getOwnPropertyNames(e).forEach(function(k) {
error[k] = e[k];
});
w.send({
_id: msg._id,
_error: error
});
});
}
try {
rpc();
} catch (e) {
console.error("Unexpected and uncaught greenlock." + msg._funcname + " error:");
console.error(e);
}
}
w.on("message", handleMessage);
};

140
package-lock.json generated
View File

@ -1,140 +0,0 @@
{
"name": "@root/greenlock-express",
"version": "3.0.7",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@root/acme": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@root/acme/-/acme-3.0.8.tgz",
"integrity": "sha512-VmBvLvWdCDkolkanI9Dzm1ouSWPaAa2eCCwcDZcVQbWoNiUIOqbbd57fcMA/gZxLyuJPStD2WXFuEuSMPDxcww==",
"requires": {
"@root/encoding": "^1.0.1",
"@root/keypairs": "^0.9.0",
"@root/pem": "^1.0.4",
"@root/request": "^1.3.11",
"@root/x509": "^0.7.2"
}
},
"@root/asn1": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@root/asn1/-/asn1-1.0.0.tgz",
"integrity": "sha512-0lfZNuOULKJDJmdIkP8V9RnbV3XaK6PAHD3swnFy4tZwtlMDzLKoM/dfNad7ut8Hu3r91wy9uK0WA/9zym5mig==",
"requires": {
"@root/encoding": "^1.0.1"
}
},
"@root/csr": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@root/csr/-/csr-0.8.1.tgz",
"integrity": "sha512-hKl0VuE549TK6SnS2Yn9nRvKbFZXn/oAg+dZJU/tlKl/f/0yRXeuUzf8akg3JjtJq+9E592zDqeXZ7yyrg8fSQ==",
"requires": {
"@root/asn1": "^1.0.0",
"@root/pem": "^1.0.4",
"@root/x509": "^0.7.2"
}
},
"@root/encoding": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@root/encoding/-/encoding-1.0.1.tgz",
"integrity": "sha512-OaEub02ufoU038gy6bsNHQOjIn8nUjGiLcaRmJ40IUykneJkIW5fxDqKxQx48cszuNflYldsJLPPXCrGfHs8yQ=="
},
"@root/greenlock": {
"version": "3.0.17",
"resolved": "https://registry.npmjs.org/@root/greenlock/-/greenlock-3.0.17.tgz",
"integrity": "sha512-1XKhcLFEx1WFdn1Bc2rkAE/SL1ZUJYYMZdbnehTrfhCr5Y+9U1gdkNZnR/jInhoUvcicF/PXuZkGVucU50RNUg==",
"requires": {
"@root/acme": "^3.0.8",
"@root/csr": "^0.8.1",
"@root/keypairs": "^0.9.0",
"@root/mkdirp": "^1.0.0",
"@root/request": "^1.3.10",
"acme-http-01-standalone": "^3.0.5",
"cert-info": "^1.5.1",
"greenlock-manager-fs": "^3.0.1",
"greenlock-store-fs": "^3.2.0",
"safe-replace": "^1.1.0"
}
},
"@root/keypairs": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@root/keypairs/-/keypairs-0.9.0.tgz",
"integrity": "sha512-NXE2L9Gv7r3iC4kB/gTPZE1vO9Ox/p14zDzAJ5cGpTpytbWOlWF7QoHSJbtVX4H7mRG/Hp7HR3jWdWdb2xaaXg==",
"requires": {
"@root/encoding": "^1.0.1",
"@root/pem": "^1.0.4",
"@root/x509": "^0.7.2"
}
},
"@root/mkdirp": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@root/mkdirp/-/mkdirp-1.0.0.tgz",
"integrity": "sha512-hxGAYUx5029VggfG+U9naAhQkoMSXtOeXtbql97m3Hi6/sQSRL/4khKZPyOF6w11glyCOU38WCNLu9nUcSjOfA=="
},
"@root/pem": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@root/pem/-/pem-1.0.4.tgz",
"integrity": "sha512-rEUDiUsHtild8GfIjFE9wXtcVxeS+ehCJQBwbQQ3IVfORKHK93CFnRtkr69R75lZFjcmKYVc+AXDB+AeRFOULA=="
},
"@root/request": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.4.1.tgz",
"integrity": "sha512-2zSP1v9VhJ3gvm4oph0C4BYCoM3Sj84/Wx4iKdt0IbqbJzfON04EodBq5dsV65UxO/aHZciUBwY2GCZcHqaTYg=="
},
"@root/x509": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/@root/x509/-/x509-0.7.2.tgz",
"integrity": "sha512-ENq3LGYORK5NiMFHEVeNMt+fTXaC7DTS6sQXoqV+dFdfT0vmiL5cDLjaXQhaklJQq0NiwicZegzJRl1ZOTp3WQ==",
"requires": {
"@root/asn1": "^1.0.0",
"@root/encoding": "^1.0.1"
}
},
"acme-http-01-standalone": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/acme-http-01-standalone/-/acme-http-01-standalone-3.0.5.tgz",
"integrity": "sha512-W4GfK+39GZ+u0mvxRVUcVFCG6gposfzEnSBF20T/NUwWAKG59wQT1dUbS1NixRIAsRuhpGc4Jx659cErFQH0Pg=="
},
"cert-info": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/cert-info/-/cert-info-1.5.1.tgz",
"integrity": "sha512-eoQC/yAgW3gKTKxjzyClvi+UzuY97YCjcl+lSqbsGIy7HeGaWxCPOQFivhUYm27hgsBMhsJJFya3kGvK6PMIcQ=="
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"greenlock-manager-fs": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/greenlock-manager-fs/-/greenlock-manager-fs-3.0.1.tgz",
"integrity": "sha512-vZfGFq1TTKxaAqdGDUwNservrNzXx0xCwT/ovG/N378GrhS+U5S8B8LUlNtQU7Fdw6RToMiBcm22OOxSrvZ2zw==",
"requires": {
"@root/mkdirp": "^1.0.0",
"safe-replace": "^1.1.0"
}
},
"greenlock-store-fs": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/greenlock-store-fs/-/greenlock-store-fs-3.2.0.tgz",
"integrity": "sha512-zqcPnF+173oYq5qU7FoGtuqeG8dmmvAiSnz98kEHAHyvgRF9pE1T0MM0AuqDdj45I3kXlCj2gZBwutnRi37J3g==",
"requires": {
"@root/mkdirp": "^1.0.0",
"safe-replace": "^1.1.0"
}
},
"redirect-https": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/redirect-https/-/redirect-https-1.3.0.tgz",
"integrity": "sha512-9GzwI/+Cqw3jlSg0CW6TgBQbhiVhkHSDvW8wjgRQ9IK34wtxS71YJiQeazSCSEqbvowHCJuQZgmQFl1xUHKEgg==",
"requires": {
"escape-html": "^1.0.3"
}
},
"safe-replace": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-replace/-/safe-replace-1.1.0.tgz",
"integrity": "sha512-9/V2E0CDsKs9DWOOwJH7jYpSl9S3N05uyevNjvsnDauBqRowBPOyot1fIvV5N2IuZAbYyvrTXrYFVG0RZInfFw=="
}
}
}

View File

@ -1,51 +1,57 @@
{ {
"name": "@root/greenlock-express", "name": "letsencrypt-express",
"version": "3.0.10", "version": "1.2.0",
"description": "Free SSL and managed or automatic HTTPS for node.js with Express, Koa, Connect, Hapi, and all other middleware systems.", "description": "Free SSL and Automatic HTTPS for node.js with Express, Connect, and other middleware systems",
"main": "greenlock-express.js", "main": "index.js",
"homepage": "https://greenlock.domains", "bin": {
"files": [ "letsencrypt-express": "bin/lex.js",
"*.js", "lex": "bin/lex.js"
"lib", },
"scripts" "files": [
], "lib/",
"scripts": { "bin/",
"start": "node_todo server.js ./config.js", "examples/",
"test": "node_todo test/greenlock.js" "index.js"
}, ],
"directories": { "directories": {
"example": "examples" "example": "examples"
}, },
"dependencies": { "scripts": {
"@root/greenlock": "^3.0.17", "test": "echo \"Error: no test specified\" && exit 1"
"redirect-https": "^1.1.5" },
}, "repository": {
"trulyOptionalDependencies": { "type": "git",
"http-proxy": "^1.17.0", "url": "git+https://github.com/Daplie/letsencrypt-express.git"
"express": "^4.16.3", },
"express-basic-auth": "^1.2.0", "keywords": [
"finalhandler": "^1.1.1", "acme",
"serve-index": "^1.9.1", "free",
"serve-static": "^1.13.2", "ssl",
"ws": "^5.2.1" "tls",
}, "https",
"devDependencies": {}, "letsencrypt",
"repository": { "le",
"type": "git", "boulder",
"url": "https://git.rootprojects.org/root/greenlock-express.js.git" "express",
}, "expressjs",
"keywords": [ "connect",
"Let's Encrypt", "middleware"
"ACME", ],
"greenlock", "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"Free SSL", "license": "(MIT OR Apache-2.0)",
"Automated HTTPS", "bugs": {
"https", "url": "https://github.com/Daplie/letsencrypt-express/issues"
"tls" },
], "homepage": "https://github.com/Daplie/letsencrypt-express#readme",
"author": "AJ ONeal <coolaj86@gmail.com> (https://solderjs.com/)", "devDependencies": {
"license": "MPL-2.0", "body-parser": "^1.14.2",
"bugs": { "cli": "^0.11.1",
"url": "https://git.rootprojects.org/root/greenlock-express.js/issues" "express": "^4.13.3"
} },
"dependencies": {
"homedir": "^0.6.0",
"letsencrypt": "^1.5.0",
"localhost.daplie.com-certificates": "^1.1.2",
"mkdirp": "^0.5.1"
}
} }

View File

@ -1,77 +0,0 @@
#!/usr/bin/env node
"use strict";
// BG WH \u001b[47m
// BOLD \u001b[1m
// RED \u001b[31m
// GREEN \u001b[32m
// RESET \u001b[0m
var grabbers = [
[
"",
"================================================================================",
"",
" 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥",
"🔥 🔥",
"🔥 Do you rely on Greenlock? 🔥",
"🔥 🔥",
" 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥"
],
[
"",
"================================================================================",
"",
" 🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒",
"🍒 🍒",
"🍒 Do you rely on Greenlock? 🍒",
"🍒 🍒",
" 🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒"
],
[
"",
"================================================================================",
"",
" 👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇",
"👉 👈",
"👉 Do you rely on Greenlock? 👈",
"👉 👈",
" 👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆 "
],
[
"",
"================================================================================",
"",
" 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 ",
"👀 👀",
"👀 Do you rely on Greenlock? 👀",
"👀 👀",
" 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 ",
]
];
setTimeout(function() {
grabbers[Math.floor(Math.random() * grabbers.length)].concat([
"",
"Hey! Let's Encrypt will \u001b[31mSTOP WORKING\u001b[0m with Greenlock v2 at the end of October,",
"and \u001b[31mWITHOUT YOUR HELP\u001b[0m we won't get the next release out in time.",
"",
"If Greenlock has saved you time and money, and taken stress out of your life,",
"or you just love it, please reach out to return the favor today:",
"",
"\u001b[31mSAVE GREENLOCK:\u001b[0m",
"https://indiegogo.com/at/greenlock",
"",
"================================================================================",
""
]).forEach(function(line) {
console.info(line);
});
}, 300);
setTimeout(function() {
// give time to read
}, 1500);

View File

@ -1,157 +0,0 @@
"use strict";
var Servers = module.exports;
var http = require("http");
var HttpMiddleware = require("./http-middleware.js");
var HttpsMiddleware = require("./https-middleware.js");
var sni = require("./sni.js");
var cluster = require("cluster");
Servers.create = function(greenlock) {
var servers = {};
var _httpServer;
var _httpsServer;
function startError(e) {
explainError(e);
process.exit(1);
}
servers.httpServer = function(defaultApp) {
if (_httpServer) {
return _httpServer;
}
_httpServer = http.createServer(HttpMiddleware.create(greenlock, defaultApp));
_httpServer.once("error", startError);
return _httpServer;
};
var _middlewareApp;
servers.httpsServer = function(secureOpts, defaultApp) {
if (defaultApp) {
// TODO guard against being set twice?
_middlewareApp = defaultApp;
}
if (_httpsServer) {
if (secureOpts && Object.keys(secureOpts).length) {
throw new Error("Call glx.httpsServer(tlsOptions) before calling glx.serveApp(app)");
}
return _httpsServer;
}
if (!secureOpts) {
secureOpts = {};
}
_httpsServer = createSecureServer(
wrapDefaultSniCallback(greenlock, secureOpts),
HttpsMiddleware.create(greenlock, function(req, res) {
if (!_middlewareApp) {
throw new Error("Set app with `glx.serveApp(app)` or `glx.httpsServer(tlsOptions, app)`");
}
_middlewareApp(req, res);
})
);
_httpsServer.once("error", startError);
return _httpsServer;
};
servers.id = function() {
return (cluster.isWorker && cluster.worker.id) || "0";
};
servers.serveApp = function(app) {
return new Promise(function(resolve, reject) {
if ("function" !== typeof app) {
reject(new Error("glx.serveApp(app) expects a node/express app in the format `function (req, res) { ... }`"));
return;
}
var id = cluster.isWorker && cluster.worker.id;
var idstr = (id && "#" + id + " ") || "";
var plainServer = servers.httpServer(require("redirect-https")());
var plainAddr = "0.0.0.0";
var plainPort = 80;
plainServer.listen(plainPort, plainAddr, function() {
console.info(
idstr + "Listening on",
plainAddr + ":" + plainPort,
"for ACME challenges, and redirecting to HTTPS"
);
// TODO fetch greenlock.servername
_middlewareApp = app || _middlewareApp;
var secureServer = servers.httpsServer(null, app);
var secureAddr = "0.0.0.0";
var securePort = 443;
secureServer.listen(securePort, secureAddr, function() {
console.info(idstr + "Listening on", secureAddr + ":" + securePort, "for secure traffic");
plainServer.removeListener("error", startError);
secureServer.removeListener("error", startError);
resolve();
});
});
});
};
return servers;
};
function explainError(e) {
console.error();
console.error("Error: " + e.message);
if ("EACCES" === e.errno) {
console.error("You don't have prmission to access '" + e.address + ":" + e.port + "'.");
console.error('You probably need to use "sudo" or "sudo setcap \'cap_net_bind_service=+ep\' $(which node)"');
} else if ("EADDRINUSE" === e.errno) {
console.error("'" + e.address + ":" + e.port + "' is already being used by some other program.");
console.error("You probably need to stop that program or restart your computer.");
} else {
console.error(e.code + ": '" + e.address + ":" + e.port + "'");
}
console.error();
}
function wrapDefaultSniCallback(greenlock, secureOpts) {
// I'm not sure yet if the original SNICallback
// should be called before or after, so I'm just
// going to delay making that choice until I have the use case
/*
if (!secureOpts.SNICallback) {
secureOpts.SNICallback = function(servername, cb) {
cb(null, null);
};
}
*/
if (secureOpts.SNICallback) {
console.warn();
console.warn("[warning] Ignoring the given tlsOptions.SNICallback function.");
console.warn();
console.warn(" We're very open to implementing support for this,");
console.warn(" we just don't understand the use case yet.");
console.warn(" Please open an issue to discuss. We'd love to help.");
console.warn();
}
// TODO greenlock.servername for workers
secureOpts.SNICallback = sni.create(greenlock, secureOpts);
return secureOpts;
}
function createSecureServer(secureOpts, fn) {
var major = process.versions.node.split(".")[0];
// TODO can we trust earlier versions as well?
if (major >= 12) {
secureOpts.allowHTTP1 = true;
return require("http2").createSecureServer(secureOpts, fn);
} else {
return require("https").createServer(secureOpts, fn);
}
}

View File

@ -1,25 +0,0 @@
"use strict";
require("./main.js");
var Single = module.exports;
var Servers = require("./servers.js");
Single.create = function(opts) {
var greenlock = require("./greenlock.js").create(opts);
var servers = Servers.create(greenlock);
var single = {
serve: function(fn) {
fn(servers);
return single;
},
master: function(/*fn*/) {
// ignore
//fn(master);
return single;
}
};
return single;
};

194
sni.js
View File

@ -1,194 +0,0 @@
"use strict";
var sni = module.exports;
var tls = require("tls");
var servernameRe = /^[a-z0-9\.\-]+$/i;
// a nice, round, irrational number - about every 6¼ hours
var refreshOffset = Math.round(Math.PI * 2 * (60 * 60 * 1000));
// and another, about 15 minutes
var refreshStagger = Math.round(Math.PI * 5 * (60 * 1000));
// and another, about 30 seconds
var smallStagger = Math.round(Math.PI * (30 * 1000));
//secureOpts.SNICallback = sni.create(greenlock, secureOpts);
sni.create = function(greenlock, secureOpts) {
var _cache = {};
var defaultServername = greenlock.servername || "";
if (secureOpts.cert) {
// Note: it's fine if greenlock.servername is undefined,
// but if the caller wants this to auto-renew, they should define it
_cache[defaultServername] = {
refreshAt: 0,
secureContext: tls.createSecureContext(secureOpts)
};
}
return getSecureContext;
function notify(ev, args) {
try {
// TODO _notify() or notify()?
(greenlock.notify || greenlock._notify)(ev, args);
} catch (e) {
console.error(e);
console.error(ev, args);
}
}
function getSecureContext(servername, cb) {
//console.log("debug sni", servername);
if ("string" !== typeof servername) {
// this will never happen... right? but stranger things have...
console.error("[sanity fail] non-string servername:", servername);
cb(new Error("invalid servername"), null);
return;
}
var secureContext = getCachedContext(servername);
if (secureContext) {
//console.log("debug sni got cached context", servername, getCachedMeta(servername));
cb(null, secureContext);
return;
}
getFreshContext(servername)
.then(function(secureContext) {
if (secureContext) {
//console.log("debug sni got fresh context", servername, getCachedMeta(servername));
cb(null, secureContext);
return;
}
// Note: this does not replace tlsSocket.setSecureContext()
// as it only works when SNI has been sent
//console.log("debug sni got default context", servername, getCachedMeta(servername));
cb(null, getDefaultContext());
})
.catch(function(err) {
if (!err.context) {
err.context = "sni_callback";
}
notify("error", err);
//console.log("debug sni error", servername, err);
cb(err);
});
}
function getCachedMeta(servername) {
var meta = _cache[servername];
if (!meta) {
if (!_cache[wildname(servername)]) {
return null;
}
}
return meta;
}
function getCachedContext(servername) {
var meta = getCachedMeta(servername);
if (!meta) {
return null;
}
// always renew in background
if (!meta.refreshAt || Date.now() >= meta.refreshAt) {
getFreshContext(servername).catch(function(e) {
if (!e.context) {
e.context = "sni_background_refresh";
}
notify("error", e);
});
}
// under normal circumstances this would never be expired
// and, if it is expired, something is so wrong it's probably
// not worth wating for the renewal - it has probably failed
return meta.secureContext;
}
function getFreshContext(servername) {
var meta = getCachedMeta(servername);
if (!meta && !validServername(servername)) {
return Promise.resolve(null);
}
if (meta) {
// prevent stampedes
meta.refreshAt = Date.now() + randomRefreshOffset();
}
// TODO don't get unknown certs at all, rely on auto-updates from greenlock
// Note: greenlock.get() will return an existing fresh cert or issue a new one
return greenlock.get({ servername: servername }).then(function(result) {
var meta = getCachedMeta(servername);
if (!meta) {
meta = _cache[servername] = { secureContext: { _valid: false } };
}
// prevent from being punked by bot trolls
meta.refreshAt = Date.now() + smallStagger;
// nothing to do
if (!result) {
return null;
}
// we only care about the first one
var pems = result.pems;
var site = result.site;
if (!pems || !pems.cert) {
// nothing to do
// (and the error should have been reported already)
return null;
}
meta = {
refreshAt: Date.now() + randomRefreshOffset(),
secureContext: tls.createSecureContext({
// TODO support passphrase-protected privkeys
key: pems.privkey,
cert: pems.cert + "\n" + pems.chain + "\n"
})
};
meta.secureContext._valid = true;
// copy this same object into every place
(result.altnames || site.altnames || [result.subject || site.subject]).forEach(function(altname) {
_cache[altname] = meta;
});
return meta.secureContext;
});
}
function getDefaultContext() {
return getCachedContext(defaultServername);
}
};
// whenever we need to know when to refresh next
function randomRefreshOffset() {
var stagger = Math.round(refreshStagger / 2) - Math.round(Math.random() * refreshStagger);
return refreshOffset + stagger;
}
function validServername(servername) {
// format and (lightly) sanitize sni so that users can be naive
// and not have to worry about SQL injection or fs discovery
servername = (servername || "").toLowerCase();
// hostname labels allow a-z, 0-9, -, and are separated by dots
// _ is sometimes allowed, but not as a "hostname", and not by Let's Encrypt ACME
// REGEX // https://www.codeproject.com/Questions/1063023/alphanumeric-validation-javascript-without-regex
return servernameRe.test(servername) && -1 === servername.indexOf("..");
}
function wildname(servername) {
return (
"*." +
servername
.split(".")
.slice(1)
.join(".")
);
}

View File

@ -1,85 +0,0 @@
#!/usr/bin/env node
var Greenlock = require("../");
var greenlock = Greenlock.create({
version: "draft-11",
server: "https://acme-staging-v02.api.letsencrypt.org/directory",
agreeTos: true,
approvedDomains: ["example.com", "www.example.com"],
configDir: require("path").join(require("os").tmpdir(), "acme"),
app: require("express")().use("/", function(req, res) {
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end("Hello, World!\n\n💚 🔒.js");
})
});
var server1 = greenlock.listen(5080, 5443);
server1.on("listening", function() {
console.log("### THREE 3333 - All is well server1", this.address());
setTimeout(function() {
// so that the address() object doesn't disappear
server1.close();
server1.unencrypted.close();
}, 10);
});
setTimeout(function() {
var server2 = greenlock.listen(6080, 6443, function() {
console.log("### FIVE 55555 - Started server 2!");
setTimeout(function() {
server2.close();
server2.unencrypted.close();
server6.close();
server6.unencrypted.close();
server7.close();
server7.unencrypted.close();
setTimeout(function() {
// TODO greenlock needs a close event (and to listen to its server's close event)
process.exit(0);
}, 1000);
}, 1000);
});
server2.on("listening", function() {
console.log("### FOUR 44444 - All is well server2", server2.address());
});
}, 1000);
var server3 = greenlock.listen(
22,
22,
function() {
console.error("Error: expected to get an error when launching plain server on port 22");
},
function() {
console.error("Error: expected to get an error when launching " + server3.type + " server on port 22");
}
);
server3.unencrypted.on("error", function() {
console.log("Success: caught expected (plain) error");
});
server3.on("error", function() {
console.log("Success: caught expected " + server3.type + " error");
//server3.close();
});
var server4 = greenlock.listen(
7080,
7443,
function() {
console.log("Success: server4: plain");
server4.unencrypted.close();
},
function() {
console.log("Success: server4: " + server4.type);
server4.close();
}
);
var server5 = greenlock.listen(10080, 10443, function() {
console.log("Server 5 with one fn", this.address());
server5.close();
server5.unencrypted.close();
});
var server6 = greenlock.listen("[::]:11080", "[::1]:11443");
var server7 = greenlock.listen("/tmp/gl.plain.sock", "/tmp/gl.sec.sock");

View File

@ -1,62 +0,0 @@
"use strict";
var Worker = module.exports;
// *very* generous, but well below the http norm of 120
var messageTimeout = 30 * 1000;
var msgPrefix = "greenlock:";
Worker.create = function() {
var greenlock = {};
["getAcmeHttp01ChallengeResponse", "get", "notify"].forEach(function(k) {
greenlock[k] = function(args) {
return rpc(k, args);
};
});
var worker = {
serve: function(fn) {
var servers = require("./servers.js").create(greenlock);
fn(servers);
return worker;
},
master: function() {
// ignore
return worker;
}
};
return worker;
};
function rpc(funcname, msg) {
return new Promise(function(resolve, reject) {
var rnd = Math.random()
.toString()
.slice(2)
.toString(16);
var id = msgPrefix + rnd;
var timeout;
function getResponse(msg) {
if (msg._id !== id) {
return;
}
process.removeListener("message", getResponse);
clearTimeout(timeout);
resolve(msg._result);
}
// TODO keep a single listener than just responds
// via a collection of callbacks? or leave as is?
process.on("message", getResponse);
process.send({
_id: id,
_funcname: funcname,
_input: msg
});
timeout = setTimeout(function() {
process.removeListener("message", getResponse);
reject(new Error("worker rpc request timeout"));
}, messageTimeout);
});
}