mirror of
				https://git.coolaj86.com/coolaj86/greenlock-store-fs.js.git
				synced 2025-11-04 02:52:47 +00:00 
			
		
		
		
	initial commit
This commit is contained in:
		
						commit
						2b4b714126
					
				
							
								
								
									
										61
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,61 @@
 | 
				
			|||||||
 | 
					# ---> Node
 | 
				
			||||||
 | 
					# Logs
 | 
				
			||||||
 | 
					logs
 | 
				
			||||||
 | 
					*.log
 | 
				
			||||||
 | 
					npm-debug.log*
 | 
				
			||||||
 | 
					yarn-debug.log*
 | 
				
			||||||
 | 
					yarn-error.log*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Runtime data
 | 
				
			||||||
 | 
					pids
 | 
				
			||||||
 | 
					*.pid
 | 
				
			||||||
 | 
					*.seed
 | 
				
			||||||
 | 
					*.pid.lock
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Directory for instrumented libs generated by jscoverage/JSCover
 | 
				
			||||||
 | 
					lib-cov
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Coverage directory used by tools like istanbul
 | 
				
			||||||
 | 
					coverage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# nyc test coverage
 | 
				
			||||||
 | 
					.nyc_output
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
 | 
				
			||||||
 | 
					.grunt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Bower dependency directory (https://bower.io/)
 | 
				
			||||||
 | 
					bower_components
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# node-waf configuration
 | 
				
			||||||
 | 
					.lock-wscript
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Compiled binary addons (http://nodejs.org/api/addons.html)
 | 
				
			||||||
 | 
					build/Release
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Dependency directories
 | 
				
			||||||
 | 
					node_modules/
 | 
				
			||||||
 | 
					jspm_packages/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Typescript v1 declaration files
 | 
				
			||||||
 | 
					typings/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Optional npm cache directory
 | 
				
			||||||
 | 
					.npm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Optional eslint cache
 | 
				
			||||||
 | 
					.eslintcache
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Optional REPL history
 | 
				
			||||||
 | 
					.node_repl_history
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Output of 'npm pack'
 | 
				
			||||||
 | 
					*.tgz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Yarn Integrity file
 | 
				
			||||||
 | 
					.yarn-integrity
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# dotenv environment variables file
 | 
				
			||||||
 | 
					.env
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										41
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					Copyright 2019 AJ ONeal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This is open source software; you can redistribute it and/or modify it under the
 | 
				
			||||||
 | 
					terms of either:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   a) the "MIT License"
 | 
				
			||||||
 | 
					   b) the "Apache-2.0 License"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					MIT License
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
				
			||||||
 | 
					   of this software and associated documentation files (the "Software"), to deal
 | 
				
			||||||
 | 
					   in the Software without restriction, including without limitation the rights
 | 
				
			||||||
 | 
					   to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
				
			||||||
 | 
					   copies of the Software, and to permit persons to whom the Software is
 | 
				
			||||||
 | 
					   furnished to do so, subject to the following conditions:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   The above copyright notice and this permission notice shall be included in all
 | 
				
			||||||
 | 
					   copies or substantial portions of the Software.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
				
			||||||
 | 
					   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
				
			||||||
 | 
					   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
				
			||||||
 | 
					   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
				
			||||||
 | 
					   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
				
			||||||
 | 
					   OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 | 
				
			||||||
 | 
					   SOFTWARE.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Apache-2.0 License Summary
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					   you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					   You may obtain a copy of the License at
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					   distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					   See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					   limitations under the License.
 | 
				
			||||||
							
								
								
									
										107
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,107 @@
 | 
				
			|||||||
 | 
					# le-store-fs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A greenlock keypair and certificate storage strategy with wildcard support (simpler successor to le-store-certbot).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Usage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```js
 | 
				
			||||||
 | 
					var greenlock = require('greenlock');
 | 
				
			||||||
 | 
					var gl = greenlock.create({
 | 
				
			||||||
 | 
					  configDir: '~/.config/acme'
 | 
				
			||||||
 | 
					, store: require('le-store-fs')
 | 
				
			||||||
 | 
					, approveDomains: approveDomains
 | 
				
			||||||
 | 
					, ...
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# File System
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The default file system layout mirrors that of le-store-certbot in order to make transitioning effortless,
 | 
				
			||||||
 | 
					in most situations:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					acme
 | 
				
			||||||
 | 
					├── accounts
 | 
				
			||||||
 | 
					│   └── acme-staging-v02.api.letsencrypt.org
 | 
				
			||||||
 | 
					│       └── directory
 | 
				
			||||||
 | 
					│           └── sites@example.com.json
 | 
				
			||||||
 | 
					└── live
 | 
				
			||||||
 | 
					    ├── example.com
 | 
				
			||||||
 | 
					    │   ├── bundle.pem
 | 
				
			||||||
 | 
					    │   ├── cert.pem
 | 
				
			||||||
 | 
					    │   ├── chain.pem
 | 
				
			||||||
 | 
					    │   ├── fullchain.pem
 | 
				
			||||||
 | 
					    │   └── privkey.pem
 | 
				
			||||||
 | 
					    └── www.example.com
 | 
				
			||||||
 | 
					        ├── bundle.pem
 | 
				
			||||||
 | 
					        ├── cert.pem
 | 
				
			||||||
 | 
					        ├── chain.pem
 | 
				
			||||||
 | 
					        ├── fullchain.pem
 | 
				
			||||||
 | 
					        └── privkey.pem
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Wildcards & AltNames
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Working with wildcards and multiple altnames requires greenlock >= v2.7.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To do so you must set `opts.subject` and `opts.domains` within the `approvedomains()` callback.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					`subject` refers to "the subject of the ssl certificate" as opposed to `domain` which indicates "the domain servername
 | 
				
			||||||
 | 
					used in the current request". For single-domain certificates they're always the same, but for multiple-domain
 | 
				
			||||||
 | 
					certificates `subject` must be the name no matter what `domain` is receiving a request. `subject` is used as
 | 
				
			||||||
 | 
					part of the name of the file storage path where the certificate will be saved (or retrieved).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					`domains` should be the list of "altnames" on the certificate, which should include the `subject`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Simple Example
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```js
 | 
				
			||||||
 | 
					function approveDomains(opts, certs, cb) {
 | 
				
			||||||
 | 
					  // foo.example.com => *.example.com
 | 
				
			||||||
 | 
					  var wild = '*.' + opts.domain.split('.').slice(1).join('.');
 | 
				
			||||||
 | 
						if ('*.example.com' !== wild) { cb(new Error(opts.domain + " is not allowed")); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opts.subject = '*.example.com';
 | 
				
			||||||
 | 
						opts.domains = ['*.example.com'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  cb({ options: opts, certs: certs });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Realistic Example
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```js
 | 
				
			||||||
 | 
					function approveDomains(opts, certs, cb) {
 | 
				
			||||||
 | 
					  var related = getRelated(opts.domain);
 | 
				
			||||||
 | 
						if (!related) { cb(new Error(opts.domain + " is not allowed")); };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opts.subject = related.subject;
 | 
				
			||||||
 | 
						opts.domains = related.domains;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  cb({ options: opts, certs: certs });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```js
 | 
				
			||||||
 | 
					function getRelated(domain) {
 | 
				
			||||||
 | 
					  var related;
 | 
				
			||||||
 | 
					  var wild = '*.' + domain.split('.').slice(1).join('.');
 | 
				
			||||||
 | 
						if (Object.keys(allAllowedDomains).some(function (k) {
 | 
				
			||||||
 | 
							return allAllowedDomains[k].some(function (name) {
 | 
				
			||||||
 | 
								if (domain === name || wild === name) {
 | 
				
			||||||
 | 
					        related = { subject: k, altnames: allAllowedDomains[k] };
 | 
				
			||||||
 | 
									return true;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					  })) {
 | 
				
			||||||
 | 
					    return related;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```js
 | 
				
			||||||
 | 
					var allAllowedDomains = {
 | 
				
			||||||
 | 
					  'example.com': ['example.com', '*.example.com']
 | 
				
			||||||
 | 
					, 'example.net': ['example.net', '*.example.net']
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
							
								
								
									
										299
									
								
								index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										299
									
								
								index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,299 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*global Promise*/
 | 
				
			||||||
 | 
					var PromiseA;
 | 
				
			||||||
 | 
					var util = require('util');
 | 
				
			||||||
 | 
					if (!util.promisify) {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    PromiseA = require('bluebird');
 | 
				
			||||||
 | 
					    util.promisify = PromiseA.promisify;
 | 
				
			||||||
 | 
					  } catch(e) {
 | 
				
			||||||
 | 
					    console.error("Your version of node is missing Promise. Please run `npm install --save bluebird` in your project to fix");
 | 
				
			||||||
 | 
					    process.exit(10);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					if ('undefined' !== typeof Promise) { PromiseA = Promise; }
 | 
				
			||||||
 | 
					var fs = require('fs');
 | 
				
			||||||
 | 
					var path = require('path');
 | 
				
			||||||
 | 
					var readFileAsync = util.promisify(fs.readFile);
 | 
				
			||||||
 | 
					var writeFileAsync = util.promisify(fs.writeFile);
 | 
				
			||||||
 | 
					var sfs = require('safe-replace');
 | 
				
			||||||
 | 
					var mkdirpAsync = util.promisify(require('mkdirp'));
 | 
				
			||||||
 | 
					var os = require("os");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// create():
 | 
				
			||||||
 | 
					// Your storage plugin may take special options, or it may not.
 | 
				
			||||||
 | 
					// If it does, document to your users that they must call create() with those options.
 | 
				
			||||||
 | 
					// If you user does not call create(), greenlock will call it for you with the options it has.
 | 
				
			||||||
 | 
					// It's kind of stupid, but it's done this way so that it can be more convenient for users to not repeat shared options
 | 
				
			||||||
 | 
					// (such as the config directory), but sometimes configs would clash. I hate having ambiguity, so I may change this in
 | 
				
			||||||
 | 
					// a future version, but it's very much an issue of "looks cleaner" vs "behaves cleaner".
 | 
				
			||||||
 | 
					module.exports.create = function (config) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // This file has been laid out in the order that options are used and calls are made
 | 
				
			||||||
 | 
					  // greenlock.approveDomains)
 | 
				
			||||||
 | 
					  //   greenlock.store.certificates.checkAsync()
 | 
				
			||||||
 | 
					  //   greenlock.store.accounts.checkAsync()
 | 
				
			||||||
 | 
					  //   greenlock.store.accounts.setKeypairAsync()
 | 
				
			||||||
 | 
					  //   greenlock.store.accounts.setAsync()
 | 
				
			||||||
 | 
					  //   greenlock.store.certificates.checkKeypairAsync()
 | 
				
			||||||
 | 
					  //   greenlock.store.certificates.setKeypairAsync()
 | 
				
			||||||
 | 
					  //   greenlock.store.certificates.setAsync()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // store
 | 
				
			||||||
 | 
					  // Bear in mind that the only time any of this gets called is on first access after startup, new registration,
 | 
				
			||||||
 | 
					  // and renewal - so none of this needs to be particularly fast. It may need to be memory efficient, however
 | 
				
			||||||
 | 
					  // (if you have more than 10,000 domains, for example).
 | 
				
			||||||
 | 
					  var store = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // options:
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  // If your module requires options (i.e. file paths or database urls) you should check what you get from create()
 | 
				
			||||||
 | 
					  // and copy over the things you'll use into this options object. You should also merge in any defaults for options
 | 
				
			||||||
 | 
					  // that have not been set. This object should not be circular, should not be changed after it is set, and should
 | 
				
			||||||
 | 
					  // contain every property that you can use, using falsey JSON-able values like 0, null, false, or '' for "unset"
 | 
				
			||||||
 | 
					  // values.
 | 
				
			||||||
 | 
					  // See the note on create() above.
 | 
				
			||||||
 | 
					  store.options = mergeOptions(config);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // getOptions():
 | 
				
			||||||
 | 
					  // This must be implemented for backwards compatibility. That is all.
 | 
				
			||||||
 | 
					  store.getOptions = function () { return store.options; };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // set and check account keypairs and account data
 | 
				
			||||||
 | 
					  store.accounts = {};
 | 
				
			||||||
 | 
					  // set and check domain keypairs and domain certificates
 | 
				
			||||||
 | 
					  store.certificates = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // certificates.checkAsync({ subject, ... }):
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  // The first check is that a certificate looked for by domain name.
 | 
				
			||||||
 | 
					  // If that lookup succeeds, then nothing else needs to happen. Otherwise accounts.checkAsync will happen next.
 | 
				
			||||||
 | 
					  // What should happen here is a lookup in a database (or filesystem). Generally the pattern will be to see if the
 | 
				
			||||||
 | 
					  // domain is an exact match for a single-subject (single domain) or multi-subject (many domains via SANS/AltName)
 | 
				
			||||||
 | 
					  // and then stripping the first part of the domain to see if there's a wildcard match. If you're clever you could
 | 
				
			||||||
 | 
					  // also do these checks in parallel, but this only happens at startup and before renewal, so you don't have to get
 | 
				
			||||||
 | 
					  // unless you want to for fun.
 | 
				
			||||||
 | 
					  // The only input you need to be concerned with is opts.subject (which falls back to opts.domains[0] if not set).
 | 
				
			||||||
 | 
					  // However, this is called after `approveDomains)`, so any options that you set there will be available here too,
 | 
				
			||||||
 | 
					  // as well as any other config you might need to access from other modules, if you're doing something special.
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  // On Success: Promise.resolve({ ... }) - the pem or jwk for the certificate
 | 
				
			||||||
 | 
					  // On Failure: Promise.resolve(null) - do not return undefined, do not throw, do not reject
 | 
				
			||||||
 | 
					  // On Error: Promise.reject(new Error("something descriptive for the user"))
 | 
				
			||||||
 | 
					  store.certificates.checkAsync = function (opts) {
 | 
				
			||||||
 | 
					    // { domain, ... }
 | 
				
			||||||
 | 
					    console.log('certificates.checkAsync for', opts.domain, opts.subject, opts.domains);
 | 
				
			||||||
 | 
					    console.log(opts);
 | 
				
			||||||
 | 
					    console.log(new Error("just for the stack trace:").stack);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Just to show that any options set in approveDomains will be available here
 | 
				
			||||||
 | 
					    // (the same is true for all of the hooks in this file)
 | 
				
			||||||
 | 
					    if (opts.exampleThrowError) { return Promise.reject(new Error("You want an error? You got it!")); }
 | 
				
			||||||
 | 
					    if (opts.exampleReturnNull) { return Promise.resolve(null); }
 | 
				
			||||||
 | 
					    if (opts.exampleReturnCerts) { return Promise.resolve(opts.exampleReturnCerts); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var liveDir = opts.liveDir || path.join(opts.configDir, 'live', opts.subject);
 | 
				
			||||||
 | 
					    // TODO this shouldn't be necessary here (we should get it from checkKeypairAsync)
 | 
				
			||||||
 | 
					    var privkeyPath = opts.privkeyPath || opts.domainKeyPath || path.join(liveDir, 'privkey.pem');
 | 
				
			||||||
 | 
					    var certPath = opts.certPath || path.join(liveDir, 'cert.pem');
 | 
				
			||||||
 | 
					    var chainPath = opts.chainPath || path.join(liveDir, 'chain.pem');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return PromiseA.all([
 | 
				
			||||||
 | 
					      readFileAsync(privkeyPath, 'ascii')   // 0
 | 
				
			||||||
 | 
					    , readFileAsync(certPath, 'ascii')      // 1
 | 
				
			||||||
 | 
					    , readFileAsync(chainPath, 'ascii')     // 2
 | 
				
			||||||
 | 
					    ]).then(function (all) {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        privkey: all[0]
 | 
				
			||||||
 | 
					      , cert: all[1]
 | 
				
			||||||
 | 
					      , chain: all[2]
 | 
				
			||||||
 | 
					      // When using a database, these should be retrieved
 | 
				
			||||||
 | 
					      // (as is they'll be read via cert-info)
 | 
				
			||||||
 | 
					      //, subject: certinfo.subject
 | 
				
			||||||
 | 
					      //, altnames: certinfo.altnames
 | 
				
			||||||
 | 
					      //, issuedAt: certinfo.issuedAt // a.k.a. NotBefore
 | 
				
			||||||
 | 
					      //, expiresAt: certinfo.expiresAt // a.k.a. NotAfter
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    }).catch(function (err) {
 | 
				
			||||||
 | 
					      if ('ENOENT' === err.code) { return null; }
 | 
				
			||||||
 | 
					      throw err;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // accounts.checkAsync({ accountId, email, [...] }): // Optional
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  // This is where you promise an account corresponding to the given the email and ID. All instance options
 | 
				
			||||||
 | 
					  // (i.e. 'options' above, merged with other "override" or per-use options, such as from 'approveDomains)')
 | 
				
			||||||
 | 
					  // are also available. You can ignore them unless your implementation is using them in some way.
 | 
				
			||||||
 | 
					  // You should error if the account cannot be found (otherwise an unexpected error will be thrown)
 | 
				
			||||||
 | 
					  // Although you can supply a 'check' thunk (node-style callback) here, it's going to be converted to a proper
 | 
				
			||||||
 | 
					  // promise, so just go ahead and use that from the get-go.
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  // On Success: Promise.resolve({ id, keypair, ... }) - an id and, for backwards compatibility, the abstract keypair
 | 
				
			||||||
 | 
					  // On Failure: Promise.resolve(null) - do not return undefined, do not throw, do not reject
 | 
				
			||||||
 | 
					  // On Error: Promise.reject(new Error("something descriptive for the user"))
 | 
				
			||||||
 | 
					  store.accounts.checkAsync = function (opts) {
 | 
				
			||||||
 | 
					    var id = opts.account.id || 'single-user';
 | 
				
			||||||
 | 
					    console.log('accounts.checkAsync for', id);
 | 
				
			||||||
 | 
					    // Since accounts are based on public key, the act of creating a new account or returning an existing account
 | 
				
			||||||
 | 
					    // are the same in regards to the API and so we don't really need to store the account id or retrieve it.
 | 
				
			||||||
 | 
					    // This method only needs to be implemented if you need it for your own purposes
 | 
				
			||||||
 | 
					    return Promise.resolve(null);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // accounts.checkKeypairAsync({ email, ... }):
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  // Same rules as above apply, except for the private key of the account, not the account object itself.
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  // On Success: Promise.resolve({ ... }) - the abstract object representing the keypair
 | 
				
			||||||
 | 
					  // On Failure: Promise.resolve(null) - do not return undefined, do not throw, do not reject
 | 
				
			||||||
 | 
					  // On Error: Promise.reject(new Error("something descriptive for the user"))
 | 
				
			||||||
 | 
					  store.accounts.checkKeypairAsync = function (opts) {
 | 
				
			||||||
 | 
					    var id = opts.account.id || 'single-user';
 | 
				
			||||||
 | 
					    console.log('accounts.checkKeypairAsync for', id);
 | 
				
			||||||
 | 
					    if (!opts.account.id) { return Promise.reject(new Error("'account.id' should have been set in approveDomains()")); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return readFileAsync(path.join(opts.accountsDir, sanitizeFilename(id) + '.json'), 'utf8').then(function (blob) {
 | 
				
			||||||
 | 
					      // keypair is an opaque object that should be treated as blob
 | 
				
			||||||
 | 
					      return JSON.parse(blob);
 | 
				
			||||||
 | 
					    }).catch(function (err) {
 | 
				
			||||||
 | 
					      if ('ENOENT' === err.code) { return null; }
 | 
				
			||||||
 | 
					      throw err;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // accounts.setKeypairAsync({ keypair, email, ... }):
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  // The keypair details (RSA, ECDSA, etc) are chosen either by the greenlock defaults, global user defaults,
 | 
				
			||||||
 | 
					  // or whatever you set in approveDomains)
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  // On Success: Promise.resolve(null) - just knowing the operation is successful will do
 | 
				
			||||||
 | 
					  // On Error: Promise.reject(new Error("something descriptive for the user"))
 | 
				
			||||||
 | 
					  store.accounts.setKeypairAsync = function (opts, keypair) {
 | 
				
			||||||
 | 
					    var id = opts.account.id || 'single-user';
 | 
				
			||||||
 | 
					    console.log('accounts.setKeypairAsync for', id);
 | 
				
			||||||
 | 
					    keypair = opts.keypair || keypair;
 | 
				
			||||||
 | 
					    if (!opts.account.id) { return Promise.reject(new Error("'account.id' should have been set in approveDomains()")); }
 | 
				
			||||||
 | 
					    return mkdirpAsync(opts.accountsDir).then(function () {
 | 
				
			||||||
 | 
					      // keypair is an opaque object that should be treated as blob
 | 
				
			||||||
 | 
					      return writeFileAsync(path.join(opts.accountsDir, sanitizeFilename(id) + '.json'), JSON.stringify(keypair), 'utf8');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // accounts.setAsync({ account, keypair, email, ... }):
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  // The account details, from ACME, if everything is successful.
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  // On Success: Promise.resolve(null||{ id }) - do not return undefined, do not throw, do not reject
 | 
				
			||||||
 | 
					  // On Error: Promise.reject(new Error("something descriptive for the user"))
 | 
				
			||||||
 | 
					  store.accounts.setAsync = function (opts, receipt) {
 | 
				
			||||||
 | 
					    receipt = opts.receipt || receipt;
 | 
				
			||||||
 | 
					    console.log('account.setAsync:', receipt);
 | 
				
			||||||
 | 
					    return Promise.resolve(null);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // certificates.checkKeypairAsync({ subject, ... }):
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  // Same rules as above apply, except for the private key of the certificate, not the public certificate itself.
 | 
				
			||||||
 | 
					  store.certificates.checkKeypairAsync = function (opts) {
 | 
				
			||||||
 | 
					    console.log('certificates.checkKeypairAsync:');
 | 
				
			||||||
 | 
					    var liveDir = opts.liveDir || path.join(opts.configDir, 'live', opts.subject);
 | 
				
			||||||
 | 
					    var privkeyPath = opts.privkeyPath || opts.domainKeyPath || path.join(liveDir, 'privkey.pem');
 | 
				
			||||||
 | 
					    return readFileAsync(privkeyPath, 'ascii').then(function (key) {
 | 
				
			||||||
 | 
					      // keypair is normally an opaque object, but here it's a pem for the filesystem
 | 
				
			||||||
 | 
					      return { privateKeyPem: key };
 | 
				
			||||||
 | 
					    }).catch(function (err) {
 | 
				
			||||||
 | 
					      if ('ENOENT' === err.code) { return null; }
 | 
				
			||||||
 | 
					      throw err;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // certificates.setKeypairAsync({ domain, keypair, ... }):
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  // Same as accounts.setKeypairAsync, but by domains rather than email / accountId
 | 
				
			||||||
 | 
					  store.certificates.setKeypairAsync = function (opts, keypair) {
 | 
				
			||||||
 | 
					    keypair = opts.keypair || keypair;
 | 
				
			||||||
 | 
					    var liveDir = opts.liveDir || path.join(opts.configDir, 'live', opts.subject);
 | 
				
			||||||
 | 
					    var privkeyPath = opts.privkeyPath || opts.domainKeyPath || path.join(liveDir, 'privkey.pem');
 | 
				
			||||||
 | 
					    // keypair is normally an opaque object, but here it's a PEM for the FS
 | 
				
			||||||
 | 
					    return mkdirpAsync(path.dirname(privkeyPath)).then(function () {
 | 
				
			||||||
 | 
					      return writeFileAsync(privkeyPath, keypair.privateKeyPem, 'ascii').then(function () {
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // certificates.setAsync({ domain, certs, ... }):
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  // This is where certificates are set, as well as certinfo
 | 
				
			||||||
 | 
					  store.certificates.setAsync = function (opts) {
 | 
				
			||||||
 | 
					    console.log('certificates.setAsync:');
 | 
				
			||||||
 | 
					    console.log(opts.domain, '<=', opts.subject);
 | 
				
			||||||
 | 
					    var pems = {
 | 
				
			||||||
 | 
					      privkey: opts.pems.privkey
 | 
				
			||||||
 | 
					    , cert: opts.pems.cert
 | 
				
			||||||
 | 
					    , chain: opts.pems.chain
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    var liveDir = opts.liveDir || path.join(opts.configDir, 'live', opts.subject);
 | 
				
			||||||
 | 
					    var certPath = opts.certPath || path.join(liveDir, 'cert.pem');
 | 
				
			||||||
 | 
					    var fullchainPath = opts.fullchainPath || path.join(liveDir, 'fullchain.pem');
 | 
				
			||||||
 | 
					    var chainPath = opts.chainPath || path.join(liveDir, 'chain.pem');
 | 
				
			||||||
 | 
					    //var privkeyPath = opts.privkeyPath || opts.domainKeyPath || path.join(liveDir, 'privkey.pem');
 | 
				
			||||||
 | 
					    var bundlePath = opts.bundlePath || path.join(liveDir, 'bundle.pem');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return mkdirpAsync(path.dirname(certPath)).then(function () {
 | 
				
			||||||
 | 
					      return mkdirpAsync(path.dirname(chainPath)).then(function () {
 | 
				
			||||||
 | 
					        return mkdirpAsync(path.dirname(fullchainPath)).then(function () {
 | 
				
			||||||
 | 
					          return mkdirpAsync(path.dirname(bundlePath)).then(function () {
 | 
				
			||||||
 | 
					            return PromiseA.all([
 | 
				
			||||||
 | 
					              sfs.writeFileAsync(certPath, pems.cert, 'ascii')
 | 
				
			||||||
 | 
					            , sfs.writeFileAsync(chainPath, pems.chain, 'ascii')
 | 
				
			||||||
 | 
					              // Most platforms need these two
 | 
				
			||||||
 | 
					            , sfs.writeFileAsync(fullchainPath, [ pems.cert, pems.chain ].join('\n'), 'ascii')
 | 
				
			||||||
 | 
					            //, sfs.writeFileAsync(privkeyPath, pems.privkey, 'ascii')
 | 
				
			||||||
 | 
					              // HAProxy needs "bundle.pem" aka "combined.pem"
 | 
				
			||||||
 | 
					            , sfs.writeFileAsync(bundlePath, [ pems.privkey, pems.cert, pems.chain ].join('\n'), 'ascii')
 | 
				
			||||||
 | 
					            ]);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }).then(function () {
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return store;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var defaults = {
 | 
				
			||||||
 | 
					  configDir: path.join(os.homedir(), 'acme', 'etc')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					, accountsDir: path.join(':configDir', 'accounts', ':serverDir')
 | 
				
			||||||
 | 
					, serverDirGet: function (copy) {
 | 
				
			||||||
 | 
					    return (copy.server || '').replace('https://', '').replace(/(\/)$/, '').replace(/\//g, path.sep);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					, privkeyPath: path.join(':configDir', 'live', ':hostname', 'privkey.pem')
 | 
				
			||||||
 | 
					, fullchainPath: path.join(':configDir', 'live', ':hostname', 'fullchain.pem')
 | 
				
			||||||
 | 
					, certPath: path.join(':configDir', 'live', ':hostname', 'cert.pem')
 | 
				
			||||||
 | 
					, chainPath: path.join(':configDir', 'live', ':hostname', 'chain.pem')
 | 
				
			||||||
 | 
					, bundlePath: path.join(':configDir', 'live', ':hostname', 'bundle.pem')
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function mergeOptions(configs) {
 | 
				
			||||||
 | 
					  if (!configs.domainKeyPath) {
 | 
				
			||||||
 | 
					    configs.domainKeyPath = configs.privkeyPath || defaults.privkeyPath;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Object.keys(defaults).forEach(function (key) {
 | 
				
			||||||
 | 
					    if (!configs[key]) {
 | 
				
			||||||
 | 
					      configs[key] = defaults[key];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return configs;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function sanitizeFilename(id) {
 | 
				
			||||||
 | 
					  return id.replace(/(\.\.)|\\|\//g, '_').replace(/[^!-~]/g, '_');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										26
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "le-store-json",
 | 
				
			||||||
 | 
					  "version": "1.0.0",
 | 
				
			||||||
 | 
					  "lockfileVersion": 1,
 | 
				
			||||||
 | 
					  "requires": true,
 | 
				
			||||||
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "minimist": {
 | 
				
			||||||
 | 
					      "version": "0.0.8",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "mkdirp": {
 | 
				
			||||||
 | 
					      "version": "0.5.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
 | 
				
			||||||
 | 
					      "requires": {
 | 
				
			||||||
 | 
					        "minimist": "0.0.8"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "safe-replace": {
 | 
				
			||||||
 | 
					      "version": "1.1.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/safe-replace/-/safe-replace-1.1.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-9/V2E0CDsKs9DWOOwJH7jYpSl9S3N05uyevNjvsnDauBqRowBPOyot1fIvV5N2IuZAbYyvrTXrYFVG0RZInfFw=="
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										31
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "le-store-fs",
 | 
				
			||||||
 | 
					  "version": "0.9.0",
 | 
				
			||||||
 | 
					  "description": "A file-based certificate store for greenlock that supports wildcards.",
 | 
				
			||||||
 | 
					  "homepage": "https://git.coolaj86.com/coolaj86/le-store-fs.js",
 | 
				
			||||||
 | 
					  "main": "index.js",
 | 
				
			||||||
 | 
					  "directories": {
 | 
				
			||||||
 | 
					    "test": "tests"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "scripts": {
 | 
				
			||||||
 | 
					    "test": "node tests"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "repository": {
 | 
				
			||||||
 | 
					    "type": "git",
 | 
				
			||||||
 | 
					    "url": "https://git.coolaj86.com/coolaj86/le-store-fs.js.git"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "keywords": [
 | 
				
			||||||
 | 
					    "greenlock",
 | 
				
			||||||
 | 
					    "json",
 | 
				
			||||||
 | 
					    "keypairs",
 | 
				
			||||||
 | 
					    "certificates",
 | 
				
			||||||
 | 
					    "store",
 | 
				
			||||||
 | 
					    "database"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
 | 
				
			||||||
 | 
					  "license": "MPL-2.0",
 | 
				
			||||||
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "mkdirp": "^0.5.1",
 | 
				
			||||||
 | 
					    "safe-replace": "^1.1.0"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user