refactor: replace array-based expressions with typed class hierarchy

Replace the old Lisp-like array expression representation
(["AND", ["PARAM", ...], ...]) with a class hierarchy rooted in
an abstract Expression class (Literal, Parameter, Binary, Unary,
FunctionCall, Conditional). This gives compile-time type safety
to expression construction, evaluation, and pattern matching.

Key changes:
- New lib/common/expression.ts defines the class hierarchy with
  map/evaluate/toString methods
- Rewrite parser as hand-rolled recursive descent, removing the
  parsimmon dependency
- Extend Path with a colon separator to unify parameter identity
  and attributes into a single address space (e.g.
  "Device.WiFi.SSID.1.Name:writable"). The colon field tracks
  how many trailing segments are attribute qualifiers. PathSet
  rewritten with separate param/attr segment indexes
- Flatten device attribute representation in the UI to
  colon-delimited keys mirroring the new path syntax
This commit is contained in:
Zaid Humaimidi committed 2026-03-12 14:58:20 -07:00
1 parent 75908e624f
commit 6f3ce6f7c8
71 files changed
+4045 -3449

No files matched your search

+65 -74
View File
@@ -31,8 +31,8 @@ import {
udpConnectionRequest,
xmppConnectionRequest,
} from "./connection-request.ts";
import { Expression, Task } from "./types.ts";
import { evaluate } from "./common/expression/util.ts";
import { Task } from "./types.ts";
import Expression, { Value } from "./common/expression.ts";
import { hashPassword } from "./auth.ts";
import { flattenDevice } from "./ui/db.ts";
import { ResourceLockedError } from "./common/errors.ts";
@@ -42,7 +42,7 @@ const XMPP_CONFIGURED = !!config.get("XMPP_JID");
export async function connectionRequest(
deviceId: string,
device?: Record<string, { value?: [boolean | number | string, string] }>,
device?: Record<string, Value>,
): Promise<string> {
if (!device) {
const res = await collections.devices.findOne({ _id: deviceId });
@@ -58,47 +58,36 @@ export async function connectionRequest(
password;
if (device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"]) {
connectionRequestUrl = (device[
"InternetGatewayDevice.ManagementServer.ConnectionRequestURL"
].value || [""])[0];
udpConnectionRequestAddress = ((
connectionRequestUrl =
device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"] ||
"";
udpConnectionRequestAddress =
device[
"InternetGatewayDevice.ManagementServer.UDPConnectionRequestAddress"
] || {}
).value || [""])[0];
stunEnable = ((
device["InternetGatewayDevice.ManagementServer.STUNEnable"] || {}
).value || [""])[0];
connReqJabberId = ((
device["InternetGatewayDevice.ManagementServer.ConnReqJabberID"] || {}
).value || [""])[0];
username = ((
] || "";
stunEnable =
device["InternetGatewayDevice.ManagementServer.STUNEnable"] || "";
connReqJabberId =
device["InternetGatewayDevice.ManagementServer.ConnReqJabberID"] || "";
username =
device[
"InternetGatewayDevice.ManagementServer.ConnectionRequestUsername"
] || {}
).value || [""])[0];
password = ((
] || "";
password =
device[
"InternetGatewayDevice.ManagementServer.ConnectionRequestPassword"
] || {}
).value || [""])[0];
] || "";
} else {
connectionRequestUrl = (device[
"Device.ManagementServer.ConnectionRequestURL"
].value || [""])[0];
udpConnectionRequestAddress = ((
device["Device.ManagementServer.UDPConnectionRequestAddress"] || {}
).value || [""])[0];
stunEnable = ((device["Device.ManagementServer.STUNEnable"] || {})
.value || [""])[0];
connReqJabberId = ((device["Device.ManagementServer.ConnReqJabberID"] || {})
.value || [""])[0];
username = ((
device["Device.ManagementServer.ConnectionRequestUsername"] || {}
).value || [""])[0];
password = ((
device["Device.ManagementServer.ConnectionRequestPassword"] || {}
).value || [""])[0];
connectionRequestUrl =
device["Device.ManagementServer.ConnectionRequestURL"] || "";
udpConnectionRequestAddress =
device["Device.ManagementServer.UDPConnectionRequestAddress"] || "";
stunEnable = device["Device.ManagementServer.STUNEnable"] || "";
connReqJabberId = device["Device.ManagementServer.ConnReqJabberID"] || "";
username =
device["Device.ManagementServer.ConnectionRequestUsername"] || "";
password =
device["Device.ManagementServer.ConnectionRequestPassword"] || "";
}
let remoteAddress;
try {
@@ -107,68 +96,70 @@ export async function connectionRequest(
return "Invalid connection request URL";
}
const evalCallback = (exp): Expression => {
if (!Array.isArray(exp)) return exp;
if (exp[0] === "PARAM" && typeof exp[1] === "string") {
let name = exp[1];
const snapshot = await getRevision();
const now = Date.now();
const evalCallback = (exp: Expression): Expression => {
if (exp instanceof Expression.Parameter) {
let name = exp.path.toString();
if (name === "id") name = "DeviceID.ID";
else if (name === "serialNumber") name = "DeviceID.SerialNumber";
else if (name === "productClass") name = "DeviceID.ProductClass";
else if (name === "oui") name = "DeviceID.OUI";
else if (name === "remoteAddress") return remoteAddress;
else if (name === "username") return username;
else if (name === "password") return password;
const p = device[name];
if (p?.value) return p.value[0];
} else if (exp[0] === "FUNC") {
if (exp[1] === "REMOTE_ADDRESS") return remoteAddress;
else if (exp[1] === "USERNAME") return username;
else if (exp[1] === "PASSWORD") return password;
else if (name === "remoteAddress")
return new Expression.Literal(remoteAddress);
else if (name === "username") return new Expression.Literal(username);
else if (name === "password") return new Expression.Literal(password);
return new Expression.Literal(device[name] ?? null);
} else if (exp instanceof Expression.FunctionCall) {
if (exp.name === "NOW") return new Expression.Literal(now);
else if (exp.name === "REMOTE_ADDRESS")
return new Expression.Literal(remoteAddress);
else if (exp.name === "USERNAME") return new Expression.Literal(username);
else if (exp.name === "PASSWORD") return new Expression.Literal(password);
}
return exp;
};
const snapshot = await getRevision();
const now = Date.now();
const UDP_CONNECTION_REQUEST_PORT = +getConfig(
const configCallback = (exp: Expression): Expression.Literal => {
const e = evalCallback(exp);
if (e instanceof Expression.Literal) return e;
return new Expression.Literal(null);
};
const UDP_CONNECTION_REQUEST_PORT = getConfig(
snapshot,
"cwmp.udpConnectionRequestPort",
{},
now,
evalCallback,
0,
configCallback,
);
const CONNECTION_REQUEST_TIMEOUT = +getConfig(
const CONNECTION_REQUEST_TIMEOUT = getConfig(
snapshot,
"cwmp.connectionRequestTimeout",
{},
now,
evalCallback,
2000,
configCallback,
);
const CONNECTION_REQUEST_ALLOW_BASIC_AUTH = !!getConfig(
const CONNECTION_REQUEST_ALLOW_BASIC_AUTH = getConfig(
snapshot,
"cwmp.connectionRequestAllowBasicAuth",
{},
now,
evalCallback,
false,
configCallback,
);
let authExp: Expression = getConfigExpression(
snapshot,
"cwmp.connectionRequestAuth",
);
if (authExp === undefined) {
authExp = [
"FUNC",
"AUTH",
["PARAM", "username"],
["PARAM", "password"],
] as Expression;
if (!authExp) {
authExp = new Expression.FunctionCall("AUTH", [
new Expression.FunctionCall("USERNAME", []),
new Expression.FunctionCall("PASSWORD", []),
]);
}
authExp = evaluate(authExp, {}, now, evalCallback);
authExp = authExp.evaluate(evalCallback);
const debug = !!getConfig(snapshot, "cwmp.debug", {}, now, evalCallback);
const debug = getConfig(snapshot, "cwmp.debug", false, configCallback);
let udpProm = Promise.resolve(false);
if (udpConnectionRequestAddress && +stunEnable) {