Compare commits

...
63 Commits
Author SHA1 Message Date
Zaid Abdulla fa95d7a310 1.2.8 2021-10-27 13:07:03 -07:00
Zaid Abdulla d299ef3a59 Changelog for v1.2.8 2021-10-27 13:06:37 -07:00
Zaid Abdulla 2c606b268e Update dependencies 2021-10-27 13:04:34 -07:00
Zaid Abdulla 1b86f41ad7 Replace later dependency with @breejs/later 2021-10-26 17:40:17 -07:00
Zaid Abdulla c4754a9b55 Strip dev deps from new lock file format 2021-10-26 10:33:33 -07:00
Zaid Abdulla f24111ea9f Support modulo operator in expressions 2021-10-25 16:51:46 -07:00
Zaid Abdulla 8f662fb152 Fix expr parser treating - and / as commutative 2021-10-25 13:40:48 -07:00
Zaid Abdulla dd722ba6e0 Improve sorting toggle behavior
Last sorted column now takes precedence over any previously sorted
columns. This makes for a more intuitive user experience.
2021-10-25 13:36:23 -07:00
Zaid Abdulla 88a84788a5 Evaluate component attrs before use 2021-10-24 18:17:03 -07:00
Zaid Abdulla 73baffc3b9 Improve container component
Allow passing custom attribute to the HTML element. Implement function
encodeUriComponent for attribute expressions.
2021-10-24 18:14:36 -07:00
Zaid Abdulla 0188ac860b Handle unsupported ACS methods gracefully 2021-10-15 16:21:08 -07:00
Zaid Abdulla 7f295beeec Validate host arg passed to ping
Fixes remote code execution vulnerability reported by Alex Hordijk.
2021-10-15 16:21:08 -07:00
Zaid Abdulla 2ac536bf8f Fix ui.pageSize regression 2021-09-22 09:27:11 -07:00
Zaid Abdulla f63e357b29 Fix synthesized expressions being non-minimal 2021-09-20 12:41:09 -07:00
Zaid Abdulla ceec3f2cc8 1.2.7 2021-09-18 14:36:19 -07:00
Zaid Abdulla 2b60feb2f4 Changelog for v1.2.7 2021-09-18 14:35:34 -07:00
Zaid Abdulla 9203417686 Fix a couple of regressions in 1.2.6 2021-09-18 14:34:11 -07:00
Zaid Abdulla 8477cd1cfa 1.2.6 2021-09-16 14:25:41 -07:00
Zaid Abdulla 12c958dc03 Changelog for v1.2.6 2021-09-16 14:25:20 -07:00
Zaid Abdulla fdb81b7a43 Update dependencies 2021-09-16 14:17:39 -07:00
Zaid Abdulla 529386a9b6 Prevent invalid YAML config from bricking the UI 2021-09-16 13:27:37 -07:00
Zaid Abdulla 98698b775c Update copyright notice in README.md 2021-09-16 13:27:37 -07:00
Zaid Abdulla 9706026ea5 Update Prettier 2021-09-16 13:27:37 -07:00
Zaid Abdulla 9e1d7eca9b Rename WLAN hosts to LAN hosts in default config 2021-09-16 13:27:37 -07:00
Zaid Abdulla d48deaaf47 Catch JSON parse exceptions in NBI 2021-09-16 13:27:37 -07:00
Zaid Abdulla ec770b4e1b Fix redundant SPV triggered for unknown types
Fix issue where declarations against parameters with non-standard value
type trigger an SPV call when the value being declared is a boolean or
a number.
2021-09-16 13:27:37 -07:00
Zaid Abdulla c3c7ab72b2 Utilize ?. operator and set TypeScript target 2021-09-16 13:27:37 -07:00
Zaid Abdulla ca83704958 Ignore session cookie on Inform
Some devices preserve cookies set in a previous session and pass them
on to the next session. The presence of a session cookie in the Inform
request causes an unnecessary DB query and, in some edge cases, an
Invalid Session error.
2021-09-16 13:27:37 -07:00
Zaid Abdulla bc2d258f9c Refactor bool exp synthesizer
It's now more intelligent about generating IS NULL clauses. For example,
instead of 'LOWER(param) IS NULL' it now generates 'param IS NULL'.
2021-09-16 13:27:37 -07:00
Zaid Abdulla 7ae3bcdc0f Fix escape char not interpreted in expressions 2021-09-16 00:17:21 -07:00
Zaid Abdulla 53bb4d3552 Fix filters resetting user input on redraw 2021-09-16 00:17:21 -07:00
Zaid Abdulla 595f0eddf1 Fix deprecations 2021-09-16 00:17:21 -07:00
Zaid Abdulla 0861b2db40 Update TypeScript 2021-09-16 00:17:21 -07:00
Zaid Abdulla f4692faed6 Update MongoDB driver 2021-09-16 00:17:19 -07:00
Zaid Abdulla cc885e7d04 Update mithril types 2021-08-13 14:36:59 -07:00
Michael Ducharme cd8d264f03 Fix typo in default provision 2021-08-11 14:14:09 -07:00
JonasGhost 1e10d394bb Default to dual stack interface binding 2021-08-11 13:24:22 -07:00
Zaid Abdulla 359eca7bbc Fix upload metadata missing when behind nginx 2021-08-11 13:08:27 -07:00
Zaid Abdulla 088e25cbb0 New config option cwmp.downloadSuccessOnTimeout
Workaround for CPEs that neglect to send TransferComplete message after
a successful download.
2021-08-11 12:27:51 -07:00
Zaid Abdulla 1d02893692 Fix repeating downloads if timestamp > now 2021-08-05 01:15:27 -07:00
Zaid Abdulla da01793ac5 Fix inability to close unsupported preset notif 2021-08-02 14:32:32 -07:00
Zaid Abdulla cce85e48ed Fix crash in NBI when device not found 2021-08-02 14:04:06 -07:00
Zaid Abdulla be90214bf9 Update min Node version in installation guide 2021-08-02 13:14:04 -07:00
Zaid Abdulla 6ce91df127 Display progress bar when uploading file 2021-07-27 14:18:35 -07:00
Zaid Abdulla 1a53a320da Detect and correct for client side clock skew 2021-07-25 12:48:42 -07:00
Zaid Abdulla cfe279b511 Tolerate responses with invalid/missing params 2021-07-24 23:18:51 -07:00
Zaid Abdulla 08cfe4171c Make parsing param paths strict 2021-07-16 13:20:07 -07:00
Zaid Abdulla 6c37e88ab1 Fix crash when CPE sends invalid auth header 2021-07-13 18:45:23 -07:00
Zaid Abdulla 8b27794cc7 Fix crash when passing invalid attrs to declare 2021-07-13 17:00:59 -07:00
Zaid Abdulla 9cf18503ee Fix exceptions from ext showing as timeout error 2021-07-13 15:34:38 -07:00
Zaid Abdulla 3b36cefbca Fix UDP conn request promise rejection warning 2021-06-21 18:17:08 -07:00
Zaid Abdulla 02e09dfdab Add dot to list of encoded chars in tags 2021-06-14 14:06:05 -07:00
Zaid Abdulla 63a75cc765 Fix bug causing lock expired exceptions 2021-06-09 16:32:12 -07:00
Zaid Abdulla 630697a0cb Ignore UDP connection request errors 2021-04-18 11:24:27 -07:00
Zaid Abdulla f6aa444a23 Ignore invalid UDPConnectionRequestAddress 2021-04-17 23:37:40 -07:00
Zaid Abdulla 139dc5410e Don't crash when ping result can't be parsed 2021-04-01 18:54:44 -07:00
Zaid Abdulla e4582695b7 1.2.5 2021-03-12 15:36:19 -08:00
Zaid Abdulla 0064c2b951 Changelog for v1.2.5 2021-03-12 15:36:05 -08:00
Zaid Abdulla 47c2d6ee6f Update dependencies 2021-03-12 15:30:21 -08:00
Zaid Abdulla f290ce696f Support custom types for uploaded files 2021-03-12 15:30:13 -08:00
Zaid Abdulla 556424d4d6 Fix missing padding at bottom of page in Chrome 2021-03-12 13:08:45 -08:00
Zaid Abdulla 7e507125c4 Remove --unsafe-perm as it's no longer necessary 2021-03-10 16:56:45 -08:00
Zaid Abdulla 2ca0caa258 Reimplement foldString without regex
Lookbehind in regex is not supported in Safari browsers. This implementation
should be more efficient so why not.
2021-02-26 13:22:58 -08:00
81 changed files with 12068 additions and 4731 deletions

No files matched your search

