Сравнить коммиты
Нет общих коммитов. «master» и «v1.x» имеют совершенно разные истории.
15
.gitignore
поставляемый
15
.gitignore
поставляемый
@ -1,8 +1,6 @@
|
|||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
|
||||||
.vscode
|
|
||||||
|
|
||||||
# Runtime data
|
# Runtime data
|
||||||
pids
|
pids
|
||||||
@ -15,9 +13,6 @@ lib-cov
|
|||||||
# Coverage directory used by tools like istanbul
|
# Coverage directory used by tools like istanbul
|
||||||
coverage
|
coverage
|
||||||
|
|
||||||
# nyc test coverage
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
.grunt
|
.grunt
|
||||||
|
|
||||||
@ -27,12 +22,6 @@ coverage
|
|||||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||||
build/Release
|
build/Release
|
||||||
|
|
||||||
# Dependency directories
|
# Dependency directory
|
||||||
|
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||||
node_modules
|
node_modules
|
||||||
jspm_packages
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional REPL history
|
|
||||||
.node_repl_history
|
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"bracketSpacing": true,
|
|
||||||
"printWidth": 120,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"trailingComma": "none",
|
|
||||||
"useTabs": true
|
|
||||||
}
|
|
575
LICENSE
575
LICENSE
@ -1,375 +1,202 @@
|
|||||||
Copyright 2015-2019 AJ ONeal
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright {yyyy} {name of copyright owner}
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
Mozilla Public License Version 2.0
|
|
||||||
==================================
|
|
||||||
|
|
||||||
1. Definitions
|
|
||||||
--------------
|
|
||||||
|
|
||||||
1.1. "Contributor"
|
|
||||||
means each individual or legal entity that creates, contributes to
|
|
||||||
the creation of, or owns Covered Software.
|
|
||||||
|
|
||||||
1.2. "Contributor Version"
|
|
||||||
means the combination of the Contributions of others (if any) used
|
|
||||||
by a Contributor and that particular Contributor's Contribution.
|
|
||||||
|
|
||||||
1.3. "Contribution"
|
|
||||||
means Covered Software of a particular Contributor.
|
|
||||||
|
|
||||||
1.4. "Covered Software"
|
|
||||||
means Source Code Form to which the initial Contributor has attached
|
|
||||||
the notice in Exhibit A, the Executable Form of such Source Code
|
|
||||||
Form, and Modifications of such Source Code Form, in each case
|
|
||||||
including portions thereof.
|
|
||||||
|
|
||||||
1.5. "Incompatible With Secondary Licenses"
|
|
||||||
means
|
|
||||||
|
|
||||||
(a) that the initial Contributor has attached the notice described
|
|
||||||
in Exhibit B to the Covered Software; or
|
|
||||||
|
|
||||||
(b) that the Covered Software was made available under the terms of
|
|
||||||
version 1.1 or earlier of the License, but not also under the
|
|
||||||
terms of a Secondary License.
|
|
||||||
|
|
||||||
1.6. "Executable Form"
|
|
||||||
means any form of the work other than Source Code Form.
|
|
||||||
|
|
||||||
1.7. "Larger Work"
|
|
||||||
means a work that combines Covered Software with other material, in
|
|
||||||
a separate file or files, that is not Covered Software.
|
|
||||||
|
|
||||||
1.8. "License"
|
|
||||||
means this document.
|
|
||||||
|
|
||||||
1.9. "Licensable"
|
|
||||||
means having the right to grant, to the maximum extent possible,
|
|
||||||
whether at the time of the initial grant or subsequently, any and
|
|
||||||
all of the rights conveyed by this License.
|
|
||||||
|
|
||||||
1.10. "Modifications"
|
|
||||||
means any of the following:
|
|
||||||
|
|
||||||
(a) any file in Source Code Form that results from an addition to,
|
|
||||||
deletion from, or modification of the contents of Covered
|
|
||||||
Software; or
|
|
||||||
|
|
||||||
(b) any new file in Source Code Form that contains any Covered
|
|
||||||
Software.
|
|
||||||
|
|
||||||
1.11. "Patent Claims" of a Contributor
|
|
||||||
means any patent claim(s), including without limitation, method,
|
|
||||||
process, and apparatus claims, in any patent Licensable by such
|
|
||||||
Contributor that would be infringed, but for the grant of the
|
|
||||||
License, by the making, using, selling, offering for sale, having
|
|
||||||
made, import, or transfer of either its Contributions or its
|
|
||||||
Contributor Version.
|
|
||||||
|
|
||||||
1.12. "Secondary License"
|
|
||||||
means either the GNU General Public License, Version 2.0, the GNU
|
|
||||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
|
||||||
Public License, Version 3.0, or any later versions of those
|
|
||||||
licenses.
|
|
||||||
|
|
||||||
1.13. "Source Code Form"
|
|
||||||
means the form of the work preferred for making modifications.
|
|
||||||
|
|
||||||
1.14. "You" (or "Your")
|
|
||||||
means an individual or a legal entity exercising rights under this
|
|
||||||
License. For legal entities, "You" includes any entity that
|
|
||||||
controls, is controlled by, or is under common control with You. For
|
|
||||||
purposes of this definition, "control" means (a) the power, direct
|
|
||||||
or indirect, to cause the direction or management of such entity,
|
|
||||||
whether by contract or otherwise, or (b) ownership of more than
|
|
||||||
fifty percent (50%) of the outstanding shares or beneficial
|
|
||||||
ownership of such entity.
|
|
||||||
|
|
||||||
2. License Grants and Conditions
|
|
||||||
--------------------------------
|
|
||||||
|
|
||||||
2.1. Grants
|
|
||||||
|
|
||||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
|
||||||
non-exclusive license:
|
|
||||||
|
|
||||||
(a) under intellectual property rights (other than patent or trademark)
|
|
||||||
Licensable by such Contributor to use, reproduce, make available,
|
|
||||||
modify, display, perform, distribute, and otherwise exploit its
|
|
||||||
Contributions, either on an unmodified basis, with Modifications, or
|
|
||||||
as part of a Larger Work; and
|
|
||||||
|
|
||||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
|
||||||
for sale, have made, import, and otherwise transfer either its
|
|
||||||
Contributions or its Contributor Version.
|
|
||||||
|
|
||||||
2.2. Effective Date
|
|
||||||
|
|
||||||
The licenses granted in Section 2.1 with respect to any Contribution
|
|
||||||
become effective for each Contribution on the date the Contributor first
|
|
||||||
distributes such Contribution.
|
|
||||||
|
|
||||||
2.3. Limitations on Grant Scope
|
|
||||||
|
|
||||||
The licenses granted in this Section 2 are the only rights granted under
|
|
||||||
this License. No additional rights or licenses will be implied from the
|
|
||||||
distribution or licensing of Covered Software under this License.
|
|
||||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
|
||||||
Contributor:
|
|
||||||
|
|
||||||
(a) for any code that a Contributor has removed from Covered Software;
|
|
||||||
or
|
|
||||||
|
|
||||||
(b) for infringements caused by: (i) Your and any other third party's
|
|
||||||
modifications of Covered Software, or (ii) the combination of its
|
|
||||||
Contributions with other software (except as part of its Contributor
|
|
||||||
Version); or
|
|
||||||
|
|
||||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
|
||||||
its Contributions.
|
|
||||||
|
|
||||||
This License does not grant any rights in the trademarks, service marks,
|
|
||||||
or logos of any Contributor (except as may be necessary to comply with
|
|
||||||
the notice requirements in Section 3.4).
|
|
||||||
|
|
||||||
2.4. Subsequent Licenses
|
|
||||||
|
|
||||||
No Contributor makes additional grants as a result of Your choice to
|
|
||||||
distribute the Covered Software under a subsequent version of this
|
|
||||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
|
||||||
permitted under the terms of Section 3.3).
|
|
||||||
|
|
||||||
2.5. Representation
|
|
||||||
|
|
||||||
Each Contributor represents that the Contributor believes its
|
|
||||||
Contributions are its original creation(s) or it has sufficient rights
|
|
||||||
to grant the rights to its Contributions conveyed by this License.
|
|
||||||
|
|
||||||
2.6. Fair Use
|
|
||||||
|
|
||||||
This License is not intended to limit any rights You have under
|
|
||||||
applicable copyright doctrines of fair use, fair dealing, or other
|
|
||||||
equivalents.
|
|
||||||
|
|
||||||
2.7. Conditions
|
|
||||||
|
|
||||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
|
||||||
in Section 2.1.
|
|
||||||
|
|
||||||
3. Responsibilities
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
3.1. Distribution of Source Form
|
|
||||||
|
|
||||||
All distribution of Covered Software in Source Code Form, including any
|
|
||||||
Modifications that You create or to which You contribute, must be under
|
|
||||||
the terms of this License. You must inform recipients that the Source
|
|
||||||
Code Form of the Covered Software is governed by the terms of this
|
|
||||||
License, and how they can obtain a copy of this License. You may not
|
|
||||||
attempt to alter or restrict the recipients' rights in the Source Code
|
|
||||||
Form.
|
|
||||||
|
|
||||||
3.2. Distribution of Executable Form
|
|
||||||
|
|
||||||
If You distribute Covered Software in Executable Form then:
|
|
||||||
|
|
||||||
(a) such Covered Software must also be made available in Source Code
|
|
||||||
Form, as described in Section 3.1, and You must inform recipients of
|
|
||||||
the Executable Form how they can obtain a copy of such Source Code
|
|
||||||
Form by reasonable means in a timely manner, at a charge no more
|
|
||||||
than the cost of distribution to the recipient; and
|
|
||||||
|
|
||||||
(b) You may distribute such Executable Form under the terms of this
|
|
||||||
License, or sublicense it under different terms, provided that the
|
|
||||||
license for the Executable Form does not attempt to limit or alter
|
|
||||||
the recipients' rights in the Source Code Form under this License.
|
|
||||||
|
|
||||||
3.3. Distribution of a Larger Work
|
|
||||||
|
|
||||||
You may create and distribute a Larger Work under terms of Your choice,
|
|
||||||
provided that You also comply with the requirements of this License for
|
|
||||||
the Covered Software. If the Larger Work is a combination of Covered
|
|
||||||
Software with a work governed by one or more Secondary Licenses, and the
|
|
||||||
Covered Software is not Incompatible With Secondary Licenses, this
|
|
||||||
License permits You to additionally distribute such Covered Software
|
|
||||||
under the terms of such Secondary License(s), so that the recipient of
|
|
||||||
the Larger Work may, at their option, further distribute the Covered
|
|
||||||
Software under the terms of either this License or such Secondary
|
|
||||||
License(s).
|
|
||||||
|
|
||||||
3.4. Notices
|
|
||||||
|
|
||||||
You may not remove or alter the substance of any license notices
|
|
||||||
(including copyright notices, patent notices, disclaimers of warranty,
|
|
||||||
or limitations of liability) contained within the Source Code Form of
|
|
||||||
the Covered Software, except that You may alter any license notices to
|
|
||||||
the extent required to remedy known factual inaccuracies.
|
|
||||||
|
|
||||||
3.5. Application of Additional Terms
|
|
||||||
|
|
||||||
You may choose to offer, and to charge a fee for, warranty, support,
|
|
||||||
indemnity or liability obligations to one or more recipients of Covered
|
|
||||||
Software. However, You may do so only on Your own behalf, and not on
|
|
||||||
behalf of any Contributor. You must make it absolutely clear that any
|
|
||||||
such warranty, support, indemnity, or liability obligation is offered by
|
|
||||||
You alone, and You hereby agree to indemnify every Contributor for any
|
|
||||||
liability incurred by such Contributor as a result of warranty, support,
|
|
||||||
indemnity or liability terms You offer. You may include additional
|
|
||||||
disclaimers of warranty and limitations of liability specific to any
|
|
||||||
jurisdiction.
|
|
||||||
|
|
||||||
4. Inability to Comply Due to Statute or Regulation
|
|
||||||
---------------------------------------------------
|
|
||||||
|
|
||||||
If it is impossible for You to comply with any of the terms of this
|
|
||||||
License with respect to some or all of the Covered Software due to
|
|
||||||
statute, judicial order, or regulation then You must: (a) comply with
|
|
||||||
the terms of this License to the maximum extent possible; and (b)
|
|
||||||
describe the limitations and the code they affect. Such description must
|
|
||||||
be placed in a text file included with all distributions of the Covered
|
|
||||||
Software under this License. Except to the extent prohibited by statute
|
|
||||||
or regulation, such description must be sufficiently detailed for a
|
|
||||||
recipient of ordinary skill to be able to understand it.
|
|
||||||
|
|
||||||
5. Termination
|
|
||||||
--------------
|
|
||||||
|
|
||||||
5.1. The rights granted under this License will terminate automatically
|
|
||||||
if You fail to comply with any of its terms. However, if You become
|
|
||||||
compliant, then the rights granted under this License from a particular
|
|
||||||
Contributor are reinstated (a) provisionally, unless and until such
|
|
||||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
|
||||||
ongoing basis, if such Contributor fails to notify You of the
|
|
||||||
non-compliance by some reasonable means prior to 60 days after You have
|
|
||||||
come back into compliance. Moreover, Your grants from a particular
|
|
||||||
Contributor are reinstated on an ongoing basis if such Contributor
|
|
||||||
notifies You of the non-compliance by some reasonable means, this is the
|
|
||||||
first time You have received notice of non-compliance with this License
|
|
||||||
from such Contributor, and You become compliant prior to 30 days after
|
|
||||||
Your receipt of the notice.
|
|
||||||
|
|
||||||
5.2. If You initiate litigation against any entity by asserting a patent
|
|
||||||
infringement claim (excluding declaratory judgment actions,
|
|
||||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
|
||||||
directly or indirectly infringes any patent, then the rights granted to
|
|
||||||
You by any and all Contributors for the Covered Software under Section
|
|
||||||
2.1 of this License shall terminate.
|
|
||||||
|
|
||||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
|
||||||
end user license agreements (excluding distributors and resellers) which
|
|
||||||
have been validly granted by You or Your distributors under this License
|
|
||||||
prior to termination shall survive termination.
|
|
||||||
|
|
||||||
************************************************************************
|
|
||||||
* *
|
|
||||||
* 6. Disclaimer of Warranty *
|
|
||||||
* ------------------------- *
|
|
||||||
* *
|
|
||||||
* Covered Software is provided under this License on an "as is" *
|
|
||||||
* basis, without warranty of any kind, either expressed, implied, or *
|
|
||||||
* statutory, including, without limitation, warranties that the *
|
|
||||||
* Covered Software is free of defects, merchantable, fit for a *
|
|
||||||
* particular purpose or non-infringing. The entire risk as to the *
|
|
||||||
* quality and performance of the Covered Software is with You. *
|
|
||||||
* Should any Covered Software prove defective in any respect, You *
|
|
||||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
|
||||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
|
||||||
* essential part of this License. No use of any Covered Software is *
|
|
||||||
* authorized under this License except under this disclaimer. *
|
|
||||||
* *
|
|
||||||
************************************************************************
|
|
||||||
|
|
||||||
************************************************************************
|
|
||||||
* *
|
|
||||||
* 7. Limitation of Liability *
|
|
||||||
* -------------------------- *
|
|
||||||
* *
|
|
||||||
* Under no circumstances and under no legal theory, whether tort *
|
|
||||||
* (including negligence), contract, or otherwise, shall any *
|
|
||||||
* Contributor, or anyone who distributes Covered Software as *
|
|
||||||
* permitted above, be liable to You for any direct, indirect, *
|
|
||||||
* special, incidental, or consequential damages of any character *
|
|
||||||
* including, without limitation, damages for lost profits, loss of *
|
|
||||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
|
||||||
* and all other commercial damages or losses, even if such party *
|
|
||||||
* shall have been informed of the possibility of such damages. This *
|
|
||||||
* limitation of liability shall not apply to liability for death or *
|
|
||||||
* personal injury resulting from such party's negligence to the *
|
|
||||||
* extent applicable law prohibits such limitation. Some *
|
|
||||||
* jurisdictions do not allow the exclusion or limitation of *
|
|
||||||
* incidental or consequential damages, so this exclusion and *
|
|
||||||
* limitation may not apply to You. *
|
|
||||||
* *
|
|
||||||
************************************************************************
|
|
||||||
|
|
||||||
8. Litigation
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Any litigation relating to this License may be brought only in the
|
|
||||||
courts of a jurisdiction where the defendant maintains its principal
|
|
||||||
place of business and such litigation shall be governed by laws of that
|
|
||||||
jurisdiction, without reference to its conflict-of-law provisions.
|
|
||||||
Nothing in this Section shall prevent a party's ability to bring
|
|
||||||
cross-claims or counter-claims.
|
|
||||||
|
|
||||||
9. Miscellaneous
|
|
||||||
----------------
|
|
||||||
|
|
||||||
This License represents the complete agreement concerning the subject
|
|
||||||
matter hereof. If any provision of this License is held to be
|
|
||||||
unenforceable, such provision shall be reformed only to the extent
|
|
||||||
necessary to make it enforceable. Any law or regulation which provides
|
|
||||||
that the language of a contract shall be construed against the drafter
|
|
||||||
shall not be used to construe this License against a Contributor.
|
|
||||||
|
|
||||||
10. Versions of the License
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
10.1. New Versions
|
|
||||||
|
|
||||||
Mozilla Foundation is the license steward. Except as provided in Section
|
|
||||||
10.3, no one other than the license steward has the right to modify or
|
|
||||||
publish new versions of this License. Each version will be given a
|
|
||||||
distinguishing version number.
|
|
||||||
|
|
||||||
10.2. Effect of New Versions
|
|
||||||
|
|
||||||
You may distribute the Covered Software under the terms of the version
|
|
||||||
of the License under which You originally received the Covered Software,
|
|
||||||
or under the terms of any subsequent version published by the license
|
|
||||||
steward.
|
|
||||||
|
|
||||||
10.3. Modified Versions
|
|
||||||
|
|
||||||
If you create software not governed by this License, and you want to
|
|
||||||
create a new license for such software, you may create and use a
|
|
||||||
modified version of this License if you rename the license and remove
|
|
||||||
any references to the name of the license steward (except to note that
|
|
||||||
such modified license differs from this License).
|
|
||||||
|
|
||||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
|
||||||
Licenses
|
|
||||||
|
|
||||||
If You choose to distribute Source Code Form that is Incompatible With
|
|
||||||
Secondary Licenses under the terms of this version of the License, the
|
|
||||||
notice described in Exhibit B of this License must be attached.
|
|
||||||
|
|
||||||
Exhibit A - Source Code Form License Notice
|
|
||||||
-------------------------------------------
|
|
||||||
|
|
||||||
This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
If it is not possible or desirable to put the notice in a particular
|
|
||||||
file, then You may include the notice in a location (such as a LICENSE
|
|
||||||
file in a relevant directory) where a recipient would be likely to look
|
|
||||||
for such a notice.
|
|
||||||
|
|
||||||
You may add additional accurate notices of copyright ownership.
|
|
||||||
|
|
||||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
|
||||||
---------------------------------------------------------
|
|
||||||
|
|
||||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
|
||||||
defined by the Mozilla Public License, v. 2.0.
|
|
||||||
|
800
README.md
800
README.md
@ -1,376 +1,478 @@
|
|||||||
# New Documentation & [v2/v3 Migration Guide](https://git.rootprojects.org/root/greenlock.js/src/branch/v3/MIGRATION_GUIDE_V2_V3.md)
|
[](https://gitter.im/Daplie/letsencrypt-express?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
|
|
||||||
Greenlock v3 just came out of private beta **today** (Nov 1st, 2019).
|
| [letsencrypt (library)](https://github.com/Daplie/node-letsencrypt)
|
||||||
|
| [letsencrypt-cli](https://github.com/Daplie/letsencrypt-cli)
|
||||||
|
| **letsencrypt-express**
|
||||||
|
| [letsencrypt-koa](https://github.com/Daplie/letsencrypt-koa)
|
||||||
|
| [letsencrypt-hapi](https://github.com/Daplie/letsencrypt-hapi)
|
||||||
|
|
|
||||||
|
|
||||||
The code is complete and we're working on great documentation.
|
# HELP WANTED
|
||||||
|
|
||||||
Many **examples** and **full API** documentation are still coming.
|
There are a number of easy to fix bugs (the most important of which is basically requires tracing some functions, doing some console.log-ing and returning the right type of object).
|
||||||
|
|
||||||
# [Greenlock Express](https://git.rootprojects.org/root/greenlock-express.js) is Let's Encrypt for Node
|
If you've got some free cycles to help, I can guide you through the process, I'm just still too busy to fix them right now and the workarounds work mentioned in the comments work.
|
||||||
|
|
||||||

|
Email me coolaj86@gmail.com if you want to help.
|
||||||
|
|
||||||
| Built by [Root](https://therootcompany.com) for [Hub](https://rootprojects.org/hub/)
|
# LetsEncrypt Express
|
||||||
|
|
||||||
Free SSL, Automated HTTPS / HTTP2, served with Node via Express, Koa, hapi, etc.
|
[](https://gitter.im/Daplie/letsencrypt-express?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
|
|
||||||
### Let's Encrypt for Node, Express, etc
|
Free SSL and managed or automatic HTTPS for node.js with Express, Koa, Connect, Hapi, and all other middleware systems.
|
||||||
|
|
||||||
Greenlock Express is a **Web Server** with **Fully Automated HTTPS** and renewals.
|
* Automatic Registration via SNI (`httpsOptions.SNICallback`)
|
||||||
|
* **registrations** require an **approval callback** in *production*
|
||||||
|
* Automatic Renewal (around 80 days)
|
||||||
|
* **renewals** are *fully automatic* and happen in the *background*, with **no downtime**
|
||||||
|
* Automatic vhost / virtual hosting
|
||||||
|
|
||||||
```js
|
All you have to do is start the webserver and then visit it at it's domain name.
|
||||||
"use strict";
|
|
||||||
|
|
||||||
function httpsWorker(glx) {
|
## Install
|
||||||
// Serves on 80 and 443
|
|
||||||
// Get's SSL certificates magically!
|
|
||||||
|
|
||||||
glx.serveApp(function(req, res) {
|
```
|
||||||
res.end("Hello, Encrypted World!");
|
npm install --save letsencrypt-express
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
* standalone
|
||||||
|
* express
|
||||||
|
* http / https
|
||||||
|
* http / http2 / spdy
|
||||||
|
* koa
|
||||||
|
|
||||||
|
### Setup (same for all examples)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* Note: using staging server url, remove .testing() for production
|
||||||
|
Using .testing() will overwrite the debug flag with true */
|
||||||
|
var LEX = require('letsencrypt-express').testing();
|
||||||
|
|
||||||
|
// Change these two lines!
|
||||||
|
var DOMAIN = 'myservice.example.com';
|
||||||
|
var EMAIL = 'user@example.com';
|
||||||
|
|
||||||
|
var lex = LEX.create({
|
||||||
|
configDir: require('os').homedir() + '/letsencrypt/etc'
|
||||||
|
, approveRegistration: function (hostname, approve) { // leave `null` to disable automatic registration
|
||||||
|
if (hostname === DOMAIN) { // Or check a database or list of allowed domains
|
||||||
|
approve(null, {
|
||||||
|
domains: [DOMAIN]
|
||||||
|
, email: EMAIL
|
||||||
|
, agreeTos: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
WARNING: If you don't do any checks and simply complete `approveRegistration` callback, an attacker will spoof SNI packets with bad hostnames and that will cause you to be rate-limited and or blocked from the ACME server. Alternatively, You can run registration *manually*:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g letsencrypt-cli
|
||||||
|
|
||||||
|
letsencrypt certonly --standalone \
|
||||||
|
--config-dir ~/letsencrypt/etc \
|
||||||
|
--agree-tos --domains example.com --email user@example.com
|
||||||
|
|
||||||
|
# Note: the '--webrootPath' option is also available if you don't want to shut down your webserver to get the cert.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Standalone
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
lex.onRequest = function (req, res) {
|
||||||
|
res.end('Hello, World!');
|
||||||
|
};
|
||||||
|
|
||||||
|
lex.listen([80], [443, 5001], function () {
|
||||||
|
console.log("ENCRYPT __ALL__ THE DOMAINS!");
|
||||||
|
});
|
||||||
|
|
||||||
|
// NOTE:
|
||||||
|
// `~/letsencrypt/etc` is the default `configDir`
|
||||||
|
// ports 80, 443, and 5001 are the default ports to listen on.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Express
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --save spdy
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// A happy little express app
|
||||||
|
var express = require('express');
|
||||||
|
var app = express();
|
||||||
|
|
||||||
|
app.use(function (req, res) {
|
||||||
|
res.send({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
lex.onRequest = app;
|
||||||
|
|
||||||
|
lex.listen([80], [443, 5001], function () {
|
||||||
|
var protocol = ('requestCert' in this) ? 'https': 'http';
|
||||||
|
console.log("Listening at " + protocol + '://localhost:' + this.address().port);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use with raw http / https modules
|
||||||
|
|
||||||
|
Let's say you want to redirect all http to https.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var http = require('http');
|
||||||
|
var https = require('spdy');
|
||||||
|
// NOTE: you could use the old https module if for some reason you don't want to support modern browsers
|
||||||
|
|
||||||
|
function redirectHttp() {
|
||||||
|
http.createServer(LEX.createAcmeResponder(lex, function redirectHttps(req, res) {
|
||||||
|
res.setHeader('Location', 'https://' + req.headers.host + req.url);
|
||||||
|
res.statusCode = 302; // use 307 if you want to redirect requests with POST, DELETE or PUT action.
|
||||||
|
res.end('<!-- Hello Developer Person! Please use HTTPS instead -->');
|
||||||
|
})).listen(80);
|
||||||
|
}
|
||||||
|
|
||||||
|
function serveHttps() {
|
||||||
|
var app = require('express')();
|
||||||
|
|
||||||
|
app.use('/', function (req, res) {
|
||||||
|
res.end('Hello!');
|
||||||
|
});
|
||||||
|
|
||||||
|
https.createServer(lex.httpsOptions, LEX.createAcmeResponder(lex, app)).listen(443);
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectHttp();
|
||||||
|
serveHttps();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Let's Encrypt with Koa
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var http = require('http');
|
||||||
|
var https = require('spdy'); // Note: some have reported trouble with `http2` and success with `spdy`
|
||||||
|
var koa = require('koa');
|
||||||
|
var app = koa();
|
||||||
|
var redirectHttps = koa().use(require('koa-sslify')()).callback();
|
||||||
|
|
||||||
|
app.use(function *() {
|
||||||
|
this.body = 'Hello World';
|
||||||
|
});
|
||||||
|
|
||||||
|
var server = https.createServer(lex.httpsOptions, LEX.createAcmeResponder(lex, app.callback()));
|
||||||
|
var redirectServer = http.createServer(LEX.createAcmeResponder(lex, redirectHttps)));
|
||||||
|
|
||||||
|
server.listen(443, function () {
|
||||||
|
console.log('Listening at https://localhost:' + this.address().port);
|
||||||
|
});
|
||||||
|
|
||||||
|
redirectServer.listen(80, function () {
|
||||||
|
console.log('Redirecting insecure traffic from http://localhost:' + this.address().port + ' to https');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSockets with Let's Encrypt
|
||||||
|
|
||||||
|
Note: you don't need to create websockets for the plain ports.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var WebSocketServer = require('ws').Server;
|
||||||
|
var https = require('spdy');
|
||||||
|
var server = https.createServer(lex.httpsOptions, LEX.createAcmeResponder(lex, app));
|
||||||
|
var wss = new WebSocketServer({ server: server });
|
||||||
|
|
||||||
|
wss.on('connection', onConnection);
|
||||||
|
server.listen(443);
|
||||||
|
|
||||||
|
function onConnection(ws) {
|
||||||
|
var location = url.parse(ws.upgradeReq.url, true);
|
||||||
|
// you might use location.query.access_token to authenticate or share sessions
|
||||||
|
// or ws.upgradeReq.headers.cookie (see http://stackoverflow.com/a/16395220/151312)
|
||||||
|
|
||||||
|
ws.on('message', function incoming(message) {
|
||||||
|
console.log('received: %s', message);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.send('something');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```
|
||||||
|
// checks options and sets up defaults. returns object with `listen`
|
||||||
|
LEX.create(options) // (it was really just done this way to appeal to what people are used to seeing)
|
||||||
|
|
||||||
|
lex.listen(plain, tls, fn) // actually creates the servers and causes them to listen
|
||||||
|
|
||||||
|
|
||||||
|
// receives an instance of letsencrypt, returns an SNICallback handler for https.createServer()
|
||||||
|
LEX.createSniCallback(opts) // this will call letsencrypt.renew and letsencrypt.register as appropriate
|
||||||
|
// it will randomly stagger renewals such that they don't all happen at once on boot
|
||||||
|
// or at any other time. registrations will be handled as per `handleRegistration`
|
||||||
|
opts = {
|
||||||
|
letsencrypt: <obj> // letsencrypt instance
|
||||||
|
, memorizeFor: <1 day> // how long to wait before checking the disk for updated certificates
|
||||||
|
, renewWithin: <3 days> // the first possible moment the certificate staggering should begin
|
||||||
|
, failedWait: <5 minutes> // how long to wait before trying again if the certificate registration failed
|
||||||
|
|
||||||
|
|
||||||
|
// registrations are NOT approved automatically by default due to security concerns
|
||||||
|
, approveRegistration: func // (someone can spoof servername indication to your server and cause you to be rate-limited)
|
||||||
|
// but you can implement handling of them if you wish
|
||||||
|
// (note that you should probably call the callback immediately with a tlsContext)
|
||||||
|
//
|
||||||
|
// default function (hostname, cb) { cb(null, null); }
|
||||||
|
//
|
||||||
|
// example function (hostname, cb) {
|
||||||
|
// cb(null, { domains: [hostname], agreeTos: true, email: 'user@example.com' });
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
, handleRenewFailure: func // renewals are automatic, but sometimes they may fail. If that happens, you should handle it
|
||||||
|
// (note that renewals happen in the background)
|
||||||
|
//
|
||||||
|
// default function (err, letsencrypt, hostname, certInfo) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// uses `opts.webrootPath` to read from the filesystem
|
||||||
|
LEX.getChallenge(opts, hostname, key cb)
|
||||||
|
|
||||||
|
LEX.createAcmeResponder(opts, fn) // this will return the necessary request handler for /.well-known/acme-challenges
|
||||||
|
// which then calls `fn` (such as express app) to complete the request
|
||||||
|
//
|
||||||
|
// opts lex instance created with LEX.create(opts)
|
||||||
|
// more generally, any object with a compatible `getChallenge` will work:
|
||||||
|
// `lex.getChallenge(opts, domain, key, function (err, val) {})`
|
||||||
|
//
|
||||||
|
// fn function (req, res) {
|
||||||
|
// console.log(req.method, req.url);
|
||||||
|
//
|
||||||
|
// res.end('Hello!');
|
||||||
|
// }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
If any of these values are `undefined` or `null` the will assume use reasonable defaults.
|
||||||
|
|
||||||
|
Partially defined values will be merged with the defaults.
|
||||||
|
|
||||||
|
Setting the value to `false` will, in many cases (as documented), disable the defaults.
|
||||||
|
|
||||||
|
```
|
||||||
|
configDir: string // string the letsencrypt configuration path (de facto /etc/letsencrypt)
|
||||||
|
//
|
||||||
|
// default os.homedir() + '/letsencrypt/etc'
|
||||||
|
|
||||||
|
|
||||||
|
webrootPath: string // string a path to a folder where temporary challenge files will be stored and read
|
||||||
|
//
|
||||||
|
// default os.tmpdir() + '/acme-challenge'
|
||||||
|
|
||||||
|
|
||||||
|
getChallenge: func | false // false do not handle getChallenge
|
||||||
|
//
|
||||||
|
// func Example:
|
||||||
|
//
|
||||||
|
// default function (defaults, hostname, key, cb) {
|
||||||
|
// var filename = path.join(defaults.webrootPath.replace(':hostname', hostname), key);
|
||||||
|
// fs.readFile(filename, 'ascii', function (cb, text) {
|
||||||
|
// cb(null, text);
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
httpsOptions: object // object will be merged with internal defaults and passed to https.createServer()
|
||||||
|
// { pfx, key, cert, passphrase, ca, ciphers, rejectUnauthorized, secureProtocol }
|
||||||
|
// See https://nodejs.org/api/https.html
|
||||||
|
// Note: if SNICallback is specified, it will be run *before*
|
||||||
|
// the internal SNICallback that manages automated certificates
|
||||||
|
//
|
||||||
|
// default uses a localhost cert and key to prevent https.createServer() from throwing an error
|
||||||
|
// and also uses our SNICallback, which manages certificates
|
||||||
|
|
||||||
|
|
||||||
|
sniCallback: func // func replace the default sniCallback handler (which manages certificates) with your own
|
||||||
|
|
||||||
|
|
||||||
|
letsencrypt: object // object configure the letsencrypt object yourself and pass it in directly
|
||||||
|
//
|
||||||
|
// default we create the letsencrypt object using parameters you specify
|
||||||
|
|
||||||
|
server: url // url use letsencrypt.productionServerUrl (i.e. https://acme-v01.api.letsencrypt.org/directory)
|
||||||
|
// or letsencrypt.stagingServerUrl (i.e. https://acme-staging.api.letsencrypt.org/directory)
|
||||||
|
//
|
||||||
|
// default production
|
||||||
|
```
|
||||||
|
|
||||||
|
## More Examples
|
||||||
|
|
||||||
|
### < 140 Characters
|
||||||
|
|
||||||
|
Let's Encrypt in 128 characters, with spaces!
|
||||||
|
|
||||||
|
```
|
||||||
|
node -e 'require("letsencrypt-express").testing().create( require("express")().use(function (_, r) { r.end("Hi!") }) ).listen()'
|
||||||
|
```
|
||||||
|
|
||||||
|
### More realistic
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Note: using staging server url, remove .testing() for production
|
||||||
|
var LEX = require('letsencrypt-express').testing();
|
||||||
|
var express = require('express');
|
||||||
|
var app = express();
|
||||||
|
|
||||||
|
app.use('/', function (req, res) {
|
||||||
|
res.send({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
LEX.create({
|
||||||
|
configDir: './letsencrypt.config' // ~/letsencrypt, /etc/letsencrypt, whatever you want
|
||||||
|
|
||||||
|
, onRequest: app // your express app (or plain node http app)
|
||||||
|
|
||||||
|
, letsencrypt: null // you can provide you own instance of letsencrypt
|
||||||
|
// if you need to configure it (with an agreeToTerms
|
||||||
|
// callback, for example)
|
||||||
|
|
||||||
|
, approveRegistration: function (hostname, cb) { // PRODUCTION MODE needs this function, but only if you want
|
||||||
|
// automatic registration (usually not necessary)
|
||||||
|
// renewals for registered domains will still be automatic
|
||||||
|
cb(null, {
|
||||||
|
domains: [hostname]
|
||||||
|
, email: 'user@example.com'
|
||||||
|
, agreeTos: true // you
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).listen([80], [443, 5001], function () {
|
||||||
|
console.log("ENCRYPT __ALL__ THE DOMAINS!");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### More Options Exposed
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var lex = require('letsencrypt-express');
|
||||||
|
var express = require('express');
|
||||||
|
var app = express();
|
||||||
|
|
||||||
|
app.use('/', function (req, res) {
|
||||||
|
res.send({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
var results = lex.create({
|
||||||
|
configDir: '/etc/letsencrypt'
|
||||||
|
, onRequest: app
|
||||||
|
, server: require('letsencrypt').productionServerUrl
|
||||||
|
}).listen(
|
||||||
|
|
||||||
|
// you can give just the port, or expand out to the full options
|
||||||
|
[80, { port: 8080, address: 'localhost', onListening: function () { console.log('http://localhost'); } }]
|
||||||
|
|
||||||
|
// you can give just the port, or expand out to the full options
|
||||||
|
, [443, 5001, { port: 8443, address: 'localhost' }]
|
||||||
|
|
||||||
|
// this is pretty much the default onListening handler
|
||||||
|
, function onListening() {
|
||||||
|
var server = this;
|
||||||
|
var protocol = ('requestCert' in server) ? 'https': 'http';
|
||||||
|
console.log("Listening at " + protocol + '://localhost:' + this.address().port);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// In case you need access to the raw servers (i.e. using websockets)
|
||||||
|
console.log(results.plainServers);
|
||||||
|
console.log(results.tlsServers);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### All Options Exposed
|
||||||
|
|
||||||
|
Here's absolutely every option and function exposed
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var http = require('http');
|
||||||
|
var https = require('spdy');
|
||||||
|
var LEX = require('letsencrypt-express');
|
||||||
|
var LE = require('letsencrypt');
|
||||||
|
var lex;
|
||||||
|
|
||||||
|
lex = LEX.create({
|
||||||
|
webrootPath: '/tmp/.well-known/acme-challenge'
|
||||||
|
|
||||||
|
, lifetime: 90 * 24 * 60 * 60 * 1000 // expect certificates to last 90 days
|
||||||
|
, failedWait: 5 * 60 * 1000 // if registering fails wait 5 minutes before trying again
|
||||||
|
, renewWithin: 3 * 24 * 60 * 60 * 1000 // renew at least 3 days before expiration
|
||||||
|
, memorizeFor: 1 * 24 * 60 * 60 * 1000 // keep certificates in memory for 1 day
|
||||||
|
|
||||||
|
, approveRegistration: function (hostname, cb) {
|
||||||
|
cb(null, {
|
||||||
|
domains: [hostname]
|
||||||
|
, email: 'user@example.com'
|
||||||
|
, agreeTos: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var pkg = require("./package.json");
|
, handleRenewFailure: function (err, hostname, certInfo) {
|
||||||
require("greenlock-express")
|
console.error("ERROR: Failed to renew domain '", hostname, "':");
|
||||||
.init(function getConfig() {
|
if (err) {
|
||||||
// Greenlock Config
|
console.error(err.stack || err);
|
||||||
|
}
|
||||||
return {
|
if (certInfo) {
|
||||||
package: { name: pkg.name, version: pkg.version },
|
console.error(certInfo);
|
||||||
maintainerEmail: pkg.author,
|
|
||||||
cluster: false
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.serve(httpsWorker);
|
|
||||||
```
|
|
||||||
|
|
||||||
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"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
# Let's Encrypt for...
|
, letsencrypt: LE.create(
|
||||||
|
// options
|
||||||
|
{ configDir: './letsencrypt.config'
|
||||||
|
, manual: true
|
||||||
|
|
||||||
- IoT
|
, server: LE.productionServerUrl
|
||||||
- Enterprise On-Prem
|
, privkeyPath: LE.privkeyPath
|
||||||
- Local Development
|
, fullchainPath: LE.fullchainPath
|
||||||
- Home Servers
|
, certPath: LE.certPath
|
||||||
- Quitting Heroku
|
, chainPath: LE.chainPath
|
||||||
|
, renewalPath: LE.renewalPath
|
||||||
|
, accountsDir: LE.accountsDir
|
||||||
|
|
||||||
# Features
|
, debug: false
|
||||||
|
|
||||||
- [x] Let's Encrypt v2 (November 2019)
|
|
||||||
- [x] ACME Protocol (RFC 8555)
|
|
||||||
- [x] HTTP Validation (HTTP-01)
|
|
||||||
- [x] DNS Validation (DNS-01)
|
|
||||||
- [ ] ALPN Validation (TLS-ALPN-01)
|
|
||||||
- Need ALPN validation? [contact us](mailto:greenlock-support@therootcompany.com)
|
|
||||||
- [x] Automated HTTPS
|
|
||||||
- [x] Fully Automatic Renewals every 45 days
|
|
||||||
- [x] Free SSL
|
|
||||||
- [x] **Wildcard** SSL
|
|
||||||
- [x] **Localhost** certificates
|
|
||||||
- [x] HTTPS-enabled Secure **WebSockets** (`wss://`)
|
|
||||||
- [x] Fully customizable
|
|
||||||
- [x] **Reasonable defaults**
|
|
||||||
- [x] Domain Management
|
|
||||||
- [x] Key and Certificate Management
|
|
||||||
- [x] ACME Challenge Plugins
|
|
||||||
|
|
||||||
# QuickStart Guide
|
|
||||||
|
|
||||||
Easy as 1, 2, 3... 4
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>1. Create a node project</summary>
|
|
||||||
|
|
||||||
## 1. Create a node project
|
|
||||||
|
|
||||||
Create an empty node project.
|
|
||||||
|
|
||||||
Be sure to fill out the package name, version, and an author email.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir ~/my-project
|
|
||||||
pushd ~/my-project
|
|
||||||
npm init
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>2. Create an http app (i.e. express)</summary>
|
|
||||||
|
|
||||||
## 2. Create an http app (i.e. express)
|
|
||||||
|
|
||||||
This example is shown with Express, but any node app will do. Greenlock
|
|
||||||
works with everything.
|
|
||||||
(or any node-style http app)
|
|
||||||
|
|
||||||
`my-express-app.js`:
|
|
||||||
|
|
||||||
```js
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// A plain, node-style app
|
|
||||||
|
|
||||||
function myPlainNodeHttpApp(req, res) {
|
|
||||||
res.end("Hello, Encrypted World!");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap that plain app in express,
|
// handlers
|
||||||
// because that's what you're used to
|
, { setChallenge: LEX.setChallenge
|
||||||
|
, removeChallenge: LEX.removeChallenge
|
||||||
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) {
|
, debug: false
|
||||||
// Works with any Node app (Express, etc)
|
});
|
||||||
var app = require("my-express-app.js");
|
|
||||||
server.serveApp(app);
|
http.createServer(LEX.createAcmeResponder(lex, function (req, res) {
|
||||||
}
|
res.setHeader('Location', 'https://' + req.headers.host + req.url);
|
||||||
|
res.end('<!-- Hello Mr Developer! Please use HTTPS instead -->');
|
||||||
|
}));
|
||||||
|
|
||||||
|
https.createServer(lex.httpsOptions, LEX.createAcmeResponder(lex, function (req, res) {
|
||||||
|
res.end('Hello!');
|
||||||
|
}));
|
||||||
```
|
```
|
||||||
|
|
||||||
And start your server:
|
## Heroku?
|
||||||
|
|
||||||
```bash
|
This doesn't work on heroku because heroku uses a proxy with built-in https
|
||||||
# Allow non-root node to use ports 80 (HTTP) and 443 (HTTPS)
|
(which is a smart thing to do) and besides, they want you to pay big bucks
|
||||||
sudo setcap 'cap_net_bind_service=+ep' $(which node)
|
for https. (hopefully not for long?...)
|
||||||
```
|
|
||||||
|
|
||||||
```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)
|
|
||||||
|
226
bin/lex.js
Исполняемый файл
226
bin/lex.js
Исполняемый файл
@ -0,0 +1,226 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var fs = require('fs');
|
||||||
|
var path = require('path');
|
||||||
|
var mkdirp = require('fs');
|
||||||
|
var cli = require('cli');
|
||||||
|
var mkdirp = require('mkdirp');
|
||||||
|
var homedir = require('os').homedir();
|
||||||
|
var configDir = path.join(homedir, 'letsencrypt');
|
||||||
|
var desktop = path.join(homedir, 'Desktop');
|
||||||
|
var vhostDir = path.join(fs.existsSync(desktop) ? desktop : configDir, 'www');
|
||||||
|
var welcomeHtml = fs.readFileSync(path.join(__dirname, '..', 'lib', 'public', 'welcome.html'), 'utf8');
|
||||||
|
var express = require('express');
|
||||||
|
|
||||||
|
cli.parse({
|
||||||
|
'agree-tos': [ false, " Agree to the Let's Encrypt Subscriber Agreement", 'boolean', false ]
|
||||||
|
, email: [ false, " Email used for registration and recovery contact. (default: null)", 'email' ]
|
||||||
|
, domains: [ false, " Domain names to apply. To include the www domain with your main domain your can enter both with a comma. Ex --domains example.com,www.example.com (default: [])", 'string' ]
|
||||||
|
, debug: [ false, " show traces and logs", 'boolean', false ]
|
||||||
|
, server: [ false, " ACME Directory Resource URI.", 'string', 'https://acme-v01.api.letsencrypt.org/directory)' ]
|
||||||
|
});
|
||||||
|
|
||||||
|
// ignore certonly and extraneous arguments
|
||||||
|
cli.main(function(_, options) {
|
||||||
|
console.log('');
|
||||||
|
var args = {};
|
||||||
|
|
||||||
|
Object.keys(options).forEach(function (key) {
|
||||||
|
var val = options[key];
|
||||||
|
|
||||||
|
if ('string' === typeof val) {
|
||||||
|
val = val.replace(/^~/, homedir);
|
||||||
|
}
|
||||||
|
|
||||||
|
key = key.replace(/\-([a-z0-9A-Z])/g, function (c) { return c[1].toUpperCase(); });
|
||||||
|
args[key] = val;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (args.domains) {
|
||||||
|
args.domains = args.domains.split(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
makeDirectories();
|
||||||
|
|
||||||
|
function makeDirectories() {
|
||||||
|
mkdirp(configDir, function (err) {
|
||||||
|
if (err) {
|
||||||
|
console.error("Could not create config directory '" + configDir + "':", err.code);
|
||||||
|
console.error(err.stack);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdirp(vhostDir, function (err) {
|
||||||
|
if (err) {
|
||||||
|
console.error("Could not create vhost directory '" + vhostDir + "':", err.code);
|
||||||
|
console.error(err.stack);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
startServers();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function configure(le, args, cb) {
|
||||||
|
var vhost;
|
||||||
|
var pubDir;
|
||||||
|
var index;
|
||||||
|
|
||||||
|
if (!(args.email && args.agreeTos && args.server && args.domains)) {
|
||||||
|
cb({ error : { message: "missing one or more of agreeTos,domains,email,server" } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vhost = args.domains[0];
|
||||||
|
pubDir = path.join(vhostDir, vhost);
|
||||||
|
index = path.join(pubDir, 'index.html');
|
||||||
|
|
||||||
|
makeLandingPage();
|
||||||
|
|
||||||
|
function makeLandingPage() {
|
||||||
|
mkdirp(pubDir, function (err) {
|
||||||
|
if (err) {
|
||||||
|
cb(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.exists(index, function (exists) {
|
||||||
|
if (exists) {
|
||||||
|
configureForHttps();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFile(path.join(pubDir, 'index.html'), welcomeHtml.replace(/:hostname/g, vhost), 'utf8', function (err) {
|
||||||
|
if (err) {
|
||||||
|
cb(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
configureForHttps();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function configureForHttps() {
|
||||||
|
if (args.debug) {
|
||||||
|
console.log('[LEX] configureForHttps');
|
||||||
|
console.log(args);
|
||||||
|
}
|
||||||
|
le.setConfig(args, cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createConfigurator(le) {
|
||||||
|
var app = express();
|
||||||
|
|
||||||
|
app.use('/', express.static(path.join(__dirname, '..', 'lib', 'configurator')));
|
||||||
|
|
||||||
|
app.use(require('body-parser').json());
|
||||||
|
|
||||||
|
app.get('/api/com.daplie.lex/sites', function (req, res, next) {
|
||||||
|
le.getConfigs({ configDir: configDir }, function (err, configs) {
|
||||||
|
if (err) {
|
||||||
|
next(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.send(configs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/com.daplie.lex/sites', function (req, res, next) {
|
||||||
|
var data = req.body;
|
||||||
|
|
||||||
|
configure(le, data, function (err, configs) {
|
||||||
|
if (err) {
|
||||||
|
console.error("[LEX/bin] configure");
|
||||||
|
console.error(err.stack);
|
||||||
|
next(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send(configs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startServers() {
|
||||||
|
// Note: using staging server url, remove .testing() for production
|
||||||
|
var LE = require('letsencrypt');
|
||||||
|
var LEX = require('../');
|
||||||
|
var le = LE.create({
|
||||||
|
configDir: configDir
|
||||||
|
, manual: true
|
||||||
|
|
||||||
|
, privkeyPath: LE.privkeyPath
|
||||||
|
, fullchainPath: LE.fullchainPath
|
||||||
|
, certPath: LE.certPath
|
||||||
|
, chainPath: LE.chainPath
|
||||||
|
, renewalPath: LE.renewalPath
|
||||||
|
, accountsDir: LE.accountsDir
|
||||||
|
}, {
|
||||||
|
setChallenge: LEX.setChallenge
|
||||||
|
, removeChallenge: LEX.removeChallenge
|
||||||
|
});
|
||||||
|
var app = express();
|
||||||
|
var vhosts = {};
|
||||||
|
|
||||||
|
vhosts['localhost.daplie.com'] = createConfigurator(le, vhosts);
|
||||||
|
|
||||||
|
app.use('/', function (req, res, next) {
|
||||||
|
var hostname = (req.hostname||req.headers.host||'').replace(/^www\./, '');
|
||||||
|
var pubDir = path.join(vhostDir, hostname);
|
||||||
|
|
||||||
|
if (vhosts[hostname]) {
|
||||||
|
vhosts[hostname](req, res, next);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.exists(pubDir, function (exists) {
|
||||||
|
if (exists) {
|
||||||
|
vhosts[hostname] = express().use('/', express.static(pubDir));
|
||||||
|
vhosts[hostname](req, res, next);
|
||||||
|
} else {
|
||||||
|
vhosts['localhost.daplie.com'](req, res, next);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
app.use('/', express.static(path.join(__dirname, '..', 'lib', 'public')));
|
||||||
|
|
||||||
|
LEX.create({
|
||||||
|
onRequest: app
|
||||||
|
, configDir: configDir
|
||||||
|
, letsencrypt: le
|
||||||
|
, approveRegistration: function (domain, cb) {
|
||||||
|
le.getConfig({ domains: [domain] }, function (err, config) {
|
||||||
|
if (!(config && config.checkpoints >= 0)) {
|
||||||
|
cb(null, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(null, {
|
||||||
|
email: config.email
|
||||||
|
// can't remember which it is, but the pyconf is different that the regular variable
|
||||||
|
, agreeTos: config.tos || config.agree || config.agreeTos
|
||||||
|
, server: config.server || LE.productionServerUrl
|
||||||
|
, domains: config.domains || [domain]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).listen([80], [443, 5001]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// should get back account, path to certs, pems, etc?
|
||||||
|
console.log('\nCertificates installed at:');
|
||||||
|
console.log(Object.keys(results).filter(function (key) {
|
||||||
|
return /Path/.test(key);
|
||||||
|
}).map(function (key) {
|
||||||
|
return results[key];
|
||||||
|
}).join('\n'));
|
||||||
|
*/
|
||||||
|
});
|
20
config.js
20
config.js
@ -1,20 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
var path = require("path");
|
|
||||||
module.exports = {
|
|
||||||
email: "jon.doe@example.com",
|
|
||||||
configDir: path.join(__dirname, "acme"),
|
|
||||||
srv: "/srv/www/",
|
|
||||||
api: "/srv/api/",
|
|
||||||
proxy: {
|
|
||||||
"example.com": "http://localhost:4080",
|
|
||||||
"*.example.com": "http://localhost:4080"
|
|
||||||
},
|
|
||||||
|
|
||||||
// DNS-01 challenges only
|
|
||||||
challenges: {
|
|
||||||
"*.example.com": require("acme-dns-01-YOUR_DNS_HOST").create({
|
|
||||||
token: "xxxx"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
35
demo.js
35
demo.js
@ -1,35 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
require("./")
|
|
||||||
.init(initialize)
|
|
||||||
.serve(worker)
|
|
||||||
.master(function() {
|
|
||||||
console.log("Hello from master");
|
|
||||||
});
|
|
||||||
|
|
||||||
function initialize() {
|
|
||||||
var pkg = require("./package.json");
|
|
||||||
var config = {
|
|
||||||
package: {
|
|
||||||
name: "Greenlock_Express_Demo",
|
|
||||||
version: pkg.version,
|
|
||||||
author: pkg.author
|
|
||||||
},
|
|
||||||
staging: true,
|
|
||||||
cluster: true,
|
|
||||||
|
|
||||||
notify: function(ev, params) {
|
|
||||||
console.info(ev, params);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
function worker(glx) {
|
|
||||||
console.info();
|
|
||||||
console.info("Hello from worker #" + glx.id());
|
|
||||||
|
|
||||||
glx.serveApp(function(req, res) {
|
|
||||||
res.end("Hello, Encrypted World!");
|
|
||||||
});
|
|
||||||
}
|
|
53
dist/etc/systemd/system/greenlock-express.service
поставляемый
53
dist/etc/systemd/system/greenlock-express.service
поставляемый
@ -1,53 +0,0 @@
|
|||||||
# sudo systemctl daemon-reload
|
|
||||||
# sudo systemctl restart greenlock-express
|
|
||||||
# sudo journalctl -xefu greenlock-express
|
|
||||||
[Unit]
|
|
||||||
Description=Greenlock Static Server
|
|
||||||
Documentation=https://git.coolaj86.com/coolaj86/greenlock-express.js/
|
|
||||||
After=network.target
|
|
||||||
Wants=network.target systemd-networkd-wait-online.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
# Restart on crash (bad signal), 'clean' failure (error exit code), everything
|
|
||||||
# Allow up to 3 restarts within 10 seconds
|
|
||||||
# (it's unlikely that a user or properly-running script will do this)
|
|
||||||
Restart=always
|
|
||||||
StartLimitInterval=10
|
|
||||||
StartLimitBurst=3
|
|
||||||
|
|
||||||
# User and group the process will run as
|
|
||||||
# (git is the de facto standard on most systems)
|
|
||||||
User=ubuntu
|
|
||||||
Group=ubuntu
|
|
||||||
|
|
||||||
WorkingDirectory=/srv/www
|
|
||||||
# custom directory cannot be set and will be the place where gitea exists, not the working directory
|
|
||||||
ExecStart=/opt/node/bin/node /opt/greenlock-express.js/server.js /opt/greenlock-express.js/config.js
|
|
||||||
|
|
||||||
# Limit the number of file descriptors and processes; see `man systemd.exec` for more limit settings.
|
|
||||||
# greenlock is not expected to use more than this.
|
|
||||||
LimitNOFILE=1048576
|
|
||||||
LimitNPROC=64
|
|
||||||
|
|
||||||
# Use private /tmp and /var/tmp, which are discarded after gitea stops.
|
|
||||||
PrivateTmp=true
|
|
||||||
# Use a minimal /dev
|
|
||||||
PrivateDevices=true
|
|
||||||
# Hide /home, /root, and /run/user. Nobody will steal your SSH-keys.
|
|
||||||
ProtectHome=true
|
|
||||||
# Make /usr, /boot, /etc and possibly some more folders read-only.
|
|
||||||
ProtectSystem=full
|
|
||||||
# ... except /opt/greenlock-express.js/acme because we want a place for the database
|
|
||||||
# and /opt/greenlock-express.js/var because we want a place where logs can go.
|
|
||||||
# This merely retains r/w access rights, it does not add any new.
|
|
||||||
# Must still be writable on the host!
|
|
||||||
ReadWriteDirectories=/srv/www /opt/greenlock-express.js
|
|
||||||
|
|
||||||
# Note: in v231 and above ReadWritePaths has been renamed to ReadWriteDirectories
|
|
||||||
; ReadWritePaths=/opt/gitea /var/log/gitea
|
|
||||||
|
|
||||||
# The following additional security directives only work with systemd v229 or later.
|
|
||||||
# They further retrict privileges that can be gained by gitea.
|
|
||||||
# Note that you may have to add capabilities required by any plugins in use.
|
|
||||||
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
|
||||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
|
9
examples/116.js
Обычный файл
9
examples/116.js
Обычный файл
@ -0,0 +1,9 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Don't try this at home kids, it's just for fun
|
||||||
|
//
|
||||||
|
|
||||||
|
// require('letsencrypt-express')
|
||||||
|
require('../').testing().create(require('express')().use(function (_, r) {
|
||||||
|
r.end('Hi!');
|
||||||
|
})).listen();
|
@ -1,39 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
var pkg = require("../../package.json");
|
|
||||||
//require("greenlock-express")
|
|
||||||
require("../../")
|
|
||||||
.init(function getConfig() {
|
|
||||||
// Greenlock Config
|
|
||||||
|
|
||||||
return {
|
|
||||||
package: { name: "websocket-example", version: pkg.version },
|
|
||||||
maintainerEmail: "jon@example.com",
|
|
||||||
|
|
||||||
// When you're ready to go full cloud scale, you just change this to true:
|
|
||||||
// Note: in cluster you CANNOT use in-memory state (see below)
|
|
||||||
cluster: true,
|
|
||||||
|
|
||||||
// This will default to the number of workers being equal to
|
|
||||||
// n-1 cpus, with a minimum of 2
|
|
||||||
workers: 4
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.serve(httpsWorker);
|
|
||||||
|
|
||||||
function httpsWorker(glx) {
|
|
||||||
// WRONG
|
|
||||||
// This won't work like you
|
|
||||||
// think because EACH worker
|
|
||||||
// has ITS OWN `count`.
|
|
||||||
var count = 0;
|
|
||||||
|
|
||||||
var app = function(req, res) {
|
|
||||||
res.end("Hello... how many times now? Oh, " + count + " times");
|
|
||||||
count += 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Serves on 80 and 443... for each worker
|
|
||||||
// Get's SSL certificates magically!
|
|
||||||
glx.serveApp(app);
|
|
||||||
}
|
|
50
examples/express.js
Обычный файл
50
examples/express.js
Обычный файл
@ -0,0 +1,50 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// allow node.js to bind to port 80 and 443 without root:
|
||||||
|
//
|
||||||
|
// sudo setcap 'cap_net_bind_service=+ep' `which node`
|
||||||
|
|
||||||
|
/* Note: using staging server url, remove .testing() for production
|
||||||
|
Using .testing() will overwrite the debug flag with true */
|
||||||
|
var LEX = require('letsencrypt-express').testing();
|
||||||
|
var http = require('http');
|
||||||
|
// NOTE: you could use the old https module if for some reason
|
||||||
|
// you don't want to support modern browsers
|
||||||
|
var https = require('spdy');
|
||||||
|
var leConfDir = require('os').homedir() + '/letsencrypt/etc';
|
||||||
|
throw new Error(
|
||||||
|
"You must edit the example to change the email address (and remove this error)."
|
||||||
|
+ " Also, you'll need to remove .testing() and rm -rf '" + leConfDir + "'"
|
||||||
|
+ " to get actual, trusted production certificates.");
|
||||||
|
var lex = LEX.create({
|
||||||
|
configDir: leConfDir
|
||||||
|
, approveRegistration: function (hostname, cb) { // leave `null` to disable automatic registration
|
||||||
|
// Note: this is the place to check your database to get the user associated with this domain
|
||||||
|
cb(null, {
|
||||||
|
domains: [hostname]
|
||||||
|
, email: 'CHANGE_ME' // user@example.com
|
||||||
|
, agreeTos: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function redirectHttp() {
|
||||||
|
http.createServer(LEX.createAcmeResponder(lex, function redirectHttps(req, res) {
|
||||||
|
res.setHeader('Location', 'https://' + req.headers.host + req.url);
|
||||||
|
res.statusCode = 302;
|
||||||
|
res.end('<!-- Hello Developer Person! Please use HTTPS instead -->');
|
||||||
|
})).listen(80);
|
||||||
|
}
|
||||||
|
|
||||||
|
function serveHttps() {
|
||||||
|
var app = require('express')();
|
||||||
|
|
||||||
|
app.use('/', function (req, res) {
|
||||||
|
res.end('Hello!');
|
||||||
|
});
|
||||||
|
|
||||||
|
https.createServer(lex.httpsOptions, LEX.createAcmeResponder(lex, app)).listen(443);
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectHttp();
|
||||||
|
serveHttps();
|
@ -1,17 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
var express = require("express");
|
|
||||||
var app = express();
|
|
||||||
|
|
||||||
app.use("/", function(req, res) {
|
|
||||||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
||||||
res.end("Hello, World!\n\n💚 🔒.js");
|
|
||||||
});
|
|
||||||
|
|
||||||
// DO NOT DO app.listen() unless we're testing this directly
|
|
||||||
if (require.main === module) {
|
|
||||||
app.listen(3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Instead do export the app:
|
|
||||||
module.exports = app;
|
|
@ -1,27 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
function httpsWorker(glx) {
|
|
||||||
var app = require("./my-express-app.js");
|
|
||||||
|
|
||||||
app.get("/hello", function(req, res) {
|
|
||||||
res.end("Hello, Encrypted World!");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Serves on 80 and 443
|
|
||||||
// Get's SSL certificates magically!
|
|
||||||
glx.serveApp(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
var pkg = require("../../package.json");
|
|
||||||
//require("greenlock-express")
|
|
||||||
require("../../")
|
|
||||||
.init(function getConfig() {
|
|
||||||
// Greenlock Config
|
|
||||||
|
|
||||||
return {
|
|
||||||
package: { name: "http2-example", version: pkg.version },
|
|
||||||
maintainerEmail: "jon@example.com",
|
|
||||||
cluster: false
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.serve(httpsWorker);
|
|
@ -1,44 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
function httpsWorker(glx) {
|
|
||||||
// we need the raw https server
|
|
||||||
var server = glx.httpsServer();
|
|
||||||
var proxy = require("http-proxy").createProxyServer({ xfwd: true });
|
|
||||||
|
|
||||||
// catches error events during proxying
|
|
||||||
proxy.on("error", function(err, req, res) {
|
|
||||||
console.error(err);
|
|
||||||
res.statusCode = 500;
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
|
|
||||||
// We'll proxy websockets too
|
|
||||||
server.on("upgrade", function(req, socket, head) {
|
|
||||||
proxy.ws(req, socket, head, {
|
|
||||||
ws: true,
|
|
||||||
target: "ws://localhost:3000"
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// servers a node app that proxies requests to a localhost
|
|
||||||
glx.serveApp(function(req, res) {
|
|
||||||
proxy.web(req, res, {
|
|
||||||
target: "http://localhost:3000"
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var pkg = require("../../package.json");
|
|
||||||
//require("greenlock-express")
|
|
||||||
require("../../")
|
|
||||||
.init(function getConfig() {
|
|
||||||
// Greenlock Config
|
|
||||||
|
|
||||||
return {
|
|
||||||
package: { name: "http-proxy-example", version: pkg.version },
|
|
||||||
maintainerEmail: "jon@example.com",
|
|
||||||
cluster: false
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.serve(httpsWorker);
|
|
@ -1,42 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
var pkg = require("../../package.json");
|
|
||||||
|
|
||||||
// The WRONG way:
|
|
||||||
//var http = require('http');
|
|
||||||
//var httpServer = https.createSecureServer(redirectToHttps);
|
|
||||||
//
|
|
||||||
// Why is that wrong?
|
|
||||||
// Greenlock needs to change some low-level http and https options.
|
|
||||||
// Use glx.httpServer(redirectToHttps) instead.
|
|
||||||
|
|
||||||
function httpsWorker(glx) {
|
|
||||||
//
|
|
||||||
// HTTP can only be used for ACME HTTP-01 Challenges
|
|
||||||
// (and it is not required for DNS-01 challenges)
|
|
||||||
//
|
|
||||||
|
|
||||||
// Get the raw http server:
|
|
||||||
var httpServer = glx.httpServer(function(req, res) {
|
|
||||||
res.statusCode = 301;
|
|
||||||
res.setHeader("Location", "https://" + req.headers.host + req.path);
|
|
||||||
res.end("Insecure connections are not allowed. Redirecting...");
|
|
||||||
});
|
|
||||||
|
|
||||||
httpServer.listen(80, "0.0.0.0", function() {
|
|
||||||
console.info("Listening on ", httpServer.address());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//require("greenlock-express")
|
|
||||||
require("../../")
|
|
||||||
.init(function getConfig() {
|
|
||||||
// Greenlock Config
|
|
||||||
|
|
||||||
return {
|
|
||||||
package: { name: "plain-http-example", version: pkg.version },
|
|
||||||
maintainerEmail: "jon@example.com",
|
|
||||||
cluster: false
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.serve(httpsWorker);
|
|
@ -1,48 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
var pkg = require("../../package.json");
|
|
||||||
|
|
||||||
// The WRONG way:
|
|
||||||
//var http2 = require('http2');
|
|
||||||
//var http2Server = https.createSecureServer(tlsOptions, app);
|
|
||||||
//
|
|
||||||
// Why is that wrong?
|
|
||||||
// Greenlock needs to change some low-level http and https options.
|
|
||||||
// Use glx.httpsServer(tlsOptions, app) instead.
|
|
||||||
|
|
||||||
function httpsWorker(glx) {
|
|
||||||
//
|
|
||||||
// HTTP2 is the default httpsServer for node v12+
|
|
||||||
// (HTTPS/1.1 is used for node <= v11)
|
|
||||||
//
|
|
||||||
|
|
||||||
// Get the raw http2 server:
|
|
||||||
var http2Server = glx.httpsServer(function(req, res) {
|
|
||||||
res.end("Hello, Encrypted World!");
|
|
||||||
});
|
|
||||||
|
|
||||||
http2Server.listen(443, "0.0.0.0", function() {
|
|
||||||
console.info("Listening on ", http2Server.address());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Note:
|
|
||||||
// You must ALSO listen on port 80 for ACME HTTP-01 Challenges
|
|
||||||
// (the ACME and http->https middleware are loaded by glx.httpServer)
|
|
||||||
var httpServer = glx.httpServer();
|
|
||||||
httpServer.listen(80, "0.0.0.0", function() {
|
|
||||||
console.info("Listening on ", httpServer.address());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//require("greenlock-express")
|
|
||||||
require("../../")
|
|
||||||
.init(function getConfig() {
|
|
||||||
// Greenlock Config
|
|
||||||
|
|
||||||
return {
|
|
||||||
package: { name: "http2-example", version: pkg.version },
|
|
||||||
maintainerEmail: "jon@example.com",
|
|
||||||
cluster: false
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.serve(httpsWorker);
|
|
@ -1,49 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
var pkg = require("../../package.json");
|
|
||||||
|
|
||||||
// The WRONG way:
|
|
||||||
//var https = require('https');
|
|
||||||
//var httpsServer = https.createServer(tlsOptions, app);
|
|
||||||
//
|
|
||||||
// Why is that wrong?
|
|
||||||
// Greenlock needs to change some low-level http and https options.
|
|
||||||
// Use glx.httpsServer(tlsOptions, app) instead.
|
|
||||||
|
|
||||||
function httpsWorker(glx) {
|
|
||||||
//
|
|
||||||
// HTTPS/1.1 is only used for node v11 or lower
|
|
||||||
// (HTTP2 is used for node v12+)
|
|
||||||
//
|
|
||||||
// Why not just require('https')?
|
|
||||||
|
|
||||||
// Get the raw https server:
|
|
||||||
var httpsServer = glx.httpsServer(null, function(req, res) {
|
|
||||||
res.end("Hello, Encrypted World!");
|
|
||||||
});
|
|
||||||
|
|
||||||
httpsServer.listen(443, "0.0.0.0", function() {
|
|
||||||
console.info("Listening on ", httpsServer.address());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Note:
|
|
||||||
// You must ALSO listen on port 80 for ACME HTTP-01 Challenges
|
|
||||||
// (the ACME and http->https middleware are loaded by glx.httpServer)
|
|
||||||
var httpServer = glx.httpServer();
|
|
||||||
httpServer.listen(80, "0.0.0.0", function() {
|
|
||||||
console.info("Listening on ", httpServer.address());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//require("greenlock-express")
|
|
||||||
require("../../")
|
|
||||||
.init(function getConfig() {
|
|
||||||
// Greenlock Config
|
|
||||||
|
|
||||||
return {
|
|
||||||
package: { name: "https1-example", version: pkg.version },
|
|
||||||
maintainerEmail: "jon@example.com",
|
|
||||||
cluster: false
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.serve(httpsWorker);
|
|
@ -1,22 +0,0 @@
|
|||||||
# Quick Start for Let's Encrypt with Node.js
|
|
||||||
|
|
||||||
```js
|
|
||||||
npm install --save greenlock-express
|
|
||||||
```
|
|
||||||
|
|
||||||
Manage via API or the config file:
|
|
||||||
|
|
||||||
`~/.config/greenlock/manage.json`: (default filesystem config)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"subscriberEmail": "letsencrypt-test@therootcompany.com",
|
|
||||||
"agreeToTerms": true,
|
|
||||||
"sites": {
|
|
||||||
"example.com": {
|
|
||||||
"subject": "example.com",
|
|
||||||
"altnames": ["example.com", "www.example.com"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
@ -1,32 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
function httpsWorker(glx) {
|
|
||||||
// This can be a node http app (shown),
|
|
||||||
// an Express app, or Hapi, Koa, Rill, etc
|
|
||||||
var app = function(req, res) {
|
|
||||||
res.end("Hello, Encrypted World!");
|
|
||||||
};
|
|
||||||
|
|
||||||
// Serves on 80 and 443
|
|
||||||
// Get's SSL certificates magically!
|
|
||||||
glx.serveApp(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
var pkg = require("../../package.json");
|
|
||||||
//require("greenlock-express")
|
|
||||||
require("../../")
|
|
||||||
.init(function getConfig() {
|
|
||||||
// Greenlock Config
|
|
||||||
|
|
||||||
return {
|
|
||||||
// Package name+version is used for ACME client user agent
|
|
||||||
package: { name: "websocket-example", version: pkg.version },
|
|
||||||
|
|
||||||
// Maintainer email is the contact for critical bug and security notices
|
|
||||||
maintainerEmail: "jon@example.com",
|
|
||||||
|
|
||||||
// Change to true when you're ready to make your app cloud-scale
|
|
||||||
cluster: false
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.serve(httpsWorker);
|
|
14
examples/serve.js
Обычный файл
14
examples/serve.js
Обычный файл
@ -0,0 +1,14 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Note: using staging server url, remove .testing() for production
|
||||||
|
var lex = require('letsencrypt-express').testing();
|
||||||
|
var express = require('express');
|
||||||
|
var app = express();
|
||||||
|
|
||||||
|
app.use('/', function (req, res) {
|
||||||
|
res.send({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
lex.create('./letsencrypt.config', app).listen([80], [443, 5001], function () {
|
||||||
|
console.log("ENCRYPT __ALL__ THE DOMAINS!");
|
||||||
|
});
|
@ -1,49 +0,0 @@
|
|||||||
// First and foremost:
|
|
||||||
// I'm not a fan of `socket.io` because it's huge and complex.
|
|
||||||
// I much prefer `ws` because it's very simple and easy.
|
|
||||||
// That said, it's popular.......
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// Note: You DO NOT NEED socket.io
|
|
||||||
// You can just use WebSockets
|
|
||||||
// (see the websocket example)
|
|
||||||
|
|
||||||
function httpsWorker(glx) {
|
|
||||||
var socketio = require("socket.io");
|
|
||||||
var io;
|
|
||||||
|
|
||||||
// we need the raw https server
|
|
||||||
var server = glx.httpsServer();
|
|
||||||
|
|
||||||
io = socketio(server);
|
|
||||||
|
|
||||||
// Then you do your socket.io stuff
|
|
||||||
io.on("connection", function(socket) {
|
|
||||||
console.log("a user connected");
|
|
||||||
socket.emit("Welcome");
|
|
||||||
|
|
||||||
socket.on("chat message", function(msg) {
|
|
||||||
socket.broadcast.emit("chat message", msg);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// servers a node app that proxies requests to a localhost
|
|
||||||
glx.serveApp(function(req, res) {
|
|
||||||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
||||||
res.end("Hello, World!\n\n💚 🔒.js");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var pkg = require("../../package.json");
|
|
||||||
//require("greenlock-express")
|
|
||||||
require("../../")
|
|
||||||
.init(function getConfig() {
|
|
||||||
// Greenlock Config
|
|
||||||
|
|
||||||
return {
|
|
||||||
package: { name: "socket-io-example", version: pkg.version },
|
|
||||||
maintainerEmail: "jon@example.com",
|
|
||||||
cluster: false
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.serve(httpsWorker);
|
|
@ -1,3 +0,0 @@
|
|||||||
// SPDY is dead. It was replaced by HTTP2, which is a native node module
|
|
||||||
//
|
|
||||||
// Greenlock uses HTTP2 as the default https server in node v12+
|
|
1
examples/tweet.js
Символическая ссылка
1
examples/tweet.js
Символическая ссылка
@ -0,0 +1 @@
|
|||||||
|
116.js
|
@ -1,42 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
function httpsWorker(glx) {
|
|
||||||
// we need the raw https server
|
|
||||||
var server = glx.httpsServer();
|
|
||||||
var WebSocket = require("ws");
|
|
||||||
var ws = new WebSocket.Server({ server: server });
|
|
||||||
ws.on("connection", function(ws, req) {
|
|
||||||
// inspect req.headers.authorization (or cookies) for session info
|
|
||||||
ws.send(
|
|
||||||
"[Secure Echo Server] Hello!\nAuth: '" +
|
|
||||||
(req.headers.authorization || "none") +
|
|
||||||
"'\n" +
|
|
||||||
"Cookie: '" +
|
|
||||||
(req.headers.cookie || "none") +
|
|
||||||
"'\n"
|
|
||||||
);
|
|
||||||
ws.on("message", function(data) {
|
|
||||||
ws.send(data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// servers a node app that proxies requests to a localhost
|
|
||||||
glx.serveApp(function(req, res) {
|
|
||||||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
||||||
res.end("Hello, World!\n\n💚 🔒.js");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var pkg = require("../../package.json");
|
|
||||||
//require("greenlock-express")
|
|
||||||
require("../../")
|
|
||||||
.init(function getConfig() {
|
|
||||||
// Greenlock Config
|
|
||||||
|
|
||||||
return {
|
|
||||||
package: { name: "websocket-example", version: pkg.version },
|
|
||||||
maintainerEmail: "jon@example.com",
|
|
||||||
cluster: false
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.serve(httpsWorker);
|
|
@ -1,44 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
require("./lib/compat");
|
|
||||||
var cluster = require("cluster");
|
|
||||||
|
|
||||||
// Greenlock Express
|
|
||||||
var GLE = module.exports;
|
|
||||||
|
|
||||||
// Node's cluster is awesome, because it encourages writing scalable services.
|
|
||||||
//
|
|
||||||
// The point of this provide an API that is consistent between single-process
|
|
||||||
// and multi-process services so that beginners can more easily take advantage
|
|
||||||
// of what cluster has to offer.
|
|
||||||
//
|
|
||||||
// This API provides just enough abstraction to make it easy, but leaves just
|
|
||||||
// enough hoopla so that there's not a large gap in understanding what happens
|
|
||||||
// under the hood. That's the hope, anyway.
|
|
||||||
|
|
||||||
GLE.init = function(fn) {
|
|
||||||
if (cluster.isWorker) {
|
|
||||||
// ignore the init function and launch the worker
|
|
||||||
return require("./worker.js").create();
|
|
||||||
}
|
|
||||||
|
|
||||||
var opts = fn();
|
|
||||||
if (!opts || "object" !== typeof opts) {
|
|
||||||
throw new Error(
|
|
||||||
"the `Greenlock.init(fn)` function should return an object `{ maintainerEmail, packageAgent, notify }`"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// just for ironic humor
|
|
||||||
["cloudnative", "cloudscale", "webscale", "distributed", "blockchain"].forEach(function(k) {
|
|
||||||
if (opts[k]) {
|
|
||||||
opts.cluster = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (opts.cluster) {
|
|
||||||
return require("./master.js").create(opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
return require("./single.js").create(opts);
|
|
||||||
};
|
|
119
greenlock.js
119
greenlock.js
@ -1,119 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
module.exports.create = function(opts) {
|
|
||||||
opts = parsePackage(opts);
|
|
||||||
opts.packageAgent = addGreenlockAgent(opts);
|
|
||||||
|
|
||||||
var Greenlock = require("@root/greenlock");
|
|
||||||
var greenlock = Greenlock.create(opts);
|
|
||||||
|
|
||||||
// TODO move to greenlock proper
|
|
||||||
greenlock.getAcmeHttp01ChallengeResponse = function(opts) {
|
|
||||||
// TODO some sort of caching to prevent database hits?
|
|
||||||
return greenlock
|
|
||||||
._config({ servername: opts.servername })
|
|
||||||
.then(function(site) {
|
|
||||||
if (!site) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hmm... this _should_ be impossible
|
|
||||||
if (!site.challenges || !site.challenges["http-01"]) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Greenlock._loadChallenge(site.challenges, "http-01");
|
|
||||||
})
|
|
||||||
.then(function(plugin) {
|
|
||||||
return plugin
|
|
||||||
.get({
|
|
||||||
challenge: {
|
|
||||||
type: opts.type,
|
|
||||||
//hostname: opts.servername,
|
|
||||||
altname: opts.servername,
|
|
||||||
identifier: { value: opts.servername },
|
|
||||||
token: opts.token
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(function(result) {
|
|
||||||
var keyAuth;
|
|
||||||
if (result) {
|
|
||||||
// backwards compat that shouldn't be dropped
|
|
||||||
// because new v3 modules had to do this to be
|
|
||||||
// backwards compatible with Greenlock v2.7 at
|
|
||||||
// the time.
|
|
||||||
if (result.challenge) {
|
|
||||||
result = challenge;
|
|
||||||
}
|
|
||||||
keyAuth = result.keyAuthorization;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
keyAuthorization: keyAuth
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return greenlock;
|
|
||||||
};
|
|
||||||
|
|
||||||
function addGreenlockAgent(opts) {
|
|
||||||
// Add greenlock as part of Agent, unless this is greenlock
|
|
||||||
var packageAgent = opts.packageAgent || "";
|
|
||||||
if (!/greenlock(-express|-pro)?/i.test(packageAgent)) {
|
|
||||||
var pkg = require("./package.json");
|
|
||||||
packageAgent += " Greenlock_Express/" + pkg.version;
|
|
||||||
}
|
|
||||||
|
|
||||||
return packageAgent.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ex: "John Doe <john@example.com> (https://john.doe)"
|
|
||||||
// ex: "John Doe <john@example.com>"
|
|
||||||
// ex: "<john@example.com>"
|
|
||||||
// ex: "john@example.com"
|
|
||||||
var looseEmailRe = /(^|[\s<])([^'" <>:;`]+@[^'" <>:;`]+\.[^'" <>:;`]+)/;
|
|
||||||
function parsePackage(opts) {
|
|
||||||
// 'package' is sometimes a reserved word
|
|
||||||
var pkg = opts.package || opts.pkg;
|
|
||||||
if (!pkg) {
|
|
||||||
opts.maintainerEmail = parseMaintainer(opts.maintainerEmail);
|
|
||||||
return opts;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!opts.packageAgent) {
|
|
||||||
var err = "missing `package.THING`, which is used for the ACME client user agent string";
|
|
||||||
if (!pkg.name) {
|
|
||||||
throw new Error(err.replace("THING", "name"));
|
|
||||||
}
|
|
||||||
if (!pkg.version) {
|
|
||||||
throw new Error(err.replace("THING", "version"));
|
|
||||||
}
|
|
||||||
opts.packageAgent = pkg.name + "/" + pkg.version;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!opts.maintainerEmail) {
|
|
||||||
try {
|
|
||||||
opts.maintainerEmail = pkg.author.email || pkg.author.match(looseEmailRe)[2];
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
if (!opts.maintainerEmail) {
|
|
||||||
throw new Error("missing or malformed `package.author`, which is used as the contact for support notices");
|
|
||||||
}
|
|
||||||
opts.package = undefined;
|
|
||||||
opts.maintainerEmail = parseMaintainer(opts.maintainerEmail);
|
|
||||||
|
|
||||||
return opts;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseMaintainer(maintainerEmail) {
|
|
||||||
try {
|
|
||||||
maintainerEmail = maintainerEmail.match(looseEmailRe)[2];
|
|
||||||
} catch (e) {
|
|
||||||
maintainerEmail = null;
|
|
||||||
}
|
|
||||||
if (!maintainerEmail) {
|
|
||||||
throw new Error("missing or malformed `maintainerEmail`, which is used as the contact for support notices");
|
|
||||||
}
|
|
||||||
return maintainerEmail;
|
|
||||||
}
|
|
@ -1,106 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
var HttpMiddleware = module.exports;
|
|
||||||
var servernameRe = /^[a-z0-9\.\-]+$/i;
|
|
||||||
var challengePrefix = "/.well-known/acme-challenge/";
|
|
||||||
|
|
||||||
HttpMiddleware.create = function(gl, defaultApp) {
|
|
||||||
if (defaultApp && "function" !== typeof defaultApp) {
|
|
||||||
throw new Error("use greenlock.httpMiddleware() or greenlock.httpMiddleware(function (req, res) {})");
|
|
||||||
}
|
|
||||||
|
|
||||||
return function(req, res, next) {
|
|
||||||
var hostname = HttpMiddleware.sanitizeHostname(req);
|
|
||||||
|
|
||||||
req.on("error", function(err) {
|
|
||||||
explainError(gl, err, "http_01_middleware_socket", hostname);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (skipIfNeedBe(req, res, next, defaultApp, hostname)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var token = req.url.slice(challengePrefix.length);
|
|
||||||
|
|
||||||
gl.getAcmeHttp01ChallengeResponse({ type: "http-01", servername: hostname, token: token })
|
|
||||||
.catch(function(err) {
|
|
||||||
respondToError(gl, res, err, "http_01_middleware_challenge_response", hostname);
|
|
||||||
return { __done: true };
|
|
||||||
})
|
|
||||||
.then(function(result) {
|
|
||||||
if (result && result.__done) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return respondWithGrace(res, result, hostname, token);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
function skipIfNeedBe(req, res, next, defaultApp, hostname) {
|
|
||||||
if (!hostname || 0 !== req.url.indexOf(challengePrefix)) {
|
|
||||||
if ("function" === typeof defaultApp) {
|
|
||||||
defaultApp(req, res, next);
|
|
||||||
} else if ("function" === typeof next) {
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
res.statusCode = 500;
|
|
||||||
res.end("[500] Developer Error: app.use('/', greenlock.httpMiddleware()) or greenlock.httpMiddleware(app)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function respondWithGrace(res, result, hostname, token) {
|
|
||||||
var keyAuth = result && result.keyAuthorization;
|
|
||||||
if (keyAuth && "string" === typeof keyAuth) {
|
|
||||||
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
||||||
res.end(keyAuth);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.statusCode = 404;
|
|
||||||
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
||||||
res.end(JSON.stringify({ error: { message: "domain '" + hostname + "' has no token '" + token + "'." } }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function explainError(gl, err, ctx, hostname) {
|
|
||||||
if (!err.servername) {
|
|
||||||
err.servername = hostname;
|
|
||||||
}
|
|
||||||
if (!err.context) {
|
|
||||||
err.context = ctx;
|
|
||||||
}
|
|
||||||
(gl.notify || gl._notify)("error", err);
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
function respondToError(gl, res, err, ctx, hostname) {
|
|
||||||
err = explainError(gl, err, ctx, hostname);
|
|
||||||
res.statusCode = 500;
|
|
||||||
res.end("Internal Server Error: See logs for details.");
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpMiddleware.getHostname = function(req) {
|
|
||||||
return req.hostname || req.headers["x-forwarded-host"] || (req.headers.host || "");
|
|
||||||
};
|
|
||||||
HttpMiddleware.sanitizeHostname = function(req) {
|
|
||||||
// we can trust XFH because spoofing causes no ham in this limited use-case scenario
|
|
||||||
// (and only telebit would be legitimately setting XFH)
|
|
||||||
var servername = HttpMiddleware.getHostname(req)
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/:.*/, "");
|
|
||||||
try {
|
|
||||||
req.hostname = servername;
|
|
||||||
} catch (e) {
|
|
||||||
// read-only express property
|
|
||||||
}
|
|
||||||
if (req.headers["x-forwarded-host"]) {
|
|
||||||
req.headers["x-forwarded-host"] = servername;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
req.headers.host = servername;
|
|
||||||
} catch (e) {
|
|
||||||
// TODO is this a possible error?
|
|
||||||
}
|
|
||||||
|
|
||||||
return (servernameRe.test(servername) && -1 === servername.indexOf("..") && servername) || "";
|
|
||||||
};
|
|
@ -1,139 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
var SanitizeHost = module.exports;
|
|
||||||
var HttpMiddleware = require("./http-middleware.js");
|
|
||||||
|
|
||||||
SanitizeHost.create = function(gl, app) {
|
|
||||||
return function(req, res, next) {
|
|
||||||
function realNext() {
|
|
||||||
if ("function" === typeof app) {
|
|
||||||
app(req, res);
|
|
||||||
} else if ("function" === typeof next) {
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
res.statusCode = 500;
|
|
||||||
res.end("Error: no middleware assigned");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var hostname = HttpMiddleware.getHostname(req);
|
|
||||||
// Replace the hostname, and get the safe version
|
|
||||||
var safehost = HttpMiddleware.sanitizeHostname(req);
|
|
||||||
|
|
||||||
// if no hostname, move along
|
|
||||||
if (!hostname) {
|
|
||||||
realNext();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there were unallowed characters, complain
|
|
||||||
if (safehost.length !== hostname.length) {
|
|
||||||
res.statusCode = 400;
|
|
||||||
res.end("Malformed HTTP Header: 'Host: " + hostname + "'");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: This sanitize function is also called on plain sockets, which don't need Domain Fronting checks
|
|
||||||
if (req.socket.encrypted) {
|
|
||||||
if (req.socket && "string" === typeof req.socket.servername) {
|
|
||||||
// Workaround for https://github.com/nodejs/node/issues/22389
|
|
||||||
if (!SanitizeHost._checkServername(safehost, req.socket)) {
|
|
||||||
res.statusCode = 400;
|
|
||||||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
||||||
res.end(
|
|
||||||
"<h1>Domain Fronting Error</h1>" +
|
|
||||||
"<p>This connection was secured using TLS/SSL for '" +
|
|
||||||
(req.socket.servername || "").toLowerCase() +
|
|
||||||
"'</p>" +
|
|
||||||
"<p>The HTTP request specified 'Host: " +
|
|
||||||
safehost +
|
|
||||||
"', which is (obviously) different.</p>" +
|
|
||||||
"<p>Because this looks like a domain fronting attack, the connection has been terminated.</p>"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
else if (safehost && !gl._skip_fronting_check) {
|
|
||||||
|
|
||||||
// We used to print a log message here, but it turns out that it's
|
|
||||||
// really common for IoT devices to not use SNI (as well as many bots
|
|
||||||
// and such).
|
|
||||||
// It was common for the log message to pop up as the first request
|
|
||||||
// to the server, and that was confusing. So instead now we do nothing.
|
|
||||||
|
|
||||||
//console.warn("no string for req.socket.servername," + " skipping fronting check for '" + safehost + "'");
|
|
||||||
//gl._skip_fronting_check = true;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
// carry on
|
|
||||||
realNext();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
var warnDomainFronting = true;
|
|
||||||
var warnUnexpectedError = true;
|
|
||||||
SanitizeHost._checkServername = function(safeHost, tlsSocket) {
|
|
||||||
var servername = (tlsSocket.servername || "").toLowerCase();
|
|
||||||
|
|
||||||
// acceptable: older IoT devices may lack SNI support
|
|
||||||
if (!servername) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// acceptable: odd... but acceptable
|
|
||||||
if (!safeHost) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (safeHost === servername) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("function" !== typeof tlsSocket.getCertificate) {
|
|
||||||
// domain fronting attacks allowed
|
|
||||||
if (warnDomainFronting) {
|
|
||||||
// https://github.com/nodejs/node/issues/24095
|
|
||||||
console.warn(
|
|
||||||
"Warning: node " +
|
|
||||||
process.version +
|
|
||||||
" is vulnerable to domain fronting attacks. Please use node v11.2.0 or greater."
|
|
||||||
);
|
|
||||||
warnDomainFronting = false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// connection established with servername and session is re-used for allowed name
|
|
||||||
// See https://github.com/nodejs/node/issues/24095
|
|
||||||
var cert = tlsSocket.getCertificate();
|
|
||||||
try {
|
|
||||||
// TODO optimize / cache?
|
|
||||||
// *should* always have a string, right?
|
|
||||||
// *should* always be lowercase already, right?
|
|
||||||
//console.log(safeHost, cert.subject.CN, cert.subjectaltname);
|
|
||||||
var isSubject = (cert.subject.CN || "").toLowerCase() === safeHost;
|
|
||||||
if (isSubject) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dnsnames = (cert.subjectaltname || "").split(/,\s+/);
|
|
||||||
var inSanList = dnsnames.some(function(name) {
|
|
||||||
// always prefixed with "DNS:"
|
|
||||||
return safeHost === name.slice(4).toLowerCase();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (inSanList) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// not sure what else to do in this situation...
|
|
||||||
if (warnUnexpectedError) {
|
|
||||||
console.warn("Warning: encoutered error while performing domain fronting check: " + e.message);
|
|
||||||
warnUnexpectedError = false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
3
index.js
Обычный файл
3
index.js
Обычный файл
@ -0,0 +1,3 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = require('./lib/standalone');
|
14
install.sh
14
install.sh
@ -1,14 +0,0 @@
|
|||||||
# This is just an example (but it works)
|
|
||||||
export NODE_PATH=$NPM_CONFIG_PREFIX/lib/node_modules
|
|
||||||
export NPM_CONFIG_PREFIX=/opt/node
|
|
||||||
curl -fsSL https://bit.ly/node-installer | bash
|
|
||||||
|
|
||||||
/opt/node/bin/node /opt/node/bin/npm config set scripts-prepend-node-path true
|
|
||||||
/opt/node/bin/node /opt/node/bin/npm ci
|
|
||||||
sudo setcap 'cap_net_bind_service=+ep' /opt/node/bin/node
|
|
||||||
/opt/node/bin/node /opt/node/bin/npm start
|
|
||||||
|
|
||||||
sudo rsync -av dist/etc/systemd/system/greenlock-express.service /etc/systemd/system/
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
|
|
||||||
sudo systemctl restart greenlock-express
|
|
61
lib/challenge-handlers.js
Обычный файл
61
lib/challenge-handlers.js
Обычный файл
@ -0,0 +1,61 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var fs = require('fs');
|
||||||
|
var path = require('path');
|
||||||
|
var mkdirp = require('mkdirp');
|
||||||
|
|
||||||
|
// TODO handle templating :hostname in letsencrypt proper
|
||||||
|
|
||||||
|
// Note: we're explicitly doing this on the filesystem
|
||||||
|
// rather than in-memory to support node cluster
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
set: function setChallenge(args, hostname, key, value, cb) {
|
||||||
|
var webrootPath = args.webrootPath;
|
||||||
|
var keyfile = path.join(webrootPath, key);
|
||||||
|
|
||||||
|
if (args.debug) {
|
||||||
|
console.debug('[LEX] write file', hostname, webrootPath);
|
||||||
|
console.debug('challenge:', key);
|
||||||
|
console.debug('response:', value);
|
||||||
|
}
|
||||||
|
fs.writeFile(keyfile, value, 'utf8', function (err) {
|
||||||
|
if (!err) { cb(null); return; }
|
||||||
|
|
||||||
|
|
||||||
|
if (args.debug) {
|
||||||
|
console.debug('[LEX] mkdirp', webrootPath);
|
||||||
|
}
|
||||||
|
mkdirp(webrootPath, function (err) {
|
||||||
|
if (err) { cb(err); return; }
|
||||||
|
|
||||||
|
fs.writeFile(keyfile, value, 'utf8', cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
, get: function getChallenge(args, hostname, key, cb) {
|
||||||
|
var keyfile = path.join(args.webrootPath, key);
|
||||||
|
|
||||||
|
if (args.debug) {
|
||||||
|
console.debug('[LEX] getChallenge', keyfile, hostname, key);
|
||||||
|
}
|
||||||
|
fs.readFile(keyfile, 'utf8', function (err, text) {
|
||||||
|
cb(null, text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
, remove: function removeChallenge(args, hostname, key, cb) {
|
||||||
|
var keyfile = path.join(args.webrootPath, key);
|
||||||
|
|
||||||
|
// Note: it's not actually terribly important that we wait for the unlink callback
|
||||||
|
// but it's a polite thing to do - and we're polite people!
|
||||||
|
if (args.debug) {
|
||||||
|
console.debug('[LEX] removeChallenge', keyfile, hostname, key);
|
||||||
|
}
|
||||||
|
fs.unlink(keyfile, function (err) {
|
||||||
|
if (err) { console.warn(err.stack); }
|
||||||
|
cb(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -1,37 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
function requireBluebird() {
|
|
||||||
try {
|
|
||||||
return require("bluebird");
|
|
||||||
} catch (e) {
|
|
||||||
console.error("");
|
|
||||||
console.error("DON'T PANIC. You're running an old version of node with incomplete Promise support.");
|
|
||||||
console.error("EASY FIX: `npm install --save bluebird`");
|
|
||||||
console.error("");
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("undefined" === typeof Promise) {
|
|
||||||
global.Promise = requireBluebird();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("function" !== typeof require("util").promisify) {
|
|
||||||
require("util").promisify = requireBluebird().promisify;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!console.debug) {
|
|
||||||
console.debug = console.log;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fs = require("fs");
|
|
||||||
var fsAsync = {};
|
|
||||||
Object.keys(fs).forEach(function(key) {
|
|
||||||
var fn = fs[key];
|
|
||||||
if ("function" !== typeof fn || !/[a-z]/.test(key[0])) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fsAsync[key] = require("util").promisify(fn);
|
|
||||||
});
|
|
||||||
|
|
||||||
exports.fsAsync = fsAsync;
|
|
41
lib/configurator/app.js
Обычный файл
41
lib/configurator/app.js
Обычный файл
@ -0,0 +1,41 @@
|
|||||||
|
$(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var tpl = $('.js-hostnames').html();
|
||||||
|
$('.js-hostnames').html('');
|
||||||
|
|
||||||
|
$('body').on('submit', 'form.js-add-site', function (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
// I don't think this actually has any meaning when listening on body
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
email: $('.js-email').val()
|
||||||
|
, agreeTos: !!$('.js-agree-tos').prop('checked')
|
||||||
|
, server: $('.js-server').val()
|
||||||
|
, domains: $('.js-domains').val().split(/\s*,\s*/)
|
||||||
|
};
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
method: 'POST'
|
||||||
|
, url: '/api/com.daplie.lex/sites'
|
||||||
|
, data: JSON.stringify(data)
|
||||||
|
, headers: { 'Content-Type': 'application/json; charset=utf-8' }
|
||||||
|
}).then(function (data) {
|
||||||
|
console.log(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
method: 'GET'
|
||||||
|
, url: '/api/com.daplie.lex/sites'
|
||||||
|
}).then(function (data) {
|
||||||
|
var $hostnames = $('.js-hostnames');
|
||||||
|
$hostnames.html('');
|
||||||
|
data.forEach(function (config) {
|
||||||
|
var $conf = $(tpl);
|
||||||
|
$conf.text(config.domains);
|
||||||
|
$hostnames.append($conf);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
37
lib/configurator/index.html
Обычный файл
37
lib/configurator/index.html
Обычный файл
@ -0,0 +1,37 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Welcome to :hostname!</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self';">
|
||||||
|
<link rel="stylesheet" type="text/css" href="./bootstrap.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="jumbotron">
|
||||||
|
<h1>Add a Site</h1>
|
||||||
|
<form class="js-add-site">
|
||||||
|
<label>Domain</label>: <input type="text" class="js-domains" placeholder="ex: example.com,www.example.com">
|
||||||
|
<br/>
|
||||||
|
<label>Email</label>: <input type="email" class="js-email" placeholder="ex: user@example.com">
|
||||||
|
<br/>
|
||||||
|
<label>LE Server</label>: <input type="text" class="js-server" placeholder="ex: https://acme-staging.api.letsencrypt.org/directory" value="https://acme-v01.api.letsencrypt.org/directory">
|
||||||
|
<br/>
|
||||||
|
<label><input type="checkbox" class="js-agree-tos"> Agree to Let's Encrypt Terms of Service?</label>
|
||||||
|
<br/>
|
||||||
|
<button class="btn btn-primary" type="submit">Add Site</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<p>Sites:
|
||||||
|
<div class="js-hostnames-container">
|
||||||
|
<ul class="js-hostnames">
|
||||||
|
<li class="js-hostname"></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="./jquery.js"></script>
|
||||||
|
<script src="./app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
6961
lib/public/bootstrap.css
поставляемый
Обычный файл
6961
lib/public/bootstrap.css
поставляемый
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
9210
lib/public/jquery.js
поставляемый
Обычный файл
9210
lib/public/jquery.js
поставляемый
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
24
lib/public/welcome.html
Обычный файл
24
lib/public/welcome.html
Обычный файл
@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Welcome to :hostname!</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self';">
|
||||||
|
<link rel="stylesheet" type="text/css" href="./bootstrap.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="jumbotron">
|
||||||
|
<h1>Hello, world!</h1>
|
||||||
|
<p>Your website is located in one of two places:</p>
|
||||||
|
<ul>
|
||||||
|
<li>on your <code>Desktop</code> inside of the <code>www</code> directory and <code>:hostname</code></li>
|
||||||
|
<li><code>~/www/:hostname</code> (your home folder, under <code>www/:hostname</code></li>
|
||||||
|
<ul>
|
||||||
|
<p>This very file is called <code>index.html</code>.
|
||||||
|
You can open it up in <code>notepad</code> and change it up a little if you'd like.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="./jquery.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
265
lib/sni-callback.js
Обычный файл
265
lib/sni-callback.js
Обычный файл
@ -0,0 +1,265 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var crypto = require('crypto');
|
||||||
|
var tls = require('tls');
|
||||||
|
|
||||||
|
module.exports.create = function (opts) {
|
||||||
|
var ipc = {}; // in-process cache
|
||||||
|
|
||||||
|
// function (/*err, hostname, certInfo*/) {}
|
||||||
|
function handleRenewFailure(err, hostname, certInfo) {
|
||||||
|
console.error("ERROR: Failed to renew domain '", hostname, "':");
|
||||||
|
if (err) {
|
||||||
|
console.error(err.stack || err);
|
||||||
|
}
|
||||||
|
if (certInfo) {
|
||||||
|
console.error(certInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opts) { throw new Error("requires opts to be an object"); }
|
||||||
|
if (opts.debug) {
|
||||||
|
console.debug("[LEX] creating sniCallback", JSON.stringify(opts, function(k,v){
|
||||||
|
if(v instanceof Array)
|
||||||
|
return JSON.stringify(v);
|
||||||
|
return v;
|
||||||
|
},' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opts.letsencrypt) { throw new Error("requires opts.letsencrypt to be a letsencrypt instance"); }
|
||||||
|
|
||||||
|
if (!opts.lifetime) { opts.lifetime = 90 * 24 * 60 * 60 * 1000; }
|
||||||
|
if (!opts.failedWait) { opts.failedWait = 5 * 60 * 1000; }
|
||||||
|
if (!opts.renewWithin) { opts.renewWithin = 3 * 24 * 60 * 60 * 1000; }
|
||||||
|
if (!opts.memorizeFor) { opts.memorizeFor = 1 * 24 * 60 * 60 * 1000; }
|
||||||
|
|
||||||
|
if (!opts.approveRegistration) { opts.approveRegistration = function (hostname, cb) { cb(null, null); }; }
|
||||||
|
//opts.approveRegistration = function (hostname, cb) { cb(null, null); };
|
||||||
|
if (!opts.handleRenewFailure) { opts.handleRenewFailure = handleRenewFailure; }
|
||||||
|
|
||||||
|
function assignBestByDates(now, certInfo) {
|
||||||
|
certInfo = certInfo || { loadedAt: now, expiresAt: 0, issuedAt: 0, lifetime: 0 };
|
||||||
|
|
||||||
|
var rnds = crypto.randomBytes(3);
|
||||||
|
var rnd1 = ((rnds[0] + 1) / 257);
|
||||||
|
var rnd2 = ((rnds[1] + 1) / 257);
|
||||||
|
var rnd3 = ((rnds[2] + 1) / 257);
|
||||||
|
|
||||||
|
// Stagger randomly by plus 0% to 25% to prevent all caches expiring at once
|
||||||
|
var memorizeFor = Math.floor(opts.memorizeFor + ((opts.memorizeFor / 4) * rnd1));
|
||||||
|
// Stagger randomly to renew between n and 2n days before renewal is due
|
||||||
|
// this *greatly* reduces the risk of multiple cluster processes renewing the same domain at once
|
||||||
|
var bestIfUsedBy = certInfo.expiresAt - (opts.renewWithin + Math.floor(opts.renewWithin * rnd2));
|
||||||
|
// Stagger randomly by plus 0 to 5 min to reduce risk of multiple cluster processes
|
||||||
|
// renewing at once on boot when the certs have expired
|
||||||
|
var renewTimeout = Math.floor((5 * 60 * 1000) * rnd3);
|
||||||
|
|
||||||
|
certInfo.loadedAt = now;
|
||||||
|
certInfo.memorizeFor = memorizeFor;
|
||||||
|
certInfo.bestIfUsedBy = bestIfUsedBy;
|
||||||
|
certInfo.renewTimeout = renewTimeout;
|
||||||
|
|
||||||
|
return certInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renewInBackground(now, hostname, certInfo) {
|
||||||
|
if ((now - certInfo.loadedAt) < opts.failedWait) {
|
||||||
|
// wait a few minutes
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now > certInfo.bestIfUsedBy && !certInfo.timeout) {
|
||||||
|
// EXPIRING
|
||||||
|
if (now > certInfo.expiresAt) {
|
||||||
|
// EXPIRED
|
||||||
|
certInfo.renewTimeout = Math.floor(certInfo.renewTimeout / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.debug) {
|
||||||
|
console.debug("[LEX] skipping stagger '" + certInfo.renewTimeout + "' and renewing '" + hostname + "' now");
|
||||||
|
certInfo.renewTimeout = 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
certInfo.timeout = setTimeout(function () {
|
||||||
|
var args = { domains: [ hostname ], duplicate: false };
|
||||||
|
opts.letsencrypt.renew(args, function (err, certInfo) {
|
||||||
|
if (err || !certInfo) {
|
||||||
|
opts.handleRenewFailure(err, hostname, certInfo);
|
||||||
|
}
|
||||||
|
ipc[hostname] = assignBestByDates(now, certInfo);
|
||||||
|
});
|
||||||
|
}, certInfo.renewTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheResult(err, hostname, certInfo, sniCb) {
|
||||||
|
if (certInfo && certInfo.fullchain && certInfo.privkey) {
|
||||||
|
if (opts.debug) {
|
||||||
|
console.debug('cert is looking good');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
certInfo.tlsContext = tls.createSecureContext({
|
||||||
|
key: certInfo.privkey || certInfo.key // privkey.pem
|
||||||
|
, cert: certInfo.fullchain || certInfo.cert // fullchain.pem (cert.pem + '\n' + chain.pem)
|
||||||
|
, ca: (opts.httpsOptions.ca ? opts.httpsOptions.ca + '\n' + certInfo.ca : certInfo.ca)
|
||||||
|
, crl: opts.httpsOptions.crl
|
||||||
|
, requestCert: opts.httpsOptions.requestCert
|
||||||
|
, rejectUnauthorized: opts.httpsOptions.rejectUnauthorized
|
||||||
|
});
|
||||||
|
} catch(e) {
|
||||||
|
console.warn("[Sanity Check Fail]: a weird object was passed back through le.fetch to lex.fetch");
|
||||||
|
console.warn("(either missing or malformed certInfo.key and / or certInfo.fullchain)");
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
sniCb(err, certInfo.tlsContext);
|
||||||
|
} else {
|
||||||
|
if (opts.debug) {
|
||||||
|
console.debug('cert is NOT looking good');
|
||||||
|
}
|
||||||
|
sniCb(err || new Error("couldn't get certInfo: unknown"), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = Date.now();
|
||||||
|
certInfo = ipc[hostname] = assignBestByDates(now, certInfo);
|
||||||
|
renewInBackground(now, hostname, certInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerCert(hostname, sniCb) {
|
||||||
|
if (opts.debug) {
|
||||||
|
console.debug("[LEX] '" + hostname + "' is not registered, requesting approval");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hostname) {
|
||||||
|
sniCb(new Error('[registerCert] no hostname'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.approveRegistration(hostname, function (err, args) {
|
||||||
|
|
||||||
|
if (opts.debug) {
|
||||||
|
console.debug("[LEX] '" + hostname + "' registration approved, attempting register");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
cacheResult(err, hostname, null, sniCb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(args && args.agreeTos && args.email && args.domains)) {
|
||||||
|
cacheResult(new Error("not approved or approval is missing arguments - such as agreeTos, email, domains"), hostname, null, sniCb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.letsencrypt.register(args, function (err, certInfo) {
|
||||||
|
if (opts.debug) {
|
||||||
|
console.debug("[LEX] '" + hostname + "' register completed", err && err.stack || null, certInfo);
|
||||||
|
if ((!err || !err.stack) && !certInfo) {
|
||||||
|
console.error((new Error("[LEX] SANITY FAIL: no error and yet no certs either")).stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheResult(err, hostname, certInfo, sniCb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetch(hostname, sniCb) {
|
||||||
|
if (!hostname) {
|
||||||
|
sniCb(new Error('[sniCallback] [fetch] no hostname'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.letsencrypt.fetch({ domains: [hostname] }, function (err, certInfo) {
|
||||||
|
if (opts.debug) {
|
||||||
|
console.debug("[LEX] fetch from disk result '" + hostname + "':");
|
||||||
|
console.debug(certInfo && Object.keys(certInfo));
|
||||||
|
if (err) {
|
||||||
|
console.error(err.stack || err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
sniCb(err, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (certInfo) {
|
||||||
|
cacheResult(err, hostname, certInfo, sniCb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerCert(hostname, sniCb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return function sniCallback(hostname, cb) {
|
||||||
|
var now = Date.now();
|
||||||
|
var certInfo = ipc[hostname];
|
||||||
|
|
||||||
|
if (!hostname) {
|
||||||
|
cb(new Error('[sniCallback] no hostname'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// No cert is available in cache.
|
||||||
|
// try to fetch it from disk quickly
|
||||||
|
// and return to the browser
|
||||||
|
//
|
||||||
|
if (!certInfo) {
|
||||||
|
if (opts.debug) {
|
||||||
|
console.debug("[LEX] no certs loaded for '" + hostname + "'");
|
||||||
|
}
|
||||||
|
fetch(hostname, cb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// A cert is available
|
||||||
|
// See if it's old enough that
|
||||||
|
// we should refresh it from disk
|
||||||
|
// (in the background)
|
||||||
|
//
|
||||||
|
// TODO once ECDSA is available, wait for cert renewal if its due (maybe?)
|
||||||
|
if (certInfo.tlsContext) {
|
||||||
|
cb(null, certInfo.tlsContext);
|
||||||
|
|
||||||
|
if ((now - certInfo.loadedAt) < (certInfo.memorizeFor)) {
|
||||||
|
// these aren't stale, so don't fall through
|
||||||
|
if (opts.debug) {
|
||||||
|
console.debug("[LEX] certs for '" + hostname + "' are fresh from disk");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ((now - certInfo.loadedAt) < opts.failedWait) {
|
||||||
|
if (opts.debug) {
|
||||||
|
console.debug("[LEX] certs for '" + hostname + "' recently failed and are still in cool down");
|
||||||
|
}
|
||||||
|
// this was just fetched and failed, wait a few minutes
|
||||||
|
cb(null, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.debug) {
|
||||||
|
console.debug("[LEX] certs for '" + hostname + "' are stale on disk and should be will be fetched again");
|
||||||
|
console.debug({
|
||||||
|
age: now - certInfo.loadedAt
|
||||||
|
, loadedAt: certInfo.loadedAt
|
||||||
|
, issuedAt: certInfo.issuedAt
|
||||||
|
, expiresAt: certInfo.expiresAt
|
||||||
|
, privkey: !!certInfo.privkey
|
||||||
|
, chain: !!certInfo.chain
|
||||||
|
, fullchain: !!certInfo.fullchain
|
||||||
|
, cert: !!certInfo.cert
|
||||||
|
, memorizeFor: certInfo.memorizeFor
|
||||||
|
, failedWait: opts.failedWait
|
||||||
|
});
|
||||||
|
}
|
||||||
|
fetch(hostname, cb);
|
||||||
|
};
|
||||||
|
};
|
334
lib/standalone.js
Обычный файл
334
lib/standalone.js
Обычный файл
@ -0,0 +1,334 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var path = require('path');
|
||||||
|
var challengeStore = require('./challenge-handlers');
|
||||||
|
var createSniCallback = require('./sni-callback').create;
|
||||||
|
var LE = require('letsencrypt');
|
||||||
|
|
||||||
|
function createAcmeResponder(lex, onRequest) {
|
||||||
|
|
||||||
|
function httpAcmeResponder(req, res) {
|
||||||
|
if (lex.debug) {
|
||||||
|
console.debug('[LEX] ', req.method, req.headers.host, req.url);
|
||||||
|
}
|
||||||
|
var acmeChallengePrefix = '/.well-known/acme-challenge/';
|
||||||
|
|
||||||
|
if (0 !== req.url.indexOf(acmeChallengePrefix)) {
|
||||||
|
onRequest(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = req.url.slice(acmeChallengePrefix.length);
|
||||||
|
|
||||||
|
// lex = { debug, webrootPath }
|
||||||
|
lex.getChallenge(lex, req.headers.host, key, function (err, val) {
|
||||||
|
if (lex.debug) {
|
||||||
|
console.debug('[LEX] GET challenge, response:');
|
||||||
|
console.debug('challenge:', key);
|
||||||
|
console.debug('response:', val);
|
||||||
|
if (err) {
|
||||||
|
console.debug(err.stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.end(val || '_');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return httpAcmeResponder;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lexHelper(obj, app) {
|
||||||
|
var defaultPems = require('localhost.daplie.com-certificates');
|
||||||
|
|
||||||
|
if (!obj) {
|
||||||
|
obj = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('string' === typeof obj) {
|
||||||
|
obj = {
|
||||||
|
configDir: obj
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('function' === typeof obj) {
|
||||||
|
obj = {
|
||||||
|
onRequest: obj
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.debug = LEX.debug;
|
||||||
|
|
||||||
|
if ('function' === typeof app) {
|
||||||
|
obj.onRequest = obj.onRequest || app;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!obj.getChallenge) {
|
||||||
|
if (false !== obj.getChallenge) {
|
||||||
|
obj.getChallenge = LEX.getChallenge;
|
||||||
|
}
|
||||||
|
if (!obj.webrootPath) {
|
||||||
|
obj.webrootPath = path.join(require('os').tmpdir(), 'acme-challenge');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BEGIN LetsEncrypt options
|
||||||
|
if (!obj.configDir) {
|
||||||
|
obj.configDir = path.join(require('homedir')(), '/letsencrypt/etc');
|
||||||
|
}
|
||||||
|
if (!obj.privkeyPath) {
|
||||||
|
obj.privkeyPath = ':config/live/:hostname/privkey.pem';
|
||||||
|
}
|
||||||
|
if (!obj.fullchainPath) {
|
||||||
|
obj.fullchainPath = ':config/live/:hostname/fullchain.pem';
|
||||||
|
}
|
||||||
|
if (!obj.certPath) {
|
||||||
|
obj.certPath = ':config/live/:hostname/cert.pem';
|
||||||
|
}
|
||||||
|
if (!obj.chainPath) {
|
||||||
|
obj.chainPath = ':config/live/:hostname/chain.pem';
|
||||||
|
}
|
||||||
|
if (!obj.server) {
|
||||||
|
obj.server = LEX.defaultServerUrl;
|
||||||
|
}
|
||||||
|
// END LetsEncrypt options
|
||||||
|
|
||||||
|
obj.getChallenge = obj.getChallenge || LEX.getChallenge;
|
||||||
|
obj.setChallenge = obj.setChallenge || LEX.setChallenge;
|
||||||
|
obj.removeChallenge = obj.removeChallenge || LEX.removeChallenge;
|
||||||
|
|
||||||
|
if (!obj.letsencrypt) {
|
||||||
|
//LE.merge(obj, );
|
||||||
|
// { configDir, webrootPath, server }
|
||||||
|
obj.letsencrypt = LE.create(obj, {
|
||||||
|
setChallenge: obj.setChallenge
|
||||||
|
, removeChallenge: obj.removeChallenge
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var httpsOptions = obj.httpsOptions || {};
|
||||||
|
var sniCallback = httpsOptions.SNICallback;
|
||||||
|
|
||||||
|
// https://nodejs.org/api/https.html
|
||||||
|
// pfx, key, cert, passphrase, ca, ciphers, rejectUnauthorized, secureProtocol
|
||||||
|
if (!httpsOptions.pfx) {
|
||||||
|
if (!(httpsOptions.cert || httpsOptions.key)) {
|
||||||
|
httpsOptions.key = defaultPems.key;
|
||||||
|
httpsOptions.cert = defaultPems.cert;
|
||||||
|
}
|
||||||
|
else if (!(httpsOptions.cert && httpsOptions.key)) {
|
||||||
|
if (!httpsOptions.cert) {
|
||||||
|
console.warn("You specified httpsOptions.cert, but not httpsOptions.key");
|
||||||
|
}
|
||||||
|
if (!httpsOptions.key) {
|
||||||
|
console.warn("You specified httpsOptions.key, but not httpsOptions.cert");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!obj.approveRegistration && LEX.defaultApproveRegistration) {
|
||||||
|
obj.approveRegistration = function (domain, cb) {
|
||||||
|
if (obj.debug) {
|
||||||
|
console.debug('[LEX] auto register against staging server');
|
||||||
|
}
|
||||||
|
cb(null, {
|
||||||
|
email: 'example@gmail.com'
|
||||||
|
, domains: [domain]
|
||||||
|
, agreeTos: true
|
||||||
|
, server: LEX.stagingServerUrl
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.sniCallback) {
|
||||||
|
if (sniCallback) {
|
||||||
|
console.warn("You specified both args.sniCallback and args.httpsOptions.SNICallback,"
|
||||||
|
+ " but only args.sniCallback will be used.");
|
||||||
|
}
|
||||||
|
httpsOptions.SNICallback = obj.sniCallback;
|
||||||
|
}
|
||||||
|
else if (sniCallback) {
|
||||||
|
obj._sniCallback = createSniCallback(obj);
|
||||||
|
httpsOptions.SNICallback = function (domain, cb) {
|
||||||
|
sniCallback(domain, function (err, context) {
|
||||||
|
if (context) {
|
||||||
|
cb(err, context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
obj._sniCallback(domain, cb);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
httpsOptions.SNICallback = createSniCallback(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.httpsOptions = httpsOptions;
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LEX(obj, app) {
|
||||||
|
var https;
|
||||||
|
try {
|
||||||
|
https = require('spdy');
|
||||||
|
} catch(e) {
|
||||||
|
https = require('https');
|
||||||
|
}
|
||||||
|
var http = require('http');
|
||||||
|
|
||||||
|
function listen(plainPorts, tlsPorts, onListening) {
|
||||||
|
if (!(obj.onRequest || (obj.onHttpRequest && obj.onHttpsRequest)) && false !== obj.onRequest) {
|
||||||
|
console.warn("You should either do args.onRequest = app or server.on('request', app),"
|
||||||
|
+ " otherwise only acme-challenge requests will be handled (and the rest will hang)");
|
||||||
|
console.warn("You can silence this warning by setting args.onRequest = false");
|
||||||
|
}
|
||||||
|
obj.httpAcmeResponder = createAcmeResponder(obj, obj.onHttpRequest || obj.onRequest);
|
||||||
|
obj.httpsAcmeResponder = createAcmeResponder(obj, obj.onHttpsRequest || obj.onRequest);
|
||||||
|
|
||||||
|
if (plainPorts && (!Array.isArray(plainPorts) || !Array.isArray(tlsPorts))) {
|
||||||
|
throw new Error(".listen() must be used with plain and tls port arrays, like this: `.listen([80], [443, 5001], function () {})`");
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = {
|
||||||
|
plainServers: []
|
||||||
|
, tlsServers: []
|
||||||
|
};
|
||||||
|
|
||||||
|
plainPorts = plainPorts || [80];
|
||||||
|
tlsPorts = tlsPorts || [443, 5001];
|
||||||
|
|
||||||
|
function defaultOnListening() {
|
||||||
|
/*jshint validthis: true*/
|
||||||
|
var server = this;
|
||||||
|
var protocol = ('honorCipherOrder' in server || 'rejectUnauthorized' in server) ? 'https': 'http';
|
||||||
|
var addr = server.address();
|
||||||
|
var port;
|
||||||
|
|
||||||
|
if (80 === addr.port || 443 === addr.port) {
|
||||||
|
port = '';
|
||||||
|
} else {
|
||||||
|
port = ':' + addr.port;
|
||||||
|
}
|
||||||
|
console.info('Listening ' + protocol + '://' + addr.address + port + '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
plainPorts.forEach(function (addr) {
|
||||||
|
var port = addr.port || addr;
|
||||||
|
var address = addr.address || '';
|
||||||
|
var server = http.createServer(obj.httpAcmeResponder);
|
||||||
|
|
||||||
|
server.__le_onListening = addr.onListen;
|
||||||
|
server.__le_port = port;
|
||||||
|
server.__le_address = address;
|
||||||
|
|
||||||
|
results.plainServers.push(server);
|
||||||
|
});
|
||||||
|
|
||||||
|
tlsPorts.forEach(function (addr) {
|
||||||
|
var port = addr.port || addr;
|
||||||
|
var address = addr.address || '';
|
||||||
|
var options = addr.httpsOptions || obj.httpsOptions;
|
||||||
|
var server = https.createServer(options, obj.httpsAcmeResponder);
|
||||||
|
|
||||||
|
server.__le_onListen = addr.onListen;
|
||||||
|
server.__le_port = port;
|
||||||
|
server.__le_address = address;
|
||||||
|
|
||||||
|
results.tlsServers.push(server);
|
||||||
|
});
|
||||||
|
|
||||||
|
results.plainServers.forEach(function (server) {
|
||||||
|
var listen = server.__le_onListening || onListening || defaultOnListening;
|
||||||
|
server.listen(server.__le_port, server.__le_address, listen);
|
||||||
|
});
|
||||||
|
|
||||||
|
results.tlsServers.forEach(function (server) {
|
||||||
|
var listen = server.__le_onListening || onListening || defaultOnListening;
|
||||||
|
server.listen(server.__le_port, server.__le_address, listen);
|
||||||
|
});
|
||||||
|
|
||||||
|
// deleting creates a "slow object", but that's okay (we only use it once)
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
obj = lexHelper(obj, app);
|
||||||
|
obj.listen = listen;
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = LEX;
|
||||||
|
|
||||||
|
LEX.create = LEX;
|
||||||
|
LEX.createServers = LEX;
|
||||||
|
LEX.setChallenge = challengeStore.set;
|
||||||
|
LEX.getChallenge = challengeStore.get;
|
||||||
|
LEX.removeChallenge = challengeStore.remove;
|
||||||
|
LEX.createSniCallback = createSniCallback;
|
||||||
|
// TODO not sure how well this works
|
||||||
|
LEX.middleware = function (defaults) {
|
||||||
|
var leCore = require('letiny-core');
|
||||||
|
var merge = require('letsencrypt/common').merge;
|
||||||
|
var tplConfigDir = require('letsencrypt/common').tplConfigDir;
|
||||||
|
var tplHostname = require('letsencrypt/common').tplHostname;
|
||||||
|
var prefix = leCore.acmeChallengePrefix;
|
||||||
|
|
||||||
|
tplConfigDir(defaults.configDir || '', defaults);
|
||||||
|
|
||||||
|
return function (req, res, next) {
|
||||||
|
if (LEX.debug) {
|
||||||
|
console.debug('[LEX middleware]:', req.hostname, req.url, req.url.slice(prefix.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 !== req.url.indexOf(prefix)) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function done(err, token) {
|
||||||
|
if (err) {
|
||||||
|
res.send("Error: These aren't the tokens you're looking for. Move along.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
var copy = merge(defaults, { domains: [req.hostname] });
|
||||||
|
tplHostname(req.hostname, copy);
|
||||||
|
|
||||||
|
LEX.getChallenge(copy, req.hostname, req.url.slice(prefix.length), done);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
LEX.stagingServerUrl = LE.stagingServerUrl;
|
||||||
|
LEX.productionServerUrl = LE.productionServerUrl || LE.liveServerUrl;
|
||||||
|
LEX.defaultServerUrl = LEX.productionServerUrl;
|
||||||
|
LEX.createAcmeResponder = createAcmeResponder;
|
||||||
|
LEX.normalizeOptions = lexHelper;
|
||||||
|
LEX.testing = function () {
|
||||||
|
LEX.debug = true;
|
||||||
|
LEX.defaultServerUrl = LEX.stagingServerUrl;
|
||||||
|
LEX.defaultApproveRegistration = true;
|
||||||
|
console.debug = console.log;
|
||||||
|
console.debug('[LEX] testing mode turned on');
|
||||||
|
console.debug('[LEX] default server: ' + LEX.defaultServerUrl);
|
||||||
|
console.debug('\n');
|
||||||
|
console.debug('###################################################');
|
||||||
|
console.debug('# #');
|
||||||
|
console.debug('# Open up a browser and visit this server #');
|
||||||
|
console.debug('# at its domain name. #');
|
||||||
|
console.debug('# #');
|
||||||
|
console.debug('# ENJOY! #');
|
||||||
|
console.debug('# #');
|
||||||
|
console.debug('###################################################');
|
||||||
|
console.debug('\n');
|
||||||
|
console.debug('Note: testing certs will be installed because .testing() was called.');
|
||||||
|
console.debug(' remove .testing() to get live certs.');
|
||||||
|
console.debug('\n');
|
||||||
|
console.debug('[LEX] automatic registration handling turned on for testing.');
|
||||||
|
console.debug('\n');
|
||||||
|
|
||||||
|
return module.exports;
|
||||||
|
};
|
36
main.js
36
main.js
@ -1,36 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
// this is the stuff that should run in the main foreground process,
|
|
||||||
// whether it's single or master
|
|
||||||
|
|
||||||
var major = process.versions.node.split(".")[0];
|
|
||||||
var minor = process.versions.node.split(".")[1];
|
|
||||||
var _hasSetSecureContext = false;
|
|
||||||
var shouldUpgrade = false;
|
|
||||||
|
|
||||||
// TODO can we trust earlier versions as well?
|
|
||||||
if (major >= 12) {
|
|
||||||
_hasSetSecureContext = !!require("http2").createSecureServer({}, function() {}).setSecureContext;
|
|
||||||
} else {
|
|
||||||
_hasSetSecureContext = !!require("https").createServer({}, function() {}).setSecureContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO document in issues
|
|
||||||
if (!_hasSetSecureContext) {
|
|
||||||
// TODO this isn't necessary if greenlock options are set with options.cert
|
|
||||||
console.warn("Warning: node " + process.version + " is missing tlsSocket.setSecureContext().");
|
|
||||||
console.warn(" The default certificate may not be set.");
|
|
||||||
shouldUpgrade = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (major < 11 || (11 === major && minor < 2)) {
|
|
||||||
// https://github.com/nodejs/node/issues/24095
|
|
||||||
console.warn("Warning: node " + process.version + " is missing tlsSocket.getCertificate().");
|
|
||||||
console.warn(" This is necessary to guard against domain fronting attacks.");
|
|
||||||
shouldUpgrade = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldUpgrade) {
|
|
||||||
console.warn("Warning: Please upgrade to node v11.2.0 or greater.");
|
|
||||||
console.warn();
|
|
||||||
}
|
|
160
master.js
160
master.js
@ -1,160 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
require("./main.js");
|
|
||||||
|
|
||||||
var Master = module.exports;
|
|
||||||
|
|
||||||
var cluster = require("cluster");
|
|
||||||
var os = require("os");
|
|
||||||
var msgPrefix = "greenlock:";
|
|
||||||
|
|
||||||
Master.create = function(opts) {
|
|
||||||
var resolveCb;
|
|
||||||
var _readyCb;
|
|
||||||
var _kicked = false;
|
|
||||||
|
|
||||||
var greenlock = require("./greenlock.js").create(opts);
|
|
||||||
|
|
||||||
var ready = new Promise(function(resolve) {
|
|
||||||
resolveCb = resolve;
|
|
||||||
}).then(function(fn) {
|
|
||||||
_readyCb = fn;
|
|
||||||
return fn;
|
|
||||||
});
|
|
||||||
|
|
||||||
function kickoff() {
|
|
||||||
if (_kicked) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_kicked = true;
|
|
||||||
|
|
||||||
Master._spawnWorkers(opts, greenlock);
|
|
||||||
|
|
||||||
ready.then(function(fn) {
|
|
||||||
// not sure what this API should be yet
|
|
||||||
fn();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var master = {
|
|
||||||
serve: function() {
|
|
||||||
kickoff();
|
|
||||||
return master;
|
|
||||||
},
|
|
||||||
master: function(fn) {
|
|
||||||
if (_readyCb) {
|
|
||||||
throw new Error("can't call master twice");
|
|
||||||
}
|
|
||||||
kickoff();
|
|
||||||
resolveCb(fn);
|
|
||||||
return master;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return master;
|
|
||||||
};
|
|
||||||
|
|
||||||
function range(n) {
|
|
||||||
n = parseInt(n, 10);
|
|
||||||
if (!n) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return new Array(n).join(",").split(",");
|
|
||||||
}
|
|
||||||
|
|
||||||
Master._spawnWorkers = function(opts, greenlock) {
|
|
||||||
var numCpus = parseInt(process.env.NUMBER_OF_PROCESSORS, 10) || os.cpus().length;
|
|
||||||
|
|
||||||
// process rpc messages
|
|
||||||
// start when dead
|
|
||||||
var numWorkers = parseInt(opts.workers || opts.numWorkers, 10);
|
|
||||||
if (!numWorkers) {
|
|
||||||
if (numCpus <= 2) {
|
|
||||||
numWorkers = 2;
|
|
||||||
} else {
|
|
||||||
numWorkers = numCpus - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cluster.once("exit", function() {
|
|
||||||
setTimeout(function() {
|
|
||||||
process.exit(3);
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
var workers = range(numWorkers);
|
|
||||||
function next() {
|
|
||||||
if (!workers.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
workers.pop();
|
|
||||||
|
|
||||||
// for a nice aesthetic
|
|
||||||
setTimeout(function() {
|
|
||||||
Master._spawnWorker(opts, greenlock);
|
|
||||||
next();
|
|
||||||
}, 250);
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
Master._spawnWorker = function(opts, greenlock) {
|
|
||||||
var w = cluster.fork();
|
|
||||||
// automatically added to master's `cluster.workers`
|
|
||||||
w.once("exit", function(code, signal) {
|
|
||||||
// TODO handle failures
|
|
||||||
// Should test if the first starts successfully
|
|
||||||
// Should exit if failures happen too quickly
|
|
||||||
|
|
||||||
// For now just kill all when any die
|
|
||||||
if (signal) {
|
|
||||||
console.error("worker was killed by signal:", signal);
|
|
||||||
} else if (code !== 0) {
|
|
||||||
console.error("worker exited with error code:", code);
|
|
||||||
} else {
|
|
||||||
console.error("worker unexpectedly quit without exit code or signal");
|
|
||||||
}
|
|
||||||
process.exit(2);
|
|
||||||
|
|
||||||
//addWorker();
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleMessage(msg) {
|
|
||||||
if (0 !== (msg._id || "").indexOf(msgPrefix)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ("string" !== typeof msg._funcname) {
|
|
||||||
// TODO developer error
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
function rpc() {
|
|
||||||
return greenlock[msg._funcname](msg._input)
|
|
||||||
.then(function(result) {
|
|
||||||
w.send({
|
|
||||||
_id: msg._id,
|
|
||||||
_result: result
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(function(e) {
|
|
||||||
var error = new Error(e.message);
|
|
||||||
Object.getOwnPropertyNames(e).forEach(function(k) {
|
|
||||||
error[k] = e[k];
|
|
||||||
});
|
|
||||||
w.send({
|
|
||||||
_id: msg._id,
|
|
||||||
_error: error
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
rpc();
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Unexpected and uncaught greenlock." + msg._funcname + " error:");
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.on("message", handleMessage);
|
|
||||||
};
|
|
140
package-lock.json
сгенерированный
140
package-lock.json
сгенерированный
@ -1,140 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@root/greenlock-express",
|
|
||||||
"version": "3.0.7",
|
|
||||||
"lockfileVersion": 1,
|
|
||||||
"requires": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@root/acme": {
|
|
||||||
"version": "3.0.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@root/acme/-/acme-3.0.8.tgz",
|
|
||||||
"integrity": "sha512-VmBvLvWdCDkolkanI9Dzm1ouSWPaAa2eCCwcDZcVQbWoNiUIOqbbd57fcMA/gZxLyuJPStD2WXFuEuSMPDxcww==",
|
|
||||||
"requires": {
|
|
||||||
"@root/encoding": "^1.0.1",
|
|
||||||
"@root/keypairs": "^0.9.0",
|
|
||||||
"@root/pem": "^1.0.4",
|
|
||||||
"@root/request": "^1.3.11",
|
|
||||||
"@root/x509": "^0.7.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@root/asn1": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@root/asn1/-/asn1-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-0lfZNuOULKJDJmdIkP8V9RnbV3XaK6PAHD3swnFy4tZwtlMDzLKoM/dfNad7ut8Hu3r91wy9uK0WA/9zym5mig==",
|
|
||||||
"requires": {
|
|
||||||
"@root/encoding": "^1.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@root/csr": {
|
|
||||||
"version": "0.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@root/csr/-/csr-0.8.1.tgz",
|
|
||||||
"integrity": "sha512-hKl0VuE549TK6SnS2Yn9nRvKbFZXn/oAg+dZJU/tlKl/f/0yRXeuUzf8akg3JjtJq+9E592zDqeXZ7yyrg8fSQ==",
|
|
||||||
"requires": {
|
|
||||||
"@root/asn1": "^1.0.0",
|
|
||||||
"@root/pem": "^1.0.4",
|
|
||||||
"@root/x509": "^0.7.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@root/encoding": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@root/encoding/-/encoding-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-OaEub02ufoU038gy6bsNHQOjIn8nUjGiLcaRmJ40IUykneJkIW5fxDqKxQx48cszuNflYldsJLPPXCrGfHs8yQ=="
|
|
||||||
},
|
|
||||||
"@root/greenlock": {
|
|
||||||
"version": "3.0.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/@root/greenlock/-/greenlock-3.0.17.tgz",
|
|
||||||
"integrity": "sha512-1XKhcLFEx1WFdn1Bc2rkAE/SL1ZUJYYMZdbnehTrfhCr5Y+9U1gdkNZnR/jInhoUvcicF/PXuZkGVucU50RNUg==",
|
|
||||||
"requires": {
|
|
||||||
"@root/acme": "^3.0.8",
|
|
||||||
"@root/csr": "^0.8.1",
|
|
||||||
"@root/keypairs": "^0.9.0",
|
|
||||||
"@root/mkdirp": "^1.0.0",
|
|
||||||
"@root/request": "^1.3.10",
|
|
||||||
"acme-http-01-standalone": "^3.0.5",
|
|
||||||
"cert-info": "^1.5.1",
|
|
||||||
"greenlock-manager-fs": "^3.0.1",
|
|
||||||
"greenlock-store-fs": "^3.2.0",
|
|
||||||
"safe-replace": "^1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@root/keypairs": {
|
|
||||||
"version": "0.9.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@root/keypairs/-/keypairs-0.9.0.tgz",
|
|
||||||
"integrity": "sha512-NXE2L9Gv7r3iC4kB/gTPZE1vO9Ox/p14zDzAJ5cGpTpytbWOlWF7QoHSJbtVX4H7mRG/Hp7HR3jWdWdb2xaaXg==",
|
|
||||||
"requires": {
|
|
||||||
"@root/encoding": "^1.0.1",
|
|
||||||
"@root/pem": "^1.0.4",
|
|
||||||
"@root/x509": "^0.7.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@root/mkdirp": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@root/mkdirp/-/mkdirp-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-hxGAYUx5029VggfG+U9naAhQkoMSXtOeXtbql97m3Hi6/sQSRL/4khKZPyOF6w11glyCOU38WCNLu9nUcSjOfA=="
|
|
||||||
},
|
|
||||||
"@root/pem": {
|
|
||||||
"version": "1.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@root/pem/-/pem-1.0.4.tgz",
|
|
||||||
"integrity": "sha512-rEUDiUsHtild8GfIjFE9wXtcVxeS+ehCJQBwbQQ3IVfORKHK93CFnRtkr69R75lZFjcmKYVc+AXDB+AeRFOULA=="
|
|
||||||
},
|
|
||||||
"@root/request": {
|
|
||||||
"version": "1.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.4.1.tgz",
|
|
||||||
"integrity": "sha512-2zSP1v9VhJ3gvm4oph0C4BYCoM3Sj84/Wx4iKdt0IbqbJzfON04EodBq5dsV65UxO/aHZciUBwY2GCZcHqaTYg=="
|
|
||||||
},
|
|
||||||
"@root/x509": {
|
|
||||||
"version": "0.7.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@root/x509/-/x509-0.7.2.tgz",
|
|
||||||
"integrity": "sha512-ENq3LGYORK5NiMFHEVeNMt+fTXaC7DTS6sQXoqV+dFdfT0vmiL5cDLjaXQhaklJQq0NiwicZegzJRl1ZOTp3WQ==",
|
|
||||||
"requires": {
|
|
||||||
"@root/asn1": "^1.0.0",
|
|
||||||
"@root/encoding": "^1.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"acme-http-01-standalone": {
|
|
||||||
"version": "3.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/acme-http-01-standalone/-/acme-http-01-standalone-3.0.5.tgz",
|
|
||||||
"integrity": "sha512-W4GfK+39GZ+u0mvxRVUcVFCG6gposfzEnSBF20T/NUwWAKG59wQT1dUbS1NixRIAsRuhpGc4Jx659cErFQH0Pg=="
|
|
||||||
},
|
|
||||||
"cert-info": {
|
|
||||||
"version": "1.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/cert-info/-/cert-info-1.5.1.tgz",
|
|
||||||
"integrity": "sha512-eoQC/yAgW3gKTKxjzyClvi+UzuY97YCjcl+lSqbsGIy7HeGaWxCPOQFivhUYm27hgsBMhsJJFya3kGvK6PMIcQ=="
|
|
||||||
},
|
|
||||||
"escape-html": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
|
||||||
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
|
|
||||||
},
|
|
||||||
"greenlock-manager-fs": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/greenlock-manager-fs/-/greenlock-manager-fs-3.0.1.tgz",
|
|
||||||
"integrity": "sha512-vZfGFq1TTKxaAqdGDUwNservrNzXx0xCwT/ovG/N378GrhS+U5S8B8LUlNtQU7Fdw6RToMiBcm22OOxSrvZ2zw==",
|
|
||||||
"requires": {
|
|
||||||
"@root/mkdirp": "^1.0.0",
|
|
||||||
"safe-replace": "^1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"greenlock-store-fs": {
|
|
||||||
"version": "3.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/greenlock-store-fs/-/greenlock-store-fs-3.2.0.tgz",
|
|
||||||
"integrity": "sha512-zqcPnF+173oYq5qU7FoGtuqeG8dmmvAiSnz98kEHAHyvgRF9pE1T0MM0AuqDdj45I3kXlCj2gZBwutnRi37J3g==",
|
|
||||||
"requires": {
|
|
||||||
"@root/mkdirp": "^1.0.0",
|
|
||||||
"safe-replace": "^1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"redirect-https": {
|
|
||||||
"version": "1.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/redirect-https/-/redirect-https-1.3.0.tgz",
|
|
||||||
"integrity": "sha512-9GzwI/+Cqw3jlSg0CW6TgBQbhiVhkHSDvW8wjgRQ9IK34wtxS71YJiQeazSCSEqbvowHCJuQZgmQFl1xUHKEgg==",
|
|
||||||
"requires": {
|
|
||||||
"escape-html": "^1.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"safe-replace": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/safe-replace/-/safe-replace-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-9/V2E0CDsKs9DWOOwJH7jYpSl9S3N05uyevNjvsnDauBqRowBPOyot1fIvV5N2IuZAbYyvrTXrYFVG0RZInfFw=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
78
package.json
78
package.json
@ -1,51 +1,57 @@
|
|||||||
{
|
{
|
||||||
"name": "@root/greenlock-express",
|
"name": "letsencrypt-express",
|
||||||
"version": "3.0.10",
|
"version": "1.2.0",
|
||||||
"description": "Free SSL and managed or automatic HTTPS for node.js with Express, Koa, Connect, Hapi, and all other middleware systems.",
|
"description": "Free SSL and Automatic HTTPS for node.js with Express, Connect, and other middleware systems",
|
||||||
"main": "greenlock-express.js",
|
"main": "index.js",
|
||||||
"homepage": "https://greenlock.domains",
|
"bin": {
|
||||||
"files": [
|
"letsencrypt-express": "bin/lex.js",
|
||||||
"*.js",
|
"lex": "bin/lex.js"
|
||||||
"lib",
|
|
||||||
"scripts"
|
|
||||||
],
|
|
||||||
"scripts": {
|
|
||||||
"start": "node_todo server.js ./config.js",
|
|
||||||
"test": "node_todo test/greenlock.js"
|
|
||||||
},
|
},
|
||||||
|
"files": [
|
||||||
|
"lib/",
|
||||||
|
"bin/",
|
||||||
|
"examples/",
|
||||||
|
"index.js"
|
||||||
|
],
|
||||||
"directories": {
|
"directories": {
|
||||||
"example": "examples"
|
"example": "examples"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"scripts": {
|
||||||
"@root/greenlock": "^3.0.17",
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
"redirect-https": "^1.1.5"
|
|
||||||
},
|
},
|
||||||
"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": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.rootprojects.org/root/greenlock-express.js.git"
|
"url": "git+https://github.com/Daplie/letsencrypt-express.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"Let's Encrypt",
|
"acme",
|
||||||
"ACME",
|
"free",
|
||||||
"greenlock",
|
"ssl",
|
||||||
"Free SSL",
|
"tls",
|
||||||
"Automated HTTPS",
|
|
||||||
"https",
|
"https",
|
||||||
"tls"
|
"letsencrypt",
|
||||||
|
"le",
|
||||||
|
"boulder",
|
||||||
|
"express",
|
||||||
|
"expressjs",
|
||||||
|
"connect",
|
||||||
|
"middleware"
|
||||||
],
|
],
|
||||||
"author": "AJ ONeal <coolaj86@gmail.com> (https://solderjs.com/)",
|
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
||||||
"license": "MPL-2.0",
|
"license": "(MIT OR Apache-2.0)",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://git.rootprojects.org/root/greenlock-express.js/issues"
|
"url": "https://github.com/Daplie/letsencrypt-express/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/Daplie/letsencrypt-express#readme",
|
||||||
|
"devDependencies": {
|
||||||
|
"body-parser": "^1.14.2",
|
||||||
|
"cli": "^0.11.1",
|
||||||
|
"express": "^4.13.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"homedir": "^0.6.0",
|
||||||
|
"letsencrypt": "^1.5.0",
|
||||||
|
"localhost.daplie.com-certificates": "^1.1.2",
|
||||||
|
"mkdirp": "^0.5.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,77 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// BG WH \u001b[47m
|
|
||||||
// BOLD \u001b[1m
|
|
||||||
// RED \u001b[31m
|
|
||||||
// GREEN \u001b[32m
|
|
||||||
// RESET \u001b[0m
|
|
||||||
|
|
||||||
var grabbers = [
|
|
||||||
[
|
|
||||||
"",
|
|
||||||
"================================================================================",
|
|
||||||
"",
|
|
||||||
" 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥",
|
|
||||||
"🔥 🔥",
|
|
||||||
"🔥 Do you rely on Greenlock? 🔥",
|
|
||||||
"🔥 🔥",
|
|
||||||
" 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥"
|
|
||||||
],
|
|
||||||
|
|
||||||
[
|
|
||||||
"",
|
|
||||||
"================================================================================",
|
|
||||||
"",
|
|
||||||
" 🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒",
|
|
||||||
"🍒 🍒",
|
|
||||||
"🍒 Do you rely on Greenlock? 🍒",
|
|
||||||
"🍒 🍒",
|
|
||||||
" 🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒🍒"
|
|
||||||
],
|
|
||||||
|
|
||||||
[
|
|
||||||
"",
|
|
||||||
"================================================================================",
|
|
||||||
"",
|
|
||||||
" 👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇",
|
|
||||||
"👉 👈",
|
|
||||||
"👉 Do you rely on Greenlock? 👈",
|
|
||||||
"👉 👈",
|
|
||||||
" 👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆 "
|
|
||||||
],
|
|
||||||
|
|
||||||
[
|
|
||||||
"",
|
|
||||||
"================================================================================",
|
|
||||||
"",
|
|
||||||
" 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 ",
|
|
||||||
"👀 👀",
|
|
||||||
"👀 Do you rely on Greenlock? 👀",
|
|
||||||
"👀 👀",
|
|
||||||
" 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 👀 ",
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
grabbers[Math.floor(Math.random() * grabbers.length)].concat([
|
|
||||||
"",
|
|
||||||
"Hey! Let's Encrypt will \u001b[31mSTOP WORKING\u001b[0m with Greenlock v2 at the end of October,",
|
|
||||||
"and \u001b[31mWITHOUT YOUR HELP\u001b[0m we won't get the next release out in time.",
|
|
||||||
"",
|
|
||||||
"If Greenlock has saved you time and money, and taken stress out of your life,",
|
|
||||||
"or you just love it, please reach out to return the favor today:",
|
|
||||||
"",
|
|
||||||
"\u001b[31mSAVE GREENLOCK:\u001b[0m",
|
|
||||||
"https://indiegogo.com/at/greenlock",
|
|
||||||
"",
|
|
||||||
"================================================================================",
|
|
||||||
""
|
|
||||||
]).forEach(function(line) {
|
|
||||||
console.info(line);
|
|
||||||
});
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
// give time to read
|
|
||||||
}, 1500);
|
|
157
servers.js
157
servers.js
@ -1,157 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
var Servers = module.exports;
|
|
||||||
|
|
||||||
var http = require("http");
|
|
||||||
var HttpMiddleware = require("./http-middleware.js");
|
|
||||||
var HttpsMiddleware = require("./https-middleware.js");
|
|
||||||
var sni = require("./sni.js");
|
|
||||||
var cluster = require("cluster");
|
|
||||||
|
|
||||||
Servers.create = function(greenlock) {
|
|
||||||
var servers = {};
|
|
||||||
var _httpServer;
|
|
||||||
var _httpsServer;
|
|
||||||
|
|
||||||
function startError(e) {
|
|
||||||
explainError(e);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
servers.httpServer = function(defaultApp) {
|
|
||||||
if (_httpServer) {
|
|
||||||
return _httpServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
_httpServer = http.createServer(HttpMiddleware.create(greenlock, defaultApp));
|
|
||||||
_httpServer.once("error", startError);
|
|
||||||
|
|
||||||
return _httpServer;
|
|
||||||
};
|
|
||||||
|
|
||||||
var _middlewareApp;
|
|
||||||
|
|
||||||
servers.httpsServer = function(secureOpts, defaultApp) {
|
|
||||||
if (defaultApp) {
|
|
||||||
// TODO guard against being set twice?
|
|
||||||
_middlewareApp = defaultApp;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_httpsServer) {
|
|
||||||
if (secureOpts && Object.keys(secureOpts).length) {
|
|
||||||
throw new Error("Call glx.httpsServer(tlsOptions) before calling glx.serveApp(app)");
|
|
||||||
}
|
|
||||||
return _httpsServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!secureOpts) {
|
|
||||||
secureOpts = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
_httpsServer = createSecureServer(
|
|
||||||
wrapDefaultSniCallback(greenlock, secureOpts),
|
|
||||||
HttpsMiddleware.create(greenlock, function(req, res) {
|
|
||||||
if (!_middlewareApp) {
|
|
||||||
throw new Error("Set app with `glx.serveApp(app)` or `glx.httpsServer(tlsOptions, app)`");
|
|
||||||
}
|
|
||||||
_middlewareApp(req, res);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
_httpsServer.once("error", startError);
|
|
||||||
|
|
||||||
return _httpsServer;
|
|
||||||
};
|
|
||||||
|
|
||||||
servers.id = function() {
|
|
||||||
return (cluster.isWorker && cluster.worker.id) || "0";
|
|
||||||
};
|
|
||||||
servers.serveApp = function(app) {
|
|
||||||
return new Promise(function(resolve, reject) {
|
|
||||||
if ("function" !== typeof app) {
|
|
||||||
reject(new Error("glx.serveApp(app) expects a node/express app in the format `function (req, res) { ... }`"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var id = cluster.isWorker && cluster.worker.id;
|
|
||||||
var idstr = (id && "#" + id + " ") || "";
|
|
||||||
var plainServer = servers.httpServer(require("redirect-https")());
|
|
||||||
var plainAddr = "0.0.0.0";
|
|
||||||
var plainPort = 80;
|
|
||||||
plainServer.listen(plainPort, plainAddr, function() {
|
|
||||||
console.info(
|
|
||||||
idstr + "Listening on",
|
|
||||||
plainAddr + ":" + plainPort,
|
|
||||||
"for ACME challenges, and redirecting to HTTPS"
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO fetch greenlock.servername
|
|
||||||
_middlewareApp = app || _middlewareApp;
|
|
||||||
var secureServer = servers.httpsServer(null, app);
|
|
||||||
var secureAddr = "0.0.0.0";
|
|
||||||
var securePort = 443;
|
|
||||||
secureServer.listen(securePort, secureAddr, function() {
|
|
||||||
console.info(idstr + "Listening on", secureAddr + ":" + securePort, "for secure traffic");
|
|
||||||
|
|
||||||
plainServer.removeListener("error", startError);
|
|
||||||
secureServer.removeListener("error", startError);
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return servers;
|
|
||||||
};
|
|
||||||
|
|
||||||
function explainError(e) {
|
|
||||||
console.error();
|
|
||||||
console.error("Error: " + e.message);
|
|
||||||
if ("EACCES" === e.errno) {
|
|
||||||
console.error("You don't have prmission to access '" + e.address + ":" + e.port + "'.");
|
|
||||||
console.error('You probably need to use "sudo" or "sudo setcap \'cap_net_bind_service=+ep\' $(which node)"');
|
|
||||||
} else if ("EADDRINUSE" === e.errno) {
|
|
||||||
console.error("'" + e.address + ":" + e.port + "' is already being used by some other program.");
|
|
||||||
console.error("You probably need to stop that program or restart your computer.");
|
|
||||||
} else {
|
|
||||||
console.error(e.code + ": '" + e.address + ":" + e.port + "'");
|
|
||||||
}
|
|
||||||
console.error();
|
|
||||||
}
|
|
||||||
|
|
||||||
function wrapDefaultSniCallback(greenlock, secureOpts) {
|
|
||||||
// I'm not sure yet if the original SNICallback
|
|
||||||
// should be called before or after, so I'm just
|
|
||||||
// going to delay making that choice until I have the use case
|
|
||||||
/*
|
|
||||||
if (!secureOpts.SNICallback) {
|
|
||||||
secureOpts.SNICallback = function(servername, cb) {
|
|
||||||
cb(null, null);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
if (secureOpts.SNICallback) {
|
|
||||||
console.warn();
|
|
||||||
console.warn("[warning] Ignoring the given tlsOptions.SNICallback function.");
|
|
||||||
console.warn();
|
|
||||||
console.warn(" We're very open to implementing support for this,");
|
|
||||||
console.warn(" we just don't understand the use case yet.");
|
|
||||||
console.warn(" Please open an issue to discuss. We'd love to help.");
|
|
||||||
console.warn();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO greenlock.servername for workers
|
|
||||||
secureOpts.SNICallback = sni.create(greenlock, secureOpts);
|
|
||||||
return secureOpts;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createSecureServer(secureOpts, fn) {
|
|
||||||
var major = process.versions.node.split(".")[0];
|
|
||||||
|
|
||||||
// TODO can we trust earlier versions as well?
|
|
||||||
if (major >= 12) {
|
|
||||||
secureOpts.allowHTTP1 = true;
|
|
||||||
return require("http2").createSecureServer(secureOpts, fn);
|
|
||||||
} else {
|
|
||||||
return require("https").createServer(secureOpts, fn);
|
|
||||||
}
|
|
||||||
}
|
|
25
single.js
25
single.js
@ -1,25 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
require("./main.js");
|
|
||||||
|
|
||||||
var Single = module.exports;
|
|
||||||
var Servers = require("./servers.js");
|
|
||||||
|
|
||||||
Single.create = function(opts) {
|
|
||||||
var greenlock = require("./greenlock.js").create(opts);
|
|
||||||
|
|
||||||
var servers = Servers.create(greenlock);
|
|
||||||
|
|
||||||
var single = {
|
|
||||||
serve: function(fn) {
|
|
||||||
fn(servers);
|
|
||||||
return single;
|
|
||||||
},
|
|
||||||
master: function(/*fn*/) {
|
|
||||||
// ignore
|
|
||||||
//fn(master);
|
|
||||||
return single;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return single;
|
|
||||||
};
|
|
194
sni.js
194
sni.js
@ -1,194 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
var sni = module.exports;
|
|
||||||
var tls = require("tls");
|
|
||||||
var servernameRe = /^[a-z0-9\.\-]+$/i;
|
|
||||||
|
|
||||||
// a nice, round, irrational number - about every 6¼ hours
|
|
||||||
var refreshOffset = Math.round(Math.PI * 2 * (60 * 60 * 1000));
|
|
||||||
// and another, about 15 minutes
|
|
||||||
var refreshStagger = Math.round(Math.PI * 5 * (60 * 1000));
|
|
||||||
// and another, about 30 seconds
|
|
||||||
var smallStagger = Math.round(Math.PI * (30 * 1000));
|
|
||||||
|
|
||||||
//secureOpts.SNICallback = sni.create(greenlock, secureOpts);
|
|
||||||
sni.create = function(greenlock, secureOpts) {
|
|
||||||
var _cache = {};
|
|
||||||
var defaultServername = greenlock.servername || "";
|
|
||||||
|
|
||||||
if (secureOpts.cert) {
|
|
||||||
// Note: it's fine if greenlock.servername is undefined,
|
|
||||||
// but if the caller wants this to auto-renew, they should define it
|
|
||||||
_cache[defaultServername] = {
|
|
||||||
refreshAt: 0,
|
|
||||||
secureContext: tls.createSecureContext(secureOpts)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return getSecureContext;
|
|
||||||
|
|
||||||
function notify(ev, args) {
|
|
||||||
try {
|
|
||||||
// TODO _notify() or notify()?
|
|
||||||
(greenlock.notify || greenlock._notify)(ev, args);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(ev, args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSecureContext(servername, cb) {
|
|
||||||
//console.log("debug sni", servername);
|
|
||||||
if ("string" !== typeof servername) {
|
|
||||||
// this will never happen... right? but stranger things have...
|
|
||||||
console.error("[sanity fail] non-string servername:", servername);
|
|
||||||
cb(new Error("invalid servername"), null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var secureContext = getCachedContext(servername);
|
|
||||||
if (secureContext) {
|
|
||||||
//console.log("debug sni got cached context", servername, getCachedMeta(servername));
|
|
||||||
cb(null, secureContext);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFreshContext(servername)
|
|
||||||
.then(function(secureContext) {
|
|
||||||
if (secureContext) {
|
|
||||||
//console.log("debug sni got fresh context", servername, getCachedMeta(servername));
|
|
||||||
cb(null, secureContext);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Note: this does not replace tlsSocket.setSecureContext()
|
|
||||||
// as it only works when SNI has been sent
|
|
||||||
//console.log("debug sni got default context", servername, getCachedMeta(servername));
|
|
||||||
cb(null, getDefaultContext());
|
|
||||||
})
|
|
||||||
.catch(function(err) {
|
|
||||||
if (!err.context) {
|
|
||||||
err.context = "sni_callback";
|
|
||||||
}
|
|
||||||
notify("error", err);
|
|
||||||
//console.log("debug sni error", servername, err);
|
|
||||||
cb(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCachedMeta(servername) {
|
|
||||||
var meta = _cache[servername];
|
|
||||||
if (!meta) {
|
|
||||||
if (!_cache[wildname(servername)]) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return meta;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCachedContext(servername) {
|
|
||||||
var meta = getCachedMeta(servername);
|
|
||||||
if (!meta) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// always renew in background
|
|
||||||
if (!meta.refreshAt || Date.now() >= meta.refreshAt) {
|
|
||||||
getFreshContext(servername).catch(function(e) {
|
|
||||||
if (!e.context) {
|
|
||||||
e.context = "sni_background_refresh";
|
|
||||||
}
|
|
||||||
notify("error", e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// under normal circumstances this would never be expired
|
|
||||||
// and, if it is expired, something is so wrong it's probably
|
|
||||||
// not worth wating for the renewal - it has probably failed
|
|
||||||
return meta.secureContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFreshContext(servername) {
|
|
||||||
var meta = getCachedMeta(servername);
|
|
||||||
if (!meta && !validServername(servername)) {
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (meta) {
|
|
||||||
// prevent stampedes
|
|
||||||
meta.refreshAt = Date.now() + randomRefreshOffset();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO don't get unknown certs at all, rely on auto-updates from greenlock
|
|
||||||
// Note: greenlock.get() will return an existing fresh cert or issue a new one
|
|
||||||
return greenlock.get({ servername: servername }).then(function(result) {
|
|
||||||
var meta = getCachedMeta(servername);
|
|
||||||
if (!meta) {
|
|
||||||
meta = _cache[servername] = { secureContext: { _valid: false } };
|
|
||||||
}
|
|
||||||
// prevent from being punked by bot trolls
|
|
||||||
meta.refreshAt = Date.now() + smallStagger;
|
|
||||||
|
|
||||||
// nothing to do
|
|
||||||
if (!result) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we only care about the first one
|
|
||||||
var pems = result.pems;
|
|
||||||
var site = result.site;
|
|
||||||
if (!pems || !pems.cert) {
|
|
||||||
// nothing to do
|
|
||||||
// (and the error should have been reported already)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
meta = {
|
|
||||||
refreshAt: Date.now() + randomRefreshOffset(),
|
|
||||||
secureContext: tls.createSecureContext({
|
|
||||||
// TODO support passphrase-protected privkeys
|
|
||||||
key: pems.privkey,
|
|
||||||
cert: pems.cert + "\n" + pems.chain + "\n"
|
|
||||||
})
|
|
||||||
};
|
|
||||||
meta.secureContext._valid = true;
|
|
||||||
|
|
||||||
// copy this same object into every place
|
|
||||||
(result.altnames || site.altnames || [result.subject || site.subject]).forEach(function(altname) {
|
|
||||||
_cache[altname] = meta;
|
|
||||||
});
|
|
||||||
|
|
||||||
return meta.secureContext;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDefaultContext() {
|
|
||||||
return getCachedContext(defaultServername);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// whenever we need to know when to refresh next
|
|
||||||
function randomRefreshOffset() {
|
|
||||||
var stagger = Math.round(refreshStagger / 2) - Math.round(Math.random() * refreshStagger);
|
|
||||||
return refreshOffset + stagger;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validServername(servername) {
|
|
||||||
// format and (lightly) sanitize sni so that users can be naive
|
|
||||||
// and not have to worry about SQL injection or fs discovery
|
|
||||||
|
|
||||||
servername = (servername || "").toLowerCase();
|
|
||||||
// hostname labels allow a-z, 0-9, -, and are separated by dots
|
|
||||||
// _ is sometimes allowed, but not as a "hostname", and not by Let's Encrypt ACME
|
|
||||||
// REGEX // https://www.codeproject.com/Questions/1063023/alphanumeric-validation-javascript-without-regex
|
|
||||||
return servernameRe.test(servername) && -1 === servername.indexOf("..");
|
|
||||||
}
|
|
||||||
|
|
||||||
function wildname(servername) {
|
|
||||||
return (
|
|
||||||
"*." +
|
|
||||||
servername
|
|
||||||
.split(".")
|
|
||||||
.slice(1)
|
|
||||||
.join(".")
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,85 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
var Greenlock = require("../");
|
|
||||||
var greenlock = Greenlock.create({
|
|
||||||
version: "draft-11",
|
|
||||||
server: "https://acme-staging-v02.api.letsencrypt.org/directory",
|
|
||||||
agreeTos: true,
|
|
||||||
approvedDomains: ["example.com", "www.example.com"],
|
|
||||||
configDir: require("path").join(require("os").tmpdir(), "acme"),
|
|
||||||
|
|
||||||
app: require("express")().use("/", function(req, res) {
|
|
||||||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
||||||
res.end("Hello, World!\n\n💚 🔒.js");
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
var server1 = greenlock.listen(5080, 5443);
|
|
||||||
server1.on("listening", function() {
|
|
||||||
console.log("### THREE 3333 - All is well server1", this.address());
|
|
||||||
setTimeout(function() {
|
|
||||||
// so that the address() object doesn't disappear
|
|
||||||
server1.close();
|
|
||||||
server1.unencrypted.close();
|
|
||||||
}, 10);
|
|
||||||
});
|
|
||||||
setTimeout(function() {
|
|
||||||
var server2 = greenlock.listen(6080, 6443, function() {
|
|
||||||
console.log("### FIVE 55555 - Started server 2!");
|
|
||||||
setTimeout(function() {
|
|
||||||
server2.close();
|
|
||||||
server2.unencrypted.close();
|
|
||||||
server6.close();
|
|
||||||
server6.unencrypted.close();
|
|
||||||
server7.close();
|
|
||||||
server7.unencrypted.close();
|
|
||||||
setTimeout(function() {
|
|
||||||
// TODO greenlock needs a close event (and to listen to its server's close event)
|
|
||||||
process.exit(0);
|
|
||||||
}, 1000);
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
server2.on("listening", function() {
|
|
||||||
console.log("### FOUR 44444 - All is well server2", server2.address());
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
var server3 = greenlock.listen(
|
|
||||||
22,
|
|
||||||
22,
|
|
||||||
function() {
|
|
||||||
console.error("Error: expected to get an error when launching plain server on port 22");
|
|
||||||
},
|
|
||||||
function() {
|
|
||||||
console.error("Error: expected to get an error when launching " + server3.type + " server on port 22");
|
|
||||||
}
|
|
||||||
);
|
|
||||||
server3.unencrypted.on("error", function() {
|
|
||||||
console.log("Success: caught expected (plain) error");
|
|
||||||
});
|
|
||||||
server3.on("error", function() {
|
|
||||||
console.log("Success: caught expected " + server3.type + " error");
|
|
||||||
//server3.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
var server4 = greenlock.listen(
|
|
||||||
7080,
|
|
||||||
7443,
|
|
||||||
function() {
|
|
||||||
console.log("Success: server4: plain");
|
|
||||||
server4.unencrypted.close();
|
|
||||||
},
|
|
||||||
function() {
|
|
||||||
console.log("Success: server4: " + server4.type);
|
|
||||||
server4.close();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
var server5 = greenlock.listen(10080, 10443, function() {
|
|
||||||
console.log("Server 5 with one fn", this.address());
|
|
||||||
server5.close();
|
|
||||||
server5.unencrypted.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
var server6 = greenlock.listen("[::]:11080", "[::1]:11443");
|
|
||||||
|
|
||||||
var server7 = greenlock.listen("/tmp/gl.plain.sock", "/tmp/gl.sec.sock");
|
|
62
worker.js
62
worker.js
@ -1,62 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
var Worker = module.exports;
|
|
||||||
// *very* generous, but well below the http norm of 120
|
|
||||||
var messageTimeout = 30 * 1000;
|
|
||||||
var msgPrefix = "greenlock:";
|
|
||||||
|
|
||||||
Worker.create = function() {
|
|
||||||
var greenlock = {};
|
|
||||||
["getAcmeHttp01ChallengeResponse", "get", "notify"].forEach(function(k) {
|
|
||||||
greenlock[k] = function(args) {
|
|
||||||
return rpc(k, args);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
var worker = {
|
|
||||||
serve: function(fn) {
|
|
||||||
var servers = require("./servers.js").create(greenlock);
|
|
||||||
fn(servers);
|
|
||||||
return worker;
|
|
||||||
},
|
|
||||||
master: function() {
|
|
||||||
// ignore
|
|
||||||
return worker;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return worker;
|
|
||||||
};
|
|
||||||
|
|
||||||
function rpc(funcname, msg) {
|
|
||||||
return new Promise(function(resolve, reject) {
|
|
||||||
var rnd = Math.random()
|
|
||||||
.toString()
|
|
||||||
.slice(2)
|
|
||||||
.toString(16);
|
|
||||||
var id = msgPrefix + rnd;
|
|
||||||
var timeout;
|
|
||||||
|
|
||||||
function getResponse(msg) {
|
|
||||||
if (msg._id !== id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
process.removeListener("message", getResponse);
|
|
||||||
clearTimeout(timeout);
|
|
||||||
resolve(msg._result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO keep a single listener than just responds
|
|
||||||
// via a collection of callbacks? or leave as is?
|
|
||||||
process.on("message", getResponse);
|
|
||||||
process.send({
|
|
||||||
_id: id,
|
|
||||||
_funcname: funcname,
|
|
||||||
_input: msg
|
|
||||||
});
|
|
||||||
|
|
||||||
timeout = setTimeout(function() {
|
|
||||||
process.removeListener("message", getResponse);
|
|
||||||
reject(new Error("worker rpc request timeout"));
|
|
||||||
}, messageTimeout);
|
|
||||||
});
|
|
||||||
}
|
|
Загрузка…
x
Ссылка в новой задаче
Block a user