sliftutils 1.7.129 → 1.7.131
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/index.d.ts +4 -51
- package/misc/dist/ownIPs.ts.cache +29 -3
- package/misc/dist/strings.ts.cache +2 -2
- package/misc/https/dist/certs.ts.cache +2 -2
- package/misc/https/dist/persistentLocalStorage.ts.cache +2 -2
- package/misc/ownIPs.ts +29 -1
- package/package.json +1 -1
- package/security/authorizedKeys/authorizedKeys.d.ts +37 -0
- package/security/authorizedKeys/daemon/changes.d.ts +17 -0
- package/security/authorizedKeys/daemon/dist/changes.ts.cache +2 -2
- package/security/authorizedKeys/daemon/dist/git.ts.cache +2 -2
- package/security/authorizedKeys/daemon/dist/notify.ts.cache +2 -2
- package/security/authorizedKeys/daemon/dist/paths.ts.cache +4 -7
- package/security/authorizedKeys/daemon/dist/repoFiles.ts.cache +2 -2
- package/security/authorizedKeys/daemon/dist/revocation.ts.cache +0 -0
- package/security/authorizedKeys/daemon/dist/sessions.ts.cache +2 -2
- package/security/authorizedKeys/daemon/dist/state.ts.cache +6 -4
- package/security/authorizedKeys/daemon/dist/trust.ts.cache +2 -2
- package/security/authorizedKeys/daemon/git.d.ts +35 -0
- package/security/authorizedKeys/daemon/notify.d.ts +8 -0
- package/security/authorizedKeys/daemon/paths.d.ts +15 -0
- package/security/authorizedKeys/daemon/paths.ts +0 -3
- package/security/authorizedKeys/daemon/repoFiles.d.ts +25 -0
- package/security/authorizedKeys/daemon/revocation.d.ts +74 -0
- package/security/authorizedKeys/daemon/revocation.ts +0 -0
- package/security/authorizedKeys/daemon/sessions.d.ts +19 -0
- package/security/authorizedKeys/daemon/state.d.ts +67 -0
- package/security/authorizedKeys/daemon/state.ts +4 -8
- package/security/authorizedKeys/daemon/trust.d.ts +20 -0
- package/security/authorizedKeys/dist/authorizedKeys.ts.cache +2 -2
- package/security/authorizedKeys/dist/keysRepo.ts.cache +111 -0
- package/security/authorizedKeys/dist/revokeSource.ts.cache +2 -2
- package/security/authorizedKeys/dist/secureSSH.ts.cache +147 -71
- package/security/authorizedKeys/dist/sources.ts.cache +2 -2
- package/security/authorizedKeys/dist/unrevoke.ts.cache +70 -51
- package/security/authorizedKeys/keysRepo.d.ts +10 -0
- package/security/authorizedKeys/keysRepo.ts +111 -0
- package/security/authorizedKeys/revokeSource.d.ts +13 -0
- package/security/authorizedKeys/secureSSH.ts +28 -38
- package/security/authorizedKeys/sources.d.ts +17 -0
- package/security/authorizedKeys/unrevoke.ts +9 -36
- package/security/helpers/dist/paths.ts.cache +2 -2
- package/security/helpers/dist/spawn.ts.cache +2 -2
- package/security/helpers/paths.d.ts +4 -0
- package/security/helpers/spawn.d.ts +16 -0
- package/security/keys/deriveKey.d.ts +16 -0
- package/security/keys/dist/deriveKey.ts.cache +2 -2
- package/security/keys/dist/sshKeyFile.ts.cache +2 -2
- package/security/keys/sshKeyFile.d.ts +18 -0
- package/security/machines/addMachine.ts +80 -27
- package/security/machines/dist/addMachine.ts.cache +6 -5
- package/security/machines/dist/machines.ts.cache +110 -101
- package/security/machines/machines.d.ts +81 -0
- package/security/machines/machines.ts +123 -99
- package/security/machines/trustServer.ts +5 -3
- package/security/notifications/discord.d.ts +36 -0
- package/security/notifications/dist/discord.ts.cache +2 -2
- package/security/notifications/dist/remoteWebhook.ts.cache +78 -0
- package/security/notifications/dist/setupNotify.ts.cache +39 -0
- package/security/signedFiles/dist/manifest.ts.cache +2 -2
- package/security/signedFiles/dist/signFiles.ts.cache +8 -10
- package/security/signedFiles/manifest.d.ts +44 -0
- package/security/signedFiles/signFiles.ts +6 -10
- package/storage/remoteStorage/ArchivesRemote.ts +8 -10
- package/storage/remoteStorage/accessPage.tsx +13 -90
- package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +11 -12
- package/storage/remoteStorage/dist/serverConfig.ts.cache +7 -49
- package/storage/remoteStorage/dist/storageController.ts.cache +33 -114
- package/storage/remoteStorage/serverConfig.d.ts +0 -10
- package/storage/remoteStorage/serverConfig.ts +6 -48
- package/storage/remoteStorage/storageController.d.ts +4 -36
- package/storage/remoteStorage/storageController.ts +40 -126
- package/storage/remoteStorage/storageServer.ts +1 -8
- package/storage/remoteStorage/storageServerCli.ts +14 -0
- package/security/machines/trustState.ts +0 -53
- package/storage/remoteStorage/grantAccess.js +0 -4
- package/storage/remoteStorage/grantAccessCli.d.ts +0 -1
- package/storage/remoteStorage/grantAccessCli.ts +0 -25
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export declare const MACHINES_DIR = "machines";
|
|
2
|
+
/** One machine we are willing to talk to, and the addresses it may talk to us from. */
|
|
3
|
+
export type MachineState = {
|
|
4
|
+
machineId: string;
|
|
5
|
+
ips: string[];
|
|
6
|
+
addedAt: string;
|
|
7
|
+
};
|
|
8
|
+
/** This machine's keys repo, worked out once and then remembered.
|
|
9
|
+
|
|
10
|
+
A failure is remembered too, but only briefly: a server may well start before the machine has
|
|
11
|
+
been set up, and should start working when it is, rather than needing a restart. */
|
|
12
|
+
export declare const keysRepo: {
|
|
13
|
+
(): Promise<{
|
|
14
|
+
repoPath: string;
|
|
15
|
+
sourceURL: string;
|
|
16
|
+
}>;
|
|
17
|
+
reset(): void;
|
|
18
|
+
set(newValue: Promise<{
|
|
19
|
+
repoPath: string;
|
|
20
|
+
sourceURL: string;
|
|
21
|
+
}>): void;
|
|
22
|
+
};
|
|
23
|
+
/** Every machine a checkout lists. Without a repo, this machine's own. */
|
|
24
|
+
export declare function getMachines(repoPath?: string): Promise<MachineState[]>;
|
|
25
|
+
/** The machines this system trusts, as the signed repo lists them.
|
|
26
|
+
|
|
27
|
+
Held rather than read per call, and refreshed in the background, so nobody asking whether a
|
|
28
|
+
machine is trusted waits for a directory of files to be read and a signature to be checked. A
|
|
29
|
+
refresh that fails leaves the list we already have: the checkout is only stale, not wrong. */
|
|
30
|
+
export declare function getTrustedMachines(): Promise<MachineState[]>;
|
|
31
|
+
/** Makes a checkout list exactly these machines: those named are written with the addresses given,
|
|
32
|
+
and anything not named is removed. So read them, change what you want, and write the result.
|
|
33
|
+
|
|
34
|
+
The addresses are part of setting rather than a separate step, because a machine with no
|
|
35
|
+
address it may talk from is not a machine that would ever be accepted - there is no state where
|
|
36
|
+
naming one without them means anything.
|
|
37
|
+
|
|
38
|
+
Nothing here signs or commits. The signature is what every other machine checks before
|
|
39
|
+
believing any of this, and it takes the hardware key, so `yarn signfiles git` is still yours to
|
|
40
|
+
run afterwards. */
|
|
41
|
+
export declare function setMachines(config: {
|
|
42
|
+
repoPath?: string;
|
|
43
|
+
machines: {
|
|
44
|
+
machineId: string;
|
|
45
|
+
ips: string[];
|
|
46
|
+
}[];
|
|
47
|
+
}): Promise<MachineState[]>;
|
|
48
|
+
/** What to run to trust a machine that has just been refused.
|
|
49
|
+
|
|
50
|
+
Everything in it is already known to whoever is being refused: their machine id, the address we
|
|
51
|
+
saw them at, and the domain they just talked to. Handing it back saves them working out the
|
|
52
|
+
parts of a command they have every right to know. Running it still takes the hardware key on
|
|
53
|
+
the machine that owns the repo, so telling them costs nothing. */
|
|
54
|
+
export declare function addMachineCommand(config: {
|
|
55
|
+
machineId: string;
|
|
56
|
+
ip: string;
|
|
57
|
+
domain?: string;
|
|
58
|
+
}): string;
|
|
59
|
+
/** Whether we will talk to this machine, coming from this address.
|
|
60
|
+
|
|
61
|
+
Both halves are required and neither is a guess: the caller knows the machine id because the
|
|
62
|
+
connection proved it, and knows the address because the packets came from there. A machine we
|
|
63
|
+
do not list is simply not accepted. A machine we do list, arriving from an address it does not
|
|
64
|
+
have, is treated as the same kind of event as a stolen ssh key - it is revoked everywhere, and
|
|
65
|
+
stays revoked until an unrevoke allows that machine from that address.
|
|
66
|
+
|
|
67
|
+
Says why when the answer is no, because the three ways to be refused are entirely different
|
|
68
|
+
things to a person reading it: never trusted at all, trusted but frozen, or trusted and talking
|
|
69
|
+
from somewhere it should not be.
|
|
70
|
+
|
|
71
|
+
Throws when the repo itself cannot be read or is not signed, rather than answering false: that
|
|
72
|
+
is a broken installation, not a rejected machine, and the two deserve different handling. */
|
|
73
|
+
export type MachineVerdict = {
|
|
74
|
+
accepted: boolean;
|
|
75
|
+
reason: string;
|
|
76
|
+
};
|
|
77
|
+
export declare function isMachineAccepted(config: {
|
|
78
|
+
machineId: string;
|
|
79
|
+
ip: string;
|
|
80
|
+
domain?: string;
|
|
81
|
+
}): Promise<MachineVerdict>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
2
|
import os from "os";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import {
|
|
4
|
+
import { resolveKeysRepo } from "../authorizedKeys/keysRepo";
|
|
5
5
|
import { revokeRepo, syncRepoFiles } from "../authorizedKeys/daemon/repoFiles";
|
|
6
6
|
import { listRepoDir, readRepoFile } from "../authorizedKeys/daemon/repoFiles";
|
|
7
7
|
import { newRevocationId, pairKey, readRevocationFiles, readUnrevokes } from "../authorizedKeys/daemon/revocation";
|
|
@@ -9,10 +9,10 @@ import { runGit } from "../authorizedKeys/daemon/git";
|
|
|
9
9
|
import { ensureRevokeKey } from "../authorizedKeys/daemon/repoFiles";
|
|
10
10
|
import { verifyCheckout } from "../authorizedKeys/daemon/trust";
|
|
11
11
|
import { revokeRepoPath, revokeRepoURL } from "../authorizedKeys/revokeSource";
|
|
12
|
-
import { sourceRepoPath } from "../authorizedKeys/sources";
|
|
13
12
|
import { notify } from "../authorizedKeys/daemon/notify";
|
|
14
13
|
import { areDiscordNotificationsConfigured, configureDiscordNotifications, DEFAULT_WEBHOOK_FILE_PATH } from "../notifications/discord";
|
|
15
|
-
import {
|
|
14
|
+
import { lazy } from "socket-function/src/caching";
|
|
15
|
+
import { runInfinitePoll } from "socket-function/src/batching";
|
|
16
16
|
import { spawnPromise } from "../helpers/spawn";
|
|
17
17
|
|
|
18
18
|
// Which machines this system talks to, kept in the same repo as the ssh keys. That repo is already
|
|
@@ -24,9 +24,14 @@ const REVOCATION_REASON = "a machine talked to us from an unapproved IP";
|
|
|
24
24
|
// The revoke repo is pulled at most this often. A check happens per request, and a request must
|
|
25
25
|
// not cost a round trip to github.
|
|
26
26
|
const REVOKE_SYNC_INTERVAL = 60 * 1000;
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
27
|
+
// And the machine list is re-read from the checkout on this interval, in the background. The
|
|
28
|
+
// checkout itself only moves when the daemon pulls it, which is on the same sort of interval, so
|
|
29
|
+
// reading more often than this would only re-read the same bytes.
|
|
30
|
+
const MACHINES_REFRESH_INTERVAL = 60 * 1000;
|
|
31
|
+
// How long a failure to find the keys repo is remembered. A long lived process may start before
|
|
32
|
+
// the machine is set up, and should pick it up once it is, without being restarted - but it must
|
|
33
|
+
// not pay for resolving the repo on every request either.
|
|
34
|
+
const REPO_RETRY_DELAY = 15 * 1000;
|
|
30
35
|
|
|
31
36
|
/** One machine we are willing to talk to, and the addresses it may talk to us from. */
|
|
32
37
|
export type MachineState = {
|
|
@@ -48,27 +53,78 @@ function machineFilePath(repoPath: string, machineId: string) {
|
|
|
48
53
|
return path.join(repoPath, MACHINES_DIR, `${machineId}.json`);
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
/**
|
|
56
|
+
/** This machine's keys repo, worked out once and then remembered.
|
|
52
57
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
A failure is remembered too, but only briefly: a server may well start before the machine has
|
|
59
|
+
been set up, and should start working when it is, rather than needing a restart. */
|
|
60
|
+
export const keysRepo = lazy(async () => {
|
|
61
|
+
let lookup = resolveKeysRepo();
|
|
62
|
+
lookup.catch(() => {
|
|
63
|
+
setTimeout(() => keysRepo.reset(), REPO_RETRY_DELAY).unref();
|
|
64
|
+
});
|
|
65
|
+
return await lookup;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/** Every machine a checkout lists. Without a repo, this machine's own. */
|
|
69
|
+
export async function getMachines(repoPath?: string): Promise<MachineState[]> {
|
|
70
|
+
return [...(await readMachines(repoPath || (await keysRepo()).repoPath)).values()];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Reads the machine list, once its signature has been checked. An unsigned machine list, or one
|
|
74
|
+
signed over different contents, is not evidence of anything, so this throws rather than
|
|
75
|
+
quietly trusting nobody. */
|
|
76
|
+
async function readTrustedMachines() {
|
|
77
|
+
let { repoPath } = await keysRepo();
|
|
78
|
+
await verifyCheckout(repoPath);
|
|
79
|
+
return await getMachines(repoPath);
|
|
80
|
+
}
|
|
58
81
|
|
|
59
|
-
|
|
82
|
+
const trustedMachines = lazy(async () => {
|
|
83
|
+
let read = readTrustedMachines();
|
|
84
|
+
read.catch(() => trustedMachines.reset());
|
|
85
|
+
return await read;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
let refreshing = false;
|
|
89
|
+
|
|
90
|
+
/** The machines this system trusts, as the signed repo lists them.
|
|
91
|
+
|
|
92
|
+
Held rather than read per call, and refreshed in the background, so nobody asking whether a
|
|
93
|
+
machine is trusted waits for a directory of files to be read and a signature to be checked. A
|
|
94
|
+
refresh that fails leaves the list we already have: the checkout is only stale, not wrong. */
|
|
95
|
+
export async function getTrustedMachines(): Promise<MachineState[]> {
|
|
96
|
+
if (!refreshing) {
|
|
97
|
+
refreshing = true;
|
|
98
|
+
runInfinitePoll(MACHINES_REFRESH_INTERVAL, async () => {
|
|
99
|
+
try {
|
|
100
|
+
trustedMachines.set(Promise.resolve(await readTrustedMachines()));
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.log(`Could not re-read the trusted machines, keeping the ones we have. ${e}`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return await trustedMachines();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Makes a checkout list exactly these machines: those named are written with the addresses given,
|
|
110
|
+
and anything not named is removed. So read them, change what you want, and write the result.
|
|
111
|
+
|
|
112
|
+
The addresses are part of setting rather than a separate step, because a machine with no
|
|
113
|
+
address it may talk from is not a machine that would ever be accepted - there is no state where
|
|
114
|
+
naming one without them means anything.
|
|
115
|
+
|
|
116
|
+
Nothing here signs or commits. The signature is what every other machine checks before
|
|
60
117
|
believing any of this, and it takes the hardware key, so `yarn signfiles git` is still yours to
|
|
61
118
|
run afterwards. */
|
|
62
|
-
export async function
|
|
63
|
-
repoPath
|
|
64
|
-
machines
|
|
119
|
+
export async function setMachines(config: {
|
|
120
|
+
repoPath?: string;
|
|
121
|
+
machines: { machineId: string; ips: string[] }[];
|
|
65
122
|
}): Promise<MachineState[]> {
|
|
66
|
-
let {
|
|
123
|
+
let { machines } = config;
|
|
124
|
+
let repoPath = config.repoPath || (await keysRepo()).repoPath;
|
|
125
|
+
// Whatever was held is now stale, whoever reads it next.
|
|
126
|
+
trustedMachines.reset();
|
|
67
127
|
let existing = await readMachines(repoPath);
|
|
68
|
-
if (!machines) {
|
|
69
|
-
return [...existing.values()];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
128
|
let directory = path.join(repoPath, MACHINES_DIR);
|
|
73
129
|
let written: MachineState[] = [];
|
|
74
130
|
for (let machine of machines) {
|
|
@@ -122,61 +178,6 @@ async function readMachines(repoPath: string) {
|
|
|
122
178
|
return machines;
|
|
123
179
|
}
|
|
124
180
|
|
|
125
|
-
async function isKeysRepo(repoPath: string) {
|
|
126
|
-
return await pathExists(path.join(repoPath, ".git"))
|
|
127
|
-
&& await pathExists(path.join(repoPath, "authorized_keys"));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function originOf(repoPath: string) {
|
|
131
|
-
let origin = await spawnPromise({ command: "git", args: ["remote", "get-url", "origin"], cwd: repoPath });
|
|
132
|
-
let url = origin.stdout.trim();
|
|
133
|
-
if (origin.status !== 0 || !url) {
|
|
134
|
-
throw new Error(`Expected ${repoPath} to have an origin remote, it has none`);
|
|
135
|
-
}
|
|
136
|
-
return url;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** The keys repo this machine answers from, and the one anything editing the machine list edits.
|
|
140
|
-
|
|
141
|
-
On a host, that is the checkout the daemon already keeps up to date. On Windows there is no
|
|
142
|
-
daemon and no /etc, so the repo is expected beside the working directory, which is where a
|
|
143
|
-
developer working on both would have it.
|
|
144
|
-
|
|
145
|
-
Never the directory the command happens to be run from. Which repo this machine trusts is a
|
|
146
|
-
property of the machine, not of where somebody was standing when they typed something. */
|
|
147
|
-
export async function resolveKeysRepo() {
|
|
148
|
-
if (os.platform() === "win32") {
|
|
149
|
-
let repoPath = path.resolve(WINDOWS_REPO_PATH);
|
|
150
|
-
if (!await isKeysRepo(repoPath)) {
|
|
151
|
-
throw new Error(
|
|
152
|
-
`Expected ${repoPath} to be an authorized_keys repo, it is not.\n`
|
|
153
|
-
+ `On Windows the repo is read from there, so clone it beside this one:\n`
|
|
154
|
-
+ ` git clone <your authorized_keys repo> ${repoPath}`
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
return { repoPath, sourceURL: await originOf(repoPath) };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
let config = await fs.readFile(CONFIG_PATH, "utf8").catch(() => "");
|
|
161
|
-
let sourceURL = config && (JSON.parse(config).repoSources || [])[0] || "";
|
|
162
|
-
if (!sourceURL) {
|
|
163
|
-
throw new Error(
|
|
164
|
-
`Expected this machine to be set up with an authorized_keys repo, ${CONFIG_PATH} names none.\n`
|
|
165
|
-
+ `Set it up first:\n`
|
|
166
|
-
+ ` yarn setupnotify <discord-webhook-url>\n`
|
|
167
|
-
+ ` yarn securessh add <repo-private-key> <repo-url>`
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
let repoPath = sourceRepoPath(sourceURL);
|
|
171
|
-
if (!await isKeysRepo(repoPath)) {
|
|
172
|
-
throw new Error(
|
|
173
|
-
`Expected a checkout of ${sourceURL} at ${repoPath}, there is none.\n`
|
|
174
|
-
+ `Run \`yarn securessh update\` to put it back.`
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
return { repoPath, sourceURL };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
181
|
let lastRevokeSync = 0;
|
|
181
182
|
|
|
182
183
|
/** Pulled at most once a REVOKE_SYNC_INTERVAL, because this is asked per request. A sync that
|
|
@@ -218,6 +219,17 @@ async function readMachineRevocations(sourceURL: string) {
|
|
|
218
219
|
return revocations;
|
|
219
220
|
}
|
|
220
221
|
|
|
222
|
+
/** What to run to trust a machine that has just been refused.
|
|
223
|
+
|
|
224
|
+
Everything in it is already known to whoever is being refused: their machine id, the address we
|
|
225
|
+
saw them at, and the domain they just talked to. Handing it back saves them working out the
|
|
226
|
+
parts of a command they have every right to know. Running it still takes the hardware key on
|
|
227
|
+
the machine that owns the repo, so telling them costs nothing. */
|
|
228
|
+
export function addMachineCommand(config: { machineId: string; ip: string; domain?: string }) {
|
|
229
|
+
let { machineId, ip, domain } = config;
|
|
230
|
+
return `To trust it, run: yarn addmachine ${domain || "<domain>"} ${machineId} ${ip} git`;
|
|
231
|
+
}
|
|
232
|
+
|
|
221
233
|
/** Sends the one notification this file is allowed to send, when it can.
|
|
222
234
|
|
|
223
235
|
The daemon configures notifications on startup and would simply send. This can also run in some
|
|
@@ -285,7 +297,7 @@ async function recordMachineRevocation(config: {
|
|
|
285
297
|
+ `\n\nIt is frozen everywhere now, and nothing accepts it.`
|
|
286
298
|
+ `\n\nIf this was an attack, remove \`machines/${machineId}.json\` from \`${sourceURL}\` now.`
|
|
287
299
|
+ `\nIf it was legitimate, run \`yarn unrevoke git\` in that repo. It allows ${ip} for that`
|
|
288
|
-
+ ` machine, and takes
|
|
300
|
+
+ ` machine, and takes effect as each machine picks it up.`
|
|
289
301
|
+ `\n\nmachine: \`${machineId}\``
|
|
290
302
|
+ `\nfrozen by: \`${hostLabel}\``
|
|
291
303
|
);
|
|
@@ -299,48 +311,59 @@ async function recordMachineRevocation(config: {
|
|
|
299
311
|
have, is treated as the same kind of event as a stolen ssh key - it is revoked everywhere, and
|
|
300
312
|
stays revoked until an unrevoke allows that machine from that address.
|
|
301
313
|
|
|
314
|
+
Says why when the answer is no, because the three ways to be refused are entirely different
|
|
315
|
+
things to a person reading it: never trusted at all, trusted but frozen, or trusted and talking
|
|
316
|
+
from somewhere it should not be.
|
|
317
|
+
|
|
302
318
|
Throws when the repo itself cannot be read or is not signed, rather than answering false: that
|
|
303
319
|
is a broken installation, not a rejected machine, and the two deserve different handling. */
|
|
304
|
-
export
|
|
305
|
-
|
|
320
|
+
export type MachineVerdict = { accepted: boolean; reason: string };
|
|
321
|
+
|
|
322
|
+
export async function isMachineAccepted(config: {
|
|
323
|
+
machineId: string;
|
|
324
|
+
ip: string;
|
|
325
|
+
// Only used to write a complete command into the refusal. A caller that knows which domain it
|
|
326
|
+
// is answering as saves whoever is refused from having to guess it.
|
|
327
|
+
domain?: string;
|
|
328
|
+
}): Promise<MachineVerdict> {
|
|
329
|
+
let { machineId, ip, domain } = config;
|
|
306
330
|
if (!machineId || !ip) {
|
|
307
331
|
throw new Error(`Expected a machineId and an ip, was ${JSON.stringify(machineId)} and ${JSON.stringify(ip)}`);
|
|
308
332
|
}
|
|
309
|
-
let {
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
await verifyCheckout(repoPath);
|
|
313
|
-
|
|
314
|
-
let machine = (await readMachines(repoPath)).get(machineId);
|
|
333
|
+
let { sourceURL } = await keysRepo();
|
|
334
|
+
// Verified before it is believed, and cached, since this is asked once per request.
|
|
335
|
+
let machine = (await getTrustedMachines()).find(entry => entry.machineId === machineId);
|
|
315
336
|
if (!machine) {
|
|
316
|
-
return false;
|
|
337
|
+
return { accepted: false, reason: `that machine is not trusted. ${addMachineCommand({ machineId, ip, domain })}` };
|
|
317
338
|
}
|
|
318
339
|
|
|
319
340
|
await syncRevocations(sourceURL);
|
|
320
341
|
let unrevokes = await readUnrevokes(sourceURL).catch(() => ({ pairs: new Map(), legacyIds: new Map() }));
|
|
321
|
-
// An unrevoke
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
};
|
|
342
|
+
// An unrevoke counts as soon as it is seen. It is signed, so writing one already takes the
|
|
343
|
+
// hardware key, and anyone holding that can sign anything at all - a wait before honouring it
|
|
344
|
+
// guards against an attacker who has no need of it.
|
|
345
|
+
let allowedAgain = (pair: string) => !!unrevokes.pairs.get(pair)?.length;
|
|
326
346
|
let revocations = await readMachineRevocations(sourceURL);
|
|
327
347
|
// Any revocation nothing has undone keeps the machine out, from everywhere, the way a revoked
|
|
328
348
|
// ssh key is out everywhere rather than only from the address it was misused from.
|
|
329
|
-
let
|
|
349
|
+
let frozenFrom = "";
|
|
330
350
|
for (let revocation of revocations) {
|
|
331
351
|
if (revocation.machineId !== machineId) {
|
|
332
352
|
continue;
|
|
333
353
|
}
|
|
334
|
-
if (!
|
|
335
|
-
|
|
354
|
+
if (!allowedAgain(pairKey({ fingerprint: revocation.machineId, ip: revocation.ip }))) {
|
|
355
|
+
frozenFrom = revocation.ip;
|
|
336
356
|
}
|
|
337
357
|
}
|
|
338
|
-
if (
|
|
339
|
-
return
|
|
358
|
+
if (frozenFrom) {
|
|
359
|
+
return {
|
|
360
|
+
accepted: false,
|
|
361
|
+
reason: `that machine is frozen, it was used from ${frozenFrom} which it is not allowed from`,
|
|
362
|
+
};
|
|
340
363
|
}
|
|
341
364
|
|
|
342
365
|
if (machine.ips.includes(ip)) {
|
|
343
|
-
return true;
|
|
366
|
+
return { accepted: true, reason: "" };
|
|
344
367
|
}
|
|
345
368
|
|
|
346
369
|
// Listed, but talking to us from somewhere it should not be. Recorded once for this machine
|
|
@@ -348,8 +371,9 @@ export async function isMachineAccepted(config: { machineId: string; ip: string
|
|
|
348
371
|
let pair = pairKey({ fingerprint: machineId, ip });
|
|
349
372
|
let alreadyRecorded = revocations.some(revocation =>
|
|
350
373
|
pairKey({ fingerprint: revocation.machineId, ip: revocation.ip }) === pair);
|
|
351
|
-
if (!alreadyRecorded && !
|
|
374
|
+
if (!alreadyRecorded && !allowedAgain(pair)) {
|
|
352
375
|
await recordMachineRevocation({ sourceURL, machineId, ip, hostLabel: os.hostname() });
|
|
376
|
+
return { accepted: false, reason: `that machine is not allowed from ${ip}, so it is now frozen everywhere` };
|
|
353
377
|
}
|
|
354
|
-
return false;
|
|
378
|
+
return { accepted: false, reason: `that machine is not allowed from ${ip}` };
|
|
355
379
|
}
|
|
@@ -47,14 +47,16 @@ export function startTrustServer(config: { port: number }) {
|
|
|
47
47
|
// Not a rejection of the machine, a packet that does not prove anything about one.
|
|
48
48
|
return { status: 400, body: { trusted: false, ip, reason: verified.problem } };
|
|
49
49
|
}
|
|
50
|
-
let
|
|
50
|
+
let verdict = await isMachineAccepted({ machineId: packet.machineId, ip });
|
|
51
51
|
return {
|
|
52
52
|
status: 200,
|
|
53
53
|
body: {
|
|
54
|
-
trusted,
|
|
54
|
+
trusted: verdict.accepted,
|
|
55
55
|
machineId: packet.machineId,
|
|
56
56
|
ip,
|
|
57
|
-
|
|
57
|
+
// Whatever the check itself said. It is the only thing that knows which of the
|
|
58
|
+
// ways to be refused this was.
|
|
59
|
+
reason: verdict.reason,
|
|
58
60
|
},
|
|
59
61
|
};
|
|
60
62
|
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export declare const DEFAULT_WEBHOOK_FILE_PATH = "/etc/portsecure/discord-webhook";
|
|
2
|
+
/** Pulls the webhook URL out of webhook file contents. `sourceName` only appears in errors, so
|
|
3
|
+
callers can name a remote path. */
|
|
4
|
+
export declare function parseWebhookFile(config: {
|
|
5
|
+
contents: string;
|
|
6
|
+
sourceName: string;
|
|
7
|
+
}): string;
|
|
8
|
+
/** Keeps the id and both ends of the token, so a reader can confirm which webhook replaced theirs
|
|
9
|
+
without receiving one they could post to. */
|
|
10
|
+
export declare function redactWebhookURL(webhookURL: string): string;
|
|
11
|
+
export declare function formatWebhookFile(webhookURL: string): string;
|
|
12
|
+
/** When the message was sent, in the sending machine's own time zone. Discord shows when it
|
|
13
|
+
received something, which is not the same thing when a machine has been offline or a send has
|
|
14
|
+
been retried, and the zone matters when the machines are not all in one place. */
|
|
15
|
+
export declare function messageTimestamp(now: Date): string;
|
|
16
|
+
/** Sends to an explicit webhook URL, for tooling that acts on a webhook before (or instead of)
|
|
17
|
+
configureDiscordNotifications - setup, migrations, connectivity checks. */
|
|
18
|
+
export declare function sendToWebhookURL(config: {
|
|
19
|
+
webhookURL: string;
|
|
20
|
+
message: string;
|
|
21
|
+
}): Promise<void>;
|
|
22
|
+
/** Must be called once on startup, before any notification is sent. Aborts the process if the
|
|
23
|
+
webhook file is missing or invalid, then re-checks the file on an interval and warns the old
|
|
24
|
+
webhook whenever it changes. */
|
|
25
|
+
export declare function configureDiscordNotifications(config?: {
|
|
26
|
+
filePath?: string;
|
|
27
|
+
checkInterval?: number;
|
|
28
|
+
}): Promise<{
|
|
29
|
+
filePath: string;
|
|
30
|
+
checkInterval: number;
|
|
31
|
+
}>;
|
|
32
|
+
export declare function sendDiscordNotification(message: string): Promise<void>;
|
|
33
|
+
/** Whether notifications are set up in this process. For code that may run outside the daemon,
|
|
34
|
+
where sending is worth doing if it can be and not worth failing over if it cannot. */
|
|
35
|
+
export declare function areDiscordNotificationsConfigured(): boolean;
|
|
36
|
+
export declare function stopDiscordNotifications(): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";
|
|
1
|
+
"use strict"; var console = (function shimConsole() { let baseConsole = globalThis.console; if (typeof window !== "undefined") return baseConsole; let name = __filename.replaceAll("\\", "/").split("/").pop(); let dirName = __dirname.replaceAll("\\", "/").split("/").pop(); let context = { __FILE__: __filename, __DIR__: __dirname, __NAME__: name, __DIRNAME__: dirName }; let newConsole = { ...baseConsole }; for (let shim of ["log", "info", "warn", "error"]) { newConsole[shim] = (...args) => { args.push(context); baseConsole[shim](...args); }; } newConsole.__context = context; return newConsole; })(); "use strict";
|
|
2
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
3
|
return (mod && mod.__esModule && mod.default) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
@@ -203,4 +203,4 @@ function stopDiscordNotifications() {
|
|
|
203
203
|
}
|
|
204
204
|
exports.stopDiscordNotifications = stopDiscordNotifications;
|
|
205
205
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGlzY29yZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImRpc2NvcmQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBQUEsMkRBQTZCO0FBRTdCLHNIQUFzSDtBQUN0SCxrR0FBa0c7QUFDbEcsOENBQThDO0FBRWpDLFFBQUEseUJBQXlCLEdBQUcsaUNBQWlDLENBQUM7QUFDM0UsTUFBTSxzQkFBc0IsR0FBRyxDQUFDLEdBQUcsRUFBRSxHQUFHLElBQUksQ0FBQztBQUM3QyxNQUFNLHNCQUFzQixHQUFHO0lBQzNCLG1DQUFtQztJQUNuQyxzQ0FBc0M7SUFDdEMsMENBQTBDO0NBQzdDLENBQUM7QUFDRixNQUFNLHFCQUFxQixHQUFHLElBQUksQ0FBQztBQUNuQyxNQUFNLHFCQUFxQixHQUFHLEdBQUcsQ0FBQztBQUNsQyxNQUFNLGlCQUFpQixHQUFHLENBQUMsQ0FBQztBQUM1QixNQUFNLHNCQUFzQixHQUFHLENBQUMsQ0FBQztBQUNqQyxNQUFNLHVCQUF1QixHQUFHLENBQUMsR0FBRyxJQUFJLENBQUM7QUFFekMsSUFBSSxpQkFJUyxDQUFDO0FBRWQsc0ZBQXNGO0FBQ3RGLDRDQUE0QztBQUM1QyxJQUFJLFNBQVMsR0FBcUIsT0FBTyxDQUFDLE9BQU8sRUFBRSxDQUFDO0FBRXBEO3NDQUNzQztBQUN0QyxTQUFnQixnQkFBZ0IsQ0FBQyxNQUFnRDtJQUM3RSxJQUFJLEVBQUUsUUFBUSxFQUFFLFVBQVUsRUFBRSxHQUFHLE1BQU0sQ0FBQztJQUN0QyxJQUFJLEtBQUssR0FBRyxRQUFRLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLElBQUksQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLElBQUksSUFBSSxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztJQUN4RyxJQUFJLFVBQVUsR0FBRyxL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Q29udGVudCI6WyJpbXBvcnQgZnMgZnJvbSBcImZzL3Byb21pc2VzXCI7XG5cbi8vIFBPUlRFRCBDT0RFOiBzZWN1cml0eS9hdXRob3JpemVkS2V5cy9kYWVtb24vcG9ydHNlY3VyZURhZW1vbi5qcyBlbWJlZHMgYSBoYW5kIHBvcnQgb2YgdGhpcyBmaWxlIGluIHBsYWluIEpTLCBzbyB0aGVcbi8vIGRhZW1vbiBjYW4gcnVuIHdpdGggbm8gZGVwZW5kZW5jaWVzLiBUaGUgdHdvIGFyZSBleHBlY3RlZCB0byBiZWhhdmUgaWRlbnRpY2FsbHkgLSBpZiB5b3UgY2hhbmdlXG4vLyBvbmUsIG1ha2UgdGhlIG1hdGNoaW5nIGNoYW5nZSBpbiB0aGUgb3RoZXIuXG5cbmV4cG9ydCBjb25zdCBERUZBVUxUX1dFQkhPT0tfRklMRV9QQVRIID0gXCIvZXRjL3BvcnRzZWN1cmUvZGlzY29yZC13ZWJob29rXCI7XG5jb25zdCBERUZBVUxUX0NIRUNLX0lOVEVSVkFMID0gNSAqIDYwICogMTAwMDtcbmNvbnN0IFZBTElEX1dFQkhPT0tfUFJFRklYRVMgPSBbXG4gICAgXCJodHRwczovL2Rpc2NvcmQuY29tL2FwaS93ZWJob29rcy9cIixcbiAgICBcImh0dHBzOi8vZGlzY29yZGFwcC5jb20vYXBpL3dlYmhvb2tzL1wiLFxuICAgIFwiaHR0cHM6Ly9jYW5hcnkuZGlzY29yZC5jb20vYXBpL3dlYmhvb2tzL1wiLFxuXTtcbmNvbnN0IERJU0NPUkRfTUVTU0FHRV9MSU1JVCA9IDIwMDA7XG5jb25zdCBNQVhfRVJST1JfQk9EWV9MRU5HVEggPSA1MDA7XG5jb25zdCBNQVhfU0VORF9BVFRFTVBUUyA9IDM7XG5jb25zdCBSRURBQ1RFRF9UT0tFTl9WSVNJQkxFID0gODtcbmNvbnN0IERFRkFVTFRfUkFURV9MSU1JVF9XQUlUID0gMiAqIDEwMDA7XG5cbmxldCBub3RpZmljYXRpb25TdGF0ZToge1xuICAgIGZpbGVQYXRoOiBzdHJpbmc7XG4gICAgd2ViaG9va1VSTDogc3RyaW5nO1xuICAgIGNoZWNrVGltZXI6IE5vZGVKUy5UaW1lb3V0O1xufSB8IHVuZGVmaW5lZDtcblxuLy8gRGlzY29yZCB3ZWJob29rcyBhcmUgcmF0ZSBsaW1pdGVkIChyb3VnaGx5IDUgcmVxdWVzdHMgcGVyIDIgc2Vjb25kcyksIHNvIGV2ZXJ5IHNlbmRcbi8vIGdvZXMgdGhyb3VnaCBvbmUgY2hhaW4gaW5zdGVhZCBvZiByYWNpbmcuXG5sZXQgc2VuZENoYWluOiBQcm9taXNlPHVua25vd24+ID0gUHJvbWlzZS5yZXNvbHZlKCk7XG5cbi8qKiBQdWxscyB0aGUgd2ViaG9vayBVUkwgb3V0IG9mIHdlYmhvb2sgZmlsZSBjb250ZW50cy4gYHNvdXJjZU5hbWVgIG9ubHkgYXBwZWFycyBpbiBlcnJvcnMsIHNvXG4gICAgY2FsbGVycyBjYW4gbmFtZSBhIHJlbW90ZSBwYXRoLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHBhcnNlV2ViaG9va0ZpbGUoY29uZmlnOiB7IGNvbnRlbnRzOiBzdHJpbmc7IHNvdXJjZU5hbWU6IHN0cmluZyB9KSB7XG4gICAgbGV0IHsgY29udGVudHMsIHNvdXJjZU5hbWUgfSA9IGNvbmZpZztcbiAgICBsZXQgbGluZXMgPSBjb250ZW50cy5zcGxpdChcIlxcblwiKS5tYXAobGluZSA9PiBsaW5lLnRyaW0oKSkuZmlsdGVyKGxpbmUgPT4gbGluZSAmJiAhbGluZS5zdGFydHNXaXRoKFwiI1wiKSk7XG4gICAgbGV0IHdlYmhvb2tVUkwgPSBsaW5lc1swXTtcbiAgICBpZiAoIXdlYmhvb2tVUkwpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBFeHBlY3RlZCBhIERpc2NvcmQgd2ViaG9vayBVUkwgaW4gJHtzb3VyY2VOYW1lfSwgdGhlIGZpbGUgaGFzIG5vIHVzYWJsZSBsaW5lc2ApO1xuICAgIH1cbiAgICBpZiAoIVZBTElEX1dFQkhPT0tfUFJFRklYRVMuc29tZShwcmVmaXggPT4gd2ViaG9va1VSTC5zdGFydHNXaXRoKHByZWZpeCkpKSB7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihcbiAgICAgICAgICAgIGBFeHBlY3RlZCBhIERpc2NvcmQgd2ViaG9vayBVUkwgc3RhcnRpbmcgd2l0aCBvbmUgb2YgJHtWQUxJRF9XRUJIT09LX1BSRUZJWEVTLmpvaW4oXCIsIFwiKX0sIGBcbiAgICAgICAgICAgICsgYHdhcyAke3dlYmhvb2tVUkwuc2xpY2UoMCwgTUFYX0VSUk9SX0JPRFlfTEVOR1RIKX0gKGluICR7c291cmNlTmFtZX0pYFxuICAgICAgICApO1xuICAgIH1cbiAgICByZXR1cm4gd2ViaG9va1VSTDtcbn1cblxuLyoqIEtlZXBzIHRoZSBpZCBhbmQgYm90aCBlbmRzIG9mIHRoZSB0b2tlbiwgc28gYSByZWFkZXIgY2FuIGNvbmZpcm0gd2hpY2ggd2ViaG9vayByZXBsYWNlZCB0aGVpcnNcbiAgICB3aXRob3V0IHJlY2VpdmluZyBvbmUgdGhleSBjb3VsZCBwb3N0IHRvLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHJlZGFjdFdlYmhvb2tVUkwod2ViaG9va1VSTDogc3RyaW5nKSB7XG4gICAgbGV0IHNlcGFyYXRvckluZGV4ID0gd2ViaG9va1VSTC5sYXN0SW5kZXhPZihcIi9cIik7XG4gICAgbGV0IGJhc2UgPSB3ZWJob29rVVJMLnNsaWNlKDAsIHNlcGFyYXRvckluZGV4ICsgMSk7XG4gICAgbGV0IHRva2VuID0gd2ViaG9va1VSTC5zbGljZShzZXBhcmF0b3JJbmRleCArIDEpO1xuICAgIGlmICh0b2tlbi5sZW5ndGggPD0gUkVEQUNURURfVE9LRU5fVklTSUJMRSAqIDIpIHtcbiAgICAgICAgLy8gVG9vIHNob3J0IHRvIHNob3cgYm90aCBlbmRzIHdpdGhvdXQgZ2l2aW5nIGF3YXkgdGhlIHdob2xlIHRva2VuLlxuICAgICAgICByZXR1cm4gYCR7YmFzZX0ke3Rva2VuLnNsaWNlKDAsIFJFREFDVEVEX1RPS0VOX1ZJU0lCTEUpfS4uLmA7XG4gICAgfVxuICAgIHJldHVybiBgJHtiYXNlfSR7dG9rZW4uc2xpY2UoMCwgUkVEQUNURURfVE9LRU5fVklTSUJMRSl9Li4uJHt0b2tlbi5zbGljZSgtUkVEQUNURURfVE9LRU5fVklTSUJMRSl9YDtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGZvcm1hdFdlYmhvb2tGaWxlKHdlYmhvb2tVUkw6IHN0cmluZykge1xuICAgIHJldHVybiBgIyBwb3J0c2VjdXJlIERpc2NvcmQgd2ViaG9vay4gRmlyc3Qgbm9uLWNvbW1lbnQgbGluZSBpcyB1c2VkLlxcbiR7d2ViaG9va1VSTH1cXG5gO1xufVxuXG5hc3luYyBmdW5jdGlvbiByZWFkV2ViaG9va0ZpbGUoZmlsZVBhdGg6IHN0cmluZykge1xuICAgIGxldCBjb250ZW50cztcbiAgICB0cnkge1xuICAgICAgICBjb250ZW50cyA9IGF3YWl0IGZzLnJlYWRGaWxlKGZpbGVQYXRoLCBcInV0ZjhcIik7XG4gICAgfSBjYXRjaCAoZSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEV4cGVjdGVkIGEgcmVhZGFibGUgRGlzY29yZCB3ZWJob29rIGZpbGUgYXQgJHtmaWxlUGF0aH0sICR7ZX1gKTtcbiAgICB9XG4gICAgcmV0dXJuIHBhcnNlV2ViaG9va0ZpbGUoeyBjb250ZW50cywgc291cmNlTmFtZTogZmlsZVBhdGggfSk7XG59XG5cbi8qKiBXaGVuIHRoZSBtZXNzYWdlIHdhcyBzZW50LCBpbiB0aGUgc2VuZGluZyBtYWNoaW5lJ3Mgb3duIHRpbWUgem9uZS4gRGlzY29yZCBzaG93cyB3aGVuIGl0XG4gICAgcmVjZWl2ZWQgc29tZXRoaW5nLCB3aGljaCBpcyBub3QgdGhlIHNhbWUgdGhpbmcgd2hlbiBhIG1hY2hpbmUgaGFzIGJlZW4gb2ZmbGluZSBvciBhIHNlbmQgaGFzXG4gICAgYmVlbiByZXRyaWVkLCBhbmQgdGhlIHpvbmUgbWF0dGVycyB3aGVuIHRoZSBtYWNoaW5lcyBhcmUgbm90IGFsbCBpbiBvbmUgcGxhY2UuICovXG5leHBvcnQgZnVuY3Rpb24gbWVzc2FnZVRpbWVzdGFtcChub3c6IERhdGUpIHtcbiAgICBsZXQgZGF0ZSA9IG5vdy50b0xvY2FsZURhdGVTdHJpbmcoXCJlbi1DQVwiKTtcbiAgICBsZXQgdGltZSA9IG5vdy50b0xvY2FsZVRpbWVTdHJpbmcoXCJlbi1VU1wiLCB7XG4gICAgICAgIGhvdXI6IFwiMi1kaWdpdFwiLFxuICAgICAgICBtaW51dGU6IFwiMi1kaWdpdFwiLFxuICAgICAgICBzZWNvbmQ6IFwiMi1kaWdpdFwiLFxuICAgICAgICBob3VyMTI6IHRydWUsXG4gICAgICAgIHRpbWVab25lTmFtZTogXCJzaG9ydFwiLFxuICAgIH0pO1xuICAgIHJldHVybiBgJHtkYXRlfSAke3RpbWV9YDtcbn1cblxuYXN5bmMgZnVuY3Rpb24gcG9zdFRvV2ViaG9vayh3ZWJob29rVVJMOiBzdHJpbmcsIG1lc3NhZ2U6IHN0cmluZykge1xuICAgIC8vIFN0YW1wZWQgb25jZSByYXRoZXIgdGhhbiBwZXIgYXR0ZW1wdCwgc28gYSByZXRyeSBzYXlzIHdoZW4gdGhlIHRoaW5nIGhhcHBlbmVkIHJhdGhlciB0aGFuXG4gICAgLy8gd2hlbiB3ZSBsYXN0IG1hbmFnZWQgdG8gZ2V0IGl0IG91dC4gQXQgdGhlIGVuZCwgYmVjYXVzZSB0aGUgZmlyc3QgbGluZSBvZiBhIG1lc3NhZ2UgaXMgdGhlXG4gICAgLy8gbm90aWZpY2F0aW9uIHByZXZpZXcgYW5kIGEgdGltZXN0YW1wIHRoZXJlIHdvdWxkIHNwZW5kIGl0IHNheWluZyBub3RoaW5nLlxuICAgIGxldCBjb250ZW50ID0gYCR7bWVzc2FnZX0gwrcgXFxgJHttZXNzYWdlVGltZXN0YW1wKG5ldyBEYXRlKCkpfVxcYGA7XG4gICAgaWYgKGNvbnRlbnQubGVuZ3RoID4gRElTQ09SRF9NRVNTQUdFX0xJTUlUKSB7XG4gICAgICAgIGNvbnRlbnQgPSBjb250ZW50LnNsaWNlKDAsIERJU0NPUkRfTUVTU0FHRV9MSU1JVCAtIDMpICsgXCIuLi5cIjtcbiAgICB9XG4gICAgZm9yIChsZXQgYXR0ZW1wdCA9IDE7IGF0dGVtcHQgPD0gTUFYX1NFTkRfQVRURU1QVFM7IGF0dGVtcHQrKykge1xuICAgICAgICBsZXQgcmVzcG9uc2UgPSBhd2FpdCBmZXRjaCh3ZWJob29rVVJMLCB7XG4gICAgICAgICAgICBtZXRob2Q6IFwiUE9TVFwiLFxuICAgICAgICAgICAgaGVhZGVyczogeyBcIkNvbnRlbnQtVHlwZVwiOiBcImFwcGxpY2F0aW9uL2pzb25cIiB9LFxuICAgICAgICAgICAgYm9keTogSlNPTi5zdHJpbmdpZnkoeyBjb250ZW50IH0pLFxuICAgICAgICB9KTtcbiAgICAgICAgaWYgKHJlc3BvbnNlLm9rKSB7XG4gICAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cbiAgICAgICAgbGV0IGJvZHkgPSAoYXdhaXQgcmVzcG9uc2UudGV4dCgpKS5zbGljZSgwLCBNQVhfRVJST1JfQk9EWV9MRU5HVEgpO1xuICAgICAgICBpZiAocmVzcG9uc2Uuc3RhdHVzID09PSA0MjkgJiYgYXR0ZW1wdCA8IE1BWF9TRU5EX0FUVEVNUFRTKSB7XG4gICAgICAgICAgICBsZXQgcmV0cnlBZnRlciA9IE51bWJlcihyZXNwb25zZS5oZWFkZXJzLmdldChcInJldHJ5LWFmdGVyXCIpKTtcbiAgICAgICAgICAgIGxldCB3YWl0VGltZSA9IHJldHJ5QWZ0ZXIgJiYgcmV0cnlBZnRlciAqIDEwMDAgfHwgREVGQVVMVF9SQVRFX0xJTUlUX1dBSVQ7XG4gICAgICAgICAgICBhd2FpdCBuZXcgUHJvbWlzZShyZXNvbHZlID0+IHNldFRpbWVvdXQocmVzb2x2ZSwgd2FpdFRpbWUpKTtcbiAgICAgICAgICAgIGNvbnRpbnVlO1xuICAgICAgICB9XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihcbiAgICAgICAgICAgIGBFeHBlY3RlZCBhIDJ4eCByZXNwb25zZSBmcm9tIHRoZSBEaXNjb3JkIHdlYmhvb2ssIHdhcyAke3Jlc3BvbnNlLnN0YXR1c30gJHtyZXNwb25zZS5zdGF0dXNUZXh0fSwgYm9keSAke2JvZHl9YFxuICAgICAgICApO1xuICAgIH1cbn1cblxuZnVuY3Rpb24gcXVldWVTZW5kKHdlYmhvb2tVUkw6IHN0cmluZywgbWVzc2FnZTogc3RyaW5nKSB7XG4gICAgbGV0IHNlbmQgPSAoKSA9PiBwb3N0VG9XZWJob29rKHdlYmhvb2tVUkwsIG1lc3NhZ2UpO1xuICAgIGxldCByZXN1bHQgPSBzZW5kQ2hhaW4udGhlbihzZW5kLCBzZW5kKTtcbiAgICBzZW5kQ2hhaW4gPSByZXN1bHQuY2F0Y2goKCkgPT4gdW5kZWZpbmVkKTtcbiAgICByZXR1cm4gcmVzdWx0O1xufVxuXG4vKiogU2VuZHMgdG8gYW4gZXhwbGljaXQgd2ViaG9vayBVUkwsIGZvciB0b29saW5nIHRoYXQgYWN0cyBvbiBhIHdlYmhvb2sgYmVmb3JlIChvciBpbnN0ZWFkIG9mKVxuICAgIGNvbmZpZ3VyZURpc2NvcmROb3RpZmljYXRpb25zIC0gc2V0dXAsIG1pZ3JhdGlvbnMsIGNvbm5lY3Rpdml0eSBjaGVja3MuICovXG5leHBvcnQgZnVuY3Rpb24gc2VuZFRvV2ViaG9va1VSTChjb25maWc6IHsgd2ViaG9va1VSTDogc3RyaW5nOyBtZXNzYWdlOiBzdHJpbmcgfSkge1xuICAgIHJldHVybiBxdWV1ZVNlbmQoY29uZmlnLndlYmhvb2tVUkwsIGNvbmZpZy5tZXNzYWdlKTtcbn1cblxuYXN5bmMgZnVuY3Rpb24gY2hlY2tXZWJob29rRmlsZUNoYW5nZWQoKSB7XG4gICAgbGV0IHN0YXRlID0gbm90aWZpY2F0aW9uU3RhdGU7XG4gICAgaWYgKCFzdGF0ZSkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGxldCBuZXdXZWJob29rVVJMO1xuICAgIHRyeSB7XG4gICAgICAgIG5ld1dlYmhvb2tVUkwgPSBhd2FpdCByZWFkV2ViaG9va0ZpbGUoc3RhdGUuZmlsZVBhdGgpO1xuICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgY29uc29sZS5lcnJvcihgcG9ydHNlY3VyZTogRGlzY29yZCB3ZWJob29rIGZpbGUgaXMgbm8gbG9uZ2VyIHJlYWRhYmxlLCBzdGlsbCB1c2luZyB0aGUgbG9hZGVkIHdlYmhvb2suICR7ZX1gKTtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBpZiAobmV3V2ViaG9va1VSTCA9PT0gc3RhdGUud2ViaG9va1VSTCkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIC8vIFdhcm4gdGhlIG9sZCBjaGFubmVsIGZpcnN0LCB3aXRoIHRoZSBuZXcgd2ViaG9vayByZWRhY3RlZCwgc28gYSBzdG9sZW4gb2xkIHdlYmhvb2sgZG9lcyBub3RcbiAgICAvLyBoYW5kIHRoZSBhdHRhY2tlciBhIHVzYWJsZSBuZXcgb25lLlxuICAgIHRyeSB7XG4gICAgICAgIGF3YWl0IHF1ZXVlU2VuZChcbiAgICAgICAgICAgIHN0YXRlLndlYmhvb2tVUkwsXG4gICAgICAgICAgICBgKipwb3J0c2VjdXJlKio6ICoqTk9USUZJQ0FUSU9OUyBBUkUgTU9WSU5HIFRPIEFOT1RIRVIgQ0hBTk5FTCoqYFxuICAgICAgICAgICAgKyBgXFxuXFxuVGhpcyBjaGFubmVsIHN0b3BzIHJlY2VpdmluZyB0aGVtIG5vdy4gSWYgdGhhdCB3YXMgbm90IHlvdSwgc29tZWJvZHkgd2l0aCBhY2Nlc3NgXG4gICAgICAgICAgICArIGAgdG8gdGhpcyBtYWNoaW5lIGp1c3QgcmVkaXJlY3RlZCBpdHMgYWxlcnRzLmBcbiAgICAgICAgICAgICsgYFxcblxcbm5ldyB3ZWJob29rOiBcXGAke3JlZGFjdFdlYmhvb2tVUkwobmV3V2ViaG9va1VSTCl9XFxgYFxuICAgICAgICAgICAgKyBgXFxuc2V0IGluOiBcXGAke3N0YXRlLmZpbGVQYXRofVxcYGBcbiAgICAgICAgKTtcbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgIGNvbnNvbGUuZXJyb3IoYHBvcnRzZWN1cmU6IGZhaWxlZCB0byB3YXJuIHRoZSBvbGQgRGlzY29yZCB3ZWJob29rIGFib3V0IHRoZSBjaGFuZ2UuICR7ZX1gKTtcbiAgICB9XG4gICAgc3RhdGUud2ViaG9va1VSTCA9IG5ld1dlYmhvb2tVUkw7XG59XG5cbi8qKiBNdXN0IGJlIGNhbGxlZCBvbmNlIG9uIHN0YXJ0dXAsIGJlZm9yZSBhbnkgbm90aWZpY2F0aW9uIGlzIHNlbnQuIEFib3J0cyB0aGUgcHJvY2VzcyBpZiB0aGVcbiAgICB3ZWJob29rIGZpbGUgaXMgbWlzc2luZyBvciBpbnZhbGlkLCB0aGVuIHJlLWNoZWNrcyB0aGUgZmlsZSBvbiBhbiBpbnRlcnZhbCBhbmQgd2FybnMgdGhlIG9sZFxuICAgIHdlYmhvb2sgd2hlbmV2ZXIgaXQgY2hhbmdlcy4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBjb25maWd1cmVEaXNjb3JkTm90aWZpY2F0aW9ucyhjb25maWc/OiB7XG4gICAgZmlsZVBhdGg/OiBzdHJpbmc7XG4gICAgY2hlY2tJbnRlcnZhbD86IG51bWJlcjtcbn0pIHtcbiAgICBpZiAobm90aWZpY2F0aW9uU3RhdGUpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBFeHBlY3RlZCBjb25maWd1cmVEaXNjb3JkTm90aWZpY2F0aW9ucyB0byBiZSBjYWxsZWQgb25jZSwgd2FzIGNhbGxlZCBhZ2FpbiAoYWxyZWFkeSB1c2luZyAke25vdGlmaWNhdGlvblN0YXRlLmZpbGVQYXRofSlgKTtcbiAgICB9XG4gICAgbGV0IGZpbGVQYXRoID0gY29uZmlnPy5maWxlUGF0aCB8fCBERUZBVUxUX1dFQkhPT0tfRklMRV9QQVRIO1xuICAgIGxldCBjaGVja0ludGVydmFsID0gY29uZmlnPy5jaGVja0ludGVydmFsIHx8IERFRkFVTFRfQ0hFQ0tfSU5URVJWQUw7XG4gICAgbGV0IHdlYmhvb2tVUkw7XG4gICAgdHJ5IHtcbiAgICAgICAgd2ViaG9va1VSTCA9IGF3YWl0IHJlYWRXZWJob29rRmlsZShmaWxlUGF0aCk7XG4gICAgfSBjYXRjaCAoZSkge1xuICAgICAgICBjb25zb2xlLmVycm9yKGBwb3J0c2VjdXJlOiByZWZ1c2luZyB0byBzdGFydCB3aXRob3V0IGEgdmFsaWQgRGlzY29yZCB3ZWJob29rIGZpbGUuXFxuJHtlfWApO1xuICAgICAgICBwcm9jZXNzLmV4aXQoMSk7XG4gICAgfVxuICAgIGxldCBjaGVja1RpbWVyID0gc2V0SW50ZXJ2YWwoKCkgPT4ge1xuICAgICAgICBjaGVja1dlYmhvb2tGaWxlQ2hhbmdlZCgpLmNhdGNoKGUgPT4gY29uc29sZS5lcnJvcihgcG9ydHNlY3VyZTogRGlzY29yZCB3ZWJob29rIGZpbGUgY2hlY2sgZmFpbGVkLiAke2V9YCkpO1xuICAgIH0sIGNoZWNrSW50ZXJ2YWwpO1xuICAgIC8vIFRoZSBjaGVjayBzaG91bGQgbmV2ZXIgYmUgdGhlIHJlYXNvbiB0aGUgcHJvY2VzcyBzdGF5cyBhbGl2ZS5cbiAgICBjaGVja1RpbWVyLnVucmVmKCk7XG4gICAgbm90aWZpY2F0aW9uU3RhdGUgPSB7IGZpbGVQYXRoLCB3ZWJob29rVVJMLCBjaGVja1RpbWVyIH07XG4gICAgcmV0dXJuIHsgZmlsZVBhdGgsIGNoZWNrSW50ZXJ2YWwgfTtcbn1cblxuLy8gRE8gTk9UIGFkZCBuZXcgY2FsbHMgdG8gdGhpcy4gRXZlcnkgbWVzc2FnZSBnb2VzIHRvIGEgcmVhbCBEaXNjb3JkIHNlcnZlciBzb21lb25lIHJlYWRzLCBzbyBhXG4vLyBub3RpZmljYXRpb24gaXMgb25seSBldmVyIGFkZGVkIHdoZW4gdGhlIHVzZXIgZXhwbGljaXRseSBhc2tzIGZvciB0aGF0IHNwZWNpZmljIGNhc2UuIFN0YXJ0dXAsXG4vLyBzdWNjZXNzLCBlcnJvcnMsIHJldHJpZXMgYW5kIHJlY292ZXJpZXMgYWxsIGJlbG9uZyBpbiBhIGxvZyBpbnN0ZWFkLlxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIHNlbmREaXNjb3JkTm90aWZpY2F0aW9uKG1lc3NhZ2U6IHN0cmluZykge1xuICAgIGxldCBzdGF0ZSA9IG5vdGlmaWNhdGlvblN0YXRlO1xuICAgIGlmICghc3RhdGUpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBFeHBlY3RlZCBjb25maWd1cmVEaXNjb3JkTm90aWZpY2F0aW9ucyB0byBiZSBjYWxsZWQgYmVmb3JlIHNlbmRpbmcsIHdhcyBjYWxsZWQgd2l0aCBtZXNzYWdlICR7bWVzc2FnZS5zbGljZSgwLCBNQVhfRVJST1JfQk9EWV9MRU5HVEgpfWApO1xuICAgIH1cbiAgICBhd2FpdCBxdWV1ZVNlbmQoc3RhdGUud2ViaG9va1VSTCwgbWVzc2FnZSk7XG59XG5cbi8qKiBXaGV0aGVyIG5vdGlmaWNhdGlvbnMgYXJlIHNldCB1cCBpbiB0aGlzIHByb2Nlc3MuIEZvciBjb2RlIHRoYXQgbWF5IHJ1biBvdXRzaWRlIHRoZSBkYWVtb24sXG4gICAgd2hlcmUgc2VuZGluZyBpcyB3b3J0aCBkb2luZyBpZiBpdCBjYW4gYmUgYW5kIG5vdCB3b3J0aCBmYWlsaW5nIG92ZXIgaWYgaXQgY2Fubm90LiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGFyZURpc2NvcmROb3RpZmljYXRpb25zQ29uZmlndXJlZCgpIHtcbiAgICByZXR1cm4gISFub3RpZmljYXRpb25TdGF0ZTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHN0b3BEaXNjb3JkTm90aWZpY2F0aW9ucygpIHtcbiAgICBpZiAoIW5vdGlmaWNhdGlvblN0YXRlKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgY2xlYXJJbnRlcnZhbChub3RpZmljYXRpb25TdGF0ZS5jaGVja1RpbWVyKTtcbiAgICBub3RpZmljYXRpb25TdGF0ZSA9IHVuZGVmaW5lZDtcbn1cbiJdfQ==
|
|
206
|
-
/* _JS_SOURCE_HASH = "
|
|
206
|
+
/* _JS_SOURCE_HASH = "a4fe18f537fa6da028d1607b16331b146697f9fabb96c6de058f0b41c5553b17"; */
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
|
|
3
|
+
//exports.ensureRemoteWebhook = exports.REPLACE_KEYWORD = void 0;
|
|
4
|
+
const discord_1 = require("./discord");
|
|
5
|
+
const remoteSSH_1 = require("../helpers/remoteSSH");
|
|
6
|
+
exports.REPLACE_KEYWORD = "replace";
|
|
7
|
+
const UNPARSEABLE_LABEL = "(unparseable)";
|
|
8
|
+
/** Puts a webhook on a remote host, refusing to clobber a different existing one unless asked.
|
|
9
|
+
Returns what happened, so callers can decide whether to keep going. */
|
|
10
|
+
async function ensureRemoteWebhook(config) {
|
|
11
|
+
let { host, webhookURL, replace } = config;
|
|
12
|
+
let filePath = config.filePath || discord_1.DEFAULT_WEBHOOK_FILE_PATH;
|
|
13
|
+
let replacedURL = "";
|
|
14
|
+
let existingContents = await (0, remoteSSH_1.readRemoteFile)({ host, filePath });
|
|
15
|
+
if (existingContents) {
|
|
16
|
+
// A corrupt existing file still counts as a conflict, so "replace" can repair it instead
|
|
17
|
+
// of the setup dead ending on a parse error.
|
|
18
|
+
let existingURL = UNPARSEABLE_LABEL;
|
|
19
|
+
try {
|
|
20
|
+
existingURL = (0, discord_1.parseWebhookFile)({ contents: existingContents, sourceName: `${host}:${filePath}` });
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
console.error(`${host}:${filePath} exists but could not be parsed. ${e}`);
|
|
24
|
+
}
|
|
25
|
+
if (existingURL === webhookURL) {
|
|
26
|
+
return { outcome: "unchanged", filePath };
|
|
27
|
+
}
|
|
28
|
+
if (!replace) {
|
|
29
|
+
throw new Error(`Expected no conflicting webhook on ${host}, but ${filePath} already holds a different one.\n`
|
|
30
|
+
+ `Existing: ${existingURL}\n`
|
|
31
|
+
+ `New: ${webhookURL}\n`
|
|
32
|
+
+ `Pass "${exports.REPLACE_KEYWORD}" to overwrite it.`);
|
|
33
|
+
}
|
|
34
|
+
replacedURL = existingURL;
|
|
35
|
+
}
|
|
36
|
+
await (0, remoteSSH_1.writeRemoteFile)({
|
|
37
|
+
host,
|
|
38
|
+
filePath,
|
|
39
|
+
contents: (0, discord_1.formatWebhookFile)(webhookURL),
|
|
40
|
+
fileMode: "600",
|
|
41
|
+
directoryMode: "700",
|
|
42
|
+
});
|
|
43
|
+
let writtenContents = await (0, remoteSSH_1.readRemoteFile)({ host, filePath });
|
|
44
|
+
if (!writtenContents) {
|
|
45
|
+
throw new Error(`Expected ${filePath} to exist on ${host} after writing, no such file exists`);
|
|
46
|
+
}
|
|
47
|
+
let writtenURL = (0, discord_1.parseWebhookFile)({ contents: writtenContents, sourceName: `${host}:${filePath}` });
|
|
48
|
+
if (writtenURL !== webhookURL) {
|
|
49
|
+
throw new Error(`Expected ${host}:${filePath} to hold the new webhook, was ${writtenURL}`);
|
|
50
|
+
}
|
|
51
|
+
if (replacedURL && replacedURL !== UNPARSEABLE_LABEL) {
|
|
52
|
+
// The new webhook is redacted, so this channel can identify the replacement without
|
|
53
|
+
// receiving a webhook it could post to.
|
|
54
|
+
try {
|
|
55
|
+
await (0, discord_1.sendToWebhookURL)({
|
|
56
|
+
webhookURL: replacedURL,
|
|
57
|
+
message: `**portsecure**: the Discord webhook for \`${host}\` was replaced with`
|
|
58
|
+
+ ` \`${(0, discord_1.redactWebhookURL)(webhookURL)}\`.`
|
|
59
|
+
+ ` This channel will stop receiving notifications for that host.`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
// The replacement already happened, so a dead old webhook must not fail the setup.
|
|
64
|
+
console.error(`Could not notify the old webhook that it was replaced. ${e}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Failing here means the file is in place but the webhook itself does not work, which is
|
|
68
|
+
// exactly what the operator needs to hear about.
|
|
69
|
+
await (0, discord_1.sendToWebhookURL)({
|
|
70
|
+
webhookURL,
|
|
71
|
+
message: `**portsecure**: notifications are now configured for \`${host}\`.`
|
|
72
|
+
+ ` This channel will receive its security notifications.`,
|
|
73
|
+
});
|
|
74
|
+
return { outcome: replacedURL && "replaced" || "created", filePath };
|
|
75
|
+
}
|
|
76
|
+
exports.ensureRemoteWebhook = ensureRemoteWebhook;
|
|
77
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVtb3RlV2ViaG9vay5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInJlbW90ZVdlYmhvb2sudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEsdUNBQStIO0FBQy9ILG9EQUF1RTtBQUUxRCxRQUFBLGVBQWUsR0FBRyxTQUFTLENBQUM7QUFDekMsTUFBTSxpQkFBaUIsR0FBRyxlQUFlLENBQUM7QUFFMUM7MEVBQzBFO0FBQ25FLEtBQUssVUFBVSxtQkFBbUIsQ0FBQyxNQUt6QztJQUNHLElBQUksRUFBRSxJQUFJLEVBQUUsVUFBVSxFQUFFLE9BQU8sRUFBRSxHQUFHLE1BQU0sQ0FBQztJQUMzQyxJQUFJLFFBQVEsR0FBRyxNQUFNLENBQUMsUUFBUSxJQUFJLG1DQUF5QixDQUFDO0lBRTVELElBQUksV0FBVyxHQUFHLEVBQUUsQ0FBQztJQUNyQixJQUFJLGdCQUFnQixHQUFHLE1BQU0sSUFBQSwwQkFBYyxFQUFDLEVBQUUsSUFBSSxFQUFFLFFBQVEsRUFBRSxDQUFDLENBQUM7SUFDaEUsSUFBSSxnQkFBZ0IsRUFBRSxDQUFDO1FBQ25CLHlGQUF5RjtRQUN6Riw2Q0FBNkM7UUFDN0MsSUFBSSxXQUFXLEdBQUcsaUJBQWlCLENBQUM7UUFDcEMsSUFBSSxDQUFDO1lBQ0QsV0FBVyxHQUFHLElBQUEsMEJBQWdCLEVBQUMsRUFBRSxRQUFRLEVBQUUsZ0JBQWdCLEVBQUUsVUFBVSxFQUFFLEdBQUcsSUFBSSxJQUFJLFFBQVEsRUFBRSxFQUFFLENBQUMsQ0FBQztRQUN0RyxDQUFDO1FBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQztZQUNULE9BQU8sQ0FBQyxLQUFLLENBQUMsR0FBRyxJQUFJLElBQUksUUFBUSxvQ0FBb0MsQ0FBQyxFQUFFLENBQUMsQ0FBQztRQUM5RSxDQUFDO1FBQ0QsSUFBSSxXQUFXLEtBQUssVUFBVSxFQUFFLENBQUM7WUFDN0IsT0FBTyxFQUFFLE9BQU8sRUFBRSxXQUFvQixFQUFFLFFBQVEsRUFBRSxDQUFDO1FBQ3ZELENBQUM7UUFDRCxJQUFJLENBQUMsT0FBTyxFQUFFLENBQUM7WUFDWCxNQUFNLElBQUksS0FBSyxDQUNYLHNDQUFzQyxJQUFJLFNBQVMsUUFBUSxtQ0FBbUM7a0JBQzVGLGFBQWEsV0FBVyxJQUFJO2tCQUM1QixhQUFhLFVBQVUsSUFBSTtrQkFDM0IsU0FBUyx1QkFBZSxvQkFBb0IsQ0FDakQsQ0FBQztRQUNOLENBQUM7UUFDRCxXQUFXLEdBQUcsV0FBVyxDQUFDO0lBQzlCLENBQUM7SUFFRCxNQUFNLElBQUEsMkJBQWUsRUFBQztRQUNsQixJQUFJO1FBQ0osUUFBUTtRQUNSLFFBQVEsRUFBRSxJQUFBLDJCQUFpQixFQUFDLFVBQVUsQ0FBQztRQUN2QyxRQUFRLEVBQUUsS0FBSztRQUNmLGFBQWEsRUFBRSxLQUFLO0tBQ3ZCLENBQUMsQ0FBQztJQUVILElBQUksZUFBZSxHQUFHLE1BQU0sSUFBQSwwQkFBYyxFQUFDLEVBQUUsSUFBSSxFQUFFLFFBQVEsRUFBRSxDQUFDLENBQUM7SUFDL0QsSUFBSSxDQUFDLGVBQWUsRUFBRSxDQUFDO1FBQ25CLE1BQU0sSUFBSSxLQUFLLENBQUMsWUFBWSxRQUFRLGdCQUFnQixJQUFJLHFDQUFxQyxDQUFDLENBQUM7SUFDbkcsQ0FBQztJQUNELElBQUksVUFBVSxHQUFHLElBQUEsMEJBQWdCLEVBQUMsRUFBRSxRQUFRLEVBQUUsZUFBZSxFQUFFLFVBQVUsRUFBRSxHQUFHLElBQUksSUFBSSxRQUFRLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDcEcsSUFBSSxVQUFVLEtBQUssVUFBVSxFQUFFLENBQUM7UUFDNUIsTUFBTSxJQUFJLEtBQUssQ0FBQyxZQUFZLElBQUksSUFBSSxRQUFRLGlDQUFpQyxVQUFVLEVBQUUsQ0FBQyxDQUFDO0lBQy9GLENBQUM7SUFFRCxJQUFJLFdBQVcsSUFBSSxXQUFXLEtBQUssaUJBQWlCLEVBQUUsQ0FBQztRQUNuRCxvRkFBb0Y7UUFDcEYsd0NBQXdDO1FBQ3hDLElBQUksQ0FBQztZQUNELE1BQU0sSUFBQSwwQkFBZ0IsRUFBQztnQkFDbkIsVUFBVSxFQUFFLFdBQVc7Z0JBQ3ZCLE9BQU8sRUFBRSw2Q0FBNkMsSUFBSSxzQkFBc0I7c0JBQzFFLE1BQU0sSUFBQSwwQkFBZ0IsRUFBQyxVQUFVLENBQUMsS0FBSztzQkFDdkMsZ0VBQWdFO2FBQ3pFLENBQUMsQ0FBQztRQUNQLENBQUM7UUFBQyxPQUFPLENBQUMsRUFBRSxDQUFDO1lBQ1QsbUZBQW1GO1lBQ25GLE9BQU8sQ0FBQyxLQUFLLENBQUMsMERBQTBELENBQUMsRUFBRSxDQUFDLENBQUM7UUFDakYsQ0FBQztJQUNMLENBQUM7SUFFRCx5RkFBeUY7SUFDekYsaURBQWlEO0lBQ2pELE1BQU0sSUFBQSwwQkFBZ0IsRUFBQztRQUNuQixVQUFVO1FBQ1YsT0FBTyxFQUFFLDBEQUEwRCxJQUFJLEtBQUs7Y0FDdEUsd0RBQXdEO0tBQ2pFLENBQUMsQ0FBQztJQUVILE9BQU8sRUFBRSxPQUFPLEVBQUUsV0FBVyxJQUFJLFVBQW1CLElBQUksU0FBa0IsRUFBRSxRQUFRLEVBQUUsQ0FBQztBQUMzRixDQUFDO0FBNUVELGtEQTRFQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IERFRkFVTFRfV0VCSE9PS19GSUxFX1BBVEgsIGZvcm1hdFdlYmhvb2tGaWxlLCBwYXJzZVdlYmhvb2tGaWxlLCByZWRhY3RXZWJob29rVVJMLCBzZW5kVG9XZWJob29rVVJMIH0gZnJvbSBcIi4vZGlzY29yZFwiO1xuaW1wb3J0IHsgcmVhZFJlbW90ZUZpbGUsIHdyaXRlUmVtb3RlRmlsZSB9IGZyb20gXCIuLi9oZWxwZXJzL3JlbW90ZVNTSFwiO1xuXG5leHBvcnQgY29uc3QgUkVQTEFDRV9LRVlXT1JEID0gXCJyZXBsYWNlXCI7XG5jb25zdCBVTlBBUlNFQUJMRV9MQUJFTCA9IFwiKHVucGFyc2VhYmxlKVwiO1xuXG4vKiogUHV0cyBhIHdlYmhvb2sgb24gYSByZW1vdGUgaG9zdCwgcmVmdXNpbmcgdG8gY2xvYmJlciBhIGRpZmZlcmVudCBleGlzdGluZyBvbmUgdW5sZXNzIGFza2VkLlxuICAgIFJldHVybnMgd2hhdCBoYXBwZW5lZCwgc28gY2FsbGVycyBjYW4gZGVjaWRlIHdoZXRoZXIgdG8ga2VlcCBnb2luZy4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBlbnN1cmVSZW1vdGVXZWJob29rKGNvbmZpZzoge1xuICAgIGhvc3Q6IHN0cmluZztcbiAgICB3ZWJob29rVVJMOiBzdHJpbmc7XG4gICAgcmVwbGFjZTogYm9vbGVhbjtcbiAgICBmaWxlUGF0aD86IHN0cmluZztcbn0pIHtcbiAgICBsZXQgeyBob3N0LCB3ZWJob29rVVJMLCByZXBsYWNlIH0gPSBjb25maWc7XG4gICAgbGV0IGZpbGVQYXRoID0gY29uZmlnLmZpbGVQYXRoIHx8IERFRkFVTFRfV0VCSE9PS19GSUxFX1BBVEg7XG5cbiAgICBsZXQgcmVwbGFjZWRVUkwgPSBcIlwiO1xuICAgIGxldCBleGlzdGluZ0NvbnRlbnRzID0gYXdhaXQgcmVhZFJlbW90ZUZpbGUoeyBob3N0LCBmaWxlUGF0aCB9KTtcbiAgICBpZiAoZXhpc3RpbmdDb250ZW50cykge1xuICAgICAgICAvLyBBIGNvcnJ1cHQgZXhpc3RpbmcgZmlsZSBzdGlsbCBjb3VudHMgYXMgYSBjb25mbGljdCwgc28gXCJyZXBsYWNlXCIgY2FuIHJlcGFpciBpdCBpbnN0ZWFkXG4gICAgICAgIC8vIG9mIHRoZSBzZXR1cCBkZWFkIGVuZGluZyBvbiBhIHBhcnNlIGVycm9yLlxuICAgICAgICBsZXQgZXhpc3RpbmdVUkwgPSBVTlBBUlNFQUJMRV9MQUJFTDtcbiAgICAgICAgdHJ5IHtcbiAgICAgICAgICAgIGV4aXN0aW5nVVJMID0gcGFyc2VXZWJob29rRmlsZSh7IGNvbnRlbnRzOiBleGlzdGluZ0NvbnRlbnRzLCBzb3VyY2VOYW1lOiBgJHtob3N0fToke2ZpbGVQYXRofWAgfSk7XG4gICAgICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgICAgIGNvbnNvbGUuZXJyb3IoYCR7aG9zdH06JHtmaWxlUGF0aH0gZXhpc3RzIGJ1dCBjb3VsZCBub3QgYmUgcGFyc2VkLiAke2V9YCk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKGV4aXN0aW5nVVJMID09PSB3ZWJob29rVVJMKSB7XG4gICAgICAgICAgICByZXR1cm4geyBvdXRjb21lOiBcInVuY2hhbmdlZFwiIGFzIGNvbnN0LCBmaWxlUGF0aCB9O1xuICAgICAgICB9XG4gICAgICAgIGlmICghcmVwbGFjZSkge1xuICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKFxuICAgICAgICAgICAgICAgIGBFeHBlY3RlZCBubyBjb25mbGljdGluZyB3ZWJob29rIG9uICR7aG9zdH0sIGJ1dCAke2ZpbGVQYXRofSBhbHJlYWR5IGhvbGRzIGEgZGlmZmVyZW50IG9uZS5cXG5gXG4gICAgICAgICAgICAgICAgKyBgRXhpc3Rpbmc6ICR7ZXhpc3RpbmdVUkx9XFxuYFxuICAgICAgICAgICAgICAgICsgYE5ldzogICAgICAke3dlYmhvb2tVUkx9XFxuYFxuICAgICAgICAgICAgICAgICsgYFBhc3MgXCIke1JFUExBQ0VfS0VZV09SRH1cIiB0byBvdmVyd3JpdGUgaXQuYFxuICAgICAgICAgICAgKTtcbiAgICAgICAgfVxuICAgICAgICByZXBsYWNlZFVSTCA9IGV4aXN0aW5nVVJMO1xuICAgIH1cblxuICAgIGF3YWl0IHdyaXRlUmVtb3RlRmlsZSh7XG4gICAgICAgIGhvc3QsXG4gICAgICAgIGZpbGVQYXRoLFxuICAgICAgICBjb250ZW50czogZm9ybWF0V2ViaG9va0ZpbGUod2ViaG9va1VSTCksXG4gICAgICAgIGZpbGVNb2RlOiBcIjYwMFwiLFxuICAgICAgICBkaXJlY3RvcnlNb2RlOiBcIjcwMFwiLFxuICAgIH0pO1xuXG4gICAgbGV0IHdyaXR0ZW5Db250ZW50cyA9IGF3YWl0IHJlYWRSZW1vdGVGaWxlKHsgaG9zdCwgZmlsZVBhdGggfSk7XG4gICAgaWYgKCF3cml0dGVuQ29udGVudHMpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBFeHBlY3RlZCAke2ZpbGVQYXRofSB0byBleGlzdCBvbiAke2hvc3R9IGFmdGVyIHdyaXRpbmcsIG5vIHN1Y2ggZmlsZSBleGlzdHNgKTtcbiAgICB9XG4gICAgbGV0IHdyaXR0ZW5VUkwgPSBwYXJzZVdlYmhvb2tGaWxlKHsgY29udGVudHM6IHdyaXR0ZW5Db250ZW50cywgc291cmNlTmFtZTogYCR7aG9zdH06JHtmaWxlUGF0aH1gIH0pO1xuICAgIGlmICh3cml0dGVuVVJMICE9PSB3ZWJob29rVVJMKSB7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihgRXhwZWN0ZWQgJHtob3N0fToke2ZpbGVQYXRofSB0byBob2xkIHRoZSBuZXcgd2ViaG9vaywgd2FzICR7d3JpdHRlblVSTH1gKTtcbiAgICB9XG5cbiAgICBpZiAocmVwbGFjZWRVUkwgJiYgcmVwbGFjZWRVUkwgIT09IFVOUEFSU0VBQkxFX0xBQkVMKSB7XG4gICAgICAgIC8vIFRoZSBuZXcgd2ViaG9vayBpcyByZWRhY3RlZCwgc28gdGhpcyBjaGFubmVsIGNhbiBpZGVudGlmeSB0aGUgcmVwbGFjZW1lbnQgd2l0aG91dFxuICAgICAgICAvLyByZWNlaXZpbmcgYSB3ZWJob29rIGl0IGNvdWxkIHBvc3QgdG8uXG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgICBhd2FpdCBzZW5kVG9XZWJob29rVVJMKHtcbiAgICAgICAgICAgICAgICB3ZWJob29rVVJMOiByZXBsYWNlZFVSTCxcbiAgICAgICAgICAgICAgICBtZXNzYWdlOiBgKipwb3J0c2VjdXJlKio6IHRoZSBEaXNjb3JkIHdlYmhvb2sgZm9yIFxcYCR7aG9zdH1cXGAgd2FzIHJlcGxhY2VkIHdpdGhgXG4gICAgICAgICAgICAgICAgICAgICsgYCBcXGAke3JlZGFjdFdlYmhvb2tVUkwod2ViaG9va1VSTCl9XFxgLmBcbiAgICAgICAgICAgICAgICAgICAgKyBgIFRoaXMgY2hhbm5lbCB3aWxsIHN0b3AgcmVjZWl2aW5nIG5vdGlmaWNhdGlvbnMgZm9yIHRoYXQgaG9zdC5gLFxuICAgICAgICAgICAgfSk7XG4gICAgICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgICAgIC8vIFRoZSByZXBsYWNlbWVudCBhbHJlYWR5IGhhcHBlbmVkLCBzbyBhIGRlYWQgb2xkIHdlYmhvb2sgbXVzdCBub3QgZmFpbCB0aGUgc2V0dXAuXG4gICAgICAgICAgICBjb25zb2xlLmVycm9yKGBDb3VsZCBub3Qgbm90aWZ5IHRoZSBvbGQgd2ViaG9vayB0aGF0IGl0IHdhcyByZXBsYWNlZC4gJHtlfWApO1xuICAgICAgICB9XG4gICAgfVxuXG4gICAgLy8gRmFpbGluZyBoZXJlIG1lYW5zIHRoZSBmaWxlIGlzIGluIHBsYWNlIGJ1dCB0aGUgd2ViaG9vayBpdHNlbGYgZG9lcyBub3Qgd29yaywgd2hpY2ggaXNcbiAgICAvLyBleGFjdGx5IHdoYXQgdGhlIG9wZXJhdG9yIG5lZWRzIHRvIGhlYXIgYWJvdXQuXG4gICAgYXdhaXQgc2VuZFRvV2ViaG9va1VSTCh7XG4gICAgICAgIHdlYmhvb2tVUkwsXG4gICAgICAgIG1lc3NhZ2U6IGAqKnBvcnRzZWN1cmUqKjogbm90aWZpY2F0aW9ucyBhcmUgbm93IGNvbmZpZ3VyZWQgZm9yIFxcYCR7aG9zdH1cXGAuYFxuICAgICAgICAgICAgKyBgIFRoaXMgY2hhbm5lbCB3aWxsIHJlY2VpdmUgaXRzIHNlY3VyaXR5IG5vdGlmaWNhdGlvbnMuYCxcbiAgICB9KTtcblxuICAgIHJldHVybiB7IG91dGNvbWU6IHJlcGxhY2VkVVJMICYmIFwicmVwbGFjZWRcIiBhcyBjb25zdCB8fCBcImNyZWF0ZWRcIiBhcyBjb25zdCwgZmlsZVBhdGggfTtcbn1cbiJdfQ==
|
|
78
|
+
/* _JS_SOURCE_HASH = "7383d5b5b9fee5c3deb4ea3e75d9c556e64aef84ab8d71e7683a02bc87c5070a"; */
|