Compare commits
	
		
			No commits in common. "master" and "v0.9.2" have entirely different histories.
		
	
	
		
	
		
| @ -1,7 +0,0 @@ | |||||||
| { |  | ||||||
|   "bracketSpacing": true, |  | ||||||
|   "printWidth": 80, |  | ||||||
|   "tabWidth": 4, |  | ||||||
|   "trailingComma": "none", |  | ||||||
|   "useTabs": false |  | ||||||
| } |  | ||||||
							
								
								
									
										248
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										248
									
								
								README.md
									
									
									
									
									
								
							| @ -1,195 +1,107 @@ | |||||||
| # [greenlock-store-fs](https://git.rootprojects.org/root/greenlock-store-fs.js) | A [Root](https://rootprojects.org) project | # le-store-fs | ||||||
| 
 | 
 | ||||||
| A keypair and certificate storage strategy for Greenlock v2.7+ (and v3). | A greenlock keypair and certificate storage strategy with wildcard support (simpler successor to le-store-certbot). | ||||||
| The (much simpler) successor to le-store-certbot. |  | ||||||
| 
 |  | ||||||
| Works with all ACME (Let's Encrypt) SSL certificate sytles: |  | ||||||
| 
 |  | ||||||
| -   [x] single domains |  | ||||||
| -   [x] multiple domains (SANs, AltNames) |  | ||||||
| -   [x] wildcards |  | ||||||
| -   [x] private / localhost domains |  | ||||||
| 
 | 
 | ||||||
| # Usage | # Usage | ||||||
| 
 | 
 | ||||||
| **Global** config: |  | ||||||
| 
 |  | ||||||
| ```js | ```js | ||||||
| greenlock.manager.defaults({ | var greenlock = require('greenlock'); | ||||||
|     store: { | var gl = greenlock.create({ | ||||||
|         module: "greenlock-store-fs", |   configDir: '~/.config/acme' | ||||||
|         basePath: "~/.config/greenlock" | , store: require('le-store-fs') | ||||||
|     } | , 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 certbot (python Let's Encrypt implementation) and | The default file system layout mirrors that of le-store-certbot in order to make transitioning effortless, | ||||||
| the prior le-store-certbot in order to make transitioning effortless. | in most situations: | ||||||
| 
 | 
 | ||||||
| The default structure looks like this: | ``` | ||||||
| 
 | acme | ||||||
| ```txt | ├── accounts | ||||||
| .config | │   └── acme-staging-v02.api.letsencrypt.org | ||||||
| └── greenlock | │       └── directory | ||||||
|     ├── accounts | │           └── sites@example.com.json | ||||||
|     │   └── acme-staging-v02.api.letsencrypt.org | └── live | ||||||
|     │       └── directory |     ├── example.com | ||||||
|     │           └── sites@example.com.json |     │   ├── bundle.pem | ||||||
|     ├── staging |     │   ├── cert.pem | ||||||
|     │   └── (same as live) |     │   ├── chain.pem | ||||||
|     └── live |     │   ├── fullchain.pem | ||||||
|         ├── example.com |     │   └── privkey.pem | ||||||
|         │   ├── bundle.pem |     └── www.example.com | ||||||
|         │   ├── cert.pem |         ├── bundle.pem | ||||||
|         │   ├── chain.pem |         ├── cert.pem | ||||||
|         │   ├── fullchain.pem |         ├── chain.pem | ||||||
|         │   └── privkey.pem |         ├── fullchain.pem | ||||||
|         └── www.example.com |         └── privkey.pem | ||||||
|             ├── bundle.pem |  | ||||||
|             ├── cert.pem |  | ||||||
|             ├── chain.pem |  | ||||||
|             ├── fullchain.pem |  | ||||||
|             └── privkey.pem |  | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| # Internal Implementation Details | # Wildcards & AltNames | ||||||
| 
 | 
 | ||||||
| You **DO NOT NEED TO KNOW** these details. | Working with wildcards and multiple altnames requires greenlock >= v2.7. | ||||||
| 
 | 
 | ||||||
| They're provided for the sake of understanding what happens "under the hood" | To do so you must set `opts.subject` and `opts.domains` within the `approvedomains()` callback. | ||||||
| to help you make better choices "in the seat". |  | ||||||
| 
 | 
 | ||||||
| **Note**: The actual code could stand to be tidied up. | `subject` refers to "the subject of the ssl certificate" as opposed to `domain` which indicates "the domain servername | ||||||
| It does need to continue to support Greenlock v2 for a few more months, | used in the current request". For single-domain certificates they're always the same, but for multiple-domain | ||||||
| so I didn't rip out the old v1 -> v2 -> v3 cruft yet. | 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). | ||||||
| 
 | 
 | ||||||
| # Parameters | `domains` should be the list of "altnames" on the certificate, which should include the `subject`. | ||||||
| 
 | 
 | ||||||
| | parameters        | example                                                  | notes            | | ## Simple Example | ||||||
| | ----------------- | -------------------------------------------------------- | ---------------- | |  | ||||||
| | `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 | ||||||
| accounts.setKeypair = async function({ | function approveDomains(opts, certs, cb) { | ||||||
|     env, |   // foo.example.com => *.example.com | ||||||
|     basePath, |   var wild = '*.' + opts.domain.split('.').slice(1).join('.'); | ||||||
|     directoryUrl, |   if ('*.example.com' !== wild) { cb(new Error(opts.domain + " is not allowed")); } | ||||||
|     email, | 
 | ||||||
|     account |   opts.subject = '*.example.com'; | ||||||
| }) { |   opts.domains = ['*.example.com']; | ||||||
|     var id = account.id || email; | 
 | ||||||
|     var serverDir = directoryUrl.replace("https://", ""); |   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 | ```js | ||||||
| accounts.checkKeypair = async function({ | function getRelated(domain) { | ||||||
|     env, |   var related; | ||||||
|     basePath, |   var wild = '*.' + domain.split('.').slice(1).join('.'); | ||||||
|     directoryUrl, |   if (Object.keys(allAllowedDomains).some(function (k) { | ||||||
|     email, |     return allAllowedDomains[k].some(function (name) { | ||||||
|     account |       if (domain === name || wild === name) { | ||||||
| }) { |         related = { subject: k, altnames: allAllowedDomains[k] }; | ||||||
|     var id = account.id || email; |         return true; | ||||||
|     var serverDir = directoryUrl.replace("https://", ""); |       } | ||||||
| 
 |     }); | ||||||
|     return { |   })) { | ||||||
|         privateKeyPem, |     return related; | ||||||
|         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 | ||||||
| certificate.checkKeypair = async function({ | var allAllowedDomains = { | ||||||
|     env, |   'example.com': ['example.com', '*.example.com'] | ||||||
|     basePath, | , 'example.net': ['example.net', '*.example.net'] | ||||||
|     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
									
									
									
									
									
								
							
							
						
						
									
										113
									
								
								accounts.js
									
									
									
									
									
								
							| @ -1,113 +0,0 @@ | |||||||
| "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
									
									
									
									
									
								
							
							
						
						
									
										265
									
								
								certificates.js
									
									
									
									
									
								
							| @ -1,265 +0,0 @@ | |||||||
| "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 || ""); |  | ||||||
| } |  | ||||||
							
								
								
									
										399
									
								
								index.js
									
									
									
									
									
								
							
							
						
						
									
										399
									
								
								index.js
									
									
									
									
									
								
							| @ -1,119 +1,318 @@ | |||||||
| "use strict"; | '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"); | var os = require("os"); | ||||||
| var path = require("path"); |  | ||||||
| 
 | 
 | ||||||
| // How Storage Works in Greenlock: High-Level Call Stack
 | // create():
 | ||||||
| //
 | // Your storage plugin may take special options, or it may not.
 | ||||||
| // nested === skipped if parent succeeds (or has cached result)
 | // 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.
 | ||||||
| // tls.SNICallback()                                      // TLS connection with SNI kicks of the request
 | // 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
 | ||||||
| //   greenlock.approveDomains(opts)                       // Greenlokc does some housekeeping, checks for a cert in
 | // a future version, but it's very much an issue of "looks cleaner" vs "behaves cleaner".
 | ||||||
| //                                                        // an internal cache, and only asks you to approve new
 | module.exports.create = function (config) { | ||||||
| //                                                        // certificate // registration if it doesn't find anything.
 |  | ||||||
| //                                                        // In `opts` you'll receive `domain` and a few other things.
 |  | ||||||
| //                                                        // You should return { subject: '...', altnames: ['...'] }
 |  | ||||||
| //                                                        // Anything returned by approveDomains() will be received
 |  | ||||||
| //                                                        // by all plugins at all stages
 |  | ||||||
| //
 |  | ||||||
| //     greenlock.store.certificates.check()               // Certificate checking happens after approval for several
 |  | ||||||
| //                                                        // reasons, including preventing duplicate registrations
 |  | ||||||
| //                                                        // but most importantly because you can dynamically swap the
 |  | ||||||
| //                                                        // storage plugin right from approveDomains().
 |  | ||||||
| //     greenlock.store.certificates.checkKeypair()        // Check for a keypair associated with the domain
 |  | ||||||
| //
 |  | ||||||
| //     greenlock.store.accounts.check()                   // Optional. If you need it, look at other Greenlock docs
 |  | ||||||
| //
 |  | ||||||
| //     greenlock.store.accounts.checkKeypair()            // Check storage for registered account key
 |  | ||||||
| //       (opts.generateKeypair||RSA.generateKeypair)()    // Generates a new keypair
 |  | ||||||
| //       greenlock.core.accounts.register()               // Registers the keypair as an ACME account
 |  | ||||||
| //       greenlock.store.accounts.setKeypair()            // Saves the keypair of the registered account
 |  | ||||||
| //       greenlock.store.accounts.set()                   // Optional. Saves superfluous ACME account metadata
 |  | ||||||
| //
 |  | ||||||
| //     greenlock.core.certificates.register()             // Begin certificate registration process & housekeeping
 |  | ||||||
| //       (opts.generateKeypair||RSA.generateKeypair)()    // Generates a new certificate keypair
 |  | ||||||
| //       greenlock.acme.certificates.register()           // Performs the ACME challenge processes
 |  | ||||||
| //       greenlock.store.certificates.setKeypair()        // Saves the keypair for the valid certificate
 |  | ||||||
| //       greenlock.store.certificates.set()               // Saves the valid certificate
 |  | ||||||
| 
 | 
 | ||||||
| ////////////////////////////////////////////
 |   // This file has been laid out in the order that options are used and calls are made
 | ||||||
| // Recap of the high-level overview above //
 |   // SNICallback() // le-sni-auto has a cache
 | ||||||
| ////////////////////////////////////////////
 |   //   greenlock.approveDomains()
 | ||||||
| //
 |   //        // you get opts.domain passed to you from SNI
 | ||||||
| //  None of this ever gets called except if there's not a cert already cached.
 |   //        // you should set opts.subject as the cert "id" domain
 | ||||||
| //  That only happens on service boot, and about every 75 days for each cert's renewal.
 |   //        // you should set opts.domains as all domains on the cert
 | ||||||
| //
 |   //        // you should set opts.account.id, otherwise opts.email will be used
 | ||||||
| //  Therefore, none of this needs to be fast, fancy, or clever
 |   //     greenlock.store.certificates.checkAsync() // on success -> SNI cache, on fail \/
 | ||||||
| //
 |   //     greenlock.store.accounts.checkAsync()     // optional (you can always return null)
 | ||||||
| //  For any type of customization, whatever is set in `approveDomains()` is available everywhere else.
 |   //     greenlock.store.accounts.checkKeypairAsync()
 | ||||||
|  |   //       greenlock.core.RSA.generateKeypair() // TODO double check name
 | ||||||
|  |   //       greenlock.core.accounts.register() // TODO double check name
 | ||||||
|  |   //     greenlock.store.accounts.setKeypairAsync() // TODO make sure this only happens on generate
 | ||||||
|  |   //     greenlock.store.accounts.setAsync() // optional
 | ||||||
|  |   //     greenlock.store.certificates.checkKeypairAsync()
 | ||||||
|  |   //       greenlock.core.RSA.generateKeypair() // TODO double check name
 | ||||||
|  |   //       greenlock.core.certificates.register() // TODO double check name
 | ||||||
|  |   //     greenlock.store.certificates.setKeypairAsync()
 | ||||||
|  |   //     greenlock.store.certificates.setAsync()
 | ||||||
| 
 | 
 | ||||||
| // Either your user calls create with specific options, or greenlock calls it for you with a big options blob
 |   // store
 | ||||||
| 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,
 | ||||||
|     // Bear in mind that the only time any of this gets called is on first access after startup, new registration, and
 |   // and renewal - so none of this needs to be particularly fast. It may need to be memory efficient, however
 | ||||||
|     // renewal - so none of this needs to be particularly fast. It may need to be memory efficient, however - if you have
 |   // (if you have more than 10,000 domains, for example).
 | ||||||
|     // more than 10,000 domains, for example.
 |   var store = {}; | ||||||
| 
 | 
 | ||||||
|     // basic setup
 |   // options:
 | ||||||
|     var store = { |   //
 | ||||||
|         accounts: require("./accounts.js"), |   // If your module requires options (i.e. file paths or database urls) you should check what you get from create()
 | ||||||
|         certificates: require("./certificates.js") |   // 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(tameWild(privkeyPath, opts.subject), 'ascii')   // 0
 | ||||||
|  |     , readFileAsync(tameWild(certPath, opts.subject), 'ascii')      // 1
 | ||||||
|  |     , readFileAsync(tameWild(chainPath, opts.subject), '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()")); } | ||||||
|  | 
 | ||||||
|  |     var pathname = path.join(tameWild(opts.accountsDir, opts.subject), sanitizeFilename(id) + '.json'); | ||||||
|  |     return readFileAsync(tameWild(pathname, opts.subject), '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(tameWild(opts.accountsDir, opts.subject)).then(function () { | ||||||
|  |       // keypair is an opaque object that should be treated as blob
 | ||||||
|  |       var pathname = tameWild(path.join(opts.accountsDir, sanitizeFilename(id) + '.json'), opts.subject); | ||||||
|  |       return writeFileAsync(tameWild(pathname, opts.subject), 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(tameWild(privkeyPath, opts.subject), '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(tameWild(path.dirname(privkeyPath), opts.subject)).then(function () { | ||||||
|  |       return writeFileAsync(tameWild(privkeyPath, opts.subject), 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'); | ||||||
| 
 | 
 | ||||||
|     // For you store.options should probably start empty and get a minimal set of options copied from `config` above.
 |     return mkdirpAsync(path.dirname(tameWild(certPath, opts.subject))).then(function () { | ||||||
|     // Example:
 |       return mkdirpAsync(path.dirname(tameWild(chainPath, opts.subject))).then(function () { | ||||||
|     //store.options = {};
 |         return mkdirpAsync(path.dirname(tameWild(fullchainPath, opts.subject))).then(function () { | ||||||
|     //store.options.databaseUrl = config.databaseUrl;
 |           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 () { | ||||||
|  |       return null; | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
| 
 | 
 | ||||||
|     // In the case of greenlock-store-fs there's a bunch of legacy stuff that goes on, so we just clobber it all on.
 |   return store; | ||||||
|     // Don't be like greenlock-store-fs (see note above).
 |  | ||||||
|     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); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return store; |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| ///////////////////////////////////////////////////////////////////////////////
 |  | ||||||
| //                                  Ignore                                   //
 |  | ||||||
| ///////////////////////////////////////////////////////////////////////////////
 |  | ||||||
| //
 |  | ||||||
| // Everything below this line is just implementation specific
 |  | ||||||
| var defaults = { | var defaults = { | ||||||
|     basePath: path.join(os.homedir(), ".config", "greenlock"), |   configDir: path.join(os.homedir(), 'acme', 'etc') | ||||||
| 
 | 
 | ||||||
|     accountsDir: path.join(":basePath", "accounts", ":directoryUrl"), | , accountsDir: path.join(':configDir', 'accounts', ':serverDir') | ||||||
|     serverDirGet: function(copy) { | , serverDirGet: function (copy) { | ||||||
|         return (copy.directoryUrl || copy.server || "") |     return (copy.server || '').replace('https://', '').replace(/(\/)$/, '').replace(/\//g, path.sep); | ||||||
|             .replace("https://", "") |   } | ||||||
|             .replace(/(\/)$/, "") | , privkeyPath: path.join(':configDir', 'live', ':hostname', 'privkey.pem') | ||||||
|             .replace(/\//g, path.sep); | , fullchainPath: path.join(':configDir', 'live', ':hostname', 'fullchain.pem') | ||||||
|     }, | , certPath: path.join(':configDir', 'live', ':hostname', 'cert.pem') | ||||||
|     privkeyPath: path.join(":basePath", ":env", ":subject", "privkey.pem"), | , chainPath: path.join(':configDir', 'live', ':hostname', 'chain.pem') | ||||||
|     fullchainPath: path.join(":basePath", ":env", ":subject", "fullchain.pem"), | , bundlePath: path.join(':configDir', 'live', ':hostname', 'bundle.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.serverKeyPath) { |   if (!configs.domainKeyPath) { | ||||||
|         configs.serverKeyPath = |     configs.domainKeyPath = configs.privkeyPath || defaults.privkeyPath; | ||||||
|             configs.domainKeyPath || |   } | ||||||
|             configs.privkeyPath || | 
 | ||||||
|             defaults.privkeyPath; |   Object.keys(defaults).forEach(function (key) { | ||||||
|  |     if (!configs[key]) { | ||||||
|  |       configs[key] = defaults[key]; | ||||||
|     } |     } | ||||||
|  |   }); | ||||||
| 
 | 
 | ||||||
|     Object.keys(defaults).forEach(function(key) { |   return configs; | ||||||
|         if (!configs[key]) { | } | ||||||
|             configs[key] = defaults[key]; | 
 | ||||||
|         } | function sanitizeFilename(id) { | ||||||
|     }); |   return id.replace(/(\.\.)|\\|\//g, '_').replace(/[^!-~]/g, '_'); | ||||||
| 
 | } | ||||||
|     return configs; | 
 | ||||||
|  | // 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); | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										38
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										38
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -1,18 +1,26 @@ | |||||||
| { | { | ||||||
|     "name": "greenlock-store-fs", |   "name": "le-store-json", | ||||||
|     "version": "3.2.0", |   "version": "1.0.0", | ||||||
|     "lockfileVersion": 1, |   "lockfileVersion": 1, | ||||||
|     "requires": true, |   "requires": true, | ||||||
|     "dependencies": { |   "dependencies": { | ||||||
|         "@root/mkdirp": { |     "minimist": { | ||||||
|             "version": "1.0.0", |       "version": "0.0.8", | ||||||
|             "resolved": "https://registry.npmjs.org/@root/mkdirp/-/mkdirp-1.0.0.tgz", |       "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", | ||||||
|             "integrity": "sha512-hxGAYUx5029VggfG+U9naAhQkoMSXtOeXtbql97m3Hi6/sQSRL/4khKZPyOF6w11glyCOU38WCNLu9nUcSjOfA==" |       "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" | ||||||
|         }, |     }, | ||||||
|         "safe-replace": { |     "mkdirp": { | ||||||
|             "version": "1.1.0", |       "version": "0.5.1", | ||||||
|             "resolved": "https://registry.npmjs.org/safe-replace/-/safe-replace-1.1.0.tgz", |       "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", | ||||||
|             "integrity": "sha512-9/V2E0CDsKs9DWOOwJH7jYpSl9S3N05uyevNjvsnDauBqRowBPOyot1fIvV5N2IuZAbYyvrTXrYFVG0RZInfFw==" |       "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==" | ||||||
|     } |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										58
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								package.json
									
									
									
									
									
								
							| @ -1,31 +1,31 @@ | |||||||
| { | { | ||||||
|     "name": "greenlock-store-fs", |   "name": "le-store-fs", | ||||||
|     "version": "3.2.2", |   "version": "0.9.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.coolaj86.com/coolaj86/le-store-fs.js", | ||||||
|     "main": "index.js", |   "main": "index.js", | ||||||
|     "directories": { |   "directories": { | ||||||
|         "test": "tests" |     "test": "tests" | ||||||
|     }, |   }, | ||||||
|     "scripts": { |   "scripts": { | ||||||
|         "test": "node tests" |     "test": "node tests" | ||||||
|     }, |   }, | ||||||
|     "repository": { |   "repository": { | ||||||
|         "type": "git", |     "type": "git", | ||||||
|         "url": "https://git.rootprojects.org/root/greenlock-store-fs.js.git" |     "url": "https://git.coolaj86.com/coolaj86/le-store-fs.js.git" | ||||||
|     }, |   }, | ||||||
|     "keywords": [ |   "keywords": [ | ||||||
|         "greenlock", |     "greenlock", | ||||||
|         "json", |     "json", | ||||||
|         "keypairs", |     "keypairs", | ||||||
|         "certificates", |     "certificates", | ||||||
|         "store", |     "store", | ||||||
|         "database" |     "database" | ||||||
|     ], |   ], | ||||||
|     "author": "AJ ONeal <solderjs@gmail.com> (https://solderjs.com/)", |   "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", | ||||||
|     "license": "MPL-2.0", |   "license": "MPL-2.0", | ||||||
|     "dependencies": { |   "dependencies": { | ||||||
|         "@root/mkdirp": "^1.0.0", |     "mkdirp": "^0.5.1", | ||||||
|         "safe-replace": "^1.1.0" |     "safe-replace": "^1.1.0" | ||||||
|     } |   } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										22
									
								
								promise.js
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								promise.js
									
									
									
									
									
								
							| @ -1,22 +0,0 @@ | |||||||
| "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(); |  | ||||||
							
								
								
									
										33
									
								
								test.js
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								test.js
									
									
									
									
									
								
							| @ -1,33 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
| 
 |  | ||||||
| var tester = require("greenlock-store-test"); |  | ||||||
| 
 |  | ||||||
| var crypto = require("crypto"); |  | ||||||
| var os = require("os"); |  | ||||||
| var path = require("path"); |  | ||||||
| var basedir = path.join( |  | ||||||
|     os.tmpdir(), |  | ||||||
|     "greenlock-store-fs-test-" + crypto.randomBytes(4).toString("hex") |  | ||||||
| ); |  | ||||||
| var domain = "*.example.com"; |  | ||||||
| var store = require("./").create({ |  | ||||||
|     configDir: basedir, |  | ||||||
|     accountsDir: path.join(basedir, "accounts"), |  | ||||||
|     privkeyPath: path.join(basedir, "live", domain, "privkey.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); |  | ||||||
| 
 |  | ||||||
| tester |  | ||||||
|     .test(store) |  | ||||||
|     .then(function() { |  | ||||||
|         console.info("PASS"); |  | ||||||
|     }) |  | ||||||
|     .catch(function(err) { |  | ||||||
|         console.error("FAIL"); |  | ||||||
|         console.error(err); |  | ||||||
|         process.exit(20); |  | ||||||
|     }); |  | ||||||
							
								
								
									
										51
									
								
								utils.js
									
									
									
									
									
								
							
							
						
						
									
										51
									
								
								utils.js
									
									
									
									
									
								
							| @ -1,51 +0,0 @@ | |||||||
| "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