azot 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-4ZXWXL4G.js +7692 -0
- package/dist/cli.cjs +8110 -18
- package/dist/cli.js +446 -7
- package/dist/lib.cjs +7720 -12
- package/dist/lib.js +24 -1
- package/package.json +16 -7
- package/dist/chunk-G2YGFBUI.js +0 -21
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,447 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
import {
|
|
3
|
+
Client,
|
|
4
|
+
__dirname,
|
|
5
|
+
fetchDecryptionKeys
|
|
6
|
+
} from "./chunk-4ZXWXL4G.js";
|
|
7
|
+
|
|
8
|
+
// src/cli/main.ts
|
|
9
|
+
import { parseArgs } from "node:util";
|
|
10
|
+
|
|
11
|
+
// src/cli/utils.ts
|
|
12
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
var importClient = async (input) => {
|
|
15
|
+
const inputStat = await stat(input);
|
|
16
|
+
const isDir = inputStat.isDirectory();
|
|
17
|
+
if (isDir) {
|
|
18
|
+
const entries = isDir ? await readdir(input) : [];
|
|
19
|
+
const idFilename = entries.find((entry) => entry.includes("client_id"));
|
|
20
|
+
const keyFilename = entries.find((entry) => entry.includes("private_key"));
|
|
21
|
+
const wvdFilename = entries.find((entry) => entry.endsWith("wvd"));
|
|
22
|
+
const isUnpacked = !!(idFilename && keyFilename);
|
|
23
|
+
const isPacked = !!wvdFilename;
|
|
24
|
+
if (isUnpacked) {
|
|
25
|
+
const idPath = join(input, idFilename);
|
|
26
|
+
const keyPath = join(input, keyFilename);
|
|
27
|
+
const id = await readFile(idPath);
|
|
28
|
+
const key = await readFile(keyPath);
|
|
29
|
+
return Client.fromUnpacked(id, key);
|
|
30
|
+
} else if (isPacked) {
|
|
31
|
+
const wvdPath = join(input, wvdFilename);
|
|
32
|
+
const wvd = await readFile(wvdPath);
|
|
33
|
+
return await Client.fromPacked(wvd);
|
|
34
|
+
} else {
|
|
35
|
+
console.log(`Unable to find client files in ${input}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
} else if (input.endsWith(".wvd")) {
|
|
39
|
+
const wvd = await readFile(input);
|
|
40
|
+
return Client.fromPacked(wvd);
|
|
41
|
+
} else {
|
|
42
|
+
console.log(`Unable to find client files in ${input}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var col = (str, offset = 2, width = 30) => `${" ".repeat(offset)}${str.padEnd(width)}`;
|
|
47
|
+
|
|
48
|
+
// src/cli/commands/client/help.ts
|
|
49
|
+
var help = () => {
|
|
50
|
+
console.log(`azot client: Widevine client utilities
|
|
51
|
+
`);
|
|
52
|
+
console.log(`Usage: azot client <subcommand> [...flags]
|
|
53
|
+
`);
|
|
54
|
+
console.log(`Commands:`);
|
|
55
|
+
console.log(
|
|
56
|
+
col(`info <input>`) + "print info about client that is on <input> path (*.wvd filepath or directory with id and private key)"
|
|
57
|
+
);
|
|
58
|
+
console.log(
|
|
59
|
+
col(`pack <input> <output>`) + "pack client id and private key from <input> directory into single *.wvd file with <output> path"
|
|
60
|
+
);
|
|
61
|
+
console.log(
|
|
62
|
+
col(`unpack <input> <output>`) + "unpack *.wvd from <input> path to separate client id and private key placed in <output> directory"
|
|
63
|
+
);
|
|
64
|
+
console.log("");
|
|
65
|
+
console.log(`Flags:`);
|
|
66
|
+
console.log(col(`-h, --help`) + "Display this menu and exit");
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// src/cli/commands/client/info.ts
|
|
70
|
+
var info = async (input) => {
|
|
71
|
+
const client2 = await importClient(input);
|
|
72
|
+
for (const [key, value] of client2.info.entries()) {
|
|
73
|
+
console.log(`${key}: ${value}`);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// src/cli/commands/client/pack.ts
|
|
78
|
+
import { writeFile } from "node:fs/promises";
|
|
79
|
+
import { join as join2 } from "node:path";
|
|
80
|
+
var pack = async (input = process.cwd(), output) => {
|
|
81
|
+
const client2 = await importClient(input);
|
|
82
|
+
const wvd = await client2.pack();
|
|
83
|
+
const wvdName = `${client2.info.get("company_name")}_${client2.info.get("model_name")}`;
|
|
84
|
+
const wvdOutput = output || join2(process.cwd(), `${wvdName}.wvd`);
|
|
85
|
+
await writeFile(wvdOutput, wvd);
|
|
86
|
+
console.log(`Client packed: ${wvdOutput}`);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// src/cli/commands/client/unpack.ts
|
|
90
|
+
import { writeFile as writeFile2 } from "node:fs/promises";
|
|
91
|
+
import { join as join3 } from "node:path";
|
|
92
|
+
var unpack = async (input = process.cwd(), output) => {
|
|
93
|
+
const client2 = await importClient(input);
|
|
94
|
+
const [id, key] = await client2.unpack();
|
|
95
|
+
const idOutput = join3(output || process.cwd(), `device_client_id_blob`);
|
|
96
|
+
const keyOutput = join3(output || process.cwd(), `device_private_key`);
|
|
97
|
+
await writeFile2(idOutput, id);
|
|
98
|
+
await writeFile2(keyOutput, key);
|
|
99
|
+
console.log(`Client unpacked: ${idOutput}, ${keyOutput}`);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// src/cli/commands/client/index.ts
|
|
103
|
+
var client = { info, pack, unpack, help };
|
|
104
|
+
|
|
105
|
+
// src/cli/commands/license/help.ts
|
|
106
|
+
var help2 = () => {
|
|
107
|
+
console.log(`azot license: Make license request
|
|
108
|
+
`);
|
|
109
|
+
console.log(`Usage: azot license <url> [...flags]
|
|
110
|
+
`);
|
|
111
|
+
console.log(`Commands:`);
|
|
112
|
+
console.log(
|
|
113
|
+
col(`<url>`) + "URL of license server (e.g. https://cwip-shaka-proxy.appspot.com/no_auth)"
|
|
114
|
+
);
|
|
115
|
+
console.log("");
|
|
116
|
+
console.log(`Flags:`);
|
|
117
|
+
console.log(
|
|
118
|
+
col(`-H, --header`) + 'headers to send with license request, compatible with curl (e.g. -H "Authorization: Bearer ...")'
|
|
119
|
+
);
|
|
120
|
+
console.log(col(`-p, --pssh`) + "widevine PSSH data in Base64");
|
|
121
|
+
console.log(
|
|
122
|
+
col(`-c, --client`) + "path to client (directory with id and private key or path to *.wvd file)"
|
|
123
|
+
);
|
|
124
|
+
console.log(
|
|
125
|
+
col(`-e, --encrypt`) + "enable client encryption with service certificate, disabled by default"
|
|
126
|
+
);
|
|
127
|
+
console.log(col(`-h, --help`) + "display this menu and exit");
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/cli/commands/license/license.ts
|
|
131
|
+
var license = async (params) => {
|
|
132
|
+
const headers = Object.fromEntries(
|
|
133
|
+
params.headers?.map((header) => header.split(":").map((s) => s.trim())) || []
|
|
134
|
+
);
|
|
135
|
+
const keys = await fetchDecryptionKeys({
|
|
136
|
+
server: params.url,
|
|
137
|
+
pssh: params.pssh,
|
|
138
|
+
client: await importClient(params.clientPath || process.cwd()),
|
|
139
|
+
headers
|
|
140
|
+
});
|
|
141
|
+
for (const key of keys) {
|
|
142
|
+
console.log(`${key.id}:${key.value}`);
|
|
143
|
+
}
|
|
144
|
+
return keys;
|
|
145
|
+
};
|
|
146
|
+
license.help = help2;
|
|
147
|
+
|
|
148
|
+
// package.json
|
|
149
|
+
var package_default = {
|
|
150
|
+
name: "azot",
|
|
151
|
+
version: "0.7.0",
|
|
152
|
+
description: "Swiss Army knife for pentesting DRMs",
|
|
153
|
+
type: "module",
|
|
154
|
+
files: [
|
|
155
|
+
"dist",
|
|
156
|
+
"scripts"
|
|
157
|
+
],
|
|
158
|
+
main: "dist/lib.cjs",
|
|
159
|
+
module: "dist/lib.js",
|
|
160
|
+
bin: {
|
|
161
|
+
azot: "dist/cli.js"
|
|
162
|
+
},
|
|
163
|
+
scripts: {
|
|
164
|
+
prepublishOnly: "npm run build:lib",
|
|
165
|
+
dev: "wxt",
|
|
166
|
+
"dev:firefox": "wxt -b firefox",
|
|
167
|
+
build: "npm run build:lib && npm run build:chrome",
|
|
168
|
+
"build:lib": "tsup --entry.lib src/lib/main.ts --entry.cli src/cli/main.ts --format cjs,esm --shims --dts --clean",
|
|
169
|
+
"build:chrome": "wxt build",
|
|
170
|
+
"build:firefox": "wxt build -b firefox",
|
|
171
|
+
zip: "wxt zip",
|
|
172
|
+
"zip:firefox": "wxt zip -b firefox",
|
|
173
|
+
compile: "tsc --noEmit",
|
|
174
|
+
postinstall: "node scripts/postinstall.js",
|
|
175
|
+
test: "vitest"
|
|
176
|
+
},
|
|
177
|
+
repository: {
|
|
178
|
+
type: "git",
|
|
179
|
+
url: "https://github.com/vitalygashkov/azot"
|
|
180
|
+
},
|
|
181
|
+
keywords: [
|
|
182
|
+
"widevine",
|
|
183
|
+
"drm",
|
|
184
|
+
"google",
|
|
185
|
+
"pssh",
|
|
186
|
+
"eme",
|
|
187
|
+
"encrypted-media-extensions",
|
|
188
|
+
"media"
|
|
189
|
+
],
|
|
190
|
+
author: "Vitaly Gashkov <vitalygashkov@vk.com>",
|
|
191
|
+
license: "AGPL-3.0",
|
|
192
|
+
readmeFilename: "README.md",
|
|
193
|
+
bugs: {
|
|
194
|
+
url: "https://github.com/vitalygashkov/azot/issues",
|
|
195
|
+
email: "vitalygashkov@vk.com"
|
|
196
|
+
},
|
|
197
|
+
funding: [
|
|
198
|
+
{
|
|
199
|
+
type: "individual",
|
|
200
|
+
url: "https://boosty.to/vitalygashkov"
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
type: "patreon",
|
|
204
|
+
url: "https://www.patreon.com/vitalygashkov"
|
|
205
|
+
}
|
|
206
|
+
],
|
|
207
|
+
exports: {
|
|
208
|
+
".": {
|
|
209
|
+
require: {
|
|
210
|
+
types: "./dist/lib.d.cts",
|
|
211
|
+
default: "./dist/lib.cjs"
|
|
212
|
+
},
|
|
213
|
+
import: {
|
|
214
|
+
types: "./dist/lib.d.ts",
|
|
215
|
+
default: "./dist/lib.js"
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
engines: {
|
|
220
|
+
node: "20 || 21 || 22 || 23"
|
|
221
|
+
},
|
|
222
|
+
dependencies: {
|
|
223
|
+
"@fastify/autoload": "^6.0.2",
|
|
224
|
+
"@fastify/helmet": "^13.0.0",
|
|
225
|
+
"@fastify/rate-limit": "^10.2.1",
|
|
226
|
+
"@fastify/sensible": "^6.0.1",
|
|
227
|
+
"@fastify/type-provider-typebox": "^5.1.0",
|
|
228
|
+
"@sinclair/typebox": "^0.34.11",
|
|
229
|
+
fastify: "^5.1.0",
|
|
230
|
+
jsrsasign: "^11.1.0",
|
|
231
|
+
protobufjs: "^7.4.0"
|
|
232
|
+
},
|
|
233
|
+
devDependencies: {
|
|
234
|
+
"@eslint/js": "^9.12.0",
|
|
235
|
+
"@solidjs/router": "^0.14.10",
|
|
236
|
+
"@types/jsrsasign": "^10.5.14",
|
|
237
|
+
"@types/node": "^22.7.5",
|
|
238
|
+
"@wxt-dev/module-solid": "^1.1.2",
|
|
239
|
+
autoprefixer: "^10.4.17",
|
|
240
|
+
clsx: "^2.1.1",
|
|
241
|
+
eslint: "^9.12.0",
|
|
242
|
+
"eslint-config-prettier": "^9.1.0",
|
|
243
|
+
"eslint-plugin-prettier": "^5.2.1",
|
|
244
|
+
globals: "^15.11.0",
|
|
245
|
+
postcss: "^8.4.33",
|
|
246
|
+
"solid-icons": "^1.1.0",
|
|
247
|
+
"solid-js": "^1.9.1",
|
|
248
|
+
"tailwind-merge": "^2.5.4",
|
|
249
|
+
tailwindcss: "^3.4.1",
|
|
250
|
+
tsup: "^8.3.0",
|
|
251
|
+
typescript: "^5.6.2",
|
|
252
|
+
"typescript-eslint": "^8.9.0",
|
|
253
|
+
"vite-plugin-arraybuffer": "^0.0.8",
|
|
254
|
+
vitest: "^2.1.3",
|
|
255
|
+
wxt: "^0.19.11"
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// src/cli/commands/serve/serve.ts
|
|
260
|
+
import { readdir as readdir2 } from "node:fs/promises";
|
|
261
|
+
import { basename, extname } from "node:path";
|
|
262
|
+
import fastify from "fastify";
|
|
263
|
+
import AutoLoad from "@fastify/autoload";
|
|
264
|
+
import RateLimit from "@fastify/rate-limit";
|
|
265
|
+
import Helmet from "@fastify/helmet";
|
|
266
|
+
import Sensible from "@fastify/sensible";
|
|
267
|
+
|
|
268
|
+
// src/cli/commands/serve/help.ts
|
|
269
|
+
var help3 = () => {
|
|
270
|
+
console.log(`azot serve: Run your API instance
|
|
271
|
+
`);
|
|
272
|
+
console.log(`Usage: azot serve [...flags]
|
|
273
|
+
`);
|
|
274
|
+
console.log(`Flags:`);
|
|
275
|
+
console.log(col(`--host`) + "server host (default: 0.0.0.0)");
|
|
276
|
+
console.log(col(`--port`) + "server port (default: 4000)");
|
|
277
|
+
console.log(
|
|
278
|
+
col(`--config`) + "path to config file (default: azot.config.json)"
|
|
279
|
+
);
|
|
280
|
+
console.log(
|
|
281
|
+
col(`-c, --client`) + "path to client (directory with id and private key or path to *.wvd file)"
|
|
282
|
+
);
|
|
283
|
+
console.log(col(`-s, --secret`) + "secret key to access API endpoints");
|
|
284
|
+
console.log(col(`-h, --help`) + "display this menu and exit");
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// src/cli/commands/serve/state.ts
|
|
288
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
289
|
+
var defaultConfig = {
|
|
290
|
+
host: "0.0.0.0",
|
|
291
|
+
port: 4e3,
|
|
292
|
+
clients: [],
|
|
293
|
+
users: {},
|
|
294
|
+
forcePrivacyMode: true
|
|
295
|
+
};
|
|
296
|
+
var config = defaultConfig;
|
|
297
|
+
var loadConfig = async (configPath) => {
|
|
298
|
+
const data = await readFile2(configPath, { encoding: "utf-8" }).then((data2) => JSON.parse(data2)).catch(() => defaultConfig);
|
|
299
|
+
Object.assign(config, data);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// src/cli/commands/serve/serve.ts
|
|
303
|
+
var serve = async (options = {}) => {
|
|
304
|
+
const server = fastify({ logger: true });
|
|
305
|
+
const configPath = options.config || "azot.config.json";
|
|
306
|
+
await loadConfig(configPath);
|
|
307
|
+
if (options.client) config.clients.push(options.client);
|
|
308
|
+
if (!config.clients.length) {
|
|
309
|
+
const files = await readdir2(process.cwd());
|
|
310
|
+
const clientPath = files.find((file) => file.endsWith(".wvd"));
|
|
311
|
+
if (clientPath) config.clients.push(clientPath);
|
|
312
|
+
}
|
|
313
|
+
if (options.secret) {
|
|
314
|
+
const anonymousUser = { name: "anonymous", clients: [] };
|
|
315
|
+
const user = config.users[options.secret] || anonymousUser;
|
|
316
|
+
const clientFilename = basename(options.client || config.clients.at(-1));
|
|
317
|
+
const clientName = clientFilename.replace(extname(clientFilename), "");
|
|
318
|
+
if (!user.clients.length) user.clients.push(clientName);
|
|
319
|
+
config.users[options.secret] = user;
|
|
320
|
+
}
|
|
321
|
+
server.register(AutoLoad, { dir: `${import.meta.dirname || __dirname}/api` });
|
|
322
|
+
server.register(RateLimit, { max: 100, timeWindow: "1 minute" });
|
|
323
|
+
server.register(Helmet, { global: true });
|
|
324
|
+
server.register(Sensible);
|
|
325
|
+
server.listen({ port: options.port || 4e3, host: options.host || "0.0.0.0" }).catch((err) => {
|
|
326
|
+
server.log.error(err);
|
|
327
|
+
process.exit(1);
|
|
328
|
+
});
|
|
329
|
+
};
|
|
330
|
+
serve.help = help3;
|
|
331
|
+
|
|
332
|
+
// src/cli/main.ts
|
|
333
|
+
var args = parseArgs({
|
|
334
|
+
args: process.argv.slice(2),
|
|
335
|
+
options: {
|
|
336
|
+
pssh: { type: "string", short: "p" },
|
|
337
|
+
client: { type: "string", short: "c" },
|
|
338
|
+
encrypt: { type: "boolean", short: "e", default: false },
|
|
339
|
+
header: { type: "string", short: "H", multiple: true },
|
|
340
|
+
help: { type: "boolean", short: "h" },
|
|
341
|
+
version: { type: "boolean", short: "v" },
|
|
342
|
+
debug: { type: "boolean", short: "d" },
|
|
343
|
+
host: { type: "string" },
|
|
344
|
+
port: { type: "string" },
|
|
345
|
+
secret: { type: "string", short: "s" },
|
|
346
|
+
config: { type: "string" }
|
|
347
|
+
},
|
|
348
|
+
strict: false,
|
|
349
|
+
allowPositionals: true
|
|
350
|
+
});
|
|
351
|
+
var help4 = () => {
|
|
352
|
+
console.log(
|
|
353
|
+
`Azot is a research & pentesting toolkit for Google's Widevine DRM. (${package_default.version})
|
|
354
|
+
`
|
|
355
|
+
);
|
|
356
|
+
console.log(`Usage: azot <command> [...flags]
|
|
357
|
+
`);
|
|
358
|
+
console.log(`Commands:`);
|
|
359
|
+
console.log(col(`serve`) + "Run your API instance");
|
|
360
|
+
console.log(col(`license <url>`) + "Make a license request");
|
|
361
|
+
console.log(
|
|
362
|
+
col(`client <subcommand>`) + "Additional Widevine client utilities"
|
|
363
|
+
);
|
|
364
|
+
console.log("");
|
|
365
|
+
console.log(`Flags:`);
|
|
366
|
+
console.log(col(`-d, --debug`) + "Enable debug level logging");
|
|
367
|
+
console.log(col(`-v, --version`) + "Print version and exit");
|
|
368
|
+
console.log(col(`-h, --help`) + "Display this menu and exit");
|
|
369
|
+
console.log("");
|
|
370
|
+
console.log(`(more flags in azot license --help and azot client --help)`);
|
|
371
|
+
};
|
|
372
|
+
(async () => {
|
|
373
|
+
if (args.values.version) {
|
|
374
|
+
console.log(package_default.version);
|
|
375
|
+
process.exit(0);
|
|
376
|
+
}
|
|
377
|
+
const [command, ...positionals] = args.positionals;
|
|
378
|
+
switch (command) {
|
|
379
|
+
case "serve": {
|
|
380
|
+
if (args.values.help) {
|
|
381
|
+
serve.help();
|
|
382
|
+
process.exit(0);
|
|
383
|
+
}
|
|
384
|
+
serve({
|
|
385
|
+
host: args.values.host,
|
|
386
|
+
port: args.values.port ? parseInt(args.values.port) : void 0,
|
|
387
|
+
config: args.values.config,
|
|
388
|
+
client: args.values.client,
|
|
389
|
+
secret: args.values.secret
|
|
390
|
+
});
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
case "client": {
|
|
394
|
+
const subcommand = positionals.shift();
|
|
395
|
+
const [input, output] = positionals;
|
|
396
|
+
switch (subcommand) {
|
|
397
|
+
case "pack":
|
|
398
|
+
client.pack(input, output);
|
|
399
|
+
break;
|
|
400
|
+
case "unpack":
|
|
401
|
+
client.unpack(input, output);
|
|
402
|
+
break;
|
|
403
|
+
case "info":
|
|
404
|
+
client.info(input);
|
|
405
|
+
break;
|
|
406
|
+
default: {
|
|
407
|
+
if (args.values.help) {
|
|
408
|
+
client.help();
|
|
409
|
+
process.exit(0);
|
|
410
|
+
} else {
|
|
411
|
+
console.log("Client subcommand required: pack, unpack, info");
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
case "license": {
|
|
419
|
+
if (args.values.help) {
|
|
420
|
+
license.help();
|
|
421
|
+
process.exit(0);
|
|
422
|
+
}
|
|
423
|
+
const url = positionals.shift();
|
|
424
|
+
if (!url) throw new Error("License URL required");
|
|
425
|
+
const pssh = args.values.pssh;
|
|
426
|
+
if (!pssh) throw new Error("PSSH required");
|
|
427
|
+
const clientPath = args.values.client;
|
|
428
|
+
const encrypt = args.values.encrypt;
|
|
429
|
+
const headers = args.values.header;
|
|
430
|
+
await license({ url, pssh, clientPath, encrypt, headers });
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
case "pssh":
|
|
434
|
+
break;
|
|
435
|
+
case "test":
|
|
436
|
+
break;
|
|
437
|
+
default: {
|
|
438
|
+
if (args.values.help) {
|
|
439
|
+
help4();
|
|
440
|
+
process.exit(0);
|
|
441
|
+
} else {
|
|
442
|
+
console.log("Command not found");
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
})();
|