-
Notifications
You must be signed in to change notification settings - Fork 1
/
critical.js
299 lines (242 loc) · 11.3 KB
/
critical.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
import util from "node:util";
import got from "got";
import FormData from "form-data";
import { isEqual } from "lodash-es";
import tus from "tus-js-client";
import { calculateElapsedTime, getResponseErrorData, isPortalModuleEnabled } from "../utils.js";
import { genKeyPairAndSeed, getRegistryEntry, setRegistryEntry } from "../utils-registry.js";
const exampleSkylink = "AACogzrAimYPG42tDOKhS3lXZD8YvlF8Q8R17afe95iV2Q";
const exampleSkylinkBase32 = "000ah0pqo256c3orhmmgpol19dslep1v32v52v23ohqur9uuuuc9bm8";
// this resolver skylink points to latest release of webportal-website and
// is updated automatically on each merged pull request via github-actions
// source: https://github.com/SkynetLabs/webportal-website
const exampleResolverSkylink = "AQCExZYFmmc75OPgjPpHuF4WVN0pc4FX2p09t4naLKfTLw";
// check that any relevant configuration is properly set in skyd
export async function skydConfigCheck() {
const time = process.hrtime();
const data = { up: false };
try {
const response = await got(`http://10.10.10.10:9980/renter`, {
headers: { "User-Agent": "Sia-Agent" },
timeout: { connect: 5000 }, // timeout after 5 seconds when skyd is not available
}).json();
// make sure initial funding is set to 10SC
if (response.settings.allowance.paymentcontractinitialfunding !== "10000000000000000000000000") {
throw new Error("Skynet Portal Per-Contract Budget is not set correctly!");
}
data.up = true;
} catch (error) {
Object.assign(data, getResponseErrorData(error)); // extend data object with error data
}
return { name: "skyd_config", time: calculateElapsedTime(time), ...data };
}
// check skyd for total number of workers on cooldown
export async function skydWorkersCooldownCheck() {
const workersCooldownThreshold = 0.6; // set to 60% initially, can be increased later
const time = process.hrtime();
const data = { up: false };
try {
const response = await got(`http://10.10.10.10:9980/renter/workers`, {
headers: { "User-Agent": "Sia-Agent" },
timeout: { connect: 5000 }, // timeout after 5 seconds when skyd is not available
}).json();
const workersCooldown =
response.totaldownloadcooldown + response.totalmaintenancecooldown + response.totaluploadcooldown;
const workersCooldownRatio = workersCooldown / response.numworkers;
if (workersCooldownRatio > workersCooldownThreshold) {
const workersCooldownPercentage = Math.floor(workersCooldownRatio * 100);
const workersCooldownThresholdPercentage = Math.floor(workersCooldownThreshold * 100);
throw new Error(
`${workersCooldown}/${response.numworkers} skyd workers on cooldown (current ${workersCooldownPercentage}%, threshold ${workersCooldownThresholdPercentage}%)`
);
}
data.up = true;
} catch (error) {
Object.assign(data, getResponseErrorData(error)); // extend data object with error data
}
return { name: "skyd_renter_workers", time: calculateElapsedTime(time), ...data };
}
// uploadCheck returns the result of uploading a sample file
export async function uploadCheck() {
const time = process.hrtime();
const form = new FormData();
const payload = Buffer.from(new Date()); // current date to ensure data uniqueness
const data = { up: false };
form.append("file", payload, { filename: "time.txt", contentType: "text/plain" });
try {
const response = await got.post(`https://${process.env.PORTAL_DOMAIN}/skynet/skyfile`, {
body: form,
headers: { "Skynet-Api-Key": process.env.ACCOUNTS_TEST_USER_API_KEY },
});
data.statusCode = response.statusCode;
data.up = true;
data.ip = response.ip;
} catch (error) {
Object.assign(data, getResponseErrorData(error)); // extend data object with error data
}
return { name: "upload_file", time: calculateElapsedTime(time), ...data };
}
// uploadTusCheck returns the result of uploading a sample file through tus endpoint
export async function uploadTusCheck() {
const time = process.hrtime();
const headers = { "Skynet-Api-Key": process.env.ACCOUNTS_TEST_USER_API_KEY ?? "" };
const payload = Buffer.from(new Date()); // current date to ensure data uniqueness
const data = { name: "upload_file_tus", up: false };
try {
return new Promise((resolve, reject) => {
const upload = new tus.Upload(payload, {
endpoint: `https://${process.env.PORTAL_DOMAIN}/skynet/tus`,
headers,
onError: (error) => {
reject(error); // reject with error to trigger failed check
},
onSuccess: async () => {
const response = await got.head(upload.url, { headers });
const skylink = response.headers["skynet-skylink"];
resolve({ time: calculateElapsedTime(time), ...data, skylink, up: Boolean(skylink) });
},
});
upload.start();
});
} catch (error) {
Object.assign(data, getResponseErrorData(error)); // extend data object with error data
return { name: "upload_file_tus", time: calculateElapsedTime(time), ...data };
}
}
// websiteCheck checks whether the main website is working
export async function websiteCheck() {
return genericAccessCheck("website", `https://${process.env.PORTAL_DOMAIN}`);
}
// downloadSkylinkCheck returns the result of downloading the hard coded link
export async function downloadSkylinkCheck() {
const url = `https://${process.env.PORTAL_DOMAIN}/${exampleSkylink}`;
return genericAccessCheck("skylink", url);
}
// downloadResolverSkylinkCheck returns the result of downloading an example resolver skylink
export async function downloadResolverSkylinkCheck() {
const url = `https://${process.env.PORTAL_DOMAIN}/${exampleResolverSkylink}`;
return genericAccessCheck("resolver_skylink", url);
}
// skylinkSubdomainCheck returns the result of downloading the hard coded link via subdomain
export async function skylinkSubdomainCheck() {
const url = `https://${exampleSkylinkBase32}.${process.env.PORTAL_DOMAIN}`;
return genericAccessCheck("skylink_via_subdomain", url);
}
// handshakeSubdomainCheck returns the result of downloading the skylink via handshake domain
export async function handshakeSubdomainCheck() {
const url = `https://note-to-self.hns.${process.env.PORTAL_DOMAIN}`;
return genericAccessCheck("hns_via_subdomain", url);
}
// accountWebsiteCheck returns the result of accessing account dashboard website
export async function accountWebsiteCheck() {
if (!isPortalModuleEnabled("a")) return; // runs only when accounts are enabled
const url = `https://account.${process.env.PORTAL_DOMAIN}/auth/login`;
return genericAccessCheck("account_website", url);
}
// registryWriteAndReadCheck writes to registry and immediately reads and compares the data
export async function registryWriteAndReadCheck() {
const time = process.hrtime();
const data = { name: "registry_write_and_read", up: false };
const { privateKey, publicKey } = await genKeyPairAndSeed();
const expected = { dataKey: "foo-key", data: Uint8Array.from(Buffer.from("foo-data", "utf-8")), revision: BigInt(0) };
try {
await setRegistryEntry(privateKey, publicKey, expected);
const entry = await getRegistryEntry(publicKey, expected.dataKey);
if (isEqual(expected, entry)) {
data.up = true;
} else {
data.errors = [
{
message: "Data mismatch in registry (read after write)",
// use util.inspect to serialize the entries, otherwise built in JSON.stringify will throw error
// on revision being BigInt (unsupported) and data will not be printed properly as Uint8Array
received: util.inspect(entry, { breakLength: Infinity, compact: true }),
expected: util.inspect(expected, { breakLength: Infinity, compact: true }),
},
];
}
} catch (error) {
console.log(error?.request?.body?.message);
data.errors = [{ message: error?.response?.data?.message ?? error.message }];
}
return { ...data, time: calculateElapsedTime(time) };
}
// directServerApiAccessCheck returns the basic server api check on direct server address
export async function directServerApiAccessCheck() {
// skip if SERVER_DOMAIN is not set or it equals PORTAL_DOMAIN (single server portals)
if (!process.env.SERVER_DOMAIN || process.env.SERVER_DOMAIN === process.env.PORTAL_DOMAIN) {
return;
}
const [portalAccessCheck, serverAccessCheck] = await Promise.all([
genericAccessCheck("portal_api_access", `https://${process.env.PORTAL_DOMAIN}`),
genericAccessCheck("server_api_access", `https://${process.env.SERVER_DOMAIN}`),
]);
if (portalAccessCheck.ip !== serverAccessCheck.ip) {
serverAccessCheck.up = false;
serverAccessCheck.errors = serverAccessCheck.errors ?? [];
serverAccessCheck.errors.push({
message: "Access ip mismatch between portal and server access",
response: {
portal: { name: process.env.PORTAL_DOMAIN, ip: portalAccessCheck.ip },
server: { name: process.env.SERVER_DOMAIN, ip: serverAccessCheck.ip },
},
});
}
return serverAccessCheck;
}
// accountHealthCheck returns the result of accounts service health checks
export async function accountHealthCheck(retries = 2) {
if (!isPortalModuleEnabled("a")) return; // runs only when accounts are enabled
const time = process.hrtime();
const data = { up: false };
try {
const response = await got(`https://account.${process.env.PORTAL_DOMAIN}/health`, { responseType: "json" });
data.statusCode = response.statusCode;
data.response = response.body;
data.up = response.body.dbAlive === true;
data.ip = response.ip;
} catch (error) {
Object.assign(data, getResponseErrorData(error)); // extend data object with error data
}
// db checks can be a false negative due to slow network, retry to make sure it is actually down
if (data.up === false && retries > 0) {
setTimeout(() => accountHealthCheck(retries - 1), 3000); // delay 3 seconds and retry
} else {
return { name: "accounts", time: calculateElapsedTime(time), ...data };
}
}
// blockerHealthCheck returns the result of blocker container health endpoint
export async function blockerHealthCheck(retries = 2) {
if (!isPortalModuleEnabled("b")) return; // runs only when blocker is enabled
const time = process.hrtime();
const data = { up: false };
try {
const response = await got(`http://${process.env.BLOCKER_HOST}:${process.env.BLOCKER_PORT}/health`, {
responseType: "json",
});
data.statusCode = response.statusCode;
data.response = response.body;
data.up = response.body.dbAlive === true;
} catch (error) {
Object.assign(data, getResponseErrorData(error)); // extend data object with error data
}
// db checks can be a false negative due to slow network, retry to make sure it is actually down
if (data.up === false && retries > 0) {
setTimeout(() => blockerHealthCheck(retries - 1), 3000); // delay 3 seconds and retry
} else {
return { name: "blocker", time: calculateElapsedTime(time), ...data };
}
}
async function genericAccessCheck(name, url) {
const time = process.hrtime();
const data = { up: false, url };
try {
const response = await got(url, { headers: { "Skynet-Api-Key": process.env.ACCOUNTS_TEST_USER_API_KEY } });
data.statusCode = response.statusCode;
data.up = true;
data.ip = response.ip;
} catch (error) {
Object.assign(data, getResponseErrorData(error)); // extend data object with error data
}
return { name, time: calculateElapsedTime(time), ...data };
}