Compare commits

..

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

46 changed files with 2385 additions and 5721 deletions

8
.gitignore vendored
View File

@ -1,12 +1,6 @@
greenlock.json*
TODO*
TODO.txt
link.sh
.env
.greenlockrc
# generated by init
app.js
server.js
example.js
# ---> Node
# Logs

View File

@ -1,8 +0,0 @@
{
"bracketSpacing": true,
"printWidth": 80,
"singleQuote": true,
"tabWidth": 4,
"trailingComma": "none",
"useTabs": false
}

View File

@ -1,403 +0,0 @@
# Migrating Guide
Greenlock v4 is the current version.
# v3 to v4
v4 is a very minor, but breaking, change from v3
### `configFile` is replaced with `configDir`
The default config file `./greenlock.json` is now `./greenlock.d/config.json`.
This was change was mode to eliminate unnecessary configuration that was inadvertantly introduced in v3.
### `.greenlockrc` is auto-generated
`.greenlockrc` exists for the sake of tooling - so that the CLI, Web API, and your code naturally stay in sync.
It looks like this:
```json
{
"manager": {
"module": "@greenlock/manager"
},
"configDir": "./greenlock.d"
}
```
If you deploy to a read-only filesystem, it is best that you create the `.greenlockrc` file as part
of your image and use that rather than including any configuration in your code.
# v2 to v4
**Greenlock Express** uses Greenlock directly, the same as before.
All options described for `Greenlock.create({...})` also apply to the Greenlock Express `init()` callback.
# Overview of Major Differences
- Reduced API
- No code in the config
- (config is completely serializable)
- Manager callbacks replace `approveDomains`
- Greenlock Express does more, with less config
- cluster is supported out-of-the-box
- high-performance
- scalable
- ACME challenges are simplified
- init
- zones (dns-01)
- set
- get
- remove
- Store callbacks are simplified
- accounts
- checkKeypairs
- certificates
- checkKeypairs
- check
- set
# Greenlock JavaScript API greatly reduced
Whereas before there were many different methods with nuance differences,
now there's just `create`, `get`, `renew`, and sometimes `add` ().
- Greenlock.create({ maintainerEmail, packageAgent, notify })
- Greenlock.get({ servername, wildname, duplicate, force })
- (just a convenience wrapper around renew)
- Greenlock.renew({ subject, altnames, issuedBefore, expiresAfter })
- (retrieves, issues, renews, all-in-one)
- _optional_ Greenlock.add({ subject, altnames, subscriberEmail })
- (partially replaces `approveDomains`)
Also, some disambiguation on terms:
- `domains` was often ambiguous and confusing, it has been replaced by:
- `subject` refers to the subject of a certificate - the primary domain
- `altnames` refers to the domains in the SAN (Subject Alternative Names) section of the certificate
- `servername` refers to the TLS (SSL) SNI (Server Name Indication) request for a cetificate
- `wildname` refers to the wildcard version of the servername (ex: `www.example.com => *.example.com`)
When you create an instance of Greenlock, you only supply package and maintainer info.
All other configuration is A) optional and B) handled by the _Manager_.
```js
'use strict';
var pkg = require('./package.json');
var Greenlock = require('greenlock');
var greenlock = Greenlock.create({
// used for the ACME client User-Agent string as per RFC 8555 and RFC 7231
packageAgent: pkg.name + '/' + pkg.version,
// used as the contact for critical bug and security notices
// should be the same as pkg.author.email
maintainerEmail: 'jon@example.com',
// used for logging background events and errors
notify: function(ev, args) {
if ('error' === ev || 'warning' === ev) {
console.error(ev, args);
return;
}
console.info(ev, args);
}
});
```
By default **no certificates will be issued**. See the _manager_ section.
When you want to get a single certificate, you use `get`, which will:
- will return null if neither the `servername` or its `wildname` (wildcard) variant can be found
- retrieve a non-expired certificate, if possible
- will renew the certificate in the background, if stale
- will wait for the certificate to be issued if new
```js
greenlock
.get({ servername: 'www.example.com' })
.then(function(result) {
if (!result) {
// certificate is not on the approved list
return null;
}
var fullchain = result.pems.cert + '\n' + result.pems.chain + '\n';
var privkey = result.pems.privkey;
return {
fullchain: fullchain,
privkey: privkey
};
})
.catch(function(e) {
// something went wrong in the renew process
console.error(e);
});
```
By default **no certificates will be issued**. See the _manager_ section.
When you want to renew certificates, _en masse_, you use `renew`, which will:
- check all certificates matching the given criteria
- only renew stale certificates by default
- return error objects (will NOT throw exception for failed renewals)
```js
greenlock
.renew({})
.then(function(results) {
if (!result.length) {
// no certificates found
return null;
}
// [{ site, error }]
return results;
})
.catch(function(e) {
// an unexpected error, not related to renewal
console.error(e);
});
```
Options:
| Option | Description |
| ------------- | -------------------------------------------------------------------------- |
| `altnames` | only check and renew certs matching these altnames (including wildcards) |
| `renewBefore` | only check and renew certs marked for renewal before the given date, in ms |
| `duplicate` | renew certificates regardless of timing |
| `force` | allow silly things, like tiny `renewOffset`s |
By default **no certificates will be issued**. See the _manager_ section.
# Greenlock Express Example
The options that must be returned from `init()` are the same that are used in `Greenlock.create()`,
with a few extra that are specific to Greenlock Express:
```js
require('@root/greenlock-express')
.init(function() {
// This object will be passed to Greenlock.create()
var options = {
// some options, like cluster, are special to Greenlock Express
cluster: false,
// The rest are the same as for Greenlock
packageAgent: pkg.name + '/' + pkg.version,
maintainerEmail: 'jon@example.com',
notify: function(ev, args) {
console.info(ev, args);
}
};
return options;
})
.serve(function(glx) {
// will start servers on port 80 and 443
glx.serveApp(function(req, res) {
res.end('Hello, Encrypted World!');
});
// you can get access to the raw server (i.e. for websockets)
glx.httpsServer(); // returns raw server object
});
```
# _Manager_ replaces `approveDomains`
`approveDomains` was always a little confusing. Most people didn't need it.
Instead, now there is a simple config file that will work for most people,
as well as a set of callbacks for easy configurability.
### Default Manager
The default manager is `@greenlock/manager` and the default `configDir` is `./.greenlock.d`.
The config file should look something like this:
`./greenlock.d/config.json`:
```json
{
"subscriberEmail": "jon@example.com",
"agreeToTerms": true,
"sites": {
"example.com": {
"subject": "example.com",
"altnames": ["example.com", "www.example.com"]
}
}
}
```
You can specify a `acme-dns-01-*` or `acme-http-01-*` challenge plugin globally, or per-site.
```json
{
"subscriberEmail": "jon@example.com",
"agreeToTerms": true,
"sites": {
"example.com": {
"subject": "example.com",
"altnames": ["example.com", "www.example.com"],
"challenges": {
"dns-01": {
"module": "acme-dns-01-digitalocean",
"token": "apikey-xxxxx"
}
}
}
}
}
```
The same is true with `greenlock-store-*` plugins:
```json
{
"subscriberEmail": "jon@example.com",
"agreeToTerms": true,
"sites": {
"example.com": {
"subject": "example.com",
"altnames": ["example.com", "www.example.com"]
}
},
"store": {
"module": "greenlock-store-fs",
"basePath": "~/.config/greenlock"
}
}
```
### Customer Manager, the lazy way
At the very least you have to implement `get({ servername, wildname })`.
```js
var greenlock = Greenlock.create({
packageAgent: pkg.name + '/' + pkg.version,
maintainerEmail: 'jon@example.com',
notify: notify,
packageRoot: __dirname,
manager: {
module: './manager.js'
}
});
function notify(ev, args) {
if ('error' === ev || 'warning' === ev) {
console.error(ev, args);
return;
}
console.info(ev, args);
}
```
In the simplest case you can ignore all incoming options
and return a single site config in the same format as the config file
`./manager.js`:
```js
'use strict';
module.exports.create = function() {
return {
get: async function({ servername }) {
// do something to fetch the site
var site = {
subject: 'example.com',
altnames: ['example.com', 'www.example.com']
};
return site;
}
};
};
```
If you want to use wildcards or local domains for a specific domain, you must specify the `dns-01` challenge plugin to use:
```js
'use strict';
module.exports.create = function() {
return {
get: async function({ servername }) {
// do something to fetch the site
var site = {
subject: 'example.com',
altnames: ['example.com', 'www.example.com'],
// dns-01 challenge
challenges: {
'dns-01': {
module: 'acme-dns-01-namedotcom',
apikey: 'xxxx'
}
}
};
return site;
}
};
};
```
### Customer Manager, Complete
See <https://git.rootprojects.org/root/greenlock-manager-test.js#quick-start>
# ACME Challenge Plugins
The ACME challenge plugins are just a few simple callbacks:
- `init`
- `zones` (dns-01 only)
- `set`
- `get`
- `remove`
They are described here:
- [dns-01 documentation](https://git.rootprojects.org/root/acme-dns-01-test.js)
- [http-01 documentation](https://git.rootprojects.org/root/acme-http-01-test.js)
# Key and Cert Store Plugins
Again, these are just a few simple callbacks:
- `certificates.checkKeypair`
- `certificates.check`
- `certificates.setKeypair`
- `certificates.set`
- `accounts.checkKeypair`
- `accounts.check` (optional)
- `accounts.setKeypair`
- `accounts.set` (optional)
The name `check` is used instead of `get` because they only need to return something if it exists. They do not need to fail, nor do they need to generate anything.
They are described here:
- [greenlock store documentation](https://git.rootprojects.org/root/greenlock-store-test.js)
If you are just implenting in-house and are not going to publish a module, you can also do some hack things like this:

622
README.md
View File

@ -1,29 +1,8 @@
# New Documentation &amp; [v4 Migration Guide](https://git.rootprojects.org/root/greenlock.js/src/branch/master/MIGRATION_GUIDE.md)
# @root/greenlock
We're still working on the full documentation for this new version,
so please be patient.
🔐 Free SSL, Free Wildcard SSL, and Fully Automated HTTPS for Node.js and Browsers, issued by Let's Encrypt v2 via ACME
To start, check out the
[Migration Guide](https://git.rootprojects.org/root/greenlock.js/src/branch/master/MIGRATION_GUIDE.md).
!["Greenlock Logo"](https://git.rootprojects.org/root/greenlock.js/raw/branch/master/logo/greenlock-1063x250.png 'Greenlock lock logo and work mark')
!["Greenlock Function"](https://git.rootprojects.org/root/greenlock.js/raw/branch/master/logo/from-not-secure-to-secure-url-bar.png 'from url bar showing not secure to url bar showing secure')
# [Greenlock](https://git.rootprojects.org/root/greenlock.js) is Let's Encrypt for JavaScript
| Built by [Root](https://rootprojects.org) for [Hub](https://rootprojects.org/hub/)
Greenlock&trade; is an Automated Certificate Management Environement 🔐.
| **Greenlock** | [Greenlock Express](https://git.rootprojects.org/root/greenlock-express.js) | [ACME.js](https://git.rootprojects.org/root/acme.js) |
It uses **Let's Encrypt** to generate Free SSL Certificates, including **Wildcard** SSL.
It supports **Automated Renewal** of certs for Fully Automated HTTPS.
It's written in plain JavaScript and works in Node, Browsers, and WebPack.
the easiest way to integrate Let's Encrypt into your projects, products, and infrastructure.
Greenlock&trade; is the easiest way to integrate Let's Encrypt into your projects, products, and infrastructure.
- [x] **Wildcard** Certificates
- [x] **IoT** Environments
@ -60,351 +39,114 @@ TODO
-->
# Quick Start
# JavaScript Library
Greenlock is fully-automated, **SSL Certificate Manager** for IoT, Web Hosting, and Enterprise On-Prem, Edge, and Hybrid Cloud.
(though we started building it for [Home Servers](https://rootprojects.org/hub/))
You can use it for one-off certificates, like `certbot`,
but it is _much_ more powerful than that.
By setting just a few callbacks to let it know where it should store private keys and certificates,
it will automatically renew any certificate that you add to it, as long as the process is running.
Certificates are renewed every 45 days by default, and renewal checks will happen several times a day.
<details>
<summary>1. Configure</summary>
```js
'use strict';
var pkg = require('./package.json');
var Greenlock = require('greenlock');
var greenlock = Greenlock.create({
packageRoot: __dirname,
configDir: "./greenlock.d/",
packageAgent: pkg.name + '/' + pkg.version,
maintainerEmail: pkg.author,
staging: true,
notify: function(event, details) {
if ('error' === event) {
// `details` is an error object in this case
console.error(details);
}
}
});
greenlock.manager
.defaults({
agreeToTerms: true,
subscriberEmail: 'webhosting@example.com'
})
.then(function(fullConfig) {
// ...
});
```
</details>
<details>
<summary>2. Add Domains</summary>
The `subject` (primary domain on certificate) will be the id,
so it's very important that the order of the given domains
be deterministic.
```js
var altnames = ['example.com', 'www.example.com'];
greenlock
.add({
subject: altnames[0],
altnames: altnames
})
.then(function() {
// saved config to db (or file system)
});
```
Issuance and renewal will start immediately, and run continually.
</details>
<details>
<summary>3. Test for Success</summary>
The `store` callbacks will be called every any of your certificates
are renewed.
However, you can do a quick one-off check with `get`.
It will return a certificate immediately (if available),
or wait for the renewal to complete (or for it to fail again).
```js
greenlock
.get({ servername: subject })
.then(function(pems) {
if (pems && pems.privkey && pems.cert && pems.chain) {
console.info('Success');
}
//console.log(pems);
})
.catch(function(e) {
console.error('Big bad error:', e.code);
console.error(e);
});
```
</details>
# JavaScript API
<!--
<details>
<summary>Greenlock API (shared among JS implementations)</summary>
-->
<details>
<summary>Greenlock.create({ configDir, packageAgent, maintainerEmail, staging })</summary>
## Greenlock.create()
Creates an instance of greenlock with _environment_-level values.
### Instantiate
```js
// Creates an instance of greenlock with certain default values
var pkg = require('./package.json');
var gl = Greenlock.create({
configDir: './greenlock.d/',
// Staging for testing environments
staging: true,
// Staging for testing environments
staging: true,
// This should be the contact who receives critical bug and security notifications
// Optionally, you may receive other (very few) updates, such as important new features
maintainerEmail: 'jon@example.com',
maintainerUpdates: true, // default: false
// This should be the contact who receives critical bug and security notifications
// Optionally, you may receive other (very few) updates, such as important new features
maintainerEmail: 'jon@example.com',
// for an RFC 8555 / RFC 7231 ACME client user agent
packageAgent: pkg.name + '/' pkg.version
// The "Let's Encrypt Subscriber" (often the same as the maintainer)
// NOT the end customer (except where that is also the maintainer)
subscriberEmail: 'jon@example.com',
agreeToTerms: true // default: false
});
```
| Parameter | Description |
| --------------- | ------------------------------------------------------------------------------------ |
| configDir | the directory to use for file-based plugins |
| maintainerEmail | the developer contact for critical bug and security notifications |
| packageAgent | if you publish your package for others to use, `require('./package.json').name` here |
| staging | use the Let's Encrypt staging URL instead of the production URL |
| directoryUrl | for use with other (not Let's Encrypt) ACME services, and the Pebble test server |
<!--
| Parameter | Description |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| servername | the default servername to use for non-sni requests (many IoT clients) |
| maintainerEmail | the developer contact for critical bug and security notifications |
| maintainerUpdates | (default: false) receive occasional non-critical notifications |
maintainerUpdates: true // default: false
-->
| maintainerPackage | if you publish your package for others to use, `require('./package.json').name` here |
| maintainerPackageVersion | if you publish your package for others to use, `require('./package.json').version` here |
| subscriberEmail | the contact who agrees to the Let's Encrypt Subscriber Agreement and the Greenlock Terms of Service<br>this contact receives renewal failure notifications |
| agreeToTerms | (default: false) either 'true' or a function that presents the Terms of Service and returns it once accepted |
| store | override the default storage module |
| store.module | the name of your storage module |
| store.xxxx | options specific to your storage module |
| challenges['http-01'] | provide an http-01 challenge module |
| challenges['dns-01'] | provide a dns-01 challenge module |
| challenges['tls-alpn-01'] | provide a tls-alpn-01 challenge module |
| challenges[type].module | the name of your challenge module |
| challenges[type].xxxx | module-specific options |
</details>
<details>
<summary>Greenlock#manager.defaults()</summary>
## Greenlock#manager.defaults()
Acts as a getter when given no arguments.
Otherwise sets default, site-wide values as described below.
```js
greenlock.manager.defaults({
// The "Let's Encrypt Subscriber" (often the same as the maintainer)
// NOT the end customer (except where that is also the maintainer)
subscriberEmail: 'jon@example.com',
agreeToTerms: true
challenges: {
"http-01": {
module: "acme-http-01-webroot",
webroot: "/path/to/webroot"
}
}
});
```
| Parameter | Description |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| agreeToTerms | (default: false) either 'true' or a function that presents the Terms of Service and returns it once accepted |
| challenges['http-01'] | provide an http-01 challenge module |
| challenges['dns-01'] | provide a dns-01 challenge module |
| challenges['tls-alpn-01'] | provide a tls-alpn-01 challenge module |
| challenges[type].module | the name of your challenge module |
| challenges[type].xxxx | module-specific options |
| renewOffset | **leave the default** Other than for testing, leave this at the default of 45 days before expiration date (`'-45d'`) . Can also be set like `5w`, meaning 5 weeks after issue date |
| servername | the default servername to use for non-sni requests (many IoT clients) |
| subscriberEmail | the contact who agrees to the Let's Encrypt Subscriber Agreement and the Greenlock Terms of Service<br>this contact receives renewal failure notifications |
| store | override the default storage module |
| store.module | the name of your storage module |
| store.xxxx | options specific to your storage module |
<!--
| serverId | an arbitrary name to distinguish this server within a cluster of servers |
-->
</details>
<details>
<summary>Greenlock#add({ subject, altnames })</summary>
## Greenlock#add()
Greenlock is a **Automated Certificate Management Environment**.
Once you add a "site", it will begin to automatically renew, immediately.
The certificates will provided to the `store` callbacks as soon as they are ready, and whenever they renew.
Failure to renew will be reported to the `notify` callback.
You can also retrieve them one-off with `get`.
### Add Approved Domains
```js
gl.add({
subject: 'example.com',
altnames: ['example.com', 'www.example.com', 'exampleapi.com']
subject: 'example.com',
altnames: ['example.com', 'www.example.com', 'exampleapi.com']
});
```
| Parameter | Description |
| --------------- | -------------------------------------------------------------------------------------------- |
| subject | the first domain on, and identifier of the certificate |
| altnames | first domain, plus additional domains<br>note: the order should always be the same |
| subscriberEmail | if different from the default (i.e. multi-tenant, whitelabel) |
| challenges | (same as main config) use if this site needs to use non-default http-01 or dns-01 validation |
| Parameter | Description |
| --------------- | ---------------------------------------------------------------------------------- |
| subject | the first domain on, and identifier of the certificate |
| altnames | first domain, plus additional domains<br>note: the order should always be the same |
| subscriberEmail | if different from the default (i.e. multi-tenant, whitelabel) |
| agreeToTerms | if subscriber is different from the default |
</details>
<details>
<summary>Greenlock#get({ servername })</summary>
## Greenlock#get()
**Disclaimer**: This is only intended for testing, demos, and SNICallback
(in [Greenlock Express](https://git.rootprojects.org/root/greenlock-express.js)).
Greenlock is intended to be left running to allow it to fetech and renew certifictates automatically.
It is intended that you use the `store` callbacks to new certificates instantly as soon as they renew.
This also protects you from accidentally stampeding the Let's Encrypt API with hundreds (or thousands)
of certificate requests.
- [Store Callback Documentation](https://git.rootprojects.org/root/greenlock-store-test.js)
```js
return greenlock.get({ servername }).then(function(site) {
if (!site) {
console.log(servername + ' was not found in any site config');
return;
}
var privkey = site.pems.privkey;
var fullchain = site.pems.cert + '\n' + site.pems.chain + '\n';
console.log(privkey);
console.log(fullchain);
});
```
| Parameter | Description |
| ---------- | ------------------------------------------------------------- |
| servername | any altname listed on the certificate (including the subject) |
</details>
<details>
<summary>Greenlock#renew({ renewBefore })</summary>
## Greenlock#renew()
### Issue and Renew Certificates
This will renew only domains that have reached their `renewAt` or are within the befault `renewOffset`.
**Note**: This runs at regular intervals, multiple times a day, in the background.
You are not required to call it. If you implement the `store` callbacks, the certificates
will automatically be saved (and if you don't implement them, they all get saved to disk).
```js
return greenlock.renew({}).then(function(results) {
results.forEach(function(site) {
if (site.error) {
console.error(site.subject, site.error);
return;
}
console.log('Renewed certificate for', site.subject, site.altnames);
});
});
return greenlock
.renew()
.then(function(pems) {
console.info(pems);
})
.then(function(results) {
results.forEach(function(site) {
if (site.error) {
console.error(site.subject, site.error);
return;
}
});
});
```
| Parameter | Type | Description |
| ----------- | ---- | ------------------------------------------------------------------------------- |
| (optional) | | ALL parameters are optional, but some should be paired |
| force | bool | force silly options, such as tiny durations |
| renewBefore | ms | Check domains that are scheduled to renew before the given date in milliseconds |
| Parameter | Type | Description |
| ---------- | ---- | ---------------------------------------------------------- |
| (optional) | - | ALL parameters are optional, but some should be paired |
| force | bool | force silly options, such as tiny durations |
| duplicate | bool | force the domain to renew, regardless of age or expiration |
<!--
| issuedBefore | ms | Check domains issued before the given date in milliseconds |
| expiresBefore | ms | Check domains that expire before the given date in milliseconds |
| servername | string<br>hostname | renew the certificate that has this domain in its altnames (for ServerName Indication / SNI lookup) |
| renewOffset | string<br>+ duration | renew domains that have been **issued** after the given duration. ex: '45d' (45 days _after_) |
| renewOffset | string<br>- duration | renew domains, by this duration, before they **expire**. ex: '-3w' (3 weeks _before_) |
-->
</details>
Note: only previous approved domains (via `gl.add()`) may be renewed
<details>
<summary>Greenlock#remove({ subject })</summary>
Note: this will NOT throw an **error**. It will return an array of certifates or errors.
## Greenlock#manager.remove()
### More
To stop certificates from being renewed, you must remove them.
If you are implementing your own `manager` callbacks, I recommend that you mark them as deleted
(i.e. `deleted_at` in your database) rather than actually removing them. Just in case.
```js
gl.remove({
subject: 'example.com'
}).then(function(siteConfig) {
// save the old site config elsewhere, just in case you need it again
});
```
| Parameter | Description |
| --------- | ------------------------------------------------------ |
| subject | the first domain on, and identifier of the certificate |
TODO
</details>
<details>
<summary>Events</summary>
Most of the events bubble from ACME.js.
See https://git.rootprojects.org/root/acme.js#api-overview
_TODO_: document the greenlock-specific events.
</details>
<!--
<details>
<summary>Node.js</summary>
-->
# Install
Greenlock comes with reasonable defaults but when you install it,
you should also install any plugins that you need.
```bash
npm install --save @root/greenlock@v4
npm install --save @greenlock/manager
npm install --save @root/greenlock
npm install --save greenlock-manager-fs
npm install --save greenlock-store-fs
npm install --save acme-http-01-standalone
```
@ -446,150 +188,12 @@ TODO
-->
# Easy to Customize
<!-- greenlock-manager-test => greenlock-manager-custom -->
<!--
- [greenlock.js/examples/](https://git.rootprojects.org/root/greenlock.js/src/branch/master/examples)
-->
<details>
<summary>SSL Cert & Domain Management</summary>
## SSL Certificate & Domain Management
Full Docs: https://git.rootprojects.org/root/greenlock-manager-test.js
This is what keeps the mapping of domains <-> certificates.
In many cases it will interact with the same database as the Key & Cert Store, and probably the code as well.
- set({ subject, altnames, renewAt })
- find({ servernames, renewBefore })
```js
// should return a list of site configs:
[
{
subject: 'example.com',
altnames: ['example.com', 'exampleapi.com'],
renewAt: 1575197231760
},
{
subject: '*.example.com',
altnames: ['*.example.com'],
renewAt: 1575197231760,
challenges: {
'dns-01': {
module: 'acme-dns-01-dnsimple',
apikey: 'xxxx'
}
}
}
];
```
- remove({ subject })
- defaults() (both getter and setter)
```json
{
"subscriberEmail": "jane@example.com",
"agreeToTerms": true,
"challenges": {
"http-01": {
"module": "acme-http-01-standalone"
}
}
}
```
</details>
<details>
<summary>Key & Cert Storage</summary>
## Key and Certificate Store
Full Docs: https://git.rootprojects.org/root/greenlock-store-test.js
This set of callbacks update your service with new certificates and keypairs.
### Account Keys (JWK)
(though typically you only have one account key - because you only have one subscriber email)
- accounts.setKeypair({ email, keypair })
- accounts.checkKeypair({ email })
### Certificate Keys (JWK + PEM)
(typically you have one for each set of domains, and each load balancer)
- certificates.setKeypair({ subject, keypair })
- certificates.checkKeypair({ subject })
(these are fine to implement the same as above, swapping subject/email)
### Certificate PEMs
- certificates.set({ subject, pems })
- certificates.check({ subject })
</details>
<details>
<summary>ACME HTTP-01 Challenges</summary>
## ACME Challenge HTTP-01 Strategies
Full Docs: https://git.rootprojects.org/root/acme-http-01-test.js
This validation and authorization strategy is done over plain HTTP on Port 80.
These are used to set files containing tokens that Let's Encrypt will fetch from each domain
before authorizing a certificate.
**NOT for Wildcards**.
- init({ request })
- set({ challenge: { type, token, keyAuthorization, challengeUrl } })
- get({ challenge: { type, token } })
- remove({ challenge: { type, token } })
<!--
TODO: getAcmeHttp01Challenge
-->
</details>
<details>
<summary>ACME DNS-01 Challenges</summary>
## ACME Challenge DNS-01 Strategies
Full Docs https://git.rootprojects.org/root/acme-dns-01-test.js
This validation and authorization strategy is done over DNS on UDP and TCP ports 53.
**For Wildcards**
These are used to set TXT records containing tokens that Let's Encrypt will fetch for
each domain before authorizing a certificate.
- init({ request })
- zones()
- set({ challenge: { type, dnsZone, dnsPrefix, dnsHost, keyAuthorizationDigest } })
- get({ challenge: { type, dnsZone, dnsPrefix, dnsHost } })
- remove({ challenge: { type, dnsZone, dnsPrefix, dnsHost } })
</details>
<details>
<summary>Notes on HTTP-01 &amp; DNS-01 Integrations</summary>
## Notes on HTTP-01 &amp; DNS-01 Integrations
# HTTP-01 &amp; DNS-01 Integrations
For Public Web Servers running on a VPS, the **default HTTP-01 challenge plugin**
will work just fine, for most people.
will work just fine for most people.
However, for environments that cannot be verified via public HTTP, such as
However, for
- **Wildcard Certificates**
- **IoT Environments**
@ -597,71 +201,31 @@ However, for environments that cannot be verified via public HTTP, such as
- **Private Networks**
Greenlock provides an easy way to integrate Let's Encrypt with your existing services
through a variety of **DNS-01** challenges.
### Why not use dns01 for everything?
through a variety of **DNS-01** infrastructure
Why
Typically file propagation is faster and more reliably than DNS propagation.
Therefore, http-01 will be preferred to dns-01 except when wildcards or **private domains** are in use.
http-01 will only be supplied as a defaut if no other challenge is provided.
</details>
You can use ACME (Let's Encrypt) with
# Ready-made Integrations
Greenlock Express integrates between Let's Encrypt's ACME Challenges and many popular services.
| Type | Service | Plugin |
| ----------- | ----------------------------------------------------------------------------------- | ------------------------ |
| dns-01 | CloudFlare | acme-dns-01-cloudflare |
| dns-01 | [Digital Ocean](https://git.rootprojects.org/root/acme-dns-01-digitalocean.js) | acme-dns-01-digitalocean |
| dns-01 | [DNSimple](https://git.rootprojects.org/root/acme-dns-01-dnsimple.js) | acme-dns-01-dnsimple |
| dns-01 | [DuckDNS](https://git.rootprojects.org/root/acme-dns-01-duckdns.js) | acme-dns-01-duckdns |
| http-01 | File System / [Web Root](https://git.rootprojects.org/root/acme-http-01-webroot.js) | acme-http-01-webroot |
| dns-01 | [GoDaddy](https://git.rootprojects.org/root/acme-dns-01-godaddy.js) | acme-dns-01-godaddy |
| dns-01 | [Gandi](https://git.rootprojects.org/root/acme-dns-01-gandi.js) | acme-dns-01-gandi |
| dns-01 | [NameCheap](https://git.rootprojects.org/root/acme-dns-01-namecheap.js) | acme-dns-01-namecheap |
| dns-01 | [Name&#46;com](https://git.rootprojects.org/root/acme-dns-01-namedotcom.js) | acme-dns-01-namedotcom |
| dns-01 | Route53 (AWS) | acme-dns-01-route53 |
| http-01 | S3 (AWS, Digital Ocean, Scaleway) | acme-http-01-s3 |
| dns-01 | [Vultr](https://git.rootprojects.org/root/acme-dns-01-vultr.js) | acme-dns-01-vultr |
| dns-01 | [Build your own](https://git.rootprojects.org/root/acme-dns-01-test.js) | acme-dns-01-test |
| http-01 | [Build your own](https://git.rootprojects.org/root/acme-http-01-test.js) | acme-http-01-test |
| tls-alpn-01 | [Contact us](mailto:support@therootcompany.com) | - |
Search `acme-http-01-` or `acme-dns-01-` on npm to find more.
# Commercial Support
Do you need...
- training?
- specific features?
- different integrations?
- bugfixes, on _your_ timeline?
- custom code, built by experts?
- commercial support and licensing?
You're welcome to [contact us](mailto:aj@therootcompany.com) in regards to IoT, On-Prem,
Enterprise, and Internal installations, integrations, and deployments.
We have both commercial support and commercial licensing available.
We also offer consulting for all-things-ACME and Let's Encrypt.
# Legal &amp; Rules of the Road
Greenlock&trade; is a [trademark](https://rootprojects.org/legal/#trademark) of AJ ONeal
The rule of thumb is "attribute, but don't confuse". For example:
> Built with [Greenlock Express](https://git.rootprojects.org/root/greenlock.js) (a [Root](https://rootprojects.org) project).
Please [contact us](mailto:aj@therootcompany.com) if you have any questions in regards to our trademark,
attribution, and/or visible source policies. We want to build great software and a great community.
[Greenlock&trade;](https://git.rootprojects.org/root/greenlock.js) |
MPL-2.0 |
[Terms of Use](https://therootcompany.com/legal/#terms) |
[Privacy Policy](https://therootcompany.com/legal/#privacy)
- [x] DNS-01 Challenges
- CloudFlare
- [Digital Ocean](https://git.rootprojects.org/root/acme-dns-01-digitalocean.js)
- [DNSimple](https://git.rootprojects.org/root/acme-dns-01-dnsimple.js)
- [DuckDNS](https://git.rootprojects.org/root/acme-dns-01-duckdns.js)
- [GoDaddy](https://git.rootprojects.org/root/acme-dns-01-godaddy.js)
- [Gandi](https://git.rootprojects.org/root/acme-dns-01-gandi.js)
- [NameCheap](https://git.rootprojects.org/root/acme-dns-01-namecheap.js)
- [Name&#46;com](https://git.rootprojects.org/root/acme-dns-01-namedotcom.js)
- Route53 (AWS)
- [Vultr](https://git.rootprojects.org/root/acme-dns-01-vultr.js)
- Build your own
- [x] HTTP-01 Challenges
- [In-Memory](https://git.rootprojects.org/root/acme-http-01-standalone.js) (Standalone)
- [FileSystem](https://git.rootprojects.org/root/acme-http-01-webroot.js) (WebRoot)
- S3 (AWS, Digital Ocean, etc)
- [x] TLS-ALPN-01 Challenges
- Contact us to learn about Greenlock Pro

View File

@ -7,213 +7,208 @@ var E = require('./errors.js');
var pending = {};
A._getOrCreate = function(gnlck, mconf, db, acme, args) {
var email = args.subscriberEmail || mconf.subscriberEmail;
var email =
args.subscriberEmail ||
mconf.subscriberEmail ||
gnlck._defaults.subscriberEmail;
if (!email) {
throw E.NO_SUBSCRIBER('get account', args.subject);
}
if (!email) {
throw E.NO_SUBSCRIBER('get account', args.subject);
}
// TODO send welcome message with benefit info
return U._validMx(email)
.catch(function() {
throw E.NO_SUBSCRIBER('get account', args.subcriberEmail);
})
.then(function() {
if (pending[email]) {
return pending[email];
}
// TODO send welcome message with benefit info
return U._validMx(email)
.catch(function() {
throw E.NO_SUBSCRIBER('get account', args.subcriberEmail);
})
.then(function() {
if (pending[email]) {
return pending[email];
}
pending[email] = A._rawGetOrCreate(
gnlck,
mconf,
db,
acme,
args,
email
)
.catch(function(e) {
delete pending[email];
throw e;
})
.then(function(result) {
delete pending[email];
return result;
});
pending[email] = A._rawGetOrCreate(
gnlck,
mconf,
db,
acme,
args,
email
)
.catch(function(e) {
delete pending[email];
throw e;
})
.then(function(result) {
delete pending[email];
return result;
});
return pending[email];
});
return pending[email];
});
};
// What we really need out of this is the private key and the ACME "key" id
A._rawGetOrCreate = function(gnlck, mconf, db, acme, args, email) {
var p;
if (db.check) {
p = A._checkStore(gnlck, mconf, db, acme, args, email);
} else {
p = Promise.resolve(null);
}
var p;
if (db.check) {
p = A._checkStore(gnlck, mconf, db, acme, args, email);
} else {
p = Promise.resolve(null);
}
return p.then(function(fullAccount) {
if (!fullAccount) {
return A._newAccount(gnlck, mconf, db, acme, args, email, null);
}
return p.then(function(fullAccount) {
if (!fullAccount) {
return A._newAccount(gnlck, mconf, db, acme, args, email, null);
}
if (fullAccount.keypair && fullAccount.key && fullAccount.key.kid) {
return fullAccount;
}
if (fullAccount.keypair && fullAccount.key && fullAccount.key.kid) {
return fullAccount;
}
return A._newAccount(gnlck, mconf, db, acme, args, email, fullAccount);
});
return A._newAccount(gnlck, mconf, db, acme, args, email, fullAccount);
});
};
A._newAccount = function(gnlck, mconf, db, acme, args, email, fullAccount) {
var keyType = args.accountKeyType || mconf.accountKeyType;
var query = {
subject: args.subject,
email: email,
subscriberEmail: email,
customerEmail: args.customerEmail,
account: fullAccount || {},
directoryUrl:
args.directoryUrl ||
mconf.directoryUrl ||
gnlck._defaults.directoryUrl
};
var keyType =
args.accountKeyType ||
mconf.accountKeyType ||
gnlck._defaults.accountKeyType;
var query = {
subject: args.subject,
email: email,
subscriberEmail: email,
customerEmail: args.customerEmail,
account: fullAccount || {}
};
return U._getOrCreateKeypair(db, args.subject, query, keyType).then(
function(kresult) {
var keypair = kresult.keypair;
var accReg = {
subscriberEmail: email,
agreeToTerms:
args.agreeToTerms ||
mconf.agreeToTerms ||
gnlck._defaults.agreeToTerms,
accountKey: keypair.privateKeyJwk || keypair.private,
debug: args.debug
};
return acme.accounts.create(accReg).then(function(receipt) {
var reg = {
keypair: keypair,
receipt: receipt,
// shudder... not actually a KeyID... but so it is called anyway...
kid:
receipt &&
receipt.key &&
(receipt.key.kid || receipt.kid),
email: args.email,
subscriberEmail: email,
customerEmail: args.customerEmail
};
return U._getOrCreateKeypair(db, args.subject, query, keyType).then(
function(kresult) {
var keypair = kresult.keypair;
var accReg = {
subscriberEmail: email,
agreeToTerms:
args.agreeToTerms ||
mconf.agreeToTerms ||
gnlck._defaults.agreeToTerms,
accountKey: keypair.privateKeyJwk || keypair.private,
debug: args.debug
};
return acme.accounts.create(accReg).then(function(receipt) {
var reg = {
keypair: keypair,
receipt: receipt,
// shudder... not actually a KeyID... but so it is called anyway...
kid:
receipt &&
receipt.key &&
(receipt.key.kid || receipt.kid),
email: args.email,
subscriberEmail: email,
customerEmail: args.customerEmail
};
var keyP;
if (kresult.exists) {
keyP = Promise.resolve();
} else {
query.keypair = keypair;
query.receipt = receipt;
/*
var keyP;
if (kresult.exists) {
keyP = Promise.resolve();
} else {
query.keypair = keypair;
query.receipt = receipt;
query.directoryUrl = gnlck._defaults.directoryUrl;
/*
query.server = gnlck._defaults.directoryUrl.replace(
/^https?:\/\//i,
''
);
*/
keyP = db.setKeypair(query, keypair);
}
*/
keyP = db.setKeypair(query, keypair);
}
return keyP
.then(function() {
if (!db.set) {
return Promise.resolve({
keypair: keypair
});
}
return db.set(
{
// id to be set by Store
email: email,
subscriberEmail: email,
customerEmail: args.customerEmail,
agreeTos: true,
agreeToTerms: true,
directoryUrl:
args.directoryUrl ||
mconf.directoryUrl ||
gnlck._defaults.directoryUrl
/*
return keyP
.then(function() {
if (!db.set) {
return Promise.resolve({
keypair: keypair
});
}
return db.set(
{
// id to be set by Store
email: email,
subscriberEmail: email,
customerEmail: args.customerEmail,
agreeTos: true,
directoryUrl: gnlck._defaults.directoryUrl
/*
server: gnlck._defaults.directoryUrl.replace(
/^https?:\/\//i,
''
)
*/
},
reg
);
})
.then(function(fullAccount) {
if (fullAccount && 'object' !== typeof fullAccount) {
throw new Error(
"accounts.set should either return 'null' or an object with an 'id' string"
);
}
*/
},
reg
);
})
.then(function(fullAccount) {
if (fullAccount && 'object' !== typeof fullAccount) {
throw new Error(
"accounts.set should either return 'null' or an object with an 'id' string"
);
}
if (!fullAccount) {
fullAccount = {};
}
fullAccount.keypair = keypair;
if (!fullAccount.key) {
fullAccount.key = {};
}
fullAccount.key.kid = reg.kid;
if (!fullAccount) {
fullAccount = {};
}
fullAccount.keypair = keypair;
if (!fullAccount.key) {
fullAccount.key = {};
}
fullAccount.key.kid = reg.kid;
return fullAccount;
});
});
}
);
return fullAccount;
});
});
}
);
};
A._checkStore = function(gnlck, mconf, db, acme, args, email) {
if ((args.domain || args.domains) && !args.subject) {
console.warn("use 'subject' instead of 'domain'");
args.subject = args.domain;
}
if ((args.domain || args.domains) && !args.subject) {
console.warn("use 'subject' instead of 'domain'");
args.subject = args.domain;
}
var account = args.account;
if (!account) {
account = {};
}
var account = args.account;
if (!account) {
account = {};
}
if (args.accountKey) {
console.warn(
'rather than passing accountKey, put it directly into your account key store'
);
// TODO we probably don't need this
return U._importKeypair(args.accountKey);
}
if (args.accountKey) {
console.warn(
'rather than passing accountKey, put it directly into your account key store'
);
// TODO we probably don't need this
return U._importKeypair(args.accountKey);
}
if (!db.check) {
return Promise.resolve(null);
}
if (!db.check) {
return Promise.resolve(null);
}
return db
.check({
//keypair: undefined,
//receipt: undefined,
email: email,
subscriberEmail: email,
customerEmail: args.customerEmail || mconf.customerEmail,
account: account,
directoryUrl:
args.directoryUrl ||
mconf.directoryUrl ||
gnlck._defaults.directoryUrl
})
.then(function(fullAccount) {
if (!fullAccount) {
return null;
}
return db
.check({
//keypair: undefined,
//receipt: undefined,
email: email,
subscriberEmail: email,
customerEmail: args.customerEmail || mconf.customerEmail,
account: account
})
.then(function(fullAccount) {
if (!fullAccount) {
return null;
}
return fullAccount;
});
return fullAccount;
});
};

View File

@ -1,91 +0,0 @@
'use strict';
var args = process.argv.slice(3);
var cli = require('./lib/cli.js');
//var path = require('path');
//var pkgpath = path.join(__dirname, '..', 'package.json');
//var pkgpath = path.join(process.cwd(), 'package.json');
var Flags = require('./lib/flags.js');
Flags.init().then(function({ flagOptions, greenlock, mconf }) {
var myFlags = {};
[
'subject',
'altnames',
'renew-offset',
'subscriber-email',
'customer-email',
'server-key-type',
'challenge-http-01',
'challenge-http-01-xxxx',
'challenge-dns-01',
'challenge-dns-01-xxxx',
'challenge-tls-alpn-01',
'challenge-tls-alpn-01-xxxx',
'challenge',
'challenge-xxxx',
'challenge-json',
'force-save'
].forEach(function(k) {
myFlags[k] = flagOptions[k];
});
cli.parse(myFlags);
cli.main(function(argList, flags) {
Flags.mangleFlags(flags, mconf);
main(argList, flags, greenlock);
}, args);
});
async function main(_, flags, greenlock) {
if (!flags.subject || !flags.altnames) {
console.error(
'--subject and --altnames must be provided and should be valid domains'
);
process.exit(1);
return;
}
greenlock
.add(flags)
.catch(function(err) {
console.error();
console.error('error:', err.message);
console.error();
process.exit(1);
})
.then(function() {
return greenlock
._config({
servername:
flags.altnames[
Math.floor(Math.random() * flags.altnames.length)
]
})
.then(function(site) {
if (!site) {
console.info();
console.info(
'Internal bug or configuration mismatch: No config found.'
);
console.info();
process.exit(1);
return;
}
console.info();
Object.keys(site).forEach(function(k) {
if ('defaults' === k) {
console.info(k + ':');
Object.keys(site.defaults).forEach(function(key) {
var value = JSON.stringify(site.defaults[key]);
console.info('\t' + key + ':' + value);
});
} else {
console.info(k + ': ' + JSON.stringify(site[k]));
}
});
});
});
}

View File

@ -4,375 +4,375 @@ var mkdirp = require('@root/mkdirp');
var cli = require('./cli.js');
cli.parse({
'directory-url': [
false,
' ACME Directory Resource URL',
'string',
'https://acme-v02.api.letsencrypt.org/directory',
'server,acme-url'
],
email: [
false,
' Email used for registration and recovery contact. (default: null)',
'email'
],
'agree-tos': [
false,
" Agree to the Greenlock and Let's Encrypt Subscriber Agreements",
'boolean',
false
],
'community-member': [
false,
' Submit stats to and get updates from Greenlock',
'boolean',
false
],
domains: [
false,
' Domain names to apply. For multiple domains you can enter a comma separated list of domains as a parameter. (default: [])',
'string'
],
'renew-offset': [
false,
' Positive (time after issue) or negative (time before expiry) offset, such as 30d or -45d',
'string',
'45d'
],
'renew-within': [
false,
' (ignored) use renew-offset instead',
'ignore',
undefined
],
'cert-path': [
false,
' Path to where new cert.pem is saved',
'string',
':configDir/live/:hostname/cert.pem'
],
'fullchain-path': [
false,
' Path to where new fullchain.pem (cert + chain) is saved',
'string',
':configDir/live/:hostname/fullchain.pem'
],
'bundle-path': [
false,
' Path to where new bundle.pem (fullchain + privkey) is saved',
'string',
':configDir/live/:hostname/bundle.pem'
],
'chain-path': [
false,
' Path to where new chain.pem is saved',
'string',
':configDir/live/:hostname/chain.pem'
],
'privkey-path': [
false,
' Path to where privkey.pem is saved',
'string',
':configDir/live/:hostname/privkey.pem'
],
'config-dir': [
false,
' Configuration directory.',
'string',
'~/letsencrypt/etc/'
],
store: [
false,
' The name of the storage module to use',
'string',
'greenlock-store-fs'
],
'store-xxxx': [
false,
' An option for the chosen storage module, such as --store-apikey or --store-bucket',
'bag'
],
'store-json': [
false,
' A JSON string containing all option for the chosen store module (instead of --store-xxxx)',
'json',
'{}'
],
challenge: [
false,
' The name of the HTTP-01, DNS-01, or TLS-ALPN-01 challenge module to use',
'string',
'@greenlock/acme-http-01-fs'
],
'challenge-xxxx': [
false,
' An option for the chosen challenge module, such as --challenge-apikey or --challenge-bucket',
'bag'
],
'challenge-json': [
false,
' A JSON string containing all option for the chosen challenge module (instead of --challenge-xxxx)',
'json',
'{}'
],
'skip-dry-run': [
false,
' Use with caution (and test with the staging url first). Creates an Order on the ACME server without a self-test.',
'boolean'
],
'skip-challenge-tests': [
false,
' Use with caution (and with the staging url first). Presents challenges to the ACME server without first testing locally.',
'boolean'
],
'http-01-port': [
false,
' Required to be 80 for live servers. Do not use. For special test environments only.',
'int'
],
'dns-01': [false, ' Use DNS-01 challange type', 'boolean', false],
standalone: [
false,
' Obtain certs using a "standalone" webserver.',
'boolean',
false
],
manual: [
false,
' Print the token and key to the screen and wait for you to hit enter, giving you time to copy it somewhere before continuing (uses acme-http-01-cli or acme-dns-01-cli)',
'boolean',
false
],
debug: [false, ' show traces and logs', 'boolean', false],
root: [
false,
' public_html / webroot path (may use the :hostname template such as /srv/www/:hostname)',
'string',
undefined,
'webroot-path'
],
'directory-url': [
false,
' ACME Directory Resource URL',
'string',
'https://acme-v02.api.letsencrypt.org/directory',
'server,acme-url'
],
email: [
false,
' Email used for registration and recovery contact. (default: null)',
'email'
],
'agree-tos': [
false,
" Agree to the Greenlock and Let's Encrypt Subscriber Agreements",
'boolean',
false
],
'community-member': [
false,
' Submit stats to and get updates from Greenlock',
'boolean',
false
],
domains: [
false,
' Domain names to apply. For multiple domains you can enter a comma separated list of domains as a parameter. (default: [])',
'string'
],
'renew-offset': [
false,
' Positive (time after issue) or negative (time before expiry) offset, such as 30d or -45d',
'string',
'45d'
],
'renew-within': [
false,
' (ignored) use renew-offset instead',
'ignore',
undefined
],
'cert-path': [
false,
' Path to where new cert.pem is saved',
'string',
':configDir/live/:hostname/cert.pem'
],
'fullchain-path': [
false,
' Path to where new fullchain.pem (cert + chain) is saved',
'string',
':configDir/live/:hostname/fullchain.pem'
],
'bundle-path': [
false,
' Path to where new bundle.pem (fullchain + privkey) is saved',
'string',
':configDir/live/:hostname/bundle.pem'
],
'chain-path': [
false,
' Path to where new chain.pem is saved',
'string',
':configDir/live/:hostname/chain.pem'
],
'privkey-path': [
false,
' Path to where privkey.pem is saved',
'string',
':configDir/live/:hostname/privkey.pem'
],
'config-dir': [
false,
' Configuration directory.',
'string',
'~/letsencrypt/etc/'
],
store: [
false,
' The name of the storage module to use',
'string',
'greenlock-store-fs'
],
'store-xxxx': [
false,
' An option for the chosen storage module, such as --store-apikey or --store-bucket',
'bag'
],
'store-json': [
false,
' A JSON string containing all option for the chosen store module (instead of --store-xxxx)',
'json',
'{}'
],
challenge: [
false,
' The name of the HTTP-01, DNS-01, or TLS-ALPN-01 challenge module to use',
'string',
'@greenlock/acme-http-01-fs'
],
'challenge-xxxx': [
false,
' An option for the chosen challenge module, such as --challenge-apikey or --challenge-bucket',
'bag'
],
'challenge-json': [
false,
' A JSON string containing all option for the chosen challenge module (instead of --challenge-xxxx)',
'json',
'{}'
],
'skip-dry-run': [
false,
' Use with caution (and test with the staging url first). Creates an Order on the ACME server without a self-test.',
'boolean'
],
'skip-challenge-tests': [
false,
' Use with caution (and with the staging url first). Presents challenges to the ACME server without first testing locally.',
'boolean'
],
'http-01-port': [
false,
' Required to be 80 for live servers. Do not use. For special test environments only.',
'int'
],
'dns-01': [false, ' Use DNS-01 challange type', 'boolean', false],
standalone: [
false,
' Obtain certs using a "standalone" webserver.',
'boolean',
false
],
manual: [
false,
' Print the token and key to the screen and wait for you to hit enter, giving you time to copy it somewhere before continuing (uses acme-http-01-cli or acme-dns-01-cli)',
'boolean',
false
],
debug: [false, ' show traces and logs', 'boolean', false],
root: [
false,
' public_html / webroot path (may use the :hostname template such as /srv/www/:hostname)',
'string',
undefined,
'webroot-path'
],
//
// backwards compat
//
duplicate: [
false,
' Allow getting a certificate that duplicates an existing one/is an early renewal',
'boolean',
false
],
'rsa-key-size': [
false,
' (ignored) use server-key-type or account-key-type instead',
'ignore',
2048
],
'server-key-path': [
false,
' Path to privkey.pem to use for certificate (default: generate new)',
'string',
undefined,
'domain-key-path'
],
'server-key-type': [
false,
" One of 'RSA' (2048), 'RSA-3084', 'RSA-4096', 'ECDSA' (P-256), or 'P-384'. For best compatibility, security, and efficiency use the default (More bits != More security)",
'string',
'RSA'
],
'account-key-path': [
false,
' Path to privkey.pem to use for account (default: generate new)',
'string'
],
'account-key-type': [
false,
" One of 'ECDSA' (P-256), 'P-384', 'RSA', 'RSA-3084', or 'RSA-4096'. Stick with 'ECDSA' (P-256) unless you need 'RSA' (2048) for legacy compatibility. (More bits != More security)",
'string',
'P-256'
],
webroot: [false, ' (ignored) for certbot compatibility', 'ignore', false],
//, 'standalone-supported-challenges': [ false, " Supported challenges, order preferences are randomly chosen. (default: http-01,tls-alpn-01)", 'string', 'http-01']
'work-dir': [
false,
' for certbot compatibility (ignored)',
'string',
'~/letsencrypt/var/lib/'
],
'logs-dir': [
false,
' for certbot compatibility (ignored)',
'string',
'~/letsencrypt/var/log/'
],
'acme-version': [
false,
' (ignored) ACME is now RFC 8555 and prior drafts are no longer supported',
'ignore',
'rfc8555'
]
//
// backwards compat
//
duplicate: [
false,
' Allow getting a certificate that duplicates an existing one/is an early renewal',
'boolean',
false
],
'rsa-key-size': [
false,
' (ignored) use server-key-type or account-key-type instead',
'ignore',
2048
],
'server-key-path': [
false,
' Path to privkey.pem to use for certificate (default: generate new)',
'string',
undefined,
'domain-key-path'
],
'server-key-type': [
false,
" One of 'RSA' (2048), 'RSA-3084', 'RSA-4096', 'ECDSA' (P-256), or 'P-384'. For best compatibility, security, and efficiency use the default (More bits != More security)",
'string',
'RSA'
],
'account-key-path': [
false,
' Path to privkey.pem to use for account (default: generate new)',
'string'
],
'account-key-type': [
false,
" One of 'ECDSA' (P-256), 'P-384', 'RSA', 'RSA-3084', or 'RSA-4096'. Stick with 'ECDSA' (P-256) unless you need 'RSA' (2048) for legacy compatibility. (More bits != More security)",
'string',
'P-256'
],
webroot: [false, ' (ignored) for certbot compatibility', 'ignore', false],
//, 'standalone-supported-challenges': [ false, " Supported challenges, order preferences are randomly chosen. (default: http-01,tls-alpn-01)", 'string', 'http-01']
'work-dir': [
false,
' for certbot compatibility (ignored)',
'string',
'~/letsencrypt/var/lib/'
],
'logs-dir': [
false,
' for certbot compatibility (ignored)',
'string',
'~/letsencrypt/var/log/'
],
'acme-version': [
false,
' (ignored) ACME is now RFC 8555 and prior drafts are no longer supported',
'ignore',
'rfc8555'
]
});
// ignore certonly and extraneous arguments
cli.main(function(_, options) {
console.info('');
console.info('');
[
'configDir',
'privkeyPath',
'certPath',
'chainPath',
'fullchainPath',
'bundlePath'
].forEach(function(k) {
if (options[k]) {
options.storeOpts[k] = options[k];
}
delete options[k];
});
[
'configDir',
'privkeyPath',
'certPath',
'chainPath',
'fullchainPath',
'bundlePath'
].forEach(function(k) {
if (options[k]) {
options.storeOpts[k] = options[k];
}
delete options[k];
});
if (options.workDir) {
options.challengeOpts.workDir = options.workDir;
delete options.workDir;
}
if (options.workDir) {
options.challengeOpts.workDir = options.workDir;
delete options.workDir;
}
if (options.debug) {
console.debug(options);
}
if (options.debug) {
console.debug(options);
}
var args = {};
var homedir = require('os').homedir();
var args = {};
var homedir = require('os').homedir();
Object.keys(options).forEach(function(key) {
var val = options[key];
Object.keys(options).forEach(function(key) {
var val = options[key];
if ('string' === typeof val) {
val = val.replace(/^~/, homedir);
}
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;
});
key = key.replace(/\-([a-z0-9A-Z])/g, function(c) {
return c[1].toUpperCase();
});
args[key] = val;
});
Object.keys(args).forEach(function(key) {
var val = args[key];
Object.keys(args).forEach(function(key) {
var val = args[key];
if ('string' === typeof val) {
val = val.replace(/(\:configDir)|(\:config)/, args.configDir);
}
if ('string' === typeof val) {
val = val.replace(/(\:configDir)|(\:config)/, args.configDir);
}
args[key] = val;
});
args[key] = val;
});
if (args.domains) {
args.domains = args.domains.split(',');
}
if (args.domains) {
args.domains = args.domains.split(',');
}
if (
!(Array.isArray(args.domains) && args.domains.length) ||
!args.email ||
!args.agreeTos ||
(!args.server && !args.directoryUrl)
) {
console.error('\nUsage:\n\ngreenlock certonly --standalone \\');
console.error(
'\t--agree-tos --email user@example.com --domains example.com \\'
);
console.error('\t--config-dir ~/acme/etc \\');
console.error('\nSee greenlock --help for more details\n');
return;
}
if (
!(Array.isArray(args.domains) && args.domains.length) ||
!args.email ||
!args.agreeTos ||
(!args.server && !args.directoryUrl)
) {
console.error('\nUsage:\n\ngreenlock certonly --standalone \\');
console.error(
'\t--agree-tos --email user@example.com --domains example.com \\'
);
console.error('\t--config-dir ~/acme/etc \\');
console.error('\nSee greenlock --help for more details\n');
return;
}
if (args.http01Port) {
// [@agnat]: Coerce to string. cli returns a number although we request a string.
args.http01Port = '' + args.http01Port;
args.http01Port = args.http01Port.split(',').map(function(port) {
return parseInt(port, 10);
});
}
if (args.http01Port) {
// [@agnat]: Coerce to string. cli returns a number although we request a string.
args.http01Port = '' + args.http01Port;
args.http01Port = args.http01Port.split(',').map(function(port) {
return parseInt(port, 10);
});
}
function run() {
var challenges = {};
if (/http.?01/i.test(args.challenge)) {
challenges['http-01'] = args.challengeOpts;
}
if (/dns.?01/i.test(args.challenge)) {
challenges['dns-01'] = args.challengeOpts;
}
if (/alpn.?01/i.test(args.challenge)) {
challenges['tls-alpn-01'] = args.challengeOpts;
}
if (!Object.keys(challenges).length) {
throw new Error(
"Could not determine the challenge type for '" +
args.challengeOpts.module +
"'. Expected a name like @you/acme-xxxx-01-foo. Please name the module with http-01, dns-01, or tls-alpn-01."
);
}
args.challengeOpts.module = args.challenge;
args.storeOpts.module = args.store;
function run() {
var challenges = {};
if (/http.?01/i.test(args.challenge)) {
challenges['http-01'] = args.challengeOpts;
}
if (/dns.?01/i.test(args.challenge)) {
challenges['dns-01'] = args.challengeOpts;
}
if (/alpn.?01/i.test(args.challenge)) {
challenges['tls-alpn-01'] = args.challengeOpts;
}
if (!Object.keys(challenges).length) {
throw new Error(
"Could not determine the challenge type for '" +
args.challengeOpts.module +
"'. Expected a name like @you/acme-xxxx-01-foo. Please name the module with http-01, dns-01, or tls-alpn-01."
);
}
args.challengeOpts.module = args.challenge;
args.storeOpts.module = args.store;
console.log('\ngot to the run step');
require(args.challenge);
require(args.store);
console.log('\ngot to the run step');
require(args.challenge);
require(args.store);
var greenlock = require('../').create({
maintainerEmail: args.maintainerEmail || 'coolaj86@gmail.com',
manager: './manager.js',
configFile: '~/.config/greenlock/certs.json',
challenges: challenges,
store: args.storeOpts,
renewOffset: args.renewOffset || '30d',
renewStagger: '1d'
});
var greenlock = require('../').create({
maintainerEmail: args.maintainerEmail || 'coolaj86@gmail.com',
manager: './manager.js',
configFile: '~/.config/greenlock/certs.json',
challenges: challenges,
store: args.storeOpts,
renewOffset: args.renewOffset || '30d',
renewStagger: '1d'
});
// for long-running processes
if (args.renewEvery) {
setInterval(function() {
greenlock.renew({
period: args.renewEvery
});
}, args.renewEvery);
}
// for long-running processes
if (args.renewEvery) {
setInterval(function() {
greenlock.renew({
period: args.renewEvery
});
}, args.renewEvery);
}
// TODO should greenlock.add simply always include greenlock.renew?
// the concern is conflating error events
return greenlock
.add({
subject: args.subject,
altnames: args.altnames,
subscriberEmail: args.subscriberEmail || args.email
})
.then(function(changes) {
console.info(changes);
// renew should always
return greenlock
.renew({
subject: args.subject,
force: false
})
.then(function() {});
});
}
// TODO should greenlock.add simply always include greenlock.renew?
// the concern is conflating error events
return greenlock
.add({
subject: args.subject,
altnames: args.altnames,
subscriberEmail: args.subscriberEmail || args.email
})
.then(function(changes) {
console.info(changes);
// renew should always
return greenlock
.renew({
subject: args.subject,
force: false
})
.then(function() {});
});
}
if ('greenlock-store-fs' !== args.store) {
run();
return;
}
if ('greenlock-store-fs' !== args.store) {
run();
return;
}
// TODO remove mkdirp and let greenlock-store-fs do this?
mkdirp(args.storeOpts.configDir, function(err) {
if (!err) {
run();
}
// TODO remove mkdirp and let greenlock-store-fs do this?
mkdirp(args.storeOpts.configDir, function(err) {
if (!err) {
run();
}
console.error(
"Could not create --config-dir '" + args.configDir + "':",
err.code
);
console.error("Try setting --config-dir '/tmp'");
return;
});
console.error(
"Could not create --config-dir '" + args.configDir + "':",
err.code
);
console.error("Try setting --config-dir '/tmp'");
return;
});
}, process.argv.slice(3));

234
bin/cli.js Normal file
View File

@ -0,0 +1,234 @@
'use strict';
var CLI = module.exports;
var defaultConf;
var defaultOpts;
var bags = [];
CLI.parse = function(conf) {
var opts = (defaultOpts = {});
defaultConf = conf;
Object.keys(conf).forEach(function(k) {
var v = conf[k];
var aliases = v[5];
var bag;
var bagName;
// the name of the argument set is now the 0th argument
v.unshift(k);
// v[0] flagname
// v[1] short flagname
// v[2] description
// v[3] type
// v[4] default value
// v[5] aliases
if ('bag' === v[3]) {
bag = v[0]; // 'bag-option-xxxx' => '--bag-option-'
bag = '--' + bag.replace(/xxx.*/, '');
bags.push(bag);
bagName = toBagName(bag.replace(/^--/, ''));
opts[bagName] = {};
}
if ('json' === v[3]) {
bagName = toBagName(v[0].replace(/-json$/, '')); // 'bag-option-json' => 'bagOptionOpts'
opts[bagName] = {};
} else if ('ignore' !== v[3] && 'undefined' !== typeof v[4]) {
// set the default values (where 'undefined' is not an allowed value)
opts[toCamel(k)] = v[4];
}
if (!aliases) {
aliases = [];
} else if ('string' === typeof aliases) {
aliases = aliases.split(',');
}
aliases.forEach(function(alias) {
if (alias in conf) {
throw new Error(
"Cannot alias '" +
alias +
"' from '" +
k +
"': option already exists"
);
}
conf[alias] = v;
});
});
};
CLI.main = function(cb, args) {
var leftovers = [];
var conf = defaultConf;
var opts = defaultOpts;
if (!opts) {
throw new Error("you didn't call `CLI.parse(configuration)`");
}
// TODO what's the existing API for this?
if (!args) {
args = process.argv.slice(2);
}
var flag;
var cnf;
var typ;
function grab(bag) {
var bagName = toBagName(bag);
if (bag !== flag.slice(0, bag.length)) {
return false;
}
console.log(bagName, toCamel(flag.slice(bag.length)));
opts[bagName][toCamel(flag.slice(bag.length))] = args.shift();
return true;
}
while (args.length) {
// take one off the top
flag = args.shift();
// mind the gap
if ('--' === flag) {
leftovers = leftovers.concat(args);
break;
}
// help!
if (
'--help' === flag ||
'-h' === flag ||
'/?' === flag ||
'help' === flag
) {
printHelp(conf);
process.exit(1);
}
// only long names are actually used
if ('--' !== flag.slice(0, 2)) {
console.error("Unrecognized argument '" + flag + "'");
process.exit(1);
}
cnf = conf[flag.slice(2)];
if (!cnf) {
// look for arbitrary flags
if (bags.some(grab)) {
continue;
}
// other arbitrary args are not used
console.error("Unrecognized flag '" + flag + "'");
process.exit(1);
}
// encourage switching to non-aliased version
if (flag !== '--' + cnf[0]) {
console.warn(
"use of '" +
flag +
"' is deprecated, use '--" +
cnf[0] +
"' instead"
);
}
// look for xxx-json flags
if ('json' === cnf[3]) {
try {
var json = JSON.parse(args.shift());
var bagName = toBagName(cnf[0].replace(/-json$/, ''));
Object.keys(json).forEach(function(k) {
opts[bagName][k] = json[k];
});
} catch (e) {
console.error("Could not parse option '" + flag + "' as JSON:");
console.error(e.message);
process.exit(1);
}
continue;
}
// set booleans, otherwise grab the next arg in line
typ = cnf[3];
// TODO --no-<whatever> to negate
if (Boolean === typ || 'boolean' === typ) {
opts[toCamel(cnf[0])] = true;
continue;
}
opts[toCamel(cnf[0])] = args.shift();
continue;
}
cb(leftovers, opts);
};
function toCamel(str) {
return str.replace(/-([a-z0-9])/g, function(m) {
return m[1].toUpperCase();
});
}
function toBagName(bag) {
// trim leading and trailing '-'
bag = bag.replace(/^-+/g, '').replace(/-+$/g, '')
return toCamel(bag) + 'Opts'; // '--bag-option-' => bagOptionOpts
}
function printHelp(conf) {
var flagLen = 0;
var typeLen = 0;
var defLen = 0;
Object.keys(conf).forEach(function(k) {
flagLen = Math.max(flagLen, conf[k][0].length);
typeLen = Math.max(typeLen, conf[k][3].length);
if ('undefined' !== typeof conf[k][4]) {
defLen = Math.max(
defLen,
'(Default: )'.length + String(conf[k][4]).length
);
}
});
Object.keys(conf).forEach(function(k) {
var v = conf[k];
// skip aliases
if (v[0] !== k) {
return;
}
var def = v[4];
if ('undefined' === typeof def) {
def = '';
} else {
def = '(default: ' + JSON.stringify(def) + ')';
}
var msg =
' --' +
v[0].padEnd(flagLen) +
' ' +
v[3].padStart(typeLen + 1) +
' ' +
(v[2] || '') +
' ' +
def; /*.padStart(defLen)*/
// v[0] flagname
// v[1] short flagname
// v[2] description
// v[3] type
// v[4] default value
// v[5] aliases
console.info(msg);
});
}

View File

@ -1,96 +0,0 @@
'use strict';
var args = process.argv.slice(3);
var cli = require('./lib/cli.js');
//var path = require('path');
//var pkgpath = path.join(__dirname, '..', 'package.json');
//var pkgpath = path.join(process.cwd(), 'package.json');
var Flags = require('./lib/flags.js');
Flags.init().then(function({ flagOptions, greenlock, mconf }) {
var myFlags = {};
['all', 'subject', 'servername' /*, 'servernames', 'altnames'*/].forEach(
function(k) {
myFlags[k] = flagOptions[k];
}
);
cli.parse(myFlags);
cli.main(function(argList, flags) {
Flags.mangleFlags(flags, mconf);
main(argList, flags, greenlock);
}, args);
});
async function main(_, flags, greenlock) {
var servernames = [flags.subject]
.concat([flags.servername])
//.concat(flags.servernames)
//.concat(flags.altnames)
.filter(Boolean);
delete flags.subject;
delete flags.altnames;
flags.servernames = servernames;
if (!flags.all && flags.servernames.length > 1) {
console.error('Error: should specify either --subject OR --servername');
process.exit(1);
return;
} else if (!flags.all && flags.servernames.length !== 1) {
console.error('error: missing --servername <example.com>');
process.exit(1);
return;
}
if (!flags.all) {
flags.servername = flags.servernames[0];
} else if (flags.servername) {
console.error(
'error: missing cannot have --all and --servername / --subject'
);
process.exit(1);
}
delete flags.servernames;
var getter = function() {
return greenlock._config(flags);
};
if (flags.all) {
getter = function() {
return greenlock._configAll(flags);
};
}
return getter()
.catch(function(err) {
console.error();
console.error('error:', err.message);
//console.log(err.stack);
console.error();
process.exit(1);
})
.then(function(sites) {
if (!sites) {
console.info();
if (flags.all) {
console.info('No configs found');
} else {
console.info('No config found for', flags.servername);
}
console.info();
process.exit(1);
return;
}
if (!Array.isArray(sites)) {
sites = [sites];
}
sites.forEach(function(site) {
console.info();
console.info(
'Config for ' +
JSON.stringify(flags.servername || site.subject) +
':'
);
console.info(JSON.stringify(site, null, 2));
});
});
}

View File

@ -1,62 +0,0 @@
'use strict';
var args = process.argv.slice(3);
var cli = require('./lib/cli.js');
//var path = require('path');
//var pkgpath = path.join(__dirname, '..', 'package.json');
//var pkgpath = path.join(process.cwd(), 'package.json');
var Flags = require('./lib/flags.js');
Flags.init({ forceSave: true }).then(function({
flagOptions,
greenlock,
mconf
}) {
var myFlags = {};
[
'agree-to-terms',
'account-key-type',
'server-key-type',
'subscriber-email',
'renew-offset',
'store',
'store-xxxx',
'challenge-http-01-xxxx',
'challenge-dns-01',
'challenge-dns-01-xxxx',
'challenge-tls-alpn-01',
'challenge-tls-alpn-01-xxxx',
'challenge',
'challenge-xxxx',
'challenge-http-01'
].forEach(function(k) {
myFlags[k] = flagOptions[k];
});
cli.parse(myFlags);
cli.main(function(argList, flags) {
Flags.mangleFlags(flags, mconf, null, { forceSave: true });
main(argList, flags, greenlock);
}, args);
});
async function main(_, flags, greenlock) {
greenlock.manager
.defaults(flags)
.catch(function(err) {
console.error();
console.error('error:', err.message);
//console.log(err.stack);
console.error();
process.exit(1);
})
.then(function() {
return greenlock.manager.defaults();
})
.then(function(dconf) {
console.info();
console.info('Global config');
console.info(JSON.stringify(dconf, null, 2));
});
}

View File

@ -2,25 +2,8 @@
'use strict';
var args = process.argv.slice(2);
var arg0 = args[0];
//console.log(args);
var found = [
'certonly',
'add',
'update',
'config',
'defaults',
'remove',
'init'
].some(function(k) {
if (k === arg0) {
require('./' + k);
return true;
}
});
if (!found) {
console.error(arg0 + ': command not yet implemented');
process.exit(1);
console.log(args);
if ('certonly' === args[0]) {
require('./certonly.js');
return;
}

View File

@ -1,162 +0,0 @@
'use strict';
var P = require('../plugins.js');
var args = process.argv.slice(3);
var cli = require('./lib/cli.js');
var Greenlock = require('../');
var Flags = require('./lib/flags.js');
var flagOptions = Flags.flags();
var myFlags = {};
[
'config-dir',
'maintainer-email',
'cluster',
'manager',
'manager-xxxx'
].forEach(function(k) {
myFlags[k] = flagOptions[k];
});
cli.parse(myFlags);
cli.main(async function(argList, flags) {
var pkgRoot = process.cwd();
var manager = flags.manager;
if (['fs', 'cloud'].includes(manager)) {
manager = '@greenlock/manager';
}
if (['cloud'].includes(manager)) {
flags.managerOpts.cloud = true;
}
flags.manager = flags.managerOpts;
delete flags.managerOpts;
flags.manager.module = manager;
try {
if ('.' === String(manager)[0]) {
manager = require('path').resolve(pkgRoot, manager);
}
P._loadSync(manager);
} catch (e) {
try {
P._installSync(manager);
} catch (e) {
console.error(
'error:',
JSON.stringify(manager),
'could not be loaded, and could not be installed.'
);
process.exit(1);
}
}
var greenlock = Greenlock.create({
packageRoot: pkgRoot,
manager: flags.manager,
configDir: flags.configDir,
maintainerEmail: flags.maintainerEmail,
_mustPackage: true
});
await greenlock.manager.defaults();
//writeGreenlockJs(pkgdir, flags);
writeServerJs(pkgRoot, flags);
writeAppJs(pkgRoot);
/*
rc._bin_mode = true;
var Greenlock = require('../');
// this is a copy, so it's safe to modify
var greenlock = Greenlock.create(rc);
var mconf = await greenlock.manager.defaults();
var flagOptions = Flags.flags(mconf, myOpts);
*/
}, args);
/*
function writeGreenlockJs(pkgdir, flags) {
var greenlockJs = 'greenlock.js';
var fs = require('fs');
var path = require('path');
var tmpl = fs.readFileSync(
path.join(__dirname, 'tmpl/greenlock.tmpl.js'),
'utf8'
);
try {
fs.accessSync(path.join(pkgdir, greenlockJs));
console.warn("[skip] '%s' exists", greenlockJs);
return;
} catch (e) {
// continue
}
if (flags.maintainerEmail) {
tmpl = tmpl.replace(
/pkg.author/g,
JSON.stringify(flags.maintainerEmail)
);
}
fs.writeFileSync(path.join(pkgdir, greenlockJs), tmpl);
console.info("created '%s'", greenlockJs);
}
*/
function writeServerJs(pkgdir, flags) {
var serverJs = 'server.js';
var fs = require('fs');
var path = require('path');
var tmpl;
try {
fs.accessSync(path.join(pkgdir, serverJs));
console.warn("[skip] '%s' exists", serverJs);
return;
} catch (e) {
// continue
}
if (flags.cluster) {
tmpl = fs.readFileSync(
path.join(__dirname, 'tmpl/cluster.tmpl.js'),
'utf8'
);
tmpl = tmpl.replace(/cluster: false/g, 'cluster: true');
} else {
tmpl = fs.readFileSync(
path.join(__dirname, 'tmpl/server.tmpl.js'),
'utf8'
);
}
if (flags.maintainerEmail) {
tmpl = tmpl
.replace(/pkg.author/g, JSON.stringify(flags.maintainerEmail))
.replace(/\/\/maintainerEmail/g, 'maintainerEmail');
}
fs.writeFileSync(path.join(pkgdir, serverJs), tmpl);
console.info("created '%s'", serverJs);
}
function writeAppJs(pkgdir) {
var appJs = 'app.js';
var fs = require('fs');
var path = require('path');
var tmpl = fs.readFileSync(
path.join(__dirname, 'tmpl/app.tmpl.js'),
'utf8'
);
try {
fs.accessSync(path.join(pkgdir, appJs));
console.warn("[skip] '%s' exists", appJs);
return;
} catch (e) {
fs.writeFileSync(path.join(pkgdir, appJs), tmpl);
console.info("created '%s'", appJs);
}
}

View File

@ -1,240 +0,0 @@
'use strict';
var CLI = module.exports;
var defaultConf;
var defaultOpts;
var bags = [];
CLI.parse = function(conf) {
var opts = (defaultOpts = {});
defaultConf = conf;
Object.keys(conf).forEach(function(k) {
var v = conf[k];
if (!v) {
console.error(
'Developer Error: missing cli flag definition for',
JSON.stringify(k)
);
process.exit(1);
}
var aliases = v[5];
var bag;
var bagName;
// the name of the argument set is now the 0th argument
v.unshift(k);
// v[0] flagname
// v[1] short flagname
// v[2] description
// v[3] type
// v[4] default value
// v[5] aliases
if ('bag' === v[3]) {
bag = v[0]; // 'bag-option-xxxx' => '--bag-option-'
bag = '--' + bag.replace(/xxx.*/, '');
bags.push(bag);
bagName = toBagName(bag.replace(/^--/, ''));
opts[bagName] = {};
}
if ('json' === v[3]) {
bagName = toBagName(v[0].replace(/-json$/, '')); // 'bag-option-json' => 'bagOptionOpts'
opts[bagName] = {};
} else if ('ignore' !== v[3] && 'undefined' !== typeof v[4]) {
// set the default values (where 'undefined' is not an allowed value)
opts[toCamel(k)] = v[4];
}
if (!aliases) {
aliases = [];
} else if ('string' === typeof aliases) {
aliases = aliases.split(',');
}
aliases.forEach(function(alias) {
if (alias in conf) {
throw new Error(
"Cannot alias '" +
alias +
"' from '" +
k +
"': option already exists"
);
}
conf[alias] = v;
});
});
};
CLI.main = function(cb, args) {
var leftovers = [];
var conf = defaultConf;
var opts = defaultOpts;
if (!opts) {
throw new Error("you didn't call `CLI.parse(configuration)`");
}
// TODO what's the existing API for this?
if (!args) {
args = process.argv.slice(2);
}
var flag;
var cnf;
var typ;
function grab(bag) {
var bagName = toBagName(bag);
if (bag !== flag.slice(0, bag.length)) {
return false;
}
opts[bagName][toCamel(flag.slice(bag.length))] = args.shift();
return true;
}
while (args.length) {
// take one off the top
flag = args.shift();
// mind the gap
if ('--' === flag) {
leftovers = leftovers.concat(args);
break;
}
// help!
if (
'--help' === flag ||
'-h' === flag ||
'/?' === flag ||
'help' === flag
) {
printHelp(conf);
process.exit(1);
}
// only long names are actually used
if ('--' !== flag.slice(0, 2)) {
console.error("error: unrecognized flag '" + flag + "'");
process.exit(1);
}
cnf = conf[flag.slice(2)];
if (!cnf) {
// look for arbitrary flags
if (bags.some(grab)) {
continue;
}
// other arbitrary args are not used
console.error("unrecognized elided flag '" + flag + "'");
process.exit(1);
}
// encourage switching to non-aliased version
if (flag !== '--' + cnf[0]) {
console.warn(
"use of '" +
flag +
"' is deprecated, use '--" +
cnf[0] +
"' instead"
);
}
// look for xxx-json flags
if ('json' === cnf[3]) {
try {
var json = JSON.parse(args.shift());
var bagName = toBagName(cnf[0].replace(/-json$/, ''));
Object.keys(json).forEach(function(k) {
opts[bagName][k] = json[k];
});
} catch (e) {
console.error("Could not parse option '" + flag + "' as JSON:");
console.error(e.message);
process.exit(1);
}
continue;
}
// set booleans, otherwise grab the next arg in line
typ = cnf[3];
// TODO --no-<whatever> to negate
if (Boolean === typ || 'boolean' === typ) {
opts[toCamel(cnf[0])] = true;
continue;
}
opts[toCamel(cnf[0])] = args.shift();
continue;
}
cb(leftovers, opts);
};
function toCamel(str) {
return str.replace(/-([a-z0-9])/g, function(m) {
return m[1].toUpperCase();
});
}
function toBagName(bag) {
// trim leading and trailing '-'
bag = bag.replace(/^-+/g, '').replace(/-+$/g, '');
return toCamel(bag) + 'Opts'; // '--bag-option-' => bagOptionOpts
}
function printHelp(conf) {
var flagLen = 0;
var typeLen = 0;
var defLen = 0;
Object.keys(conf).forEach(function(k) {
flagLen = Math.max(flagLen, conf[k][0].length);
typeLen = Math.max(typeLen, conf[k][3].length);
if ('undefined' !== typeof conf[k][4]) {
defLen = Math.max(
defLen,
'(Default: )'.length + String(conf[k][4]).length
);
}
});
Object.keys(conf).forEach(function(k) {
var v = conf[k];
// skip aliases
if (v[0] !== k) {
return;
}
var def = v[4];
if ('undefined' === typeof def) {
def = '';
} else {
def = '(default: ' + JSON.stringify(def) + ')';
}
var msg =
' --' +
v[0].padEnd(flagLen) +
' ' +
v[3].padStart(typeLen + 1) +
' ' +
(v[2] || '') +
' ' +
def; /*.padStart(defLen)*/
// v[0] flagname
// v[1] short flagname
// v[2] description
// v[3] type
// v[4] default value
// v[5] aliases
console.info(msg);
});
}

View File

@ -1,400 +0,0 @@
'use strict';
var Flags = module.exports;
//var path = require('path');
var pkgRoot = process.cwd();
//var Init = require('../../lib/init.js');
// These are ALL options
// The individual CLI files each select a subset of them
Flags.flags = function(mconf, myOpts) {
// Current Manager Config
if (!mconf) {
mconf = {};
}
// Extra Override Options
if (!myOpts) {
myOpts = {};
}
return {
all: [
false,
'search all site configs rather than by --subject or --servernames',
'boolean'
],
'agree-to-terms': [
false,
"agree to the Let's Encrypts Subscriber Agreement and Greenlock Terms of Use",
'boolean'
],
subject: [
false,
'the "subject" (primary domain) of the certificate',
'string'
],
altnames: [
false,
'the "subject alternative names" (additional domains) on the certificate, the first of which MUST be the subject',
'string'
],
servername: [
false,
'a name that matches a subject or altname',
'string'
],
servernames: [
false,
'a list of names that matches a subject or altname',
'string'
],
cluster: [false, 'initialize with cluster mode on', 'boolean', false],
'renew-offset': [
false,
"time to wait until renewing the cert such as '45d' (45 days after being issued) or '-3w' (3 weeks before expiration date)",
'string',
mconf.renewOffset
],
'customer-email': [
false,
"the email address of the owner of the domain or site (not necessarily the Let's Encrypt or ACME subscriber)",
'string'
],
'subscriber-email': [
false,
"the email address of the Let's Encrypt or ACME Account subscriber (not necessarily the domain owner)",
'string'
],
'config-dir': [
false,
'the directory in which config.json and other config and storage files should be written',
'string'
],
'maintainer-email': [
false,
'the maintainance contact for security and critical bug notices',
'string'
],
'account-key-type': [
false,
"either 'P-256' (ECDSA) or 'RSA-2048' - although other values are technically supported, they don't make sense and won't work with many services (More bits != More security)",
'string',
mconf.accountKeyType
],
'server-key-type': [
false,
"either 'RSA-2048' or 'P-256' (ECDSA) - although other values are technically supported, they don't make sense and won't work with many services (More bits != More security)",
'string',
mconf.serverKeyType
],
store: [
false,
'the module name or file path of the store module to use',
'string'
//mconf.store.module
],
'store-xxxx': [
false,
'an option for the chosen store module, such as --store-apikey or --store-bucket',
'bag'
],
manager: [
false,
'the module name or file path of the manager module to use',
'string',
'@greenlock/manager'
],
'manager-xxxx': [
false,
'an option for the chosen manager module, such as --manager-apikey or --manager-dburl',
'bag'
],
challenge: [
false,
'the module name or file path of the HTTP-01, DNS-01, or TLS-ALPN-01 challenge module to use',
'string',
''
/*
Object.keys(mconf.challenges)
.map(function(typ) {
return mconf.challenges[typ].module;
})
.join(',')
*/
],
'challenge-xxxx': [
false,
'an option for the chosen challenge module, such as --challenge-apikey or --challenge-bucket',
'bag'
],
'challenge-json': [
false,
'a JSON string containing all option for the chosen challenge module (instead of --challenge-xxxx)',
'json',
'{}'
],
'challenge-http-01': [
false,
'the module name or file path of the HTTP-01 to add',
'string'
//(mconf.challenges['http-01'] || {}).module
],
'challenge-http-01-xxxx': [
false,
'an option for the chosen challenge module, such as --challenge-http-01-apikey or --challenge-http-01-bucket',
'bag'
],
'challenge-dns-01': [
false,
'the module name or file path of the DNS-01 to add',
'string'
//(mconf.challenges['dns-01'] || {}).module
],
'challenge-dns-01-xxxx': [
false,
'an option for the chosen challenge module, such as --challenge-dns-01-apikey or --challenge-dns-01-bucket',
'bag'
],
'challenge-tls-alpn-01': [
false,
'the module name or file path of the DNS-01 to add',
'string'
//(mconf.challenges['tls-alpn-01'] || {}).module
],
'challenge-tls-alpn-01-xxxx': [
false,
'an option for the chosen challenge module, such as --challenge-tls-alpn-01-apikey or --challenge-tls-alpn-01-bucket',
'bag'
],
'force-save': [
false,
"save all options for this site, even if it's the same as the defaults",
'boolean',
myOpts.forceSave || false
]
};
};
Flags.init = async function(myOpts) {
var Greenlock = require('../../');
// this is a copy, so it's safe to modify
var greenlock = Greenlock.create({
packageRoot: pkgRoot,
_mustPackage: true,
_init: true,
_bin_mode: true
});
var mconf = await greenlock.manager.defaults();
var flagOptions = Flags.flags(mconf, myOpts);
return {
flagOptions,
greenlock,
mconf
};
};
Flags.mangleFlags = function(flags, mconf, sconf, extras) {
if (extras) {
if (extras.forceSave) {
flags.forceSave = true;
}
}
//console.log('debug a:', flags);
if ('altnames' in flags) {
flags.altnames = (flags.altnames || '').split(/[,\s]+/).filter(Boolean);
}
if ('servernames' in flags) {
flags.servernames = (flags.servernames || '')
.split(/[,\s]+/)
.filter(Boolean);
}
var store;
if (flags.store) {
store = flags.storeOpts;
store.module = flags.store;
flags.store = store;
} else {
delete flags.store;
}
delete flags.storeOpts;
// If this is additive, make an object to hold all values
var isAdditive = [
['http-01', 'Http01'],
['dns-01', 'Dns01'],
['tls-alpn-01', 'TlsAlpn01']
].some(function(types) {
var typCamel = types[1];
var modname = 'challenge' + typCamel;
if (flags[modname]) {
if (!flags.challenges) {
flags.challenges = {};
}
return true;
}
});
if (isAdditive && sconf) {
// copy over the old
var schallenges = sconf.challenges || {};
Object.keys(schallenges).forEach(function(k) {
if (!flags.challenges[k]) {
flags.challenges[k] = schallenges[k];
}
});
}
var typ;
var challenge;
if (flags.challenge) {
// this varient of the flag is exclusive
flags.challenges = {};
isAdditive = false;
if (/http-01/.test(flags.challenge)) {
typ = 'http-01';
} else if (/dns-01/.test(flags.challenge)) {
typ = 'dns-01';
} else if (/tls-alpn-01/.test(flags.challenge)) {
typ = 'tls-alpn-01';
}
var modname = 'challenge';
var optsname = 'challengeOpts';
challenge = flags[optsname];
// JSON may already have module name
if (challenge.module) {
if (flags[modname] && challenge.module !== flags[modname]) {
console.error(
'module names do not match:',
JSON.stringify(challenge.module),
JSON.stringify(flags[modname])
);
process.exit(1);
}
} else {
challenge.module = flags[modname];
}
flags.challenges[typ] = challenge;
var chall = mconf.challenges[typ];
if (chall && challenge.module === chall.module) {
var keys = Object.keys(challenge);
var same =
!keys.length ||
keys.every(function(k) {
return chall[k] === challenge[k];
});
if (same && !flags.forceSave) {
delete flags.challenges;
}
}
}
delete flags.challenge;
delete flags.challengeOpts;
// Add each of the values, including the existing
[
['http-01', 'Http01'],
['dns-01', 'Dns01'],
['tls-alpn-01', 'TlsAlpn01']
].forEach(function(types) {
var typ = types[0];
var typCamel = types[1];
var modname = 'challenge' + typCamel;
var optsname = 'challenge' + typCamel + 'Opts';
var chall = mconf.challenges[typ];
var challenge = flags[optsname];
// this variant of the flag is additive
if (isAdditive && chall && flags.forceSave) {
if (flags.challenges && !flags.challenges[typ]) {
flags.challenges[typ] = chall;
}
}
if (!flags[modname]) {
delete flags[modname];
delete flags[optsname];
return;
}
// JSON may already have module name
if (challenge.module) {
if (flags[modname] && challenge.module !== flags[modname]) {
console.error(
'module names do not match:',
JSON.stringify(challenge.module),
JSON.stringify(flags[modname])
);
process.exit(1);
}
} else {
challenge.module = flags[modname];
}
if (flags[modname]) {
if (!flags.challenges) {
flags.challenges = {};
}
flags.challenges[typ] = challenge;
}
// Check to see if this is already what's set in the defaults
if (chall && challenge.module === chall.module) {
var keys = Object.keys(challenge);
// Check if all of the options are also the same
var same =
!keys.length ||
keys.every(function(k) {
return chall[k] === challenge[k];
});
if (same && !flags.forceSave) {
// If it's already the global, don't make it the per-site
delete flags[modname];
delete flags[optsname];
}
}
delete flags[modname];
delete flags[optsname];
});
[
['accountKeyType', [/256/, /384/, /EC/], 'EC-P256'],
['serverKeyType', [/RSA/], 'RSA-2048']
].forEach(function(k) {
var key = k[0];
var vals = k[1];
var val = flags[key];
if (val) {
if (
!vals.some(function(v) {
return v.test(val);
})
) {
flags[key] = k[2];
console.warn(
key,
"does not allow the value '",
val,
"' using the default '",
k[2],
"' instead."
);
}
}
});
Object.keys(flags).forEach(function(k) {
if (flags[k] === mconf[k] && !flags.forceSave) {
delete flags[k];
}
});
//console.log('debug z:', flags);
delete flags.forceSave;
};

View File

@ -1,55 +0,0 @@
'use strict';
var args = process.argv.slice(3);
var cli = require('./lib/cli.js');
//var path = require('path');
//var pkgpath = path.join(__dirname, '..', 'package.json');
//var pkgpath = path.join(process.cwd(), 'package.json');
var Flags = require('./lib/flags.js');
Flags.init().then(function({ flagOptions, greenlock, mconf }) {
var myFlags = {};
['subject'].forEach(function(k) {
myFlags[k] = flagOptions[k];
});
cli.parse(myFlags);
cli.main(function(argList, flags) {
Flags.mangleFlags(flags, mconf);
main(argList, flags, greenlock);
}, args);
});
async function main(_, flags, greenlock) {
if (!flags.subject) {
console.error('--subject must be provided as a valid domain');
process.exit(1);
return;
}
greenlock
.remove(flags)
.catch(function(err) {
console.error();
console.error('error:', err.message);
//console.log(err.stack);
console.error();
process.exit(1);
})
.then(function(site) {
if (!site) {
console.info();
console.info('No config found for', flags.subject);
console.info();
process.exit(1);
return;
}
console.info();
console.info(
'Deleted config for ' + JSON.stringify(flags.subject) + ':'
);
console.info(JSON.stringify(site, null, 2));
console.info();
});
}

View File

@ -1,9 +0,0 @@
'use strict';
// Here's a vanilla HTTP app to start,
// but feel free to replace it with Express, Koa, etc
var app = function(req, res) {
res.end('Hello, Encrypted World!');
};
module.exports = app;

View File

@ -1,30 +0,0 @@
'use strict';
require('greenlock-express')
.init(function() {
// var pkg = require('./package.json');
return {
// where to find .greenlockrc and set default paths
packageRoot: __dirname,
// name & version for ACME client user agent
//packageAgent: pkg.name + '/' + pkg.version,
// contact for security and critical bug notices
//maintainerEmail: pkg.author,
// where to look for configuration
configDir: './greenlock.d',
// whether or not to run at cloudscale
cluster: true
};
})
.ready(function(glx) {
var app = require('./app.js');
// Serves on 80 and 443
// Get's SSL certificates magically!
glx.serveApp(app);
});

View File

@ -1,13 +0,0 @@
'use strict';
var pkg = require('./package.json');
module.exports = require('@root/greenlock').create({
// name & version for ACME client user agent
packageAgent: pkg.name + '/' + pkg.version,
// contact for security and critical bug notices
//maintainerEmail: pkg.author,
// where to find .greenlockrc and set default paths
packageRoot: __dirname
});

View File

@ -1,20 +0,0 @@
'use strict';
var app = require('./app.js');
require('greenlock-express')
.init({
packageRoot: __dirname,
// contact for security and critical bug notices
//maintainerEmail: pkg.author,
// where to look for configuration
configDir: './greenlock.d',
// whether or not to run at cloudscale
cluster: false
})
// Serves on 80 and 443
// Get's SSL certificates magically!
.serve(app);

View File

@ -1,79 +0,0 @@
'use strict';
var args = process.argv.slice(3);
var cli = require('./lib/cli.js');
var Flags = require('./lib/flags.js');
Flags.init().then(function({ flagOptions, greenlock, mconf }) {
var myFlags = {};
[
'subject',
'altnames',
'renew-offset',
'subscriber-email',
'customer-email',
'server-key-type',
'challenge-http-01',
'challenge-http-01-xxxx',
'challenge-dns-01',
'challenge-dns-01-xxxx',
'challenge-tls-alpn-01',
'challenge-tls-alpn-01-xxxx',
'challenge',
'challenge-xxxx',
'challenge-json',
'force-save'
].forEach(function(k) {
myFlags[k] = flagOptions[k];
});
cli.parse(myFlags);
cli.main(async function(argList, flags) {
var sconf = await greenlock._config({ servername: flags.subject });
Flags.mangleFlags(flags, mconf, sconf);
main(argList, flags, greenlock);
}, args);
});
async function main(_, flags, greenlock) {
if (!flags.subject) {
console.error('--subject must be provided as a valid domain');
process.exit(1);
return;
}
greenlock
.update(flags)
.catch(function(err) {
console.error();
console.error('error:', err.message);
console.error();
process.exit(1);
})
.then(function() {
return greenlock
._config({ servername: flags.subject })
.then(function(site) {
if (!site) {
console.info();
console.info('No config found for', flags.subject);
console.info();
process.exit(1);
return;
}
console.info();
Object.keys(site).forEach(function(k) {
if ('defaults' === k) {
console.info(k + ':');
Object.keys(site.defaults).forEach(function(key) {
var value = JSON.stringify(site.defaults[key]);
console.info('\t' + key + ':' + value);
});
} else {
console.info(k + ': ' + JSON.stringify(site[k]));
}
});
});
});
}

View File

@ -21,304 +21,277 @@ var rawPending = {};
// Certificates
C._getOrOrder = function(gnlck, mconf, db, acme, chs, acc, args) {
var email = args.subscriberEmail || mconf.subscriberEmail;
var email =
args.subscriberEmail ||
mconf.subscriberEmail ||
gnlck._defaults.subscriberEmail;
var id = args.altnames
.slice(0)
.sort()
.join(' ');
if (pending[id]) {
return pending[id];
}
var id = args.altnames.join(' ');
if (pending[id]) {
return pending[id];
}
pending[id] = C._rawGetOrOrder(
gnlck,
mconf,
db,
acme,
chs,
acc,
email,
args
)
.then(function(pems) {
delete pending[id];
return pems;
})
.catch(function(err) {
delete pending[id];
throw err;
});
pending[id] = C._rawGetOrOrder(
gnlck,
mconf,
db,
acme,
chs,
acc,
email,
args
)
.then(function(pems) {
delete pending[id];
return pems;
})
.catch(function(err) {
delete pending[id];
throw err;
});
return pending[id];
return pending[id];
};
// Certificates
C._rawGetOrOrder = function(gnlck, mconf, db, acme, chs, acc, email, args) {
return C._check(gnlck, mconf, db, args).then(function(pems) {
// Nice and fresh? We're done!
if (pems) {
if (!C._isStale(gnlck, mconf, args, pems)) {
// return existing unexpired (although potentially stale) certificates when available
// there will be an additional .renewing property if the certs are being asynchronously renewed
//pems._type = 'current';
return pems;
}
}
return C._check(gnlck, mconf, db, args).then(function(pems) {
// No pems? get some!
if (!pems) {
return C._rawOrder(
gnlck,
mconf,
db,
acme,
chs,
acc,
email,
args
).then(function(newPems) {
// do not wait on notify
gnlck._notify('cert_issue', {
options: args,
subject: args.subject,
altnames: args.altnames,
account: acc,
email: email,
pems: newPems
});
return newPems;
});
}
// We're either starting fresh or freshening up...
var p = C._rawOrder(gnlck, mconf, db, acme, chs, acc, email, args);
var evname = pems ? 'cert_renewal' : 'cert_issue';
p.then(function(newPems) {
// notify in the background
var renewAt = C._renewWithStagger(gnlck, mconf, args, newPems);
gnlck._notify(evname, {
renewAt: renewAt,
subject: args.subject,
altnames: args.altnames
});
gnlck._notify('_cert_issue', {
renewAt: renewAt,
subject: args.subject,
altnames: args.altnames,
pems: newPems
});
}).catch(function(err) {
if (!err.context) {
err.context = evname;
}
err.subject = args.subject;
err.altnames = args.altnames;
gnlck._notify('error', err);
});
// Nice and fresh? We're done!
if (!C._isStale(gnlck, mconf, args, pems)) {
// return existing unexpired (although potentially stale) certificates when available
// there will be an additional .renewing property if the certs are being asynchronously renewed
//pems._type = 'current';
return pems;
}
// No choice but to hang tight and wait for it
if (
!pems ||
pems.renewAt < Date.now() - 24 * 60 * 60 * 1000 ||
pems.expiresAt <= Date.now() + 24 * 60 * 60 * 1000
) {
return p;
}
// Getting stale? Let's renew to freshen up!
var p = C._rawOrder(gnlck, mconf, db, acme, chs, acc, email, args).then(
function(renewedPems) {
// do not wait on notify
gnlck._notify('cert_renewal', {
options: args,
subject: args.subject,
altnames: args.altnames,
account: acc,
email: email,
pems: renewedPems
});
return renewedPems;
}
);
// Wait it out
// TODO should we call this waitForRenewal?
if (args.waitForRenewal) {
return p;
}
// TODO what should this be?
if (args.waitForRenewal) {
return p;
}
// Let the certs renew in the background
return pems;
});
return pems;
});
};
// we have another promise here because it the optional renewal
// may resolve in a different stack than the returned pems
C._rawOrder = function(gnlck, mconf, db, acme, chs, acc, email, args) {
var id = args.altnames
.slice(0)
.sort()
.join(' ');
if (rawPending[id]) {
return rawPending[id];
}
var id = args.altnames
.slice(0)
.sort()
.join(' ');
if (rawPending[id]) {
return rawPending[id];
}
var keyType = args.serverKeyType || mconf.serverKeyType;
var query = {
subject: args.subject,
certificate: args.certificate || {},
directoryUrl:
args.directoryUrl ||
mconf.directoryUrl ||
gnlck._defaults.directoryUrl
};
rawPending[id] = U._getOrCreateKeypair(db, args.subject, query, keyType)
.then(function(kresult) {
var serverKeypair = kresult.keypair;
var domains = args.altnames.slice(0);
var keyType =
args.serverKeyType ||
mconf.serverKeyType ||
gnlck._defaults.serverKeyType;
var query = {
subject: args.subject,
certificate: args.certificate || {},
directoryUrl:
args.directoryUrl ||
mconf.directoryUrl ||
gnlck._defaults.directoryUrl
};
rawPending[id] = U._getOrCreateKeypair(db, args.subject, query, keyType)
.then(function(kresult) {
var serverKeypair = kresult.keypair;
var domains = args.altnames.slice(0);
return CSR.csr({
jwk: serverKeypair.privateKeyJwk || serverKeypair.private,
domains: domains,
encoding: 'der'
})
.then(function(csrDer) {
// TODO let CSR support 'urlBase64' ?
return Enc.bufToUrlBase64(csrDer);
})
.then(function(csr) {
function notify(ev, opts) {
gnlck._notify(ev, opts);
}
var certReq = {
debug: args.debug || gnlck._defaults.debug,
return CSR.csr({
jwk: serverKeypair.privateKeyJwk || serverKeypair.private,
domains: domains,
encoding: 'der'
})
.then(function(csrDer) {
// TODO let CSR support 'urlBase64' ?
return Enc.bufToUrlBase64(csrDer);
})
.then(function(csr) {
function notify(ev, opts) {
gnlck._notify(ev, opts);
}
var certReq = {
debug: args.debug || gnlck._defaults.debug,
challenges: chs,
account: acc, // only used if accounts.key.kid exists
accountKey:
acc.keypair.privateKeyJwk || acc.keypair.private,
keypair: acc.keypair, // TODO
csr: csr,
domains: domains, // because ACME.js v3 uses `domains` still, actually
onChallengeStatus: notify,
notify: notify // TODO
challenges: chs,
account: acc, // only used if accounts.key.kid exists
accountKey:
acc.keypair.privateKeyJwk || acc.keypair.private,
keypair: acc.keypair, // TODO
csr: csr,
domains: domains, // because ACME.js v3 uses `domains` still, actually
onChallengeStatus: notify,
notify: notify // TODO
// TODO handle this in acme-v2
//subject: args.subject,
//altnames: args.altnames.slice(0),
};
return acme.certificates
.create(certReq)
.then(U._attachCertInfo);
})
.then(function(pems) {
if (kresult.exists) {
return pems;
}
query.keypair = serverKeypair;
return db.setKeypair(query, serverKeypair).then(function() {
return pems;
});
});
})
.then(function(pems) {
// TODO put this in the docs
// { cert, chain, privkey, subject, altnames, issuedAt, expiresAt }
// Note: the query has been updated
query.pems = pems;
return db.set(query);
})
.then(function() {
return C._check(gnlck, mconf, db, args);
})
.then(function(bundle) {
// TODO notify Manager
delete rawPending[id];
return bundle;
})
.catch(function(err) {
// Todo notify manager
delete rawPending[id];
throw err;
});
// TODO handle this in acme-v2
//subject: args.subject,
//altnames: args.altnames.slice(0),
};
return acme.certificates
.create(certReq)
.then(U._attachCertInfo);
})
.then(function(pems) {
if (kresult.exists) {
return pems;
}
query.keypair = serverKeypair;
return db.setKeypair(query, serverKeypair).then(function() {
return pems;
});
});
})
.then(function(pems) {
// TODO put this in the docs
// { cert, chain, privkey, subject, altnames, issuedAt, expiresAt }
// Note: the query has been updated
query.pems = pems;
return db.set(query);
})
.then(function() {
return C._check(gnlck, mconf, db, args);
})
.then(function(bundle) {
// TODO notify Manager
delete rawPending[id];
return bundle;
})
.catch(function(err) {
// Todo notify manager
delete rawPending[id];
throw err;
});
return rawPending[id];
return rawPending[id];
};
// returns pems, if they exist
C._check = function(gnlck, mconf, db, args) {
var query = {
subject: args.subject,
// may contain certificate.id
certificate: args.certificate,
directoryUrl:
args.directoryUrl ||
mconf.directoryUrl ||
gnlck._defaults.directoryUrl
};
return db.check(query).then(function(pems) {
if (!pems) {
return null;
}
var query = {
subject: args.subject,
// may contain certificate.id
certificate: args.certificate,
directoryUrl:
args.directoryUrl ||
mconf.directoryUrl ||
gnlck._defaults.directoryUrl
};
return db.check(query).then(function(pems) {
if (!pems) {
return null;
}
pems = U._attachCertInfo(pems);
pems = U._attachCertInfo(pems);
// For eager management
if (args.subject && !U._certHasDomain(pems, args.subject)) {
// TODO report error, but continue the process as with no cert
return null;
}
// For eager management
if (args.subject && !U._certHasDomain(pems, args.subject)) {
// TODO report error, but continue the process as with no cert
return null;
}
// For lazy SNI requests
if (args.domain && !U._certHasDomain(pems, args.domain)) {
// TODO report error, but continue the process as with no cert
return null;
}
// For lazy SNI requests
if (args.domain && !U._certHasDomain(pems, args.domain)) {
// TODO report error, but continue the process as with no cert
return null;
}
return U._getKeypair(db, args.subject, query)
.then(function(keypair) {
return Keypairs.export({
jwk: keypair.privateKeyJwk || keypair.private,
encoding: 'pem'
}).then(function(pem) {
pems.privkey = pem;
return pems;
});
})
.catch(function() {
// TODO report error, but continue the process as with no cert
return null;
});
});
return U._getKeypair(db, args.subject, query)
.then(function(keypair) {
return Keypairs.export({
jwk: keypair.privateKeyJwk || keypair.private,
encoding: 'pem'
}).then(function(pem) {
pems.privkey = pem;
return pems;
});
})
.catch(function() {
// TODO report error, but continue the process as with no cert
return null;
});
});
};
// Certificates
C._isStale = function(gnlck, mconf, args, pems) {
if (args.duplicate) {
return true;
}
if (args.duplicate) {
return true;
}
var renewAt = C._renewableAt(gnlck, mconf, args, pems);
var renewAt = C._renewableAt(gnlck, mconf, args, pems);
if (Date.now() >= renewAt) {
return true;
}
if (Date.now() >= renewAt) {
return true;
}
return false;
return false;
};
C._renewWithStagger = function(gnlck, mconf, args, pems) {
var renewOffset = C._renewOffset(gnlck, mconf, args, pems);
var renewStagger;
try {
renewStagger = U._parseDuration(
args.renewStagger || mconf.renewStagger || 0
);
} catch (e) {
renewStagger = U._parseDuration(
args.renewStagger || mconf.renewStagger
);
}
// TODO check this beforehand
if (!args.force && renewStagger / renewOffset >= 0.5) {
renewStagger = renewOffset * 0.1;
}
if (renewOffset > 0) {
// stagger forward, away from issued at
return Math.round(
pems.issuedAt + renewOffset + Math.random() * renewStagger
);
}
// stagger backward, toward issued at
return Math.round(
pems.expiresAt + renewOffset - Math.random() * renewStagger
);
};
C._renewOffset = function(gnlck, mconf, args /*, pems*/) {
var renewOffset = U._parseDuration(
args.renewOffset || mconf.renewOffset || 0
);
var week = 1000 * 60 * 60 * 24 * 6;
if (!args.force && Math.abs(renewOffset) < week) {
throw new Error(
'developer error: `renewOffset` should always be at least a week, use `force` to not safety-check renewOffset'
);
}
return renewOffset;
};
C._renewableAt = function(gnlck, mconf, args, pems) {
if (args.renewAt) {
return args.renewAt;
}
if (args.renewAt) {
return args.renewAt;
}
var renewOffset = C._renewOffset(gnlck, mconf, args, pems);
var renewOffset =
args.renewOffset ||
mconf.renewOffset ||
gnlck._defaults.renewOffset ||
0;
var week = 1000 * 60 * 60 * 24 * 6;
if (!args.force && Math.abs(renewOffset) < week) {
throw new Error(
'developer error: `renewOffset` should always be at least a week, use `force` to not safety-check renewOffset'
);
}
if (renewOffset > 0) {
return pems.issuedAt + renewOffset;
}
if (renewOffset > 0) {
return pems.issuedAt + renewOffset;
}
return pems.expiresAt + renewOffset;
return pems.expiresAt + renewOffset;
};

View File

@ -3,14 +3,14 @@
var E = module.exports;
function create(code, msg) {
E[code] = function(ctx, msg2) {
var err = new Error(msg);
err.code = code;
err.context = ctx;
if (msg2) {
err.message += ': ' + msg2;
}
/*
E[code] = function(ctx, msg2) {
var err = new Error(msg);
err.code = code;
err.context = ctx;
if (msg2) {
err.message += ': ' + msg2;
}
/*
Object.keys(extras).forEach(function(k) {
if ('message' === k) {
err.message += ': ' + extras[k];
@ -19,39 +19,39 @@ function create(code, msg) {
}
});
*/
return err;
};
return err;
};
}
// TODO open issues and link to them as the error url
create(
'NO_MAINTAINER',
'please supply `maintainerEmail` as a contact for security and critical bug notices'
'NO_MAINTAINER',
'please supply `maintainerEmail` as a contact for security and critical bug notices'
);
create(
'BAD_ORDER',
'altnames should be in deterministic order, with subject as the first altname'
'BAD_ORDER',
'altnames should be in deterministic order, with subject as the first altname'
);
create('NO_SUBJECT', 'no certificate subject given');
create(
'NO_SUBSCRIBER',
'please supply `subscriberEmail` as a contact for failed renewal and certificate revocation'
'NO_SUBSCRIBER',
'please supply `subscriberEmail` as a contact for failed renewal and certificate revocation'
);
create(
'INVALID_SUBSCRIBER',
'`subscriberEmail` is not a valid address, please check for typos'
'INVALID_SUBSCRIBER',
'`subscriberEmail` is not a valid address, please check for typos'
);
create(
'INVALID_HOSTNAME',
'valid hostnames must be restricted to a-z0-9_.- and contain at least one "."'
'INVALID_HOSTNAME',
'valid hostnames must be restricted to a-z0-9_.- and contain at least one "."'
);
create(
'INVALID_DOMAIN',
'one or more domains do not exist on public DNS SOA record'
'INVALID_DOMAIN',
'one or more domains do not exist on public DNS SOA record'
);
create(
'NOT_UNIQUE',
'found duplicate domains, or a subdomain that overlaps a wildcard'
'NOT_UNIQUE',
'found duplicate domains, or a subdomain that overlaps a wildcard'
);
// exported for testing only

60
examples/manage.js Normal file
View File

@ -0,0 +1,60 @@
'use strict';
// tradeoff - lazy load certs vs DOS invalid sni
var Manager = module.exports;
var Cache = {};
Manager.create = function(conf) {
var domains = conf.domains;
var manager = {};
// { servername, wildname }
manager.getSubject = function(opts) {
if (
!opts.domains.includes(opts.domain) &&
!opts.domains.includes(opts.wildname)
) {
throw new Error('not a registered domain');
}
return opts.domains[0];
};
manager.add = function() {};
// { servername, wildname }
manager.configure = function(opts) {};
// { servername }
manager._contexts = {};
};
var manager = Manager.create({
domains: ['example.com', '*.example.com']
});
Cache.getTlsContext = function(servername) {
// TODO exponential fallback certificate renewal
if (Cache._contexts[servername]) {
// may be a context, or a promise for a context
return Cache._contexts[servername];
}
var wildname =
'*.' +
(servername || '')
.split('.')
.slice(1)
.join('.');
var opts = {
servername: servername,
domain: servername,
wildname: wildname
};
manager._contexts[servername] = manager
.orderCertificate(opts)
.then(function() {})
.catch(function(e) {});
};

16
examples/server.js Normal file
View File

@ -0,0 +1,16 @@
'use strict';
var http = require('http');
var https = require('http2');
var greenlock = require('../greenlock.js').create({
maintainerEmail: 'jon@example.com'
});
function app(req, res) {
res.end('Hello, Encrypted World!');
}
http.createServer(greenlock.plainMiddleware()).listen(8080);
https
.createServer(greenlock.tlsOptions, greenlock.secureMiddleware(app))
.listen(8443);

42
express.js Normal file
View File

@ -0,0 +1,42 @@
'use strict';
var Greenlock = module.exports;
Greenlock.server = function (opts) {
var opts = Greenlock.create(opts);
opts.plainMiddleware = function(req, res) {
return Greenlock._plainMiddleware(opts, req, res);
};
opts.secureMiddleware = function(req, res) {
return Greenlock._secureMiddleware(opts, req, res);
};
opts.tlsOptions = {
SNICallback: function(servername, cb) {
return Greenlock._sniCallback(opts, servername)
.then(function() {
cb(null);
})
.catch(function(err) {
cb(err);
});
}
};
return opts;
};
// must handle http-01 challenges
Greenlock._plainMiddleware = function(opts, req, res) {};
// should check for domain fronting
Greenlock._secureMiddleware = function(opts, req, res) {};
// should check to see if domain is allowed, and if domain should be renewed
// manage should be able to clear the internal cache
Greenlock._sniCallback = function(opts, servername) {};
Greenlock._onSniRejection(function () {
});

File diff suppressed because it is too large Load Diff

View File

@ -1,191 +0,0 @@
'use strict';
// TODO how to handle path differences when run from npx vs when required by greenlock?
var fs = require('fs');
var path = require('path');
function saveFile(rcpath, data, enc) {
// because this may have a database url or some such
fs.writeFileSync(rcpath, data, enc);
return fs.chmodSync(rcpath, parseInt('0600', 8));
}
var GRC = (module.exports = function(pkgpath, manager, rc) {
// TODO when run from package
// Run from the package root (assumed) or exit
var pkgdir = path.dirname(pkgpath);
try {
require(pkgpath);
} catch (e) {
console.error(
'npx greenlock must be run from the package root (where package.json is)'
);
process.exit(1);
}
try {
return module.exports._defaults(pkgdir, manager, rc);
} catch (e) {
if ('package.json' === e.context) {
console.error(e.desc);
process.exit(1);
}
console.error(e.message);
process.exit(1);
}
});
// Figure out what to do between what's hard-coded,
// what's in the config file, and what's left unset
module.exports.resolve = function(gconf) {
var rc = GRC.read(gconf.packageRoot);
if (gconf.configFile) {
rc = { configFile: gconf.configFile };
}
var manager;
var updates;
if (rc.manager) {
if (gconf.manager && rc.manager !== gconf.manager) {
console.warn(
'warn: ignoring hard-coded ' +
gconf.manager +
' in favor of ' +
rc.manager
);
}
gconf.manager = rc.manager;
} else if (gconf.manager) {
manager = gconf.manager;
}
if (rc.configFile) {
if (gconf.configFile && rc.configFile !== gconf.configFile) {
console.warn(
'warn: ignoring hard-coded ' +
gconf.configFile +
' in favor of ' +
rc.configFile
);
}
gconf.configFile = rc.configFile;
} else if (gconf.manager) {
updates = { configFile: gconf.configFile };
}
return GRC._defaults(gconf.packageRoot, manager, rc);
};
module.exports._defaults = function(pkgdir, manager, rc) {
var rcpath = path.join(pkgdir, '.greenlockrc');
var _rc;
var created = false;
if (manager) {
if ('.' === manager[0]) {
manager = path.resolve(pkgdir, manager);
}
try {
require(manager);
} catch (e) {
console.error('could not load ' + manager + ' from ' + pkgdir);
throw e;
}
}
var stuff = module.exports._read(pkgdir);
_rc = stuff.rc;
created = stuff.created;
var changed;
if (manager) {
if (!_rc.manager) {
_rc.manager = manager;
}
if (_rc.manager !== manager) {
console.info('Switching manager:');
var older = _rc.manager;
var newer = manager;
if ('/' === older[0]) {
older = path.relative(pkgdir, older);
}
if ('/' === newer[0]) {
newer = path.relative(pkgdir, newer);
}
console.info('\told: ' + older);
console.info('\tnew: ' + newer);
changed = true;
}
}
if (rc) {
changed = true;
Object.keys(rc).forEach(function(k) {
_rc[k] = rc[k];
});
}
if (['@greenlock/manager', 'greenlock-manager-fs'].includes(_rc.manager)) {
if (!_rc.configFile) {
changed = true;
_rc.configFile = path.join(pkgdir, 'greenlock.json');
}
}
if (!changed) {
return _rc;
}
var data = JSON.stringify(_rc, null, 2);
if (created) {
console.info('Wrote ' + rcpath);
}
saveFile(rcpath, data, 'utf8');
return _rc;
};
module.exports.read = function(pkgdir) {
return module.exports._read(pkgdir).rc;
};
module.exports._read = function(pkgdir) {
var created;
var rcpath = path.join(pkgdir, '.greenlockrc');
var _data;
try {
_data = fs.readFileSync(rcpath, 'utf8');
} catch (err) {
if ('ENOENT' !== err.code) {
throw err;
}
try {
require(path.resolve(path.join(pkgdir, './package.json')));
} catch (e) {
e.context = 'package.json';
e.desc =
'run `greenlock` from the same directory as `package.json`, or specify `packageRoot` of `.greenlockrc`';
throw e;
}
console.info('Creating ' + rcpath);
created = true;
_data = '{}';
saveFile(rcpath, _data, 'utf8');
}
var rc;
try {
rc = JSON.parse(_data);
} catch (e) {
console.error("couldn't parse " + rcpath, _data);
console.error('(perhaps you should just delete it and try again?)');
process.exit(1);
}
return {
created: created,
rc: rc
};
};

View File

@ -1,88 +0,0 @@
'use strict';
var Greenlock = require('../');
module.exports.wrap = function(greenlock) {
greenlock.challenges = {};
greenlock.challenges.get = async function(chall) {
// TODO pick one and warn on the others
// (just here due to some backwards compat issues with early v3 plugins)
var servername =
chall.servername ||
chall.altname ||
(chall.identifier && chall.identifier.value);
// TODO some sort of caching to prevent database hits?
var site = await greenlock._config({ servername: servername });
if (!site) {
return null;
}
// Hmm... this _should_ be impossible
if (!site.challenges || !site.challenges['http-01']) {
var copy = JSON.parse(JSON.stringify(site));
sanitizeCopiedConf(copy);
sanitizeCopiedConf(copy.store);
if (site.challenges) {
sanitizeCopiedConf(copy.challenges['http-01']);
sanitizeCopiedConf(copy.challenges['dns-01']);
sanitizeCopiedConf(copy.challenges['tls-alpn-01']);
}
console.warn('[Bug] Please report this error:');
console.warn(
'\terror: http-01 challenge requested, but not even a default http-01 config exists'
);
console.warn('\tservername:', JSON.stringify(servername));
console.warn('\tsite:', JSON.stringify(copy));
return null;
}
var plugin = await Greenlock._loadChallenge(site.challenges, 'http-01');
if (!plugin) {
return null;
}
var keyAuth;
var keyAuthDigest;
var result = await plugin.get({
challenge: {
type: chall.type,
//hostname: chall.servername,
altname: chall.servername,
identifier: { value: chall.servername },
token: chall.token
}
});
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 = result.challenge;
}
keyAuth = result.keyAuthorization;
keyAuthDigest = result.keyAuthorizationDigest;
}
if (/dns/.test(chall.type)) {
return { keyAuthorizationDigest: keyAuthDigest };
}
return { keyAuthorization: keyAuth };
};
};
function sanitizeCopiedConf(copy) {
if (!copy) {
return;
}
Object.keys(copy).forEach(function(k) {
if (/(api|key|token)/i.test(k) && 'string' === typeof copy[k]) {
copy[k] = '**redacted**';
}
});
return copy;
}

View File

@ -1,46 +0,0 @@
var DIR = module.exports;
// This will ALWAYS print out a notice if the URL is clearly a staging URL
DIR._getDirectoryUrl = function(dirUrl, domain) {
var liveUrl = 'https://acme-v02.api.letsencrypt.org/directory';
dirUrl = DIR._getDefaultDirectoryUrl(dirUrl, '', domain);
if (!dirUrl) {
dirUrl = liveUrl;
// This will print out a notice (just once) if no directoryUrl has been supplied
if (!DIR._shownDirectoryUrl) {
DIR._shownDirectoryUrl = true;
console.info('ACME Directory URL:', dirUrl);
}
}
return dirUrl;
};
// Handle staging URLs, pebble test server, etc
DIR._getDefaultDirectoryUrl = function(dirUrl, staging, domain) {
var stagingUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory';
var stagingRe = /(^http:|staging|^127\.0\.|^::|localhost)/;
var env = '';
var args = [];
if ('undefined' !== typeof process) {
env = (process.env && process.env.ENV) || '';
args = (process.argv && process.argv.slice(1)) || [];
}
if (
staging ||
stagingRe.test(dirUrl) ||
args.includes('--staging') ||
/DEV|STAG/i.test(env)
) {
if (!stagingRe.test(dirUrl)) {
dirUrl = stagingUrl;
}
console.info('[staging] ACME Staging Directory URL:', dirUrl, env);
console.warn('FAKE CERTIFICATES (for testing) only', env, domain);
console.warn('');
}
return dirUrl;
};
DIR._shownDirectoryUrl = false;

View File

@ -1,194 +0,0 @@
'use strict';
var Init = module.exports;
var fs = require('fs');
var path = require('path');
//var promisify = require("util").promisify;
Init._init = function(opts) {
//var Rc = require("@root/greenlock/rc");
var Rc = require('./rc.js');
var pkgText;
var pkgErr;
var msgErr;
//var emailErr;
var realPkg;
var userPkg;
var myPkg = {};
// we want to be SUPER transparent that we're reading from package.json
// we don't want anything unexpected
var implicitConfig = [];
var rc;
if (opts.packageRoot) {
try {
pkgText = fs.readFileSync(
path.resolve(opts.packageRoot, 'package.json'),
'utf8'
);
opts._hasPackage = true;
} catch (e) {
pkgErr = e;
if (opts._mustPackage) {
console.error(
'Should be run from package root (the same directory as `package.json`)'
);
process.exit(1);
return;
}
console.warn(
'`packageRoot` should be the root of the package (probably `__dirname`)'
);
}
}
if (pkgText) {
try {
realPkg = JSON.parse(pkgText);
} catch (e) {
pkgErr = e;
}
}
userPkg = opts.package;
if (realPkg || userPkg) {
userPkg = userPkg || {};
realPkg = realPkg || {};
// build package agent
if (!opts.packageAgent) {
// name
myPkg.name = userPkg.name;
if (!myPkg.name) {
myPkg.name = realPkg.name;
implicitConfig.push('name');
}
// version
myPkg.version = userPkg.version;
if (!myPkg.version) {
myPkg.version = realPkg.version;
implicitConfig.push('version');
}
if (myPkg.name && myPkg.version) {
opts.packageAgent = myPkg.name + '/' + myPkg.version;
}
}
// build author
myPkg.author = opts.maintainerEmail;
if (!myPkg.author) {
myPkg.author =
(userPkg.author && userPkg.author.email) || userPkg.author;
}
if (!myPkg.author) {
implicitConfig.push('author');
myPkg.author =
(realPkg.author && realPkg.author.email) || realPkg.author;
}
if (!opts._init) {
opts.maintainerEmail = myPkg.author;
}
}
if (!opts.packageAgent) {
msgErr =
'missing `packageAgent` and also failed to read `name` and/or `version` from `package.json`';
if (pkgErr) {
msgErr += ': ' + pkgErr.message;
}
throw new Error(msgErr);
}
if (!opts._init) {
opts.maintainerEmail = parseMaintainer(opts.maintainerEmail);
if (!opts.maintainerEmail) {
msgErr =
'missing or malformed `maintainerEmail` (or `author` from `package.json`), which is used as the contact for support notices';
throw new Error(msgErr);
}
}
if (opts.packageRoot) {
// Place the rc file in the packageroot
rc = Rc._initSync(opts.packageRoot, opts.manager, opts.configDir);
opts.configDir = rc.configDir;
opts.manager = rc.manager;
}
if (!opts.configDir && !opts.manager) {
throw new Error(
'missing `packageRoot` and `configDir`, but no `manager` was supplied'
);
}
opts.configFile = path.join(
path.resolve(opts.packageRoot, opts.configDir),
'config.json'
);
var config;
try {
config = JSON.parse(fs.readFileSync(opts.configFile));
} catch (e) {
if ('ENOENT' !== e.code) {
throw e;
}
config = { defaults: {} };
}
opts.manager =
rc.manager ||
(config.defaults && config.defaults.manager) ||
config.manager;
if (!opts.manager) {
opts.manager = '@greenlock/manager';
}
if ('string' === typeof opts.manager) {
opts.manager = {
module: opts.manager
};
}
opts.manager = JSON.parse(JSON.stringify(opts.manager));
var confconf = ['configDir', 'configFile', 'staging', 'directoryUrl'];
Object.keys(opts).forEach(function(k) {
if (!confconf.includes(k)) {
return;
}
if ('undefined' !== typeof opts.manager[k]) {
return;
}
opts.manager[k] = opts[k];
});
/*
var ignore = ["packageRoot", "maintainerEmail", "packageAgent", "staging", "directoryUrl", "manager"];
Object.keys(opts).forEach(function(k) {
if (ignore.includes(k)) {
return;
}
opts.manager[k] = opts[k];
});
*/
// Place the rc file in the configDir itself
//Rc._initSync(opts.configDir, opts.configDir);
return opts;
};
// 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 parseMaintainer(maintainerEmail) {
try {
maintainerEmail = maintainerEmail.match(looseEmailRe)[2];
} catch (e) {
maintainerEmail = null;
}
return maintainerEmail;
}

View File

@ -1,645 +0,0 @@
'use strict';
var U = require('../utils.js');
var E = require('../errors.js');
var warned = {};
// The purpose of this file is to try to auto-build
// partial managers so that the external API can be smaller.
module.exports.wrap = function(greenlock, gconf) {
var myFind = gconf.find;
delete gconf.find;
var mega = mergeManager(greenlock, gconf);
greenlock.manager = {};
greenlock.sites = {};
//greenlock.accounts = {};
//greenlock.certs = {};
greenlock.manager._modulename = gconf.manager.module;
if ('/' === String(gconf.manager.module)[0]) {
greenlock.manager._modulename = require('path').relative(
gconf.packageRoot,
greenlock.manager._modulename
);
if ('.' !== String(greenlock.manager._modulename)[0]) {
greenlock.manager._modulename =
'./' + greenlock.manager._modulename;
}
}
var allowed = [
'accountKeyType', //: ["P-256", "RSA-2048"],
'serverKeyType', //: ["RSA-2048", "P-256"],
'store', // : { module, specific opts },
'challenges', // : { "http-01", "dns-01", "tls-alpn-01" },
'subscriberEmail',
'agreeToTerms',
'agreeTos',
'customerEmail',
'renewOffset',
'renewStagger',
'module', // not allowed, just ignored
'manager'
];
// get / set default site settings such as
// subscriberEmail, store, challenges, renewOffset, renewStagger
greenlock.manager.defaults = function(conf) {
return greenlock._init().then(function() {
if (!conf) {
return mega.defaults();
}
if (conf.sites) {
throw new Error('cannot set sites as global config');
}
if (conf.routes) {
throw new Error('cannot set routes as global config');
}
// disallow keys we know to be bad
[
'subject',
'deletedAt',
'altnames',
'lastAttemptAt',
'expiresAt',
'issuedAt',
'renewAt',
'sites',
'routes'
].some(function(k) {
if (k in conf) {
throw new Error(
'`' + k + '` not allowed as a default setting'
);
}
});
Object.keys(conf).forEach(function(k) {
if (!allowed.includes(k) && !warned[k]) {
warned[k] = true;
console.warn(
k +
" isn't a known key. Please open an issue and let us know the use case."
);
}
});
Object.keys(conf).forEach(function(k) {
if (-1 !== ['module', 'manager'].indexOf(k)) {
return;
}
if ('undefined' === typeof k) {
throw new Error(
"'" +
k +
"' should be set to a value, or `null`, but not left `undefined`"
);
}
});
return mega.defaults(conf);
});
};
greenlock.manager._defaults = function(opts) {
return mega.defaults(opts);
};
greenlock.manager.add = function(args) {
if (!args || !Array.isArray(args.altnames) || !args.altnames.length) {
throw new Error(
'you must specify `altnames` when adding a new site'
);
}
if (args.renewAt) {
throw new Error(
'you cannot specify `renewAt` when adding a new site'
);
}
return greenlock.manager.set(args);
};
// TODO agreeToTerms should be handled somewhere... maybe?
// Add and update remains because I said I had locked the API
greenlock.manager.set = greenlock.manager.update = function(args) {
return greenlock._init().then(function() {
// The goal is to make this decently easy to manage by hand without mistakes
// but also reasonably easy to error check and correct
// and to make deterministic auto-corrections
args.subject = checkSubject(args);
//var subscriberEmail = args.subscriberEmail;
// TODO shortcut the other array checks when not necessary
if (Array.isArray(args.altnames)) {
args.altnames = checkAltnames(args.subject, args);
}
// at this point we know that subject is the first of altnames
return Promise.all(
(args.altnames || []).map(function(d) {
d = d.replace('*.', '');
return U._validDomain(d);
})
).then(function() {
if (!U._uniqueNames(args.altnames || [])) {
throw E.NOT_UNIQUE(
'add',
"'" + args.altnames.join("' '") + "'"
);
}
// durations
if (args.renewOffset) {
args.renewOffset = U._parseDuration(args.renewOffset);
}
if (args.renewStagger) {
args.renewStagger = U._parseDuration(args.renewStagger);
}
return mega.set(args).then(function(result) {
if (!gconf._bin_mode) {
greenlock.renew({}).catch(function(err) {
if (!err.context) {
err.contxt = 'renew';
}
greenlock._notify('error', err);
});
}
return result;
});
});
});
};
greenlock.manager.get = greenlock.sites.get = function(args) {
return Promise.resolve().then(function() {
if (args.subject) {
throw new Error(
'get({ servername }) searches certificates by altnames, not by subject specifically'
);
}
if (args.servernames || args.altnames || args.renewBefore) {
throw new Error(
'get({ servername }) does not take arguments that could lead to multiple results'
);
}
return mega.get(args);
});
};
greenlock.manager.remove = function(args) {
return Promise.resolve().then(function() {
args.subject = checkSubject(args);
if (args.servername) {
throw new Error(
'remove() should be called with `subject` only, if you wish to remove altnames use `update()`'
);
}
if (args.altnames) {
throw new Error(
'remove() should be called with `subject` only, not `altnames`'
);
}
// TODO check no altnames
return mega.remove(args);
});
};
/*
{
subject: site.subject,
altnames: site.altnames,
//issuedAt: site.issuedAt,
//expiresAt: site.expiresAt,
renewOffset: site.renewOffset,
renewStagger: site.renewStagger,
renewAt: site.renewAt,
subscriberEmail: site.subscriberEmail,
customerEmail: site.customerEmail,
challenges: site.challenges,
store: site.store
};
*/
// no transaction promise here because it calls set
greenlock._find = async function(args) {
args = _mangleFindArgs(args);
var ours = await mega.find(args);
if (!myFind) {
return ours;
}
// if the user has an overlay find function we'll do a diff
// between the managed state and the overlay, and choose
// what was found.
var theirs = await myFind(args);
theirs = theirs.filter(function(site) {
if (!site || 'string' !== typeof site.subject) {
throw new Error('found site is missing subject');
}
if (
!Array.isArray(site.altnames) ||
!site.altnames.length ||
!site.altnames[0] ||
site.altnames[0] !== site.subject
) {
throw new Error('missing or malformed altnames');
}
['renewAt', 'issuedAt', 'expiresAt'].forEach(function(k) {
if (site[k]) {
throw new Error(
'`' +
k +
'` should be updated by `set()`, not by `find()`'
);
}
});
if (!site) {
return;
}
if (args.subject && site.subject !== args.subject) {
return false;
}
var servernames = args.servernames || args.altnames;
if (
servernames &&
!site.altnames.some(function(altname) {
return servernames.includes(altname);
})
) {
return false;
}
return site.renewAt < (args.renewBefore || Infinity);
});
return _mergeFind(ours, theirs);
};
function _mergeFind(ours, theirs) {
var toUpdate = [];
theirs.forEach(function(_newer) {
var hasCurrent = ours.some(function(_older) {
var changed = false;
if (_newer.subject !== _older.subject) {
return false;
}
// BE SURE TO SET THIS UNDEFINED AFTERWARDS
_older._exists = true;
_newer.deletedAt = _newer.deletedAt || 0;
Object.keys(_newer).forEach(function(k) {
if (_older[k] !== _newer[k]) {
changed = true;
_older[k] = _newer[k];
}
});
if (changed) {
toUpdate.push(_older);
}
// handled the (only) match
return true;
});
if (!hasCurrent) {
toUpdate.push(_newer);
}
});
// delete the things that are gone
ours.forEach(function(_older) {
if (!_older._exists) {
_older.deletedAt = Date.now();
toUpdate.push(_older);
}
_older._exists = undefined;
});
Promise.all(
toUpdate.map(function(site) {
return greenlock.sites.update(site).catch(function(err) {
console.error(
'Developer Error: cannot update sites from user-supplied `find()`:'
);
console.error(err);
});
})
);
// ours is updated from theirs
return ours;
}
greenlock.manager.init = mega.init;
};
function checkSubject(args) {
if (!args || !args.subject) {
throw new Error('you must specify `subject` when configuring a site');
}
/*
if (!args.subject) {
throw E.NO_SUBJECT('add');
}
*/
var subject = (args.subject || '').toLowerCase();
if (subject !== args.subject) {
console.warn('`subject` must be lowercase', args.subject);
}
return U._encodeName(subject);
}
function checkAltnames(subject, args) {
// the things we have to check and get right
var altnames = (args.altnames || []).map(function(name) {
return String(name || '').toLowerCase();
});
// punycode BEFORE validation
// (set, find, remove)
if (altnames.join() !== args.altnames.join()) {
console.warn(
'all domains in `altnames` must be lowercase:',
args.altnames
);
}
args.altnames = args.altnames.map(U._encodeName);
if (
!args.altnames.every(function(d) {
return U._validName(d);
})
) {
throw E.INVALID_HOSTNAME('add', "'" + args.altnames.join("' '") + "'");
}
if (subject && subject !== args.altnames[0]) {
throw E.BAD_ORDER(
'add',
'(' + args.subject + ") '" + args.altnames.join("' '") + "'"
);
}
/*
if (subject && subject !== altnames[0]) {
throw new Error(
'`subject` must be the first domain in `altnames`',
args.subject,
altnames.join(' ')
);
}
*/
return altnames;
}
function loadManager(gconf) {
var m;
// 1. Get the manager
// 2. Figure out if we need to wrap it
/*
if (!gconf.manager) {
gconf.manager = '@greenlock/manager';
}
if ('string' !== typeof gconf.manager) {
throw new Error(
'`manager` should be a string representing the npm name or file path of the module'
);
}
*/
try {
// wrap this to be safe for @greenlock/manager
m = require(gconf.manager.module).create(gconf.manager);
} catch (e) {
console.error('Error loading manager:');
console.error(e.code);
console.error(e.message);
}
if (!m) {
console.error();
console.error(
'Failed to load manager plugin ',
JSON.stringify(gconf.manager)
);
console.error();
process.exit(1);
}
return m;
}
function mergeManager(greenlock, gconf) {
var mng;
function m() {
if (mng) {
return mng;
}
mng = require('@greenlock/manager').create(gconf);
return mng;
}
var mini = loadManager(gconf);
var mega = {};
// optional
if (mini.defaults) {
mega.defaults = function(opts) {
return mini.defaults(opts);
};
} else {
mega.defaults = m().defaults;
}
// optional
if (mini.remove) {
mega.remove = function(opts) {
return mini.remove(opts);
};
} else {
mega.remove = function(opts) {
mega.get(opts).then(function(site) {
if (!site) {
return null;
}
site.deletedAt = Date.now();
return mega.set(site).then(function() {
return site;
});
});
};
}
if (mini.find) {
// without this there cannot be fully automatic renewal
mega.find = function(opts) {
return mini.find(opts);
};
}
// set and (find and/or get) should be from the same set
if (mini.set) {
mega.set = function(opts) {
if (!mini.find) {
// TODO create the list so that find can be implemented
}
return mini.set(opts);
};
} else {
mega.set = m().set;
mega.get = m().get;
}
if (mini.get) {
mega.get = async function(opts) {
if (mini.set) {
return mini.get(opts);
}
if (!mega._get) {
mega._get = m().get;
}
var existing = await mega._get(opts);
var site = await mini.get(opts);
if (!existing) {
// Add
if (!site) {
return;
}
site.renewAt = 1;
site.deletedAt = 0;
await mega.set(site);
existing = await mega._get(opts);
} else if (!site) {
// Delete
existing.deletedAt = site.deletedAt || Date.now();
await mega.set(existing);
existing = null;
} else if (
site.subject !== existing.subject ||
site.altnames.join(' ') !== existing.altnames.join(' ')
) {
// Update
site.renewAt = 1;
site.deletedAt = 0;
await mega.set(site);
existing = await mega._get(opts);
if (!existing) {
throw new Error('failed to `get` after `set`');
}
}
return existing;
};
} else if (mini.find) {
mega.get = function(opts) {
var servername = opts.servername;
delete opts.servername;
opts.servernames = (servername && [servername]) || undefined;
return mini.find(opts).then(function(sites) {
return sites.filter(function(site) {
return site.altnames.include(servername);
})[0];
});
};
} else if (mini.set) {
throw new Error(
gconf.manager.module +
' implements `set()`, but not `get()` or `find()`'
);
} else {
mega.find = m().find;
mega.get = m().get;
}
if (!mega.find) {
mega._nofind = false;
mega.find = async function(opts) {
if (!mega._nofind) {
console.warn(
'Warning: manager `' +
greenlock.manager._modulename +
'` does not implement `find({})`\n'
);
mega._nofind = true;
}
return [];
};
}
if (!mega.get) {
mega.get = function(opts) {
var servername = opts.servername;
delete opts.servername;
opts.servernames = (servername && [servername]) || undefined;
return mega.find(opts).then(function(sites) {
return sites.filter(function(site) {
return site.altnames.include(servername);
})[0];
});
};
}
mega.init = function(deps) {
if (mini.init) {
return mini.init(deps).then(function() {
if (mng) {
return mng.init(deps);
}
});
} else if (mng) {
return mng.init(deps);
} else {
return Promise.resolve(null);
}
};
return mega;
}
function _mangleFindArgs(args) {
var servernames = (args.servernames || [])
.concat(args.altnames || [])
.filter(Boolean)
.slice(0);
var modified = servernames.slice(0);
// servername, wildname, and altnames are all the same
['wildname', 'servername'].forEach(function(k) {
var altname = args[k] || '';
if (altname && !modified.includes(altname)) {
modified.push(altname);
}
});
if (modified.length) {
servernames = modified;
servernames = servernames.map(U._encodeName);
args.altnames = servernames;
args.servernames = args.altnames = checkAltnames(false, args);
}
// documented as args.servernames
// preserved as args.altnames for v3 beta backwards compat
// my only hesitancy in this choice is that a "servername"
// may NOT contain '*.', in which case `altnames` is a better choice.
// However, `altnames` is ambiguous - as if it means to find a
// certificate by that specific collection of altnames.
// ... perhaps `domains` could work?
return args;
}

View File

@ -1,77 +0,0 @@
'use strict';
var Rc = module.exports;
var fs = require('fs');
var path = require('path');
// This is only called if packageRoot is specified
// (which it should be most of the time)
Rc._initSync = function(dirname, manager, configDir) {
if (!dirname) {
return {};
}
// dirname / opts.packageRoot
var rcpath = path.resolve(dirname, '.greenlockrc');
var rc;
try {
rc = JSON.parse(fs.readFileSync(rcpath));
} catch (e) {
if ('ENOENT' !== e.code) {
throw e;
}
rc = {};
}
var changed = true;
// In the general case the manager should be specified in the
// config file, which is in the config dir, but for the specific
// case in which all custom plugins are being used and no config
// dir is needed, we allow the manager to be read from the rc.
// ex: manager: { module: 'name', xxxx: 'xxxx' }
if (manager) {
if (rc.manager) {
if (
('string' === typeof rc.manager && rc.manager !== manager) ||
('string' !== typeof rc.manager &&
rc.manager.module !== manager.module)
) {
changed = true;
console.info(
"changing `manager` from '%s' to '%s'",
rc.manager.module || rc.manager,
manager.module || manager
);
}
}
rc.manager = manager;
}
if (!configDir) {
configDir = rc.configDir;
}
if (configDir && configDir !== rc.configDir) {
if (rc.configDir) {
console.info(
"changing `configDir` from '%s' to '%s'",
rc.configDir,
configDir
);
}
changed = true;
rc.configDir = configDir;
} else if (!rc.configDir) {
changed = true;
configDir = './greenlock.d';
rc.configDir = configDir;
}
if (changed) {
fs.writeFileSync(rcpath, JSON.stringify(rc));
}
return rc;
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

180
order.js
View File

@ -1,95 +1,97 @@
var accountKeypair = await Keypairs.generate({ kty: accKty });
if (config.debug) {
console.info('Account Key Created');
console.info(JSON.stringify(accountKeypair, null, 2));
console.info();
console.info();
}
var accountKeypair = await Keypairs.generate({ kty: accKty });
if (config.debug) {
console.info('Account Key Created');
console.info(JSON.stringify(accountKeypair, null, 2));
console.info();
console.info();
}
var account = await acme.accounts.create({
agreeToTerms: agree,
// TODO detect jwk/pem/der?
accountKeypair: { privateKeyJwk: accountKeypair.private },
subscriberEmail: config.email
});
var account = await acme.accounts.create({
agreeToTerms: agree,
// TODO detect jwk/pem/der?
accountKeypair: { privateKeyJwk: accountKeypair.private },
subscriberEmail: config.email
});
// TODO top-level agree
function agree(tos) {
if (config.debug) {
console.info('Agreeing to Terms of Service:');
console.info(tos);
console.info();
console.info();
}
agreed = true;
return Promise.resolve(tos);
}
if (config.debug) {
console.info('New Subscriber Account');
console.info(JSON.stringify(account, null, 2));
console.info();
console.info();
}
if (!agreed) {
throw new Error('Failed to ask the user to agree to terms');
}
// TODO top-level agree
function agree(tos) {
if (config.debug) {
console.info('Agreeing to Terms of Service:');
console.info(tos);
console.info();
console.info();
}
agreed = true;
return Promise.resolve(tos);
}
if (config.debug) {
console.info('New Subscriber Account');
console.info(JSON.stringify(account, null, 2));
console.info();
console.info();
}
if (!agreed) {
throw new Error('Failed to ask the user to agree to terms');
}
var certKeypair = await Keypairs.generate({ kty: srvKty });
var pem = await Keypairs.export({
jwk: certKeypair.private,
encoding: 'pem'
});
if (config.debug) {
console.info('Server Key Created');
console.info('privkey.jwk.json');
console.info(JSON.stringify(certKeypair, null, 2));
// This should be saved as `privkey.pem`
console.info();
console.info('privkey.' + srvKty.toLowerCase() + '.pem:');
console.info(pem);
console.info();
}
var certKeypair = await Keypairs.generate({ kty: srvKty });
var pem = await Keypairs.export({
jwk: certKeypair.private,
encoding: 'pem'
});
if (config.debug) {
console.info('Server Key Created');
console.info('privkey.jwk.json');
console.info(JSON.stringify(certKeypair, null, 2));
// This should be saved as `privkey.pem`
console.info();
console.info('privkey.' + srvKty.toLowerCase() + '.pem:');
console.info(pem);
console.info();
}
// 'subject' should be first in list
var domains = randomDomains(rnd);
if (config.debug) {
console.info('Get certificates for random domains:');
console.info(
domains
.map(function(puny) {
var uni = punycode.toUnicode(puny);
if (puny !== uni) {
return puny + ' (' + uni + ')';
}
return puny;
})
.join('\n')
);
console.info();
}
// 'subject' should be first in list
var domains = randomDomains(rnd);
if (config.debug) {
console.info('Get certificates for random domains:');
console.info(
domains
.map(function(puny) {
var uni = punycode.toUnicode(puny);
if (puny !== uni) {
return puny + ' (' + uni + ')';
}
return puny;
})
.join('\n')
);
console.info();
}
// Create CSR
var csrDer = await CSR.csr({
jwk: certKeypair.private,
domains: domains,
encoding: 'der'
});
var csr = Enc.bufToUrlBase64(csrDer);
var csrPem = PEM.packBlock({
type: 'CERTIFICATE REQUEST',
bytes: csrDer /* { jwk: jwk, domains: opts.domains } */
});
if (config.debug) {
console.info('Certificate Signing Request');
console.info(csrPem);
console.info();
}
var results = await acme.certificates.create({
account: account,
accountKeypair: { privateKeyJwk: accountKeypair.private },
csr: csr,
domains: domains,
challenges: challenges, // must be implemented
customerEmail: null
});
// Create CSR
var csrDer = await CSR.csr({
jwk: certKeypair.private,
domains: domains,
encoding: 'der'
});
var csr = Enc.bufToUrlBase64(csrDer);
var csrPem = PEM.packBlock({
type: 'CERTIFICATE REQUEST',
bytes: csrDer /* { jwk: jwk, domains: opts.domains } */
});
if (config.debug) {
console.info('Certificate Signing Request');
console.info(csrPem);
console.info();
}
var results = await acme.certificates.create({
account: account,
accountKeypair: { privateKeyJwk: accountKeypair.private },
csr: csr,
domains: domains,
challenges: challenges, // must be implemented
customerEmail: null
});

258
package-lock.json generated
View File

@ -1,140 +1,122 @@
{
"name": "@root/greenlock",
"version": "4.0.5",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@greenlock/manager": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@greenlock/manager/-/manager-3.1.0.tgz",
"integrity": "sha512-PBy5CMK+j4oD7sj7hF5qE+xKEOSiiuL2hHd5X5ttEbtnTSDKjNeqbrR5k2ZddwVNdjOVeBIeuqlm81IFZ+Ftew==",
"requires": {
"greenlock-manager-fs": "^3.1.0"
},
"dependencies": {
"greenlock-manager-fs": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/greenlock-manager-fs/-/greenlock-manager-fs-3.1.1.tgz",
"integrity": "sha512-np6qdnPIOZx40PAcSQcqK1eMPWjTKxsxcgRd/OVg0ai49WC1Ds74CTrwmB84pq2n53ikbnDBQFmKEQ4AC0DK8w==",
"requires": {
"@root/mkdirp": "^1.0.0",
"safe-replace": "^1.1.0"
}
}
}
},
"@root/acme": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@root/acme/-/acme-3.1.0.tgz",
"integrity": "sha512-GAyaW63cpSYd2KvVp5lHLbCWeEhJPKZK9nsJvZJOKsD9Uv88KEttn4FpDZEJ+2q3Jsey0DWpuQ2I4ft0JV9p2w==",
"requires": {
"@root/csr": "^0.8.1",
"@root/encoding": "^1.0.1",
"@root/keypairs": "^0.10.0",
"@root/pem": "^1.0.4",
"@root/request": "^1.6.1",
"@root/x509": "^0.7.2"
},
"dependencies": {
"@root/request": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.6.1.tgz",
"integrity": "sha512-8wrWyeBLRp7T8J36GkT3RODJ6zYmL0/maWlAUD5LOXT28D3TDquUepyYDKYANNA3Gc8R5ZCgf+AXvSTYpJEWwQ=="
}
}
},
"@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/keypairs": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@root/keypairs/-/keypairs-0.10.0.tgz",
"integrity": "sha512-t8VocY46Mtb0NTsxzyLLf5tsgfw0BXLYVADAyiRdEdqHcvPFGJdjkXNtHVQuSV/FMaC65iTOHVP4E6X8iT3Ikg==",
"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.6.1",
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.6.1.tgz",
"integrity": "sha512-8wrWyeBLRp7T8J36GkT3RODJ6zYmL0/maWlAUD5LOXT28D3TDquUepyYDKYANNA3Gc8R5ZCgf+AXvSTYpJEWwQ=="
},
"@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=="
},
"dotenv": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==",
"dev": true
},
"greenlock-store-fs": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/greenlock-store-fs/-/greenlock-store-fs-3.2.2.tgz",
"integrity": "sha512-92ejLB4DyV4qv/2b6VLGF2nKfYQeIfg3o+e/1cIoYLjlIaUFdbBXkzLTRozFlHsQPZt2ALi5qYrpC9IwH7GK8A==",
"requires": {
"@root/mkdirp": "^1.0.0",
"safe-replace": "^1.1.0"
}
},
"punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
"dev": true
},
"safe-replace": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-replace/-/safe-replace-1.1.0.tgz",
"integrity": "sha512-9/V2E0CDsKs9DWOOwJH7jYpSl9S3N05uyevNjvsnDauBqRowBPOyot1fIvV5N2IuZAbYyvrTXrYFVG0RZInfFw=="
}
}
"name": "@root/greenlock",
"version": "3.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@root/acme": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@root/acme/-/acme-3.0.6.tgz",
"integrity": "sha512-KfgwcyWDsT90vz+gmWbCwuOBolwV5Gcg0WHsG8/dznDC7a6QF4AmZsil7mIWKGJxHdi6MElkyrHZyK53OhPnug==",
"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/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.3.11",
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.3.11.tgz",
"integrity": "sha512-3a4Eeghcjsfe6zh7EJ+ni1l8OK9Fz2wL1OjP4UCa0YdvtH39kdXB9RGWuzyNv7dZi0+Ffkc83KfH0WbPMiuJFw=="
},
"@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.0",
"resolved": "https://registry.npmjs.org/acme-http-01-standalone/-/acme-http-01-standalone-3.0.0.tgz",
"integrity": "sha512-lZqVab2UZ1Dp36HemfhGEvdYOcVNg5wyVXNjtPUqGSAOVUOKqwi3gDrTGwqz+FBrEEEEpTngDPaZn2g3hfmPLA=="
},
"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=="
},
"dotenv": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==",
"dev": true
},
"greenlock-manager-fs": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/greenlock-manager-fs/-/greenlock-manager-fs-0.6.0.tgz",
"integrity": "sha512-o5RZ/T4j6eaUXCVnZ2dScE1pAjFuS8/R4ZDn7mCyRkxBjZvXJU7TpYe5Bc/wmN8x+gLHqVUECZWC6VdA/DbShQ==",
"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"
}
},
"punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
"dev": true
},
"safe-replace": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-replace/-/safe-replace-1.1.0.tgz",
"integrity": "sha512-9/V2E0CDsKs9DWOOwJH7jYpSl9S3N05uyevNjvsnDauBqRowBPOyot1fIvV5N2IuZAbYyvrTXrYFVG0RZInfFw=="
}
}
}

View File

@ -1,56 +1,53 @@
{
"name": "@root/greenlock",
"version": "4.0.5",
"description": "The easiest Let's Encrypt client for Node.js and Browsers",
"homepage": "https://rootprojects.org/greenlock/",
"main": "greenlock.js",
"browser": {},
"bin": {
"greenlock": "bin/greenlock.js"
},
"files": [
"*.js",
"lib",
"bin",
"dist"
],
"scripts": {
"build": "nodex bin/bundle.js",
"lint": "jshint lib bin",
"test": "node server.js",
"start": "node server.js"
},
"repository": {
"type": "git",
"url": "https://git.rootprojects.org/root/greenlock.js.git"
},
"keywords": [
"Let's Encrypt",
"ACME",
"browser",
"EC",
"RSA",
"CSR",
"greenlock",
"VanillaJS",
"ZeroSSL"
],
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"license": "MPL-2.0",
"dependencies": {
"@greenlock/manager": "^3.1.0",
"@root/acme": "^3.1.0",
"@root/csr": "^0.8.1",
"@root/keypairs": "^0.10.0",
"@root/mkdirp": "^1.0.0",
"@root/request": "^1.6.1",
"acme-http-01-standalone": "^3.0.5",
"cert-info": "^1.5.1",
"greenlock-store-fs": "^3.2.2",
"safe-replace": "^1.1.0"
},
"devDependencies": {
"dotenv": "^8.2.0",
"punycode": "^1.4.1"
}
"name": "@root/greenlock",
"version": "3.0.1",
"description": "The easiest Let's Encrypt client for Node.js and Browsers",
"homepage": "https://rootprojects.org/greenlock/",
"main": "greenlock.js",
"browser": {},
"files": [
"*.js",
"lib",
"bin",
"dist"
],
"scripts": {
"build": "nodex bin/bundle.js",
"lint": "jshint lib bin",
"test": "node server.js",
"start": "node server.js"
},
"repository": {
"type": "git",
"url": "https://git.rootprojects.org/root/greenlock.js.git"
},
"keywords": [
"Let's Encrypt",
"ACME",
"browser",
"EC",
"RSA",
"CSR",
"greenlock",
"VanillaJS",
"ZeroSSL"
],
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"license": "MPL-2.0",
"dependencies": {
"@root/acme": "^3.0.6",
"@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.0",
"cert-info": "^1.5.1",
"greenlock-manager-fs": "^0.6.0",
"greenlock-store-fs": "^3.2.0",
"safe-replace": "^1.1.0"
},
"devDependencies": {
"dotenv": "^8.2.0",
"punycode": "^1.4.1"
}
}

View File

@ -4,338 +4,152 @@ var P = module.exports;
var spawn = require('child_process').spawn;
var spawnSync = require('child_process').spawnSync;
var promisify = require('util').promisify;
// Exported for CLIs and such to override
P.PKG_DIR = __dirname;
P._loadStore = function(storeConf) {
return P._loadHelper(storeConf.module).then(function(plugin) {
return P._normalizeStore(storeConf.module, plugin.create(storeConf));
});
};
P._loadChallenge = function(chConfs, typ01) {
return P._loadHelper(chConfs[typ01].module).then(function(plugin) {
var ch = P._normalizeChallenge(
chConfs[typ01].module,
plugin.create(chConfs[typ01])
);
ch._type = typ01;
return ch;
});
};
P._loadHelper = function(modname) {
try {
return Promise.resolve(require(modname));
} catch (e) {
console.error("Could not load '%s'", modname);
console.error('Did you install it?');
console.error('\tnpm install --save %s', modname);
e.context = 'load_plugin';
throw e;
// Fun experiment, bad idea
/*
P._load = function(modname) {
try {
return Promise.resolve(require(modname));
} catch (e) {
return P._install(modname).then(function() {
return require(modname);
});
*/
}
};
P._normalizeStore = function(name, store) {
var acc = store.accounts;
var crt = store.certificates;
var warned = false;
function warn() {
if (warned) {
return;
}
warned = true;
console.warn(
"'" +
name +
"' may have incorrect function signatures, or contains deprecated use of callbacks"
);
}
// accs
if (acc.check && 2 === acc.check.length) {
warn();
acc._thunk_check = acc.check;
acc.check = promisify(acc._thunk_check);
}
if (acc.set && 3 === acc.set.length) {
warn();
acc._thunk_set = acc.set;
acc.set = promisify(acc._thunk_set);
}
if (2 === acc.checkKeypair.length) {
warn();
acc._thunk_checkKeypair = acc.checkKeypair;
acc.checkKeypair = promisify(acc._thunk_checkKeypair);
}
if (3 === acc.setKeypair.length) {
warn();
acc._thunk_setKeypair = acc.setKeypair;
acc.setKeypair = promisify(acc._thunk_setKeypair);
}
// certs
if (2 === crt.check.length) {
warn();
crt._thunk_check = crt.check;
crt.check = promisify(crt._thunk_check);
}
if (3 === crt.set.length) {
warn();
crt._thunk_set = crt.set;
crt.set = promisify(crt._thunk_set);
}
if (2 === crt.checkKeypair.length) {
warn();
crt._thunk_checkKeypair = crt.checkKeypair;
crt.checkKeypair = promisify(crt._thunk_checkKeypair);
}
if (2 === crt.setKeypair.length) {
warn();
crt._thunk_setKeypair = crt.setKeypair;
crt.setKeypair = promisify(crt._thunk_setKeypair);
}
return store;
};
P._normalizeChallenge = function(name, ch) {
var gch = {};
var warned = false;
function warn() {
if (warned) {
return;
}
warned = true;
console.warn(
"'" +
name +
"' may have incorrect function signatures, or contains deprecated use of callbacks"
);
}
var warned2 = false;
function warn2() {
if (warned2) {
return;
}
warned2 = true;
console.warn(
"'" +
name +
"' did not return a Promise when called. This should be fixed by the maintainer."
);
}
function wrappy(fn) {
return function(_params) {
return Promise.resolve().then(function() {
var result = fn.call(ch, _params);
if (!result || !result.then) {
warn2();
}
return result;
});
};
}
// init, zones, set, get, remove, propagationDelay
if (ch.init) {
if (2 === ch.init.length) {
warn();
ch._thunk_init = ch.init;
ch.init = promisify(ch._thunk_init);
}
gch.init = wrappy(ch.init);
}
if (ch.zones) {
if (2 === ch.zones.length) {
warn();
ch._thunk_zones = ch.zones;
ch.zones = promisify(ch._thunk_zones);
}
gch.zones = wrappy(ch.zones);
}
if (2 === ch.set.length) {
warn();
ch._thunk_set = ch.set;
ch.set = promisify(ch._thunk_set);
}
gch.set = wrappy(ch.set);
if (2 === ch.remove.length) {
warn();
ch._thunk_remove = ch.remove;
ch.remove = promisify(ch._thunk_remove);
}
gch.remove = wrappy(ch.remove);
if (ch.get) {
if (2 === ch.get.length) {
warn();
ch._thunk_get = ch.get;
ch.get = promisify(ch._thunk_get);
}
gch.get = wrappy(ch.get);
}
if("number" === typeof ch.propagationDelay) {
gch.propagationDelay = ch.propagationDelay;
}
return gch;
}
};
P._loadSync = function(modname) {
try {
return require(modname);
} catch (e) {
console.error("Could not load '%s'", modname);
console.error('Did you install it?');
console.error('\tnpm install --save %s', modname);
e.context = 'load_plugin';
throw e;
}
/*
var mod;
try {
mod = require(modname);
} catch (e) {
P._installSync(modname);
mod = require(modname);
}
*/
return mod;
};
P._installSync = function(moduleName) {
try {
return require(moduleName);
} catch (e) {
// continue
}
var npm = 'npm';
var args = ['install', '--save', moduleName];
var out = '';
var cmd;
var npm = 'npm';
var args = ['install', '--save', moduleName];
var out = '';
var cmd;
try {
cmd = spawnSync(npm, args, {
cwd: P.PKG_DIR,
windowsHide: true
});
} catch (e) {
console.error(
"Failed to start: '" +
npm +
' ' +
args.join(' ') +
"' in '" +
P.PKG_DIR +
"'"
);
console.error(e.message);
process.exit(1);
}
try {
cmd = spawnSync(npm, args, {
cwd: P.PKG_DIR,
windowsHide: true
});
} catch (e) {
console.error(
"Failed to start: '" +
npm +
' ' +
args.join(' ') +
"' in '" +
P.PKG_DIR +
"'"
);
console.error(e.message);
process.exit(1);
}
if (!cmd.status) {
return;
}
if (!cmd.status) {
return;
}
out += cmd.stdout.toString('utf8');
out += cmd.stderr.toString('utf8');
out += cmd.stdout.toString('utf8');
out += cmd.stderr.toString('utf8');
if (out) {
console.error(out);
console.error();
console.error();
}
if (out) {
console.error(out);
console.error();
console.error();
}
console.error(
"Failed to run: '" +
npm +
' ' +
args.join(' ') +
"' in '" +
P.PKG_DIR +
"'"
);
console.error(
"Failed to run: '" +
npm +
' ' +
args.join(' ') +
"' in '" +
P.PKG_DIR +
"'"
);
console.error(
'Try for yourself:\n\tcd ' + P.PKG_DIR + '\n\tnpm ' + args.join(' ')
);
console.error(
'Try for yourself:\n\tcd ' + P.PKG_DIR + '\n\tnpm ' + args.join(' ')
);
process.exit(1);
process.exit(1);
};
P._install = function(moduleName) {
return new Promise(function(resolve) {
if (!moduleName) {
throw new Error('no module name given');
}
return new Promise(function(resolve) {
if (!moduleName) {
throw new Error('no module name given');
}
var npm = 'npm';
var args = ['install', '--save', moduleName];
var out = '';
var cmd = spawn(npm, args, {
cwd: P.PKG_DIR,
windowsHide: true
});
var npm = 'npm';
var args = ['install', '--save', moduleName];
var out = '';
var cmd = spawn(npm, args, {
cwd: P.PKG_DIR,
windowsHide: true
});
cmd.stdout.on('data', function(chunk) {
out += chunk.toString('utf8');
});
cmd.stdout.on('data', function(chunk) {
out += chunk.toString('utf8');
});
cmd.stdout.on('data', function(chunk) {
out += chunk.toString('utf8');
});
cmd.stdout.on('data', function(chunk) {
out += chunk.toString('utf8');
});
cmd.on('error', function(e) {
console.error(
"Failed to start: '" +
npm +
' ' +
args.join(' ') +
"' in '" +
P.PKG_DIR +
"'"
);
console.error(e.message);
process.exit(1);
});
cmd.on('error', function(e) {
console.error(
"Failed to start: '" +
npm +
' ' +
args.join(' ') +
"' in '" +
P.PKG_DIR +
"'"
);
console.error(e.message);
process.exit(1);
});
cmd.on('exit', function(code) {
if (!code) {
resolve();
return;
}
cmd.on('exit', function(code) {
if (!code) {
resolve();
return;
}
if (out) {
console.error(out);
console.error();
console.error();
}
console.error(
"Failed to run: '" +
npm +
' ' +
args.join(' ') +
"' in '" +
P.PKG_DIR +
"'"
);
console.error(
'Try for yourself:\n\tcd ' +
P.PKG_DIR +
'\n\tnpm ' +
args.join(' ')
);
process.exit(1);
});
});
if (out) {
console.error(out);
console.error();
console.error();
}
console.error(
"Failed to run: '" +
npm +
' ' +
args.join(' ') +
"' in '" +
P.PKG_DIR +
"'"
);
console.error(
'Try for yourself:\n\tcd ' +
P.PKG_DIR +
'\n\tnpm ' +
args.join(' ')
);
process.exit(1);
});
});
};
if (require.main === module) {
P._installSync(process.argv[2]);
P._installSync(process.argv[2]);
}

View File

@ -1,31 +0,0 @@
#!/bin/bash
set -e
# TODO notify if wildcard is selected and no dns challenge is present
node bin/greenlock.js add --subject example.com --altnames 'example.com,*.example.com'
node bin/greenlock.js update --subject example.com
node bin/greenlock.js config --subject example.com
node bin/greenlock.js config --subject *.example.com
node bin/greenlock.js defaults
node bin/greenlock.js defaults --account-key-type
node bin/greenlock.js defaults
# using --challenge-xx-xx-xxx is additive
node bin/greenlock.js defaults --challenge-dns-01 foo-http-01-bar --challenge-dns-01-token BIG_TOKEN
# using --challenge is exclusive (will delete things not mentioned)
node bin/greenlock.js defaults --challenge acme-http-01-standalone
# should delete all and add just this one anew
node bin/greenlock.js update --subject example.com --challenge bar-http-01-baz
# should add, leaving the existing
node bin/greenlock.js update --subject example.com --challenge-dns-01 baz-dns-01-qux --challenge-dns-01-token BIG_TOKEN
# should delete all and add just this one anew
node bin/greenlock.js update --subject example.com --challenge bar-http-01-baz
node bin/greenlock.js remove --subject example.com
# TODO test for failure
# node bin/greenlock.js add --subject example.com
# node bin/greenlock.js add --subject example --altnames example
# node bin/greenlock.js add --subject example.com --altnames '*.example.com'
# node bin/greenlock.js add --subject example.com --altnames '*.example.com,example.com'
# node bin/greenlock.js update --altnames example.com
# node bin/greenlock.js config foo.example.com

View File

@ -2,6 +2,7 @@
require('dotenv').config();
var path = require('path');
var Greenlock = require('../');
var subject = process.env.BASE_DOMAIN;
@ -11,44 +12,32 @@ var challenge = JSON.parse(process.env.CHALLENGE_OPTIONS);
challenge.module = process.env.CHALLENGE_PLUGIN;
var greenlock = Greenlock.create({
packageAgent: 'Greenlock_Test/v0',
maintainerEmail: email,
staging: true,
manager: require('greenlock-manager-fs').create({
//configFile: '~/.config/greenlock/certs.json',
})
agreeTos: true,
maintainerEmail: email,
staging: true,
manager: path.join(__dirname, 'manager.js'),
challenges: {
'dns-01': challenge
}
//configFile: '~/.config/greenlock/certs.json',
//challenges: challenges,
//store: args.storeOpts,
//renewOffset: args.renewOffset || '30d',
//renewStagger: '1d'
});
greenlock.manager
.defaults({
agreeToTerms: true,
subscriberEmail: email,
challenges: {
'dns-01': challenge
}
//store: args.storeOpts,
//renewOffset: args.renewOffset || '30d',
//renewStagger: '1d'
})
.then(function() {
return greenlock
.add({
subject: subject,
altnames: altnames,
subscriberEmail: email
})
.then(function() {
return greenlock
.get({ servername: subject })
.then(function(pems) {
if (pems && pems.privkey && pems.cert && pems.chain) {
console.info('Success');
}
//console.log(pems);
});
});
})
.catch(function(e) {
console.error('Big bad error:', e.code);
console.error(e);
greenlock
.add({
subject: subject,
altnames: altnames,
subscriberEmail: email
})
.then(function() {
return greenlock.renew().then(function (pems) {
console.info(pems);
});
})
.catch(function(e) {
console.error('yo', e.code);
console.error(e);
});

View File

@ -3,5 +3,5 @@
var UserEvents = module.exports;
UserEvents.notify = function() {
// TODO not implemented yet
// TODO not implemented yet
};

402
utils.js
View File

@ -11,79 +11,79 @@ var Keypairs = require('@root/keypairs');
var certParser = require('cert-info');
U._parseDuration = function(str) {
if ('number' === typeof str) {
return str;
}
if ('number' === typeof str) {
return str;
}
var pattern = /^(\-?\d+(\.\d+)?)([wdhms]|ms)$/;
var matches = str.match(pattern);
if (!matches || !matches[0]) {
throw new Error('invalid duration string: ' + str);
}
var pattern = /^(\-?\d+(\.\d+)?)([wdhms]|ms)$/;
var matches = str.match(pattern);
if (!matches || !matches[0]) {
throw new Error('invalid duration string: ' + str);
}
var n = parseInt(matches[1], 10);
var unit = matches[3];
var n = parseInt(matches[1], 10);
var unit = matches[3];
switch (unit) {
case 'w':
n *= 7;
/*falls through*/
case 'd':
n *= 24;
/*falls through*/
case 'h':
n *= 60;
/*falls through*/
case 'm':
n *= 60;
/*falls through*/
case 's':
n *= 1000;
/*falls through*/
case 'ms':
n *= 1; // for completeness
}
switch (unit) {
case 'w':
n *= 7;
/*falls through*/
case 'd':
n *= 24;
/*falls through*/
case 'h':
n *= 60;
/*falls through*/
case 'm':
n *= 60;
/*falls through*/
case 's':
n *= 1000;
/*falls through*/
case 'ms':
n *= 1; // for completeness
}
return n;
return n;
};
U._encodeName = function(str) {
return punycode.toASCII(str.toLowerCase(str));
return punycode.toASCII(str.toLowerCase(str));
};
U._validName = function(str) {
// A quick check of the 38 and two ½ valid characters
// 253 char max full domain, including dots
// 63 char max each label segment
// Note: * is not allowed, but it's allowable here
// Note: _ (underscore) is only allowed for "domain names", not "hostnames"
// Note: - (hyphen) is not allowed as a first character (but a number is)
return (
/^(\*\.)?[a-z0-9_\.\-]+\.[a-z0-9_\.\-]+$/.test(str) &&
str.length < 254 &&
str.split('.').every(function(label) {
return label.length > 0 && label.length < 64;
})
);
// A quick check of the 38 and two ½ valid characters
// 253 char max full domain, including dots
// 63 char max each label segment
// Note: * is not allowed, but it's allowable here
// Note: _ (underscore) is only allowed for "domain names", not "hostnames"
// Note: - (hyphen) is not allowed as a first character (but a number is)
return (
/^(\*\.)?[a-z0-9_\.\-]+$/.test(str) &&
str.length < 254 &&
str.split('.').every(function(label) {
return label.length > 0 && label.length < 64;
})
);
};
U._validMx = function(email) {
var host = email.split('@').slice(1)[0];
// try twice, just because DNS hiccups sometimes
// Note: we don't care if the domain exists, just that it *can* exist
return resolveMx(host).catch(function() {
return U._timeout(1000).then(function() {
return resolveMx(host);
});
});
var host = email.split('@').slice(1)[0];
// try twice, just because DNS hiccups sometimes
// Note: we don't care if the domain exists, just that it *can* exist
return resolveMx(host).catch(function() {
return U._timeout(1000).then(function() {
return resolveMx(host);
});
});
};
// should be called after _validName
U._validDomain = function(str) {
// TODO use @root/dns (currently dns-suite)
// because node's dns can't read Authority records
return Promise.resolve(str);
/*
// TODO use @root/dns (currently dns-suite)
// because node's dns can't read Authority records
return Promise.resolve(str);
/*
// try twice, just because DNS hiccups sometimes
// Note: we don't care if the domain exists, just that it *can* exist
return resolveSoa(str).catch(function() {
@ -98,184 +98,184 @@ U._validDomain = function(str) {
// should be called after _validName
// (which enforces *. or no *)
U._uniqueNames = function(altnames) {
var dups = {};
var wilds = {};
if (
altnames.some(function(w) {
if ('*.' !== w.slice(0, 2)) {
return;
}
if (wilds[w]) {
return true;
}
wilds[w] = true;
})
) {
return false;
}
var dups = {};
var wilds = {};
if (
altnames.some(function(w) {
if ('*.' !== w.slice(0, 2)) {
return;
}
if (wilds[w]) {
return true;
}
wilds[w] = true;
})
) {
return false;
}
return altnames.every(function(name) {
var w;
if ('*.' !== name.slice(0, 2)) {
w =
'*.' +
name
.split('.')
.slice(1)
.join('.');
} else {
return true;
}
return altnames.every(function(name) {
var w;
if ('*.' !== name.slice(0, 2)) {
w =
'*.' +
name
.split('.')
.slice(1)
.join('.');
} else {
return true;
}
if (!dups[name] && !dups[w]) {
dups[name] = true;
return true;
}
});
if (!dups[name] && !dups[w]) {
dups[name] = true;
return true;
}
});
};
U._timeout = function(d) {
return new Promise(function(resolve) {
setTimeout(resolve, d);
});
return new Promise(function(resolve) {
setTimeout(resolve, d);
});
};
U._genKeypair = function(keyType) {
var keyopts;
var len = parseInt(keyType.replace(/.*?(\d)/, '$1') || 0, 10);
if (/RSA/.test(keyType)) {
keyopts = {
kty: 'RSA',
modulusLength: len || 2048
};
} else if (/^(EC|P\-?\d)/i.test(keyType)) {
keyopts = {
kty: 'EC',
namedCurve: 'P-' + (len || 256)
};
} else {
// TODO put in ./errors.js
throw new Error('invalid key type: ' + keyType);
}
var keyopts;
var len = parseInt(keyType.replace(/.*?(\d)/, '$1') || 0, 10);
if (/RSA/.test(keyType)) {
keyopts = {
kty: 'RSA',
modulusLength: len || 2048
};
} else if (/^(EC|P\-?\d)/i.test(keyType)) {
keyopts = {
kty: 'EC',
namedCurve: 'P-' + (len || 256)
};
} else {
// TODO put in ./errors.js
throw new Error('invalid key type: ' + keyType);
}
return Keypairs.generate(keyopts).then(function(pair) {
return U._jwkToSet(pair.private);
});
return Keypairs.generate(keyopts).then(function(pair) {
return U._jwkToSet(pair.private);
});
};
// TODO use ACME._importKeypair ??
U._importKeypair = function(keypair) {
// this should import all formats equally well:
// 'object' (JWK), 'string' (private key pem), kp.privateKeyPem, kp.privateKeyJwk
if (keypair.private || keypair.d) {
return U._jwkToSet(keypair.private || keypair);
}
if (keypair.privateKeyJwk) {
return U._jwkToSet(keypair.privateKeyJwk);
}
// this should import all formats equally well:
// 'object' (JWK), 'string' (private key pem), kp.privateKeyPem, kp.privateKeyJwk
if (keypair.private || keypair.d) {
return U._jwkToSet(keypair.private || keypair);
}
if (keypair.privateKeyJwk) {
return U._jwkToSet(keypair.privateKeyJwk);
}
if ('string' !== typeof keypair && !keypair.privateKeyPem) {
// TODO put in errors
throw new Error('missing private key');
}
if ('string' !== typeof keypair && !keypair.privateKeyPem) {
// TODO put in errors
throw new Error('missing private key');
}
return Keypairs.import({ pem: keypair.privateKeyPem || keypair }).then(
function(priv) {
if (!priv.d) {
throw new Error('missing private key');
}
return U._jwkToSet(priv);
}
);
return Keypairs.import({ pem: keypair.privateKeyPem || keypair }).then(
function(priv) {
if (!priv.d) {
throw new Error('missing private key');
}
return U._jwkToSet(priv);
}
);
};
U._jwkToSet = function(jwk) {
var keypair = {
privateKeyJwk: jwk
};
return Promise.all([
Keypairs.export({
jwk: jwk,
encoding: 'pem'
}).then(function(pem) {
keypair.privateKeyPem = pem;
}),
Keypairs.export({
jwk: jwk,
encoding: 'pem',
public: true
}).then(function(pem) {
keypair.publicKeyPem = pem;
}),
Keypairs.publish({
jwk: jwk
}).then(function(pub) {
keypair.publicKeyJwk = pub;
})
]).then(function() {
return keypair;
});
var keypair = {
privateKeyJwk: jwk
};
return Promise.all([
Keypairs.export({
jwk: jwk,
encoding: 'pem'
}).then(function(pem) {
keypair.privateKeyPem = pem;
}),
Keypairs.export({
jwk: jwk,
encoding: 'pem',
public: true
}).then(function(pem) {
keypair.publicKeyPem = pem;
}),
Keypairs.publish({
jwk: jwk
}).then(function(pub) {
keypair.publicKeyJwk = pub;
})
]).then(function() {
return keypair;
});
};
U._attachCertInfo = function(results) {
var certInfo = certParser.info(results.cert);
var certInfo = certParser.info(results.cert);
// subject, altnames, issuedAt, expiresAt
Object.keys(certInfo).forEach(function(key) {
results[key] = certInfo[key];
});
// subject, altnames, issuedAt, expiresAt
Object.keys(certInfo).forEach(function(key) {
results[key] = certInfo[key];
});
return results;
return results;
};
U._certHasDomain = function(certInfo, _domain) {
var names = (certInfo.altnames || []).slice(0);
return names.some(function(name) {
var domain = _domain.toLowerCase();
name = name.toLowerCase();
if ('*.' === name.substr(0, 2)) {
name = name.substr(2);
domain = domain
.split('.')
.slice(1)
.join('.');
}
return name === domain;
});
var names = (certInfo.altnames || []).slice(0);
return names.some(function(name) {
var domain = _domain.toLowerCase();
name = name.toLowerCase();
if ('*.' === name.substr(0, 2)) {
name = name.substr(2);
domain = domain
.split('.')
.slice(1)
.join('.');
}
return name === domain;
});
};
// a bit heavy to be labeled 'utils'... perhaps 'common' would be better?
U._getOrCreateKeypair = function(db, subject, query, keyType, mustExist) {
var exists = false;
return db
.checkKeypair(query)
.then(function(kp) {
if (kp) {
exists = true;
return U._importKeypair(kp);
}
var exists = false;
return db
.checkKeypair(query)
.then(function(kp) {
if (kp) {
exists = true;
return U._importKeypair(kp);
}
if (mustExist) {
// TODO put in errors
throw new Error(
'required keypair not found: ' +
(subject || '') +
' ' +
JSON.stringify(query)
);
}
if (mustExist) {
// TODO put in errors
throw new Error(
'required keypair not found: ' +
(subject || '') +
' ' +
JSON.stringify(query)
);
}
return U._genKeypair(keyType);
})
.then(function(keypair) {
return { exists: exists, keypair: keypair };
});
return U._genKeypair(keyType);
})
.then(function(keypair) {
return { exists: exists, keypair: keypair };
});
};
U._getKeypair = function(db, subject, query) {
return U._getOrCreateKeypair(db, subject, query, '', true).then(function(
result
) {
return result.keypair;
});
return U._getOrCreateKeypair(db, subject, query, '', true).then(function(
result
) {
return result.keypair;
});
};