Compare commits

..
42 Commits
Author SHA1 Message Date
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
69 changed files with 2779 additions and 5376 deletions

No files matched your search

+37
View File
@@ -1,5 +1,42 @@
# Change Log # Change Log
## 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) ## 1.2.5 (2021-03-12)
- Support specifying custom types when uploading files. - Support specifying custom types when uploading files.
+1 -1
View File
@@ -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).
+9 -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";
@@ -299,12 +299,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));
} }
+3
View File
@@ -165,3 +165,6 @@ Brotli
CSV CSV
hostInfo hostInfo
IPv IPv
downloadSuccessOnTimeout
TransferComplete
nginx
+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.
+1 -1
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
+21 -16
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) {
udpProm = udpConnectionRequest( try {
udpConnectionRequestAddress, const u = new URL("udp://" + udpConnectionRequestAddress);
authExp, udpProm = udpConnectionRequest(
UDP_CONNECTION_REQUEST_PORT, u.hostname,
debug, parseInt(u.port || "80"),
deviceId authExp,
); UDP_CONNECTION_REQUEST_PORT,
debug,
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;
+287 -257
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); getVariable(exp: Expression): number {
if (!r) relations.set(lhs, (r = [])); const expStr = JSON.stringify(exp);
r.push({ op: "", rhs: null, var: n }); if (this._variables.has(expStr)) return this._variables.get(expStr);
} else { const v = this._variables.size;
const lhs = JSON.stringify(e); this._variables.set(expStr, v);
let r = relations.get(lhs); this._expressions.set(v, exp);
if (!r) relations.set(lhs, (r = [])); this.generateIsNullDc(v);
r.push({ op: "", rhs: null, var: n }); this.generateComparisionDc(v);
if (
expStr === '["IS NULL",["PARAM","DeviceID.ID"]]' ||
expStr === '["IS NULL",["PARAM","_id"]]'
)
this._dcSet.push([(v << 2) ^ 3]);
return v;
}
private generateIsNullDc(v: number): void {
this._isNullRelations.set(v, []);
const exp = this._expressions.get(v);
const isNull = new Set(
findIsNullDeps(exp).map((e) => this.getVariable(["IS NULL", e]))
);
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])
);
}
private generateComparisionDc(v: number): void {
const exp = this._expressions.get(v);
if (!Array.isArray(exp)) return;
const op = exp[0];
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;
const comp = {
eq: -1,
gt: -1,
lt: -1,
};
allComps.set(rhsStr, comp);
comp.eq = this.getVariable(["=", exp[1], exp[2]]);
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]);
} }
} }
const dcSet: number[][] = []; getExpression(v: number): Expression {
const isNull: Map<number, number> = new Map(); return this._expressions.get(v);
for (const [key, rels] of relations) {
const n = variables.size;
variables.set(n, ["IS NULL", JSON.parse(key)]);
if (key === '["PARAM","DeviceID.ID"]' || key === '["PARAM","_id"]')
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) {
if (v.size > 1) {
if (!v.has("=")) {
const va = variables.size;
variables.set(va, ["=", JSON.parse(key), JSON.parse(k)]);
rels.push({ op: "=", rhs: JSON.parse(k), var: va });
v.set("=", va);
}
if (!v.has(">")) {
const va = variables.size;
variables.set(va, [">", JSON.parse(key), JSON.parse(k)]);
rels.push({ op: ">", rhs: JSON.parse(k), var: va });
v.set(">", va);
}
if (!v.has("<")) {
const va = variables.size;
variables.set(va, ["<", JSON.parse(key), JSON.parse(k)]);
rels.push({ op: "<", rhs: JSON.parse(k), var: va });
v.set("<", va);
}
dcSet.push([
(v.get("=") << 2) ^ 1,
(v.get(">") << 2) ^ 1,
(v.get("<") << 2) ^ 1,
]);
}
}
for (let i = 0; i < rels.length; ++i) {
const rel1 = rels[i];
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) {
if (comp === 0)
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]);
}
}
}
} }
return { dcSet, isNull }; getIsNullRelations(v: number): number[] {
return this._isNullRelations.get(v);
}
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]);
}
}
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; if (!set.has(idx ^ 3)) return true;
if (set.has((n << 2) ^ 3)) return true; for (const n of vars) if (set.has((n << 2) ^ 3)) return true;
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,
]);
} }
+1 -1
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));
+3 -11
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 (
@@ -334,15 +334,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);
} }
+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: "" },
+25 -22
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"]) {
authHeader = auth.parseWwwAuthenticateHeader( try {
res.headers["www-authenticate"] authHeader = auth.parseWwwAuthenticateHeader(
); 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) {
+154 -175
View File
@@ -84,12 +84,16 @@ async function authenticate(
let authentication; let authentication;
if (sessionContext.httpRequest.headers["authorization"]) { if (sessionContext.httpRequest.headers["authorization"]) {
authentication = auth.parseAuthorizationHeader( try {
sessionContext.httpRequest.headers["authorization"] authentication = auth.parseAuthorizationHeader(
); 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 +187,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 +213,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 +435,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 +459,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 +498,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 +574,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 +608,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 +790,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 +882,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 +1028,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 +1086,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 +1133,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
); );
@@ -1304,13 +1291,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 +1306,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 +1374,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 +1436,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 +1451,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" }); httpResponse,
if (sessionContext.debug) { sessionContext,
debug.outgoingHttpResponse( body.toString(),
httpResponse, "Invalid session"
sessionContext.deviceId, );
_body
);
}
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 +1478,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 +1497,87 @@ async function listenerAsync(
httpResponse: httpResponse, httpResponse: httpResponse,
}, },
}); });
httpResponse.setHeader("Content-Length", Buffer.byteLength(msg)); return clientError(
httpResponse.writeHead(400, { Connection: "close" }); httpRequest,
if (sessionContext) { httpResponse,
if (sessionContext.debug) { sessionContext,
debug.incomingHttpRequest( body.toString(),
httpRequest, msg
sessionContext.deviceId, );
body.toString()
);
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) { httpRequest,
if (sessionContext.debug) { httpResponse,
debug.incomingHttpRequest( sessionContext,
httpRequest, bodyStr,
sessionContext.deviceId, err.message
bodyStr );
); }
debug.outgoingHttpResponse(
httpResponse, if (!sessionContext && sessionId && rpc.cpeRequest?.name !== "Inform") {
sessionContext.deviceId, await new Promise((resolve) => setTimeout(resolve, 100));
error.message const sessionContextString = await cache.pop(`session_${sessionId}`);
); if (sessionContextString) {
} sessionContext = await session.deserialize(sessionContextString);
await endSession(sessionContext); sessionContext.httpRequest = httpRequest;
} else { sessionContext.httpResponse = httpResponse;
const cacheSnapshot = await localCache.getCurrentSnapshot(); httpRequest.socket.setTimeout(sessionContext.timeout * 1000);
const d = !!localCache.getConfig( if (sessionContext.authState !== 1) sessionContext.authState = 0;
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, 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); if (stats.concurrentRequests > MAX_CONCURRENT_REQUESTS) {
} // Check again just in case device included old session ID
httpResponse.end(_body); // from the previous session
httpResponse.writeHead(503, { "Retry-after": 60, Connection: "close" });
httpResponse.end("503 Service Unavailable");
stats.droppedRequests += 1;
return; return;
} }
+45 -27
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"]];
res.push([Path.parse(path), t, attrs]); try {
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();
+6 -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;
@@ -366,12 +360,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 +496,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")
+97 -42
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) => {
response.writeHead(200); if (!dev) {
response.end(); 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.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(
+8 -4
View File
@@ -39,16 +39,20 @@ export function ping(
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:
+165 -92
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,22 +452,50 @@ 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)
delete sessionContext.operations[commandKey]; continue;
if (!sessionContext.operationsTouched)
sessionContext.operationsTouched = {};
sessionContext.operationsTouched[commandKey] = 1;
faults.push({ logger.accessWarn({
code: "timeout", sessionContext: sessionContext,
message: "Download operation timed out", message: "Download operation timed out",
timestamp: operation.timestamp, commandKey: commandKey,
}); });
operations.push(operation); const SUCCESS_ON_TIMEOUT = +localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.downloadSuccessOnTimeout",
{},
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e)
);
revertDownloadParameters(sessionContext, operation.args.instance); 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];
if (!sessionContext.operationsTouched)
sessionContext.operationsTouched = {};
sessionContext.operationsTouched[commandKey] = 1;
faults.push({
code: "timeout",
message: "Download operation timed out",
timestamp: operation.timestamp,
});
operations.push(operation);
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`,
message:
"The list of parameters returned in the GetParameterValuesResponse message does not match the requested parameters",
};
}
for (const p of rpcRes.parameterList) { for (const [path, value, type] 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,
{ {
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]);
+43 -15
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;
} }
if (param && !param.endsWith(".")) try {
return [Path.parse(param), false, parsed]; if (param && !param.endsWith("."))
else return [Path.parse(param.slice(0, -1)), true, parsed]; return [Path.parse(param), false, 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));
return [Path.parse(param), notification, accessList]; try {
}); 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 {
@@ -798,14 +827,13 @@ 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,
@@ -848,7 +876,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;
+1 -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 {
+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);
+7 -8
View File
@@ -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";
@@ -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();
}); });
+16 -12
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(
metadata: metadata, filename as unknown as ObjectId,
}); filename,
{
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];
+1094 -4244
View File
File diff suppressed because it is too large. Load diff
+36 -36
View File
@@ -1,6 +1,6 @@
{ {
"name": "genieacs", "name": "genieacs",
"version": "1.2.5", "version": "1.2.7",
"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", "espresso-iisojs": "^1.0.4",
"iconv-lite": "^0.6.2", "iconv-lite": "^0.6.3",
"ipaddr.js": "^2.0.0", "ipaddr.js": "^2.0.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"koa": "^2.13.1", "koa": "^2.13.1",
"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.1",
"koa-router": "^10.0.0", "koa-router": "^10.1.1",
"koa-static": "^5.0.0", "koa-static": "^5.0.0",
"later": "^1.2.0", "later": "^1.2.0",
"mongodb": "^3.6.3", "mongodb": "^4.1.2",
"parsimmon": "^1.16.0", "parsimmon": "^1.18.0",
"seedrandom": "^3.0.5" "seedrandom": "^3.0.5"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^17.1.0", "@rollup/plugin-commonjs": "^20.0.0",
"@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.4",
"@rollup/plugin-typescript": "^8.2.0", "@rollup/plugin-typescript": "^8.2.5",
"@types/codemirror": "0.0.108", "@types/codemirror": "^5.60.2",
"@types/jsonwebtoken": "^8.5.0", "@types/jsonwebtoken": "^8.5.5",
"@types/koa": "^2.13.1", "@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.9", "@types/node": "^16.9.2",
"@types/node": "^14.14.34",
"@types/parsimmon": "^1.10.6", "@types/parsimmon": "^1.10.6",
"@types/seedrandom": "^3.0.0", "@types/seedrandom": "^3.0.1",
"@typescript-eslint/eslint-plugin": "^4.17.0", "@typescript-eslint/eslint-plugin": "^4.31.1",
"@typescript-eslint/parser": "^4.17.0", "@typescript-eslint/parser": "^4.31.1",
"ava": "^3.15.0", "ava": "^3.15.0",
"codemirror": "^5.59.4", "codemirror": "^5.62.3",
"cssnano": "^4.1.10", "cssnano": "^5.0.8",
"eslint": "^7.22.0", "eslint": "^7.32.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.8", "postcss": "^8.3.6",
"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.41.2", "rollup": "^2.56.3",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"svgo": "^2.2.2", "sql.js": "^1.6.1",
"ts-node": "^9.1.1", "svgo": "^2.6.1",
"typescript": "^4.2.3", "ts-node": "^10.2.1",
"yaml": "^1.10.0" "typescript": "^4.4.3",
"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;
+1 -1
View File
@@ -85,7 +85,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);
+5 -5
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];
@@ -78,13 +78,13 @@ const component: ClosureComponent = (): Component => {
const { value, timestamp, parameter } = evaluateParam( const { value, timestamp, parameter } = evaluateParam(
vnode.attrs["parameter"], vnode.attrs["parameter"],
vnode.attrs["device"], vnode.attrs["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)})`;
} }
+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)
+10
View File
@@ -787,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;
}
}
+5 -4
View File
@@ -328,10 +328,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"),
+5 -4
View File
@@ -200,10 +200,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"),
+99 -83
View File
@@ -73,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; "Content-Type": "application/octet-stream",
delete object["file"]; },
headers
if (!file) return void resolve({ file: "File not selected" }); );
return store.xhrRequest({
const id = file.name; method: "PUT",
headers: headers,
store url: `api/files/${encodeURIComponent(file.name)}`,
.resourceExists("files", id) serialize: (body) => body, // Identity function to prevent JSON.parse on blob data
.then((exists) => { body: file,
if (exists) { config: (xhr) => {
store.setTimestamp(Date.now()); if (progressListener)
return void resolve({ file: "File already exists" }); xhr.upload.addEventListener("progress", progressListener);
} if (abortSignal) {
const headers = Object.assign( if (abortSignal.aborted) xhr.abort();
{ abortSignal.addEventListener("abort", () => xhr.abort());
"Content-Type": "application/octet-stream", }
Accept: "application/octet-stream", },
},
object
);
store
.xhrRequest({
method: "PUT",
headers: headers,
url: `api/files/${encodeURIComponent(id)}`,
serialize: (body) => body, // Identity function to prevent JSON.parse on blob data
body: file,
})
.then(() => {
notifications.push(
"success",
`File ${exists ? "updated" : "created"}`
);
store.setTimestamp(Date.now());
resolve(null);
})
.catch(reject);
})
.catch(reject);
} else {
reject(new Error("Undefined action"));
}
}); });
} }
@@ -224,42 +198,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"] || "",
} };
resolve();
}) if (!file) {
.catch((err) => { notifications.push("error", "File not selected");
notifications.push("error", err.message); return;
resolve(); }
});
}); if (await store.resourceExists("files", file.name)) {
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);
}
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"
@@ -301,10 +316,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"),
+72 -59
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;
}); }
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 {
filterTouched = false;
filterInvalid = 0;
filterList = filterList.filter((f) => f);
const list = filterList.map((f, idx) => {
try {
return parseFilter(attrs.resource, f);
} catch (err) {
filterInvalid |= 1 << idx;
}
return null;
});
filterList.push("");
if (filterInvalid) {
m.redraw();
return;
}
if (list.length === 0) attrs.onChange("");
else if (list.length > 1) attrs.onChange(stringify(["AND", ...list]));
else attrs.onChange(stringify(list[0]));
}
const component: ClosureComponent = (): Component => {
return { return {
onupdate: (vnode) => {
getAutocomplete(vnode.attrs.resource).reposition();
},
view: (vnode) => { view: (vnode) => {
if ( if (attrs.filter !== vnode.attrs.filter) {
!vnode.state["filterList"] || filterInvalid = 0;
vnode.attrs["filter"] !== vnode.state["filter"] filterList = splitFilter(vnode.attrs.filter);
) {
vnode.state["filterInvalid"] = 0;
vnode.state["filter"] = vnode.attrs["filter"];
vnode.state["filterList"] = splitFilter(vnode.attrs["filter"]);
} }
function onChange(): void { attrs = vnode.attrs;
vnode.state["filterInvalid"] = 0;
vnode.state["filterList"] = vnode.state["filterList"].filter((f) => f);
let filter = vnode.state["filterList"].map((f, idx) => {
try {
return parseFilter(vnode.attrs["resource"], f);
} catch (err) {
vnode.state["filterInvalid"] |= 1 << idx;
}
return null;
});
vnode.state["filterList"].push("");
if (!vnode.state["filterInvalid"]) { return m("div.filter", [
delete vnode.state["filter"]; m("b", "Filter"),
if (filter.length === 0) { ...filterList.map((fltr, idx) => {
vnode.attrs["onChange"](""); return m("input", {
} else { type: "text",
if (filter.length > 1) filter = ["AND"].concat(filter); class: `${(filterInvalid >> idx) & 1 ? "error" : ""}`,
else filter = filter[0]; value: fltr,
vnode.attrs["onChange"](stringify(filter)); oninput: (e) => {
} e.redraw = false;
} filterList[idx] = e.target.value;
} filterTouched = true;
},
oncreate: (vn) => {
const el = vn.dom as HTMLInputElement;
getAutocomplete(vnode.attrs.resource).attach(el);
return m( el.addEventListener("blur", () => {
"div.filter", if (filterTouched) onChange();
[m("b", "Filter")].concat( });
vnode.state["filterList"].map((fltr, idx) => {
return m("input", { el.addEventListener("keydown", (e) => {
type: "text", if (e.key === "Enter" && filterTouched) onChange();
class: `${ });
(vnode.state["filterInvalid"] >> idx) & 1 ? "error" : "" },
}`, });
value: fltr, }),
onchange: (e) => { ]);
vnode.state["filterList"] = vnode.state["filterList"].slice();
vnode.state["filterList"][idx] = e.target.value.trim();
onChange();
},
oncreate: (vn) => {
getAutocomplete(vnode.attrs["resource"]).attach(
vn.dom as HTMLInputElement
);
},
});
})
)
);
}, },
}; };
}; };
+5 -4
View File
@@ -365,10 +365,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"),
+7 -6
View File
@@ -323,7 +323,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 +331,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 +427,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"),
+5 -4
View File
@@ -346,10 +346,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"),
+20 -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 { 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"; import { getDatalistId } from "./datalist";
@@ -175,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"],
@@ -226,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;
}); });
}, },
@@ -255,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])]
+5 -4
View File
@@ -401,10 +401,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"),
+5 -4
View File
@@ -346,10 +346,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"),