querysub 0.526.0 → 0.528.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/.claude/settings.local.json +2 -1
- package/appSecrets.ts +23 -0
- package/bin/storageserve.js +4 -0
- package/package.json +4 -3
- package/src/-b-authorities/cdnAuthority.ts +1 -1
- package/src/-b-authorities/cloudflareHelpers.ts +2 -45
- package/src/-b-authorities/dnsAuthority.ts +44 -185
- package/src/-c-identity/IdentityController.ts +6 -4
- package/src/-d-trust/NetworkTrust2.ts +6 -1
- package/src/-f-node-discovery/NodeDiscovery.ts +1 -1
- package/src/config.ts +5 -0
- package/src/diagnostics/managementPages.tsx +0 -6
- package/src/diagnostics/misc-pages/RoutingTablePage.tsx +3 -3
- package/src/library-components/EdgeNodeSelector.tsx +2 -1
- package/src/library-components/LatencyGraph.tsx +27 -8
- package/src/storageSetup.ts +27 -0
- package/test.ts +2 -2
- package/src/diagnostics/misc-pages/DNSPage.tsx +0 -354
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"WebSearch",
|
|
30
30
|
"WebFetch(domain:blog.cloudflare.com)",
|
|
31
31
|
"WebFetch(domain:developers.cloudflare.com)",
|
|
32
|
-
"PowerShell(yarn type *)"
|
|
32
|
+
"PowerShell(yarn type *)",
|
|
33
|
+
"PowerShell(Remove-Item -Confirm:$false \"C:\\\\Users\\\\quent\\\\.claude\\\\projects\\\\D--repos-querysub\\\\memory\\\\no-naming-new-bins.md\")"
|
|
33
34
|
]
|
|
34
35
|
}
|
|
35
36
|
}
|
package/appSecrets.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { getBackblazePath } from "./src/-a-archives/archivesBackBlaze";
|
|
3
|
+
import { getCloudflareCreds } from "./src/-b-authorities/cloudflareHelpers";
|
|
4
|
+
import { Querysub } from "./src/4-querysub/Querysub";
|
|
5
|
+
|
|
6
|
+
/** Serves the secret keys sliftutils' getSecret expects, reading them the way querysub already does: backblaze creds off disk (getBackblazePath), cloudflare creds from the keys archives bucket (with its local file fallback). */
|
|
7
|
+
export async function getAppSecret(key: string): Promise<string | undefined> {
|
|
8
|
+
// Use this so some of our default imports get imported
|
|
9
|
+
Querysub;
|
|
10
|
+
if (key.startsWith("backblaze.json.")) {
|
|
11
|
+
let creds = JSON.parse(fs.readFileSync(getBackblazePath(), "utf8")) as { applicationKeyId?: string; applicationKey?: string };
|
|
12
|
+
if (key === "backblaze.json.applicationKeyId") return creds.applicationKeyId;
|
|
13
|
+
if (key === "backblaze.json.applicationKey") return creds.applicationKey;
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
if (key.startsWith("cloudflare.json.")) {
|
|
17
|
+
let creds = await getCloudflareCreds();
|
|
18
|
+
if (key === "cloudflare.json.key") return creds.key;
|
|
19
|
+
if (key === "cloudflare.json.email") return creds.email;
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "querysub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.528.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",
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"audit-imports": "./bin/audit-imports.js",
|
|
50
50
|
"audit-disk-values": "./bin/audit-disk-values.js",
|
|
51
51
|
"mcp-indexed-logs": "./bin/mcp-indexed-logs.js",
|
|
52
|
-
"autofix": "./bin/autofix.js"
|
|
52
|
+
"autofix": "./bin/autofix.js",
|
|
53
|
+
"storageserve": "./bin/storageserve.js"
|
|
53
54
|
},
|
|
54
55
|
"dependencies": {
|
|
55
56
|
"@types/fs-ext": "^2.0.3",
|
|
@@ -70,7 +71,7 @@
|
|
|
70
71
|
"pako": "^2.1.0",
|
|
71
72
|
"peggy": "^5.0.6",
|
|
72
73
|
"sliftutils": "^1.7.5",
|
|
73
|
-
"socket-function": "^1.2.
|
|
74
|
+
"socket-function": "^1.2.16",
|
|
74
75
|
"terser": "^5.31.0",
|
|
75
76
|
"typenode": "^6.6.1",
|
|
76
77
|
"typesafecss": "^0.32.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Archives } from "../-a-archives/archives";
|
|
2
2
|
import { ArchivesBackblaze } from "../-a-archives/archivesBackBlaze";
|
|
3
|
-
import { cloudflareGETCall, cloudflarePOSTCall } from "
|
|
3
|
+
import { cloudflareGETCall, cloudflarePOSTCall } from "sliftutils/misc/https/cloudflareHelpers";
|
|
4
4
|
import { setRecord, getZoneId } from "./dnsAuthority";
|
|
5
5
|
import debugbreak from "debugbreak";
|
|
6
6
|
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { httpsRequest } from "socket-function/src/https";
|
|
2
1
|
import { getArchives } from "../-a-archives/archives";
|
|
3
2
|
import { lazy } from "socket-function/src/caching";
|
|
4
|
-
import { getStorageDir
|
|
3
|
+
import { getStorageDir } from "../fs";
|
|
5
4
|
import fs from "fs";
|
|
6
|
-
import { isNodeTrue } from "socket-function/src/misc";
|
|
7
5
|
|
|
8
|
-
|
|
6
|
+
const keys = lazy(() => getArchives("keys"));
|
|
9
7
|
|
|
10
8
|
export const getCloudflareCreds = lazy(async (): Promise<{ key: string; email: string }> => {
|
|
11
9
|
let credsJSON = await keys().get("cloudflare.json");
|
|
@@ -20,44 +18,3 @@ export const getCloudflareCreds = lazy(async (): Promise<{ key: string; email: s
|
|
|
20
18
|
}
|
|
21
19
|
return JSON.parse(credsJSON.toString());
|
|
22
20
|
});
|
|
23
|
-
|
|
24
|
-
export async function cloudflareGETCall<T>(path: string, params?: { [key: string]: string }): Promise<T> {
|
|
25
|
-
let url = new URL(`https://api.cloudflare.com/client/v4` + path);
|
|
26
|
-
for (let key in params) {
|
|
27
|
-
url.searchParams.set(key, params[key]);
|
|
28
|
-
}
|
|
29
|
-
let creds = await getCloudflareCreds();
|
|
30
|
-
let result = await httpsRequest(url.toString(), [], "GET", undefined, {
|
|
31
|
-
headers: {
|
|
32
|
-
"Content-Type": "application/json",
|
|
33
|
-
"X-Auth-Email": creds.email,
|
|
34
|
-
"X-Auth-User-Service-Key": creds.key,
|
|
35
|
-
"X-Auth-Key": creds.key,
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
let result2 = JSON.parse(result.toString()) as { result: unknown; success: boolean; errors: { code: number; message: string }[] };
|
|
39
|
-
if (!result2.success) {
|
|
40
|
-
throw new Error(`Cloudflare call failed: ${result2.errors.map(x => x.message).join(", ")}`);
|
|
41
|
-
}
|
|
42
|
-
return result2.result as T;
|
|
43
|
-
}
|
|
44
|
-
export async function cloudflarePOSTCall<T>(path: string, params: { [key: string]: unknown }): Promise<T> {
|
|
45
|
-
return await cloudflareCall(path, Buffer.from(JSON.stringify(params)), "POST");
|
|
46
|
-
}
|
|
47
|
-
export async function cloudflareCall<T>(path: string, payload: Buffer, method: string): Promise<T> {
|
|
48
|
-
let url = new URL(`https://api.cloudflare.com/client/v4` + path);
|
|
49
|
-
let creds = await getCloudflareCreds();
|
|
50
|
-
let result = await httpsRequest(url.toString(), payload, method, undefined, {
|
|
51
|
-
headers: {
|
|
52
|
-
"Content-Type": "application/json",
|
|
53
|
-
"X-Auth-Email": creds.email,
|
|
54
|
-
"X-Auth-User-Service-Key": creds.key,
|
|
55
|
-
"X-Auth-Key": creds.key,
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
let result2 = JSON.parse(result.toString()) as { result: unknown; success: boolean; errors: { code: number; message: string }[] };
|
|
59
|
-
if (!result2.success) {
|
|
60
|
-
throw new Error(`Cloudflare call failed: ${result2.errors.map(x => x.message).join(", ")}`);
|
|
61
|
-
}
|
|
62
|
-
return result2.result as T;
|
|
63
|
-
}
|
|
@@ -1,185 +1,44 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
export
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
await getCloudflareCreds();
|
|
47
|
-
return true;
|
|
48
|
-
} catch {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
export const getZoneId = cache(async (rootDomain: string): Promise<string> => {
|
|
54
|
-
let zones = await cloudflareGETCall<{ id: string; name: string }[]>("/zones", {});
|
|
55
|
-
let selected = zones.find(x => x.name === rootDomain);
|
|
56
|
-
if (!selected) {
|
|
57
|
-
throw new Error(`Could not find zone for ${rootDomain}. Found ${zones.map(x => x.name).join(", ")}`);
|
|
58
|
-
}
|
|
59
|
-
return selected.id;
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
function getRootDomain(key: string) {
|
|
63
|
-
if (key.endsWith(".")) {
|
|
64
|
-
key = key.slice(0, -1);
|
|
65
|
-
}
|
|
66
|
-
return key.split(".").slice(-2).join(".");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export async function getRecordsRaw(type: string, key: string) {
|
|
70
|
-
if (key.endsWith(".")) key = key.slice(0, -1);
|
|
71
|
-
let zoneId = await getZoneId(getRootDomain(key));
|
|
72
|
-
let results = await cloudflareGETCall<{
|
|
73
|
-
id: string;
|
|
74
|
-
type: string;
|
|
75
|
-
name: string;
|
|
76
|
-
content: string;
|
|
77
|
-
proxied: boolean;
|
|
78
|
-
modified_on: string;
|
|
79
|
-
// Omitted by Cloudflare when the record has no comment.
|
|
80
|
-
comment?: string;
|
|
81
|
-
}[]>(`/zones/${zoneId}/dns_records`);
|
|
82
|
-
// DNS names are case-insensitive and Cloudflare returns them lowercased, so a mixed-case key
|
|
83
|
-
// (e.g. a machine id subdomain) would never match an exact-case compare - match lowercased.
|
|
84
|
-
let keyLower = key.toLowerCase();
|
|
85
|
-
return results.filter(x => x.type === type && x.name.toLowerCase() === keyLower);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Cloudflare's batch endpoint applies deletes, then patches, then posts in a single database
|
|
89
|
-
* transaction. We route edits (patches) through here because the standalone PATCH/PUT verbs
|
|
90
|
-
* aren't usable in our setup, and because it lets "remove others + assert target" happen
|
|
91
|
-
* without a window where the name resolves to nothing. */
|
|
92
|
-
export async function batchRecords(zoneId: string, batch: {
|
|
93
|
-
deletes?: { id: string }[];
|
|
94
|
-
patches?: { id: string; comment?: string }[];
|
|
95
|
-
posts?: { type: string; name: string; content: string; ttl: number; proxied: boolean; comment?: string }[];
|
|
96
|
-
}) {
|
|
97
|
-
let payload: { [key: string]: unknown } = {};
|
|
98
|
-
if (batch.deletes && batch.deletes.length > 0) payload.deletes = batch.deletes;
|
|
99
|
-
if (batch.patches && batch.patches.length > 0) payload.patches = batch.patches;
|
|
100
|
-
if (batch.posts && batch.posts.length > 0) payload.posts = batch.posts;
|
|
101
|
-
try {
|
|
102
|
-
await cloudflarePOSTCall(`/zones/${zoneId}/dns_records/batch`, payload);
|
|
103
|
-
} catch (error) {
|
|
104
|
-
console.error(`Error updating DNS records:`, { error: error, batch });
|
|
105
|
-
throw new Error(`Error updating DNS records. ${JSON.stringify(batch)}. Error: ${error}`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
export async function getRecords(type: string, key: string) {
|
|
109
|
-
if (key.endsWith(".")) key = key.slice(0, -1);
|
|
110
|
-
let raw = await getRecordsRaw(type, key);
|
|
111
|
-
return raw.map(x => x.content);
|
|
112
|
-
}
|
|
113
|
-
export async function deleteRecord(type: string, key: string, value: string) {
|
|
114
|
-
if (key.endsWith(".")) key = key.slice(0, -1);
|
|
115
|
-
let zoneId = await getZoneId(getRootDomain(key));
|
|
116
|
-
let prevValues = await getRecordsRaw(type, key);
|
|
117
|
-
prevValues = prevValues.filter(x => x.content === value);
|
|
118
|
-
if (prevValues.length === 0) {
|
|
119
|
-
if (!SocketFunction.silent) {
|
|
120
|
-
console.log(`No need to delete record, it was not found. ${JSON.stringify(value)} value was not in ${type} for ${key}, values ${JSON.stringify(prevValues.map(x => x.content))}`);
|
|
121
|
-
}
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
console.log(`Removing records of ${type} for ${key}, values ${JSON.stringify(prevValues.map(x => x.content))}`);
|
|
126
|
-
for (let value of prevValues) {
|
|
127
|
-
await cloudflareCall(`/zones/${zoneId}/dns_records/${value.id}`, Buffer.from([]), "DELETE");
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
/** Removes all existing records (unless the record is already present and fresh) */
|
|
131
|
-
export async function setRecord(type: string, key: string, value: string, proxied?: "proxied", staleAfter = DNS_REFRESH_STALE_AFTER) {
|
|
132
|
-
if (key.endsWith(".")) key = key.slice(0, -1);
|
|
133
|
-
let zoneId = await getZoneId(getRootDomain(key));
|
|
134
|
-
let prevValues = await getRecordsRaw(type, key);
|
|
135
|
-
let existing = prevValues.find(x => x.content === value);
|
|
136
|
-
let others = prevValues.filter(x => x.content !== value);
|
|
137
|
-
|
|
138
|
-
// Already correct and recently asserted - a prior run also cleaned up the other records,
|
|
139
|
-
// so there is nothing left to do.
|
|
140
|
-
if (existing && Date.now() - freshnessTime(existing.comment) < staleAfter) return;
|
|
141
|
-
|
|
142
|
-
// A single atomic batch: drop the wrong records, and either refresh the existing record's
|
|
143
|
-
// comment in place or create it - so the name is never left resolving to nothing.
|
|
144
|
-
let ttl = DNS_TTLSeconds[type as "A"] || 60;
|
|
145
|
-
let comment = stampFreshness(existing?.comment);
|
|
146
|
-
console.log(magenta(`Setting ${type} record for ${key} to ${value} (previously had ${JSON.stringify(prevValues.map(x => x.content))})`));
|
|
147
|
-
await batchRecords(zoneId, {
|
|
148
|
-
deletes: others.map(x => ({ id: x.id })),
|
|
149
|
-
patches: existing ? [{ id: existing.id, comment }] : [],
|
|
150
|
-
posts: existing ? [] : [{ type, name: key, content: value, ttl, proxied: proxied === "proxied", comment }],
|
|
151
|
-
});
|
|
152
|
-
// Only a brand new record needs to propagate; an in-place comment refresh doesn't change the answer.
|
|
153
|
-
if (!existing) {
|
|
154
|
-
console.log(`Waiting ${ttl} seconds for DNS to propagate...`);
|
|
155
|
-
for (let ttlLeft = ttl; ttlLeft > 0; ttlLeft--) {
|
|
156
|
-
await delay(1000);
|
|
157
|
-
console.log(`${ttlLeft} seconds left...`);
|
|
158
|
-
}
|
|
159
|
-
console.log(`Done waiting for DNS to update.`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
/** Keeps existing records */
|
|
163
|
-
export async function addRecord(type: string, key: string, value: string, proxied?: "proxied", staleAfter = DNS_REFRESH_STALE_AFTER) {
|
|
164
|
-
if (key.endsWith(".")) key = key.slice(0, -1);
|
|
165
|
-
let zoneId = await getZoneId(getRootDomain(key));
|
|
166
|
-
let prevValues = await getRecordsRaw(type, key);
|
|
167
|
-
let existing = prevValues.find(x => x.content === value);
|
|
168
|
-
if (existing && Date.now() - freshnessTime(existing.comment) < staleAfter) return;
|
|
169
|
-
|
|
170
|
-
// Same single-batch flow as setRecord, minus the deletes (we keep sibling records here).
|
|
171
|
-
let ttl = DNS_TTLSeconds[type as "A"] || 60;
|
|
172
|
-
let comment = stampFreshness(existing?.comment);
|
|
173
|
-
console.log(`Adding ${type} record for ${key} to ${value} (previously had ${JSON.stringify(prevValues.map(x => x.content))})`);
|
|
174
|
-
await batchRecords(zoneId, {
|
|
175
|
-
patches: existing ? [{ id: existing.id, comment }] : [],
|
|
176
|
-
posts: existing ? [] : [{ type, name: key, content: value, ttl, proxied: proxied === "proxied", comment }],
|
|
177
|
-
});
|
|
178
|
-
if (existing) return;
|
|
179
|
-
console.log(`Waiting ${ttl} seconds for DNS to propagate...`);
|
|
180
|
-
for (let ttlLeft = ttl; ttlLeft > 0; ttlLeft--) {
|
|
181
|
-
await delay(1000);
|
|
182
|
-
console.log(`${ttlLeft} seconds left...`);
|
|
183
|
-
}
|
|
184
|
-
console.log(`Done waiting for DNS to update.`);
|
|
185
|
-
}
|
|
1
|
+
import { lazy } from "socket-function/src/caching";
|
|
2
|
+
import { isNode } from "socket-function/src/misc";
|
|
3
|
+
import { isClient } from "../config2";
|
|
4
|
+
import {
|
|
5
|
+
addRecord as addRecordBase,
|
|
6
|
+
deleteRecord as deleteRecordBase,
|
|
7
|
+
freshnessTime as freshnessTimeBase,
|
|
8
|
+
getRecords as getRecordsBase,
|
|
9
|
+
getZoneId as getZoneIdBase,
|
|
10
|
+
hasDNSWritePermissions as hasDNSWritePermissionsBase,
|
|
11
|
+
setRecord as setRecordBase,
|
|
12
|
+
} from "sliftutils/misc/https/dns";
|
|
13
|
+
|
|
14
|
+
export function freshnessTime(comment?: string): number {
|
|
15
|
+
return freshnessTimeBase(comment);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const hasDNSWritePermissions = lazy(async () => {
|
|
19
|
+
if (!isNode()) return false;
|
|
20
|
+
if (isClient()) return false;
|
|
21
|
+
return await hasDNSWritePermissionsBase();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export async function getZoneId(rootDomain: string): Promise<string> {
|
|
25
|
+
return await getZoneIdBase(rootDomain);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function getRecords(type: string, key: string): Promise<string[]> {
|
|
29
|
+
return await getRecordsBase(type, key);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function deleteRecord(type: string, key: string, value: string): Promise<void> {
|
|
33
|
+
return await deleteRecordBase(type, key, value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Removes all existing records (unless the record is already present and fresh) */
|
|
37
|
+
export async function setRecord(type: string, key: string, value: string, proxied?: "proxied", staleAfter?: number): Promise<void> {
|
|
38
|
+
return await setRecordBase(type, key, value, proxied, staleAfter);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Keeps existing records */
|
|
42
|
+
export async function addRecord(type: string, key: string, value: string, proxied?: "proxied", staleAfter?: number): Promise<void> {
|
|
43
|
+
return await addRecordBase(type, key, value, proxied, staleAfter);
|
|
44
|
+
}
|
|
@@ -197,7 +197,7 @@ class IdentityControllerBase {
|
|
|
197
197
|
});
|
|
198
198
|
|
|
199
199
|
let duration = Date.now() - time;
|
|
200
|
-
console.
|
|
200
|
+
console.info(`Authenticated identity for ${caller.nodeId} in ${formatTime(duration)}, at ${Date.now()}`, {
|
|
201
201
|
clientId: caller.nodeId,
|
|
202
202
|
reconnectNodeId,
|
|
203
203
|
duration,
|
|
@@ -208,7 +208,7 @@ class IdentityControllerBase {
|
|
|
208
208
|
SocketFunction.onNextDisconnect(caller.nodeId, () => {
|
|
209
209
|
// NOTE: I don't really see any purpose of deleting from caller info. I don't think we're going to run out of memory because of too many callers authenticating.
|
|
210
210
|
// However, logging here is useful as it allows us to complete the life cycle so we know how long a client was connected for.
|
|
211
|
-
console.
|
|
211
|
+
console.info(`Disconnected client`, {
|
|
212
212
|
clientId: caller.nodeId,
|
|
213
213
|
});
|
|
214
214
|
});
|
|
@@ -251,6 +251,7 @@ const changeIdentityOnce = cacheWeak(async function changeIdentityOnce(connectio
|
|
|
251
251
|
() => new Error(`Timeout calling changeIdentity for ${nodeId}`)
|
|
252
252
|
);
|
|
253
253
|
});
|
|
254
|
+
let startupTime = Date.now();
|
|
254
255
|
SocketFunction.addGlobalClientHook(async function identityHook(context) {
|
|
255
256
|
if (context.call.classGuid === IdentityController._classGuid) return;
|
|
256
257
|
// This is for US to tell them our identity. And if they established the connection the identity will come from their original connection url (that they used to connect to us), and they validated it either being a real certificate, or they added the cert from the trusted backblaze bucket. If it just from a real certificate it means we identified them, but they might not have network trust. But that's fine, as IdentityController is JUST for identification, and if it's a real certificate we know who they are! (Which doesn't mean we trust them).
|
|
@@ -259,8 +260,9 @@ SocketFunction.addGlobalClientHook(async function identityHook(context) {
|
|
|
259
260
|
}
|
|
260
261
|
let time = Date.now();
|
|
261
262
|
await changeIdentityOnce(context.connectionId);
|
|
262
|
-
let
|
|
263
|
-
|
|
263
|
+
let now = Date.now();
|
|
264
|
+
let duration = now - time;
|
|
265
|
+
if (duration > 200 && now - startupTime > timeInMinute) {
|
|
264
266
|
console.log(red(`IdentityHook took ${formatTime(duration)} for ${context.connectionId.nodeId} ${context.call.classGuid}.${context.call.functionName}`));
|
|
265
267
|
}
|
|
266
268
|
});
|
|
@@ -104,6 +104,11 @@ let populateTrustedCache = lazy(async () => {
|
|
|
104
104
|
}, TRUSTED_CACHE_RESET_INTERVAL);
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
export async function getTrustedMachineIds() {
|
|
108
|
+
await populateTrustedCache();
|
|
109
|
+
return Array.from(trustedCache);
|
|
110
|
+
}
|
|
111
|
+
|
|
107
112
|
export async function isNodeTrusted(nodeId: string) {
|
|
108
113
|
let domainName = getNodeIdDomainMaybeUndefined(nodeId);
|
|
109
114
|
if (!domainName) return false;
|
|
@@ -119,7 +124,7 @@ export const loadServerCert = cache(async (machineId: string) => {
|
|
|
119
124
|
console.warn(`Could not find certificate in archives for ${machineId}`);
|
|
120
125
|
return;
|
|
121
126
|
}
|
|
122
|
-
console.
|
|
127
|
+
console.info(magenta(`Loading certificate for ${machineId}`));
|
|
123
128
|
trustCertificate(certFile);
|
|
124
129
|
});
|
|
125
130
|
|
|
@@ -216,7 +216,7 @@ async function setNodeIds(nodeIds: string[]) {
|
|
|
216
216
|
if (newNodeIds.length === 0 && removedNodeIds.length === 0) return;
|
|
217
217
|
if (logging) {
|
|
218
218
|
for (let nodeId of newNodeIds) {
|
|
219
|
-
console.
|
|
219
|
+
console.info(blue(`Discovered node ${nodeId}`));
|
|
220
220
|
}
|
|
221
221
|
for (let nodeId of removedNodeIds) {
|
|
222
222
|
console.log(red(`Removed node from setNodeIds ${nodeId}`));
|
package/src/config.ts
CHANGED
|
@@ -35,6 +35,7 @@ let yargObj = parseArgsFactory()
|
|
|
35
35
|
.option("slowdown", { type: "number", desc: "Delay all input data values by this amount of time, pretending like we didn't even receive it until this time is up." })
|
|
36
36
|
.option("network", { type: "array", desc: `The networks this node is on (pass multiple arguments to be on multiple, ex: --network test --network default). Authorities only satisfy paths on their networks ("default" if unset). Function calls are put on the first network in the list. If no FunctionRunner is on a call's network, the call will fail to run.` })
|
|
37
37
|
.option("networkfile", { type: "string", desc: `The same as --network, except the networks are read from the given file (one per line). Supports "~/" for the home directory. If the file doesn't exist, the process exits with an error.` })
|
|
38
|
+
.option("port", { type: "number", desc: "The storage port for `yarn storageserve`" })
|
|
38
39
|
.argv
|
|
39
40
|
;
|
|
40
41
|
|
|
@@ -51,6 +52,10 @@ export function expandHomePath(path: string): string {
|
|
|
51
52
|
return path;
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
export function getPort(): number | undefined {
|
|
56
|
+
return yargObj.port;
|
|
57
|
+
}
|
|
58
|
+
|
|
54
59
|
let networkFileNetworks = lazy((): string[] => {
|
|
55
60
|
if (!yargObj.networkfile) return [];
|
|
56
61
|
let path = expandHomePath(String(yargObj.networkfile));
|
|
@@ -128,12 +128,6 @@ export async function registerManagementPages2(config: {
|
|
|
128
128
|
controllerName: "SnapshotViewerController",
|
|
129
129
|
getModule: () => import("./misc-pages/SnapshotViewer"),
|
|
130
130
|
});
|
|
131
|
-
inputPages.push({
|
|
132
|
-
title: "DNS",
|
|
133
|
-
componentName: "DNSPage",
|
|
134
|
-
controllerName: "DNSPageController",
|
|
135
|
-
getModule: () => import("./misc-pages/DNSPage"),
|
|
136
|
-
});
|
|
137
131
|
inputPages.push({
|
|
138
132
|
title: "Fnc Capture",
|
|
139
133
|
componentName: "FunctionCapturePage",
|
|
@@ -36,7 +36,7 @@ const MACHINE_LATENCY_WIDTH_PX = 235;
|
|
|
36
36
|
|
|
37
37
|
// Clicking a machine node in the graph (or a row) sorts that machine's nodes to the top of the table.
|
|
38
38
|
const selectedMachineParam = new URLParam("rtSelectedMachine", "", { reset: [mainResets] });
|
|
39
|
-
const trafficWindowParam = new URLParam<number>("rtTrafficWindow", timeInMinute *
|
|
39
|
+
const trafficWindowParam = new URLParam<number>("rtTrafficWindow", timeInMinute * 5, { reset: [mainResets] });
|
|
40
40
|
|
|
41
41
|
// The per-node table columns, in order. `color` tints the function/path-value/querysub columns to match the graph;
|
|
42
42
|
// `perMachine` columns (the IP and machine id) only print on the first row of each machine group.
|
|
@@ -638,7 +638,7 @@ class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: N
|
|
|
638
638
|
return {
|
|
639
639
|
id: machineId,
|
|
640
640
|
labelLines: machineLabelLines({ machineId, threads: allThreads, trafficMaps, ip: machineIp.get(machineId), networks: machineNetworks.get(machineId) || [] }),
|
|
641
|
-
weight: totals.
|
|
641
|
+
weight: totals.valuesSent + totals.valuesReceived,
|
|
642
642
|
};
|
|
643
643
|
});
|
|
644
644
|
let links: LatencyGraphLink[] = [];
|
|
@@ -649,7 +649,7 @@ class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: N
|
|
|
649
649
|
if (pv && (pv.fwd || pv.back)) {
|
|
650
650
|
extraLabel = { text: `↑${formatNumber(pv.fwd)}/s ↓${formatNumber(pv.back)}/s values`, color: PATHVALUE_COLOR };
|
|
651
651
|
}
|
|
652
|
-
links.push({ source, destination, latencyMs, weight: machinePairTraffic.get(key) || 0, extraLabel });
|
|
652
|
+
links.push({ source, destination, latencyMs, weight: machinePairTraffic.get(key) || 0, valueWeight: pv && pv.fwd + pv.back || 0, extraLabel });
|
|
653
653
|
}
|
|
654
654
|
|
|
655
655
|
// One row per thread node, grouped so a machine's threads sit together; the selected machine sorts to the top.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { css } from "typesafecss";
|
|
2
2
|
import { qreact } from "../4-dom/qreact";
|
|
3
3
|
import { Querysub } from "../4-querysub/Querysub";
|
|
4
|
-
import { timeInSecond } from "socket-function/src/misc";
|
|
4
|
+
import { sort, timeInSecond } from "socket-function/src/misc";
|
|
5
5
|
import { formatTime } from "socket-function/src/formatting/format";
|
|
6
6
|
import { isCurrentUserSuperUser } from "../user-implementation/userData";
|
|
7
7
|
import type { EdgeNodeConfig, EdgeNodeStat } from "../4-deploy/edgeNodes";
|
|
@@ -43,6 +43,7 @@ export class EdgeNodeSelector extends qreact.Component<{}> {
|
|
|
43
43
|
nodes = [...nodes, { host: booted.host, nodeId: booted.nodeId, public: booted.public, live: true, finished: false }];
|
|
44
44
|
}
|
|
45
45
|
let autoStat = nodes.find(x => x.host === stats?.autoPickedHost);
|
|
46
|
+
sort(nodes, x => x.latency);
|
|
46
47
|
|
|
47
48
|
return <div className={css.hbox(6)}>
|
|
48
49
|
<span className={css.opacity(0.7)}>Edge</span>
|
|
@@ -9,7 +9,7 @@ import { formatTime } from "socket-function/src/formatting/format";
|
|
|
9
9
|
|
|
10
10
|
export type LatencyGraphLabelLine = { text: string; color?: string; };
|
|
11
11
|
export type LatencyGraphNode = { id: string; label?: string; latitude?: number; longitude?: number; labelLines?: LatencyGraphLabelLine[]; weight?: number; };
|
|
12
|
-
export type LatencyGraphLink = { source: string; destination: string; latencyMs: number; weight?: number; extraLabel?: LatencyGraphLabelLine; };
|
|
12
|
+
export type LatencyGraphLink = { source: string; destination: string; latencyMs: number; weight?: number; valueWeight?: number; extraLabel?: LatencyGraphLabelLine; };
|
|
13
13
|
export type LatencyGraphProps = {
|
|
14
14
|
nodes: LatencyGraphNode[];
|
|
15
15
|
links: LatencyGraphLink[];
|
|
@@ -59,7 +59,7 @@ const HIGHLIGHT_COLOR = "hsl(40, 90%, 60%)";
|
|
|
59
59
|
const NODE_WEIGHT_MULT = 2.5;
|
|
60
60
|
const LINE_BASE_WIDTH = 1.5;
|
|
61
61
|
const LINE_MIN_WIDTH = 1;
|
|
62
|
-
const LINE_MAX_WIDTH =
|
|
62
|
+
const LINE_MAX_WIDTH = 35;
|
|
63
63
|
const LABEL_LINE_HEIGHT = 13;
|
|
64
64
|
const ZOOM_STEP = 1.1;
|
|
65
65
|
const MIN_SCALE = 0.05;
|
|
@@ -84,6 +84,8 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
84
84
|
maxNodeWeight = 0;
|
|
85
85
|
pairWeight = new Map<number, number>();
|
|
86
86
|
maxPairWeight = 0;
|
|
87
|
+
pairValueWeight = new Map<number, number>();
|
|
88
|
+
maxPairValueWeight = 0;
|
|
87
89
|
pairExtraLabel = new Map<number, LatencyGraphLabelLine>();
|
|
88
90
|
// Cached from props in render() so draw() (a rAF callback, not synced) never reads this.props.
|
|
89
91
|
formatWeight: ((weight: number) => string) | undefined = undefined;
|
|
@@ -156,15 +158,23 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
156
158
|
}
|
|
157
159
|
this.pairWeight = new Map();
|
|
158
160
|
this.maxPairWeight = 0;
|
|
161
|
+
this.pairValueWeight = new Map();
|
|
162
|
+
this.maxPairValueWeight = 0;
|
|
159
163
|
for (let link of this.props.links) {
|
|
160
|
-
if (!link.weight) continue;
|
|
161
164
|
let a = index.get(link.source);
|
|
162
165
|
let b = index.get(link.destination);
|
|
163
166
|
if (a === undefined || b === undefined || a === b) continue;
|
|
164
167
|
let pk = Math.min(a, b) * n + Math.max(a, b);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
+
if (link.weight) {
|
|
169
|
+
let combined = (this.pairWeight.get(pk) || 0) + link.weight;
|
|
170
|
+
this.pairWeight.set(pk, combined);
|
|
171
|
+
this.maxPairWeight = Math.max(this.maxPairWeight, combined);
|
|
172
|
+
}
|
|
173
|
+
if (link.valueWeight) {
|
|
174
|
+
let combined = (this.pairValueWeight.get(pk) || 0) + link.valueWeight;
|
|
175
|
+
this.pairValueWeight.set(pk, combined);
|
|
176
|
+
this.maxPairValueWeight = Math.max(this.maxPairValueWeight, combined);
|
|
177
|
+
}
|
|
168
178
|
}
|
|
169
179
|
}
|
|
170
180
|
|
|
@@ -554,10 +564,19 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
554
564
|
this.drawHover(ctx, toScreenX, toScreenY);
|
|
555
565
|
}
|
|
556
566
|
|
|
567
|
+
// Opacity encodes the values sent back and forth on the connection (normalized to the busiest pair). Latency-based opacity is only the fallback when no link has value data.
|
|
568
|
+
edgeOpacity(a: number, b: number, latency: number) {
|
|
569
|
+
if (this.maxPairValueWeight <= 0) {
|
|
570
|
+
return this.opacityFor(latency, this.renderMin, this.renderMax);
|
|
571
|
+
}
|
|
572
|
+
let valueWeight = this.pairValueWeight.get(Math.min(a, b) * this.nodes.length + Math.max(a, b)) || 0;
|
|
573
|
+
return Math.max(MIN_OPACITY, valueWeight / this.maxPairValueWeight);
|
|
574
|
+
}
|
|
575
|
+
|
|
557
576
|
drawEdges(ctx: CanvasRenderingContext2D, toScreenX: (wx: number) => number, toScreenY: (wy: number) => number) {
|
|
558
577
|
for (let edge of this.renderEdges) {
|
|
559
578
|
ctx.lineWidth = this.lineWidthFor(edge.a, edge.b);
|
|
560
|
-
ctx.strokeStyle = `hsla(${NODE_HUE}, 70%, 62%, ${this.
|
|
579
|
+
ctx.strokeStyle = `hsla(${NODE_HUE}, 70%, 62%, ${this.edgeOpacity(edge.a, edge.b, edge.latency)})`;
|
|
561
580
|
ctx.beginPath();
|
|
562
581
|
ctx.moveTo(toScreenX(this.positionsX[edge.a]), toScreenY(this.positionsY[edge.a]));
|
|
563
582
|
ctx.lineTo(toScreenX(this.positionsX[edge.b]), toScreenY(this.positionsY[edge.b]));
|
|
@@ -836,7 +855,7 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
836
855
|
weightSig += node.weight || 0;
|
|
837
856
|
}
|
|
838
857
|
for (let link of this.props.links) {
|
|
839
|
-
weightSig += link.weight || 0;
|
|
858
|
+
weightSig += (link.weight || 0) + (link.valueWeight || 0);
|
|
840
859
|
}
|
|
841
860
|
if (sig !== this.builtSig || geoParam.value !== this.builtGeo) {
|
|
842
861
|
// Switching layout mode (geographic vs solved) changes the whole coordinate space, so re-fit the view.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import "./inject";
|
|
2
|
+
import { getDomain, getPort } from "./config";
|
|
3
|
+
import { getTrustedMachineIds } from "./-d-trust/NetworkTrust2";
|
|
4
|
+
import { getExternalIP } from "socket-function/src/networking";
|
|
5
|
+
|
|
6
|
+
// The storage server entry point (see bin/storageserve.js): sets up the CLI args the sliftutils storage server expects, synchronizes our network trust list into its trust store, and runs its CLI.
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
if (!process.argv.includes("--url")) {
|
|
10
|
+
let ip = await getExternalIP();
|
|
11
|
+
let ipDomain = ip.replaceAll(".", "-");
|
|
12
|
+
process.argv.push("--url", `https://${ipDomain}.${getDomain()}:${getPort()}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let trustedMachineIds = await getTrustedMachineIds();
|
|
16
|
+
|
|
17
|
+
let { setTrustedMachines } = await import("sliftutils/storage/remoteStorage/storageServerState");
|
|
18
|
+
await setTrustedMachines({ account: "root", machineIds: trustedMachineIds });
|
|
19
|
+
console.log(`Synchronized ${trustedMachineIds.length} trusted machines`);
|
|
20
|
+
|
|
21
|
+
await import("sliftutils/storage/remoteStorage/storageServerCli");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
main().catch(e => {
|
|
25
|
+
console.error(e.stack || e);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
});
|
package/test.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { chdir } from "process";
|
|
2
|
-
import {
|
|
2
|
+
import { getRecords, setRecord } from "./src/-b-authorities/dnsAuthority";
|
|
3
3
|
import { timeInMinute } from "socket-function/src/misc";
|
|
4
4
|
chdir("D:/repos/qs-cyoa/");
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
async function main() {
|
|
8
|
-
let rawRecords = await
|
|
8
|
+
let rawRecords = await getRecords("A", "test.querysubtest.com");
|
|
9
9
|
for (let record of rawRecords) {
|
|
10
10
|
console.log(record);
|
|
11
11
|
}
|
|
@@ -1,354 +0,0 @@
|
|
|
1
|
-
module.allowclient = true;
|
|
2
|
-
|
|
3
|
-
import { qreact } from "../../4-dom/qreact";
|
|
4
|
-
import { css } from "typesafecss";
|
|
5
|
-
import { SocketFunction } from "socket-function/SocketFunction";
|
|
6
|
-
import { getBrowserUrlNode } from "../../-f-node-discovery/NodeDiscovery";
|
|
7
|
-
import { getSyncedController } from "../../library-components/SyncedController";
|
|
8
|
-
import { assertIsManagementUser } from "../managementPages";
|
|
9
|
-
import { t } from "../../2-proxy/schema2";
|
|
10
|
-
import { Querysub } from "../../4-querysub/Querysub";
|
|
11
|
-
import { sort, timeInDay } from "socket-function/src/misc";
|
|
12
|
-
import { isNode } from "typesafecss";
|
|
13
|
-
import { formatDateJSX } from "../../misc/formatJSX";
|
|
14
|
-
|
|
15
|
-
const RECORDS_PER_PAGE = 1000;
|
|
16
|
-
const HEX_SUBDOMAIN_LENGTH = 17;
|
|
17
|
-
const STALE_HEX_SUBDOMAIN_AGE = timeInDay * 7;
|
|
18
|
-
const HEX_SUBDOMAIN_REGEX = new RegExp(`^(\\*\\.)?[0-9a-f]{${HEX_SUBDOMAIN_LENGTH}}\\..+$`);
|
|
19
|
-
|
|
20
|
-
export type DNSRecord = {
|
|
21
|
-
id: string;
|
|
22
|
-
zoneId: string;
|
|
23
|
-
zoneName: string;
|
|
24
|
-
type: string;
|
|
25
|
-
name: string;
|
|
26
|
-
content: string;
|
|
27
|
-
proxied: boolean;
|
|
28
|
-
ttl: number;
|
|
29
|
-
createdOn: number;
|
|
30
|
-
modifiedOn: number;
|
|
31
|
-
// Time we last asserted this record, parsed from our freshness comment tag, falling back to
|
|
32
|
-
// Cloudflare's modified_on when the tag is absent. This is the time used for staleness.
|
|
33
|
-
setOn: number;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
class DNSPageControllerBase {
|
|
37
|
-
public async getAllRecords(): Promise<DNSRecord[]> {
|
|
38
|
-
if (!isNode()) throw new Error(`getAllRecords must be called serverside`);
|
|
39
|
-
const { cloudflareGETCall } = await import("../../-b-authorities/cloudflareHelpers");
|
|
40
|
-
const { freshnessTime } = await import("../../-b-authorities/dnsAuthority");
|
|
41
|
-
const { getDomain } = await import("../../config");
|
|
42
|
-
|
|
43
|
-
let zones = await cloudflareGETCall<{ id: string; name: string }[]>("/zones", {
|
|
44
|
-
per_page: String(RECORDS_PER_PAGE),
|
|
45
|
-
});
|
|
46
|
-
let domain = getDomain();
|
|
47
|
-
zones = zones.filter(z => z.name === domain);
|
|
48
|
-
|
|
49
|
-
let allRecords: DNSRecord[] = [];
|
|
50
|
-
for (let zone of zones) {
|
|
51
|
-
let page = 1;
|
|
52
|
-
while (true) {
|
|
53
|
-
let records = await cloudflareGETCall<{
|
|
54
|
-
id: string;
|
|
55
|
-
type: string;
|
|
56
|
-
name: string;
|
|
57
|
-
content: string;
|
|
58
|
-
proxied: boolean;
|
|
59
|
-
ttl: number;
|
|
60
|
-
created_on: string;
|
|
61
|
-
modified_on: string;
|
|
62
|
-
comment?: string;
|
|
63
|
-
}[]>(`/zones/${zone.id}/dns_records`, {
|
|
64
|
-
per_page: String(RECORDS_PER_PAGE),
|
|
65
|
-
page: String(page),
|
|
66
|
-
});
|
|
67
|
-
for (let record of records) {
|
|
68
|
-
let modifiedOn = new Date(record.modified_on).getTime();
|
|
69
|
-
allRecords.push({
|
|
70
|
-
id: record.id,
|
|
71
|
-
zoneId: zone.id,
|
|
72
|
-
zoneName: zone.name,
|
|
73
|
-
type: record.type,
|
|
74
|
-
name: record.name,
|
|
75
|
-
content: record.content,
|
|
76
|
-
proxied: record.proxied,
|
|
77
|
-
ttl: record.ttl,
|
|
78
|
-
createdOn: new Date(record.created_on).getTime(),
|
|
79
|
-
modifiedOn,
|
|
80
|
-
setOn: freshnessTime(record.comment) || modifiedOn,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
if (records.length < RECORDS_PER_PAGE) break;
|
|
84
|
-
page++;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return allRecords;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
public async deleteRecord(config: { zoneId: string; recordId: string }): Promise<void> {
|
|
91
|
-
if (!isNode()) throw new Error(`deleteRecord must be called serverside`);
|
|
92
|
-
const { cloudflareCall } = await import("../../-b-authorities/cloudflareHelpers");
|
|
93
|
-
await cloudflareCall(`/zones/${config.zoneId}/dns_records/${config.recordId}`, Buffer.from([]), "DELETE");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
public async deleteRecordsByContent(config: { content: string }): Promise<number> {
|
|
97
|
-
if (!isNode()) throw new Error(`deleteRecordsByContent must be called serverside`);
|
|
98
|
-
const { cloudflareCall } = await import("../../-b-authorities/cloudflareHelpers");
|
|
99
|
-
let all = await this.getAllRecords();
|
|
100
|
-
let matching = all.filter(x => x.content === config.content);
|
|
101
|
-
for (let record of matching) {
|
|
102
|
-
await cloudflareCall(`/zones/${record.zoneId}/dns_records/${record.id}`, Buffer.from([]), "DELETE");
|
|
103
|
-
}
|
|
104
|
-
return matching.length;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export const DNSPageController = SocketFunction.register(
|
|
109
|
-
"DNSPageController-3e7b1d92-44a8-4f7e-9bba-d12c4f87a6e0",
|
|
110
|
-
new DNSPageControllerBase(),
|
|
111
|
-
() => ({
|
|
112
|
-
getAllRecords: {},
|
|
113
|
-
deleteRecord: {},
|
|
114
|
-
deleteRecordsByContent: {},
|
|
115
|
-
}),
|
|
116
|
-
() => ({
|
|
117
|
-
hooks: [assertIsManagementUser],
|
|
118
|
-
}),
|
|
119
|
-
{
|
|
120
|
-
noAutoExpose: true,
|
|
121
|
-
}
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
const DNSPageSynced = getSyncedController(DNSPageController, {
|
|
125
|
-
reads: {
|
|
126
|
-
getAllRecords: ["dnsRecords"],
|
|
127
|
-
},
|
|
128
|
-
writes: {
|
|
129
|
-
deleteRecord: ["dnsRecords"],
|
|
130
|
-
deleteRecordsByContent: ["dnsRecords"],
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
export class DNSPage extends qreact.Component {
|
|
135
|
-
state = t.state({
|
|
136
|
-
expandedGroups: t.lookup(t.boolean),
|
|
137
|
-
busyKeys: t.lookup(t.boolean),
|
|
138
|
-
errorMessage: t.string,
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
private async deleteOne(record: DNSRecord) {
|
|
142
|
-
let typed = prompt(`Type the content "${record.content}" to confirm deleting this record:\n\n${record.type} ${record.name} → ${record.content}`);
|
|
143
|
-
if (typed === null) return;
|
|
144
|
-
if (typed !== record.content) {
|
|
145
|
-
alert(`Confirmation does not match. Aborted.`);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
let busyKey = `record:${record.id}`;
|
|
149
|
-
Querysub.commit(() => {
|
|
150
|
-
this.state.busyKeys[busyKey] = true;
|
|
151
|
-
this.state.errorMessage = "";
|
|
152
|
-
});
|
|
153
|
-
try {
|
|
154
|
-
await DNSPageSynced(getBrowserUrlNode()).deleteRecord.promise({
|
|
155
|
-
zoneId: record.zoneId,
|
|
156
|
-
recordId: record.id,
|
|
157
|
-
});
|
|
158
|
-
} catch (err) {
|
|
159
|
-
console.error(`DNS deleteRecord failed:`, (err as Error).stack ?? err);
|
|
160
|
-
Querysub.commit(() => {
|
|
161
|
-
this.state.errorMessage = (err as Error).stack ?? String(err);
|
|
162
|
-
});
|
|
163
|
-
} finally {
|
|
164
|
-
Querysub.commit(() => {
|
|
165
|
-
delete this.state.busyKeys[busyKey];
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
private async deleteStaleHexRecords(content: string, matching: DNSRecord[]) {
|
|
171
|
-
let listText = matching.map(r => ` ${r.type} ${r.name} (set ${new Date(r.setOn).toISOString()})`).join("\n");
|
|
172
|
-
let typed = prompt(`Type "${content}" to confirm deleting ${matching.length} stale hex-subdomain record(s):\n\n${listText}`);
|
|
173
|
-
if (typed === null) return;
|
|
174
|
-
if (typed !== content) {
|
|
175
|
-
alert(`Confirmation does not match. Aborted.`);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
let busyKey = `staleHex:${content}`;
|
|
179
|
-
Querysub.commit(() => {
|
|
180
|
-
this.state.busyKeys[busyKey] = true;
|
|
181
|
-
this.state.errorMessage = "";
|
|
182
|
-
});
|
|
183
|
-
try {
|
|
184
|
-
for (let record of matching) {
|
|
185
|
-
await DNSPageSynced(getBrowserUrlNode()).deleteRecord.promise({
|
|
186
|
-
zoneId: record.zoneId,
|
|
187
|
-
recordId: record.id,
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
} catch (err) {
|
|
191
|
-
console.error(`DNS deleteStaleHexRecords failed:`, (err as Error).stack ?? err);
|
|
192
|
-
Querysub.commit(() => {
|
|
193
|
-
this.state.errorMessage = (err as Error).stack ?? String(err);
|
|
194
|
-
});
|
|
195
|
-
} finally {
|
|
196
|
-
Querysub.commit(() => {
|
|
197
|
-
delete this.state.busyKeys[busyKey];
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
private async deleteGroup(content: string, count: number) {
|
|
203
|
-
let typed = prompt(`Type "${content}" to confirm deleting ALL ${count} record(s) pointing to it:`);
|
|
204
|
-
if (typed === null) return;
|
|
205
|
-
if (typed !== content) {
|
|
206
|
-
alert(`Confirmation does not match. Aborted.`);
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
let busyKey = `group:${content}`;
|
|
210
|
-
Querysub.commit(() => {
|
|
211
|
-
this.state.busyKeys[busyKey] = true;
|
|
212
|
-
this.state.errorMessage = "";
|
|
213
|
-
});
|
|
214
|
-
try {
|
|
215
|
-
await DNSPageSynced(getBrowserUrlNode()).deleteRecordsByContent.promise({ content });
|
|
216
|
-
} catch (err) {
|
|
217
|
-
console.error(`DNS deleteRecordsByContent failed:`, (err as Error).stack ?? err);
|
|
218
|
-
Querysub.commit(() => {
|
|
219
|
-
this.state.errorMessage = (err as Error).stack ?? String(err);
|
|
220
|
-
});
|
|
221
|
-
} finally {
|
|
222
|
-
Querysub.commit(() => {
|
|
223
|
-
delete this.state.busyKeys[busyKey];
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
render() {
|
|
229
|
-
let records = DNSPageSynced(getBrowserUrlNode()).getAllRecords();
|
|
230
|
-
if (!records) {
|
|
231
|
-
return <div className={css.pad2(16)}>Loading DNS records...</div>;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
let groups = new Map<string, DNSRecord[]>();
|
|
235
|
-
for (let record of records) {
|
|
236
|
-
let list = groups.get(record.content);
|
|
237
|
-
if (!list) {
|
|
238
|
-
list = [];
|
|
239
|
-
groups.set(record.content, list);
|
|
240
|
-
}
|
|
241
|
-
list.push(record);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
let groupEntries = Array.from(groups.entries()).map(([content, list]) => ({ content, records: list }));
|
|
245
|
-
sort(groupEntries, x => -x.records.length);
|
|
246
|
-
for (let group of groupEntries) {
|
|
247
|
-
sort(group.records, r => -r.setOn);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return <div className={css.vbox(12).pad2(16).fillWidth}>
|
|
251
|
-
<div className={css.hbox(12).alignItems("center")}>
|
|
252
|
-
<h2 className={css.flexGrow(1)}>DNS Records ({records.length} total, {groupEntries.length} unique contents)</h2>
|
|
253
|
-
</div>
|
|
254
|
-
{this.state.errorMessage && <pre className={css.colorhsl(0, 60, 40).whiteSpace("pre-wrap").pad2(8).bord2(0, 60, 60).hsl(0, 50, 95)}>{this.state.errorMessage}</pre>}
|
|
255
|
-
<div className={css.vbox(8).fillWidth}>
|
|
256
|
-
{groupEntries.map(({ content, records: list }) => {
|
|
257
|
-
let expanded = !!this.state.expandedGroups[content];
|
|
258
|
-
let groupBusy = !!this.state.busyKeys[`group:${content}`];
|
|
259
|
-
return <div key={content} className={css.vbox(0).fillWidth.bord2(0, 0, 80).hsl(0, 0, 99)}>
|
|
260
|
-
<div className={css.hbox(10).alignItems("center").pad2(10).button}
|
|
261
|
-
onClick={() => {
|
|
262
|
-
if (this.state.expandedGroups[content]) {
|
|
263
|
-
delete this.state.expandedGroups[content];
|
|
264
|
-
} else {
|
|
265
|
-
this.state.expandedGroups[content] = true;
|
|
266
|
-
}
|
|
267
|
-
}}
|
|
268
|
-
>
|
|
269
|
-
<span>{expanded ? "▼" : "▶"}</span>
|
|
270
|
-
<span
|
|
271
|
-
className={css.boldStyle.fontFamily("monospace").button.pad2(4, 2).bord2(0, 0, 80).hsl(0, 0, 100).hbox(6).alignItems("center")}
|
|
272
|
-
title="Click to copy"
|
|
273
|
-
onClick={(e) => {
|
|
274
|
-
e.stopPropagation();
|
|
275
|
-
void navigator.clipboard.writeText(content);
|
|
276
|
-
}}
|
|
277
|
-
>
|
|
278
|
-
<span>{content}</span>
|
|
279
|
-
<span className={css.colorhsl(0, 0, 50)}>📋</span>
|
|
280
|
-
</span>
|
|
281
|
-
<span className={css.colorhsl(0, 0, 40)}>{list.length} record(s)</span>
|
|
282
|
-
<div className={css.flexGrow(1)} />
|
|
283
|
-
<button
|
|
284
|
-
className={css.pad2(10, 6).button.bord2(0, 80, 50)
|
|
285
|
-
+ (groupBusy ? css.hsl(0, 0, 90).colorhsl(0, 0, 50) : css.hsl(0, 80, 92).colorhsl(0, 80, 30))}
|
|
286
|
-
disabled={groupBusy}
|
|
287
|
-
onClick={(e) => {
|
|
288
|
-
e.stopPropagation();
|
|
289
|
-
void this.deleteGroup(content, list.length);
|
|
290
|
-
}}
|
|
291
|
-
>
|
|
292
|
-
{groupBusy ? "Deleting..." : `🗑️ Delete all ${list.length}`}
|
|
293
|
-
</button>
|
|
294
|
-
</div>
|
|
295
|
-
{expanded && <div className={css.vbox(4).pad2(10).hsl(0, 0, 100)}>
|
|
296
|
-
{(() => {
|
|
297
|
-
let staleHex = list.filter(r =>
|
|
298
|
-
HEX_SUBDOMAIN_REGEX.test(r.name)
|
|
299
|
-
&& Date.now() - r.setOn > STALE_HEX_SUBDOMAIN_AGE
|
|
300
|
-
);
|
|
301
|
-
if (staleHex.length === 0) return undefined;
|
|
302
|
-
let staleHexBusy = !!this.state.busyKeys[`staleHex:${content}`];
|
|
303
|
-
return <div className={css.vbox(4).pad2(8).bord2(30, 60, 60).hsl(30, 70, 96)}>
|
|
304
|
-
<div className={css.hbox(10).alignItems("center")}>
|
|
305
|
-
<span className={css.boldStyle}>Stale hex-subdomain records (set >7d ago): {staleHex.length}</span>
|
|
306
|
-
<div className={css.flexGrow(1)} />
|
|
307
|
-
<button
|
|
308
|
-
className={css.pad2(10, 6).button.bord2(0, 80, 50)
|
|
309
|
-
+ (staleHexBusy ? css.hsl(0, 0, 90).colorhsl(0, 0, 50) : css.hsl(0, 80, 92).colorhsl(0, 80, 30))}
|
|
310
|
-
disabled={staleHexBusy}
|
|
311
|
-
onClick={() => void this.deleteStaleHexRecords(content, staleHex)}
|
|
312
|
-
>
|
|
313
|
-
{staleHexBusy ? "Deleting..." : `🗑️ Delete ${staleHex.length} stale hex record(s)`}
|
|
314
|
-
</button>
|
|
315
|
-
</div>
|
|
316
|
-
<div className={css.vbox(2).fontFamily("monospace").colorhsl(0, 0, 30)}>
|
|
317
|
-
{staleHex.map(r => <div key={r.id}>{r.type} {r.name} — set {formatDateJSX(r.setOn)}</div>)}
|
|
318
|
-
</div>
|
|
319
|
-
</div>;
|
|
320
|
-
})()}
|
|
321
|
-
{list.map(record => {
|
|
322
|
-
let recordBusy = !!this.state.busyKeys[`record:${record.id}`];
|
|
323
|
-
return <div key={record.id} className={css.hbox(10).alignItems("center").pad2(6).bord2(0, 0, 92)}>
|
|
324
|
-
<span className={css.boldStyle.minWidth(50)}>{record.type}</span>
|
|
325
|
-
<span className={css.fontFamily("monospace").flexGrow(1)}>{record.name}</span>
|
|
326
|
-
{record.proxied && <span className={css.colorhsl(30, 80, 40).pad2(4, 2).bord2(30, 80, 70).hsl(30, 80, 95)}>proxied</span>}
|
|
327
|
-
<span className={css.colorhsl(0, 0, 50)}>ttl {record.ttl}</span>
|
|
328
|
-
<span className={css.colorhsl(0, 0, 50)}>
|
|
329
|
-
created {formatDateJSX(record.createdOn)}
|
|
330
|
-
</span>
|
|
331
|
-
<span className={css.colorhsl(0, 0, 50)}>
|
|
332
|
-
modified {formatDateJSX(record.modifiedOn)}
|
|
333
|
-
</span>
|
|
334
|
-
<span className={css.colorhsl(0, 0, 50)}>
|
|
335
|
-
set {formatDateJSX(record.setOn)}
|
|
336
|
-
</span>
|
|
337
|
-
<span className={css.colorhsl(0, 0, 50)}>{record.zoneName}</span>
|
|
338
|
-
<button
|
|
339
|
-
className={css.pad2(8, 4).button.bord2(0, 0, 60)
|
|
340
|
-
+ (recordBusy ? css.hsl(0, 0, 90).colorhsl(0, 0, 50) : css.hsl(0, 0, 100))}
|
|
341
|
-
disabled={recordBusy}
|
|
342
|
-
onClick={() => void this.deleteOne(record)}
|
|
343
|
-
>
|
|
344
|
-
{recordBusy ? "Deleting..." : "🗑️ Delete"}
|
|
345
|
-
</button>
|
|
346
|
-
</div>;
|
|
347
|
-
})}
|
|
348
|
-
</div>}
|
|
349
|
-
</div>;
|
|
350
|
-
})}
|
|
351
|
-
</div>
|
|
352
|
-
</div>;
|
|
353
|
-
}
|
|
354
|
-
}
|