Compare commits
	
		
			7 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b8fe2e5cbe | |||
| 5490a194eb | |||
| e0a9fff07d | |||
| de0f4d25b4 | |||
| ba284d0004 | |||
| 6d398d36c4 | |||
| a4aae8647d | 
							
								
								
									
										7
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | { | ||||||
|  |   "bracketSpacing": true, | ||||||
|  |   "printWidth": 80, | ||||||
|  |   "tabWidth": 4, | ||||||
|  |   "trailingComma": "none", | ||||||
|  |   "useTabs": false | ||||||
|  | } | ||||||
							
								
								
									
										217
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										217
									
								
								README.md
									
									
									
									
									
								
							| @ -1,37 +1,57 @@ | |||||||
| # [greenlock-store-fs](https://git.coolaj86.com/coolaj86/greenlock-store-fs.js) | A [Root](https://rootprojects.org) project | # [greenlock-store-fs](https://git.rootprojects.org/root/greenlock-store-fs.js) | A [Root](https://rootprojects.org) project | ||||||
| 
 | 
 | ||||||
| A keypair and certificate storage strategy for Greenlock v2.7+ (and v3). | A keypair and certificate storage strategy for Greenlock v2.7+ (and v3). | ||||||
| The (much simpler) successor to le-store-certbot. | The (much simpler) successor to le-store-certbot. | ||||||
| 
 | 
 | ||||||
| Works with all ACME (Let's Encrypt) SSL certificate sytles: | Works with all ACME (Let's Encrypt) SSL certificate sytles: | ||||||
| * [x] single domains | 
 | ||||||
| * [x] multiple domains (SANs, AltNames) | -   [x] single domains | ||||||
| * [x] wildcards | -   [x] multiple domains (SANs, AltNames) | ||||||
| * [x] private / localhost domains | -   [x] wildcards | ||||||
|  | -   [x] private / localhost domains | ||||||
| 
 | 
 | ||||||
| # Usage | # Usage | ||||||
| 
 | 
 | ||||||
|  | **Global** config: | ||||||
|  | 
 | ||||||
