hfs 3.3.4 → 3.4.0-alpha2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/admin/assets/AccountsPage-CoiPofEv.js +4 -0
  2. package/admin/assets/ConfigFilePage-jUH2feqi.js +4 -0
  3. package/admin/assets/CustomHtmlPage-Pl_AJZrl.js +2 -0
  4. package/admin/assets/InternetPage-DIcAwhFk.js +6 -0
  5. package/admin/assets/LangPage-BwVY8QRD.js +2 -0
  6. package/admin/assets/LogsPage-CcjS12cP.js +3 -0
  7. package/admin/assets/MonitorPage-DMVFXyMe.js +2 -0
  8. package/admin/assets/OptionsPage-BB40ax9G.js +3 -0
  9. package/admin/assets/PluginsPage-CC2dINcb.js +1 -0
  10. package/admin/assets/icons-C_ux1zLA.js +1 -0
  11. package/admin/assets/index-BSSQOMG_.js +755 -0
  12. package/admin/assets/index-mI9kZlpb.css +1 -0
  13. package/admin/assets/routing-Be72yFmd.js +1 -0
  14. package/admin/index.html +2 -2
  15. package/central.json +100 -1
  16. package/frontend/assets/{dist-legacy-CnYHQXZ6.js → dist-legacy-D7Dg5w3L.js} +1 -1
  17. package/frontend/assets/index-legacy-CnazC3Dj.js +1 -0
  18. package/frontend/assets/{lib-legacy-3xYueNHt.js → lib-legacy-BxFHpdER.js} +1 -1
  19. package/frontend/assets/{sha512-legacy-Cl3f_PhN.js → sha512-legacy-C8xoQBnP.js} +1 -1
  20. package/frontend/index.html +1 -1
  21. package/npm-shrinkwrap.json +9 -9
  22. package/package.json +2 -2
  23. package/plugins/antibrute/plugin.js +62 -15
  24. package/src/acme.js +143 -50
  25. package/src/acmeConfig.js +43 -0
  26. package/src/acmeDns.js +285 -0
  27. package/src/admin-langs/embedded.js +33 -0
  28. package/src/admin-langs/hfs-admin-lang-ar.json +1149 -0
  29. package/src/admin-langs/hfs-admin-lang-bg.json +1149 -0
  30. package/src/admin-langs/hfs-admin-lang-de.json +1149 -0
  31. package/src/admin-langs/hfs-admin-lang-el.json +1149 -0
  32. package/src/admin-langs/hfs-admin-lang-en.json +1149 -0
  33. package/src/admin-langs/hfs-admin-lang-es.json +1149 -0
  34. package/src/admin-langs/hfs-admin-lang-fi.json +1149 -0
  35. package/src/admin-langs/hfs-admin-lang-fr.json +1149 -0
  36. package/src/admin-langs/hfs-admin-lang-hu.json +1149 -0
  37. package/src/admin-langs/hfs-admin-lang-it.json +1149 -0
  38. package/src/admin-langs/hfs-admin-lang-ja.json +1149 -0
  39. package/src/admin-langs/hfs-admin-lang-ko.json +1149 -0
  40. package/src/admin-langs/hfs-admin-lang-lt.json +1149 -0
  41. package/src/admin-langs/hfs-admin-lang-ms.json +1149 -0
  42. package/src/admin-langs/hfs-admin-lang-nl.json +1149 -0
  43. package/src/admin-langs/hfs-admin-lang-pl.json +1149 -0
  44. package/src/admin-langs/hfs-admin-lang-pt-br.json +1149 -0
  45. package/src/admin-langs/hfs-admin-lang-ro.json +1149 -0
  46. package/src/admin-langs/hfs-admin-lang-ru.json +1149 -0
  47. package/src/admin-langs/hfs-admin-lang-sr-latn.json +1149 -0
  48. package/src/admin-langs/hfs-admin-lang-sr.json +1149 -0
  49. package/src/admin-langs/hfs-admin-lang-th.json +1149 -0
  50. package/src/admin-langs/hfs-admin-lang-tr.json +1149 -0
  51. package/src/admin-langs/hfs-admin-lang-uk.json +1149 -0
  52. package/src/admin-langs/hfs-admin-lang-vi.json +1149 -0
  53. package/src/admin-langs/hfs-admin-lang-zh-hant.json +1149 -0
  54. package/src/admin-langs/hfs-admin-lang-zh.json +1149 -0
  55. package/src/adminApis.js +2 -2
  56. package/src/adminLang.js +51 -0
  57. package/src/api.auth.js +1 -1
  58. package/src/api.lang.js +22 -13
  59. package/src/api.log.js +5 -3
  60. package/src/api.net.js +20 -3
  61. package/src/api.plugins.js +53 -19
  62. package/src/api.vfs.js +3 -2
  63. package/src/argv.js +4 -0
  64. package/src/auth.js +13 -9
  65. package/src/basicWeb.js +6 -2
  66. package/src/block.js +1 -1
  67. package/src/config.js +1 -0
  68. package/src/connections.js +4 -7
  69. package/src/consoleLog.js +4 -0
  70. package/src/const.js +1 -1
  71. package/src/cross-const.js +8 -6
  72. package/src/cross.js +16 -2
  73. package/src/customHtml.js +3 -2
  74. package/src/frontEndApis.js +44 -7
  75. package/src/github.js +6 -37
  76. package/src/i18n.js +16 -4
  77. package/src/index.js +4 -3
  78. package/src/lang.js +21 -9
  79. package/src/langs/embedded.js +3 -2
  80. package/src/langs/hfs-lang-ar.json +7 -2
  81. package/src/langs/hfs-lang-bg.json +7 -2
  82. package/src/langs/hfs-lang-de.json +7 -2
  83. package/src/langs/hfs-lang-el.json +7 -2
  84. package/src/langs/hfs-lang-en.json +189 -0
  85. package/src/langs/hfs-lang-es.json +9 -4
  86. package/src/langs/hfs-lang-fi.json +8 -3
  87. package/src/langs/hfs-lang-fr.json +12 -7
  88. package/src/langs/hfs-lang-hu.json +7 -2
  89. package/src/langs/hfs-lang-it.json +11 -5
  90. package/src/langs/hfs-lang-ja.json +7 -2
  91. package/src/langs/hfs-lang-ko.json +7 -2
  92. package/src/langs/hfs-lang-lt.json +7 -2
  93. package/src/langs/hfs-lang-ms.json +9 -2
  94. package/src/langs/hfs-lang-nl.json +7 -2
  95. package/src/langs/hfs-lang-pl.json +7 -2
  96. package/src/langs/hfs-lang-pt-br.json +9 -4
  97. package/src/langs/hfs-lang-ro.json +7 -2
  98. package/src/langs/hfs-lang-ru.json +9 -4
  99. package/src/langs/hfs-lang-sr-latn.json +7 -2
  100. package/src/langs/hfs-lang-sr.json +7 -2
  101. package/src/langs/hfs-lang-th.json +8 -3
  102. package/src/langs/hfs-lang-tr.json +8 -3
  103. package/src/langs/hfs-lang-uk.json +16 -11
  104. package/src/langs/hfs-lang-vi.json +7 -2
  105. package/src/langs/hfs-lang-zh-hant.json +189 -0
  106. package/src/langs/hfs-lang-zh.json +97 -95
  107. package/src/listen.js +6 -3
  108. package/src/log.js +7 -1
  109. package/src/middlewares.js +87 -31
  110. package/src/multipartUpload.js +21 -0
  111. package/src/perm.js +38 -20
  112. package/src/pluginCatalog.js +66 -0
  113. package/src/pluginUsage.js +64 -0
  114. package/src/plugins.js +14 -2
  115. package/src/roots.js +22 -13
  116. package/src/serveFile.js +28 -11
  117. package/src/serveGuiAndSharedFiles.js +6 -6
  118. package/src/serveGuiFiles.js +7 -4
  119. package/src/update.js +51 -18
  120. package/src/upload.js +49 -8
  121. package/src/util-http.js +31 -5
  122. package/src/vfs.js +93 -4
  123. package/src/webdav.js +14 -8
  124. package/admin/assets/AccountsPage-CnZGcUox.js +0 -6
  125. package/admin/assets/ArrayField-F7fpCJIv.js +0 -2
  126. package/admin/assets/ConfigFilePage-CEgZ8Wq4.js +0 -4
  127. package/admin/assets/CustomHtmlPage-DDUNxF1H.js +0 -2
  128. package/admin/assets/DataTable-D_TeYxvk.js +0 -119
  129. package/admin/assets/DateTimeField-CsIdkTqz.js +0 -1
  130. package/admin/assets/EventManager--HiXIAY0.js +0 -1
  131. package/admin/assets/FileField-HsDCdr5S.js +0 -117
  132. package/admin/assets/InternetPage-DDwKrXJH.js +0 -8
  133. package/admin/assets/LangPage-Dd8hi_9K.js +0 -2
  134. package/admin/assets/LogsPage-BcPfugNY.js +0 -4
  135. package/admin/assets/MenuButton-cuehhVig.js +0 -1
  136. package/admin/assets/MonitorPage-Bre9d6MA.js +0 -2
  137. package/admin/assets/OptionsPage-Dd_cRDf4.js +0 -3
  138. package/admin/assets/PluginsPage-DJiseD4G.js +0 -1
  139. package/admin/assets/Tabs-C87E85Fh.js +0 -1
  140. package/admin/assets/TextEditor-BeS-mR2I.css +0 -1
  141. package/admin/assets/TextEditor-Cr0VI9Xi.js +0 -34
  142. package/admin/assets/VfsPathField-D1BkfhAM.js +0 -1
  143. package/admin/assets/agentIcons-CDoUTPUC.js +0 -1
  144. package/admin/assets/baseUrl-CnrV7Huu.js +0 -2
  145. package/admin/assets/icons-DJJYoK9E.js +0 -1
  146. package/admin/assets/index-11n5z0Ez.css +0 -1
  147. package/admin/assets/index-DwXlUHAs.js +0 -477
  148. package/admin/assets/routing-BQpoUdjq.js +0 -1
  149. package/frontend/assets/index-legacy-JYULclbY.js +0 -1
  150. package/src/langs/hfs-lang-zh-tw.json +0 -183
