sni.js
7.76 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
"use strict";
var sni = module.exports;
var tls = require("tls");
var servernameRe = /^[a-z0-9\.\-]+$/i;
// a nice, round, irrational number - about every 6¼ hours
var refreshOffset = Math.round(Math.PI * 2 * (60 * 60 * 1000));
// and another, about 15 minutes
var refreshStagger = Math.round(Math.PI * 5 * (60 * 1000));
// and another, about 30 seconds
var smallStagger = Math.round(Math.PI * (30 * 1000));
//secureOpts.SNICallback = sni.create(greenlock, secureOpts);
sni.create = function(greenlock, secureOpts) {
var _cache = {};
var defaultServername = greenlock.servername || "";
if (secureOpts.cert) {
// Note: it's fine if greenlock.servername is undefined,
// but if the caller wants this to auto-renew, they should define it
_cache[defaultServername] = {
refreshAt: 0,
secureContext: tls.createSecureContext(secureOpts)
};
}
return getSecureContext;
function notify(ev, args) {
try {
// TODO _notify() or notify()?
(greenlock.notify || greenlock._notify)(ev, args);
} catch (e) {
console.error(e);
console.error(ev, args);
}
}
function getSecureContext(servername, cb) {
//console.log("debug sni", servername);
if ("string" !== typeof servername) {
// this will never happen... right? but stranger things have...
console.error("[sanity fail] non-string servername:", servername);
cb(new Error("invalid servername"), null);
return;
}
var secureContext = getCachedContext(servername);
if (secureContext) {
//console.log("debug sni got cached context", servername, getCachedMeta(servername));
cb(null, secureContext);
return;
}
getFreshContext(servername)
.then(function(secureContext) {
if (secureContext) {
//console.log("debug sni got fresh context", servername, getCachedMeta(servername));
cb(null, secureContext);
return;
}
// Note: this does not replace tlsSocket.setSecureContext()
// as it only works when SNI has been sent
//console.log("debug sni got default context", servername, getCachedMeta(servername));
if (!/PROD/.test(process.env.ENV) || /DEV|STAG/.test(process.env.ENV)) {
// Change this once
// A) the 'notify' message passing is verified fixed in cluster mode
// B) we have a good way to let people know their server isn't configured
console.debug("debug: ignoring servername " + JSON.stringify(servername));
console.debug(" (it's probably either missing from your config, or a bot)");
notify("servername_unknown", {
servername: servername
});
}
cb(null, getDefaultContext());
})
.catch(function(err) {
if (!err.context) {
err.context = "sni_callback";
}
notify("error", err);
//console.log("debug sni error", servername, err);
cb(err);
});
}
function getCachedMeta(servername) {
var meta = _cache[servername];
if (!meta) {
if (!_cache[wildname(servername)]) {
return null;
}
}
return meta;
}
function getCachedContext(servername) {
var meta = getCachedMeta(servername);
if (!meta) {
return null;
}
// always renew in background
if (!meta.refreshAt || Date.now() >= meta.refreshAt) {
getFreshContext(servername).catch(function(e) {
if (!e.context) {
e.context = "sni_background_refresh";
}
notify("error", e);
});
}
// under normal circumstances this would never be expired
// and, if it is expired, something is so wrong it's probably
// not worth wating for the renewal - it has probably failed
return meta.secureContext;
}
function getFreshContext(servername) {
var meta = getCachedMeta(servername);
if (!meta && !validServername(servername)) {
if ((servername && !/PROD/.test(process.env.ENV)) || /DEV|STAG/.test(process.env.ENV)) {
// Change this once
// A) the 'notify' message passing is verified fixed in cluster mode
// B) we have a good way to let people know their server isn't configured
console.debug("debug: invalid servername " + JSON.stringify(servername));
console.debug(" (it's probably just a bot trolling for vulnerable servers)");
notify("servername_invalid", {
servername: servername
});
}
return Promise.resolve(null);
}
if (meta) {
// prevent stampedes
meta.refreshAt = Date.now() + randomRefreshOffset();
}
// TODO don't get unknown certs at all, rely on auto-updates from greenlock
// Note: greenlock.get() will return an existing fresh cert or issue a new one
return greenlock.get({ servername: servername }).then(function(result) {
var meta = getCachedMeta(servername);
if (!meta) {
meta = _cache[servername] = { secureContext: { _valid: false } };
}
// prevent from being punked by bot trolls
meta.refreshAt = Date.now() + smallStagger;
// nothing to do
if (!result) {
return null;
}
// we only care about the first one
var pems = result.pems;
var site = result.site;
if (!pems || !pems.cert) {
// nothing to do
// (and the error should have been reported already)
return null;
}
meta = {
refreshAt: Date.now() + randomRefreshOffset(),
secureContext: tls.createSecureContext({
// TODO support passphrase-protected privkeys
key: pems.privkey,
cert: pems.cert + "\n" + pems.chain + "\n"
})
};
meta.secureContext._valid = true;
// copy this same object into every place
(result.altnames || site.altnames || [result.subject || site.subject]).forEach(function(altname) {
_cache[altname] = meta;
});
return meta.secureContext;
});
}
function getDefaultContext() {
return getCachedContext(defaultServername);
}
};
// whenever we need to know when to refresh next
function randomRefreshOffset() {
var stagger = Math.round(refreshStagger / 2) - Math.round(Math.random() * refreshStagger);
return refreshOffset + stagger;
}
function validServername(servername) {
// format and (lightly) sanitize sni so that users can be naive
// and not have to worry about SQL injection or fs discovery
servername = (servername || "").toLowerCase();
// hostname labels allow a-z, 0-9, -, and are separated by dots
// _ is sometimes allowed, but not as a "hostname", and not by Let's Encrypt ACME
// REGEX // https://www.codeproject.com/Questions/1063023/alphanumeric-validation-javascript-without-regex
return servernameRe.test(servername) && -1 === servername.indexOf("..");
}
function wildname(servername) {
return (
"*." +
servername
.split(".")
.slice(1)
.join(".")
);
}