querysub 0.100.0 → 0.102.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/package.json +2 -2
- package/src/-a-archives/archives.ts +9 -1
- package/src/-a-archives/archivesBackBlaze.ts +107 -25
- package/src/-b-authorities/cdnAuthority.ts +53 -0
- package/src/-b-authorities/cloudflareHelpers.ts +52 -0
- package/src/-b-authorities/dnsAuthority.ts +12 -85
- package/src/-e-certs/EdgeCertController.ts +23 -18
- package/src/-e-certs/certAuthority.ts +2 -0
- package/src/-f-node-discovery/NodeDiscovery.ts +9 -31
- package/src/4-querysub/Querysub.ts +26 -17
- package/src/4-querysub/edge/edgeBootstrap.ts +349 -0
- package/src/4-querysub/edge/edgeNodes.ts +166 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "querysub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.102.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
|
|
25
25
|
"pako": "^2.1.0",
|
|
26
26
|
"preact": "^10.11.3",
|
|
27
|
-
"socket-function": "^0.
|
|
27
|
+
"socket-function": "^0.66.0",
|
|
28
28
|
"terser": "^5.31.0",
|
|
29
29
|
"typesafecss": "^0.6.3",
|
|
30
30
|
"yaml": "^2.5.0",
|
|
@@ -92,7 +92,15 @@ export function nestArchives(path: string, archives: Archives): Archives {
|
|
|
92
92
|
targetPath: config.targetPath,
|
|
93
93
|
}),
|
|
94
94
|
assertPathValid: (path: string) => archives.assertPathValid(path),
|
|
95
|
-
getBaseArchives: () =>
|
|
95
|
+
getBaseArchives: () => {
|
|
96
|
+
let result = { parentPath: path, archives };
|
|
97
|
+
let base = archives.getBaseArchives?.();
|
|
98
|
+
if (base) {
|
|
99
|
+
result.parentPath = base.parentPath + path;
|
|
100
|
+
result.archives = base.archives;
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
},
|
|
96
104
|
};
|
|
97
105
|
}
|
|
98
106
|
|
|
@@ -8,7 +8,7 @@ import { httpsRequest } from "../https";
|
|
|
8
8
|
import { delay } from "socket-function/src/batching";
|
|
9
9
|
import { devDebugbreak } from "../config";
|
|
10
10
|
import { formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
11
|
-
import { blue, green } from "socket-function/src/formatting/logColors";
|
|
11
|
+
import { blue, green, magenta } from "socket-function/src/formatting/logColors";
|
|
12
12
|
import debugbreak from "debugbreak";
|
|
13
13
|
import { onTimeProfile } from "../-0-hooks/hooks";
|
|
14
14
|
|
|
@@ -49,6 +49,7 @@ const getAPI = lazy(async () => {
|
|
|
49
49
|
Authorization: "Basic " + Buffer.from(creds.applicationKeyId + ":" + creds.applicationKey).toString("base64"),
|
|
50
50
|
}
|
|
51
51
|
});
|
|
52
|
+
|
|
52
53
|
let auth = JSON.parse(authorizeRaw.toString()) as {
|
|
53
54
|
accountId: string;
|
|
54
55
|
authorizationToken: string;
|
|
@@ -88,6 +89,9 @@ const getAPI = lazy(async () => {
|
|
|
88
89
|
bucketType: "allPrivate" | "allPublic";
|
|
89
90
|
lifecycleRules?: any[];
|
|
90
91
|
corsRules?: unknown[];
|
|
92
|
+
bucketInfo?: {
|
|
93
|
+
[key: string]: unknown;
|
|
94
|
+
};
|
|
91
95
|
}, {
|
|
92
96
|
accountId: string;
|
|
93
97
|
bucketId: string;
|
|
@@ -101,6 +105,28 @@ const getAPI = lazy(async () => {
|
|
|
101
105
|
revision: number;
|
|
102
106
|
}>("b2_create_bucket", "POST");
|
|
103
107
|
|
|
108
|
+
const updateBucket = createB2Function<{
|
|
109
|
+
accountId: string;
|
|
110
|
+
bucketId: string;
|
|
111
|
+
bucketType?: "allPrivate" | "allPublic";
|
|
112
|
+
lifecycleRules?: any[];
|
|
113
|
+
bucketInfo?: {
|
|
114
|
+
[key: string]: unknown;
|
|
115
|
+
};
|
|
116
|
+
corsRules?: unknown[];
|
|
117
|
+
}, {
|
|
118
|
+
accountId: string;
|
|
119
|
+
bucketId: string;
|
|
120
|
+
bucketName: string;
|
|
121
|
+
bucketType: "allPrivate" | "allPublic";
|
|
122
|
+
bucketInfo: {
|
|
123
|
+
lifecycleRules: any[];
|
|
124
|
+
};
|
|
125
|
+
corsRules: any[];
|
|
126
|
+
lifecycleRules: any[];
|
|
127
|
+
revision: number;
|
|
128
|
+
}>("b2_update_bucket", "POST");
|
|
129
|
+
|
|
104
130
|
// https://www.backblaze.com/apidocs/b2-update-bucket
|
|
105
131
|
// TODO: b2_update_bucket, so we can update CORS, etc
|
|
106
132
|
|
|
@@ -316,8 +342,16 @@ const getAPI = lazy(async () => {
|
|
|
316
342
|
fileId: string;
|
|
317
343
|
}, {}>("b2_cancel_large_file", "POST", "noAccountId");
|
|
318
344
|
|
|
345
|
+
async function getDownloadURL(path: string) {
|
|
346
|
+
if (!path.startsWith("/")) {
|
|
347
|
+
path = "/" + path;
|
|
348
|
+
}
|
|
349
|
+
return auth.downloadUrl + path;
|
|
350
|
+
}
|
|
351
|
+
|
|
319
352
|
return {
|
|
320
353
|
createBucket,
|
|
354
|
+
updateBucket,
|
|
321
355
|
listBuckets,
|
|
322
356
|
downloadFileByName,
|
|
323
357
|
uploadFile,
|
|
@@ -329,17 +363,19 @@ const getAPI = lazy(async () => {
|
|
|
329
363
|
uploadPart,
|
|
330
364
|
finishLargeFile,
|
|
331
365
|
cancelLargeFile,
|
|
366
|
+
getDownloadURL,
|
|
332
367
|
};
|
|
333
368
|
});
|
|
334
369
|
|
|
335
370
|
type B2Api = (typeof getAPI) extends () => Promise<infer T> ? T : never;
|
|
336
371
|
|
|
337
372
|
|
|
338
|
-
class
|
|
373
|
+
export class ArchivesBackblaze {
|
|
339
374
|
public constructor(private config: {
|
|
340
375
|
bucketName: string;
|
|
341
376
|
public?: boolean;
|
|
342
377
|
immutable?: boolean;
|
|
378
|
+
cacheTime?: number;
|
|
343
379
|
}) { }
|
|
344
380
|
|
|
345
381
|
private bucketName = this.config.bucketName.replaceAll(/[^\w\d]/g, "-");
|
|
@@ -360,6 +396,27 @@ class Backblaze {
|
|
|
360
396
|
|
|
361
397
|
private getBucketAPI = lazy(async () => {
|
|
362
398
|
let api = await getAPI();
|
|
399
|
+
|
|
400
|
+
let cacheTime = this.config.cacheTime ?? 0;
|
|
401
|
+
if (this.config.immutable) {
|
|
402
|
+
cacheTime = 86400 * 1000;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
let desiredCorsRules = this.config.public ? [{
|
|
406
|
+
corsRuleName: "onlyCurrentOrigin",
|
|
407
|
+
allowedOrigins: ["*"],
|
|
408
|
+
allowedOperations: ["b2_download_file_by_id", "b2_download_file_by_name"],
|
|
409
|
+
allowedHeaders: [],
|
|
410
|
+
exposeHeaders: ["x-bz-content-sha1"],
|
|
411
|
+
maxAgeSeconds: cacheTime / 1000,
|
|
412
|
+
}] : [];
|
|
413
|
+
let bucketInfo: Record<string, unknown> = {};
|
|
414
|
+
if (cacheTime) {
|
|
415
|
+
bucketInfo["cache-control"] = `max-age=${cacheTime / 1000}`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
let exists = false;
|
|
363
420
|
try {
|
|
364
421
|
await api.createBucket({
|
|
365
422
|
bucketName: this.bucketName,
|
|
@@ -370,27 +427,38 @@ class Backblaze {
|
|
|
370
427
|
"daysFromHidingToDeleting": 7,
|
|
371
428
|
"fileNamePrefix": ""
|
|
372
429
|
}],
|
|
373
|
-
corsRules:
|
|
374
|
-
|
|
375
|
-
allowedOrigins: ["*"],
|
|
376
|
-
allowedOperations: ["b2_download_file_by_id", "b2_download_file_by_name"],
|
|
377
|
-
allowedHeaders: [],
|
|
378
|
-
exposeHeaders: ["x-bz-content-sha1"],
|
|
379
|
-
maxAgeSeconds: this.config.immutable ? 86400 : 0,
|
|
380
|
-
}] : [],
|
|
430
|
+
corsRules: desiredCorsRules,
|
|
431
|
+
bucketInfo
|
|
381
432
|
});
|
|
382
433
|
} catch (e: any) {
|
|
383
434
|
if (!e.stack.includes(`"duplicate_bucket_name"`)) {
|
|
384
435
|
throw e;
|
|
385
436
|
}
|
|
437
|
+
exists = true;
|
|
386
438
|
}
|
|
387
|
-
|
|
439
|
+
|
|
440
|
+
let bucketList = await api.listBuckets({
|
|
388
441
|
bucketName: this.bucketName,
|
|
389
442
|
});
|
|
390
|
-
if (
|
|
443
|
+
if (bucketList.buckets.length === 0) {
|
|
391
444
|
throw new Error(`Bucket name "${this.bucketName}" is being used by someone else. Bucket names have to be globally unique. Try a different name until you find a free one.`);
|
|
392
445
|
}
|
|
393
|
-
this.bucketId =
|
|
446
|
+
this.bucketId = bucketList.buckets[0].bucketId;
|
|
447
|
+
|
|
448
|
+
if (exists) {
|
|
449
|
+
let bucket = bucketList.buckets[0];
|
|
450
|
+
if (JSON.stringify(bucket.corsRules) !== JSON.stringify(desiredCorsRules) || JSON.stringify(bucket.bucketInfo) !== JSON.stringify(bucketInfo)) {
|
|
451
|
+
console.log(magenta(`Updating CORS rules for ${this.bucketName}`));
|
|
452
|
+
await api.updateBucket({
|
|
453
|
+
accountId: bucket.accountId,
|
|
454
|
+
bucketId: bucket.bucketId,
|
|
455
|
+
bucketType: bucket.bucketType,
|
|
456
|
+
lifecycleRules: bucket.lifecycleRules,
|
|
457
|
+
corsRules: desiredCorsRules,
|
|
458
|
+
bucketInfo: bucketInfo,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
394
462
|
return api;
|
|
395
463
|
});
|
|
396
464
|
|
|
@@ -719,19 +787,17 @@ class Backblaze {
|
|
|
719
787
|
copyInstead?: boolean;
|
|
720
788
|
}) {
|
|
721
789
|
let { path, target, targetPath } = config;
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
target = targetUnwrapped.archives;
|
|
727
|
-
targetPath = targetUnwrapped.parentPath + targetPath;
|
|
790
|
+
let base = target.getBaseArchives?.();
|
|
791
|
+
if (base) {
|
|
792
|
+
target = base.archives;
|
|
793
|
+
targetPath = base.parentPath + targetPath;
|
|
728
794
|
}
|
|
729
795
|
// A self move should NOOP (and definitely not copy, and then delete itself!)
|
|
730
796
|
if (target === this && path === targetPath) {
|
|
731
797
|
this.log(`Backblaze move path to itself. Skipping move, as there is no work to do. ${path}`);
|
|
732
798
|
return;
|
|
733
799
|
}
|
|
734
|
-
if (target instanceof
|
|
800
|
+
if (target instanceof ArchivesBackblaze) {
|
|
735
801
|
let targetBucketId = target.bucketId;
|
|
736
802
|
if (targetBucketId === this.bucketId && path === targetPath) return;
|
|
737
803
|
await this.apiRetryLogic(async (api) => {
|
|
@@ -771,6 +837,15 @@ class Backblaze {
|
|
|
771
837
|
}): Promise<void> {
|
|
772
838
|
return this.move({ ...config, copyInstead: true });
|
|
773
839
|
}
|
|
840
|
+
|
|
841
|
+
public async getDownloadURL(path: string) {
|
|
842
|
+
return await this.apiRetryLogic(async (api) => {
|
|
843
|
+
if (path.startsWith("/")) {
|
|
844
|
+
path = path.slice(1);
|
|
845
|
+
}
|
|
846
|
+
return await api.getDownloadURL("file/" + this.bucketName + "/" + path);
|
|
847
|
+
});
|
|
848
|
+
}
|
|
774
849
|
}
|
|
775
850
|
|
|
776
851
|
/*
|
|
@@ -783,16 +858,23 @@ Names should be a UTF-8 string up to 1024 bytes with the following exceptions:
|
|
|
783
858
|
|
|
784
859
|
|
|
785
860
|
export const getArchivesBackblaze = cache((domain: string) => {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
return archivesBackblaze;
|
|
861
|
+
return new ArchivesBackblaze({ bucketName: domain });
|
|
789
862
|
});
|
|
790
863
|
export const getArchivesBackblazePublicImmutable = cache((domain: string) => {
|
|
791
|
-
|
|
864
|
+
return new ArchivesBackblaze({
|
|
792
865
|
bucketName: domain + "-public-immutable",
|
|
793
866
|
public: true,
|
|
794
867
|
immutable: true
|
|
795
868
|
});
|
|
869
|
+
});
|
|
796
870
|
|
|
797
|
-
|
|
871
|
+
// NOTE: Cache by a minute. This might be a bad idea, but... usually whole reason for public is
|
|
872
|
+
// for cloudflare caching (as otherwise we can just access it through a server), or for large files
|
|
873
|
+
// (which should be cached anyways, and probably even use immutable caching).
|
|
874
|
+
export const getArchivesBackblazePublic = cache((domain: string) => {
|
|
875
|
+
return new ArchivesBackblaze({
|
|
876
|
+
bucketName: domain + "-public",
|
|
877
|
+
public: true,
|
|
878
|
+
cacheTime: timeInMinute,
|
|
879
|
+
});
|
|
798
880
|
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Archives } from "../-a-archives/archives";
|
|
2
|
+
import { ArchivesBackblaze } from "../-a-archives/archivesBackBlaze";
|
|
3
|
+
import { cloudflareGETCall, cloudflarePOSTCall } from "./cloudflareHelpers";
|
|
4
|
+
import { setRecord, getZoneId } from "./dnsAuthority";
|
|
5
|
+
import debugbreak from "debugbreak";
|
|
6
|
+
|
|
7
|
+
// servicestest.querysub.com/servicesIndex.json
|
|
8
|
+
|
|
9
|
+
export async function hostArchives(config: {
|
|
10
|
+
archives: Archives;
|
|
11
|
+
// Ex, "archives"
|
|
12
|
+
subdomain: string;
|
|
13
|
+
// Ex, "example.com"
|
|
14
|
+
domain: string;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
getURL: (path: string) => Promise<string>;
|
|
17
|
+
}> {
|
|
18
|
+
let { archives, subdomain, domain } = config;
|
|
19
|
+
|
|
20
|
+
let base = archives.getBaseArchives?.();
|
|
21
|
+
let parentPath = base?.parentPath ?? "";
|
|
22
|
+
archives = base?.archives ?? archives;
|
|
23
|
+
if (parentPath && !parentPath.endsWith("/")) {
|
|
24
|
+
parentPath += "/";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!(archives instanceof ArchivesBackblaze)) {
|
|
28
|
+
throw new Error(`hostArchives not implemented for ${archives.constructor.name}, only "ArchivesBackblaze" is implemented at the moment`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let archiveT = archives as ArchivesBackblaze;
|
|
32
|
+
let baseURL = await archiveT.getDownloadURL("");
|
|
33
|
+
// Remove the trailing slash if it exists
|
|
34
|
+
if (baseURL.endsWith("/")) {
|
|
35
|
+
baseURL = baseURL.slice(0, -1);
|
|
36
|
+
}
|
|
37
|
+
let backblazeHost = new URL(baseURL).hostname;
|
|
38
|
+
|
|
39
|
+
// HACK: Assuming cloudflare is still our DNS provider. If it isn't... this won't work.
|
|
40
|
+
await setRecord("CNAME", subdomain + "." + domain, backblazeHost, "proxied");
|
|
41
|
+
|
|
42
|
+
async function getURL(path: string) {
|
|
43
|
+
if (path.startsWith("/")) {
|
|
44
|
+
path = path.slice(1);
|
|
45
|
+
}
|
|
46
|
+
let targetPath = await archiveT.getDownloadURL(parentPath + path);
|
|
47
|
+
let url = new URL(targetPath);
|
|
48
|
+
url.hostname = subdomain + "." + domain;
|
|
49
|
+
return url.toString();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { getURL };
|
|
53
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { httpsRequest } from "socket-function/src/https";
|
|
2
|
+
import { getArchives } from "../-a-archives/archives";
|
|
3
|
+
import { lazy } from "socket-function/src/caching";
|
|
4
|
+
|
|
5
|
+
export const keys = getArchives("keys");
|
|
6
|
+
|
|
7
|
+
export const getCloudflareCreds = lazy(async (): Promise<{ key: string; email: string }> => {
|
|
8
|
+
let credsJSON = await keys.get("cloudflare.json");
|
|
9
|
+
if (!credsJSON) throw new Error(`b2:/keys/cloudflare.json is missing. It should contain { "key": "your-key", "email": "your-email" }`);
|
|
10
|
+
return JSON.parse(credsJSON.toString());
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export async function cloudflareGETCall<T>(path: string, params?: { [key: string]: string }): Promise<T> {
|
|
14
|
+
let url = new URL(`https://api.cloudflare.com/client/v4` + path);
|
|
15
|
+
for (let key in params) {
|
|
16
|
+
url.searchParams.set(key, params[key]);
|
|
17
|
+
}
|
|
18
|
+
let creds = await getCloudflareCreds();
|
|
19
|
+
let result = await httpsRequest(url.toString(), [], "GET", undefined, {
|
|
20
|
+
headers: {
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
"X-Auth-Email": creds.email,
|
|
23
|
+
"X-Auth-User-Service-Key": creds.key,
|
|
24
|
+
"X-Auth-Key": creds.key,
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
let result2 = JSON.parse(result.toString()) as { result: unknown; success: boolean; errors: { code: number; message: string }[] };
|
|
28
|
+
if (!result2.success) {
|
|
29
|
+
throw new Error(`Cloudflare call failed: ${result2.errors.map(x => x.message).join(", ")}`);
|
|
30
|
+
}
|
|
31
|
+
return result2.result as T;
|
|
32
|
+
}
|
|
33
|
+
export async function cloudflarePOSTCall<T>(path: string, params: { [key: string]: unknown }): Promise<T> {
|
|
34
|
+
return await cloudflareCall(path, Buffer.from(JSON.stringify(params)), "POST");
|
|
35
|
+
}
|
|
36
|
+
export async function cloudflareCall<T>(path: string, payload: Buffer, method: string): Promise<T> {
|
|
37
|
+
let url = new URL(`https://api.cloudflare.com/client/v4` + path);
|
|
38
|
+
let creds = await getCloudflareCreds();
|
|
39
|
+
let result = await httpsRequest(url.toString(), payload, method, undefined, {
|
|
40
|
+
headers: {
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
"X-Auth-Email": creds.email,
|
|
43
|
+
"X-Auth-User-Service-Key": creds.key,
|
|
44
|
+
"X-Auth-Key": creds.key,
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
let result2 = JSON.parse(result.toString()) as { result: unknown; success: boolean; errors: { code: number; message: string }[] };
|
|
48
|
+
if (!result2.success) {
|
|
49
|
+
throw new Error(`Cloudflare call failed: ${result2.errors.map(x => x.message).join(", ")}`);
|
|
50
|
+
}
|
|
51
|
+
return result2.result as T;
|
|
52
|
+
}
|
|
@@ -9,6 +9,7 @@ import { delay } from "socket-function/src/batching";
|
|
|
9
9
|
import debugbreak from "debugbreak";
|
|
10
10
|
import { isClient } from "../config2";
|
|
11
11
|
import { getArchives } from "../-a-archives/archives";
|
|
12
|
+
import { cloudflareCall, cloudflareGETCall, cloudflarePOSTCall, getCloudflareCreds } from "./cloudflareHelpers";
|
|
12
13
|
|
|
13
14
|
const DNS_TTLSeconds = {
|
|
14
15
|
"TXT": 60,
|
|
@@ -17,24 +18,18 @@ const DNS_TTLSeconds = {
|
|
|
17
18
|
|
|
18
19
|
export const keys = getArchives("keys");
|
|
19
20
|
|
|
20
|
-
export const getDNSCreds = lazy(async (): Promise<{ key: string; email: string }> => {
|
|
21
|
-
let credsJSON = await keys.get("cloudflare.json");
|
|
22
|
-
if (!credsJSON) throw new Error(`b2:/keys/cloudflare.json is missing. It should contain { "key": "your-key", "email": "your-email" }`);
|
|
23
|
-
return JSON.parse(credsJSON.toString());
|
|
24
|
-
});
|
|
25
|
-
|
|
26
21
|
export const hasDNSWritePermissions = lazy(async () => {
|
|
27
22
|
if (!isNode()) return false;
|
|
28
23
|
if (isClient()) return false;
|
|
29
24
|
try {
|
|
30
|
-
await
|
|
25
|
+
await getCloudflareCreds();
|
|
31
26
|
return true;
|
|
32
27
|
} catch {
|
|
33
28
|
return false;
|
|
34
29
|
}
|
|
35
30
|
});
|
|
36
31
|
|
|
37
|
-
const getZoneId = cache(async (rootDomain: string): Promise<string> => {
|
|
32
|
+
export const getZoneId = cache(async (rootDomain: string): Promise<string> => {
|
|
38
33
|
let zones = await cloudflareGETCall<{ id: string; name: string }[]>("/zones", {});
|
|
39
34
|
let selected = zones.find(x => x.name === rootDomain);
|
|
40
35
|
if (!selected) {
|
|
@@ -53,7 +48,13 @@ function getRootDomain(key: string) {
|
|
|
53
48
|
export async function getRecordsRaw(type: string, key: string) {
|
|
54
49
|
if (key.endsWith(".")) key = key.slice(0, -1);
|
|
55
50
|
let zoneId = await getZoneId(getRootDomain(key));
|
|
56
|
-
let results = await cloudflareGETCall<{
|
|
51
|
+
let results = await cloudflareGETCall<{
|
|
52
|
+
id: string;
|
|
53
|
+
type: string;
|
|
54
|
+
name: string;
|
|
55
|
+
content: string;
|
|
56
|
+
proxied: boolean;
|
|
57
|
+
}[]>(`/zones/${zoneId}/dns_records`);
|
|
57
58
|
return results.filter(x => x.type === type && x.name === key);
|
|
58
59
|
}
|
|
59
60
|
export async function getRecords(type: string, key: string) {
|
|
@@ -84,7 +85,7 @@ export async function setRecord(type: string, key: string, value: string, proxie
|
|
|
84
85
|
if (key.endsWith(".")) key = key.slice(0, -1);
|
|
85
86
|
let zoneId = await getZoneId(getRootDomain(key));
|
|
86
87
|
let prevValues = await getRecordsRaw(type, key);
|
|
87
|
-
if (prevValues.some(x => x.content === value)) return;
|
|
88
|
+
if (prevValues.some(x => x.content === value && x.proxied === !!proxied)) return;
|
|
88
89
|
|
|
89
90
|
console.log(`Removing previous records of ${type} for ${key} ${JSON.stringify(prevValues.map(x => x.content))}`);
|
|
90
91
|
let didDeletions = false;
|
|
@@ -126,78 +127,4 @@ export async function addRecord(type: string, key: string, value: string, proxie
|
|
|
126
127
|
console.log(`Waiting ${ttl} seconds for DNS to propagate...`);
|
|
127
128
|
await delay(ttl * 1000);
|
|
128
129
|
console.log(`Done waiting for DNS to update.`);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
async function cloudflareGETCall<T>(path: string, params?: { [key: string]: string }): Promise<T> {
|
|
133
|
-
let url = new URL(`https://api.cloudflare.com/client/v4` + path);
|
|
134
|
-
for (let key in params) {
|
|
135
|
-
url.searchParams.set(key, params[key]);
|
|
136
|
-
}
|
|
137
|
-
let creds = await getDNSCreds();
|
|
138
|
-
let result = await httpsRequest(url.toString(), [], "GET", undefined, {
|
|
139
|
-
headers: {
|
|
140
|
-
"Content-Type": "application/json",
|
|
141
|
-
"X-Auth-Email": creds.email,
|
|
142
|
-
"X-Auth-User-Service-Key": creds.key,
|
|
143
|
-
"X-Auth-Key": creds.key,
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
let result2 = JSON.parse(result.toString()) as { result: unknown; success: boolean; errors: { code: number; message: string }[] };
|
|
147
|
-
if (!result2.success) {
|
|
148
|
-
throw new Error(`Cloudflare call failed: ${result2.errors.map(x => x.message).join(", ")}`);
|
|
149
|
-
}
|
|
150
|
-
return result2.result as T;
|
|
151
|
-
}
|
|
152
|
-
async function cloudflarePOSTCall<T>(path: string, params: { [key: string]: unknown }): Promise<T> {
|
|
153
|
-
return await cloudflareCall(path, Buffer.from(JSON.stringify(params)), "POST");
|
|
154
|
-
}
|
|
155
|
-
async function cloudflareCall<T>(path: string, payload: Buffer, method: string): Promise<T> {
|
|
156
|
-
let url = new URL(`https://api.cloudflare.com/client/v4` + path);
|
|
157
|
-
let creds = await getDNSCreds();
|
|
158
|
-
let result = await httpsRequest(url.toString(), payload, method, undefined, {
|
|
159
|
-
headers: {
|
|
160
|
-
"Content-Type": "application/json",
|
|
161
|
-
"X-Auth-Email": creds.email,
|
|
162
|
-
"X-Auth-User-Service-Key": creds.key,
|
|
163
|
-
"X-Auth-Key": creds.key,
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
let result2 = JSON.parse(result.toString()) as { result: unknown; success: boolean; errors: { code: number; message: string }[] };
|
|
167
|
-
if (!result2.success) {
|
|
168
|
-
throw new Error(`Cloudflare call failed: ${result2.errors.map(x => x.message).join(", ")}`);
|
|
169
|
-
}
|
|
170
|
-
return result2.result as T;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
/*
|
|
175
|
-
async function setRecord(googleDns: googleCloud.DNS, zone: googleCloud.Zone, type: string, key: string, value: unknown) {
|
|
176
|
-
let recordsResponse = await zone.getRecords();
|
|
177
|
-
let prevValue = recordsResponse[0].filter(x => x.metadata.name === key && x.type === type);
|
|
178
|
-
|
|
179
|
-
// NOTE: It seems like the data is ALWAYS an array, even if we set a value? So... just assume it is an array,
|
|
180
|
-
// and take the first entry. If we ever need to actually support arrays this will change, but... I don't
|
|
181
|
-
// think we will...
|
|
182
|
-
if (prevValue.some(x => JSON.stringify((x.data as any)?.[0]) === JSON.stringify(value))) {
|
|
183
|
-
console.log(`Record already set, (${type} ${zone.metadata.dnsName}) ${key} === ${JSON.stringify(value)}`);
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
for (let record of prevValue) {
|
|
188
|
-
console.log(`Deleting record (${type} ${zone.metadata.dnsName}) ${key} = ${JSON.stringify(record.data)}`);
|
|
189
|
-
await record.delete();
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
console.log((`Setting record (${type} in ${zone.metadata.dnsName}) ${key} = ${JSON.stringify(value)}`));
|
|
193
|
-
const TTL = DNS_TTLSeconds[type as "A"] || 60;
|
|
194
|
-
await zone.addRecords(zone.record(type, {
|
|
195
|
-
name: key,
|
|
196
|
-
data: value as any,
|
|
197
|
-
ttl: TTL,
|
|
198
|
-
}));
|
|
199
|
-
|
|
200
|
-
// Wait for the record to propagate
|
|
201
|
-
await delay(TTL * 1000 * 2);
|
|
202
|
-
}
|
|
203
|
-
*/
|
|
130
|
+
}
|
|
@@ -31,10 +31,21 @@ import { shutdown } from "../diagnostics/periodic";
|
|
|
31
31
|
|
|
32
32
|
let publicPort = -1;
|
|
33
33
|
|
|
34
|
+
const getHostedIP = lazy(async () => {
|
|
35
|
+
if (!isPublic()) {
|
|
36
|
+
return "127.0.0.1";
|
|
37
|
+
}
|
|
38
|
+
return await getExternalIP();
|
|
39
|
+
});
|
|
40
|
+
const getIPDomain = lazy(async () => {
|
|
41
|
+
let ip = await getHostedIP();
|
|
42
|
+
return ip.replaceAll(".", "-") + "." + getDomain();
|
|
43
|
+
});
|
|
44
|
+
|
|
34
45
|
// Figure out how to hook up our watch so it actually updates the underlying server
|
|
35
|
-
export function getSNICerts(config: {
|
|
46
|
+
export async function getSNICerts(config: {
|
|
36
47
|
publicPort: number
|
|
37
|
-
}): Exclude<SocketServerConfig["SNICerts"], undefined
|
|
48
|
+
}): Promise<Exclude<SocketServerConfig["SNICerts"], undefined>> {
|
|
38
49
|
if (config.publicPort > 0) {
|
|
39
50
|
if (publicPort !== -1 && publicPort !== config.publicPort) {
|
|
40
51
|
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.`);
|
|
@@ -48,6 +59,7 @@ export function getSNICerts(config: {
|
|
|
48
59
|
};
|
|
49
60
|
certs["noproxy." + getDomain()] = certs[getDomain()];
|
|
50
61
|
certs["127-0-0-1." + getDomain()] = certs[getDomain()];
|
|
62
|
+
certs[await getIPDomain()] = certs[getDomain()];
|
|
51
63
|
return certs;
|
|
52
64
|
}
|
|
53
65
|
|
|
@@ -156,29 +168,21 @@ export async function publishMachineARecords() {
|
|
|
156
168
|
throw new Error(`Must mount before publishing machine A records`);
|
|
157
169
|
}
|
|
158
170
|
|
|
159
|
-
let ip =
|
|
160
|
-
if (ip === "0.0.0.0") {
|
|
161
|
-
ip = await getExternalIP();
|
|
162
|
-
}
|
|
171
|
+
let ip = await getHostedIP();
|
|
163
172
|
|
|
164
173
|
let selfNodeId = SocketFunction.mountedNodeId;
|
|
165
174
|
let nodeObj = getNodeIdLocation(selfNodeId);
|
|
166
175
|
if (!nodeObj) throw new Error(`Invalid nodeId ${selfNodeId}`);
|
|
167
176
|
let machineAddress = nodeObj.address.split(".").slice(1).join(".");
|
|
168
|
-
if (ip === "127.0.0.1") {
|
|
169
|
-
let prev = await getRecords("A", machineAddress);
|
|
170
|
-
if (prev.length > 0 && prev[0] !== "127.0.0.1") {
|
|
171
|
-
// NOTE: We NEED to publish some records. HOWEVER, it is very likely they will run some services
|
|
172
|
-
// and not set public (such as the FunctionRunner). SO... just ignore this, leaving the records as
|
|
173
|
-
// public, even though the current run doesn't have public set.
|
|
174
|
-
if (process.argv[1].includes("server.ts")) {
|
|
175
|
-
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 port forwarded (or open). I recommend using noproxy.DOMAIN.com to access the server. 127-0-0-1.DOMAIN.com might work as well.`));
|
|
176
|
-
}
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
177
|
await setRecord("A", machineAddress, ip);
|
|
181
178
|
await setRecord("A", "*." + machineAddress, ip);
|
|
179
|
+
let ipDomain = await getIPDomain();
|
|
180
|
+
await setRecord("A", ipDomain, ip);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
ip,
|
|
184
|
+
ipDomain,
|
|
185
|
+
};
|
|
182
186
|
}
|
|
183
187
|
|
|
184
188
|
const runEdgeDomainAliveLoop = lazy(() => {
|
|
@@ -264,6 +268,7 @@ async function getHTTPSKeyCertInner(callerIP: string) {
|
|
|
264
268
|
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}).`);
|
|
265
269
|
}
|
|
266
270
|
*/
|
|
271
|
+
console.log(yellow(`Current process is not marked as public, but machine previous had public services. NOT setting ${edgeDomain} to 127.0.0.1, as this would make the public services inaccessible. Assuming the current process is for development, I recommend using noproxy.${edgeDomain} or 127-0-0-1.${edgeDomain} to access the server.`));
|
|
267
272
|
}
|
|
268
273
|
} else {
|
|
269
274
|
if (existingIPs.includes("127.0.0.1")) {
|
|
@@ -59,6 +59,8 @@ export const getHTTPSKeyCert = cache(async (domain: string): Promise<{ key: stri
|
|
|
59
59
|
// any HTTPS domains from impersonating servers. But... servers have two levels, so that isn't
|
|
60
60
|
// an issue. And even if they didn't they store their public key in their domain, so you
|
|
61
61
|
// can't really impersonate them anyways...
|
|
62
|
+
// - AND, we need this for IP type A records, which... we need to pick the server we want
|
|
63
|
+
// to connect to.
|
|
62
64
|
altDomains.push("*." + domain);
|
|
63
65
|
|
|
64
66
|
let isPending = await archives().getInfo(domain + "_pending");
|
|
@@ -23,6 +23,7 @@ import dns from "dns/promises";
|
|
|
23
23
|
import { isDefined } from "../misc";
|
|
24
24
|
import { diskLog, noDiskLogPrefix } from "../diagnostics/logs/diskLogger";
|
|
25
25
|
import { getDebuggerUrl } from "../diagnostics/listenOnDebugger";
|
|
26
|
+
import { getBootedEdgeNode } from "../4-querysub/edge/edgeBootstrap";
|
|
26
27
|
|
|
27
28
|
let HEARTBEAT_INTERVAL = timeInMinute * 2;
|
|
28
29
|
// Interval which we check other heartbeats
|
|
@@ -65,7 +66,6 @@ export function isNodeDiscoveryLogging() {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
export const getOurNodeId = getOwnNodeId;
|
|
68
|
-
export const getOurMachineId = getOwnNodeId;
|
|
69
69
|
export const getOurNodeIdAssert = getOwnNodeIdAssert;
|
|
70
70
|
|
|
71
71
|
export const SPECIAL_NODE_ID_FOR_UNMOUNTED_NODE = "SPECIAL_NODE_ID_FOR_UNMOUNTED_NODE";
|
|
@@ -430,6 +430,7 @@ beforeGetNodeAllId = async () => {
|
|
|
430
430
|
export async function onNodeDiscoveryReady() {
|
|
431
431
|
await getAllNodeIds();
|
|
432
432
|
}
|
|
433
|
+
|
|
433
434
|
if (isServer()) {
|
|
434
435
|
setImmediate(() => {
|
|
435
436
|
logErrors(runHeartbeatAuditLoop());
|
|
@@ -456,36 +457,13 @@ if (isServer()) {
|
|
|
456
457
|
};
|
|
457
458
|
} else {
|
|
458
459
|
setImmediate(() => {
|
|
459
|
-
let
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
// - Only if isNoNetwork, otherwise it causes unnecessary lag.
|
|
467
|
-
let time = Date.now();
|
|
468
|
-
let isNoNetwork = await NodeDiscoveryController.nodes[browserNodeId].isNoNetwork();
|
|
469
|
-
console.log(magenta(`isNoNetwork took ${Date.now() - time}ms`));
|
|
470
|
-
if (isNoNetwork && !location.search.includes("nolocalhost")) {
|
|
471
|
-
let url = new URL("https://" + browserNodeId);
|
|
472
|
-
url.hostname = "127-0-0-1." + url.hostname;
|
|
473
|
-
let localhostNode = url.host;
|
|
474
|
-
// NOTE: The timeout isn't that important, as this is only if no network, and only on page load.
|
|
475
|
-
// - A low timeout here causes failure to use the localhost ip sometimes. WHICH, often breaks
|
|
476
|
-
// authentication (as we are likely not authenticated for our public IP).
|
|
477
|
-
let localhostNodeId = await timeoutToUndefinedSilent(1000, NodeDiscoveryController.nodes[localhostNode].getNodeId());
|
|
478
|
-
let httpServerNodeId = await NodeDiscoveryController.nodes[browserNodeId].getNodeId();
|
|
479
|
-
if (localhostNodeId === httpServerNodeId) {
|
|
480
|
-
nodes[0] = localhostNode;
|
|
481
|
-
rootDiscoveryNodeId = localhostNode;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
allNodeIds2 = new Set(nodes);
|
|
486
|
-
discoveryReady.resolve();
|
|
487
|
-
}))());
|
|
488
|
-
|
|
460
|
+
let edgeNode = getBootedEdgeNode();
|
|
461
|
+
if (!edgeNode) {
|
|
462
|
+
throw new Error(`No edge node set during edgeBootstrap? This should be impossible.`);
|
|
463
|
+
}
|
|
464
|
+
let nodes = [edgeNode.host];
|
|
465
|
+
allNodeIds2 = new Set(nodes);
|
|
466
|
+
discoveryReady.resolve();
|
|
489
467
|
|
|
490
468
|
// NOTE: We run into TLS issues (as in, our servers use self signed certs), if we try to talk to just
|
|
491
469
|
// any node, so... we better just talk to the edge node
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// a file prevents it from transforming it, so we'll miss a lot of files doing it this late).
|
|
4
4
|
import "../inject";
|
|
5
5
|
|
|
6
|
-
import { isNode, timeInMinute } from "socket-function/src/misc";
|
|
6
|
+
import { isNode, timeInDay, timeInHour, timeInMinute } from "socket-function/src/misc";
|
|
7
7
|
|
|
8
8
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
9
9
|
import { isHotReloading, onHotReload, watchFilesAndTriggerHotReloading } from "socket-function/hot/HotReloadController";
|
|
@@ -484,15 +484,24 @@ export class Querysub {
|
|
|
484
484
|
|
|
485
485
|
await this.addSourceMapCheck(config);
|
|
486
486
|
|
|
487
|
-
// NOTE: This is just static hosting, and unrelated to querysub (sort of). Any site with some kind of transpiler
|
|
488
|
-
// and way to serve transpiled files can server our code.
|
|
489
487
|
SocketFunction.expose(RequireController);
|
|
490
488
|
setRequireBootRequire(config.rootPath);
|
|
489
|
+
|
|
490
|
+
let entryPaths = [config.clientsideEntryPoint, ...Querysub.afterBootImports];
|
|
491
491
|
SocketFunction.setDefaultHTTPCall(RequireController, "requireHTML", {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
492
|
+
// NOTE: This should be cached in our CDN anyways, so it should be fast
|
|
493
|
+
// to access even if every request hit the server (as it will hit the CDN's,
|
|
494
|
+
// server, which is usually close to the client). And it only impacts the initial
|
|
495
|
+
// load, which isn't that important for us.
|
|
496
|
+
cacheTime: timeInMinute * 5,
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
RequireController.injectHTMLBeforeStartup(async () => {
|
|
501
|
+
let edgeBootstrapFile = await getEdgeBootstrapScript({
|
|
502
|
+
edgeNodeConfigURL: await getEdgeNodeConfigURL(),
|
|
503
|
+
});
|
|
504
|
+
return `<script>${edgeBootstrapFile}</script>`;
|
|
496
505
|
});
|
|
497
506
|
|
|
498
507
|
if (!config.justHTTP) {
|
|
@@ -510,14 +519,19 @@ export class Querysub {
|
|
|
510
519
|
autoForwardPort: true,
|
|
511
520
|
...await getThreadKeyCert(),
|
|
512
521
|
SNICerts: {
|
|
513
|
-
...getSNICerts({
|
|
522
|
+
...await getSNICerts({
|
|
514
523
|
publicPort: config.port,
|
|
515
524
|
}),
|
|
516
525
|
},
|
|
517
526
|
allowHostnames,
|
|
518
527
|
});
|
|
519
528
|
|
|
520
|
-
await publishMachineARecords();
|
|
529
|
+
let { ip, ipDomain } = await publishMachineARecords();
|
|
530
|
+
|
|
531
|
+
await registerEdgeNode({
|
|
532
|
+
host: ipDomain + ":" + config.port,
|
|
533
|
+
entryPaths,
|
|
534
|
+
});
|
|
521
535
|
}
|
|
522
536
|
private static async addSourceMapCheck(config: {
|
|
523
537
|
sourceCheck?: MachineSourceCheck;
|
|
@@ -719,13 +733,6 @@ export class Querysub {
|
|
|
719
733
|
public: isPublic(),
|
|
720
734
|
port,
|
|
721
735
|
...await getThreadKeyCert(),
|
|
722
|
-
// WAIT! Why were services getting SNI CERTS!!!??? This is used to set the
|
|
723
|
-
// HTTPS cloudflare pool ips, so... services should DEFINITELY not use this!
|
|
724
|
-
// SNICerts: {
|
|
725
|
-
// ...getSNICerts({
|
|
726
|
-
// publicPort: port,
|
|
727
|
-
// }),
|
|
728
|
-
// }
|
|
729
736
|
});
|
|
730
737
|
|
|
731
738
|
await publishMachineARecords();
|
|
@@ -883,4 +890,6 @@ registerGetCompressDisk(() => Querysub.COMPRESS_DISK);
|
|
|
883
890
|
|
|
884
891
|
import "../diagnostics/watchdog";
|
|
885
892
|
import "../diagnostics/trackResources";
|
|
886
|
-
import "../diagnostics/benchmark";
|
|
893
|
+
import "../diagnostics/benchmark";
|
|
894
|
+
import { getEdgeNodeConfigURL, registerEdgeNode } from "./edge/edgeNodes"; import { getEdgeBootstrapScript } from "./edge/edgeBootstrap";
|
|
895
|
+
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { cache } from "socket-function/src/caching";
|
|
2
|
+
import { isServer } from "../../config2";
|
|
3
|
+
import { EdgeNodeConfig, EdgeNodesIndex } from "./edgeNodes";
|
|
4
|
+
import { timeInMinute } from "socket-function/src/misc";
|
|
5
|
+
import { measureBlock } from "socket-function/src/profiling/measure";
|
|
6
|
+
|
|
7
|
+
module.hotreload = true;
|
|
8
|
+
module.noserverhotreload = false;
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
declare global {
|
|
12
|
+
var BOOTED_EDGE_NODE: EdgeNodeConfig | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
export function getBootedEdgeNode(): EdgeNodeConfig {
|
|
17
|
+
if (isServer()) throw new Error(`getBootedEdgeNode is not available on the server`);
|
|
18
|
+
|
|
19
|
+
let edgeNode = globalThis.BOOTED_EDGE_NODE;
|
|
20
|
+
if (!edgeNode) throw new Error(`No edge node booted? This should be impossible.`);
|
|
21
|
+
return edgeNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let getCachedConfig = cache(async (url: string): Promise<EdgeNodesIndex | undefined> => {
|
|
25
|
+
setTimeout(() => {
|
|
26
|
+
getCachedConfig.clear(url);
|
|
27
|
+
}, timeInMinute * 15);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
let response = await fetch(url);
|
|
31
|
+
return await response.json();
|
|
32
|
+
} catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export async function getEdgeBootstrapScript(config: {
|
|
38
|
+
edgeNodeConfigURL: string;
|
|
39
|
+
}): Promise<string> {
|
|
40
|
+
return await measureBlock(async function getEdgeBootstrapScript() {
|
|
41
|
+
let cachedConfig = await getCachedConfig(config.edgeNodeConfigURL);
|
|
42
|
+
return `(${edgeNodeFunction.toString()})(${JSON.stringify({ ...config, cachedConfig })});`;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// DEFINED elsewhere
|
|
47
|
+
/*
|
|
48
|
+
declare global {
|
|
49
|
+
var onProgressHandler: undefined | ((progress: {
|
|
50
|
+
type: string;
|
|
51
|
+
addValue: number;
|
|
52
|
+
addMax: number;
|
|
53
|
+
}) => void);
|
|
54
|
+
var onErrorHandler: undefined | ((error: string) => void);
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
var BOOT_TIME: number;
|
|
58
|
+
var builtInModuleExports: {
|
|
59
|
+
[key: string]: unknown;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
async function edgeNodeFunction(config: {
|
|
65
|
+
edgeNodeConfigURL: string;
|
|
66
|
+
cachedConfig: EdgeNodesIndex | undefined;
|
|
67
|
+
}) {
|
|
68
|
+
// IMPORTANT! Everything in this function is embedded, so this function can't use any external imports
|
|
69
|
+
// (except for type imports, of course).
|
|
70
|
+
|
|
71
|
+
function formatNumber(value: number) {
|
|
72
|
+
if (value < 1024) return value + "";
|
|
73
|
+
else if (value < 1024 * 1024) return (value / 1024).toFixed(1) + "K";
|
|
74
|
+
else if (value < 1024 * 1024 * 1024) return (value / 1024 / 1024).toFixed(1) + "M";
|
|
75
|
+
else return (value / 1024 / 1024 / 1024).toFixed(1) + "G";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function createProgressUI(): {
|
|
79
|
+
stop: () => void;
|
|
80
|
+
setMessage: (message: string) => void;
|
|
81
|
+
} {
|
|
82
|
+
let progressUI = document.createElement("div");
|
|
83
|
+
progressUI.style.position = "fixed";
|
|
84
|
+
progressUI.style.top = "0";
|
|
85
|
+
progressUI.style.left = "0";
|
|
86
|
+
progressUI.style.width = "100%";
|
|
87
|
+
progressUI.style.height = "100%";
|
|
88
|
+
progressUI.style.backgroundColor = "hsl(0, 0%, 20%)";
|
|
89
|
+
progressUI.style.display = "flex";
|
|
90
|
+
progressUI.style.justifyContent = "center";
|
|
91
|
+
progressUI.style.alignItems = "center";
|
|
92
|
+
progressUI.style.fontFamily = "Verdana, sans-serif";
|
|
93
|
+
progressUI.style.fontSize = "16px";
|
|
94
|
+
document.body.appendChild(progressUI);
|
|
95
|
+
|
|
96
|
+
const svgContainer = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
97
|
+
svgContainer.setAttribute("viewBox", "0 0 100 100");
|
|
98
|
+
svgContainer.setAttribute("width", "100%");
|
|
99
|
+
svgContainer.setAttribute("height", "100%");
|
|
100
|
+
progressUI.appendChild(svgContainer);
|
|
101
|
+
|
|
102
|
+
const progressText = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
103
|
+
progressText.setAttribute("x", "50");
|
|
104
|
+
progressText.setAttribute("y", "50");
|
|
105
|
+
progressText.setAttribute("text-anchor", "middle");
|
|
106
|
+
progressText.setAttribute("dominant-baseline", "middle");
|
|
107
|
+
progressText.setAttribute("fill", "#ffffff");
|
|
108
|
+
progressText.setAttribute("font-size", "5");
|
|
109
|
+
svgContainer.appendChild(progressText);
|
|
110
|
+
|
|
111
|
+
const progressState = new Map<string, {
|
|
112
|
+
arc: SVGPathElement;
|
|
113
|
+
radius: number;
|
|
114
|
+
value: number;
|
|
115
|
+
max: number;
|
|
116
|
+
}>();
|
|
117
|
+
|
|
118
|
+
let lastRenderTime = 0;
|
|
119
|
+
let renderPending = false;
|
|
120
|
+
|
|
121
|
+
function updateProgress() {
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
if (now - lastRenderTime < 10) {
|
|
124
|
+
if (!renderPending) {
|
|
125
|
+
renderPending = true;
|
|
126
|
+
setTimeout(() => {
|
|
127
|
+
renderPending = false;
|
|
128
|
+
updateProgress();
|
|
129
|
+
}, 10);
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
lastRenderTime = now;
|
|
135
|
+
|
|
136
|
+
for (const [type, state] of progressState) {
|
|
137
|
+
let { arc, radius, value, max } = state;
|
|
138
|
+
|
|
139
|
+
let startAngle = -Math.PI / 2;
|
|
140
|
+
let fraction = value / max;
|
|
141
|
+
if (max === 0) fraction = 0;
|
|
142
|
+
|
|
143
|
+
let endAngle = startAngle + fraction * 2 * Math.PI;
|
|
144
|
+
|
|
145
|
+
let startX = 50 + radius * Math.cos(startAngle);
|
|
146
|
+
let startY = 50 + radius * Math.sin(startAngle);
|
|
147
|
+
let endX = 50 + radius * Math.cos(endAngle);
|
|
148
|
+
let endY = 50 + radius * Math.sin(endAngle);
|
|
149
|
+
|
|
150
|
+
let largeArcFlag = endAngle - startAngle > Math.PI ? 1 : 0;
|
|
151
|
+
|
|
152
|
+
let pathData = [
|
|
153
|
+
`M ${startX} ${startY}`,
|
|
154
|
+
`A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endX} ${endY}`
|
|
155
|
+
].join(" ");
|
|
156
|
+
|
|
157
|
+
if (fraction > 0.99) {
|
|
158
|
+
// Draw a complete circle using two 180-degree arcs
|
|
159
|
+
pathData = [
|
|
160
|
+
`M ${50 + radius} ${50}`,
|
|
161
|
+
`A ${radius} ${radius} 0 1 1 ${50 - radius} ${50}`,
|
|
162
|
+
`A ${radius} ${radius} 0 1 1 ${50 + radius} ${50}`
|
|
163
|
+
].join(" ");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
arc.setAttribute("d", pathData);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Set up the progress handler
|
|
171
|
+
globalThis.onProgressHandler = (progress) => {
|
|
172
|
+
let state = progressState.get(progress.type);
|
|
173
|
+
if (!state) {
|
|
174
|
+
const radius = 40 - (progressState.size * 5);
|
|
175
|
+
const arc = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
176
|
+
arc.setAttribute("fill", "transparent");
|
|
177
|
+
let hue = progressState.size * 60 + 120;
|
|
178
|
+
arc.setAttribute("stroke", `hsl(${hue}, 75%, 75%)`);
|
|
179
|
+
arc.setAttribute("stroke-width", "4");
|
|
180
|
+
svgContainer.appendChild(arc);
|
|
181
|
+
state = {
|
|
182
|
+
arc,
|
|
183
|
+
radius,
|
|
184
|
+
value: 0,
|
|
185
|
+
max: 0
|
|
186
|
+
};
|
|
187
|
+
progressState.set(progress.type, state);
|
|
188
|
+
}
|
|
189
|
+
state.value += progress.addValue;
|
|
190
|
+
state.max += progress.addMax;
|
|
191
|
+
|
|
192
|
+
progressText.textContent = `${progress.type} | ${formatNumber(state.value)}`;
|
|
193
|
+
|
|
194
|
+
updateProgress();
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
function setMessage(message: string) {
|
|
198
|
+
progressText.textContent = message;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let stopped = false;
|
|
202
|
+
return {
|
|
203
|
+
stop() {
|
|
204
|
+
stopped = true;
|
|
205
|
+
progressUI.remove();
|
|
206
|
+
globalThis.onProgressHandler = undefined;
|
|
207
|
+
},
|
|
208
|
+
setMessage
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let cachedConfig = config.cachedConfig;
|
|
213
|
+
|
|
214
|
+
let progressUI = createProgressUI();
|
|
215
|
+
progressUI.setMessage("Finding available server...");
|
|
216
|
+
while (true) {
|
|
217
|
+
try {
|
|
218
|
+
let booted = await tryToBoot();
|
|
219
|
+
if (booted) break;
|
|
220
|
+
} catch (e) {
|
|
221
|
+
console.error(e);
|
|
222
|
+
}
|
|
223
|
+
progressUI.setMessage("No available servers, trying again in 15 seconds...");
|
|
224
|
+
console.log(`Failing to boot, trying in 15 seconds`);
|
|
225
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * 15));
|
|
226
|
+
}
|
|
227
|
+
progressUI.stop();
|
|
228
|
+
|
|
229
|
+
function isIPDomain(domain: string) {
|
|
230
|
+
return domain.split("-").length === 4;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function tryToBoot(): Promise<boolean> {
|
|
234
|
+
let nodeConfig = await getEdgeNodeConfig();
|
|
235
|
+
await bootEdgeNode(nodeConfig);
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
async function isNodeAlive(node: EdgeNodeConfig): Promise<boolean> {
|
|
239
|
+
try {
|
|
240
|
+
let abortController = new AbortController();
|
|
241
|
+
let url = `https://${node.host}?classGuid=RequireController-e2f811f3-14b8-4759-b0d6-73f14516cf1d&functionName=getModules&args=[[]]`;
|
|
242
|
+
let response = await fetch(url, { signal: abortController.signal });
|
|
243
|
+
// Cancel, so we don't use up all of our sockets (we are limited to 6 per domain in chrome)
|
|
244
|
+
// on non-responsive servers.
|
|
245
|
+
setTimeout(() => {
|
|
246
|
+
abortController.abort();
|
|
247
|
+
}, 1000 * 15);
|
|
248
|
+
return response.ok;
|
|
249
|
+
} catch (e) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function getEdgeNodeConfig(): Promise<EdgeNodeConfig> {
|
|
254
|
+
let edgeIndex = cachedConfig || await (await fetch(config.edgeNodeConfigURL)).json() as EdgeNodesIndex;
|
|
255
|
+
cachedConfig = undefined;
|
|
256
|
+
|
|
257
|
+
console.group(`Found edge nodes`);
|
|
258
|
+
|
|
259
|
+
let edgeNodes = edgeIndex.edgeNodes;
|
|
260
|
+
|
|
261
|
+
function getNodeDebugString(node: EdgeNodeConfig) {
|
|
262
|
+
return `${node.host} | ${node.entryPaths.join(" + ")} | git = ${node.gitHash.slice(0, 16)} | ${node.nodeId} | ${node.public ? "public" : "private"}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
for (let node of edgeNodes) {
|
|
266
|
+
console.log(getNodeDebugString(node));
|
|
267
|
+
}
|
|
268
|
+
console.groupEnd();
|
|
269
|
+
|
|
270
|
+
// If two values have the same host, ignore the older one(s)
|
|
271
|
+
edgeNodes.sort((a, b) => -(a.bootTime - b.bootTime));
|
|
272
|
+
let usedHosts = new Set<string>();
|
|
273
|
+
edgeNodes = edgeNodes.filter(x => {
|
|
274
|
+
if (usedHosts.has(x.host)) {
|
|
275
|
+
console.log(`IGNORE overlapping: ${getNodeDebugString(x)}`);
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
return true;
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
// If they are using an IP domain, that's the only node we accept (we still need to pick the node
|
|
283
|
+
// though, as we need to know the entryPaths to import)
|
|
284
|
+
{
|
|
285
|
+
let curHost = document.location.host;
|
|
286
|
+
let exactNode = edgeNodes.find(x => x.host === curHost);
|
|
287
|
+
if (exactNode) {
|
|
288
|
+
console.log(`MATCH exact host: ${getNodeDebugString(exactNode)}`);
|
|
289
|
+
return exactNode;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let subDomain = document.location.hostname.split(".").slice(0, -2).join(".");
|
|
293
|
+
if (isIPDomain(subDomain)) {
|
|
294
|
+
throw new Error(`Using an IP domain, but cannot find a node for it`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// I guess... only allow private nodes, if they specify the exact host (which would match above).
|
|
299
|
+
edgeNodes = edgeNodes.filter(x => x.public);
|
|
300
|
+
|
|
301
|
+
// TODO: Instead of randomly shuffling, use the node's current load when picking a node.
|
|
302
|
+
// All our future traffic (such as syncing) goes through the edge node we use, so it preferrable
|
|
303
|
+
// to pick a node with low utilization.
|
|
304
|
+
edgeNodes = shuffle(edgeNodes);
|
|
305
|
+
|
|
306
|
+
// We check multiple at a time, so a node being unresponsive doesn't cause lag
|
|
307
|
+
let PARALLEL_FACTOR = 3;
|
|
308
|
+
for (let i = 0; i < edgeNodes.length; i += PARALLEL_FACTOR) {
|
|
309
|
+
let promises = [];
|
|
310
|
+
for (let j = 0; j < PARALLEL_FACTOR; j++) {
|
|
311
|
+
promises.push((async () => {
|
|
312
|
+
let node = edgeNodes[i + j];
|
|
313
|
+
try {
|
|
314
|
+
let alive = await isNodeAlive(node);
|
|
315
|
+
if (alive) return node;
|
|
316
|
+
} catch { }
|
|
317
|
+
return undefined;
|
|
318
|
+
})());
|
|
319
|
+
}
|
|
320
|
+
let node = await Promise.race(promises);
|
|
321
|
+
if (node) {
|
|
322
|
+
return node;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
throw new Error(`No alive nodes found`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function shuffle<T>(array: T[]): T[] {
|
|
329
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
330
|
+
let j = Math.floor(Math.random() * (i + 1));
|
|
331
|
+
[array[i], array[j]] = [array[j], array[i]];
|
|
332
|
+
}
|
|
333
|
+
return array;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function bootEdgeNode(edgeNodeConfig: EdgeNodeConfig) {
|
|
337
|
+
globalThis.BOOTED_EDGE_NODE = edgeNodeConfig;
|
|
338
|
+
const require = (globalThis as any).requireAsync || globalThis.require;
|
|
339
|
+
let host = edgeNodeConfig.host;
|
|
340
|
+
if (!host.endsWith("/")) host += "/";
|
|
341
|
+
for (let entryPath of edgeNodeConfig.entryPaths) {
|
|
342
|
+
try {
|
|
343
|
+
await require("https://" + host + entryPath);
|
|
344
|
+
} catch (e) {
|
|
345
|
+
console.error(e);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { waitForFirstTimeSync } from "socket-function/time/trueTimeShim";
|
|
2
|
+
import { getOwnMachineId } from "../../-a-auth/certs";
|
|
3
|
+
import { getDomain, isPublic } from "../../config";
|
|
4
|
+
import child_process from "child_process";
|
|
5
|
+
import { getAllNodeIds, getOwnNodeId } from "../../-f-node-discovery/NodeDiscovery";
|
|
6
|
+
import { getArchivesBackblazePublic } from "../../-a-archives/archivesBackBlaze";
|
|
7
|
+
import { nestArchives } from "../../-a-archives/archives";
|
|
8
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
9
|
+
import { runInfinitePoll, runInfinitePollCallAtStart } from "socket-function/src/batching";
|
|
10
|
+
import { compare, compareArray, timeInMinute } from "socket-function/src/misc";
|
|
11
|
+
import { cacheLimited, lazy } from "socket-function/src/caching";
|
|
12
|
+
import { canHaveChildren } from "socket-function/src/types";
|
|
13
|
+
import { shutdown } from "../../diagnostics/periodic";
|
|
14
|
+
import { hostArchives } from "../../-b-authorities/cdnAuthority";
|
|
15
|
+
|
|
16
|
+
const UPDATE_POLL_INTERVAL = timeInMinute;
|
|
17
|
+
const DEAD_NODE_COUNT_THRESHOLD = 15;
|
|
18
|
+
|
|
19
|
+
const edgeNodeStorage = nestArchives("edgenodes/", getArchivesBackblazePublic(getDomain()));
|
|
20
|
+
|
|
21
|
+
const getEdgeNodeConfig = cacheLimited(10000, async (nodeId: string) => {
|
|
22
|
+
let fileName = "node_" + nodeId;
|
|
23
|
+
let edgeNodeConfig = await edgeNodeStorage.get(fileName);
|
|
24
|
+
if (!edgeNodeConfig) return undefined;
|
|
25
|
+
return JSON.parse(edgeNodeConfig.toString());
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// NOTE: We update this in the update loop
|
|
29
|
+
const getEdgeNodesIndex = lazy(async (): Promise<EdgeNodesIndex> => {
|
|
30
|
+
await startUpdateLoop();
|
|
31
|
+
let edgeNodesIndex = await edgeNodeStorage.get("edgeNodesIndex.json");
|
|
32
|
+
if (!edgeNodesIndex) return {
|
|
33
|
+
edgeNodes: [],
|
|
34
|
+
};
|
|
35
|
+
return JSON.parse(edgeNodesIndex.toString());
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export type EdgeNodesIndex = {
|
|
39
|
+
edgeNodes: EdgeNodeConfig[];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// IMPORTANT! The EdgeNodeConfig MUST be immutable, otherwise every node has to read every EdgeNodeConfig
|
|
43
|
+
// every poll, which is too much reading.
|
|
44
|
+
export type EdgeNodeConfig = {
|
|
45
|
+
// NOTE: Only information needed for routing should be added here. The rest can be obtained by talking
|
|
46
|
+
// to the node itself (after picking the edgeNode node).
|
|
47
|
+
nodeId: string;
|
|
48
|
+
// (Redundant from nodeId)
|
|
49
|
+
machineId: string;
|
|
50
|
+
// EX: 127-0-0-1.example.com:3334
|
|
51
|
+
host: string;
|
|
52
|
+
gitHash: string;
|
|
53
|
+
bootTime: number;
|
|
54
|
+
entryPaths: string[];
|
|
55
|
+
public: boolean;
|
|
56
|
+
};
|
|
57
|
+
let registeredEdgeNode: EdgeNodeConfig | boolean | undefined;
|
|
58
|
+
export async function registerEdgeNode(config: {
|
|
59
|
+
host: string;
|
|
60
|
+
entryPaths: string[];
|
|
61
|
+
}) {
|
|
62
|
+
if (!SocketFunction.isMounted()) {
|
|
63
|
+
throw new Error("registerEdgeNode must be called after SocketFunction.mount");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (registeredEdgeNode) {
|
|
67
|
+
throw new Error(`registerEdgeNode already called. Should only host 1 edgeNode per node. Previously registered with ${JSON.stringify(registeredEdgeNode)}, new call used ${JSON.stringify(config)}`);
|
|
68
|
+
}
|
|
69
|
+
registeredEdgeNode = true;
|
|
70
|
+
|
|
71
|
+
await waitForFirstTimeSync();
|
|
72
|
+
|
|
73
|
+
let gitHash = child_process.execSync("git rev-parse HEAD").toString().trim();
|
|
74
|
+
let nodeId = getOwnNodeId();
|
|
75
|
+
let machineId = getOwnMachineId();
|
|
76
|
+
|
|
77
|
+
let edgeNodeConfig: EdgeNodeConfig = {
|
|
78
|
+
nodeId,
|
|
79
|
+
machineId,
|
|
80
|
+
host: config.host,
|
|
81
|
+
gitHash,
|
|
82
|
+
bootTime: Date.now(),
|
|
83
|
+
entryPaths: config.entryPaths,
|
|
84
|
+
public: isPublic(),
|
|
85
|
+
};
|
|
86
|
+
registeredEdgeNode = edgeNodeConfig;
|
|
87
|
+
|
|
88
|
+
await edgeNodeStorage.set("node_" + nodeId, Buffer.from(JSON.stringify(edgeNodeConfig)));
|
|
89
|
+
|
|
90
|
+
await startUpdateLoop();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const getEdgeNodeConfigURL = lazy(async () => {
|
|
94
|
+
let { getURL } = await hostArchives({
|
|
95
|
+
archives: edgeNodeStorage,
|
|
96
|
+
subdomain: `edge`,
|
|
97
|
+
domain: getDomain(),
|
|
98
|
+
});
|
|
99
|
+
return await getURL("edge-nodes-index.json");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const startUpdateLoop = lazy(async () => {
|
|
103
|
+
await getEdgeNodeConfigURL();
|
|
104
|
+
await runInfinitePollCallAtStart(UPDATE_POLL_INTERVAL, updateLoop);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
async function updateLoop() {
|
|
108
|
+
await runEdgeNodesAliveCheck();
|
|
109
|
+
await updateEdgeNodesFile();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let deadCounts = new Map<string, number>();
|
|
113
|
+
async function runEdgeNodesAliveCheck() {
|
|
114
|
+
let edgeNodeNodeIds = await edgeNodeStorage.find("node_", { type: "files" });
|
|
115
|
+
let nodeIds = new Set(await getAllNodeIds());
|
|
116
|
+
for (let fileName of edgeNodeNodeIds) {
|
|
117
|
+
let nodeId = fileName.slice("node_".length);
|
|
118
|
+
if (!nodeIds.has(nodeId)) {
|
|
119
|
+
deadCounts.set(fileName, (deadCounts.get(fileName) ?? 0) + 1);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (let [fileName, count] of Array.from(deadCounts)) {
|
|
124
|
+
if (count < DEAD_NODE_COUNT_THRESHOLD) continue;
|
|
125
|
+
console.log(`Node is dead. Removing from edgeNodes.`, { fileName, count });
|
|
126
|
+
await edgeNodeStorage.del(fileName);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function updateEdgeNodesFile() {
|
|
130
|
+
let prevEdgeNodeFile = await edgeNodeStorage.get("edgeNodesIndex.json");
|
|
131
|
+
let prevEdgeNodeIndex: EdgeNodesIndex = {
|
|
132
|
+
edgeNodes: [],
|
|
133
|
+
};
|
|
134
|
+
try {
|
|
135
|
+
if (prevEdgeNodeFile) {
|
|
136
|
+
prevEdgeNodeIndex = JSON.parse(prevEdgeNodeFile.toString());
|
|
137
|
+
}
|
|
138
|
+
} catch (e) {
|
|
139
|
+
console.error("Failed to parse edgeNodesIndex.json", { error: e, prevEdgeNodeFile });
|
|
140
|
+
}
|
|
141
|
+
getEdgeNodesIndex.set(Promise.resolve(prevEdgeNodeIndex));
|
|
142
|
+
|
|
143
|
+
let edgeNodeNodeIds = await edgeNodeStorage.find("node_", { type: "files" });
|
|
144
|
+
edgeNodeNodeIds = edgeNodeNodeIds.map(x => x.slice("node_".length));
|
|
145
|
+
|
|
146
|
+
if (!edgeNodeNodeIds.includes(getOwnNodeId())) {
|
|
147
|
+
console.log(`Our node was removed as a edgeNode. This is a fatal error. Terminating now.`);
|
|
148
|
+
await shutdown();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let diff = !!compareArray(edgeNodeNodeIds.sort(), prevEdgeNodeIndex.edgeNodes.map(x => x.nodeId).sort());
|
|
152
|
+
if (!diff) return;
|
|
153
|
+
|
|
154
|
+
console.log("Detected edgeNodes changed. Updating edgeNodesIndex.json", { edgeNodeNodeIds });
|
|
155
|
+
|
|
156
|
+
let newEdgeNodeIndex: EdgeNodesIndex = {
|
|
157
|
+
edgeNodes: [],
|
|
158
|
+
};
|
|
159
|
+
for (let nodeId of edgeNodeNodeIds) {
|
|
160
|
+
let edgeNodeConfig = await getEdgeNodeConfig(nodeId);
|
|
161
|
+
if (!edgeNodeConfig) continue;
|
|
162
|
+
newEdgeNodeIndex.edgeNodes.push(edgeNodeConfig);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
await edgeNodeStorage.set("edge-nodes-index.json", Buffer.from(JSON.stringify(newEdgeNodeIndex)));
|
|
166
|
+
}
|