Compare commits

...
17 Commits
Author SHA1 Message Date
Zaid Abdulla fa95d7a310 1.2.8 2021-10-27 13:07:03 -07:00
Zaid Abdulla d299ef3a59 Changelog for v1.2.8 2021-10-27 13:06:37 -07:00
Zaid Abdulla 2c606b268e Update dependencies 2021-10-27 13:04:34 -07:00
Zaid Abdulla 1b86f41ad7 Replace later dependency with @breejs/later 2021-10-26 17:40:17 -07:00
Zaid Abdulla c4754a9b55 Strip dev deps from new lock file format 2021-10-26 10:33:33 -07:00
Zaid Abdulla f24111ea9f Support modulo operator in expressions 2021-10-25 16:51:46 -07:00
Zaid Abdulla 8f662fb152 Fix expr parser treating - and / as commutative 2021-10-25 13:40:48 -07:00
Zaid Abdulla dd722ba6e0 Improve sorting toggle behavior
Last sorted column now takes precedence over any previously sorted
columns. This makes for a more intuitive user experience.
2021-10-25 13:36:23 -07:00
Zaid Abdulla 88a84788a5 Evaluate component attrs before use 2021-10-24 18:17:03 -07:00
Zaid Abdulla 73baffc3b9 Improve container component
Allow passing custom attribute to the HTML element. Implement function
encodeUriComponent for attribute expressions.
2021-10-24 18:14:36 -07:00
Zaid Abdulla 0188ac860b Handle unsupported ACS methods gracefully 2021-10-15 16:21:08 -07:00
Zaid Abdulla 7f295beeec Validate host arg passed to ping
Fixes remote code execution vulnerability reported by Alex Hordijk.
2021-10-15 16:21:08 -07:00
Zaid Abdulla 2ac536bf8f Fix ui.pageSize regression 2021-09-22 09:27:11 -07:00
Zaid Abdulla f63e357b29 Fix synthesized expressions being non-minimal 2021-09-20 12:41:09 -07:00
Zaid Abdulla ceec3f2cc8 1.2.7 2021-09-18 14:36:19 -07:00
Zaid Abdulla 2b60feb2f4 Changelog for v1.2.7 2021-09-18 14:35:34 -07:00
Zaid Abdulla 9203417686 Fix a couple of regressions in 1.2.6 2021-09-18 14:34:11 -07:00
33 changed files with 9395 additions and 3112 deletions

No files matched your search

