Merge pull request #40 from Daplie/v2.x

v2.x
This commit is contained in:
AJ ONeal 2016-08-09 14:14:04 -06:00 committed by GitHub
commit 6911b7bcca
27 changed files with 1410 additions and 1433 deletions

372
README.md
View File

@ -10,271 +10,249 @@
letsencrypt
===========
Automatic [Let's Encrypt](https://letsencrypt.org) HTTPS Certificates for node.js
Automatic [Let's Encrypt](https://letsencrypt.org) HTTPS / TLS / SSL Certificates for node.js
* [Automatic HTTPS with ExpressJS](https://github.com/Daplie/letsencrypt-express)
* [Automatic live renewal](https://github.com/Daplie/letsencrypt-express#how-automatic)
* On-the-fly HTTPS certificates for Dynamic DNS (in-process, no server restart)
* Works with node cluster out of the box
* usable [via commandline](https://github.com/Daplie/letsencrypt-cli) as well
* Free SSL (HTTPS Certificates for TLS)
* [90-day certificates](https://letsencrypt.org/2015/11/09/why-90-days.html)
**See Also**
* [Let's Encrypt in (exactly) 90 seconds with Caddy](https://daplie.com/articles/lets-encrypt-in-literally-90-seconds/)
* [lego](https://github.com/xenolf/lego): Let's Encrypt for golang
Free SLL with [90-day](https://letsencrypt.org/2015/11/09/why-90-days.html) HTTPS / TLS Certificates
STOP
====
**These aren't the droids you're looking for.**
> **These aren't the droids you're looking for.**
This is a low-level library for implementing CLIs,
This is a **low-level library** for implementing ACME / LetsEncrypt Clients, CLIs,
system tools, and abstracting storage backends (file vs db, etc).
This is not the thing to use in your webserver directly.
### Use [letsencrypt-express](https://github.com/Daplie/letsencrypt-express) if...
For `express`, raw `https` or `spdy`, or `restify` (same as raw https) see
[**letsencrypt-express**](https://github.com/Daplie/letsencrypt-express).
you are planning to use one of these:
For `hapi` see [letsencrypt-hapi](https://github.com/Daplie/letsencrypt-hapi).
* `express`
* `connect`
* raw `https`
* raw `spdy`
* `restify` (same as raw https)
* `hapi` See [letsencrypt-hapi](https://github.com/Daplie/letsencrypt-hapi)
* `koa` See [letsencrypt-koa](https://github.com/Daplie/letsencrypt-koa)
* `rill` (similar to koa example)
For `koa` or `rill`
see [letsencrypt-koa](https://github.com/Daplie/letsencrypt-koa).
### Use [letsencrypt-cli](https://github.com/Daplie/letsencrypt-cli) if...
For `bash`, `fish`, `zsh`, `cmd.exe`, `PowerShell`
see [**letsencrypt-cli**](https://github.com/Daplie/letsencrypt-cli).
You are planning to use one of these:
CONTINUE
========
* `bash`
* `fish`
* `zsh`
* `cmd.exe`
* `PowerShell`
If you're sure you're at the right place, here's what you need to know now:
Install
=======
-------
`letsencrypt` requires at least two plugins:
one for managing certificate storage and the other for handling ACME challenges.
The default storage plugin is [`le-store-certbot`](https://github.com/Daplie/le-store-certbot)
and the default challenge is [`le-challenge-fs`](https://github.com/Daplie/le-challenge-fs).
```bash
npm install --save letsencrypt
npm install --save letsencrypt@2.x
npm install --save le-store-certbot@2.x
npm install --save le-challenge-fs@2.x
```
Usage
=====
-----
### letsencrypt
It's very simple and easy to use, but also very complete and easy to extend and customize.
There are **NO DEFAULTS**.
### Overly Simplified Example
A number of **constants** (such as LE.stagingServerUrl and LE.configDir)
are exported for your convenience, but all required options must be specified by the library invoking the call.
Open an issue if you need a variable for something that isn't there yet.
Against my better judgement I'm providing a terribly oversimplified example
of how to use this library:
```javascript
var le = require('letsencrypt').create({ server: 'staging' });
le.register(
{ domains: ['example.com'], email: 'user@email.com', agreeTos: true }
, function (err, results) {
console.log(err, results);
}
);
```
You also need some sort of server to handle the acme challenge:
```javascript
var app = express();
app.use('/', le.middleware());
```
Note: The `webrootPath` string is a template.
Any occurance of `:hostname` will be replaced
with the domain for which we are requested certificates.
### Useful Example
The configuration consists of 3 components:
* Storage Backend (search npm for projects starting with 'le-store-')
* ACME Challenge Handlers (search npm for projects starting with 'le-challenge-')
* Letsencryt Config (this is all you)
```javascript
'use strict';
var LE = require('letsencrypt');
var le;
var config = {
server: LE.stagingServerUrl // or LE.productionServerUrl
, configDir: require('homedir')() + '/letsencrypt/etc' // or /etc/letsencrypt or wherever
, privkeyPath: ':config/live/:hostname/privkey.pem' //
, fullchainPath: ':config/live/:hostname/fullchain.pem' // Note: both that :config and :hostname
, certPath: ':config/live/:hostname/cert.pem' // will be templated as expected
, chainPath: ':config/live/:hostname/chain.pem' //
// Storage Backend
var leStore = require('le-store-certbot').create({
configDir: '~/letsencrypt/etc' // or /etc/letsencrypt or wherever
, debug: false
};
});
var handlers = {
setChallenge: function (opts, hostname, key, val, cb) {} // called during the ACME server handshake, before validation
, removeChallenge: function (opts, hostname, key, cb) {} // called after validation on both success and failure
, getChallenge: function (opts, hostname, key, cb) {} // this is special because it is called by the webserver
// (see letsencrypt-cli/bin & letsencrypt-express/standalone),
// not by the library itself
, agreeToTerms: function (tosUrl, cb) {} // gives you an async way to expose the legal agreement
// (terms of use) to your users before accepting
};
// ACME Challenge Handlers
var leChallenge = require('le-challenge-fs').create({
webrootPath: '~/letsencrypt/var/' // or template string such as
, debug: false // '/srv/www/:hostname/.well-known/acme-challenge'
});
var le = LE.create(config, handlers);
function leAgree(opts, agreeCb) {
// opts = { email, domains, tosUrl }
agreeCb(null, opts.tosUrl);
}
// checks :conf/renewal/:hostname.conf
le.register({ // and either renews or registers
le = LE.create({
server: LE.stagingServerUrl // or LE.productionServerUrl
, store: leStore // handles saving of config, accounts, and certificates
, challenge: leChallenge // handles /.well-known/acme-challege keys and tokens
, agreeToTerms: leAgree // hook to allow user to view and accept LE TOS
, debug: false
});
domains: ['example.com'] // CHANGE TO YOUR DOMAIN
, email: 'user@email.com' // CHANGE TO YOUR EMAIL
, agreeTos: false // set to true to automatically accept an agreement
// which you have pre-approved (not recommended)
}, function (err) {
if (err) {
// Note: you must have a webserver running
// and expose handlers.getChallenge to it
// in order to pass validation
// See letsencrypt-cli and or letsencrypt-express
// If using express you should use the middleware
// app.use('/', le.middleware());
//
// Otherwise you should see the test file for usage of this:
// le.challenge.get(opts.domain, key, val, done)
// Check in-memory cache of certificates for the named domain
le.check({ domains: [ 'example.com' ] }).then(function (results) {
if (results) {
// we already have certificates
return;
}
// Register Certificate manually
le.register({
domains: ['example.com'] // CHANGE TO YOUR DOMAIN (list for SANS)
, email: 'user@email.com' // CHANGE TO YOUR EMAIL
, agreeTos: '' // set to tosUrl string (or true) to pre-approve (and skip agreeToTerms)
, rsaKeySize: 2048 // 2048 or higher
, challengeType: 'http-01' // http-01, tls-sni-01, or dns-01
}).then(function (results) {
console.log('success');
}, function (err) {
// Note: you must either use le.middleware() with express,
// manually use le.challenge.get(opts, domain, key, val, done)
// or have a webserver running and responding
// to /.well-known/acme-challenge at `webrootPath`
console.error('[Error]: node-letsencrypt/examples/standalone');
console.error(err.stack);
} else {
console.log('success');
}
});
});
```
**However**, due to the nature of what this library does, it has a few more "moving parts"
than what makes sense to show in a minimal snippet.
Here's what `results` looks like:
```javascript
{ privkey: '' // PEM encoded private key
, cert: '' // PEM encoded cert
, chain: '' // PEM encoded intermediate cert
, fullchain: '' // cert + chain
, issuedAt: 0 // notBefore date (in ms) parsed from cert
, expiresAt: 0 // notAfter date (in ms) parsed from cert
}
```
API
===
---
```javascript
LetsEncrypt.create(leConfig, handlers, backend) // wraps a given "backend" (the python or node client)
LetsEncrypt.stagingServer // string of staging server for testing
The full end-user API is exposed in the example above and includes all relevant options.
le.middleware() // middleware for serving webrootPath to /.well-known/acme-challenge
le.sniCallback(hostname, function (err, tlsContext) {}) // uses fetch (below) and formats for https.SNICallback
le.register({ domains, email, agreeTos, ... }, cb) // registers or renews certs for a domain
le.fetch({domains, email, agreeTos, ... }, cb) // fetches certs from in-memory cache, occasionally refreshes from disk
le.registrationFailureCallback(err, args, certInfo, cb) // called when registration fails (not implemented yet)
```
le.register
le.check
```
### `LetsEncrypt.create(backend, leConfig, handlers)`
### Helper Functions
#### leConfig
We do expose a few helper functions:
The arguments passed here (typically `webpathRoot`, `configDir`, etc) will be merged with
any `args` (typically `domains`, `email`, and `agreeTos`) and passed to the backend whenever
it is called.
* LE.validDomain(hostname) // returns '' or the hostname string if it's a valid ascii or punycode domain name
Typically the backend wrapper will already merge any necessary backend-specific arguments.
TODO fetch domain tld list
**Example**:
```javascript
{ webrootPath: __dirname, '/acme-challenge'
, fullchainTpl: '/live/:hostname/fullchain.pem'
, privkeyTpl: '/live/:hostname/fullchain.pem'
, configDir: '/etc/letsencrypt'
}
```
### Template Strings
Note: `webrootPath` can be set as a default, semi-locally with `webrootPathTpl`, or per
registration as `webrootPath` (which overwrites `leConfig.webrootPath`).
The following variables will be tempalted in any strings passed to the options object:
#### handlers *optional*
* `~/` replaced with `os.homedir()` i.e. `/Users/aj`
* `:hostname` replaced with the first domain in the list i.e. `example.com`
`h.setChallenge(hostnames, name, value, cb)`:
Developer API
-------------
default is to write to fs
If you are developing an `le-store-*` or `le-challenge-*` plugin you need to be aware of
additional internal API expectations.
`h.getChallenge(hostnames, value cb)`
**IMPORTANT**:
default is to read from fs
Use `v2.0.0` as your initial version - NOT v0.1.0 and NOT v1.0.0 and NOT v3.0.0.
This is to indicate that your module is compatible with v2.x of node-letsencrypt.
`h.sniRegisterCallback(args, currentCerts, cb)`
Since the public API for your module is defined by node-letsencrypt the major version
should be kept in sync.
The default is to immediately call `cb(null, null)` and register (or renew) in the background
during the `SNICallback` phase. Right now it isn't reasonable to renew during SNICallback,
but around February when it is possible to use ECDSA keys (as opposed to RSA at present),
registration will take very little time.
### store implementation
This will not be called while another registration is already in progress.
TODO double check and finish
### `le.middleware()`
* accounts
* accounts.byDomain
* accounts.all
* accounts.get
* accounts.exists
* certs
* certs.byAccount
* certs.all
* certs.get
* certs.exists
An express handler for `/.well-known/acme-challenge/<challenge>`.
Will call `getChallenge([hostname], key, cb)` if present or otherwise read `challenge` from disk.
### challenge implementation
Example:
```javascript
app.use('/', le.middleware())
```
TODO finish
### `le.sniCallback(hostname, function (err, tlsContext) {});`
Will call `fetch`. If fetch does not return certificates or returns expired certificates
it will call `sniRegisterCallback(args, currentCerts, cb)` and then return the error,
the new certificates, or call `fetch` a final time.
Example:
```javascript
var server = require('https').createServer({ SNICallback: le.sniCallback, cert: '...', key: '...' });
server.on('request', app);
```
### `le.register({ domains, email, agreeTos, ... }, cb)`
Get certificates for a domain
Example:
```javascript
le.register({
domains: ['example.com', 'www.example.com']
, email: 'user@example.com'
, webrootPath: '/srv/www/example.com/public'
, agreeTos: true
}, function (err, certs) {
// err is some error
console.log(certs);
/*
{ cert: "contents of fullchain.pem"
, key: "contents of privkey.pem"
, renewedAt: <date in milliseconds>
, duration: <duration in milliseconds (90-days)>
}
*/
});
```
### `le.isValidDomain(hostname)`
returns `true` if `hostname` is a valid ascii or punycode domain name.
(also exposed on the main exported module as `LetsEncrypt.isValidDomain()`)
### `le.fetch(args, cb)`
Used internally, but exposed for convenience.
Checks in-memory cache of certificates for `args.domains` and calls then calls `backend.fetch(args, cb)`
**after** merging `args` if necessary.
### `le.registrationFailureCallback(err, args, certInfo, cb)`
Not yet implemented
This is what `args` looks like:
```javascript
{ domains: ['example.com', 'www.example.com']
, email: 'user@email.com'
, agreeTos: true
, configDir: '/etc/letsencrypt'
, fullchainTpl: '/live/:hostname/fullchain.pem' // :hostname will be replaced with the domainname
, privkeyTpl: '/live/:hostname/privkey.pem'
, webrootPathTpl: '/srv/www/:hostname/public'
, webrootPath: '/srv/www/example.com/public' // templated from webrootPathTpl
}
```
This is what the implementation should look like:
(it's expected that the client will follow the same conventions as
the python client, but it's not necessary)
* `.set(opts, domain, key, value, done);` // opts will be saved with domain/key
* `.get(opts, domain, key, done);` // opts will be retrieved by domain/key
* `.remove(opts, domain, key, done);` // opts will be retrieved by domain/key
Change History
==============
* v2.0.0 - Aug 5th 2016
* major refactor
* simplified API
* modular pluigns
* knock out bugs
* v1.5.0 now using letiny-core v2.0.0 and rsa-compat
* v1.4.x I can't remember... but it's better!
* v1.1.0 Added letiny-core, removed node-letsencrypt-python

View File

@ -29,9 +29,6 @@ No, I wanted node-letsencrypt
=============================
Well, take a look at the API in the main README
and you can also check out the [scraps](https://github.com/Daplie/node-letsencrypt/tree/master/scraps).
and you can also check out the code in the repos above.
Feel free to create issues for examples that don't work and pull requests if you fix one.
And please, please, do open an issue. We haven't updated the scrap examples
(hence being moved), but we do have it on the roadmap to bring back some raw API examples.
Feel free to open an issues to request any particular type of example.

64
examples/simple.js Normal file
View File

@ -0,0 +1,64 @@
'use strict';
//var le = require('letsencrypt');
var LE = require('../');
var db = {};
var config = {
server: LE.stagingServerUrl // or LE.productionServerUrl
, configDir: require('homedir')() + '/letsencrypt/etc' // or /etc/letsencrypt or wherever
, privkeyPath: ':config/live/:hostname/privkey.pem' //
, fullchainPath: ':config/live/:hostname/fullchain.pem' // Note: both that :config and :hostname
, certPath: ':config/live/:hostname/cert.pem' // will be templated as expected
, chainPath: ':config/live/:hostname/chain.pem' //
, rsaKeySize: 2048
, debug: true
};
var handlers = {
setChallenge: function (opts, hostname, key, val, cb) { // called during the ACME server handshake, before validation
db[key] = {
hostname: hostname
, key: key
, val: val
};
cb(null);
}
, removeChallenge: function (opts, hostname, key, cb) { // called after validation on both success and failure
db[key] = null;
cb(null);
}
, getChallenge: function (opts, hostname, key, cb) { // this is special because it is called by the webserver
cb(null, db[key].val); // (see letsencrypt-cli/bin & letsencrypt-express/standalone),
// not by the library itself
}
, agreeToTerms: function (tosUrl, cb) { // gives you an async way to expose the legal agreement
cb(null, tosUrl); // (terms of use) to your users before accepting
}
};
var le = LE.create(config, handlers);
// checks :conf/renewal/:hostname.conf
le.register({ // and either renews or registers
domains: ['example.com'] // CHANGE TO YOUR DOMAIN
, email: 'user@email.com' // CHANGE TO YOUR EMAIL
, agreeTos: false // set to true to automatically accept an agreement
// which you have pre-approved (not recommended)
, rsaKeySize: 2048
}, function (err) {
if (err) {
// Note: you must have a webserver running
// and expose handlers.getChallenge to it
// in order to pass validation
// See letsencrypt-cli and or letsencrypt-express
console.error('[Error]: node-letsencrypt/examples/standalone');
console.error(err.stack);
} else {
console.log('success');
}
});

366
index.js
View File

@ -1,246 +1,150 @@
'use strict';
// TODO handle www and no-www together somehow?
var PromiseA = require('bluebird');
var leCore = require('letiny-core');
var merge = require('./lib/common').merge;
var tplCopy = require('./lib/common').tplCopy;
var ACME = require('le-acme-core').ACME;
var LE = module.exports;
LE.productionServerUrl = leCore.productionServerUrl;
LE.stagingServerUrl = leCore.stagingServerUrl;
LE.configDir = leCore.configDir;
LE.logsDir = leCore.logsDir;
LE.workDir = leCore.workDir;
LE.acmeChallengPrefix = leCore.acmeChallengPrefix;
LE.knownEndpoints = leCore.knownEndpoints;
LE.LE = LE;
// in-process cache, shared between all instances
var ipc = {};
LE.privkeyPath = ':config/live/:hostname/privkey.pem';
LE.fullchainPath = ':config/live/:hostname/fullchain.pem';
LE.certPath = ':config/live/:hostname/cert.pem';
LE.chainPath = ':config/live/:hostname/chain.pem';
LE.renewalPath = ':config/renewal/:hostname.conf';
LE.accountsDir = ':config/accounts/:server';
LE.defaults = {
privkeyPath: LE.privkeyPath
, fullchainPath: LE.fullchainPath
, certPath: LE.certPath
, chainPath: LE.chainPath
, renewalPath: LE.renewalPath
, accountsDir: LE.accountsDir
, server: LE.productionServerUrl
productionServerUrl: ACME.productionServerUrl
, stagingServerUrl: ACME.stagingServerUrl
, rsaKeySize: ACME.rsaKeySize || 2048
, challengeType: ACME.challengeType || 'http-01'
, acmeChallengePrefix: ACME.acmeChallengePrefix
};
// backwards compat
LE.stagingServer = leCore.stagingServerUrl;
LE.liveServer = leCore.productionServerUrl;
LE.knownUrls = leCore.knownEndpoints;
Object.keys(LE.defaults).forEach(function (key) {
LE[key] = LE.defaults[key];
});
LE.merge = require('./lib/common').merge;
LE.tplConfigDir = require('./lib/common').tplConfigDir;
// show all possible options
var u; // undefined
LE._undefined = {
acme: u
, store: u
, challenge: u
// backend, defaults, handlers
LE.create = function (defaults, handlers, backend) {
if (!backend) { backend = require('./lib/core'); }
if (!handlers) { handlers = {}; }
if (!handlers.lifetime) { handlers.lifetime = 90 * 24 * 60 * 60 * 1000; }
if (!handlers.renewWithin) { handlers.renewWithin = 3 * 24 * 60 * 60 * 1000; }
if (!handlers.memorizeFor) { handlers.memorizeFor = 1 * 24 * 60 * 60 * 1000; }
if (!handlers.sniRegisterCallback) {
handlers.sniRegisterCallback = function (args, cache, cb) {
// TODO when we have ECDSA, just do this automatically
cb(null, null);
};
}
if (!handlers.getChallenge) {
if (!defaults.manual && !defaults.webrootPath) {
// GET /.well-known/acme-challenge/{{challengeKey}} should return {{tokenValue}}
throw new Error("handlers.getChallenge or defaults.webrootPath must be set");
, register: u
, check: u
, renewWithin: u
, memorizeFor: u
, acmeChallengePrefix: u
, rsaKeySize: u
, challengeType: u
, server: u
, agreeToTerms: u
, _ipc: u
, duplicate: u
, _acmeUrls: u
};
LE._undefine = function (le) {
Object.keys(LE._undefined).forEach(function (key) {
if (!(key in le)) {
le[key] = u;
}
handlers.getChallenge = function (hostname, key, done) {
// TODO associate by hostname?
// hmm... I don't think there's a direct way to associate this with
// the request it came from... it's kinda stateless in that way
// but realistically there only needs to be one handler and one
// "directory" for this. It's not that big of a deal.
var defaultos = LE.merge(defaults, {});
var getChallenge = require('./lib/default-handlers').getChallenge;
var copy = merge(defaults, { domains: [hostname] });
tplCopy(copy);
defaultos.domains = [hostname];
if (3 === getChallenge.length) {
getChallenge(defaultos, key, done);
}
else if (4 === getChallenge.length) {
getChallenge(defaultos, hostname, key, done);
}
else {
done(new Error("handlers.getChallenge [1] receives the wrong number of arguments"));
}
};
}
if (!handlers.setChallenge) {
if (!defaults.webrootPath) {
// GET /.well-known/acme-challenge/{{challengeKey}} should return {{tokenValue}}
throw new Error("handlers.setChallenge or defaults.webrootPath must be set");
}
handlers.setChallenge = require('./lib/default-handlers').setChallenge;
}
if (!handlers.removeChallenge) {
if (!defaults.webrootPath) {
// GET /.well-known/acme-challenge/{{challengeKey}} should return {{tokenValue}}
throw new Error("handlers.removeChallenge or defaults.webrootPath must be set");
}
handlers.removeChallenge = require('./lib/default-handlers').removeChallenge;
}
if (!handlers.agreeToTerms) {
if (defaults.agreeTos) {
console.warn("[WARN] Agreeing to terms by default is risky business...");
}
handlers.agreeToTerms = require('./lib/default-handlers').agreeToTerms;
}
if ('function' === typeof backend.create) {
backend = backend.create(defaults, handlers);
}
else {
// ignore
// this backend was created the v1.0.0 way
}
// replaces strings of workDir, certPath, etc
// if they have :config/etc/live or :conf/etc/archive
// to instead have the path of the configDir
LE.tplConfigDir(defaults.configDir, defaults);
backend = PromiseA.promisifyAll(backend);
var utils = require('./lib/common');
//var attempts = {}; // should exist in master process only
var le;
// TODO check certs on initial load
// TODO expect that certs expire every 90 days
// TODO check certs with setInterval?
//options.cacheContextsFor = options.cacheContextsFor || (1 * 60 * 60 * 1000);
le = {
backend: backend
, pyToJson: function (pyobj) {
if (!pyobj) {
return null;
}
var jsobj = {};
Object.keys(pyobj).forEach(function (key) {
jsobj[key] = pyobj[key];
});
jsobj.__lines = undefined;
jsobj.__keys = undefined;
return jsobj;
}
, register: function (args, cb) {
if (defaults.debug || args.debug) {
console.log('[LE] register');
}
if (!Array.isArray(args.domains)) {
cb(new Error('args.domains should be an array of domains'));
return;
}
var copy = LE.merge(defaults, args);
var err;
if (!utils.isValidDomain(args.domains[0])) {
err = new Error("invalid domain name: '" + args.domains + "'");
err.code = "INVALID_DOMAIN";
cb(err);
return;
}
if ((!args.domains.length && args.domains.every(le.isValidDomain))) {
// NOTE: this library can't assume to handle the http loopback
// (or dns-01 validation may be used)
// so we do not check dns records or attempt a loopback here
cb(new Error("node-letsencrypt: invalid hostnames: " + args.domains.join(',')));
return;
}
if (defaults.debug || args.debug) {
console.log("[NLE]: begin registration");
}
return backend.registerAsync(copy).then(function (pems) {
if (defaults.debug || args.debug) {
console.log("[NLE]: end registration");
}
cb(null, pems);
//return le.fetch(args, cb);
}, cb);
}
, fetch: function (args, cb) {
if (defaults.debug || args.debug) {
console.log('[LE] fetch');
}
return backend.fetchAsync(args).then(function (certInfo) {
if (args.debug) {
console.log('[LE] raw fetch certs', certInfo && Object.keys(certInfo));
}
if (!certInfo) { cb(null, null); return; }
// key, cert, issuedAt, lifetime, expiresAt
if (!certInfo.expiresAt) {
certInfo.expiresAt = certInfo.issuedAt + (certInfo.lifetime || handlers.lifetime);
}
if (!certInfo.lifetime) {
certInfo.lifetime = (certInfo.lifetime || handlers.lifetime);
}
// a pretty good hard buffer
certInfo.expiresAt -= (1 * 24 * 60 * 60 * 100);
cb(null, certInfo);
}, cb);
}
, getConfig: function (args, cb) {
if (defaults.debug || args.debug) {
console.log('[LE] getConfig');
}
backend.getConfigAsync(args).then(function (pyobj) {
cb(null, le.pyToJson(pyobj));
}, function (err) {
console.error("[letsencrypt/index.js] getConfig");
console.error(err.stack);
return cb(null, []);
});
}
, getConfigs: function (args, cb) {
if (defaults.debug || args.debug) {
console.log('[LE] getConfigs');
}
backend.getConfigsAsync(args).then(function (configs) {
cb(null, configs.map(le.pyToJson));
}, function (err) {
if ('ENOENT' === err.code) {
cb(null, []);
} else {
console.error("[letsencrypt/index.js] getConfigs");
console.error(err.stack);
cb(err);
}
});
}
, setConfig: function (args, cb) {
if (defaults.debug || args.debug) {
console.log('[LE] setConfig');
}
backend.configureAsync(args).then(function (pyobj) {
cb(null, le.pyToJson(pyobj));
});
}
};
});
return le;
};
LE.create = function (le) {
var PromiseA = require('bluebird');
le.acme = le.acme || ACME.create({ debug: le.debug });
le.store = le.store || require('le-store-certbot').create({ debug: le.debug });
le.challenge = le.challenge || require('le-challenge-certbot').create({ debug: le.debug });
le.core = require('./lib/core');
le = LE._undefine(le);
le.acmeChallengePrefix = LE.acmeChallengePrefix;
le.rsaKeySize = le.rsaKeySize || LE.rsaKeySize;
le.challengeType = le.challengeType || LE.challengeType;
le._ipc = ipc;
le.agreeToTerms = le.agreeToTerms || function (args, agreeCb) {
agreeCb(new Error("'agreeToTerms' was not supplied to LE and 'agreeTos' was not supplied to LE.register"));
};
if (!le.renewWithin) { le.renewWithin = 3 * 24 * 60 * 60 * 1000; }
if (!le.memorizeFor) { le.memorizeFor = 1 * 24 * 60 * 60 * 1000; }
if (!le.server) {
throw new Error("opts.server must be set to 'staging' or a production url, such as LE.productionServerUrl'");
}
if ('staging' === le.server) {
le.server = LE.stagingServerUrl;
}
else if ('production' === le.server) {
le.server = LE.productionServerUrl;
}
if (le.acme.create) {
le.acme = le.acme.create(le);
}
le.acme = PromiseA.promisifyAll(le.acme);
le._acmeOpts = le.acme.getOptions();
Object.keys(le._acmeOpts).forEach(function (key) {
if (!(key in le)) {
le[key] = le._acmeOpts[key];
}
});
if (le.store.create) {
le.store = le.store.create(le);
}
le.store = PromiseA.promisifyAll(le.store);
le._storeOpts = le.store.getOptions();
Object.keys(le._storeOpts).forEach(function (key) {
if (!(key in le)) {
le[key] = le._storeOpts[key];
}
});
if (le.challenge.create) {
le.challenge = le.challenge.create(le);
}
le.challenge = PromiseA.promisifyAll(le.challenge);
le._challengeOpts = le.challenge.getOptions();
Object.keys(le._challengeOpts).forEach(function (key) {
if (!(key in le)) {
le[key] = le._challengeOpts[key];
}
});
// TODO wrap these here and now with tplCopy?
if (5 !== le.challenge.set.length) {
throw new Error("le.challenge.set receives the wrong number of arguments."
+ " You must define setChallenge as function (opts, domain, key, val, cb) { }");
}
if (4 !== le.challenge.get.length) {
throw new Error("le.challenge.get receives the wrong number of arguments."
+ " You must define getChallenge as function (opts, domain, key, cb) { }");
}
if (4 !== le.challenge.remove.length) {
throw new Error("le.challenge.remove receives the wrong number of arguments."
+ " You must define removeChallenge as function (opts, domain, key, cb) { }");
}
if (le.core.create) {
le.core = le.core.create(le);
}
le.register = function (args) {
return le.core.certificates.getAsync(args);
};
le.check = function (args) {
// TODO must return email, domains, tos, pems
return le.core.certificates.checkAsync(args);
};
le.middleware = le.middleware || require('./lib/middleware');
if (le.middleware.create) {
le.middleware = le.middleware.create(le);
}
return le;
};

View File

@ -1,195 +0,0 @@
'use strict';
var PromiseA = require('bluebird');
var crypto = require('crypto');
var LeCore = require('letiny-core');
var RSA = PromiseA.promisifyAll(require('rsa-compat').RSA);
var path = require('path');
var mkdirpAsync = PromiseA.promisify(require('mkdirp'));
var fs = PromiseA.promisifyAll(require('fs'));
function createAccount(args, handlers) {
var os = require("os");
var localname = os.hostname();
// arg.rsaBitLength args.rsaExponent
return RSA.generateKeypairAsync(args.rsaKeySize || 2048, 65537, { public: true, pem: true }).then(function (keypair) {
return LeCore.registerNewAccountAsync({
email: args.email
, newRegUrl: args._acmeUrls.newReg
, agreeToTerms: function (tosUrl, agree) {
// args.email = email; // already there
args.tosUrl = tosUrl;
handlers.agreeToTerms(args, agree);
}
, accountKeypair: keypair
, debug: args.debug || handlers.debug
}).then(function (body) {
// TODO XXX use sha256 (the python client uses md5)
// TODO ssh fingerprint (noted on rsa-compat issues page, I believe)
keypair.publicKeyMd5 = crypto.createHash('md5').update(RSA.exportPublicPem(keypair)).digest('hex');
keypair.publicKeySha256 = crypto.createHash('sha256').update(RSA.exportPublicPem(keypair)).digest('hex');
var accountId = keypair.publicKeyMd5;
var accountDir = path.join(args.accountsDir, accountId);
var regr = { body: body };
args.accountId = accountId;
args.accountDir = accountDir;
return mkdirpAsync(accountDir).then(function () {
var isoDate = new Date().toISOString();
var accountMeta = {
creation_host: localname
, creation_dt: isoDate
};
// TODO abstract file writing
return PromiseA.all([
// meta.json {"creation_host": "ns1.redirect-www.org", "creation_dt": "2015-12-11T04:14:38Z"}
fs.writeFileAsync(path.join(accountDir, 'meta.json'), JSON.stringify(accountMeta), 'utf8')
// private_key.json { "e", "d", "n", "q", "p", "kty", "qi", "dp", "dq" }
, fs.writeFileAsync(path.join(accountDir, 'private_key.json'), JSON.stringify(RSA.exportPrivateJwk(keypair)), 'utf8')
// regr.json:
/*
{ body: { contact: [ 'mailto:coolaj86@gmail.com' ],
agreement: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf',
key: { e: 'AQAB', kty: 'RSA', n: '...' } },
uri: 'https://acme-v01.api.letsencrypt.org/acme/reg/71272',
new_authzr_uri: 'https://acme-v01.api.letsencrypt.org/acme/new-authz',
terms_of_service: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf' }
*/
, fs.writeFileAsync(path.join(accountDir, 'regr.json'), JSON.stringify(regr), 'utf8')
]).then(function () {
var pems = {};
// pems.private_key;
pems.meta = accountMeta;
pems.keypair = keypair;
pems.regr = regr;
pems.accountId = accountId;
pems.id = accountId;
return pems;
});
});
});
});
}
function getAccount(args, handlers) {
var accountId = args.accountId;
var accountDir = path.join(args.accountsDir, accountId);
var files = {};
var configs = ['meta.json', 'private_key.json', 'regr.json'];
return PromiseA.all(configs.map(function (filename) {
var keyname = filename.slice(0, -5);
return fs.readFileAsync(path.join(accountDir, filename), 'utf8').then(function (text) {
var data;
try {
data = JSON.parse(text);
} catch(e) {
files[keyname] = { error: e };
return;
}
files[keyname] = data;
}, function (err) {
files[keyname] = { error: err };
});
})).then(function () {
if (!Object.keys(files).every(function (key) {
return !files[key].error;
})) {
// TODO log renewal.conf
console.warn("Account '" + accountId + "' was corrupt. No big deal (I think?). Creating a new one...");
//console.log(accountId, files);
return createAccount(args, handlers);
}
var keypair = { privateKeyJwk: files.private_key };
keypair.privateKeyPem = RSA.exportPrivatePem(keypair);
keypair.publicKeyPem = RSA.exportPublicPem(keypair);
//files.private_key;
//files.regr;
//files.meta;
files.accountId = accountId; // preserve current account id
files.id = accountId;
files.keypair = keypair;
return files;
});
}
function getAccountIdByEmail(args) {
// If we read 10,000 account directories looking for
// just one email address, that could get crazy.
// We should have a folder per email and list
// each account as a file in the folder
// TODO
var email = args.email;
if ('string' !== typeof email) {
if (args.debug) {
console.log("[LE] No email given");
}
return PromiseA.resolve(null);
}
return fs.readdirAsync(args.accountsDir).then(function (nodes) {
if (args.debug) {
console.log("[LE] arg.accountsDir success");
}
return PromiseA.all(nodes.map(function (node) {
return fs.readFileAsync(path.join(args.accountsDir, node, 'regr.json'), 'utf8').then(function (text) {
var regr = JSON.parse(text);
regr.__accountId = node;
return regr;
});
})).then(function (regrs) {
var accountId;
/*
if (args.debug) {
console.log('read many regrs');
console.log('regrs', regrs);
}
*/
regrs.some(function (regr) {
return regr.body.contact.some(function (contact) {
var match = contact.toLowerCase() === 'mailto:' + email.toLowerCase();
if (match) {
accountId = regr.__accountId;
return true;
}
});
});
if (!accountId) {
return null;
}
return accountId;
});
}).then(function (accountId) {
return accountId;
}, function (err) {
if ('ENOENT' === err.code) {
// ignore error
return null;
}
return PromiseA.reject(err);
});
}
module.exports.getAccountIdByEmail = getAccountIdByEmail;
module.exports.getAccount = getAccount;
module.exports.createAccount = createAccount;

View File

@ -35,28 +35,66 @@ certInfo.getCertInfo = function (pem) {
return certSimpl;
};
certInfo.getBasicInfo = function (pem) {
var c = certInfo.getCertInfo(pem);
var domains = [];
var sub;
c.extensions.forEach(function (ext) {
if (ext.parsedValue && ext.parsedValue.altNames) {
ext.parsedValue.altNames.forEach(function (alt) {
domains.push(alt.Name);
});
}
});
sub = c.subject.types_and_values[0].value.value_block.value || null;
return {
subject: sub
, altnames: domains
// for debugging during console.log
// do not expect these values to be here
, _issuedAt: c.notBefore.value
, _expiresAt: c.notAfter.value
, issuedAt: new Date(c.notBefore.value).valueOf()
, expiresAt: new Date(c.notAfter.value).valueOf()
};
};
certInfo.getCertInfoFromFile = function (pemFile) {
return require('fs').readFileSync(pemFile, 'ascii');
};
certInfo.testGetCertInfo = function () {
certInfo.testGetCertInfo = function (pathname) {
var path = require('path');
var pemFile = path.join(__dirname, '..', 'tests', 'example.cert.pem');
var pemFile = pathname || path.join(__dirname, '..', 'tests', 'example.cert.pem');
return certInfo.getCertInfo(certInfo.getCertInfoFromFile(pemFile));
};
certInfo.testBasicCertInfo = function (pathname) {
var path = require('path');
var pemFile = pathname || path.join(__dirname, '..', 'tests', 'example.cert.pem');
return certInfo.getBasicInfo(certInfo.getCertInfoFromFile(pemFile));
};
if (require.main === module) {
var c = certInfo.testGetCertInfo();
var c = certInfo.testGetCertInfo(process.argv[2]);
console.log('');
console.info('');
console.log(c.notBefore.value);
console.log(Date(c.notBefore.value).valueOf());
console.info(c.notBefore.value);
console.info(new Date(c.notBefore.value).valueOf());
console.log('');
console.info('');
console.log(c.notAfter.value);
console.log(Date(c.notAfter.value).valueOf());
console.info(c.notAfter.value);
console.info(new Date(c.notAfter.value).valueOf());
console.log('');
console.info('');
var b = certInfo.testBasicCertInfo(process.argv[2]);
console.info('');
console.info(JSON.stringify(b, null, ' '));
console.info('');
}

View File

@ -1,125 +0,0 @@
'use strict';
var fs = require('fs');
var path = require('path');
var PromiseA = require('bluebird');
var homeRe = new RegExp("^~(\\/|\\\|\\" + path.sep + ")");
var re = /^[a-zA-Z0-9\.\-]+$/;
var punycode = require('punycode');
module.exports.isValidDomain = function (domain) {
if (re.test(domain)) {
return domain;
}
domain = punycode.toASCII(domain);
if (re.test(domain)) {
return domain;
}
return '';
};
module.exports.tplConfigDir = function merge(configDir, defaults) {
var homedir = require('homedir')();
Object.keys(defaults).forEach(function (key) {
if ('string' === typeof defaults[key]) {
defaults[key] = defaults[key].replace(':config', configDir).replace(':conf', configDir);
defaults[key] = defaults[key].replace(homeRe, homedir + path.sep);
}
});
};
module.exports.merge = function merge(defaults, args) {
var copy = {};
Object.keys(defaults).forEach(function (key) {
copy[key] = defaults[key];
});
Object.keys(args).forEach(function (key) {
copy[key] = args[key];
});
return copy;
};
module.exports.tplCopy = function merge(copy) {
var homedir = require('homedir')();
var tpls = {
hostname: (copy.domains || [])[0]
, server: (copy.server || '').replace('https://', '').replace(/(\/)$/, '')
, conf: copy.configDir
, config: copy.configDir
};
Object.keys(copy).forEach(function (key) {
if ('string' === typeof copy[key]) {
Object.keys(tpls).sort(function (a, b) {
return b.length - a.length;
}).forEach(function (tplname) {
if (!tpls[tplname]) {
// what can't be templated now may be templatable later
return;
}
copy[key] = copy[key].replace(':' + tplname, tpls[tplname]);
copy[key] = copy[key].replace(homeRe, homedir + path.sep);
});
}
});
//return copy;
};
module.exports.fetchFromDisk = function (args) {
// TODO NO HARD-CODED DEFAULTS
if (!args.fullchainPath || !args.privkeyPath || !args.certPath || !args.chainPath) {
console.warn("missing one or more of args.privkeyPath, args.fullchainPath, args.certPath, args.chainPath");
console.warn("hard-coded conventional pathnames were for debugging and are not a stable part of the API");
}
//, fs.readFileAsync(fullchainPath, 'ascii')
// note: if this ^^ gets added back in, the arrays below must change
return PromiseA.all([
fs.readFileAsync(args.privkeyPath, 'ascii') // 0
, fs.readFileAsync(args.certPath, 'ascii') // 1
, fs.readFileAsync(args.chainPath, 'ascii') // 2
// stat the file, not the link
, fs.statAsync(args.certPath) // 3
]).then(function (arr) {
var cert = arr[1];
var getCertInfo = require('./cert-info').getCertInfo;
// XXX Note: Parsing the certificate info comes at a great cost (~500kb)
var certInfo = getCertInfo(cert);
return {
key: arr[0] // privkey.pem
, privkey: arr[0] // privkey.pem
, fullchain: arr[1] + '\n' + arr[2] // fullchain.pem
, cert: cert // cert.pem
, chain: arr[2] // chain.pem
, ca: arr[2] // chain.pem
, privkeyPath: args.privkeyPath
, fullchainPath: args.fullchainPath
, certPath: args.certPath
, chainPath: args.chainPath
//, issuedAt: arr[3].mtime.valueOf()
, issuedAt: Date(certInfo.notBefore.value).valueOf() // Date.now()
, expiresAt: Date(certInfo.notAfter.value).valueOf()
, lifetime: args.lifetime
};
}, function (err) {
if (args.debug) {
console.error("[letsencrypt/lib/common.js] fetchFromDisk");
console.error(err.stack);
}
return null;
});
};

View File

@ -1,516 +1,366 @@
'use strict';
var PromiseA = require('bluebird');
var RSA = PromiseA.promisifyAll(require('rsa-compat').RSA);
var mkdirpAsync = PromiseA.promisify(require('mkdirp'));
var path = require('path');
var fs = PromiseA.promisifyAll(require('fs'));
var sfs = require('safe-replace');
var LE = require('../');
var LeCore = PromiseA.promisifyAll(require('letiny-core'));
var Accounts = require('./accounts');
var merge = require('./common').merge;
var tplCopy = require('./common').tplCopy;
var fetchFromConfigLiveDir = require('./common').fetchFromDisk;
var ipc = {}; // in-process cache
function getAcmeUrls(args) {
var now = Date.now();
// TODO check response header on request for cache time
if ((now - ipc.acmeUrlsUpdatedAt) < 10 * 60 * 1000) {
return PromiseA.resolve(ipc.acmeUrls);
function log(debug) {
if (debug) {
var args = Array.prototype.slice.call(arguments);
args.shift();
args.unshift("[le/lib/core.js]");
console.log.apply(console, args);
}
return LeCore.getAcmeUrlsAsync(args.server).then(function (data) {
ipc.acmeUrlsUpdatedAt = Date.now();
ipc.acmeUrls = data;
return ipc.acmeUrls;
});
}
function readRenewalConfig(args) {
var pyconf = PromiseA.promisifyAll(require('pyconf'));
module.exports.create = function (le) {
var PromiseA = require('bluebird');
var utils = require('./utils');
var RSA = PromiseA.promisifyAll(require('rsa-compat').RSA);
return pyconf.readFileAsync(args.renewalPath).then(function (pyobj) {
return pyobj;
}, function () {
return pyconf.readFileAsync(path.join(__dirname, 'renewal.conf.tpl')).then(function (pyobj) {
return pyobj;
});
});
}
var core = {
//
// Helpers
//
getAcmeUrlsAsync: function (args) {
var now = Date.now();
function writeRenewalConfig(args) {
function log() {
if (args.debug) {
console.log.apply(console, arguments);
}
}
// TODO check response header on request for cache time
if ((now - le._ipc.acmeUrlsUpdatedAt) < 10 * 60 * 1000) {
return PromiseA.resolve(le._ipc.acmeUrls);
}
var pyobj = args.pyobj;
pyobj.checkpoints = parseInt(pyobj.checkpoints, 10) || 0;
return le.acme.getAcmeUrlsAsync(args.server).then(function (data) {
le._ipc.acmeUrlsUpdatedAt = Date.now();
le._ipc.acmeUrls = data;
var pyconf = PromiseA.promisifyAll(require('pyconf'));
var liveDir = args.liveDir || path.join(args.configDir, 'live', args.domains[0]);
var certPath = args.certPath || pyobj.cert || path.join(liveDir, 'cert.pem');
var fullchainPath = args.fullchainPath || pyobj.fullchain || path.join(liveDir, 'fullchain.pem');
var chainPath = args.chainPath || pyobj.chain || path.join(liveDir, 'chain.pem');
var privkeyPath = args.privkeyPath || pyobj.privkey
//|| args.domainPrivateKeyPath || args.domainKeyPath || pyobj.keyPath
|| path.join(liveDir, 'privkey.pem');
log('[le/core.js] privkeyPath', privkeyPath);
var updates = {
account: args.account.id
, configDir: args.configDir
, domains: args.domains
, email: args.email
, tos: args.agreeTos && true
// yes, it's an array. weird, right?
, webrootPath: args.webrootPath && [args.webrootPath] || []
, server: args.server || args.acmeDiscoveryUrl
, privkey: privkeyPath
, fullchain: fullchainPath
, cert: certPath
, chain: chainPath
, http01Port: args.http01Port
, keyPath: args.domainPrivateKeyPath || args.privkeyPath
, rsaKeySize: args.rsaKeySize
, checkpoints: pyobj.checkpoints
/* // TODO XXX what's the deal with these? they don't make sense
// are they just old junk? or do they have a meaning that I don't know about?
, fullchainPath: path.join(args.configDir, 'chain.pem')
, certPath: path.join(args.configDir, 'cert.pem')
, chainPath: path.join(args.configDir, 'chain.pem')
*/ // TODO XXX end
, workDir: args.workDir
, logsDir: args.logsDir
};
// final section is completely dynamic
// :hostname = :webroot_path
args.domains.forEach(function (hostname) {
updates[hostname] = args.webrootPath;
});
// must write back to the original pyobject or
// annotations will be lost
Object.keys(updates).forEach(function (key) {
pyobj[key] = updates[key];
});
return mkdirpAsync(path.dirname(args.renewalPath)).then(function () {
return pyconf.writeFileAsync(args.renewalPath, pyobj);
}).then(function () {
// NOTE
// writing twice seems to causes a bug,
// so instead we re-read the file from the disk
return pyconf.readFileAsync(args.renewalPath);
});
}
function getOrCreateRenewal(args) {
return readRenewalConfig(args).then(function (pyobj) {
var minver = pyobj.checkpoints >= 0;
args.pyobj = pyobj;
if (!minver) {
args.checkpoints = 0;
pyobj.checkpoints = 0;
return writeRenewalConfig(args);
return le._ipc.acmeUrls;
});
}
// args.account.id = pyobj.account
// args.configDir = args.configDir || pyobj.configDir;
args.checkpoints = pyobj.checkpoints;
//
// The Main Enchilada
//
args.agreeTos = (args.agreeTos || pyobj.tos) && true;
args.email = args.email || pyobj.email;
args.domains = args.domains || pyobj.domains;
//
// Accounts
//
, accounts: {
// Accounts
registerAsync: function (args) {
var err;
var copy = utils.merge(args, le);
var disagreeTos;
args = utils.tplCopy(copy);
// yes, it's an array. weird, right?
args.webrootPath = args.webrootPath || pyobj.webrootPath[0];
args.server = args.server || args.acmeDiscoveryUrl || pyobj.server;
disagreeTos = (!args.agreeTos && 'undefined' !== typeof args.agreeTos);
if (!args.email || disagreeTos || (parseInt(args.rsaKeySize, 10) < 2048)) {
err = new Error(
"In order to register an account both 'email' and 'agreeTos' must be present"
+ " and 'rsaKeySize' must be 2048 or greater."
);
err.code = 'E_ARGS';
return PromiseA.reject(err);
}
args.certPath = args.certPath || pyobj.cert;
args.privkeyPath = args.privkeyPath || pyobj.privkey;
args.chainPath = args.chainPath || pyobj.chain;
args.fullchainPath = args.fullchainPath || pyobj.fullchain;
return utils.testEmail(args.email).then(function () {
var keypairOpts = { public: true, pem: true };
//, workDir: args.workDir
//, logsDir: args.logsDir
args.rsaKeySize = args.rsaKeySize || pyobj.rsaKeySize;
args.http01Port = args.http01Port || pyobj.http01Port;
args.domainKeyPath = args.domainPrivateKeyPath || args.domainKeyPath || args.keyPath || pyobj.keyPath;
var promise = le.store.accounts.checkKeypairAsync(args).then(function (keypair) {
if (keypair) {
return RSA.import(keypair);
}
return writeRenewalConfig(args);
});
}
if (args.accountKeypair) {
return le.store.accounts.setKeypairAsync(args, RSA.import(args.accountKeypair));
}
function writeCertificateAsync(args, defaults, handlers) {
function log() {
if (args.debug) {
console.log.apply(console, arguments);
}
}
return RSA.generateKeypairAsync(args.rsaKeySize, 65537, keypairOpts).then(function (keypair) {
keypair.privateKeyPem = RSA.exportPrivatePem(keypair);
keypair.publicKeyPem = RSA.exportPublicPem(keypair);
keypair.privateKeyJwk = RSA.exportPrivateJwk(keypair);
return le.store.accounts.setKeypairAsync(args, keypair);
});
});
log("[le/core.js] got certificate!");
return promise.then(function (keypair) {
// Note: the ACME urls are always fetched fresh on purpose
// TODO is this the right place for this?
return core.getAcmeUrlsAsync(args).then(function (urls) {
args._acmeUrls = urls;
var obj = args.pyobj;
var result = args.pems;
return le.acme.registerNewAccountAsync({
email: args.email
, newRegUrl: args._acmeUrls.newReg
, agreeToTerms: function (tosUrl, agreeCb) {
if (true === args.agreeTos || tosUrl === args.agreeTos || tosUrl === le.agreeToTerms) {
agreeCb(null, tosUrl);
return;
}
result.fullchain = result.cert + '\n' + (result.chain || result.ca);
obj.checkpoints = parseInt(obj.checkpoints, 10) || 0;
// args.email = email; // already there
// args.domains = domains // already there
args.tosUrl = tosUrl;
le.agreeToTerms(args, agreeCb);
}
, accountKeypair: keypair
var liveDir = args.liveDir || path.join(args.configDir, 'live', args.domains[0]);
, debug: le.debug || args.debug
}).then(function (receipt) {
var reg = {
keypair: keypair
, receipt: receipt
, email: args.email
};
var certPath = args.certPath || obj.cert || path.join(liveDir, 'cert.pem');
var fullchainPath = args.fullchainPath || obj.fullchain || path.join(liveDir, 'fullchain.pem');
var chainPath = args.chainPath || obj.chain || path.join(liveDir, 'chain.pem');
var privkeyPath = args.privkeyPath || obj.privkey
//|| args.domainPrivateKeyPath || args.domainKeyPath || obj.keyPath
|| path.join(liveDir, 'privkey.pem');
log('[le/core.js] privkeyPath', privkeyPath);
var archiveDir = args.archiveDir || path.join(args.configDir, 'archive', args.domains[0]);
var checkpoints = obj.checkpoints.toString();
var certArchive = path.join(archiveDir, 'cert' + checkpoints + '.pem');
var fullchainArchive = path.join(archiveDir, 'fullchain' + checkpoints + '.pem');
var chainArchive = path.join(archiveDir, 'chain'+ checkpoints + '.pem');
var privkeyArchive = path.join(archiveDir, 'privkey' + checkpoints + '.pem');
return mkdirpAsync(archiveDir).then(function () {
return PromiseA.all([
sfs.writeFileAsync(certArchive, result.cert, 'ascii')
, sfs.writeFileAsync(chainArchive, (result.chain || result.ca), 'ascii')
, sfs.writeFileAsync(fullchainArchive, result.fullchain, 'ascii')
, sfs.writeFileAsync(
privkeyArchive
// TODO nix args.key, args.domainPrivateKeyPem ??
, (result.privkey || result.key) || RSA.exportPrivatePem(args.domainKeypair)
, 'ascii'
)
]);
}).then(function () {
return mkdirpAsync(liveDir);
}).then(function () {
return PromiseA.all([
sfs.writeFileAsync(certPath, result.cert, 'ascii')
, sfs.writeFileAsync(chainPath, (result.chain || result.ca), 'ascii')
, sfs.writeFileAsync(fullchainPath, result.fullchain, 'ascii')
, sfs.writeFileAsync(
privkeyPath
// TODO nix args.key, args.domainPrivateKeyPem ??
, (result.privkey || result.key) || RSA.exportPrivatePem(args.domainKeypair)
, 'ascii'
)
]);
}).then(function () {
obj.checkpoints += 1;
args.checkpoints += 1;
return writeRenewalConfig(args);
}).then(function () {
var getCertInfo = require('./cert-info').getCertInfo;
// XXX Note: Parsing the certificate info comes at a great cost (~500kb)
var certInfo = getCertInfo(result.cert);
return {
certPath: certPath
, chainPath: chainPath
, fullchainPath: fullchainPath
, privkeyPath: privkeyPath
// TODO nix keypair
, keypair: args.domainKeypair
// TODO nix args.key, args.domainPrivateKeyPem ??
// some ambiguity here...
, privkey: (result.privkey || result.key) || RSA.exportPrivatePem(args.domainKeypair)
, fullchain: result.fullchain || (result.cert + '\n' + result.chain)
, chain: (result.chain || result.ca)
// especially this one... might be cert only, might be fullchain
, cert: result.cert
, issuedAt: Date(certInfo.notBefore.value).valueOf() // Date.now()
, expiresAt: Date(certInfo.notAfter.value).valueOf()
, lifetime: defaults.lifetime || handlers.lifetime
};
});
}
function getCertificateAsync(args, defaults, handlers) {
function log() {
if (args.debug || defaults.debug) {
console.log.apply(console, arguments);
}
}
var account = args.account;
var promise;
var keypairOpts = { public: true, pem: true };
log('[le/core.js] domainKeyPath:', args.domainKeyPath);
promise = fs.readFileAsync(args.domainKeyPath, 'ascii').then(function (pem) {
return RSA.import({ privateKeyPem: pem });
}, function (/*err*/) {
return RSA.generateKeypairAsync(args.rsaKeySize, 65537, keypairOpts).then(function (keypair) {
return mkdirpAsync(path.dirname(args.domainKeyPath)).then(function () {
return fs.writeFileAsync(args.domainKeyPath, keypair.privateKeyPem, 'ascii').then(function () {
return keypair;
// TODO move templating of arguments to right here?
return le.store.accounts.setAsync(args, reg).then(function (account) {
// should now have account.id and account.accountId
args.account = account;
args.accountId = account.id;
return account;
});
});
});
});
});
});
});
});
return promise.then(function (domainKeypair) {
log("[le/core.js] get certificate");
args.domainKeypair = domainKeypair;
//args.registration = domainKey;
return LeCore.getCertificateAsync({
debug: args.debug
, newAuthzUrl: args._acmeUrls.newAuthz
, newCertUrl: args._acmeUrls.newCert
, accountKeypair: RSA.import(account.keypair)
, domainKeypair: domainKeypair
, domains: args.domains
//
// IMPORTANT
//
// setChallenge and removeChallenge are handed defaults
// instead of args because getChallenge does not have
// access to args
// (args is per-request, defaults is per instance)
//
, setChallenge: function (domain, key, value, done) {
var copy = merge(defaults, { domains: [domain] });
tplCopy(copy);
args.domains = [domain];
args.webrootPath = args.webrootPath;
if (4 === handlers.setChallenge.length) {
handlers.setChallenge(copy, key, value, done);
}
else if (5 === handlers.setChallenge.length) {
handlers.setChallenge(copy, domain, key, value, done);
}
else {
done(new Error("handlers.setChallenge receives the wrong number of arguments"));
}
}
, removeChallenge: function (domain, key, done) {
var copy = merge(defaults, { domains: [domain] });
tplCopy(copy);
if (3 === handlers.removeChallenge.length) {
handlers.removeChallenge(copy, key, done);
}
else if (4 === handlers.removeChallenge.length) {
handlers.removeChallenge(copy, domain, key, done);
}
else {
done(new Error("handlers.removeChallenge receives the wrong number of arguments"));
}
}
});
}).then(function (results) {
// { cert, chain, fullchain, privkey }
args.pems = results;
return writeCertificateAsync(args, defaults, handlers);
});
}
function getOrCreateDomainCertificate(args, defaults, handlers) {
if (args.duplicate) {
// we're forcing a refresh via 'dupliate: true'
return getCertificateAsync(args, defaults, handlers);
}
return fetchFromConfigLiveDir(args).then(function (certs) {
var halfLife = (certs.expiresAt - certs.issuedAt) / 2;
if (!certs || (Date.now() - certs.issuedAt) > halfLife) {
// There is no cert available
// Or the cert is more than half-expired
return getCertificateAsync(args, defaults, handlers);
}
return PromiseA.reject(new Error(
"[ERROR] Certificate issued at '"
+ new Date(certs.issuedAt).toISOString() + "' and expires at '"
+ new Date(certs.expiresAt).toISOString() + "'. Ignoring renewal attempt until half-life at '"
+ new Date(certs.issuedA + halfLife).toISOString() + "'. Set { duplicate: true } to force."
));
});
}
// returns 'account' from lib/accounts { meta, regr, keypair, accountId (id) }
function getOrCreateAcmeAccount(args, defaults, handlers) {
function log() {
if (args.debug) {
console.log.apply(console, arguments);
}
}
var pyconf = PromiseA.promisifyAll(require('pyconf'));
return pyconf.readFileAsync(args.renewalPath).then(function (renewal) {
var accountId = renewal.account;
renewal = renewal.account;
return accountId;
}, function (err) {
if ("ENOENT" === err.code) {
log("[le/core.js] try email");
return Accounts.getAccountIdByEmail(args, handlers);
}
return PromiseA.reject(err);
}).then(function (accountId) {
// Note: the ACME urls are always fetched fresh on purpose
return getAcmeUrls(args).then(function (urls) {
args._acmeUrls = urls;
if (accountId) {
log('[le/core.js] use account');
args.accountId = accountId;
return Accounts.getAccount(args, handlers);
} else {
log('[le/core.js] create account');
return Accounts.createAccount(args, handlers);
}
});
}).then(function (account) {
/*
if (renewal.account !== account) {
// the account has become corrupt, re-register
return;
}
*/
log('[le/core.js] created account');
return account;
});
/*
return fs.readdirAsync(accountsDir, function (nodes) {
return PromiseA.all(nodes.map(function (node) {
var reMd5 = /[a-f0-9]{32}/i;
if (reMd5.test(node)) {
}
}));
});
*/
}
module.exports.create = function (defaults, handlers) {
defaults.server = defaults.server || LE.liveServer;
var wrapped = {
registerAsync: function (args) {
var copy;
// TODO move these defaults elsewhere?
//args.renewalDir = args.renewalDir || ':config/renewal/';
args.renewalPath = args.renewalPath || ':config/renewal/:hostname.conf';
// Note: the /directory is part of the server url and, as such, bleeds into the pathname
// So :config/accounts/:server/directory is *incorrect*, but the following *is* correct:
args.accountsDir = args.accountsDir || ':config/accounts/:server';
copy = merge(args, defaults);
tplCopy(copy);
var url = require('url');
var acmeLocation = url.parse(copy.server);
var acmeHostpath = path.join(acmeLocation.hostname, acmeLocation.pathname);
copy.renewalPath = copy.renewalPath || path.join(copy.configDir, 'renewal', copy.domains[0] + '.conf');
copy.accountsDir = copy.accountsDir || path.join(copy.configDir, 'accounts', acmeHostpath);
return getOrCreateAcmeAccount(copy, defaults, handlers).then(function (account) {
copy.account = account;
return getOrCreateRenewal(copy).then(function (pyobj) {
copy.pyobj = pyobj;
return getOrCreateDomainCertificate(copy, defaults, handlers);
// Accounts
, getAsync: function (args) {
return core.accounts.checkAsync(args).then(function (account) {
if (account) {
return account;
} else {
return core.accounts.registerAsync(args);
}
});
}).then(function (result) {
return result;
}, function (err) {
return PromiseA.reject(err);
});
}
// Accounts
, checkAsync: function (args) {
var requiredArgs = ['accountId', 'email', 'domains', 'domain'];
if (!requiredArgs.some(function (key) { return -1 !== Object.keys(args).indexOf(key); })) {
return PromiseA.reject(new Error(
"In order to register or retrieve an account one of '" + requiredArgs.join("', '") + "' must be present"
));
}
var copy = utils.merge(args, le);
args = utils.tplCopy(copy);
return le.store.accounts.checkAsync(args).then(function (account) {
if (!account) {
return null;
}
args.account = account;
args.accountId = account.id;
return account;
});
}
}
, fetchAsync: function (args) {
var copy = merge(args, defaults);
tplCopy(copy);
return fetchFromConfigLiveDir(copy, defaults);
}
, configureAsync: function (hargs) {
hargs.renewalPath = hargs.renewalPath || ':config/renewal/:hostname.conf';
var copy = merge(hargs, defaults);
tplCopy(copy);
, certificates: {
// Certificates
registerAsync: function (args) {
var err;
var copy = utils.merge(args, le);
args = utils.tplCopy(copy);
return getOrCreateAcmeAccount(copy, defaults, handlers).then(function (account) {
copy.account = account;
return getOrCreateRenewal(copy);
});
}
, getConfigAsync: function (hargs) {
hargs.renewalPath = hargs.renewalPath || ':config/renewal/:hostname.conf';
hargs.domains = [];
if (!Array.isArray(args.domains)) {
return PromiseA.reject(new Error('args.domains should be an array of domains'));
}
var copy = merge(hargs, defaults);
tplCopy(copy);
if (!(args.domains.length && args.domains.every(utils.isValidDomain))) {
// NOTE: this library can't assume to handle the http loopback
// (or dns-01 validation may be used)
// so we do not check dns records or attempt a loopback here
err = new Error("invalid domain name(s): '" + args.domains + "'");
err.code = "INVALID_DOMAIN";
return PromiseA.reject(err);
}
// TODO renewal cb
// accountId and or email
return core.accounts.getAsync(copy).then(function (account) {
copy.account = account;
//var account = args.account;
var keypairOpts = { public: true, pem: true };
var promise = le.store.certificates.checkKeypairAsync(args).then(function (keypair) {
if (keypair) {
return RSA.import(keypair);
}
if (args.domainKeypair) {
return le.store.certificates.setKeypairAsync(args, RSA.import(args.domainKeypair));
}
return RSA.generateKeypairAsync(args.rsaKeySize, 65537, keypairOpts).then(function (keypair) {
keypair.privateKeyPem = RSA.exportPrivatePem(keypair);
keypair.publicKeyPem = RSA.exportPublicPem(keypair);
keypair.privateKeyJwk = RSA.exportPrivateJwk(keypair);
return le.store.certificates.setKeypairAsync(args, keypair);
});
});
return promise.then(function (domainKeypair) {
args.domainKeypair = domainKeypair;
//args.registration = domainKey;
// Note: the ACME urls are always fetched fresh on purpose
// TODO is this the right place for this?
return core.getAcmeUrlsAsync(args).then(function (urls) {
args._acmeUrls = urls;
var certReq = {
debug: args.debug || le.debug
, newAuthzUrl: args._acmeUrls.newAuthz
, newCertUrl: args._acmeUrls.newCert
, accountKeypair: RSA.import(account.keypair)
, domainKeypair: domainKeypair
, domains: args.domains
, challengeType: args.challengeType
};
//
// IMPORTANT
//
// setChallenge and removeChallenge are handed defaults
// instead of args because getChallenge does not have
// access to args
// (args is per-request, defaults is per instance)
//
// Each of these fires individually for each domain,
// even though the certificate on the whole may have many domains
//
certReq.setChallenge = function (domain, key, value, done) {
log(args.debug, "setChallenge called for '" + domain + "'");
var copy = utils.merge({ domains: [domain] }, le);
utils.tplCopy(copy);
le.challenge.set(copy, domain, key, value, done);
};
certReq.removeChallenge = function (domain, key, done) {
log(args.debug, "setChallenge called for '" + domain + "'");
var copy = utils.merge({ domains: [domain] }, le);
utils.tplCopy(copy);
le.challenge.remove(copy, domain, key, done);
};
log(args.debug, 'BEFORE GET CERT');
log(args.debug, certReq);
return le.acme.getCertificateAsync(certReq).then(utils.attachCertInfo);
});
}).then(function (results) {
// { cert, chain, privkey }
args.pems = results;
return le.store.certificates.setAsync(args).then(function () {
return results;
});
});
});
}
// Certificates
, renewAsync: function (args, certs) {
var renewableAt = core.certificates._getRenewableAt(args, certs);
var err;
//var halfLife = (certs.expiresAt - certs.issuedAt) / 2;
//var renewable = (Date.now() - certs.issuedAt) > halfLife;
log(args.debug, "(Renew) Expires At", new Date(certs.expiresAt).toISOString());
log(args.debug, "(Renew) Renewable At", new Date(renewableAt).toISOString());
if (!args.duplicate && Date.now() < renewableAt) {
err = new Error(
"[ERROR] Certificate issued at '"
+ new Date(certs.issuedAt).toISOString() + "' and expires at '"
+ new Date(certs.expiresAt).toISOString() + "'. Ignoring renewal attempt until '"
+ new Date(renewableAt).toISOString() + "'. Set { duplicate: true } to force."
);
err.code = 'E_NOT_RENEWABLE';
return PromiseA.reject(err);
}
// Either the cert has entered its renewal period
// or we're forcing a refresh via 'dupliate: true'
log(args.debug, "Renewing!");
// TODO fetch email address / accountId (accountBydomain) if not present
// store.config.getAsync(args.domains).then(function (config) { /*...*/ });
if (!args.domains || (args.domains.length || 0) <= 2) {
// this is a renewal, therefore we should renewal ALL of the domains
// associated with this certificate, unless args.domains is a list larger
// than example.com,www.example.com
// TODO check www. prefix
args.domains = certs.altnames;
if (Array.isArray(certs.domains) && certs.domains.length) {
args.domains = certs.domains;
}
}
return core.certificates.registerAsync(args);
}
// Certificates
, _isRenewable: function (args, certs) {
var renewableAt = core.certificates._getRenewableAt(args, certs);
log(args.debug, "Check Expires At", new Date(certs.expiresAt).toISOString());
log(args.debug, "Check Renewable At", new Date(renewableAt).toISOString());
if (args.duplicate || Date.now() >= renewableAt) {
return true;
}
return false;
}
, _getRenewableAt: function (args, certs) {
return certs.expiresAt - (args.renewWithin || le.renewWithin);
}
, checkAsync: function (args) {
var copy = utils.merge(args, le);
utils.tplCopy(copy);
// returns pems
return le.store.certificates.checkAsync(copy).then(function (cert) {
if (cert) {
return utils.attachCertInfo(cert);
}
return readRenewalConfig(copy).then(function (pyobj) {
var exists = pyobj.checkpoints >= 0;
if (!exists) {
return null;
}
return pyobj;
});
}
, getConfigsAsync: function (hargs) {
hargs.renewalDir = hargs.renewalDir || ':config/renewal/';
hargs.renewalPath = hargs.renewalPath || ':config/renewal/:hostname.conf';
hargs.domains = [];
var copy = merge(hargs, defaults);
tplCopy(copy);
return fs.readdirAsync(copy.renewalDir).then(function (nodes) {
nodes = nodes.filter(function (node) {
return /^[a-z0-9]+.*\.conf$/.test(node);
});
}
// Certificates
, getAsync: function (args) {
var copy = utils.merge(args, le);
args = utils.tplCopy(copy);
return PromiseA.all(nodes.map(function (node) {
copy.domains = [node.replace(/\.conf$/, '')];
return wrapped.getConfigAsync(copy);
}));
});
return core.certificates.checkAsync(args).then(function (certs) {
if (!certs) {
// There is no cert available
log(args.debug, "no certificate found");
return core.certificates.registerAsync(args);
}
if (core.certificates._isRenewable(args, certs)) {
certs._renewing = core.certificates.renewAsync(args, certs);
}
return certs;
}).then(function (results) {
// returns pems
return results;
});
}
}
};
return wrapped;
return core;
};

View File

@ -1,40 +0,0 @@
'use strict';
var fs = require('fs');
var path = require('path');
module.exports.agreeToTerms = function (args, agree) {
agree(null, args.agreeTos);
};
module.exports.setChallenge = function (args, challengePath, keyAuthorization, done) {
//var hostname = args.domains[0];
var mkdirp = require('mkdirp');
// TODO should be args.webrootPath
//console.log('args.webrootPath, challengePath');
//console.log(args.webrootPath, challengePath);
mkdirp(args.webrootPath, function (err) {
if (err) {
done(err);
return;
}
fs.writeFile(path.join(args.webrootPath, challengePath), keyAuthorization, 'utf8', function (err) {
done(err);
});
});
};
module.exports.getChallenge = function (args, key, done) {
//var hostname = args.domains[0];
//console.log("getting the challenge", args, key);
fs.readFile(path.join(args.webrootPath, key), 'utf8', done);
};
module.exports.removeChallenge = function (args, key, done) {
//var hostname = args.domains[0];
fs.unlink(path.join(args.webrootPath, key), done);
};

54
lib/middleware.js Normal file
View File

@ -0,0 +1,54 @@
'use strict';
var utils = require('./utils');
function log(debug) {
if (debug) {
var args = Array.prototype.slice.call(arguments);
args.shift();
args.unshift("[le/lib/middleware.js]");
console.log.apply(console, args);
}
}
module.exports.create = function (le) {
if (!le.challenge || !le.challenge.get) {
throw new Error("middleware requires challenge plugin with get method");
}
log(le.debug, "created middleware");
return function () {
var prefix = le.acmeChallengePrefix; // /.well-known/acme-challenge/:token
return function (req, res, next) {
if (0 !== req.url.indexOf(prefix)) {
log(le.debug, "no match, skipping middleware");
next();
return;
}
log(le.debug, "this must be tinder, 'cuz it's a match!");
var token = req.url.slice(prefix.length);
var hostname = req.hostname || (req.headers.host || '').toLowerCase().replace(/:.*/, '');
log(le.debug, "hostname", hostname, "token", token);
var copy = utils.merge({ domains: [ hostname ] }, le);
copy = utils.tplCopy(copy);
// TODO tpl copy?
le.challenge.get(copy, hostname, token, function (err, secret) {
if (err || !token) {
res.statusCode = 404;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end('{ "error": { "message": "Error: These aren\'t the tokens you\'re looking for. Move along." } }');
return;
}
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.end(secret);
});
};
};
};

View File

@ -1,68 +0,0 @@
#cert = :config/live/:hostname/cert.pem
cert = :cert_path
privkey = :privkey_path
chain = :chain_path
fullchain = :fullchain_path
# Options and defaults used in the renewal process
[renewalparams]
apache_enmod = a2enmod
no_verify_ssl = False
ifaces = None
apache_dismod = a2dismod
register_unsafely_without_email = False
uir = None
installer = none
config_dir = :config
text_mode = True
# junk?
# https://github.com/letsencrypt/letsencrypt/issues/1955
func = <function obtain_cert at 0x30c9500>
prepare = False
work_dir = :work_dir
tos = :agree_tos
init = False
http01_port = :http_01_port
duplicate = False
# this is for the domain
key_path = :privkey_path
nginx = False
fullchain_path = :fullchain_path
email = :email
csr = None
agree_dev_preview = None
redirect = None
verbose_count = -3
config_file = None
renew_by_default = True
hsts = False
authenticator = webroot
domains = :hostnames #comma,delimited,list
rsa_key_size = :rsa_key_size
# starts at 0 and increments at every renewal
checkpoints = -1
manual_test_mode = False
apache = False
cert_path = :cert_path
webroot_path = :webroot_paths # comma,delimited,list
strict_permissions = False
apache_server_root = /etc/apache2
# https://github.com/letsencrypt/letsencrypt/issues/1948
account = :account_id
manual_public_ip_logging_ok = False
chain_path = :chain_path
standalone = False
manual = False
server = :acme_discovery_url
standalone_supported_challenges = "http-01,tls-sni-01"
webroot = True
apache_init_script = None
user_agent = None
apache_ctl = apache2ctl
apache_le_vhost_ext = -le-ssl.conf
debug = False
tls_sni_01_port = 443
logs_dir = :logs_dir
configurator = None
[[webroot_map]]
# :hostname = :webroot_path

126
lib/utils.js Normal file
View File

@ -0,0 +1,126 @@
'use strict';
var path = require('path');
var homeRe = new RegExp("^~(\\/|\\\|\\" + path.sep + ")");
var re = /^[a-zA-Z0-9\.\-]+$/;
var punycode = require('punycode');
var PromiseA = require('bluebird');
var dns = PromiseA.promisifyAll(require('dns'));
module.exports.attachCertInfo = function (results) {
var getCertInfo = require('./cert-info').getBasicInfo;
// XXX Note: Parsing the certificate info comes at a great cost (~500kb)
var certInfo = getCertInfo(results.cert);
// subject, altnames, issuedAt, expiresAt
Object.keys(certInfo).forEach(function (key) {
results[key] = certInfo[key];
});
return results;
};
module.exports.isValidDomain = function (domain) {
if (re.test(domain)) {
return domain;
}
domain = punycode.toASCII(domain);
if (re.test(domain)) {
return domain;
}
return '';
};
module.exports.merge = function (/*defaults, args*/) {
var allDefaults = Array.prototype.slice.apply(arguments);
var args = allDefaults.shift();
var copy = {};
allDefaults.forEach(function (defaults) {
Object.keys(defaults).forEach(function (key) {
copy[key] = defaults[key];
});
});
Object.keys(args).forEach(function (key) {
copy[key] = args[key];
});
return copy;
};
module.exports.tplCopy = function (copy) {
var homedir = require('homedir')();
var tplKeys;
copy.hostnameGet = function (copy) {
return (copy.domains || [])[0] || copy.domain;
};
Object.keys(copy).forEach(function (key) {
var newName;
if (!/Get$/.test(key)) {
return;
}
newName = key.replace(/Get$/, '');
copy[newName] = copy[newName] || copy[key](copy);
});
tplKeys = Object.keys(copy);
tplKeys.sort(function (a, b) {
return b.length - a.length;
});
tplKeys.forEach(function (key) {
if ('string' !== typeof copy[key]) {
return;
}
copy[key] = copy[key].replace(homeRe, homedir + path.sep);
});
tplKeys.forEach(function (key) {
if ('string' !== typeof copy[key]) {
return;
}
tplKeys.forEach(function (tplname) {
if (!copy[tplname]) {
// what can't be templated now may be templatable later
return;
}
copy[key] = copy[key].replace(':' + tplname, copy[tplname]);
});
});
return copy;
};
module.exports.testEmail = function (email) {
var parts = (email||'').split('@');
var err;
if (2 !== parts.length || !parts[0] || !parts[1]) {
err = new Error("malformed email address '" + email + "'");
err.code = 'E_EMAIL';
return PromiseA.reject(err);
}
return dns.resolveMxAsync(parts[1]).then(function (records) {
// records only returns when there is data
if (!records.length) {
throw new Error("sanity check fail: success, but no MX records returned");
}
return email;
}, function (err) {
if ('ENODATA' === err.code) {
err = new Error("no MX records found for '" + parts[1] + "'");
err.code = 'E_EMAIL';
return PromiseA.reject(err);
}
});
};

View File

@ -1,6 +1,6 @@
{
"name": "letsencrypt",
"version": "1.5.1",
"version": "2.0.1",
"description": "Let's Encrypt for node.js on npm",
"main": "index.js",
"scripts": {
@ -29,19 +29,17 @@
"url": "https://github.com/Daplie/node-letsencrypt/issues"
},
"homepage": "https://github.com/Daplie/node-letsencrypt#readme",
"devDependencies": {
"express": "^4.13.3",
"localhost.daplie.com-certificates": "^1.1.2"
},
"devDependencies": {},
"optionalDependencies": {},
"dependencies": {
"asn1": "^0.2.3",
"bluebird": "^3.0.6",
"homedir": "^0.6.0",
"letiny-core": "^2.0.1",
"mkdirp": "^0.5.1",
"pyconf": "^1.1.2",
"request": "^2.67.0",
"rsa-compat": "^1.2.1",
"safe-replace": "^1.0.2"
"le-acme-core": "^2.0.5",
"le-challenge-fs": "^2.0.2",
"le-store-certbot": "^2.0.1",
"node.extend": "^1.1.5",
"pkijs": "^1.3.27",
"rsa-compat": "^1.2.1"
}
}

View File

@ -1,9 +0,0 @@
moved the tests to the examples folder
```bash
node examples/commandline.js example.com,www.example.com user@example.com agree
```
Try it for yourself.
Go watch [Let's Encrypt in (exactly) 90 seconds](https://daplie.com/articles/lets-encrypt-in-literally-90-seconds/) and swap out the Caddy instructions with the node instructions.

View File

@ -1 +0,0 @@
hello

27
tests/cert-info.js Normal file
View File

@ -0,0 +1,27 @@
'use strict';
var certInfo = require('../lib/cert-info.js');
var c = certInfo.testGetCertInfo();
console.info('');
console.info(c.notBefore.value);
console.info(new Date(c.notBefore.value).valueOf());
console.info('');
console.info(c.notAfter.value);
console.info(new Date(c.notAfter.value).valueOf());
console.info('');
var json = certInfo.testBasicCertInfo();
console.log('');
console.log(JSON.stringify(json, null, ' '));
console.log('');
console.info('');
console.info('If we got values at all, it must have passed.');
console.info('');

View File

@ -0,0 +1,106 @@
'use strict';
var PromiseA = require('bluebird');
var path = require('path');
var requestAsync = PromiseA.promisify(require('request'));
var LE = require('../').LE;
var le = LE.create({
server: 'staging'
, acme: require('le-acme-core').ACME.create()
, store: require('le-store-certbot').create({
configDir: '~/letsencrypt.test/etc'.split('/').join(path.sep)
, webrootPath: '~/letsencrypt.test/var/:hostname'.split('/').join(path.sep)
})
, challenge: require('le-challenge-fs').create({
webrootPath: '~/letsencrypt.test/var/:hostname'.split('/').join(path.sep)
})
, debug: true
});
var utils = require('../lib/utils');
if ('/.well-known/acme-challenge/' !== LE.acmeChallengePrefix) {
throw new Error("Bad constant 'acmeChallengePrefix'");
}
var baseUrl;
// could use localhost as well, but for the sake of an FQDN for testing, we use this
// also, example.com is just a junk domain to make sure that it is ignored
// (even though it should always be an array of only one element in lib/core.js)
var domains = [ 'localhost.daplie.com', 'example.com' ]; // or just localhost
var token = 'token-id';
var secret = 'key-secret';
var tests = [
function () {
console.log('Test Url:', baseUrl + token);
return requestAsync({ url: baseUrl + token }).then(function (req) {
if (404 !== req.statusCode) {
console.log(req.statusCode);
throw new Error("Should be status 404");
}
});
}
, function () {
var copy = utils.merge({ domains: domains }, le);
copy = utils.tplCopy(copy);
return PromiseA.promisify(le.challenge.set)(copy, domains[0], token, secret);
}
, function () {
return requestAsync(baseUrl + token).then(function (req) {
if (200 !== req.statusCode) {
console.log(req.statusCode, req.body);
throw new Error("Should be status 200");
}
if (req.body !== secret) {
console.error(token, secret, req.body);
throw new Error("req.body should be secret");
}
});
}
, function () {
var copy = utils.merge({ domains: domains }, le);
copy = utils.tplCopy(copy);
return PromiseA.promisify(le.challenge.remove)(copy, domains[0], token);
}
, function () {
return requestAsync(baseUrl + token).then(function (req) {
if (404 !== req.statusCode) {
console.log(req.statusCode);
throw new Error("Should be status 404");
}
});
}
];
function run() {
//var express = require(express);
var server = require('http').createServer(le.middleware());
server.listen(0, function () {
console.log('Server running, proceeding to test.');
baseUrl = 'http://' + domains[0] + ':' + server.address().port + LE.acmeChallengePrefix;
function next() {
var test = tests.shift();
if (!test) {
console.info('All tests passed');
server.close();
return;
}
test().then(next, function (err) {
console.error('ERROR');
console.error(err.stack);
server.close();
});
}
next();
});
}
run();

56
tests/check-account.js Normal file
View File

@ -0,0 +1,56 @@
'use strict';
var LE = require('../').LE;
var le = LE.create({
server: 'staging'
, acme: require('le-acme-core').ACME.create()
, store: require('le-store-certbot').create({
configDir: '~/letsencrypt.test/etc/'
, webrootPath: '~/letsencrypt.test/tmp/:hostname'
})
, debug: true
});
// TODO test generateRsaKey code path separately
// and then provide opts.accountKeypair to create account
//var testId = Math.round(Date.now() / 1000).toString();
var testId = 'test1000';
var testEmail = 'coolaj86+le.' + testId + '@gmail.com';
var testAccountId = '939573edbf2506c92c9ab32131209d7b';
var tests = [
function () {
return le.core.accounts.checkAsync({
accountId: testAccountId
}).then(function (account) {
if (!account) {
throw new Error("Test account should exist when searched by account id.");
}
});
}
, function () {
return le.core.accounts.checkAsync({
email: testEmail
}).then(function (account) {
console.log('account.regr');
console.log(account.regr);
if (!account) {
throw new Error("Test account should exist when searched by email.");
}
});
}
];
function run() {
var test = tests.shift();
if (!test) {
console.info('All tests passed');
return;
}
test().then(run);
}
run();

View File

@ -1,14 +0,0 @@
'use strict';
var path = require('path');
module.exports = {
server: "https://acme-staging.api.letsencrypt.org/directory"
, tlsSni01Port: 5001
, http01Port: 80
, webrootPath: path.join(__dirname, "acme-challenge")
, configDir: path.join(__dirname, "letsencrypt.config")
, workDir: path.join(__dirname, "letsencrypt.work")
, logsDir: path.join(__dirname, "letsencrypt.logs")
, allowedDomains: ['example.com']
};

105
tests/create-account.js Normal file
View File

@ -0,0 +1,105 @@
'use strict';
var LE = require('../').LE;
var le = LE.create({
server: 'staging'
, acme: require('le-acme-core').ACME.create()
, store: require('le-store-certbot').create({
configDir: '~/letsencrypt.test/etc/'
, webrootPath: '~/letsencrypt.test/tmp/:hostname'
})
, debug: true
});
//var testId = Math.round(Date.now() / 1000).toString();
var testId = 'test1000';
var fakeEmail = 'coolaj86+le.' + testId + '@example.com';
var testEmail = 'coolaj86+le.' + testId + '@gmail.com';
var testAccount;
var tests = [
function () {
return le.core.accounts.checkAsync({
email: testEmail
}).then(function (account) {
if (account) {
console.error(account);
throw new Error("Test account should not exist.");
}
});
}
, function () {
return le.core.accounts.registerAsync({
email: testEmail
, agreeTos: false
, rsaKeySize: 2048
}).then(function (/*account*/) {
throw new Error("Should not register if 'agreeTos' is not truthy.");
}, function (err) {
if (err.code !== 'E_ARGS') {
throw err;
}
});
}
, function () {
return le.core.accounts.registerAsync({
email: testEmail
, agreeTos: true
, rsaKeySize: 1024
}).then(function (/*account*/) {
throw new Error("Should not register if 'rsaKeySize' is less than 2048.");
}, function (err) {
if (err.code !== 'E_ARGS') {
throw err;
}
});
}
, function () {
return le.core.accounts.registerAsync({
email: fakeEmail
, agreeTos: true
, rsaKeySize: 2048
}).then(function (/*account*/) {
// TODO test mx record
throw new Error("Registration should NOT succeed with a bad email address.");
}, function (err) {
if (err.code !== 'E_EMAIL') {
throw err;
}
});
}
, function () {
return le.core.accounts.registerAsync({
email: testEmail
, agreeTos: true
, rsaKeySize: 2048
}).then(function (account) {
testAccount = account;
console.log(testEmail);
console.log(testAccount);
if (!account) {
throw new Error("Registration should always return a new account.");
}
if (!account.email) {
throw new Error("Registration should return the email.");
}
if (!account.id) {
throw new Error("Registration should return the account id.");
}
});
}
];
function run() {
var test = tests.shift();
if (!test) {
console.info('All tests passed');
return;
}
test().then(run);
}
run();

View File

@ -1,50 +0,0 @@
'use strict';
var PromiseA = require('bluebird');
var pyconf = PromiseA.promisifyAll(require('pyconf'));
var mkdirpAsync = PromiseA.promisify(require('mkdirp'));
var path = require('path');
pyconf.readFileAsync(path.join(__dirname, 'lib', 'renewal.conf.tpl')).then(function (obj) {
var domains = ['example.com', 'www.example.com'];
var webrootPath = '/tmp/www/example.com';
console.log(obj);
var keys = obj.__keys;
var lines = obj.__lines;
obj.__keys = null;
obj.__lines = null;
var updates = {
account: 'ACCOUNT_ID'
, cert: 'CERT_PATH'
, privkey: 'PRIVATEKEY_PATH'
, configDir: 'CONFIG_DIR'
, tos: true
, http01Port: 80
, domains: domains
};
// final section is completely dynamic
// :hostname = :webroot_path
domains.forEach(function (hostname) {
updates[hostname] = webrootPath;
});
// must write back to the original object or
// annotations will be lost
Object.keys(updates).forEach(function (key) {
obj[key] = updates[key];
});
var renewalPath = '/tmp/letsencrypt/renewal/example.com.conf';
return mkdirpAsync(path.dirname(renewalPath)).then(function () {
console.log(obj);
obj.__keys = keys;
obj.__lines = lines;
return pyconf.writeFileAsync(renewalPath, obj);
});
});

View File

@ -0,0 +1,74 @@
'use strict';
var LE = require('../').LE;
var le = LE.create({
server: 'staging'
, acme: require('le-acme-core').ACME.create()
, store: require('le-store-certbot').create({
configDir: '~/letsencrypt.test/etc'
, webrootPath: '~/letsencrypt.test/var/:hostname'
})
, challenge: require('le-challenge-fs').create({
webrootPath: '~/letsencrypt.test/var/:hostname'
})
, debug: true
});
// TODO test generateRsaKey code path separately
// and then provide opts.accountKeypair to create account
//var testId = Math.round(Date.now() / 1000).toString();
var testId = 'test1000';
var testEmail = 'coolaj86+le.' + testId + '@gmail.com';
// TODO integrate with Daplie Domains for junk domains to test with
var testDomains = [ 'pokemap.hellabit.com', 'www.pokemap.hellabit.com' ];
var tests = [
function () {
return le.core.certificates.checkAsync({
domains: [ 'example.com', 'www.example.com' ]
}).then(function (cert) {
if (cert) {
throw new Error("Bogus domain should not have certificate.");
}
});
}
, function () {
return le.core.certificates.getAsync({
email: testEmail
, domains: testDomains
}).then(function (certs) {
if (!certs) {
throw new Error("Should have acquired certificate for domains.");
}
});
}
];
function run() {
//var express = require(express);
var server = require('http').createServer(le.middleware());
server.listen(80, function () {
console.log('Server running, proceeding to test.');
function next() {
var test = tests.shift();
if (!test) {
server.close();
console.info('All tests passed');
return;
}
test().then(next, function (err) {
console.error('ERROR');
console.error(err.stack);
server.close();
});
}
next();
});
}
run();

102
tests/renew-certificate.js Normal file
View File

@ -0,0 +1,102 @@
'use strict';
var LE = require('../').LE;
var le = LE.create({
server: 'staging'
, acme: require('le-acme-core').ACME.create()
, store: require('le-store-certbot').create({
configDir: '~/letsencrypt.test/etc'
, webrootPath: '~/letsencrypt.test/var/:hostname'
})
, challenge: require('le-challenge-fs').create({
webrootPath: '~/letsencrypt.test/var/:hostname'
})
, debug: true
});
// TODO test generateRsaKey code path separately
// and then provide opts.accountKeypair to create account
//var testId = Math.round(Date.now() / 1000).toString();
var testId = 'test1000';
var testEmail = 'coolaj86+le.' + testId + '@gmail.com';
// TODO integrate with Daplie Domains for junk domains to test with
var testDomains = [ 'pokemap.hellabit.com', 'www.pokemap.hellabit.com' ];
var testCerts;
var tests = [
function () {
// TODO test that an altname also fetches the proper certificate
return le.core.certificates.checkAsync({
domains: testDomains
}).then(function (certs) {
if (!certs) {
throw new Error("Either certificates.registerAsync (in previous test)"
+ " or certificates.checkAsync (in this test) failed.");
}
testCerts = certs;
console.log('Issued At', new Date(certs.issuedAt).toISOString());
console.log('Expires At', new Date(certs.expiresAt).toISOString());
if (certs.expiresAt <= Date.now()) {
throw new Error("Certificates are already expired. They cannot be tested for duplicate or forced renewal.");
}
});
}
, function () {
return le.core.certificates.renewAsync({
email: testEmail
, domains: testDomains
}, testCerts).then(function () {
throw new Error("Should not have renewed non-expired certificates.");
}, function (err) {
if ('E_NOT_RENEWABLE' !== err.code) {
throw err;
}
});
}
, function () {
return le.core.certificates.renewAsync({
email: testEmail
, domains: testDomains
, renewWithin: 720 * 24 * 60 * 60 * 1000
}, testCerts).then(function (certs) {
console.log('Issued At', new Date(certs.issuedAt).toISOString());
console.log('Expires At', new Date(certs.expiresAt).toISOString());
if (certs.issuedAt === testCerts.issuedAt) {
throw new Error("Should not have returned existing certificates.");
}
});
}
];
function run() {
//var express = require(express);
var server = require('http').createServer(le.middleware());
server.listen(80, function () {
console.log('Server running, proceeding to test.');
function next() {
var test = tests.shift();
if (!test) {
server.close();
console.info('All tests passed');
return;
}
test().then(next, function (err) {
console.error('ERROR');
console.error(err.stack);
server.close();
});
}
next();
});
}
run();