| ```js | ```js | ||||||
| var greenlock = require('greenlock'); | greenlock.manager.defaults({ | ||||||
| var gl = greenlock.create({ |     store: { | ||||||
|   configDir: '~/.config/acme' |         module: "greenlock-store-fs", | ||||||
| , store: require('greenlock-store-fs') |         basePath: "~/.config/greenlock" | ||||||
| , approveDomains: approveDomains |     } | ||||||
| , ... | }); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | **Per-site** config: | ||||||
|  | 
 | ||||||
|  | ```js | ||||||
|  | greenlock.add({ | ||||||
|  |     subject: "example.com", | ||||||
|  |     altnames: ["example.com", "www.example.com"], | ||||||
|  |     store: { | ||||||
|  |         module: "greenlock-store-fs", | ||||||
|  |         basePath: "~/.config/greenlock" | ||||||
|  |     } | ||||||
| }); | }); | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| # File System | # File System | ||||||
| 
 | 
 | ||||||
| The default file system layout mirrors that of le-store-certbot in order to make transitioning effortless, | The default file system layout mirrors that of certbot (python Let's Encrypt implementation) and | ||||||
| in most situations: | the prior le-store-certbot in order to make transitioning effortless. | ||||||
| 
 | 
 | ||||||
| ``` | The default structure looks like this: | ||||||
| acme | 
 | ||||||
|  | ```txt | ||||||
|  | .config | ||||||
|  | └── greenlock | ||||||
|     ├── accounts |     ├── accounts | ||||||
|     │   └── acme-staging-v02.api.letsencrypt.org |     │   └── acme-staging-v02.api.letsencrypt.org | ||||||
|     │       └── directory |     │       └── directory | ||||||
|     │           └── sites@example.com.json |     │           └── sites@example.com.json | ||||||
|  |     ├── staging | ||||||
|  |     │   └── (same as live) | ||||||
|     └── live |     └── live | ||||||
|         ├── example.com |         ├── example.com | ||||||
|         │   ├── bundle.pem |         │   ├── bundle.pem | ||||||
| @ -47,72 +67,129 @@ acme | |||||||
|             └── privkey.pem |             └── privkey.pem | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| # Wildcards & AltNames | # Internal Implementation Details | ||||||
| 
 | 
 | ||||||
| Working with wildcards and multiple altnames requires greenlock >= v2.7 (or v3). | You **DO NOT NEED TO KNOW** these details. | ||||||
| 
 | 
 | ||||||
| To do so you must return `{ subject: '...', altnames: ['...', ...] }` within the `approveDomains()` callback. | They're provided for the sake of understanding what happens "under the hood" | ||||||
|  | to help you make better choices "in the seat". | ||||||
| 
 | 
 | ||||||
| `subject` refers to "the subject of the ssl certificate" as opposed to `domain` which indicates "the domain servername | **Note**: The actual code could stand to be tidied up. | ||||||
| used in the current request". For single-domain certificates they're always the same, but for multiple-domain | It does need to continue to support Greenlock v2 for a few more months, | ||||||
| certificates `subject` must be the name no matter what `domain` is receiving a request. `subject` is used as | so I didn't rip out the old v1 -> v2 -> v3 cruft yet. | ||||||
| part of the name of the file storage path where the certificate will be saved (or retrieved). |  | ||||||
| 
 | 
 | ||||||
| `altnames` should be the list of SubjectAlternativeNames (SANs) on the certificate. | # Parameters | ||||||
| The subject and the first altname must be an exact match: `subject === altnames[0]`. |  | ||||||
| 
 | 
 | ||||||
| ## Simple Example | | parameters        | example                                                  | notes            | | ||||||
|  | | ----------------- | -------------------------------------------------------- | ---------------- | | ||||||
|  | | `env`             | `staging` or `live`                                      | -                | | ||||||
|  | | `directoryUrl`    | `https://acme-staging-v02.api.letsencrypt.org/directory` | -                | | ||||||
|  | | `keypair`         | `{ privateKeyPem, privateKeyJwk }`                       |                  | | ||||||
|  | | `account`         | `{ id: "an-arbitrary-id" }`                              | account only     | | ||||||
|  | | `subscriberEmail` | `webhost@example.com`                                    | account only     | | ||||||
|  | | `certificate`     | `{ id: "an-arbitrary-id" }`                              | certificate only | | ||||||
|  | | `subject`         | `example.com`                                            | certificate only | | ||||||
|  | | `pems`            | `{ privkey, cert, chain, issuedAt, expiresAt }`          | certificate only | | ||||||
|  | 
 | ||||||
|  | ### Account Keypair | ||||||
| 
 | 
 | ||||||
| ```js | ```js | ||||||
| function approveDomains(opts) { | accounts.setKeypair = async function({ | ||||||
|   // Allow only example.com and *.example.com (such as foo.example.com) |     env, | ||||||
| 
 |     basePath, | ||||||
|   // foo.example.com => *.example.com |     directoryUrl, | ||||||
|   var wild = '*.' + opts.domain.split('.').slice(1).join('.'); |     email, | ||||||
| 
 |     account | ||||||
|   if ('example.com' !== opts.domain && '*.example.com' !== wild) { | }) { | ||||||
|     cb(new Error(opts.domain + " is not allowed")); |     var id = account.id || email; | ||||||
|   } |     var serverDir = directoryUrl.replace("https://", ""); | ||||||
| 
 | }; | ||||||
|   var result = { subject: 'example.com', altnames: [ 'example.com', '*.example.com' ] }; |  | ||||||
|   return Promise.resolve(result); |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ## 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 | ```js | ||||||
| function getRelated(domain) { | accounts.checkKeypair = async function({ | ||||||
|   var related; |     env, | ||||||
|   var wild = '*.' + domain.split('.').slice(1).join('.'); |     basePath, | ||||||
|   if (Object.keys(allAllowedDomains).some(function (k) { |     directoryUrl, | ||||||
|     return allAllowedDomains[k].some(function (name) { |     email, | ||||||
|       if (domain === name || wild === name) { |     account | ||||||
|         related = { subject: k, altnames: allAllowedDomains[k] }; | }) { | ||||||
|         return true; |     var id = account.id || email; | ||||||
|       } |     var serverDir = directoryUrl.replace("https://", ""); | ||||||
|     }); | 
 | ||||||
|   })) { |     return { | ||||||
|     return related; |         privateKeyPem, | ||||||
|   } |         privateKeyJwk | ||||||
| } |     }; | ||||||
|  | }; | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### Certificate Keypair | ||||||
|  | 
 | ||||||
|  | ```js | ||||||
|  | certificate.setKeypair = async function({ | ||||||
|  |     env, | ||||||
|  |     basePath, | ||||||
|  |     directoryUrl, | ||||||
|  |     subject, | ||||||
|  |     certificate | ||||||
|  | }) { | ||||||
|  |     var id = account.id || email; | ||||||
|  |     env = env || directoryUrl.replace("https://", ""); | ||||||
|  | }; | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ```js | ```js | ||||||
| var allAllowedDomains = { | certificate.checkKeypair = async function({ | ||||||
|   'example.com': ['example.com', '*.example.com'] |     env, | ||||||
| , 'example.net': ['example.net', '*.example.net'] |     basePath, | ||||||
| } |     directoryUrl, | ||||||
|  |     subject, | ||||||
|  |     certificate | ||||||
|  | }) { | ||||||
|  |     var id = account.id || email; | ||||||
|  |     env = env || directoryUrl.replace("https://", ""); | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |         privateKeyPem, | ||||||
|  |         privateKeyJwk | ||||||
|  |     }; | ||||||
|  | }; | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### Certificate PEMs | ||||||
|  | 
 | ||||||
|  | ```js | ||||||
|  | certificate.set = async function({ | ||||||
|  |     env, | ||||||
|  |     basePath, | ||||||
|  |     directoryUrl, | ||||||
|  |     subject, | ||||||
|  |     certificate, | ||||||
|  |     pems | ||||||
|  | }) { | ||||||
|  |     var id = account.id || email; | ||||||
|  |     env = env || directoryUrl.replace("https://", ""); | ||||||
|  | }; | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ```js | ||||||
|  | certificate.check = async function({ | ||||||
|  |     env, | ||||||
|  |     basePath, | ||||||
|  |     directoryUrl, | ||||||
|  |     subject, | ||||||
|  |     certificate | ||||||
|  | }) { | ||||||
|  |     var id = account.id || email; | ||||||
|  |     env = env || directoryUrl.replace("https://", ""); | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |         privkey, | ||||||
|  |         cert, | ||||||
|  |         chain, | ||||||
|  |         issuedAt, | ||||||
|  |         expiresAt | ||||||
|  |     }; | ||||||
|  | }; | ||||||
| ``` | ``` | ||||||
|  | |||||||
							
								
								
									
										113
									
								
								accounts.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								accounts.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,113 @@ | |||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | var accounts = module.exports; | ||||||
|  | var store = accounts; | ||||||
|  | var U = require("./utils.js"); | ||||||
|  | 
 | ||||||
|  | var fs = require("fs"); | ||||||
|  | var path = require("path"); | ||||||
|  | var PromiseA = require("./promise.js"); | ||||||
|  | var readFileAsync = PromiseA.promisify(fs.readFile); | ||||||
|  | var writeFileAsync = PromiseA.promisify(fs.writeFile); | ||||||
|  | var mkdirpAsync = PromiseA.promisify(require("@root/mkdirp")); | ||||||
|  | 
 | ||||||
|  | // Implement if you need the ACME account metadata elsewhere in the chain of events
 | ||||||
|  | //store.accounts.check = function (opts) {
 | ||||||
|  | //  console.log('accounts.check for', opts.account, opts.email);
 | ||||||
|  | //  return PromiseA.resolve(null);
 | ||||||
|  | //};
 | ||||||
|  | 
 | ||||||
|  | // Accounts.checkKeypair
 | ||||||
|  | //
 | ||||||
|  | // Use account.id, or email, if id hasn't been set, to find an account keypair.
 | ||||||
|  | // Return an object with string privateKeyPem and/or object privateKeyJwk (or null, not undefined)
 | ||||||
|  | accounts.checkKeypair = function(opts) { | ||||||
|  |     var id = | ||||||
|  |         (opts.account && opts.account.id) || | ||||||
|  |         (opts.subscriberEmail || opts.email) || | ||||||
|  |         "single-user"; | ||||||
|  |     //console.log('accounts.checkKeypair for', id);
 | ||||||
|  | 
 | ||||||
|  |     var pathname = path.join( | ||||||
|  |         accountsDir(store, opts), | ||||||
|  |         sanitizeFilename(id) + ".json" | ||||||
|  |     ); | ||||||
|  |     return readFileAsync(U._tameWild(pathname, opts.subject), "utf8") | ||||||
|  |         .then(function(blob) { | ||||||
|  |             // keypair can treated as an opaque object and just passed along,
 | ||||||
|  |             // but just to show you what it is...
 | ||||||
|  |             var keypair = JSON.parse(blob); | ||||||
|  |             return keypair; | ||||||
|  |             /* | ||||||
|  |       { | ||||||
|  | 				privateKeyPem: keypair.privateKeyPem, // string PEM private key
 | ||||||
|  | 				privateKeyJwk: keypair.privateKeyJwk, // object JWK private key
 | ||||||
|  | 				private: keypair.private, | ||||||
|  | 				public: keypair.public | ||||||
|  | 			}; | ||||||
|  |       */ | ||||||
|  |         }) | ||||||
|  |         .catch(function(err) { | ||||||
|  |             if ("ENOENT" === err.code) { | ||||||
|  |                 return null; | ||||||
|  |             } | ||||||
|  |             throw err; | ||||||
|  |         }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // Accounts.setKeypair({ account, email, keypair, ... }):
 | ||||||
|  | //
 | ||||||
|  | // Use account.id (or email if no id is present) to save an account keypair
 | ||||||
|  | // Return null (not undefined) on success, or throw on error
 | ||||||
|  | accounts.setKeypair = function(opts) { | ||||||
|  |     //console.log('accounts.setKeypair for', opts.account, opts.email, opts.keypair);
 | ||||||
|  |     var id = opts.account.id || opts.email || "single-user"; | ||||||
|  | 
 | ||||||
|  |     // you can just treat the keypair as opaque and save and retrieve it as JSON
 | ||||||
|  |     var keyblob = JSON.stringify(opts.keypair); | ||||||
|  |     /* | ||||||
|  | 	var keyblob = JSON.stringify({ | ||||||
|  | 		privateKeyPem: opts.keypair.privateKeyPem, // string PEM
 | ||||||
|  | 		privateKeyJwk: opts.keypair.privateKeyJwk, // object JWK
 | ||||||
|  |     private: opts.keypair.private | ||||||
|  | 	}); | ||||||
|  |   */ | ||||||
|  | 
 | ||||||
|  |     // Ignore.
 | ||||||
|  |     // Just implementation specific details here.
 | ||||||
|  |     return mkdirpAsync(accountsDir(store, opts)) | ||||||
|  |         .then(function() { | ||||||
|  |             var pathname = path.join( | ||||||
|  |                 accountsDir(store, opts), | ||||||
|  |                 sanitizeFilename(id) + ".json" | ||||||
|  |             ); | ||||||
|  |             return writeFileAsync( | ||||||
|  |                 U._tameWild(pathname, opts.subject), | ||||||
|  |                 keyblob, | ||||||
|  |                 "utf8" | ||||||
|  |             ); | ||||||
|  |         }) | ||||||
|  |         .then(function() { | ||||||
|  |             // This is your job: return null, not undefined
 | ||||||
|  |             return null; | ||||||
|  |         }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // Implement if you need the ACME account metadata elsewhere in the chain of events
 | ||||||
|  | //accounts.set = function (opts) {
 | ||||||
|  | //  console.log('account.set:', opts.account, opts.email, opts.receipt);
 | ||||||
|  | //  return PromiseA.resolve(null);
 | ||||||
|  | //};
 | ||||||
|  | 
 | ||||||
|  | function sanitizeFilename(id) { | ||||||
|  |     return id.replace(/(\.\.)|\\|\//g, "_").replace(/[^!-~]/g, "_"); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function accountsDir(store, opts) { | ||||||
|  |     var dir = U._tpl( | ||||||
|  |         store, | ||||||
|  |         opts, | ||||||
|  |         opts.accountsDir || store.options.accountsDir | ||||||
|  |     ); | ||||||
|  |     return U._tameWild(dir, opts.subject || ""); | ||||||
|  | } | ||||||
							
								
								
									
										265
									
								
								certificates.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										265
									
								
								certificates.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,265 @@ | |||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | var certificates = module.exports; | ||||||
|  | var store = certificates; | ||||||
|  | var U = require("./utils.js"); | ||||||
|  | 
 | ||||||
|  | var fs = require("fs"); | ||||||
|  | var path = require("path"); | ||||||
|  | var PromiseA = require("./promise.js"); | ||||||
|  | var sfs = require("safe-replace"); | ||||||
|  | var readFileAsync = PromiseA.promisify(fs.readFile); | ||||||
|  | var writeFileAsync = PromiseA.promisify(fs.writeFile); | ||||||
|  | var mkdirpAsync = PromiseA.promisify(require("@root/mkdirp")); | ||||||
|  | 
 | ||||||
|  | // Certificates.check
 | ||||||
|  | //
 | ||||||
|  | // Use certificate.id, or subject, if id hasn't been set, to find a certificate.
 | ||||||
|  | // Return an object with string PEMs for cert and chain (or null, not undefined)
 | ||||||
|  | certificates.check = function(opts) { | ||||||
|  |     // { directoryUrl, subject, certificate.id, ... }
 | ||||||
|  |     var id = (opts.certificate && opts.certificate.id) || opts.subject; | ||||||
|  |     //console.log('certificates.check for', opts);
 | ||||||
|  | 
 | ||||||
|  |     // For advanced use cases:
 | ||||||
|  |     // This just goes 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); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return Promise.all([ | ||||||
|  |         readFileAsync(U._tameWild(privkeyPath(store, opts), id), "ascii"), // 0 // all other PEM types are just
 | ||||||
|  |         readFileAsync(U._tameWild(certPath(store, opts), id), "ascii"), // 1 // some arrangement of these 3
 | ||||||
|  |         readFileAsync(U._tameWild(chainPath(store, opts), id), "ascii") // 2 // (bundle, combined, fullchain, etc)
 | ||||||
|  |     ]) | ||||||
|  |         .then(function(all) { | ||||||
|  |             ////////////////////////
 | ||||||
|  |             // PAY ATTENTION HERE //
 | ||||||
|  |             ////////////////////////
 | ||||||
|  |             // This is all you have to return: cert, chain
 | ||||||
|  |             return { | ||||||
|  |                 cert: all[1], // string PEM. the bare cert, half of the concatonated fullchain.pem you need
 | ||||||
|  |                 chain: all[2], // string PEM. the bare chain, the second half of the fullchain.pem
 | ||||||
|  |                 privkey: all[0] // string PEM. optional, allows checkKeypair to be skipped
 | ||||||
|  | 
 | ||||||
|  |                 // These can be useful to store in your database,
 | ||||||
|  |                 // but otherwise they're easy to derive from the cert.
 | ||||||
|  |                 // (when not available they'll be generated from cert-info)
 | ||||||
|  |                 //, subject: certinfo.subject     // string domain name
 | ||||||
|  |                 //, altnames: certinfo.altnames   // array of domain name strings
 | ||||||
|  |                 //, issuedAt: certinfo.issuedAt   // number in ms (a.k.a. NotBefore)
 | ||||||
|  |                 //, expiresAt: certinfo.expiresAt // number in ms (a.k.a. NotAfter)
 | ||||||
|  |             }; | ||||||
|  |         }) | ||||||
|  |         .catch(function(err) { | ||||||
|  |             // Treat non-exceptional failures as null returns (not undefined)
 | ||||||
|  |             if ("ENOENT" === err.code) { | ||||||
|  |                 return null; | ||||||
|  |             } | ||||||
|  |             throw err; // True exceptions should be thrown
 | ||||||
|  |         }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // Certificates.checkKeypair
 | ||||||
|  | //
 | ||||||
|  | // Use certificate.kid, certificate.id, or subject to find a certificate keypair
 | ||||||
|  | // Return an object with string privateKeyPem and/or object privateKeyJwk (or null, not undefined)
 | ||||||
|  | certificates.checkKeypair = function(opts) { | ||||||
|  |     //console.log('certificates.checkKeypair:', opts);
 | ||||||
|  | 
 | ||||||
|  |     return readFileAsync( | ||||||
|  |         U._tameWild(privkeyPath(store, opts), opts.subject), | ||||||
|  |         "ascii" | ||||||
|  |     ) | ||||||
|  |         .then(function(key) { | ||||||
|  |             ////////////////////////
 | ||||||
|  |             // PAY ATTENTION HERE //
 | ||||||
|  |             ////////////////////////
 | ||||||
|  |             return { | ||||||
|  |                 privateKeyPem: key // In this case we only saved privateKeyPem, so we only return it
 | ||||||
|  |                 //privateKeyJwk: null     // (but it's fine, just different encodings of the same thing)
 | ||||||
|  |             }; | ||||||
|  |         }) | ||||||
|  |         .catch(function(err) { | ||||||
|  |             if ("ENOENT" === err.code) { | ||||||
|  |                 return null; | ||||||
|  |             } | ||||||
|  |             throw err; | ||||||
|  |         }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // Certificates.setKeypair({ certificate, subject, keypair, ... }):
 | ||||||
|  | //
 | ||||||
|  | // Use certificate.kid (or certificate.id or subject if no kid is present) to find a certificate keypair
 | ||||||
|  | // Return null (not undefined) on success, or throw on error
 | ||||||
|  | certificates.setKeypair = function(opts) { | ||||||
|  |     var keypair = opts.keypair || keypair; | ||||||
|  | 
 | ||||||
|  |     // Ignore.
 | ||||||
|  |     // Just specific implementation details.
 | ||||||
|  |     return mkdirpAsync( | ||||||
|  |         U._tameWild(path.dirname(privkeyPath(store, opts)), opts.subject) | ||||||
|  |     ).then(function() { | ||||||
|  |         // keypair is normally an opaque object, but here it's a PEM for the FS (for things like Apache and Nginx)
 | ||||||
|  |         return writeFileAsync( | ||||||
|  |             U._tameWild(privkeyPath(store, opts), opts.subject), | ||||||
|  |             keypair.privateKeyPem, | ||||||
|  |             "ascii" | ||||||
|  |         ).then(function() { | ||||||
|  |             return null; | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // Certificates.set({ subject, pems, ... }):
 | ||||||
|  | //
 | ||||||
|  | // Use certificate.id (or subject if no ki is present) to save a certificate
 | ||||||
|  | // Return null (not undefined) on success, or throw on error
 | ||||||
|  | certificates.set = function(opts) { | ||||||
|  |     //console.log('certificates.set:', opts);
 | ||||||
|  |     var pems = { | ||||||
|  |         cert: opts.pems.cert, // string PEM the first half of the concatonated fullchain.pem cert
 | ||||||
|  |         chain: opts.pems.chain, // string PEM the second half (yes, you need this too)
 | ||||||
|  |         privkey: opts.pems.privkey // Ignore. string PEM, useful if you have to create bundle.pem
 | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     // Ignore
 | ||||||
|  |     // Just implementation specific details (writing lots of combinatons of files)
 | ||||||
|  |     return mkdirpAsync(path.dirname(certPath(store, opts))) | ||||||
|  |         .then(function() { | ||||||
|  |             return mkdirpAsync( | ||||||
|  |                 path.dirname(U._tameWild(chainPath(store, opts), opts.subject)) | ||||||
|  |             ).then(function() { | ||||||
|  |                 return mkdirpAsync( | ||||||
|  |                     path.dirname( | ||||||
|  |                         U._tameWild(fullchainPath(store, opts), opts.subject) | ||||||
|  |                     ) | ||||||
|  |                 ).then(function() { | ||||||
|  |                     return mkdirpAsync( | ||||||
|  |                         path.dirname( | ||||||
|  |                             U._tameWild(bundlePath(store, opts), opts.subject) | ||||||
|  |                         ) | ||||||
|  |                     ).then(function() { | ||||||
|  |                         var fullchainPem = [ | ||||||
|  |                             pems.cert.trim() + "\n", | ||||||
|  |                             pems.chain.trim() + "\n" | ||||||
|  |                         ].join("\n"); // for Apache, Nginx, etc
 | ||||||
|  |                         var bundlePem = [ | ||||||
|  |                             pems.privkey, | ||||||
|  |                             pems.cert, | ||||||
|  |                             pems.chain | ||||||
|  |                         ].join("\n"); // for HAProxy
 | ||||||
|  |                         return PromiseA.all([ | ||||||
|  |                             sfs.writeFileAsync( | ||||||
|  |                                 U._tameWild( | ||||||
|  |                                     certPath(store, opts), | ||||||
|  |                                     opts.subject | ||||||
|  |                                 ), | ||||||
|  |                                 pems.cert, | ||||||
|  |                                 "ascii" | ||||||
|  |                             ), | ||||||
|  |                             sfs.writeFileAsync( | ||||||
|  |                                 U._tameWild( | ||||||
|  |                                     chainPath(store, opts), | ||||||
|  |                                     opts.subject | ||||||
|  |                                 ), | ||||||
|  |                                 pems.chain, | ||||||
|  |                                 "ascii" | ||||||
|  |                             ), | ||||||
|  |                             // Most web servers need these two
 | ||||||
|  |                             sfs.writeFileAsync( | ||||||
|  |                                 U._tameWild( | ||||||
|  |                                     fullchainPath(store, opts), | ||||||
|  |                                     opts.subject | ||||||
|  |                                 ), | ||||||
|  |                                 fullchainPem, | ||||||
|  |                                 "ascii" | ||||||
|  |                             ), | ||||||
|  |                             // HAProxy needs "bundle.pem" aka "combined.pem"
 | ||||||
|  |                             sfs.writeFileAsync( | ||||||
|  |                                 U._tameWild( | ||||||
|  |                                     bundlePath(store, opts), | ||||||
|  |                                     opts.subject | ||||||
|  |                                 ), | ||||||
|  |                                 bundlePem, | ||||||
|  |                                 "ascii" | ||||||
|  |                             ) | ||||||
|  |                         ]); | ||||||
|  |                     }); | ||||||
|  |                 }); | ||||||
|  |             }); | ||||||
|  |         }) | ||||||
|  |         .then(function() { | ||||||
|  |             // That's your job: return null
 | ||||||
|  |             return null; | ||||||
|  |         }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | function liveDir(store, opts) { | ||||||
|  |     return opts.liveDir || path.join(opts.configDir, "live", opts.subject); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function privkeyPath(store, opts) { | ||||||
|  |     var dir = U._tpl( | ||||||
|  |         store, | ||||||
|  |         opts, | ||||||
|  |         opts.serverKeyPath || | ||||||
|  |             opts.privkeyPath || | ||||||
|  |             opts.domainKeyPath || | ||||||
|  |             store.options.serverKeyPath || | ||||||
|  |             store.options.privkeyPath || | ||||||
|  |             store.options.domainKeyPath || | ||||||
|  |             path.join(liveDir(), "privkey.pem") | ||||||
|  |     ); | ||||||
|  |     return U._tameWild(dir, opts.subject || ""); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function certPath(store, opts) { | ||||||
|  |     var pathname = | ||||||
|  |         opts.certPath || | ||||||
|  |         store.options.certPath || | ||||||
|  |         path.join(liveDir(), "cert.pem"); | ||||||
|  | 
 | ||||||
|  |     var dir = U._tpl(store, opts, pathname); | ||||||
|  |     return U._tameWild(dir, opts.subject || ""); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function fullchainPath(store, opts) { | ||||||
|  |     var dir = U._tpl( | ||||||
|  |         store, | ||||||
|  |         opts, | ||||||
|  |         opts.fullchainPath || | ||||||
|  |             store.options.fullchainPath || | ||||||
|  |             path.join(liveDir(), "fullchain.pem") | ||||||
|  |     ); | ||||||
|  |     return U._tameWild(dir, opts.subject || ""); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function chainPath(store, opts) { | ||||||
|  |     var dir = U._tpl( | ||||||
|  |         store, | ||||||
|  |         opts, | ||||||
|  |         opts.chainPath || | ||||||
|  |             store.options.chainPath || | ||||||
|  |             path.join(liveDir(), "chain.pem") | ||||||
|  |     ); | ||||||
|  |     return U._tameWild(dir, opts.subject || ""); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function bundlePath(store, opts) { | ||||||
|  |     var dir = U._tpl( | ||||||
|  |         store, | ||||||
|  |         opts, | ||||||
|  |         opts.bundlePath || | ||||||
|  |             store.options.bundlePath || | ||||||
|  |             path.join(liveDir(), "bundle.pem") | ||||||
|  |     ); | ||||||
|  |     return U._tameWild(dir, opts.subject || ""); | ||||||
|  | } | ||||||
							
								
								
									
										303
									
								
								index.js
									
									
									
									
									
								
							
							
						
						
									
										303
									
								
								index.js
									
									
									
									
									
								
							| @ -1,15 +1,7 @@ | |||||||
| 'use strict'; | "use strict"; | ||||||
| 
 | 
 | ||||||
| var os = require("os"); | var os = require("os"); | ||||||
| var fs = require('fs'); | var path = require("path"); | ||||||
| var path = require('path'); |  | ||||||
| var sfs = require('safe-replace'); |  | ||||||
| var PromiseA = getPromise(); |  | ||||||
| var readFileAsync = PromiseA.promisify(fs.readFile); |  | ||||||
| var writeFileAsync = PromiseA.promisify(fs.writeFile); |  | ||||||
| // TODO replace with zero-depenency version
 |  | ||||||
| var mkdirpAsync = PromiseA.promisify(require('@root/mkdirp')); |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| // How Storage Works in Greenlock: High-Level Call Stack
 | // How Storage Works in Greenlock: High-Level Call Stack
 | ||||||
| //
 | //
 | ||||||
| @ -18,7 +10,7 @@ var mkdirpAsync = PromiseA.promisify(require('@root/mkdirp')); | |||||||
| // tls.SNICallback()                                      // TLS connection with SNI kicks of the request
 | // tls.SNICallback()                                      // TLS connection with SNI kicks of the request
 | ||||||
| //
 | //
 | ||||||
| //   greenlock.approveDomains(opts)                       // Greenlokc does some housekeeping, checks for a cert in
 | //   greenlock.approveDomains(opts)                       // Greenlokc does some housekeeping, checks for a cert in
 | ||||||
| //                                                        // an internal cash, and only asks you to approve new
 | //                                                        // an internal cache, and only asks you to approve new
 | ||||||
| //                                                        // certificate // registration if it doesn't find anything.
 | //                                                        // certificate // registration if it doesn't find anything.
 | ||||||
| //                                                        // In `opts` you'll receive `domain` and a few other things.
 | //                                                        // In `opts` you'll receive `domain` and a few other things.
 | ||||||
| //                                                        // You should return { subject: '...', altnames: ['...'] }
 | //                                                        // You should return { subject: '...', altnames: ['...'] }
 | ||||||
| @ -45,7 +37,6 @@ var mkdirpAsync = PromiseA.promisify(require('@root/mkdirp')); | |||||||
| //       greenlock.store.certificates.setKeypair()        // Saves the keypair for the valid certificate
 | //       greenlock.store.certificates.setKeypair()        // Saves the keypair for the valid certificate
 | ||||||
| //       greenlock.store.certificates.set()               // Saves the valid certificate
 | //       greenlock.store.certificates.set()               // Saves the valid certificate
 | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| ////////////////////////////////////////////
 | ////////////////////////////////////////////
 | ||||||
| // Recap of the high-level overview above //
 | // Recap of the high-level overview above //
 | ||||||
| ////////////////////////////////////////////
 | ////////////////////////////////////////////
 | ||||||
| @ -57,8 +48,6 @@ var mkdirpAsync = PromiseA.promisify(require('@root/mkdirp')); | |||||||
| //
 | //
 | ||||||
| //  For any type of customization, whatever is set in `approveDomains()` is available everywhere else.
 | //  For any type of customization, whatever is set in `approveDomains()` is available everywhere else.
 | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| // Either your user calls create with specific options, or greenlock calls it for you with a big options blob
 | // Either your user calls create with specific options, or greenlock calls it for you with a big options blob
 | ||||||
| module.exports.create = function(config) { | module.exports.create = function(config) { | ||||||
|     // Bear in mind that the only time any of this gets called is on first access after startup, new registration, and
 |     // Bear in mind that the only time any of this gets called is on first access after startup, new registration, and
 | ||||||
| @ -66,7 +55,10 @@ module.exports.create = function (config) { | |||||||
|     // more than 10,000 domains, for example.
 |     // more than 10,000 domains, for example.
 | ||||||
| 
 | 
 | ||||||
|     // basic setup
 |     // basic setup
 | ||||||
|   var store = { accounts: {}, certificates: {} }; |     var store = { | ||||||
|  |         accounts: require("./accounts.js"), | ||||||
|  |         certificates: require("./certificates.js") | ||||||
|  |     }; | ||||||
| 
 | 
 | ||||||
|     // For you store.options should probably start empty and get a minimal set of options copied from `config` above.
 |     // For you store.options should probably start empty and get a minimal set of options copied from `config` above.
 | ||||||
|     // Example:
 |     // Example:
 | ||||||
| @ -76,225 +68,12 @@ module.exports.create = function (config) { | |||||||
|     // In the case of greenlock-store-fs there's a bunch of legacy stuff that goes on, so we just clobber it all on.
 |     // In the case of greenlock-store-fs there's a bunch of legacy stuff that goes on, so we just clobber it all on.
 | ||||||
|     // Don't be like greenlock-store-fs (see note above).
 |     // Don't be like greenlock-store-fs (see note above).
 | ||||||
|     store.options = mergeOptions(config); |     store.options = mergeOptions(config); | ||||||
|  |     store.accounts.options = store.options; | ||||||
|  |     store.certificates.options = store.options; | ||||||
| 
 | 
 | ||||||
| 
 |     if (!config.basePath && !config.configDir) { | ||||||
| 
 |         console.info("Greenlock Store FS Path:", store.options.configDir); | ||||||
| 
 |     } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|   // Certificates.check
 |  | ||||||
|   //
 |  | ||||||
|   // Use certificate.id, or subject, if id hasn't been set, to find a certificate.
 |  | ||||||
|   // Return an object with string PEMs for cert and chain (or null, not undefined)
 |  | ||||||
|   store.certificates.check = function (opts) { |  | ||||||
|     // { certificate.id, subject, ... }
 |  | ||||||
|     var id = opts.certificate && opts.certificate.id || opts.subject; |  | ||||||
|     //console.log('certificates.check for', opts.certificate, opts.subject);
 |  | ||||||
|     //console.log(opts);
 |  | ||||||
| 
 |  | ||||||
|     // For advanced use cases:
 |  | ||||||
|     // This just goes 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 PromiseA.reject(new Error("You want an error? You got it!")); } |  | ||||||
|     if (opts.exampleReturnNull) { return PromiseA.resolve(null); } |  | ||||||
|     if (opts.exampleReturnCerts) { return PromiseA.resolve(opts.exampleReturnCerts); } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     // Ignore this first bit, it's just file system template / compatibility stuff
 |  | ||||||
|     var liveDir = opts.liveDir || path.join(opts.configDir, 'live', opts.subject); |  | ||||||
|     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(tameWild(privkeyPath, id), 'ascii')   // 0 // all other PEM types are just
 |  | ||||||
|     , readFileAsync(tameWild(certPath, id), 'ascii')      // 1 // some arrangement of these 3
 |  | ||||||
|     , readFileAsync(tameWild(chainPath, id), 'ascii')     // 2 // (bundle, combined, fullchain, etc)
 |  | ||||||
|     ]).then(function (all) { |  | ||||||
| 
 |  | ||||||
|       ////////////////////////
 |  | ||||||
|       // PAY ATTENTION HERE //
 |  | ||||||
|       ////////////////////////
 |  | ||||||
|       // This is all you have to return: cert, chain
 |  | ||||||
|       return { |  | ||||||
|         cert: all[1]      // string PEM. the bare cert, half of the concatonated fullchain.pem you need
 |  | ||||||
|       , chain: all[2]     // string PEM. the bare chain, the second half of the fullchain.pem
 |  | ||||||
|       , privkey: all[0]   // string PEM. optional, allows checkKeypair to be skipped
 |  | ||||||
| 
 |  | ||||||
|       // These can be useful to store in your database,
 |  | ||||||
|       // but otherwise they're easy to derive from the cert.
 |  | ||||||
|       // (when not available they'll be generated from cert-info)
 |  | ||||||
|       //, subject: certinfo.subject     // string domain name
 |  | ||||||
|       //, altnames: certinfo.altnames   // array of domain name strings
 |  | ||||||
|       //, issuedAt: certinfo.issuedAt   // number in ms (a.k.a. NotBefore)
 |  | ||||||
|       //, expiresAt: certinfo.expiresAt // number in ms (a.k.a. NotAfter)
 |  | ||||||
|       }; |  | ||||||
|     }).catch(function (err) { |  | ||||||
|       // Treat non-exceptional failures as null returns (not undefined)
 |  | ||||||
|       if ('ENOENT' === err.code) { return null; } |  | ||||||
|       throw err; // True exceptions should be thrown
 |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|   // Implement if you need the ACME account metadata elsewhere in the chain of events
 |  | ||||||
|   //store.accounts.check = function (opts) {
 |  | ||||||
|   //  console.log('accounts.check for', opts.account, opts.email);
 |  | ||||||
|   //  return PromiseA.resolve(null);
 |  | ||||||
|   //};
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|   // Accounts.checkKeypair
 |  | ||||||
|   //
 |  | ||||||
|   // Use account.id, or email, if id hasn't been set, to find an account keypair.
 |  | ||||||
|   // Return an object with string privateKeyPem and/or object privateKeyJwk (or null, not undefined)
 |  | ||||||
|   store.accounts.checkKeypair = function (opts) { |  | ||||||
|     var id = opts.account.id || opts.email || 'single-user'; |  | ||||||
|     //console.log('accounts.checkKeypair for', id);
 |  | ||||||
| 
 |  | ||||||
|     var pathname = path.join(tameWild(opts.accountsDir, opts.subject), sanitizeFilename(id) + '.json'); |  | ||||||
|     return readFileAsync(tameWild(pathname, opts.subject), 'utf8').then(function (blob) { |  | ||||||
|       // keypair can treated as an opaque object and just passed along,
 |  | ||||||
|       // but just to show you what it is...
 |  | ||||||
|       var keypair = JSON.parse(blob); |  | ||||||
|       return { |  | ||||||
|         privateKeyPem: keypair.privateKeyPem // string PEM private key
 |  | ||||||
|       , privateKeyJwk: keypair.privateKeyJwk // object JWK private key
 |  | ||||||
|       }; |  | ||||||
|     }).catch(function (err) { |  | ||||||
|       if ('ENOENT' === err.code) { return null; } |  | ||||||
|       throw err; |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|   // Accounts.setKeypair({ account, email, keypair, ... }):
 |  | ||||||
|   //
 |  | ||||||
|   // Use account.id (or email if no id is present) to save an account keypair
 |  | ||||||
|   // Return null (not undefined) on success, or throw on error
 |  | ||||||
|   store.accounts.setKeypair = function (opts) { |  | ||||||
|     //console.log('accounts.setKeypair for', opts.account, opts.email, opts.keypair);
 |  | ||||||
|     var id = opts.account.id || opts.email || 'single-user'; |  | ||||||
| 
 |  | ||||||
|     // you can just treat the keypair as opaque and save and retrieve it as JSON
 |  | ||||||
|     var keyblob = JSON.stringify({ |  | ||||||
|       privateKeyPem: opts.keypair.privateKeyPem // string PEM
 |  | ||||||
|     , privateKeyJwk: opts.keypair.privateKeyJwk // object JWK
 |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     // Ignore.
 |  | ||||||
|     // Just implementation specific details here.
 |  | ||||||
|     return mkdirpAsync(tameWild(opts.accountsDir, opts.subject)).then(function () { |  | ||||||
|       var pathname = tameWild(path.join(opts.accountsDir, sanitizeFilename(id) + '.json'), opts.subject); |  | ||||||
|       return writeFileAsync(tameWild(pathname, opts.subject), keyblob, 'utf8'); |  | ||||||
|     }).then(function () { |  | ||||||
|       // This is your job: return null, not undefined
 |  | ||||||
|       return null; |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|   // Implement if you need the ACME account metadata elsewhere in the chain of events
 |  | ||||||
|   //store.accounts.set = function (opts) {
 |  | ||||||
|   //  console.log('account.set:', opts.account, opts.email, opts.receipt);
 |  | ||||||
|   //  return PromiseA.resolve(null);
 |  | ||||||
|   //};
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|   // Certificates.checkKeypair
 |  | ||||||
|   //
 |  | ||||||
|   // Use certificate.kid, certificate.id, or subject to find a certificate keypair
 |  | ||||||
|   // Return an object with string privateKeyPem and/or object privateKeyJwk (or null, not undefined)
 |  | ||||||
|   store.certificates.checkKeypair = function (opts) { |  | ||||||
|     //console.log('certificates.checkKeypair:', opts.certificate, opts.subject);
 |  | ||||||
| 
 |  | ||||||
|     // Ignore this. It's just special stuff for file system compat with the old le-store-certbot
 |  | ||||||
|     var liveDir = opts.liveDir || path.join(opts.configDir, 'live', opts.subject); |  | ||||||
|     var privkeyPath = opts.privkeyPath || opts.domainKeyPath || path.join(liveDir, 'privkey.pem'); |  | ||||||
|     return readFileAsync(tameWild(privkeyPath, opts.subject), 'ascii').then(function (key) { |  | ||||||
|       ////////////////////////
 |  | ||||||
|       // PAY ATTENTION HERE //
 |  | ||||||
|       ////////////////////////
 |  | ||||||
|       return { |  | ||||||
|         privateKeyPem: key      // In this case we only saved privateKeyPem, so we only return it
 |  | ||||||
|       //privateKeyJwk: null     // (but it's fine, just different encodings of the same thing)
 |  | ||||||
|       }; |  | ||||||
|     }).catch(function (err) { |  | ||||||
|       if ('ENOENT' === err.code) { return null; } |  | ||||||
|       throw err; |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|   // Certificates.setKeypair({ certificate, subject, keypair, ... }):
 |  | ||||||
|   //
 |  | ||||||
|   // Use certificate.kid (or certificate.id or subject if no kid is present) to find a certificate keypair
 |  | ||||||
|   // Return null (not undefined) on success, or throw on error
 |  | ||||||
|   store.certificates.setKeypair = function (opts) { |  | ||||||
|     var keypair = opts.keypair || keypair; |  | ||||||
| 
 |  | ||||||
|     // Ignore.
 |  | ||||||
|     // Just specific implementation details.
 |  | ||||||
|     var liveDir = opts.liveDir || path.join(opts.configDir, 'live', opts.subject); |  | ||||||
|     var privkeyPath = opts.privkeyPath || opts.domainKeyPath || path.join(liveDir, 'privkey.pem'); |  | ||||||
|     return mkdirpAsync(tameWild(path.dirname(privkeyPath), opts.subject)).then(function () { |  | ||||||
|       // keypair is normally an opaque object, but here it's a PEM for the FS (for things like Apache and Nginx)
 |  | ||||||
|       return writeFileAsync(tameWild(privkeyPath, opts.subject), keypair.privateKeyPem, 'ascii').then(function () { |  | ||||||
|         return null; |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|   // Certificates.set({ subject, pems, ... }):
 |  | ||||||
|   //
 |  | ||||||
|   // Use certificate.id (or subject if no ki is present) to save a certificate
 |  | ||||||
|   // Return null (not undefined) on success, or throw on error
 |  | ||||||
|   store.certificates.set = function (opts) { |  | ||||||
|     //console.log('certificates.set:', opts.subject, opts.pems);
 |  | ||||||
|     var pems = { |  | ||||||
|       cert: opts.pems.cert        // string PEM the first half of the concatonated fullchain.pem cert
 |  | ||||||
|     , chain: opts.pems.chain      // string PEM the second half (yes, you need this too)
 |  | ||||||
|     , privkey: opts.pems.privkey  // Ignore. string PEM, useful if you have to create bundle.pem
 |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     // Ignore
 |  | ||||||
|     // Just implementation specific details (writing lots of combinatons of files)
 |  | ||||||
|     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 bundlePath = opts.bundlePath || path.join(liveDir, 'bundle.pem'); |  | ||||||
|     return mkdirpAsync(path.dirname(tameWild(certPath, opts.subject))).then(function () { |  | ||||||
|       return mkdirpAsync(path.dirname(tameWild(chainPath, opts.subject))).then(function () { |  | ||||||
|         return mkdirpAsync(path.dirname(tameWild(fullchainPath, opts.subject))).then(function () { |  | ||||||
|           return mkdirpAsync(path.dirname(tameWild(bundlePath, opts.subject))).then(function () { |  | ||||||
|             var fullchainPem = [ pems.cert, pems.chain ].join('\n'); // for Apache, Nginx, etc
 |  | ||||||
|             var bundlePem = [ pems.privkey, pems.cert, pems.chain ].join('\n'); // for HAProxy
 |  | ||||||
|             return PromiseA.all([ |  | ||||||
|               sfs.writeFileAsync(tameWild(certPath, opts.subject), pems.cert, 'ascii') |  | ||||||
|             , sfs.writeFileAsync(tameWild(chainPath, opts.subject), pems.chain, 'ascii') |  | ||||||
|               // Most web servers need these two
 |  | ||||||
|             , sfs.writeFileAsync(tameWild(fullchainPath, opts.subject), fullchainPem, 'ascii') |  | ||||||
|               // HAProxy needs "bundle.pem" aka "combined.pem"
 |  | ||||||
|             , sfs.writeFileAsync(tameWild(bundlePath, opts.subject), bundlePem, 'ascii') |  | ||||||
|             ]); |  | ||||||
|           }); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     }).then(function () { |  | ||||||
|       // That's your job: return null
 |  | ||||||
|       return null; |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|     return store; |     return store; | ||||||
| }; | }; | ||||||
| @ -305,22 +84,29 @@ module.exports.create = function (config) { | |||||||
| //
 | //
 | ||||||
| // Everything below this line is just implementation specific
 | // Everything below this line is just implementation specific
 | ||||||
| var defaults = { | var defaults = { | ||||||
|   configDir: path.join(os.homedir(), 'acme', 'etc') |     basePath: path.join(os.homedir(), ".config", "greenlock"), | ||||||
| 
 | 
 | ||||||
| , accountsDir: path.join(':configDir', 'accounts', ':serverDir') |     accountsDir: path.join(":basePath", "accounts", ":directoryUrl"), | ||||||
| , serverDirGet: function (copy) { |     serverDirGet: function(copy) { | ||||||
|     return (copy.server || '').replace('https://', '').replace(/(\/)$/, '').replace(/\//g, path.sep); |         return (copy.directoryUrl || copy.server || "") | ||||||
|   } |             .replace("https://", "") | ||||||
| , privkeyPath: path.join(':configDir', 'live', ':hostname', 'privkey.pem') |             .replace(/(\/)$/, "") | ||||||
| , fullchainPath: path.join(':configDir', 'live', ':hostname', 'fullchain.pem') |             .replace(/\//g, path.sep); | ||||||
| , certPath: path.join(':configDir', 'live', ':hostname', 'cert.pem') |     }, | ||||||
| , chainPath: path.join(':configDir', 'live', ':hostname', 'chain.pem') |     privkeyPath: path.join(":basePath", ":env", ":subject", "privkey.pem"), | ||||||
| , bundlePath: path.join(':configDir', 'live', ':hostname', 'bundle.pem') |     fullchainPath: path.join(":basePath", ":env", ":subject", "fullchain.pem"), | ||||||
|  |     certPath: path.join(":basePath", ":env", ":subject", "cert.pem"), | ||||||
|  |     chainPath: path.join(":basePath", ":env", ":subject", "chain.pem"), | ||||||
|  |     bundlePath: path.join(":basePath", ":env", ":subject", "bundle.pem") | ||||||
| }; | }; | ||||||
|  | defaults.configDir = defaults.basePath; | ||||||
| 
 | 
 | ||||||
| function mergeOptions(configs) { | function mergeOptions(configs) { | ||||||
|   if (!configs.domainKeyPath) { |     if (!configs.serverKeyPath) { | ||||||
|     configs.domainKeyPath = configs.privkeyPath || defaults.privkeyPath; |         configs.serverKeyPath = | ||||||
|  |             configs.domainKeyPath || | ||||||
|  |             configs.privkeyPath || | ||||||
|  |             defaults.privkeyPath; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Object.keys(defaults).forEach(function(key) { |     Object.keys(defaults).forEach(function(key) { | ||||||
| @ -331,30 +117,3 @@ function mergeOptions(configs) { | |||||||
| 
 | 
 | ||||||
|     return configs; |     return configs; | ||||||
| } | } | ||||||
| 
 |  | ||||||
| function sanitizeFilename(id) { |  | ||||||
|   return id.replace(/(\.\.)|\\|\//g, '_').replace(/[^!-~]/g, '_'); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // because not all file systems like '*' in a name (and they're scary)
 |  | ||||||
| function tameWild(path, wild) { |  | ||||||
|   var tame = wild.replace(/\*/g, '_'); |  | ||||||
|   return path.replace(wild, tame); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function getPromise() { |  | ||||||
|   var util = require('util'); |  | ||||||
|   var PromiseA; |  | ||||||
|   if (util.promisify && global.Promise) { |  | ||||||
|     PromiseA = global.Promise; |  | ||||||
|     PromiseA.promisify = util.promisify; |  | ||||||
|   } else { |  | ||||||
|     try { |  | ||||||
|       PromiseA = require('bluebird'); |  | ||||||
|     } 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); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   return PromiseA; |  | ||||||
| } |  | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|     "name": "greenlock-store-fs", |     "name": "greenlock-store-fs", | ||||||
|   "version": "3.0.1", |     "version": "3.2.0", | ||||||
|     "lockfileVersion": 1, |     "lockfileVersion": 1, | ||||||
|     "requires": true, |     "requires": true, | ||||||
|     "dependencies": { |     "dependencies": { | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|     "name": "greenlock-store-fs", |     "name": "greenlock-store-fs", | ||||||
|   "version": "3.0.1", |     "version": "3.2.2", | ||||||
|     "description": "A file-based certificate store for greenlock that supports wildcards.", |     "description": "A file-based certificate store for greenlock that supports wildcards.", | ||||||
|     "homepage": "https://git.rootprojects.org/root/greenlock-store-fs.js", |     "homepage": "https://git.rootprojects.org/root/greenlock-store-fs.js", | ||||||
|     "main": "index.js", |     "main": "index.js", | ||||||
|  | |||||||
							
								
								
									
										22
									
								
								promise.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								promise.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | |||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | function getPromise() { | ||||||
|  |     var util = require("util"); | ||||||
|  |     var PromiseA; | ||||||
|  |     if (util.promisify && global.Promise) { | ||||||
|  |         PromiseA = global.Promise; | ||||||
|  |         PromiseA.promisify = util.promisify; | ||||||
|  |     } else { | ||||||
|  |         try { | ||||||
|  |             PromiseA = require("bluebird"); | ||||||
|  |         } 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); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     return PromiseA; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = getPromise(); | ||||||
							
								
								
									
										42
									
								
								test.js
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								test.js
									
									
									
									
									
								
							| @ -1,26 +1,32 @@ | |||||||
| 'use strict'; | "use strict"; | ||||||
| 
 | 
 | ||||||
| var tester = require('greenlock-store-test'); | var tester = require("greenlock-store-test"); | ||||||
| 
 | 
 | ||||||
| var crypto = require('crypto'); | var crypto = require("crypto"); | ||||||
| var os = require('os'); | var os = require("os"); | ||||||
| var path = require('path'); | var path = require("path"); | ||||||
| var basedir = path.join(os.tmpdir(), 'greenlock-store-fs-test-' + crypto.randomBytes(4).toString('hex')); | var basedir = path.join( | ||||||
| var domain = '*.example.com'; |     os.tmpdir(), | ||||||
| var store = require('./').create({ |     "greenlock-store-fs-test-" + crypto.randomBytes(4).toString("hex") | ||||||
|   configDir: basedir | ); | ||||||
| , accountsDir: path.join(basedir, 'accounts') | var domain = "*.example.com"; | ||||||
| , privkeyPath: path.join(basedir, 'live', domain, 'privkey.pem') | var store = require("./").create({ | ||||||
| , fullchainPath: path.join(basedir, 'live', domain, 'fullchain.pem') |     configDir: basedir, | ||||||
| , certPath: path.join(basedir, 'live', domain, 'cert.pem') |     accountsDir: path.join(basedir, "accounts"), | ||||||
| , chainPath: path.join(basedir, 'live', domain, 'chain.pem') |     privkeyPath: path.join(basedir, "live", domain, "privkey.pem"), | ||||||
| , bundlePath: path.join(basedir, 'live', domain, 'bundle.pem') |     fullchainPath: path.join(basedir, "live", domain, "fullchain.pem"), | ||||||
|  |     certPath: path.join(basedir, "live", domain, "cert.pem"), | ||||||
|  |     chainPath: path.join(basedir, "live", domain, "chain.pem"), | ||||||
|  |     bundlePath: path.join(basedir, "live", domain, "bundle.pem") | ||||||
| }); | }); | ||||||
| console.info('Test Dir:', basedir); | console.info("Test Dir:", basedir); | ||||||
| 
 | 
 | ||||||
| tester.test(store).then(function () { | tester | ||||||
|  |     .test(store) | ||||||
|  |     .then(function() { | ||||||
|         console.info("PASS"); |         console.info("PASS"); | ||||||
| }).catch(function (err) { |     }) | ||||||
|  |     .catch(function(err) { | ||||||
|         console.error("FAIL"); |         console.error("FAIL"); | ||||||
|         console.error(err); |         console.error(err); | ||||||
|         process.exit(20); |         process.exit(20); | ||||||
|  | |||||||
							
								
								
									
										51
									
								
								utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								utils.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | |||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | var U = module.exports; | ||||||
|  | 
 | ||||||
|  | // because not all file systems like '*' in a name (and they're scary)
 | ||||||
|  | U._tameWild = function tameWild(pathname, wild) { | ||||||
|  |     if (!wild) { | ||||||
|  |         return pathname; | ||||||
|  |     } | ||||||
|  |     var tame = wild.replace(/\*/g, "_"); | ||||||
|  |     return pathname.replace(wild, tame); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | U._tpl = function tpl(store, opts, str) { | ||||||
|  |     var server = ["directoryUrl", "serverDir", "server"]; | ||||||
|  |     var env = ["env", "directoryUrl"]; | ||||||
|  |     [ | ||||||
|  |         ["basePath", "configDir"], | ||||||
|  |         server, | ||||||
|  |         ["subject", "hostname", "domain"], | ||||||
|  |         env | ||||||
|  |     ].forEach(function(group) { | ||||||
|  |         group.forEach(function(tmpl) { | ||||||
|  |             group.forEach(function(key) { | ||||||
|  |                 var item = opts[key] || store.options[key]; | ||||||
|  |                 if ("string" !== typeof item) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if ("directoryUrl" === key) { | ||||||
|  |                     item = item.replace(/^https?:\/\//i, ""); | ||||||
|  |                 } | ||||||
|  |                 if ("env" === tmpl) { | ||||||
|  |                     if (/staging/.test(item)) { | ||||||
|  |                         item = "staging"; | ||||||
|  |                     } else if (/acme-v02/.test(item)) { | ||||||
|  |                         item = "live"; | ||||||
|  |                     } else { | ||||||
|  |                         // item = item;
 | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if (-1 === str.indexOf(":" + tmpl)) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 str = str.replace(":" + tmpl, item); | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
|  |     return str; | ||||||
|  | }; | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user