diff --git a/build/build.ts b/build/build.ts index 5000f8c..899083f 100644 --- a/build/build.ts +++ b/build/build.ts @@ -92,6 +92,19 @@ const assetsPlugin = { }, } as esbuild.Plugin; +const seedPlugin = { + name: "seed", + setup(build) { + build.onLoad({ filter: /\/seed\// }, (args) => { + if (args.with?.["type"] !== "text") return undefined; + let contents = fs.readFileSync(args.path, "utf8"); + // Strip TypeScript directives that are only needed for type-checking + contents = contents.replace(/^\s*\/\/\s*@ts-.*\n/gm, ""); + return { contents, loader: "text" }; + }); + }, +} as esbuild.Plugin; + const packageDotJsonPlugin = { name: "packageDotJson", setup(build) { @@ -295,7 +308,7 @@ async function generateBackendJs(): Promise { banner: { js: "#!/usr/bin/env node" }, entryPoints: services.map((s) => `bin/${s}.ts`), outdir: path.join(OUTPUT_DIR, "bin"), - plugins: [packageDotJsonPlugin, assetsPlugin], + plugins: [packageDotJsonPlugin, assetsPlugin, seedPlugin], }); for (const bin of services) { diff --git a/build/lint.ts b/build/lint.ts index 7449ca0..cd5ae5a 100644 --- a/build/lint.ts +++ b/build/lint.ts @@ -5,7 +5,7 @@ const execPromise = promisify(exec); async function runEslint(): Promise { const CMD = - "eslint 'bin/*.ts' 'lib/**/*.ts' 'ui/**/*.ts' 'test/**/*.ts' 'build/**/*.ts'"; + "eslint 'bin/*.ts' 'lib/**/*.ts' 'ui/**/*.ts' 'test/**/*.ts' 'build/**/*.ts' 'seed/*'"; const env = { ...(process.stdout.isTTY && { FORCE_COLOR: "1" }), ...process.env, @@ -21,7 +21,7 @@ async function runEslint(): Promise { } async function runTsc(): Promise { - const CMD = "tsc --noEmit"; + const CMD = "tsc --noEmit && tsc -p seed/tsconfig.json"; const env = { ...(process.stdout.isTTY && { FORCE_COLOR: "1" }), ...process.env, diff --git a/eslint.config.mjs b/eslint.config.mjs index 4c34b96..9b8b8fd 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -51,4 +51,27 @@ export default defineConfig( }, }, }, + { + files: ["seed/*.js"], + languageOptions: { + parserOptions: { + project: null, + }, + globals: { + declare: "readonly", + clear: "readonly", + commit: "readonly", + ext: "readonly", + log: "readonly", + args: "readonly", + }, + }, + rules: { + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-floating-promises": "off", + "@typescript-eslint/no-misused-promises": "off", + "@typescript-eslint/no-shadow": "off", + "no-shadow": ["error", { allow: ["err", "total"] }], + }, + }, ); diff --git a/lib/init.ts b/lib/init.ts index 4c912db..fad0b13 100644 --- a/lib/init.ts +++ b/lib/init.ts @@ -9,6 +9,9 @@ import { putUser, } from "./ui/db.ts"; import { del } from "./cache.ts"; +import BOOTSTRAP_SCRIPT from "../seed/bootstrap.js" with { type: "text" }; +import DEFAULT_SCRIPT from "../seed/default.js" with { type: "text" }; +import INFORM_SCRIPT from "../seed/inform.js" with { type: "text" }; interface Status { users: boolean; @@ -19,58 +22,6 @@ interface Status { overview: boolean; } -const BOOTSTRAP_SCRIPT = ` -const now = Date.now(); - -// Clear cached data model to force a refresh -clear("Device", now); -clear("InternetGatewayDevice", now); -`.trim(); - -const DEFAULT_SCRIPT = ` -const hourly = Date.now(3600000); - -// Refresh basic parameters hourly -declare("InternetGatewayDevice.DeviceInfo.HardwareVersion", {path: hourly, value: hourly}); -declare("InternetGatewayDevice.DeviceInfo.SoftwareVersion", {path: hourly, value: hourly}); -declare("InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.MACAddress", {path: hourly, value: hourly}); -declare("InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.ExternalIPAddress", {path: hourly, value: hourly}); -declare("InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.SSID", {path: hourly, value: hourly}); -// Don't refresh password field periodically because CPEs always report blank passowrds for security reasons -declare("InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.KeyPassphrase", {path: hourly, value: 1}); -declare("InternetGatewayDevice.LANDevice.*.Hosts.Host.*.HostName", {path: hourly, value: hourly}); -declare("InternetGatewayDevice.LANDevice.*.Hosts.Host.*.IPAddress", {path: hourly, value: hourly}); -declare("InternetGatewayDevice.LANDevice.*.Hosts.Host.*.MACAddress", {path: hourly, value: hourly}); -`.trim(); - -const INFORM_SCRIPT = ` -// Device ID as user name -const username = declare("DeviceID.ID", {value: 1}).value[0] - -// Password will be fixed for a given device because Math.random() is seeded with device ID by default. -const password = Math.trunc(Math.random() * Number.MAX_SAFE_INTEGER).toString(36); - -const informInterval = 300; - -// Refresh values daily -const daily = Date.now(86400000); - -// Unique inform offset per device for better load distribution -const informTime = daily % 86400000; - -declare("InternetGatewayDevice.ManagementServer.ConnectionRequestUsername", {value: daily}, {value: username}); -declare("InternetGatewayDevice.ManagementServer.ConnectionRequestPassword", {value: daily}, {value: password}); -declare("InternetGatewayDevice.ManagementServer.PeriodicInformEnable", {value: daily}, {value: true}); -declare("InternetGatewayDevice.ManagementServer.PeriodicInformInterval", {value: daily}, {value: informInterval}); -declare("InternetGatewayDevice.ManagementServer.PeriodicInformTime", {value: daily}, {value: informTime}); - -declare("Device.ManagementServer.ConnectionRequestUsername", {value: daily}, {value: username}); -declare("Device.ManagementServer.ConnectionRequestPassword", {value: daily}, {value: password}); -declare("Device.ManagementServer.PeriodicInformEnable", {value: daily}, {value: true}); -declare("Device.ManagementServer.PeriodicInformInterval", {value: daily}, {value: informInterval}); -declare("Device.ManagementServer.PeriodicInformTime", {value: daily}, {value: informTime}); -`.trim(); - export async function getStatus(): Promise { const [configSnapshot, presetCount] = await Promise.all([ getRevision(), diff --git a/seed/bootstrap.js b/seed/bootstrap.js new file mode 100644 index 0000000..d1e2356 --- /dev/null +++ b/seed/bootstrap.js @@ -0,0 +1,5 @@ +const now = Date.now(); + +// Clear cached data model to force a refresh +clear("Device", now); +clear("InternetGatewayDevice", now); diff --git a/seed/default.js b/seed/default.js new file mode 100644 index 0000000..20506df --- /dev/null +++ b/seed/default.js @@ -0,0 +1,40 @@ +const hourly = Date.now(3600000); + +// Refresh basic parameters hourly +declare("InternetGatewayDevice.DeviceInfo.HardwareVersion", { + path: hourly, + value: hourly, +}); +declare("InternetGatewayDevice.DeviceInfo.SoftwareVersion", { + path: hourly, + value: hourly, +}); +declare( + "InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.MACAddress", + { path: hourly, value: hourly }, +); +declare( + "InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.ExternalIPAddress", + { path: hourly, value: hourly }, +); +declare("InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.SSID", { + path: hourly, + value: hourly, +}); +// Don't refresh password field periodically because CPEs always report blank passowrds for security reasons +declare("InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.KeyPassphrase", { + path: hourly, + value: 1, +}); +declare("InternetGatewayDevice.LANDevice.*.Hosts.Host.*.HostName", { + path: hourly, + value: hourly, +}); +declare("InternetGatewayDevice.LANDevice.*.Hosts.Host.*.IPAddress", { + path: hourly, + value: hourly, +}); +declare("InternetGatewayDevice.LANDevice.*.Hosts.Host.*.MACAddress", { + path: hourly, + value: hourly, +}); diff --git a/seed/inform.js b/seed/inform.js new file mode 100644 index 0000000..a2b3d3c --- /dev/null +++ b/seed/inform.js @@ -0,0 +1,67 @@ +// Device ID as user name +const username = declare("DeviceID.ID", { value: 1 }).value[0]; + +// Password will be fixed for a given device because Math.random() is seeded with device ID by default. +const password = Math.trunc(Math.random() * Number.MAX_SAFE_INTEGER).toString( + 36, +); + +const informInterval = 300; + +// Refresh values daily +const daily = Date.now(86400000); + +// Unique inform offset per device for better load distribution +const informTime = daily % 86400000; + +declare( + "InternetGatewayDevice.ManagementServer.ConnectionRequestUsername", + { value: daily }, + { value: username }, +); +declare( + "InternetGatewayDevice.ManagementServer.ConnectionRequestPassword", + { value: daily }, + { value: password }, +); +declare( + "InternetGatewayDevice.ManagementServer.PeriodicInformEnable", + { value: daily }, + { value: true }, +); +declare( + "InternetGatewayDevice.ManagementServer.PeriodicInformInterval", + { value: daily }, + { value: informInterval }, +); +declare( + "InternetGatewayDevice.ManagementServer.PeriodicInformTime", + { value: daily }, + { value: informTime }, +); + +declare( + "Device.ManagementServer.ConnectionRequestUsername", + { value: daily }, + { value: username }, +); +declare( + "Device.ManagementServer.ConnectionRequestPassword", + { value: daily }, + { value: password }, +); +declare( + "Device.ManagementServer.PeriodicInformEnable", + { value: daily }, + { value: true }, +); +declare( + "Device.ManagementServer.PeriodicInformInterval", + { value: daily }, + { value: informInterval }, +); +declare( + "Device.ManagementServer.PeriodicInformTime", + { value: daily }, + { value: informTime }, +); diff --git a/seed/provisions.d.ts b/seed/provisions.d.ts new file mode 100644 index 0000000..1140026 --- /dev/null +++ b/seed/provisions.d.ts @@ -0,0 +1,71 @@ +interface Timestamps { + path?: number; + object?: number; + writable?: number; + value?: number; + notification?: number; + accessList?: number; +} + +interface Values { + path?: number | [number, number]; + object?: boolean; + writable?: boolean; + value?: string | number | boolean | [string | number | boolean, string?]; + notification?: number; + accessList?: string[]; +} + +interface ParameterWrapper extends Iterable { + readonly path: string | undefined; + readonly size: number | undefined; + readonly object: 0 | 1 | undefined; + readonly writable: 0 | 1 | undefined; + readonly value: [string | number | boolean, string] | undefined; + readonly notification: number | undefined; + readonly accessList: string[] | undefined; +} + +declare function declare( + path: string, + timestamps?: Timestamps | null, + values?: Values | null, +): ParameterWrapper; + +declare function clear( + path: string, + timestamp: number, + attributes?: Timestamps, +): void; + +declare function commit(): void; + +declare function ext(...args: unknown[]): unknown; + +declare function log(msg: string, meta?: Record): void; + +declare const args: unknown[]; + +interface DateConstructor { + new (): Date; + new ( + year: number, + monthIndex?: number, + day?: number, + hours?: number, + minutes?: number, + seconds?: number, + milliseconds?: number, + ): Date; + now(intervalOrCron?: number | string, variance?: number): number; + parse(dateString: string): number; + UTC( + year: number, + monthIndex?: number, + day?: number, + hours?: number, + minutes?: number, + seconds?: number, + milliseconds?: number, + ): number; +} diff --git a/seed/tsconfig.json b/seed/tsconfig.json new file mode 100644 index 0000000..38e304c --- /dev/null +++ b/seed/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "target": "ES2022", + "types": [] + }, + "include": ["./*.js", "./*.d.ts"] +} diff --git a/tsconfig.json b/tsconfig.json index 03a4903..0d22b74 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "lib": ["ES2022", "dom"], - "module": "ES2022", + "module": "Preserve", "moduleResolution": "bundler", "target": "ES2022", "resolveJsonModule": true,