Compare commits

...
41 Commits
Author SHA1 Message Date
Zaid Abdulla 444183da0a 1.2.9 2022-08-22 09:51:55 -07:00
Zaid Abdulla c02f667623 Changelog for v1.2.9 2022-08-22 09:51:25 -07:00
Zaid Abdulla 25ea0c8b64 Update dependencies 2022-08-22 09:44:25 -07:00
Zaid Abdulla 2789961596 Skip invalid timestamps in declare() 2022-08-12 16:30:00 -07:00
Zaid Abdulla d91be02599 Fix exception matching IPv6 against IPv4 CIDR 2022-08-12 11:32:15 -07:00
Zaid Abdulla 5806fe2df6 Better locking in local cache
Fixes occassional lock expired errors after updating presets, etc.
2022-08-12 01:11:04 -07:00
Zaid Abdulla eb5caa6b5a Skip root GPN workaround 2022-08-01 11:32:05 -07:00
Zaid Abdulla ffbcf2bbc0 Fix Mongo query bug when using <> operator 2022-07-15 11:36:15 -07:00
Zaid Abdulla 6f29267266 Fix HTML Quirks mode issues 2022-06-29 11:07:42 -07:00
Zaid Abdulla b0d50b07e7 Fix synthesized expressions being non-minimal
Let's try this again. The implementation of canRaise callback was
overly complicated and was causing the minimizer to ocassionally
get stuck in a local optima.
2022-06-14 11:23:33 -07:00
Zaid Abdulla c06538f9cc Fix browser quirks mode warning 2022-04-08 13:14:27 -07:00
Divya Jagadish 3d0ac9b415 Uses debounce function for parameter search 2022-04-08 13:09:04 -07:00
Divya Jagadish dabf1b2f43 Improve sorting behavior by adding delay 2022-04-08 13:08:41 -07:00
Divya Jagadish 7941d929d2 Fix ping bug on certain platforms 2022-04-08 13:01:05 -07:00
Divya Jagadish 7ccdbace70 Authenticate ping API 2022-04-08 12:59:32 -07:00
Zaid Abdulla 344f0ec5e0 Fix DB update calls containing unchanged params 2022-03-11 11:43:49 -08:00
Zaid Abdulla 4b533d0aad Fix issues caused by invalid xsd:dateTime values
Invalid dateTime strings were saved in the DB as NaN causing uncaught
exception in CSV download.
2022-03-11 11:42:10 -08:00
Zaid Abdulla 2b37f11e2e Clean up MongoDB query projection
This prevents potential 'path collision' errors from MongoDB.
2022-02-17 18:14:53 -08:00
Zaid Abdulla adaf801d6e Stream query results as they become available 2021-11-29 23:36:27 -08:00
Zaid Abdulla 94756e463f Pass BROTLI_OPERATION_FLUSH to koa-compress 2021-11-29 23:11:20 -08:00
Zaid Abdulla 3c55306e22 Fix false warning message 2021-11-15 15:42:33 -08:00
Zaid Abdulla aa12b7c724 Fix query unpacking breaking $regex operator
If the regex string can be parsed as a date or a number, the unpacked
query will be rejected by MongoDB because $regex operator only accepts
a string.
2021-11-08 14:21:14 -08:00
Zaid Abdulla 8f827997cd Fix tags set via script saved in encoded form 2021-11-08 13:55:30 -08:00
Zaid Abdulla c9a4be0475 Deubg log for HTTP and HTTPS client errors 2021-11-08 12:21:02 -08:00
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
55 changed files with 10564 additions and 4558 deletions

No files matched your search

