querysub 0.2.0

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 (169) hide show
  1. package/.dependency-cruiser.js +304 -0
  2. package/.eslintrc.js +51 -0
  3. package/.github/copilot-instructions.md +1 -0
  4. package/.vscode/settings.json +25 -0
  5. package/bin/deploy.js +4 -0
  6. package/bin/function.js +4 -0
  7. package/bin/server.js +4 -0
  8. package/costsBenefits.txt +112 -0
  9. package/deploy.ts +3 -0
  10. package/inject.ts +1 -0
  11. package/package.json +60 -0
  12. package/prompts.txt +54 -0
  13. package/spec.txt +820 -0
  14. package/src/-a-archives/archiveCache.ts +913 -0
  15. package/src/-a-archives/archives.ts +148 -0
  16. package/src/-a-archives/archivesBackBlaze.ts +792 -0
  17. package/src/-a-archives/archivesDisk.ts +418 -0
  18. package/src/-a-archives/copyLocalToBackblaze.ts +24 -0
  19. package/src/-a-auth/certs.ts +517 -0
  20. package/src/-a-auth/der.ts +122 -0
  21. package/src/-a-auth/ed25519.ts +1015 -0
  22. package/src/-a-auth/node-forge-ed25519.d.ts +17 -0
  23. package/src/-b-authorities/dnsAuthority.ts +203 -0
  24. package/src/-b-authorities/emailAuthority.ts +57 -0
  25. package/src/-c-identity/IdentityController.ts +200 -0
  26. package/src/-d-trust/NetworkTrust2.ts +150 -0
  27. package/src/-e-certs/EdgeCertController.ts +288 -0
  28. package/src/-e-certs/certAuthority.ts +192 -0
  29. package/src/-f-node-discovery/NodeDiscovery.ts +543 -0
  30. package/src/-g-core-values/NodeCapabilities.ts +134 -0
  31. package/src/-g-core-values/oneTimeForward.ts +91 -0
  32. package/src/-h-path-value-serialize/PathValueSerializer.ts +769 -0
  33. package/src/-h-path-value-serialize/stringSerializer.ts +176 -0
  34. package/src/0-path-value-core/LoggingClient.tsx +24 -0
  35. package/src/0-path-value-core/NodePathAuthorities.ts +978 -0
  36. package/src/0-path-value-core/PathController.ts +1 -0
  37. package/src/0-path-value-core/PathValueCommitter.ts +565 -0
  38. package/src/0-path-value-core/PathValueController.ts +231 -0
  39. package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +154 -0
  40. package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +820 -0
  41. package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +180 -0
  42. package/src/0-path-value-core/debugLogs.ts +90 -0
  43. package/src/0-path-value-core/pathValueArchives.ts +483 -0
  44. package/src/0-path-value-core/pathValueCore.ts +2217 -0
  45. package/src/1-path-client/RemoteWatcher.ts +558 -0
  46. package/src/1-path-client/pathValueClientWatcher.ts +702 -0
  47. package/src/2-proxy/PathValueProxyWatcher.ts +1857 -0
  48. package/src/2-proxy/archiveMoveHarness.ts +376 -0
  49. package/src/2-proxy/garbageCollection.ts +753 -0
  50. package/src/2-proxy/pathDatabaseProxyBase.ts +37 -0
  51. package/src/2-proxy/pathValueProxy.ts +139 -0
  52. package/src/2-proxy/schema2.ts +518 -0
  53. package/src/3-path-functions/PathFunctionHelpers.ts +129 -0
  54. package/src/3-path-functions/PathFunctionRunner.ts +619 -0
  55. package/src/3-path-functions/PathFunctionRunnerMain.ts +67 -0
  56. package/src/3-path-functions/deployBlock.ts +10 -0
  57. package/src/3-path-functions/deployCheck.ts +7 -0
  58. package/src/3-path-functions/deployMain.ts +160 -0
  59. package/src/3-path-functions/pathFunctionLoader.ts +282 -0
  60. package/src/3-path-functions/syncSchema.ts +475 -0
  61. package/src/3-path-functions/tests/functionsTest.ts +135 -0
  62. package/src/3-path-functions/tests/rejectTest.ts +77 -0
  63. package/src/4-dom/css.tsx +29 -0
  64. package/src/4-dom/cssTypes.d.ts +212 -0
  65. package/src/4-dom/qreact.tsx +2322 -0
  66. package/src/4-dom/qreactTest.tsx +417 -0
  67. package/src/4-querysub/Querysub.ts +877 -0
  68. package/src/4-querysub/QuerysubController.ts +620 -0
  69. package/src/4-querysub/copyEvent.ts +0 -0
  70. package/src/4-querysub/permissions.ts +289 -0
  71. package/src/4-querysub/permissionsShared.ts +1 -0
  72. package/src/4-querysub/querysubPrediction.ts +525 -0
  73. package/src/5-diagnostics/FullscreenModal.tsx +67 -0
  74. package/src/5-diagnostics/GenericFormat.tsx +165 -0
  75. package/src/5-diagnostics/Modal.tsx +79 -0
  76. package/src/5-diagnostics/Table.tsx +183 -0
  77. package/src/5-diagnostics/TimeGrouper.tsx +114 -0
  78. package/src/5-diagnostics/diskValueAudit.ts +216 -0
  79. package/src/5-diagnostics/memoryValueAudit.ts +442 -0
  80. package/src/5-diagnostics/nodeMetadata.ts +135 -0
  81. package/src/5-diagnostics/qreactDebug.tsx +309 -0
  82. package/src/5-diagnostics/shared.ts +26 -0
  83. package/src/5-diagnostics/synchronousLagTracking.ts +47 -0
  84. package/src/TestController.ts +35 -0
  85. package/src/allowclient.flag +0 -0
  86. package/src/bits.ts +86 -0
  87. package/src/buffers.ts +69 -0
  88. package/src/config.ts +53 -0
  89. package/src/config2.ts +48 -0
  90. package/src/diagnostics/ActionsHistory.ts +56 -0
  91. package/src/diagnostics/NodeViewer.tsx +503 -0
  92. package/src/diagnostics/SizeLimiter.ts +62 -0
  93. package/src/diagnostics/TimeDebug.tsx +18 -0
  94. package/src/diagnostics/benchmark.ts +139 -0
  95. package/src/diagnostics/errorLogs/ErrorLogController.ts +515 -0
  96. package/src/diagnostics/errorLogs/ErrorLogCore.ts +274 -0
  97. package/src/diagnostics/errorLogs/LogClassifiers.tsx +302 -0
  98. package/src/diagnostics/errorLogs/LogFilterUI.tsx +84 -0
  99. package/src/diagnostics/errorLogs/LogNotify.tsx +101 -0
  100. package/src/diagnostics/errorLogs/LogTimeSelector.tsx +724 -0
  101. package/src/diagnostics/errorLogs/LogViewer.tsx +757 -0
  102. package/src/diagnostics/errorLogs/hookErrors.ts +60 -0
  103. package/src/diagnostics/errorLogs/logFiltering.tsx +149 -0
  104. package/src/diagnostics/heapTag.ts +13 -0
  105. package/src/diagnostics/listenOnDebugger.ts +77 -0
  106. package/src/diagnostics/logs/DiskLoggerPage.tsx +572 -0
  107. package/src/diagnostics/logs/ObjectDisplay.tsx +165 -0
  108. package/src/diagnostics/logs/ansiFormat.ts +108 -0
  109. package/src/diagnostics/logs/diskLogGlobalContext.ts +38 -0
  110. package/src/diagnostics/logs/diskLogger.ts +305 -0
  111. package/src/diagnostics/logs/diskShimConsoleLogs.ts +32 -0
  112. package/src/diagnostics/logs/injectFileLocationToConsole.ts +50 -0
  113. package/src/diagnostics/logs/logGitHashes.ts +30 -0
  114. package/src/diagnostics/managementPages.tsx +289 -0
  115. package/src/diagnostics/periodic.ts +89 -0
  116. package/src/diagnostics/runSaturationTest.ts +416 -0
  117. package/src/diagnostics/satSchema.ts +64 -0
  118. package/src/diagnostics/trackResources.ts +82 -0
  119. package/src/diagnostics/watchdog.ts +55 -0
  120. package/src/errors.ts +132 -0
  121. package/src/forceProduction.ts +3 -0
  122. package/src/fs.ts +72 -0
  123. package/src/heapDumps.ts +666 -0
  124. package/src/https.ts +2 -0
  125. package/src/inject.ts +1 -0
  126. package/src/library-components/ATag.tsx +84 -0
  127. package/src/library-components/Button.tsx +344 -0
  128. package/src/library-components/ButtonSelector.tsx +64 -0
  129. package/src/library-components/DropdownCustom.tsx +151 -0
  130. package/src/library-components/DropdownSelector.tsx +32 -0
  131. package/src/library-components/Input.tsx +334 -0
  132. package/src/library-components/InputLabel.tsx +198 -0
  133. package/src/library-components/InputPicker.tsx +125 -0
  134. package/src/library-components/LazyComponent.tsx +62 -0
  135. package/src/library-components/MeasureHeightCSS.tsx +48 -0
  136. package/src/library-components/MeasuredDiv.tsx +47 -0
  137. package/src/library-components/ShowMore.tsx +51 -0
  138. package/src/library-components/SyncedController.ts +171 -0
  139. package/src/library-components/TimeRangeSelector.tsx +407 -0
  140. package/src/library-components/URLParam.ts +263 -0
  141. package/src/library-components/colors.tsx +14 -0
  142. package/src/library-components/drag.ts +114 -0
  143. package/src/library-components/icons.tsx +692 -0
  144. package/src/library-components/niceStringify.ts +50 -0
  145. package/src/library-components/renderToString.ts +52 -0
  146. package/src/misc/PromiseRace.ts +101 -0
  147. package/src/misc/color.ts +30 -0
  148. package/src/misc/getParentProcessId.cs +53 -0
  149. package/src/misc/getParentProcessId.ts +53 -0
  150. package/src/misc/hash.ts +83 -0
  151. package/src/misc/ipPong.js +13 -0
  152. package/src/misc/networking.ts +2 -0
  153. package/src/misc/random.ts +45 -0
  154. package/src/misc.ts +19 -0
  155. package/src/noserverhotreload.flag +0 -0
  156. package/src/path.ts +226 -0
  157. package/src/persistentLocalStore.ts +37 -0
  158. package/src/promise.ts +15 -0
  159. package/src/server.ts +73 -0
  160. package/src/src.d.ts +1 -0
  161. package/src/test/heapProcess.ts +36 -0
  162. package/src/test/mongoSatTest.tsx +55 -0
  163. package/src/test/satTest.ts +193 -0
  164. package/src/test/test.tsx +552 -0
  165. package/src/zip.ts +92 -0
  166. package/src/zipThreaded.ts +106 -0
  167. package/src/zipThreadedWorker.js +19 -0
  168. package/tsconfig.json +27 -0
  169. package/yarnSpec.txt +56 -0
