Compare commits

...

31 Commits

Author SHA1 Message Date
AJ ONeal 6df8692385 v4.0.2: resolve config file when running out of project directory 2020-01-09 04:19:54 -07:00
AJ ONeal 8ab7ad25f2 ignore TODO files 2020-01-09 03:58:15 -07:00
AJ ONeal 8c11b56aa9 use explicit versions 2020-01-09 03:54:57 -07:00
AJ ONeal 18e39905ba minor typo fixes 2020-01-09 03:36:07 -07:00
AJ ONeal f913f8d193 wip: update docs 2019-11-30 16:52:29 -07:00
AJ ONeal 08743cc6ba wip: update docs (and helpers) 2019-11-30 16:52:16 -07:00
AJ ONeal bba2e2012d v4.0.0: simplify config files and dirs 2019-11-30 14:01:11 -07:00
AJ ONeal b99ce6b9a5 set configDir with init 2019-11-29 18:14:09 -07:00
AJ ONeal 63e6ff0b22 set directoryUrl for store 2019-11-29 17:25:21 -07:00
AJ ONeal c945da9b48 log whitespace and servername 2019-11-29 16:58:40 -07:00
AJ ONeal 1ce3964aab bugfixes for defaults 2019-11-19 02:32:48 -07:00
AJ ONeal e3153c5c8b bugfixes for defaults 2019-11-19 02:24:09 -07:00
AJ ONeal a5d0cde282 wip: more precise defaults 2019-11-19 01:39:05 -07:00
AJ ONeal 8e632aee63 wip: replace rc/init handling 2019-11-18 22:55:29 -07:00
AJ ONeal b8d30b2b91 wip: simpler config and defaults 2019-11-18 01:21:31 -07:00
AJ ONeal 2f29362693 wip: cleanup 2019-11-11 22:38:32 -07:00
AJ ONeal 71746ca759 sprinkle in some async/await 2019-11-06 14:29:50 -07:00
AJ ONeal 297b932db2 print all default values when setting them 2019-11-06 13:55:44 -07:00
AJ ONeal bd817d2a24 v3.1.5: quiet down 2019-11-05 15:49:46 -07:00
AJ ONeal b558c1f0ec v3.1.5: quiet down 2019-11-05 15:49:32 -07:00
AJ ONeal fcafde98be v3.1.4: bugfix basePath for issue #13 2019-11-05 15:38:17 -07:00
AJ ONeal 9ea7961bb6 v3.1.4: bugfix basePath for issue #13 2019-11-05 15:38:08 -07:00
AJ ONeal d6c7da17d8 v3.1.3: npm bump 2019-11-05 04:00:21 -07:00
AJ ONeal d57089f49c v3.1.2: bugfix cli output 2019-11-05 03:54:51 -07:00
AJ ONeal d47d03c980 update templates 2019-11-05 03:18:48 -07:00
AJ ONeal e3e7f18d4d add --agree-to-terms flag 2019-11-05 03:14:18 -07:00
AJ ONeal 8f3872e82e template improvements 2019-11-05 02:50:27 -07:00
AJ ONeal 4dc324f26f v3.1.0: add CLI and more reasonable defaults 2019-11-05 00:29:04 -07:00
AJ ONeal 5788abac1e default to local basePath 2019-11-05 00:28:14 -07:00
AJ ONeal 7313167ca0 allow for partial manager 2019-11-05 00:28:14 -07:00
AJ ONeal af3a2f621c allow for all configs with --all 2019-11-05 00:27:53 -07:00
28 changed files with 1842 additions and 1193 deletions

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
TODO.txt greenlock.json*
TODO*
link.sh link.sh
.env .env
.greenlockrc .greenlockrc

View File