+59
View File
@@ -1,5 +1,64 @@
# Change Log # Change Log
## 1.2.8 (2021-10-27)
- Fix a remote code execution security vulnerability in genieacs-ui.
- All UI components can now be configured using fully dynamic expressions.
Previously some component types only accept fixed string values as properties.
- The `container` UI component can now be configured with an `element` property
that's either a string or a nested pair of `tag` and `attributes` properties.
The various attributes under the `attributes` property can now make use of a
new function `ENCODEURICOMPONENT()` to facilitate creating custom hyperlinks
in the UI.
- Improve sorting buttons' behavior in the device listing page and other pages.
Sorting by multiple columns should now feel more intuitive.
- Support the modulo (%) operator in expressions.
- Fix a regression in the previous release where the config option 'ui.pageSize'
no longer works.
- Fix process crash when a CPE sends an unsupported ACS method.
## 1.2.7 (2021-09-18)
- Fix regression causing frequent invalid session errors.
- Fix regression breaking digest authentication in connection requests.
## 1.2.6 (2021-09-16)
- New config option `cwmp.downloadSuccessOnTimeout` to enable a workaround for
CPEs that neglect to send a TransferComplete message after a successful
download.
- Display a progress bar when uploading new files.
- Default to dual stack interface binding (i.e. `::` instead of `0.0.0.0`).
Unless the binding interface is explicitly set, this will cause IPv4 addresses
in the logs to be displayed as IPv4-mapped IPv6 addresses (e.g.
`::ffff:127.0.0.1` instead of `127.0.0.1`).
- Detect and correct for client side (browser) clock skew that would otherwise
alter the numbers in the pie charts.
- Various improvements relating to dealing with buggy TR-069 client
implementations.
- Fix a bug causing "lock expired" exceptions when a CWMP session remains open
for a very long time due to slow clients.
- Fix metadata of uploaded files going missing due to nginx stripping away what
it considers to be invalid headers. The nginx directive
`ignore_invalid_headers` is no longer required.
- Fix crash when a CPE is assigned a tag containing a dot.
- Fix bug preventing the user from closing the preset pop-up after being
presented with unsupported preset message.
- Fix exceptions raised from ext scripts manifesting as timeout faults.
- Fix download getting triggered repeatedly when the value passed to `Download`
parameter is greater than the current timestamp.
- Fix crash when passing invalid attributes to `declare`.
- Fix crash in NBI when pushing tasks to non-existing devices.
- Fix crash in NBI when passing invalid JSON to various API endpoints.
- Fix crash when the output from `ping` command cannot be parsed in some rare
edge cases.
- A number of other fixes and stability improvements.
## 1.2.5 (2021-03-12)
- Support specifying custom types when uploading files.
- Fix JS compatibility issue with Safari browser.
## 1.2.4 (2021-02-24) ## 1.2.4 (2021-02-24)
- The data model state of a CPE is no longer forgotten after unsuccessful - The data model state of a CPE is no longer forgotten after unsuccessful
+2 -2
View File
@@ -17,7 +17,7 @@ supported versions are:
Install GenieACS from NPM: Install GenieACS from NPM:
sudo npm install -g --unsafe-perm genieacs sudo npm install -g genieacs
To build from source instead, clone this repo or download the source archive To build from source instead, clone this repo or download the source archive
then _cd_ into the source directory then run: then _cd_ into the source directory then run:
@@ -72,5 +72,5 @@ For commercial support options, please visit
## License ## License
Copyright 2013-2020 GenieACS Inc. GenieACS is released under the Copyright 2013-2021 GenieACS Inc. GenieACS is released under the
[AGPLv3 license terms](https://raw.githubusercontent.com/genieacs/genieacs/master/LICENSE). [AGPLv3 license terms](https://raw.githubusercontent.com/genieacs/genieacs/master/LICENSE).
+20 -5
View File
@@ -29,7 +29,7 @@ import postcss from "postcss";
import postcssImport from "postcss-import"; import postcssImport from "postcss-import";
import postcssPresetEnv from "postcss-preset-env"; import postcssPresetEnv from "postcss-preset-env";
import cssnano from "cssnano"; import cssnano from "cssnano";
import { optimize, extendDefaultPlugins } from "svgo"; import { optimize } from "svgo";
import * as xmlParser from "../lib/xml-parser"; import * as xmlParser from "../lib/xml-parser";
const MODE = process.env["NODE_ENV"] || "production"; const MODE = process.env["NODE_ENV"] || "production";
@@ -74,6 +74,7 @@ function rmDirSync(dirPath): void {
fs.rmdirSync(dirPath); fs.rmdirSync(dirPath);
} }
// For lockfileVersion = 1
function stripDevDeps(deps): void { function stripDevDeps(deps): void {
if (!deps["dependencies"]) return; if (!deps["dependencies"]) return;
for (const [k, v] of Object.entries(deps["dependencies"])) { for (const [k, v] of Object.entries(deps["dependencies"])) {
@@ -83,6 +84,15 @@ function stripDevDeps(deps): void {
if (!Object.keys(deps["dependencies"]).length) delete deps["dependencies"]; if (!Object.keys(deps["dependencies"]).length) delete deps["dependencies"];
} }
// For lockfileVersion = 2
function stripDevDeps2(deps): void {
if (!deps["packages"]) return;
for (const [k, v] of Object.entries(deps["packages"])) {
delete v["devDependencies"];
if (v["dev"]) delete deps["packages"][k];
}
}
function xmlTostring(xml): string { function xmlTostring(xml): string {
const children = []; const children = [];
for (const c of xml.children || []) children.push(xmlTostring(c)); for (const c of xml.children || []) children.push(xmlTostring(c));
@@ -140,6 +150,7 @@ async function init(): Promise<string[]> {
); );
npmShrinkwrapJson["version"] = packageJson["version"]; npmShrinkwrapJson["version"] = packageJson["version"];
stripDevDeps(npmShrinkwrapJson); stripDevDeps(npmShrinkwrapJson);
stripDevDeps2(npmShrinkwrapJson);
fs.writeFileSync( fs.writeFileSync(
path.resolve(OUTPUT_DIR, "npm-shrinkwrap.json"), path.resolve(OUTPUT_DIR, "npm-shrinkwrap.json"),
JSON.stringify(npmShrinkwrapJson, null, 2) JSON.stringify(npmShrinkwrapJson, null, 2)
@@ -299,12 +310,16 @@ async function generateIconsSprite(): Promise<void> {
const id = path.parse(file).name; const id = path.parse(file).name;
const filePath = path.join(iconsDir, file); const filePath = path.join(iconsDir, file);
const { data } = await optimize(fs.readFileSync(filePath).toString(), { const { data } = await optimize(fs.readFileSync(filePath).toString(), {
plugins: extendDefaultPlugins([ plugins: [
{ {
name: "removeViewBox", name: "preset-default",
active: false, params: {
overrides: {
removeViewBox: false,
}, },
]), },
},
],
}); });
symbols.push(generateSymbol(id, data)); symbols.push(generateSymbol(id, data));
} }
+5
View File
@@ -165,3 +165,8 @@ Brotli
CSV CSV
hostInfo hostInfo
IPv IPv
downloadSuccessOnTimeout
TransferComplete
nginx
ENCODEURICOMPONENT
pageSize
+4 -4
View File
@@ -64,7 +64,7 @@ CWMP_PORT
CWMP_INTERFACE CWMP_INTERFACE
The network interface that genieacs-cwmp binds to. The network interface that genieacs-cwmp binds to.
Default: ``0.0.0.0`` Default: ``::``
CWMP_SSL_CERT CWMP_SSL_CERT
Path to certificate file. If omitted, non-secure HTTP will be used. Path to certificate file. If omitted, non-secure HTTP will be used.
@@ -102,7 +102,7 @@ NBI_PORT
NBI_INTERFACE NBI_INTERFACE
The network interface that genieacs-nbi binds to. The network interface that genieacs-nbi binds to.
Default: ``0.0.0.0`` Default: ``::``
NBI_SSL_CERT NBI_SSL_CERT
Path to certificate file. If omitted, non-secure HTTP will be used. Path to certificate file. If omitted, non-secure HTTP will be used.
@@ -140,7 +140,7 @@ FS_PORT
FS_INTERFACE FS_INTERFACE
The network interface that genieacs-fs binds to. The network interface that genieacs-fs binds to.
Default: ``0.0.0.0`` Default: ``::``
FS_SSL_CERT FS_SSL_CERT
Path to certificate file. If omitted, non-secure HTTP will be used. Path to certificate file. If omitted, non-secure HTTP will be used.
@@ -186,7 +186,7 @@ UI_PORT
UI_INTERFACE UI_INTERFACE
The network interface that genieacs-ui binds to. The network interface that genieacs-ui binds to.
Default: ``0.0.0.0`` Default: ``::``
UI_SSL_CERT UI_SSL_CERT
Path to certificate file. If omitted, non-secure HTTP will be used. Path to certificate file. If omitted, non-secure HTTP will be used.
+2 -2
View File
@@ -19,7 +19,7 @@ Prerequisites
.. topic:: Node.js .. topic:: Node.js
GenieACS requires Node.js 10.x and up. Refer to https://nodejs.org/ for GenieACS requires Node.js 12.13 and up. Refer to https://nodejs.org/ for
instructions. instructions.
.. topic:: MongoDB .. topic:: MongoDB
@@ -34,7 +34,7 @@ Install GenieACS
.. parsed-literal:: .. parsed-literal::
sudo npm install -g --unsafe-perm genieacs@\ |release| sudo npm install -g genieacs@\ |release|
.. topic:: Installing from source .. topic:: Installing from source
+15 -10
View File
@@ -17,7 +17,6 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { parse } from "url";
import * as db from "./db"; import * as db from "./db";
import * as common from "./common"; import * as common from "./common";
import * as cache from "./cache"; import * as cache from "./cache";
@@ -80,7 +79,7 @@ export async function connectionRequest(
).value || [""])[0]; ).value || [""])[0];
} }
const remoteAddress = parse(connectionRequestUrl).host; const remoteAddress = new URL(connectionRequestUrl).hostname;
const evalCallback = (exp): Expression => { const evalCallback = (exp): Expression => {
if (!Array.isArray(exp)) return exp; if (!Array.isArray(exp)) return exp;
@@ -95,7 +94,7 @@ export async function connectionRequest(
else if (name === "password") return password; else if (name === "password") return password;
const p = device[name]; const p = device[name];
if (p && p.value) return p.value[0]; if (p?.value) return p.value[0];
} else if (exp[0] === "FUNC") { } else if (exp[0] === "FUNC") {
if (exp[1] === "REMOTE_ADDRESS") return remoteAddress; if (exp[1] === "REMOTE_ADDRESS") return remoteAddress;
else if (exp[1] === "USERNAME") return username; else if (exp[1] === "USERNAME") return username;
@@ -145,15 +144,24 @@ export async function connectionRequest(
const debug = !!getConfig(snapshot, "cwmp.debug", {}, now, evalCallback); const debug = !!getConfig(snapshot, "cwmp.debug", {}, now, evalCallback);
let udpProm; let udpProm = Promise.resolve(false);
if (udpConnectionRequestAddress) { if (udpConnectionRequestAddress) {
try {
const u = new URL("udp://" + udpConnectionRequestAddress);
udpProm = udpConnectionRequest( udpProm = udpConnectionRequest(
udpConnectionRequestAddress, u.hostname,
parseInt(u.port || "80"),
authExp, authExp,
UDP_CONNECTION_REQUEST_PORT, UDP_CONNECTION_REQUEST_PORT,
debug, debug,
deviceId deviceId
).then(
() => true,
() => false
); );
} catch (err) {
// Ignore invalid address
}
} }
const status = await httpConnectionRequest( const status = await httpConnectionRequest(
@@ -165,10 +173,7 @@ export async function connectionRequest(
deviceId deviceId
); );
if (udpProm) { if (await udpProm) return "";
await udpProm;
return "";
}
return status; return status;
} }
@@ -312,7 +317,7 @@ function sanitizeTask(task): void {
export async function insertTasks(tasks: any[]): Promise<Task[]> { export async function insertTasks(tasks: any[]): Promise<Task[]> {
if (tasks && !Array.isArray(tasks)) tasks = [tasks]; if (tasks && !Array.isArray(tasks)) tasks = [tasks];
else if (!tasks || tasks.length === 0) return tasks || []; else if (!tasks?.length) return tasks || [];
for (const task of tasks) { for (const task of tasks) {
sanitizeTask(task); sanitizeTask(task);
+3 -3
View File
@@ -52,9 +52,9 @@ function parseHeaderFeilds(str: string): Record<string, string> {
return res; return res;
} }
export function parseAuthorizationHeader( export function parseAuthorizationHeader(authHeader: string): {
authHeader: string method: string;
): { method: string } { } {
authHeader = authHeader.trim(); authHeader = authHeader.trim();
const method = authHeader.split(" ", 1)[0]; const method = authHeader.split(" ", 1)[0];
const res = { method: method }; const res = { method: method };
+4 -4
View File
@@ -59,7 +59,7 @@ export async function set(
export async function pop(key: string): Promise<any> { export async function pop(key: string): Promise<any> {
const res = await cacheCollection.findOneAndDelete({ _id: key }); const res = await cacheCollection.findOneAndDelete({ _id: key });
if (res && res["value"]) return res["value"]["value"]; if (res?.["value"]) return res["value"]["value"];
return null; return null;
} }
@@ -78,9 +78,9 @@ export async function acquireLock(
value: token, value: token,
expire: new Date(now + ttl + CLOCK_SKEW_TOLERANCE), expire: new Date(now + ttl + CLOCK_SKEW_TOLERANCE),
}, },
$currentDate: { timestamp: true }, $currentDate: { timestamp: true as unknown as Date },
}, },
{ upsert: true, returnOriginal: false } { upsert: true, returnDocument: "after" }
); );
const v = r.value; const v = r.value;
if (Math.abs(v["timestamp"].getTime() - now) > CLOCK_SKEW_TOLERANCE) if (Math.abs(v["timestamp"].getTime() - now) > CLOCK_SKEW_TOLERANCE)
@@ -114,5 +114,5 @@ export async function releaseLock(
_id: lockName, _id: lockName,
value: token, value: token,
}); });
if (res["result"]["n"] !== 1) throw new Error("Lock expired"); if (res.deletedCount !== 1) throw new Error("Lock expired");
} }
+2 -2
View File
@@ -17,14 +17,14 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import cluster from "cluster"; import cluster, { Worker } from "cluster";
import { cpus } from "os"; import { cpus } from "os";
import * as logger from "./logger"; import * as logger from "./logger";
let respawnTimestamp = 0; let respawnTimestamp = 0;
let crashes: number[] = []; let crashes: number[] = [];
function fork(): cluster.Worker { function fork(): Worker {
const w = cluster.fork(); const w = cluster.fork();
w.on("error", (err: NodeJS.ErrnoException) => { w.on("error", (err: NodeJS.ErrnoException) => {
// Avoid exception when attempting to kill the process just as it's exiting // Avoid exception when attempting to kill the process just as it's exiting
+1 -1
View File
@@ -51,7 +51,7 @@ export function escapeRegExp(str: string): string {
export function encodeTag(tag: string): string { export function encodeTag(tag: string): string {
return encodeURIComponent(tag) return encodeURIComponent(tag)
.replace( .replace(
/[!~*'()]/g, /[!~*'().]/g,
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase() (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()
) )
.replace(/0x(?=[0-9A-Z]{2})/g, "0%78") .replace(/0x(?=[0-9A-Z]{2})/g, "0%78")
+4 -4
View File
@@ -21,13 +21,13 @@ import { PermissionSet, Expression } from "../types";
import { evaluate, or } from "./expression"; import { evaluate, or } from "./expression";
export default class Authorizer { export default class Authorizer {
private permissionSets: PermissionSet[]; private declare permissionSets: PermissionSet[];
private validatorCache: WeakMap< private declare validatorCache: WeakMap<
any, any,
(mutationType, mutation, any) => boolean (mutationType, mutation, any) => boolean
>; >;
private hasAccessCache: Map<string, boolean>; private declare hasAccessCache: Map<string, boolean>;
private getFilterCache: Map<string, Expression>; private declare getFilterCache: Map<string, Expression>;
public constructor(permissionSets: PermissionSet[]) { public constructor(permissionSets: PermissionSet[]) {
this.permissionSets = permissionSets; this.permissionSets = permissionSets;
+271 -241
View File
@@ -29,8 +29,8 @@ const TWO = BI.BigInt(2);
const NEGATIVE_ONE = BI.BigInt(-1); const NEGATIVE_ONE = BI.BigInt(-1);
class Indeterminates { class Indeterminates {
public map: Map<string, number>; public declare map: Map<string, number>;
public sortedKeys: string[]; public declare sortedKeys: string[];
public constructor(str?: string) { public constructor(str?: string) {
this.map = new Map(); this.map = new Map();
@@ -118,7 +118,7 @@ function findGcd(a: BI.bigint, b: BI.bigint): BI.bigint {
} }
class Polynomial { class Polynomial {
public terms: Term[]; public declare terms: Term[];
public constructor(terms: Term[]) { public constructor(terms: Term[]) {
this.terms = terms; this.terms = terms;
@@ -298,12 +298,11 @@ class Polynomial {
} }
type Minterm = number[]; type Minterm = number[];
type Context = Map<string, number>;
abstract class BoolExprSynth { abstract class BoolExprSynth {
public abstract true(context: Context): Minterm[]; public abstract true(context: SynthContext): Minterm[];
public abstract false(context: Context): Minterm[]; public abstract false(context: SynthContext): Minterm[];
public abstract null(context: Context): Minterm[]; public abstract null(context: SynthContext): Minterm[];
} }
class TrueSynth extends BoolExprSynth { class TrueSynth extends BoolExprSynth {
@@ -343,8 +342,8 @@ class NullSynth extends BoolExprSynth {
} }
class VarSynth extends BoolExprSynth { class VarSynth extends BoolExprSynth {
private expStr: string; private declare exp: Expression;
private negate: boolean; private declare negate: boolean;
public constructor(exp: Expression) { public constructor(exp: Expression) {
super(); super();
this.negate = false; this.negate = false;
@@ -368,52 +367,49 @@ class VarSynth extends BoolExprSynth {
this.negate = true; this.negate = true;
} }
} }
this.expStr = JSON.stringify(exp); this.exp = exp;
} }
public true(context: Context): Minterm[] { public true(context: SynthContext): Minterm[] {
let v = context.get(this.expStr); const v = context.getVariable(this.exp);
if (v == null) context.set(this.expStr, (v = context.size));
return [[(v << 2) ^ (this.negate ? 1 : 3)]]; return [[(v << 2) ^ (this.negate ? 1 : 3)]];
} }
public false(context: Context): Minterm[] { public false(context: SynthContext): Minterm[] {
let v = context.get(this.expStr); const v = context.getVariable(this.exp);
if (v == null) context.set(this.expStr, (v = context.size));
return [[(v << 2) ^ (this.negate ? 3 : 1)]]; return [[(v << 2) ^ (this.negate ? 3 : 1)]];
} }
public null(context: Context): Minterm[] { public null(context: SynthContext): Minterm[] {
let v = context.get(this.expStr); const v = context.getVariable(this.exp);
if (v == null) context.set(this.expStr, (v = context.size));
return [[v << 2, (v << 2) ^ 2]]; return [[v << 2, (v << 2) ^ 2]];
} }
} }
class NotSynth extends BoolExprSynth { class NotSynth extends BoolExprSynth {
private exprSynth: BoolExprSynth; private declare exprSynth: BoolExprSynth;
public constructor(e: BoolExprSynth) { public constructor(e: BoolExprSynth) {
super(); super();
this.exprSynth = e; this.exprSynth = e;
} }
public true(context: Context): Minterm[] { public true(context: SynthContext): Minterm[] {
return this.exprSynth.false(context); return this.exprSynth.false(context);
} }
public false(context: Context): Minterm[] { public false(context: SynthContext): Minterm[] {
return this.exprSynth.true(context); return this.exprSynth.true(context);
} }
public null(context: Context): Minterm[] { public null(context: SynthContext): Minterm[] {
return this.exprSynth.null(context); return this.exprSynth.null(context);
} }
} }
class IsNullSynth extends BoolExprSynth { class IsNullSynth extends BoolExprSynth {
private exprSynth: BoolExprSynth; private declare exprSynth: BoolExprSynth;
public constructor(e: BoolExprSynth) { public constructor(e: BoolExprSynth) {
super(); super();
this.exprSynth = e; this.exprSynth = e;
} }
public true(context: Context): Minterm[] { public true(context: SynthContext): Minterm[] {
return this.exprSynth.null(context); return this.exprSynth.null(context);
} }
public false(context: Context): Minterm[] { public false(context: SynthContext): Minterm[] {
return [...this.exprSynth.true(context), ...this.exprSynth.false(context)]; return [...this.exprSynth.true(context), ...this.exprSynth.false(context)];
} }
public null(): Minterm[] { public null(): Minterm[] {
@@ -422,7 +418,7 @@ class IsNullSynth extends BoolExprSynth {
} }
class OrSynth extends BoolExprSynth { class OrSynth extends BoolExprSynth {
private exprSynths: BoolExprSynth[]; private declare exprSynths: BoolExprSynth[];
public constructor(...e: BoolExprSynth[]) { public constructor(...e: BoolExprSynth[]) {
super(); super();
this.exprSynths = e.filter((ee) => !(ee instanceof FalseSynth)); this.exprSynths = e.filter((ee) => !(ee instanceof FalseSynth));
@@ -436,13 +432,13 @@ class OrSynth extends BoolExprSynth {
}); });
this.exprSynths.push(...unpacked); this.exprSynths.push(...unpacked);
} }
public true(context: Context): Minterm[] { public true(context: SynthContext): Minterm[] {
if (this.exprSynths.length === 0) return []; if (this.exprSynths.length === 0) return [];
if (this.exprSynths.length === 1) return this.exprSynths[0].true(context); if (this.exprSynths.length === 1) return this.exprSynths[0].true(context);
if (this.exprSynths.some((e) => e instanceof TrueSynth)) return [[]]; if (this.exprSynths.some((e) => e instanceof TrueSynth)) return [[]];
return this.exprSynths.map((e) => e.true(context)).flat(); return this.exprSynths.map((e) => e.true(context)).flat();
} }
public false(context: Context): Minterm[] { public false(context: SynthContext): Minterm[] {
if (this.exprSynths.length === 0) return [[]]; if (this.exprSynths.length === 0) return [[]];
if (this.exprSynths.length === 1) return this.exprSynths[0].false(context); if (this.exprSynths.length === 1) return this.exprSynths[0].false(context);
if ( if (
@@ -455,7 +451,7 @@ class OrSynth extends BoolExprSynth {
this.exprSynths.map((e) => complement(e.false(context))).flat() this.exprSynths.map((e) => complement(e.false(context))).flat()
); );
} }
public null(context: Context): Minterm[] { public null(context: SynthContext): Minterm[] {
if (this.exprSynths.length === 0) return []; if (this.exprSynths.length === 0) return [];
if (this.exprSynths.length === 1) return this.exprSynths[0].null(context); if (this.exprSynths.length === 1) return this.exprSynths[0].null(context);
const n = this.exprSynths.map((e) => e.null(context)).flat(); const n = this.exprSynths.map((e) => e.null(context)).flat();
@@ -465,7 +461,7 @@ class OrSynth extends BoolExprSynth {
} }
class AndSynth extends BoolExprSynth { class AndSynth extends BoolExprSynth {
private exprSynths: BoolExprSynth[]; private declare exprSynths: BoolExprSynth[];
public constructor(...e: BoolExprSynth[]) { public constructor(...e: BoolExprSynth[]) {
super(); super();
this.exprSynths = e.filter((ee) => !(ee instanceof TrueSynth)); this.exprSynths = e.filter((ee) => !(ee instanceof TrueSynth));
@@ -479,7 +475,7 @@ class AndSynth extends BoolExprSynth {
}); });
this.exprSynths.push(...unpacked); this.exprSynths.push(...unpacked);
} }
public true(context: Context): Minterm[] { public true(context: SynthContext): Minterm[] {
if (this.exprSynths.length === 0) return [[]]; if (this.exprSynths.length === 0) return [[]];
if (this.exprSynths.length === 1) return this.exprSynths[0].true(context); if (this.exprSynths.length === 1) return this.exprSynths[0].true(context);
if ( if (
@@ -492,13 +488,13 @@ class AndSynth extends BoolExprSynth {
this.exprSynths.map((e) => complement(e.true(context))).flat() this.exprSynths.map((e) => complement(e.true(context))).flat()
); );
} }
public false(context: Context): Minterm[] { public false(context: SynthContext): Minterm[] {
if (this.exprSynths.length === 0) return []; if (this.exprSynths.length === 0) return [];
if (this.exprSynths.length === 1) return this.exprSynths[0].false(context); if (this.exprSynths.length === 1) return this.exprSynths[0].false(context);
if (this.exprSynths.some((e) => e instanceof FalseSynth)) return [[]]; if (this.exprSynths.some((e) => e instanceof FalseSynth)) return [[]];
return this.exprSynths.map((e) => e.false(context)).flat(); return this.exprSynths.map((e) => e.false(context)).flat();
} }
public null(context: Context): Minterm[] { public null(context: SynthContext): Minterm[] {
if (this.exprSynths.length === 0) return []; if (this.exprSynths.length === 0) return [];
if (this.exprSynths.length === 1) return this.exprSynths[0].null(context); if (this.exprSynths.length === 1) return this.exprSynths[0].null(context);
const n = this.exprSynths.map((e) => e.null(context)).flat(); const n = this.exprSynths.map((e) => e.null(context)).flat();
@@ -508,12 +504,12 @@ class AndSynth extends BoolExprSynth {
} }
class CaseSynth extends BoolExprSynth { class CaseSynth extends BoolExprSynth {
private exprSynths: BoolExprSynth[]; private declare exprSynths: BoolExprSynth[];
public constructor(e: BoolExprSynth[]) { public constructor(e: BoolExprSynth[]) {
super(); super();
this.exprSynths = e; this.exprSynths = e;
} }
public true(context: Context): Minterm[] { public true(context: SynthContext): Minterm[] {
const minterms: Minterm[] = []; const minterms: Minterm[] = [];
const cumulative: Minterm[] = []; const cumulative: Minterm[] = [];
for (let i = 0; i < this.exprSynths.length; i += 2) { for (let i = 0; i < this.exprSynths.length; i += 2) {
@@ -533,7 +529,7 @@ class CaseSynth extends BoolExprSynth {
} }
return minterms; return minterms;
} }
public false(context: Context): Minterm[] { public false(context: SynthContext): Minterm[] {
const minterms: Minterm[] = []; const minterms: Minterm[] = [];
const cumulative: Minterm[] = []; const cumulative: Minterm[] = [];
for (let i = 0; i < this.exprSynths.length; i += 2) { for (let i = 0; i < this.exprSynths.length; i += 2) {
@@ -553,7 +549,7 @@ class CaseSynth extends BoolExprSynth {
} }
return minterms; return minterms;
} }
public null(context: Context): Minterm[] { public null(context: SynthContext): Minterm[] {
const minterms: Minterm[] = []; const minterms: Minterm[] = [];
const cumulative: Minterm[] = []; const cumulative: Minterm[] = [];
for (let i = 0; i < this.exprSynths.length; i += 2) { for (let i = 0; i < this.exprSynths.length; i += 2) {
@@ -666,10 +662,10 @@ function normalizeCallback(exp: Expression): Expression {
if (p == null) return null; if (p == null) return null;
args.push(p); args.push(p);
} }
return (args.reduce( return args.reduce(
(previousValue, currentValue) => previousValue.add(currentValue), (previousValue, currentValue) => previousValue.add(currentValue),
ADDITIVE_IDENTITY ADDITIVE_IDENTITY
) as unknown) as Expression; ) as unknown as Expression;
} else if (op === "*") { } else if (op === "*") {
const args: Polynomial[] = []; const args: Polynomial[] = [];
for (let i = 1; i < exp.length; ++i) { for (let i = 1; i < exp.length; ++i) {
@@ -677,10 +673,10 @@ function normalizeCallback(exp: Expression): Expression {
if (p == null) return null; if (p == null) return null;
args.push(p); args.push(p);
} }
return (args.reduce( return args.reduce(
(previousValue, currentValue) => previousValue.multiply(currentValue), (previousValue, currentValue) => previousValue.multiply(currentValue),
MULTIPLICATIVE_IDENTITY MULTIPLICATIVE_IDENTITY
) as unknown) as Expression; ) as unknown as Expression;
} else if (op === "-") { } else if (op === "-") {
const args: Polynomial[] = []; const args: Polynomial[] = [];
for (let i = 1; i < exp.length; ++i) { for (let i = 1; i < exp.length; ++i) {
@@ -688,9 +684,9 @@ function normalizeCallback(exp: Expression): Expression {
if (p == null) return null; if (p == null) return null;
args.push(p); args.push(p);
} }
return (args.reduce((previousValue, currentValue) => return args.reduce((previousValue, currentValue) =>
previousValue.subtract(currentValue) previousValue.subtract(currentValue)
) as unknown) as Expression; ) as unknown as Expression;
} else if (op === "/") { } else if (op === "/") {
const args: Polynomial[] = []; const args: Polynomial[] = [];
for (let i = 1; i < exp.length; ++i) { for (let i = 1; i < exp.length; ++i) {
@@ -698,9 +694,9 @@ function normalizeCallback(exp: Expression): Expression {
if (p == null) return null; if (p == null) return null;
args.push(p); args.push(p);
} }
return (args.reduce((previousValue, currentValue) => return args.reduce((previousValue, currentValue) =>
previousValue.divide(currentValue) previousValue.divide(currentValue)
) as unknown) as Expression; ) as unknown as Expression;
} else if (["=", "<>", ">", ">=", "<", "<="].includes(op)) { } else if (["=", "<>", ">", ">=", "<", "<="].includes(op)) {
if (exp[1] == null || exp[2] == null) return null; if (exp[1] == null || exp[2] == null) return null;
let lhs: Polynomial, rhs: Polynomial; let lhs: Polynomial, rhs: Polynomial;
@@ -798,17 +794,14 @@ function toBoolExprSynth(e: Expression | BoolExprSynth): BoolExprSynth {
return new FalseSynth(); return new FalseSynth();
} }
function sopToExpression( function sopToExpression(sop: number[][], context: SynthContext): Expression {
sop: number[][],
expressions: Map<number, Expression>
): Expression {
if (!sop.length) return false; if (!sop.length) return false;
const res: Expression[] = []; const res: Expression[] = [];
for (const s of sop) { for (const s of sop) {
if (!s.length) return true; if (!s.length) return true;
const conjs: Expression[] = []; const conjs: Expression[] = [];
for (const i of s) { for (const i of s) {
let expr = expressions.get(i >>> 2); let expr = context.getExpression(i >>> 2);
if (!(i & 1) !== !(i & 2)) expr = ["NOT", expr]; if (!(i & 1) !== !(i & 2)) expr = ["NOT", expr];
if (Array.isArray(expr) && expr[0] === "NOT" && Array.isArray(expr[1])) { if (Array.isArray(expr) && expr[0] === "NOT" && Array.isArray(expr[1])) {
const e: Expression[] = expr[1]; const e: Expression[] = expr[1];
@@ -832,163 +825,196 @@ function sopToExpression(
return res[0]; return res[0];
} }
function generateDcSetAndIsNull( function findIsNullDeps(exp: Expression): Expression[] {
variables: Map<number, Expression> if (!Array.isArray(exp)) return [];
): { dcSet: number[][]; isNull: Map<number, number> } { const op = exp[0];
const relations: Map< if (op === "IS NULL" || op === "IS NOT NULL") return [];
if (op === "FUNC") {
if (exp[1] === "NOW") {
return [];
} else if (exp[1] === "LOWER" || exp[1] === "UPPER") {
return findIsNullDeps(exp[2]);
} else if (exp[1] === "ROUND") {
return exp
.slice(2, 4)
.map((e) => findIsNullDeps(e))
.flat();
}
} else if (op !== "PARAM") {
return exp
.slice(1)
.map((e) => findIsNullDeps(e))
.flat();
}
return [exp];
}
class SynthContext {
private declare _dcSet: number[][];
private declare _variables: Map<string, number>;
private declare _expressions: Map<number, Expression>;
private declare _isNullRelations: Map<number, number[]>;
private declare _isNullVars: Set<number>;
private declare _comparisons: Map<
string, string,
{ op: string; rhs: boolean | number | string; var: number }[] Map<string, { eq: number; gt: number; lt: number }>
> = new Map(); >;
for (const [n, e] of variables) { constructor() {
const op = e[0] as string; this._dcSet = [];
if (["=", ">", "<"].includes(op) && !Array.isArray(e[2])) { this._variables = new Map();
const lhs = JSON.stringify(e[1]); this._expressions = new Map();
let r = relations.get(lhs); this._isNullRelations = new Map();
if (!r) relations.set(lhs, (r = [])); this._isNullVars = new Set();
r.push({ op: op, rhs: e[2], var: n }); this._comparisons = new Map();
} else if (op === "LIKE" && !Array.isArray(e[2]) && !Array.isArray(e[3])) {
const lhs = JSON.stringify(e[1]);
let r = relations.get(lhs);
if (!r) relations.set(lhs, (r = []));
r.push({ op: "", rhs: null, var: n });
} else {
const lhs = JSON.stringify(e);
let r = relations.get(lhs);
if (!r) relations.set(lhs, (r = []));
r.push({ op: "", rhs: null, var: n });
}
} }
const dcSet: number[][] = []; getVariable(exp: Expression): number {
const isNull: Map<number, number> = new Map(); const expStr = JSON.stringify(exp);
if (this._variables.has(expStr)) return this._variables.get(expStr);
const v = this._variables.size;
this._variables.set(expStr, v);
this._expressions.set(v, exp);
this.generateIsNullDc(v);
this.generateComparisionDc(v);
for (const [key, rels] of relations) { if (
const n = variables.size; expStr === '["IS NULL",["PARAM","DeviceID.ID"]]' ||
variables.set(n, ["IS NULL", JSON.parse(key)]); expStr === '["IS NULL",["PARAM","_id"]]'
)
this._dcSet.push([(v << 2) ^ 3]);
if (key === '["PARAM","DeviceID.ID"]' || key === '["PARAM","_id"]') return v;
dcSet.push([(n << 2) ^ 3]);
const existingOps = new Map() as Map<string, Map<string, number>>;
for (const rel of rels) {
if (!rel.op) continue;
const rhsStr = JSON.stringify(rel.rhs);
const s = existingOps.get(rhsStr);
if (s) s.set(rel.op, rel.var);
else existingOps.set(rhsStr, new Map([[rel.op, rel.var]]));
} }
for (const [k, v] of existingOps) { private generateIsNullDc(v: number): void {
if (v.size > 1) { this._isNullRelations.set(v, []);
if (!v.has("=")) { const exp = this._expressions.get(v);
const va = variables.size;
variables.set(va, ["=", JSON.parse(key), JSON.parse(k)]); const isNull = new Set(
rels.push({ op: "=", rhs: JSON.parse(k), var: va }); findIsNullDeps(exp).map((e) => this.getVariable(["IS NULL", e]))
v.set("=", va); );
if (!isNull.size) return;
for (const n of isNull) {
this._isNullVars.add(n);
this._isNullRelations.get(n).push(v);
this._isNullRelations.get(v).push(n);
this._dcSet.push([(n << 2) ^ 3, (v << 2) ^ 1]);
this._dcSet.push([(n << 2) ^ 3, (v << 2) ^ 3]);
}
this._dcSet.push(
[...isNull].map((n) => (n << 2) ^ 2).concat([(v << 2) ^ 0, (v << 2) ^ 2])
);
} }
if (!v.has(">")) { private generateComparisionDc(v: number): void {
const va = variables.size; const exp = this._expressions.get(v);
variables.set(va, [">", JSON.parse(key), JSON.parse(k)]); if (!Array.isArray(exp)) return;
rels.push({ op: ">", rhs: JSON.parse(k), var: va }); const op = exp[0];
v.set(">", va); if (![">", "<", "="].includes(op)) return;
} const rhs = exp[2];
const rhsStr = JSON.stringify(rhs);
const lhsStr = JSON.stringify(exp[1]);
let allComps = this._comparisons.get(lhsStr);
if (!allComps) this._comparisons.set(lhsStr, (allComps = new Map()));
if (allComps.has(rhsStr)) return;
if (!v.has("<")) { const comp = {
const va = variables.size; eq: -1,
variables.set(va, ["<", JSON.parse(key), JSON.parse(k)]); gt: -1,
rels.push({ op: "<", rhs: JSON.parse(k), var: va }); lt: -1,
v.set("<", va); };
}
dcSet.push([ allComps.set(rhsStr, comp);
(v.get("=") << 2) ^ 1,
(v.get(">") << 2) ^ 1, comp.eq = this.getVariable(["=", exp[1], exp[2]]);
(v.get("<") << 2) ^ 1, comp.gt = this.getVariable([">", exp[1], exp[2]]);
comp.lt = this.getVariable(["<", exp[1], exp[2]]);
this._dcSet.push([
(comp.eq << 2) ^ 1,
(comp.gt << 2) ^ 1,
(comp.lt << 2) ^ 1,
]); ]);
this._dcSet.push([(comp.eq << 2) ^ 3, (comp.gt << 2) ^ 3]);
this._dcSet.push([(comp.eq << 2) ^ 3, (comp.lt << 2) ^ 3]);
this._dcSet.push([(comp.gt << 2) ^ 3, (comp.lt << 2) ^ 3]);
const type1 = typeof exp[2];
if (!["boolean", "number", "string"].includes(type1)) return;
for (const [rhs2Str, comp2] of allComps) {
if (comp2 === comp) continue;
const rhs2 = JSON.parse(rhs2Str);
const type2 = typeof rhs2;
if (!["boolean", "number", "string"].includes(type2)) continue;
let cmp = 0;
if (type1 === type2) cmp = rhs > rhs2 ? 1 : -1;
else if (type1 === "string") cmp = 1;
else if (type2 === "string") cmp = -1;
else cmp = +rhs - +rhs2;
const c1 = cmp > 0 ? comp : comp2;
const c2 = cmp > 0 ? comp2 : comp;
// This is the minimum clauses required if all relavent vars
// were included in the DC set.
// this._dcSet.push([(c1.eq << 2) ^ 3, (c2.eq << 2) ^ 3]);
// this._dcSet.push([(c1.lt << 2) ^ 1, (c2.gt << 2) ^ 1]);
// But we use non-minimal set because intermediate vars
// between any two may not be present in the DC set.
this._dcSet.push([(c1.lt << 2) ^ 1, (c2.gt << 2) ^ 1]);
this._dcSet.push([(c1.lt << 2) ^ 1, (c2.eq << 2) ^ 3]);
this._dcSet.push([(c1.lt << 2) ^ 1, (c2.lt << 2) ^ 3]);
this._dcSet.push([(c1.eq << 2) ^ 3, (c2.gt << 2) ^ 1]);
this._dcSet.push([(c1.eq << 2) ^ 3, (c2.eq << 2) ^ 3]);
this._dcSet.push([(c1.eq << 2) ^ 3, (c2.lt << 2) ^ 3]);
this._dcSet.push([(c1.gt << 2) ^ 3, (c2.gt << 2) ^ 1]);
this._dcSet.push([(c1.gt << 2) ^ 3, (c2.eq << 2) ^ 3]);
this._dcSet.push([(c1.gt << 2) ^ 3, (c2.lt << 2) ^ 3]);
} }
} }
for (let i = 0; i < rels.length; ++i) { getExpression(v: number): Expression {
const rel1 = rels[i]; return this._expressions.get(v);
isNull.set(rel1.var, n);
dcSet.push([(n << 2) ^ 3, (rel1.var << 2) ^ 1]);
dcSet.push([(n << 2) ^ 3, (rel1.var << 2) ^ 3]);
dcSet.push([(rel1.var << 2) ^ 0, (rel1.var << 2) ^ 2, (n << 2) ^ 2]);
if (!rel1.op) continue;
for (let j = i + 1; j < rels.length; ++j) {
const rel2 = rels[j];
if (!rel2.op) continue;
let comp = 0;
if (Array.isArray(rel1.rhs) || Array.isArray(rel2.rhs)) {
if (JSON.stringify(rel1.rhs) !== JSON.stringify(rel2.rhs)) continue;
} else if (typeof rel1.rhs === "string") {
if (typeof rel2.rhs === "string") {
if (rel1.rhs > rel2.rhs) comp = 1;
else if (rel1.rhs < rel2.rhs) comp = -1;
} else {
comp = 1;
}
} else if (typeof rel2.rhs === "string") {
comp = -1;
} else {
comp = +rel1.rhs - +rel2.rhs;
} }
if (rel1.op === rel2.op) { getIsNullRelations(v: number): number[] {
if (comp === 0) return this._isNullRelations.get(v);
dcSet.push([(rel1.var << 2) ^ 1, (rel2.var << 2) ^ 1]);
else if (rel1.op === "=")
dcSet.push([(rel1.var << 2) ^ 3, (rel2.var << 2) ^ 3]);
else if (
(rel1.op === ">" && comp > 0) ||
(rel1.op === "<" && comp < 0)
)
dcSet.push([(rel1.var << 2) ^ 3, (rel2.var << 2) ^ 1]);
else if (
(rel1.op === "<" && comp > 0) ||
(rel1.op === ">" && comp < 0)
)
dcSet.push([(rel1.var << 2) ^ 1, (rel2.var << 2) ^ 3]);
} else if (rel1.op === "=" || rel2.op === "=") {
let r1 = rel1,
r2 = rel2,
cmp = comp;
if (rel1.op !== "=") {
r2 = rel1;
r1 = rel2;
cmp = -comp;
}
if (r2.op === ">" || r2.op === "<") {
if (cmp === 0) {
dcSet.push([(r1.var << 2) ^ 3, (r2.var << 2) ^ 3]);
} else {
if ((cmp > 0 && r2.op === ">") || (cmp < 0 && r2.op === "<"))
dcSet.push([(r1.var << 2) ^ 3, (r2.var << 2) ^ 1]);
else dcSet.push([(r1.var << 2) ^ 3, (r2.var << 2) ^ 3]);
}
}
} else if (
[">", "<"].includes(rel1.op) &&
[">", "<"].includes(rel2.op)
) {
if ((rel1.op === ">" && comp < 0) || (rel1.op === "<" && comp > 0))
dcSet.push([(rel1.var << 2) ^ 1, (rel2.var << 2) ^ 1]);
else dcSet.push([(rel1.var << 2) ^ 3, (rel2.var << 2) ^ 3]);
} }
isIsNull(v: number): boolean {
return this._isNullVars.has(v);
} }
getDcSet(minterms?: Minterm[]): number[][] {
if (!minterms) return this._dcSet;
const vars = new Set(minterms.flat().map((v) => v >> 2));
for (const comps of this._comparisons.values()) {
for (const comp of comps.values()) {
const c = Object.values(comp).filter((v) => !vars.has(v));
if (c.length === 1) vars.add(c[0]);
} }
} }
return { dcSet, isNull }; for (const n of this._isNullVars)
if (this._isNullRelations.get(n).some((v) => vars.has(v))) vars.add(n);
const dcSet = this._dcSet.filter((m) => m.every((v) => vars.has(v >> 2)));
return dcSet;
}
} }
function sanitizeMinterms( function sanitizeMinterms(
minterms: number[][], minterms: number[][],
isNull: Map<number, number> context: SynthContext
): number[][] { ): number[][] {
const res = [] as number[][]; const res = [] as number[][];
loop: for (const m of minterms) { loop: for (const m of minterms) {
@@ -999,13 +1025,20 @@ function sanitizeMinterms(
const perms: number[][] = []; const perms: number[][] = [];
for (const [k, v] of merged) { for (const [k, v] of merged) {
if (v === 0b1010) continue loop; if (v === 0b1010) continue loop;
const n = isNull.get(k) << 2; const isNullVars = context.getIsNullRelations(k);
const t = k << 2; const t = k << 2;
if (v === 0b0101) minterm.push(n ^ 3); if (v === 0b0101) {
else if (v === 0b0001) perms.push([n ^ 3, t ^ 3]); if (isNullVars.length === 1) minterm.push((isNullVars[0] << 2) ^ 3);
else if (v === 0b0100) perms.push([n ^ 3, t ^ 1]); else perms.push(isNullVars.map((n) => (n << 2) ^ 3));
else if (v & 0b1000) minterm.push(t ^ 3); } else if (v === 0b0001) {
else if (v & 0b0010) minterm.push(t ^ 1); perms.push([...isNullVars.map((n) => (n << 2) ^ 3), t ^ 3]);
} else if (v === 0b0100) {
perms.push([...isNullVars.map((n) => (n << 2) ^ 3), t ^ 1]);
} else if (v & 0b1000) {
minterm.push(t ^ 3);
} else if (v & 0b0010) {
minterm.push(t ^ 1);
}
} }
let ms = [minterm]; let ms = [minterm];
while (perms.length) { while (perms.length) {
@@ -1020,17 +1053,13 @@ function sanitizeMinterms(
} }
function boolExprSynthToExpression(boolExpr: BoolExprSynth): Expression { function boolExprSynthToExpression(boolExpr: BoolExprSynth): Expression {
const context: Map<string, number> = new Map(); const context = new SynthContext();
let minterms = boolExpr.true(context); let minterms = boolExpr.true(context);
const variables = new Map( minterms = sanitizeMinterms(minterms, context);
Array.from(context).map(([k, v]) => [v, JSON.parse(k) as Expression]) const canRaise = getCanRaiseCallback(context);
); const dcSet = context.getDcSet(minterms);
const { dcSet, isNull } = generateDcSetAndIsNull(variables);
minterms = sanitizeMinterms(minterms, isNull);
const canRaise = getCanRaiseCallback(isNull);
minterms = espresso(minterms, dcSet, { canRaise }); minterms = espresso(minterms, dcSet, { canRaise });
return sopToExpression(minterms, variables); return sopToExpression(minterms, context);
} }
function mapCallback(exp: Expression | BoolExprSynth): Expression { function mapCallback(exp: Expression | BoolExprSynth): Expression {
@@ -1043,22 +1072,22 @@ function mapCallback(exp: Expression | BoolExprSynth): Expression {
const op = exp[0]; const op = exp[0];
if (op === "IS NULL") if (op === "IS NULL")
return (new IsNullSynth(toBoolExprSynth(exp[1])) as unknown) as Expression; return new IsNullSynth(toBoolExprSynth(exp[1])) as unknown as Expression;
if (op === "IS NOT NULL") { if (op === "IS NOT NULL") {
return (new NotSynth( return new NotSynth(
new IsNullSynth(toBoolExprSynth(exp[1])) new IsNullSynth(toBoolExprSynth(exp[1]))
) as unknown) as Expression; ) as unknown as Expression;
} else if (op === "NOT") { } else if (op === "NOT") {
return (new NotSynth(toBoolExprSynth(exp[1])) as unknown) as Expression; return new NotSynth(toBoolExprSynth(exp[1])) as unknown as Expression;
} else if (op === "OR") { } else if (op === "OR") {
return (new OrSynth( return new OrSynth(
...exp.slice(1).map((a) => toBoolExprSynth(a)) ...exp.slice(1).map((a) => toBoolExprSynth(a))
) as unknown) as Expression; ) as unknown as Expression;
} else if (op === "AND") { } else if (op === "AND") {
return (new AndSynth( return new AndSynth(
...exp.slice(1).map((a) => toBoolExprSynth(a)) ...exp.slice(1).map((a) => toBoolExprSynth(a))
) as unknown) as Expression; ) as unknown as Expression;
} }
for (let i = 1; i < exp.length; ++i) { for (let i = 1; i < exp.length; ++i) {
if (exp[i] instanceof BoolExprSynth) { if (exp[i] instanceof BoolExprSynth) {
@@ -1074,19 +1103,20 @@ function mapCallback(exp: Expression | BoolExprSynth): Expression {
} }
function getCanRaiseCallback( function getCanRaiseCallback(
isNull: Map<number, number> context: SynthContext
): (idx: number, set: Set<number>) => boolean { ): (idx: number, set: Set<number>) => boolean {
return (idx: number, set: Set<number>): boolean => { return (idx: number, set: Set<number>): boolean => {
const i = idx >> 2; const i = idx >> 2;
const n = isNull.get(i); const vars = context.getIsNullRelations(i);
if (n != null) { if (!context.isIsNull(i)) {
if (!(idx & 1)) return true; if (!(idx & 1)) return true;
if (set.has((n << 2) ^ 2)) return true; for (const n of vars) if (set.has((n << 2) ^ 3)) return true;
if (set.has((n << 2) ^ 3)) return true; if (set.has(idx ^ 3)) return false;
return !set.has(idx ^ 3); for (const n of vars) if (!set.has((n << 2) ^ 2)) return false;
return true;
} }
for (const [k, v] of isNull) {
if (v !== i) continue; for (const k of vars) {
if (set.has((k << 2) ^ 1)) continue; if (set.has((k << 2) ^ 1)) continue;
if (set.has((k << 2) ^ 3)) continue; if (set.has((k << 2) ^ 3)) continue;
if (set.has((k << 2) ^ 0)) return false; if (set.has((k << 2) ^ 0)) return false;
@@ -1101,33 +1131,31 @@ export function minimize(expr: Expression, boolean = false): Expression {
expr = map(expr, mapCallback); expr = map(expr, mapCallback);
if (Array.isArray(expr) && expr[0] === "CASE") { if (Array.isArray(expr) && expr[0] === "CASE") {
if (!boolean) { if (!boolean) {
const context: Map<string, number> = new Map(); const context = new SynthContext();
const whens = expr.filter((e, i) => i % 2).map((e) => e.true(context)); const whens = expr.filter((e, i) => i % 2).map((e) => e.true(context));
const variables = new Map( const caseDcSet: Minterm[] = [];
Array.from(context).map(([k, v]) => [v, JSON.parse(k) as Expression]) const canRaise = getCanRaiseCallback(context);
);
const { dcSet, isNull } = generateDcSetAndIsNull(variables);
const canRaise = getCanRaiseCallback(isNull);
const res: Expression = ["CASE"]; const res: Expression = ["CASE"];
for (let i = 1; i < expr.length; i += 2) { for (let i = 1; i < expr.length; i += 2) {
let minterms = sanitizeMinterms(whens[(i - 1) / 2], isNull); let minterms = sanitizeMinterms(whens[(i - 1) / 2], context);
const dcSet = context.getDcSet(minterms.concat(caseDcSet));
minterms = espresso(minterms, dcSet, { canRaise }); minterms = espresso(minterms, dcSet, { canRaise });
if (!minterms.length) continue; if (!minterms.length) continue;
const w = sopToExpression(minterms, variables); const w = sopToExpression(minterms, context);
let t = expr[i + 1]; let t = expr[i + 1];
if (t instanceof BoolExprSynth) t = boolExprSynthToExpression(t); if (t instanceof BoolExprSynth) t = boolExprSynthToExpression(t);
res.push(w, t); res.push(w, t);
if (w === true) break; if (w === true) break;
dcSet.push(...minterms); caseDcSet.push(...minterms);
} }
while (res[res.length - 1] == null) res.splice(-2); while (res[res.length - 1] == null) res.splice(-2);
if (res.length < 3) return null; if (res.length < 3) return null;
return res; return res;
} }
expr = (toBoolExprSynth(expr) as unknown) as Expression; expr = toBoolExprSynth(expr) as unknown as Expression;
} }
if (boolean) expr = (toBoolExprSynth(expr) as unknown) as Expression; if (boolean) expr = toBoolExprSynth(expr) as unknown as Expression;
if (expr instanceof BoolExprSynth) expr = boolExprSynthToExpression(expr); if (expr instanceof BoolExprSynth) expr = boolExprSynthToExpression(expr);
return expr; return expr;
@@ -1145,7 +1173,7 @@ export function unionDiff(
if (!expr1) { if (!expr1) {
if (Array.isArray(expr2) && expr2[0] === "CASE") if (Array.isArray(expr2) && expr2[0] === "CASE")
expr2 = (toBoolExprSynth(expr2) as unknown) as Expression; expr2 = toBoolExprSynth(expr2) as unknown as Expression;
if (expr2 instanceof BoolExprSynth) if (expr2 instanceof BoolExprSynth)
expr2 = boolExprSynthToExpression(expr2); expr2 = boolExprSynthToExpression(expr2);
return [expr2, expr2]; return [expr2, expr2];
@@ -1156,35 +1184,37 @@ export function unionDiff(
expr1 = map(expr1, mapCallback); expr1 = map(expr1, mapCallback);
const b1 = toBoolExprSynth(expr1); const b1 = toBoolExprSynth(expr1);
const context: Map<string, number> = new Map(); const context = new SynthContext();
const expr2Minterms = b2.true(context); const expr2Minterms = b2.true(context);
const expr1Minterms = b1.true(context); const expr1Minterms = b1.true(context);
const expr1NullMinterms = b1.null(context); const expr1NullMinterms = b1.null(context);
const expr1FalseMinterms = b1.false(context); const expr1FalseMinterms = b1.false(context);
const variables = new Map(
Array.from(context).map(([k, v]) => [v, JSON.parse(k) as Expression])
);
const { dcSet, isNull } = generateDcSetAndIsNull(variables);
const unionMinterms = sanitizeMinterms( const unionMinterms = sanitizeMinterms(
[...expr1Minterms, ...expr2Minterms], [...expr1Minterms, ...expr2Minterms],
isNull context
); );
const diffMinterms = sanitizeMinterms( const diffMinterms = sanitizeMinterms(
complement([ complement([
...complement([...expr1NullMinterms, ...expr1FalseMinterms]), ...complement([...expr1NullMinterms, ...expr1FalseMinterms]),
...complement(expr2Minterms), ...complement(expr2Minterms),
]), ]),
isNull context
); );
const canRaise = getCanRaiseCallback(isNull); const canRaise = getCanRaiseCallback(context);
const union = espresso(unionMinterms, dcSet, { canRaise }); const union = espresso(unionMinterms, context.getDcSet(unionMinterms), {
const diff = espresso(diffMinterms, dcSet, { canRaise }); canRaise,
});
return [sopToExpression(union, variables), sopToExpression(diff, variables)]; const diff = espresso(diffMinterms, context.getDcSet(diffMinterms), {
canRaise,
});
return [sopToExpression(union, context), sopToExpression(diff, context)];
} }
export function covers(expr1: Expression, expr2: Expression): boolean { export function covers(expr1: Expression, expr2: Expression): boolean {
@@ -1198,13 +1228,13 @@ export function covers(expr1: Expression, expr2: Expression): boolean {
expr2 = map(expr2, mapCallback); expr2 = map(expr2, mapCallback);
const b2 = toBoolExprSynth(expr2); const b2 = toBoolExprSynth(expr2);
const context: Map<string, number> = new Map(); const context = new SynthContext();
const expr1Minterms = b1.true(context); const expr1Minterms = b1.true(context);
const expr2Minterms = b2.true(context); const expr2Minterms = b2.true(context);
const variables = new Map(
Array.from(context).map(([k, v]) => [v, JSON.parse(k) as Expression])
);
const { dcSet } = generateDcSetAndIsNull(variables);
return tautology([...complement(expr2Minterms), ...dcSet, ...expr1Minterms]); return tautology([
...complement(expr2Minterms),
...context.getDcSet(),
...expr1Minterms,
]);
} }
+7 -4
View File
@@ -29,7 +29,7 @@ function interpretEscapes(str): string {
r: "\r", r: "\r",
t: "\t", t: "\t",
}; };
return str.replace(/\\(u[0-9a-fA-F]{4}|[^u])/, (_, escape) => { return str.replace(/\\(u[0-9a-fA-F]{4}|[^u])/g, (_, escape) => {
const type = escape.charAt(0); const type = escape.charAt(0);
const hex = escape.slice(1); const hex = escape.slice(1);
if (type === "u") return String.fromCharCode(parseInt(hex, 16)); if (type === "u") return String.fromCharCode(parseInt(hex, 16));
@@ -87,8 +87,6 @@ function binaryLeft(
rest.reduce((acc, ch) => { rest.reduce((acc, ch) => {
const [op, another] = ch; const [op, another] = ch;
if (Array.isArray(acc) && op === acc[0]) return acc.concat([another]); if (Array.isArray(acc) && op === acc[0]) return acc.concat([another]);
if (Array.isArray(another) && op === another[0])
return [op, acc].concat(another.slice(1));
return [op, acc, another]; return [op, acc, another];
}, first) }, first)
); );
@@ -287,7 +285,11 @@ const lang = parsimmon.createLanguage({
.skip(parsimmon.optWhitespace), .skip(parsimmon.optWhitespace),
binaryLeft( binaryLeft(
parsimmon parsimmon
.alt(parsimmon.string("*"), parsimmon.string("/")) .alt(
parsimmon.string("*"),
parsimmon.string("/"),
parsimmon.string("%")
)
.skip(parsimmon.optWhitespace), .skip(parsimmon.optWhitespace),
parsimmon.alt( parsimmon.alt(
r.Value, r.Value,
@@ -386,6 +388,7 @@ export function stringify(exp: Expression, level = 0): string {
"-": 31, "-": 31,
"*": 32, "*": 32,
"/": 32, "/": 32,
"%": 32,
}; };
const op = exp[0].toUpperCase(); const op = exp[0].toUpperCase();
+18 -14
View File
@@ -178,11 +178,11 @@ export function evaluateCallback(exp: Expression): Expression {
} else if (exp[0] === "IS NULL") { } else if (exp[0] === "IS NULL") {
if (isArray(exp[1])) return exp; if (isArray(exp[1])) return exp;
else if (exp[1] == null) return true; else if (exp[1] == null) return true;
else return null; else return false;
} else if (exp[0] === "IS NOT NULL") { } else if (exp[0] === "IS NOT NULL") {
if (isArray(exp[1])) return exp; if (isArray(exp[1])) return exp;
else if (exp[1] != null) return true; else if (exp[1] != null) return true;
else return null; else return false;
} else if (exp[0] === "LIKE") { } else if (exp[0] === "LIKE") {
if (isArray(exp[1]) || isArray(exp[2]) || isArray(exp[3])) return exp; if (isArray(exp[1]) || isArray(exp[2]) || isArray(exp[3])) return exp;
else if ( else if (
@@ -244,9 +244,12 @@ export function evaluateCallback(exp: Expression): Expression {
} else if (exp[0] === "/") { } else if (exp[0] === "/") {
return reduce(exp, (a, b, i) => { return reduce(exp, (a, b, i) => {
if (a == null || b == null) return null; if (a == null || b == null) return null;
if (!isArray(a) && !isArray(b)) if (isArray(a) || isArray(b)) return REDUCE_SKIP;
return i === 0 ? toNumber(a) / toNumber(b) : toNumber(a) * toNumber(b); const n1 = toNumber(a);
return REDUCE_SKIP; const n2 = toNumber(b);
if (i !== 0) return n1 * n2;
if (n2 === 0) return null;
return n1 / n2;
}); });
} else if (exp[0] === "+") { } else if (exp[0] === "+") {
return reduce(exp, (a, b) => { return reduce(exp, (a, b) => {
@@ -261,6 +264,15 @@ export function evaluateCallback(exp: Expression): Expression {
return i === 0 ? toNumber(a) - toNumber(b) : toNumber(a) + toNumber(b); return i === 0 ? toNumber(a) - toNumber(b) : toNumber(a) + toNumber(b);
return REDUCE_SKIP; return REDUCE_SKIP;
}); });
} else if (exp[0] === "%") {
return reduce(exp, (a, b, i) => {
if (a == null || b == null) return null;
if (isArray(a) || isArray(b) || i !== 0) return REDUCE_SKIP;
const n1 = toNumber(a);
const n2 = Math.trunc(toNumber(b));
if (n2 === 0) return null;
return n1 % n2;
});
} else if (exp[0] === "||") { } else if (exp[0] === "||") {
return reduce(exp, (a, b) => { return reduce(exp, (a, b) => {
if (a == null || b == null) return null; if (a == null || b == null) return null;
@@ -334,15 +346,7 @@ export async function evaluateAsync(
if (!isArray(e)) return e; if (!isArray(e)) return e;
if (e[0] === "FUNC") { if (e[0] === "FUNC") {
if (e[1] === "NOW") { if (e[1] === "NOW") if (now) return now;
if (now) return now;
} else if (e[1] === "UPPER") {
if (e[2] == null) return null;
if (!isArray(e[2])) return `${e[2]}`.toUpperCase();
} else if (e[1] === "LOWER") {
if (e[2] == null) return null;
if (!isArray(e[2])) return `${e[2]}`.toLowerCase();
}
} else if (e[0] === "PARAM") { } else if (e[0] === "PARAM") {
if (e[1] == null) return null; if (e[1] == null) return null;
if (obj && !isArray(e[1])) { if (obj && !isArray(e[1])) {
+3 -3
View File
@@ -20,9 +20,9 @@
import Path from "./path"; import Path from "./path";
export default class PathSet { export default class PathSet {
private lengthIndex: Set<Path>[]; private declare lengthIndex: Set<Path>[];
private fragmentIndex: Map<string, Set<Path>>[]; private declare fragmentIndex: Map<string, Set<Path>>[];
private stringIndex: Map<string, Path>; private declare stringIndex: Map<string, Path>;
public constructor() { public constructor() {
this.lengthIndex = []; this.lengthIndex = [];
+109 -54
View File
@@ -23,44 +23,102 @@ type Alias = [Path, string][];
let cache1 = new Map<string, Path>(); let cache1 = new Map<string, Path>();
let cache2 = new Map<string, Path>(); let cache2 = new Map<string, Path>();
const CHAR_SPACE = 32;
const CHAR_BACKSLASH = 92;
const CHAR_COLON = 58;
const CHAR_QUOTE = 34;
const CHAR_OPEN_BRACKET = 91;
const CHAR_CLOSE_BRACKET = 93;
const CHAR_ASTERISK = 42;
const CHAR_COMMA = 44;
const CHAR_DOT = 46;
function charCodeAt(str: string, index: number): number {
if (index >= str.length) return 0;
return str.charCodeAt(index);
}
function legalChar(c: number): boolean {
return (
(c >= 97 && c <= 122) ||
(c >= 65 && c <= 90) ||
(c >= 48 && c <= 57) ||
c === 95 ||
c === 45
);
}
export default class Path { export default class Path {
public readonly segments: Segments; public declare readonly segments: Segments;
public readonly wildcard: number; public declare readonly wildcard: number;
public readonly alias: number; public declare readonly alias: number;
protected _string: string; protected declare _string: string;
protected _stringIndex: number[]; protected declare _stringIndex: number[];
protected static parseAliasValue(
pattern: string,
index: number
): { index: number; value: string } {
let i = index;
while (charCodeAt(pattern, i) === CHAR_SPACE) ++i;
if (charCodeAt(pattern, i) === CHAR_QUOTE) {
for (let j = i + 1; j < pattern.length; ++j) {
if (
pattern.charCodeAt(j) === CHAR_QUOTE &&
pattern.charCodeAt(j - 1) !== CHAR_BACKSLASH
) {
try {
++j;
const v = JSON.parse(pattern.slice(i, j));
return { index: j, value: v };
} catch {
return { index, value: null };
}
}
}
return { index, value: null };
}
for (; i < pattern.length; ++i) {
const c = pattern.charCodeAt(i);
if (c === CHAR_CLOSE_BRACKET || c === CHAR_COMMA) break;
}
return { index: i, value: pattern.slice(index, i).trim() };
}
protected static parseAliasPath(
pattern: string,
index: number
): { index: number; path: Path } {
let i = index;
while (charCodeAt(pattern, i) === CHAR_SPACE) ++i;
const { index: idx, segments } = Path.parsePath(pattern, i);
if (idx === i) return { index, path: null };
i = idx;
while (charCodeAt(pattern, i) === CHAR_SPACE) ++i;
if (charCodeAt(pattern, i) !== CHAR_COLON) return { index, path: null };
return { index: i + 1, path: new Path(segments) };
}
protected static parseAlias( protected static parseAlias(
pattern: string, pattern: string,
index: number index: number
): { index: number; alias: Alias } { ): { index: number; alias: Alias } {
const alias: Alias = []; const alias: Alias = [];
while (index < pattern.length && pattern[index] !== "]") { let i = index;
const { index: idx, segments } = Path.parsePath(pattern, index); for (;;) {
let j = (index = idx + 1); let path: Path;
while (pattern[j] !== "]" && pattern[j] !== ",") { let value: string;
if (pattern[j] === '"' && index === j) { ({ index: i, path } = Path.parseAliasPath(pattern, i));
++j; if (!path) break;
while (pattern[j] !== '"' || pattern[j - 1] === "\\") { ({ index: i, value } = Path.parseAliasValue(pattern, i));
if (++j >= pattern.length) if (value == null) break;
throw new Error("Invalid alias expression"); alias.push([path, value]);
} index = i;
}
if (++j >= pattern.length) throw new Error("Invalid alias expression");
}
let value = pattern.slice(index, j).trim(); if (charCodeAt(pattern, index) !== CHAR_COMMA) break;
index = j; ++i;
if (value[0] === '"') {
try {
value = JSON.parse(value);
} catch (error) {
throw new Error("Invalid alias expression");
}
}
alias.push([new Path(segments), value]);
if (pattern[index] === ",") ++index;
} }
// Ensure identical expressions have idential string representation // Ensure identical expressions have idential string representation
@@ -81,30 +139,26 @@ export default class Path {
index: number index: number
): { index: number; segments: Segments } { ): { index: number; segments: Segments } {
const segments = []; const segments = [];
// Colon separator is needed for parseAlias
if (index < pattern.length && pattern[index] !== ":") {
for (;;) {
if (pattern[index] === "[") {
const { index: idx, alias } = Path.parseAlias(pattern, index + 1);
index = idx + 1;
segments.push(alias);
} else {
const j = index;
while (
index < pattern.length &&
pattern[index] !== ":" &&
pattern[index] !== "."
)
++index;
const s = pattern.slice(j, index).trim();
if (!s) throw new Error("Invalid parameter path");
segments.push(s);
}
if (index >= pattern.length || pattern[index] === ":") break; let segStart = index;
else if (pattern[index] !== ".") let wildcard = -1;
throw new Error("Invalid alias expression"); for (let i = index; i <= pattern.length; ++i) {
++index; const c = charCodeAt(pattern, i);
if (c === CHAR_OPEN_BRACKET && i === segStart) {
const { index: idx, alias } = Path.parseAlias(pattern, i + 1);
if (charCodeAt(pattern, idx) !== CHAR_CLOSE_BRACKET) break;
segments.push(alias);
index = i = idx + 1;
segStart = i + 1;
} else if (c === CHAR_ASTERISK && i === segStart) {
wildcard = i + 1;
} else if (!legalChar(c) || i === wildcard) {
if (i === segStart) break;
const s = pattern.slice(segStart, i);
segments.push(s);
index = i;
if (c !== CHAR_DOT) break;
segStart = i + 1;
} }
} }
@@ -143,7 +197,8 @@ export default class Path {
if (!path) { if (!path) {
path = cache2.get(str); path = cache2.get(str);
if (!path) { if (!path) {
const { segments } = Path.parsePath(str, 0); const { index, segments } = Path.parsePath(str, 0);
if (index < str.length) throw new Error("Invalid parameter path");
path = new Path(segments); path = new Path(segments);
if (path.toString() !== str) cache1.set(path.toString(), path); if (path.toString() !== str) cache1.set(path.toString(), path);
} }
+25 -10
View File
@@ -29,21 +29,36 @@ function stringifyKey(str: string): string {
function foldString(str: string): string[] { function foldString(str: string): string[] {
if (str.length <= LINE_WIDTH) return [str]; if (str.length <= LINE_WIDTH) return [str];
if (str.startsWith(" ")) return [str]; if (str.startsWith(" ")) return [str];
const words = str.split(/(?<=[^ ]) (?=[^ ])/);
const lines: string[] = []; const lines: string[] = [];
let idx = 0; let idx = 0;
let len = 0; let cand = 0;
for (const [i, word] of words.entries()) { for (let i = 1; i < str.length - 1; ++i) {
len += word.length + 1; if (str[i] !== " ") continue;
if (len >= LINE_WIDTH && len !== word.length) {
lines.push(words.slice(idx, i).join(" ")); if (str[i + 1] === " ") {
idx = i; i += 2;
len = 0; while (str[i] === " ") ++i;
} continue;
} }
if (idx < words.length) lines.push(words.slice(idx).join(" ")); if (i <= idx + LINE_WIDTH) {
cand = i;
continue;
}
const c = cand > idx ? cand : i;
lines.push(str.slice(idx, c));
idx = c + 1;
cand = i;
}
if (cand > idx && str.length > idx + LINE_WIDTH) {
lines.push(str.slice(idx, cand));
idx = cand + 1;
}
lines.push(str.slice(idx));
return lines; return lines;
} }
+4 -4
View File
@@ -43,7 +43,7 @@ const options = {
CWMP_WORKER_PROCESSES: { type: "int", default: 0 }, CWMP_WORKER_PROCESSES: { type: "int", default: 0 },
CWMP_PORT: { type: "int", default: 7547 }, CWMP_PORT: { type: "int", default: 7547 },
CWMP_INTERFACE: { type: "string", default: "0.0.0.0" }, CWMP_INTERFACE: { type: "string", default: "::" },
CWMP_SSL_CERT: { type: "string", default: "" }, CWMP_SSL_CERT: { type: "string", default: "" },
CWMP_SSL_KEY: { type: "string", default: "" }, CWMP_SSL_KEY: { type: "string", default: "" },
CWMP_LOG_FILE: { type: "path", default: "" }, CWMP_LOG_FILE: { type: "path", default: "" },
@@ -51,7 +51,7 @@ const options = {
NBI_WORKER_PROCESSES: { type: "int", default: 0 }, NBI_WORKER_PROCESSES: { type: "int", default: 0 },
NBI_PORT: { type: "int", default: 7557 }, NBI_PORT: { type: "int", default: 7557 },
NBI_INTERFACE: { type: "string", default: "0.0.0.0" }, NBI_INTERFACE: { type: "string", default: "::" },
NBI_SSL_CERT: { type: "string", default: "" }, NBI_SSL_CERT: { type: "string", default: "" },
NBI_SSL_KEY: { type: "string", default: "" }, NBI_SSL_KEY: { type: "string", default: "" },
NBI_LOG_FILE: { type: "path", default: "" }, NBI_LOG_FILE: { type: "path", default: "" },
@@ -59,7 +59,7 @@ const options = {
FS_WORKER_PROCESSES: { type: "int", default: 0 }, FS_WORKER_PROCESSES: { type: "int", default: 0 },
FS_PORT: { type: "int", default: 7567 }, FS_PORT: { type: "int", default: 7567 },
FS_INTERFACE: { type: "string", default: "0.0.0.0" }, FS_INTERFACE: { type: "string", default: "::" },
FS_SSL_CERT: { type: "string", default: "" }, FS_SSL_CERT: { type: "string", default: "" },
FS_SSL_KEY: { type: "string", default: "" }, FS_SSL_KEY: { type: "string", default: "" },
FS_URL_PREFIX: { type: "string", default: "" }, FS_URL_PREFIX: { type: "string", default: "" },
@@ -68,7 +68,7 @@ const options = {
UI_WORKER_PROCESSES: { type: "int", default: 0 }, UI_WORKER_PROCESSES: { type: "int", default: 0 },
UI_PORT: { type: "int", default: 3000 }, UI_PORT: { type: "int", default: 3000 },
UI_INTERFACE: { type: "string", default: "0.0.0.0" }, UI_INTERFACE: { type: "string", default: "::" },
UI_SSL_CERT: { type: "string", default: "" }, UI_SSL_CERT: { type: "string", default: "" },
UI_SSL_KEY: { type: "string", default: "" }, UI_SSL_KEY: { type: "string", default: "" },
UI_LOG_FILE: { type: "path", default: "" }, UI_LOG_FILE: { type: "path", default: "" },
+22 -19
View File
@@ -19,7 +19,7 @@
import * as crypto from "crypto"; import * as crypto from "crypto";
import * as dgram from "dgram"; import * as dgram from "dgram";
import { parse } from "url"; import { URL } from "url";
import * as http from "http"; import * as http from "http";
import { evaluateAsync } from "./common/expression"; import { evaluateAsync } from "./common/expression";
import { Expression } from "./types"; import { Expression } from "./types";
@@ -61,24 +61,26 @@ async function extractAuth(
} }
function httpGet( function httpGet(
url: URL,
options: http.RequestOptions, options: http.RequestOptions,
_debug: boolean, _debug: boolean,
deviceId: string deviceId: string
): Promise<{ statusCode: number; headers: http.IncomingHttpHeaders }> { ): Promise<{ statusCode: number; headers: http.IncomingHttpHeaders }> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const req = http const req = http
.get(options, (res) => { .get(url, options, (res) => {
res.resume(); res.resume();
resolve({ statusCode: res.statusCode, headers: res.headers }); resolve({ statusCode: res.statusCode, headers: res.headers });
if (_debug) { if (_debug) {
debug.outgoingHttpRequest(req, deviceId, options, null); debug.outgoingHttpRequest(req, deviceId, "GET", url, null);
debug.incomingHttpResponse(res, deviceId, null); debug.incomingHttpResponse(res, deviceId, null);
} }
}) })
.on("error", (err) => { .on("error", (err) => {
req.destroy(); req.destroy();
reject(err); reject(err);
if (_debug) debug.outgoingHttpRequestError(req, deviceId, options, err); if (_debug)
debug.outgoingHttpRequestError(req, deviceId, "GET", url, err);
}) })
.on("timeout", () => { .on("timeout", () => {
req.destroy(); req.destroy();
@@ -94,15 +96,13 @@ export async function httpConnectionRequest(
_debug: boolean, _debug: boolean,
deviceId: string deviceId: string
): Promise<string> { ): Promise<string> {
const options: http.RequestOptions = parse(address); const url = new URL(address);
if (options.protocol !== "http:") if (url.protocol !== "http:")
return "Invalid connection request URL or protocol"; return "Invalid connection request URL or protocol";
options.agent = new http.Agent({ const options: http.RequestOptions = {
maxSockets: 1, agent: new http.Agent({ maxSockets: 1, keepAlive: true, timeout: timeout }),
keepAlive: true, };
timeout: timeout,
});
let authHeader: Record<string, string>; let authHeader: Record<string, string>;
let username: string; let username: string;
@@ -129,7 +129,7 @@ export async function httpConnectionRequest(
Authorization: auth.solveDigest( Authorization: auth.solveDigest(
username, username,
password, password,
options.path, url.pathname + url.search,
"GET", "GET",
null, null,
authHeader authHeader
@@ -145,12 +145,12 @@ export async function httpConnectionRequest(
let res: { statusCode: number; headers: http.IncomingHttpHeaders }; let res: { statusCode: number; headers: http.IncomingHttpHeaders };
try { try {
res = await httpGet(opts, _debug, deviceId); res = await httpGet(url, opts, _debug, deviceId);
} catch (err) { } catch (err) {
// Workaround for some devices unexpectedly closing the connection // Workaround for some devices unexpectedly closing the connection
if (authHeader) { if (authHeader) {
try { try {
res = await httpGet(opts, _debug, deviceId); res = await httpGet(url, opts, _debug, deviceId);
} catch (err) { } catch (err) {
return `Connection request error: ${err.message}`; return `Connection request error: ${err.message}`;
} }
@@ -169,9 +169,13 @@ export async function httpConnectionRequest(
if (res.statusCode === 503) return "Device is offline"; if (res.statusCode === 503) return "Device is offline";
if (res.statusCode === 401 && res.headers["www-authenticate"]) { if (res.statusCode === 401 && res.headers["www-authenticate"]) {
try {
authHeader = auth.parseWwwAuthenticateHeader( authHeader = auth.parseWwwAuthenticateHeader(
res.headers["www-authenticate"] res.headers["www-authenticate"]
); );
} catch (err) {
return "Connection request error: Error parsing www-authenticate header";
}
[username, password, authExp] = await extractAuth(authExp, false); [username, password, authExp] = await extractAuth(authExp, false);
} else { } else {
return `Connection request error: Unexpected status code ${res.statusCode}`; return `Connection request error: Unexpected status code ${res.statusCode}`;
@@ -182,14 +186,13 @@ export async function httpConnectionRequest(
} }
export async function udpConnectionRequest( export async function udpConnectionRequest(
address: string, host: string,
port: number,
authExp: Expression, authExp: Expression,
sourcePort = 0, sourcePort = 0,
_debug: boolean, _debug: boolean,
deviceId: string deviceId: string
): Promise<void> { ): Promise<void> {
const [host, portStr] = address.split(":", 2);
const port = portStr ? parseInt(portStr) : 80;
const now = Date.now(); const now = Date.now();
const client = dgram.createSocket({ type: "udp4", reuseAddr: true }); const client = dgram.createSocket({ type: "udp4", reuseAddr: true });
@@ -216,8 +219,8 @@ export async function udpConnectionRequest(
.createHmac("sha1", password) .createHmac("sha1", password)
.update(`${ts}${id}${username}${cn}`) .update(`${ts}${id}${username}${cn}`)
.digest("hex"); .digest("hex");
const uri = `http://${address}?ts=${ts}&id=${id}&un=${username}&cn=${cn}&sig=${sig}`; const uri = `http://${host}:${port}?ts=${ts}&id=${id}&un=${username}&cn=${cn}&sig=${sig}`;
const msg = `GET ${uri} HTTP/1.1\r\nHost: ${address}\r\n\r\n`; const msg = `GET ${uri} HTTP/1.1\r\nHost: ${host}:${port}\r\n\r\n`;
const message = Buffer.from(msg); const message = Buffer.from(msg);
for (let i = 0; i < 3; ++i) { for (let i = 0; i < 3; ++i) {
+178 -163
View File
@@ -45,6 +45,7 @@ import {
InformRequest, InformRequest,
Preset, Preset,
GetRPCMethodsResponse, GetRPCMethodsResponse,
CpeFault,
} from "./types"; } from "./types";
import { IncomingMessage, ServerResponse } from "http"; import { IncomingMessage, ServerResponse } from "http";
import { Readable } from "stream"; import { Readable } from "stream";
@@ -84,12 +85,16 @@ async function authenticate(
let authentication; let authentication;
if (sessionContext.httpRequest.headers["authorization"]) { if (sessionContext.httpRequest.headers["authorization"]) {
try {
authentication = auth.parseAuthorizationHeader( authentication = auth.parseAuthorizationHeader(
sessionContext.httpRequest.headers["authorization"] sessionContext.httpRequest.headers["authorization"]
); );
} catch (err) {
return false;
}
} }
if (authentication && authentication.method === "Digest") { if (authentication?.method === "Digest") {
const sessionNonce = sessionsNonces.get(sessionContext.httpRequest.socket); const sessionNonce = sessionsNonces.get(sessionContext.httpRequest.socket);
if ( if (
@@ -183,7 +188,8 @@ async function writeResponse(
} }
const httpResponse = sessionContext.httpResponse; const httpResponse = sessionContext.httpResponse;
const connection = httpResponse.socket; // Don't use httpResponse.socket as it may be null, even before end() is called
const connection = sessionContext.httpRequest.socket;
httpResponse.setHeader("Content-Length", Buffer.byteLength(data)); httpResponse.setHeader("Content-Length", Buffer.byteLength(data));
httpResponse.writeHead(res.code, res.headers); httpResponse.writeHead(res.code, res.headers);
@@ -208,7 +214,7 @@ async function writeResponse(
sessionContext.extendLock = now + 10000; sessionContext.extendLock = now + 10000;
await cache.acquireLock( await cache.acquireLock(
`cwmp_session_${sessionContext.deviceId}`, `cwmp_session_${sessionContext.deviceId}`,
sessionContext.timeout + 15000, sessionContext.timeout * 1000 + 15000,
0, 0,
sessionContext.sessionId sessionContext.sessionId
); );
@@ -430,7 +436,7 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
const deviceEvents = {}; const deviceEvents = {};
for (const p of deviceData.paths.find(Path.parse("Events.*"), false, true)) { for (const p of deviceData.paths.find(Path.parse("Events.*"), false, true)) {
const attrs = deviceData.attributes.get(p); const attrs = deviceData.attributes.get(p);
if (attrs && attrs.value && attrs.value[1][0] >= sessionContext.timestamp) if (attrs?.value && attrs.value[1][0] >= sessionContext.timestamp)
deviceEvents[p.segments[1] as string] = true; deviceEvents[p.segments[1] as string] = true;
} }
@@ -454,7 +460,7 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
if (!eventsMatch) continue; if (!eventsMatch) continue;
if (preset.schedule && preset.schedule.schedule) { if (preset.schedule?.schedule) {
const r = scheduling.cron( const r = scheduling.cron(
sessionContext.timestamp, sessionContext.timestamp,
preset.schedule.schedule preset.schedule.schedule
@@ -493,10 +499,11 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
defer: true, defer: true,
})); }));
const { fault: flt, rpcId: reqId, rpc: acsReq } = await session.rpcRequest( const {
sessionContext, fault: flt,
declarations rpcId: reqId,
); rpc: acsReq,
} = await session.rpcRequest(sessionContext, declarations);
if (flt) { if (flt) {
recordFault(sessionContext, flt); recordFault(sessionContext, flt);
@@ -568,10 +575,11 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
deviceData.timestamps.dirty = 0; deviceData.timestamps.dirty = 0;
deviceData.attributes.dirty = 0; deviceData.attributes.dirty = 0;
const { fault: fault, rpcId: id, rpc: acsRequest } = await session.rpcRequest( const {
sessionContext, fault: fault,
null rpcId: id,
); rpc: acsRequest,
} = await session.rpcRequest(sessionContext, null);
if (fault) { if (fault) {
recordFault(sessionContext, fault); recordFault(sessionContext, fault);
@@ -601,10 +609,11 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
} }
async function nextRpc(sessionContext: SessionContext): Promise<void> { async function nextRpc(sessionContext: SessionContext): Promise<void> {
const { fault: fault, rpcId: id, rpc: acsRequest } = await session.rpcRequest( const {
sessionContext, fault: fault,
null rpcId: id,
); rpc: acsRequest,
} = await session.rpcRequest(sessionContext, null);
if (fault) { if (fault) {
recordFault(sessionContext, fault); recordFault(sessionContext, fault);
@@ -782,7 +791,7 @@ async function endSession(sessionContext: SessionContext): Promise<void> {
} }
} }
if (sessionContext.doneTasks && sessionContext.doneTasks.length) { if (sessionContext.doneTasks?.length) {
saveCache = true; saveCache = true;
promises.push( promises.push(
db.clearTasks(sessionContext.deviceId, sessionContext.doneTasks) db.clearTasks(sessionContext.deviceId, sessionContext.doneTasks)
@@ -874,27 +883,6 @@ async function sendAcsRequest(
return writeResponse(sessionContext, res); return writeResponse(sessionContext, res);
} }
async function getSession(
connection: Socket,
sessionId: string
): Promise<SessionContext> {
let sessionContext = currentSessions.get(connection);
if (sessionContext) {
currentSessions.delete(connection);
return sessionContext;
}
if (!sessionId) return null;
await new Promise((resolve) => setTimeout(resolve, 100));
const sessionContextString = await cache.pop(`session_${sessionId}`);
if (!sessionContextString) return null;
sessionContext = await session.deserialize(sessionContextString);
connection.setTimeout(sessionContext.timeout);
return sessionContext;
}
// Only needed to prevent tree shaking from removing the remoteAddress // Only needed to prevent tree shaking from removing the remoteAddress
// workaround in onConnection function. // workaround in onConnection function.
const remoteAddressWorkaround = new WeakMap<Socket, string>(); const remoteAddressWorkaround = new WeakMap<Socket, string>();
@@ -1041,13 +1029,13 @@ async function reportBadState(sessionContext: SessionContext): Promise<void> {
sessionContext: sessionContext, sessionContext: sessionContext,
}); });
const httpResponse = sessionContext.httpResponse; const httpResponse = sessionContext.httpResponse;
currentSessions.delete(httpResponse.socket);
const body = "Bad session state"; const body = "Bad session state";
httpResponse.setHeader("Content-Length", Buffer.byteLength(body)); httpResponse.setHeader("Content-Length", Buffer.byteLength(body));
httpResponse.writeHead(400, { Connection: "close" }); httpResponse.writeHead(400, { Connection: "close" });
if (sessionContext.debug) if (sessionContext.debug)
debug.outgoingHttpResponse(httpResponse, sessionContext.deviceId, body); debug.outgoingHttpResponse(httpResponse, sessionContext.deviceId, body);
httpResponse.end(body); httpResponse.end(body);
if (sessionContext.state) return endSession(sessionContext);
} }
async function responseUnauthorized( async function responseUnauthorized(
@@ -1099,7 +1087,7 @@ async function processRequest(
} }
if (sessionContext.state === 0) { if (sessionContext.state === 0) {
if (!rpc.cpeRequest || rpc.cpeRequest.name !== "Inform") if (rpc.cpeRequest?.name !== "Inform")
return reportBadState(sessionContext); return reportBadState(sessionContext);
const res = await inform(sessionContext, rpc); const res = await inform(sessionContext, rpc);
@@ -1146,7 +1134,7 @@ async function processRequest(
sessionContext.extendLock = sessionContext.timestamp + 10000; sessionContext.extendLock = sessionContext.timestamp + 10000;
await cache.acquireLock( await cache.acquireLock(
`cwmp_session_${sessionContext.deviceId}`, `cwmp_session_${sessionContext.deviceId}`,
sessionContext.timeout + 15000, sessionContext.timeout * 1000 + 15000,
0, 0,
sessionContext.sessionId sessionContext.sessionId
); );
@@ -1269,6 +1257,41 @@ async function processRequest(
session.clearProvisions(sessionContext); session.clearProvisions(sessionContext);
} }
return nextRpc(sessionContext); return nextRpc(sessionContext);
} else if (rpc.unknownMethod) {
if (sessionContext.state === 1) {
logger.accessWarn({
sessionContext: sessionContext,
message: "Method not supported",
method: rpc.unknownMethod,
});
const f: CpeFault = {
faultCode: "Server",
faultString: "CWMP fault",
detail: {
faultCode: "8000",
faultString: "Method not supported",
},
};
const res = soap.response({
id: rpc.id,
acsFault: f,
cwmpVersion: sessionContext.cwmpVersion,
});
return writeResponse(sessionContext, res);
} else if (sessionContext.state === 2) {
const fault = {
code: "invalid_response",
message: "Response name does not match request name",
};
recordFault(sessionContext, fault);
session.clearProvisions(sessionContext);
return nextRpc(sessionContext);
} else {
return reportBadState(sessionContext);
}
} else { } else {
// CPE sent empty response // CPE sent empty response
if (sessionContext.state !== 1) return reportBadState(sessionContext); if (sessionContext.state !== 1) return reportBadState(sessionContext);
@@ -1304,13 +1327,13 @@ export function listener(
stats.concurrentRequests -= 1; stats.concurrentRequests -= 1;
}) })
.catch((err) => { .catch((err) => {
currentSessions.delete(httpResponse.socket); currentSessions.delete(httpRequest.socket);
stats.concurrentRequests -= 1; stats.concurrentRequests -= 1;
setTimeout(() => { setTimeout(() => {
throw err; throw err;
}); });
try { try {
httpResponse.socket.unref(); httpRequest.socket.unref();
httpResponse.writeHead(500, { Connection: "close" }); httpResponse.writeHead(500, { Connection: "close" });
httpResponse.end(`${err.name}: ${err.message}`); httpResponse.end(`${err.name}: ${err.message}`);
} catch (err) { } catch (err) {
@@ -1319,6 +1342,48 @@ export function listener(
}); });
} }
async function clientError(
httpRequest: IncomingMessage,
httpResponse: ServerResponse,
sessionContext: SessionContext,
body: string,
msg: string
): Promise<void> {
let debugEnabled = false;
let deviceId: string = null;
if (sessionContext) {
debugEnabled = sessionContext.debug;
deviceId = sessionContext.deviceId;
} else {
const cacheSnapshot = await localCache.getCurrentSnapshot();
debugEnabled = !!localCache.getConfig(
cacheSnapshot,
"cwmp.debug",
{
remoteAddress: getRequestOrigin(httpRequest).remoteAddress,
},
Date.now(),
(e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "REMOTE_ADDRESS")
return getRequestOrigin(httpRequest).remoteAddress;
return e;
}
);
}
httpResponse.setHeader("Content-Length", Buffer.byteLength(msg));
httpResponse.writeHead(400, { Connection: "close" });
if (debugEnabled) {
debug.incomingHttpRequest(httpRequest, deviceId, body);
debug.outgoingHttpResponse(httpResponse, deviceId, msg);
}
httpResponse.end(msg);
if (sessionContext?.state) await endSession(sessionContext);
}
function decodeString(buffer: Buffer, charset: string): string { function decodeString(buffer: Buffer, charset: string): string {
try { try {
return buffer.toString(charset as BufferEncoding); return buffer.toString(charset as BufferEncoding);
@@ -1345,7 +1410,8 @@ async function listenerAsync(
let sessionId; let sessionId;
// Separation by comma is important as some devices don't comform to standard // Separation by comma is important as some devices don't comform to standard
const COOKIE_REGEX = /\s*([a-zA-Z0-9\-_]+?)\s*=\s*"?([a-zA-Z0-9\-_]*?)"?\s*(,|;|$)/g; const COOKIE_REGEX =
/\s*([a-zA-Z0-9\-_]+?)\s*=\s*"?([a-zA-Z0-9\-_]*?)"?\s*(,|;|$)/g;
let match; let match;
while ((match = COOKIE_REGEX.exec(httpRequest.headers.cookie))) while ((match = COOKIE_REGEX.exec(httpRequest.headers.cookie)))
if (match[1] === "session") sessionId = match[2]; if (match[1] === "session") sessionId = match[2];
@@ -1406,15 +1472,14 @@ async function listenerAsync(
// Request aborted // Request aborted
if (!body) return; if (!body) return;
const newConnection = !currentSessions.has(httpRequest.socket); let sessionContext = currentSessions.get(httpRequest.socket);
const sessionContext = await getSession(httpRequest.socket, sessionId);
if (sessionContext) { if (sessionContext) {
currentSessions.delete(httpRequest.socket);
sessionContext.httpRequest = httpRequest; sessionContext.httpRequest = httpRequest;
sessionContext.httpResponse = httpResponse; sessionContext.httpResponse = httpResponse;
if ( if (
(newConnection && sessionContext.sessionId !== sessionId) || (sessionContext.sessionId !== sessionId && sessionContext.state) ||
sessionContext.lastActivity + sessionContext.timeout * 1000 < Date.now() sessionContext.lastActivity + sessionContext.timeout * 1000 < Date.now()
) { ) {
logger.accessError({ logger.accessError({
@@ -1422,29 +1487,14 @@ async function listenerAsync(
sessionContext: sessionContext, sessionContext: sessionContext,
}); });
const _body = "Invalid session"; return clientError(
httpResponse.setHeader("Content-Length", Buffer.byteLength(_body)); httpRequest,
httpResponse.writeHead(400, { Connection: "close" });
if (sessionContext.debug) {
debug.outgoingHttpResponse(
httpResponse, httpResponse,
sessionContext.deviceId, sessionContext,
_body body.toString(),
"Invalid session"
); );
} }
httpResponse.end(_body);
await endSession(sessionContext);
return;
}
if (newConnection && sessionContext.authState !== 1)
sessionContext.authState = 0;
} else if (stats.concurrentRequests > MAX_CONCURRENT_REQUESTS) {
// Check again just in case device included old session ID
// from the previous session
httpResponse.writeHead(503, { "Retry-after": 60, Connection: "close" });
httpResponse.end("503 Service Unavailable");
stats.droppedRequests += 1;
return;
} }
let charset: string; let charset: string;
@@ -1464,6 +1514,16 @@ async function listenerAsync(
const bodyStr = decodeString(body, charset); const bodyStr = decodeString(body, charset);
if (bodyStr == null) { if (bodyStr == null) {
if (!sessionContext && sessionId) {
await new Promise((resolve) => setTimeout(resolve, 100));
const sessionContextString = await cache.pop(`session_${sessionId}`);
if (sessionContextString) {
sessionContext = await session.deserialize(sessionContextString);
sessionContext.httpRequest = httpRequest;
sessionContext.httpResponse = httpResponse;
}
}
const msg = `Unknown encoding '${charset}'`; const msg = `Unknown encoding '${charset}'`;
logger.accessError({ logger.accessError({
message: "XML parse error", message: "XML parse error",
@@ -1473,132 +1533,87 @@ async function listenerAsync(
httpResponse: httpResponse, httpResponse: httpResponse,
}, },
}); });
httpResponse.setHeader("Content-Length", Buffer.byteLength(msg)); return clientError(
httpResponse.writeHead(400, { Connection: "close" });
if (sessionContext) {
if (sessionContext.debug) {
debug.incomingHttpRequest(
httpRequest, httpRequest,
sessionContext.deviceId, httpResponse,
body.toString() sessionContext,
body.toString(),
msg
); );
debug.outgoingHttpResponse(httpResponse, sessionContext.deviceId, msg);
}
await endSession(sessionContext);
} else {
const cacheSnapshot = await localCache.getCurrentSnapshot();
const d = !!localCache.getConfig(
cacheSnapshot,
"cwmp.debug",
{
remoteAddress: getRequestOrigin(httpRequest).remoteAddress,
},
Date.now(),
(e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "REMOTE_ADDRESS")
return getRequestOrigin(httpRequest).remoteAddress;
return e;
}
);
if (d) {
debug.incomingHttpRequest(httpRequest, null, body.toString());
debug.outgoingHttpResponse(httpResponse, null, msg);
}
}
httpResponse.end(msg);
return;
} }
const parseWarnings = []; const parseWarnings = [];
let rpc: SoapMessage; let rpc: SoapMessage;
try { try {
rpc = soap.request( rpc = soap.request(bodyStr, parseWarnings);
bodyStr, } catch (err) {
sessionContext ? sessionContext.cwmpVersion : null, if (!sessionContext && sessionId) {
parseWarnings await new Promise((resolve) => setTimeout(resolve, 100));
); const sessionContextString = await cache.pop(`session_${sessionId}`);
} catch (error) { if (sessionContextString) {
sessionContext = await session.deserialize(sessionContextString);
sessionContext.httpRequest = httpRequest;
sessionContext.httpResponse = httpResponse;
}
}
logger.accessError({ logger.accessError({
message: "XML parse error", message: "XML parse error",
parseError: error.message.trim(), parseError: err.message,
sessionContext: sessionContext || { sessionContext: sessionContext || {
httpRequest: httpRequest, httpRequest: httpRequest,
httpResponse: httpResponse, httpResponse: httpResponse,
}, },
}); });
httpResponse.setHeader("Content-Length", Buffer.byteLength(error.message));
httpResponse.writeHead(400, { Connection: "close" }); return clientError(
if (sessionContext) {
if (sessionContext.debug) {
debug.incomingHttpRequest(
httpRequest, httpRequest,
sessionContext.deviceId,
bodyStr
);
debug.outgoingHttpResponse(
httpResponse, httpResponse,
sessionContext.deviceId, sessionContext,
error.message bodyStr,
err.message
); );
} }
await endSession(sessionContext);
} else { if (!sessionContext && sessionId && rpc.cpeRequest?.name !== "Inform") {
const cacheSnapshot = await localCache.getCurrentSnapshot(); await new Promise((resolve) => setTimeout(resolve, 100));
const d = !!localCache.getConfig( const sessionContextString = await cache.pop(`session_${sessionId}`);
cacheSnapshot, if (sessionContextString) {
"cwmp.debug", sessionContext = await session.deserialize(sessionContextString);
{ sessionContext.httpRequest = httpRequest;
remoteAddress: getRequestOrigin(httpRequest).remoteAddress, sessionContext.httpResponse = httpResponse;
}, httpRequest.socket.setTimeout(sessionContext.timeout * 1000);
Date.now(), if (sessionContext.authState !== 1) sessionContext.authState = 0;
(e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "REMOTE_ADDRESS")
return getRequestOrigin(httpRequest).remoteAddress;
return e;
} }
);
if (d) {
debug.incomingHttpRequest(httpRequest, null, bodyStr);
debug.outgoingHttpResponse(httpResponse, null, error.message);
}
}
httpResponse.end(error.message);
return;
} }
if (sessionContext) if (sessionContext)
return processRequest(sessionContext, rpc, parseWarnings, bodyStr); return processRequest(sessionContext, rpc, parseWarnings, bodyStr);
if (!(rpc.cpeRequest && rpc.cpeRequest.name === "Inform")) { if (rpc.cpeRequest?.name !== "Inform") {
logger.accessError({ logger.accessError({
message: "Invalid session", message: "Invalid session",
sessionContext: sessionContext || { sessionContext: {
httpRequest: httpRequest, httpRequest: httpRequest,
httpResponse: httpResponse, httpResponse: httpResponse,
}, },
}); });
const _body = "Invalid session";
httpResponse.setHeader("Content-Length", Buffer.byteLength(_body)); return clientError(
httpResponse.writeHead(400, { Connection: "close" }); httpRequest,
const cacheSnapshot = await localCache.getCurrentSnapshot(); httpResponse,
const d = !!localCache.getConfig( null,
cacheSnapshot, bodyStr,
"cwmp.debug", "Invalid session"
{
remoteAddress: getRequestOrigin(httpRequest).remoteAddress,
},
Date.now(),
(e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "REMOTE_ADDRESS")
return getRequestOrigin(httpRequest).remoteAddress;
return e;
}
); );
if (d) {
debug.incomingHttpRequest(httpRequest, null, bodyStr);
debug.outgoingHttpResponse(httpResponse, null, _body);
} }
httpResponse.end(_body);
if (stats.concurrentRequests > MAX_CONCURRENT_REQUESTS) {
// Check again just in case device included old session ID
// from the previous session
httpResponse.writeHead(503, { "Retry-after": 60, Connection: "close" });
httpResponse.end("503 Service Unavailable");
stats.droppedRequests += 1;
return; return;
} }
+44 -26
View File
@@ -17,9 +17,9 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { MongoClient, ObjectID, Collection, Db } from "mongodb"; import { MongoClient, ObjectId, Collection, Db } from "mongodb";
import { get } from "./config"; import { get } from "./config";
import { encodeTag, escapeRegExp } from "./common"; import { decodeTag, encodeTag, escapeRegExp } from "./common";
import { parse } from "./common/expression-parser"; import { parse } from "./common/expression-parser";
import { import {
DeviceData, DeviceData,
@@ -46,6 +46,8 @@ export let tasksCollection: Collection,
let clientPromise: Promise<MongoClient>; let clientPromise: Promise<MongoClient>;
const INVALID_PATH_SUFFIX = "__invalid";
function compareAccessLists(list1: string[], list2: string[]): boolean { function compareAccessLists(list1: string[], list2: string[]): boolean {
if (list1.length !== list2.length) return false; if (list1.length !== list2.length) return false;
for (const [i, v] of list1.entries()) if (v !== list2[i]) return false; for (const [i, v] of list1.entries()) if (v !== list2[i]) return false;
@@ -59,10 +61,7 @@ export function onConnect(callback: (db: Db) => Promise<void>): void {
} }
export async function connect(): Promise<void> { export async function connect(): Promise<void> {
clientPromise = MongoClient.connect("" + get("MONGODB_CONNECTION_URL"), { clientPromise = MongoClient.connect("" + get("MONGODB_CONNECTION_URL"));
useNewUrlParser: true,
useUnifiedTopology: true,
});
const client = await clientPromise; const client = await clientPromise;
const db = client.db(); const db = client.db();
@@ -167,7 +166,19 @@ export async function fetchDevice(
if (obj["_accessList"] != null) if (obj["_accessList"] != null)
attrs.accessList = [obj["_attributesTimestamp"] || 1, obj["_accessList"]]; attrs.accessList = [obj["_attributesTimestamp"] || 1, obj["_accessList"]];
try {
res.push([Path.parse(path), t, attrs]); res.push([Path.parse(path), t, attrs]);
} catch (err) {
// The path parser is now more strict so we might be in a situation where
// the database contains invalid paths from before this change So here we
// encode the invalid characters.
const splits = path.split(".");
splits[splits.length - 1] =
encodeTag(splits[splits.length - 1]) + INVALID_PATH_SUFFIX;
path = splits.join(".");
res.push([Path.parse(path), t, attrs]);
return;
}
for (const [k, v] of Object.entries(obj)) { for (const [k, v] of Object.entries(obj)) {
if (!k.startsWith("_")) { if (!k.startsWith("_")) {
@@ -449,7 +460,16 @@ export async function saveDevice(
break; break;
default: default:
if (!diff[2]) { if (!diff[2]) {
update["$unset"][path.toString()] = 1; let pathStr = path.toString();
// Paths with that suffix are encoded and need to be decoded
if (pathStr.endsWith(INVALID_PATH_SUFFIX)) {
const splits = pathStr.split(".");
splits[splits.length - 1] = decodeTag(
splits[splits.length - 1].slice(0, 0 - INVALID_PATH_SUFFIX.length)
);
pathStr = splits.join(".");
}
update["$unset"][pathStr] = 1;
continue; continue;
} }
@@ -485,7 +505,7 @@ export async function saveDevice(
break; break;
case "object": case "object":
if (!diff[1] || !diff[1].object || object2 !== object1) { if (!diff[1]?.object || object2 !== object1) {
update["$set"][ update["$set"][
path.length ? path.toString() + "._object" : "_object" path.length ? path.toString() + "._object" : "_object"
] = !!object2; ] = !!object2;
@@ -493,7 +513,7 @@ export async function saveDevice(
break; break;
case "writable": case "writable":
if (!diff[1] || !diff[1].writable || writable2 !== writable1) { if (!diff[1]?.writable || writable2 !== writable1) {
update["$set"][ update["$set"][
path.length ? path.toString() + "._writable" : "_writable" path.length ? path.toString() + "._writable" : "_writable"
] = !!writable2; ] = !!writable2;
@@ -514,9 +534,8 @@ export async function saveDevice(
} }
if (attributesTimestamp2 !== attributesTimestamp1) { if (attributesTimestamp2 !== attributesTimestamp1) {
update["$set"][ update["$set"][path.toString() + "._attributesTimestamp"] =
path.toString() + "._attributesTimestamp" new Date(attributesTimestamp2);
] = new Date(attributesTimestamp2);
} }
break; break;
@@ -534,9 +553,8 @@ export async function saveDevice(
} }
if (attributesTimestamp2 !== attributesTimestamp1) { if (attributesTimestamp2 !== attributesTimestamp1) {
update["$set"][ update["$set"][path.toString() + "._attributesTimestamp"] =
path.toString() + "._attributesTimestamp" new Date(attributesTimestamp2);
] = new Date(attributesTimestamp2);
} }
} }
} }
@@ -546,7 +564,7 @@ export async function saveDevice(
for (const attrName of Object.keys(diff[1])) { for (const attrName of Object.keys(diff[1])) {
if ( if (
diff[1][attrName][1] != null && diff[1][attrName][1] != null &&
(!diff[2] || !diff[2][attrName] || diff[2][attrName][1] == null) diff[2]?.[attrName]?.[1] == null
) { ) {
const p = path.length ? path.toString() + "." : ""; const p = path.length ? path.toString() + "." : "";
update["$unset"][`${p}_${attrName}`] = 1; update["$unset"][`${p}_${attrName}`] = 1;
@@ -600,7 +618,7 @@ export async function saveDevice(
upsert: isNew, upsert: isNew,
}); });
if (result.result.n !== 1) if (!result.matchedCount && !result.upsertedCount)
throw new Error(`Device ${deviceId} not found in database`); throw new Error(`Device ${deviceId} not found in database`);
if (update2) { if (update2) {
@@ -624,7 +642,7 @@ export async function getFaults(
delete r.device; delete r.device;
r.timestamp = +r.timestamp; r.timestamp = +r.timestamp;
r.provisions = JSON.parse(r.provisions); r.provisions = JSON.parse(r.provisions);
faults[channel] = r; faults[channel] = r as SessionFault;
} }
return faults; return faults;
@@ -665,13 +683,13 @@ export async function getDueTasks(
if (task.timestamp >= timestamp) return [tasks, +task.timestamp]; if (task.timestamp >= timestamp) return [tasks, +task.timestamp];
task._id = String(task._id); task._id = String(task._id);
tasks.push(task); tasks.push(task as Task);
// For API compatibility // For API compatibility
if (task.name === "download" && task.file) { if (task.name === "download" && task.file) {
let q; let q;
if (ObjectID.isValid(task.file)) if (ObjectId.isValid(task.file))
q = { _id: { $in: [task.file, new ObjectID(task.file)] } }; q = { _id: { $in: [task.file, new ObjectId(task.file)] } };
else q = { _id: task.file }; else q = { _id: task.file };
const res = await filesCollection.find(q).toArray(); const res = await filesCollection.find(q).toArray();
@@ -692,7 +710,7 @@ export async function clearTasks(
taskIds: string[] taskIds: string[]
): Promise<void> { ): Promise<void> {
await tasksCollection.deleteMany({ await tasksCollection.deleteMany({
_id: { $in: taskIds.map((id) => new ObjectID(id)) }, _id: { $in: taskIds.map((id) => new ObjectId(id)) },
}); });
} }
@@ -709,14 +727,14 @@ export async function getOperations(
delete r._id; delete r._id;
// Workaround for a bug in v1.2.1 where operation object is saved without deserialization // Workaround for a bug in v1.2.1 where operation object is saved without deserialization
if (typeof r.provisions !== "string") { if (typeof r.provisions !== "string") {
operations[commandKey] = r; operations[commandKey] = r as Operation;
continue; continue;
} }
r.timestamp = +r.timestamp; r.timestamp = +r.timestamp;
if (r.args) r.args = JSON.parse(r.args); if (r.args) r.args = JSON.parse(r.args);
r.provisions = JSON.parse(r.provisions); r.provisions = JSON.parse(r.provisions);
r.retries = JSON.parse(r.retries); r.retries = JSON.parse(r.retries);
operations[commandKey] = r; operations[commandKey] = r as Operation;
} }
return operations; return operations;
} }
@@ -784,7 +802,7 @@ interface Permission {
} }
export async function getPermissions(): Promise<Permission[]> { export async function getPermissions(): Promise<Permission[]> {
return permissionsCollection.find().toArray(); return permissionsCollection.find().toArray() as unknown as Permission[];
} }
interface User { interface User {
@@ -795,5 +813,5 @@ interface User {
} }
export async function getUsers(): Promise<User[]> { export async function getUsers(): Promise<User[]> {
return usersCollection.find().toArray(); return usersCollection.find().toArray() as unknown as User[];
} }
+12 -15
View File
@@ -17,12 +17,7 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { import { IncomingMessage, ServerResponse, ClientRequest } from "http";
IncomingMessage,
ServerResponse,
ClientRequest,
RequestOptions,
} from "http";
import { Socket } from "net"; import { Socket } from "net";
import { appendFileSync } from "fs"; import { appendFileSync } from "fs";
import { stringify } from "./common/yaml"; import { stringify } from "./common/yaml";
@@ -99,7 +94,8 @@ export function outgoingHttpResponse(
export function outgoingHttpRequest( export function outgoingHttpRequest(
httpRequest: ClientRequest, httpRequest: ClientRequest,
deviceId: string, deviceId: string,
options: RequestOptions, method: "GET" | "PUT" | "POST" | "DELETE",
url: URL,
body: string body: string
): void { ): void {
if (!DEBUG_FILE) return; if (!DEBUG_FILE) return;
@@ -111,9 +107,9 @@ export function outgoingHttpRequest(
remoteAddress: con.remoteAddress, remoteAddress: con.remoteAddress,
deviceId: deviceId, deviceId: deviceId,
connection: getConnectionTimestamp(con), connection: getConnectionTimestamp(con),
remotePort: options.port, remotePort: url.port,
method: options.method || "GET", method: method,
url: options.path, url: url.pathname + url.search,
headers: httpRequest.getHeaders(), headers: httpRequest.getHeaders(),
body: body, body: body,
}; };
@@ -128,7 +124,8 @@ export function outgoingHttpRequest(
export function outgoingHttpRequestError( export function outgoingHttpRequestError(
httpRequest: ClientRequest, httpRequest: ClientRequest,
deviceId: string, deviceId: string,
options: RequestOptions, method: "GET" | "PUT" | "POST" | "DELETE",
url: URL,
err: Error err: Error
): void { ): void {
if (!DEBUG_FILE) return; if (!DEBUG_FILE) return;
@@ -136,12 +133,12 @@ export function outgoingHttpRequestError(
const msg = { const msg = {
event: "outgoing HTTP request", event: "outgoing HTTP request",
timestamp: now, timestamp: now,
remoteAddress: options.hostname, remoteAddress: url.hostname,
deviceId: deviceId, deviceId: deviceId,
connection: null, connection: null,
remotePort: options.port, remotePort: url.port,
method: options.method, method: method,
url: options.path, url: url.pathname + url.search,
headers: httpRequest.getHeaders(), headers: httpRequest.getHeaders(),
error: err.message, error: err.message,
}; };
+2 -9
View File
@@ -54,13 +54,6 @@ export function sanitizeParameterValue(
else parameterValue[0] = b; else parameterValue[0] = b;
} }
break; break;
case "xsd:string":
case "xsd:base64":
case "xsd:hexBinary":
if (typeof parameterValue[0] !== "string")
parameterValue[0] = "" + parameterValue[0];
break;
case "xsd:int": case "xsd:int":
case "xsd:unsignedInt": case "xsd:unsignedInt":
if (typeof parameterValue[0] !== "number") { if (typeof parameterValue[0] !== "number") {
@@ -83,7 +76,7 @@ export function sanitizeParameterValue(
} }
break; break;
default: default:
parameterValue[0] = JSON.parse(JSON.stringify(parameterValue[0])); parameterValue[0] = "" + parameterValue[0];
break; break;
} }
} }
@@ -208,7 +201,7 @@ export function clear(
timestamp = timestamp || 0; timestamp = timestamp || 0;
let descendantsTimestamp = timestamp; let descendantsTimestamp = timestamp;
if (attributes && attributes.object) { if (attributes?.object) {
if (attributes.object > descendantsTimestamp) if (attributes.object > descendantsTimestamp)
descendantsTimestamp = attributes.object; descendantsTimestamp = attributes.object;
if (!(attributes.object <= attributes.value)) if (!(attributes.object <= attributes.value))
+10 -9
View File
@@ -30,14 +30,6 @@ const TIMEOUT = +config.get("EXT_TIMEOUT");
const processes: { [script: string]: ChildProcess } = {}; const processes: { [script: string]: ChildProcess } = {};
const jobs = new Map(); const jobs = new Map();
function messageHandler(message): void {
const func = jobs.get(message[0]);
if (func) {
jobs.delete(message[0]);
func({ fault: message[1], value: message[2] });
}
}
export function run(args: string[]): Promise<{ fault: Fault; value: any }> { export function run(args: string[]): Promise<{ fault: Fault; value: any }> {
return new Promise((resolve) => { return new Promise((resolve) => {
const scriptName = args[0]; const scriptName = args[0];
@@ -70,7 +62,16 @@ export function run(args: string[]): Promise<{ fault: Fault; value: any }> {
if (processes[scriptName] === p) delete processes[scriptName]; if (processes[scriptName] === p) delete processes[scriptName];
}); });
p.on("message", messageHandler); p.on("message", (message) => {
const func = jobs.get(message[0]);
if (func) {
jobs.delete(message[0]);
// Wait for any disconnect even to fire
setTimeout(() => {
func({ fault: message[1], value: message[2] });
});
}
});
const rlstdout = readline.createInterface(p.stdout); const rlstdout = readline.createInterface(p.stdout);
rlstdout.on("line", (line) => { rlstdout.on("line", (line) => {
+8 -5
View File
@@ -18,7 +18,6 @@
*/ */
import * as url from "url"; import * as url from "url";
import * as querystring from "querystring";
import { IncomingMessage, ServerResponse } from "http"; import { IncomingMessage, ServerResponse } from "http";
import { Collection, GridFSBucket } from "mongodb"; import { Collection, GridFSBucket } from "mongodb";
import { onConnect } from "./db"; import { onConnect } from "./db";
@@ -35,7 +34,7 @@ onConnect(async (db) => {
}); });
const getFile = memoize( const getFile = memoize(
(md5: string, size: number, filename: string): Promise<Buffer> => { (uploadDate: number, size: number, filename: string): Promise<Buffer> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const buffer = Buffer.allocUnsafe(size); const buffer = Buffer.allocUnsafe(size);
let i = 0; let i = 0;
@@ -64,7 +63,7 @@ export async function listener(
} }
const urlParts = url.parse(request.url, true); const urlParts = url.parse(request.url, true);
const filename = querystring.unescape(urlParts.pathname.substring(1)); const filename = decodeURIComponent(urlParts.pathname.substring(1));
const log = { const log = {
message: "Fetch file", message: "Fetch file",
@@ -82,10 +81,14 @@ export async function listener(
return; return;
} }
const buffer = await getFile(file.md5, file.length, filename); const buffer = await getFile(
file["uploadDate"].getTime(),
file.length,
filename
);
response.writeHead(200, { response.writeHead(200, {
"Content-Type": file.contentType || "application/octet-stream", "Content-Type": "application/octet-stream",
"Content-Length": file.length, "Content-Length": file.length,
}); });
+2 -2
View File
@@ -59,7 +59,7 @@ const INFORM_SCRIPT = `
// Device ID as user name // Device ID as user name
const username = declare("DeviceID.ID", {value: 1}).value[0] const username = declare("DeviceID.ID", {value: 1}).value[0]
// Password will be fixed a given device because Math.random() is seeded with devcie ID by default. // 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 password = Math.trunc(Math.random() * Number.MAX_SAFE_INTEGER).toString(36);
const informInterval = 300; const informInterval = 300;
@@ -257,7 +257,7 @@ export async function seed(options: Record<string, boolean>): Promise<void> {
"InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.KeyPassphrase", "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.KeyPassphrase",
}, },
{ _id: "ui.device.3.type", value: "'parameter-table'" }, { _id: "ui.device.3.type", value: "'parameter-table'" },
{ _id: "ui.device.3.label", value: "'WLAN hosts'" }, { _id: "ui.device.3.label", value: "'LAN hosts'" },
{ {
_id: "ui.device.3.parameter", _id: "ui.device.3.parameter",
value: "InternetGatewayDevice.LANDevice.1.Hosts.Host", value: "InternetGatewayDevice.LANDevice.1.Hosts.Host",
+1 -1
View File
@@ -22,7 +22,7 @@ interface Instance {
} }
export default class InstanceSet { export default class InstanceSet {
private set: Set<Instance>; private declare set: Set<Instance>;
public constructor() { public constructor() {
this.set = new Set(); this.set = new Set();
+7 -11
View File
@@ -131,7 +131,7 @@ async function fetchPresets(): Promise<Preset[]> {
// If no keys are defined, consider all parameters as keys to keep the // If no keys are defined, consider all parameters as keys to keep the
// same behavior from v1.0 // same behavior from v1.0
if (!obj["_keys"] || !obj["_keys"].length) if (!obj["_keys"]?.length)
obj["_keys"] = Object.keys(obj).filter((k) => !k.startsWith("_")); obj["_keys"] = Object.keys(obj).filter((k) => !k.startsWith("_"));
return obj; return obj;
@@ -266,9 +266,7 @@ async function fetchProvisions(): Promise<Provisions> {
.createHash("md5") .createHash("md5")
.update(r["script"]) .update(r["script"])
.digest("hex"); .digest("hex");
provisions[ provisions[r["_id"]].script = new vm.Script(
r["_id"]
].script = new vm.Script(
`"use strict";(function(){\n${r["script"]}\n})();`, `"use strict";(function(){\n${r["script"]}\n})();`,
{ filename: r["_id"], lineOffset: -1, timeout: 50 } { filename: r["_id"], lineOffset: -1, timeout: 50 }
); );
@@ -287,9 +285,7 @@ async function fetchVirtualParameters(): Promise<VirtualParameters> {
.createHash("md5") .createHash("md5")
.update(r["script"]) .update(r["script"])
.digest("hex"); .digest("hex");
virtualParameters[ virtualParameters[r["_id"]].script = new vm.Script(
r["_id"]
].script = new vm.Script(
`"use strict";(function(){\n${r["script"]}\n})();`, `"use strict";(function(){\n${r["script"]}\n})();`,
{ filename: r["_id"], lineOffset: -1, timeout: 50 } { filename: r["_id"], lineOffset: -1, timeout: 50 }
); );
@@ -325,8 +321,6 @@ async function fetchFiles(): Promise<Files> {
const id = r["filename"] || r["_id"].toString(); const id = r["filename"] || r["_id"].toString();
files[id] = {}; files[id] = {};
files[id].length = r["length"]; files[id].length = r["length"];
files[id].md5 = r["md5"];
files[id].contentType = r["contentType"];
} }
return files; return files;
@@ -357,6 +351,7 @@ async function fetchConfig(): Promise<[Config, UiConfig]> {
device: {}, device: {},
index: {}, index: {},
overview: {}, overview: {},
pageSize: null,
}; };
const _config = {}; const _config = {};
@@ -366,12 +361,13 @@ async function fetchConfig(): Promise<[Config, UiConfig]> {
_config[c.id] = val; _config[c.id] = val;
if (c.id.startsWith("ui.")) { if (c.id.startsWith("ui.")) {
const keys = c.id.split("."); const keys = c.id.split(".");
if (!(keys[1] in ui)) continue;
// remove the first key(ui) // remove the first key(ui)
keys.shift(); keys.shift();
let ref = ui; let ref = ui;
while (keys.length > 1) { while (keys.length > 1) {
const k = keys.shift(); const k = keys.shift();
if (typeof ref[k] !== "object") ref[k] = {}; if (ref[k] == null || typeof ref[k] !== "object") ref[k] = {};
ref = ref[k]; ref = ref[k];
} }
ref[keys[0]] = val; ref[keys[0]] = val;
@@ -501,7 +497,7 @@ export function getConfig(
if (!(key in snapshot.config)) { if (!(key in snapshot.config)) {
if (key in oldOpts) { if (key in oldOpts) {
let id; let id;
if (context && context["id"]) { if (context?.["id"]) {
id = context["id"]; id = context["id"];
} else if (cb) { } else if (cb) {
id = cb(["PARAM", "DeviceID.ID"]); id = cb(["PARAM", "DeviceID.ID"]);
+3 -5
View File
@@ -292,11 +292,9 @@ function formatSimple(
return `${severity}${remote}${details.message}${meta}${os.EOL}`; return `${severity}${remote}${details.message}${meta}${os.EOL}`;
} }
return `${ return `${details.timestamp} [${(
details.timestamp details.severity as string
} [${(details.severity as string).toUpperCase()}] ${remote}${ ).toUpperCase()}] ${remote}${details.message}${meta}${os.EOL}`;
details.message
}${meta}${os.EOL}`;
} }
function log(details: Record<string, unknown>): void { function log(details: Record<string, unknown>): void {
+2 -2
View File
@@ -17,7 +17,7 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { ObjectID } from "mongodb"; import { ObjectId } from "mongodb";
import { map, parse, stringify, parseList } from "./common/expression-parser"; import { map, parse, stringify, parseList } from "./common/expression-parser";
import { likePatternToRegExp } from "./common/expression"; import { likePatternToRegExp } from "./common/expression";
import { Expression, Fault, Task } from "./types"; import { Expression, Fault, Task } from "./types";
@@ -90,7 +90,7 @@ export function processTasksFilter(filter: Expression): Expression {
if (!isArray(exp)) return exp; if (!isArray(exp)) return exp;
if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) { if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) {
const e = exp.slice(); const e = exp.slice();
if (e[1][0] === "PARAM" && e[1][1] === "_id") e[2] = new ObjectID(e[2]); if (e[1][0] === "PARAM" && e[1][1] === "_id") e[2] = new ObjectId(e[2]);
else if (e[1][0] === "PARAM" && e[1][1] === "timestamp") else if (e[1][0] === "PARAM" && e[1][1] === "timestamp")
e[2] = new Date(e[2]); e[2] = new Date(e[2]);
else if (e[1][0] === "PARAM" && e[1][1] === "expiry") else if (e[1][0] === "PARAM" && e[1][1] === "expiry")
+95 -40
View File
@@ -19,7 +19,6 @@
import * as url from "url"; import * as url from "url";
import { Collection, GridFSBucket, ObjectId } from "mongodb"; import { Collection, GridFSBucket, ObjectId } from "mongodb";
import * as querystring from "querystring";
import * as vm from "vm"; import * as vm from "vm";
import * as config from "./config"; import * as config from "./config";
import { onConnect } from "./db"; import { onConnect } from "./db";
@@ -30,10 +29,12 @@ import * as cache from "./cache";
import { version as VERSION } from "../package.json"; import { version as VERSION } from "../package.json";
import { ping } from "./ping"; import { ping } from "./ping";
import * as logger from "./logger"; import * as logger from "./logger";
import { flattenDevice } from "./mongodb-functions";
const DEVICE_TASKS_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/tasks\/?$/; const DEVICE_TASKS_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/tasks\/?$/;
const TASKS_REGEX = /^\/tasks\/([a-zA-Z0-9\-_%]+)(\/[a-zA-Z_]*)?$/; const TASKS_REGEX = /^\/tasks\/([a-zA-Z0-9\-_%]+)(\/[a-zA-Z_]*)?$/;
const TAGS_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/tags\/([a-zA-Z0-9\-_%]+)\/?$/; const TAGS_REGEX =
/^\/devices\/([a-zA-Z0-9\-_%]+)\/tags\/([a-zA-Z0-9\-_%]+)\/?$/;
const PRESETS_REGEX = /^\/presets\/([a-zA-Z0-9\-_%]+)\/?$/; const PRESETS_REGEX = /^\/presets\/([a-zA-Z0-9\-_%]+)\/?$/;
const OBJECTS_REGEX = /^\/objects\/([a-zA-Z0-9\-_%]+)\/?$/; const OBJECTS_REGEX = /^\/objects\/([a-zA-Z0-9\-_%]+)\/?$/;
const FILES_REGEX = /^\/files\/([a-zA-Z0-9%!*'();:@&=+$,?#[\]\-_.~]+)\/?$/; const FILES_REGEX = /^\/files\/([a-zA-Z0-9%!*'();:@&=+$,?#[\]\-_.~]+)\/?$/;
@@ -41,10 +42,11 @@ const PING_REGEX = /^\/ping\/([a-zA-Z0-9\-_.:]+)\/?$/;
const QUERY_REGEX = /^\/([a-zA-Z0-9_]+)\/?$/; const QUERY_REGEX = /^\/([a-zA-Z0-9_]+)\/?$/;
const DELETE_DEVICE_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/?$/; const DELETE_DEVICE_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/?$/;
const PROVISIONS_REGEX = /^\/provisions\/([a-zA-Z0-9\-_%]+)\/?$/; const PROVISIONS_REGEX = /^\/provisions\/([a-zA-Z0-9\-_%]+)\/?$/;
const VIRTUAL_PARAMETERS_REGEX = /^\/virtual_parameters\/([a-zA-Z0-9\-_%]+)\/?$/; const VIRTUAL_PARAMETERS_REGEX =
/^\/virtual_parameters\/([a-zA-Z0-9\-_%]+)\/?$/;
const FAULTS_REGEX = /^\/faults\/([a-zA-Z0-9\-_%:]+)\/?$/; const FAULTS_REGEX = /^\/faults\/([a-zA-Z0-9\-_%:]+)\/?$/;
const collections = { const collections: Record<string, Collection> = {
tasks: null as Collection, tasks: null as Collection,
devices: null as Collection, devices: null as Collection,
presets: null as Collection, presets: null as Collection,
@@ -102,17 +104,24 @@ export function listener(
logger.accessInfo( logger.accessInfo(
Object.assign({}, urlParts.query, { Object.assign({}, urlParts.query, {
remoteAddress: request.connection.remoteAddress, remoteAddress: request.socket.remoteAddress,
message: `${request.method} ${urlParts.pathname}`, message: `${request.method} ${urlParts.pathname}`,
}) })
); );
if (PRESETS_REGEX.test(urlParts.pathname)) { if (PRESETS_REGEX.test(urlParts.pathname)) {
const presetName = querystring.unescape( const presetName = decodeURIComponent(
PRESETS_REGEX.exec(urlParts.pathname)[1] PRESETS_REGEX.exec(urlParts.pathname)[1]
); );
if (request.method === "PUT") { if (request.method === "PUT") {
const preset = JSON.parse(body.toString()); let preset;
try {
preset = JSON.parse(body.toString());
} catch (err) {
response.writeHead(400);
response.end(`${err.name}: ${err.message}`);
return;
}
preset._id = presetName; preset._id = presetName;
collections.presets.replaceOne( collections.presets.replaceOne(
{ _id: presetName }, { _id: presetName },
@@ -155,11 +164,18 @@ export function listener(
response.end("405 Method Not Allowed"); response.end("405 Method Not Allowed");
} }
} else if (OBJECTS_REGEX.test(urlParts.pathname)) { } else if (OBJECTS_REGEX.test(urlParts.pathname)) {
const objectName = querystring.unescape( const objectName = decodeURIComponent(
OBJECTS_REGEX.exec(urlParts.pathname)[1] OBJECTS_REGEX.exec(urlParts.pathname)[1]
); );
if (request.method === "PUT") { if (request.method === "PUT") {
const object = JSON.parse(body.toString()); let object;
try {
object = JSON.parse(body.toString());
} catch (err) {
response.writeHead(400);
response.end(`${err.name}: ${err.message}`);
return;
}
object._id = objectName; object._id = objectName;
collections.objects.replaceOne( collections.objects.replaceOne(
{ _id: objectName }, { _id: objectName },
@@ -202,7 +218,7 @@ export function listener(
response.end("405 Method Not Allowed"); response.end("405 Method Not Allowed");
} }
} else if (PROVISIONS_REGEX.test(urlParts.pathname)) { } else if (PROVISIONS_REGEX.test(urlParts.pathname)) {
const provisionName = querystring.unescape( const provisionName = decodeURIComponent(
PROVISIONS_REGEX.exec(urlParts.pathname)[1] PROVISIONS_REGEX.exec(urlParts.pathname)[1]
); );
if (request.method === "PUT") { if (request.method === "PUT") {
@@ -260,7 +276,7 @@ export function listener(
response.end("405 Method Not Allowed"); response.end("405 Method Not Allowed");
} }
} else if (VIRTUAL_PARAMETERS_REGEX.test(urlParts.pathname)) { } else if (VIRTUAL_PARAMETERS_REGEX.test(urlParts.pathname)) {
const virtualParameterName = querystring.unescape( const virtualParameterName = decodeURIComponent(
VIRTUAL_PARAMETERS_REGEX.exec(urlParts.pathname)[1] VIRTUAL_PARAMETERS_REGEX.exec(urlParts.pathname)[1]
); );
if (request.method === "PUT") { if (request.method === "PUT") {
@@ -322,8 +338,8 @@ export function listener(
} }
} else if (TAGS_REGEX.test(urlParts.pathname)) { } else if (TAGS_REGEX.test(urlParts.pathname)) {
const r = TAGS_REGEX.exec(urlParts.pathname); const r = TAGS_REGEX.exec(urlParts.pathname);
const deviceId = querystring.unescape(r[1]); const deviceId = decodeURIComponent(r[1]);
const tag = querystring.unescape(r[2]); const tag = decodeURIComponent(r[2]);
if (request.method === "POST") { if (request.method === "POST") {
collections.devices.updateOne( collections.devices.updateOne(
{ _id: deviceId }, { _id: deviceId },
@@ -351,7 +367,7 @@ export function listener(
} }
} else if (FAULTS_REGEX.test(urlParts.pathname)) { } else if (FAULTS_REGEX.test(urlParts.pathname)) {
if (request.method === "DELETE") { if (request.method === "DELETE") {
const faultId = querystring.unescape( const faultId = decodeURIComponent(
FAULTS_REGEX.exec(urlParts.pathname)[1] FAULTS_REGEX.exec(urlParts.pathname)[1]
); );
const deviceId = faultId.split(":", 1)[0]; const deviceId = faultId.split(":", 1)[0];
@@ -396,15 +412,31 @@ export function listener(
} }
} else if (DEVICE_TASKS_REGEX.test(urlParts.pathname)) { } else if (DEVICE_TASKS_REGEX.test(urlParts.pathname)) {
if (request.method === "POST") { if (request.method === "POST") {
const deviceId = querystring.unescape( const deviceId = decodeURIComponent(
DEVICE_TASKS_REGEX.exec(urlParts.pathname)[1] DEVICE_TASKS_REGEX.exec(urlParts.pathname)[1]
); );
if (body.length) { if (body.length) {
const task = JSON.parse(body.toString()); let task;
try {
task = JSON.parse(body.toString());
} catch (err) {
response.writeHead(400);
response.end(`${err.name}: ${err.message}`);
return;
}
task.device = deviceId; task.device = deviceId;
apiFunctions collections.devices
.insertTasks(task) .findOne({ _id: deviceId })
.then(async () => { .then(async (dev) => {
if (!dev) {
response.writeHead(404);
response.end("No such device");
return;
}
const device = flattenDevice(dev);
await apiFunctions.insertTasks(task);
const lastInform = Date.now(); const lastInform = Date.now();
const socketTimeout: number = request.socket["timeout"]; const socketTimeout: number = request.socket["timeout"];
@@ -435,7 +467,10 @@ export function listener(
return; return;
} }
const status = await apiFunctions.connectionRequest(deviceId); const status = await apiFunctions.connectionRequest(
deviceId,
device
);
if (status) { if (status) {
if (socketTimeout) request.socket.setTimeout(socketTimeout); if (socketTimeout) request.socket.setTimeout(socketTimeout);
@@ -518,15 +553,28 @@ export function listener(
}); });
} else if (urlParts.query.connection_request != null) { } else if (urlParts.query.connection_request != null) {
// No task, send connection request only // No task, send connection request only
apiFunctions collections.devices
.connectionRequest(deviceId) .findOne({ _id: deviceId })
.then(() => { .then(async (dev) => {
if (!dev) {
response.writeHead(404);
response.end("No such device");
return;
}
return apiFunctions.connectionRequest(deviceId).then((status) => {
if (status) {
response.writeHead(504, status);
response.end(status);
return;
}
response.writeHead(200); response.writeHead(200);
response.end(); response.end();
});
}) })
.catch((err) => { .catch((err) => {
response.writeHead(504); setTimeout(() => {
response.end(`${err.name}: ${err.message}`); throwError(err, response);
});
}); });
} else { } else {
response.writeHead(400); response.writeHead(400);
@@ -538,7 +586,7 @@ export function listener(
} }
} else if (TASKS_REGEX.test(urlParts.pathname)) { } else if (TASKS_REGEX.test(urlParts.pathname)) {
const r = TASKS_REGEX.exec(urlParts.pathname); const r = TASKS_REGEX.exec(urlParts.pathname);
const taskId = querystring.unescape(r[1]); const taskId = decodeURIComponent(r[1]);
const action = r[2]; const action = r[2];
if (!action || action === "/") { if (!action || action === "/") {
if (request.method === "DELETE") { if (request.method === "DELETE") {
@@ -624,7 +672,7 @@ export function listener(
response.end(); response.end();
} }
} else if (FILES_REGEX.test(urlParts.pathname)) { } else if (FILES_REGEX.test(urlParts.pathname)) {
const filename = querystring.unescape( const filename = decodeURIComponent(
FILES_REGEX.exec(urlParts.pathname)[1] FILES_REGEX.exec(urlParts.pathname)[1]
); );
if (request.method === "PUT") { if (request.method === "PUT") {
@@ -634,9 +682,9 @@ export function listener(
productClass: request.headers.productclass, productClass: request.headers.productclass,
version: request.headers.version, version: request.headers.version,
}; };
filesBucket.delete((filename as unknown) as ObjectId, () => { filesBucket.delete(filename as unknown as ObjectId, () => {
const uploadStream = filesBucket.openUploadStreamWithId( const uploadStream = filesBucket.openUploadStreamWithId(
filename, filename as unknown as ObjectId,
filename, filename,
{ {
metadata: metadata, metadata: metadata,
@@ -653,7 +701,7 @@ export function listener(
}); });
}); });
} else if (request.method === "DELETE") { } else if (request.method === "DELETE") {
filesBucket.delete((filename as unknown) as ObjectId, (err) => { filesBucket.delete(filename as unknown as ObjectId, (err) => {
if (err) { if (err) {
if (err.message.startsWith("FileNotFound")) { if (err.message.startsWith("FileNotFound")) {
response.writeHead(404); response.writeHead(404);
@@ -671,7 +719,7 @@ export function listener(
response.end("405 Method Not Allowed"); response.end("405 Method Not Allowed");
} }
} else if (PING_REGEX.test(urlParts.pathname)) { } else if (PING_REGEX.test(urlParts.pathname)) {
const host = querystring.unescape(PING_REGEX.exec(urlParts.pathname)[1]); const host = decodeURIComponent(PING_REGEX.exec(urlParts.pathname)[1]);
ping(host, (err, res, stdout) => { ping(host, (err, res, stdout) => {
if (err) { if (err) {
if (!res) { if (!res) {
@@ -697,7 +745,7 @@ export function listener(
return; return;
} }
const deviceId = querystring.unescape( const deviceId = decodeURIComponent(
DELETE_DEVICE_REGEX.exec(urlParts.pathname)[1] DELETE_DEVICE_REGEX.exec(urlParts.pathname)[1]
); );
apiFunctions apiFunctions
@@ -776,7 +824,14 @@ export function listener(
const cur = collection.find(q, { projection: projection }); const cur = collection.find(q, { projection: projection });
if (urlParts.query.sort) { if (urlParts.query.sort) {
const s = JSON.parse(urlParts.query.sort as string); let s;
try {
s = JSON.parse(urlParts.query.sort as string);
} catch (err) {
response.writeHead(400);
response.end(`${err.name}: ${err.message}`);
return;
}
const sort = {}; const sort = {};
for (const [k, v] of Object.entries(s)) { for (const [k, v] of Object.entries(s)) {
if (k[k.lastIndexOf(".") + 1] !== "_" && collectionName === "devices") if (k[k.lastIndexOf(".") + 1] !== "_" && collectionName === "devices")
@@ -787,13 +842,7 @@ export function listener(
cur.sort(sort); cur.sort(sort);
} }
if (urlParts.query.skip) cur.count((err, total) => {
cur.skip(parseInt(urlParts.query.skip as string));
if (urlParts.query.limit)
cur.limit(parseInt(urlParts.query.limit as string));
cur.count(false, (err, total) => {
if (err) return void throwError(err); if (err) return void throwError(err);
response.writeHead(200, { response.writeHead(200, {
@@ -806,6 +855,12 @@ export function listener(
return; return;
} }
if (urlParts.query.skip)
cur.skip(parseInt(urlParts.query.skip as string));
if (urlParts.query.limit)
cur.limit(parseInt(urlParts.query.limit as string));
response.write("[\n"); response.write("[\n");
i = 0; i = 0;
cur.forEach( cur.forEach(
+21 -4
View File
@@ -19,6 +19,7 @@
import { platform } from "os"; import { platform } from "os";
import { exec } from "child_process"; import { exec } from "child_process";
import { domainToASCII } from "url";
export interface PingResult { export interface PingResult {
packetsTransmitted: number; packetsTransmitted: number;
@@ -30,25 +31,41 @@ export interface PingResult {
mdev: number; mdev: number;
} }
function isValidHost(host: string): boolean {
// Valid chars in IPv4, IPv6, domain names
if (/^[a-zA-Z0-9\-.:[\]-]+$/.test(host)) return true;
// Check if input is an IDN convert to Punycode
// Can't merge with above because domainToASCII doesn't accept IP addresses
return /^[a-zA-Z0-9\-.:[\]-]+$/.test(domainToASCII(host));
}
export function ping( export function ping(
host: string, host: string,
callback: (err: Error, res?: PingResult, stdout?: string) => void callback: (err: Error, res?: PingResult, stdout?: string) => void
): void { ): void {
let cmd: string, parseRegExp1: RegExp, parseRegExp2: RegExp; let cmd: string, parseRegExp1: RegExp, parseRegExp2: RegExp;
// Validate input to prevent possible remote code execution
// Credit to Alex Hordijk for reporting this vulnerability
if (!isValidHost(host)) return callback(new Error("Invalid host"));
host = host.replace("[", "").replace("]", ""); host = host.replace("[", "").replace("]", "");
switch (platform()) { switch (platform()) {
case "linux": case "linux":
cmd = `ping -w 1 -i 0.2 -c 3 ${host}`; cmd = `ping -w 1 -i 0.2 -c 3 ${host}`;
parseRegExp1 = /(\d+) packets transmitted, (\d+) received, ([\d.]+)% packet loss[^]*([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+)/; parseRegExp1 =
parseRegExp2 = /(\d+) packets transmitted, (\d+) received, ([\d.]+)% packet loss/; /(\d+) packets transmitted, (\d+) received, ([\d.]+)% packet loss[^]*([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+)/;
parseRegExp2 =
/(\d+) packets transmitted, (\d+) received, ([\d.]+)% packet loss/;
break; break;
case "freebsd": case "freebsd":
// Send a single packet because on FreeBSD only superuser can send // Send a single packet because on FreeBSD only superuser can send
// packets that are only 200 ms apart. // packets that are only 200 ms apart.
cmd = `ping -t 1 -c 3 ${host}`; cmd = `ping -t 1 -c 3 ${host}`;
parseRegExp1 = /(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss\nround-trip min\/avg\/max\/stddev = ([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+) ms/; parseRegExp1 =
parseRegExp2 = /(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss/; /(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss\nround-trip min\/avg\/max\/stddev = ([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+) ms/;
parseRegExp2 =
/(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss/;
break; break;
default: default:
+1 -1
View File
@@ -18,7 +18,7 @@
*/ */
import * as crypto from "crypto"; import * as crypto from "crypto";
import * as later from "later"; import * as later from "@breejs/later";
function md532(str): number { function md532(str): number {
const digest = crypto.createHash("md5").update(str).digest(); const digest = crypto.createHash("md5").update(str).digest();
+155 -82
View File
@@ -59,6 +59,7 @@ import {
GetParameterValuesResponse, GetParameterValuesResponse,
} from "./types"; } from "./types";
import { getRequestOrigin } from "./forwarded"; import { getRequestOrigin } from "./forwarded";
import * as logger from "./logger";
const VALID_PARAM_TYPES = new Set([ const VALID_PARAM_TYPES = new Set([
"xsd:int", "xsd:int",
@@ -135,7 +136,7 @@ export function configContextCallback(
const path = paths.get(Path.parse(name)); const path = paths.get(Path.parse(name));
if (path) { if (path) {
const attrs = deviceData.attributes.get(path, 1); const attrs = deviceData.attributes.get(path, 1);
if (attrs && attrs.value && attrs.value[1]) return attrs.value[1][0]; if (attrs?.value?.[1]) return attrs.value[1][0];
} }
} else if (exp[0] === "FUNC") { } else if (exp[0] === "FUNC") {
if (exp[1] === "REMOTE_ADDRESS") if (exp[1] === "REMOTE_ADDRESS")
@@ -298,7 +299,7 @@ export async function transferComplete(
if (!sessionContext.operationsTouched) sessionContext.operationsTouched = {}; if (!sessionContext.operationsTouched) sessionContext.operationsTouched = {};
sessionContext.operationsTouched[commandKey] = 1; sessionContext.operationsTouched[commandKey] = 1;
if (rpcReq.faultStruct && rpcReq.faultStruct.faultCode !== "0") { if (rpcReq.faultStruct?.faultCode !== "0") {
revertDownloadParameters(sessionContext, operation.args.instance); revertDownloadParameters(sessionContext, operation.args.instance);
const fault: Fault = { const fault: Fault = {
@@ -417,13 +418,7 @@ function revertDownloadParameters(
); );
const toClear = device.set(sessionContext.deviceData, p, timestamp, { const toClear = device.set(sessionContext.deviceData, p, timestamp, {
value: [ value: [timestamp, [lastDownload?.value[1]?.[0] || 0, "xsd:dateTime"]],
timestamp,
[
lastDownload && lastDownload.value[1] ? lastDownload.value[1][0] : 0,
"xsd:dateTime",
],
],
}); });
if (toClear) { if (toClear) {
@@ -457,7 +452,36 @@ export async function timeoutOperations(
(e) => configContextCallback(sessionContext, e) (e) => configContextCallback(sessionContext, e)
) * 1000; ) * 1000;
if (sessionContext.timestamp > operation.timestamp + DOWNLOAD_TIMEOUT) { if (sessionContext.timestamp < operation.timestamp + DOWNLOAD_TIMEOUT)
continue;
logger.accessWarn({
sessionContext: sessionContext,
message: "Download operation timed out",
commandKey: commandKey,
});
const SUCCESS_ON_TIMEOUT = +localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.downloadSuccessOnTimeout",
{},
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e)
);
if (SUCCESS_ON_TIMEOUT) {
const r = {
name: "TransferComplete" as const,
commandKey: commandKey,
startTime: 0,
completeTime: 0,
};
// Call transferComplete code and ignore the response
await transferComplete(sessionContext, r);
continue;
}
delete sessionContext.operations[commandKey]; delete sessionContext.operations[commandKey];
if (!sessionContext.operationsTouched) if (!sessionContext.operationsTouched)
sessionContext.operationsTouched = {}; sessionContext.operationsTouched = {};
@@ -473,7 +497,6 @@ export async function timeoutOperations(
revertDownloadParameters(sessionContext, operation.args.instance); revertDownloadParameters(sessionContext, operation.args.instance);
} }
}
return { faults, operations }; return { faults, operations };
} }
@@ -1012,7 +1035,7 @@ export async function rpcRequest(
if ( if (
!sessionContext.virtualParameters.length && !sessionContext.virtualParameters.length &&
!sessionContext.declarations.length && !sessionContext.declarations.length &&
!(_declarations && _declarations.length) && !_declarations?.length &&
!sessionContext.provisions.length !sessionContext.provisions.length
) )
return { fault: null, rpcId: null, rpc: null }; return { fault: null, rpcId: null, rpc: null };
@@ -1087,12 +1110,11 @@ export async function rpcRequest(
return rpcRequest(sessionContext, _declarations); return rpcRequest(sessionContext, _declarations);
} }
if (_declarations && _declarations.length) { if (_declarations?.length) {
delete sessionContext.syncState; delete sessionContext.syncState;
if (!sessionContext.declarations[0]) sessionContext.declarations[0] = []; if (!sessionContext.declarations[0]) sessionContext.declarations[0] = [];
sessionContext.declarations[0] = sessionContext.declarations[0].concat( sessionContext.declarations[0] =
_declarations sessionContext.declarations[0].concat(_declarations);
);
return rpcRequest(sessionContext, null); return rpcRequest(sessionContext, null);
} }
@@ -1423,7 +1445,7 @@ export async function rpcRequest(
for (const [p, v] of sessionContext.syncState.downloadsValues) { for (const [p, v] of sessionContext.syncState.downloadsValues) {
const attrs = sessionContext.deviceData.attributes.get(p); const attrs = sessionContext.deviceData.attributes.get(p);
if (attrs) { if (attrs) {
if (attrs.writable && attrs.writable[1] && attrs.value) { if (attrs.writable?.[1] && attrs.value) {
const val = device.sanitizeParameterValue([v, attrs.value[1][1]]); const val = device.sanitizeParameterValue([v, attrs.value[1][1]]);
if (val[0] !== attrs.value[1][0]) { if (val[0] !== attrs.value[1][0]) {
toClear = device.set( toClear = device.set(
@@ -1664,7 +1686,7 @@ function generateGetRpcRequest(
syncState.refreshAttributes.value.delete(path); syncState.refreshAttributes.value.delete(path);
// Need to check in case param is deleted or changed to object // Need to check in case param is deleted or changed to object
const attrs = sessionContext.deviceData.attributes.get(path); const attrs = sessionContext.deviceData.attributes.get(path);
if (attrs && attrs.object && attrs.object[1] === 0) { if (attrs?.object?.[1] === 0) {
parameterNames.push(path.toString()); parameterNames.push(path.toString());
if (parameterNames.length >= GPV_BATCH_SIZE) break; if (parameterNames.length >= GPV_BATCH_SIZE) break;
} }
@@ -1817,7 +1839,7 @@ function generateSetRpcRequest(
for (const [k, v] of syncState.spv) { for (const [k, v] of syncState.spv) {
syncState.spv.delete(k); syncState.spv.delete(k);
const attrs = sessionContext.deviceData.attributes.get(k); const attrs = sessionContext.deviceData.attributes.get(k);
const curVal = attrs.value ? attrs.value[1] : null; const curVal = attrs.value?.[1];
if (curVal && canWrite(attrs)) { if (curVal && canWrite(attrs)) {
const val = v.slice() as [string | number | boolean, string]; const val = v.slice() as [string | number | boolean, string];
if (!val[1]) val[1] = curVal[1]; if (!val[1]) val[1] = curVal[1];
@@ -1881,8 +1903,9 @@ function generateSetRpcRequest(
// Downloads // Downloads
for (const [p, t] of syncState.downloadsDownload) { for (const [p, t] of syncState.downloadsDownload) {
if (!(t > 0 && t <= sessionContext.timestamp)) continue;
const attrs = deviceData.attributes.get(p); const attrs = deviceData.attributes.get(p);
if (!(attrs && attrs.value && t <= attrs.value[1][0])) { if (!(t <= attrs?.value?.[1]?.[0])) {
const fileTypeAttrs = deviceData.attributes.get( const fileTypeAttrs = deviceData.attributes.get(
deviceData.paths.get(p.slice(0, -1).concat(Path.parse("FileType"))) deviceData.paths.get(p.slice(0, -1).concat(Path.parse("FileType")))
); );
@@ -1899,42 +1922,31 @@ function generateSetRpcRequest(
name: "Download", name: "Download",
commandKey: generateRpcId(sessionContext), commandKey: generateRpcId(sessionContext),
instance: p.segments[1] as string, instance: p.segments[1] as string,
fileType: fileTypeAttrs fileType: fileTypeAttrs?.value?.[1][0] as string,
? fileTypeAttrs.value fileName: fileNameAttrs?.value?.[1][0] as string,
? (fileTypeAttrs.value[1][0] as string) targetFileName: targetFileNameAttrs?.value?.[1][0] as string,
: null
: null,
fileName: fileNameAttrs
? fileNameAttrs.value
? (fileNameAttrs.value[1][0] as string)
: null
: null,
targetFileName: targetFileNameAttrs
? targetFileNameAttrs.value
? (targetFileNameAttrs.value[1][0] as string)
: null
: null,
}; };
} }
} }
// Reboot // Reboot
if (syncState.reboot) { if (syncState.reboot > 0 && syncState.reboot <= sessionContext.timestamp) {
const p = sessionContext.deviceData.paths.get(Path.parse("Reboot")); const p = sessionContext.deviceData.paths.get(Path.parse("Reboot"));
const attrs = p ? sessionContext.deviceData.attributes.get(p) : null; const attrs = p ? sessionContext.deviceData.attributes.get(p) : null;
if (!(attrs && attrs.value && attrs.value[1][0] >= syncState.reboot)) { if (!(attrs?.value?.[1][0] >= syncState.reboot)) {
delete syncState.reboot; delete syncState.reboot;
return { name: "Reboot" }; return { name: "Reboot" };
} }
} }
// Factory reset // Factory reset
if (syncState.factoryReset) { if (
syncState.factoryReset > 0 &&
syncState.factoryReset <= sessionContext.timestamp
) {
const p = sessionContext.deviceData.paths.get(Path.parse("FactoryReset")); const p = sessionContext.deviceData.paths.get(Path.parse("FactoryReset"));
const attrs = p ? sessionContext.deviceData.attributes.get(p) : null; const attrs = p ? sessionContext.deviceData.attributes.get(p) : null;
if ( if (!(attrs?.value?.[1][0] >= syncState.factoryReset)) {
!(attrs && attrs.value && attrs.value[1][0] >= syncState.factoryReset)
) {
delete syncState.factoryReset; delete syncState.factoryReset;
return { name: "FactoryReset" }; return { name: "FactoryReset" };
} }
@@ -2002,7 +2014,7 @@ function generateSetVirtualParameterProvisions(
let provisions; let provisions;
if (virtualParameterDeclarations) { if (virtualParameterDeclarations) {
for (const declaration of virtualParameterDeclarations) { for (const declaration of virtualParameterDeclarations) {
if (declaration[2] && declaration[2].value != null) { if (declaration[2]?.value != null) {
const attrs = sessionContext.deviceData.attributes.get(declaration[0]); const attrs = sessionContext.deviceData.attributes.get(declaration[0]);
if ( if (
attrs && attrs &&
@@ -2131,9 +2143,7 @@ function processDeclarations(
if (currentAttributes) { if (currentAttributes) {
leafParam = currentPath; leafParam = currentPath;
leafIsObject = currentAttributes.object leafIsObject = currentAttributes.object?.[1];
? currentAttributes.object[1]
: null;
// Possible V8 bug causes null === 0 // Possible V8 bug causes null === 0
if (leafIsObject != null && leafIsObject === 0) if (leafIsObject != null && leafIsObject === 0)
leafTimestamp = Math.max(leafTimestamp, currentAttributes.object[0]); leafTimestamp = Math.max(leafTimestamp, currentAttributes.object[0]);
@@ -2148,13 +2158,13 @@ function processDeclarations(
) { ) {
case "Reboot": case "Reboot":
if (currentPath.length === 1) { if (currentPath.length === 1) {
if (declareAttributeValues && declareAttributeValues.value) if (declareAttributeValues?.value)
syncState.reboot = +new Date(declareAttributeValues.value[0]); syncState.reboot = +new Date(declareAttributeValues.value[0]);
} }
break; break;
case "FactoryReset": case "FactoryReset":
if (currentPath.length === 1) { if (currentPath.length === 1) {
if (declareAttributeValues && declareAttributeValues.value) if (declareAttributeValues?.value)
syncState.factoryReset = +new Date(declareAttributeValues.value[0]); syncState.factoryReset = +new Date(declareAttributeValues.value[0]);
} }
break; break;
@@ -2290,7 +2300,7 @@ function processDeclarations(
syncState.refreshAttributes.object.add(currentPath); syncState.refreshAttributes.object.add(currentPath);
else if (currentAttributes.object[1] === 0) else if (currentAttributes.object[1] === 0)
syncState.refreshAttributes.value.add(currentPath); syncState.refreshAttributes.value.add(currentPath);
} else { } else if (attrName in syncState.refreshAttributes) {
syncState.refreshAttributes[attrName].add(currentPath); syncState.refreshAttributes[attrName].add(currentPath);
} }
} }
@@ -2420,8 +2430,15 @@ export async function rpcResponse(
id: string, id: string,
_rpcRes: CpeResponse _rpcRes: CpeResponse
): Promise<Fault> { ): Promise<Fault> {
function invalidResponse(message: string): Fault {
return {
code: "invalid_response",
message: message,
};
}
if (id !== generateRpcId(sessionContext)) if (id !== generateRpcId(sessionContext))
throw new Error("Request ID not recognized"); return invalidResponse("Request ID not recognized");
++sessionContext.rpcCount; ++sessionContext.rpcCount;
@@ -2512,47 +2529,99 @@ export async function rpcResponse(
if (rpcRes.name === "GetParameterValuesResponse") { if (rpcRes.name === "GetParameterValuesResponse") {
if (rpcReq.name !== "GetParameterValues") if (rpcReq.name !== "GetParameterValues")
throw new Error("Response name does not match request name"); return invalidResponse("Response name does not match request name");
if (rpcRes.parameterList.length !== rpcReq.parameterNames.length) { const requested = new Set(rpcReq.parameterNames);
return {
code: `invalid_response`, for (const [path, value, type] of rpcRes.parameterList) {
message: if (!requested.delete(path.toString())) {
"The list of parameters returned in the GetParameterValuesResponse message does not match the requested parameters", logger.accessWarn({
}; sessionContext: sessionContext,
message: "Unexpected parameter in response",
parameter: path.toString(),
});
continue;
} }
for (const p of rpcRes.parameterList) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
p[0], path,
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
value: [timestamp, p.slice(1) as [string | number | boolean, string]], value: [timestamp, [value, type]],
}, },
toClear toClear
); );
} }
if (requested.size) {
for (const p of requested) {
logger.accessWarn({
sessionContext: sessionContext,
message: "Missing parameter in response",
parameter: p,
});
toClear = device.set(
sessionContext.deviceData,
Path.parse(p),
timestamp,
{
object: [timestamp, 0],
value: [timestamp, ["", "xsd:string"]],
},
toClear
);
}
}
} else if (rpcRes.name === "GetParameterAttributesResponse") { } else if (rpcRes.name === "GetParameterAttributesResponse") {
if (rpcReq.name !== "GetParameterAttributes") if (rpcReq.name !== "GetParameterAttributes")
throw new Error("Response name does not match request name"); throw new Error("Response name does not match request name");
for (const p of rpcRes.parameterList) { const requested = new Set(rpcReq.parameterNames);
for (const [path, notification, accessList] of rpcRes.parameterList) {
if (!requested.delete(path.toString())) {
logger.accessWarn({
sessionContext: sessionContext,
message: "Unexpected parameter in response",
parameter: path.toString(),
});
continue;
}
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
p[0], path,
timestamp, timestamp,
{ {
notification: [timestamp, p[1] as number], notification: [timestamp, notification],
accessList: [timestamp, p[2] as string[]], accessList: [timestamp, accessList],
}, },
toClear toClear
); );
} }
if (requested.size) {
for (const p of requested) {
logger.accessWarn({
sessionContext: sessionContext,
message: "Missing parameter in response",
parameter: p,
});
toClear = device.set(
sessionContext.deviceData,
Path.parse(p),
timestamp,
{
notification: [timestamp, 0],
accessList: [timestamp, []],
},
toClear
);
}
}
} else if (rpcRes.name === "GetParameterNamesResponse") { } else if (rpcRes.name === "GetParameterNamesResponse") {
if (rpcReq.name !== "GetParameterNames") if (rpcReq.name !== "GetParameterNames")
throw new Error("Response name does not match request name"); return invalidResponse("Response name does not match request name");
let root: Path; let root: Path;
if (rpcReq.parameterPath.endsWith(".")) if (rpcReq.parameterPath.endsWith("."))
@@ -2609,6 +2678,14 @@ export async function rpcResponse(
const wildcardParams: Path[] = [root.concat(wildcardPath)]; const wildcardParams: Path[] = [root.concat(wildcardPath)];
for (const [path, object, writable] of rpcRes.parameterList) { for (const [path, object, writable] of rpcRes.parameterList) {
if (!path.toString().startsWith(rpcReq.parameterPath)) {
logger.accessWarn({
sessionContext: sessionContext,
message: "Unexpected parameter in response",
parameter: path.toString(),
});
continue;
}
if (object && !rpcReq.nextLevel) if (object && !rpcReq.nextLevel)
wildcardParams.push(path.concat(wildcardPath)); wildcardParams.push(path.concat(wildcardPath));
@@ -2635,7 +2712,7 @@ export async function rpcResponse(
} }
} else if (rpcRes.name === "SetParameterValuesResponse") { } else if (rpcRes.name === "SetParameterValuesResponse") {
if (rpcReq.name !== "SetParameterValues") if (rpcReq.name !== "SetParameterValues")
throw new Error("Response name does not match request name"); return invalidResponse("Response name does not match request name");
for (const p of rpcReq.parameterList) { for (const p of rpcReq.parameterList) {
toClear = device.set( toClear = device.set(
@@ -2654,7 +2731,7 @@ export async function rpcResponse(
} }
} else if (rpcRes.name === "SetParameterAttributesResponse") { } else if (rpcRes.name === "SetParameterAttributesResponse") {
if (rpcReq.name !== "SetParameterAttributes") if (rpcReq.name !== "SetParameterAttributes")
throw new Error("Response name does not match request name"); return invalidResponse("Response name does not match request name");
for (const p of rpcReq.parameterList) { for (const p of rpcReq.parameterList) {
let attrs; let attrs;
@@ -2684,7 +2761,7 @@ export async function rpcResponse(
} }
} else if (rpcRes.name === "AddObjectResponse") { } else if (rpcRes.name === "AddObjectResponse") {
if (rpcReq.name !== "AddObject") if (rpcReq.name !== "AddObject")
throw new Error("Response name does not match request name"); return invalidResponse("Response name does not match request name");
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
@@ -2695,7 +2772,7 @@ export async function rpcResponse(
); );
} else if (rpcRes.name === "DeleteObjectResponse") { } else if (rpcRes.name === "DeleteObjectResponse") {
if (rpcReq.name !== "DeleteObject") if (rpcReq.name !== "DeleteObject")
throw new Error("Response name does not match request name"); return invalidResponse("Response name does not match request name");
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
@@ -2706,7 +2783,7 @@ export async function rpcResponse(
); );
} else if (rpcRes.name === "RebootResponse") { } else if (rpcRes.name === "RebootResponse") {
if (rpcReq.name !== "Reboot") if (rpcReq.name !== "Reboot")
throw new Error("Response name does not match request name"); return invalidResponse("Response name does not match request name");
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
@@ -2717,7 +2794,7 @@ export async function rpcResponse(
); );
} else if (rpcRes.name === "FactoryResetResponse") { } else if (rpcRes.name === "FactoryResetResponse") {
if (rpcReq.name !== "FactoryReset") if (rpcReq.name !== "FactoryReset")
throw new Error("Response name does not match request name"); return invalidResponse("Response name does not match request name");
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
@@ -2728,7 +2805,7 @@ export async function rpcResponse(
); );
} else if (rpcRes.name === "DownloadResponse") { } else if (rpcRes.name === "DownloadResponse") {
if (rpcReq.name !== "Download") if (rpcReq.name !== "Download")
throw new Error("Response name does not match request name"); return invalidResponse("Response name does not match request name");
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
@@ -2814,7 +2891,7 @@ export async function rpcResponse(
sessionContext.operationsTouched[rpcReq.commandKey] = 1; sessionContext.operationsTouched[rpcReq.commandKey] = 1;
} }
} else { } else {
throw new Error("Response name not recognized"); return invalidResponse("Response name not recognized");
} }
if (toClear) { if (toClear) {
@@ -2855,13 +2932,9 @@ export async function rpcFault(
(p) => [Path.parse(p.replace(/\.$/, "")), timestamp] as Clear (p) => [Path.parse(p.replace(/\.$/, "")), timestamp] as Clear
); );
} else if (rpcReq.name === "SetParameterValues") { } else if (rpcReq.name === "SetParameterValues") {
toClear = (rpcReq.parameterList as [ toClear = (
string, rpcReq.parameterList as [string, string | number | boolean, string][]
string | number | boolean, ).map((p) => [Path.parse(p[0].replace(/\.$/, "")), timestamp] as Clear);
string
][]).map(
(p) => [Path.parse(p[0].replace(/\.$/, "")), timestamp] as Clear
);
} else if (rpcReq.name === "AddObject") { } else if (rpcReq.name === "AddObject") {
toClear = [[Path.parse(rpcReq.objectName.replace(/\.$/, "")), timestamp]]; toClear = [[Path.parse(rpcReq.objectName.replace(/\.$/, "")), timestamp]];
} else if (rpcReq.name === "DeleteObject") { } else if (rpcReq.name === "DeleteObject") {
@@ -2898,10 +2971,10 @@ export async function deserialize(
const sessionContext = JSON.parse(sessionContextString) as SessionContext; const sessionContext = JSON.parse(sessionContextString) as SessionContext;
for (const decs of sessionContext.declarations) for (const decs of sessionContext.declarations)
for (const d of decs) d.path = Path.parse((d.path as unknown) as string); for (const d of decs) d.path = Path.parse(d.path as unknown as string);
const deviceData = initDeviceData(); const deviceData = initDeviceData();
for (const r of (sessionContext.deviceData as unknown) as any[]) { for (const r of sessionContext.deviceData as unknown as any[]) {
const path = deviceData.paths.add(Path.parse(r[0])); const path = deviceData.paths.add(Path.parse(r[0]));
if (r[1]) deviceData.trackers.set(path, r[1]); if (r[1]) deviceData.trackers.set(path, r[1]);
+53 -8
View File
@@ -109,8 +109,8 @@ function event(xml: Element): string[] {
function parameterInfoList(xml: Element): [Path, boolean, boolean][] { function parameterInfoList(xml: Element): [Path, boolean, boolean][] {
return xml.children return xml.children
.filter((e) => e.localName === "ParameterInfoStruct")
.map<[Path, boolean, boolean]>((e) => { .map<[Path, boolean, boolean]>((e) => {
if (e.localName !== "ParameterInfoStruct") return null;
let param: string, value: string; let param: string, value: string;
for (const c of e.children) { for (const c of e.children) {
switch (c.localName) { switch (c.localName) {
@@ -134,10 +134,20 @@ function parameterInfoList(xml: Element): [Path, boolean, boolean][] {
parsed = false; parsed = false;
} }
try {
if (param && !param.endsWith(".")) if (param && !param.endsWith("."))
return [Path.parse(param), false, parsed]; return [Path.parse(param), false, parsed];
else return [Path.parse(param.slice(0, -1)), true, parsed]; else return [Path.parse(param.slice(0, -1)), true, parsed];
} catch (err) {
warnings.push({
message: "Missing or invalid XML node",
element: "Name",
parameter: param,
}); });
return null;
}
})
.filter((e) => e != null);
} }
const getValueType = memoize((str: string) => { const getValueType = memoize((str: string) => {
@@ -151,8 +161,8 @@ function parameterValueList(
xml: Element xml: Element
): [Path, string | number | boolean, string][] { ): [Path, string | number | boolean, string][] {
return xml.children return xml.children
.filter((e) => e.localName === "ParameterValueStruct")
.map<[Path, string | number | boolean, string]>((e) => { .map<[Path, string | number | boolean, string]>((e) => {
if (e.localName !== "ParameterValueStruct") return null;
let valueElement: Element, param: string; let valueElement: Element, param: string;
for (const c of e.children) { for (const c of e.children) {
switch (c.localName) { switch (c.localName) {
@@ -208,15 +218,24 @@ function parameterValueList(
parsed = value; parsed = value;
} }
} }
try {
return [Path.parse(param), parsed, valueType]; return [Path.parse(param), parsed, valueType];
} catch (err) {
warnings.push({
message: "Missing or invalid XML node",
element: "Name",
parameter: param,
}); });
return null;
}
})
.filter((e) => e != null);
} }
function parameterAttributeList(xml: Element): [Path, number, string[]][] { function parameterAttributeList(xml: Element): [Path, number, string[]][] {
return xml.children return xml.children
.filter((e) => e.localName === "ParameterAttributeStruct")
.map<[Path, number, string[]]>((e) => { .map<[Path, number, string[]]>((e) => {
if (e.localName !== "ParameterAttributeStruct") return null;
let notificationElement: Element, let notificationElement: Element,
accessListElement: Element, accessListElement: Element,
param: string; param: string;
@@ -248,8 +267,18 @@ function parameterAttributeList(xml: Element): [Path, number, string[]][] {
.filter((c) => c.localName === "string") .filter((c) => c.localName === "string")
.map((c) => decodeEntities(c.text)); .map((c) => decodeEntities(c.text));
try {
return [Path.parse(param), notification, accessList]; return [Path.parse(param), notification, accessList];
} catch (err) {
warnings.push({
message: "Missing or invalid XML node",
element: "Name",
parameter: param,
}); });
return null;
}
})
.filter((e) => e != null);
} }
function GetParameterNames(methodRequest): string { function GetParameterNames(methodRequest): string {
@@ -701,6 +730,18 @@ function RequestDownloadResponse(): string {
return "<cwmp:RequestDownloadResponse></cwmp:RequestDownloadResponse>"; return "<cwmp:RequestDownloadResponse></cwmp:RequestDownloadResponse>";
} }
function AcsFault(f: CpeFault): string {
return `<soap-env:Body:Fault><faultcode>${encodeEntities(
f.faultCode
)}</faultcode><faultstring>${encodeEntities(
f.faultString
)}</faultstring><detail><cwmp:Fault><FaultCode>${encodeEntities(
f.detail.faultCode
)}</FaultCode><FaultString>${encodeEntities(
f.detail.faultString
)}</FaultString></cwmp:Fault></detail></soap-env:Body:Fault>`;
}
function faultStruct(xml: Element): FaultStruct { function faultStruct(xml: Element): FaultStruct {
let faultCode: string, let faultCode: string,
faultString: string, faultString: string,
@@ -798,18 +839,18 @@ function fault(xml: Element): CpeFault {
export function request( export function request(
body: string, body: string,
cwmpVersion: string,
warn: Record<string, unknown>[] warn: Record<string, unknown>[]
): SoapMessage { ): SoapMessage {
warnings = warn; warnings = warn;
const rpc = { const rpc = {
id: null, id: null,
cwmpVersion: cwmpVersion, cwmpVersion: null,
sessionTimeout: null, sessionTimeout: null,
cpeRequest: null, cpeRequest: null,
cpeFault: null, cpeFault: null,
cpeResponse: null, cpeResponse: null,
unknownMethod: null,
}; };
if (!body.length) return rpc; if (!body.length) return rpc;
@@ -848,7 +889,7 @@ export function request(
const methodElement = bodyElement.children[0]; const methodElement = bodyElement.children[0];
if (!rpc.cwmpVersion && methodElement.localName !== "Fault") { if (methodElement.localName === "Inform") {
let namespace, namespaceHref; let namespace, namespaceHref;
for (const e of [methodElement, bodyElement, envelope]) { for (const e of [methodElement, bodyElement, envelope]) {
namespace = namespace || e.namespace; namespace = namespace || e.namespace;
@@ -931,7 +972,8 @@ export function request(
rpc.cpeFault = fault(methodElement); rpc.cpeFault = fault(methodElement);
break; break;
default: default:
throw new Error(`8000 Method not supported ${methodElement.localName}`); rpc.unknownMethod = methodElement.localName;
break;
} }
return rpc; return rpc;
@@ -959,6 +1001,7 @@ export function response(rpc: {
id: string; id: string;
acsRequest?: AcsRequest; acsRequest?: AcsRequest;
acsResponse?: AcsResponse; acsResponse?: AcsResponse;
acsFault?: CpeFault;
cwmpVersion?: string; cwmpVersion?: string;
}): { code: number; headers: Record<string, string>; data: string } { }): { code: number; headers: Record<string, string>; data: string } {
const headers = { const headers = {
@@ -1027,6 +1070,8 @@ export function response(rpc: {
`Unknown method request ${(rpc.acsRequest as AcsRequest).name}` `Unknown method request ${(rpc.acsRequest as AcsRequest).name}`
); );
} }
} else if (rpc.acsFault) {
body = AcsFault(rpc.acsFault);
} }
headers["Content-Type"] = 'text/xml; charset="utf-8"'; headers["Content-Type"] = 'text/xml; charset="utf-8"';
+2 -1
View File
@@ -477,7 +477,7 @@ export interface VirtualParameters {
} }
export interface Files { export interface Files {
[name: string]: { length: number; md5: string; contentType: string }; [name: string]: { length: number };
} }
export interface Users { export interface Users {
@@ -526,6 +526,7 @@ export interface SoapMessage {
cpeRequest?: CpeRequest; cpeRequest?: CpeRequest;
cpeFault?: CpeFault; cpeFault?: CpeFault;
cpeResponse?: CpeResponse; cpeResponse?: CpeResponse;
unknownMethod?: string;
} }
export interface ScriptResult { export interface ScriptResult {
+6 -3
View File
@@ -59,8 +59,10 @@ const getAuthorizer = memoize(
} }
); );
koa.on("error", async (err) => { koa.on("error", async (err, ctx) => {
setTimeout(() => { setTimeout(() => {
// Ignored errors resulting from aborted requests
if (ctx?.req.aborted) return;
throw err; throw err;
}); });
}); });
@@ -93,7 +95,7 @@ koa.use(
koa.use(async (ctx, next) => { koa.use(async (ctx, next) => {
let roles: string[] = []; let roles: string[] = [];
if (ctx.state.user && ctx.state.user.username) { if (ctx.state.user?.username) {
let user; let user;
if (ctx.state.user.authMethod === "local") { if (ctx.state.user.authMethod === "local") {
user = localCache.getUsers(ctx.state.configSnapshot)[ user = localCache.getUsers(ctx.state.configSnapshot)[
@@ -219,7 +221,8 @@ router.post("/init", async (ctx) => {
}); });
router.get("/", async (ctx) => { router.get("/", async (ctx) => {
const permissionSets: PermissionSet[] = ctx.state.authorizer.getPermissionSets(); const permissionSets: PermissionSet[] =
ctx.state.authorizer.getPermissionSets();
let wizard = ""; let wizard = "";
if (!Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length) if (!Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length)
+8 -8
View File
@@ -17,7 +17,7 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { ObjectID } from "mongodb"; import { ObjectId } from "mongodb";
import * as db from "./db"; import * as db from "./db";
import { del } from "../cache"; import { del } from "../cache";
import { getUsers } from "../local-cache"; import { getUsers } from "../local-cache";
@@ -38,7 +38,7 @@ async function deleteFault(id): Promise<void> {
await Promise.all([ await Promise.all([
db.deleteFault(id), db.deleteFault(id),
channel.startsWith("task_") channel.startsWith("task_")
? db.deleteTask(new ObjectID(channel.slice(5))) ? db.deleteTask(new ObjectId(channel.slice(5)))
: null, : null,
]); ]);
@@ -108,7 +108,7 @@ export async function postTasks(
const lastInform = Date.now(); const lastInform = Date.now();
const notInSession = await awaitSessionEnd(deviceId, 30000); const notInSession = await awaitSessionEnd(deviceId, 30000);
if (!notInSession) { if (!notInSession) {
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectID(t._id)))); await Promise.all(statuses.map((t) => db.deleteTask(new ObjectId(t._id))));
return { return {
connectionRequest: "Session took too long to complete", connectionRequest: "Session took too long to complete",
tasks: statuses, tasks: statuses,
@@ -120,7 +120,7 @@ export async function postTasks(
const status = await connectionRequest(deviceId, device); const status = await connectionRequest(deviceId, device);
if (status) { if (status) {
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectID(t._id)))); await Promise.all(statuses.map((t) => db.deleteTask(new ObjectId(t._id))));
return { return {
connectionRequest: status, connectionRequest: status,
tasks: statuses, tasks: statuses,
@@ -129,7 +129,7 @@ export async function postTasks(
const sessionStarted = await awaitSessionStart(deviceId, lastInform, timeout); const sessionStarted = await awaitSessionStart(deviceId, lastInform, timeout);
if (!sessionStarted) { if (!sessionStarted) {
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectID(t._id)))); await Promise.all(statuses.map((t) => db.deleteTask(new ObjectId(t._id))));
return { return {
connectionRequest: "No contact from CPE", connectionRequest: "No contact from CPE",
tasks: statuses, tasks: statuses,
@@ -138,7 +138,7 @@ export async function postTasks(
const sessionEnded = await awaitSessionEnd(deviceId, 120000); const sessionEnded = await awaitSessionEnd(deviceId, 120000);
if (!sessionEnded) { if (!sessionEnded) {
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectID(t._id)))); await Promise.all(statuses.map((t) => db.deleteTask(new ObjectId(t._id))));
return { return {
connectionRequest: "Session took too long to complete", connectionRequest: "Session took too long to complete",
tasks: statuses, tasks: statuses,
@@ -163,7 +163,7 @@ export async function postTasks(
} }
} }
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectID(t._id)))); await Promise.all(statuses.map((t) => db.deleteTask(new ObjectId(t._id))));
return { connectionRequest: "OK", tasks: statuses }; return { connectionRequest: "OK", tasks: statuses };
} }
@@ -200,7 +200,7 @@ export function authLocal(
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const users = getUsers(snapshot); const users = getUsers(snapshot);
const user = users[username]; const user = users[username];
if (!user || !user.password) return void resolve(null); if (!user?.password) return void resolve(null);
hashPassword(password, user.salt) hashPassword(password, user.salt)
.then((hash) => { .then((hash) => {
if (hash === user.password) resolve(true); if (hash === user.password) resolve(true);
+11 -12
View File
@@ -17,7 +17,7 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import * as stream from "stream"; import { PassThrough } from "stream";
import Router from "koa-router"; import Router from "koa-router";
import * as db from "./db"; import * as db from "./db";
import * as apiFunctions from "./api-functions"; import * as apiFunctions from "./api-functions";
@@ -30,7 +30,6 @@ import { generateSalt, hashPassword } from "../auth";
import { del } from "../cache"; import { del } from "../cache";
import Authorizer from "../common/authorizer"; import Authorizer from "../common/authorizer";
import { ping } from "../ping"; import { ping } from "../ping";
import * as url from "url";
import { decodeTag } from "../common"; import { decodeTag } from "../common";
import { stringify as yamlStringify } from "../common/yaml"; import { stringify as yamlStringify } from "../common/yaml";
@@ -104,7 +103,7 @@ router.get(`/devices/:id.csv`, async (ctx) => {
.replace(/[:.]/g, "")}.csv` .replace(/[:.]/g, "")}.csv`
); );
ctx.body = new stream.PassThrough(); ctx.body = new PassThrough();
ctx.body.write( ctx.body.write(
"Parameter,Object,Object timestamp,Writable,Writable timestamp,Value,Value type,Value timestamp,Notification,Notification timestamp,Access list,Access list timestamp\n" "Parameter,Object,Object timestamp,Writable,Writable timestamp,Value,Value type,Value timestamp,Notification,Notification timestamp,Access list,Access list timestamp\n"
); );
@@ -217,7 +216,7 @@ for (const [resource, flags] of Object.entries(resources)) {
]); ]);
} }
ctx.body = new stream.PassThrough(); ctx.body = new PassThrough();
ctx.type = "application/json"; ctx.type = "application/json";
let c = 0; let c = 0;
@@ -277,7 +276,7 @@ for (const [resource, flags] of Object.entries(resources)) {
]); ]);
} }
ctx.body = new stream.PassThrough(); ctx.body = new PassThrough();
ctx.type = "text/csv"; ctx.type = "text/csv";
ctx.attachment( ctx.attachment(
`${resource}-${new Date(now).toISOString().replace(/[:.]/g, "")}.csv` `${resource}-${new Date(now).toISOString().replace(/[:.]/g, "")}.csv`
@@ -509,11 +508,11 @@ router.put("/files/:id", async (ctx) => {
} }
const metadata = { const metadata = {
fileType: singleParam(ctx.request.headers["metadata.filetype"]) || "", fileType: singleParam(ctx.request.headers["metadata-filetype"]) || "",
oui: singleParam(ctx.request.headers["metadata.oui"]) || "", oui: singleParam(ctx.request.headers["metadata-oui"]) || "",
productClass: productClass:
singleParam(ctx.request.headers["metadata.productclass"]) || "", singleParam(ctx.request.headers["metadata-productclass"]) || "",
version: singleParam(ctx.request.headers["metadata.version"]) || "", version: singleParam(ctx.request.headers["metadata-version"]) || "",
}; };
const validate = authorizer.getValidator(resource, metadata); const validate = authorizer.getValidator(resource, metadata);
@@ -574,12 +573,12 @@ router.post("/devices/:id/tasks", async (ctx) => {
if (!Array.isArray(exp)) return exp; if (!Array.isArray(exp)) return exp;
if (exp[0] === "PARAM") { if (exp[0] === "PARAM") {
const p = device[exp[1]]; const p = device[exp[1]];
if (p && p.value) return p.value[0]; if (p?.value) return p.value[0];
} else if (exp[0] === "FUNC") { } else if (exp[0] === "FUNC") {
if (exp[1] === "REMOTE_ADDRESS") { if (exp[1] === "REMOTE_ADDRESS") {
for (const root of ["InternetGatewayDevice", "Device"]) { for (const root of ["InternetGatewayDevice", "Device"]) {
const p = device[`${root}.ManagementServer.ConnectionRequestURL`]; const p = device[`${root}.ManagementServer.ConnectionRequestURL`];
if (p && p.value) return url.parse(p.value[0]).host; if (p?.value) return new URL(p.value[0]).hostname;
} }
return null; return null;
} }
@@ -659,7 +658,7 @@ router.get("/ping/:host", async (ctx) => {
ctx.body = parsed; ctx.body = parsed;
} else { } else {
ctx.status = 500; ctx.status = 500;
ctx.body = `${err.name}: ${err.message}`; ctx.body = err ? `${err.name}: ${err.message}` : "Unknown error";
} }
resolve(); resolve();
}); });
+15 -11
View File
@@ -17,13 +17,13 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Db, GridFSBucket, ObjectID } from "mongodb"; import { Db, GridFSBucket, ObjectId } from "mongodb";
import { Script } from "vm"; import { Script } from "vm";
import { onConnect } from "../db"; import { onConnect } from "../db";
import * as mongodbFunctions from "../mongodb-functions"; import * as mongodbFunctions from "../mongodb-functions";
import * as expression from "../common/expression"; import * as expression from "../common/expression";
import { QueryOptions, Expression } from "../types"; import { QueryOptions, Expression } from "../types";
import { Readable } from "stream"; import { Readable, Writable } from "stream";
import { minimize } from "../common/boolean-expression"; import { minimize } from "../common/boolean-expression";
const RESOURCE_COLLECTION = { const RESOURCE_COLLECTION = {
@@ -190,8 +190,8 @@ export async function updateDeviceTags(
const collection = db.collection("devices"); const collection = db.collection("devices");
const object = {}; const object = {};
if (add && add.length) object["$addToSet"] = { _tags: { $each: add } }; if (add?.length) object["$addToSet"] = { _tags: { $each: add } };
if (pull && pull.length) object["$pullAll"] = { _tags: pull }; if (pull?.length) object["$pullAll"] = { _tags: pull };
await collection.updateOne({ _id: deviceId }, object); await collection.updateOne({ _id: deviceId }, object);
} }
@@ -208,7 +208,7 @@ function putResource(resource, id, object): Promise<void> {
function deleteResource( function deleteResource(
resource: string, resource: string,
id: string | ObjectID id: string | ObjectId
): Promise<void> { ): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const collection = db.collection(RESOURCE_COLLECTION[resource] || resource); const collection = db.collection(RESOURCE_COLLECTION[resource] || resource);
@@ -242,7 +242,7 @@ export function putProvision(
lineOffset: -1, lineOffset: -1,
}); });
} catch (err) { } catch (err) {
if (err.stack && err.stack.startsWith(`${id}:`)) { if (err.stack?.startsWith(`${id}:`)) {
return Promise.reject( return Promise.reject(
new Error(`${err.name} at ${err.stack.split("\n", 1)[0]}`) new Error(`${err.name} at ${err.stack.split("\n", 1)[0]}`)
); );
@@ -267,7 +267,7 @@ export function putVirtualParameter(
lineOffset: -1, lineOffset: -1,
}); });
} catch (err) { } catch (err) {
if (err.stack && err.stack.startsWith(`${id}:`)) { if (err.stack?.startsWith(`${id}:`)) {
return Promise.reject( return Promise.reject(
new Error(`${err.name} at ${err.stack.split("\n", 1)[0]}`) new Error(`${err.name} at ${err.stack.split("\n", 1)[0]}`)
); );
@@ -338,13 +338,17 @@ export function putFile(
): Promise<void> { ): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const bucket = new GridFSBucket(db); const bucket = new GridFSBucket(db);
const uploadStream = bucket.openUploadStreamWithId(filename, filename, { const uploadStream = bucket.openUploadStreamWithId(
filename as unknown as ObjectId,
filename,
{
metadata: metadata, metadata: metadata,
}); }
);
uploadStream.on("error", reject); uploadStream.on("error", reject);
contentStream.on("error", reject); contentStream.on("error", reject);
uploadStream.on("finish", resolve); uploadStream.on("finish", resolve);
contentStream.pipe(uploadStream); contentStream.pipe(uploadStream as Writable);
}); });
} }
@@ -362,6 +366,6 @@ export function deleteFault(id: string): Promise<void> {
return deleteResource("faults", id); return deleteResource("faults", id);
} }
export function deleteTask(id: ObjectID): Promise<void> { export function deleteTask(id: ObjectId): Promise<void> {
return deleteResource("tasks", id); return deleteResource("tasks", id);
} }
+4 -4
View File
@@ -26,10 +26,10 @@ interface Revisions<V> {
} }
export default class VersionedMap<K, V> { export default class VersionedMap<K, V> {
private _sizeDiff: number[]; private declare _sizeDiff: number[];
private _revision: number; private declare _revision: number;
private map: Map<K, (V | symbol)[]>; private declare map: Map<K, (V | symbol)[]>;
public dirty: number; public declare dirty: number;
public constructor() { public constructor() {
this._sizeDiff = [0]; this._sizeDiff = [0];
+10101 -3599
View File
File diff suppressed because it is too large. Load diff
+38 -38
View File
@@ -1,6 +1,6 @@
{ {
"name": "genieacs", "name": "genieacs",
"version": "1.2.4", "version": "1.2.8",
"description": "A TR-069 Auto Configuration Server (ACS)", "description": "A TR-069 Auto Configuration Server (ACS)",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -19,56 +19,56 @@
"bin": "bin" "bin": "bin"
}, },
"dependencies": { "dependencies": {
"espresso-iisojs": "^1.0.3", "@breejs/later": "^4.1.0",
"iconv-lite": "^0.6.2", "espresso-iisojs": "^1.0.4",
"ipaddr.js": "^2.0.0", "iconv-lite": "^0.6.3",
"ipaddr.js": "^2.0.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"koa": "^2.13.1", "koa": "^2.13.4",
"koa-bodyparser": "^4.3.0", "koa-bodyparser": "^4.3.0",
"koa-compress": "^5.0.1", "koa-compress": "^5.1.0",
"koa-jwt": "^4.0.0", "koa-jwt": "^4.0.3",
"koa-router": "^10.0.0", "koa-router": "^10.1.1",
"koa-static": "^5.0.0", "koa-static": "^5.0.0",
"later": "^1.2.0", "mongodb": "^4.1.3",
"mongodb": "^3.6.3", "parsimmon": "^1.18.0",
"parsimmon": "^1.16.0",
"seedrandom": "^3.0.5" "seedrandom": "^3.0.5"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^17.1.0", "@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^11.2.0", "@rollup/plugin-node-resolve": "^13.0.6",
"@rollup/plugin-typescript": "^8.2.0", "@rollup/plugin-typescript": "^8.3.0",
"@types/codemirror": "0.0.108", "@types/codemirror": "^5.60.5",
"@types/jsonwebtoken": "^8.5.0", "@types/jsonwebtoken": "^8.5.5",
"@types/koa": "^2.13.0", "@types/koa": "^2.13.4",
"@types/koa-compress": "^4.0.1", "@types/koa-compress": "^4.0.3",
"@types/koa-router": "^7.4.1", "@types/koa-router": "^7.4.4",
"@types/koa-static": "^4.0.1", "@types/koa-static": "^4.0.2",
"@types/mithril": "^2.0.6", "@types/mithril": "^2.0.8",
"@types/mongodb": "^3.6.8", "@types/node": "^16.11.6",
"@types/node": "^14.14.31",
"@types/parsimmon": "^1.10.6", "@types/parsimmon": "^1.10.6",
"@types/seedrandom": "^2.4.28", "@types/seedrandom": "^3.0.1",
"@typescript-eslint/eslint-plugin": "^4.15.2", "@typescript-eslint/eslint-plugin": "^5.2.0",
"@typescript-eslint/parser": "^4.15.2", "@typescript-eslint/parser": "^5.2.0",
"ava": "^3.15.0", "ava": "^3.15.0",
"codemirror": "^5.59.4", "codemirror": "^5.63.3",
"cssnano": "^4.1.10", "cssnano": "^5.0.8",
"eslint": "^7.20.0", "eslint": "^8.1.0",
"eslint-config-prettier": "^8.1.0", "eslint-config-prettier": "^8.3.0",
"esm": "^3.2.25", "esm": "^3.2.25",
"mithril": "^2.0.4", "mithril": "^2.0.4",
"postcss": "^8.2.6", "postcss": "^8.3.11",
"postcss-import": "^14.0.0", "postcss-import": "^14.0.2",
"postcss-preset-env": "^6.7.0", "postcss-preset-env": "^6.7.0",
"prettier": "^2.2.1", "prettier": "^2.4.1",
"rollup": "^2.39.1", "rollup": "^2.58.3",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"svgo": "^2.1.0", "sql.js": "^1.6.2",
"ts-node": "^9.1.1", "svgo": "^2.7.0",
"typescript": "^4.1.5", "ts-node": "^10.4.0",
"yaml": "^1.10.0" "typescript": "^4.4.4",
"yaml": "^1.10.2"
}, },
"engines": { "engines": {
"node": ">=12.13.0" "node": ">=12.13.0"
+118
View File
@@ -0,0 +1,118 @@
import ava, { TestInterface } from "ava";
import { minimize, unionDiff } from "../lib/common/boolean-expression";
import { parse, stringify } from "../lib/common/expression-parser";
import initSqlJs from "sql.js";
const test = ava as TestInterface<{
query: (filter: string) => Promise<Set<number>>;
}>;
const STRING_VALUES = [null, "", "a", "ab", "ab10", "ab-10"];
const DECIMAL_VALUES = [null, 0, -10, 10];
test.before(async (t) => {
const sql = await initSqlJs();
const db = new sql.Database();
t.context.query = async (filter: string): Promise<Set<number>> => {
const res = db.exec(`SELECT id FROM test WHERE ${filter}`);
if (!res.length) return new Set();
return new Set(res[0].values.flat());
};
db.run(
"CREATE TABLE test (id INTEGER PRIMARY KEY, string STRING, decimal DECIMAL(4,2))"
);
const stmt = db.prepare("INSERT INTO test (string, decimal) VALUES (?, ?)");
const count = STRING_VALUES.length * DECIMAL_VALUES.length;
for (let i = 0; i < count; ++i) {
const str = i % STRING_VALUES.length;
const dec = Math.trunc(i / STRING_VALUES.length) % DECIMAL_VALUES.length;
stmt.run([STRING_VALUES[str], DECIMAL_VALUES[dec]]);
}
stmt.free();
});
function setsEqual(set1: Set<number>, set2: Set<number>): boolean {
if (set1.size !== set2.size) return false;
for (const s of set1) if (!set2.has(s)) return false;
return true;
}
function getPermutations(...arrs: any[][]): any[][] {
const count = arrs.reduce((total, arr) => total * arr.length, 1);
const res = [];
for (let i = 0; i < count; ++i) {
let j = i;
const row = [];
for (const arr of arrs) {
const v = arr[j % arr.length];
j = Math.trunc(j / arr.length);
row.push(v);
}
res.push(row);
}
return res;
}
test("minimize", async (t) => {
const cases: string[] = [];
cases.push("null");
cases.push("false");
cases.push("true");
cases.push("string");
cases.push("(string + decimal) IS NULL");
cases.push("(string + decimal) = NULL");
cases.push("COALESCE(string, decimal) = 0");
for (const [s1, s2, s3, op1, op2] of getPermutations(
STRING_VALUES.filter((s) => s),
STRING_VALUES.filter((s) => s),
STRING_VALUES.filter((s) => s),
[">", "=", "<"],
["<>", ">="]
)) {
cases.push(
`string ${op1} "${s1}" OR string ='${s2}' OR NOT string ${op2} '${s3}'`
);
}
for (const [s1, s2] of getPermutations(
STRING_VALUES.filter((s) => s),
STRING_VALUES.filter((s) => s)
))
cases.push(`string > "${s1}" AND string < '${s2}'`);
for (const c of cases) {
const res1 = await t.context.query(c);
const min = stringify(minimize(parse(c), true));
const res2 = await t.context.query(min);
t.true(setsEqual(res1, res2));
}
});
test("unionDiff", async (t) => {
const cases = [
"true",
"decimal > 0",
"decimal > 10",
"UPPER(string || decimal) LIKE 'AB10'",
"COALESCE(string, decimal) = 0",
];
for (const [c1, c2] of getPermutations(cases, cases)) {
const res1 = await t.context.query(c1);
const res2 = await t.context.query(c2);
const [union, diff] = unionDiff(parse(c1), parse(c2));
const res3 = await t.context.query(stringify(union));
const res4 = await t.context.query(stringify(diff));
const unionSet = new Set([...res1, ...res2]);
const diffSet = new Set(Array.from(res2).filter((r) => !res1.has(r)));
t.true(setsEqual(res3, unionSet));
t.true(setsEqual(res4, diffSet));
}
});
+10 -2
View File
@@ -49,8 +49,16 @@ ava("convertOldPrecondition", (t) => {
]; ];
t.plan(tests.length + 2 * shouldFailTests.length); t.plan(tests.length + 2 * shouldFailTests.length);
for (const test of tests) for (const test of tests) {
t.is(stringify(mongodbFunctions.convertOldPrecondition(test[0])), test[1]); t.is(
stringify(
mongodbFunctions.convertOldPrecondition(
test[0] as Record<string, unknown>
)
),
test[1]
);
}
for (const test of shouldFailTests) { for (const test of shouldFailTests) {
const func = (): void => { const func = (): void => {
+19
View File
@@ -1,6 +1,25 @@
import ava from "ava"; import ava from "ava";
import Path from "../lib/common/path"; import Path from "../lib/common/path";
ava("parse", (t) => {
t.throws(() => Path.parse("."));
t.throws(() => Path.parse("a "));
t.throws(() => Path.parse(".a"));
t.throws(() => Path.parse("a."));
t.throws(() => Path.parse("a..b"));
t.throws(() => Path.parse("b*"));
t.throws(() => Path.parse("*b"));
t.throws(() => Path.parse("a.b c.d"));
t.throws(() => Path.parse("a["));
t.throws(() => Path.parse("a[b"));
t.throws(() => Path.parse("a[b:"));
t.throws(() => Path.parse('a[b:"waef]'));
t.notThrows(() => Path.parse("[]"));
t.notThrows(() => Path.parse("[a:]"));
t.notThrows(() => Path.parse("*"));
t.notThrows(() => Path.parse(""));
});
ava("toString", (t) => { ava("toString", (t) => {
const path1 = Path.parse('abc.[ abc : 123 , 123 : abc , 123: " abc "].123'); const path1 = Path.parse('abc.[ abc : 123 , 123 : abc , 123: " abc "].123');
const path2 = Path.parse('abc.[123: " abc ",abc:123,123:abc].123'); const path2 = Path.parse('abc.[123: " abc ",abc:123,123:abc].123');
+3 -3
View File
@@ -1,9 +1,9 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": ["esnext", "dom"], "lib": ["ES2019", "dom"],
"module": "esnext", "module": "ES2020",
"moduleResolution": "node", "moduleResolution": "node",
"target": "esnext", "target": "ES2019",
"resolveJsonModule": true, "resolveJsonModule": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"alwaysStrict": true, "alwaysStrict": true,
+1 -1
View File
@@ -69,7 +69,7 @@ function pagify(pageName, page): RouteResolver {
render: () => { render: () => {
const lastRenderTimestamp = Date.now(); const lastRenderTimestamp = Date.now();
let p; let p;
if (state && state.error) p = m(errorPage.component, state); if (state?.error) p = m(errorPage.component, state);
else p = m(contextifyComponent(page.component), state); else p = m(contextifyComponent(page.component), state);
const attrs = {}; const attrs = {};
attrs["page"] = pageName; attrs["page"] = pageName;
+24 -12
View File
@@ -23,13 +23,13 @@ type AutocompleteCallback = (
) => void; ) => void;
export default class Autocomplete { export default class Autocomplete {
private callback: AutocompleteCallback; private declare callback: AutocompleteCallback;
private element: HTMLInputElement; private declare element: HTMLInputElement;
private hideTimeout: NodeJS.Timeout; private declare hideTimeout: NodeJS.Timeout;
private visible: boolean; private declare visible: boolean;
private default: string; private declare default: string;
private selection: number; private declare selection: number;
private container: HTMLElement; private declare container: HTMLElement;
public constructor(className: string, callback: AutocompleteCallback) { public constructor(className: string, callback: AutocompleteCallback) {
this.callback = callback; this.callback = callback;
@@ -51,11 +51,8 @@ export default class Autocomplete {
el.addEventListener("focus", () => { el.addEventListener("focus", () => {
this.element = el; this.element = el;
const domRect = el.getBoundingClientRect();
this.container.style.left = `${domRect.left + window.pageXOffset}px`;
this.container.style.width = `${domRect.width}px`;
this.container.style.top = `${domRect.bottom + window.pageYOffset}px`;
this.update(); this.update();
this.reposition();
}); });
el.addEventListener("blur", () => { el.addEventListener("blur", () => {
@@ -71,7 +68,8 @@ export default class Autocomplete {
} else if (e.key === "Enter") { } else if (e.key === "Enter") {
if (this.default != null) { if (this.default != null) {
el.value = this.default; el.value = this.default;
e.preventDefault(); e.stopImmediatePropagation();
el.dispatchEvent(new InputEvent("input"));
this.update(); this.update();
} }
} else if (e.key === "ArrowDown") { } else if (e.key === "ArrowDown") {
@@ -93,6 +91,19 @@ export default class Autocomplete {
}); });
} }
public reposition(): void {
if (!this.element) return;
const domRect = this.element.getBoundingClientRect();
if (!domRect.width) {
// Element has been removed
if (this.visible) this.hide();
return;
}
this.container.style.left = `${domRect.left + window.pageXOffset}px`;
this.container.style.width = `${domRect.width}px`;
this.container.style.top = `${domRect.bottom + window.pageYOffset}px`;
}
private hide(): void { private hide(): void {
this.container.style.opacity = "0"; this.container.style.opacity = "0";
this.visible = false; this.visible = false;
@@ -159,6 +170,7 @@ export default class Autocomplete {
e.addEventListener("mousedown", (ev) => { e.addEventListener("mousedown", (ev) => {
ev.preventDefault(); ev.preventDefault();
el.value = suggestion.value; el.value = suggestion.value;
el.dispatchEvent(new InputEvent("input"));
if (this.element === el) this.update(); if (this.element === el) this.update();
}); });
this.container.appendChild(e); this.container.appendChild(e);
+11 -5
View File
@@ -17,17 +17,23 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { VnodeDOM, ClosureComponent, Component } from "mithril"; import { VnodeDOM, ClosureComponent } from "mithril";
import * as notifications from "./notifications"; import * as notifications from "./notifications";
import { changePassword } from "./store"; import { changePassword } from "./store";
import { m } from "./components"; import { m } from "./components";
const component: ClosureComponent = (): Component => { interface Attrs {
noAuth?: boolean;
username?: string;
onPasswordChange: () => void;
}
const component: ClosureComponent<Attrs> = () => {
return { return {
view: (vnode) => { view: (vnode) => {
const onPasswordChange = vnode.attrs["onPasswordChange"]; const onPasswordChange = vnode.attrs.onPasswordChange;
const enforceAuth = !vnode.attrs["noAuth"]; const enforceAuth = !vnode.attrs.noAuth;
const username = vnode.attrs["username"]; const username = vnode.attrs.username;
if (username) vnode.state["username"] = username; if (username) vnode.state["username"] = username;
+24 -14
View File
@@ -17,44 +17,54 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { ClosureComponent, Component } from "mithril"; import { Editor } from "codemirror";
import { ClosureComponent } from "mithril";
import { m } from "./components"; import { m } from "./components";
import { codeMirror } from "./dynamic-loader"; import { codeMirror } from "./dynamic-loader";
const component: ClosureComponent = (): Component => { interface Attrs {
id: string;
value: string;
mode: string;
readOnly?: boolean;
focus?: boolean;
onSubmit?: (dom: Element) => void;
onChange?: (value: string) => void;
onReady?: (editor: Editor) => void;
}
const component: ClosureComponent<Attrs> = () => {
return { return {
view: (vnode) => { view: (vnode) => {
return m("textarea", { return m("textarea", {
name: vnode.attrs["id"], name: vnode.attrs.id,
value: vnode.attrs["value"], value: vnode.attrs.value,
oncreate: (_vnode) => { oncreate: (_vnode) => {
const editor = codeMirror.fromTextArea( const editor = codeMirror.fromTextArea(
_vnode.dom as HTMLTextAreaElement, _vnode.dom as HTMLTextAreaElement,
{ {
mode: vnode.attrs["mode"], mode: vnode.attrs.mode,
lineNumbers: true, lineNumbers: true,
readOnly: vnode.attrs["readOnly"], readOnly: vnode.attrs.readOnly,
extraKeys: { extraKeys: {
"Ctrl-Enter": () => { "Ctrl-Enter": () => {
if (vnode.attrs["onSubmit"]) if (vnode.attrs.onSubmit) vnode.attrs.onSubmit(_vnode.dom);
vnode.attrs["onSubmit"](_vnode.dom);
}, },
"Cmd-Enter": () => { "Cmd-Enter": () => {
if (vnode.attrs["onSubmit"]) if (vnode.attrs.onSubmit) vnode.attrs.onSubmit(_vnode.dom);
vnode.attrs["onSubmit"](_vnode.dom);
}, },
}, },
} }
); );
if (vnode.attrs["onChange"]) { if (vnode.attrs.onChange) {
editor.on("change", (e) => { editor.on("change", (e) => {
vnode.attrs["onChange"](e.getValue()); vnode.attrs.onChange(e.getValue());
}); });
} }
if (vnode.attrs["focus"]) editor.focus(); if (vnode.attrs.focus) editor.focus();
if (vnode.attrs["onReady"]) vnode.attrs["onReady"](editor); if (vnode.attrs.onReady) vnode.attrs.onReady(editor);
}, },
}); });
}, },
+12 -5
View File
@@ -22,7 +22,7 @@ import m, {
Attributes, Attributes,
Children, Children,
ComponentTypes, ComponentTypes,
Lifecycle, CommonAttributes,
ClosureComponent, ClosureComponent,
Vnode, Vnode,
} from "mithril"; } from "mithril";
@@ -81,7 +81,7 @@ interface MC extends Static {
<Attrs, State>( <Attrs, State>(
ctx: Attributes, ctx: Attributes,
component: ComponentTypes<Attrs, State>, component: ComponentTypes<Attrs, State>,
attributes: Attrs & Lifecycle<Attrs, State> & { key?: string | number }, attributes: Attrs & CommonAttributes<Attrs, State>,
...args: Children[] ...args: Children[]
): Vnode<Attrs, State>; ): Vnode<Attrs, State>;
}; };
@@ -116,7 +116,7 @@ function applyContext(vnode, parentContext): void {
vnodeContext.set(vnode, vc); vnodeContext.set(vnode, vc);
vnode.attrs = Object.assign({}, vc, vnode.attrs); vnode.attrs = Object.assign({}, vc, vnode.attrs);
} }
if (vnode.children && vnode.children.length) if (vnode.children?.length)
for (const c of vnode.children) applyContext(c, vc); for (const c of vnode.children) applyContext(c, vc);
} }
} }
@@ -133,7 +133,7 @@ export function contextifyComponent(component: ComponentTypes): ComponentTypes {
applyContext(res, context); applyContext(res, context);
return res; return res;
}; };
} else if (!component.prototype || !component.prototype.view) { } else if (!component.prototype?.view) {
c = (initialNode) => { c = (initialNode) => {
const state = (component as ClosureComponent)(initialNode); const state = (component as ClosureComponent)(initialNode);
const view = state.view; const view = state.view;
@@ -144,7 +144,14 @@ export function contextifyComponent(component: ComponentTypes): ComponentTypes {
applyContext(res, context); applyContext(res, context);
return res; return res;
} catch (err) { } catch (err) {
return m("p.error", { title: err.message }, "Error!"); return m(
"p.error",
{
title: "Click to print stack trace to console",
onclick: () => console.error(err),
},
"Error!"
);
} }
}; };
return state; return state;
+3 -2
View File
@@ -23,6 +23,7 @@ import * as taskQueue from "../task-queue";
import { parse } from "../../lib/common/expression-parser"; import { parse } from "../../lib/common/expression-parser";
import memoize from "../../lib/common/memoize"; import memoize from "../../lib/common/memoize";
import { getIcon } from "../icons"; import { getIcon } from "../icons";
import { evaluateExpression } from "../store";
const memoizedParse = memoize(parse); const memoizedParse = memoize(parse);
@@ -55,7 +56,7 @@ const component: ClosureComponent = (): Component => {
view: (vnode) => { view: (vnode) => {
const device = vnode.attrs["device"]; const device = vnode.attrs["device"];
const limit = vnode.attrs["limit"] > 0 ? vnode.attrs["limit"] : 100; const limit = evaluateExpression(vnode.attrs["limit"], device) || 100;
const search = m("input", { const search = m("input", {
type: "text", type: "text",
@@ -85,7 +86,7 @@ const component: ClosureComponent = (): Component => {
let c = 0; let c = 0;
for (const k of keys) { for (const k of keys) {
const p = device[k]; const p = device[k];
const str = p.value && p.value[0] ? `${k} ${p.value[0]}` : k; const str = p.value?.[0] ? `${k} ${p.value[0]}` : k;
if (re && !re.test(str)) continue; if (re && !re.test(str)) continue;
++c; ++c;
if (count < limit) filteredKeys.push(k); if (count < limit) filteredKeys.push(k);
+59 -12
View File
@@ -17,29 +17,76 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { ClosureComponent, Component } from "mithril"; import { Attributes, ClosureComponent, Component } from "mithril";
import { map } from "../../lib/common/expression-parser";
import memoize from "../../lib/common/memoize";
import { Expression } from "../../lib/types";
import { m } from "../components"; import { m } from "../components";
import * as store from "../store"; import { evaluateExpression, getTimestamp } from "../store";
const evaluateAttributes = memoize(
(
attrs: Record<string, Expression>,
obj: Record<string, unknown>,
now: number // eslint-disable-line @typescript-eslint/no-unused-vars
): Attributes => {
const res: Attributes = {};
for (const [k, v] of Object.entries(attrs)) {
const vv = map(v, (e) => {
if (
Array.isArray(e) &&
e[0] === "FUNC" &&
e[1] === "ENCODEURICOMPONENT"
) {
const a = evaluateExpression(e[2], obj);
if (a == null) return null;
return encodeURIComponent(a as string);
}
return e;
});
res[k] = evaluateExpression(vv, obj);
}
return res;
}
);
const component: ClosureComponent = (): Component => { const component: ClosureComponent = (): Component => {
return { return {
view: (vnode) => { view: (vnode) => {
if (vnode.attrs["filter"]) { const device = vnode.attrs["device"];
if ( if ("filter" in vnode.attrs) {
!store.evaluateExpression( if (!evaluateExpression(vnode.attrs["filter"], device || {}))
vnode.attrs["filter"],
vnode.attrs["device"]
)
)
return null; return null;
} }
const children = Object.values(vnode.attrs["components"]).map((c) => { const children = Object.values(vnode.attrs["components"]).map((c) => {
if (Array.isArray(c)) c = evaluateExpression(c, device || {});
if (typeof c !== "object") return `${c}`; if (typeof c !== "object") return `${c}`;
return m(c["type"], c); const type = evaluateExpression(c["type"], device || {});
if (!type) return null;
return m(type as string, c);
}); });
if (vnode.attrs["element"]) return m(vnode.attrs["element"], children);
else return children; let el = vnode.attrs["element"];
if (el == null) return children;
let attrs: Attributes;
if (Array.isArray(el)) {
el = evaluateExpression(el, device || {});
} else if (typeof el === "object") {
if (el["attributes"] != null) {
attrs = evaluateAttributes(
el["attributes"],
device || {},
getTimestamp()
);
}
el = evaluateExpression(el["tag"], device || {});
}
return m(el, attrs, children);
}, },
}; };
}; };
+7 -3
View File
@@ -19,18 +19,22 @@
import { ClosureComponent, Component } from "mithril"; import { ClosureComponent, Component } from "mithril";
import { m } from "../components"; import { m } from "../components";
import { evaluateExpression } from "../store";
const component: ClosureComponent = (): Component => { const component: ClosureComponent = (): Component => {
return { return {
view: (vnode) => { view: (vnode) => {
let deviceId; let deviceId;
if (vnode.attrs["device"]) const device = vnode.attrs["device"];
deviceId = vnode.attrs["device"]["DeviceID.ID"].value[0]; if (device) deviceId = device["DeviceID.ID"].value[0];
const children = Object.values(vnode.attrs["components"]).map((c) => { const children = Object.values(vnode.attrs["components"]).map((c) => {
if (Array.isArray(c)) c = evaluateExpression(c, device || {});
if (typeof c !== "object") return `${c}`; if (typeof c !== "object") return `${c}`;
const type = evaluateExpression(c["type"], device || {});
if (!type) return null;
const attrs = Object.assign({}, vnode.attrs, c); const attrs = Object.assign({}, vnode.attrs, c);
return m(attrs["type"], attrs); return m(type as string, attrs);
}); });
if (deviceId) { if (deviceId) {
return m( return m(
+4 -3
View File
@@ -20,7 +20,7 @@
import { ClosureComponent, Component } from "mithril"; import { ClosureComponent, Component } from "mithril";
import { m } from "../components"; import { m } from "../components";
import config from "../config"; import config from "../config";
import * as store from "../store"; import { evaluateExpression } from "../store";
const CHARTS = config.ui.overview.charts; const CHARTS = config.ui.overview.charts;
@@ -28,11 +28,12 @@ const component: ClosureComponent = (): Component => {
return { return {
view: (vnode) => { view: (vnode) => {
const device = vnode.attrs["device"]; const device = vnode.attrs["device"];
const chart = CHARTS[vnode.attrs["chart"]] as Record<string, unknown>; const chartName = evaluateExpression(vnode.attrs["chart"], device || {});
const chart = CHARTS[chartName as string] as Record<string, unknown>;
if (!chart) return null; if (!chart) return null;
for (const slice of Object.values(chart.slices)) { for (const slice of Object.values(chart.slices)) {
const filter = slice["filter"]; const filter = slice["filter"];
if (store.evaluateExpression(filter, device)) { if (evaluateExpression(filter, device || {})) {
const dot = m( const dot = m(
"svg", "svg",
{ {
+3 -1
View File
@@ -19,6 +19,7 @@
import { ClosureComponent, Component, VnodeDOM } from "mithril"; import { ClosureComponent, Component, VnodeDOM } from "mithril";
import { m } from "../components"; import { m } from "../components";
import { evaluateExpression } from "../store";
const component: ClosureComponent = (): Component => { const component: ClosureComponent = (): Component => {
return { return {
@@ -26,12 +27,13 @@ const component: ClosureComponent = (): Component => {
const device = vnode.attrs["device"]; const device = vnode.attrs["device"];
const rows = Object.values(vnode.attrs["parameters"]).map((parameter) => { const rows = Object.values(vnode.attrs["parameters"]).map((parameter) => {
const type = evaluateExpression(parameter["type"], device);
const p = m.context( const p = m.context(
{ {
device: device, device: device,
parameter: parameter["parameter"], parameter: parameter["parameter"],
}, },
parameter["type"] || "parameter", (type as string) || "parameter",
parameter parameter
); );
+7 -6
View File
@@ -20,7 +20,7 @@
import { ClosureComponent, Component } from "mithril"; import { ClosureComponent, Component } from "mithril";
import { m } from "../components"; import { m } from "../components";
import * as taskQueue from "../task-queue"; import * as taskQueue from "../task-queue";
import * as store from "../store"; import { evaluateExpression } from "../store";
import * as expressionParser from "../../lib/common/expression-parser"; import * as expressionParser from "../../lib/common/expression-parser";
import { getIcon } from "../icons"; import { getIcon } from "../icons";
@@ -35,7 +35,7 @@ const component: ClosureComponent = (): Component => {
}, },
view: (vnode) => { view: (vnode) => {
const device = vnode.attrs["device"]; const device = vnode.attrs["device"];
const object = store.evaluateExpression(vnode.state["object"], device); const object = evaluateExpression(vnode.state["object"], device);
const parameters = vnode.state["parameters"]; const parameters = vnode.state["parameters"];
if (typeof object !== "string" || !device[object]) return null; if (typeof object !== "string" || !device[object]) return null;
@@ -56,8 +56,7 @@ const component: ClosureComponent = (): Component => {
const rows = []; const rows = [];
for (const i of instances) { for (const i of instances) {
let filter = let filter = "filter" in vnode.attrs ? vnode.attrs["filter"] : true;
vnode.attrs["filter"] != null ? vnode.attrs["filter"] : true;
filter = expressionParser.map(filter, (e) => { filter = expressionParser.map(filter, (e) => {
if (Array.isArray(e) && e[0] === "PARAM") if (Array.isArray(e) && e[0] === "PARAM")
@@ -65,7 +64,7 @@ const component: ClosureComponent = (): Component => {
return e; return e;
}); });
if (!store.evaluateExpression(filter, device)) continue; if (!evaluateExpression(filter, device)) continue;
const row = parameters.map((p) => { const row = parameters.map((p) => {
const param = expressionParser.map(p.parameter, (e) => { const param = expressionParser.map(p.parameter, (e) => {
@@ -147,7 +146,9 @@ const component: ClosureComponent = (): Component => {
} }
let label; let label;
if (vnode.attrs["label"]) label = m("h2", vnode.attrs["label"]);
const l = evaluateExpression(vnode.attrs["label"], device);
if (l != null) label = m("h2", l);
return [ return [
label, label,
+6 -6
View File
@@ -38,7 +38,7 @@ const evaluateParam = memoize((exp, obj, now: number) => {
) { ) {
let v = null; let v = null;
const p = obj[e[i][1]]; const p = obj[e[i][1]];
if (p && p.value) { if (p?.value) {
v = p.value[0]; v = p.value[0];
timestamp = Math.min(timestamp, p.valueTimestamp); timestamp = Math.min(timestamp, p.valueTimestamp);
} }
@@ -58,7 +58,7 @@ const evaluateParam = memoize((exp, obj, now: number) => {
value = exp; value = exp;
} else if (exp[0] === "PARAM") { } else if (exp[0] === "PARAM") {
const p = obj[exp[1]]; const p = obj[exp[1]];
if (p && p.value) { if (p?.value) {
timestamp = p.valueTimestamp; timestamp = p.valueTimestamp;
value = p.value[0]; value = p.value[0];
parameter = exp[1]; parameter = exp[1];
@@ -77,14 +77,14 @@ const component: ClosureComponent = (): Component => {
const { value, timestamp, parameter } = evaluateParam( const { value, timestamp, parameter } = evaluateParam(
vnode.attrs["parameter"], vnode.attrs["parameter"],
vnode.attrs["device"], device,
store.getTimestamp() store.getTimestamp() + store.getClockSkew()
); );
if (value == null) return null; if (value == null) return null;
let edit; let edit;
if (device[parameter] && device[parameter].writable) { if (device[parameter]?.writable) {
edit = m( edit = m(
"button", "button",
{ {
@@ -117,7 +117,7 @@ const component: ClosureComponent = (): Component => {
e.redraw = false; e.redraw = false;
// Don't update any child element // Don't update any child element
if (e.target === (el as VnodeDOM).dom) { if (e.target === (el as VnodeDOM).dom) {
const now = Date.now(); const now = Date.now() + store.getClockSkew();
const localeString = new Date(timestamp).toLocaleString(); const localeString = new Date(timestamp).toLocaleString();
e.target.title = `${localeString} (${timeAgo(now - timestamp)})`; e.target.title = `${localeString} (${timeAgo(now - timestamp)})`;
} }
+2 -1
View File
@@ -29,7 +29,8 @@ const component: ClosureComponent = (): Component => {
view: (vnode) => { view: (vnode) => {
const device = vnode.attrs["device"]; const device = vnode.attrs["device"];
let writable = true; let writable = true;
if (vnode.attrs["writable"] != null) writable = vnode.attrs["writable"]; if ("writable" in vnode.attrs)
writable = !!store.evaluateExpression(vnode.attrs["writable"], device);
const tags = []; const tags = [];
for (const p of Object.keys(device)) for (const p of Object.keys(device))
+3 -3
View File
@@ -45,7 +45,7 @@ export function flattenConfig(config: Record<string, unknown>): any {
// Order keys such that nested objects come last // Order keys such that nested objects come last
function orderKeys(config: any): number { function orderKeys(config: any): number {
let res = 1; let res = 1;
if (typeof config !== "object") return res; if (config == null || typeof config !== "object") return res;
if (Array.isArray(config)) { if (Array.isArray(config)) {
for (const c of config) res += orderKeys(c); for (const c of config) res += orderKeys(c);
return res; return res;
@@ -79,7 +79,7 @@ export function structureConfig(config: Config[]): any {
let ref = _config; let ref = _config;
while (keys.length > 1) { while (keys.length > 1) {
const k = keys.shift(); const k = keys.shift();
if (typeof ref[k] !== "object") ref[k] = {}; if (ref[k] == null || typeof ref[k] !== "object") ref[k] = {};
ref = ref[k]; ref = ref[k];
} }
ref[keys[0]] = c.value; ref[keys[0]] = c.value;
@@ -137,7 +137,7 @@ export function diffConfig(
}; };
for (const [k, v] of Object.entries(target)) for (const [k, v] of Object.entries(target))
if (!current[k] || current[k] !== v) diff.add.push({ _id: k, value: v }); if (v && current[k] !== v) diff.add.push({ _id: k, value: v });
for (const k of Object.keys(current)) if (!target[k]) diff.remove.push(k); for (const k of Object.keys(current)) if (!target[k]) diff.remove.push(k);
+4 -4
View File
@@ -154,7 +154,7 @@ function renderTable(confsResponse, searchString): Children {
{ {
base: conf, base: conf,
actionHandler: (action, object) => { actionHandler: (action, object) => {
return new Promise((resolve) => { return new Promise<void>((resolve) => {
putActionHandler(action, object, false) putActionHandler(action, object, false)
.then((errors) => { .then((errors) => {
const ErrorList = errors ? Object.values(errors) : []; const ErrorList = errors ? Object.values(errors) : [];
@@ -164,11 +164,11 @@ function renderTable(confsResponse, searchString): Children {
} else { } else {
overlay.close(cb); overlay.close(cb);
} }
resolve(null); resolve();
}) })
.catch((err) => { .catch((err) => {
notifications.push("error", err.message); notifications.push("error", err.message);
resolve(null); resolve();
}); });
}); });
}, },
@@ -322,7 +322,7 @@ export const component: ClosureComponent = (): Component => {
uiConfigComponent, uiConfigComponent,
Object.assign( Object.assign(
{ {
onUpdate: (errs: string[]) => { onUpdate: (errs: Record<string, string>) => {
const errors = errs ? Object.values(errs) : []; const errors = errs ? Object.values(errs) : [];
if (errors.length) { if (errors.length) {
for (const err of errors) for (const err of errors)
+17
View File
@@ -342,6 +342,13 @@ body {
flex-grow: 1; flex-grow: 1;
margin: 10px; margin: 10px;
min-height: 250px; min-height: 250px;
/* Workaround for Chrome ignoring bottom margin when flex container overflows */
&::after {
content: "";
display: block;
padding-bottom: 1px;
}
} }
table.table { table.table {
@@ -780,3 +787,13 @@ span.parameter-value {
background-size: 100% 100%; background-size: 100% 100%;
} }
} }
.progress {
background-color: #eee;
overflow: hidden;
height: 3px;
& > .progress-bar {
height: 100%;
background-color: #b12d5c;
}
}
+62
View File
@@ -0,0 +1,62 @@
/**
* Copyright 2013-2021 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import m, { ClosureComponent, Component, Vnode } from "mithril";
const elements: Map<string, Vnode> = new Map();
// Source: https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
function hash(str: string): number {
let res = 0;
for (let i = 0; i < str.length; ++i) {
const c = str.charCodeAt(i);
res = (res << 5) - res + c;
res |= 0;
}
return res;
}
export function getDatalistId(options: string[]): string {
const id = "datalist" + options.reduce((acc, cur) => acc ^ hash(cur), 0);
if (!elements.has(id)) {
const n = m(
"datalist",
{ id },
options.map((o) => m("option", { value: o }))
);
elements.set(id, n);
}
return id;
}
const component: ClosureComponent = (): Component => {
return {
view: () => {
return [...elements.values()];
},
onupdate: () => {
for (const id of elements.keys()) {
const used = document.querySelector(`[list='${id}']`);
if (!used) elements.delete(id);
}
},
};
};
export default component;
+9 -7
View File
@@ -268,17 +268,18 @@ export const component: ClosureComponent = (): Component => {
} }
function onSortChange(sortedAttrs): void { function onSortChange(sortedAttrs): void {
const _sort = Object.assign({}, sort); let _sort = Object.assign({}, sort);
for (const [index, direction] of Object.entries(sortedAttrs)) { for (const [index, direction] of Object.entries(sortedAttrs)) {
const param = memoizedGetSortable(attributes[index].parameter); const param = memoizedGetSortable(attributes[index].parameter);
if (param) { if (param) {
// Changing the priority of columns // Changing the priority of columns
delete _sort[param]; delete _sort[param];
_sort[param] = direction; if (direction) _sort = Object.assign({ [param]: direction }, _sort);
} }
} }
const ops = { sort: JSON.stringify(_sort) }; const ops = {};
if (Object.keys(_sort).length) ops["sort"] = JSON.stringify(_sort);
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
m.route.set("/devices", ops); m.route.set("/devices", ops);
} }
@@ -328,10 +329,11 @@ export const component: ClosureComponent = (): Component => {
if (window.authorizer.hasAccess("devices", 3)) if (window.authorizer.hasAccess("devices", 3))
attrs["actionsCallback"] = renderActions; attrs["actionsCallback"] = renderActions;
const filterAttrs = {}; const filterAttrs = {
filterAttrs["resource"] = "devices"; resource: "devices",
filterAttrs["filter"] = vnode.attrs["filter"]; filter: vnode.attrs["filter"],
filterAttrs["onChange"] = onFilterChanged; onChange: onFilterChanged,
};
return [ return [
m("h1", "Listing devices"), m("h1", "Listing devices"),
+3
View File
@@ -121,12 +121,15 @@ function renderStagingDownload(task: StageTask): Children {
if (productClass) productClass = decodeURIComponent(productClass); if (productClass) productClass = decodeURIComponent(productClass);
const typesList = [ const typesList = [
...new Set([
"", "",
"1 Firmware Upgrade Image", "1 Firmware Upgrade Image",
"2 Web Content", "2 Web Content",
"3 Vendor Configuration File", "3 Vendor Configuration File",
"4 Tone File", "4 Tone File",
"5 Ringer File", "5 Ringer File",
...files.value.map((f) => f["metadata.fileType"]).filter((f) => f),
]),
].map((t) => ].map((t) =>
m( m(
"option", "option",
+10 -7
View File
@@ -107,14 +107,16 @@ export const component: ClosureComponent = (): Component => {
} }
function onSortChange(sortAttrs): void { function onSortChange(sortAttrs): void {
const _sort = Object.assign({}, sort); let _sort = Object.assign({}, sort);
for (const [index, direction] of Object.entries(sortAttrs)) { for (const [index, direction] of Object.entries(sortAttrs)) {
// Changing the priority of columns // Changing the priority of columns
delete _sort[attributes[index].id]; delete _sort[attributes[index].id];
_sort[attributes[index].id] = direction; if (direction)
_sort = Object.assign({ [attributes[index].id]: direction }, _sort);
} }
const ops = { sort: JSON.stringify(_sort) }; const ops = {};
if (Object.keys(_sort).length) ops["sort"] = JSON.stringify(_sort);
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
m.route.set("/faults", ops); m.route.set("/faults", ops);
} }
@@ -200,10 +202,11 @@ export const component: ClosureComponent = (): Component => {
}; };
} }
const filterAttrs = {}; const filterAttrs = {
filterAttrs["resource"] = "faults"; resource: "faults",
filterAttrs["filter"] = vnode.attrs["filter"]; filter: vnode.attrs["filter"],
filterAttrs["onChange"] = onFilterChanged; onChange: onFilterChanged,
};
return [ return [
m("h1", "Listing faults"), m("h1", "Listing faults"),
+93 -76
View File
@@ -45,7 +45,6 @@ const attributes: {
{ {
id: "metadata.fileType", id: "metadata.fileType",
label: "Type", label: "Type",
type: "combo",
options: [ options: [
"1 Firmware Upgrade Image", "1 Firmware Upgrade Image",
"2 Web Content", "2 Web Content",
@@ -74,58 +73,32 @@ const unpackSmartQuery = memoize((query) => {
}); });
}); });
interface ValidationErrors { function upload(
[prop: string]: string; file: File,
} headers: Record<string, string>,
abortSignal?: AbortSignal,
function putActionHandler(action, _object): Promise<ValidationErrors> { progressListener?: (e: ProgressEvent) => void
return new Promise((resolve, reject) => { ): Promise<void> {
const object = Object.assign({}, _object); headers = Object.assign(
if (action === "save") {
const file = object["file"] ? object["file"][0] : null;
delete object["file"];
if (!file) return void resolve({ file: "File not selected" });
const id = file.name;
store
.resourceExists("files", id)
.then((exists) => {
if (exists) {
store.setTimestamp(Date.now());
return void resolve({ file: "File already exists" });
}
const headers = Object.assign(
{ {
"Content-Type": "application/octet-stream", "Content-Type": "application/octet-stream",
Accept: "application/octet-stream",
}, },
object headers
); );
return store.xhrRequest({
store
.xhrRequest({
method: "PUT", method: "PUT",
headers: headers, headers: headers,
url: `api/files/${encodeURIComponent(id)}`, url: `api/files/${encodeURIComponent(file.name)}`,
serialize: (body) => body, // Identity function to prevent JSON.parse on blob data serialize: (body) => body, // Identity function to prevent JSON.parse on blob data
body: file, body: file,
}) config: (xhr) => {
.then(() => { if (progressListener)
notifications.push( xhr.upload.addEventListener("progress", progressListener);
"success", if (abortSignal) {
`File ${exists ? "updated" : "created"}` if (abortSignal.aborted) xhr.abort();
); abortSignal.addEventListener("abort", () => xhr.abort());
store.setTimestamp(Date.now());
resolve(null);
})
.catch(reject);
})
.catch(reject);
} else {
reject(new Error("Undefined action"));
} }
},
}); });
} }
@@ -178,14 +151,16 @@ export const component: ClosureComponent = (): Component => {
sortAttributes[i] = sort[attributes[i].id] || 0; sortAttributes[i] = sort[attributes[i].id] || 0;
function onSortChange(sortAttrs): void { function onSortChange(sortAttrs): void {
const _sort = Object.assign({}, sort); let _sort = Object.assign({}, sort);
for (const [index, direction] of Object.entries(sortAttrs)) { for (const [index, direction] of Object.entries(sortAttrs)) {
// Changing the priority of columns // Changing the priority of columns
delete _sort[attributes[index].id]; delete _sort[attributes[index].id];
_sort[attributes[index].id] = direction; if (direction)
_sort = Object.assign({ [attributes[index].id]: direction }, _sort);
} }
const ops = { sort: JSON.stringify(_sort) }; const ops = {};
if (Object.keys(_sort).length) ops["sort"] = JSON.stringify(_sort);
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
m.route.set("/admin/files", ops); m.route.set("/admin/files", ops);
} }
@@ -225,42 +200,83 @@ export const component: ClosureComponent = (): Component => {
title: "Create new file", title: "Create new file",
onclick: () => { onclick: () => {
let cb: () => Children = null; let cb: () => Children = null;
const abortController = new AbortController();
let progress = -1;
const comp = m( const comp = m(
putFormComponent, putFormComponent,
Object.assign( Object.assign(
{ {
actionHandler: (action, object) => { actionHandler: async (action, obj) => {
return new Promise<void>((resolve) => { if (action !== "save")
putActionHandler(action, object) throw new Error("Undefined action");
.then((errors) => { const file = obj["file"]?.[0];
const errorList = errors
? Object.values(errors) // nginx strips out headers with dot, so replace with dash
: []; const headers = {
if (errorList.length) { "metadata-fileType": obj["metadata.fileType"] || "",
for (const err of errorList) "metadata-oui": obj["metadata.oui"] || "",
notifications.push("error", err); "metadata-productclass":
} else { obj["metadata.productClass"] || "",
overlay.close(cb); "metadata-version": obj["metadata.version"] || "",
};
if (!file) {
notifications.push("error", "File not selected");
return;
} }
resolve();
}) if (await store.resourceExists("files", file.name)) {
.catch((err) => { store.setTimestamp(Date.now());
notifications.push("error", "File already exists");
return;
}
const progressListener = (e: ProgressEvent): void => {
progress = e.loaded / e.total;
m.redraw();
};
progress = 0;
try {
await upload(
file,
headers,
abortController.signal,
progressListener
);
store.setTimestamp(Date.now());
notifications.push("success", "File created");
overlay.close(cb);
} catch (err) {
notifications.push("error", err.message); notifications.push("error", err.message);
resolve(); }
}); progress = -1;
});
}, },
}, },
formData formData
) )
); );
cb = () => comp; cb = () => {
overlay.open( if (progress < 0) return [null, comp];
cb, return [
() => m(
!comp.state["current"]["modified"] || "div.progress",
confirm("You have unsaved changes. Close anyway?") m("div.progress-bar", {
); style: `width: ${Math.trunc(progress * 100)}%`,
})
),
comp,
];
};
overlay.open(cb, () => {
if (
comp.state["current"]["modified"] &&
!confirm("You have unsaved changes. Close anyway?")
)
return false;
abortController.abort();
return true;
});
}, },
}, },
"New" "New"
@@ -302,10 +318,11 @@ export const component: ClosureComponent = (): Component => {
}; };
} }
const filterAttrs = {}; const filterAttrs = {
filterAttrs["resource"] = "files"; resource: "files",
filterAttrs["filter"] = vnode.attrs["filter"]; filter: vnode.attrs["filter"],
filterAttrs["onChange"] = onFilterChanged; onChange: onFilterChanged,
};
return [ return [
m("h1", "Listing files"), m("h1", "Listing files"),
+60 -47
View File
@@ -17,7 +17,7 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import m, { ClosureComponent, Component } from "mithril"; import m, { ClosureComponent } from "mithril";
import { parse, stringify, map } from "../lib/common/expression-parser"; import { parse, stringify, map } from "../lib/common/expression-parser";
import memoize from "../lib/common/memoize"; import memoize from "../lib/common/memoize";
import Autocomplete from "./autocomplete-compnent"; import Autocomplete from "./autocomplete-compnent";
@@ -82,9 +82,9 @@ function stringifyFilter(f: Expression): string {
return stringify(f); return stringify(f);
} }
const splitFilter = memoize((filter) => { function splitFilter(filter: string): string[] {
if (!filter) return [""]; if (!filter) return [""];
const list = []; const list: string[] = [];
const f = parse(filter); const f = parse(filter);
if (Array.isArray(f) && f[0] === "AND") if (Array.isArray(f) && f[0] === "AND")
for (const ff of f.slice(1)) list.push(stringifyFilter(ff)); for (const ff of f.slice(1)) list.push(stringifyFilter(ff));
@@ -92,69 +92,82 @@ const splitFilter = memoize((filter) => {
list.push(""); list.push("");
return list; return list;
});
const component: ClosureComponent = (): Component => {
return {
view: (vnode) => {
if (
!vnode.state["filterList"] ||
vnode.attrs["filter"] !== vnode.state["filter"]
) {
vnode.state["filterInvalid"] = 0;
vnode.state["filter"] = vnode.attrs["filter"];
vnode.state["filterList"] = splitFilter(vnode.attrs["filter"]);
} }
interface Attrs {
resource: string;
filter: string;
onChange: (filter: string) => void;
}
const component: ClosureComponent<Attrs> = (initialVnode) => {
let filterList = splitFilter(initialVnode.attrs.filter);
let filterInvalid = 0;
let filterTouched = false;
let attrs: Attrs = initialVnode.attrs;
function onChange(): void { function onChange(): void {
vnode.state["filterInvalid"] = 0; filterTouched = false;
vnode.state["filterList"] = vnode.state["filterList"].filter((f) => f); filterInvalid = 0;
let filter = vnode.state["filterList"].map((f, idx) => { filterList = filterList.filter((f) => f);
const list = filterList.map((f, idx) => {
try { try {
return parseFilter(vnode.attrs["resource"], f); return parseFilter(attrs.resource, f);
} catch (err) { } catch (err) {
vnode.state["filterInvalid"] |= 1 << idx; filterInvalid |= 1 << idx;
} }
return null; return null;
}); });
vnode.state["filterList"].push(""); filterList.push("");
if (!vnode.state["filterInvalid"]) { if (filterInvalid) {
delete vnode.state["filter"]; m.redraw();
if (filter.length === 0) { return;
vnode.attrs["onChange"]("");
} else {
if (filter.length > 1) filter = ["AND"].concat(filter);
else filter = filter[0];
vnode.attrs["onChange"](stringify(filter));
}
} }
if (list.length === 0) attrs.onChange("");
else if (list.length > 1) attrs.onChange(stringify(["AND", ...list]));
else attrs.onChange(stringify(list[0]));
} }
return m( return {
"div.filter", onupdate: (vnode) => {
[m("b", "Filter")].concat( getAutocomplete(vnode.attrs.resource).reposition();
vnode.state["filterList"].map((fltr, idx) => { },
view: (vnode) => {
if (attrs.filter !== vnode.attrs.filter) {
filterInvalid = 0;
filterList = splitFilter(vnode.attrs.filter);
}
attrs = vnode.attrs;
return m("div.filter", [
m("b", "Filter"),
...filterList.map((fltr, idx) => {
return m("input", { return m("input", {
type: "text", type: "text",
class: `${ class: `${(filterInvalid >> idx) & 1 ? "error" : ""}`,
(vnode.state["filterInvalid"] >> idx) & 1 ? "error" : ""
}`,
value: fltr, value: fltr,
onchange: (e) => { oninput: (e) => {
vnode.state["filterList"] = vnode.state["filterList"].slice(); e.redraw = false;
vnode.state["filterList"][idx] = e.target.value.trim(); filterList[idx] = e.target.value;
onChange(); filterTouched = true;
}, },
oncreate: (vn) => { oncreate: (vn) => {
getAutocomplete(vnode.attrs["resource"]).attach( const el = vn.dom as HTMLInputElement;
vn.dom as HTMLInputElement getAutocomplete(vnode.attrs.resource).attach(el);
);
el.addEventListener("blur", () => {
if (filterTouched) onChange();
});
el.addEventListener("keydown", (e) => {
if (e.key === "Enter" && filterTouched) onChange();
});
}, },
}); });
}) }),
) ]);
);
}, },
}; };
}; };
+1 -3
View File
@@ -79,8 +79,6 @@ function renderTable(
continue; continue;
} }
let direction = 1;
let symbol = getIcon("unsorted"); let symbol = getIcon("unsorted");
if (sortAttributes[i] > 0) symbol = getIcon("sorted-asc"); if (sortAttributes[i] > 0) symbol = getIcon("sorted-asc");
else if (sortAttributes[i] < 0) symbol = getIcon("sorted-dsc"); else if (sortAttributes[i] < 0) symbol = getIcon("sorted-dsc");
@@ -89,7 +87,7 @@ function renderTable(
"button", "button",
{ {
onclick: () => { onclick: () => {
if (sortAttributes[i] > 0) direction *= -1; const direction = ((sortAttributes[i] + 2) % 3) - 1;
return onSortChange({ [i]: direction }); return onSortChange({ [i]: direction });
}, },
}, },
+2
View File
@@ -24,6 +24,7 @@ import userMenu from "./user-menu";
import adminMenu from "./admin-menu"; import adminMenu from "./admin-menu";
import * as overlay from "./overlay"; import * as overlay from "./overlay";
import { version as VERSION } from "../package.json"; import { version as VERSION } from "../package.json";
import datalist from "./datalist";
const adminPages = [ const adminPages = [
"presets", "presets",
@@ -69,6 +70,7 @@ const component: ClosureComponent = (): Component => {
]) ])
), ),
overlay.render(), overlay.render(),
m(datalist),
]; ];
}, },
}; };
+10 -7
View File
@@ -205,14 +205,16 @@ export const component: ClosureComponent = (): Component => {
} }
function onSortChange(sortAttrs): void { function onSortChange(sortAttrs): void {
const _sort = Object.assign({}, sort); let _sort = Object.assign({}, sort);
for (const [index, direction] of Object.entries(sortAttrs)) { for (const [index, direction] of Object.entries(sortAttrs)) {
// Changing the priority of columns // Changing the priority of columns
delete _sort[attributes[index].id]; delete _sort[attributes[index].id];
_sort[attributes[index].id] = direction; if (direction)
_sort = Object.assign({ [attributes[index].id]: direction }, _sort);
} }
const ops = { sort: JSON.stringify(_sort) }; const ops = {};
if (Object.keys(_sort).length) ops["sort"] = JSON.stringify(_sort);
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
m.route.set("/admin/permissions", ops); m.route.set("/admin/permissions", ops);
} }
@@ -365,10 +367,11 @@ export const component: ClosureComponent = (): Component => {
}; };
} }
const filterAttrs = {}; const filterAttrs = {
filterAttrs["resource"] = "permissions"; resource: "permissions",
filterAttrs["filter"] = vnode.attrs["filter"]; filter: vnode.attrs["filter"],
filterAttrs["onChange"] = onFilterChanged; onChange: onFilterChanged,
};
return [ return [
m("h1", "Listing permissions"), m("h1", "Listing permissions"),
+12 -9
View File
@@ -190,14 +190,16 @@ export const component: ClosureComponent = (): Component => {
} }
function onSortChange(sortAttrs): void { function onSortChange(sortAttrs): void {
const _sort = Object.assign({}, sort); let _sort = Object.assign({}, sort);
for (const [index, direction] of Object.entries(sortAttrs)) { for (const [index, direction] of Object.entries(sortAttrs)) {
// Changing the priority of columns // Changing the priority of columns
delete _sort[attributes[index].id]; delete _sort[attributes[index].id];
_sort[attributes[index].id] = direction; if (direction)
_sort = Object.assign({ [attributes[index].id]: direction }, _sort);
} }
const ops = { sort: JSON.stringify(_sort) }; const ops = {};
if (Object.keys(_sort).length) ops["sort"] = JSON.stringify(_sort);
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
m.route.set("/admin/presets", ops); m.route.set("/admin/presets", ops);
} }
@@ -323,7 +325,7 @@ export const component: ClosureComponent = (): Component => {
return m( return m(
"div", "div",
{ style: "margin:20px" }, { style: "margin:20px" },
"This UI only supports presets with a single 'provision' configuraiton. If this preset was originally created from the old UI (genieacs-gui), you must edit it there." "This UI only supports presets with a single 'provision' configuration. If this preset was originally created from the old UI (genieacs-gui), you must edit it there."
); );
} }
return comp; return comp;
@@ -331,7 +333,7 @@ export const component: ClosureComponent = (): Component => {
overlay.open( overlay.open(
cb, cb,
() => () =>
!comp.state["current"]["modified"] || !comp.state?.["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?") confirm("You have unsaved changes. Close anyway?")
); );
}, },
@@ -427,10 +429,11 @@ export const component: ClosureComponent = (): Component => {
}; };
} }
const filterAttrs = {}; const filterAttrs = {
filterAttrs["resource"] = "presets"; resource: "presets",
filterAttrs["filter"] = vnode.attrs["filter"]; filter: vnode.attrs["filter"],
filterAttrs["onChange"] = onFilterChanged; onChange: onFilterChanged,
};
return [ return [
m("h1", "Listing presets"), m("h1", "Listing presets"),
+10 -7
View File
@@ -173,14 +173,16 @@ export const component: ClosureComponent = (): Component => {
sortAttributes[i] = sort[attributes[i].id] || 0; sortAttributes[i] = sort[attributes[i].id] || 0;
function onSortChange(sortAttrs): void { function onSortChange(sortAttrs): void {
const _sort = Object.assign({}, sort); let _sort = Object.assign({}, sort);
for (const [index, direction] of Object.entries(sortAttrs)) { for (const [index, direction] of Object.entries(sortAttrs)) {
// Changing the priority of columns // Changing the priority of columns
delete _sort[attributes[index].id]; delete _sort[attributes[index].id];
_sort[attributes[index].id] = direction; if (direction)
_sort = Object.assign({ [attributes[index].id]: direction }, _sort);
} }
const ops = { sort: JSON.stringify(_sort) }; const ops = {};
if (Object.keys(_sort).length) ops["sort"] = JSON.stringify(_sort);
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
m.route.set("/admin/provisions", ops); m.route.set("/admin/provisions", ops);
} }
@@ -346,10 +348,11 @@ export const component: ClosureComponent = (): Component => {
}; };
} }
const filterAttrs = {}; const filterAttrs = {
filterAttrs["resource"] = "provisions"; resource: "provisions",
filterAttrs["filter"] = vnode.attrs["filter"]; filter: vnode.attrs["filter"],
filterAttrs["onChange"] = onFilterChanged; onChange: onFilterChanged,
};
return [ return [
m("h1", "Listing provisions"), m("h1", "Listing provisions"),
+26 -8
View File
@@ -17,9 +17,10 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { VnodeDOM, ClosureComponent, Component, Children } from "mithril"; import { VnodeDOM, ClosureComponent, Children } from "mithril";
import { m } from "./components"; import { m } from "./components";
import codeEditorComponent from "./code-editor-component"; import codeEditorComponent from "./code-editor-component";
import { getDatalistId } from "./datalist";
const singular = { const singular = {
presets: "preset", presets: "preset",
@@ -151,9 +152,14 @@ function createField(current, attr, focus): Children {
}); });
} }
let datalist: string = null;
if (attr.options) datalist = getDatalistId(attr.options);
return m("input", { return m("input", {
type: attr.type === "password" ? "password" : "text", type: attr.type === "password" ? "password" : "text",
name: attr.id, name: attr.id,
list: datalist,
autocomplete: datalist ? "off" : null,
disabled: attr.id === "_id" && !current.isNew, disabled: attr.id === "_id" && !current.isNew,
value: current.object[attr.id], value: current.object[attr.id],
oncreate: focus oncreate: focus
@@ -169,13 +175,25 @@ function createField(current, attr, focus): Children {
}); });
} }
const component: ClosureComponent = (): Component => { interface Attrs {
base?: Record<string, any>;
actionHandler: (action: string, object: any) => Promise<void>;
resource: string;
attributes: {
id: string;
label: string;
type?: string;
options?: string[];
}[];
}
const component: ClosureComponent<Attrs> = () => {
return { return {
view: (vnode) => { view: (vnode) => {
const actionHandler = vnode.attrs["actionHandler"]; const actionHandler = vnode.attrs.actionHandler;
const attributes = vnode.attrs["attributes"]; const attributes = vnode.attrs.attributes;
const resource = vnode.attrs["resource"]; const resource = vnode.attrs.resource;
const base = vnode.attrs["base"] || {}; const base = vnode.attrs.base || {};
if (!vnode.state["current"]) { if (!vnode.state["current"]) {
vnode.state["current"] = { vnode.state["current"] = {
isNew: !base["_id"], isNew: !base["_id"],
@@ -220,7 +238,7 @@ const component: ClosureComponent = (): Component => {
onclick: (e) => { onclick: (e) => {
e.redraw = false; e.redraw = false;
e.target.disabled = true; e.target.disabled = true;
actionHandler("delete", current.object).then(() => { actionHandler("delete", current.object).finally(() => {
e.target.disabled = false; e.target.disabled = false;
}); });
}, },
@@ -249,7 +267,7 @@ const component: ClosureComponent = (): Component => {
// e.target.onsubmit = null; // e.target.onsubmit = null;
(submit.dom as HTMLFormElement).disabled = true; (submit.dom as HTMLFormElement).disabled = true;
// submit.dom.textContent = "Loading ..."; // submit.dom.textContent = "Loading ...";
actionHandler("save", current.object).then(() => { actionHandler("save", current.object).finally(() => {
// submit.dom.textContent = "Save"; // submit.dom.textContent = "Save";
// e.target.onsubmit = onsubmit; // e.target.onsubmit = onsubmit;
(submit.dom as HTMLFormElement).disabled = false; (submit.dom as HTMLFormElement).disabled = false;
+1 -1
View File
@@ -145,7 +145,7 @@ function queryTag(tag: string): Expression {
export function getTip(resource: string, label: string): string { export function getTip(resource: string, label: string): string {
let tip; let tip;
if (resources[resource] && resources[resource][label]) { if (resources[resource]?.[label]) {
const param = resources[resource][label]; const param = resources[resource][label];
const types = const types =
resource === "devices" ? param["type"] : param["type"].split(","); resource === "devices" ? param["type"] : param["type"].split(",");
+37 -9
View File
@@ -34,6 +34,8 @@ const memoizedEvaluate = memoize(evaluate);
let fulfillTimestamp = 0; let fulfillTimestamp = 0;
let connectionNotification, configNotification, versionNotification; let connectionNotification, configNotification, versionNotification;
let clockSkew = 0;
const queries = { const queries = {
filter: new WeakMap(), filter: new WeakMap(),
bookmark: new WeakMap(), bookmark: new WeakMap(),
@@ -93,11 +95,13 @@ class QueryResponse {
} }
function checkConnection(): void { function checkConnection(): void {
const now1 = Date.now();
m.request({ m.request({
url: "status", url: "status",
method: "GET", method: "GET",
background: true, background: true,
extract: (xhr) => { extract: (xhr) => {
const now2 = Date.now();
if (xhr.status !== 200) { if (xhr.status !== 200) {
if (!connectionNotification) { if (!connectionNotification) {
connectionNotification = notifications.push( connectionNotification = notifications.push(
@@ -112,6 +116,21 @@ function checkConnection(): void {
connectionNotification = null; connectionNotification = null;
} }
try {
const nowAvg = Math.trunc((now1 + now2) / 2);
const skew = Date.parse(xhr.getResponseHeader("Date")) - nowAvg;
if (Math.abs(skew - clockSkew) > 5000 && now2 - now1 < 1000) {
clockSkew = skew;
console.warn(
`System and server clocks are out of sync. Adding ${clockSkew}ms offset to any client-side time relative calculations.`
);
setTimestamp(now2);
m.redraw();
}
} catch (err) {
// Ignore in case of missing or invalid Date header
}
const configChanged = const configChanged =
xhr.getResponseHeader("x-config-snapshot") !== configSnapshot; xhr.getResponseHeader("x-config-snapshot") !== configSnapshot;
const versionChanged = const versionChanged =
@@ -209,7 +228,7 @@ export async function xhrRequest(
export function unpackExpression(exp: Expression): Expression { export function unpackExpression(exp: Expression): Expression {
if (!Array.isArray(exp)) return exp; if (!Array.isArray(exp)) return exp;
const e = memoizedEvaluate(exp, null, fulfillTimestamp); const e = memoizedEvaluate(exp, null, fulfillTimestamp + clockSkew);
return e; return e;
} }
@@ -295,7 +314,7 @@ function compareFunction(sort: {
function findMatches(resourceType, filter, sort, limit): any[] { function findMatches(resourceType, filter, sort, limit): any[] {
let value = []; let value = [];
for (const obj of resources[resourceType].objects.values()) for (const obj of resources[resourceType].objects.values())
if (evaluate(filter, obj, fulfillTimestamp)) value.push(obj); if (evaluate(filter, obj, fulfillTimestamp + clockSkew)) value.push(obj);
value = value.sort(compareFunction(sort)); value = value.sort(compareFunction(sort));
if (limit) value = value.slice(0, limit); if (limit) value = value.slice(0, limit);
@@ -461,7 +480,7 @@ export function fulfill(accessTimestamp: number): void {
for (const queryResponse of toFetch) { for (const queryResponse of toFetch) {
let filter = queries.filter.get(queryResponse); let filter = queries.filter.get(queryResponse);
filter = memoizedEvaluate(filter, null, fulfillTimestamp); filter = memoizedEvaluate(filter, null, fulfillTimestamp + clockSkew);
const bookmark = queries.bookmark.get(queryResponse); const bookmark = queries.bookmark.get(queryResponse);
const sort = queries.sort.get(queryResponse); const sort = queries.sort.get(queryResponse);
if (bookmark) filter = limitFilter(filter, sort, bookmark); if (bookmark) filter = limitFilter(filter, sort, bookmark);
@@ -476,7 +495,11 @@ export function fulfill(accessTimestamp: number): void {
if (!diff) { if (!diff) {
for (const queryResponse of toFetch) { for (const queryResponse of toFetch) {
let filter = queries.filter.get(queryResponse); let filter = queries.filter.get(queryResponse);
filter = memoizedEvaluate(filter, null, fulfillTimestamp); filter = memoizedEvaluate(
filter,
null,
fulfillTimestamp + clockSkew
);
const limit = queries.limit.get(queryResponse); const limit = queries.limit.get(queryResponse);
const bookmark = queries.bookmark.get(queryResponse); const bookmark = queries.bookmark.get(queryResponse);
const sort = queries.sort.get(queryResponse); const sort = queries.sort.get(queryResponse);
@@ -521,7 +544,9 @@ export function fulfill(accessTimestamp: number): void {
for (const d of deleted) { for (const d of deleted) {
const obj = resources[resourceType].objects.get(d); const obj = resources[resourceType].objects.get(d);
if (evaluate(combinedFilterDiff, obj, fulfillTimestamp)) if (
evaluate(combinedFilterDiff, obj, fulfillTimestamp + clockSkew)
)
resources[resourceType].objects.delete(d); resources[resourceType].objects.delete(d);
} }
@@ -563,6 +588,10 @@ export function setTimestamp(t: number): void {
} }
} }
export function getClockSkew(): number {
return clockSkew;
}
export function postTasks( export function postTasks(
deviceId: string, deviceId: string,
tasks: QueueTask[] tasks: QueueTask[]
@@ -580,9 +609,8 @@ export function postTasks(
if (xhr.status === 403) throw new Error("Not authorized"); if (xhr.status === 403) throw new Error("Not authorized");
if (!xhr.status) throw new Error("Server is unreachable"); if (!xhr.status) throw new Error("Server is unreachable");
if (xhr.status !== 200) throw new Error(xhr.response); if (xhr.status !== 200) throw new Error(xhr.response);
const connectionRequestStatus = xhr.getResponseHeader( const connectionRequestStatus =
"Connection-Request" xhr.getResponseHeader("Connection-Request");
);
const st = JSON.parse(xhr.response); const st = JSON.parse(xhr.response);
for (const [i, t] of st.entries()) { for (const [i, t] of st.entries()) {
tasks[i]._id = t._id; tasks[i]._id = t._id;
@@ -664,7 +692,7 @@ export function evaluateExpression(
obj: Record<string, unknown> obj: Record<string, unknown>
): Expression { ): Expression {
if (!Array.isArray(exp)) return exp; if (!Array.isArray(exp)) return exp;
return memoizedEvaluate(exp, obj, fulfillTimestamp); return memoizedEvaluate(exp, obj, fulfillTimestamp + clockSkew);
} }
export function changePassword( export function changePassword(
+16 -8
View File
@@ -17,7 +17,7 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { ClosureComponent, Component } from "mithril"; import { ClosureComponent } from "mithril";
import { m } from "./components"; import { m } from "./components";
import * as store from "./store"; import * as store from "./store";
import { yaml } from "./dynamic-loader"; import { yaml } from "./dynamic-loader";
@@ -65,7 +65,7 @@ function putActionHandler(prefix: string[], dataYaml: string): Promise<any> {
store.putResource( store.putResource(
"config", "config",
obj._id, obj._id,
(obj as unknown) as Record<string, unknown> obj as unknown as Record<string, unknown>
) )
); );
} }
@@ -86,12 +86,20 @@ function putActionHandler(prefix: string[], dataYaml: string): Promise<any> {
}); });
} }
const component: ClosureComponent = (): Component => { interface Attrs {
prefix: string;
name: string;
data: { _id: string; value: string }[];
onUpdate: (errs: Record<string, string>) => void;
onError: (err: Error) => void;
}
const component: ClosureComponent<Attrs> = () => {
return { return {
view: (vnode) => { view: (vnode) => {
const prefix = vnode.attrs["prefix"].split("."); const prefix = vnode.attrs.prefix.split(".");
const name = vnode.attrs["name"]; const name = vnode.attrs.name;
const data = vnode.attrs["data"]; const data = vnode.attrs.data;
if (prefix[prefix.length - 1] === "") prefix.pop(); if (prefix[prefix.length - 1] === "") prefix.pop();
@@ -135,8 +143,8 @@ const component: ClosureComponent = (): Component => {
vnode.state["updatedYaml"] = yamlString; vnode.state["updatedYaml"] = yamlString;
putActionHandler(prefix, vnode.state["updatedYaml"]) putActionHandler(prefix, vnode.state["updatedYaml"])
.then(vnode.attrs["onUpdate"]) .then(vnode.attrs.onUpdate)
.catch(vnode.attrs["onError"]); .catch(vnode.attrs.onError);
}, },
}, },
[code, m(".actions-bar", [submit])] [code, m(".actions-bar", [submit])]
+10 -7
View File
@@ -186,14 +186,16 @@ export const component: ClosureComponent = (): Component => {
} }
function onSortChange(sortAttrs): void { function onSortChange(sortAttrs): void {
const _sort = Object.assign({}, sort); let _sort = Object.assign({}, sort);
for (const [index, direction] of Object.entries(sortAttrs)) { for (const [index, direction] of Object.entries(sortAttrs)) {
// Changing the priority of columns // Changing the priority of columns
delete _sort[attributes[index].id]; delete _sort[attributes[index].id];
_sort[attributes[index].id] = direction; if (direction)
_sort = Object.assign({ [attributes[index].id]: direction }, _sort);
} }
const ops = { sort: JSON.stringify(_sort) }; const ops = {};
if (Object.keys(_sort).length) ops["sort"] = JSON.stringify(_sort);
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
m.route.set("/admin/users", ops); m.route.set("/admin/users", ops);
} }
@@ -401,10 +403,11 @@ export const component: ClosureComponent = (): Component => {
}; };
} }
const filterAttrs = {}; const filterAttrs = {
filterAttrs["resource"] = "users"; resource: "users",
filterAttrs["filter"] = vnode.attrs["filter"]; filter: vnode.attrs["filter"],
filterAttrs["onChange"] = onFilterChanged; onChange: onFilterChanged,
};
return [ return [
m("h1", "Listing users"), m("h1", "Listing users"),
+10 -7
View File
@@ -173,14 +173,16 @@ export const component: ClosureComponent = (): Component => {
sortAttributes[i] = sort[attributes[i].id] || 0; sortAttributes[i] = sort[attributes[i].id] || 0;
function onSortChange(sortAttrs): void { function onSortChange(sortAttrs): void {
const _sort = Object.assign({}, sort); let _sort = Object.assign({}, sort);
for (const [index, direction] of Object.entries(sortAttrs)) { for (const [index, direction] of Object.entries(sortAttrs)) {
// Changing the priority of columns // Changing the priority of columns
delete _sort[attributes[index].id]; delete _sort[attributes[index].id];
_sort[attributes[index].id] = direction; if (direction)
_sort = Object.assign({ [attributes[index].id]: direction }, _sort);
} }
const ops = { sort: JSON.stringify(_sort) }; const ops = {};
if (Object.keys(_sort).length) ops["sort"] = JSON.stringify(_sort);
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
m.route.set("/admin/virtualParameters", ops); m.route.set("/admin/virtualParameters", ops);
} }
@@ -346,10 +348,11 @@ export const component: ClosureComponent = (): Component => {
}; };
} }
const filterAttrs = {}; const filterAttrs = {
filterAttrs["resource"] = "virtualParameters"; resource: "virtualParameters",
filterAttrs["filter"] = vnode.attrs["filter"]; filter: vnode.attrs["filter"],
filterAttrs["onChange"] = onFilterChanged; onChange: onFilterChanged,
};
return [ return [
m("h1", "Listing virtual parameters"), m("h1", "Listing virtual parameters"),