package/src/acme.js CHANGED
@@ -3,7 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.acmeRenewError = exports.makeCert = exports.acmeMiddleware = void 0;
6
+ exports.acmeRenewError = exports.acmeMiddleware = void 0;
7
+ exports.getAcmeStatus = getAcmeStatus;
8
+ exports.getAcmeStatusEvents = getAcmeStatusEvents;
9
+ exports.makeCert = makeCert;
7
10
  const misc_1 = require("./misc");
8
11
  const http_1 = require("http");
9
12
  const nat_1 = require("./nat");
@@ -12,7 +15,11 @@ const apiMiddleware_1 = require("./apiMiddleware");
12
15
  const acme_client_1 = __importDefault(require("acme-client"));
13
16
  const debounceAsync_1 = require("./debounceAsync");
14
17
  const promises_1 = __importDefault(require("fs/promises"));
15
- const config_1 = require("./config");
18
+ const acmeConfig_1 = require("./acmeConfig");
19
+ const acmeDns_1 = require("./acmeDns");
20
+ const github_1 = require("./github");
21
+ const promises_2 = require("node:dns/promises");
22
+ const node_stream_1 = require("node:stream");
16
23
  const events_1 = __importDefault(require("./events"));
17
24
  const selfCheck_1 = require("./selfCheck");
18
25
  const net_1 = require("net");
@@ -49,32 +56,52 @@ const TEMP_MAP = (0, nat_1.upnpMappingParam)(80, 80, 'hfs temporary', 5000); //
49
56
  stop();
50
57
  return client.removeMapping(TEMP_MAP);
51
58
  });