+22
View File
@@ -1,5 +1,27 @@
# Change Log # Change Log
## 1.2.8 (2021-10-27)
- Fix a remote code execution security vulnerability in genieacs-ui.
- All UI components can now be configured using fully dynamic expressions.
Previously some component types only accept fixed string values as properties.
- The `container` UI component can now be configured with an `element` property
that's either a string or a nested pair of `tag` and `attributes` properties.
The various attributes under the `attributes` property can now make use of a
new function `ENCODEURICOMPONENT()` to facilitate creating custom hyperlinks
in the UI.
- Improve sorting buttons' behavior in the device listing page and other pages.
Sorting by multiple columns should now feel more intuitive.
- Support the modulo (%) operator in expressions.
- Fix a regression in the previous release where the config option 'ui.pageSize'
no longer works.
- Fix process crash when a CPE sends an unsupported ACS method.
## 1.2.7 (2021-09-18)
- Fix regression causing frequent invalid session errors.
- Fix regression breaking digest authentication in connection requests.
## 1.2.6 (2021-09-16) ## 1.2.6 (2021-09-16)
- New config option `cwmp.downloadSuccessOnTimeout` to enable a workaround for - New config option `cwmp.downloadSuccessOnTimeout` to enable a workaround for
+11
View File
@@ -74,6 +74,7 @@ function rmDirSync(dirPath): void {
fs.rmdirSync(dirPath); fs.rmdirSync(dirPath);
} }
// For lockfileVersion = 1
function stripDevDeps(deps): void { function stripDevDeps(deps): void {
if (!deps["dependencies"]) return; if (!deps["dependencies"]) return;
for (const [k, v] of Object.entries(deps["dependencies"])) { for (const [k, v] of Object.entries(deps["dependencies"])) {
@@ -83,6 +84,15 @@ function stripDevDeps(deps): void {
if (!Object.keys(deps["dependencies"]).length) delete deps["dependencies"]; if (!Object.keys(deps["dependencies"]).length) delete deps["dependencies"];
} }
// For lockfileVersion = 2
function stripDevDeps2(deps): void {
if (!deps["packages"]) return;
for (const [k, v] of Object.entries(deps["packages"])) {
delete v["devDependencies"];
if (v["dev"]) delete deps["packages"][k];
}
}
function xmlTostring(xml): string { function xmlTostring(xml): string {
const children = []; const children = [];
for (const c of xml.children || []) children.push(xmlTostring(c)); for (const c of xml.children || []) children.push(xmlTostring(c));
@@ -140,6 +150,7 @@ async function init(): Promise<string[]> {
); );
npmShrinkwrapJson["version"] = packageJson["version"]; npmShrinkwrapJson["version"] = packageJson["version"];
stripDevDeps(npmShrinkwrapJson); stripDevDeps(npmShrinkwrapJson);
stripDevDeps2(npmShrinkwrapJson);
fs.writeFileSync( fs.writeFileSync(
path.resolve(OUTPUT_DIR, "npm-shrinkwrap.json"), path.resolve(OUTPUT_DIR, "npm-shrinkwrap.json"),
JSON.stringify(npmShrinkwrapJson, null, 2) JSON.stringify(npmShrinkwrapJson, null, 2)
+2
View File
@@ -168,3 +168,5 @@ IPv
downloadSuccessOnTimeout downloadSuccessOnTimeout
TransferComplete TransferComplete
nginx nginx
ENCODEURICOMPONENT
pageSize
+1 -1
View File
@@ -1110,8 +1110,8 @@ function getCanRaiseCallback(
const vars = context.getIsNullRelations(i); const vars = context.getIsNullRelations(i);
if (!context.isIsNull(i)) { if (!context.isIsNull(i)) {
if (!(idx & 1)) return true; if (!(idx & 1)) return true;
if (!set.has(idx ^ 3)) return true;
for (const n of vars) if (set.has((n << 2) ^ 3)) return true; for (const n of vars) if (set.has((n << 2) ^ 3)) return true;
if (set.has(idx ^ 3)) return false;
for (const n of vars) if (!set.has((n << 2) ^ 2)) return false; for (const n of vars) if (!set.has((n << 2) ^ 2)) return false;
return true; return true;
} }
+6 -3
View File
@@ -87,8 +87,6 @@ function binaryLeft(
rest.reduce((acc, ch) => { rest.reduce((acc, ch) => {
const [op, another] = ch; const [op, another] = ch;
if (Array.isArray(acc) && op === acc[0]) return acc.concat([another]); if (Array.isArray(acc) && op === acc[0]) return acc.concat([another]);
if (Array.isArray(another) && op === another[0])
return [op, acc].concat(another.slice(1));
return [op, acc, another]; return [op, acc, another];
}, first) }, first)
); );
@@ -287,7 +285,11 @@ const lang = parsimmon.createLanguage({
.skip(parsimmon.optWhitespace), .skip(parsimmon.optWhitespace),
binaryLeft( binaryLeft(
parsimmon parsimmon
.alt(parsimmon.string("*"), parsimmon.string("/")) .alt(
parsimmon.string("*"),
parsimmon.string("/"),
parsimmon.string("%")
)
.skip(parsimmon.optWhitespace), .skip(parsimmon.optWhitespace),
parsimmon.alt( parsimmon.alt(
r.Value, r.Value,
@@ -386,6 +388,7 @@ export function stringify(exp: Expression, level = 0): string {
"-": 31, "-": 31,
"*": 32, "*": 32,
"/": 32, "/": 32,
"%": 32,
}; };
const op = exp[0].toUpperCase(); const op = exp[0].toUpperCase();
+15 -3
View File
@@ -244,9 +244,12 @@ export function evaluateCallback(exp: Expression): Expression {
} else if (exp[0] === "/") { } else if (exp[0] === "/") {
return reduce(exp, (a, b, i) => { return reduce(exp, (a, b, i) => {
if (a == null || b == null) return null; if (a == null || b == null) return null;
if (!isArray(a) && !isArray(b)) if (isArray(a) || isArray(b)) return REDUCE_SKIP;
return i === 0 ? toNumber(a) / toNumber(b) : toNumber(a) * toNumber(b); const n1 = toNumber(a);
return REDUCE_SKIP; const n2 = toNumber(b);
if (i !== 0) return n1 * n2;
if (n2 === 0) return null;
return n1 / n2;
}); });
} else if (exp[0] === "+") { } else if (exp[0] === "+") {
return reduce(exp, (a, b) => { return reduce(exp, (a, b) => {
@@ -261,6 +264,15 @@ export function evaluateCallback(exp: Expression): Expression {
return i === 0 ? toNumber(a) - toNumber(b) : toNumber(a) + toNumber(b); return i === 0 ? toNumber(a) - toNumber(b) : toNumber(a) + toNumber(b);
return REDUCE_SKIP; return REDUCE_SKIP;
}); });
} else if (exp[0] === "%") {
return reduce(exp, (a, b, i) => {
if (a == null || b == null) return null;
if (isArray(a) || isArray(b) || i !== 0) return REDUCE_SKIP;
const n1 = toNumber(a);
const n2 = Math.trunc(toNumber(b));
if (n2 === 0) return null;
return n1 % n2;
});
} else if (exp[0] === "||") { } else if (exp[0] === "||") {
return reduce(exp, (a, b) => { return reduce(exp, (a, b) => {
if (a == null || b == null) return null; if (a == null || b == null) return null;
+1 -1
View File
@@ -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
+43 -6
View File
@@ -45,6 +45,7 @@ import {
InformRequest, InformRequest,
Preset, Preset,
GetRPCMethodsResponse, GetRPCMethodsResponse,
CpeFault,
} from "./types"; } from "./types";
import { IncomingMessage, ServerResponse } from "http"; import { IncomingMessage, ServerResponse } from "http";
import { Readable } from "stream"; import { Readable } from "stream";
@@ -187,7 +188,8 @@ async function writeResponse(
} }
const httpResponse = sessionContext.httpResponse; const httpResponse = sessionContext.httpResponse;
const connection = httpResponse.socket; // Don't use httpResponse.socket as it may be null, even before end() is called
const connection = sessionContext.httpRequest.socket;
httpResponse.setHeader("Content-Length", Buffer.byteLength(data)); httpResponse.setHeader("Content-Length", Buffer.byteLength(data));
httpResponse.writeHead(res.code, res.headers); httpResponse.writeHead(res.code, res.headers);
@@ -1033,7 +1035,7 @@ async function reportBadState(sessionContext: SessionContext): Promise<void> {
if (sessionContext.debug) if (sessionContext.debug)
debug.outgoingHttpResponse(httpResponse, sessionContext.deviceId, body); debug.outgoingHttpResponse(httpResponse, sessionContext.deviceId, body);
httpResponse.end(body); httpResponse.end(body);
return endSession(sessionContext); if (sessionContext.state) return endSession(sessionContext);
} }
async function responseUnauthorized( async function responseUnauthorized(
@@ -1255,6 +1257,41 @@ async function processRequest(
session.clearProvisions(sessionContext); session.clearProvisions(sessionContext);
} }
return nextRpc(sessionContext); return nextRpc(sessionContext);
} else if (rpc.unknownMethod) {
if (sessionContext.state === 1) {
logger.accessWarn({
sessionContext: sessionContext,
message: "Method not supported",
method: rpc.unknownMethod,
});
const f: CpeFault = {
faultCode: "Server",
faultString: "CWMP fault",
detail: {
faultCode: "8000",
faultString: "Method not supported",
},
};
const res = soap.response({
id: rpc.id,
acsFault: f,
cwmpVersion: sessionContext.cwmpVersion,
});
return writeResponse(sessionContext, res);
} else if (sessionContext.state === 2) {
const fault = {
code: "invalid_response",
message: "Response name does not match request name",
};
recordFault(sessionContext, fault);
session.clearProvisions(sessionContext);
return nextRpc(sessionContext);
} else {
return reportBadState(sessionContext);
}
} else { } else {
// CPE sent empty response // CPE sent empty response
if (sessionContext.state !== 1) return reportBadState(sessionContext); if (sessionContext.state !== 1) return reportBadState(sessionContext);
@@ -1290,13 +1327,13 @@ export function listener(
stats.concurrentRequests -= 1; stats.concurrentRequests -= 1;
}) })
.catch((err) => { .catch((err) => {
currentSessions.delete(httpResponse.socket); currentSessions.delete(httpRequest.socket);
stats.concurrentRequests -= 1; stats.concurrentRequests -= 1;
setTimeout(() => { setTimeout(() => {
throw err; throw err;
}); });
try { try {
httpResponse.socket.unref(); httpRequest.socket.unref();
httpResponse.writeHead(500, { Connection: "close" }); httpResponse.writeHead(500, { Connection: "close" });
httpResponse.end(`${err.name}: ${err.message}`); httpResponse.end(`${err.name}: ${err.message}`);
} catch (err) { } catch (err) {
@@ -1344,7 +1381,7 @@ async function clientError(
} }
httpResponse.end(msg); httpResponse.end(msg);
if (sessionContext) await endSession(sessionContext); if (sessionContext?.state) await endSession(sessionContext);
} }
function decodeString(buffer: Buffer, charset: string): string { function decodeString(buffer: Buffer, charset: string): string {
@@ -1442,7 +1479,7 @@ async function listenerAsync(
sessionContext.httpRequest = httpRequest; sessionContext.httpRequest = httpRequest;
sessionContext.httpResponse = httpResponse; sessionContext.httpResponse = httpResponse;
if ( if (
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({
+2
View File
@@ -351,6 +351,7 @@ async function fetchConfig(): Promise<[Config, UiConfig]> {
device: {}, device: {},
index: {}, index: {},
overview: {}, overview: {},
pageSize: null,
}; };
const _config = {}; const _config = {};
@@ -360,6 +361,7 @@ 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;
+13
View File
@@ -19,6 +19,7 @@
import { platform } from "os"; import { platform } from "os";
import { exec } from "child_process"; import { exec } from "child_process";
import { domainToASCII } from "url";
export interface PingResult { export interface PingResult {
packetsTransmitted: number; packetsTransmitted: number;
@@ -30,11 +31,23 @@ export interface PingResult {
mdev: number; mdev: number;
} }
function isValidHost(host: string): boolean {
// Valid chars in IPv4, IPv6, domain names
if (/^[a-zA-Z0-9\-.:[\]-]+$/.test(host)) return true;
// Check if input is an IDN convert to Punycode
// Can't merge with above because domainToASCII doesn't accept IP addresses
return /^[a-zA-Z0-9\-.:[\]-]+$/.test(domainToASCII(host));
}
export function ping( export function ping(
host: string, host: string,
callback: (err: Error, res?: PingResult, stdout?: string) => void callback: (err: Error, res?: PingResult, stdout?: string) => void
): void { ): void {
let cmd: string, parseRegExp1: RegExp, parseRegExp2: RegExp; let cmd: string, parseRegExp1: RegExp, parseRegExp2: RegExp;
// Validate input to prevent possible remote code execution
// Credit to Alex Hordijk for reporting this vulnerability
if (!isValidHost(host)) return callback(new Error("Invalid host"));
host = host.replace("[", "").replace("]", ""); host = host.replace("[", "").replace("]", "");
switch (platform()) { switch (platform()) {
case "linux": case "linux":
+1 -1
View File
@@ -18,7 +18,7 @@
*/ */
import * as crypto from "crypto"; import * as crypto from "crypto";
import * as later from "later"; import * as later from "@breejs/later";
function md532(str): number { function md532(str): number {
const digest = crypto.createHash("md5").update(str).digest(); const digest = crypto.createHash("md5").update(str).digest();
+18 -1
View File
@@ -730,6 +730,18 @@ function RequestDownloadResponse(): string {
return "<cwmp:RequestDownloadResponse></cwmp:RequestDownloadResponse>"; return "<cwmp:RequestDownloadResponse></cwmp:RequestDownloadResponse>";
} }
function AcsFault(f: CpeFault): string {
return `<soap-env:Body:Fault><faultcode>${encodeEntities(
f.faultCode
)}</faultcode><faultstring>${encodeEntities(
f.faultString
)}</faultstring><detail><cwmp:Fault><FaultCode>${encodeEntities(
f.detail.faultCode
)}</FaultCode><FaultString>${encodeEntities(
f.detail.faultString
)}</FaultString></cwmp:Fault></detail></soap-env:Body:Fault>`;
}
function faultStruct(xml: Element): FaultStruct { function faultStruct(xml: Element): FaultStruct {
let faultCode: string, let faultCode: string,
faultString: string, faultString: string,
@@ -838,6 +850,7 @@ export function request(
cpeRequest: null, cpeRequest: null,
cpeFault: null, cpeFault: null,
cpeResponse: null, cpeResponse: null,
unknownMethod: null,
}; };
if (!body.length) return rpc; if (!body.length) return rpc;
@@ -959,7 +972,8 @@ export function request(
rpc.cpeFault = fault(methodElement); rpc.cpeFault = fault(methodElement);
break; break;
default: default:
throw new Error(`8000 Method not supported ${methodElement.localName}`); rpc.unknownMethod = methodElement.localName;
break;
} }
return rpc; return rpc;
@@ -987,6 +1001,7 @@ export function response(rpc: {
id: string; id: string;
acsRequest?: AcsRequest; acsRequest?: AcsRequest;
acsResponse?: AcsResponse; acsResponse?: AcsResponse;
acsFault?: CpeFault;
cwmpVersion?: string; cwmpVersion?: string;
}): { code: number; headers: Record<string, string>; data: string } { }): { code: number; headers: Record<string, string>; data: string } {
const headers = { const headers = {
@@ -1055,6 +1070,8 @@ export function response(rpc: {
`Unknown method request ${(rpc.acsRequest as AcsRequest).name}` `Unknown method request ${(rpc.acsRequest as AcsRequest).name}`
); );
} }
} else if (rpc.acsFault) {
body = AcsFault(rpc.acsFault);
} }
headers["Content-Type"] = 'text/xml; charset="utf-8"'; headers["Content-Type"] = 'text/xml; charset="utf-8"';
+1
View File
@@ -526,6 +526,7 @@ export interface SoapMessage {
cpeRequest?: CpeRequest; cpeRequest?: CpeRequest;
cpeFault?: CpeFault; cpeFault?: CpeFault;
cpeResponse?: CpeResponse; cpeResponse?: CpeResponse;
unknownMethod?: string;
} }
export interface ScriptResult { export interface ScriptResult {
+4 -4
View File
@@ -17,7 +17,7 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import * as stream from "stream"; import { PassThrough } from "stream";
import Router from "koa-router"; import Router from "koa-router";
import * as db from "./db"; import * as db from "./db";
import * as apiFunctions from "./api-functions"; import * as apiFunctions from "./api-functions";
@@ -103,7 +103,7 @@ router.get(`/devices/:id.csv`, async (ctx) => {
.replace(/[:.]/g, "")}.csv` .replace(/[:.]/g, "")}.csv`
); );
ctx.body = new stream.PassThrough(); ctx.body = new PassThrough();
ctx.body.write( ctx.body.write(
"Parameter,Object,Object timestamp,Writable,Writable timestamp,Value,Value type,Value timestamp,Notification,Notification timestamp,Access list,Access list timestamp\n" "Parameter,Object,Object timestamp,Writable,Writable timestamp,Value,Value type,Value timestamp,Notification,Notification timestamp,Access list,Access list timestamp\n"
); );
@@ -216,7 +216,7 @@ for (const [resource, flags] of Object.entries(resources)) {
]); ]);
} }
ctx.body = new stream.PassThrough(); ctx.body = new PassThrough();
ctx.type = "application/json"; ctx.type = "application/json";
let c = 0; let c = 0;
@@ -276,7 +276,7 @@ for (const [resource, flags] of Object.entries(resources)) {
]); ]);
} }
ctx.body = new stream.PassThrough(); ctx.body = new PassThrough();
ctx.type = "text/csv"; ctx.type = "text/csv";
ctx.attachment( ctx.attachment(
`${resource}-${new Date(now).toISOString().replace(/[:.]/g, "")}.csv` `${resource}-${new Date(now).toISOString().replace(/[:.]/g, "")}.csv`
+9110 -3017
View File
File diff suppressed because it is too large. Load diff
+20 -20
View File
@@ -1,6 +1,6 @@
{ {
"name": "genieacs", "name": "genieacs",
"version": "1.2.6", "version": "1.2.8",
"description": "A TR-069 Auto Configuration Server (ACS)", "description": "A TR-069 Auto Configuration Server (ACS)",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -19,55 +19,55 @@
"bin": "bin" "bin": "bin"
}, },
"dependencies": { "dependencies": {
"@breejs/later": "^4.1.0",
"espresso-iisojs": "^1.0.4", "espresso-iisojs": "^1.0.4",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"ipaddr.js": "^2.0.1", "ipaddr.js": "^2.0.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"koa": "^2.13.1", "koa": "^2.13.4",
"koa-bodyparser": "^4.3.0", "koa-bodyparser": "^4.3.0",
"koa-compress": "^5.1.0", "koa-compress": "^5.1.0",
"koa-jwt": "^4.0.1", "koa-jwt": "^4.0.3",
"koa-router": "^10.1.1", "koa-router": "^10.1.1",
"koa-static": "^5.0.0", "koa-static": "^5.0.0",
"later": "^1.2.0", "mongodb": "^4.1.3",
"mongodb": "^4.1.2",
"parsimmon": "^1.18.0", "parsimmon": "^1.18.0",
"seedrandom": "^3.0.5" "seedrandom": "^3.0.5"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^20.0.0", "@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.0.4", "@rollup/plugin-node-resolve": "^13.0.6",
"@rollup/plugin-typescript": "^8.2.5", "@rollup/plugin-typescript": "^8.3.0",
"@types/codemirror": "^5.60.2", "@types/codemirror": "^5.60.5",
"@types/jsonwebtoken": "^8.5.5", "@types/jsonwebtoken": "^8.5.5",
"@types/koa": "^2.13.4", "@types/koa": "^2.13.4",
"@types/koa-compress": "^4.0.3", "@types/koa-compress": "^4.0.3",
"@types/koa-router": "^7.4.4", "@types/koa-router": "^7.4.4",
"@types/koa-static": "^4.0.2", "@types/koa-static": "^4.0.2",
"@types/mithril": "^2.0.8", "@types/mithril": "^2.0.8",
"@types/node": "^16.9.1", "@types/node": "^16.11.6",
"@types/parsimmon": "^1.10.6", "@types/parsimmon": "^1.10.6",
"@types/seedrandom": "^3.0.1", "@types/seedrandom": "^3.0.1",
"@typescript-eslint/eslint-plugin": "^4.31.1", "@typescript-eslint/eslint-plugin": "^5.2.0",
"@typescript-eslint/parser": "^4.31.1", "@typescript-eslint/parser": "^5.2.0",
"ava": "^3.15.0", "ava": "^3.15.0",
"codemirror": "^5.62.3", "codemirror": "^5.63.3",
"cssnano": "^5.0.8", "cssnano": "^5.0.8",
"eslint": "^7.32.0", "eslint": "^8.1.0",
"eslint-config-prettier": "^8.3.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.3.6", "postcss": "^8.3.11",
"postcss-import": "^14.0.2", "postcss-import": "^14.0.2",
"postcss-preset-env": "^6.7.0", "postcss-preset-env": "^6.7.0",
"prettier": "^2.4.1", "prettier": "^2.4.1",
"rollup": "^2.56.3", "rollup": "^2.58.3",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"sql.js": "^1.6.1", "sql.js": "^1.6.2",
"svgo": "^2.6.1", "svgo": "^2.7.0",
"ts-node": "^10.2.1", "ts-node": "^10.4.0",
"typescript": "^4.4.3", "typescript": "^4.4.4",
"yaml": "^1.10.2" "yaml": "^1.10.2"
}, },
"engines": { "engines": {
+2 -1
View File
@@ -23,6 +23,7 @@ import * as taskQueue from "../task-queue";
import { parse } from "../../lib/common/expression-parser"; import { parse } from "../../lib/common/expression-parser";
import memoize from "../../lib/common/memoize"; import memoize from "../../lib/common/memoize";
import { getIcon } from "../icons"; import { getIcon } from "../icons";
import { evaluateExpression } from "../store";
const memoizedParse = memoize(parse); const memoizedParse = memoize(parse);
@@ -55,7 +56,7 @@ const component: ClosureComponent = (): Component => {
view: (vnode) => { view: (vnode) => {
const device = vnode.attrs["device"]; const device = vnode.attrs["device"];
const limit = vnode.attrs["limit"] > 0 ? vnode.attrs["limit"] : 100; const limit = evaluateExpression(vnode.attrs["limit"], device) || 100;
const search = m("input", { const search = m("input", {
type: "text", type: "text",
+59 -12
View File
@@ -17,29 +17,76 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { ClosureComponent, Component } from "mithril"; import { Attributes, ClosureComponent, Component } from "mithril";
import { map } from "../../lib/common/expression-parser";
import memoize from "../../lib/common/memoize";
import { Expression } from "../../lib/types";
import { m } from "../components"; import { m } from "../components";
import * as store from "../store"; import { evaluateExpression, getTimestamp } from "../store";
const evaluateAttributes = memoize(
(
attrs: Record<string, Expression>,
obj: Record<string, unknown>,
now: number // eslint-disable-line @typescript-eslint/no-unused-vars
): Attributes => {
const res: Attributes = {};
for (const [k, v] of Object.entries(attrs)) {
const vv = map(v, (e) => {
if (
Array.isArray(e) &&
e[0] === "FUNC" &&
e[1] === "ENCODEURICOMPONENT"
) {
const a = evaluateExpression(e[2], obj);
if (a == null) return null;
return encodeURIComponent(a as string);
}
return e;
});
res[k] = evaluateExpression(vv, obj);
}
return res;
}
);
const component: ClosureComponent = (): Component => { const component: ClosureComponent = (): Component => {
return { return {
view: (vnode) => { view: (vnode) => {
if (vnode.attrs["filter"]) { const device = vnode.attrs["device"];
if ( if ("filter" in vnode.attrs) {
!store.evaluateExpression( if (!evaluateExpression(vnode.attrs["filter"], device || {}))
vnode.attrs["filter"],
vnode.attrs["device"]
)
)
return null; return null;
} }
const children = Object.values(vnode.attrs["components"]).map((c) => { const children = Object.values(vnode.attrs["components"]).map((c) => {
if (Array.isArray(c)) c = evaluateExpression(c, device || {});
if (typeof c !== "object") return `${c}`; if (typeof c !== "object") return `${c}`;
return m(c["type"], c); const type = evaluateExpression(c["type"], device || {});
if (!type) return null;
return m(type as string, c);
}); });
if (vnode.attrs["element"]) return m(vnode.attrs["element"], children);
else return children; let el = vnode.attrs["element"];
if (el == null) return children;
let attrs: Attributes;
if (Array.isArray(el)) {
el = evaluateExpression(el, device || {});
} else if (typeof el === "object") {
if (el["attributes"] != null) {
attrs = evaluateAttributes(
el["attributes"],
device || {},
getTimestamp()
);
}
el = evaluateExpression(el["tag"], device || {});
}
return m(el, attrs, children);
}, },
}; };
}; };
+7 -3
View File
@@ -19,18 +19,22 @@
import { ClosureComponent, Component } from "mithril"; import { ClosureComponent, Component } from "mithril";
import { m } from "../components"; import { m } from "../components";
import { evaluateExpression } from "../store";
const component: ClosureComponent = (): Component => { const component: ClosureComponent = (): Component => {
return { return {
view: (vnode) => { view: (vnode) => {
let deviceId; let deviceId;
if (vnode.attrs["device"]) const device = vnode.attrs["device"];
deviceId = vnode.attrs["device"]["DeviceID.ID"].value[0]; if (device) deviceId = device["DeviceID.ID"].value[0];
const children = Object.values(vnode.attrs["components"]).map((c) => { const children = Object.values(vnode.attrs["components"]).map((c) => {
if (Array.isArray(c)) c = evaluateExpression(c, device || {});
if (typeof c !== "object") return `${c}`; if (typeof c !== "object") return `${c}`;
const type = evaluateExpression(c["type"], device || {});
if (!type) return null;
const attrs = Object.assign({}, vnode.attrs, c); const attrs = Object.assign({}, vnode.attrs, c);
return m(attrs["type"], attrs); return m(type as string, attrs);
}); });
if (deviceId) { if (deviceId) {
return m( return m(
+4 -3
View File
@@ -20,7 +20,7 @@
import { ClosureComponent, Component } from "mithril"; import { ClosureComponent, Component } from "mithril";
import { m } from "../components"; import { m } from "../components";
import config from "../config"; import config from "../config";
import * as store from "../store"; import { evaluateExpression } from "../store";
const CHARTS = config.ui.overview.charts; const CHARTS = config.ui.overview.charts;
@@ -28,11 +28,12 @@ const component: ClosureComponent = (): Component => {
return { return {
view: (vnode) => { view: (vnode) => {
const device = vnode.attrs["device"]; const device = vnode.attrs["device"];
const chart = CHARTS[vnode.attrs["chart"]] as Record<string, unknown>; const chartName = evaluateExpression(vnode.attrs["chart"], device || {});
const chart = CHARTS[chartName as string] as Record<string, unknown>;
if (!chart) return null; if (!chart) return null;
for (const slice of Object.values(chart.slices)) { for (const slice of Object.values(chart.slices)) {
const filter = slice["filter"]; const filter = slice["filter"];
if (store.evaluateExpression(filter, device)) { if (evaluateExpression(filter, device || {})) {
const dot = m( const dot = m(
"svg", "svg",
{ {
+3 -1
View File
@@ -19,6 +19,7 @@
import { ClosureComponent, Component, VnodeDOM } from "mithril"; import { ClosureComponent, Component, VnodeDOM } from "mithril";
import { m } from "../components"; import { m } from "../components";
import { evaluateExpression } from "../store";
const component: ClosureComponent = (): Component => { const component: ClosureComponent = (): Component => {
return { return {
@@ -26,12 +27,13 @@ const component: ClosureComponent = (): Component => {
const device = vnode.attrs["device"]; const device = vnode.attrs["device"];
const rows = Object.values(vnode.attrs["parameters"]).map((parameter) => { const rows = Object.values(vnode.attrs["parameters"]).map((parameter) => {
const type = evaluateExpression(parameter["type"], device);
const p = m.context( const p = m.context(
{ {
device: device, device: device,
parameter: parameter["parameter"], parameter: parameter["parameter"],
}, },
parameter["type"] || "parameter", (type as string) || "parameter",
parameter parameter
); );
+7 -6
View File
@@ -20,7 +20,7 @@
import { ClosureComponent, Component } from "mithril"; import { ClosureComponent, Component } from "mithril";
import { m } from "../components"; import { m } from "../components";
import * as taskQueue from "../task-queue"; import * as taskQueue from "../task-queue";
import * as store from "../store"; import { evaluateExpression } from "../store";
import * as expressionParser from "../../lib/common/expression-parser"; import * as expressionParser from "../../lib/common/expression-parser";
import { getIcon } from "../icons"; import { getIcon } from "../icons";
@@ -35,7 +35,7 @@ const component: ClosureComponent = (): Component => {
}, },
view: (vnode) => { view: (vnode) => {
const device = vnode.attrs["device"]; const device = vnode.attrs["device"];
const object = store.evaluateExpression(vnode.state["object"], device); const object = evaluateExpression(vnode.state["object"], device);
const parameters = vnode.state["parameters"]; const parameters = vnode.state["parameters"];
if (typeof object !== "string" || !device[object]) return null; if (typeof object !== "string" || !device[object]) return null;
@@ -56,8 +56,7 @@ const component: ClosureComponent = (): Component => {
const rows = []; const rows = [];
for (const i of instances) { for (const i of instances) {
let filter = let filter = "filter" in vnode.attrs ? vnode.attrs["filter"] : true;
vnode.attrs["filter"] != null ? vnode.attrs["filter"] : true;
filter = expressionParser.map(filter, (e) => { filter = expressionParser.map(filter, (e) => {
if (Array.isArray(e) && e[0] === "PARAM") if (Array.isArray(e) && e[0] === "PARAM")
@@ -65,7 +64,7 @@ const component: ClosureComponent = (): Component => {
return e; return e;
}); });
if (!store.evaluateExpression(filter, device)) continue; if (!evaluateExpression(filter, device)) continue;
const row = parameters.map((p) => { const row = parameters.map((p) => {
const param = expressionParser.map(p.parameter, (e) => { const param = expressionParser.map(p.parameter, (e) => {
@@ -147,7 +146,9 @@ const component: ClosureComponent = (): Component => {
} }
let label; let label;
if (vnode.attrs["label"]) label = m("h2", vnode.attrs["label"]);
const l = evaluateExpression(vnode.attrs["label"], device);
if (l != null) label = m("h2", l);
return [ return [
label, label,
+1 -1
View File
@@ -77,7 +77,7 @@ const component: ClosureComponent = (): Component => {
const { value, timestamp, parameter } = evaluateParam( const { value, timestamp, parameter } = evaluateParam(
vnode.attrs["parameter"], vnode.attrs["parameter"],
vnode.attrs["device"], device,
store.getTimestamp() + store.getClockSkew() store.getTimestamp() + store.getClockSkew()
); );
+2 -1
View File
@@ -29,7 +29,8 @@ const component: ClosureComponent = (): Component => {
view: (vnode) => { view: (vnode) => {
const device = vnode.attrs["device"]; const device = vnode.attrs["device"];
let writable = true; let writable = true;
if (vnode.attrs["writable"] != null) writable = vnode.attrs["writable"]; if ("writable" in vnode.attrs)
writable = !!store.evaluateExpression(vnode.attrs["writable"], device);
const tags = []; const tags = [];
for (const p of Object.keys(device)) for (const p of Object.keys(device))
+4 -3
View File
@@ -268,17 +268,18 @@ export const component: ClosureComponent = (): Component => {
} }
function onSortChange(sortedAttrs): void { function onSortChange(sortedAttrs): void {
const _sort = Object.assign({}, sort); let _sort = Object.assign({}, sort);
for (const [index, direction] of Object.entries(sortedAttrs)) { for (const [index, direction] of Object.entries(sortedAttrs)) {
const param = memoizedGetSortable(attributes[index].parameter); const param = memoizedGetSortable(attributes[index].parameter);
if (param) { if (param) {
// Changing the priority of columns // Changing the priority of columns
delete _sort[param]; delete _sort[param];
_sort[param] = direction; if (direction) _sort = Object.assign({ [param]: direction }, _sort);
} }
} }
const ops = { sort: JSON.stringify(_sort) }; const ops = {};
if (Object.keys(_sort).length) ops["sort"] = JSON.stringify(_sort);
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
m.route.set("/devices", ops); m.route.set("/devices", ops);
} }
+5 -3
View File
@@ -107,14 +107,16 @@ export const component: ClosureComponent = (): Component => {
} }
function onSortChange(sortAttrs): void { function onSortChange(sortAttrs): void {
const _sort = Object.assign({}, sort); let _sort = Object.assign({}, sort);
for (const [index, direction] of Object.entries(sortAttrs)) { for (const [index, direction] of Object.entries(sortAttrs)) {
// Changing the priority of columns // Changing the priority of columns
delete _sort[attributes[index].id]; delete _sort[attributes[index].id];
_sort[attributes[index].id] = direction; if (direction)
_sort = Object.assign({ [attributes[index].id]: direction }, _sort);
} }
const ops = { sort: JSON.stringify(_sort) }; const ops = {};
if (Object.keys(_sort).length) ops["sort"] = JSON.stringify(_sort);
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
m.route.set("/faults", ops); m.route.set("/faults", ops);
} }
+5 -3
View File
@@ -151,14 +151,16 @@ export const component: ClosureComponent = (): Component => {
sortAttributes[i] = sort[attributes[i].id] || 0; sortAttributes[i] = sort[attributes[i].id] || 0;
function onSortChange(sortAttrs): void { function onSortChange(sortAttrs): void {
const _sort = Object.assign({}, sort); let _sort = Object.assign({}, sort);
for (const [index, direction] of Object.entries(sortAttrs)) { for (const [index, direction] of Object.entries(sortAttrs)) {
// Changing the priority of columns // Changing the priority of columns
delete _sort[attributes[index].id]; delete _sort[attributes[index].id];
_sort[attributes[index].id] = direction; if (direction)
_sort = Object.assign({ [attributes[index].id]: direction }, _sort);
} }
const ops = { sort: JSON.stringify(_sort) }; const ops = {};
if (Object.keys(_sort).length) ops["sort"] = JSON.stringify(_sort);
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
m.route.set("/admin/files", ops); m.route.set("/admin/files", ops);
} }
+1 -3
View File
@@ -79,8 +79,6 @@ function renderTable(
continue; continue;
} }
let direction = 1;
let symbol = getIcon("unsorted"); let symbol = getIcon("unsorted");
if (sortAttributes[i] > 0) symbol = getIcon("sorted-asc"); if (sortAttributes[i] > 0) symbol = getIcon("sorted-asc");
else if (sortAttributes[i] < 0) symbol = getIcon("sorted-dsc"); else if (sortAttributes[i] < 0) symbol = getIcon("sorted-dsc");
@@ -89,7 +87,7 @@ function renderTable(
"button", "button",
{ {
onclick: () => { onclick: () => {
if (sortAttributes[i] > 0) direction *= -1; const direction = ((sortAttributes[i] + 2) % 3) - 1;
return onSortChange({ [i]: direction }); return onSortChange({ [i]: direction });
}, },
}, },
+5 -3
View File
@@ -205,14 +205,16 @@ export const component: ClosureComponent = (): Component => {
} }
function onSortChange(sortAttrs): void { function onSortChange(sortAttrs): void {
const _sort = Object.assign({}, sort); let _sort = Object.assign({}, sort);
for (const [index, direction] of Object.entries(sortAttrs)) { for (const [index, direction] of Object.entries(sortAttrs)) {
// Changing the priority of columns // Changing the priority of columns
delete _sort[attributes[index].id]; delete _sort[attributes[index].id];
_sort[attributes[index].id] = direction; if (direction)
_sort = Object.assign({ [attributes[index].id]: direction }, _sort);
} }
const ops = { sort: JSON.stringify(_sort) }; const ops = {};
if (Object.keys(_sort).length) ops["sort"] = JSON.stringify(_sort);
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
m.route.set("/admin/permissions", ops); m.route.set("/admin/permissions", ops);
} }
+5 -3
View File
@@ -190,14 +190,16 @@ export const component: ClosureComponent = (): Component => {
} }
function onSortChange(sortAttrs): void { function onSortChange(sortAttrs): void {
const _sort = Object.assign({}, sort); let _sort = Object.assign({}, sort);
for (const [index, direction] of Object.entries(sortAttrs)) { for (const [index, direction] of Object.entries(sortAttrs)) {
// Changing the priority of columns // Changing the priority of columns
delete _sort[attributes[index].id]; delete _sort[attributes[index].id];
_sort[attributes[index].id] = direction; if (direction)
_sort = Object.assign({ [attributes[index].id]: direction }, _sort);
} }
const ops = { sort: JSON.stringify(_sort) }; const ops = {};
if (Object.keys(_sort).length) ops["sort"] = JSON.stringify(_sort);
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
m.route.set("/admin/presets", ops); m.route.set("/admin/presets", ops);
} }
+5 -3
View File
@@ -173,14 +173,16 @@ export const component: ClosureComponent = (): Component => {
sortAttributes[i] = sort[attributes[i].id] || 0; sortAttributes[i] = sort[attributes[i].id] || 0;
function onSortChange(sortAttrs): void { function onSortChange(sortAttrs): void {
const _sort = Object.assign({}, sort); let _sort = Object.assign({}, sort);
for (const [index, direction] of Object.entries(sortAttrs)) { for (const [index, direction] of Object.entries(sortAttrs)) {
// Changing the priority of columns // Changing the priority of columns
delete _sort[attributes[index].id]; delete _sort[attributes[index].id];
_sort[attributes[index].id] = direction; if (direction)
_sort = Object.assign({ [attributes[index].id]: direction }, _sort);
} }
const ops = { sort: JSON.stringify(_sort) }; const ops = {};
if (Object.keys(_sort).length) ops["sort"] = JSON.stringify(_sort);
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
m.route.set("/admin/provisions", ops); m.route.set("/admin/provisions", ops);
} }
+5 -3
View File
@@ -186,14 +186,16 @@ export const component: ClosureComponent = (): Component => {
} }
function onSortChange(sortAttrs): void { function onSortChange(sortAttrs): void {
const _sort = Object.assign({}, sort); let _sort = Object.assign({}, sort);
for (const [index, direction] of Object.entries(sortAttrs)) { for (const [index, direction] of Object.entries(sortAttrs)) {
// Changing the priority of columns // Changing the priority of columns
delete _sort[attributes[index].id]; delete _sort[attributes[index].id];
_sort[attributes[index].id] = direction; if (direction)
_sort = Object.assign({ [attributes[index].id]: direction }, _sort);
} }
const ops = { sort: JSON.stringify(_sort) }; const ops = {};
if (Object.keys(_sort).length) ops["sort"] = JSON.stringify(_sort);
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
m.route.set("/admin/users", ops); m.route.set("/admin/users", ops);
} }
+5 -3
View File
@@ -173,14 +173,16 @@ export const component: ClosureComponent = (): Component => {
sortAttributes[i] = sort[attributes[i].id] || 0; sortAttributes[i] = sort[attributes[i].id] || 0;
function onSortChange(sortAttrs): void { function onSortChange(sortAttrs): void {
const _sort = Object.assign({}, sort); let _sort = Object.assign({}, sort);
for (const [index, direction] of Object.entries(sortAttrs)) { for (const [index, direction] of Object.entries(sortAttrs)) {
// Changing the priority of columns // Changing the priority of columns
delete _sort[attributes[index].id]; delete _sort[attributes[index].id];
_sort[attributes[index].id] = direction; if (direction)
_sort = Object.assign({ [attributes[index].id]: direction }, _sort);
} }
const ops = { sort: JSON.stringify(_sort) }; const ops = {};
if (Object.keys(_sort).length) ops["sort"] = JSON.stringify(_sort);
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
m.route.set("/admin/virtualParameters", ops); m.route.set("/admin/virtualParameters", ops);
} }