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

368
README.md
View File

@ -10,271 +10,249 @@
letsencrypt 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) Free SLL with [90-day](https://letsencrypt.org/2015/11/09/why-90-days.html) HTTPS / TLS Certificates
* [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
STOP 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). 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` For `koa` or `rill`
* `connect` see [letsencrypt-koa](https://github.com/Daplie/letsencrypt-koa).
* 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)
### 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` If you're sure you're at the right place, here's what you need to know now:
* `fish`
* `zsh`
* `cmd.exe`
* `PowerShell`
Install 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 ```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 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) Against my better judgement I'm providing a terribly oversimplified example
are exported for your convenience, but all required options must be specified by the library invoking the call. of how to use this library:
Open an issue if you need a variable for something that isn't there yet.
```javascript ```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 = require('letsencrypt');
var le;
var config = { // Storage Backend
server: LE.stagingServerUrl // or LE.productionServerUrl var leStore = require('le-store-certbot').create({
configDir: '~/letsencrypt/etc' // or /etc/letsencrypt or wherever
, 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' //
, debug: false , debug: false
}; });
var handlers = { // ACME Challenge Handlers
setChallenge: function (opts, hostname, key, val, cb) {} // called during the ACME server handshake, before validation var leChallenge = require('le-challenge-fs').create({
, removeChallenge: function (opts, hostname, key, cb) {} // called after validation on both success and failure webrootPath: '~/letsencrypt/var/' // or template string such as
, getChallenge: function (opts, hostname, key, cb) {} // this is special because it is called by the webserver , debug: false // '/srv/www/:hostname/.well-known/acme-challenge'
// (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
};
var le = LE.create(config, handlers); function leAgree(opts, agreeCb) {
// opts = { email, domains, tosUrl }
agreeCb(null, opts.tosUrl);
}
// checks :conf/renewal/:hostname.conf le = LE.create({
le.register({ // and either renews or registers 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
// 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 , email: 'user@email.com' // CHANGE TO YOUR EMAIL
, agreeTos: false // set to true to automatically accept an agreement , agreeTos: '' // set to tosUrl string (or true) to pre-approve (and skip agreeToTerms)
// which you have pre-approved (not recommended) , rsaKeySize: 2048 // 2048 or higher
, challengeType: 'http-01' // http-01, tls-sni-01, or dns-01
}).then(function (results) {
console.log('success');
}, function (err) { }, function (err) {
if (err) { // Note: you must either use le.middleware() with express,
// Note: you must have a webserver running // manually use le.challenge.get(opts, domain, key, val, done)
// and expose handlers.getChallenge to it // or have a webserver running and responding
// in order to pass validation // to /.well-known/acme-challenge at `webrootPath`
// See letsencrypt-cli and or letsencrypt-express
console.error('[Error]: node-letsencrypt/examples/standalone'); console.error('[Error]: node-letsencrypt/examples/standalone');
console.error(err.stack); 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" Here's what `results` looks like:
than what makes sense to show in a minimal snippet.
```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 API
=== ---
```javascript The full end-user API is exposed in the example above and includes all relevant options.
LetsEncrypt.create(leConfig, handlers, backend) // wraps a given "backend" (the python or node client)
LetsEncrypt.stagingServer // string of staging server for testing
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
le.register({ domains, email, agreeTos, ... }, cb) // registers or renews certs for a domain le.check
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)
``` ```
### `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 * LE.validDomain(hostname) // returns '' or the hostname string if it's a valid ascii or punycode domain name
any `args` (typically `domains`, `email`, and `agreeTos`) and passed to the backend whenever
it is called.
Typically the backend wrapper will already merge any necessary backend-specific arguments. TODO fetch domain tld list
**Example**: ### Template Strings
```javascript
{ webrootPath: __dirname, '/acme-challenge'
, fullchainTpl: '/live/:hostname/fullchain.pem'
, privkeyTpl: '/live/:hostname/fullchain.pem'
, configDir: '/etc/letsencrypt'
}
```
Note: `webrootPath` can be set as a default, semi-locally with `webrootPathTpl`, or per The following variables will be tempalted in any strings passed to the options object:
registration as `webrootPath` (which overwrites `leConfig.webrootPath`).
#### 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 ### store implementation
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.
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>`. ### challenge implementation
Will call `getChallenge([hostname], key, cb)` if present or otherwise read `challenge` from disk.
Example: TODO finish
```javascript
app.use('/', le.middleware())
```
### `le.sniCallback(hostname, function (err, tlsContext) {});` * `.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
Will call `fetch`. If fetch does not return certificates or returns expired certificates * `.remove(opts, domain, key, done);` // opts will be retrieved by domain/key
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)
Change History 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.5.0 now using letiny-core v2.0.0 and rsa-compat
* v1.4.x I can't remember... but it's better! * v1.4.x I can't remember... but it's better!
* v1.1.0 Added letiny-core, removed node-letsencrypt-python * 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 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. Feel free to open an issues to request any particular type of example.
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.

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');
}
});

