sliftutils 1.7.125 → 1.7.127
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/bin/derivekey.js +10 -0
- package/bin/portsecuredaemon.js +13 -0
- package/bin/testnotify.js +9 -0
- package/bin/unrevoke.js +9 -0
- package/package.json +10 -3
- package/security/README.md +66 -2
- package/security/authorizedKeys/authorizedKeys.ts +68 -5
- package/security/authorizedKeys/daemon/authLog.ts +186 -0
- package/security/authorizedKeys/daemon/changes.ts +26 -0
- package/security/authorizedKeys/daemon/daemon.ts +335 -0
- package/security/authorizedKeys/daemon/git.ts +119 -0
- package/security/authorizedKeys/daemon/notify.ts +25 -0
- package/security/authorizedKeys/daemon/paths.ts +26 -0
- package/security/authorizedKeys/daemon/portsecure.service +3 -2
- package/security/authorizedKeys/daemon/revocation.ts +329 -0
- package/security/authorizedKeys/daemon/rootKeys.ts +198 -0
- package/security/authorizedKeys/daemon/sessions.ts +139 -0
- package/security/authorizedKeys/daemon/sshdConfig.ts +85 -0
- package/security/authorizedKeys/daemon/state.ts +122 -0
- package/security/authorizedKeys/daemon/trust.ts +291 -0
- package/security/authorizedKeys/daemon/userKeys.ts +76 -0
- package/security/authorizedKeys/dist/authorizedKeys.ts.cache +111 -0
- package/security/authorizedKeys/dist/revokeSource.ts.cache +44 -0
- package/security/authorizedKeys/dist/secureSSH.ts.cache +539 -0
- package/security/authorizedKeys/dist/sources.ts.cache +24 -0
- package/security/authorizedKeys/dist/unrevoke.ts.cache +167 -0
- package/security/authorizedKeys/revokeSource.ts +40 -0
- package/security/authorizedKeys/secureSSH.ts +240 -38
- package/security/authorizedKeys/unrevoke.ts +175 -0
- package/security/helpers/dist/paths.ts.cache +28 -0
- package/security/helpers/dist/remoteSSH.ts.cache +90 -0
- package/security/helpers/dist/spawn.ts.cache +34 -0
- package/security/helpers/remoteSSH.ts +18 -6
- package/security/helpers/spawn.ts +5 -1
- package/security/keys/deriveKey.ts +72 -0
- package/security/keys/dist/deriveKey.ts.cache +72 -0
- package/security/keys/dist/sshKeyFile.ts.cache +153 -0
- package/security/keys/sshKeyFile.ts +156 -0
- package/security/notifications/discord.ts +18 -1
- package/security/notifications/dist/discord.ts.cache +197 -0
- package/security/notifications/setupNotify.ts +17 -8
- package/security/notifications/testNotify.ts +48 -0
- package/security/signedFiles/dist/manifest.ts.cache +85 -0
- package/security/signedFiles/dist/signFiles.ts.cache +181 -0
- package/security/signedFiles/manifest.ts +38 -8
- package/security/signedFiles/signFiles.ts +123 -49
- package/security/authorizedKeys/daemon/portsecureDaemon.js +0 -1032
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
|
|
3
|
+
// OpenSSH keeps private keys in its own container rather than anything node can read, so the few
|
|
4
|
+
// bytes of it we need are handled here. Only unencrypted ed25519 keys are supported, which is what
|
|
5
|
+
// a key used by automation is.
|
|
6
|
+
|
|
7
|
+
const MAGIC = "openssh-key-v1\0";
|
|
8
|
+
const KEY_TYPE = "ssh-ed25519";
|
|
9
|
+
const NONE = "none";
|
|
10
|
+
const SEED_LENGTH = 32;
|
|
11
|
+
const PUBLIC_LENGTH = 32;
|
|
12
|
+
// The one block size that matters here, since nothing is encrypted.
|
|
13
|
+
const PADDING_BLOCK = 8;
|
|
14
|
+
const PEM_LINE_LENGTH = 70;
|
|
15
|
+
const PRIVATE_HEADER = "-----BEGIN OPENSSH PRIVATE KEY-----";
|
|
16
|
+
const PRIVATE_FOOTER = "-----END OPENSSH PRIVATE KEY-----";
|
|
17
|
+
// The prefix that turns a bare 32 byte seed into a pkcs8 ed25519 key node will accept.
|
|
18
|
+
const PKCS8_ED25519_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
|
|
19
|
+
|
|
20
|
+
class ByteReader {
|
|
21
|
+
private offset = 0;
|
|
22
|
+
constructor(private buffer: Buffer) { }
|
|
23
|
+
|
|
24
|
+
readBytes(length: number) {
|
|
25
|
+
let value = this.buffer.subarray(this.offset, this.offset + length);
|
|
26
|
+
if (value.length !== length) {
|
|
27
|
+
throw new Error(`Expected ${length} bytes at offset ${this.offset}, was ${value.length}`);
|
|
28
|
+
}
|
|
29
|
+
this.offset += length;
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
readUInt32() {
|
|
34
|
+
let value = this.buffer.readUInt32BE(this.offset);
|
|
35
|
+
this.offset += 4;
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
readString() {
|
|
40
|
+
return this.readBytes(this.readUInt32());
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function writeString(value: Buffer | string) {
|
|
45
|
+
let contents = Buffer.isBuffer(value) && value || Buffer.from(value, "utf8");
|
|
46
|
+
let length = Buffer.alloc(4);
|
|
47
|
+
length.writeUInt32BE(contents.length);
|
|
48
|
+
return Buffer.concat([length, contents]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function writeUInt32(value: number) {
|
|
52
|
+
let bytes = Buffer.alloc(4);
|
|
53
|
+
bytes.writeUInt32BE(value);
|
|
54
|
+
return bytes;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The public key of a seed, worked out by node rather than by reimplementing the curve. */
|
|
58
|
+
export function publicKeyFromSeed(seed: Buffer) {
|
|
59
|
+
if (seed.length !== SEED_LENGTH) {
|
|
60
|
+
throw new Error(`Expected a ${SEED_LENGTH} byte seed, was ${seed.length}`);
|
|
61
|
+
}
|
|
62
|
+
let privateKey = crypto.createPrivateKey({
|
|
63
|
+
key: Buffer.concat([PKCS8_ED25519_PREFIX, seed]),
|
|
64
|
+
format: "der",
|
|
65
|
+
type: "pkcs8",
|
|
66
|
+
});
|
|
67
|
+
// The spki wrapper is a fixed 12 byte header followed by the raw key.
|
|
68
|
+
return crypto.createPublicKey(privateKey).export({ format: "der", type: "spki" }).subarray(12);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function parseOpenSSHPrivateKey(contents: string) {
|
|
72
|
+
let body = contents.split("\n").filter(line => line.trim() && !line.startsWith("-----")).join("");
|
|
73
|
+
if (!contents.includes(PRIVATE_HEADER)) {
|
|
74
|
+
throw new Error(`Expected an OpenSSH private key starting with ${PRIVATE_HEADER}`);
|
|
75
|
+
}
|
|
76
|
+
let reader = new ByteReader(Buffer.from(body, "base64"));
|
|
77
|
+
if (reader.readBytes(MAGIC.length).toString() !== MAGIC) {
|
|
78
|
+
throw new Error(`Expected the key to start with ${JSON.stringify(MAGIC)}`);
|
|
79
|
+
}
|
|
80
|
+
let cipherName = reader.readString().toString();
|
|
81
|
+
let kdfName = reader.readString().toString();
|
|
82
|
+
reader.readString();
|
|
83
|
+
if (cipherName !== NONE || kdfName !== NONE) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Expected an unencrypted key, was encrypted with ${cipherName}/${kdfName}.`
|
|
86
|
+
+ ` Remove the passphrase with "ssh-keygen -p" first.`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
let keyCount = reader.readUInt32();
|
|
90
|
+
if (keyCount !== 1) {
|
|
91
|
+
throw new Error(`Expected the key file to hold one key, held ${keyCount}`);
|
|
92
|
+
}
|
|
93
|
+
reader.readString();
|
|
94
|
+
|
|
95
|
+
let privateSection = new ByteReader(reader.readString());
|
|
96
|
+
let checkOne = privateSection.readUInt32();
|
|
97
|
+
let checkTwo = privateSection.readUInt32();
|
|
98
|
+
if (checkOne !== checkTwo) {
|
|
99
|
+
throw new Error(`Expected the key's two check values to match, were ${checkOne} and ${checkTwo}`);
|
|
100
|
+
}
|
|
101
|
+
let keyType = privateSection.readString().toString();
|
|
102
|
+
if (keyType !== KEY_TYPE) {
|
|
103
|
+
throw new Error(`Expected an ${KEY_TYPE} key, was ${keyType}`);
|
|
104
|
+
}
|
|
105
|
+
privateSection.readString();
|
|
106
|
+
// OpenSSH stores the seed and the public key together in the private field.
|
|
107
|
+
let privateValue = privateSection.readString();
|
|
108
|
+
if (privateValue.length !== SEED_LENGTH + PUBLIC_LENGTH) {
|
|
109
|
+
throw new Error(`Expected a ${SEED_LENGTH + PUBLIC_LENGTH} byte private value, was ${privateValue.length}`);
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
seed: privateValue.subarray(0, SEED_LENGTH),
|
|
113
|
+
publicKey: privateValue.subarray(SEED_LENGTH),
|
|
114
|
+
comment: privateSection.readString().toString(),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function formatOpenSSHPrivateKey(config: { seed: Buffer; comment: string }) {
|
|
119
|
+
let { seed, comment } = config;
|
|
120
|
+
let publicKey = publicKeyFromSeed(seed);
|
|
121
|
+
let publicBlob = Buffer.concat([writeString(KEY_TYPE), writeString(publicKey)]);
|
|
122
|
+
|
|
123
|
+
// The check value only proves a passphrase decrypted correctly, and nothing here is encrypted.
|
|
124
|
+
// Taking it from the seed keeps the same input producing the same file, so deriving a key twice
|
|
125
|
+
// is something you can compare rather than something that looks different every time.
|
|
126
|
+
let check = crypto.createHash("sha256").update(seed).digest().readUInt32BE(0);
|
|
127
|
+
let privateSection = Buffer.concat([
|
|
128
|
+
writeUInt32(check),
|
|
129
|
+
writeUInt32(check),
|
|
130
|
+
writeString(KEY_TYPE),
|
|
131
|
+
writeString(publicKey),
|
|
132
|
+
writeString(Buffer.concat([seed, publicKey])),
|
|
133
|
+
writeString(comment),
|
|
134
|
+
]);
|
|
135
|
+
let paddingLength = (PADDING_BLOCK - privateSection.length % PADDING_BLOCK) % PADDING_BLOCK;
|
|
136
|
+
let padding = Buffer.from(Array.from({ length: paddingLength }, (unused, index) => index + 1));
|
|
137
|
+
|
|
138
|
+
let contents = Buffer.concat([
|
|
139
|
+
Buffer.from(MAGIC),
|
|
140
|
+
writeString(NONE),
|
|
141
|
+
writeString(NONE),
|
|
142
|
+
writeString(""),
|
|
143
|
+
writeUInt32(1),
|
|
144
|
+
writeString(publicBlob),
|
|
145
|
+
writeString(Buffer.concat([privateSection, padding])),
|
|
146
|
+
]);
|
|
147
|
+
let encoded = contents.toString("base64").match(new RegExp(`.{1,${PEM_LINE_LENGTH}}`, "g")) || [];
|
|
148
|
+
return `${PRIVATE_HEADER}\n${encoded.join("\n")}\n${PRIVATE_FOOTER}\n`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The single line form, as it appears in authorized_keys and in a .pub file. */
|
|
152
|
+
export function formatPublicKeyLine(config: { publicKey: Buffer; comment: string }) {
|
|
153
|
+
let { publicKey, comment } = config;
|
|
154
|
+
let blob = Buffer.concat([writeString(KEY_TYPE), writeString(publicKey)]);
|
|
155
|
+
return `${KEY_TYPE} ${blob.toString("base64")}${comment && ` ${comment}` || ""}\n`;
|
|
156
|
+
}
|
|
@@ -72,8 +72,25 @@ async function readWebhookFile(filePath: string) {
|
|
|
72
72
|
return parseWebhookFile({ contents, sourceName: filePath });
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/** When the message was sent, in the sending machine's own time zone. Discord shows when it
|
|
76
|
+
received something, which is not the same thing when a machine has been offline or a send has
|
|
77
|
+
been retried, and the zone matters when the machines are not all in one place. */
|
|
78
|
+
export function messageTimestamp(now: Date) {
|
|
79
|
+
let date = now.toLocaleDateString("en-CA");
|
|
80
|
+
let time = now.toLocaleTimeString("en-US", {
|
|
81
|
+
hour: "2-digit",
|
|
82
|
+
minute: "2-digit",
|
|
83
|
+
second: "2-digit",
|
|
84
|
+
hour12: true,
|
|
85
|
+
timeZoneName: "short",
|
|
86
|
+
});
|
|
87
|
+
return `${date} ${time}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
75
90
|
async function postToWebhook(webhookURL: string, message: string) {
|
|
76
|
-
|
|
91
|
+
// Stamped once rather than per attempt, so a retry says when the thing happened rather than
|
|
92
|
+
// when we last managed to get it out.
|
|
93
|
+
let content = `\`${messageTimestamp(new Date())}\` ${message}`;
|
|
77
94
|
if (content.length > DISCORD_MESSAGE_LIMIT) {
|
|
78
95
|
content = content.slice(0, DISCORD_MESSAGE_LIMIT - 3) + "...";
|
|
79
96
|
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule && mod.default) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
|
|
6
|
+
//exports.stopDiscordNotifications = exports.sendDiscordNotification = exports.configureDiscordNotifications = exports.sendToWebhookURL = exports.messageTimestamp = exports.formatWebhookFile = exports.redactWebhookURL = exports.parseWebhookFile = exports.DEFAULT_WEBHOOK_FILE_PATH = void 0;
|
|
7
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
8
|
+
// PORTED CODE: security/authorizedKeys/daemon/portsecureDaemon.js embeds a hand port of this file in plain JS, so the
|
|
9
|
+
// daemon can run with no dependencies. The two are expected to behave identically - if you change
|
|
10
|
+
// one, make the matching change in the other.
|
|
11
|
+
exports.DEFAULT_WEBHOOK_FILE_PATH = "/etc/portsecure/discord-webhook";
|
|
12
|
+
const DEFAULT_CHECK_INTERVAL = 5 * 60 * 1000;
|
|
13
|
+
const VALID_WEBHOOK_PREFIXES = [
|
|
14
|
+
"https://discord.com/api/webhooks/",
|
|
15
|
+
"https://discordapp.com/api/webhooks/",
|
|
16
|
+
"https://canary.discord.com/api/webhooks/",
|
|
17
|
+
];
|
|
18
|
+
const DISCORD_MESSAGE_LIMIT = 2000;
|
|
19
|
+
const MAX_ERROR_BODY_LENGTH = 500;
|
|
20
|
+
const MAX_SEND_ATTEMPTS = 3;
|
|
21
|
+
const REDACTED_TOKEN_VISIBLE = 8;
|
|
22
|
+
const DEFAULT_RATE_LIMIT_WAIT = 2 * 1000;
|
|
23
|
+
let notificationState;
|
|
24
|
+
// Discord webhooks are rate limited (roughly 5 requests per 2 seconds), so every send
|
|
25
|
+
// goes through one chain instead of racing.
|
|
26
|
+
let sendChain = Promise.resolve();
|
|
27
|
+
/** Pulls the webhook URL out of webhook file contents. `sourceName` only appears in errors, so
|
|
28
|
+
callers can name a remote path. */
|
|
29
|
+
function parseWebhookFile(config) {
|
|
30
|
+
let { contents, sourceName } = config;
|
|
31
|
+
let lines = contents.split("\n").map(line => line.trim()).filter(line => line && !line.startsWith("#"));
|
|
32
|
+
let webhookURL = lines[0];
|
|
33
|
+
if (!webhookURL) {
|
|
34
|
+
throw new Error(`Expected a Discord webhook URL in ${sourceName}, the file has no usable lines`);
|
|
35
|
+
}
|
|
36
|
+
if (!VALID_WEBHOOK_PREFIXES.some(prefix => webhookURL.startsWith(prefix))) {
|
|
37
|
+
throw new Error(`Expected a Discord webhook URL starting with one of ${VALID_WEBHOOK_PREFIXES.join(", ")}, `
|
|
38
|
+
+ `was ${webhookURL.slice(0, MAX_ERROR_BODY_LENGTH)} (in ${sourceName})`);
|
|
39
|
+
}
|
|
40
|
+
return webhookURL;
|
|
41
|
+
}
|
|
42
|
+
exports.parseWebhookFile = parseWebhookFile;
|
|
43
|
+
/** Keeps the id and both ends of the token, so a reader can confirm which webhook replaced theirs
|
|
44
|
+
without receiving one they could post to. */
|
|
45
|
+
function redactWebhookURL(webhookURL) {
|
|
46
|
+
let separatorIndex = webhookURL.lastIndexOf("/");
|
|
47
|
+
let base = webhookURL.slice(0, separatorIndex + 1);
|
|
48
|
+
let token = webhookURL.slice(separatorIndex + 1);
|
|
49
|
+
if (token.length <= REDACTED_TOKEN_VISIBLE * 2) {
|
|
50
|
+
// Too short to show both ends without giving away the whole token.
|
|
51
|
+
return `${base}${token.slice(0, REDACTED_TOKEN_VISIBLE)}...`;
|
|
52
|
+
}
|
|
53
|
+
return `${base}${token.slice(0, REDACTED_TOKEN_VISIBLE)}...${token.slice(-REDACTED_TOKEN_VISIBLE)}`;
|
|
54
|
+
}
|
|
55
|
+
exports.redactWebhookURL = redactWebhookURL;
|
|
56
|
+
function formatWebhookFile(webhookURL) {
|
|
57
|
+
return `# portsecure Discord webhook. First non-comment line is used.\n${webhookURL}\n`;
|
|
58
|
+
}
|
|
59
|
+
exports.formatWebhookFile = formatWebhookFile;
|
|
60
|
+
async function readWebhookFile(filePath) {
|
|
61
|
+
let contents;
|
|
62
|
+
try {
|
|
63
|
+
contents = await promises_1.default.readFile(filePath, "utf8");
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
throw new Error(`Expected a readable Discord webhook file at ${filePath}, ${e}`);
|
|
67
|
+
}
|
|
68
|
+
return parseWebhookFile({ contents, sourceName: filePath });
|
|
69
|
+
}
|
|
70
|
+
/** When the message was sent, in the sending machine's own time zone. Discord shows when it
|
|
71
|
+
received something, which is not the same thing when a machine has been offline or a send has
|
|
72
|
+
been retried, and the zone matters when the machines are not all in one place. */
|
|
73
|
+
function messageTimestamp(now) {
|
|
74
|
+
let date = now.toLocaleDateString("en-CA");
|
|
75
|
+
let time = now.toLocaleTimeString("en-US", {
|
|
76
|
+
hour: "2-digit",
|
|
77
|
+
minute: "2-digit",
|
|
78
|
+
second: "2-digit",
|
|
79
|
+
hour12: true,
|
|
80
|
+
timeZoneName: "short",
|
|
81
|
+
});
|
|
82
|
+
return `${date} ${time}`;
|
|
83
|
+
}
|
|
84
|
+
exports.messageTimestamp = messageTimestamp;
|
|
85
|
+
async function postToWebhook(webhookURL, message) {
|
|
86
|
+
// Stamped once rather than per attempt, so a retry says when the thing happened rather than
|
|
87
|
+
// when we last managed to get it out.
|
|
88
|
+
let content = `\`${messageTimestamp(new Date())}\` ${message}`;
|
|
89
|
+
if (content.length > DISCORD_MESSAGE_LIMIT) {
|
|
90
|
+
content = content.slice(0, DISCORD_MESSAGE_LIMIT - 3) + "...";
|
|
91
|
+
}
|
|
92
|
+
for (let attempt = 1; attempt <= MAX_SEND_ATTEMPTS; attempt++) {
|
|
93
|
+
let response = await fetch(webhookURL, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: { "Content-Type": "application/json" },
|
|
96
|
+
body: JSON.stringify({ content }),
|
|
97
|
+
});
|
|
98
|
+
if (response.ok) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
let body = (await response.text()).slice(0, MAX_ERROR_BODY_LENGTH);
|
|
102
|
+
if (response.status === 429 && attempt < MAX_SEND_ATTEMPTS) {
|
|
103
|
+
let retryAfter = Number(response.headers.get("retry-after"));
|
|
104
|
+
let waitTime = retryAfter && retryAfter * 1000 || DEFAULT_RATE_LIMIT_WAIT;
|
|
105
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
throw new Error(`Expected a 2xx response from the Discord webhook, was ${response.status} ${response.statusText}, body ${body}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function queueSend(webhookURL, message) {
|
|
112
|
+
let send = () => postToWebhook(webhookURL, message);
|
|
113
|
+
let result = sendChain.then(send, send);
|
|
114
|
+
sendChain = result.catch(() => undefined);
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
/** Sends to an explicit webhook URL, for tooling that acts on a webhook before (or instead of)
|
|
118
|
+
configureDiscordNotifications - setup, migrations, connectivity checks. */
|
|
119
|
+
function sendToWebhookURL(config) {
|
|
120
|
+
return queueSend(config.webhookURL, config.message);
|
|
121
|
+
}
|
|
122
|
+
exports.sendToWebhookURL = sendToWebhookURL;
|
|
123
|
+
async function checkWebhookFileChanged() {
|
|
124
|
+
let state = notificationState;
|
|
125
|
+
if (!state) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
let newWebhookURL;
|
|
129
|
+
try {
|
|
130
|
+
newWebhookURL = await readWebhookFile(state.filePath);
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
console.error(`portsecure: Discord webhook file is no longer readable, still using the loaded webhook. ${e}`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (newWebhookURL === state.webhookURL) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// Warn the old channel first, with the new webhook redacted, so a stolen old webhook does not
|
|
140
|
+
// hand the attacker a usable new one.
|
|
141
|
+
try {
|
|
142
|
+
await queueSend(state.webhookURL, `**portsecure**: the Discord webhook in \`${state.filePath}\` changed to`
|
|
143
|
+
+ ` \`${redactWebhookURL(newWebhookURL)}\`.`
|
|
144
|
+
+ ` Notifications are moving to the new webhook and this channel will stop receiving them.`);
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
console.error(`portsecure: failed to warn the old Discord webhook about the change. ${e}`);
|
|
148
|
+
}
|
|
149
|
+
state.webhookURL = newWebhookURL;
|
|
150
|
+
}
|
|
151
|
+
/** Must be called once on startup, before any notification is sent. Aborts the process if the
|
|
152
|
+
webhook file is missing or invalid, then re-checks the file on an interval and warns the old
|
|
153
|
+
webhook whenever it changes. */
|
|
154
|
+
async function configureDiscordNotifications(config) {
|
|
155
|
+
if (notificationState) {
|
|
156
|
+
throw new Error(`Expected configureDiscordNotifications to be called once, was called again (already using ${notificationState.filePath})`);
|
|
157
|
+
}
|
|
158
|
+
let filePath = (config === null || config === void 0 ? void 0 : config.filePath) || exports.DEFAULT_WEBHOOK_FILE_PATH;
|
|
159
|
+
let checkInterval = (config === null || config === void 0 ? void 0 : config.checkInterval) || DEFAULT_CHECK_INTERVAL;
|
|
160
|
+
let webhookURL;
|
|
161
|
+
try {
|
|
162
|
+
webhookURL = await readWebhookFile(filePath);
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
console.error(`portsecure: refusing to start without a valid Discord webhook file.\n${e}`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
let checkTimer = setInterval(() => {
|
|
169
|
+
checkWebhookFileChanged().catch(e => console.error(`portsecure: Discord webhook file check failed. ${e}`));
|
|
170
|
+
}, checkInterval);
|
|
171
|
+
// The check should never be the reason the process stays alive.
|
|
172
|
+
checkTimer.unref();
|
|
173
|
+
notificationState = { filePath, webhookURL, checkTimer };
|
|
174
|
+
return { filePath, checkInterval };
|
|
175
|
+
}
|
|
176
|
+
exports.configureDiscordNotifications = configureDiscordNotifications;
|
|
177
|
+
// DO NOT add new calls to this. Every message goes to a real Discord server someone reads, so a
|
|
178
|
+
// notification is only ever added when the user explicitly asks for that specific case. Startup,
|
|
179
|
+
// success, errors, retries and recoveries all belong in a log instead.
|
|
180
|
+
async function sendDiscordNotification(message) {
|
|
181
|
+
let state = notificationState;
|
|
182
|
+
if (!state) {
|
|
183
|
+
throw new Error(`Expected configureDiscordNotifications to be called before sending, was called with message ${message.slice(0, MAX_ERROR_BODY_LENGTH)}`);
|
|
184
|
+
}
|
|
185
|
+
await queueSend(state.webhookURL, message);
|
|
186
|
+
}
|
|
187
|
+
exports.sendDiscordNotification = sendDiscordNotification;
|
|
188
|
+
function stopDiscordNotifications() {
|
|
189
|
+
if (!notificationState) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
clearInterval(notificationState.checkTimer);
|
|
193
|
+
notificationState = undefined;
|
|
194
|
+
}
|
|
195
|
+
exports.stopDiscordNotifications = stopDiscordNotifications;
|
|
196
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGlzY29yZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImRpc2NvcmQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBQUEsMkRBQTZCO0FBRTdCLHNIQUFzSDtBQUN0SCxrR0FBa0c7QUFDbEcsOENBQThDO0FBRWpDLFFBQUEseUJBQXlCLEdBQUcsaUNBQWlDLENBQUM7QUFDM0UsTUFBTSxzQkFBc0IsR0FBRyxDQUFDLEdBQUcsRUFBRSxHQUFHLElBQUksQ0FBQztBQUM3QyxNQUFNLHNCQUFzQixHQUFHO0lBQzNCLG1DQUFtQztJQUNuQyxzQ0FBc0M7SUFDdEMsMENBQTBDO0NBQzdDLENBQUM7QUFDRixNQUFNLHFCQUFxQixHQUFHLElBQUksQ0FBQztBQUNuQyxNQUFNLHFCQUFxQixHQUFHLEdBQUcsQ0FBQztBQUNsQyxNQUFNLGlCQUFpQixHQUFHLENBQUMsQ0FBQztBQUM1QixNQUFNLHNCQUFzQixHQUFHLENBQUMsQ0FBQztBQUNqQyxNQUFNLHVCQUF1QixHQUFHLENBQUMsR0FBRyxJQUFJLENBQUM7QUFFekMsSUFBSSxpQkFJUyxDQUFDO0FBRWQsc0ZBQXNGO0FBQ3RGLDRDQUE0QztBQUM1QyxJQUFJLFNBQVMsR0FBcUIsT0FBTyxDQUFDLE9BQU8sRUFBRSxDQUFDO0FBRXBEO3NDQUNzQztBQUN0QyxTQUFnQixnQkFBZ0IsQ0FBQyxNQUFnRDtJQUM3RSxJQUFJLEVBQUUsUUFBUSxFQUFFLFVBQVUsRUFBRSxHQUFHLE1BQU0sQ0FBQztJQUN0QyxJQUFJLEtBQUssR0FBRyxRQUFRLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLElBQUksQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLElBQUksSUFBSSxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztJQUN4RyxJQUFJLFVBQVUsR0FBRyxL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RUFBRSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsa0RBQWtELENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQztJQUMvRyxDQUFDLEVBQUUsYUFBYSxDQUFDLENBQUM7SUFDbEIsZ0VBQWdFO0lBQ2hFLFVBQVUsQ0FBQyxLQUFLLEVBQUUsQ0FBQztJQUNuQixpQkFBaUIsR0FBRyxFQUFFLFFBQVEsRUFBRSxVQUFVLEVBQUUsVUFBVSxFQUFFLENBQUM7SUFDekQsT0FBTyxFQUFFLFFBQVEsRUFBRSxhQUFhLEVBQUUsQ0FBQztBQUN2QyxDQUFDO0FBdkJELHNFQXVCQztBQUVELGdHQUFnRztBQUNoRyxpR0FBaUc7QUFDakcsdUVBQXVFO0FBQ2hFLEtBQUssVUFBVSx1QkFBdUIsQ0FBQyxPQUFlO0lBQ3pELElBQUksS0FBSyxHQUFHLGlCQUFpQixDQUFDO0lBQzlCLElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQztRQUNULE1BQU0sSUFBSSxLQUFLLENBQUMsK0ZBQStGLE9BQU8sQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLHFCQUFxQixDQUFDLEVBQUUsQ0FBQyxDQUFDO0lBQzlKLENBQUM7SUFDRCxNQUFNLFNBQVMsQ0FBQyxLQUFLLENBQUMsVUFBVSxFQUFFLE9BQU8sQ0FBQyxDQUFDO0FBQy9DLENBQUM7QUFORCwwREFNQztBQUVELFNBQWdCLHdCQUF3QjtJQUNwQyxJQUFJLENBQUMsaUJBQWlCLEVBQUUsQ0FBQztRQUNyQixPQUFPO0lBQ1gsQ0FBQztJQUNELGFBQWEsQ0FBQyxpQkFBaUIsQ0FBQyxVQUFVLENBQUMsQ0FBQztJQUM1QyxpQkFBaUIsR0FBRyxTQUFTLENBQUM7QUFDbEMsQ0FBQztBQU5ELDREQU1DIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IGZzIGZyb20gXCJmcy9wcm9taXNlc1wiO1xuXG4vLyBQT1JURUQgQ09ERTogc2VjdXJpdHkvYXV0aG9yaXplZEtleXMvZGFlbW9uL3BvcnRzZWN1cmVEYWVtb24uanMgZW1iZWRzIGEgaGFuZCBwb3J0IG9mIHRoaXMgZmlsZSBpbiBwbGFpbiBKUywgc28gdGhlXG4vLyBkYWVtb24gY2FuIHJ1biB3aXRoIG5vIGRlcGVuZGVuY2llcy4gVGhlIHR3byBhcmUgZXhwZWN0ZWQgdG8gYmVoYXZlIGlkZW50aWNhbGx5IC0gaWYgeW91IGNoYW5nZVxuLy8gb25lLCBtYWtlIHRoZSBtYXRjaGluZyBjaGFuZ2UgaW4gdGhlIG90aGVyLlxuXG5leHBvcnQgY29uc3QgREVGQVVMVF9XRUJIT09LX0ZJTEVfUEFUSCA9IFwiL2V0Yy9wb3J0c2VjdXJlL2Rpc2NvcmQtd2ViaG9va1wiO1xuY29uc3QgREVGQVVMVF9DSEVDS19JTlRFUlZBTCA9IDUgKiA2MCAqIDEwMDA7XG5jb25zdCBWQUxJRF9XRUJIT09LX1BSRUZJWEVTID0gW1xuICAgIFwiaHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvXCIsXG4gICAgXCJodHRwczovL2Rpc2NvcmRhcHAuY29tL2FwaS93ZWJob29rcy9cIixcbiAgICBcImh0dHBzOi8vY2FuYXJ5LmRpc2NvcmQuY29tL2FwaS93ZWJob29rcy9cIixcbl07XG5jb25zdCBESVNDT1JEX01FU1NBR0VfTElNSVQgPSAyMDAwO1xuY29uc3QgTUFYX0VSUk9SX0JPRFlfTEVOR1RIID0gNTAwO1xuY29uc3QgTUFYX1NFTkRfQVRURU1QVFMgPSAzO1xuY29uc3QgUkVEQUNURURfVE9LRU5fVklTSUJMRSA9IDg7XG5jb25zdCBERUZBVUxUX1JBVEVfTElNSVRfV0FJVCA9IDIgKiAxMDAwO1xuXG5sZXQgbm90aWZpY2F0aW9uU3RhdGU6IHtcbiAgICBmaWxlUGF0aDogc3RyaW5nO1xuICAgIHdlYmhvb2tVUkw6IHN0cmluZztcbiAgICBjaGVja1RpbWVyOiBOb2RlSlMuVGltZW91dDtcbn0gfCB1bmRlZmluZWQ7XG5cbi8vIERpc2NvcmQgd2ViaG9va3MgYXJlIHJhdGUgbGltaXRlZCAocm91Z2hseSA1IHJlcXVlc3RzIHBlciAyIHNlY29uZHMpLCBzbyBldmVyeSBzZW5kXG4vLyBnb2VzIHRocm91Z2ggb25lIGNoYWluIGluc3RlYWQgb2YgcmFjaW5nLlxubGV0IHNlbmRDaGFpbjogUHJvbWlzZTx1bmtub3duPiA9IFByb21pc2UucmVzb2x2ZSgpO1xuXG4vKiogUHVsbHMgdGhlIHdlYmhvb2sgVVJMIG91dCBvZiB3ZWJob29rIGZpbGUgY29udGVudHMuIGBzb3VyY2VOYW1lYCBvbmx5IGFwcGVhcnMgaW4gZXJyb3JzLCBzb1xuICAgIGNhbGxlcnMgY2FuIG5hbWUgYSByZW1vdGUgcGF0aC4gKi9cbmV4cG9ydCBmdW5jdGlvbiBwYXJzZVdlYmhvb2tGaWxlKGNvbmZpZzogeyBjb250ZW50czogc3RyaW5nOyBzb3VyY2VOYW1lOiBzdHJpbmcgfSkge1xuICAgIGxldCB7IGNvbnRlbnRzLCBzb3VyY2VOYW1lIH0gPSBjb25maWc7XG4gICAgbGV0IGxpbmVzID0gY29udGVudHMuc3BsaXQoXCJcXG5cIikubWFwKGxpbmUgPT4gbGluZS50cmltKCkpLmZpbHRlcihsaW5lID0+IGxpbmUgJiYgIWxpbmUuc3RhcnRzV2l0aChcIiNcIikpO1xuICAgIGxldCB3ZWJob29rVVJMID0gbGluZXNbMF07XG4gICAgaWYgKCF3ZWJob29rVVJMKSB7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihgRXhwZWN0ZWQgYSBEaXNjb3JkIHdlYmhvb2sgVVJMIGluICR7c291cmNlTmFtZX0sIHRoZSBmaWxlIGhhcyBubyB1c2FibGUgbGluZXNgKTtcbiAgICB9XG4gICAgaWYgKCFWQUxJRF9XRUJIT09LX1BSRUZJWEVTLnNvbWUocHJlZml4ID0+IHdlYmhvb2tVUkwuc3RhcnRzV2l0aChwcmVmaXgpKSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoXG4gICAgICAgICAgICBgRXhwZWN0ZWQgYSBEaXNjb3JkIHdlYmhvb2sgVVJMIHN0YXJ0aW5nIHdpdGggb25lIG9mICR7VkFMSURfV0VCSE9PS19QUkVGSVhFUy5qb2luKFwiLCBcIil9LCBgXG4gICAgICAgICAgICArIGB3YXMgJHt3ZWJob29rVVJMLnNsaWNlKDAsIE1BWF9FUlJPUl9CT0RZX0xFTkdUSCl9IChpbiAke3NvdXJjZU5hbWV9KWBcbiAgICAgICAgKTtcbiAgICB9XG4gICAgcmV0dXJuIHdlYmhvb2tVUkw7XG59XG5cbi8qKiBLZWVwcyB0aGUgaWQgYW5kIGJvdGggZW5kcyBvZiB0aGUgdG9rZW4sIHNvIGEgcmVhZGVyIGNhbiBjb25maXJtIHdoaWNoIHdlYmhvb2sgcmVwbGFjZWQgdGhlaXJzXG4gICAgd2l0aG91dCByZWNlaXZpbmcgb25lIHRoZXkgY291bGQgcG9zdCB0by4gKi9cbmV4cG9ydCBmdW5jdGlvbiByZWRhY3RXZWJob29rVVJMKHdlYmhvb2tVUkw6IHN0cmluZykge1xuICAgIGxldCBzZXBhcmF0b3JJbmRleCA9IHdlYmhvb2tVUkwubGFzdEluZGV4T2YoXCIvXCIpO1xuICAgIGxldCBiYXNlID0gd2ViaG9va1VSTC5zbGljZSgwLCBzZXBhcmF0b3JJbmRleCArIDEpO1xuICAgIGxldCB0b2tlbiA9IHdlYmhvb2tVUkwuc2xpY2Uoc2VwYXJhdG9ySW5kZXggKyAxKTtcbiAgICBpZiAodG9rZW4ubGVuZ3RoIDw9IFJFREFDVEVEX1RPS0VOX1ZJU0lCTEUgKiAyKSB7XG4gICAgICAgIC8vIFRvbyBzaG9ydCB0byBzaG93IGJvdGggZW5kcyB3aXRob3V0IGdpdmluZyBhd2F5IHRoZSB3aG9sZSB0b2tlbi5cbiAgICAgICAgcmV0dXJuIGAke2Jhc2V9JHt0b2tlbi5zbGljZSgwLCBSRURBQ1RFRF9UT0tFTl9WSVNJQkxFKX0uLi5gO1xuICAgIH1cbiAgICByZXR1cm4gYCR7YmFzZX0ke3Rva2VuLnNsaWNlKDAsIFJFREFDVEVEX1RPS0VOX1ZJU0lCTEUpfS4uLiR7dG9rZW4uc2xpY2UoLVJFREFDVEVEX1RPS0VOX1ZJU0lCTEUpfWA7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBmb3JtYXRXZWJob29rRmlsZSh3ZWJob29rVVJMOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gYCMgcG9ydHNlY3VyZSBEaXNjb3JkIHdlYmhvb2suIEZpcnN0IG5vbi1jb21tZW50IGxpbmUgaXMgdXNlZC5cXG4ke3dlYmhvb2tVUkx9XFxuYDtcbn1cblxuYXN5bmMgZnVuY3Rpb24gcmVhZFdlYmhvb2tGaWxlKGZpbGVQYXRoOiBzdHJpbmcpIHtcbiAgICBsZXQgY29udGVudHM7XG4gICAgdHJ5IHtcbiAgICAgICAgY29udGVudHMgPSBhd2FpdCBmcy5yZWFkRmlsZShmaWxlUGF0aCwgXCJ1dGY4XCIpO1xuICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBFeHBlY3RlZCBhIHJlYWRhYmxlIERpc2NvcmQgd2ViaG9vayBmaWxlIGF0ICR7ZmlsZVBhdGh9LCAke2V9YCk7XG4gICAgfVxuICAgIHJldHVybiBwYXJzZVdlYmhvb2tGaWxlKHsgY29udGVudHMsIHNvdXJjZU5hbWU6IGZpbGVQYXRoIH0pO1xufVxuXG4vKiogV2hlbiB0aGUgbWVzc2FnZSB3YXMgc2VudCwgaW4gdGhlIHNlbmRpbmcgbWFjaGluZSdzIG93biB0aW1lIHpvbmUuIERpc2NvcmQgc2hvd3Mgd2hlbiBpdFxuICAgIHJlY2VpdmVkIHNvbWV0aGluZywgd2hpY2ggaXMgbm90IHRoZSBzYW1lIHRoaW5nIHdoZW4gYSBtYWNoaW5lIGhhcyBiZWVuIG9mZmxpbmUgb3IgYSBzZW5kIGhhc1xuICAgIGJlZW4gcmV0cmllZCwgYW5kIHRoZSB6b25lIG1hdHRlcnMgd2hlbiB0aGUgbWFjaGluZXMgYXJlIG5vdCBhbGwgaW4gb25lIHBsYWNlLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIG1lc3NhZ2VUaW1lc3RhbXAobm93OiBEYXRlKSB7XG4gICAgbGV0IGRhdGUgPSBub3cudG9Mb2NhbGVEYXRlU3RyaW5nKFwiZW4tQ0FcIik7XG4gICAgbGV0IHRpbWUgPSBub3cudG9Mb2NhbGVUaW1lU3RyaW5nKFwiZW4tVVNcIiwge1xuICAgICAgICBob3VyOiBcIjItZGlnaXRcIixcbiAgICAgICAgbWludXRlOiBcIjItZGlnaXRcIixcbiAgICAgICAgc2Vjb25kOiBcIjItZGlnaXRcIixcbiAgICAgICAgaG91cjEyOiB0cnVlLFxuICAgICAgICB0aW1lWm9uZU5hbWU6IFwic2hvcnRcIixcbiAgICB9KTtcbiAgICByZXR1cm4gYCR7ZGF0ZX0gJHt0aW1lfWA7XG59XG5cbmFzeW5jIGZ1bmN0aW9uIHBvc3RUb1dlYmhvb2sod2ViaG9va1VSTDogc3RyaW5nLCBtZXNzYWdlOiBzdHJpbmcpIHtcbiAgICAvLyBTdGFtcGVkIG9uY2UgcmF0aGVyIHRoYW4gcGVyIGF0dGVtcHQsIHNvIGEgcmV0cnkgc2F5cyB3aGVuIHRoZSB0aGluZyBoYXBwZW5lZCByYXRoZXIgdGhhblxuICAgIC8vIHdoZW4gd2UgbGFzdCBtYW5hZ2VkIHRvIGdldCBpdCBvdXQuXG4gICAgbGV0IGNvbnRlbnQgPSBgXFxgJHttZXNzYWdlVGltZXN0YW1wKG5ldyBEYXRlKCkpfVxcYCAke21lc3NhZ2V9YDtcbiAgICBpZiAoY29udGVudC5sZW5ndGggPiBESVNDT1JEX01FU1NBR0VfTElNSVQpIHtcbiAgICAgICAgY29udGVudCA9IGNvbnRlbnQuc2xpY2UoMCwgRElTQ09SRF9NRVNTQUdFX0xJTUlUIC0gMykgKyBcIi4uLlwiO1xuICAgIH1cbiAgICBmb3IgKGxldCBhdHRlbXB0ID0gMTsgYXR0ZW1wdCA8PSBNQVhfU0VORF9BVFRFTVBUUzsgYXR0ZW1wdCsrKSB7XG4gICAgICAgIGxldCByZXNwb25zZSA9IGF3YWl0IGZldGNoKHdlYmhvb2tVUkwsIHtcbiAgICAgICAgICAgIG1ldGhvZDogXCJQT1NUXCIsXG4gICAgICAgICAgICBoZWFkZXJzOiB7IFwiQ29udGVudC1UeXBlXCI6IFwiYXBwbGljYXRpb24vanNvblwiIH0sXG4gICAgICAgICAgICBib2R5OiBKU09OLnN0cmluZ2lmeSh7IGNvbnRlbnQgfSksXG4gICAgICAgIH0pO1xuICAgICAgICBpZiAocmVzcG9uc2Uub2spIHtcbiAgICAgICAgICAgIHJldHVybjtcbiAgICAgICAgfVxuICAgICAgICBsZXQgYm9keSA9IChhd2FpdCByZXNwb25zZS50ZXh0KCkpLnNsaWNlKDAsIE1BWF9FUlJPUl9CT0RZX0xFTkdUSCk7XG4gICAgICAgIGlmIChyZXNwb25zZS5zdGF0dXMgPT09IDQyOSAmJiBhdHRlbXB0IDwgTUFYX1NFTkRfQVRURU1QVFMpIHtcbiAgICAgICAgICAgIGxldCByZXRyeUFmdGVyID0gTnVtYmVyKHJlc3BvbnNlLmhlYWRlcnMuZ2V0KFwicmV0cnktYWZ0ZXJcIikpO1xuICAgICAgICAgICAgbGV0IHdhaXRUaW1lID0gcmV0cnlBZnRlciAmJiByZXRyeUFmdGVyICogMTAwMCB8fCBERUZBVUxUX1JBVEVfTElNSVRfV0FJVDtcbiAgICAgICAgICAgIGF3YWl0IG5ldyBQcm9taXNlKHJlc29sdmUgPT4gc2V0VGltZW91dChyZXNvbHZlLCB3YWl0VGltZSkpO1xuICAgICAgICAgICAgY29udGludWU7XG4gICAgICAgIH1cbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKFxuICAgICAgICAgICAgYEV4cGVjdGVkIGEgMnh4IHJlc3BvbnNlIGZyb20gdGhlIERpc2NvcmQgd2ViaG9vaywgd2FzICR7cmVzcG9uc2Uuc3RhdHVzfSAke3Jlc3BvbnNlLnN0YXR1c1RleHR9LCBib2R5ICR7Ym9keX1gXG4gICAgICAgICk7XG4gICAgfVxufVxuXG5mdW5jdGlvbiBxdWV1ZVNlbmQod2ViaG9va1VSTDogc3RyaW5nLCBtZXNzYWdlOiBzdHJpbmcpIHtcbiAgICBsZXQgc2VuZCA9ICgpID0+IHBvc3RUb1dlYmhvb2sod2ViaG9va1VSTCwgbWVzc2FnZSk7XG4gICAgbGV0IHJlc3VsdCA9IHNlbmRDaGFpbi50aGVuKHNlbmQsIHNlbmQpO1xuICAgIHNlbmRDaGFpbiA9IHJlc3VsdC5jYXRjaCgoKSA9PiB1bmRlZmluZWQpO1xuICAgIHJldHVybiByZXN1bHQ7XG59XG5cbi8qKiBTZW5kcyB0byBhbiBleHBsaWNpdCB3ZWJob29rIFVSTCwgZm9yIHRvb2xpbmcgdGhhdCBhY3RzIG9uIGEgd2ViaG9vayBiZWZvcmUgKG9yIGluc3RlYWQgb2YpXG4gICAgY29uZmlndXJlRGlzY29yZE5vdGlmaWNhdGlvbnMgLSBzZXR1cCwgbWlncmF0aW9ucywgY29ubmVjdGl2aXR5IGNoZWNrcy4gKi9cbmV4cG9ydCBmdW5jdGlvbiBzZW5kVG9XZWJob29rVVJMKGNvbmZpZzogeyB3ZWJob29rVVJMOiBzdHJpbmc7IG1lc3NhZ2U6IHN0cmluZyB9KSB7XG4gICAgcmV0dXJuIHF1ZXVlU2VuZChjb25maWcud2ViaG9va1VSTCwgY29uZmlnLm1lc3NhZ2UpO1xufVxuXG5hc3luYyBmdW5jdGlvbiBjaGVja1dlYmhvb2tGaWxlQ2hhbmdlZCgpIHtcbiAgICBsZXQgc3RhdGUgPSBub3RpZmljYXRpb25TdGF0ZTtcbiAgICBpZiAoIXN0YXRlKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgbGV0IG5ld1dlYmhvb2tVUkw7XG4gICAgdHJ5IHtcbiAgICAgICAgbmV3V2ViaG9va1VSTCA9IGF3YWl0IHJlYWRXZWJob29rRmlsZShzdGF0ZS5maWxlUGF0aCk7XG4gICAgfSBjYXRjaCAoZSkge1xuICAgICAgICBjb25zb2xlLmVycm9yKGBwb3J0c2VjdXJlOiBEaXNjb3JkIHdlYmhvb2sgZmlsZSBpcyBubyBsb25nZXIgcmVhZGFibGUsIHN0aWxsIHVzaW5nIHRoZSBsb2FkZWQgd2ViaG9vay4gJHtlfWApO1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGlmIChuZXdXZWJob29rVVJMID09PSBzdGF0ZS53ZWJob29rVVJMKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgLy8gV2FybiB0aGUgb2xkIGNoYW5uZWwgZmlyc3QsIHdpdGggdGhlIG5ldyB3ZWJob29rIHJlZGFjdGVkLCBzbyBhIHN0b2xlbiBvbGQgd2ViaG9vayBkb2VzIG5vdFxuICAgIC8vIGhhbmQgdGhlIGF0dGFja2VyIGEgdXNhYmxlIG5ldyBvbmUuXG4gICAgdHJ5IHtcbiAgICAgICAgYXdhaXQgcXVldWVTZW5kKFxuICAgICAgICAgICAgc3RhdGUud2ViaG9va1VSTCxcbiAgICAgICAgICAgIGAqKnBvcnRzZWN1cmUqKjogdGhlIERpc2NvcmQgd2ViaG9vayBpbiBcXGAke3N0YXRlLmZpbGVQYXRofVxcYCBjaGFuZ2VkIHRvYFxuICAgICAgICAgICAgKyBgIFxcYCR7cmVkYWN0V2ViaG9va1VSTChuZXdXZWJob29rVVJMKX1cXGAuYFxuICAgICAgICAgICAgKyBgIE5vdGlmaWNhdGlvbnMgYXJlIG1vdmluZyB0byB0aGUgbmV3IHdlYmhvb2sgYW5kIHRoaXMgY2hhbm5lbCB3aWxsIHN0b3AgcmVjZWl2aW5nIHRoZW0uYFxuICAgICAgICApO1xuICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgY29uc29sZS5lcnJvcihgcG9ydHNlY3VyZTogZmFpbGVkIHRvIHdhcm4gdGhlIG9sZCBEaXNjb3JkIHdlYmhvb2sgYWJvdXQgdGhlIGNoYW5nZS4gJHtlfWApO1xuICAgIH1cbiAgICBzdGF0ZS53ZWJob29rVVJMID0gbmV3V2ViaG9va1VSTDtcbn1cblxuLyoqIE11c3QgYmUgY2FsbGVkIG9uY2Ugb24gc3RhcnR1cCwgYmVmb3JlIGFueSBub3RpZmljYXRpb24gaXMgc2VudC4gQWJvcnRzIHRoZSBwcm9jZXNzIGlmIHRoZVxuICAgIHdlYmhvb2sgZmlsZSBpcyBtaXNzaW5nIG9yIGludmFsaWQsIHRoZW4gcmUtY2hlY2tzIHRoZSBmaWxlIG9uIGFuIGludGVydmFsIGFuZCB3YXJucyB0aGUgb2xkXG4gICAgd2ViaG9vayB3aGVuZXZlciBpdCBjaGFuZ2VzLiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGNvbmZpZ3VyZURpc2NvcmROb3RpZmljYXRpb25zKGNvbmZpZz86IHtcbiAgICBmaWxlUGF0aD86IHN0cmluZztcbiAgICBjaGVja0ludGVydmFsPzogbnVtYmVyO1xufSkge1xuICAgIGlmIChub3RpZmljYXRpb25TdGF0ZSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEV4cGVjdGVkIGNvbmZpZ3VyZURpc2NvcmROb3RpZmljYXRpb25zIHRvIGJlIGNhbGxlZCBvbmNlLCB3YXMgY2FsbGVkIGFnYWluIChhbHJlYWR5IHVzaW5nICR7bm90aWZpY2F0aW9uU3RhdGUuZmlsZVBhdGh9KWApO1xuICAgIH1cbiAgICBsZXQgZmlsZVBhdGggPSBjb25maWc/LmZpbGVQYXRoIHx8IERFRkFVTFRfV0VCSE9PS19GSUxFX1BBVEg7XG4gICAgbGV0IGNoZWNrSW50ZXJ2YWwgPSBjb25maWc/LmNoZWNrSW50ZXJ2YWwgfHwgREVGQVVMVF9DSEVDS19JTlRFUlZBTDtcbiAgICBsZXQgd2ViaG9va1VSTDtcbiAgICB0cnkge1xuICAgICAgICB3ZWJob29rVVJMID0gYXdhaXQgcmVhZFdlYmhvb2tGaWxlKGZpbGVQYXRoKTtcbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgIGNvbnNvbGUuZXJyb3IoYHBvcnRzZWN1cmU6IHJlZnVzaW5nIHRvIHN0YXJ0IHdpdGhvdXQgYSB2YWxpZCBEaXNjb3JkIHdlYmhvb2sgZmlsZS5cXG4ke2V9YCk7XG4gICAgICAgIHByb2Nlc3MuZXhpdCgxKTtcbiAgICB9XG4gICAgbGV0IGNoZWNrVGltZXIgPSBzZXRJbnRlcnZhbCgoKSA9PiB7XG4gICAgICAgIGNoZWNrV2ViaG9va0ZpbGVDaGFuZ2VkKCkuY2F0Y2goZSA9PiBjb25zb2xlLmVycm9yKGBwb3J0c2VjdXJlOiBEaXNjb3JkIHdlYmhvb2sgZmlsZSBjaGVjayBmYWlsZWQuICR7ZX1gKSk7XG4gICAgfSwgY2hlY2tJbnRlcnZhbCk7XG4gICAgLy8gVGhlIGNoZWNrIHNob3VsZCBuZXZlciBiZSB0aGUgcmVhc29uIHRoZSBwcm9jZXNzIHN0YXlzIGFsaXZlLlxuICAgIGNoZWNrVGltZXIudW5yZWYoKTtcbiAgICBub3RpZmljYXRpb25TdGF0ZSA9IHsgZmlsZVBhdGgsIHdlYmhvb2tVUkwsIGNoZWNrVGltZXIgfTtcbiAgICByZXR1cm4geyBmaWxlUGF0aCwgY2hlY2tJbnRlcnZhbCB9O1xufVxuXG4vLyBETyBOT1QgYWRkIG5ldyBjYWxscyB0byB0aGlzLiBFdmVyeSBtZXNzYWdlIGdvZXMgdG8gYSByZWFsIERpc2NvcmQgc2VydmVyIHNvbWVvbmUgcmVhZHMsIHNvIGFcbi8vIG5vdGlmaWNhdGlvbiBpcyBvbmx5IGV2ZXIgYWRkZWQgd2hlbiB0aGUgdXNlciBleHBsaWNpdGx5IGFza3MgZm9yIHRoYXQgc3BlY2lmaWMgY2FzZS4gU3RhcnR1cCxcbi8vIHN1Y2Nlc3MsIGVycm9ycywgcmV0cmllcyBhbmQgcmVjb3ZlcmllcyBhbGwgYmVsb25nIGluIGEgbG9nIGluc3RlYWQuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gc2VuZERpc2NvcmROb3RpZmljYXRpb24obWVzc2FnZTogc3RyaW5nKSB7XG4gICAgbGV0IHN0YXRlID0gbm90aWZpY2F0aW9uU3RhdGU7XG4gICAgaWYgKCFzdGF0ZSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEV4cGVjdGVkIGNvbmZpZ3VyZURpc2NvcmROb3RpZmljYXRpb25zIHRvIGJlIGNhbGxlZCBiZWZvcmUgc2VuZGluZywgd2FzIGNhbGxlZCB3aXRoIG1lc3NhZ2UgJHttZXNzYWdlLnNsaWNlKDAsIE1BWF9FUlJPUl9CT0RZX0xFTkdUSCl9YCk7XG4gICAgfVxuICAgIGF3YWl0IHF1ZXVlU2VuZChzdGF0ZS53ZWJob29rVVJMLCBtZXNzYWdlKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHN0b3BEaXNjb3JkTm90aWZpY2F0aW9ucygpIHtcbiAgICBpZiAoIW5vdGlmaWNhdGlvblN0YXRlKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgY2xlYXJJbnRlcnZhbChub3RpZmljYXRpb25TdGF0ZS5jaGVja1RpbWVyKTtcbiAgICBub3RpZmljYXRpb25TdGF0ZSA9IHVuZGVmaW5lZDtcbn1cbiJdfQ==
|
|
197
|
+
/* _JS_SOURCE_HASH = "be450922cec9e8a268e2a78f817bfedb88063a0b59db5b1a954d81c96169543a"; */
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
import { parseWebhookFile } from "./discord";
|
|
2
2
|
import { ensureRemoteWebhook, REPLACE_KEYWORD } from "./remoteWebhook";
|
|
3
|
+
import { describeHost, THIS_MACHINE } from "../helpers/remoteSSH";
|
|
3
4
|
|
|
4
|
-
const USAGE = `Usage: yarn setupnotify
|
|
5
|
+
const USAGE = `Usage: yarn setupnotify [host] <discord-webhook-url> [${REPLACE_KEYWORD}]
|
|
6
|
+
|
|
7
|
+
With no host it sets the webhook up on this machine.`;
|
|
5
8
|
|
|
6
9
|
function parseArgs(argv: string[]) {
|
|
7
10
|
let replace = argv.includes(REPLACE_KEYWORD);
|
|
8
11
|
let positional = argv.filter(arg => arg !== REPLACE_KEYWORD);
|
|
9
|
-
if (positional.length
|
|
10
|
-
throw new Error(
|
|
12
|
+
if (!positional.length || positional.length > 2) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`Expected a webhook URL, and optionally a host before it, was ${positional.length}`
|
|
15
|
+
+ ` argument(s): ${positional.join(" ") || "(none)"}\n${USAGE}`
|
|
16
|
+
);
|
|
11
17
|
}
|
|
12
|
-
|
|
13
|
-
//
|
|
18
|
+
// The webhook url is the last one, so whatever comes before it is the host. Without one this
|
|
19
|
+
// machine is the target.
|
|
20
|
+
let webhookURL = positional[positional.length - 1];
|
|
21
|
+
let host = positional.length > 1 && positional[0] || THIS_MACHINE;
|
|
22
|
+
// Validates the URL shape up front, so we never go anywhere with a bad webhook.
|
|
14
23
|
parseWebhookFile({ contents: webhookURL, sourceName: "the command line" });
|
|
15
24
|
return { host, webhookURL, replace };
|
|
16
25
|
}
|
|
@@ -19,11 +28,11 @@ export async function main() {
|
|
|
19
28
|
let { host, webhookURL, replace } = parseArgs(process.argv.slice(2));
|
|
20
29
|
let { outcome, filePath } = await ensureRemoteWebhook({ host, webhookURL, replace });
|
|
21
30
|
if (outcome === "unchanged") {
|
|
22
|
-
console.log(`${host} already has this exact webhook in ${filePath}, nothing to do.`);
|
|
31
|
+
console.log(`${describeHost(host)} already has this exact webhook in ${filePath}, nothing to do.`);
|
|
23
32
|
return;
|
|
24
33
|
}
|
|
25
34
|
if (outcome === "replaced") {
|
|
26
|
-
console.log(`Replaced the webhook on ${host} and notified the old one.`);
|
|
35
|
+
console.log(`Replaced the webhook on ${describeHost(host)} and notified the old one.`);
|
|
27
36
|
}
|
|
28
|
-
console.log(`Wrote ${filePath} on ${host} (mode 600), and confirmed on the new webhook.`);
|
|
37
|
+
console.log(`Wrote ${filePath} on ${describeHost(host)} (mode 600), and confirmed on the new webhook.`);
|
|
29
38
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import { DEFAULT_WEBHOOK_FILE_PATH, parseWebhookFile, sendToWebhookURL } from "./discord";
|
|
4
|
+
import { readRemoteFile } from "../helpers/remoteSSH";
|
|
5
|
+
|
|
6
|
+
const USAGE = `Usage: yarn testnotify [host]
|
|
7
|
+
|
|
8
|
+
Sends one message to a webhook, to prove it works. With no host it uses this machine's webhook at
|
|
9
|
+
${DEFAULT_WEBHOOK_FILE_PATH}. With a host it reads that host's webhook and sends to that instead,
|
|
10
|
+
which is how you check a machine you have just set up.`;
|
|
11
|
+
|
|
12
|
+
export async function main() {
|
|
13
|
+
let argv = process.argv.slice(2);
|
|
14
|
+
if (argv.length > 1) {
|
|
15
|
+
throw new Error(`Expected at most a host, was ${argv.length} argument(s)\n${USAGE}`);
|
|
16
|
+
}
|
|
17
|
+
let host = argv[0];
|
|
18
|
+
|
|
19
|
+
let contents: string | undefined;
|
|
20
|
+
if (host) {
|
|
21
|
+
contents = await readRemoteFile({ host, filePath: DEFAULT_WEBHOOK_FILE_PATH });
|
|
22
|
+
if (!contents) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Expected a webhook at ${DEFAULT_WEBHOOK_FILE_PATH} on ${host}, no such file exists.\n`
|
|
25
|
+
+ `Set it up with:\n yarn setupnotify ${host} <discord-webhook-url>`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
try {
|
|
30
|
+
contents = await fs.readFile(DEFAULT_WEBHOOK_FILE_PATH, "utf8");
|
|
31
|
+
} catch (e) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Expected a webhook at ${DEFAULT_WEBHOOK_FILE_PATH} on this machine, ${e}\n`
|
|
34
|
+
+ `Name a host to use that host's webhook instead.\n${USAGE}`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let source = host || os.hostname();
|
|
40
|
+
let webhookURL = parseWebhookFile({ contents, sourceName: `${source}:${DEFAULT_WEBHOOK_FILE_PATH}` });
|
|
41
|
+
// Said plainly, so nobody reading the channel later has to work out whether it was real.
|
|
42
|
+
await sendToWebhookURL({
|
|
43
|
+
webhookURL,
|
|
44
|
+
message: `**portsecure test**: this is a test message, nothing has happened.`
|
|
45
|
+
+ ` Sent with \`yarn testnotify\` for the webhook \`${source}\` uses.`,
|
|
46
|
+
});
|
|
47
|
+
console.log(`Sent a test message to the webhook ${source} uses.`);
|
|
48
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule && mod.default) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
|
|
6
|
+
//exports.formatManifest = exports.buildManifest = exports.digestFile = exports.normalizeContent = exports.listRepoFiles = exports.MANIFEST_VERSION = exports.SIGN_NAMESPACE = exports.SIGNATURE_NAME = exports.MANIFEST_NAME = void 0;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const spawn_1 = require("../helpers/spawn");
|
|
11
|
+
// PORTED CODE: security/authorizedKeys/daemon/portsecureDaemon.js contains a plain JS port of the
|
|
12
|
+
// verifying half of this file, so it can check a signature with no dependencies. Both sides must
|
|
13
|
+
// agree on the manifest shape and on which files it covers - if you change one, make the matching
|
|
14
|
+
// change in the other.
|
|
15
|
+
exports.MANIFEST_NAME = "signedfiles.json";
|
|
16
|
+
exports.SIGNATURE_NAME = "signedfiles.json.sig";
|
|
17
|
+
// ssh signatures are namespaced, so a signature made for one purpose cannot be replayed as another.
|
|
18
|
+
exports.SIGN_NAMESPACE = "signfiles";
|
|
19
|
+
exports.MANIFEST_VERSION = 1;
|
|
20
|
+
/** Everything in the repo that is not ignored: what is in the index, plus what is not staged yet.
|
|
21
|
+
Git is asked because git is what knows how to apply the ignore rules.
|
|
22
|
+
|
|
23
|
+
What comes back is a hint that a file might be there, not that it is. A file deleted from the
|
|
24
|
+
working tree stays in the index until that deletion is staged, so git names it and there is
|
|
25
|
+
nothing to read, which is why each one is checked before it goes in.
|
|
26
|
+
|
|
27
|
+
The manifest and its signature are left out, since they cannot describe themselves. */
|
|
28
|
+
async function listRepoFiles(repoPath) {
|
|
29
|
+
let result = await (0, spawn_1.spawnPromise)({
|
|
30
|
+
command: "git",
|
|
31
|
+
args: ["ls-files", "--cached", "--others", "--exclude-standard"],
|
|
32
|
+
cwd: repoPath,
|
|
33
|
+
});
|
|
34
|
+
if (result.status !== 0) {
|
|
35
|
+
throw new Error(`Expected to list the files in ${repoPath}, git ls-files exited ${result.status}. `
|
|
36
|
+
+ `${(result.stdout + result.stderr).trim()}`);
|
|
37
|
+
}
|
|
38
|
+
let listed = result.stdout.split("\n")
|
|
39
|
+
.map(line => line.trim())
|
|
40
|
+
.filter(line => line && line !== exports.MANIFEST_NAME && line !== exports.SIGNATURE_NAME)
|
|
41
|
+
.sort();
|
|
42
|
+
let present = [];
|
|
43
|
+
for (let relativePath of listed) {
|
|
44
|
+
try {
|
|
45
|
+
await promises_1.default.access(path_1.default.join(repoPath, relativePath));
|
|
46
|
+
present.push(relativePath);
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
console.log(`Leaving ${relativePath} out of the manifest, it is gone from the working tree`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return present;
|
|
53
|
+
}
|
|
54
|
+
exports.listRepoFiles = listRepoFiles;
|
|
55
|
+
/** A Windows checkout turns LF into CRLF, so the same commit hashes differently there than it
|
|
56
|
+
does on the machine that pulls it. Normalising first makes the digest describe the content
|
|
57
|
+
rather than whichever checkout produced it. latin1 round trips every byte, so this is safe on
|
|
58
|
+
files that are not text. */
|
|
59
|
+
function normalizeContent(contents) {
|
|
60
|
+
return Buffer.from(contents.toString("latin1").replace(/\r\n/g, "\n"), "latin1");
|
|
61
|
+
}
|
|
62
|
+
exports.normalizeContent = normalizeContent;
|
|
63
|
+
/** The size and hash of a file's normalised content. The size is the normalised one on purpose,
|
|
64
|
+
so it agrees with the hash rather than with whatever the local checkout happens to hold. */
|
|
65
|
+
async function digestFile(filePath) {
|
|
66
|
+
let contents = normalizeContent(await promises_1.default.readFile(filePath));
|
|
67
|
+
return { size: contents.length, sha256: crypto_1.default.createHash("sha256").update(contents).digest("hex") };
|
|
68
|
+
}
|
|
69
|
+
exports.digestFile = digestFile;
|
|
70
|
+
async function buildManifest(repoPath) {
|
|
71
|
+
let files = [];
|
|
72
|
+
for (let relativePath of await listRepoFiles(repoPath)) {
|
|
73
|
+
let digest = await digestFile(path_1.default.join(repoPath, relativePath));
|
|
74
|
+
files.push({ path: relativePath, size: digest.size, sha256: digest.sha256 });
|
|
75
|
+
}
|
|
76
|
+
return { version: exports.MANIFEST_VERSION, files };
|
|
77
|
+
}
|
|
78
|
+
exports.buildManifest = buildManifest;
|
|
79
|
+
/** Sorted keys and a trailing newline, so the same tree always produces the same bytes to sign. */
|
|
80
|
+
function formatManifest(manifest) {
|
|
81
|
+
return JSON.stringify(manifest, undefined, 4) + "\n";
|
|
82
|
+
}
|
|
83
|
+
exports.formatManifest = formatManifest;
|
|
84
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFuaWZlc3QuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJtYW5pZmVzdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUFBQSxvREFBNEI7QUFDNUIsMkRBQTZCO0FBQzdCLGdEQUF3QjtBQUN4Qiw0Q0FBZ0Q7QUFFaEQsa0dBQWtHO0FBQ2xHLGlHQUFpRztBQUNqRyxrR0FBa0c7QUFDbEcsdUJBQXVCO0FBRVYsUUFBQSxhQUFhLEdBQUcsa0JBQWtCLENBQUM7QUFDbkMsUUFBQSxjQUFjLEdBQUcsc0JBQXNCLENBQUM7QUFDckQsb0dBQW9HO0FBQ3ZGLFFBQUEsY0FBYyxHQUFHLFdBQVcsQ0FBQztBQUM3QixRQUFBLGdCQUFnQixHQUFHLENBQUMsQ0FBQztBQU9sQzs7Ozs7OzswRkFPMEY7QUFDbkYsS0FBSyxVQUFVLGFBQWEsQ0FBQyxRQUFnQjtJQUNoRCxJQUFJLE1BQU0sR0FBRyxNQUFNLElBQUEsb0JBQVksRUFBQztRQUM1QixPQUFPLEVBQUUsS0FBSztRQUNkLElBQUksRUFBRSxDQUFDLFVBQVUsRUFBRSxVQUFVLEVBQUUsVUFBVSxFQUFFLG9CQUFvQixDQUFDO1FBQ2hFLEdBQUcsRUFBRSxRQUFRO0tBQ2hCLENBQUMsQ0FBQztJQUNILElBQUksTUFBTSxDQUFDLE1BQU0sS0FBSyxDQUFDLEVBQUUsQ0FBQztRQUN0QixNQUFNLElBQUksS0FBSyxDQUNYLGlDQUFpQyxRQUFRLHlCQUF5QixNQUFNLENBQUMsTUFBTSxJQUFJO2NBQ2pGLEdBQUcsQ0FBQyxNQUFNLENBQUMsTUFBTSxHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FBQyxJQUFJLEVBQUUsRUFBRSxDQUNoRCxDQUFDO0lBQ04sQ0FBQztJQUNELElBQUksTUFBTSxHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQztTQUNqQyxHQUFHLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQUMsSUFBSSxFQUFFLENBQUM7U0FDeEIsTUFBTSxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsSUFBSSxJQUFJLElBQUksS0FBSyxxQkFBYSxJQUFJLElBQUksS0FBSyxzQkFBYyxDQUFDO1NBQ3pFLElBQUksRUFBRSxDQUFDO0lBRVosSUFBSSxPQUFPLEdBQWEsRUFBRSxDQUFDO0lBQzNCLEtBQUssSUFBSSxZQUFZLElBQUksTUFBTSxFQUFFLENBQUM7UUFDOUIsSUFBSSxDQUFDO1lBQ0QsTUFBTSxrQkFBRSxDQUFDLE1BQU0sQ0FBQyxjQUFJLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxZQUFZLENBQUMsQ0FBQyxDQUFDO1lBQ25ELE9BQU8sQ0FBQyxJQUFJLENBQUMsWUFBWSxDQUFDLENBQUM7UUFDL0IsQ0FBQztRQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUM7WUFDVCxPQUFPLENBQUMsR0FBRyxDQUFDLFdBQVcsWUFBWSx3REFBd0QsQ0FBQyxDQUFDO1FBQ2pHLENBQUM7SUFDTCxDQUFDO0lBQ0QsT0FBTyxPQUFPLENBQUM7QUFDbkIsQ0FBQztBQTNCRCxzQ0EyQkM7QUFFRDs7OytCQUcrQjtBQUMvQixTQUFnQixnQkFBZ0IsQ0FBQyxRQUFnQjtJQUM3QyxPQUFPLE1BQU0sQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUMsQ0FBQyxPQUFPLENBQUMsT0FBTyxFQUFFLElBQUksQ0FBQyxFQUFFLFFBQVEsQ0FBQyxDQUFDO0FBQ3JGLENBQUM7QUFGRCw0Q0FFQztBQUVEOytGQUMrRjtBQUN4RixLQUFLLFVBQVUsVUFBVSxDQUFDLFFBQWdCO0lBQzdDLElBQUksUUFBUSxHQUFHLGdCQUFnQixDQUFDLE1BQU0sa0JBQUUsQ0FBQyxRQUFRLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQztJQUM3RCxPQUFPLEVBQUUsSUFBSSxFQUFFLFFBQVEsQ0FBQyxNQUFNLEVBQUUsTUFBTSxFQUFFLGdCQUFNLENBQUMsVUFBVSxDQUFDLFFBQVEsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQztBQUN6RyxDQUFDO0FBSEQsZ0NBR0M7QUFFTSxLQUFLLFVBQVUsYUFBYSxDQUFDLFFBQWdCO0lBQ2hELElBQUksS0FBSyxHQUFzQixFQUFFLENBQUM7SUFDbEMsS0FBSyxJQUFJLFlBQVksSUFBSSxNQUFNLGFBQWEsQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDO1FBQ3JELElBQUksTUFBTSxHQUFHLE1BQU0sVUFBVSxDQUFDLGNBQUksQ0FBQyxJQUFJLENBQUMsUUFBUSxFQUFFLFlBQVksQ0FBQyxDQUFDLENBQUM7UUFDakUsS0FBSyxDQUFDLElBQUksQ0FBQyxFQUFFLElBQUksRUFBRSxZQUFZLEVBQUUsSUFBSSxFQUFFLE1BQU0sQ0FBQyxJQUFJLEVBQUUsTUFBTSxFQUFFLE1BQU0sQ0FBQyxNQUFNLEVBQUUsQ0FBQyxDQUFDO0lBQ2pGLENBQUM7SUFDRCxPQUFPLEVBQUUsT0FBTyxFQUFFLHdCQUFnQixFQUFFLEtBQUssRUFBRSxDQUFDO0FBQ2hELENBQUM7QUFQRCxzQ0FPQztBQUVELG1HQUFtRztBQUNuRyxTQUFnQixjQUFjLENBQUMsUUFBa0I7SUFDN0MsT0FBTyxJQUFJLENBQUMsU0FBUyxDQUFDLFFBQVEsRUFBRSxTQUFTLEVBQUUsQ0FBQyxDQUFDLEdBQUcsSUFBSSxDQUFDO0FBQ3pELENBQUM7QUFGRCx3Q0FFQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBjcnlwdG8gZnJvbSBcImNyeXB0b1wiO1xuaW1wb3J0IGZzIGZyb20gXCJmcy9wcm9taXNlc1wiO1xuaW1wb3J0IHBhdGggZnJvbSBcInBhdGhcIjtcbmltcG9ydCB7IHNwYXduUHJvbWlzZSB9IGZyb20gXCIuLi9oZWxwZXJzL3NwYXduXCI7XG5cbi8vIFBPUlRFRCBDT0RFOiBzZWN1cml0eS9hdXRob3JpemVkS2V5cy9kYWVtb24vcG9ydHNlY3VyZURhZW1vbi5qcyBjb250YWlucyBhIHBsYWluIEpTIHBvcnQgb2YgdGhlXG4vLyB2ZXJpZnlpbmcgaGFsZiBvZiB0aGlzIGZpbGUsIHNvIGl0IGNhbiBjaGVjayBhIHNpZ25hdHVyZSB3aXRoIG5vIGRlcGVuZGVuY2llcy4gQm90aCBzaWRlcyBtdXN0XG4vLyBhZ3JlZSBvbiB0aGUgbWFuaWZlc3Qgc2hhcGUgYW5kIG9uIHdoaWNoIGZpbGVzIGl0IGNvdmVycyAtIGlmIHlvdSBjaGFuZ2Ugb25lLCBtYWtlIHRoZSBtYXRjaGluZ1xuLy8gY2hhbmdlIGluIHRoZSBvdGhlci5cblxuZXhwb3J0IGNvbnN0IE1BTklGRVNUX05BTUUgPSBcInNpZ25lZGZpbGVzLmpzb25cIjtcbmV4cG9ydCBjb25zdCBTSUdOQVRVUkVfTkFNRSA9IFwic2lnbmVkZmlsZXMuanNvbi5zaWdcIjtcbi8vIHNzaCBzaWduYXR1cmVzIGFyZSBuYW1lc3BhY2VkLCBzbyBhIHNpZ25hdHVyZSBtYWRlIGZvciBvbmUgcHVycG9zZSBjYW5ub3QgYmUgcmVwbGF5ZWQgYXMgYW5vdGhlci5cbmV4cG9ydCBjb25zdCBTSUdOX05BTUVTUEFDRSA9IFwic2lnbmZpbGVzXCI7XG5leHBvcnQgY29uc3QgTUFOSUZFU1RfVkVSU0lPTiA9IDE7XG5cbmV4cG9ydCB0eXBlIE1hbmlmZXN0ID0ge1xuICAgIHZlcnNpb246IG51bWJlcjtcbiAgICBmaWxlczogeyBwYXRoOiBzdHJpbmc7IHNpemU6IG51bWJlcjsgc2hhMjU2OiBzdHJpbmcgfVtdO1xufTtcblxuLyoqIEV2ZXJ5dGhpbmcgaW4gdGhlIHJlcG8gdGhhdCBpcyBub3QgaWdub3JlZDogd2hhdCBpcyBpbiB0aGUgaW5kZXgsIHBsdXMgd2hhdCBpcyBub3Qgc3RhZ2VkIHlldC5cbiAgICBHaXQgaXMgYXNrZWQgYmVjYXVzZSBnaXQgaXMgd2hhdCBrbm93cyBob3cgdG8gYXBwbHkgdGhlIGlnbm9yZSBydWxlcy5cblxuICAgIFdoYXQgY29tZXMgYmFjayBpcyBhIGhpbnQgdGhhdCBhIGZpbGUgbWlnaHQgYmUgdGhlcmUsIG5vdCB0aGF0IGl0IGlzLiBBIGZpbGUgZGVsZXRlZCBmcm9tIHRoZVxuICAgIHdvcmtpbmcgdHJlZSBzdGF5cyBpbiB0aGUgaW5kZXggdW50aWwgdGhhdCBkZWxldGlvbiBpcyBzdGFnZWQsIHNvIGdpdCBuYW1lcyBpdCBhbmQgdGhlcmUgaXNcbiAgICBub3RoaW5nIHRvIHJlYWQsIHdoaWNoIGlzIHdoeSBlYWNoIG9uZSBpcyBjaGVja2VkIGJlZm9yZSBpdCBnb2VzIGluLlxuXG4gICAgVGhlIG1hbmlmZXN0IGFuZCBpdHMgc2lnbmF0dXJlIGFyZSBsZWZ0IG91dCwgc2luY2UgdGhleSBjYW5ub3QgZGVzY3JpYmUgdGhlbXNlbHZlcy4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBsaXN0UmVwb0ZpbGVzKHJlcG9QYXRoOiBzdHJpbmcpIHtcbiAgICBsZXQgcmVzdWx0ID0gYXdhaXQgc3Bhd25Qcm9taXNlKHtcbiAgICAgICAgY29tbWFuZDogXCJnaXRcIixcbiAgICAgICAgYXJnczogW1wibHMtZmlsZXNcIiwgXCItLWNhY2hlZFwiLCBcIi0tb3RoZXJzXCIsIFwiLS1leGNsdWRlLXN0YW5kYXJkXCJdLFxuICAgICAgICBjd2Q6IHJlcG9QYXRoLFxuICAgIH0pO1xuICAgIGlmIChyZXN1bHQuc3RhdHVzICE9PSAwKSB7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihcbiAgICAgICAgICAgIGBFeHBlY3RlZCB0byBsaXN0IHRoZSBmaWxlcyBpbiAke3JlcG9QYXRofSwgZ2l0IGxzLWZpbGVzIGV4aXRlZCAke3Jlc3VsdC5zdGF0dXN9LiBgXG4gICAgICAgICAgICArIGAkeyhyZXN1bHQuc3Rkb3V0ICsgcmVzdWx0LnN0ZGVycikudHJpbSgpfWBcbiAgICAgICAgKTtcbiAgICB9XG4gICAgbGV0IGxpc3RlZCA9IHJlc3VsdC5zdGRvdXQuc3BsaXQoXCJcXG5cIilcbiAgICAgICAgLm1hcChsaW5lID0+IGxpbmUudHJpbSgpKVxuICAgICAgICAuZmlsdGVyKGxpbmUgPT4gbGluZSAmJiBsaW5lICE9PSBNQU5JRkVTVF9OQU1FICYmIGxpbmUgIT09IFNJR05BVFVSRV9OQU1FKVxuICAgICAgICAuc29ydCgpO1xuXG4gICAgbGV0IHByZXNlbnQ6IHN0cmluZ1tdID0gW107XG4gICAgZm9yIChsZXQgcmVsYXRpdmVQYXRoIG9mIGxpc3RlZCkge1xuICAgICAgICB0cnkge1xuICAgICAgICAgICAgYXdhaXQgZnMuYWNjZXNzKHBhdGguam9pbihyZXBvUGF0aCwgcmVsYXRpdmVQYXRoKSk7XG4gICAgICAgICAgICBwcmVzZW50LnB1c2gocmVsYXRpdmVQYXRoKTtcbiAgICAgICAgfSBjYXRjaCAoZSkge1xuICAgICAgICAgICAgY29uc29sZS5sb2coYExlYXZpbmcgJHtyZWxhdGl2ZVBhdGh9IG91dCBvZiB0aGUgbWFuaWZlc3QsIGl0IGlzIGdvbmUgZnJvbSB0aGUgd29ya2luZyB0cmVlYCk7XG4gICAgICAgIH1cbiAgICB9XG4gICAgcmV0dXJuIHByZXNlbnQ7XG59XG5cbi8qKiBBIFdpbmRvd3MgY2hlY2tvdXQgdHVybnMgTEYgaW50byBDUkxGLCBzbyB0aGUgc2FtZSBjb21taXQgaGFzaGVzIGRpZmZlcmVudGx5IHRoZXJlIHRoYW4gaXRcbiAgICBkb2VzIG9uIHRoZSBtYWNoaW5lIHRoYXQgcHVsbHMgaXQuIE5vcm1hbGlzaW5nIGZpcnN0IG1ha2VzIHRoZSBkaWdlc3QgZGVzY3JpYmUgdGhlIGNvbnRlbnRcbiAgICByYXRoZXIgdGhhbiB3aGljaGV2ZXIgY2hlY2tvdXQgcHJvZHVjZWQgaXQuIGxhdGluMSByb3VuZCB0cmlwcyBldmVyeSBieXRlLCBzbyB0aGlzIGlzIHNhZmUgb25cbiAgICBmaWxlcyB0aGF0IGFyZSBub3QgdGV4dC4gKi9cbmV4cG9ydCBmdW5jdGlvbiBub3JtYWxpemVDb250ZW50KGNvbnRlbnRzOiBCdWZmZXIpIHtcbiAgICByZXR1cm4gQnVmZmVyLmZyb20oY29udGVudHMudG9TdHJpbmcoXCJsYXRpbjFcIikucmVwbGFjZSgvXFxyXFxuL2csIFwiXFxuXCIpLCBcImxhdGluMVwiKTtcbn1cblxuLyoqIFRoZSBzaXplIGFuZCBoYXNoIG9mIGEgZmlsZSdzIG5vcm1hbGlzZWQgY29udGVudC4gVGhlIHNpemUgaXMgdGhlIG5vcm1hbGlzZWQgb25lIG9uIHB1cnBvc2UsXG4gICAgc28gaXQgYWdyZWVzIHdpdGggdGhlIGhhc2ggcmF0aGVyIHRoYW4gd2l0aCB3aGF0ZXZlciB0aGUgbG9jYWwgY2hlY2tvdXQgaGFwcGVucyB0byBob2xkLiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGRpZ2VzdEZpbGUoZmlsZVBhdGg6IHN0cmluZykge1xuICAgIGxldCBjb250ZW50cyA9IG5vcm1hbGl6ZUNvbnRlbnQoYXdhaXQgZnMucmVhZEZpbGUoZmlsZVBhdGgpKTtcbiAgICByZXR1cm4geyBzaXplOiBjb250ZW50cy5sZW5ndGgsIHNoYTI1NjogY3J5cHRvLmNyZWF0ZUhhc2goXCJzaGEyNTZcIikudXBkYXRlKGNvbnRlbnRzKS5kaWdlc3QoXCJoZXhcIikgfTtcbn1cblxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGJ1aWxkTWFuaWZlc3QocmVwb1BhdGg6IHN0cmluZykge1xuICAgIGxldCBmaWxlczogTWFuaWZlc3RbXCJmaWxlc1wiXSA9IFtdO1xuICAgIGZvciAobGV0IHJlbGF0aXZlUGF0aCBvZiBhd2FpdCBsaXN0UmVwb0ZpbGVzKHJlcG9QYXRoKSkge1xuICAgICAgICBsZXQgZGlnZXN0ID0gYXdhaXQgZGlnZXN0RmlsZShwYXRoLmpvaW4ocmVwb1BhdGgsIHJlbGF0aXZlUGF0aCkpO1xuICAgICAgICBmaWxlcy5wdXNoKHsgcGF0aDogcmVsYXRpdmVQYXRoLCBzaXplOiBkaWdlc3Quc2l6ZSwgc2hhMjU2OiBkaWdlc3Quc2hhMjU2IH0pO1xuICAgIH1cbiAgICByZXR1cm4geyB2ZXJzaW9uOiBNQU5JRkVTVF9WRVJTSU9OLCBmaWxlcyB9O1xufVxuXG4vKiogU29ydGVkIGtleXMgYW5kIGEgdHJhaWxpbmcgbmV3bGluZSwgc28gdGhlIHNhbWUgdHJlZSBhbHdheXMgcHJvZHVjZXMgdGhlIHNhbWUgYnl0ZXMgdG8gc2lnbi4gKi9cbmV4cG9ydCBmdW5jdGlvbiBmb3JtYXRNYW5pZmVzdChtYW5pZmVzdDogTWFuaWZlc3QpIHtcbiAgICByZXR1cm4gSlNPTi5zdHJpbmdpZnkobWFuaWZlc3QsIHVuZGVmaW5lZCwgNCkgKyBcIlxcblwiO1xufVxuIl19
|
|
85
|
+
/* _JS_SOURCE_HASH = "e58787cb53d84ba9647f11c90c737f7303e120b8e02ca37cc9f38202fce1929d"; */
|