Compare commits
228 Commits
Author | SHA1 | Date | |
---|---|---|---|
d11b45c409 | |||
36abf769be | |||
c93ecf307b | |||
3ea7d3e97b | |||
fff5192fb4 | |||
fceeb8c72c | |||
a7526ffad8 | |||
d324179cb1 | |||
18b36d7d23 | |||
7a2de022fa | |||
e478d27628 | |||
|
6f2c1ec5ba | ||
894a01fa4e | |||
df1259cd9d | |||
3f8a54a988 | |||
26adaf2037 | |||
2a9b463964 | |||
bc4a5b44ae | |||
|
378b9310aa | ||
|
405e98620c | ||
3f437c6ebb | |||
00e9d96f8b | |||
31ba1186be | |||
af7c75a0f7 | |||
8d464d6810 | |||
de051dd3a2 | |||
d14163d153 | |||
6df0dc2f76 | |||
0dd3641dc2 | |||
9ab7844ea8 | |||
afac2b01a6 | |||
0dcefa77d8 | |||
c9e62ccb05 | |||
8732029eb1 | |||
f3bae8580b | |||
|
ada2b9ccfe | ||
4d400a9828 | |||
0f34e81d8a | |||
|
f539de2611 | ||
8f369226cf | |||
44ff0ac5df | |||
fbe7549604 | |||
6cdbe36e6c | |||
778416d49b | |||
c73ad565a3 | |||
a49ccb7398 | |||
|
8ba4b88e00 | ||
78fcebd567 | |||
1843f03d87 | |||
81a63b365d | |||
c187bd0fb7 | |||
bcc1c16ec2 | |||
fec660bfeb | |||
1b42d866fb | |||
579709ae40 | |||
c7dfec515d | |||
0d2d571458 | |||
b71311b1bc | |||
f6cc67ff53 | |||
feeafc5c97 | |||
3634965e70 | |||
74b3419507 | |||
a6f4de6c78 | |||
63b5182b38 | |||
98ca31256b | |||
e83f92166d | |||
9ae6a768a7 | |||
ffbcc433b3 | |||
f088d0326d | |||
8c5e5435fc | |||
cda9bfa418 | |||
16e6a766a6 | |||
d589cc3a11 | |||
12058026ad | |||
d4365bf9d0 | |||
f722d9917c | |||
f9d3ec5f76 | |||
7bcd7a2bf7 | |||
779ab234ac | |||
b17805d1fb | |||
cb1c2ce438 | |||
6b359a7cff | |||
05e01ce947 | |||
df9a27ec20 | |||
8f6aa1cc8c | |||
7a12ffaef0 | |||
da89714865 | |||
0476905a5b | |||
d566cfc9f1 | |||
efd7693dcd | |||
ca2e5cdb99 | |||
bc4da32f15 | |||
8c25386767 | |||
39d9ae4f31 | |||
cfbaab0a27 | |||
8c0d6c718d | |||
4823a4d464 | |||
b6bdca552b | |||
0c23c522a3 | |||
af0b98ac14 | |||
a7f1dea40b | |||
872962a8dd | |||
b4d72bbd13 | |||
1dae4248c3 | |||
38de3e9d1c | |||
6f960b1a2a | |||
bfaccdf725 | |||
0285f9b40f | |||
aac54d63f2 | |||
11e0db1f20 | |||
8eaf269143 | |||
f2dfbfac14 | |||
63e2d20f1c | |||
60917e611e | |||
61bd56f94d | |||
261eff2700 | |||
648ed4e4d5 | |||
e785d9199d | |||
a48c10e082 | |||
c3d496531b | |||
8c8dde0a7d | |||
901c8659cf | |||
20de40cd1d | |||
a06095f1e0 | |||
973be987da | |||
bd5efaab3b | |||
bbf8813355 | |||
bf77d309af | |||
4c4ca98157 | |||
26a3631779 | |||
def4f6fcb9 | |||
694caf3e72 | |||
8995edd620 | |||
0f195aed95 | |||
2494bca6cc | |||
|
16e1158705 | ||
|
eb996e0cf4 | ||
|
0ee94d94ec | ||
|
4744f4050e | ||
|
6953068a7b | ||
4e9db5781a | |||
caf804cc41 | |||
ef78971ba5 | |||
b5c47c8d7c | |||
3ea55fca5b | |||
e976a410bf | |||
2d688f8551 | |||
0b2637b8e7 | |||
d5e0abf0f8 | |||
ff58ff6eb6 | |||
a36cdd83c5 | |||
462f0bfc2c | |||
4498342005 | |||
32e57aa9cb | |||
238e262f95 | |||
8d89454c0a | |||
a20f91661f | |||
|
2dab010be3 | ||
|
d61955e6b6 | ||
|
53321d219a | ||
6d2d02b1d6 | |||
6c5813d86d | |||
e192c4af11 | |||
48d990a6e8 | |||
57806ef06f | |||
27085a7f64 | |||
8cd7648df6 | |||
|
5eab460ec2 | ||
|
7f340412d7 | ||
|
d9c6a77bfc | ||
|
89e0ddaecf | ||
|
3b27aa6806 | ||
98f51392f4 | |||
|
4d8b3b6859 | ||
|
377b9efb30 | ||
9f65da895f | |||
|
2ba8ba85a5 | ||
|
88c9ab9357 | ||
|
ab6635cd4d | ||
24bbc24d90 | |||
2ffc76e242 | |||
40321b1c59 | |||
061ca7cb0a | |||
3a1d6ce9ae | |||
a0bf0596ed | |||
f23b5fa2eb | |||
85c8fdc0d9 | |||
|
26da6c84cc | ||
|
6eae454bed | ||
|
d2aa9394aa | ||
|
ab21671481 | ||
7786acc7a3 | |||
520a5676b5 | |||
dfabdd15dd | |||
b530696ed2 | |||
329ab128fd | |||
820ab2f2c5 | |||
769fe3008c | |||
eaf0748871 | |||
ec53f781e8 | |||
269b7c596f | |||
27ff2ef53f | |||
596ae53dbb | |||
456e47a9ac | |||
4b38afe581 | |||
47d1c85c0f | |||
59043f8ebd | |||
87cfc84dfa | |||
736863448d | |||
7f18123af8 | |||
52545f1530 | |||
c50a03ea35 | |||
1cd04d9f64 | |||
06c9ec31b9 | |||
2aef5f838d | |||
7247211cdd | |||
|
a9c4944dee | ||
|
4ea9115647 | ||
|
bd05ba77b6 | ||
|
5fe75b8484 | ||
|
6abffa0620 | ||
|
c8adbf331b | ||
|
5047d9aa96 | ||
|
745d635881 | ||
|
25c5def46e | ||
|
19cf4536b4 | ||
|
a3de9c6cf7 | ||
|
901d3b4a1a |
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,6 +2,7 @@
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
.vscode
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
|
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"bracketSpacing": true,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "none",
|
||||
"useTabs": true
|
||||
}
|
388
LICENSE
388
LICENSE
@ -1,21 +1,375 @@
|
||||
MIT License
|
||||
Copyright 2015-2019 AJ ONeal
|
||||
|
||||
Copyright (c) 2016 Daplie, Inc
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
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.
|
||||
|
510
README.md
510
README.md
@ -1,208 +1,376 @@
|
||||
<!-- BANNER_TPL_BEGIN -->
|
||||
# New Documentation & [v2/v3 Migration Guide](https://git.rootprojects.org/root/greenlock.js/src/branch/v3/MIGRATION_GUIDE_V2_V3.md)
|
||||
|
||||
About Daplie: We're taking back the Internet!
|
||||
--------------
|
||||
Greenlock v3 just came out of private beta **today** (Nov 1st, 2019).
|
||||
|
||||
Down with Google, Apple, and Facebook!
|
||||
The code is complete and we're working on great documentation.
|
||||
|
||||
We're re-decentralizing the web and making it read-write again - one home cloud system at a time.
|
||||
Many **examples** and **full API** documentation are still coming.
|
||||
|
||||
Tired of serving the Empire? Come join the Rebel Alliance:
|
||||
# [Greenlock Express](https://git.rootprojects.org/root/greenlock-express.js) is Let's Encrypt for Node
|
||||
|
||||
<a href="mailto:jobs@daplie.com">jobs@daplie.com</a> | [Invest in Daplie on Wefunder](https://daplie.com/invest/) | [Pre-order Cloud](https://daplie.com/preorder/), The World's First Home Server for Everyone
|
||||

|
||||
|
||||
<!-- BANNER_TPL_END -->
|
||||
| Built by [Root](https://therootcompany.com) for [Hub](https://rootprojects.org/hub/)
|
||||
|
||||
greenlock-express (letsencrypt-express)
|
||||
=================
|
||||
Free SSL, Automated HTTPS / HTTP2, served with Node via Express, Koa, hapi, etc.
|
||||
|
||||
[](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
|
||||
|
||||
| [greenlock (lib)](https://git.daplie.com/Daplie/node-greenlock)
|
||||
| [greenlock-cli](https://git.daplie.com/Daplie/greenlock-cli)
|
||||
| **greenlock-express**
|
||||
| [greenlock-cluster](https://git.daplie.com/Daplie/greenlock-cluster)
|
||||
| [greenlock-koa](https://git.daplie.com/Daplie/greenlock-koa)
|
||||
| [greenlock-hapi](https://git.daplie.com/Daplie/greenlock-hapi)
|
||||
|
|
||||
Greenlock Express is a **Web Server** with **Fully Automated HTTPS** and renewals.
|
||||
|
||||
Free SSL and managed or automatic HTTPS for node.js with Express, Koa, Connect, Hapi, and all other middleware systems.
|
||||
```js
|
||||
"use strict";
|
||||
|
||||
* 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
|
||||
function httpsWorker(glx) {
|
||||
// Serves on 80 and 443
|
||||
// Get's SSL certificates magically!
|
||||
|
||||
All you have to do is start the webserver and then visit it at its domain name.
|
||||
glx.serveApp(function(req, res) {
|
||||
res.end("Hello, Encrypted World!");
|
||||
});
|
||||
}
|
||||
|
||||
Install
|
||||
=======
|
||||
var pkg = require("./package.json");
|
||||
require("greenlock-express")
|
||||
.init(function getConfig() {
|
||||
// Greenlock Config
|
||||
|
||||
```bash
|
||||
npm install --save greenlock-express@2.x
|
||||
return {
|
||||
package: { name: pkg.name, version: pkg.version },
|
||||
maintainerEmail: pkg.author,
|
||||
cluster: false
|
||||
};
|
||||
})
|
||||
.serve(httpsWorker);
|
||||
```
|
||||
|
||||
**Important**: Use node v4.5+ or v6.x, node <= v4.4 has a [known bug](https://github.com/nodejs/node/issues/8053) in the `Buffer` implementation.
|
||||
Manage via API or the config file:
|
||||
|
||||
QuickStart
|
||||
==========
|
||||
`~/.config/greenlock/manage.json`: (default filesystem config)
|
||||
|
||||
Here's a completely working example that will get you started:
|
||||
|
||||
`app.js`:
|
||||
```javascript
|
||||
'use strict';
|
||||
|
||||
require('greenlock-express').create({
|
||||
|
||||
server: 'staging'
|
||||
|
||||
, email: 'john.doe@example.com'
|
||||
|
||||
, agreeTos: true
|
||||
|
||||
, approveDomains: [ 'example.com' ]
|
||||
|
||||
, app: require('express')().use('/', function (req, res) {
|
||||
res.end('Hello, World!');
|
||||
})
|
||||
|
||||
}).listen(80, 443);
|
||||
```
|
||||
|
||||
Certificates will be stored in `~/letsencrypt`.
|
||||
|
||||
**Important**:
|
||||
|
||||
You must set `server` to `https://acme-v01.api.letsencrypt.org/directory` **after**
|
||||
you have tested that your setup works.
|
||||
|
||||
Why You Must Use 'staging' First
|
||||
--------------------------------
|
||||
|
||||
There are a number of common problems related to system configuration -
|
||||
firewalls, ports, permissions, etc - that you are likely to run up against
|
||||
when using greenlock for your first time.
|
||||
|
||||
In order to avoid being blocked by hitting rate limits with bad requests,
|
||||
you should always test against the `'staging'` server
|
||||
(`https://acme-staging.api.letsencrypt.org/directory`) first.
|
||||
|
||||
Migrating from v1.x
|
||||
===================
|
||||
|
||||
Whereas v1.x had a few hundred lines of code, v2.x is a single small file of about 50 lines.
|
||||
|
||||
A few important things to note:
|
||||
|
||||
* Delete your v1.x `~/letsencrypt` directory, otherwise you get this:
|
||||
* `{ type: 'urn:acme:error:malformed', detail: 'Parse error reading JWS', status: 400 }`
|
||||
* `approveRegistration` has been replaced by `approveDomains`
|
||||
* All of the behavior has moved to the various plugins, which each have their own options
|
||||
* Use https and http directly, don't rely on the silly `.listen()` helper. It's just there for looks.
|
||||
* `lex.createAcmeResponder()` is now `lex.middleware(require('redirect-https')())` or `lex.middleware(app)`
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
The oversimplified example was the bait
|
||||
(because everyone seems to want an example that fits in 3 lines, even if it's terribly bad practices),
|
||||
now here's the switch:
|
||||
|
||||
`serve.js`:
|
||||
```javascript
|
||||
'use strict';
|
||||
|
||||
// returns an instance of node-greenlock with additional helper methods
|
||||
var lex = require('greenlock-express').create({
|
||||
// set to https://acme-v01.api.letsencrypt.org/directory in production
|
||||
server: 'staging'
|
||||
|
||||
// If you wish to replace the default plugins, you may do so here
|
||||
//
|
||||
, challenges: { 'http-01': require('le-challenge-fs').create({ webrootPath: '/tmp/acme-challenges' }) }
|
||||
, store: require('le-store-certbot').create({ webrootPath: '/tmp/acme-challenges' })
|
||||
|
||||
// You probably wouldn't need to replace the default sni handler
|
||||
// See https://git.daplie.com/Daplie/le-sni-auto if you think you do
|
||||
//, sni: require('le-sni-auto').create({})
|
||||
|
||||
, approveDomains: approveDomains
|
||||
});
|
||||
```
|
||||
|
||||
```javascript
|
||||
function approveDomains(opts, certs, cb) {
|
||||
// This is where you check your database and associated
|
||||
// email addresses with domains and agreements and such
|
||||
|
||||
|
||||
// The domains being approved for the first time are listed in opts.domains
|
||||
// Certs being renewed are listed in certs.altnames
|
||||
if (certs) {
|
||||
opts.domains = certs.altnames;
|
||||
}
|
||||
else {
|
||||
opts.email = 'john.doe@example.com';
|
||||
opts.agreeTos = true;
|
||||
}
|
||||
|
||||
// NOTE: you can also change other options such as `challengeType` and `challenge`
|
||||
// opts.challengeType = 'http-01';
|
||||
// opts.challenge = require('le-challenge-fs').create({});
|
||||
|
||||
cb(null, { options: opts, certs: certs });
|
||||
```json
|
||||
{
|
||||
"subscriberEmail": "letsencrypt-test@therootcompany.com",
|
||||
"agreeToTerms": true,
|
||||
"sites": {
|
||||
"example.com": {
|
||||
"subject": "example.com",
|
||||
"altnames": ["example.com", "www.example.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Let's Encrypt for...
|
||||
|
||||
```javascript
|
||||
// handles acme-challenge and redirects to https
|
||||
require('http').createServer(lex.middleware(require('redirect-https')())).listen(80, function () {
|
||||
console.log("Listening for ACME http-01 challenges on", this.address());
|
||||
});
|
||||
- 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
|
||||
|
||||
var app = require('express')();
|
||||
app.use('/', function (req, res) {
|
||||
res.end('Hello, World!');
|
||||
});
|
||||
# QuickStart Guide
|
||||
|
||||
// handles your app
|
||||
require('https').createServer(lex.httpsOptions, lex.middleware(app)).listen(443, function () {
|
||||
console.log("Listening for ACME tls-sni-01 challenges and serve app on", this.address());
|
||||
});
|
||||
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
|
||||
mkdir ~/my-project
|
||||
pushd ~/my-project
|
||||
npm init
|
||||
```
|
||||
|
||||
**Security Warning**:
|
||||
</details>
|
||||
|
||||
If you don't do proper checks in `approveDomains(opts, certs, cb)`
|
||||
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.
|
||||
<details>
|
||||
<summary>2. Create an http app (i.e. express)</summary>
|
||||
|
||||
## 2. Create an http app (i.e. express)
|
||||
|
||||
API
|
||||
===
|
||||
This example is shown with Express, but any node app will do. Greenlock
|
||||
works with everything.
|
||||
(or any node-style http app)
|
||||
|
||||
This module is an elaborate ruse (to provide an oversimplified example and to nab some SEO).
|
||||
`my-express-app.js`:
|
||||
|
||||
The API is actually located at [node-greenlock options](https://git.daplie.com/Daplie/node-greenlock)
|
||||
(because all options are simply passed through to `node-greenlock` proper without modification).
|
||||
```js
|
||||
"use strict";
|
||||
|
||||
The only "API" consists of two options, the rest is just a wrapper around `node-greenlock` to take LOC from 15 to 5:
|
||||
// A plain, node-style app
|
||||
|
||||
* `opts.app` An express app in the format `function (req, res) { ... }` (no `next`).
|
||||
* `lex.listen(plainPort, tlsPort)` Accepts port numbers (or arrays of port numbers) to listen on.
|
||||
function myPlainNodeHttpApp(req, res) {
|
||||
res.end("Hello, Encrypted World!");
|
||||
}
|
||||
|
||||
Brief overview of some simple options for `node-greenlock`:
|
||||
// Wrap that plain app in express,
|
||||
// because that's what you're used to
|
||||
|
||||
* `opts.server` set to https://acme-v01.api.letsencrypt.org/directory in production
|
||||
* `opts.email` The default email to use to accept agreements.
|
||||
* `opts.agreeTos` When set to `true`, this always accepts the LetsEncrypt TOS. When a string it checks the agreement url first.
|
||||
* `opts.approveDomains` can be either of:
|
||||
* An explicit array of allowed domains such as `[ 'example.com', 'www.example.com' ]`
|
||||
* A callback `function (opts, certs, cb) { cb(null, { options: opts, certs: certs }); }` for setting `email`, `agreeTos`, `domains`, etc (as shown in usage example above)
|
||||
* `opts.renewWithin` is the **maximum** number of days (in ms) before expiration to renew a certificate.
|
||||
* `opts.renewBy` is the **minimum** number of days (in ms) before expiration to renew a certificate.
|
||||
var express = require("express");
|
||||
var app = express();
|
||||
app.get("/", myPlainNodeHttpApp);
|
||||
|
||||
// export the app normally
|
||||
// do not .listen()
|
||||
|
||||
module.exports = app;
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>3. Serve with Greenlock Express</summary>
|
||||
|
||||
## 3. Serve with Greenlock Express
|
||||
|
||||
Greenlock Express is designed with these goals in mind:
|
||||
|
||||
- Simplicity and ease-of-use
|
||||
- Performance and scalability
|
||||
- Configurability and control
|
||||
|
||||
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) {
|
||||
// Works with any Node app (Express, etc)
|
||||
var app = require("my-express-app.js");
|
||||
server.serveApp(app);
|
||||
}
|
||||
```
|
||||
|
||||
And start your server:
|
||||
|
||||
```bash
|
||||
# Allow non-root node to use ports 80 (HTTP) and 443 (HTTPS)
|
||||
sudo setcap 'cap_net_bind_service=+ep' $(which node)
|
||||
```
|
||||
|
||||
```bash
|
||||
# `npm start` will call `node ./server.js` by default
|
||||
npm start
|
||||
```
|
||||
|
||||
```txt
|
||||
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
|
||||
Listening on 0.0.0.0:443 for secure traffic
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>4. Manage SSL Certificates and Domains</summary>
|
||||
|
||||
## 4. Manage domains
|
||||
|
||||
The management API is built to work with Databases, S3, etc.
|
||||
|
||||
HOWEVER, by default it starts with a simple config file.
|
||||
|
||||
<!--
|
||||
This will update the config file (assuming the default fs-based management plugin):
|
||||
-->
|
||||
|
||||
`~/.config/greenlock/manager.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"subscriberEmail": "letsencrypt-test@therootcompany.com",
|
||||
"agreeToTerms": true,
|
||||
"sites": {
|
||||
"example.com": {
|
||||
"subject": "example.com",
|
||||
"altnames": ["example.com", "www.example.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
COMING SOON
|
||||
|
||||
Management can be done via the **CLI** or the JavaScript [**API**](https://git.rootprojects.org/root/greenlock.js/).
|
||||
Since this is the QuickStart, we'll demo the **CLI**:
|
||||
|
||||
You need to create a Let's Encrypt _subscriber account_, which can be done globally, or per-site.
|
||||
All individuals, and most businesses, should set this globally:
|
||||
|
||||
```bash
|
||||
# COMING SOON
|
||||
# (this command should be here by Nov 5th)
|
||||
# (edit the config by hand for now)
|
||||
#
|
||||
# Set a global subscriber account
|
||||
npx greenlock config --subscriber-email 'mycompany@example.com' --agree-to-terms true
|
||||
```
|
||||
|
||||
<!-- todo print where the key was saved -->
|
||||
|
||||
A Let's Encrypt SSL certificate has a "Subject" (Primary Domain) and up to 100 "Alternative Names"
|
||||
(of which the first _must_ be the subject).
|
||||
|
||||
```bash
|
||||
# COMING SOON
|
||||
# (this command should be here by Nov 5th)
|
||||
# (edit the config by hand for now)
|
||||
#
|
||||
# Add a certificate with specific domains
|
||||
npx greenlock add --subject example.com --altnames example.com,www.example.com
|
||||
```
|
||||
|
||||
<!-- todo print where the cert was saved -->
|
||||
|
||||
Note: **Localhost**, **Wildcard**, and Certificates for Private Networks require
|
||||
[**DNS validation**](https://git.rootprojects.org/root/greenlock-exp).
|
||||
|
||||
- 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.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 & Rules of the Road
|
||||
|
||||
Greenlock™ 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™](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)
|
||||
|
20
config.js
Normal file
20
config.js
Normal file
@ -0,0 +1,20 @@
|
||||
"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
Normal file
35
demo.js
Normal file
@ -0,0 +1,35 @@
|
||||
"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!");
|
||||
});
|
||||
}
|
53
dist/etc/systemd/system/greenlock-express.service
vendored
Normal file
53
dist/etc/systemd/system/greenlock-express.service
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
# 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
|
39
examples/cluster/server.js
Normal file
39
examples/cluster/server.js
Normal file
@ -0,0 +1,39 @@
|
||||
"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);
|
||||
}
|
17
examples/express/my-express-app.js
Normal file
17
examples/express/my-express-app.js
Normal file
@ -0,0 +1,17 @@
|
||||
"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;
|
27
examples/express/server.js
Normal file
27
examples/express/server.js
Normal file
@ -0,0 +1,27 @@
|
||||
"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);
|
@ -1,22 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
//require('greenlock-express')
|
||||
require('../').create({
|
||||
|
||||
server: 'staging'
|
||||
|
||||
, email: 'john.doe@example.com'
|
||||
|
||||
, agreeTos: true
|
||||
|
||||
, approvedDomains: [ 'example.com', 'www.example.com' ]
|
||||
|
||||
, app: require('express')().use('/', function (req, res) {
|
||||
res.end('Hello, World!');
|
||||
})
|
||||
|
||||
, renewWithin: (91 * 24 * 60 * 60 * 1000)
|
||||
, renewBy: (90 * 24 * 60 * 60 * 1000)
|
||||
|
||||
, debug: true
|
||||
}).listen(80, 443);
|
44
examples/http-proxy/server.js
Normal file
44
examples/http-proxy/server.js
Normal file
@ -0,0 +1,44 @@
|
||||
"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);
|
42
examples/http/server.js
Normal file
42
examples/http/server.js
Normal file
@ -0,0 +1,42 @@
|
||||
"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);
|
48
examples/http2/server.js
Normal file
48
examples/http2/server.js
Normal file
@ -0,0 +1,48 @@
|
||||
"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);
|
49
examples/https/server.js
Normal file
49
examples/https/server.js
Normal file
@ -0,0 +1,49 @@
|
||||
"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);
|
22
examples/quickstart/README.md
Normal file
22
examples/quickstart/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
# 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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
32
examples/quickstart/server.js
Normal file
32
examples/quickstart/server.js
Normal file
@ -0,0 +1,32 @@
|
||||
"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);
|
@ -1,20 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
//require('greenlock-express')
|
||||
require('../').create({
|
||||
|
||||
server: 'staging'
|
||||
|
||||
, email: 'john.doe@example.com'
|
||||
|
||||
, agreeTos: true
|
||||
|
||||
, approvedDomains: [ 'example.com', 'www.example.com' ]
|
||||
|
||||
, app: require('express')().use('/', function (req, res) {
|
||||
res.end('Hello, World!');
|
||||
})
|
||||
|
||||
, debug: true
|
||||
|
||||
}).listen(80, 443);
|
49
examples/socket.io/server.js
Normal file
49
examples/socket.io/server.js
Normal file
@ -0,0 +1,49 @@
|
||||
// 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);
|
3
examples/spdy/server.js
Normal file
3
examples/spdy/server.js
Normal file
@ -0,0 +1,3 @@
|
||||
// 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+
|
42
examples/websockets/server.js
Normal file
42
examples/websockets/server.js
Normal file
@ -0,0 +1,42 @@
|
||||
"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);
|
44
greenlock-express.js
Normal file
44
greenlock-express.js
Normal file
@ -0,0 +1,44 @@
|
||||
"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);
|
||||
};
|
119
greenlock.js
Normal file
119
greenlock.js
Normal file
@ -0,0 +1,119 @@
|
||||
"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;
|
||||
}
|
106
http-middleware.js
Normal file
106
http-middleware.js
Normal file
@ -0,0 +1,106 @@
|
||||
"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) || "";
|
||||
};
|
139
https-middleware.js
Normal file
139
https-middleware.js
Normal file
@ -0,0 +1,139 @@
|
||||
"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;
|
||||
};
|
14
install.sh
Normal file
14
install.sh
Normal file
@ -0,0 +1,14 @@
|
||||
# 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
|
66
lex.js
66
lex.js
@ -1,66 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
// opts.approveDomains(options, certs, cb)
|
||||
module.exports.create = function (opts) {
|
||||
// accept all defaults for le.challenges, le.store, le.middleware
|
||||
var le = require('greenlock').create(opts);
|
||||
|
||||
opts.app = opts.app || function (req, res) {
|
||||
res.end("Hello, World!\nWith Love,\nLet's Encrypt Express");
|
||||
};
|
||||
|
||||
opts.listen = function (plainPort, port) {
|
||||
var PromiseA;
|
||||
try {
|
||||
PromiseA = require('bluebird');
|
||||
} catch(e) {
|
||||
console.warn("Package 'bluebird' not installed. Using global.Promise instead");
|
||||
console.warn("(want bluebird instead? npm install --save bluebird)");
|
||||
PromiseA = global.Promise;
|
||||
}
|
||||
var promises = [];
|
||||
var plainPorts = plainPort;
|
||||
var ports = port;
|
||||
var servers = [];
|
||||
|
||||
if (!plainPorts) {
|
||||
plainPorts = 80;
|
||||
}
|
||||
if (!ports) {
|
||||
ports = 443;
|
||||
}
|
||||
|
||||
if (!Array.isArray(plainPorts)) {
|
||||
plainPorts = [ plainPorts ];
|
||||
ports = [ ports ];
|
||||
}
|
||||
|
||||
plainPorts.forEach(function (p) {
|
||||
promises.push(new PromiseA(function (resolve, reject) {
|
||||
require('http').createServer(le.middleware(require('redirect-https')())).listen(p, function () {
|
||||
console.log("Handling ACME challenges and redirecting to https on plain port " + p);
|
||||
resolve();
|
||||
}).on('error', reject);
|
||||
}));
|
||||
});
|
||||
|
||||
ports.forEach(function (p) {
|
||||
promises.push(new PromiseA(function (resolve, reject) {
|
||||
var server = require('https').createServer(le.httpsOptions, le.middleware(le.app)).listen(p, function () {
|
||||
console.log("Handling ACME challenges and serving https " + p);
|
||||
resolve();
|
||||
}).on('error', reject);
|
||||
servers.push(server);
|
||||
}));
|
||||
});
|
||||
|
||||
if (!Array.isArray(port)) {
|
||||
servers = servers[0];
|
||||
}
|
||||
|
||||
return servers;
|
||||
};
|
||||
|
||||
|
||||
return le;
|
||||
};
|
37
lib/compat.js
Normal file
37
lib/compat.js
Normal file
@ -0,0 +1,37 @@
|
||||
"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;
|
36
main.js
Normal file
36
main.js
Normal file
@ -0,0 +1,36 @@
|
||||
"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
Normal file
160
master.js
Normal file
@ -0,0 +1,160 @@
|
||||
"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
Normal file
140
package-lock.json
generated
Normal file
@ -0,0 +1,140 @@
|
||||
{
|
||||
"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=="
|
||||
}
|
||||
}
|
||||
}
|
94
package.json
94
package.json
@ -1,47 +1,51 @@
|
||||
{
|
||||
"name": "greenlock-express",
|
||||
"version": "2.0.7",
|
||||
"description": "Free SSL and managed or automatic HTTPS for node.js with Express, Koa, Connect, Hapi, and all other middleware systems.",
|
||||
"main": "lex.js",
|
||||
"directories": {
|
||||
"example": "examples"
|
||||
},
|
||||
"dependencies": {
|
||||
"le-challenge-fs": "^2.0.4",
|
||||
"le-sni-auto": "^2.0.1",
|
||||
"le-store-certbot": "^2.0.3",
|
||||
"greenlock": "^2.1.9",
|
||||
"localhost.daplie.com-certificates": "^1.2.3",
|
||||
"redirect-https": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"scripts": {
|
||||
"test": "node examples/serve.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://git.daplie.com/Daplie/greenlock-cluster.git"
|
||||
},
|
||||
"keywords": [
|
||||
"acme",
|
||||
"cloud",
|
||||
"cluster",
|
||||
"free",
|
||||
"greenlock",
|
||||
"https",
|
||||
"le",
|
||||
"letsencrypt",
|
||||
"multi-core",
|
||||
"node",
|
||||
"node.js",
|
||||
"scale",
|
||||
"ssl",
|
||||
"tls"
|
||||
],
|
||||
"author": "AJ ONeal <aj@daplie.com> (https://daplie.com/)",
|
||||
"license": "(MIT OR Apache-2.0)",
|
||||
"bugs": {
|
||||
"url": "https://git.daplie.com/Daplie/greenlock-cluster/issues"
|
||||
},
|
||||
"homepage": "https://git.daplie.com/Daplie/greenlock-cluster#readme"
|
||||
"name": "@root/greenlock-express",
|
||||
"version": "3.0.10",
|
||||
"description": "Free SSL and managed or automatic HTTPS for node.js with Express, Koa, Connect, Hapi, and all other middleware systems.",
|
||||
"main": "greenlock-express.js",
|
||||
"homepage": "https://greenlock.domains",
|
||||
"files": [
|
||||
"*.js",
|
||||
"lib",
|
||||
"scripts"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "node_todo server.js ./config.js",
|
||||
"test": "node_todo test/greenlock.js"
|
||||
},
|
||||
"directories": {
|
||||
"example": "examples"
|
||||
},
|
||||
"dependencies": {
|
||||
"@root/greenlock": "^3.0.17",
|
||||
"redirect-https": "^1.1.5"
|
||||
},
|
||||
"trulyOptionalDependencies": {
|
||||
"http-proxy": "^1.17.0",
|
||||
"express": "^4.16.3",
|
||||
"express-basic-auth": "^1.2.0",
|
||||
"finalhandler": "^1.1.1",
|
||||
"serve-index": "^1.9.1",
|
||||
"serve-static": "^1.13.2",
|
||||
"ws": "^5.2.1"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.rootprojects.org/root/greenlock-express.js.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Let's Encrypt",
|
||||
"ACME",
|
||||
"greenlock",
|
||||
"Free SSL",
|
||||
"Automated HTTPS",
|
||||
"https",
|
||||
"tls"
|
||||
],
|
||||
"author": "AJ ONeal <coolaj86@gmail.com> (https://solderjs.com/)",
|
||||
"license": "MPL-2.0",
|
||||
"bugs": {
|
||||
"url": "https://git.rootprojects.org/root/greenlock-express.js/issues"
|
||||
}
|
||||
}
|
||||
|
77
scripts/postinstall
Executable file
77
scripts/postinstall
Executable file
@ -0,0 +1,77 @@
|
||||
#!/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);
|
157
servers.js
Normal file
157
servers.js
Normal file
@ -0,0 +1,157 @@
|
||||
"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);
|
||||
}
|
||||
}
|
25
single.js
Normal file
25
single.js
Normal file
@ -0,0 +1,25 @@
|
||||
"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
Normal file
194
sni.js
Normal file
@ -0,0 +1,194 @@
|
||||
"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(".")
|
||||
);
|
||||
}
|
85
test/greenlock.js
Normal file
85
test/greenlock.js
Normal file
@ -0,0 +1,85 @@
|
||||
#!/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");
|
62
worker.js
Normal file
62
worker.js
Normal file
@ -0,0 +1,62 @@
|
||||
"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);
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user