@ -1,4 +1,36 @@
# Migrating from Greenlock v2 to v3 # 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. **Greenlock Express** uses Greenlock directly, the same as before.
@ -195,11 +227,11 @@ as well as a set of callbacks for easy configurability.
### Default Manager ### Default Manager
The default manager is `greenlock-manager-fs` and the default `configFile` is `~/.config/greenlock/manager.json`. The default manager is `@greenlock/manager` and the default `configDir` is `./.greenlock.d`.
The config file should look something like this: The config file should look something like this:
`~/.config/greenlock/manager.json`: `./greenlock.d/config.json`:
```json ```json
{ {
@ -256,29 +288,20 @@ The same is true with `greenlock-store-*` plugins:
### Customer Manager, the lazy way ### Customer Manager, the lazy way
At the very least you have to implement `find({ servername })`. At the very least you have to implement `get({ servername, wildname })`.
Since this is a very common use case, it's supported out of the box as part of the default manager plugin:
```js ```js
var greenlock = Greenlock.create({ var greenlock = Greenlock.create({
packageAgent: pkg.name + '/' + pkg.version, packageAgent: pkg.name + '/' + pkg.version,
maintainerEmail: 'jon@example.com', maintainerEmail: 'jon@example.com',
notify: notify, notify: notify,
find: find
packageRoot: __dirname,
manager: {
module: './manager.js'
}
}); });
// In the simplest case you can ignore all incoming options
// and return a single site config in the same format as the config file
function find(options) {
var servername = options.servername; // www.example.com
var wildname = options.wildname; // *.example.com
return Promise.resolve([
{ subject: 'example.com', altnames: ['example.com', 'www.example.com'] }
]);
}
function notify(ev, args) { function notify(ev, args) {
if ('error' === ev || 'warning' === ev) { if ('error' === ev || 'warning' === ev) {
console.error(ev, args); console.error(ev, args);
@ -288,102 +311,61 @@ function notify(ev, args) {
} }
``` ```
If you want to use wildcards or local domains, you must specify the `dns-01` challenge plugin to use: In the simplest case you can ignore all incoming options
and return a single site config in the same format as the config file
```js `./manager.js`:
function find(options) {
var subject = options.subject;
// may include wildcard
var altnames = options.altnames;
var wildname = options.wildname; // *.example.com
return Promise.resolve([
{
subject: 'example.com',
altnames: ['example.com', 'www.example.com'],
challenges: {
'dns-01': { module: 'acme-dns-01-namedotcom', apikey: 'xxxx' }
}
}
]);
}
```
### Customer Manager, complete
To use a fully custom manager, you give the npm package name, or absolute path to the file to load
```js
Greenlock.create({
// Greenlock Options
maintainerEmail: 'jon@example.com',
packageAgent: 'my-package/v2.1.1',
notify: notify,
// file path or npm package name
manager: '/path/to/manager.js',
// options that get passed to the manager
myFooOption: 'whatever'
});
```
The manager itself is, again relatively simple:
- find(options)
- set(siteConfig)
- remove(options)
- defaults(globalOptions) (as setter)
- defaults() => globalOptions (as getter)
`/path/to/manager.js`:
```js ```js
'use strict'; 'use strict';
module.exports.create = function() { module.exports.create = function() {
var manager = {}; return {
get: async function({ servername }) {
// do something to fetch the site
var site = {
subject: 'example.com',
altnames: ['example.com', 'www.example.com']
};
manager.find = async function({ subject, altnames, renewBefore }) { return site;
if (subject) {
return getSiteConfigBySubject(subject);
} }
if (altnames) {
// may include wildcards
return getSiteConfigByAnyAltname(altnames);
}
if (renewBefore) {
return getSiteConfigsWhereRenewAtIsLessThan(renewBefore);
}
return [];
};
manage.set = function(opts) {
// this is called by greenlock.add({ subject, altnames })
// it's also called by greenlock._update({ subject, renewAt })
return mergSiteConfig(subject, opts);
};
manage.remove = function({ subject, altname }) {
if (subject) {
return removeSiteConfig(subject);
}
return removeFromSiteConfigAndResetRenewAtToZero(altname);
};
// set the global config
manage.defaults = function(options) {
if (!options) {
return getGlobalConfig();
}
return mergeGlobalConfig(options);
}; };
}; };
``` ```
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 # ACME Challenge Plugins
The ACME challenge plugins are just a few simple callbacks: The ACME challenge plugins are just a few simple callbacks:
@ -419,99 +401,3 @@ They are described here:
- [greenlock store documentation](https://git.rootprojects.org/root/greenlock-store-test.js) - [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: If you are just implenting in-house and are not going to publish a module, you can also do some hack things like this:
### Custome Store, The hacky / lazy way
`/path/to/project/my-hacky-store.js`:
```js
'use strict';
module.exports.create = function(options) {
// ex: /path/to/account.ecdsa.jwk.json
var accountJwk = require(options.accountJwkPath);
// ex: /path/to/privkey.rsa.pem
var serverPem = fs.readFileSync(options.serverPemPath, 'ascii');
var accounts = {};
var certificates = {};
var store = { accounts, certificates };
// bare essential account callbacks
accounts.checkKeypair = function() {
// ignore all options and just return a single, global keypair
return Promise.resolve({
privateKeyJwk: accountJwk
});
};
accounts.setKeypair = function() {
// this will never get called if checkKeypair always returns
return Promise.resolve({});
};
// bare essential cert and key callbacks
certificates.checkKeypair = function() {
// ignore all options and just return a global server keypair
return {
privateKeyPem: serverPem
};
};
certificates.setKeypair = function() {
// never gets called if checkKeypair always returns an existing key
return Promise.resolve(null);
};
certificates.check = function(args) {
var subject = args.subject;
// make a database call or whatever to get a certificate
return goGetCertBySubject(subject).then(function() {
return {
pems: {
chain: '<PEM>',
cert: '<PEM>'
}
};
});
};
certificates.set = function(args) {
var subject = args.subject;
var cert = args.pems.cert;
var chain = args.pems.chain;
// make a database call or whatever to get a certificate
return goSaveCert({
subject,
cert,
chain
});
};
};
```
### Using the hacky / lazy store plugin
That sort of implementation won't pass the test suite, but it'll work just fine a use case where you only have one subscriber email (most of the time),
you only have one server key (not recommended, but works), and you only really want to worry about storing cetificates.
Then you could assign it as the default for all of your sites:
```json
{
"subscriberEmail": "jon@example.com",
"agreeToTerms": true,
"sites": {
"example.com": {
"subject": "example.com",
"altnames": ["example.com", "www.example.com"]
}
},
"store": {
"module": "/path/to/project/my-hacky-store.js",
"accountJwkPath": "/path/to/account.ecdsa.jwk.json",
"serverPemPath": "/path/to/privkey.rsa.pem"
}
}
```

View File

@ -1,12 +1,10 @@
# New Documentation &amp; [v2/v3 Migration Guide](https://git.rootprojects.org/root/greenlock.js/src/branch/v3/MIGRATION_GUIDE_V2_V3.md) # New Documentation &amp; [v4 Migration Guide](https://git.rootprojects.org/root/greenlock.js/src/branch/master/MIGRATION_GUIDE.md)
Greenlock v3 was just released from private beta **today** (Nov 1st, 2019).
We're still working on the full documentation for this new version, We're still working on the full documentation for this new version,
so please be patient. so please be patient.
To start, check out the To start, check out the
[Migration Guide](https://git.rootprojects.org/root/greenlock.js/src/branch/v3/MIGRATION_GUIDE_V2_V3.md). [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 Logo"](https://git.rootprojects.org/root/greenlock.js/raw/branch/master/logo/greenlock-1063x250.png 'Greenlock lock logo and work mark')
@ -85,12 +83,10 @@ Certificates are renewed every 45 days by default, and renewal checks will happe
var pkg = require('./package.json'); var pkg = require('./package.json');
var Greenlock = require('greenlock'); var Greenlock = require('greenlock');
var greenlock = Greenlock.create({ var greenlock = Greenlock.create({
configDir: './greenlock.d/config.json',
packageAgent: pkg.name + '/' + pkg.version, packageAgent: pkg.name + '/' + pkg.version,
maintainerEmail: pkg.author, maintainerEmail: pkg.author,
staging: true, staging: true,
manager: require('greenlock-manager-fs').create({
configFile: '~/.config/greenlock/manager.json'
}),
notify: function(event, details) { notify: function(event, details) {
if ('error' === event) { if ('error' === event) {
// `details` is an error object in this case // `details` is an error object in this case
@ -171,7 +167,7 @@ greenlock
--> -->
<details> <details>
<summary>Greenlock.create({ packageAgent, maintainerEmail, staging })</summary> <summary>Greenlock.create({ configDir, packageAgent, maintainerEmail, staging })</summary>
## Greenlock.create() ## Greenlock.create()
@ -181,12 +177,15 @@ Creates an instance of greenlock with _environment_-level values.
var pkg = require('./package.json'); var pkg = require('./package.json');
var gl = Greenlock.create({ var gl = Greenlock.create({
configDir: './greenlock.d/config.json',
// Staging for testing environments // Staging for testing environments
staging: true, staging: true,
// This should be the contact who receives critical bug and security notifications // 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 // Optionally, you may receive other (very few) updates, such as important new features
maintainerEmail: 'jon@example.com', maintainerEmail: 'jon@example.com',
// for an RFC 8555 / RFC 7231 ACME client user agent // for an RFC 8555 / RFC 7231 ACME client user agent
packageAgent: pkg.name + '/' pkg.version packageAgent: pkg.name + '/' pkg.version
}); });
@ -194,6 +193,7 @@ var gl = Greenlock.create({
| Parameter | Description | | Parameter | Description |
| --------------- | ------------------------------------------------------------------------------------ | | --------------- | ------------------------------------------------------------------------------------ |
| configDir | the directory to use for file-based plugins |
| maintainerEmail | the developer contact for critical bug and security notifications | | 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 | | 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 | | staging | use the Let's Encrypt staging URL instead of the production URL |
@ -402,8 +402,8 @@ Greenlock comes with reasonable defaults but when you install it,
you should also install any plugins that you need. you should also install any plugins that you need.
```bash ```bash
npm install --save @root/greenlock npm install --save @root/greenlock@v4
npm install --save greenlock-manager-fs npm install --save @greenlock/manager
npm install --save greenlock-store-fs npm install --save greenlock-store-fs
npm install --save acme-http-01-standalone npm install --save acme-http-01-standalone
``` ```
@ -464,7 +464,7 @@ 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. 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 }) - set({ subject, altnames, renewAt })
- find({ altnames, renewBefore }) - find({ servernames, renewBefore })
```js ```js
// should return a list of site configs: // should return a list of site configs:
[ [

View File

@ -117,7 +117,7 @@ A._newAccount = function(gnlck, mconf, db, acme, args, email, fullAccount) {
/^https?:\/\//i, /^https?:\/\//i,
'' ''
); );
*/ */
keyP = db.setKeypair(query, keypair); keyP = db.setKeypair(query, keypair);
} }
@ -145,7 +145,7 @@ A._newAccount = function(gnlck, mconf, db, acme, args, email, fullAccount) {
/^https?:\/\//i, /^https?:\/\//i,
'' ''
) )
*/ */
}, },
reg reg
); );

View File

@ -8,7 +8,7 @@ var cli = require('./lib/cli.js');
var Flags = require('./lib/flags.js'); var Flags = require('./lib/flags.js');
Flags.init().then(function({ flagOptions, rc, greenlock, mconf }) { Flags.init().then(function({ flagOptions, greenlock, mconf }) {
var myFlags = {}; var myFlags = {};
[ [
'subject', 'subject',
@ -34,11 +34,11 @@ Flags.init().then(function({ flagOptions, rc, greenlock, mconf }) {
cli.parse(myFlags); cli.parse(myFlags);
cli.main(function(argList, flags) { cli.main(function(argList, flags) {
Flags.mangleFlags(flags, mconf); Flags.mangleFlags(flags, mconf);
main(argList, flags, rc, greenlock); main(argList, flags, greenlock);
}, args); }, args);
}); });
async function main(_, flags, rc, greenlock) { async function main(_, flags, greenlock) {
if (!flags.subject || !flags.altnames) { if (!flags.subject || !flags.altnames) {
console.error( console.error(
'--subject and --altnames must be provided and should be valid domains' '--subject and --altnames must be provided and should be valid domains'

View File

@ -8,22 +8,22 @@ var cli = require('./lib/cli.js');
var Flags = require('./lib/flags.js'); var Flags = require('./lib/flags.js');
Flags.init().then(function({ flagOptions, rc, greenlock, mconf }) { Flags.init().then(function({ flagOptions, greenlock, mconf }) {
var myFlags = {}; var myFlags = {};
['all', 'subject', 'servername' /*, 'servernames', 'altnames'*/].forEach(function( ['all', 'subject', 'servername' /*, 'servernames', 'altnames'*/].forEach(
k function(k) {
) { myFlags[k] = flagOptions[k];
myFlags[k] = flagOptions[k]; }
}); );
cli.parse(myFlags); cli.parse(myFlags);
cli.main(function(argList, flags) { cli.main(function(argList, flags) {
Flags.mangleFlags(flags, mconf); Flags.mangleFlags(flags, mconf);
main(argList, flags, rc, greenlock); main(argList, flags, greenlock);
}, args); }, args);
}); });
async function main(_, flags, rc, greenlock) { async function main(_, flags, greenlock) {
var servernames = [flags.subject] var servernames = [flags.subject]
.concat([flags.servername]) .concat([flags.servername])
//.concat(flags.servernames) //.concat(flags.servernames)
@ -51,8 +51,15 @@ async function main(_, flags, rc, greenlock) {
} }
delete flags.servernames; delete flags.servernames;
greenlock var getter = function() {
._config(flags) return greenlock._config(flags);
};
if (flags.all) {
getter = function() {
return greenlock._configAll(flags);
};
}
return getter()
.catch(function(err) { .catch(function(err) {
console.error(); console.error();
console.error('error:', err.message); console.error('error:', err.message);
@ -60,19 +67,30 @@ async function main(_, flags, rc, greenlock) {
console.error(); console.error();
process.exit(1); process.exit(1);
}) })
.then(function(site) { .then(function(sites) {
if (!site) { if (!sites) {
console.info(); console.info();
console.info('No config found for', flags.servername); if (flags.all) {
console.info('No configs found');
} else {
console.info('No config found for', flags.servername);
}
console.info(); console.info();
process.exit(1); process.exit(1);
return; return;
} }
if (!Array.isArray(sites)) {
sites = [sites];
}
console.info(); sites.forEach(function(site) {
console.info( console.info();
'Config for ' + JSON.stringify(flags.servername) + ':' console.info(
); 'Config for ' +
console.info(JSON.stringify(site, null, 2)); JSON.stringify(flags.servername || site.subject) +
':'
);
console.info(JSON.stringify(site, null, 2));
});
}); });
} }

View File

@ -10,12 +10,12 @@ var Flags = require('./lib/flags.js');
Flags.init({ forceSave: true }).then(function({ Flags.init({ forceSave: true }).then(function({
flagOptions, flagOptions,
rc,
greenlock, greenlock,
mconf mconf
}) { }) {
var myFlags = {}; var myFlags = {};
[ [
'agree-to-terms',
'account-key-type', 'account-key-type',
'server-key-type', 'server-key-type',
'subscriber-email', 'subscriber-email',
@ -29,7 +29,7 @@ Flags.init({ forceSave: true }).then(function({
'challenge-tls-alpn-01-xxxx', 'challenge-tls-alpn-01-xxxx',
'challenge', 'challenge',
'challenge-xxxx', 'challenge-xxxx',
'challenge-http-01', 'challenge-http-01'
].forEach(function(k) { ].forEach(function(k) {
myFlags[k] = flagOptions[k]; myFlags[k] = flagOptions[k];
}); });
@ -37,11 +37,11 @@ Flags.init({ forceSave: true }).then(function({
cli.parse(myFlags); cli.parse(myFlags);
cli.main(function(argList, flags) { cli.main(function(argList, flags) {
Flags.mangleFlags(flags, mconf, null, { forceSave: true }); Flags.mangleFlags(flags, mconf, null, { forceSave: true });
main(argList, flags, rc, greenlock); main(argList, flags, greenlock);
}, args); }, args);
}); });
async function main(_, flags, rc, greenlock) { async function main(_, flags, greenlock) {
greenlock.manager greenlock.manager
.defaults(flags) .defaults(flags)
.catch(function(err) { .catch(function(err) {

View File

@ -3,41 +3,37 @@
var P = require('../plugins.js'); var P = require('../plugins.js');
var args = process.argv.slice(3); var args = process.argv.slice(3);
var cli = require('./lib/cli.js'); var cli = require('./lib/cli.js');
//var path = require('path'); var Greenlock = require('../');
//var pkgpath = path.join(__dirname, '..', 'package.json');
//var pkgpath = path.join(process.cwd(), 'package.json');
var Flags = require('./lib/flags.js'); var Flags = require('./lib/flags.js');
var flagOptions = Flags.flags(); var flagOptions = Flags.flags();
var myFlags = {}; var myFlags = {};
['maintainer-email', 'cluster', 'manager', 'manager-xxxx'].forEach(function(k) { [
'config-dir',
'maintainer-email',
'cluster',
'manager',
'manager-xxxx'
].forEach(function(k) {
myFlags[k] = flagOptions[k]; myFlags[k] = flagOptions[k];
}); });
cli.parse(myFlags); cli.parse(myFlags);
cli.main(async function(argList, flags) { cli.main(async function(argList, flags) {
var path = require('path'); var pkgRoot = process.cwd();
var pkgpath = path.join(process.cwd(), 'package.json');
var pkgdir = path.dirname(pkgpath);
//var rcpath = path.join(pkgpath, '.greenlockrc');
var configFile = path.join(pkgdir, 'greenlock.d/manager.json');
var manager = flags.manager; var manager = flags.manager;
// TODO move to bin/lib/greenlockrc.js
if (!manager) {
manager = 'greenlock-cloud-fs';
if (!flags.managerOpts.configFile) {
flags.managerOpts.configFile = configFile;
}
}
if (['fs', 'cloud'].includes(manager)) { if (['fs', 'cloud'].includes(manager)) {
// TODO publish the 1st party modules under a secure namespace manager = '@greenlock/manager';
flags.manager = '@greenlock/manager-' + flags.manager;
} }
if (['cloud'].includes(manager)) {
flags.managerOpts.cloud = true;
}
flags.manager = flags.managerOpts; flags.manager = flags.managerOpts;
delete flags.managerOpts; delete flags.managerOpts;
flags.manager.manager = manager; flags.manager.module = manager;
try { try {
P._loadSync(manager); P._loadSync(manager);
@ -54,11 +50,18 @@ cli.main(async function(argList, flags) {
} }
} }
var GreenlockRc = require('./lib/greenlockrc.js'); var greenlock = Greenlock.create({
//var rc = await GreenlockRc(pkgpath, manager, flags.manager); packageRoot: pkgRoot,
await GreenlockRc(pkgpath, manager, flags.manager); manager: flags.manager,
writeServerJs(pkgdir, flags); configDir: flags.configDir,
writeAppJs(pkgdir); maintainerEmail: flags.maintainerEmail,
_mustPackage: true
});
await greenlock.manager.defaults();
//writeGreenlockJs(pkgdir, flags);
writeServerJs(pkgRoot, flags);
writeAppJs(pkgRoot);
/* /*
rc._bin_mode = true; rc._bin_mode = true;
@ -70,46 +73,73 @@ cli.main(async function(argList, flags) {
*/ */
}, args); }, args);
function writeServerJs(pkgdir, flags) { /*
var serverJs = 'server.js'; function writeGreenlockJs(pkgdir, flags) {
var bakTmpl = 'server-greenlock-tmpl.js'; var greenlockJs = 'greenlock.js';
var fs = require('fs'); var fs = require('fs');
var path = require('path'); var path = require('path');
var tmpl = fs.readFileSync( var tmpl = fs.readFileSync(
path.join(__dirname, 'tmpl/server.tmpl.js'), path.join(__dirname, 'tmpl/greenlock.tmpl.js'),
'utf8' 'utf8'
); );
try { try {
fs.accessSync(path.join(pkgdir, serverJs)); fs.accessSync(path.join(pkgdir, greenlockJs));
console.warn( console.warn("[skip] '%s' exists", greenlockJs);
JSON.stringify(serverJs), return;
' exists, writing to ',
JSON.stringify(bakTmpl),
'instead'
);
serverJs = bakTmpl;
} catch (e) { } catch (e) {
// continue // continue
} }
if (flags.cluster) {
tmpl = tmpl.replace(
/options.cluster = false/g,
'options.cluster = true'
);
}
if (flags.maintainerEmail) { if (flags.maintainerEmail) {
tmpl = tmpl.replace( tmpl = tmpl.replace(
/pkg.author/g, /pkg.author/g,
JSON.stringify(flags.maintainerEmail) 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); fs.writeFileSync(path.join(pkgdir, serverJs), tmpl);
console.info("created '%s'", serverJs);
} }
function writeAppJs(pkgdir) { function writeAppJs(pkgdir) {
var bakTmpl = 'app-greenlock-tmpl.js';
var appJs = 'app.js'; var appJs = 'app.js';
var fs = require('fs'); var fs = require('fs');
var path = require('path'); var path = require('path');
@ -120,16 +150,10 @@ function writeAppJs(pkgdir) {
try { try {
fs.accessSync(path.join(pkgdir, appJs)); fs.accessSync(path.join(pkgdir, appJs));
console.warn( console.warn("[skip] '%s' exists", appJs);
JSON.stringify(appJs), return;
' exists, writing to ',
JSON.stringify(bakTmpl),
'instead'
);
appJs = bakTmpl;
} catch (e) { } catch (e) {
// continue fs.writeFileSync(path.join(pkgdir, appJs), tmpl);
console.info("created '%s'", appJs);
} }
fs.writeFileSync(path.join(pkgdir, appJs), tmpl);
} }

View File

@ -119,7 +119,7 @@ CLI.main = function(cb, args) {
// only long names are actually used // only long names are actually used
if ('--' !== flag.slice(0, 2)) { if ('--' !== flag.slice(0, 2)) {
console.error("Unrecognized argument '" + flag + "'"); console.error("error: unrecognized flag '" + flag + "'");
process.exit(1); process.exit(1);
} }
@ -131,7 +131,7 @@ CLI.main = function(cb, args) {
} }
// other arbitrary args are not used // other arbitrary args are not used
console.error("Unrecognized flag '" + flag + "'"); console.error("unrecognized elided flag '" + flag + "'");
process.exit(1); process.exit(1);
} }

View File

@ -2,10 +2,9 @@
var Flags = module.exports; var Flags = module.exports;
var path = require('path'); //var path = require('path');
//var pkgpath = path.join(__dirname, '..', 'package.json'); var pkgRoot = process.cwd();
var pkgpath = path.join(process.cwd(), 'package.json'); //var Init = require('../../lib/init.js');
var GreenlockRc = require('./greenlockrc.js');
// These are ALL options // These are ALL options
// The individual CLI files each select a subset of them // The individual CLI files each select a subset of them
@ -26,6 +25,11 @@ Flags.flags = function(mconf, myOpts) {
'search all site configs rather than by --subject or --servernames', 'search all site configs rather than by --subject or --servernames',
'boolean' 'boolean'
], ],
'agree-to-terms': [
false,
"agree to the Let's Encrypts Subscriber Agreement and Greenlock Terms of Use",
'boolean'
],
subject: [ subject: [
false, false,
'the "subject" (primary domain) of the certificate', 'the "subject" (primary domain) of the certificate',
@ -63,6 +67,11 @@ Flags.flags = function(mconf, myOpts) {
"the email address of the Let's Encrypt or ACME Account subscriber (not necessarily the domain owner)", "the email address of the Let's Encrypt or ACME Account subscriber (not necessarily the domain owner)",
'string' 'string'
], ],
'config-dir': [
false,
'the directory in which config.json and other config and storage files should be written',
'string'
],
'maintainer-email': [ 'maintainer-email': [
false, false,
'the maintainance contact for security and critical bug notices', 'the maintainance contact for security and critical bug notices',
@ -95,7 +104,7 @@ Flags.flags = function(mconf, myOpts) {
false, false,
'the module name or file path of the manager module to use', 'the module name or file path of the manager module to use',
'string', 'string',
'greenlock-manager-fs' '@greenlock/manager'
], ],
'manager-xxxx': [ 'manager-xxxx': [
false, false,
@ -168,21 +177,23 @@ Flags.flags = function(mconf, myOpts) {
}; };
}; };
Flags.init = function(myOpts) { Flags.init = async function(myOpts) {
return GreenlockRc(pkgpath).then(async function(rc) { var Greenlock = require('../../');
rc._bin_mode = true;
var Greenlock = require('../../'); // this is a copy, so it's safe to modify
// this is a copy, so it's safe to modify var greenlock = Greenlock.create({
var greenlock = Greenlock.create(rc); packageRoot: pkgRoot,
var mconf = await greenlock.manager.defaults(); _mustPackage: true,
var flagOptions = Flags.flags(mconf, myOpts); _init: true,
return { _bin_mode: true
flagOptions,
rc,
greenlock,
mconf
};
}); });
var mconf = await greenlock.manager.defaults();
var flagOptions = Flags.flags(mconf, myOpts);
return {
flagOptions,
greenlock,
mconf
};
}; };
Flags.mangleFlags = function(flags, mconf, sconf, extras) { Flags.mangleFlags = function(flags, mconf, sconf, extras) {

View File

@ -1,113 +0,0 @@
'use strict';
// TODO how to handle path differences when run from npx vs when required by greenlock?
var promisify = require('util').promisify;
var fs = require('fs');
var readFile = promisify(fs.readFile);
var writeFile = promisify(fs.writeFile);
var chmodFile = promisify(fs.chmod);
var path = require('path');
function saveFile(rcpath, data, enc) {
// because this may have a database url or some such
return writeFile(rcpath, data, enc).then(function() {
return chmodFile(rcpath, parseInt('0600', 8));
});
}
module.exports = async function(pkgpath, manager, rc) {
// TODO when run from package
// Run from the package root (assumed) or exit
var pkgdir = path.dirname(pkgpath);
var rcpath = path.join(pkgdir, '.greenlockrc');
var created = false;
try {
require(pkgpath);
} catch (e) {
console.error(
'npx greenlock must be run from the package root (where package.json is)'
);
process.exit(1);
}
if (manager) {
if ('.' === manager[0]) {
manager = path.resolve(pkgdir, manager);
}
try {
require(manager);
} catch (e) {
console.error('npx greenlock must be run from the package root');
process.exit(1);
}
}
var _data = await readFile(rcpath, 'utf8').catch(function(err) {
if ('ENOENT' !== err.code) {
throw err;
}
console.info('Creating ' + rcpath);
created = true;
var data = '{}';
return saveFile(rcpath, data, 'utf8').then(function() {
return data;
});
});
var changed;
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);
}
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 (!_rc.manager) {
changed = true;
_rc.manager = 'greenlock-manager-fs';
console.info('Using default manager ' + _rc.manager);
}
if (!changed) {
return _rc;
}
var data = JSON.stringify(_rc, null, 2);
if (created) {
console.info('Wrote ' + rcpath);
}
return saveFile(rcpath, data, 'utf8').then(function() {
return _rc;
});
};

View File

@ -8,7 +8,7 @@ var cli = require('./lib/cli.js');
var Flags = require('./lib/flags.js'); var Flags = require('./lib/flags.js');
Flags.init().then(function({ flagOptions, rc, greenlock, mconf }) { Flags.init().then(function({ flagOptions, greenlock, mconf }) {
var myFlags = {}; var myFlags = {};
['subject'].forEach(function(k) { ['subject'].forEach(function(k) {
myFlags[k] = flagOptions[k]; myFlags[k] = flagOptions[k];
@ -17,11 +17,11 @@ Flags.init().then(function({ flagOptions, rc, greenlock, mconf }) {
cli.parse(myFlags); cli.parse(myFlags);
cli.main(function(argList, flags) { cli.main(function(argList, flags) {
Flags.mangleFlags(flags, mconf); Flags.mangleFlags(flags, mconf);
main(argList, flags, rc, greenlock); main(argList, flags, greenlock);
}, args); }, args);
}); });
async function main(_, flags, rc, greenlock) { async function main(_, flags, greenlock) {
if (!flags.subject) { if (!flags.subject) {
console.error('--subject must be provided as a valid domain'); console.error('--subject must be provided as a valid domain');
process.exit(1); process.exit(1);

30
bin/tmpl/cluster.tmpl.js Normal file
View File

@ -0,0 +1,30 @@
'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

@ -0,0 +1,13 @@
'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,37 +1,20 @@
'use strict'; 'use strict';
require('greenlock-express') var app = require('./app.js');
.init(function() {
// .greenlockrc defines which manager to use
// (i.e. greenlock-manager-fs or greenlock-manager-cloud)
var options = getGreenlockRc() || {};
// name & version for ACME client user agent require('greenlock-express')
var pkg = require('./package.json'); .init({
options.packageAgent = pkg.name + '/' + pkg.version; packageRoot: __dirname,
// contact for security and critical bug notices // contact for security and critical bug notices
options.maintainerEmail = pkg.author; //maintainerEmail: pkg.author,
// where to look for configuration
configDir: './greenlock.d',
// whether or not to run at cloudscale // whether or not to run at cloudscale
options.cluster = false; cluster: false
return options;
}) })
.ready(function(glx) { // Serves on 80 and 443
var app = require('./app.js'); // Get's SSL certificates magically!
.serve(app);
// Serves on 80 and 443
// Get's SSL certificates magically!
glx.serveApp(app);
});
function getGreenlockRc() {
// The RC file is also used by the (optional) CLI and (optional) Web GUI.
// You are free to forego CLI and GUI support.
var fs = require('fs');
var path = require('path');
var rcPath = path.join(__dirname, '.greenlockrc');
var rc = fs.readFileSync(rcPath, 'utf8');
return JSON.parse(rc);
}

View File

@ -4,7 +4,7 @@ var args = process.argv.slice(3);
var cli = require('./lib/cli.js'); var cli = require('./lib/cli.js');
var Flags = require('./lib/flags.js'); var Flags = require('./lib/flags.js');
Flags.init().then(function({ flagOptions, rc, greenlock, mconf }) { Flags.init().then(function({ flagOptions, greenlock, mconf }) {
var myFlags = {}; var myFlags = {};
[ [
'subject', 'subject',
@ -31,11 +31,11 @@ Flags.init().then(function({ flagOptions, rc, greenlock, mconf }) {
cli.main(async function(argList, flags) { cli.main(async function(argList, flags) {
var sconf = await greenlock._config({ servername: flags.subject }); var sconf = await greenlock._config({ servername: flags.subject });
Flags.mangleFlags(flags, mconf, sconf); Flags.mangleFlags(flags, mconf, sconf);
main(argList, flags, rc, greenlock); main(argList, flags, greenlock);
}, args); }, args);
}); });
async function main(_, flags, rc, greenlock) { async function main(_, flags, greenlock) {
if (!flags.subject) { if (!flags.subject) {
console.error('--subject must be provided as a valid domain'); console.error('--subject must be provided as a valid domain');
process.exit(1); process.exit(1);

View File

@ -127,7 +127,10 @@ C._rawOrder = function(gnlck, mconf, db, acme, chs, acc, email, args) {
var query = { var query = {
subject: args.subject, subject: args.subject,
certificate: args.certificate || {}, certificate: args.certificate || {},
directoryUrl: args.directoryUrl || gnlck._defaults.directoryUrl directoryUrl:
args.directoryUrl ||
mconf.directoryUrl ||
gnlck._defaults.directoryUrl
}; };
rawPending[id] = U._getOrCreateKeypair(db, args.subject, query, keyType) rawPending[id] = U._getOrCreateKeypair(db, args.subject, query, keyType)
.then(function(kresult) { .then(function(kresult) {
@ -208,7 +211,10 @@ C._check = function(gnlck, mconf, db, args) {
subject: args.subject, subject: args.subject,
// may contain certificate.id // may contain certificate.id
certificate: args.certificate, certificate: args.certificate,
directoryUrl: args.directoryUrl || gnlck._defaults.directoryUrl directoryUrl:
args.directoryUrl ||
mconf.directoryUrl ||
gnlck._defaults.directoryUrl
}; };
return db.check(query).then(function(pems) { return db.check(query).then(function(pems) {
if (!pems) { if (!pems) {

View File

@ -1,97 +0,0 @@
'use strict';
var Greenlock = require('./');
module.exports.wrap = function(greenlock) {
greenlock.challenges = {};
greenlock.challenges.get = 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?
return greenlock
._config({ servername: servername })
.then(function(site) {
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;
}
return Greenlock._loadChallenge(site.challenges, 'http-01');
})
.then(function(plugin) {
if (!plugin) {
return null;
}
return plugin
.get({
challenge: {
type: chall.type,
//hostname: chall.servername,
altname: chall.servername,
identifier: { value: chall.servername },
token: chall.token
}
})
.then(function(result) {
var keyAuth;
var keyAuthDigest;
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

@ -5,6 +5,7 @@ var pkg = require('./package.json');
var ACME = require('@root/acme'); var ACME = require('@root/acme');
var Greenlock = module.exports; var Greenlock = module.exports;
var request = require('@root/request'); var request = require('@root/request');
var process = require('process');
var G = Greenlock; var G = Greenlock;
var U = require('./utils.js'); var U = require('./utils.js');
@ -12,7 +13,13 @@ var E = require('./errors.js');
var P = require('./plugins.js'); var P = require('./plugins.js');
var A = require('./accounts.js'); var A = require('./accounts.js');
var C = require('./certificates.js'); var C = require('./certificates.js');
var DIR = require('./lib/directory-url.js');
var ChWrapper = require('./lib/challenges-wrapper.js');
var MngWrapper = require('./lib/manager-wrapper.js');
var UserEvents = require('./user-events.js'); var UserEvents = require('./user-events.js');
var Init = require('./lib/init.js');
var caches = {}; var caches = {};
@ -23,7 +30,6 @@ G.create = function(gconf) {
if (!gconf) { if (!gconf) {
gconf = {}; gconf = {};
} }
var manager;
greenlock._create = function() { greenlock._create = function() {
if (!gconf._bin_mode) { if (!gconf._bin_mode) {
@ -49,31 +55,31 @@ G.create = function(gconf) {
gdefaults.notify = _notify; gdefaults.notify = _notify;
} }
if (gconf.directoryUrl) { gconf = Init._init(gconf);
gdefaults = gconf.directoryUrl;
if (gconf.staging) { // OK: /path/to/blah
throw new Error( // OK: npm-name-blah
'supply `directoryUrl` or `staging`, but not both' // NOT OK: ./rel/path/to/blah
// Error: .blah
if ('.' === (gconf.manager.module || '')[0]) {
if (!gconf.packageRoot) {
gconf.packageRoot = process.cwd();
console.warn(
'`packageRoot` not defined, trying ' + gconf.packageRoot
); );
} }
} else if (gconf.staging) { gconf.manager.module =
gdefaults.directoryUrl = gconf.packageRoot + '/' + gconf.manager.module.slice(2);
'https://acme-staging-v02.api.letsencrypt.org/directory';
} else {
gdefaults.directoryUrl =
'https://acme-v02.api.letsencrypt.org/directory';
} }
console.info('ACME Directory URL:', gdefaults.directoryUrl);
manager = normalizeManager(gconf);
// Wraps each of the following with appropriate error checking // Wraps each of the following with appropriate error checking
// greenlock.manager.defaults // greenlock.manager.defaults
// greenlock.manager.add // greenlock.sites.add
// greenlock.manager.update // greenlock.sites.update
// greenlock.manager.remove // greenlock.sites.remove
// greenlock.manager.find // greenlock.sites.find
require('./manager-underlay.js').wrap(greenlock, manager, gconf); // greenlock.sites.get
MngWrapper.wrap(greenlock, gconf);
// The goal here is to reduce boilerplate, such as error checking // The goal here is to reduce boilerplate, such as error checking
// and duration parsing, that a manager must implement // and duration parsing, that a manager must implement
greenlock.sites.add = greenlock.add = greenlock.manager.add; greenlock.sites.add = greenlock.add = greenlock.manager.add;
@ -83,12 +89,17 @@ G.create = function(gconf) {
// Exports challenges.get for Greenlock Express HTTP-01, // Exports challenges.get for Greenlock Express HTTP-01,
// and whatever odd use case pops up, I suppose // and whatever odd use case pops up, I suppose
// greenlock.challenges.get // greenlock.challenges.get
require('./challenges-underlay.js').wrap(greenlock, manager, gconf); ChWrapper.wrap(greenlock);
DIR._getDefaultDirectoryUrl('', gconf.staging, '');
if (gconf.directoryUrl) {
gdefaults.directoryUrl = gconf.directoryUrl;
}
greenlock._defaults = gdefaults; greenlock._defaults = gdefaults;
greenlock._defaults.debug = gconf.debug; greenlock._defaults.debug = gconf.debug;
if (!gconf._bin_mode) { if (!gconf._bin_mode && false !== gconf.renew) {
// renew every 90-ish minutes (random for staggering) // renew every 90-ish minutes (random for staggering)
// the weak setTimeout (unref) means that when run as a CLI process this // the weak setTimeout (unref) means that when run as a CLI process this
// will still finish as expected, and not wait on the timeout // will still finish as expected, and not wait on the timeout
@ -108,26 +119,21 @@ G.create = function(gconf) {
return p; return p;
}; };
if (manager.init) { p = greenlock.manager
// TODO punycode? .init({
p = manager.init({
request: request request: request
//punycode: require('punycode') //punycode: require('punycode')
}); })
} else { .then(async function() {
p = Promise.resolve(); var MCONF = await greenlock.manager._defaults();
} mergeDefaults(MCONF, gconf);
p = p if (true === MCONF.agreeToTerms) {
.then(function() { gdefaults.agreeToTerms = function(tos) {
return manager.defaults().then(function(MCONF) { return Promise.resolve(tos);
mergeDefaults(MCONF, gconf); };
if (true === MCONF.agreeToTerms) { }
gdefaults.agreeToTerms = function(tos) {
return Promise.resolve(tos); return greenlock.manager._defaults(MCONF);
};
}
return manager.defaults(MCONF);
});
}) })
.catch(function(err) { .catch(function(err) {
if ('load_plugin' !== err.context) { if ('load_plugin' !== err.context) {
@ -200,51 +206,47 @@ G.create = function(gconf) {
}; };
// certs.get // certs.get
greenlock.get = function(args) { greenlock.get = async function(args) {
return greenlock greenlock._single(args);
._single(args) args._includePems = true;
.then(function() { var results = await greenlock.renew(args);
args._includePems = true; if (!results || !results.length) {
return greenlock.renew(args); // TODO throw an error here?
}) return null;
.then(function(results) { }
if (!results || !results.length) {
// TODO throw an error here?
return null;
}
// just get the first one // just get the first one
var result = results[0]; var result = results[0];
// (there should be only one, ideally) // (there should be only one, ideally)
if (results.length > 1) { if (results.length > 1) {
var err = new Error( var err = new Error(
"a search for '" + "a search for '" +
args.servername + args.servername +
"' returned multiple certificates" "' returned multiple certificates"
); );
err.context = 'duplicate_certs'; err.context = 'duplicate_certs';
err.servername = args.servername; err.servername = args.servername;
err.subjects = results.map(function(r) { err.subjects = results.map(function(r) {
return (r.site || {}).subject || 'N/A'; return (r.site || {}).subject || 'N/A';
});
greenlock._notify('warning', err);
}
if (result.error) {
return Promise.reject(result.error);
}
// site for plugin options, such as http-01 challenge
// pems for the obvious reasons
return result;
}); });
greenlock._notify('warning', err);
}
if (result.error) {
return Promise.reject(result.error);
}
// site for plugin options, such as http-01 challenge
// pems for the obvious reasons
return result;
}; };
greenlock._single = function(args) { // TODO remove async here, it doesn't matter
greenlock._single = async function(args) {
if ('string' !== typeof args.servername) { if ('string' !== typeof args.servername) {
return Promise.reject(new Error('no `servername` given')); throw new Error('no `servername` given');
} }
// www.example.com => *.example.com // www.example.com => *.example.com
args.wildname = args.wildname =
@ -266,116 +268,110 @@ G.create = function(gconf) {
args.issueBefore || args.issueBefore ||
args.expiresBefore args.expiresBefore
) { ) {
return Promise.reject( throw new Error(
new Error( 'bad arguments, did you mean to call greenlock.renew()?'
'bad arguments, did you mean to call greenlock.renew()?'
)
); );
} }
// duplicate, force, and others still allowed // duplicate, force, and others still allowed
return Promise.resolve(args); return args;
}; };
greenlock._config = function(args) { greenlock._config = async function(args) {
return greenlock._single(args).then(function() { greenlock._single(args);
return greenlock._configAll(args); var sites = await greenlock._configAll(args);
}); return sites[0];
}; };
greenlock._configAll = function(args) { greenlock._configAll = async function(args) {
return greenlock._find(args).then(function(sites) { var sites = await greenlock._find(args);
if (!sites || !sites.length) { if (!sites || !sites.length) {
return null; return [];
} }
var site = sites[0]; sites = JSON.parse(JSON.stringify(sites));
site = JSON.parse(JSON.stringify(site)); var mconf = await greenlock.manager._defaults();
return sites.map(function(site) {
if (site.store && site.challenges) { if (site.store && site.challenges) {
return site; return site;
} }
var dconf = site; var dconf = site;
// TODO make cli and api mode the same // TODO make cli and api mode the same
if (gconf._bin_mode) { if (gconf._bin_mode) {
dconf = site.defaults = {}; dconf = site.defaults = {};
} }
return manager.defaults().then(function(mconf) { if (!site.store) {
if (!site.store) { dconf.store = mconf.store;
dconf.store = mconf.store; }
} if (!site.challenges) {
if (!site.challenges) { dconf.challenges = mconf.challenges;
dconf.challenges = mconf.challenges; }
} return site;
return site;
});
}); });
}; };
// needs to get info about the renewal, such as which store and challenge(s) to use // needs to get info about the renewal, such as which store and challenge(s) to use
greenlock.renew = function(args) { greenlock.renew = async function(args) {
return greenlock._init().then(function() { await greenlock._init();
return manager.defaults().then(function(mconf) { var mconf = await greenlock.manager._defaults();
return greenlock._renew(mconf, args); return greenlock._renew(mconf, args);
});
});
}; };
greenlock._renew = function(mconf, args) { greenlock._renew = async function(mconf, args) {
if (!args) { if (!args) {
args = {}; args = {};
} }
var renewedOrFailed = []; var renewedOrFailed = [];
//console.log('greenlock._renew find', args); //console.log('greenlock._renew find', args);
return greenlock._find(args).then(function(sites) { var sites = await greenlock._find(args);
// Note: the manager must guaranteed that these are mutable copies // Note: the manager must guaranteed that these are mutable copies
//console.log('greenlock._renew found', sites);; //console.log('greenlock._renew found', sites);;
if (!Array.isArray(sites)) { if (!Array.isArray(sites)) {
throw new Error( throw new Error(
'Developer Error: not an array of sites returned from find: ' + 'Developer Error: not an array of sites returned from find: ' +
JSON.stringify(sites) JSON.stringify(sites)
); );
} }
function next() {
var site = sites.shift();
if (!site) {
return Promise.resolve(null);
}
var order = { site: site }; await (async function next() {
renewedOrFailed.push(order); var site = sites.shift();
// TODO merge args + result? if (!site) {
return greenlock return null;
._order(mconf, site)
.then(function(pems) {
if (args._includePems) {
order.pems = pems;
}
})
.catch(function(err) {
order.error = err;
// For greenlock express serialization
err.toJSON = errorToJSON;
err.context = err.context || 'cert_order';
err.subject = site.subject;
if (args.servername) {
err.servername = args.servername;
}
// for debugging, but not to be relied on
err._site = site;
// TODO err.context = err.context || 'renew_certificate'
greenlock._notify('error', err);
})
.then(function() {
return next();
});
} }
return next().then(function() { var order = { site: site };
return renewedOrFailed; renewedOrFailed.push(order);
}); // TODO merge args + result?
}); return greenlock
._order(mconf, site)
.then(function(pems) {
if (args._includePems) {
order.pems = pems;
}
})
.catch(function(err) {
order.error = err;
// For greenlock express serialization
err.toJSON = errorToJSON;
err.context = err.context || 'cert_order';
err.subject = site.subject;
if (args.servername) {
err.servername = args.servername;
}
// for debugging, but not to be relied on
err._site = site;
// TODO err.context = err.context || 'renew_certificate'
greenlock._notify('error', err);
})
.then(function() {
return next();
});
})();
return renewedOrFailed;
}; };
greenlock._acme = function(args) { greenlock._acme = async function(mconf, args, dirUrl) {
var packageAgent = gconf.packageAgent || ''; var packageAgent = gconf.packageAgent || '';
// because Greenlock_Express/v3.x Greenlock/v3 is redundant // because Greenlock_Express/v3.x Greenlock/v3 is redundant
if (!/greenlock/i.test(packageAgent)) { if (!/greenlock/i.test(packageAgent)) {
@ -387,84 +383,100 @@ G.create = function(gconf) {
notify: greenlock._notify, notify: greenlock._notify,
debug: greenlock._defaults.debug || args.debug debug: greenlock._defaults.debug || args.debug
}); });
var dirUrl = args.directoryUrl || greenlock._defaults.directoryUrl;
var dir = caches[dirUrl]; var dir = caches[dirUrl];
// don't cache more than an hour // don't cache more than an hour
if (dir && Date.now() - dir.ts < 1 * 60 * 60 * 1000) { if (dir && Date.now() - dir.ts < 1 * 60 * 60 * 1000) {
return dir.promise; return dir.promise;
} }
return acme await acme.init(dirUrl).catch(function(err) {
.init(dirUrl) // TODO this is a special kind of failure mode. What should we do?
.then(function(/*meta*/) { console.error(
caches[dirUrl] = { "[debug] Let's Encrypt may be down for maintenance or `directoryUrl` may be wrong"
promise: Promise.resolve(acme), );
ts: Date.now() throw err;
}; });
return acme;
}) caches[dirUrl] = {
.catch(function(err) { promise: Promise.resolve(acme),
// TODO ts: Date.now()
// let's encrypt is possibly down for maintenaince... };
// this is a special kind of failure mode return acme;
throw err;
});
}; };
greenlock.order = function(args) { greenlock.order = async function(siteConf) {
return greenlock._init().then(function() { await greenlock._init();
return manager.defaults().then(function(mconf) { var mconf = await greenlock.manager._defaults();
return greenlock._order(mconf, args); return greenlock._order(mconf, siteConf);
});
});
}; };
greenlock._order = function(mconf, args) { greenlock._order = async function(mconf, siteConf) {
// packageAgent, maintainerEmail // packageAgent, maintainerEmail
return greenlock._acme(args).then(function(acme) {
var storeConf = args.store || mconf.store; var dirUrl = DIR._getDirectoryUrl(
return P._loadStore(storeConf).then(function(store) { siteConf.directoryUrl || mconf.directoryUrl,
return A._getOrCreate( siteConf.subject
greenlock, );
mconf,
store.accounts, var acme = await greenlock._acme(mconf, siteConf, dirUrl);
acme, var storeConf = siteConf.store || mconf.store;
args storeConf = JSON.parse(JSON.stringify(storeConf));
).then(function(account) { storeConf.packageRoot = gconf.packageRoot;
var challengeConfs = args.challenges || mconf.challenges;
return Promise.all( if (!storeConf.basePath) {
Object.keys(challengeConfs).map(function(typ01) { storeConf.basePath = gconf.configDir;
return P._loadChallenge(challengeConfs, typ01); }
})
).then(function(arr) { if ('.' === (storeConf.basePath || '')[0]) {
var challenges = {}; if (!gconf.packageRoot) {
arr.forEach(function(el) { gconf.packageRoot = process.cwd();
challenges[el._type] = el; console.warn(
}); '`packageRoot` not defined, trying ' + gconf.packageRoot
return C._getOrOrder( );
greenlock, }
mconf, storeConf.basePath = require('path').resolve(
store.certificates, gconf.packageRoot || '',
acme, storeConf.basePath
challenges, );
account, }
args
).then(function(pems) { storeConf.directoryUrl = dirUrl;
if (!pems) { var store = await P._loadStore(storeConf);
throw new Error('no order result'); var account = await A._getOrCreate(
} greenlock,
if (!pems.privkey) { mconf,
throw new Error( store.accounts,
'missing private key, which is kinda important' acme,
); siteConf
} );
return pems; var challengeConfs = siteConf.challenges || mconf.challenges;
}); var challenges = {};
}); var arr = await Promise.all(
}); Object.keys(challengeConfs).map(function(typ01) {
}); return P._loadChallenge(challengeConfs, typ01);
})
);
arr.forEach(function(el) {
challenges[el._type] = el;
}); });
var pems = await C._getOrOrder(
greenlock,
mconf,
store.certificates,
acme,
challenges,
account,
siteConf
);
if (!pems) {
throw new Error('no order result');
}
if (!pems.privkey) {
throw new Error('missing private key, which is kinda important');
}
return pems;
}; };
greenlock._create(); greenlock._create();
@ -482,106 +494,6 @@ function errorToJSON(e) {
return error; return error;
} }
function normalizeManager(gconf) {
var m;
// 1. Get the manager
// 2. Figure out if we need to wrap it
if (!gconf.manager) {
gconf.manager = 'greenlock-manager-fs';
if (gconf.find) {
// { manager: 'greenlock-manager-fs', find: function () { } }
warpFind(gconf);
}
}
if ('string' === typeof gconf.manager) {
try {
// wrap this to be safe for greenlock-manager-fs
m = require(gconf.manager).create(gconf);
} catch (e) {
console.error('Error loading manager:');
console.error(e.code);
console.error(e.message);
}
} else {
m = gconf.manager;
}
if (!m) {
console.error();
console.error(
'Failed to load manager plugin ',
JSON.stringify(gconf.manager)
);
console.error();
process.exit(1);
}
if (
['set', 'remove', 'find', 'defaults'].every(function(k) {
return 'function' === typeof m[k];
})
) {
return m;
}
// { manager: { find: function () { } } }
if (m.find) {
warpFind(m);
}
// m.configFile could also be set
m = require('greenlock-manager-fs').create(m);
if ('function' !== typeof m.find) {
console.error();
console.error(
JSON.stringify(gconf.manager),
'must implement `find()` and should implement `set()`, `remove()`, `defaults()`, and `init()`'
);
console.error();
process.exit(1);
}
return m;
}
function warpFind(gconf) {
gconf.__gl_find = gconf.find;
gconf.find = function(args) {
// the incoming args will be normalized by greenlock
return gconf.__gl_find(args).then(function(sites) {
// we also need to error check the incoming sites,
// as if they were being passed through `add()` or `set()`
// (effectively they are) because the manager assumes that
// they're not bad
sites.forEach(function(s) {
if (!s || 'string' !== typeof s.subject) {
throw new Error('missing subject');
}
if (
!Array.isArray(s.altnames) ||
!s.altnames.length ||
!s.altnames[0] ||
s.altnames[0] !== s.subject
) {
throw new Error('missing or malformed altnames');
}
['renewAt', 'issuedAt', 'expiresAt'].forEach(function(k) {
if (s[k]) {
throw new Error(
'`' +
k +
'` should be updated by `set()`, not by `find()`'
);
}
});
});
return sites;
});
};
}
function mergeDefaults(MCONF, gconf) { function mergeDefaults(MCONF, gconf) {
if ( if (
gconf.agreeToTerms === true || gconf.agreeToTerms === true ||
@ -597,20 +509,29 @@ function mergeDefaults(MCONF, gconf) {
MCONF.subscriberEmail = gconf.subscriberEmail; MCONF.subscriberEmail = gconf.subscriberEmail;
} }
var homedir;
// Load the default store module // Load the default store module
if (!MCONF.store) { if (!MCONF.store) {
if (gconf.store) { if (gconf.store) {
MCONF.store = gconf.store; MCONF.store = gconf.store;
} else { } else {
homedir = require('os').homedir();
MCONF.store = { MCONF.store = {
module: 'greenlock-store-fs', module: 'greenlock-store-fs'
basePath: homedir + '/.config/greenlock/'
}; };
console.info('[default] store.module: ' + MCONF.store.module);
} }
} }
/*
if ('greenlock-store-fs' === MCONF.store.module && !MCONF.store.basePath) {
//homedir = require('os').homedir();
if (gconf.configFile) {
MCONF.store.basePath = gconf.configFile.replace(/\.json$/i, '.d');
} else {
MCONF.store.basePath = './greenlock.d';
}
}
*/
// just to test that it loads // just to test that it loads
P._loadSync(MCONF.store.module); P._loadSync(MCONF.store.module);
@ -621,6 +542,10 @@ function mergeDefaults(MCONF, gconf) {
} }
if (!challenges['http-01'] && !challenges['dns-01']) { if (!challenges['http-01'] && !challenges['dns-01']) {
challenges['http-01'] = { module: 'acme-http-01-standalone' }; challenges['http-01'] = { module: 'acme-http-01-standalone' };
console.info(
'[default] challenges.http-01.module: ' +
challenges['http-01'].module
);
} }
if (challenges['http-01']) { if (challenges['http-01']) {
if ('string' !== typeof challenges['http-01'].module) { if ('string' !== typeof challenges['http-01'].module) {
@ -644,16 +569,40 @@ function mergeDefaults(MCONF, gconf) {
if (!MCONF.renewOffset) { if (!MCONF.renewOffset) {
MCONF.renewOffset = gconf.renewOffset || '-45d'; MCONF.renewOffset = gconf.renewOffset || '-45d';
console.info('[default] renewOffset: ' + MCONF.renewOffset);
} }
if (!MCONF.renewStagger) { if (!MCONF.renewStagger) {
MCONF.renewStagger = gconf.renewStagger || '3d'; MCONF.renewStagger = gconf.renewStagger || '3d';
console.info('[default] renewStagger: ' + MCONF.renewStagger);
} }
var vers = process.versions.node.split('.');
var defaultKeyType = 'EC-P256';
if (vers[0] < 10 || (vers[0] === '10' && vers[1] < '12')) {
defaultKeyType = 'RSA-2048';
}
if (!MCONF.accountKeyType) { if (!MCONF.accountKeyType) {
MCONF.accountKeyType = gconf.accountKeyType || 'EC-P256'; MCONF.accountKeyType = gconf.accountKeyType || defaultKeyType;
console.info('[default] accountKeyType: ' + MCONF.accountKeyType);
} }
if (!MCONF.serverKeyType) { if (!MCONF.serverKeyType) {
MCONF.serverKeyType = gconf.serverKeyType || 'RSA-2048'; MCONF.serverKeyType = gconf.serverKeyType || 'RSA-2048';
console.info('[default] serverKeyType: ' + MCONF.serverKeyType);
}
if (!MCONF.subscriberEmail && false !== MCONF.subscriberEmail) {
MCONF.subscriberEmail =
gconf.subscriberEmail || gconf.maintainerEmail || undefined;
MCONF.agreeToTerms = gconf.agreeToTerms || undefined;
console.info('');
console.info('[default] subscriberEmail: ' + MCONF.subscriberEmail);
console.info(
'[default] agreeToTerms: ' +
(MCONF.agreeToTerms ||
gconf.agreeToTerms ||
'(show notice on use)')
);
console.info('');
} }
} }

191
greenlockrc.js Normal file
View File

@ -0,0 +1,191 @@
'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
};
};

88
lib/challenges-wrapper.js Normal file
View File

@ -0,0 +1,88 @@
'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;
}

46
lib/directory-url.js Normal file
View File

@ -0,0 +1,46 @@
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;

195
lib/init.js Normal file
View File

@ -0,0 +1,195 @@
'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'
);
}
//var mkdirp = promisify(require("@root/mkdirp"));
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;
}

617
lib/manager-wrapper.js Normal file
View File

@ -0,0 +1,617 @@
'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(gconf);
greenlock.manager = {};
greenlock.sites = {};
//greenlock.accounts = {};
//greenlock.certs = {};
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(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.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;
}

77
lib/rc.js Normal file
View File

@ -0,0 +1,77 @@
'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;
};

View File

@ -1,286 +0,0 @@
'use strict';
var U = require('./utils.js');
var E = require('./errors.js');
var warned = {};
module.exports.wrap = function(greenlock, manager, gconf) {
greenlock.manager = {};
greenlock.sites = {};
//greenlock.accounts = {};
//greenlock.certs = {};
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 manager.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 manager.defaults(conf);
});
};
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 manager.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.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 manager.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
};
*/
greenlock._find = function(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.altnames.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 manager.find(args);
};
};
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;
}

42
package-lock.json generated
View File

@ -1,9 +1,28 @@
{ {
"name": "@root/greenlock", "name": "@root/greenlock",
"version": "3.1.0-wip", "version": "4.0.2",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "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": { "@root/acme": {
"version": "3.0.8", "version": "3.0.8",
"resolved": "https://registry.npmjs.org/@root/acme/-/acme-3.0.8.tgz", "resolved": "https://registry.npmjs.org/@root/acme/-/acme-3.0.8.tgz",
@ -60,9 +79,9 @@
"integrity": "sha512-rEUDiUsHtild8GfIjFE9wXtcVxeS+ehCJQBwbQQ3IVfORKHK93CFnRtkr69R75lZFjcmKYVc+AXDB+AeRFOULA==" "integrity": "sha512-rEUDiUsHtild8GfIjFE9wXtcVxeS+ehCJQBwbQQ3IVfORKHK93CFnRtkr69R75lZFjcmKYVc+AXDB+AeRFOULA=="
}, },
"@root/request": { "@root/request": {
"version": "1.3.11", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.3.11.tgz", "resolved": "https://registry.npmjs.org/@root/request/-/request-1.4.2.tgz",
"integrity": "sha512-3a4Eeghcjsfe6zh7EJ+ni1l8OK9Fz2wL1OjP4UCa0YdvtH39kdXB9RGWuzyNv7dZi0+Ffkc83KfH0WbPMiuJFw==" "integrity": "sha512-J8FM4+SJuc7WRC+Jz17m+VT2lgI7HtatHhxN1F2ck5aIKUAxJEaR4u/gLBsgT60mVHevKCjKN0O8115UtJjwLw=="
}, },
"@root/x509": { "@root/x509": {
"version": "0.7.2", "version": "0.7.2",
@ -89,19 +108,10 @@
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==",
"dev": true "dev": true
}, },
"greenlock-manager-fs": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/greenlock-manager-fs/-/greenlock-manager-fs-3.0.3.tgz",
"integrity": "sha512-Jwo60nHd10PNUA9M6cylD9YB4x4hzlfO2LRIGI0X+V+zA0x3KVbNW14yj8frdfHrtsWC1JQe7oFnHVdoRbAU2A==",
"requires": {
"@root/mkdirp": "^1.0.0",
"safe-replace": "^1.1.0"
}
},
"greenlock-store-fs": { "greenlock-store-fs": {
"version": "3.2.0", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/greenlock-store-fs/-/greenlock-store-fs-3.2.0.tgz", "resolved": "https://registry.npmjs.org/greenlock-store-fs/-/greenlock-store-fs-3.2.2.tgz",
"integrity": "sha512-zqcPnF+173oYq5qU7FoGtuqeG8dmmvAiSnz98kEHAHyvgRF9pE1T0MM0AuqDdj45I3kXlCj2gZBwutnRi37J3g==", "integrity": "sha512-92ejLB4DyV4qv/2b6VLGF2nKfYQeIfg3o+e/1cIoYLjlIaUFdbBXkzLTRozFlHsQPZt2ALi5qYrpC9IwH7GK8A==",
"requires": { "requires": {
"@root/mkdirp": "^1.0.0", "@root/mkdirp": "^1.0.0",
"safe-replace": "^1.1.0" "safe-replace": "^1.1.0"

View File

@ -1,6 +1,6 @@
{ {
"name": "@root/greenlock", "name": "@root/greenlock",
"version": "3.1.0-wip", "version": "4.0.2",
"description": "The easiest Let's Encrypt client for Node.js and Browsers", "description": "The easiest Let's Encrypt client for Node.js and Browsers",
"homepage": "https://rootprojects.org/greenlock/", "homepage": "https://rootprojects.org/greenlock/",
"main": "greenlock.js", "main": "greenlock.js",
@ -38,15 +38,15 @@
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
"@greenlock/manager": "^3.1.0",
"@root/acme": "^3.0.8", "@root/acme": "^3.0.8",
"@root/csr": "^0.8.1", "@root/csr": "^0.8.1",
"@root/keypairs": "^0.9.0", "@root/keypairs": "^0.9.0",
"@root/mkdirp": "^1.0.0", "@root/mkdirp": "^1.0.0",
"@root/request": "^1.3.10", "@root/request": "^1.4.2",
"acme-http-01-standalone": "^3.0.5", "acme-http-01-standalone": "^3.0.5",
"cert-info": "^1.5.1", "cert-info": "^1.5.1",
"greenlock-manager-fs": "^3.0.3", "greenlock-store-fs": "^3.2.2",
"greenlock-store-fs": "^3.2.0",
"safe-replace": "^1.1.0" "safe-replace": "^1.1.0"
}, },
"devDependencies": { "devDependencies": {