362
index.js
View File

@ -1,246 +1,150 @@
'use strict'; 'use strict';
// TODO handle www and no-www together somehow? var ACME = require('le-acme-core').ACME;
var PromiseA = require('bluebird');
var leCore = require('letiny-core');
var merge = require('./lib/common').merge;
var tplCopy = require('./lib/common').tplCopy;
var LE = module.exports; var LE = module.exports;
LE.productionServerUrl = leCore.productionServerUrl; LE.LE = LE;
LE.stagingServerUrl = leCore.stagingServerUrl; // in-process cache, shared between all instances
LE.configDir = leCore.configDir; var ipc = {};
LE.logsDir = leCore.logsDir;
LE.workDir = leCore.workDir;
LE.acmeChallengPrefix = leCore.acmeChallengPrefix;
LE.knownEndpoints = leCore.knownEndpoints;
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 = { LE.defaults = {
privkeyPath: LE.privkeyPath productionServerUrl: ACME.productionServerUrl
, fullchainPath: LE.fullchainPath , stagingServerUrl: ACME.stagingServerUrl
, certPath: LE.certPath
, chainPath: LE.chainPath , rsaKeySize: ACME.rsaKeySize || 2048
, renewalPath: LE.renewalPath , challengeType: ACME.challengeType || 'http-01'
, accountsDir: LE.accountsDir
, server: LE.productionServerUrl , acmeChallengePrefix: ACME.acmeChallengePrefix
}; };
// backwards compat // backwards compat
LE.stagingServer = leCore.stagingServerUrl; Object.keys(LE.defaults).forEach(function (key) {
LE.liveServer = leCore.productionServerUrl; LE[key] = LE.defaults[key];
LE.knownUrls = leCore.knownEndpoints; });
LE.merge = require('./lib/common').merge; // show all possible options
LE.tplConfigDir = require('./lib/common').tplConfigDir; var u; // undefined
LE._undefined = {
acme: u
, store: u
, challenge: u
// backend, defaults, handlers , register: u
LE.create = function (defaults, handlers, backend) { , check: u
if (!backend) { backend = require('./lib/core'); }
if (!handlers) { handlers = {}; } , renewWithin: u
if (!handlers.lifetime) { handlers.lifetime = 90 * 24 * 60 * 60 * 1000; } , memorizeFor: u
if (!handlers.renewWithin) { handlers.renewWithin = 3 * 24 * 60 * 60 * 1000; } , acmeChallengePrefix: u
if (!handlers.memorizeFor) { handlers.memorizeFor = 1 * 24 * 60 * 60 * 1000; } , rsaKeySize: u
if (!handlers.sniRegisterCallback) { , challengeType: u
handlers.sniRegisterCallback = function (args, cache, cb) { , server: u
// TODO when we have ECDSA, just do this automatically , agreeToTerms: u
cb(null, null); , _ipc: u
, duplicate: u
, _acmeUrls: u
}; };
} LE._undefine = function (le) {
if (!handlers.getChallenge) { Object.keys(LE._undefined).forEach(function (key) {
if (!defaults.manual && !defaults.webrootPath) { if (!(key in le)) {
// GET /.well-known/acme-challenge/{{challengeKey}} should return {{tokenValue}} le[key] = u;
throw new Error("handlers.getChallenge or defaults.webrootPath must be set");
}
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) { return le;
if (defaults.debug || args.debug) { };
console.log('[LE] setConfig'); LE.create = function (le) {
} var PromiseA = require('bluebird');
backend.configureAsync(args).then(function (pyobj) {
cb(null, le.pyToJson(pyobj)); 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; 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; 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) { certInfo.getCertInfoFromFile = function (pemFile) {
return require('fs').readFileSync(pemFile, 'ascii'); return require('fs').readFileSync(pemFile, 'ascii');
}; };
certInfo.testGetCertInfo = function () { certInfo.testGetCertInfo = function (pathname) {
var path = require('path'); 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)); 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) { 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.info(c.notBefore.value);
console.log(Date(c.notBefore.value).valueOf()); console.info(new Date(c.notBefore.value).valueOf());
console.log(''); console.info('');
console.log(c.notAfter.value); console.info(c.notAfter.value);
console.log(Date(c.notAfter.value).valueOf()); 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,292 +1,221 @@
'use strict'; 'use strict';
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);
}
}
module.exports.create = function (le) {
var PromiseA = require('bluebird'); var PromiseA = require('bluebird');
var utils = require('./utils');
var RSA = PromiseA.promisifyAll(require('rsa-compat').RSA); 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 core = {
var tplCopy = require('./common').tplCopy; //
var fetchFromConfigLiveDir = require('./common').fetchFromDisk; // Helpers
//
var ipc = {}; // in-process cache getAcmeUrlsAsync: function (args) {
function getAcmeUrls(args) {
var now = Date.now(); var now = Date.now();
// TODO check response header on request for cache time // TODO check response header on request for cache time
if ((now - ipc.acmeUrlsUpdatedAt) < 10 * 60 * 1000) { if ((now - le._ipc.acmeUrlsUpdatedAt) < 10 * 60 * 1000) {
return PromiseA.resolve(ipc.acmeUrls); return PromiseA.resolve(le._ipc.acmeUrls);
} }
return LeCore.getAcmeUrlsAsync(args.server).then(function (data) { return le.acme.getAcmeUrlsAsync(args.server).then(function (data) {
ipc.acmeUrlsUpdatedAt = Date.now(); le._ipc.acmeUrlsUpdatedAt = Date.now();
ipc.acmeUrls = data; le._ipc.acmeUrls = data;
return ipc.acmeUrls; return le._ipc.acmeUrls;
}); });
} }
function readRenewalConfig(args) {
var pyconf = PromiseA.promisifyAll(require('pyconf'));
return pyconf.readFileAsync(args.renewalPath).then(function (pyobj) { //
return pyobj; // The Main Enchilada
}, function () { //
return pyconf.readFileAsync(path.join(__dirname, 'renewal.conf.tpl')).then(function (pyobj) {
return pyobj; //
}); // Accounts
}); //
, accounts: {
// Accounts
registerAsync: function (args) {
var err;
var copy = utils.merge(args, le);
var disagreeTos;
args = utils.tplCopy(copy);
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);
} }
function writeRenewalConfig(args) { return utils.testEmail(args.email).then(function () {
function log() {
if (args.debug) {
console.log.apply(console, arguments);
}
}
var pyobj = args.pyobj;
pyobj.checkpoints = parseInt(pyobj.checkpoints, 10) || 0;
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);
}
// args.account.id = pyobj.account
// args.configDir = args.configDir || pyobj.configDir;
args.checkpoints = pyobj.checkpoints;
args.agreeTos = (args.agreeTos || pyobj.tos) && true;
args.email = args.email || pyobj.email;
args.domains = args.domains || pyobj.domains;
// yes, it's an array. weird, right?
args.webrootPath = args.webrootPath || pyobj.webrootPath[0];
args.server = args.server || args.acmeDiscoveryUrl || pyobj.server;
args.certPath = args.certPath || pyobj.cert;
args.privkeyPath = args.privkeyPath || pyobj.privkey;
args.chainPath = args.chainPath || pyobj.chain;
args.fullchainPath = args.fullchainPath || pyobj.fullchain;
//, 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;
return writeRenewalConfig(args);
});
}
function writeCertificateAsync(args, defaults, handlers) {
function log() {
if (args.debug) {
console.log.apply(console, arguments);
}
}
log("[le/core.js] got certificate!");
var obj = args.pyobj;
var result = args.pems;
result.fullchain = result.cert + '\n' + (result.chain || result.ca);
obj.checkpoints = parseInt(obj.checkpoints, 10) || 0;
var liveDir = args.liveDir || path.join(args.configDir, 'live', args.domains[0]);
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 }; var keypairOpts = { public: true, pem: true };
log('[le/core.js] domainKeyPath:', args.domainKeyPath); var promise = le.store.accounts.checkKeypairAsync(args).then(function (keypair) {
if (keypair) {
return RSA.import(keypair);
}
if (args.accountKeypair) {
return le.store.accounts.setKeypairAsync(args, RSA.import(args.accountKeypair));
}
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 RSA.generateKeypairAsync(args.rsaKeySize, 65537, keypairOpts).then(function (keypair) {
return mkdirpAsync(path.dirname(args.domainKeyPath)).then(function () { keypair.privateKeyPem = RSA.exportPrivatePem(keypair);
return fs.writeFileAsync(args.domainKeyPath, keypair.privateKeyPem, 'ascii').then(function () { keypair.publicKeyPem = RSA.exportPublicPem(keypair);
return keypair; keypair.privateKeyJwk = RSA.exportPrivateJwk(keypair);
return le.store.accounts.setKeypairAsync(args, keypair);
}); });
}); });
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;
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;
}
// args.email = email; // already there
// args.domains = domains // already there
args.tosUrl = tosUrl;
le.agreeToTerms(args, agreeCb);
}
, accountKeypair: keypair
, debug: le.debug || args.debug
}).then(function (receipt) {
var reg = {
keypair: keypair
, receipt: receipt
, email: args.email
};
// 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;
});
});
});
});
});
}
// Accounts
, getAsync: function (args) {
return core.accounts.checkAsync(args).then(function (account) {
if (account) {
return account;
} else {
return core.accounts.registerAsync(args);
}
});
}
// 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;
});
}
}
, certificates: {
// Certificates
registerAsync: function (args) {
var err;
var copy = utils.merge(args, le);
args = utils.tplCopy(copy);
if (!Array.isArray(args.domains)) {
return PromiseA.reject(new Error('args.domains should be an array of domains'));
}
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) { return promise.then(function (domainKeypair) {
log("[le/core.js] get certificate");
args.domainKeypair = domainKeypair; args.domainKeypair = domainKeypair;
//args.registration = domainKey; //args.registration = domainKey;
return LeCore.getCertificateAsync({ // Note: the ACME urls are always fetched fresh on purpose
debug: args.debug // 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 , newAuthzUrl: args._acmeUrls.newAuthz
, newCertUrl: args._acmeUrls.newCert , newCertUrl: args._acmeUrls.newCert
@ -294,6 +223,8 @@ function getCertificateAsync(args, defaults, handlers) {
, accountKeypair: RSA.import(account.keypair) , accountKeypair: RSA.import(account.keypair)
, domainKeypair: domainKeypair , domainKeypair: domainKeypair
, domains: args.domains , domains: args.domains
, challengeType: args.challengeType
};
// //
// IMPORTANT // IMPORTANT
@ -303,214 +234,133 @@ function getCertificateAsync(args, defaults, handlers) {
// access to args // access to args
// (args is per-request, defaults is per instance) // (args is per-request, defaults is per instance)
// //
, setChallenge: function (domain, key, value, done) { // Each of these fires individually for each domain,
var copy = merge(defaults, { domains: [domain] }); // even though the certificate on the whole may have many domains
tplCopy(copy); //
certReq.setChallenge = function (domain, key, value, done) {
log(args.debug, "setChallenge called for '" + domain + "'");
var copy = utils.merge({ domains: [domain] }, le);
utils.tplCopy(copy);
args.domains = [domain]; le.challenge.set(copy, domain, key, value, done);
args.webrootPath = args.webrootPath; };
if (4 === handlers.setChallenge.length) { certReq.removeChallenge = function (domain, key, done) {
handlers.setChallenge(copy, key, value, done); log(args.debug, "setChallenge called for '" + domain + "'");
} var copy = utils.merge({ domains: [domain] }, le);
else if (5 === handlers.setChallenge.length) { utils.tplCopy(copy);
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) { le.challenge.remove(copy, domain, key, done);
handlers.removeChallenge(copy, key, done); };
}
else if (4 === handlers.removeChallenge.length) { log(args.debug, 'BEFORE GET CERT');
handlers.removeChallenge(copy, domain, key, done); log(args.debug, certReq);
}
else { return le.acme.getCertificateAsync(certReq).then(utils.attachCertInfo);
done(new Error("handlers.removeChallenge receives the wrong number of arguments"));
}
}
}); });
}).then(function (results) { }).then(function (results) {
// { cert, chain, fullchain, privkey } // { cert, chain, privkey }
args.pems = results; args.pems = results;
return writeCertificateAsync(args, defaults, handlers); 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;
function getOrCreateDomainCertificate(args, defaults, handlers) { log(args.debug, "(Renew) Expires At", new Date(certs.expiresAt).toISOString());
if (args.duplicate) { log(args.debug, "(Renew) Renewable At", new Date(renewableAt).toISOString());
// we're forcing a refresh via 'dupliate: true'
return getCertificateAsync(args, defaults, handlers);
}
return fetchFromConfigLiveDir(args).then(function (certs) { if (!args.duplicate && Date.now() < renewableAt) {
var halfLife = (certs.expiresAt - certs.issuedAt) / 2; err = new Error(
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 '" "[ERROR] Certificate issued at '"
+ new Date(certs.issuedAt).toISOString() + "' and expires at '" + new Date(certs.issuedAt).toISOString() + "' and expires at '"
+ new Date(certs.expiresAt).toISOString() + "'. Ignoring renewal attempt until half-life at '" + new Date(certs.expiresAt).toISOString() + "'. Ignoring renewal attempt until '"
+ new Date(certs.issuedA + halfLife).toISOString() + "'. Set { duplicate: true } to force." + new Date(renewableAt).toISOString() + "'. Set { duplicate: true } to force."
)); );
}); err.code = 'E_NOT_RENEWABLE';
}
// 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); 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) { // Either the cert has entered its renewal period
defaults.server = defaults.server || LE.liveServer; // or we're forcing a refresh via 'dupliate: true'
log(args.debug, "Renewing!");
var wrapped = { // TODO fetch email address / accountId (accountBydomain) if not present
registerAsync: function (args) { // store.config.getAsync(args.domains).then(function (config) { /*...*/ });
var copy; if (!args.domains || (args.domains.length || 0) <= 2) {
// TODO move these defaults elsewhere? // this is a renewal, therefore we should renewal ALL of the domains
//args.renewalDir = args.renewalDir || ':config/renewal/'; // associated with this certificate, unless args.domains is a list larger
args.renewalPath = args.renewalPath || ':config/renewal/:hostname.conf'; // than example.com,www.example.com
// Note: the /directory is part of the server url and, as such, bleeds into the pathname // TODO check www. prefix
// So :config/accounts/:server/directory is *incorrect*, but the following *is* correct: args.domains = certs.altnames;
args.accountsDir = args.accountsDir || ':config/accounts/:server'; if (Array.isArray(certs.domains) && certs.domains.length) {
copy = merge(args, defaults); args.domains = certs.domains;
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);
});
}).then(function (result) {
return result;
}, function (err) {
return PromiseA.reject(err);
});
} }
, 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);
return getOrCreateAcmeAccount(copy, defaults, handlers).then(function (account) { return core.certificates.registerAsync(args);
copy.account = account;
return getOrCreateRenewal(copy);
});
} }
, getConfigAsync: function (hargs) { // Certificates
hargs.renewalPath = hargs.renewalPath || ':config/renewal/:hostname.conf'; , _isRenewable: function (args, certs) {
hargs.domains = []; var renewableAt = core.certificates._getRenewableAt(args, certs);
var copy = merge(hargs, defaults); log(args.debug, "Check Expires At", new Date(certs.expiresAt).toISOString());
tplCopy(copy); 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 null;
}
return pyobj;
}); });
} }
, getConfigsAsync: function (hargs) { // Certificates
hargs.renewalDir = hargs.renewalDir || ':config/renewal/'; , getAsync: function (args) {
hargs.renewalPath = hargs.renewalPath || ':config/renewal/:hostname.conf'; var copy = utils.merge(args, le);
hargs.domains = []; args = utils.tplCopy(copy);
var copy = merge(hargs, defaults); return core.certificates.checkAsync(args).then(function (certs) {
tplCopy(copy); if (!certs) {
// There is no cert available
log(args.debug, "no certificate found");
return core.certificates.registerAsync(args);
}
return fs.readdirAsync(copy.renewalDir).then(function (nodes) { if (core.certificates._isRenewable(args, certs)) {
nodes = nodes.filter(function (node) { certs._renewing = core.certificates.renewAsync(args, certs);
return /^[a-z0-9]+.*\.conf$/.test(node); }
});
return PromiseA.all(nodes.map(function (node) { return certs;
copy.domains = [node.replace(/\.conf$/, '')]; }).then(function (results) {
return wrapped.getConfigAsync(copy); // 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", "name": "letsencrypt",
"version": "1.5.1", "version": "2.0.1",
"description": "Let's Encrypt for node.js on npm", "description": "Let's Encrypt for node.js on npm",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@ -29,19 +29,17 @@
"url": "https://github.com/Daplie/node-letsencrypt/issues" "url": "https://github.com/Daplie/node-letsencrypt/issues"
}, },
"homepage": "https://github.com/Daplie/node-letsencrypt#readme", "homepage": "https://github.com/Daplie/node-letsencrypt#readme",
"devDependencies": { "devDependencies": {},
"express": "^4.13.3",
"localhost.daplie.com-certificates": "^1.1.2"
},
"optionalDependencies": {}, "optionalDependencies": {},
"dependencies": { "dependencies": {
"asn1": "^0.2.3",
"bluebird": "^3.0.6", "bluebird": "^3.0.6",
"homedir": "^0.6.0", "homedir": "^0.6.0",
"letiny-core": "^2.0.1", "le-acme-core": "^2.0.5",
"mkdirp": "^0.5.1", "le-challenge-fs": "^2.0.2",
"pyconf": "^1.1.2", "le-store-certbot": "^2.0.1",
"request": "^2.67.0", "node.extend": "^1.1.5",
"rsa-compat": "^1.2.1", "pkijs": "^1.3.27",
"safe-replace": "^1.0.2" "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();