manager.js
10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
'use strict';
var Manage = module.exports;
var doctor = {};
var sfs = require('safe-replace').create({ tmp: 'tmp', bak: 'bak' });
var promisify = require('util').promisify;
var fs = require('fs');
var readFile = promisify(fs.readFile);
var statFile = promisify(fs.stat);
var chmodFile = promisify(fs.chmod);
var homedir = require('os').homedir();
var path = require('path');
var mkdirp = promisify(require('@root/mkdirp'));
// NOTE
// this is over-complicated to account for people
// doing weird things, and this just being a file system
// and wanting to be fairly sure it works and produces
// meaningful errors
// For your use case you'll probably find a better example
// in greenlock-manager-test
Manage.create = function(CONF) {
if (!CONF) {
CONF = {};
}
if (!CONF.configFile) {
CONF.configFile = '~/.config/greenlock/manager.json';
console.info('Greenlock Manager Config File: ' + CONF.configFile);
}
CONF.configFile = CONF.configFile.replace('~/', homedir + '/');
var manage = {};
manage._txPromise = Promise.resolve();
// Note: all of these top-level methods are effectively mutexed
// You cannot call them from each other or they will deadlock
manage.defaults = manage.config = async function(conf) {
manage._txPromise = manage._txPromise.then(async function() {
var config = await Manage._getLatest(manage, CONF);
// act as a getter
if (!conf) {
conf = JSON.parse(JSON.stringify(config.defaults));
return conf;
}
// act as a setter
Object.keys(conf).forEach(function(k) {
// challenges are either both overwritten, or not set
// this is as it should be
config.defaults[k] = conf[k];
});
return manage._save(config);
});
return manage._txPromise;
};
manage.set = async function(args) {
manage._txPromise = manage._txPromise.then(async function() {
var config = await Manage._getLatest(manage, CONF);
manage._merge(config, config.sites[args.subject], args);
await manage._save(config);
return JSON.parse(JSON.stringify(config.sites[args.subject]));
});
return manage._txPromise;
};
manage._merge = function(config, current, args) {
if (!current || current.deletedAt) {
current = config.sites[args.subject] = {
subject: args.subject,
altnames: [],
renewAt: 1
};
}
current.renewAt = parseInt(args.renewAt || current.renewAt, 10) || 1;
var oldAlts;
var newAlts;
if (args.altnames) {
// copy as to not disturb order, which matters
oldAlts = current.altnames.slice(0).sort();
newAlts = args.altnames.slice(0).sort();
if (newAlts.join() !== oldAlts.join()) {
// this will cause immediate renewal
args.renewAt = 1;
current.altnames = args.altnames.slice(0);
}
}
Object.keys(args).forEach(function(k) {
if ('altnames' === k) {
return;
}
current[k] = args[k];
});
};
// no transaction promise here because it calls set
manage.find = async function(args) {
var ours = await _find(args);
if (!CONF.find) {
return ours;
}
// if the user has an overlay find function we'll do a diff
// between the managed state and the overlay, and choose
// what was found.
var theirs = await CONF.find(args);
var config = await Manage._getLatest(manage, CONF);
return _mergeFind(config, ours, theirs);
};
function _find(args) {
manage._txPromise = manage._txPromise.then(async function() {
var config = await Manage._getLatest(manage, CONF);
// i.e. find certs more than 30 days old
//args.issuedBefore = Date.now() - 30 * 24 * 60 * 60 * 1000;
// i.e. find certs more that will expire in less than 45 days
//args.expiresBefore = Date.now() + 45 * 24 * 60 * 60 * 1000;
var issuedBefore = args.issuedBefore || Infinity;
var expiresBefore = args.expiresBefore || Infinity; //Date.now() + 21 * 24 * 60 * 60 * 1000;
var renewBefore = args.renewBefore || Infinity; //Date.now() + 21 * 24 * 60 * 60 * 1000;
// if there's anything to match, only return matches
// if there's nothing to match, return everything
var nameKeys = ['subject', 'altnames'];
var matchAll = !nameKeys.some(function(k) {
return k in args;
});
var querynames = (args.altnames || []).slice(0);
var sites = Object.keys(config.sites)
.filter(function(subject) {
var site = config.sites[subject];
if (site.deletedAt) {
return false;
}
if (site.expiresAt >= expiresBefore) {
return false;
}
if (site.issuedAt >= issuedBefore) {
return false;
}
if (site.renewAt >= renewBefore) {
return false;
}
// after attribute filtering, before cert filtering
if (matchAll) {
return true;
}
// if subject is specified, don't return anything else
if (site.subject === args.subject) {
return true;
}
// altnames, servername, and wildname all get rolled into one
return site.altnames.some(function(altname) {
return querynames.includes(altname);
});
})
.map(function(name) {
return doctor.site(config.sites, name);
});
return sites;
});
return manage._txPromise;
}
function _mergeFind(config, ours, theirs) {
theirs.forEach(function(_newer) {
var hasCurrent = ours.some(function(_older) {
if (_newer.subject !== _older.subject) {
return false;
}
// BE SURE TO SET THIS UNDEFINED AFTERWARDS
_older._exists = true;
manage._merge(config, _older, _newer);
_newer = config.sites[_older.subject];
// handled the (only) match
return true;
});
if (hasCurrent) {
manage._merge(config, null, _newer);
}
});
// delete the things that are gone
ours.forEach(function(_older) {
if (!_older._exists) {
delete config.sites[_older.subject];
}
_older._exists = undefined;
});
manage._txPromise = manage._txPromise.then(async function() {
// kinda redundant to pull again, but whatever...
var config = await Manage._getLatest(manage, CONF);
await manage._save(config);
// everything was either added, updated, or not different
// hence, this is everything
var copy = JSON.parse(JSON.stringify(config.sites));
return Object.keys(copy).map(function(k) {
return copy[k];
});
});
return manage._txPromise;
}
manage.remove = function(args) {
if (!args.subject) {
throw new Error('should have a subject for sites to remove');
}
manage._txPromise = manage._txPromise.then(async function() {
var config = await Manage._getLatest(manage, CONF);
var site = config.sites[args.subject];
if (!site || site.deletedAt) {
return null;
}
site.deletedAt = Date.now();
await manage._save(config);
return JSON.parse(JSON.stringify(site));
});
return manage._txPromise;
};
manage._config = {};
// (wrong type #1) specifically the wrong type (null)
manage._lastStat = { size: null, mtimeMs: null };
manage._save = async function(config) {
await mkdirp(path.dirname(CONF.configFile));
// pretty-print the config file
var data = JSON.stringify(config, null, 2);
await sfs.writeFileAsync(CONF.configFile, data, 'utf8');
// this file may contain secrets, so keep it safe
return chmodFile(CONF.configFile, parseInt('0600', 8))
.catch(function() {
/*ignore for Windows */
})
.then(async function() {
var stat = await statFile(CONF.configFile);
manage._lastStat.size = stat.size;
manage._lastStat.mtimeMs = stat.mtimeMs;
});
};
manage.init = async function(deps) {
var request = deps.request;
// how nice...
};
return manage;
};
Manage._getLatest = function(MNG, CONF) {
return statFile(CONF.configFile)
.catch(async function(err) {
if ('ENOENT' !== err.code) {
err.context = 'manager_read';
throw err;
}
await MNG._save(doctor.config());
// (wrong type #2) specifically the wrong type (bool)
return { size: false, mtimeMs: false };
})
.then(async function(stat) {
if (
stat.size === MNG._lastStat.size &&
stat.mtimeMs === MNG._lastStat.mtimeMs
) {
return MNG._config;
}
var data = await readFile(CONF.configFile, 'utf8');
MNG._lastStat = stat;
MNG._config = JSON.parse(data);
return doctor.config(MNG._config);
});
};
// users muck up config files, so we try to handle it gracefully.
doctor.config = function(config) {
if (!config) {
config = {};
}
if (!config.defaults) {
config.defaults = {};
}
doctor.sites(config);
Object.keys(config).forEach(function(key) {
if (['defaults', 'routes', 'sites'].includes(key)) {
return;
}
config.defaults[key] = config[key];
delete config[key];
});
doctor.challenges(config.defaults);
return config;
};
doctor.sites = function(config) {
var sites = config.sites;
if (!sites) {
sites = {};
}
if (Array.isArray(config.sites)) {
sites = {};
config.sites.forEach(function(site) {
sites[site.subject] = site;
});
}
Object.keys(sites).forEach(function(k) {
doctor.site(sites, k);
});
config.sites = sites;
};
doctor.site = function(sconfs, subject) {
var site = sconfs[subject];
if (!site) {
delete sconfs[subject];
site = {};
}
if ('string' !== typeof site.subject) {
console.warn('warning: deleted malformed site from config file:');
console.warn(JSON.stringify(site));
delete sconfs[subject];
site.subject = 'greenlock-error.example.com';
}
if (!Array.isArray(site.altnames)) {
site.altnames = [site.subject];
}
if (!site.renewAt) {
site.renewAt = 1;
}
return site;
};
doctor.challenges = function(defaults) {
var challenges = defaults.challenges;
if (!challenges) {
challenges = {};
}
if (Array.isArray(defaults.challenges)) {
defaults.challenges.forEach(function(challenge) {
var typ = doctor.challengeType(challenge);
challenges[typ] = challenge;
});
}
Object.keys(challenges).forEach(function(k) {
doctor.challenge(challenges, k);
});
defaults.challenges = challenges;
if (!Object.keys(defaults.challenges).length) {
delete defaults.challenges;
}
};
doctor.challengeType = function(challenge) {
var typ = challenge.type;
if (!typ) {
if (/\bhttp-01\b/.test(challenge.module)) {
typ = 'http-01';
} else if (/\bdns-01\b/.test(challenge.module)) {
typ = 'dns-01';
} else if (/\btls-alpn-01\b/.test(challenge.module)) {
typ = 'tls-alpn-01';
} else {
typ = 'error-01';
}
}
delete challenge.type;
return typ;
};
doctor.challenge = function(chconfs, typ) {
var ch = chconfs[typ];
if (!ch) {
delete chconfs[typ];
}
return;
};