+46
View File
@@ -1,5 +1,51 @@
# Change Log # Change Log
## 1.2.9 (2022-08-22)
- New config option `cwmp.skipRootGpn` to enable a workaround for some
problematic CPEs that reject GPN requests on data model root.
- Stream query results and CSV downloads as data becomes available instead of
buffering the entire response.
- Log HTTP/HTTPS client errors in debug log.
- Fix occasional lock expired errors after updating presets, etc.
- Fix bug where queries containing `<>` operator may return incorrect results.
- Fix a performance hit caused by DB calls containing the entire CPE data model
rather then just the updated parameters.
- Fix bug where tags containing special characters are saved in their encoded
form when set via a Provision script.
- Fix issue causing invalid `xsd:dateTime` values to be saved in DB as `NaN`
rather than maintain the original string value.
- Fix bug using `$regex` operator with a numeric or a datetime string.
- Fix ping not working on certain platforms.
- Ping requests are now authenticated.
- Fix error when the `FORWARDED_HEADER` environment variable contains IPv4 CIDR
while the listening interface is IPv6 and vice versa.
- Fix false warning "unexpected parameter in response" showing in
`genieacs-cwmp` logs.
- Other minor fixes and stability improvements.
## 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
+1
View File
@@ -83,6 +83,7 @@ if (!cluster.worker) {
host: SERVICE_ADDRESS, host: SERVICE_ADDRESS,
ssl: key && cert ? { key, cert } : null, ssl: key && cert ? { key, cert } : null,
onConnection: cwmp.onConnection, onConnection: cwmp.onConnection,
onClientError: cwmp.onClientError,
timeout: 30000, timeout: 30000,
keepAliveTimeout: 0, keepAliveTimeout: 0,
}; };
+14 -2
View File
@@ -28,6 +28,7 @@ import commonjs from "@rollup/plugin-commonjs";
import postcss from "postcss"; 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 postcssColorMod from "postcss-color-mod-function";
import cssnano from "cssnano"; import cssnano from "cssnano";
import { optimize } from "svgo"; import { optimize } from "svgo";
import * as xmlParser from "../lib/xml-parser"; import * as xmlParser from "../lib/xml-parser";
@@ -74,6 +75,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 +85,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 +151,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)
@@ -174,10 +186,10 @@ async function generateCss(): Promise<void> {
stage: 3, stage: 3,
features: { features: {
"nesting-rules": true, "nesting-rules": true,
"color-mod-function": true,
}, },
}), }),
cssnano, postcssColorMod,
...(MODE === "production" ? [cssnano] : []),
]).process(cssIn, { from: cssInPath, to: cssOutPath }); ]).process(cssIn, { from: cssInPath, to: cssOutPath });
fs.writeFileSync(cssOutPath, cssOut.css); fs.writeFileSync(cssOutPath, cssOut.css);
} }
+6
View File
@@ -168,3 +168,9 @@ IPv
downloadSuccessOnTimeout downloadSuccessOnTimeout
TransferComplete TransferComplete
nginx nginx
ENCODEURICOMPONENT
pageSize
skipRootGpn
GPN
NaN
CIDR
+13 -26
View File
@@ -20,11 +20,12 @@
import { Collection } from "mongodb"; import { Collection } from "mongodb";
import { onConnect } from "./db"; import { onConnect } from "./db";
import * as config from "./config"; import * as config from "./config";
import { Cache } from "./mongodb-types";
const CLOCK_SKEW_TOLERANCE = 30000; const CLOCK_SKEW_TOLERANCE = 30000;
const MAX_CACHE_TTL = +config.get("MAX_CACHE_TTL"); const MAX_CACHE_TTL = +config.get("MAX_CACHE_TTL");
let cacheCollection: Collection; let cacheCollection: Collection<Cache>;
onConnect(async (db) => { onConnect(async (db) => {
cacheCollection = db.collection("cache"); cacheCollection = db.collection("cache");
@@ -33,8 +34,7 @@ onConnect(async (db) => {
export async function get(key: string): Promise<string> { export async function get(key: string): Promise<string> {
const res = await cacheCollection.findOne({ _id: key }); const res = await cacheCollection.findOne({ _id: key });
if (res) return res["value"]; return res?.value;
return null;
} }
export async function del(key: string): Promise<void> { export async function del(key: string): Promise<void> {
@@ -52,15 +52,14 @@ export async function set(
); );
await cacheCollection.replaceOne( await cacheCollection.replaceOne(
{ _id: key }, { _id: key },
{ _id: key, value, expire, timestamp }, { value, expire, timestamp },
{ upsert: true } { upsert: true }
); );
} }
export async function pop(key: string): Promise<any> { export async function pop(key: string): Promise<string> {
const res = await cacheCollection.findOneAndDelete({ _id: key }); const res = await cacheCollection.findOneAndDelete({ _id: key });
if (res?.["value"]) return res["value"]["value"]; return res.value?.value;
return null;
} }
export async function acquireLock( export async function acquireLock(
@@ -72,35 +71,23 @@ export async function acquireLock(
try { try {
const now = Date.now(); const now = Date.now();
const r = await cacheCollection.findOneAndUpdate( const r = await cacheCollection.findOneAndUpdate(
{ _id: lockName, token }, { _id: lockName, value: token },
{ {
$set: { $set: {
value: token,
expire: new Date(now + ttl + CLOCK_SKEW_TOLERANCE), expire: new Date(now + ttl + CLOCK_SKEW_TOLERANCE),
}, },
$currentDate: { timestamp: true as unknown as Date }, $currentDate: { timestamp: true },
}, },
{ upsert: true, returnDocument: "after" } { upsert: true, returnDocument: "after" }
); );
const v = r.value; if (Math.abs(r.value.timestamp.getTime() - now) > CLOCK_SKEW_TOLERANCE)
if (Math.abs(v["timestamp"].getTime() - now) > CLOCK_SKEW_TOLERANCE)
throw new Error("Database clock skew too great"); throw new Error("Database clock skew too great");
} catch (err) { } catch (err) {
if (err.code === 11000) { if (err.code !== 11000) throw err;
if (timeout > 0) { if (!(timeout > 0)) return null;
return new Promise((resolve, reject) => {
const w = 50 + Math.random() * 50; const w = 50 + Math.random() * 50;
setTimeout(() => { await new Promise((resolve) => setTimeout(resolve, w));
acquireLock(lockName, ttl, timeout - w, token).then( return acquireLock(lockName, ttl, timeout - w, token);
resolve,
reject
);
}, w);
});
}
throw new Error("Failed to acquire lock");
}
throw err;
} }
return token; return token;
+1 -7
View File
@@ -1108,13 +1108,7 @@ function getCanRaiseCallback(
return (idx: number, set: Set<number>): boolean => { return (idx: number, set: Set<number>): boolean => {
const i = idx >> 2; const i = idx >> 2;
const vars = context.getIsNullRelations(i); const vars = context.getIsNullRelations(i);
if (!context.isIsNull(i)) { if (!context.isIsNull(i)) return !(idx & 1) || !set.has(idx ^ 3);
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) ^ 2)) return false;
return true;
}
for (const k of vars) { for (const k of vars) {
if (set.has((k << 2) ^ 1)) continue; if (set.has((k << 2) ^ 1)) continue;
+16
View File
@@ -0,0 +1,16 @@
export default function debounce<T>(
func: (args: T[]) => void,
timeout: number
): (arg: T) => void {
let timer: ReturnType<typeof setTimeout>;
let args: T[] = [];
return (arg: T) => {
args.push(arg);
clearTimeout(timer);
timer = setTimeout(() => {
const argscopy = args;
args = [];
func(argscopy);
}, timeout);
};
}
+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
+76 -19
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";
@@ -53,6 +54,7 @@ import { decode, encodingExists } from "iconv-lite";
import { parseXmlDeclaration } from "./xml-parser"; import { parseXmlDeclaration } from "./xml-parser";
import * as debug from "./debug"; import * as debug from "./debug";
import { getRequestOrigin } from "./forwarded"; import { getRequestOrigin } from "./forwarded";
import { getSocketEndpoints } from "./server";
const gzipPromisified = promisify(zlib.gzip); const gzipPromisified = promisify(zlib.gzip);
const deflatePromisified = promisify(zlib.deflate); const deflatePromisified = promisify(zlib.deflate);
@@ -187,7 +189,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);
@@ -210,12 +213,13 @@ async function writeResponse(
currentSessions.set(connection, sessionContext); currentSessions.set(connection, sessionContext);
if (now >= sessionContext.extendLock) { if (now >= sessionContext.extendLock) {
sessionContext.extendLock = now + 10000; sessionContext.extendLock = now + 10000;
await cache.acquireLock( const lockToken = await cache.acquireLock(
`cwmp_session_${sessionContext.deviceId}`, `cwmp_session_${sessionContext.deviceId}`,
sessionContext.timeout * 1000 + 15000, sessionContext.timeout * 1000 + 15000,
0, 0,
sessionContext.sessionId sessionContext.sessionId
); );
if (!lockToken) throw new Error("Failed to extend lock");
} }
} }
} }
@@ -532,7 +536,7 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
session.configContextCallback(sessionContext, e) session.configContextCallback(sessionContext, e)
) )
), ),
]); ]) as [string, ...Expression[]][];
if (blackList[p.channel] === 2) { if (blackList[p.channel] === 2) {
appendProvisionsToFaults[p.channel] = ( appendProvisionsToFaults[p.channel] = (
appendProvisionsToFaults[p.channel] || [] appendProvisionsToFaults[p.channel] || []
@@ -881,16 +885,8 @@ async function sendAcsRequest(
return writeResponse(sessionContext, res); return writeResponse(sessionContext, res);
} }
// Only needed to prevent tree shaking from removing the remoteAddress
// workaround in onConnection function.
const remoteAddressWorkaround = new WeakMap<Socket, string>();
// When socket closes, store active sessions in cache // When socket closes, store active sessions in cache
export function onConnection(socket: Socket): void { export function onConnection(socket: Socket): void {
// The property remoteAddress may be undefined after the connection is
// closed, unless we read it at least once (caching?)
remoteAddressWorkaround.set(socket, socket.remoteAddress);
socket.on("close", async () => { socket.on("close", async () => {
const sessionContext = currentSessions.get(socket); const sessionContext = currentSessions.get(socket);
if (!sessionContext) return; if (!sessionContext) return;
@@ -945,6 +941,32 @@ export function onConnection(socket: Socket): void {
}); });
} }
export function onClientError(err: Error, socket: Socket): void {
const remoteAddress = getSocketEndpoints(socket).remoteAddress;
localCache
.getCurrentSnapshot()
.then((cacheSnapshot) => {
const debugEnabled = !!localCache.getConfig(
cacheSnapshot,
"cwmp.debug",
{
remoteAddress: remoteAddress,
},
Date.now(),
(e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "REMOTE_ADDRESS")
return remoteAddress;
return e;
}
);
if (debugEnabled) debug.clientError(remoteAddress, err);
})
.catch((err) => {
throw err;
});
}
setInterval(() => { setInterval(() => {
if (stats.droppedRequests) { if (stats.droppedRequests) {
logger.warn({ logger.warn({
@@ -1033,7 +1055,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(
@@ -1128,15 +1150,15 @@ async function processRequest(
} }
} }
try {
sessionContext.extendLock = sessionContext.timestamp + 10000; sessionContext.extendLock = sessionContext.timestamp + 10000;
await cache.acquireLock( const lockToken = await cache.acquireLock(
`cwmp_session_${sessionContext.deviceId}`, `cwmp_session_${sessionContext.deviceId}`,
sessionContext.timeout * 1000 + 15000, sessionContext.timeout * 1000 + 15000,
0, 0,
sessionContext.sessionId sessionContext.sessionId
); );
} catch (err) {
if (!lockToken) {
logger.accessError({ logger.accessError({
message: "CPE already in session", message: "CPE already in session",
sessionContext: sessionContext, sessionContext: sessionContext,
@@ -1255,6 +1277,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 +1347,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 +1401,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 +1499,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({
+95 -43
View File
@@ -30,19 +30,20 @@ import {
Expression, Expression,
} from "./types"; } from "./types";
import Path from "./common/path"; import Path from "./common/path";
import * as MongoTypes from "./mongodb-types";
export let tasksCollection: Collection, export let tasksCollection: Collection<MongoTypes.Task>,
devicesCollection: Collection, devicesCollection: Collection,
presetsCollection: Collection, presetsCollection: Collection,
objectsCollection: Collection, objectsCollection: Collection,
provisionsCollection: Collection, provisionsCollection: Collection,
virtualParametersCollection: Collection, virtualParametersCollection: Collection,
faultsCollection: Collection, faultsCollection: Collection<MongoTypes.Fault>,
filesCollection: Collection, filesCollection: Collection,
operationsCollection: Collection, operationsCollection: Collection<MongoTypes.Operation>,
permissionsCollection: Collection, permissionsCollection: Collection,
usersCollection: Collection, usersCollection: Collection,
configCollection: Collection; configCollection: Collection<MongoTypes.Config>;
let clientPromise: Promise<MongoClient>; let clientPromise: Promise<MongoClient>;
@@ -91,7 +92,9 @@ export async function disconnect(): Promise<void> {
// Optimize projection by removing overlaps // Optimize projection by removing overlaps
// This can modify the object // This can modify the object
function optimizeProjection(obj: { [path: string]: 1 }): { [path: string]: 1 } { export function optimizeProjection(obj: { [path: string]: 1 }): {
[path: string]: 1;
} {
if (obj[""]) return { "": obj[""] }; if (obj[""]) return { "": obj[""] };
const keys = Object.keys(obj).sort(); const keys = Object.keys(obj).sort();
@@ -146,7 +149,7 @@ export async function fetchDevice(
if (obj["_value"] != null) { if (obj["_value"] != null) {
attrs.value = [obj["_timestamp"] || 1, [obj["_value"], obj["_type"]]]; attrs.value = [obj["_timestamp"] || 1, [obj["_value"], obj["_type"]]];
if (obj["_type"] === "xsd:dateTime") if (obj["_type"] === "xsd:dateTime" && obj["_value"] instanceof Date)
attrs.value[1][0] = +attrs.value[1][0]; attrs.value[1][0] = +attrs.value[1][0];
obj["_object"] = false; obj["_object"] = false;
@@ -446,14 +449,18 @@ export async function saveDevice(
if (value2 != null) { if (value2 != null) {
if (!update["$addToSet"]["_tags"]) if (!update["$addToSet"]["_tags"])
update["$addToSet"]["_tags"] = { $each: [] }; update["$addToSet"]["_tags"] = { $each: [] };
update["$addToSet"]["_tags"]["$each"].push(path.segments[1]); update["$addToSet"]["_tags"]["$each"].push(
decodeTag(path.segments[1] as string)
);
} else { } else {
if (!update["$pull"]["_tags"]) { if (!update["$pull"]["_tags"]) {
update["$pull"]["_tags"] = { update["$pull"]["_tags"] = {
$in: [], $in: [],
}; };
} }
update["$pull"]["_tags"]["$in"].push(path.segments[1]); update["$pull"]["_tags"]["$in"].push(
decodeTag(path.segments[1] as string)
);
} }
} }
@@ -637,12 +644,16 @@ export async function getFaults(
const faults: { [channel: string]: SessionFault } = {}; const faults: { [channel: string]: SessionFault } = {};
for (const r of res) { for (const r of res) {
const channel = r._id.slice(deviceId.length + 1); const channel = r._id.slice(deviceId.length + 1);
delete r._id; const fault: SessionFault = {
delete r.channel; code: r.code,
delete r.device; message: r.message,
r.timestamp = +r.timestamp; ...(r.detail && { detail: r.detail }),
r.provisions = JSON.parse(r.provisions); timestamp: +r.timestamp,
faults[channel] = r as SessionFault; provisions: JSON.parse(r.provisions),
retries: r.retries,
...(r.expiry && { expiry: +r.expiry }),
};
faults[channel] = fault;
} }
return faults; return faults;
@@ -654,12 +665,18 @@ export async function saveFault(
fault: SessionFault fault: SessionFault
): Promise<void> { ): Promise<void> {
const id = `${deviceId}:${channel}`; const id = `${deviceId}:${channel}`;
const f = Object.assign({}, fault) as Record<string, any>; const f: MongoTypes.Fault = {
f["_id"] = id; _id: id,
f["device"] = deviceId; device: deviceId,
f["channel"] = channel; channel: channel,
f["timestamp"] = new Date(fault.timestamp); timestamp: new Date(fault.timestamp),
f["provisions"] = JSON.stringify(fault.provisions); code: fault.code,
message: fault.message,
...(fault.detail && { detail: fault.detail }),
retries: fault.retries,
...(fault.expiry && { expiry: new Date(fault.expiry) }),
provisions: JSON.stringify(fault.provisions),
};
await faultsCollection.replaceOne({ _id: id }, f, { upsert: true }); await faultsCollection.replaceOne({ _id: id }, f, { upsert: true });
} }
@@ -677,20 +694,47 @@ export async function getDueTasks(
const cur = tasksCollection.find({ device: deviceId }).sort({ timestamp: 1 }); const cur = tasksCollection.find({ device: deviceId }).sort({ timestamp: 1 });
const tasks = [] as Task[]; const tasks = [] as Task[];
for await (const task of cur) { for await (const t of cur) {
if (task.timestamp) task.timestamp = +task.timestamp; if (+t.timestamp >= timestamp) return [tasks, +t.timestamp];
if (task.expiry) task.expiry = +task.expiry; const task: Task = {
if (task.timestamp >= timestamp) return [tasks, +task.timestamp]; _id: t._id.toString(),
task._id = String(task._id); name: t.name,
...(t.timestamp && { timestamp: +t.timestamp }),
...(t.expiry && { expiry: +t.expiry }),
...(t.name === "getParameterValues" && {
parameterNames: t.parameterNames,
}),
...(t.name === "setParameterValues" && {
parameterValues: t.parameterValues,
}),
...(t.name === "refreshObject" && {
objectName: t.objectName,
}),
...(t.name === "download" && {
fileType: t.fileType,
fileName: t.fileName,
targetFileName: t.targetFileName,
}),
...(t.name === "addObject" && {
objectName: t.objectName,
parameterValues: t.parameterValues,
}),
...(t.name === "deleteObject" && {
objectName: t.objectName,
}),
...(t.name === "provisions" && {
provisions: t.provisions,
}),
};
tasks.push(task as Task); tasks.push(task);
// For API compatibility // For API compatibility
if (task.name === "download" && task.file) { if (task.name === "download" && t["file"]) {
let q; let q;
if (ObjectId.isValid(task.file)) if (ObjectId.isValid(t["file"]))
q = { _id: { $in: [task.file, new ObjectId(task.file)] } }; q = { _id: { $in: [t["file"], new ObjectId(t["file"])] } };
else q = { _id: task.file }; else q = { _id: t["file"] };
const res = await filesCollection.find(q).toArray(); const res = await filesCollection.find(q).toArray();
@@ -724,17 +768,22 @@ export async function getOperations(
const operations: { [commandKey: string]: Operation } = {}; const operations: { [commandKey: string]: Operation } = {};
for (const r of res) { for (const r of res) {
const commandKey = r._id.slice(deviceId.length + 1); const commandKey = r._id.slice(deviceId.length + 1);
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 as Operation; delete r._id;
operations[commandKey] = r as unknown as Operation;
continue; continue;
} }
r.timestamp = +r.timestamp; const operation: Operation = {
if (r.args) r.args = JSON.parse(r.args); name: r.name,
r.provisions = JSON.parse(r.provisions); timestamp: +r.timestamp,
r.retries = JSON.parse(r.retries); channels:
operations[commandKey] = r as Operation; typeof r.channels === "string" ? JSON.parse(r.channels) : r.channels,
retries: JSON.parse(r.retries),
provisions: JSON.parse(r.provisions),
...(r.args && { args: JSON.parse(r.args) }),
};
operations[commandKey] = operation;
} }
return operations; return operations;
} }
@@ -745,12 +794,15 @@ export async function saveOperation(
operation: Operation operation: Operation
): Promise<void> { ): Promise<void> {
const id = `${deviceId}:${commandKey}`; const id = `${deviceId}:${commandKey}`;
const o = Object.assign({}, operation) as Record<string, any>; const o: MongoTypes.Operation = {
o["_id"] = id; _id: id,
o["timestamp"] = new Date(operation.timestamp); name: operation.name,
o["provisions"] = JSON.stringify(operation.provisions); timestamp: new Date(operation.timestamp),
o["retries"] = JSON.stringify(operation.retries); channels: JSON.stringify(operation.channels),
o["args"] = JSON.stringify(operation.args); provisions: JSON.stringify(operation.provisions),
retries: JSON.stringify(operation.retries),
args: JSON.stringify(operation.args),
};
await operationsCollection.replaceOne({ _id: id }, o, { await operationsCollection.replaceOne({ _id: id }, o, {
upsert: true, upsert: true,
}); });
+23 -3
View File
@@ -22,6 +22,7 @@ import { Socket } from "net";
import { appendFileSync } from "fs"; import { appendFileSync } from "fs";
import { stringify } from "./common/yaml"; import { stringify } from "./common/yaml";
import * as config from "./config"; import * as config from "./config";
import { getSocketEndpoints } from "./server";
const DEBUG_FILE = "" + config.get("DEBUG_FILE"); const DEBUG_FILE = "" + config.get("DEBUG_FILE");
const DEBUG_FORMAT = "" + config.get("DEBUG_FORMAT"); const DEBUG_FORMAT = "" + config.get("DEBUG_FORMAT");
@@ -45,13 +46,14 @@ export function incomingHttpRequest(
if (!DEBUG_FILE) return; if (!DEBUG_FILE) return;
const now = new Date(); const now = new Date();
const con = httpRequest.socket; const con = httpRequest.socket;
const socketEndpoints = getSocketEndpoints(con);
const msg = { const msg = {
event: "incoming HTTP request", event: "incoming HTTP request",
timestamp: now, timestamp: now,
remoteAddress: con.remoteAddress, remoteAddress: socketEndpoints.remoteAddress,
deviceId: deviceId, deviceId: deviceId,
connection: getConnectionTimestamp(con), connection: getConnectionTimestamp(con),
localPort: con.localPort, localPort: socketEndpoints.localPort,
method: httpRequest.method, method: httpRequest.method,
url: httpRequest.url, url: httpRequest.url,
headers: httpRequest.headers, headers: httpRequest.headers,
@@ -73,10 +75,11 @@ export function outgoingHttpResponse(
if (!DEBUG_FILE) return; if (!DEBUG_FILE) return;
const now = new Date(); const now = new Date();
const con = httpResponse.socket; const con = httpResponse.socket;
const socketEndpoints = getSocketEndpoints(con);
const msg = { const msg = {
event: "outgoing HTTP response", event: "outgoing HTTP response",
timestamp: now, timestamp: now,
remoteAddress: con.remoteAddress, remoteAddress: socketEndpoints.remoteAddress,
deviceId: deviceId, deviceId: deviceId,
connection: getConnectionTimestamp(con), connection: getConnectionTimestamp(con),
statusCode: httpResponse.statusCode, statusCode: httpResponse.statusCode,
@@ -199,3 +202,20 @@ export function outgoingUdpMessage(
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n"); appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
else throw new Error(`Unrecognized DEBUG_FORMAT option`); else throw new Error(`Unrecognized DEBUG_FORMAT option`);
} }
export function clientError(remoteAddress: string, err: Error): void {
if (!DEBUG_FILE) return;
const now = new Date();
const msg = {
event: "client error",
timestamp: now,
remoteAddress: remoteAddress,
error: err.message,
};
if (DEBUG_FORMAT === "yaml")
appendFileSync(DEBUG_FILE, "---\n" + stringify(msg));
else if (DEBUG_FORMAT === "json")
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
else throw new Error(`Unrecognized DEBUG_FORMAT option`);
}
+10 -6
View File
@@ -21,6 +21,7 @@ import { IncomingMessage } from "http";
import { TLSSocket } from "tls"; import { TLSSocket } from "tls";
import { parseCIDR, parse, IPv6, IPv4 } from "ipaddr.js"; import { parseCIDR, parse, IPv6, IPv4 } from "ipaddr.js";
import * as config from "./config"; import * as config from "./config";
import { getSocketEndpoints } from "./server";
interface RequestOrigin { interface RequestOrigin {
localAddress: string; localAddress: string;
@@ -98,19 +99,22 @@ export function getRequestOrigin(request: IncomingMessage): RequestOrigin {
let origin = cache.get(request); let origin = cache.get(request);
if (!origin) { if (!origin) {
const soc = request.socket; const soc = request.socket;
const socketEndpoints = getSocketEndpoints(soc);
origin = { origin = {
localAddress: soc.localAddress, localAddress: socketEndpoints.localAddress,
localPort: soc.localPort, localPort: socketEndpoints.localPort,
remoteAddress: soc.remoteAddress, remoteAddress: socketEndpoints.remoteAddress,
remotePort: soc.remotePort, remotePort: socketEndpoints.remotePort,
host: request.headers["host"], host: request.headers["host"],
encrypted: !!(request.socket as TLSSocket).encrypted, encrypted: !!(request.socket as TLSSocket).encrypted,
}; };
const header = request.headers["forwarded"]; const header = request.headers["forwarded"];
if (header) { if (header) {
const ip = parse(soc.remoteAddress) as IPv4; const ip = parse(socketEndpoints.remoteAddress);
if (cidrs.some((cidr) => ip.match(cidr as [IPv4, number]))) { if (
cidrs.some((cidr) => ip.kind() === cidr[0].kind() && ip.match(cidr))
) {
const parsed = parseForwardedHeader(header); const parsed = parseForwardedHeader(header);
if (parsed["proto"] === "https") { if (parsed["proto"] === "https") {
+12 -4
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;
@@ -378,13 +380,15 @@ async function fetchConfig(): Promise<[Config, UiConfig]> {
async function refresh(): Promise<void> { async function refresh(): Promise<void> {
if (!nextRefresh) { if (!nextRefresh) {
await new Promise((resolve) => setTimeout(resolve, 20)); await new Promise((resolve) => setTimeout(resolve, 20));
await refresh(); return refresh();
return;
} }
nextRefresh = 0;
const now = Date.now(); const now = Date.now();
if (now < nextRefresh) return;
nextRefresh = 0;
const dbHash = await cache.get("presets_hash"); const dbHash = await cache.get("presets_hash");
if (currentSnapshot && dbHash === currentSnapshot) { if (currentSnapshot && dbHash === currentSnapshot) {
@@ -392,7 +396,7 @@ async function refresh(): Promise<void> {
return; return;
} }
const lockToken = await cache.acquireLock("presets_hash_lock", 5000, 5000); const lockToken = await cache.acquireLock("presets_hash_lock", 5000);
const res = await Promise.all([ const res = await Promise.all([
fetchPresets(), fetchPresets(),
@@ -425,8 +429,12 @@ async function refresh(): Promise<void> {
currentSnapshot = computeHash(snapshot); currentSnapshot = computeHash(snapshot);
snapshots.set(currentSnapshot, snapshot); snapshots.set(currentSnapshot, snapshot);
if (lockToken) {
if (currentSnapshot !== dbHash)
await cache.set("presets_hash", currentSnapshot, 300); await cache.set("presets_hash", currentSnapshot, 300);
await cache.releaseLock("presets_hash_lock", lockToken); await cache.releaseLock("presets_hash_lock", lockToken);
}
nextRefresh = now + (REFRESH - (now % REFRESH)); nextRefresh = now + (REFRESH - (now % REFRESH));
} }
+2 -1
View File
@@ -77,7 +77,8 @@ export function processDeviceFilter(filter: Expression): Expression {
if (typeof e[2] === "number") { if (typeof e[2] === "number") {
const alt = (e as any[]).slice(); const alt = (e as any[]).slice();
alt[2] = new Date(e[2]); alt[2] = new Date(e[2]);
e = ["OR", e, alt]; if (exp[0] === "<>" || exp[0] === "NOT LIKE") e = ["AND", e, alt];
else e = ["OR", e, alt];
} }
return e; return e;
} }
+107
View File
@@ -0,0 +1,107 @@
import { ObjectId } from "mongodb";
import { Expression, FaultStruct } from "./types";
export interface Fault {
_id: string;
device: string;
channel: string;
timestamp: Date;
provisions: string;
retries: number;
code: string;
message: string;
detail?:
| FaultStruct
| {
name: string;
message: string;
stack?: string;
};
expiry?: Date;
}
interface TaskBase {
_id: ObjectId;
timestamp?: Date;
expiry?: Date;
name: string;
}
interface TaskGetParameterValues extends TaskBase {
name: "getParameterValues";
parameterNames: string[];
}
interface TaskSetParameterValues extends TaskBase {
name: "setParameterValues";
parameterValues: [string, string | number | boolean, string?][];
}
interface TaskRefreshObject extends TaskBase {
name: "refreshObject";
objectName: string;
}
interface TaskReboot extends TaskBase {
name: "reboot";
}
interface TaskFactoryReset extends TaskBase {
name: "factoryReset";
}
interface TaskDownload extends TaskBase {
name: "download";
fileType: string;
fileName: string;
targetFileName?: string;
}
interface TaskAddObject extends TaskBase {
name: "addObject";
objectName: string;
parameterValues: [string, string | number | boolean, string?][];
}
interface TaskDeleteObject extends TaskBase {
name: "deleteObject";
objectName: string;
}
interface TaskProvisions extends TaskBase {
name: "provisions";
provisions?: [string, ...Expression[]][];
}
export type Task =
| TaskGetParameterValues
| TaskSetParameterValues
| TaskRefreshObject
| TaskReboot
| TaskFactoryReset
| TaskDownload
| TaskAddObject
| TaskDeleteObject
| TaskProvisions;
export interface Operation {
_id: string;
name: string;
timestamp: Date;
channels: string;
retries: string;
provisions: string;
args: string;
}
export interface Config {
_id: string;
value: string;
}
export interface Cache {
_id: string;
value: string;
timestamp: Date;
expire: Date;
}
+6 -3
View File
@@ -21,7 +21,7 @@ import * as url from "url";
import { Collection, GridFSBucket, ObjectId } from "mongodb"; import { Collection, GridFSBucket, ObjectId } from "mongodb";
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, optimizeProjection } from "./db";
import * as query from "./query"; import * as query from "./query";
import * as apiFunctions from "./api-functions"; import * as apiFunctions from "./api-functions";
import { IncomingMessage, ServerResponse } from "http"; import { IncomingMessage, ServerResponse } from "http";
@@ -30,6 +30,7 @@ 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"; import { flattenDevice } from "./mongodb-functions";
import { getSocketEndpoints } from "./server";
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_]*)?$/;
@@ -101,10 +102,11 @@ export function listener(
request.addListener("end", () => { request.addListener("end", () => {
const body = getBody(); const body = getBody();
const urlParts = url.parse(request.url, true); const urlParts = url.parse(request.url, true);
const socketEndpoints = getSocketEndpoints(request.socket);
logger.accessInfo( logger.accessInfo(
Object.assign({}, urlParts.query, { Object.assign({}, urlParts.query, {
remoteAddress: request.socket.remoteAddress, remoteAddress: socketEndpoints.remoteAddress,
message: `${request.method} ${urlParts.pathname}`, message: `${request.method} ${urlParts.pathname}`,
}) })
); );
@@ -819,6 +821,7 @@ export function listener(
projection = {}; projection = {};
for (const p of (urlParts.query.projection as string).split(",")) for (const p of (urlParts.query.projection as string).split(","))
projection[p.trim()] = 1; projection[p.trim()] = 1;
projection = optimizeProjection(projection);
} }
const cur = collection.find(q, { projection: projection }); const cur = collection.find(q, { projection: projection });
@@ -842,7 +845,7 @@ export function listener(
cur.sort(sort); cur.sort(sort);
} }
cur.count((err, total) => { collection.countDocuments(q, (err, total) => {
if (err) return void throwError(err); if (err) return void throwError(err);
response.writeHead(200, { response.writeHead(200, {
+43 -20
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,38 +31,33 @@ export interface PingResult {
mdev: number; mdev: number;
} }
export function ping( function isValidHost(host: string): boolean {
host: string, // Valid chars in IPv4, IPv6, domain names
callback: (err: Error, res?: PingResult, stdout?: string) => void if (/^[a-zA-Z0-9\-.:[\]-]+$/.test(host)) return true;
): void {
let cmd: string, parseRegExp1: RegExp, parseRegExp2: RegExp; // Check if input is an IDN convert to Punycode
host = host.replace("[", "").replace("]", ""); // Can't merge with above because domainToASCII doesn't accept IP addresses
switch (platform()) { return /^[a-zA-Z0-9\-.:[\]-]+$/.test(domainToASCII(host));
}
export function parsePing(osPlatform: string, stdout: string): PingResult {
let parseRegExp1: RegExp, parseRegExp2: RegExp, parsed: PingResult;
switch (osPlatform) {
case "linux": case "linux":
cmd = `ping -w 1 -i 0.2 -c 3 ${host}`;
parseRegExp1 = parseRegExp1 =
/(\d+) packets transmitted, (\d+) received, ([\d.]+)% packet loss[^]*([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+)/; /(\d+) packets transmitted, (\d+) .*received, ([\d.]+)% .*loss[^]*= ([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+)/;
parseRegExp2 = parseRegExp2 =
/(\d+) packets transmitted, (\d+) received, ([\d.]+)% packet loss/; /(\d+) packets transmitted, (\d+) .*received, ([\d.]+)% .*loss/;
break; break;
case "freebsd": case "freebsd":
// Send a single packet because on FreeBSD only superuser can send
// packets that are only 200 ms apart.
cmd = `ping -t 1 -c 3 ${host}`;
parseRegExp1 = parseRegExp1 =
/(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss\nround-trip min\/avg\/max\/stddev = ([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+) ms/; /(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss\nround-trip min\/avg\/max\/stddev = ([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+) ms/;
parseRegExp2 = parseRegExp2 =
/(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss/; /(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss/;
break; break;
default:
return callback(new Error("Platform not supported"));
} }
exec(cmd, (err, stdout) => {
let parsed: PingResult;
if (stdout) {
const m1 = stdout.match(parseRegExp1); const m1 = stdout.match(parseRegExp1);
if (m1) { if (m1) {
parsed = { parsed = {
@@ -87,8 +83,35 @@ export function ping(
}; };
} }
} }
return parsed;
} }
callback(err, parsed, stdout); export function ping(
host: string,
callback: (err: Error, res?: PingResult, stdout?: string) => void
): void {
// 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("]", "");
let cmd: string;
switch (platform()) {
case "linux":
cmd = `ping -w 1 -i 0.2 -c 3 ${host}`;
break;
case "freebsd":
// Send a single packet because on FreeBSD only superuser can send
// packets that are only 200 ms apart.
cmd = `ping -t 1 -c 3 ${host}`;
break;
default:
return callback(new Error("Platform not supported"));
}
exec(cmd, (err, stdout) => {
if (err) return callback(err);
const parsed: PingResult = parsePing(platform(), stdout);
return callback(err, parsed, stdout);
}); });
} }
+13 -1
View File
@@ -54,6 +54,17 @@ function normalize(input): any {
return input; return input;
} }
const EXPAND_OPS = new Set([
"$eq",
"$gt",
"$gte",
"$in",
"$lt",
"$lte",
"$ne",
"$nin",
]);
function expandValue(value: unknown): unknown[] { function expandValue(value: unknown): unknown[] {
if (Array.isArray(value)) { if (Array.isArray(value)) {
let a = []; let a = [];
@@ -71,7 +82,8 @@ function expandValue(value: unknown): unknown[] {
const values = []; const values = [];
for (const [k, v] of Object.entries(value)) { for (const [k, v] of Object.entries(value)) {
keys.push(k); keys.push(k);
values.push(expandValue(v)); if (EXPAND_OPS.has(k)) values.push(expandValue(v));
else values.push([v]);
indices.push(0); indices.push(0);
} }
+1
View File
@@ -255,6 +255,7 @@ function declare(
} }
for (const [attrName, attrTimestamp] of Object.entries(timestamps)) { for (const [attrName, attrTimestamp] of Object.entries(timestamps)) {
if (!(attrTimestamp >= 1)) continue;
if (attrName === "path") { if (attrName === "path") {
declaration.pathGet = attrTimestamp; declaration.pathGet = attrTimestamp;
} else { } else {
+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();
+38
View File
@@ -59,8 +59,20 @@ interface ServerOptions {
timeout?: number; timeout?: number;
keepAliveTimeout?: number; keepAliveTimeout?: number;
onConnection?: (socket: Socket) => void; onConnection?: (socket: Socket) => void;
onClientError?: (err: Error, socket: Socket) => void;
} }
interface SocketEndpoint {
localAddress: string;
localPort: number;
remoteAddress: string;
remotePort: number;
remoteFamily: "IPv4" | "IPv6";
}
// Save this info as they're not accessible after a socket has been closed
const socketEndpoints: WeakMap<Socket, SocketEndpoint> = new WeakMap();
export function start( export function start(
options: ServerOptions, options: ServerOptions,
_listener: http.RequestListener _listener: http.RequestListener
@@ -85,6 +97,27 @@ export function start(
if (options.onConnection) server.on("connection", options.onConnection); if (options.onConnection) server.on("connection", options.onConnection);
} }
server.on("connection", (socket: Socket) => {
socketEndpoints.set(socket, {
localAddress: socket.localAddress,
localPort: socket.localPort,
remoteAddress: socket.remoteAddress,
remotePort: socket.remotePort,
remoteFamily: socket.remoteFamily as "IPv4" | "IPv6",
});
});
if (options.onClientError) {
server.on("clientError", (err, socket: Socket) => {
if (err["code"] !== "ECONNRESET" && socket.writable)
socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
// As per Node docs: This event is guaranteed to be passed an instance
// of the <net.Socket> class
options.onClientError(err, socket as Socket);
});
}
server.timeout = options.timeout || 0; server.timeout = options.timeout || 0;
if (options.keepAliveTimeout != null) if (options.keepAliveTimeout != null)
server.keepAliveTimeout = options.keepAliveTimeout; server.keepAliveTimeout = options.keepAliveTimeout;
@@ -99,3 +132,8 @@ export function stop(): Promise<void> {
closeServer(20000, resolve); closeServer(20000, resolve);
}); });
} }
export function getSocketEndpoints(socket: Socket): SocketEndpoint {
// TLSSocket keeps a reference to the raw TCP socket in _parent
return socketEndpoints.get(socket["_parent"] ?? socket);
}
+19 -2
View File
@@ -504,7 +504,7 @@ export async function timeoutOperations(
export function addProvisions( export function addProvisions(
sessionContext: SessionContext, sessionContext: SessionContext,
channel: string, channel: string,
provisions: (string | number | boolean)[][] provisions: [string, ...Expression[]][]
): void { ): void {
// Multiply by two because every iteration is two // Multiply by two because every iteration is two
// phases: read and update // phases: read and update
@@ -1624,6 +1624,20 @@ function generateGetRpcRequest(
(a, b) => b.length - a.length (a, b) => b.length - a.length
); );
let path = paths.pop(); let path = paths.pop();
// Skip root GPN workaround
if (path && !path.length) {
const SKIP_ROOT_GPN = !!localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.skipRootGpn",
{},
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e)
);
if (SKIP_ROOT_GPN) path = paths.pop();
}
while ( while (
path && path &&
path.length && path.length &&
@@ -2678,7 +2692,10 @@ 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)) { if (
!path.toString().startsWith(rpcReq.parameterPath) &&
!(`${path.toString()}.` === rpcReq.parameterPath && !rpcReq.nextLevel)
) {
logger.accessWarn({ logger.accessWarn({
sessionContext: sessionContext, sessionContext: sessionContext,
message: "Unexpected parameter in response", message: "Unexpected parameter in response",
+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"';
+2 -1
View File
@@ -180,7 +180,7 @@ export interface Task {
fileName?: string; fileName?: string;
targetFileName?: string; targetFileName?: string;
expiry?: number; expiry?: number;
provisions?: (string | number | boolean)[][]; provisions?: [string, ...Expression[]][];
} }
export interface Operation { export interface Operation {
@@ -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 {
+2 -1
View File
@@ -228,7 +228,7 @@ router.get("/", async (ctx) => {
if (!Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length) if (!Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length)
wizard = '<script>window.location.hash = "#!/wizard";</script>'; wizard = '<script>window.location.hash = "#!/wizard";</script>';
ctx.body = ` ctx.body = `<!DOCTYPE html>
<html> <html>
<head> <head>
<title>GenieACS</title> <title>GenieACS</title>
@@ -263,6 +263,7 @@ koa.use(
flush: constants.Z_SYNC_FLUSH, flush: constants.Z_SYNC_FLUSH,
}, },
br: { br: {
flush: constants.BROTLI_OPERATION_FLUSH,
params: { params: {
[constants.BROTLI_PARAM_QUALITY]: 5, [constants.BROTLI_PARAM_QUALITY]: 5,
}, },
+20 -11
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"
); );
@@ -125,7 +125,7 @@ router.get(`/devices/:id.csv`, async (ctx) => {
new Date(p.objectTimestamp).toJSON(), new Date(p.objectTimestamp).toJSON(),
p.writable, p.writable,
new Date(p.writableTimestamp).toJSON(), new Date(p.writableTimestamp).toJSON(),
`"${value.toString().replace(/"/g, '""')}"`, `"${String(value).replace(/"/g, '""')}"`,
type, type,
new Date(p.valueTimestamp).toJSON(), new Date(p.valueTimestamp).toJSON(),
p.notification, p.notification,
@@ -216,17 +216,21 @@ 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;
ctx.body.write("[\n"); ctx.body.write("[\n");
await db.query(resource, filter, options, (obj) => { db.query(resource, filter, options, (obj) => {
ctx.body.write((c++ ? "," : "") + JSON.stringify(obj) + "\n"); ctx.body.write((c++ ? "," : "") + JSON.stringify(obj) + "\n");
}); })
.then(() => {
ctx.body.end("]"); ctx.body.end("]");
logger.accessInfo(log); logger.accessInfo(log);
})
.catch((err) => {
ctx.body.emit("error", err);
});
}); });
// CSV download // CSV download
@@ -276,7 +280,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`
@@ -285,7 +289,7 @@ for (const [resource, flags] of Object.entries(resources)) {
ctx.body.write( ctx.body.write(
Object.keys(columns).map((k) => `"${k.replace(/"/, '""')}"`) + "\n" Object.keys(columns).map((k) => `"${k.replace(/"/, '""')}"`) + "\n"
); );
await db.query(resource, filter, options, (obj) => { db.query(resource, filter, options, (obj) => {
const arr = Object.values(columns).map((exp) => { const arr = Object.values(columns).map((exp) => {
const v = evaluate(exp, obj, null, (e) => { const v = evaluate(exp, obj, null, (e) => {
if (Array.isArray(e)) { if (Array.isArray(e)) {
@@ -327,10 +331,14 @@ for (const [resource, flags] of Object.entries(resources)) {
return v; return v;
}); });
ctx.body.write(arr.join(",") + "\n"); ctx.body.write(arr.join(",") + "\n");
}); })
.then(() => {
ctx.body.end(); ctx.body.end();
logger.accessInfo(log); logger.accessInfo(log);
})
.catch((err) => {
ctx.body.emit("error", err);
});
}); });
router.head(`/${resource}/:id`, async (ctx) => { router.head(`/${resource}/:id`, async (ctx) => {
@@ -652,6 +660,7 @@ router.post("/devices/:id/tags", async (ctx) => {
}); });
router.get("/ping/:host", async (ctx) => { router.get("/ping/:host", async (ctx) => {
if (!ctx.state.user) return void (ctx.status = 401);
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
ping(ctx.params.host, (err, parsed) => { ping(ctx.params.host, (err, parsed) => {
if (parsed) { if (parsed) {
+17 -20
View File
@@ -19,7 +19,7 @@
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, optimizeProjection } 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";
@@ -89,6 +89,7 @@ export function query(
} }
if (resource === "presets") projection.configurations = 1; if (resource === "presets") projection.configurations = 1;
projection = optimizeProjection(projection);
cursor.project(projection); cursor.project(projection);
} }
@@ -114,30 +115,31 @@ export function query(
cursor.toArray((err, docs) => { cursor.toArray((err, docs) => {
if (err) return reject(err); if (err) return reject(err);
if (resource === "devices") if (resource === "devices")
docs = docs.map((d) => mongodbFunctions.flattenDevice(d)); return resolve(docs.map((d) => mongodbFunctions.flattenDevice(d)));
else if (resource === "faults") else if (resource === "faults")
docs = docs.map((d) => mongodbFunctions.flattenFault(d)); return resolve(docs.map((d) => mongodbFunctions.flattenFault(d)));
else if (resource === "tasks") else if (resource === "tasks")
docs = docs.map((d) => mongodbFunctions.flattenTask(d)); return resolve(docs.map((d) => mongodbFunctions.flattenTask(d)));
else if (resource === "presets") else if (resource === "presets")
docs = docs.map((d) => mongodbFunctions.flattenPreset(d)); return resolve(docs.map((d) => mongodbFunctions.flattenPreset(d)));
else if (resource === "files") else if (resource === "files")
docs = docs.map((d) => mongodbFunctions.flattenFile(d)); return resolve(docs.map((d) => mongodbFunctions.flattenFile(d)));
return resolve(docs); return resolve(docs);
}); });
} else { } else {
cursor.forEach( cursor.forEach(
(doc) => { (doc) => {
if (resource === "devices") doc = mongodbFunctions.flattenDevice(doc); if (resource === "devices")
callback(mongodbFunctions.flattenDevice(doc));
else if (resource === "faults") else if (resource === "faults")
doc = mongodbFunctions.flattenFault(doc); callback(mongodbFunctions.flattenFault(doc));
else if (resource === "tasks") else if (resource === "tasks")
doc = mongodbFunctions.flattenTask(doc); callback(mongodbFunctions.flattenTask(doc));
else if (resource === "presets") else if (resource === "presets")
doc = mongodbFunctions.flattenPreset(doc); callback(mongodbFunctions.flattenPreset(doc));
else if (resource === "files") else if (resource === "files")
doc = mongodbFunctions.flattenFile(doc); callback(mongodbFunctions.flattenFile(doc));
callback(doc); else callback(doc);
}, },
(err) => { (err) => {
if (err) reject(err); if (err) reject(err);
@@ -149,7 +151,8 @@ export function query(
} }
export function count(resource: string, filter: Expression): Promise<number> { export function count(resource: string, filter: Expression): Promise<number> {
let q; const collection = db.collection(RESOURCE_COLLECTION[resource] || resource);
let q: Parameters<typeof collection.countDocuments>[0];
filter = expression.evaluate(filter, null, Date.now()); filter = expression.evaluate(filter, null, Date.now());
filter = minimize(filter, true); filter = minimize(filter, true);
@@ -165,13 +168,7 @@ export function count(resource: string, filter: Expression): Promise<number> {
return Promise.resolve(0); return Promise.resolve(0);
} }
return new Promise((resolve, reject) => { return collection.countDocuments(q);
const collection = db.collection(RESOURCE_COLLECTION[resource] || resource);
collection.find(q).count((err, c) => {
if (err) reject(err);
else resolve(c);
});
});
} }
export async function updateDeviceTags( export async function updateDeviceTags(
+1
View File
@@ -168,6 +168,7 @@ export default class VersionedMap<K, V> {
public *diff(): IterableIterator<[K, V, V]> { public *diff(): IterableIterator<[K, V, V]> {
for (const [key, revisions] of this.map) { for (const [key, revisions] of this.map) {
if (revisions.length === 1) continue;
let first = revisions[0]; let first = revisions[0];
let last = revisions[revisions.length - 1]; let last = revisions[revisions.length - 1];
if (first === NONEXISTENT && last === NONEXISTENT) continue; if (first === NONEXISTENT && last === NONEXISTENT) continue;
+9684 -4237
View File
File diff suppressed because it is too large. Load diff
+33 -32
View File
@@ -1,6 +1,6 @@
{ {
"name": "genieacs", "name": "genieacs",
"version": "1.2.6", "version": "1.2.9",
"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,56 @@
"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": "^12.0.0",
"koa-static": "^5.0.0", "koa-static": "^5.0.0",
"later": "^1.2.0", "mongodb": "^4.9.0",
"mongodb": "^4.1.2", "parsimmon": "^1.18.1",
"parsimmon": "^1.18.0",
"seedrandom": "^3.0.5" "seedrandom": "^3.0.5"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^20.0.0", "@rollup/plugin-commonjs": "^22.0.2",
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.0.4", "@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-typescript": "^8.2.5", "@rollup/plugin-typescript": "^8.3.4",
"@types/codemirror": "^5.60.2", "@types/codemirror": "^5.60.5",
"@types/jsonwebtoken": "^8.5.5", "@types/jsonwebtoken": "^8.5.8",
"@types/koa": "^2.13.4", "@types/koa": "^2.13.5",
"@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.11",
"@types/node": "^16.9.1", "@types/node": "^18.7.9",
"@types/parsimmon": "^1.10.6", "@types/parsimmon": "^1.10.6",
"@types/seedrandom": "^3.0.1", "@types/seedrandom": "^3.0.2",
"@typescript-eslint/eslint-plugin": "^4.31.1", "@typescript-eslint/eslint-plugin": "^5.33.1",
"@typescript-eslint/parser": "^4.31.1", "@typescript-eslint/parser": "^5.33.1",
"ava": "^3.15.0", "ava": "^3.15.0",
"codemirror": "^5.62.3", "codemirror": "^5.65.8",
"cssnano": "^5.0.8", "cssnano": "^5.1.13",
"eslint": "^7.32.0", "eslint": "^8.22.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.5.0",
"esm": "^3.2.25", "esm": "^3.2.25",
"mithril": "^2.0.4", "mithril": "^2.2.2",
"postcss": "^8.3.6", "postcss": "^8.4.16",
"postcss-import": "^14.0.2", "postcss-color-mod-function": "^3.0.3",
"postcss-preset-env": "^6.7.0", "postcss-import": "^14.1.0",
"prettier": "^2.4.1", "postcss-preset-env": "^7.8.0",
"rollup": "^2.56.3", "prettier": "^2.7.1",
"rollup": "^2.78.1",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"sql.js": "^1.6.1", "sql.js": "^1.7.0",
"svgo": "^2.6.1", "svgo": "^2.8.0",
"ts-node": "^10.2.1", "ts-node": "^10.9.1",
"typescript": "^4.4.3", "typescript": "^4.7.4",
"yaml": "^1.10.2" "yaml": "^1.10.2"
}, },
"engines": { "engines": {
+1 -1
View File
@@ -1,7 +1,7 @@
import ava, { TestInterface } from "ava"; import ava, { TestInterface } from "ava";
import { minimize, unionDiff } from "../lib/common/boolean-expression"; import { minimize, unionDiff } from "../lib/common/boolean-expression";
import { parse, stringify } from "../lib/common/expression-parser"; import { parse, stringify } from "../lib/common/expression-parser";
import initSqlJs from "sql.js"; import initSqlJs from "sql.js/dist/sql-asm.js";
const test = ava as TestInterface<{ const test = ava as TestInterface<{
query: (filter: string) => Promise<Set<number>>; query: (filter: string) => Promise<Set<number>>;
+18
View File
@@ -80,6 +80,24 @@ ava("filterToMongoQuery", async (t) => {
["Tags.tag1 = 123", { "Tags.tag1": 123 }], ["Tags.tag1 = 123", { "Tags.tag1": 123 }],
["Param1 = 'value1'", { "Param1._value": "value1" }], ["Param1 = 'value1'", { "Param1._value": "value1" }],
["Param1 <> 'value1'", { "Param1._value": { $nin: ["value1", null] } }], ["Param1 <> 'value1'", { "Param1._value": { $nin: ["value1", null] } }],
[
"Param1 <> 1657844103524",
{
$and: [
{ "Param1._value": { $nin: [1657844103524, null] } },
{ "Param1._value": { $nin: [new Date(1657844103524), null] } },
],
},
],
[
"Param1 = 1657844103524",
{
$or: [
{ "Param1._value": 1657844103524 },
{ "Param1._value": new Date(1657844103524) },
],
},
],
["Param1 > 'value'", { "Param1._value": { $gt: "value" } }], ["Param1 > 'value'", { "Param1._value": { $gt: "value" } }],
["Param1 IS NOT NULL", { "Param1._value": { $ne: null } }], ["Param1 IS NOT NULL", { "Param1._value": { $ne: null } }],
["Param1 LIKE 'value'", { "Param1._value": /^value$/ }], ["Param1 LIKE 'value'", { "Param1._value": /^value$/ }],
+36
View File
@@ -0,0 +1,36 @@
import ava from "ava";
import { parsePing } from "../lib/ping";
ava("linux Case-1", (t) => {
const platform = "linux";
const stdout =
"PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data\n64 bytes from 8.8.8.8: icmp_seq=1 ttl=118 time=6.66 ms\n64 bytes from 8.8.8.8: icmp_seq=2 ttl=118 time=6.50 ms\n64 bytes from 8.8.8.8: icmp_seq=3 ttl=118 time=6.38 ms\n\n--- 8.8.8.8 ping statistics ---\n3 packets transmitted, 3 received, 0% packet loss, time 402ms\nrtt min/avg/max/mdev = 6.381/6.511/6.656/0.112 ms";
const parsedResult = {
packetsTransmitted: 3,
packetsReceived: 3,
packetLoss: 0,
min: 6.381,
avg: 6.511,
max: 6.656,
mdev: 0.112,
};
const parsed = parsePing(platform, stdout);
t.deepEqual(parsedResult, parsed);
});
ava("linux Case-2", (t) => {
const platform = "linux";
const stdout =
"PING 10.251.9.108 (10.251.9.108): 56 data bytes\n64 bytes from 10.251.9.108: icmp_seq=0 ttl=57 time=36.758 ms\n64 bytes from 10.251.9.108: icmp_seq=1 ttl=57 time=69.094 ms\n64 bytes from 10.251.9.108: icmp_seq=2 ttl=57 time=28.868 ms\n--- 10.251.9.108 ping statistics ---3 packets transmitted, 3 packets received, 0% packet loss\nround-trip min/avg/max/stddev = 28.868/44.907/69.094/17.404 ms";
const parsedResult = {
packetsTransmitted: 3,
packetsReceived: 3,
packetLoss: 0,
min: 28.868,
avg: 44.907,
max: 69.094,
mdev: 17.404,
};
const parsed = parsePing(platform, stdout);
t.deepEqual(parsedResult, parsed);
});
+2 -2
View File
@@ -99,9 +99,9 @@ export default class Autocomplete {
if (this.visible) this.hide(); if (this.visible) this.hide();
return; return;
} }
this.container.style.left = `${domRect.left + window.pageXOffset}px`; this.container.style.left = `${domRect.left + window.scrollX}px`;
this.container.style.width = `${domRect.width}px`; this.container.style.width = `${domRect.width}px`;
this.container.style.top = `${domRect.bottom + window.pageYOffset}px`; this.container.style.top = `${domRect.bottom + window.scrollY}px`;
} }
private hide(): void { private hide(): void {
+12 -8
View File
@@ -23,6 +23,8 @@ 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";
import debounce from "../../lib/common/debounce";
const memoizedParse = memoize(parse); const memoizedParse = memoize(parse);
@@ -51,29 +53,31 @@ function orderKeysByDepth(device: Record<string, unknown>): string[][] {
} }
const component: ClosureComponent = (): Component => { const component: ClosureComponent = (): Component => {
let queryString: string;
const formQueryString = debounce((args: string[]) => {
queryString = args[args.length - 1];
m.redraw();
}, 500);
return { return {
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",
placeholder: "Search parameters", placeholder: "Search parameters",
oninput: (e) => { oninput: (e) => {
vnode.state["searchString"] = e.target.value; formQueryString(e.target.value);
e.redraw = false; e.redraw = false;
clearTimeout(vnode.state["timeout"]);
vnode.state["timeout"] = setTimeout(m.redraw, 500);
}, },
}); });
const instanceRegex = /\.[0-9]+$/; const instanceRegex = /\.[0-9]+$/;
let re; let re;
if (vnode.state["searchString"]) { if (queryString) {
const keywords = vnode.state["searchString"] const keywords = queryString.split(" ").filter((s) => s);
.split(" ")
.filter((s) => s);
if (keywords.length) if (keywords.length)
re = new RegExp(keywords.map((s) => escapeRegExp(s)).join(".*"), "i"); re = new RegExp(keywords.map((s) => escapeRegExp(s)).join(".*"), "i");
} }
+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))
+17 -4
View File
@@ -87,7 +87,7 @@ i {
} }
body { body {
height: 100%; height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@@ -169,6 +169,7 @@ body {
& input { & input {
width: 100%; width: 100%;
box-sizing: border-box;
padding: 0.3em; padding: 0.3em;
margin-top: 0.3em; margin-top: 0.3em;
margin-bottom: 0.3em; margin-bottom: 0.3em;
@@ -291,6 +292,7 @@ body {
& > img { & > img {
margin: 10px; margin: 10px;
height: 56px; height: 56px;
vertical-align: top;
} }
& > .version { & > .version {
@@ -581,6 +583,7 @@ span.na {
.all-parameters { .all-parameters {
& > input { & > input {
font-family: monospace; font-family: monospace;
font-size: 14px;
margin-bottom: 2px; margin-bottom: 2px;
padding: 4px; padding: 4px;
width: 700px; width: 700px;
@@ -588,6 +591,10 @@ span.na {
& > .parameter-list { & > .parameter-list {
font-family: monospace; font-family: monospace;
font-size: 14px;
& button {
font-size: 16px;
}
overflow-y: scroll; overflow-y: scroll;
height: 200px; height: 200px;
@@ -641,7 +648,7 @@ span.na {
span.tag { span.tag {
display: inline-block; display: inline-block;
background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="190" height="16"><path d="M6.59 15.42l-6-6a2 2 0 0 1 0-2.83l6-6A2 2 0 0 1 8 0h180a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H8a2 2 0 0 1-1.41-.58zM4.35 6.59A2 2 0 1 0 7.17 9.4 2 2 0 0 0 4.35 6.6z" fill="#e8d5b2"></path></svg>'); background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="190" height="16"><path d="M6.59 15.42l-6-6a2 2 0 0 1 0-2.83l6-6A2 2 0 0 1 8 0h180a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H8a2 2 0 0 1-1.41-.58zM4.35 6.59A2 2 0 1 0 7.17 9.4 2 2 0 0 0 4.35 6.6z" fill="%23e8d5b2"></path></svg>');
background-size: cover; background-size: cover;
background-repeat: no-repeat; background-repeat: no-repeat;
border-radius: 0.2em; border-radius: 0.2em;
@@ -725,6 +732,7 @@ span.long-text-overflowed {
textarea.long-text { textarea.long-text {
font-family: monospace; font-family: monospace;
font-size: 14px;
min-width: 10vw; min-width: 10vw;
min-height: 10vh; min-height: 10vh;
} }
@@ -746,8 +754,7 @@ span.parameter-value {
} }
.icon { .icon {
display: inline-block; display: block;
vertical-align: middle;
width: 1em; width: 1em;
height: 1em; height: 1em;
fill: none; fill: none;
@@ -797,3 +804,9 @@ span.parameter-value {
background-color: #b12d5c; background-color: #b12d5c;
} }
} }
/* To keep consistent style after changing from Quirks to Standards mode */
td,
th {
line-height: normal;
}
+6 -9
View File
@@ -268,16 +268,13 @@ export const component: ClosureComponent = (): Component => {
} }
function onSortChange(sortedAttrs): void { function onSortChange(sortedAttrs): void {
const _sort = Object.assign({}, sort); const _sort = {};
for (const [index, direction] of Object.entries(sortedAttrs)) { for (const index of sortedAttrs) {
const param = memoizedGetSortable(attributes[index].parameter); const param = memoizedGetSortable(
if (param) { attributes[Math.abs(index) - 1].parameter
// Changing the priority of columns );
delete _sort[param]; _sort[param] = Math.sign(index);
_sort[param] = direction;
} }
}
const ops = { sort: JSON.stringify(_sort) }; const 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);
+2 -2
View File
@@ -388,7 +388,7 @@ const component: ClosureComponent = (): Component => {
function repositionNotifications(): void { function repositionNotifications(): void {
let top = 10; let top = 10;
for (const c of notificationElements as VnodeDOM[]) { for (const c of notificationElements as VnodeDOM[]) {
(c.dom as HTMLDivElement).style.top = `${top}`; (c.dom as HTMLDivElement).style.top = `${top}px`;
top += (c.dom as HTMLDivElement).offsetHeight + 10; top += (c.dom as HTMLDivElement).offsetHeight + 10;
} }
} }
@@ -408,7 +408,7 @@ const component: ClosureComponent = (): Component => {
for (const c of drawerElement.children) for (const c of drawerElement.children)
height = Math.max(height, c.dom.offsetTop + c.dom.offsetHeight); height = Math.max(height, c.dom.offsetTop + c.dom.offsetHeight);
} }
drawerElement.dom.style.height = height; drawerElement.dom.style.height = height + "px";
} }
if (stagingElements.length + queueElements.length) { if (stagingElements.length + queueElements.length) {
+3 -7
View File
@@ -107,13 +107,9 @@ export const component: ClosureComponent = (): Component => {
} }
function onSortChange(sortAttrs): void { function onSortChange(sortAttrs): void {
const _sort = Object.assign({}, sort); const _sort = {};
for (const [index, direction] of Object.entries(sortAttrs)) { for (const index of sortAttrs)
// Changing the priority of columns _sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
delete _sort[attributes[index].id];
_sort[attributes[index].id] = direction;
}
const ops = { sort: JSON.stringify(_sort) }; const 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);
+3 -7
View File
@@ -151,13 +151,9 @@ 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); const _sort = {};
for (const [index, direction] of Object.entries(sortAttrs)) { for (const index of sortAttrs)
// Changing the priority of columns _sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
delete _sort[attributes[index].id];
_sort[attributes[index].id] = direction;
}
const ops = { sort: JSON.stringify(_sort) }; const 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);
+24 -8
View File
@@ -20,6 +20,7 @@
import { ClosureComponent, Component, Children } from "mithril"; import { ClosureComponent, Component, Children } from "mithril";
import { m } from "./components"; import { m } from "./components";
import { getIcon } from "./icons"; import { getIcon } from "./icons";
import debounce from "../lib/common/debounce";
interface Attribute { interface Attribute {
id?: string; id?: string;
@@ -36,7 +37,7 @@ function renderTable(
showMoreCallback: () => void, showMoreCallback: () => void,
selected: Set<string>, selected: Set<string>,
sortAttributes: Record<string, any>, sortAttributes: Record<string, any>,
onSortChange: (obj: Record<string, number>) => void, onSort: (i: number) => void,
downloadUrl?: string, downloadUrl?: string,
valueCallback?: (attr: Attribute, record: Record<string, any>) => Children, valueCallback?: (attr: Attribute, record: Record<string, any>) => Children,
actionsCallback?: Children | ((sel: Set<string>) => Children), actionsCallback?: Children | ((sel: Set<string>) => Children),
@@ -79,8 +80,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");
@@ -88,9 +87,9 @@ function renderTable(
const sortable = m( const sortable = m(
"button", "button",
{ {
onclick: () => { onclick: (e) => {
if (sortAttributes[i] > 0) direction *= -1; e.redraw = false;
return onSortChange({ [i]: direction }); onSort(i);
}, },
}, },
symbol symbol
@@ -215,7 +214,10 @@ function renderTable(
const component: ClosureComponent = (): Component => { const component: ClosureComponent = (): Component => {
let selected = new Set<string>(); let selected = new Set<string>();
let sortingfunction: (events: number[]) => void;
const onSort = debounce((events: number[]) => {
sortingfunction(events);
}, 500);
return { return {
view: (vnode) => { view: (vnode) => {
const attributes = vnode.attrs["attributes"]; const attributes = vnode.attrs["attributes"];
@@ -234,6 +236,20 @@ const component: ClosureComponent = (): Component => {
const id = record["_id"] || record["DeviceID.ID"].value[0]; const id = record["_id"] || record["DeviceID.ID"].value[0];
if (selected.has(id)) _selected.add(id); if (selected.has(id)) _selected.add(id);
} }
sortingfunction = (events) => {
const sortArray = new Set(
Object.keys(sortAttributes)
.map((x) => (parseInt(x) + 1) * sortAttributes[x])
.filter((x) => x)
);
for (const num of events) {
if (sortArray.delete(num + 1)) sortArray.add(-(num + 1));
else if (!sortArray.delete((num + 1) * -1)) sortArray.add(num + 1);
}
onSortChange(Array.from(sortArray).reverse());
};
selected = _selected; selected = _selected;
return renderTable( return renderTable(
@@ -243,7 +259,7 @@ const component: ClosureComponent = (): Component => {
showMoreCallback, showMoreCallback,
selected, selected,
sortAttributes, sortAttributes,
onSortChange, onSort,
downloadUrl, downloadUrl,
valueCallback, valueCallback,
actionsCallback, actionsCallback,
+3 -7
View File
@@ -205,13 +205,9 @@ export const component: ClosureComponent = (): Component => {
} }
function onSortChange(sortAttrs): void { function onSortChange(sortAttrs): void {
const _sort = Object.assign({}, sort); const _sort = {};
for (const [index, direction] of Object.entries(sortAttrs)) { for (const index of sortAttrs)
// Changing the priority of columns _sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
delete _sort[attributes[index].id];
_sort[attributes[index].id] = direction;
}
const ops = { sort: JSON.stringify(_sort) }; const 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);
+3 -7
View File
@@ -190,13 +190,9 @@ export const component: ClosureComponent = (): Component => {
} }
function onSortChange(sortAttrs): void { function onSortChange(sortAttrs): void {
const _sort = Object.assign({}, sort); const _sort = {};
for (const [index, direction] of Object.entries(sortAttrs)) { for (const index of sortAttrs)
// Changing the priority of columns _sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
delete _sort[attributes[index].id];
_sort[attributes[index].id] = direction;
}
const ops = { sort: JSON.stringify(_sort) }; const 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);
+3 -7
View File
@@ -173,13 +173,9 @@ 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); const _sort = {};
for (const [index, direction] of Object.entries(sortAttrs)) { for (const index of sortAttrs)
// Changing the priority of columns _sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
delete _sort[attributes[index].id];
_sort[attributes[index].id] = direction;
}
const ops = { sort: JSON.stringify(_sort) }; const 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);
+3 -7
View File
@@ -186,13 +186,9 @@ export const component: ClosureComponent = (): Component => {
} }
function onSortChange(sortAttrs): void { function onSortChange(sortAttrs): void {
const _sort = Object.assign({}, sort); const _sort = {};
for (const [index, direction] of Object.entries(sortAttrs)) { for (const index of sortAttrs)
// Changing the priority of columns _sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
delete _sort[attributes[index].id];
_sort[attributes[index].id] = direction;
}
const ops = { sort: JSON.stringify(_sort) }; const 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);
+3 -7
View File
@@ -173,13 +173,9 @@ 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); const _sort = {};
for (const [index, direction] of Object.entries(sortAttrs)) { for (const index of sortAttrs)
// Changing the priority of columns _sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
delete _sort[attributes[index].id];
_sort[attributes[index].id] = direction;
}
const ops = { sort: JSON.stringify(_sort) }; const 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);