52
- async function generateSSLCert(domain, email, altNames = []) {
53
- const ipCertificate = [domain, ...altNames].some(net_1.isIP);
54
- // will answer the challenge through our koa app (if on port 80) or must we spawn a dedicated server?
55
- const nat = await (0, nat_1.getNatInfo)();
56
- const { http } = await (0, listen_1.getServerStatus)();
57
- const tempSrv = nat.externalPort === 80 || http.listening && http.port === 80 ? undefined
58
- : (0, http_1.createServer)((req, res) => acmeListener(req, res) || res.end('HFS')); // also satisfy self-check
59
- if (tempSrv)
60
- await new Promise(resolve => tempSrv.listen(80, resolve).on('error', (e) => {
61
- console.debug("Cannot listen on 80", e.code || e);
62
- resolve(); // go on anyway
63
- }));
64
- acmeOngoing = true;
65
- console.debug("ACME challenge server ready");
59
+ async function generateSSLCert(names, email) {
60
+ const domain = names[0];
61
+ const method = acmeConfig_1.acmeChallenge.get();
62
+ const ipCertificate = names.some(net_1.isIP);
63
+ if (!['http-01', 'dns-01'].includes(method))
64
+ throw Error("Invalid certificate validation method");
65
+ if (method === 'http-01' && names.some(n => n.startsWith('*.')))
66
+ throw Error("Select a DNS provider for wildcard certificates");
67
+ if (method === 'dns-01' && ipCertificate)
68
+ throw Error("DNS validation cannot be used for IP addresses");
69
+ const config = structuredClone(acmeConfig_1.acmeDns.get()[0]);
70
+ const info = method === 'dns-01' ? await (0, github_1.getProjectInfo)() : undefined;
71
+ const provider = info && config && (0, acmeDns_1.getDnsProviders)(info.acmeDns).get(config.provider);
72
+ if (method === 'dns-01') {
73
+ if (!provider?.available || config?.config_version !== provider.config_version)
74
+ throw Error("DNS provider unavailable or incompatible; update HFS or reconfigure the provider");
75
+ if (Object.keys(provider.fields).some(k => !config.credentials?.[k]))
76
+ throw Error("Enter the DNS provider credentials");
77
+ }
78
+ const resolver = new promises_2.Resolver({ timeout: 3000, tries: 2 });
79
+ if (info)
80
+ resolver.setServers(info.dnsServers);
81
+ const cleanups = new Map();
82
+ const cleanupErrors = [];
83
+ let tempSrv;
66
84
  let tempMap;
85
+ acmeOngoing = true;
67
86
  try {
68
- const checkUrl = `http://${(0, misc_1.ipForUrl)(domain)}`;
69
- let check = await (0, selfCheck_1.selfCheck)(checkUrl); // some check services may not consider the domain, but we already verified that
70
- if (check?.success === false && nat.upnp && !nat.mapped80) {
71
- console.debug("Setting temporary port forward");
72
- tempMap = await (0, misc_1.haveTimeout)(10_000, (0, nat_1.getUpnpClient)().createMapping(TEMP_MAP)).catch(() => { });
73
- check = await (0, selfCheck_1.selfCheck)(checkUrl); // repeat test
87
+ if (method === 'http-01') {
88
+ progress("Checking port 80");
89
+ const nat = await (0, nat_1.getNatInfo)();
90
+ const { http } = await (0, listen_1.getServerStatus)();
91
+ if (!(nat.externalPort === 80 || http.listening && http.port === 80)) {
92
+ tempSrv = (0, http_1.createServer)((req, res) => acmeListener(req, res) || res.end('HFS'));
93
+ await new Promise(resolve => tempSrv.listen(80, resolve).on('error', () => resolve()));
94
+ }
95
+ const checkUrl = `http://${(0, misc_1.ipForUrl)(domain)}`;
96
+ let check = await (0, selfCheck_1.selfCheck)(checkUrl);
97
+ if (check?.success === false && nat.upnp && !nat.mapped80) {
98
+ tempMap = await (0, misc_1.haveTimeout)(10_000, (0, nat_1.getUpnpClient)().createMapping(TEMP_MAP)).catch(() => undefined);
99
+ check = await (0, selfCheck_1.selfCheck)(checkUrl);
100
+ }
101
+ if (check?.success === false)
102
+ throw new apiMiddleware_1.ApiError(misc_1.HTTP_FAILED_DEPENDENCY, "port 80 is not working on the specified domain");
74
103
  }
75
- //if (!check) throw new ApiError(HTTP_FAILED_DEPENDENCY, "couldn't test port 80")
76
- if (check?.success === false)
77
- throw new apiMiddleware_1.ApiError(misc_1.HTTP_FAILED_DEPENDENCY, "port 80 is not working on the specified domain");
104
+ progress("Requesting certificate");
78
105
  const acmeClient = new acme_client_1.default.Client({
79
106
  accountKey: await acme_client_1.default.crypto.createPrivateKey(),
80
107
  directoryUrl: acme_client_1.default.directory.letsencrypt.production
@@ -90,19 +117,57 @@ async function generateSSLCert(domain, email, altNames = []) {
90
117
  acme_client_1.default.setLogger(console.debug);
91
118
  const [key, csr] = await acme_client_1.default.crypto.createCsr({
92
119
  commonName: (0, net_1.isIP)(domain) ? undefined : domain, // rejected because Boulder doesn't accept IP addresses in the Common Name
93
- altNames: [domain, ...altNames],
120
+ altNames: names,
94
121
  });
95
122
  const cert = await acmeClient.auto({
96
123
  csr,
97
124
  email,
98
- challengePriority: ['http-01'],
125
+ challengePriority: [method],
99
126
  skipChallengeVerification: true, // on NAT, trying to connect to your external ip will likely get your modem instead of the challenge server
100
127
  termsOfServiceAgreed: true,
101
- async challengeCreateFn(_, c, ka) { acmeTokens[c.token] = ka; },
102
- async challengeRemoveFn(_, c) { delete acmeTokens[c.token]; },
128
+ async challengeCreateFn(auth, challenge, value) {
129
+ if (challenge.type !== method)
130
+ throw Error("Unsupported ACME challenge");
131
+ if (method === 'http-01') {
132
+ acmeTokens[challenge.token] = value;
133
+ }
134
+ else {
135
+ progress("Creating DNS TXT record", auth.identifier.value);
136
+ const record = await (0, acmeDns_1.dnsChallengeRecord)(auth.identifier.value, value, resolver);
137
+ cleanups.set(challenge.token, await provider.present(record, config.credentials));
138
+ progress("Waiting for DNS propagation", auth.identifier.value);
139
+ await (0, acmeDns_1.waitForDnsRecord)(record, resolver, provider.propagation_timeout === undefined ? undefined : provider.propagation_timeout * 1000);
140
+ }
141
+ progress("Validating domain", auth.identifier.value);
142
+ },
143
+ async challengeRemoveFn(_, challenge) {
144
+ delete acmeTokens[challenge.token];
145
+ const cleanup = cleanups.get(challenge.token);
146
+ if (cleanup) {
147
+ cleanups.delete(challenge.token);
148
+ // acme-client suppresses cleanup errors, so keep them visible in the final status
149
+ try {
150
+ await cleanup();
151
+ }
152
+ catch {
153
+ cleanupErrors.push("Could not remove a DNS challenge TXT record");
154
+ }
155
+ }
156
+ },
103
157
  });
104
158
  console.log("ACME certificate generated");
105
- return { key, cert };
159
+ return { key, cert, warning: cleanupErrors.join("; ") || undefined };
160
+ }
161
+ catch (error) {
162
+ if (method === 'http-01' && error instanceof Error) {
163
+ if (error.message.includes('Timeout'))
164
+ error = Error("ensure your router is forwarding port 80 correctly");
165
+ else if (error.message.includes('not match this challenge'))
166
+ error = Error("a different server is responding on port 80 of your domain(s)");
167
+ }
168
+ if (cleanupErrors.length)
169
+ throw Error(`${error instanceof Error ? error.message : error}; ${cleanupErrors.join('; ')}`);
170
+ throw error;
106
171
  }
107
172
  finally {
108
173
  if (tempMap && nat_1.upnpEnabled.get()) {
@@ -110,33 +175,61 @@ async function generateSSLCert(domain, email, altNames = []) {
110
175
  (0, nat_1.getUpnpClient)().removeMapping(TEMP_MAP).catch(() => { }); // clean after ourselves
111
176
  }
112
177
  acmeOngoing = false;
113
- if (tempSrv)
114
- await new Promise(res => tempSrv.close(res));
178
+ if (tempSrv?.listening)
179
+ await new Promise(res => tempSrv.close(() => res()));
115
180
  console.debug('ACME terminated');
116
181
  }
117
182
  }
118
- exports.makeCert = (0, debounceAsync_1.debounceAsync)(async (domain, email, altNames) => {
119
- const res = await generateSSLCert(domain, email, altNames).catch(e => {
120
- throw e.message?.includes('Timeout') ? Error("ensure your router is forwarding port 80 correctly")
121
- : e.message?.includes('not match this challenge') ? Error("a different server is responding on port 80 of your domain(s)")
122
- : e;
183
+ let acmeStatus = { id: 0, state: 'idle', message: '' };
184
+ function getAcmeStatus() { return acmeStatus; }
185
+ function progress(message, domain) {
186
+ acmeStatus = { ...acmeStatus, message, domain };
187
+ events_1.default.emit('acmeStatus', acmeStatus);
188
+ }
189
+ function getAcmeStatusEvents() {
190
+ let off;
191
+ return new node_stream_1.Readable({
192
+ objectMode: true,
193
+ read() {
194
+ if (off)
195
+ return;
196
+ off = events_1.default.on('acmeStatus', status => this.push(status));
197
+ this.once('close', off);
198
+ this.push(acmeStatus);
199
+ },
123
200
  });
124
- const CERT_FILE = 'acme.cer';
125
- const KEY_FILE = 'acme.key';
126
- await promises_1.default.writeFile(CERT_FILE, res.cert);
127
- await promises_1.default.writeFile(KEY_FILE, res.key);
128
- listen_1.cert.set(CERT_FILE); // update config
129
- listen_1.privateKey.set(KEY_FILE);
130
- exports.acmeRenewError = '';
131
- });
201
+ }
202
+ async function makeCert(domain, email, altNames = []) {
203
+ if (acmeStatus.state === 'running')
204
+ throw Error("Certificate request already running");
205
+ acmeStatus = { id: acmeStatus.id + 1, state: 'running', message: '' };
206
+ progress("Starting certificate request");
207
+ try {
208
+ const names = (0, acmeDns_1.certificateNames)([domain, ...altNames]);
209
+ const res = await generateSSLCert(names, email);
210
+ progress("Installing certificate");
211
+ const CERT_FILE = 'acme.cer';
212
+ const KEY_FILE = 'acme.key';
213
+ await promises_1.default.writeFile(KEY_FILE, res.key, { mode: 0o600 });
214
+ await promises_1.default.writeFile(CERT_FILE, res.cert);
215
+ listen_1.cert.set(CERT_FILE);
216
+ listen_1.privateKey.set(KEY_FILE);
217
+ exports.acmeRenewError = '';
218
+ acmeStatus = { ...acmeStatus, state: 'done', warning: res.warning };
219
+ progress("Certificate created");
220
+ }
221
+ catch (error) {
222
+ acmeStatus = { ...acmeStatus, state: 'error' };
223
+ progress(error instanceof Error ? error.message : String(error));
224
+ throw error;
225
+ }
226
+ }
132
227
  exports.acmeRenewError = '';
133
- const acmeDomain = (0, config_1.defineConfig)(misc_1.CFG.acme_domain, '');
134
- const acmeRenew = (0, config_1.defineConfig)(misc_1.CFG.acme_renew, false); // handle config changes
135
228
  events_1.default.once('httpsReady', () => (0, misc_1.repeat)(misc_1.HOUR, renewCert));
136
229
  // checks if the cert is near expiration date, and if so renews it
137
230
  const renewCert = (0, debounceAsync_1.debounceAsync)(async () => {
138
- const [domain, ...altNames] = acmeDomain.get().split(',');
139
- if (!acmeRenew.get() || !domain)
231
+ const [domain, ...altNames] = acmeConfig_1.acmeDomain.get().split(',');
232
+ if (!acmeConfig_1.acmeRenew.get() || !domain)
140
233
  return;
141
234
  const cert = (0, listen_1.getCertObject)();
142
235
  if (!cert)
@@ -148,6 +241,6 @@ const renewCert = (0, debounceAsync_1.debounceAsync)(async () => {
148
241
  const renewBefore = (validTo.getTime() - validFrom.getTime()) / 3;
149
242
  if (now > validFrom && now < validTo && validTo.getTime() - now.getTime() >= renewBefore)
150
243
  return console.log("Certificate still good");
151
- await (0, exports.makeCert)(domain, undefined, altNames)
244
+ await makeCert(domain, undefined, altNames)
152
245
  .catch(e => console.log(exports.acmeRenewError = `Error renewing certificate, expiring ${(0, misc_1.formatDate)(validTo)}: ${String(e.message || e)}`));
153
246
  }, { retain: misc_1.HOUR }); // short-lived certificates leave a renew window of just 2 days, and a failed renew is retained as a success here
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.acmeDns = exports.acmeChallenge = exports.acmeRenew = exports.acmeDomain = void 0;
4
+ exports.getAcmeSettings = getAcmeSettings;
5
+ exports.setAcmeSettings = setAcmeSettings;
6
+ const config_1 = require("./config");
7
+ const cross_const_1 = require("./cross-const");
8
+ const github_1 = require("./github");
9
+ const acmeDns_1 = require("./acmeDns");
10
+ exports.acmeDomain = (0, config_1.defineConfig)(cross_const_1.CFG.acme_domain, '');
11
+ exports.acmeRenew = (0, config_1.defineConfig)(cross_const_1.CFG.acme_renew, false);
12
+ exports.acmeChallenge = (0, config_1.defineConfig)(cross_const_1.CFG.acme_challenge, 'http-01');
13
+ exports.acmeDns = (0, config_1.defineConfig)(cross_const_1.CFG.acme_dns, []);
14
+ async function getAcmeSettings() {
15
+ return {
16
+ domain: exports.acmeDomain.get(), renew: exports.acmeRenew.get(), challenge: exports.acmeChallenge.get(),
17
+ dns: exports.acmeDns.get(), providers: (0, acmeDns_1.dnsProviderInfo)((await (0, github_1.getProjectInfo)()).acmeDns),
18
+ };
19
+ }
20
+ async function setAcmeSettings(settings) {
21
+ if (typeof settings.domain !== 'string' || typeof settings.renew !== 'boolean'
22
+ || !['http-01', 'dns-01'].includes(settings.challenge) || !Array.isArray(settings.dns) || settings.dns.length > 1)
23
+ throw Error("Invalid certificate settings");
24
+ const providers = (0, acmeDns_1.dnsProviderInfo)((await (0, github_1.getProjectInfo)()).acmeDns);
25
+ const configs = settings.dns.map(config => {
26
+ if (!config || typeof config.id !== 'string' || !config.id || !config.credentials || typeof config.credentials !== 'object'
27
+ || Object.values(config.credentials).some(value => typeof value !== 'string'))
28
+ throw Error("Invalid DNS configuration");
29
+ const previous = exports.acmeDns.get().find(c => c.id === config.id && c.provider === config.provider && c.config_version === config.config_version);
30
+ const provider = providers.find(p => p.id === config.provider && p.config_version === config.config_version);
31
+ // retain unavailable credentials so switching back to HTTP doesn't require discarding them
32
+ if (!provider && previous)
33
+ return config;
34
+ if (!provider || Object.keys(config.credentials).some(key => !(key in provider.fields)))
35
+ throw Error("Invalid DNS configuration; select a supported provider");
36
+ return { id: config.id, provider: config.provider, config_version: config.config_version, credentials: config.credentials };
37
+ });
38
+ await (0, config_1.setConfig)({
39
+ [cross_const_1.CFG.acme_domain]: settings.domain, [cross_const_1.CFG.acme_renew]: settings.renew,
40
+ [cross_const_1.CFG.acme_challenge]: settings.challenge, [cross_const_1.CFG.acme_dns]: configs,
41
+ });
42
+ return getAcmeSettings();
43
+ }
package/src/acmeDns.js ADDED
@@ -0,0 +1,285 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerAcmeDnsProvider = registerAcmeDnsProvider;
4
+ exports.getDnsProviders = getDnsProviders;
5
+ exports.dnsProviderInfo = dnsProviderInfo;
6
+ exports.presentHttpDns = presentHttpDns;
7
+ exports.certificateNames = certificateNames;
8
+ exports.dnsChallengeRecord = dnsChallengeRecord;
9
+ exports.waitForDnsRecord = waitForDnsRecord;
10
+ const node_url_1 = require("node:url");
11
+ const node_net_1 = require("node:net");
12
+ const node_crypto_1 = require("node:crypto");
13
+ const util_http_1 = require("./util-http");
14
+ const promises_1 = require("node:timers/promises");
15
+ const plugins = new Map();
16
+ function registerAcmeDnsProvider(id, provider) {
17
+ if (!id || plugins.has(id))
18
+ throw Error("DNS provider ID already registered");
19
+ if (!Number.isInteger(provider.config_version) || provider.config_version < 1 || typeof provider.present !== 'function')
20
+ throw Error("Invalid DNS provider");
21
+ plugins.set(id, provider);
22
+ return () => { if (plugins.get(id) === provider)
23
+ plugins.delete(id); };
24
+ }
25
+ function getDnsProviders(catalog) {
26
+ const providers = new Map();
27
+ if (!Array.isArray(catalog))
28
+ throw Error("Invalid DNS provider catalog");
29
+ for (const raw of catalog) {
30
+ try {
31
+ if (!raw || typeof raw !== 'object' || !raw.providers || typeof raw.providers !== 'object')
32
+ throw Error("Invalid DNS provider catalog");
33
+ const recipe = raw;
34
+ if (recipe.driver !== 'http-json1')
35
+ continue;
36
+ validateRecipe(recipe);
37
+ for (const [id, entry] of Object.entries(recipe.providers)) {
38
+ try {
39
+ if (!entry || typeof entry.endpoint !== 'string' || new URL(entry.endpoint).protocol !== 'https:')
40
+ throw Error("Unsupported DNS API endpoint");
41
+ if (providers.has(id))
42
+ throw Error("Duplicate DNS provider");
43
+ providers.set(id, {
44
+ label: entry.label, config_version: recipe.config_version, fields: recipe.fields,
45
+ available: entry.enabled !== false, propagation_timeout: recipe.propagation_timeout, help_url: entry.help_url || recipe.help_url,
46
+ present: (record, credentials) => presentHttpDns(recipe, entry.endpoint, record, credentials),
47
+ });
48
+ }
49
+ catch (error) {
50
+ console.warn("Skipping DNS provider", id, error instanceof Error ? error.message : String(error));
51
+ }
52
+ }
53
+ }
54
+ // a broken recipe must not prevent independent providers from being used
55
+ catch (error) {
56
+ console.warn("Skipping DNS recipe", error instanceof Error ? error.message : String(error));
57
+ }
58
+ }
59
+ for (const [id, provider] of plugins) {
60
+ if (providers.has(id))
61
+ throw Error("Duplicate DNS provider");
62
+ providers.set(id, { ...provider, available: true });
63
+ }
64
+ return providers;
65
+ }
66
+ function dnsProviderInfo(catalog) {
67
+ return Array.from(getDnsProviders(catalog), ([id, p]) => ({
68
+ id, label: p.label || id, config_version: p.config_version, fields: p.fields, available: p.available, help_url: p.help_url,
69
+ })).sort((a, b) => a.label.localeCompare(b.label));
70
+ }
71
+ function validateRecipe(recipe) {
72
+ if (!Number.isInteger(recipe.config_version) || recipe.config_version < 1 || !recipe.fields || typeof recipe.fields !== 'object' || Array.isArray(recipe.fields)
73
+ || !recipe.headers || typeof recipe.headers !== 'object' || Array.isArray(recipe.headers)
74
+ || Object.values(recipe.headers).some(value => typeof value !== 'string')
75
+ || !Array.isArray(recipe.create) || !recipe.create.length || !Array.isArray(recipe.remove) || !recipe.remove.length
76
+ || recipe.prepare !== undefined && !Array.isArray(recipe.prepare))
77
+ throw Error("Invalid DNS recipe");
78
+ if (recipe.auth !== undefined && recipe.auth !== 'ovh'
79
+ && (!recipe.auth || typeof recipe.auth !== 'object' || typeof recipe.auth.basic !== 'string'))
80
+ throw Error("Invalid DNS authentication");
81
+ if (recipe.propagation_timeout !== undefined && (!Number.isInteger(recipe.propagation_timeout) || recipe.propagation_timeout <= 0))
82
+ throw Error("Invalid DNS propagation timeout");
83
+ for (const field of Object.values(recipe.fields))
84
+ if (!field || typeof field !== 'object' || field.label !== undefined && typeof field.label !== 'string'
85
+ || field.secret !== undefined && typeof field.secret !== 'boolean')
86
+ throw Error("Invalid DNS field");
87
+ for (const step of [...recipe.prepare || [], ...recipe.create, ...recipe.remove])
88
+ if (!step || !['GET', 'POST', 'DELETE'].includes(step.method) || typeof step.path !== 'string'
89
+ || !step.path.startsWith('/') || step.path.startsWith('//')
90
+ || step.expect !== undefined && (!Array.isArray(step.expect) || step.expect.length !== 2 || typeof step.expect[0] !== 'string')
91
+ || step.select !== undefined && (!step.select || typeof step.select !== 'object'
92
+ || typeof step.select.field !== 'string' || typeof step.select.equals !== 'string')
93
+ || step.capture !== undefined && (!step.capture || typeof step.capture !== 'object' || Array.isArray(step.capture)
94
+ || Object.values(step.capture).some(value => typeof value !== 'string')))
95
+ throw Error("Invalid DNS request");
96
+ }
97
+ function property(value, path) {
98
+ for (const key of path ? path.split('.') : []) {
99
+ if (!value || typeof value !== 'object' || !Object.hasOwn(value, key))
100
+ throw Error("Missing DNS API response field");
101
+ value = value[key];
102
+ }
103
+ if (value === undefined)
104
+ throw Error("Missing DNS API response field");
105
+ return value;
106
+ }
107
+ function expand(value, variables, url = false) {
108
+ if (typeof value === 'string') {
109
+ const whole = /^\{([\w.]+)\}$/.exec(value);
110
+ if (whole && !url)
111
+ return property(variables, whole[1]);
112
+ return value.replace(/\{([\w.]+)\}/g, (_, key) => {
113
+ const v = property(variables, key);
114
+ if (typeof v !== 'string' && typeof v !== 'number')
115
+ throw Error("Invalid DNS request parameter");
116
+ return url ? encodeURIComponent(v) : String(v);
117
+ });
118
+ }
119
+ if (Array.isArray(value))
120
+ return value.map(v => expand(v, variables));
121
+ if (value && typeof value === 'object')
122
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, expand(v, variables)]));
123
+ return value;
124
+ }
125
+ async function presentHttpDns(recipe, endpoint, record, credentials) {
126
+ // retain the recipe and credentials used to create the record until cleanup finishes
127
+ recipe = structuredClone(recipe);
128
+ credentials = { ...credentials };
129
+ const vars = Object.assign(Object.create(null), { credentials, ...record });
130
+ let ovhOffset = 0;
131
+ async function run(steps, authenticate = true) {
132
+ for (const step of steps) {
133
+ const url = new URL(endpoint.replace(/\/$/, '') + expand(step.path, vars, true));
134
+ if (url.origin !== new URL(endpoint).origin || url.username || url.password)
135
+ throw Error("Invalid DNS API URL");
136
+ const body = step.body === undefined ? undefined : JSON.stringify(expand(step.body, vars));
137
+ const headers = { 'content-type': 'application/json', ...expand(recipe.headers, vars) };
138
+ if (authenticate && recipe.auth) {
139
+ if (recipe.auth === 'ovh') {
140
+ // sign the exact bytes sent, using server time to tolerate an incorrect local clock
141
+ const timestamp = Math.floor((Date.now() + ovhOffset) / 1000);
142
+ Object.assign(headers, {
143
+ 'X-Ovh-Application': credentials.applicationKey,
144
+ 'X-Ovh-Consumer': credentials.consumerKey,
145
+ 'X-Ovh-Timestamp': String(timestamp),
146
+ 'X-Ovh-Signature': '$1$' + (0, node_crypto_1.createHash)('sha1').update([
147
+ credentials.applicationSecret, credentials.consumerKey, step.method, url.href, body || '', timestamp,
148
+ ].join('+')).digest('hex'),
149
+ });
150
+ }
151
+ else
152
+ Object.assign(headers, { Authorization: 'Basic ' + Buffer.from(String(expand(recipe.auth.basic, vars))).toString('base64') });
153
+ }
154
+ // avoid exposing raw response bodies; OVH diagnostic fields are redacted below
155
+ const response = await (0, util_http_1.httpStream)(url.href, {
156
+ method: step.method, headers, body,
157
+ timeout: 20_000, noRedirect: true, httpThrow: false,
158
+ }).catch((error) => {
159
+ throw Error(`DNS API connection failed (${error.code || 'network error'})`);
160
+ });
161
+ let data;
162
+ try {
163
+ const body = await (0, util_http_1.readTextLimited)(response, 1024 * 1024).catch((error) => {
164
+ throw Error(`DNS API response interrupted (${error.code || 'network error'})`);
165
+ });
166
+ if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300) {
167
+ let detail = '';
168
+ if (recipe.auth === 'ovh' && !('error' in body)) {
169
+ try {
170
+ const { errorCode, message } = JSON.parse(body.text);
171
+ detail = [errorCode, message].filter(value => typeof value === 'string').join(': ');
172
+ for (const secret of Object.values(credentials))
173
+ if (secret)
174
+ detail = detail.replaceAll(secret, '[redacted]');
175
+ detail = detail.slice(0, 500);
176
+ }
177
+ catch { } // non-JSON errors still report the HTTP status
178
+ }
179
+ throw Error(`DNS API returned HTTP ${response.statusCode || 'unknown'} (${step.method} ${url.hostname} ${step.path})${detail ? ': ' + detail : ''}`);
180
+ }
181
+ if ('error' in body)
182
+ throw Error("DNS API response exceeds 1 MB");
183
+ try {
184
+ data = body.text ? JSON.parse(body.text) : null;
185
+ }
186
+ catch {
187
+ throw Error("DNS API returned invalid JSON");
188
+ }
189
+ }
190
+ finally {
191
+ response.destroy();
192
+ }
193
+ if (step.select) {
194
+ if (!Array.isArray(data))
195
+ throw Error("Invalid DNS list response");
196
+ const { field, equals } = step.select;
197
+ const selected = data.filter(item => property(item, field) === expand(equals, vars));
198
+ if (selected.length !== 1)
199
+ throw Error("DNS zone missing or ambiguous");
200
+ data = selected[0];
201
+ }
202
+ if (step.expect && property(data, step.expect[0]) !== step.expect[1])
203
+ throw Error("DNS API rejected the request");
204
+ for (const [key, path] of Object.entries(step.capture || {}))
205
+ vars[key] = property(data, path);
206
+ }
207
+ }
208
+ if (recipe.auth === 'ovh') {
209
+ await run([{ method: 'GET', path: '/auth/time', capture: { serverTime: '' } }], false);
210
+ if (typeof vars.serverTime !== 'number' || !Number.isFinite(vars.serverTime))
211
+ throw Error("Invalid DNS API server time");
212
+ ovhOffset = vars.serverTime * 1000 - Date.now();
213
+ }
214
+ await run(recipe.prepare || []);
215
+ try {
216
+ await run(recipe.create);
217
+ }
218
+ catch (error) {
219
+ if ('recordId' in vars) {
220
+ try {
221
+ await run(recipe.remove);
222
+ }
223
+ catch {
224
+ throw Error("DNS creation and cleanup failed; check the challenge TXT record");
225
+ }
226
+ }
227
+ throw error;
228
+ }
229
+ return () => run(recipe.remove);
230
+ }
231
+ function certificateNames(names) {
232
+ const normalized = names.flatMap(name => {
233
+ name = name.trim().replace(/\.$/, '').toLowerCase();
234
+ const wildcard = name.startsWith('*.');
235
+ const host = wildcard ? name.slice(2) : name;
236
+ const ascii = (0, node_net_1.isIP)(host) ? host : (0, node_url_1.domainToASCII)(host);
237
+ if (!ascii || host.includes('*') || !(0, node_net_1.isIP)(host) && (!ascii.includes('.') || ascii.length > 253
238
+ || ascii.split('.').some(part => !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(part)))
239
+ || wildcard && (0, node_net_1.isIP)(host))
240
+ throw Error("Invalid certificate domain");
241
+ return wildcard ? ['*.' + ascii, ascii] : [ascii];
242
+ });
243
+ if (!normalized.length)
244
+ throw Error("Enter a domain for the certificate");
245
+ return [...new Set(normalized)];
246
+ }
247
+ async function dnsChallengeRecord(domain, value, resolver) {
248
+ let name = '_acme-challenge.' + domain.replace(/^\*\./, '');
249
+ const visited = new Set();
250
+ // follow only the validation name: the website's CNAME is unrelated to DNS-01
251
+ for (;;) {
252
+ if (visited.has(name) || visited.size >= 16)
253
+ throw Error("DNS challenge CNAME loop");
254
+ visited.add(name);
255
+ const aliases = await resolver.resolveCname(name).catch(ignoreMissingDns);
256
+ if (!aliases?.length)
257
+ break;
258
+ name = aliases[0].toLowerCase().replace(/\.$/, '');
259
+ }
260
+ let zone = name;
261
+ for (;;) {
262
+ const soa = await resolver.resolveSoa(zone).catch(ignoreMissingDns);
263
+ if (soa)
264
+ break;
265
+ const dot = zone.indexOf('.');
266
+ if (dot < 0)
267
+ throw Error("Cannot find the authoritative DNS zone");
268
+ zone = zone.slice(dot + 1);
269
+ }
270
+ return { name, zone, relative: name === zone ? '' : name.slice(0, -zone.length - 1), value };
271
+ }
272
+ function ignoreMissingDns(error) {
273
+ if (error.code !== 'ENODATA' && error.code !== 'ENOTFOUND')
274
+ throw error;
275
+ }
276
+ async function waitForDnsRecord(record, resolver, timeout = 5 * 60_000) {
277
+ const until = Date.now() + timeout;
278
+ do {
279
+ const values = await resolver.resolveTxt(record.name).catch(ignoreMissingDns);
280
+ if (values?.some(parts => parts.join('') === record.value))
281
+ return;
282
+ await (0, promises_1.setTimeout)(Math.min(2000, Math.max(0, until - Date.now())));
283
+ } while (Date.now() < until);
284
+ throw Error("Timed out waiting for the DNS TXT record to propagate");
285
+ }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const hfs_admin_lang_en_json_1 = __importDefault(require("./hfs-admin-lang-en.json"));
7
+ const hfs_admin_lang_it_json_1 = __importDefault(require("./hfs-admin-lang-it.json"));
8
+ const hfs_admin_lang_zh_json_1 = __importDefault(require("./hfs-admin-lang-zh.json"));
9
+ const hfs_admin_lang_ru_json_1 = __importDefault(require("./hfs-admin-lang-ru.json"));
10
+ const hfs_admin_lang_ro_json_1 = __importDefault(require("./hfs-admin-lang-ro.json"));
11
+ const hfs_admin_lang_sr_json_1 = __importDefault(require("./hfs-admin-lang-sr.json"));
12
+ const hfs_admin_lang_sr_latn_json_1 = __importDefault(require("./hfs-admin-lang-sr-latn.json"));
13
+ const hfs_admin_lang_ko_json_1 = __importDefault(require("./hfs-admin-lang-ko.json"));
14
+ const hfs_admin_lang_ms_json_1 = __importDefault(require("./hfs-admin-lang-ms.json"));
15
+ const hfs_admin_lang_zh_hant_json_1 = __importDefault(require("./hfs-admin-lang-zh-hant.json"));
16
+ const hfs_admin_lang_fr_json_1 = __importDefault(require("./hfs-admin-lang-fr.json"));
17
+ const hfs_admin_lang_pl_json_1 = __importDefault(require("./hfs-admin-lang-pl.json"));
18
+ const hfs_admin_lang_pt_br_json_1 = __importDefault(require("./hfs-admin-lang-pt-br.json"));
19
+ const hfs_admin_lang_vi_json_1 = __importDefault(require("./hfs-admin-lang-vi.json"));
20
+ const hfs_admin_lang_es_json_1 = __importDefault(require("./hfs-admin-lang-es.json"));
21
+ const hfs_admin_lang_nl_json_1 = __importDefault(require("./hfs-admin-lang-nl.json"));
22
+ const hfs_admin_lang_el_json_1 = __importDefault(require("./hfs-admin-lang-el.json"));
23
+ const hfs_admin_lang_de_json_1 = __importDefault(require("./hfs-admin-lang-de.json"));
24
+ const hfs_admin_lang_fi_json_1 = __importDefault(require("./hfs-admin-lang-fi.json"));
25
+ const hfs_admin_lang_hu_json_1 = __importDefault(require("./hfs-admin-lang-hu.json"));
26
+ const hfs_admin_lang_ja_json_1 = __importDefault(require("./hfs-admin-lang-ja.json"));
27
+ const hfs_admin_lang_tr_json_1 = __importDefault(require("./hfs-admin-lang-tr.json"));
28
+ const hfs_admin_lang_th_json_1 = __importDefault(require("./hfs-admin-lang-th.json"));
29
+ const hfs_admin_lang_uk_json_1 = __importDefault(require("./hfs-admin-lang-uk.json"));
30
+ const hfs_admin_lang_ar_json_1 = __importDefault(require("./hfs-admin-lang-ar.json"));
31
+ const hfs_admin_lang_lt_json_1 = __importDefault(require("./hfs-admin-lang-lt.json"));
32
+ const hfs_admin_lang_bg_json_1 = __importDefault(require("./hfs-admin-lang-bg.json"));
33
+ exports.default = { en: hfs_admin_lang_en_json_1.default, it: hfs_admin_lang_it_json_1.default, zh: hfs_admin_lang_zh_json_1.default, ru: hfs_admin_lang_ru_json_1.default, ro: hfs_admin_lang_ro_json_1.default, sr: hfs_admin_lang_sr_json_1.default, 'sr-latn': hfs_admin_lang_sr_latn_json_1.default, ko: hfs_admin_lang_ko_json_1.default, ms: hfs_admin_lang_ms_json_1.default, 'zh-hant': hfs_admin_lang_zh_hant_json_1.default, fr: hfs_admin_lang_fr_json_1.default, pl: hfs_admin_lang_pl_json_1.default, 'pt-br': hfs_admin_lang_pt_br_json_1.default, vi: hfs_admin_lang_vi_json_1.default, es: hfs_admin_lang_es_json_1.default, nl: hfs_admin_lang_nl_json_1.default, el: hfs_admin_lang_el_json_1.default, de: hfs_admin_lang_de_json_1.default, fi: hfs_admin_lang_fi_json_1.default, hu: hfs_admin_lang_hu_json_1.default, ja: hfs_admin_lang_ja_json_1.default, tr: hfs_admin_lang_tr_json_1.default, th: hfs_admin_lang_th_json_1.default, uk: hfs_admin_lang_uk_json_1.default, ar: hfs_admin_lang_ar_json_1.default, lt: hfs_admin_lang_lt_json_1.default, bg: hfs_admin_lang_bg_json_1.default };