@@ -0,0 +1,288 @@
1
+ /*
2
+ Maintains an HTTPS certificate which all nodes may use (for access of nodes in the browser)
3
+ - Also maintains the multi-ip A record which all nodes contribute to
4
+
5
+ IMPORTANT! HTTPS certificates have to be shared, and therefore we should (and do) only use them for the browser
6
+ (using SNI to determine if calls are browser or server calls).
7
+ - They have to be shared because letsencrypt has a limit on how many certificates you can generate per domain,
8
+ and our server count could easily exceed that limit (and we can't reuse certificates cross machine,
9
+ both for security, and for routing reasons).
10
+ */
11
+
12
+ import { SocketFunction } from "socket-function/SocketFunction";
13
+ import { lazy } from "socket-function/src/caching";
14
+ import { createX509, generateKeyPair, getThreadKeyCert, parseCert } from "../-a-auth/certs";
15
+ import { backoffRetryLoop, logErrors } from "../errors";
16
+ import { getHTTPSKeyCert } from "./certAuthority";
17
+ import { getControllerNodeId } from "../-g-core-values/NodeCapabilities";
18
+ import * as forge from "node-forge";
19
+ import { getNodeIdIP, getNodeIdLocation } from "socket-function/src/nodeCache";
20
+ import { addRecord, deleteRecord, getRecords, hasDNSWritePermissions, setRecord } from "../-b-authorities/dnsAuthority";
21
+ import { SocketServerConfig } from "socket-function/src/webSocketServer";
22
+ import debugbreak from "debugbreak";
23
+ import { delay, runInfinitePoll, runInfinitePollCallAtStart } from "socket-function/src/batching";
24
+ import { getDomain, isNoNetwork } from "../config";
25
+ import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
26
+ import { getExternalIP, testTCPIsListening } from "../misc/networking";
27
+ import { magenta, yellow } from "socket-function/src/formatting/logColors";
28
+
29
+ let publicPort = -1;
30
+
31
+ // Figure out how to hook up our watch so it actually updates the underlying server
32
+ export function getSNICerts(config: {
33
+ publicPort: number
34
+ }): Exclude<SocketServerConfig["SNICerts"], undefined> {
35
+ if (config.publicPort > 0) {
36
+ if (publicPort !== -1 && publicPort !== config.publicPort) {
37
+ throw new Error(`getSNICerts called with different publicPort ${publicPort} and ${config.publicPort}. We require the same port, so we can correctly maintain the A record for the edge node.`);
38
+ }
39
+ publicPort = config.publicPort;
40
+ }
41
+ let certs: Exclude<SocketServerConfig["SNICerts"], undefined> = {
42
+ [getDomain()]: async (callback) => {
43
+ await EdgeCertController_watchHTTPSKeyCert(callback);
44
+ }
45
+ };
46
+ certs["noproxy." + getDomain()] = certs[getDomain()];
47
+ certs["127-0-0-1." + getDomain()] = certs[getDomain()];
48
+ return certs;
49
+ }
50
+
51
+ function createGetBaseCert() {
52
+ return async () => {
53
+ /*
54
+ // Code to create a self signed cert, for testing of expiration dates
55
+ let keyPair = generateKeyPair();
56
+ let certPair = createX509({
57
+ keyPair,
58
+ domain: rootCertDomain,
59
+ // Very short duration for testing
60
+ lifeSpan: 1000 * 60 * 15,
61
+ issuer: "self",
62
+ });
63
+ return { key: certPair.key.toString(), cert: certPair.cert.toString() };
64
+ //*/
65
+
66
+ return getHTTPSKeyCert(getDomain());
67
+ };
68
+ }
69
+
70
+ let getBaseCert = lazy(createGetBaseCert()) as () => Promise<{ key: string, cert: string }>;
71
+
72
+ const certUpdateLoop = lazy(() => {
73
+ logErrors((async () => {
74
+ let firstLoop = true;
75
+ while (true) {
76
+ let curCert = await getCertFromRemote();
77
+ if (!curCert) {
78
+ throw new Error(`Internal error, certUpdateLoop called before lastPromise was set`);
79
+ }
80
+ let certObj = parseCert(curCert.cert);
81
+ // Get expiration date
82
+ let expirationTime = +new Date(certObj.validity.notAfter);
83
+ let createTime = +new Date(certObj.validity.notBefore);
84
+
85
+ // If 75% of the lifetime has passed, renew the cert
86
+ let renewDate = createTime + (expirationTime - createTime) * 0.75;
87
+ let timeToExpire = renewDate - Date.now();
88
+ if (timeToExpire < 0) {
89
+ console.log(`HTTPS certificate is looking too old. Renewing from remote.`);
90
+ getCertFromRemote = createGetCertFromRemote();
91
+ continue;
92
+ }
93
+
94
+ if (!firstLoop) {
95
+ for (let callback of callbacks) {
96
+ callback(curCert);
97
+ }
98
+ }
99
+ firstLoop = false;
100
+
101
+ // Max timeout a signed integer, but lower is fine too
102
+ timeToExpire = Math.min(timeToExpire, 2 ** 30);
103
+ console.log(`Certicates up to date, renewing on ${new Date(Date.now() + timeToExpire)}`);
104
+ await delay(timeToExpire);
105
+ console.log(`Woke up to renew`);
106
+ }
107
+ })());
108
+ });
109
+
110
+ function createGetCertFromRemote() {
111
+ return lazy(async () => {
112
+ return backoffRetryLoop(async () => {
113
+ // Skip the remote call if we have DNS write permissions, to
114
+ // make bootstrapping easier.
115
+ if (await hasDNSWritePermissions()) {
116
+ let ip = SocketFunction.mountedIP;
117
+ if (ip === "0.0.0.0") {
118
+ ip = await getExternalIP();
119
+ }
120
+ return await getHTTPSKeyCertInner(ip);
121
+ }
122
+
123
+ let edgeNodeId = await getControllerNodeId(EdgeCertController);
124
+ if (!edgeNodeId) {
125
+ throw new Error("No EdgeCertController found");
126
+ }
127
+ return await EdgeCertController.nodes[edgeNodeId].getHTTPSKeyCert();
128
+ });
129
+ });
130
+ }
131
+ let getCertFromRemote = createGetCertFromRemote();
132
+
133
+ // NOTE: Only works if the machine is already listening. Needed for some special websocket
134
+ // stuff related to debugging.
135
+ export function debugGetRawEdgeCert() {
136
+ return getCertFromRemote();
137
+ }
138
+
139
+ let callbacks: ((newCertPair: { cert: string; key: string }) => void)[] = [];
140
+ async function EdgeCertController_watchHTTPSKeyCert(
141
+ callback: (newCertPair: { cert: string; key: string }) => void
142
+ ) {
143
+ certUpdateLoop();
144
+
145
+ callbacks.push(callback);
146
+ let certPair = await getCertFromRemote();
147
+ callback(certPair);
148
+ }
149
+
150
+ /** NOTE: Any SocketFunction.mount calls should probably call this, otherwise they won't be able to be called. */
151
+ export async function publishMachineARecords() {
152
+ if (!SocketFunction.isMounted()) {
153
+ throw new Error(`Must mount before publishing machine A records`);
154
+ }
155
+
156
+ let ip = SocketFunction.mountedIP;
157
+ if (ip === "0.0.0.0") {
158
+ ip = await getExternalIP();
159
+ }
160
+
161
+ let selfNodeId = SocketFunction.mountedNodeId;
162
+ let nodeObj = getNodeIdLocation(selfNodeId);
163
+ if (!nodeObj) throw new Error(`Invalid nodeId ${selfNodeId}`);
164
+ let machineAddress = nodeObj.address.split(".").slice(1).join(".");
165
+ if (ip === "127.0.0.1") {
166
+ let prev = await getRecords("A", machineAddress);
167
+ if (prev.length > 0 && prev[0] !== "127.0.0.1") {
168
+ // NOTE: We NEED to publish some records. HOWEVER, it is very likely they will run some services
169
+ // and not set public (such as the FunctionRunner). SO... just ignore this, leaving the records as
170
+ // public, even though the current run doesn't have public set.
171
+ if (process.argv[1].includes("server.ts")) {
172
+ console.log(yellow(`Current process is not marked as public, but machine previous had public services. NOT publishing A records to point to 127.0.0.1, which will make service inaccessible if the port is not forwarded open on the public ip.`));
173
+ }
174
+ return;
175
+ }
176
+ }
177
+ await setRecord("A", machineAddress, ip);
178
+ await setRecord("A", "*." + machineAddress, ip);
179
+ }
180
+
181
+ export const verifyServicesAlive = lazy(() => {
182
+
183
+ // NOTE: Our DNS TTL of 1 minute means we can't avoid downtime of 1 minute,
184
+ // so shorter polling intervals will have very little benefit.
185
+ runInfinitePoll(1000 * 60, removeDeadARecords);
186
+ });
187
+ async function removeDeadARecords() {
188
+ if (isNoNetwork()) return;
189
+ if (publicPort === -1) return;
190
+ const edgeDomain = getDomain();
191
+ let ips = await getRecords("A", edgeDomain);
192
+ // Don't check 127.0.0.1, if we are in development this will still be set,
193
+ // and it will be removed when we leave development
194
+ ips = ips.filter(ip => ip !== "127.0.0.1");
195
+
196
+ let results = await Promise.all(ips.map(async ip => {
197
+ return await testTCPIsListening(ip, publicPort);
198
+ }));
199
+
200
+ let deadIPs = ips.filter((ip, i) => !results[i]);
201
+ for (let deadIP of deadIPs) {
202
+ await deleteRecord("A", edgeDomain, deadIP);
203
+ }
204
+ }
205
+
206
+ async function getHTTPSKeyCertInner(callerIP: string) {
207
+ const edgeDomain = getDomain();
208
+ if (callerIP) {
209
+ let proxied = isNoNetwork() ? undefined : "proxied" as const;
210
+ try {
211
+ let promises: Promise<void>[] = [];
212
+ let existingIPs = await getRecords("A", edgeDomain);
213
+ if (callerIP === "127.0.0.1") {
214
+ if (existingIPs.length === 0) {
215
+ promises.push(addRecord("A", edgeDomain, callerIP, proxied));
216
+ } else if (existingIPs.length === 1 && existingIPs[0] === "127.0.0.1") {
217
+ // Good, do nothing
218
+ } else {
219
+ // Don't remove the public servers, this is probably just us accidentally running a
220
+ // test script for a public domain.
221
+ /*
222
+ // Maybe they took down all the public servers?
223
+ await removeDeadARecords();
224
+ existingIPs = await getRecords("A", edgeDomain);
225
+ if (existingIPs.length === 0) {
226
+ await addRecord("A", edgeDomain, callerIP);
227
+ } else {
228
+ await addRecord("A", "127-0-0-1." + edgeDomain, "127.0.0.1");
229
+ console.warn(`Tried to serve an edge node on the local domain, but SocketFunction.mount did NOT specify a public ip (ex, { ip: "0.0.0.0" }) AND there are already existing public servers. You can't load balance between real ips and 127.0.0.1! ${existingIPs.join(", ")}. You will need to use 127-0-0-1.${edgeDomain} to access the local server (instead of just ${edgeDomain}).`);
230
+ }
231
+ */
232
+ }
233
+ } else {
234
+ if (existingIPs.includes("127.0.0.1")) {
235
+ console.log(magenta(`Switching from local development to production development (removing A record for 127.0.0.1, and using a real ip)`));
236
+ promises.push(deleteRecord("A", edgeDomain, "127.0.0.1"));
237
+ }
238
+ promises.push(addRecord("A", edgeDomain, callerIP, proxied));
239
+ }
240
+ promises.push(addRecord("A", "noproxy." + edgeDomain, "127.0.0.1"));
241
+ promises.push(addRecord("A", "127-0-0-1." + edgeDomain, "127.0.0.1"));
242
+ // Add records in parallel, so we can wait for DNS propagation in parallel
243
+ await Promise.all(promises);
244
+ } catch (e) {
245
+ console.error(`Error updating DNS records, continuing without updating them`, e);
246
+ }
247
+ }
248
+
249
+ let cert = await getBaseCert();
250
+ // If the cert is 50% expired generate a new one
251
+ let certObj = parseCert(cert.cert);
252
+
253
+ // Get expiration date
254
+ let expirationTime = +new Date(certObj.validity.notAfter);
255
+ let createTime = +new Date(certObj.validity.notBefore);
256
+
257
+ // If 50% of the lifetime has passed, renew the cert
258
+ let renewDate = createTime + (expirationTime - createTime) * 0.5;
259
+ if (renewDate < Date.now()) {
260
+ console.log(`HTTPS certificate is looking too old, forcefully renewing`);
261
+ getHTTPSKeyCert.clear(getDomain());
262
+ getBaseCert = createGetBaseCert();
263
+ cert = await getBaseCert();
264
+ }
265
+
266
+ return cert;
267
+ }
268
+
269
+ class EdgeCertControllerBase {
270
+ public async getHTTPSKeyCert() {
271
+ // Ensure the a record is subscribed
272
+ const caller = SocketFunction.getCaller();
273
+ let callerIP = getNodeIdIP(caller.nodeId);
274
+ return await getHTTPSKeyCertInner(callerIP);
275
+ }
276
+ }
277
+
278
+
279
+ const EdgeCertController = SocketFunction.register(
280
+ "EdgeCertController-694c925b-fb10-4656-aed0-b53a48ded548",
281
+ new EdgeCertControllerBase(),
282
+ () => ({
283
+ getHTTPSKeyCert: {},
284
+ }),
285
+ () => ({
286
+ hooks: [requiresNetworkTrustHook],
287
+ })
288
+ );
@@ -0,0 +1,192 @@
1
+ import acme from "acme-client";
2
+ import { generateRSAKeyPair, parseCert, privateKeyToPem } from "../-a-auth/certs";
3
+ import { cache, lazy } from "socket-function/src/caching";
4
+ import { hasDNSWritePermissions, setRecord } from "../-b-authorities/dnsAuthority";
5
+ import { magenta } from "socket-function/src/formatting/logColors";
6
+ import { createKeyStore } from "../persistentLocalStore";
7
+ import { getArchives } from "../-a-archives/archives";
8
+ import { isNoNetwork } from "../config";
9
+ import { formatDateTime, formatTime } from "socket-function/src/formatting/format";
10
+ import { delay } from "socket-function/src/batching";
11
+ import { timeInMinute } from "socket-function/src/misc";
12
+
13
+ const archives = lazy(() => getArchives(`https_certs_2/`));
14
+ // Expire EXPIRATION_THRESHOLD% of the way through the certificate's lifetime
15
+ const EXPIRATION_THRESHOLD = 0.4;
16
+
17
+ export const getHTTPSKeyCert = cache(async (domain: string): Promise<{ key: string; cert: string }> => {
18
+ if (!await hasDNSWritePermissions()) {
19
+ throw new Error(`Cannot generate HTTPS key and cert unless we have DNS write permissions (need credentials in archives, at b2:/keys/cloudflare.json)`);
20
+ }
21
+ if (!domain.endsWith(".")) {
22
+ domain = domain + ".";
23
+ }
24
+ let keyCert: { key: string; cert: string } | undefined;
25
+
26
+ try {
27
+ let keyCertJSON = await archives().get(domain);
28
+ if (keyCertJSON) {
29
+ keyCert = JSON.parse(keyCertJSON.toString());
30
+ }
31
+ } catch { }
32
+ if (keyCert) {
33
+ // If 40% of the lifetime has passed, renew it (has to be < the threshold
34
+ // in EdgeCertController).
35
+ let certObj = parseCert(keyCert.cert);
36
+ let expirationTime = +new Date(certObj.validity.notAfter);
37
+ let createTime = +new Date(certObj.validity.notBefore);
38
+ let renewDate = createTime + (expirationTime - createTime) * EXPIRATION_THRESHOLD;
39
+ if (renewDate < Date.now()) {
40
+ console.log(magenta(`Renewing domain ${domain} (renew target is ${formatDateTime(renewDate)}).`));
41
+ keyCert = undefined;
42
+ }
43
+ } else {
44
+ console.log(magenta(`No cert found for domain ${domain}, generating shortly.`));
45
+ }
46
+ if (keyCert) {
47
+ return keyCert;
48
+ }
49
+
50
+ const accountKey = getAccountKey();
51
+ let altDomains: string[] = [];
52
+
53
+ // altDomains.push("noproxy." + domain);
54
+ // // NOTE: Allowing local access is just an optimization, not to avoid having to forward ports
55
+ // // (unless you type 127-0-0-1.domain into the browser... then I guess you don't have to forward ports?)
56
+ // altDomains.push("127-0-0-1." + domain);
57
+
58
+ // NOTE: I forget why we were not allowing wildcard domains. I think it was to prevent
59
+ // any HTTPS domains from impersonating servers. But... servers have two levels, so that isn't
60
+ // an issue. And even if they didn't they store their public key in their domain, so you
61
+ // can't really impersonate them anyways...
62
+ altDomains.push("*." + domain);
63
+
64
+ let isPending = await archives().getInfo(domain + "_pending");
65
+ if (isPending && isPending.writeTime > Date.now() - timeInMinute * 10) {
66
+ let pendingTime = +isPending.toString();
67
+ // Wait 2 minutes
68
+ let waitTime = Math.max(0, pendingTime - Date.now() + timeInMinute * 2);
69
+ if (waitTime) {
70
+ console.log(`getHTTPSKeyCert pending in another process, waiting ${formatTime(waitTime)} for other process to create certificate`);
71
+ await delay(waitTime);
72
+ return await getHTTPSKeyCert(domain);
73
+ }
74
+ }
75
+ let pendingTime = Date.now().toString();
76
+ await archives().set(domain + "_pending", Buffer.from(pendingTime));
77
+ // Wait a bit, and then make sure we are the last to write to pending. This helps a lot with
78
+ // race conditions (although they can still happen).
79
+ {
80
+ await delay(10 * 1000);
81
+ let pendingValue = await archives().get(domain + "_pending");
82
+ if (pendingValue && pendingValue.toString() !== pendingTime) {
83
+ return await getHTTPSKeyCert(domain);
84
+ }
85
+ }
86
+ try {
87
+ keyCert = await generateCert({ accountKey, domain, altDomains });
88
+ await archives().del(domain + "_pending");
89
+ } catch (e) {
90
+ if (String(e).includes("authorization must be pending")) {
91
+ console.log(`Authorization appears to be pending, waiting 2 minutes for other process to create certificate`);
92
+ await delay(timeInMinute * 2);
93
+ return await getHTTPSKeyCert(domain);
94
+ }
95
+ throw e;
96
+ }
97
+ await archives().set(domain, Buffer.from(JSON.stringify(keyCert)));
98
+ return keyCert;
99
+ });
100
+
101
+ let accountKeyCache = createKeyStore<{ key: string }>("letsEncryptAccountKey_1");
102
+ let getAccountKey: () => string = lazy(() => {
103
+ let keyCache = accountKeyCache.value;
104
+ if (!keyCache) {
105
+ console.log(`Generating new letsencrypt account key`);
106
+ const keyPair = generateRSAKeyPair();
107
+ keyCache = { key: privateKeyToPem(keyPair.privateKey) };
108
+ accountKeyCache.value = keyCache;
109
+ }
110
+ return keyCache.key;
111
+ });
112
+
113
+ async function generateCert(config: {
114
+ accountKey: string;
115
+ domain: string;
116
+ altDomains?: string[];
117
+ }): Promise<{
118
+ domains: string[];
119
+ key: string;
120
+ cert: string;
121
+ }> {
122
+ let { accountKey, domain } = config;
123
+
124
+ console.log(magenta(`Generating new cert for ${domain}`));
125
+
126
+ let domainList = [domain, ...config.altDomains || []];
127
+ // Strip trailing "."
128
+ domainList = domainList.map(x => x.endsWith(".") ? x.slice(0, -1) : x);
129
+
130
+ const [certificateKey, certificateCsr] = await acme.forge.createCsr({
131
+ commonName: domainList[0],
132
+ altNames: domainList.slice(1),
133
+ });
134
+
135
+ // So... acme-client is fine. Just re-implement the "auto" mode ourselves, to have more control over it.
136
+ const client = new acme.Client({
137
+ directoryUrl: acme.directory.letsencrypt.production,
138
+ accountKey: accountKey,
139
+ });
140
+
141
+ const accountPayload = {
142
+ termsOfServiceAgreed: true,
143
+ contact: [`mailto:sliftist@gmail.com`],
144
+ };
145
+
146
+ try {
147
+ await client.getAccountUrl();
148
+ } catch {
149
+ await client.createAccount(accountPayload);
150
+ }
151
+
152
+ const orderPayload = {
153
+ identifiers: domainList.map(domain => ({ type: "dns", value: domain })),
154
+ };
155
+ const order = await client.createOrder(orderPayload);
156
+ const authorizations = await client.getAuthorizations(order);
157
+
158
+ for (let auth of authorizations) {
159
+ if (auth.status === "valid") {
160
+ console.log(`Authorization already valid for ${auth.identifier.value}`);
161
+ continue;
162
+ }
163
+ console.log(`Starting authorization for ${auth.identifier.value}`);
164
+
165
+ // Only use DNS authorization
166
+ let challenge = auth.challenges.find(x => x.type === "dns-01");
167
+ if (!challenge) {
168
+ throw new Error("No DNS challenge found");
169
+ }
170
+ const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
171
+
172
+ let hostname = auth.identifier.value;
173
+ let challengeRecordName = "_acme-challenge." + hostname + ".";
174
+ await setRecord("TXT", challengeRecordName, keyAuthorization);
175
+
176
+ await client.completeChallenge(challenge);
177
+ console.log(`Challenge completed`);
178
+
179
+ await client.waitForValidStatus(challenge);
180
+ console.log(`Status of order is valid`);
181
+ }
182
+
183
+ const finalized = await client.finalizeOrder(order, certificateCsr);
184
+ console.log(`Order finalized`);
185
+
186
+ let cert = await client.getCertificate(finalized);
187
+ return {
188
+ domains: domainList,
189
+ key: certificateKey.toString(),
190
+ cert: cert,
191
+ };
192
+ }