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.
- package/.dependency-cruiser.js +304 -0
- package/.eslintrc.js +51 -0
- package/.github/copilot-instructions.md +1 -0
- package/.vscode/settings.json +25 -0
- package/bin/deploy.js +4 -0
- package/bin/function.js +4 -0
- package/bin/server.js +4 -0
- package/costsBenefits.txt +112 -0
- package/deploy.ts +3 -0
- package/inject.ts +1 -0
- package/package.json +60 -0
- package/prompts.txt +54 -0
- package/spec.txt +820 -0
- package/src/-a-archives/archiveCache.ts +913 -0
- package/src/-a-archives/archives.ts +148 -0
- package/src/-a-archives/archivesBackBlaze.ts +792 -0
- package/src/-a-archives/archivesDisk.ts +418 -0
- package/src/-a-archives/copyLocalToBackblaze.ts +24 -0
- package/src/-a-auth/certs.ts +517 -0
- package/src/-a-auth/der.ts +122 -0
- package/src/-a-auth/ed25519.ts +1015 -0
- package/src/-a-auth/node-forge-ed25519.d.ts +17 -0
- package/src/-b-authorities/dnsAuthority.ts +203 -0
- package/src/-b-authorities/emailAuthority.ts +57 -0
- package/src/-c-identity/IdentityController.ts +200 -0
- package/src/-d-trust/NetworkTrust2.ts +150 -0
- package/src/-e-certs/EdgeCertController.ts +288 -0
- package/src/-e-certs/certAuthority.ts +192 -0
- package/src/-f-node-discovery/NodeDiscovery.ts +543 -0
- package/src/-g-core-values/NodeCapabilities.ts +134 -0
- package/src/-g-core-values/oneTimeForward.ts +91 -0
- package/src/-h-path-value-serialize/PathValueSerializer.ts +769 -0
- package/src/-h-path-value-serialize/stringSerializer.ts +176 -0
- package/src/0-path-value-core/LoggingClient.tsx +24 -0
- package/src/0-path-value-core/NodePathAuthorities.ts +978 -0
- package/src/0-path-value-core/PathController.ts +1 -0
- package/src/0-path-value-core/PathValueCommitter.ts +565 -0
- package/src/0-path-value-core/PathValueController.ts +231 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +154 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +820 -0
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +180 -0
- package/src/0-path-value-core/debugLogs.ts +90 -0
- package/src/0-path-value-core/pathValueArchives.ts +483 -0
- package/src/0-path-value-core/pathValueCore.ts +2217 -0
- package/src/1-path-client/RemoteWatcher.ts +558 -0
- package/src/1-path-client/pathValueClientWatcher.ts +702 -0
- package/src/2-proxy/PathValueProxyWatcher.ts +1857 -0
- package/src/2-proxy/archiveMoveHarness.ts +376 -0
- package/src/2-proxy/garbageCollection.ts +753 -0
- package/src/2-proxy/pathDatabaseProxyBase.ts +37 -0
- package/src/2-proxy/pathValueProxy.ts +139 -0
- package/src/2-proxy/schema2.ts +518 -0
- package/src/3-path-functions/PathFunctionHelpers.ts +129 -0
- package/src/3-path-functions/PathFunctionRunner.ts +619 -0
- package/src/3-path-functions/PathFunctionRunnerMain.ts +67 -0
- package/src/3-path-functions/deployBlock.ts +10 -0
- package/src/3-path-functions/deployCheck.ts +7 -0
- package/src/3-path-functions/deployMain.ts +160 -0
- package/src/3-path-functions/pathFunctionLoader.ts +282 -0
- package/src/3-path-functions/syncSchema.ts +475 -0
- package/src/3-path-functions/tests/functionsTest.ts +135 -0
- package/src/3-path-functions/tests/rejectTest.ts +77 -0
- package/src/4-dom/css.tsx +29 -0
- package/src/4-dom/cssTypes.d.ts +212 -0
- package/src/4-dom/qreact.tsx +2322 -0
- package/src/4-dom/qreactTest.tsx +417 -0
- package/src/4-querysub/Querysub.ts +877 -0
- package/src/4-querysub/QuerysubController.ts +620 -0
- package/src/4-querysub/copyEvent.ts +0 -0
- package/src/4-querysub/permissions.ts +289 -0
- package/src/4-querysub/permissionsShared.ts +1 -0
- package/src/4-querysub/querysubPrediction.ts +525 -0
- package/src/5-diagnostics/FullscreenModal.tsx +67 -0
- package/src/5-diagnostics/GenericFormat.tsx +165 -0
- package/src/5-diagnostics/Modal.tsx +79 -0
- package/src/5-diagnostics/Table.tsx +183 -0
- package/src/5-diagnostics/TimeGrouper.tsx +114 -0
- package/src/5-diagnostics/diskValueAudit.ts +216 -0
- package/src/5-diagnostics/memoryValueAudit.ts +442 -0
- package/src/5-diagnostics/nodeMetadata.ts +135 -0
- package/src/5-diagnostics/qreactDebug.tsx +309 -0
- package/src/5-diagnostics/shared.ts +26 -0
- package/src/5-diagnostics/synchronousLagTracking.ts +47 -0
- package/src/TestController.ts +35 -0
- package/src/allowclient.flag +0 -0
- package/src/bits.ts +86 -0
- package/src/buffers.ts +69 -0
- package/src/config.ts +53 -0
- package/src/config2.ts +48 -0
- package/src/diagnostics/ActionsHistory.ts +56 -0
- package/src/diagnostics/NodeViewer.tsx +503 -0
- package/src/diagnostics/SizeLimiter.ts +62 -0
- package/src/diagnostics/TimeDebug.tsx +18 -0
- package/src/diagnostics/benchmark.ts +139 -0
- package/src/diagnostics/errorLogs/ErrorLogController.ts +515 -0
- package/src/diagnostics/errorLogs/ErrorLogCore.ts +274 -0
- package/src/diagnostics/errorLogs/LogClassifiers.tsx +302 -0
- package/src/diagnostics/errorLogs/LogFilterUI.tsx +84 -0
- package/src/diagnostics/errorLogs/LogNotify.tsx +101 -0
- package/src/diagnostics/errorLogs/LogTimeSelector.tsx +724 -0
- package/src/diagnostics/errorLogs/LogViewer.tsx +757 -0
- package/src/diagnostics/errorLogs/hookErrors.ts +60 -0
- package/src/diagnostics/errorLogs/logFiltering.tsx +149 -0
- package/src/diagnostics/heapTag.ts +13 -0
- package/src/diagnostics/listenOnDebugger.ts +77 -0
- package/src/diagnostics/logs/DiskLoggerPage.tsx +572 -0
- package/src/diagnostics/logs/ObjectDisplay.tsx +165 -0
- package/src/diagnostics/logs/ansiFormat.ts +108 -0
- package/src/diagnostics/logs/diskLogGlobalContext.ts +38 -0
- package/src/diagnostics/logs/diskLogger.ts +305 -0
- package/src/diagnostics/logs/diskShimConsoleLogs.ts +32 -0
- package/src/diagnostics/logs/injectFileLocationToConsole.ts +50 -0
- package/src/diagnostics/logs/logGitHashes.ts +30 -0
- package/src/diagnostics/managementPages.tsx +289 -0
- package/src/diagnostics/periodic.ts +89 -0
- package/src/diagnostics/runSaturationTest.ts +416 -0
- package/src/diagnostics/satSchema.ts +64 -0
- package/src/diagnostics/trackResources.ts +82 -0
- package/src/diagnostics/watchdog.ts +55 -0
- package/src/errors.ts +132 -0
- package/src/forceProduction.ts +3 -0
- package/src/fs.ts +72 -0
- package/src/heapDumps.ts +666 -0
- package/src/https.ts +2 -0
- package/src/inject.ts +1 -0
- package/src/library-components/ATag.tsx +84 -0
- package/src/library-components/Button.tsx +344 -0
- package/src/library-components/ButtonSelector.tsx +64 -0
- package/src/library-components/DropdownCustom.tsx +151 -0
- package/src/library-components/DropdownSelector.tsx +32 -0
- package/src/library-components/Input.tsx +334 -0
- package/src/library-components/InputLabel.tsx +198 -0
- package/src/library-components/InputPicker.tsx +125 -0
- package/src/library-components/LazyComponent.tsx +62 -0
- package/src/library-components/MeasureHeightCSS.tsx +48 -0
- package/src/library-components/MeasuredDiv.tsx +47 -0
- package/src/library-components/ShowMore.tsx +51 -0
- package/src/library-components/SyncedController.ts +171 -0
- package/src/library-components/TimeRangeSelector.tsx +407 -0
- package/src/library-components/URLParam.ts +263 -0
- package/src/library-components/colors.tsx +14 -0
- package/src/library-components/drag.ts +114 -0
- package/src/library-components/icons.tsx +692 -0
- package/src/library-components/niceStringify.ts +50 -0
- package/src/library-components/renderToString.ts +52 -0
- package/src/misc/PromiseRace.ts +101 -0
- package/src/misc/color.ts +30 -0
- package/src/misc/getParentProcessId.cs +53 -0
- package/src/misc/getParentProcessId.ts +53 -0
- package/src/misc/hash.ts +83 -0
- package/src/misc/ipPong.js +13 -0
- package/src/misc/networking.ts +2 -0
- package/src/misc/random.ts +45 -0
- package/src/misc.ts +19 -0
- package/src/noserverhotreload.flag +0 -0
- package/src/path.ts +226 -0
- package/src/persistentLocalStore.ts +37 -0
- package/src/promise.ts +15 -0
- package/src/server.ts +73 -0
- package/src/src.d.ts +1 -0
- package/src/test/heapProcess.ts +36 -0
- package/src/test/mongoSatTest.tsx +55 -0
- package/src/test/satTest.ts +193 -0
- package/src/test/test.tsx +552 -0
- package/src/zip.ts +92 -0
- package/src/zipThreaded.ts +106 -0
- package/src/zipThreadedWorker.js +19 -0
- package/tsconfig.json +27 -0
- 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
|
+
}
|