greenlock.js-ARCHIVED/README.md

508 lines
17 KiB
Markdown
Raw Normal View History

2018-05-16 20:44:20 +00:00
!["Greenlock Logo"](https://git.coolaj86.com/coolaj86/greenlock.js/raw/branch/master/logo/greenlock-1063x250.png "Greenlock lock logo and work mark")
2018-05-16 20:45:00 +00:00
!["Greenlock Function"](https://git.coolaj86.com/coolaj86/greenlock.js/raw/branch/master/logo/from-not-secure-to-secure-url-bar.png "from url bar showing not secure to url bar showing secure")
2018-05-12 08:03:32 +00:00
Greenlock™ for node.js
2018-04-20 06:36:22 +00:00
=====
2016-04-22 18:17:54 +00:00
2018-05-12 02:56:24 +00:00
Greenlock provides Free SSL, Free Wildcard SSL, and Fully Automated HTTPS <br>
<small>certificates issued by Let's Encrypt v2 via [ACME](https://git.coolaj86.com/coolaj86/acme-v2.js)</small>
2018-05-14 23:40:51 +00:00
!["Lifetime Downloads"](https://img.shields.io/npm/dt/greenlock.svg "Lifetime Download Count can't be shown")
!["Monthly Downloads"](https://img.shields.io/npm/dm/greenlock.svg "Monthly Download Count can't be shown")
!["Weekly Downloads"](https://img.shields.io/npm/dw/greenlock.svg "Weekly Download Count can't be shown")
2018-05-16 01:14:19 +00:00
!["Stackoverflow Questions"](https://img.shields.io/stackexchange/stackoverflow/t/greenlock.svg "S.O. Question count can't be shown")
2018-05-14 23:40:51 +00:00
2018-05-12 02:56:24 +00:00
| Sponsored by [ppl](https://ppl.family) |
Greenlock works
in the [Commandline](https://git.coolaj86.com/coolaj86/greenlock-cli.js) (cli),
as a [Web Server](https://git.coolaj86.com/coolaj86/greenlock-server.js),
in [Web Browsers](https://git.coolaj86.com/coolaj86/greenlock.html) (WebCrypto),
and with **node.js** ([npm](https://www.npmjs.com/package/greenlock)).
Features
========
- [x] Actively Maintained and Supported
- [x] Automatic HTTPS
- [x] Free SSL
- [x] Free Wildcard SSL
- [x] Multiple domain support (up to 100 altnames per SAN)
- [x] Dynamic Virtual Hosting (vhost)
- [x] Automatical renewal (10 to 14 days before expiration)
- [x] Great ACME support via [acme.js](https://git.coolaj86.com/coolaj86/acme-v2.js)
2018-05-19 23:42:49 +00:00
- [x] "dry run" with self-diagnostics
2018-05-12 02:56:24 +00:00
- [x] ACME draft 11
- [x] Let's Encrypt v2
- [x] Let's Encrypt v1
- [x] [Commandline](https://git.coolaj86.com/coolaj86/greenlock-cli.js) (cli) Utilities
- [x] Works with `bash`, `fish`, `zsh`, `cmd.exe`, `PowerShell`, and more
2018-05-15 08:43:02 +00:00
- [x] [Browser](https://git.coolaj86.com/coolaj86/greenlock.html) Support
2018-05-12 02:56:24 +00:00
- [x] Full node.js support, with modules for
2018-05-12 03:04:46 +00:00
- [x] [http/https](https://git.coolaj86.com/coolaj86/greenlock-express.js/src/branch/master/examples/https-server.js), [Express.js](https://git.coolaj86.com/coolaj86/greenlock-express.js), [cluster](https://git.coolaj86.com/coolaj86/greenlock-cluster.js), [hapi](https://git.coolaj86.com/coolaj86/greenlock-hapi.js), [Koa](https://git.coolaj86.com/coolaj86/greenlock-koa.js), [rill](https://git.coolaj86.com/coolaj86/greenlock-rill.js), [restify](https://git.coolaj86.com/coolaj86/greenlock-restify.js), spdy, etc
2018-05-12 02:56:24 +00:00
- [x] Great for securing your Raspberry Pi
- [x] Extensible Plugin Support
2018-05-12 03:04:46 +00:00
- [x] AWS S3, AWS Route53, Azure, CloudFlare, Consul, Digital Ocean, etcd, Redis
2018-05-12 02:56:24 +00:00
Greenlock.js for Middleware
------
2015-12-11 11:23:47 +00:00
2018-05-12 02:56:24 +00:00
Documentation for using Greenlock with
[http/https](https://git.coolaj86.com/coolaj86/greenlock-express.js/src/branch/master/examples/https-server.js),
[Express.js](https://git.coolaj86.com/coolaj86/greenlock-express.js),
[cluster](https://git.coolaj86.com/coolaj86/greenlock-cluster.js),
[hapi](https://git.coolaj86.com/coolaj86/greenlock-hapi.js),
[Koa](https://git.coolaj86.com/coolaj86/greenlock-koa.js),
[rill](https://git.coolaj86.com/coolaj86/greenlock-rill.js).
[restify](https://git.coolaj86.com/coolaj86/greenlock-restify.js).
Table of Contents
=================
* Install
* Simple Examples
* Example with ALL OPTIONS
* API
* Developer API
* Change History
* License
2015-12-13 09:04:44 +00:00
2018-05-12 02:56:24 +00:00
Install
=======
2016-04-18 17:01:35 +00:00
2018-05-12 02:56:24 +00:00
```bash
npm install --save greenlock@2.x
```
2016-08-05 07:03:27 +00:00
2018-05-12 02:56:24 +00:00
**Note**: Ignore errors related to `ursa`. It is an optional dependency used when available.
For many people it will not install properly, but it's only necessary on ARM devices (i.e. Raspberry Pi).
2016-04-18 17:01:35 +00:00
2018-05-19 23:42:49 +00:00
### Production vs Staging
If at first you don't succeed, stop and switch to staging.
I've implemented a "dry run" loopback test with self diagnostics
so it's pretty safe to start off with the production URLs
and be far less likely to hit the bad request rate limits.
However, if your first attempt to get a certificate fails
I'd recommend switching to the staging acme server to debug -
unless you're very clear on what the failure was and how to fix it.
```
{ server: 'https://acme-staging-v02.api.letsencrypt.org/directory' }
```
2018-05-12 02:56:24 +00:00
Easy as 1, 2, 3... 4
=====
2016-04-18 17:27:55 +00:00
2018-05-12 02:56:24 +00:00
Greenlock is built to incredibly easy to use, without sacrificing customization or extensibility.
2016-04-18 17:01:35 +00:00
2018-05-12 02:56:24 +00:00
The following examples range from just a few lines of code for getting started,
to more robust examples that you might start with for an enterprise-grade use of the ACME api.
2016-04-18 17:26:15 +00:00
2018-05-12 02:56:24 +00:00
* Automatic HTTPS (for single sites)
* Fully Automatic HTTPS (for multi-domain vhosts)
* Manual HTTPS (for API integration)
2018-04-20 06:36:22 +00:00
2018-05-12 02:56:24 +00:00
Automatic HTTPS
---------------
2015-12-12 13:11:05 +00:00
2018-05-12 02:56:24 +00:00
**Note**: For (fully) automatic HTTPS you may prefer
the [Express.js module](https://git.coolaj86.com/coolaj86/greenlock-express.js)
2016-08-05 07:20:19 +00:00
2018-05-12 02:56:24 +00:00
This works for most people, but it's not as fun as some of the other examples.
2016-08-05 07:20:19 +00:00
2018-05-12 02:56:24 +00:00
Great when
- [x] You only need a limited number of certificates
- [x] You want to use the bare node http and https modules without fluff
```js
////////////////////
// INIT GREENLOCK //
////////////////////
var path = require('path');
var os = require('os')
var Greenlock = require('greenlock');
2016-08-09 23:43:46 +00:00
2018-05-12 02:56:24 +00:00
var greenlock = Greenlock.create({
agreeTos: true // Accept Let's Encrypt v2 Agreement
, email: 'user@example.com' // IMPORTANT: Change email and domains
, approveDomains: [ 'example.com' ]
, communityMember: false // Optionally get important updates (security, api changes, etc)
// and submit stats to help make Greenlock better
, version: 'draft-11'
2018-05-19 23:42:49 +00:00
, server: 'https://acme-v02.api.letsencrypt.org/directory'
2018-05-12 02:56:24 +00:00
, configDir: path.join(os.homedir(), 'acme/etc')
});
////////////////////
// CREATE SERVERS //
////////////////////
var redir = require('redirect-https')();
require('http').createServer(greenlock.middleware(redir)).listen(80);
require('https').createServer(greenlock.tlsOptions, function (req, res) {
res.end('Hello, Secure World!');
}).listen(443);
2015-12-12 13:11:05 +00:00
```
2018-05-12 02:56:24 +00:00
Fully Automatic HTTPS
------------
2016-08-30 14:54:19 +00:00
2018-05-12 02:56:24 +00:00
**Note**: For (fully) automatic HTTPS you may prefer
the [Express.js module](https://git.coolaj86.com/coolaj86/greenlock-express.js)
2015-12-12 22:16:02 +00:00
2018-05-12 02:56:24 +00:00
Great when
2015-12-12 22:16:02 +00:00
2018-05-12 02:56:24 +00:00
- [x] You have a growing number of domains
- [x] You're integrating into your own hosting solution
- [x] Customize ACME http-01 or dns-01 challenge
2015-12-16 13:07:56 +00:00
2018-05-12 02:56:24 +00:00
```js
////////////////////
// INIT GREENLOCK //
////////////////////
2015-12-17 10:38:46 +00:00
2018-05-12 02:56:24 +00:00
var path = require('path');
var os = require('os')
var Greenlock = require('greenlock');
var greenlock = Greenlock.create({
version: 'draft-11'
2018-05-19 23:42:49 +00:00
, server: 'https://acme-v02.api.letsencrypt.org/directory'
2018-05-12 02:56:24 +00:00
2018-05-12 03:09:49 +00:00
// approve a growing list of domains
, approveDomains: approveDomains
2018-05-12 02:56:24 +00:00
// If you wish to replace the default account and domain key storage plugin
, store: require('le-store-certbot').create({
configDir: path.join(os.homedir(), 'acme/etc')
, webrootPath: '/tmp/acme-challenges'
})
});
/////////////////////
// APPROVE DOMAINS //
/////////////////////
var http01 = require('le-challenge-fs').create({ webrootPath: '/tmp/acme-challenges' });
function approveDomains(opts, certs, cb) {
// This is where you check your database and associated
// email addresses with domains and agreements and such
// Opt-in to submit stats and get important updates
opts.communityMember = true;
// If you wish to replace the default challenge plugin, you may do so here
opts.challenges = { 'http-01': http01 };
// The domains being approved for the first time are listed in opts.domains
// Certs being renewed are listed in certs.altnames
if (certs) {
opts.domains = certs.altnames;
}
else {
opts.email = 'john.doe@example.com';
opts.agreeTos = true;
}
// NOTE: you can also change other options such as `challengeType` and `challenge`
// opts.challengeType = 'http-01';
// opts.challenge = require('le-challenge-fs').create({});
cb(null, { options: opts, certs: certs });
}
////////////////////
// CREATE SERVERS //
////////////////////
var redir = require('redirect-https')();
require('http').createServer(greenlock.middleware(redir)).listen(80);
require('https').createServer(greenlock.tlsOptions, function (req, res) {
res.end('Hello, Secure World!');
}).listen(443);
```
Manual HTTPS
-------------
Here's a taste of the API that you might use if building a commandline tool or API integration
that doesn't use node's SNICallback.
```
/////////////////////
// SET USER PARAMS //
/////////////////////
2015-12-17 10:38:46 +00:00
2016-08-10 01:05:04 +00:00
var opts = {
2018-05-12 02:56:24 +00:00
domains: [ 'example.com' // CHANGE EMAIL AND DOMAINS
, 'www.example.com' ]
, email: 'user@example.com'
, agreeTos: true // Accept Let's Encrypt v2 Agreement
, communityMember: true // Help make Greenlock better by submitting
// stats and getting updates
2016-08-10 01:05:04 +00:00
};
2018-05-12 02:56:24 +00:00
////////////////////
// INIT GREENLOCK //
////////////////////
var greenlock = require('greenlock').create({
version: 'draft-11'
2018-05-19 23:42:49 +00:00
, server: 'https://acme-v02.api.letsencrypt.org/directory'
2018-05-12 02:56:24 +00:00
, configDir: '/tmp/acme/etc'
});
///////////////////
// GET TLS CERTS //
///////////////////
greenlock.register(opts).then(function (certs) {
2016-08-10 01:00:40 +00:00
console.log(certs);
2016-08-10 01:05:04 +00:00
// privkey, cert, chain, expiresAt, issuedAt, subject, altnames
2016-08-10 00:56:46 +00:00
}, function (err) {
console.error(err);
});
2016-08-05 07:20:19 +00:00
```
2018-05-12 02:56:24 +00:00
The domain key and ssl certificates you get back can be used in a webserver like this:
2016-08-05 07:03:27 +00:00
2018-05-12 02:56:24 +00:00
```js
var tlsOptions = { key: certs.privkey, cert: certs.cert + '\r\n' + certs.chain };
require('https').createServer(tlsOptions, function (req, res) {
res.end('Hello, Secure World!');
}).listen(443);
2016-08-05 07:20:19 +00:00
```
2018-05-12 02:56:24 +00:00
Example with ALL OPTIONS
=========
2015-12-12 22:16:02 +00:00
2016-08-05 07:03:27 +00:00
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)
2015-12-12 13:11:05 +00:00
2015-12-13 09:04:44 +00:00
```javascript
2016-08-05 07:03:27 +00:00
'use strict';
2018-05-15 21:49:59 +00:00
var Greenlock = require('greenlock');
var greenlock;
2015-12-12 15:38:14 +00:00
2015-12-12 22:06:36 +00:00
2016-08-05 07:03:27 +00:00
// Storage Backend
var leStore = require('le-store-certbot').create({
configDir: '~/acme/etc' // or /etc/letsencrypt or wherever
2016-08-05 07:03:27 +00:00
, debug: false
});
2015-12-12 22:06:36 +00:00
2016-08-05 07:03:27 +00:00
// ACME Challenge Handlers
2017-04-11 07:45:39 +00:00
var leHttpChallenge = require('le-challenge-fs').create({
webrootPath: '~/acme/var/' // or template string such as
2016-08-05 07:03:27 +00:00
, debug: false // '/srv/www/:hostname/.well-known/acme-challenge'
});
2015-12-12 22:06:36 +00:00
2016-08-05 07:03:27 +00:00
function leAgree(opts, agreeCb) {
// opts = { email, domains, tosUrl }
agreeCb(null, opts.tosUrl);
2015-12-12 22:06:36 +00:00
}
2015-12-12 13:11:05 +00:00
2018-05-15 21:49:59 +00:00
greenlock = Greenlock.create({
2018-04-16 01:28:05 +00:00
version: 'draft-11' // 'draft-11' or 'v01'
// 'draft-11' is for Let's Encrypt v2 otherwise known as ACME draft 11
// 'v02' is an alias for 'draft-11'
// 'v01' is for the pre-spec Let's Encrypt v1
//
// staging API
2018-05-19 23:42:49 +00:00
//server: 'https://acme-staging-v02.api.letsencrypt.org/directory'
2018-04-16 01:28:05 +00:00
//
// production API
2018-05-19 23:42:49 +00:00
server: 'https://acme-v02.api.letsencrypt.org/directory'
2018-04-16 01:28:05 +00:00
2016-08-05 07:03:27 +00:00
, store: leStore // handles saving of config, accounts, and certificates
2017-04-11 07:45:39 +00:00
, challenges: {
'http-01': leHttpChallenge // handles /.well-known/acme-challege keys and tokens
}
2016-08-15 21:33:26 +00:00
, challengeType: 'http-01' // default to this challenge type
2016-08-05 07:03:27 +00:00
, agreeToTerms: leAgree // hook to allow user to view and accept LE TOS
2016-08-15 21:39:21 +00:00
//, sni: require('le-sni-auto').create({}) // handles sni callback
// renewals happen at a random time within this window
, renewWithin: 14 * 24 * 60 * 60 * 1000 // certificate renewal may begin at this time
, renewBy: 10 * 24 * 60 * 60 * 1000 // certificate renewal should happen by this time
2016-08-05 07:03:27 +00:00
, debug: false
2016-09-21 23:47:47 +00:00
//, log: function (debug) {console.log.apply(console, args);} // handles debug outputs
2016-08-05 07:03:27 +00:00
});
2015-12-13 01:04:12 +00:00
2015-12-12 13:11:05 +00:00
2016-08-05 07:03:27 +00:00
// If using express you should use the middleware
2018-05-15 21:49:59 +00:00
// app.use('/', greenlock.middleware());
2016-08-05 07:03:27 +00:00
//
2016-08-09 20:12:16 +00:00
// Otherwise you should see the test file for usage of this:
2018-05-15 21:49:59 +00:00
// greenlock.challenges['http-01'].get(opts.domain, key, val, done)
2015-12-12 13:11:05 +00:00
2016-08-05 07:03:27 +00:00
// Check in-memory cache of certificates for the named domain
2018-05-15 21:49:59 +00:00
greenlock.check({ domains: [ 'example.com' ] }).then(function (results) {
2016-08-05 07:03:27 +00:00
if (results) {
// we already have certificates
return;
}
2015-12-12 22:06:36 +00:00
2016-08-05 22:21:10 +00:00
2016-08-05 07:03:27 +00:00
// Register Certificate manually
2018-05-15 21:49:59 +00:00
greenlock.register({
2016-08-05 22:21:10 +00:00
domains: ['example.com'] // CHANGE TO YOUR DOMAIN (list for SANS)
, email: 'user@email.com' // CHANGE TO YOUR EMAIL
2016-08-06 05:34:34 +00:00
, agreeTos: '' // set to tosUrl string (or true) to pre-approve (and skip agreeToTerms)
2016-08-05 22:21:10 +00:00
, rsaKeySize: 2048 // 2048 or higher
, challengeType: 'http-01' // http-01, tls-sni-01, or dns-01
}).then(function (results) {
console.log('success');
}, function (err) {
2018-05-15 21:49:59 +00:00
// Note: you must either use greenlock.middleware() with express,
// manually use greenlock.challenges['http-01'].get(opts, domain, key, val, done)
2016-08-05 22:21:10 +00:00
// or have a webserver running and responding
// to /.well-known/acme-challenge at `webrootPath`
2017-01-25 21:08:20 +00:00
console.error('[Error]: node-greenlock/examples/standalone');
2016-08-05 22:21:10 +00:00
console.error(err.stack);
});
2015-12-12 22:06:36 +00:00
2016-08-05 07:03:27 +00:00
});
2015-12-12 13:11:05 +00:00
```
2016-08-05 07:03:27 +00:00
Here's what `results` looks like:
2015-12-12 22:06:36 +00:00
```javascript
2016-08-05 07:03:27 +00:00
{ privkey: '' // PEM encoded private key
, cert: '' // PEM encoded cert
, chain: '' // PEM encoded intermediate cert
, issuedAt: 0 // notBefore date (in ms) parsed from cert
, expiresAt: 0 // notAfter date (in ms) parsed from cert
2016-08-10 01:05:04 +00:00
, subject: '' // example.com
, altnames: [] // example.com,www.example.com
2016-08-05 07:03:27 +00:00
}
2015-12-12 13:11:05 +00:00
```
2016-08-05 07:03:27 +00:00
API
---
2015-12-12 22:06:36 +00:00
2016-08-05 07:03:27 +00:00
The full end-user API is exposed in the example above and includes all relevant options.
2015-12-12 22:06:36 +00:00
2016-08-06 05:34:34 +00:00
```
2018-05-15 21:49:59 +00:00
greenlock.register(opts)
greenlock.check(opts)
2016-08-06 05:34:34 +00:00
```
2016-08-05 07:03:27 +00:00
### Helper Functions
2015-12-12 13:11:05 +00:00
2016-08-05 07:03:27 +00:00
We do expose a few helper functions:
2015-12-12 22:06:36 +00:00
2018-05-15 21:49:59 +00:00
* Greenlock.validDomain(hostname) // returns '' or the hostname string if it's a valid ascii or punycode domain name
2015-12-12 22:06:36 +00:00
2016-08-05 07:03:27 +00:00
TODO fetch domain tld list
2015-12-12 22:06:36 +00:00
2016-08-05 08:13:58 +00:00
### Template Strings
The following variables will be tempalted in any strings passed to the options object:
* `~/` replaced with `os.homedir()` i.e. `/Users/aj`
2016-08-08 22:10:23 +00:00
* `:hostname` replaced with the first domain in the list i.e. `example.com`
2016-08-05 08:13:58 +00:00
2016-08-05 07:03:27 +00:00
Developer API
-------------
2015-12-12 22:06:36 +00:00
2016-08-05 07:03:27 +00:00
If you are developing an `le-store-*` or `le-challenge-*` plugin you need to be aware of
additional internal API expectations.
2015-12-12 22:06:36 +00:00
2016-08-05 07:03:27 +00:00
**IMPORTANT**:
2015-12-12 22:06:36 +00:00
2016-08-05 07:03:27 +00:00
Use `v2.0.0` as your initial version - NOT v0.1.0 and NOT v1.0.0 and NOT v3.0.0.
2017-01-25 21:08:20 +00:00
This is to indicate that your module is compatible with v2.x of node-greenlock.
2015-12-13 05:03:48 +00:00
2017-01-25 21:08:20 +00:00
Since the public API for your module is defined by node-greenlock the major version
2016-08-05 07:03:27 +00:00
should be kept in sync.
2015-12-13 05:03:48 +00:00
2016-08-05 07:03:27 +00:00
### store implementation
2015-12-12 13:11:05 +00:00
2017-11-05 14:57:20 +00:00
See <https://git.coolaj86.com/coolaj86/le-store-SPEC.js>
2016-08-09 20:40:35 +00:00
* getOptions()
* accounts.
* checkKeypair(opts, cb)
* check(opts, cb)
* setKeypair(opts, keypair, cb)
* set(opts, reg, cb)
* certificates.
* checkKeypair(opts, cb)
* check(opts, cb)
* setKeypair(opts, keypair, cb)
* set(opts, reg, cb)
2016-08-05 07:03:27 +00:00
### challenge implementation
2015-12-12 13:11:05 +00:00
2017-11-05 14:57:20 +00:00
See https://git.coolaj86.com/coolaj86/le-challenge-fs.js
2015-12-12 13:11:05 +00:00
2016-08-09 20:40:35 +00:00
* `.set(opts, domain, key, value, cb);` // opts will be saved with domain/key
* `.get(opts, domain, key, cb);` // opts will be retrieved by domain/key
* `.remove(opts, domain, key, cb);` // opts will be retrieved by domain/key
2015-12-12 13:11:05 +00:00
2015-12-13 11:09:06 +00:00
Change History
==============
2018-04-20 06:39:45 +00:00
* v2.2 - Let's Encrypt v2 Support
2018-05-12 02:56:24 +00:00
* v2.2.11 - documentation updates
* v2.2.10 - don't let SNICallback swallow approveDomains errors 6286883fc2a6ebfff711a540a2e4d92f3ac2907c
* v2.2.8 - communityMember option support
* v2.2.7 - bugfix for wildcard support
* v2.2.5 - node v6.x compat
2018-04-20 06:39:45 +00:00
* v2.2.4 - don't promisify all of `dns`
* v2.2.3 - `renewWithin` default to 14 days
* v2.2.2 - replace git dependency with npm
* v2.2.1 - April 2018 **Let's Encrypt v2** support
2017-11-05 14:57:20 +00:00
* v2.1.17 - Nov 5th 2017 migrate back to personal repo
* v2.1.9 - Jan 18th 2017 renamed to greenlock
2016-08-09 20:40:35 +00:00
* v2.0.2 - Aug 9th 2016 update readme
* v2.0.1 - Aug 9th 2016
2016-08-05 07:03:27 +00:00
* major refactor
* simplified API
2016-09-24 03:18:34 +00:00
* modular plugins
2016-08-05 07:03:27 +00:00
* knock out bugs
2016-08-04 03:13:40 +00:00
* v1.5.0 now using letiny-core v2.0.0 and rsa-compat
2016-04-18 17:01:35 +00:00
* v1.4.x I can't remember... but it's better!
2015-12-16 09:19:08 +00:00
* v1.1.0 Added letiny-core, removed node-letsencrypt-python
* v1.0.2 Works with node-letsencrypt-python
* v1.0.0 Thar be dragons
2015-12-13 11:09:06 +00:00
2015-12-11 11:23:47 +00:00
LICENSE
=======
Dual-licensed MIT and Apache-2.0
See LICENSE
2018-05-12 02:56:24 +00:00
Greenlock&trade; is a trademark of AJ ONeal