Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
444183da0a | ||
|
|
c02f667623 | ||
|
|
25ea0c8b64 | ||
|
|
2789961596 | ||
|
|
d91be02599 | ||
|
|
5806fe2df6 | ||
|
|
eb5caa6b5a | ||
|
|
ffbcf2bbc0 | ||
|
|
6f29267266 | ||
|
|
b0d50b07e7 | ||
|
|
c06538f9cc | ||
|
|
3d0ac9b415 | ||
|
|
dabf1b2f43 | ||
|
|
7941d929d2 | ||
|
|
7ccdbace70 | ||
|
|
344f0ec5e0 | ||
|
|
4b533d0aad | ||
|
|
2b37f11e2e | ||
|
|
adaf801d6e | ||
|
|
94756e463f | ||
|
|
3c55306e22 | ||
|
|
aa12b7c724 | ||
|
|
8f827997cd | ||
|
|
c9a4be0475 | ||
|
|
fa95d7a310 | ||
|
|
d299ef3a59 | ||
|
|
2c606b268e | ||
|
|
1b86f41ad7 | ||
|
|
c4754a9b55 | ||
|
|
f24111ea9f | ||
|
|
8f662fb152 | ||
|
|
dd722ba6e0 | ||
|
|
88a84788a5 | ||
|
|
73baffc3b9 | ||
|
|
0188ac860b | ||
|
|
7f295beeec | ||
|
|
2ac536bf8f | ||
|
|
f63e357b29 |
No files matched your search
@@ -1,5 +1,46 @@
|
||||
# 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.
|
||||
|
||||
@@ -83,6 +83,7 @@ if (!cluster.worker) {
|
||||
host: SERVICE_ADDRESS,
|
||||
ssl: key && cert ? { key, cert } : null,
|
||||
onConnection: cwmp.onConnection,
|
||||
onClientError: cwmp.onClientError,
|
||||
timeout: 30000,
|
||||
keepAliveTimeout: 0,
|
||||
};
|
||||
|
||||
+14
-2
@@ -28,6 +28,7 @@ import commonjs from "@rollup/plugin-commonjs";
|
||||
import postcss from "postcss";
|
||||
import postcssImport from "postcss-import";
|
||||
import postcssPresetEnv from "postcss-preset-env";
|
||||
import postcssColorMod from "postcss-color-mod-function";
|
||||
import cssnano from "cssnano";
|
||||
import { optimize } from "svgo";
|
||||
import * as xmlParser from "../lib/xml-parser";
|
||||
@@ -74,6 +75,7 @@ function rmDirSync(dirPath): void {
|
||||
fs.rmdirSync(dirPath);
|
||||
}
|
||||
|
||||
// For lockfileVersion = 1
|
||||
function stripDevDeps(deps): void {
|
||||
if (!deps["dependencies"]) return;
|
||||
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"];
|
||||
}
|
||||
|
||||
// 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 {
|
||||
const children = [];
|
||||
for (const c of xml.children || []) children.push(xmlTostring(c));
|
||||
@@ -140,6 +151,7 @@ async function init(): Promise<string[]> {
|
||||
);
|
||||
npmShrinkwrapJson["version"] = packageJson["version"];
|
||||
stripDevDeps(npmShrinkwrapJson);
|
||||
stripDevDeps2(npmShrinkwrapJson);
|
||||
fs.writeFileSync(
|
||||
path.resolve(OUTPUT_DIR, "npm-shrinkwrap.json"),
|
||||
JSON.stringify(npmShrinkwrapJson, null, 2)
|
||||
@@ -174,10 +186,10 @@ async function generateCss(): Promise<void> {
|
||||
stage: 3,
|
||||
features: {
|
||||
"nesting-rules": true,
|
||||
"color-mod-function": true,
|
||||
},
|
||||
}),
|
||||
cssnano,
|
||||
postcssColorMod,
|
||||
...(MODE === "production" ? [cssnano] : []),
|
||||
]).process(cssIn, { from: cssInPath, to: cssOutPath });
|
||||
fs.writeFileSync(cssOutPath, cssOut.css);
|
||||
}
|
||||
|
||||
@@ -168,3 +168,9 @@ IPv
|
||||
downloadSuccessOnTimeout
|
||||
TransferComplete
|
||||
nginx
|
||||
ENCODEURICOMPONENT
|
||||
pageSize
|
||||
skipRootGpn
|
||||
GPN
|
||||
NaN
|
||||
CIDR
|
||||
+14
-27
@@ -20,11 +20,12 @@
|
||||
import { Collection } from "mongodb";
|
||||
import { onConnect } from "./db";
|
||||
import * as config from "./config";
|
||||
import { Cache } from "./mongodb-types";
|
||||
|
||||
const CLOCK_SKEW_TOLERANCE = 30000;
|
||||
const MAX_CACHE_TTL = +config.get("MAX_CACHE_TTL");
|
||||
|
||||
let cacheCollection: Collection;
|
||||
let cacheCollection: Collection<Cache>;
|
||||
|
||||
onConnect(async (db) => {
|
||||
cacheCollection = db.collection("cache");
|
||||
@@ -33,8 +34,7 @@ onConnect(async (db) => {
|
||||
|
||||
export async function get(key: string): Promise<string> {
|
||||
const res = await cacheCollection.findOne({ _id: key });
|
||||
if (res) return res["value"];
|
||||
return null;
|
||||
return res?.value;
|
||||
}
|
||||
|
||||
export async function del(key: string): Promise<void> {
|
||||
@@ -52,15 +52,14 @@ export async function set(
|
||||
);
|
||||
await cacheCollection.replaceOne(
|
||||
{ _id: key },
|
||||
{ _id: key, value, expire, timestamp },
|
||||
{ value, expire, timestamp },
|
||||
{ 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 });
|
||||
if (res?.["value"]) return res["value"]["value"];
|
||||
return null;
|
||||
return res.value?.value;
|
||||
}
|
||||
|
||||
export async function acquireLock(
|
||||
@@ -72,35 +71,23 @@ export async function acquireLock(
|
||||
try {
|
||||
const now = Date.now();
|
||||
const r = await cacheCollection.findOneAndUpdate(
|
||||
{ _id: lockName, token },
|
||||
{ _id: lockName, value: token },
|
||||
{
|
||||
$set: {
|
||||
value: token,
|
||||
expire: new Date(now + ttl + CLOCK_SKEW_TOLERANCE),
|
||||
},
|
||||
$currentDate: { timestamp: true as unknown as Date },
|
||||
$currentDate: { timestamp: true },
|
||||
},
|
||||
{ upsert: true, returnDocument: "after" }
|
||||
);
|
||||
const v = r.value;
|
||||
if (Math.abs(v["timestamp"].getTime() - now) > CLOCK_SKEW_TOLERANCE)
|
||||
if (Math.abs(r.value.timestamp.getTime() - now) > CLOCK_SKEW_TOLERANCE)
|
||||
throw new Error("Database clock skew too great");
|
||||
} catch (err) {
|
||||
if (err.code === 11000) {
|
||||
if (timeout > 0) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const w = 50 + Math.random() * 50;
|
||||
setTimeout(() => {
|
||||
acquireLock(lockName, ttl, timeout - w, token).then(
|
||||
resolve,
|
||||
reject
|
||||
);
|
||||
}, w);
|
||||
});
|
||||
}
|
||||
throw new Error("Failed to acquire lock");
|
||||
}
|
||||
throw err;
|
||||
if (err.code !== 11000) throw err;
|
||||
if (!(timeout > 0)) return null;
|
||||
const w = 50 + Math.random() * 50;
|
||||
await new Promise((resolve) => setTimeout(resolve, w));
|
||||
return acquireLock(lockName, ttl, timeout - w, token);
|
||||
}
|
||||
|
||||
return token;
|
||||
|
||||
@@ -1108,13 +1108,7 @@ function getCanRaiseCallback(
|
||||
return (idx: number, set: Set<number>): boolean => {
|
||||
const i = idx >> 2;
|
||||
const vars = context.getIsNullRelations(i);
|
||||
if (!context.isIsNull(i)) {
|
||||
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;
|
||||
}
|
||||
if (!context.isIsNull(i)) return !(idx & 1) || !set.has(idx ^ 3);
|
||||
|
||||
for (const k of vars) {
|
||||
if (set.has((k << 2) ^ 1)) continue;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -87,8 +87,6 @@ function binaryLeft(
|
||||
rest.reduce((acc, ch) => {
|
||||
const [op, another] = ch;
|
||||
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];
|
||||
}, first)
|
||||
);
|
||||
@@ -287,7 +285,11 @@ const lang = parsimmon.createLanguage({
|
||||
.skip(parsimmon.optWhitespace),
|
||||
binaryLeft(
|
||||
parsimmon
|
||||
.alt(parsimmon.string("*"), parsimmon.string("/"))
|
||||
.alt(
|
||||
parsimmon.string("*"),
|
||||
parsimmon.string("/"),
|
||||
parsimmon.string("%")
|
||||
)
|
||||
.skip(parsimmon.optWhitespace),
|
||||
parsimmon.alt(
|
||||
r.Value,
|
||||
@@ -386,6 +388,7 @@ export function stringify(exp: Expression, level = 0): string {
|
||||
"-": 31,
|
||||
"*": 32,
|
||||
"/": 32,
|
||||
"%": 32,
|
||||
};
|
||||
|
||||
const op = exp[0].toUpperCase();
|
||||
|
||||
@@ -244,9 +244,12 @@ export function evaluateCallback(exp: Expression): Expression {
|
||||
} else if (exp[0] === "/") {
|
||||
return reduce(exp, (a, b, i) => {
|
||||
if (a == null || b == null) return null;
|
||||
if (!isArray(a) && !isArray(b))
|
||||
return i === 0 ? toNumber(a) / toNumber(b) : toNumber(a) * toNumber(b);
|
||||
return REDUCE_SKIP;
|
||||
if (isArray(a) || isArray(b)) return REDUCE_SKIP;
|
||||
const n1 = toNumber(a);
|
||||
const n2 = toNumber(b);
|
||||
if (i !== 0) return n1 * n2;
|
||||
if (n2 === 0) return null;
|
||||
return n1 / n2;
|
||||
});
|
||||
} else if (exp[0] === "+") {
|
||||
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 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] === "||") {
|
||||
return reduce(exp, (a, b) => {
|
||||
if (a == null || b == null) return null;
|
||||
|
||||
+75
-19
@@ -45,6 +45,7 @@ import {
|
||||
InformRequest,
|
||||
Preset,
|
||||
GetRPCMethodsResponse,
|
||||
CpeFault,
|
||||
} from "./types";
|
||||
import { IncomingMessage, ServerResponse } from "http";
|
||||
import { Readable } from "stream";
|
||||
@@ -53,6 +54,7 @@ import { decode, encodingExists } from "iconv-lite";
|
||||
import { parseXmlDeclaration } from "./xml-parser";
|
||||
import * as debug from "./debug";
|
||||
import { getRequestOrigin } from "./forwarded";
|
||||
import { getSocketEndpoints } from "./server";
|
||||
|
||||
const gzipPromisified = promisify(zlib.gzip);
|
||||
const deflatePromisified = promisify(zlib.deflate);
|
||||
@@ -211,12 +213,13 @@ async function writeResponse(
|
||||
currentSessions.set(connection, sessionContext);
|
||||
if (now >= sessionContext.extendLock) {
|
||||
sessionContext.extendLock = now + 10000;
|
||||
await cache.acquireLock(
|
||||
const lockToken = await cache.acquireLock(
|
||||
`cwmp_session_${sessionContext.deviceId}`,
|
||||
sessionContext.timeout * 1000 + 15000,
|
||||
0,
|
||||
sessionContext.sessionId
|
||||
);
|
||||
if (!lockToken) throw new Error("Failed to extend lock");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -533,7 +536,7 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
|
||||
session.configContextCallback(sessionContext, e)
|
||||
)
|
||||
),
|
||||
]);
|
||||
]) as [string, ...Expression[]][];
|
||||
if (blackList[p.channel] === 2) {
|
||||
appendProvisionsToFaults[p.channel] = (
|
||||
appendProvisionsToFaults[p.channel] || []
|
||||
@@ -882,16 +885,8 @@ async function sendAcsRequest(
|
||||
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
|
||||
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 () => {
|
||||
const sessionContext = currentSessions.get(socket);
|
||||
if (!sessionContext) return;
|
||||
@@ -946,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(() => {
|
||||
if (stats.droppedRequests) {
|
||||
logger.warn({
|
||||
@@ -1129,15 +1150,15 @@ async function processRequest(
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
sessionContext.extendLock = sessionContext.timestamp + 10000;
|
||||
await cache.acquireLock(
|
||||
`cwmp_session_${sessionContext.deviceId}`,
|
||||
sessionContext.timeout * 1000 + 15000,
|
||||
0,
|
||||
sessionContext.sessionId
|
||||
);
|
||||
} catch (err) {
|
||||
sessionContext.extendLock = sessionContext.timestamp + 10000;
|
||||
const lockToken = await cache.acquireLock(
|
||||
`cwmp_session_${sessionContext.deviceId}`,
|
||||
sessionContext.timeout * 1000 + 15000,
|
||||
0,
|
||||
sessionContext.sessionId
|
||||
);
|
||||
|
||||
if (!lockToken) {
|
||||
logger.accessError({
|
||||
message: "CPE already in session",
|
||||
sessionContext: sessionContext,
|
||||
@@ -1256,6 +1277,41 @@ async function processRequest(
|
||||
session.clearProvisions(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 {
|
||||
// CPE sent empty response
|
||||
if (sessionContext.state !== 1) return reportBadState(sessionContext);
|
||||
|
||||
@@ -30,19 +30,20 @@ import {
|
||||
Expression,
|
||||
} from "./types";
|
||||
import Path from "./common/path";
|
||||
import * as MongoTypes from "./mongodb-types";
|
||||
|
||||
export let tasksCollection: Collection,
|
||||
export let tasksCollection: Collection<MongoTypes.Task>,
|
||||
devicesCollection: Collection,
|
||||
presetsCollection: Collection,
|
||||
objectsCollection: Collection,
|
||||
provisionsCollection: Collection,
|
||||
virtualParametersCollection: Collection,
|
||||
faultsCollection: Collection,
|
||||
faultsCollection: Collection<MongoTypes.Fault>,
|
||||
filesCollection: Collection,
|
||||
operationsCollection: Collection,
|
||||
operationsCollection: Collection<MongoTypes.Operation>,
|
||||
permissionsCollection: Collection,
|
||||
usersCollection: Collection,
|
||||
configCollection: Collection;
|
||||
configCollection: Collection<MongoTypes.Config>;
|
||||
|
||||
let clientPromise: Promise<MongoClient>;
|
||||
|
||||
@@ -91,7 +92,9 @@ export async function disconnect(): Promise<void> {
|
||||
|
||||
// Optimize projection by removing overlaps
|
||||
// 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[""] };
|
||||
|
||||
const keys = Object.keys(obj).sort();
|
||||
@@ -146,7 +149,7 @@ export async function fetchDevice(
|
||||
|
||||
if (obj["_value"] != null) {
|
||||
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];
|
||||
|
||||
obj["_object"] = false;
|
||||
@@ -446,14 +449,18 @@ export async function saveDevice(
|
||||
if (value2 != null) {
|
||||
if (!update["$addToSet"]["_tags"])
|
||||
update["$addToSet"]["_tags"] = { $each: [] };
|
||||
update["$addToSet"]["_tags"]["$each"].push(path.segments[1]);
|
||||
update["$addToSet"]["_tags"]["$each"].push(
|
||||
decodeTag(path.segments[1] as string)
|
||||
);
|
||||
} else {
|
||||
if (!update["$pull"]["_tags"]) {
|
||||
update["$pull"]["_tags"] = {
|
||||
$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 } = {};
|
||||
for (const r of res) {
|
||||
const channel = r._id.slice(deviceId.length + 1);
|
||||
delete r._id;
|
||||
delete r.channel;
|
||||
delete r.device;
|
||||
r.timestamp = +r.timestamp;
|
||||
r.provisions = JSON.parse(r.provisions);
|
||||
faults[channel] = r as SessionFault;
|
||||
const fault: SessionFault = {
|
||||
code: r.code,
|
||||
message: r.message,
|
||||
...(r.detail && { detail: r.detail }),
|
||||
timestamp: +r.timestamp,
|
||||
provisions: JSON.parse(r.provisions),
|
||||
retries: r.retries,
|
||||
...(r.expiry && { expiry: +r.expiry }),
|
||||
};
|
||||
faults[channel] = fault;
|
||||
}
|
||||
|
||||
return faults;
|
||||
@@ -654,12 +665,18 @@ export async function saveFault(
|
||||
fault: SessionFault
|
||||
): Promise<void> {
|
||||
const id = `${deviceId}:${channel}`;
|
||||
const f = Object.assign({}, fault) as Record<string, any>;
|
||||
f["_id"] = id;
|
||||
f["device"] = deviceId;
|
||||
f["channel"] = channel;
|
||||
f["timestamp"] = new Date(fault.timestamp);
|
||||
f["provisions"] = JSON.stringify(fault.provisions);
|
||||
const f: MongoTypes.Fault = {
|
||||
_id: id,
|
||||
device: deviceId,
|
||||
channel: channel,
|
||||
timestamp: new Date(fault.timestamp),
|
||||
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 });
|
||||
}
|
||||
|
||||
@@ -677,20 +694,47 @@ export async function getDueTasks(
|
||||
const cur = tasksCollection.find({ device: deviceId }).sort({ timestamp: 1 });
|
||||
const tasks = [] as Task[];
|
||||
|
||||
for await (const task of cur) {
|
||||
if (task.timestamp) task.timestamp = +task.timestamp;
|
||||
if (task.expiry) task.expiry = +task.expiry;
|
||||
if (task.timestamp >= timestamp) return [tasks, +task.timestamp];
|
||||
task._id = String(task._id);
|
||||
for await (const t of cur) {
|
||||
if (+t.timestamp >= timestamp) return [tasks, +t.timestamp];
|
||||
const task: Task = {
|
||||
_id: t._id.toString(),
|
||||
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
|
||||
if (task.name === "download" && task.file) {
|
||||
if (task.name === "download" && t["file"]) {
|
||||
let q;
|
||||
if (ObjectId.isValid(task.file))
|
||||
q = { _id: { $in: [task.file, new ObjectId(task.file)] } };
|
||||
else q = { _id: task.file };
|
||||
if (ObjectId.isValid(t["file"]))
|
||||
q = { _id: { $in: [t["file"], new ObjectId(t["file"])] } };
|
||||
else q = { _id: t["file"] };
|
||||
|
||||
const res = await filesCollection.find(q).toArray();
|
||||
|
||||
@@ -724,17 +768,22 @@ export async function getOperations(
|
||||
const operations: { [commandKey: string]: Operation } = {};
|
||||
for (const r of res) {
|
||||
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
|
||||
if (typeof r.provisions !== "string") {
|
||||
operations[commandKey] = r as Operation;
|
||||
delete r._id;
|
||||
operations[commandKey] = r as unknown as Operation;
|
||||
continue;
|
||||
}
|
||||
r.timestamp = +r.timestamp;
|
||||
if (r.args) r.args = JSON.parse(r.args);
|
||||
r.provisions = JSON.parse(r.provisions);
|
||||
r.retries = JSON.parse(r.retries);
|
||||
operations[commandKey] = r as Operation;
|
||||
const operation: Operation = {
|
||||
name: r.name,
|
||||
timestamp: +r.timestamp,
|
||||
channels:
|
||||
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;
|
||||
}
|
||||
@@ -745,12 +794,15 @@ export async function saveOperation(
|
||||
operation: Operation
|
||||
): Promise<void> {
|
||||
const id = `${deviceId}:${commandKey}`;
|
||||
const o = Object.assign({}, operation) as Record<string, any>;
|
||||
o["_id"] = id;
|
||||
o["timestamp"] = new Date(operation.timestamp);
|
||||
o["provisions"] = JSON.stringify(operation.provisions);
|
||||
o["retries"] = JSON.stringify(operation.retries);
|
||||
o["args"] = JSON.stringify(operation.args);
|
||||
const o: MongoTypes.Operation = {
|
||||
_id: id,
|
||||
name: operation.name,
|
||||
timestamp: new Date(operation.timestamp),
|
||||
channels: JSON.stringify(operation.channels),
|
||||
provisions: JSON.stringify(operation.provisions),
|
||||
retries: JSON.stringify(operation.retries),
|
||||
args: JSON.stringify(operation.args),
|
||||
};
|
||||
await operationsCollection.replaceOne({ _id: id }, o, {
|
||||
upsert: true,
|
||||
});
|
||||
|
||||
+23
-3
@@ -22,6 +22,7 @@ import { Socket } from "net";
|
||||
import { appendFileSync } from "fs";
|
||||
import { stringify } from "./common/yaml";
|
||||
import * as config from "./config";
|
||||
import { getSocketEndpoints } from "./server";
|
||||
|
||||
const DEBUG_FILE = "" + config.get("DEBUG_FILE");
|
||||
const DEBUG_FORMAT = "" + config.get("DEBUG_FORMAT");
|
||||
@@ -45,13 +46,14 @@ export function incomingHttpRequest(
|
||||
if (!DEBUG_FILE) return;
|
||||
const now = new Date();
|
||||
const con = httpRequest.socket;
|
||||
const socketEndpoints = getSocketEndpoints(con);
|
||||
const msg = {
|
||||
event: "incoming HTTP request",
|
||||
timestamp: now,
|
||||
remoteAddress: con.remoteAddress,
|
||||
remoteAddress: socketEndpoints.remoteAddress,
|
||||
deviceId: deviceId,
|
||||
connection: getConnectionTimestamp(con),
|
||||
localPort: con.localPort,
|
||||
localPort: socketEndpoints.localPort,
|
||||
method: httpRequest.method,
|
||||
url: httpRequest.url,
|
||||
headers: httpRequest.headers,
|
||||
@@ -73,10 +75,11 @@ export function outgoingHttpResponse(
|
||||
if (!DEBUG_FILE) return;
|
||||
const now = new Date();
|
||||
const con = httpResponse.socket;
|
||||
const socketEndpoints = getSocketEndpoints(con);
|
||||
const msg = {
|
||||
event: "outgoing HTTP response",
|
||||
timestamp: now,
|
||||
remoteAddress: con.remoteAddress,
|
||||
remoteAddress: socketEndpoints.remoteAddress,
|
||||
deviceId: deviceId,
|
||||
connection: getConnectionTimestamp(con),
|
||||
statusCode: httpResponse.statusCode,
|
||||
@@ -199,3 +202,20 @@ export function outgoingUdpMessage(
|
||||
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
|
||||
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
@@ -21,6 +21,7 @@ import { IncomingMessage } from "http";
|
||||
import { TLSSocket } from "tls";
|
||||
import { parseCIDR, parse, IPv6, IPv4 } from "ipaddr.js";
|
||||
import * as config from "./config";
|
||||
import { getSocketEndpoints } from "./server";
|
||||
|
||||
interface RequestOrigin {
|
||||
localAddress: string;
|
||||
@@ -98,19 +99,22 @@ export function getRequestOrigin(request: IncomingMessage): RequestOrigin {
|
||||
let origin = cache.get(request);
|
||||
if (!origin) {
|
||||
const soc = request.socket;
|
||||
const socketEndpoints = getSocketEndpoints(soc);
|
||||
origin = {
|
||||
localAddress: soc.localAddress,
|
||||
localPort: soc.localPort,
|
||||
remoteAddress: soc.remoteAddress,
|
||||
remotePort: soc.remotePort,
|
||||
localAddress: socketEndpoints.localAddress,
|
||||
localPort: socketEndpoints.localPort,
|
||||
remoteAddress: socketEndpoints.remoteAddress,
|
||||
remotePort: socketEndpoints.remotePort,
|
||||
host: request.headers["host"],
|
||||
encrypted: !!(request.socket as TLSSocket).encrypted,
|
||||
};
|
||||
|
||||
const header = request.headers["forwarded"];
|
||||
if (header) {
|
||||
const ip = parse(soc.remoteAddress) as IPv4;
|
||||
if (cidrs.some((cidr) => ip.match(cidr as [IPv4, number]))) {
|
||||
const ip = parse(socketEndpoints.remoteAddress);
|
||||
if (
|
||||
cidrs.some((cidr) => ip.kind() === cidr[0].kind() && ip.match(cidr))
|
||||
) {
|
||||
const parsed = parseForwardedHeader(header);
|
||||
|
||||
if (parsed["proto"] === "https") {
|
||||
|
||||
+13
-6
@@ -351,6 +351,7 @@ async function fetchConfig(): Promise<[Config, UiConfig]> {
|
||||
device: {},
|
||||
index: {},
|
||||
overview: {},
|
||||
pageSize: null,
|
||||
};
|
||||
const _config = {};
|
||||
|
||||
@@ -379,13 +380,15 @@ async function fetchConfig(): Promise<[Config, UiConfig]> {
|
||||
async function refresh(): Promise<void> {
|
||||
if (!nextRefresh) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
await refresh();
|
||||
return;
|
||||
return refresh();
|
||||
}
|
||||
|
||||
nextRefresh = 0;
|
||||
const now = Date.now();
|
||||
|
||||
if (now < nextRefresh) return;
|
||||
|
||||
nextRefresh = 0;
|
||||
|
||||
const dbHash = await cache.get("presets_hash");
|
||||
|
||||
if (currentSnapshot && dbHash === currentSnapshot) {
|
||||
@@ -393,7 +396,7 @@ async function refresh(): Promise<void> {
|
||||
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([
|
||||
fetchPresets(),
|
||||
@@ -426,8 +429,12 @@ async function refresh(): Promise<void> {
|
||||
|
||||
currentSnapshot = computeHash(snapshot);
|
||||
snapshots.set(currentSnapshot, snapshot);
|
||||
await cache.set("presets_hash", currentSnapshot, 300);
|
||||
await cache.releaseLock("presets_hash_lock", lockToken);
|
||||
|
||||
if (lockToken) {
|
||||
if (currentSnapshot !== dbHash)
|
||||
await cache.set("presets_hash", currentSnapshot, 300);
|
||||
await cache.releaseLock("presets_hash_lock", lockToken);
|
||||
}
|
||||
|
||||
nextRefresh = now + (REFRESH - (now % REFRESH));
|
||||
}
|
||||
|
||||
@@ -77,7 +77,8 @@ export function processDeviceFilter(filter: Expression): Expression {
|
||||
if (typeof e[2] === "number") {
|
||||
const alt = (e as any[]).slice();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -21,7 +21,7 @@ import * as url from "url";
|
||||
import { Collection, GridFSBucket, ObjectId } from "mongodb";
|
||||
import * as vm from "vm";
|
||||
import * as config from "./config";
|
||||
import { onConnect } from "./db";
|
||||
import { onConnect, optimizeProjection } from "./db";
|
||||
import * as query from "./query";
|
||||
import * as apiFunctions from "./api-functions";
|
||||
import { IncomingMessage, ServerResponse } from "http";
|
||||
@@ -30,6 +30,7 @@ import { version as VERSION } from "../package.json";
|
||||
import { ping } from "./ping";
|
||||
import * as logger from "./logger";
|
||||
import { flattenDevice } from "./mongodb-functions";
|
||||
import { getSocketEndpoints } from "./server";
|
||||
|
||||
const DEVICE_TASKS_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/tasks\/?$/;
|
||||
const TASKS_REGEX = /^\/tasks\/([a-zA-Z0-9\-_%]+)(\/[a-zA-Z_]*)?$/;
|
||||
@@ -101,10 +102,11 @@ export function listener(
|
||||
request.addListener("end", () => {
|
||||
const body = getBody();
|
||||
const urlParts = url.parse(request.url, true);
|
||||
const socketEndpoints = getSocketEndpoints(request.socket);
|
||||
|
||||
logger.accessInfo(
|
||||
Object.assign({}, urlParts.query, {
|
||||
remoteAddress: request.socket.remoteAddress,
|
||||
remoteAddress: socketEndpoints.remoteAddress,
|
||||
message: `${request.method} ${urlParts.pathname}`,
|
||||
})
|
||||
);
|
||||
@@ -819,6 +821,7 @@ export function listener(
|
||||
projection = {};
|
||||
for (const p of (urlParts.query.projection as string).split(","))
|
||||
projection[p.trim()] = 1;
|
||||
projection = optimizeProjection(projection);
|
||||
}
|
||||
|
||||
const cur = collection.find(q, { projection: projection });
|
||||
@@ -842,7 +845,7 @@ export function listener(
|
||||
cur.sort(sort);
|
||||
}
|
||||
|
||||
cur.count((err, total) => {
|
||||
collection.countDocuments(q, (err, total) => {
|
||||
if (err) return void throwError(err);
|
||||
|
||||
response.writeHead(200, {
|
||||
|
||||
+66
-43
@@ -19,6 +19,7 @@
|
||||
|
||||
import { platform } from "os";
|
||||
import { exec } from "child_process";
|
||||
import { domainToASCII } from "url";
|
||||
|
||||
export interface PingResult {
|
||||
packetsTransmitted: number;
|
||||
@@ -30,65 +31,87 @@ export interface PingResult {
|
||||
mdev: number;
|
||||
}
|
||||
|
||||
export function ping(
|
||||
host: string,
|
||||
callback: (err: Error, res?: PingResult, stdout?: string) => void
|
||||
): void {
|
||||
let cmd: string, parseRegExp1: RegExp, parseRegExp2: RegExp;
|
||||
host = host.replace("[", "").replace("]", "");
|
||||
switch (platform()) {
|
||||
function isValidHost(host: string): boolean {
|
||||
// Valid chars in IPv4, IPv6, domain names
|
||||
if (/^[a-zA-Z0-9\-.:[\]-]+$/.test(host)) return true;
|
||||
|
||||
// Check if input is an IDN convert to Punycode
|
||||
// Can't merge with above because domainToASCII doesn't accept IP addresses
|
||||
return /^[a-zA-Z0-9\-.:[\]-]+$/.test(domainToASCII(host));
|
||||
}
|
||||
|
||||
export function parsePing(osPlatform: string, stdout: string): PingResult {
|
||||
let parseRegExp1: RegExp, parseRegExp2: RegExp, parsed: PingResult;
|
||||
switch (osPlatform) {
|
||||
case "linux":
|
||||
cmd = `ping -w 1 -i 0.2 -c 3 ${host}`;
|
||||
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 =
|
||||
/(\d+) packets transmitted, (\d+) received, ([\d.]+)% packet loss/;
|
||||
/(\d+) packets transmitted, (\d+) .*received, ([\d.]+)% .*loss/;
|
||||
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}`;
|
||||
parseRegExp1 =
|
||||
/(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss\nround-trip min\/avg\/max\/stddev = ([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+) ms/;
|
||||
parseRegExp2 =
|
||||
/(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss/;
|
||||
break;
|
||||
}
|
||||
|
||||
const m1 = stdout.match(parseRegExp1);
|
||||
if (m1) {
|
||||
parsed = {
|
||||
packetsTransmitted: +m1[1],
|
||||
packetsReceived: +m1[2],
|
||||
packetLoss: +m1[3],
|
||||
min: +m1[4],
|
||||
avg: +m1[5],
|
||||
max: +m1[6],
|
||||
mdev: +m1[7],
|
||||
};
|
||||
} else {
|
||||
const m2 = stdout.match(parseRegExp2);
|
||||
if (m2) {
|
||||
parsed = {
|
||||
packetsTransmitted: +m2[1],
|
||||
packetsReceived: +m2[2],
|
||||
packetLoss: +m2[3],
|
||||
min: null,
|
||||
avg: null,
|
||||
max: null,
|
||||
mdev: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
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) => {
|
||||
let parsed: PingResult;
|
||||
if (stdout) {
|
||||
const m1 = stdout.match(parseRegExp1);
|
||||
if (m1) {
|
||||
parsed = {
|
||||
packetsTransmitted: +m1[1],
|
||||
packetsReceived: +m1[2],
|
||||
packetLoss: +m1[3],
|
||||
min: +m1[4],
|
||||
avg: +m1[5],
|
||||
max: +m1[6],
|
||||
mdev: +m1[7],
|
||||
};
|
||||
} else {
|
||||
const m2 = stdout.match(parseRegExp2);
|
||||
if (m2) {
|
||||
parsed = {
|
||||
packetsTransmitted: +m2[1],
|
||||
packetsReceived: +m2[2],
|
||||
packetLoss: +m2[3],
|
||||
min: null,
|
||||
avg: null,
|
||||
max: null,
|
||||
mdev: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback(err, parsed, stdout);
|
||||
if (err) return callback(err);
|
||||
const parsed: PingResult = parsePing(platform(), stdout);
|
||||
return callback(err, parsed, stdout);
|
||||
});
|
||||
}
|
||||
+13
-1
@@ -54,6 +54,17 @@ function normalize(input): any {
|
||||
return input;
|
||||
}
|
||||
|
||||
const EXPAND_OPS = new Set([
|
||||
"$eq",
|
||||
"$gt",
|
||||
"$gte",
|
||||
"$in",
|
||||
"$lt",
|
||||
"$lte",
|
||||
"$ne",
|
||||
"$nin",
|
||||
]);
|
||||
|
||||
function expandValue(value: unknown): unknown[] {
|
||||
if (Array.isArray(value)) {
|
||||
let a = [];
|
||||
@@ -71,7 +82,8 @@ function expandValue(value: unknown): unknown[] {
|
||||
const values = [];
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
keys.push(k);
|
||||
values.push(expandValue(v));
|
||||
if (EXPAND_OPS.has(k)) values.push(expandValue(v));
|
||||
else values.push([v]);
|
||||
indices.push(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -255,6 +255,7 @@ function declare(
|
||||
}
|
||||
|
||||
for (const [attrName, attrTimestamp] of Object.entries(timestamps)) {
|
||||
if (!(attrTimestamp >= 1)) continue;
|
||||
if (attrName === "path") {
|
||||
declaration.pathGet = attrTimestamp;
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
import * as crypto from "crypto";
|
||||
import * as later from "later";
|
||||
import * as later from "@breejs/later";
|
||||
|
||||
function md532(str): number {
|
||||
const digest = crypto.createHash("md5").update(str).digest();
|
||||
|
||||
@@ -59,8 +59,20 @@ interface ServerOptions {
|
||||
timeout?: number;
|
||||
keepAliveTimeout?: number;
|
||||
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(
|
||||
options: ServerOptions,
|
||||
_listener: http.RequestListener
|
||||
@@ -85,6 +97,27 @@ export function start(
|
||||
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;
|
||||
if (options.keepAliveTimeout != null)
|
||||
server.keepAliveTimeout = options.keepAliveTimeout;
|
||||
@@ -99,3 +132,8 @@ export function stop(): Promise<void> {
|
||||
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
@@ -504,7 +504,7 @@ export async function timeoutOperations(
|
||||
export function addProvisions(
|
||||
sessionContext: SessionContext,
|
||||
channel: string,
|
||||
provisions: (string | number | boolean)[][]
|
||||
provisions: [string, ...Expression[]][]
|
||||
): void {
|
||||
// Multiply by two because every iteration is two
|
||||
// phases: read and update
|
||||
@@ -1624,6 +1624,20 @@ function generateGetRpcRequest(
|
||||
(a, b) => b.length - a.length
|
||||
);
|
||||
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 (
|
||||
path &&
|
||||
path.length &&
|
||||
@@ -2678,7 +2692,10 @@ export async function rpcResponse(
|
||||
const wildcardParams: Path[] = [root.concat(wildcardPath)];
|
||||
|
||||
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({
|
||||
sessionContext: sessionContext,
|
||||
message: "Unexpected parameter in response",
|
||||
|
||||
+18
-1
@@ -730,6 +730,18 @@ function RequestDownloadResponse(): string {
|
||||
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 {
|
||||
let faultCode: string,
|
||||
faultString: string,
|
||||
@@ -838,6 +850,7 @@ export function request(
|
||||
cpeRequest: null,
|
||||
cpeFault: null,
|
||||
cpeResponse: null,
|
||||
unknownMethod: null,
|
||||
};
|
||||
|
||||
if (!body.length) return rpc;
|
||||
@@ -959,7 +972,8 @@ export function request(
|
||||
rpc.cpeFault = fault(methodElement);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`8000 Method not supported ${methodElement.localName}`);
|
||||
rpc.unknownMethod = methodElement.localName;
|
||||
break;
|
||||
}
|
||||
|
||||
return rpc;
|
||||
@@ -987,6 +1001,7 @@ export function response(rpc: {
|
||||
id: string;
|
||||
acsRequest?: AcsRequest;
|
||||
acsResponse?: AcsResponse;
|
||||
acsFault?: CpeFault;
|
||||
cwmpVersion?: string;
|
||||
}): { code: number; headers: Record<string, string>; data: string } {
|
||||
const headers = {
|
||||
@@ -1055,6 +1070,8 @@ export function response(rpc: {
|
||||
`Unknown method request ${(rpc.acsRequest as AcsRequest).name}`
|
||||
);
|
||||
}
|
||||
} else if (rpc.acsFault) {
|
||||
body = AcsFault(rpc.acsFault);
|
||||
}
|
||||
|
||||
headers["Content-Type"] = 'text/xml; charset="utf-8"';
|
||||
|
||||
+2
-1
@@ -180,7 +180,7 @@ export interface Task {
|
||||
fileName?: string;
|
||||
targetFileName?: string;
|
||||
expiry?: number;
|
||||
provisions?: (string | number | boolean)[][];
|
||||
provisions?: [string, ...Expression[]][];
|
||||
}
|
||||
|
||||
export interface Operation {
|
||||
@@ -526,6 +526,7 @@ export interface SoapMessage {
|
||||
cpeRequest?: CpeRequest;
|
||||
cpeFault?: CpeFault;
|
||||
cpeResponse?: CpeResponse;
|
||||
unknownMethod?: string;
|
||||
}
|
||||
|
||||
export interface ScriptResult {
|
||||
|
||||
@@ -228,7 +228,7 @@ router.get("/", async (ctx) => {
|
||||
if (!Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length)
|
||||
wizard = '<script>window.location.hash = "#!/wizard";</script>';
|
||||
|
||||
ctx.body = `
|
||||
ctx.body = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>GenieACS</title>
|
||||
@@ -263,6 +263,7 @@ koa.use(
|
||||
flush: constants.Z_SYNC_FLUSH,
|
||||
},
|
||||
br: {
|
||||
flush: constants.BROTLI_OPERATION_FLUSH,
|
||||
params: {
|
||||
[constants.BROTLI_PARAM_QUALITY]: 5,
|
||||
},
|
||||
|
||||
+24
-15
@@ -17,7 +17,7 @@
|
||||
* 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 * as db from "./db";
|
||||
import * as apiFunctions from "./api-functions";
|
||||
@@ -103,7 +103,7 @@ router.get(`/devices/:id.csv`, async (ctx) => {
|
||||
.replace(/[:.]/g, "")}.csv`
|
||||
);
|
||||
|
||||
ctx.body = new stream.PassThrough();
|
||||
ctx.body = new PassThrough();
|
||||
ctx.body.write(
|
||||
"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(),
|
||||
p.writable,
|
||||
new Date(p.writableTimestamp).toJSON(),
|
||||
`"${value.toString().replace(/"/g, '""')}"`,
|
||||
`"${String(value).replace(/"/g, '""')}"`,
|
||||
type,
|
||||
new Date(p.valueTimestamp).toJSON(),
|
||||
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";
|
||||
|
||||
let c = 0;
|
||||
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.end("]");
|
||||
|
||||
logger.accessInfo(log);
|
||||
})
|
||||
.then(() => {
|
||||
ctx.body.end("]");
|
||||
logger.accessInfo(log);
|
||||
})
|
||||
.catch((err) => {
|
||||
ctx.body.emit("error", err);
|
||||
});
|
||||
});
|
||||
|
||||
// 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.attachment(
|
||||
`${resource}-${new Date(now).toISOString().replace(/[:.]/g, "")}.csv`
|
||||
@@ -285,7 +289,7 @@ for (const [resource, flags] of Object.entries(resources)) {
|
||||
ctx.body.write(
|
||||
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 v = evaluate(exp, obj, null, (e) => {
|
||||
if (Array.isArray(e)) {
|
||||
@@ -327,10 +331,14 @@ for (const [resource, flags] of Object.entries(resources)) {
|
||||
return v;
|
||||
});
|
||||
ctx.body.write(arr.join(",") + "\n");
|
||||
});
|
||||
ctx.body.end();
|
||||
|
||||
logger.accessInfo(log);
|
||||
})
|
||||
.then(() => {
|
||||
ctx.body.end();
|
||||
logger.accessInfo(log);
|
||||
})
|
||||
.catch((err) => {
|
||||
ctx.body.emit("error", err);
|
||||
});
|
||||
});
|
||||
|
||||
router.head(`/${resource}/:id`, async (ctx) => {
|
||||
@@ -652,6 +660,7 @@ router.post("/devices/:id/tags", async (ctx) => {
|
||||
});
|
||||
|
||||
router.get("/ping/:host", async (ctx) => {
|
||||
if (!ctx.state.user) return void (ctx.status = 401);
|
||||
return new Promise<void>((resolve) => {
|
||||
ping(ctx.params.host, (err, parsed) => {
|
||||
if (parsed) {
|
||||
|
||||
+17
-20
@@ -19,7 +19,7 @@
|
||||
|
||||
import { Db, GridFSBucket, ObjectId } from "mongodb";
|
||||
import { Script } from "vm";
|
||||
import { onConnect } from "../db";
|
||||
import { onConnect, optimizeProjection } from "../db";
|
||||
import * as mongodbFunctions from "../mongodb-functions";
|
||||
import * as expression from "../common/expression";
|
||||
import { QueryOptions, Expression } from "../types";
|
||||
@@ -89,6 +89,7 @@ export function query(
|
||||
}
|
||||
|
||||
if (resource === "presets") projection.configurations = 1;
|
||||
projection = optimizeProjection(projection);
|
||||
cursor.project(projection);
|
||||
}
|
||||
|
||||
@@ -114,30 +115,31 @@ export function query(
|
||||
cursor.toArray((err, docs) => {
|
||||
if (err) return reject(err);
|
||||
if (resource === "devices")
|
||||
docs = docs.map((d) => mongodbFunctions.flattenDevice(d));
|
||||
return resolve(docs.map((d) => mongodbFunctions.flattenDevice(d)));
|
||||
else if (resource === "faults")
|
||||
docs = docs.map((d) => mongodbFunctions.flattenFault(d));
|
||||
return resolve(docs.map((d) => mongodbFunctions.flattenFault(d)));
|
||||
else if (resource === "tasks")
|
||||
docs = docs.map((d) => mongodbFunctions.flattenTask(d));
|
||||
return resolve(docs.map((d) => mongodbFunctions.flattenTask(d)));
|
||||
else if (resource === "presets")
|
||||
docs = docs.map((d) => mongodbFunctions.flattenPreset(d));
|
||||
return resolve(docs.map((d) => mongodbFunctions.flattenPreset(d)));
|
||||
else if (resource === "files")
|
||||
docs = docs.map((d) => mongodbFunctions.flattenFile(d));
|
||||
return resolve(docs.map((d) => mongodbFunctions.flattenFile(d)));
|
||||
return resolve(docs);
|
||||
});
|
||||
} else {
|
||||
cursor.forEach(
|
||||
(doc) => {
|
||||
if (resource === "devices") doc = mongodbFunctions.flattenDevice(doc);
|
||||
if (resource === "devices")
|
||||
callback(mongodbFunctions.flattenDevice(doc));
|
||||
else if (resource === "faults")
|
||||
doc = mongodbFunctions.flattenFault(doc);
|
||||
callback(mongodbFunctions.flattenFault(doc));
|
||||
else if (resource === "tasks")
|
||||
doc = mongodbFunctions.flattenTask(doc);
|
||||
callback(mongodbFunctions.flattenTask(doc));
|
||||
else if (resource === "presets")
|
||||
doc = mongodbFunctions.flattenPreset(doc);
|
||||
callback(mongodbFunctions.flattenPreset(doc));
|
||||
else if (resource === "files")
|
||||
doc = mongodbFunctions.flattenFile(doc);
|
||||
callback(doc);
|
||||
callback(mongodbFunctions.flattenFile(doc));
|
||||
else callback(doc);
|
||||
},
|
||||
(err) => {
|
||||
if (err) reject(err);
|
||||
@@ -149,7 +151,8 @@ export function query(
|
||||
}
|
||||
|
||||
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 = minimize(filter, true);
|
||||
|
||||
@@ -165,13 +168,7 @@ export function count(resource: string, filter: Expression): Promise<number> {
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const collection = db.collection(RESOURCE_COLLECTION[resource] || resource);
|
||||
collection.find(q).count((err, c) => {
|
||||
if (err) reject(err);
|
||||
else resolve(c);
|
||||
});
|
||||
});
|
||||
return collection.countDocuments(q);
|
||||
}
|
||||
|
||||
export async function updateDeviceTags(
|
||||
|
||||
@@ -168,6 +168,7 @@ export default class VersionedMap<K, V> {
|
||||
|
||||
public *diff(): IterableIterator<[K, V, V]> {
|
||||
for (const [key, revisions] of this.map) {
|
||||
if (revisions.length === 1) continue;
|
||||
let first = revisions[0];
|
||||
let last = revisions[revisions.length - 1];
|
||||
if (first === NONEXISTENT && last === NONEXISTENT) continue;
|
||||
|
||||
Generated
+9669
-4221
File diff suppressed because it is too large.
Load diff
+33
-32
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "genieacs",
|
||||
"version": "1.2.7",
|
||||
"version": "1.2.9",
|
||||
"description": "A TR-069 Auto Configuration Server (ACS)",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -19,55 +19,56 @@
|
||||
"bin": "bin"
|
||||
},
|
||||
"dependencies": {
|
||||
"@breejs/later": "^4.1.0",
|
||||
"espresso-iisojs": "^1.0.4",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"ipaddr.js": "^2.0.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"koa": "^2.13.1",
|
||||
"koa": "^2.13.4",
|
||||
"koa-bodyparser": "^4.3.0",
|
||||
"koa-compress": "^5.1.0",
|
||||
"koa-jwt": "^4.0.1",
|
||||
"koa-router": "^10.1.1",
|
||||
"koa-jwt": "^4.0.3",
|
||||
"koa-router": "^12.0.0",
|
||||
"koa-static": "^5.0.0",
|
||||
"later": "^1.2.0",
|
||||
"mongodb": "^4.1.2",
|
||||
"parsimmon": "^1.18.0",
|
||||
"mongodb": "^4.9.0",
|
||||
"parsimmon": "^1.18.1",
|
||||
"seedrandom": "^3.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^20.0.0",
|
||||
"@rollup/plugin-commonjs": "^22.0.2",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^13.0.4",
|
||||
"@rollup/plugin-typescript": "^8.2.5",
|
||||
"@types/codemirror": "^5.60.2",
|
||||
"@types/jsonwebtoken": "^8.5.5",
|
||||
"@types/koa": "^2.13.4",
|
||||
"@rollup/plugin-node-resolve": "^13.3.0",
|
||||
"@rollup/plugin-typescript": "^8.3.4",
|
||||
"@types/codemirror": "^5.60.5",
|
||||
"@types/jsonwebtoken": "^8.5.8",
|
||||
"@types/koa": "^2.13.5",
|
||||
"@types/koa-compress": "^4.0.3",
|
||||
"@types/koa-router": "^7.4.4",
|
||||
"@types/koa-static": "^4.0.2",
|
||||
"@types/mithril": "^2.0.8",
|
||||
"@types/node": "^16.9.2",
|
||||
"@types/mithril": "^2.0.11",
|
||||
"@types/node": "^18.7.9",
|
||||
"@types/parsimmon": "^1.10.6",
|
||||
"@types/seedrandom": "^3.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.31.1",
|
||||
"@typescript-eslint/parser": "^4.31.1",
|
||||
"@types/seedrandom": "^3.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.33.1",
|
||||
"@typescript-eslint/parser": "^5.33.1",
|
||||
"ava": "^3.15.0",
|
||||
"codemirror": "^5.62.3",
|
||||
"cssnano": "^5.0.8",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"codemirror": "^5.65.8",
|
||||
"cssnano": "^5.1.13",
|
||||
"eslint": "^8.22.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"esm": "^3.2.25",
|
||||
"mithril": "^2.0.4",
|
||||
"postcss": "^8.3.6",
|
||||
"postcss-import": "^14.0.2",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"prettier": "^2.4.1",
|
||||
"rollup": "^2.56.3",
|
||||
"mithril": "^2.2.2",
|
||||
"postcss": "^8.4.16",
|
||||
"postcss-color-mod-function": "^3.0.3",
|
||||
"postcss-import": "^14.1.0",
|
||||
"postcss-preset-env": "^7.8.0",
|
||||
"prettier": "^2.7.1",
|
||||
"rollup": "^2.78.1",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"sql.js": "^1.6.1",
|
||||
"svgo": "^2.6.1",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript": "^4.4.3",
|
||||
"sql.js": "^1.7.0",
|
||||
"svgo": "^2.8.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.7.4",
|
||||
"yaml": "^1.10.2"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ava, { TestInterface } from "ava";
|
||||
import { minimize, unionDiff } from "../lib/common/boolean-expression";
|
||||
import { parse, stringify } from "../lib/common/expression-parser";
|
||||
import initSqlJs from "sql.js";
|
||||
import initSqlJs from "sql.js/dist/sql-asm.js";
|
||||
|
||||
const test = ava as TestInterface<{
|
||||
query: (filter: string) => Promise<Set<number>>;
|
||||
|
||||
@@ -80,6 +80,24 @@ ava("filterToMongoQuery", async (t) => {
|
||||
["Tags.tag1 = 123", { "Tags.tag1": 123 }],
|
||||
["Param1 = 'value1'", { "Param1._value": "value1" }],
|
||||
["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 IS NOT NULL", { "Param1._value": { $ne: null } }],
|
||||
["Param1 LIKE 'value'", { "Param1._value": /^value$/ }],
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -99,9 +99,9 @@ export default class Autocomplete {
|
||||
if (this.visible) this.hide();
|
||||
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.top = `${domRect.bottom + window.pageYOffset}px`;
|
||||
this.container.style.top = `${domRect.bottom + window.scrollY}px`;
|
||||
}
|
||||
|
||||
private hide(): void {
|
||||
|
||||
@@ -23,6 +23,8 @@ import * as taskQueue from "../task-queue";
|
||||
import { parse } from "../../lib/common/expression-parser";
|
||||
import memoize from "../../lib/common/memoize";
|
||||
import { getIcon } from "../icons";
|
||||
import { evaluateExpression } from "../store";
|
||||
import debounce from "../../lib/common/debounce";
|
||||
|
||||
const memoizedParse = memoize(parse);
|
||||
|
||||
@@ -51,29 +53,31 @@ function orderKeysByDepth(device: Record<string, unknown>): string[][] {
|
||||
}
|
||||
|
||||
const component: ClosureComponent = (): Component => {
|
||||
let queryString: string;
|
||||
const formQueryString = debounce((args: string[]) => {
|
||||
queryString = args[args.length - 1];
|
||||
m.redraw();
|
||||
}, 500);
|
||||
|
||||
return {
|
||||
view: (vnode) => {
|
||||
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", {
|
||||
type: "text",
|
||||
placeholder: "Search parameters",
|
||||
oninput: (e) => {
|
||||
vnode.state["searchString"] = e.target.value;
|
||||
formQueryString(e.target.value);
|
||||
e.redraw = false;
|
||||
clearTimeout(vnode.state["timeout"]);
|
||||
vnode.state["timeout"] = setTimeout(m.redraw, 500);
|
||||
},
|
||||
});
|
||||
|
||||
const instanceRegex = /\.[0-9]+$/;
|
||||
let re;
|
||||
if (vnode.state["searchString"]) {
|
||||
const keywords = vnode.state["searchString"]
|
||||
.split(" ")
|
||||
.filter((s) => s);
|
||||
if (queryString) {
|
||||
const keywords = queryString.split(" ").filter((s) => s);
|
||||
if (keywords.length)
|
||||
re = new RegExp(keywords.map((s) => escapeRegExp(s)).join(".*"), "i");
|
||||
}
|
||||
|
||||
+59
-12
@@ -17,29 +17,76 @@
|
||||
* 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 * 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 => {
|
||||
return {
|
||||
view: (vnode) => {
|
||||
if (vnode.attrs["filter"]) {
|
||||
if (
|
||||
!store.evaluateExpression(
|
||||
vnode.attrs["filter"],
|
||||
vnode.attrs["device"]
|
||||
)
|
||||
)
|
||||
const device = vnode.attrs["device"];
|
||||
if ("filter" in vnode.attrs) {
|
||||
if (!evaluateExpression(vnode.attrs["filter"], device || {}))
|
||||
return null;
|
||||
}
|
||||
|
||||
const children = Object.values(vnode.attrs["components"]).map((c) => {
|
||||
if (Array.isArray(c)) c = evaluateExpression(c, device || {});
|
||||
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);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -19,18 +19,22 @@
|
||||
|
||||
import { ClosureComponent, Component } from "mithril";
|
||||
import { m } from "../components";
|
||||
import { evaluateExpression } from "../store";
|
||||
|
||||
const component: ClosureComponent = (): Component => {
|
||||
return {
|
||||
view: (vnode) => {
|
||||
let deviceId;
|
||||
if (vnode.attrs["device"])
|
||||
deviceId = vnode.attrs["device"]["DeviceID.ID"].value[0];
|
||||
const device = vnode.attrs["device"];
|
||||
if (device) deviceId = device["DeviceID.ID"].value[0];
|
||||
|
||||
const children = Object.values(vnode.attrs["components"]).map((c) => {
|
||||
if (Array.isArray(c)) c = evaluateExpression(c, device || {});
|
||||
if (typeof c !== "object") return `${c}`;
|
||||
const type = evaluateExpression(c["type"], device || {});
|
||||
if (!type) return null;
|
||||
const attrs = Object.assign({}, vnode.attrs, c);
|
||||
return m(attrs["type"], attrs);
|
||||
return m(type as string, attrs);
|
||||
});
|
||||
if (deviceId) {
|
||||
return m(
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import { ClosureComponent, Component } from "mithril";
|
||||
import { m } from "../components";
|
||||
import config from "../config";
|
||||
import * as store from "../store";
|
||||
import { evaluateExpression } from "../store";
|
||||
|
||||
const CHARTS = config.ui.overview.charts;
|
||||
|
||||
@@ -28,11 +28,12 @@ const component: ClosureComponent = (): Component => {
|
||||
return {
|
||||
view: (vnode) => {
|
||||
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;
|
||||
for (const slice of Object.values(chart.slices)) {
|
||||
const filter = slice["filter"];
|
||||
if (store.evaluateExpression(filter, device)) {
|
||||
if (evaluateExpression(filter, device || {})) {
|
||||
const dot = m(
|
||||
"svg",
|
||||
{
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import { ClosureComponent, Component, VnodeDOM } from "mithril";
|
||||
import { m } from "../components";
|
||||
import { evaluateExpression } from "../store";
|
||||
|
||||
const component: ClosureComponent = (): Component => {
|
||||
return {
|
||||
@@ -26,12 +27,13 @@ const component: ClosureComponent = (): Component => {
|
||||
const device = vnode.attrs["device"];
|
||||
|
||||
const rows = Object.values(vnode.attrs["parameters"]).map((parameter) => {
|
||||
const type = evaluateExpression(parameter["type"], device);
|
||||
const p = m.context(
|
||||
{
|
||||
device: device,
|
||||
parameter: parameter["parameter"],
|
||||
},
|
||||
parameter["type"] || "parameter",
|
||||
(type as string) || "parameter",
|
||||
parameter
|
||||
);
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import { ClosureComponent, Component } from "mithril";
|
||||
import { m } from "../components";
|
||||
import * as taskQueue from "../task-queue";
|
||||
import * as store from "../store";
|
||||
import { evaluateExpression } from "../store";
|
||||
import * as expressionParser from "../../lib/common/expression-parser";
|
||||
import { getIcon } from "../icons";
|
||||
|
||||
@@ -35,7 +35,7 @@ const component: ClosureComponent = (): Component => {
|
||||
},
|
||||
view: (vnode) => {
|
||||
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"];
|
||||
|
||||
if (typeof object !== "string" || !device[object]) return null;
|
||||
@@ -56,8 +56,7 @@ const component: ClosureComponent = (): Component => {
|
||||
|
||||
const rows = [];
|
||||
for (const i of instances) {
|
||||
let filter =
|
||||
vnode.attrs["filter"] != null ? vnode.attrs["filter"] : true;
|
||||
let filter = "filter" in vnode.attrs ? vnode.attrs["filter"] : true;
|
||||
|
||||
filter = expressionParser.map(filter, (e) => {
|
||||
if (Array.isArray(e) && e[0] === "PARAM")
|
||||
@@ -65,7 +64,7 @@ const component: ClosureComponent = (): Component => {
|
||||
return e;
|
||||
});
|
||||
|
||||
if (!store.evaluateExpression(filter, device)) continue;
|
||||
if (!evaluateExpression(filter, device)) continue;
|
||||
|
||||
const row = parameters.map((p) => {
|
||||
const param = expressionParser.map(p.parameter, (e) => {
|
||||
@@ -147,7 +146,9 @@ const component: ClosureComponent = (): Component => {
|
||||
}
|
||||
|
||||
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 [
|
||||
label,
|
||||
|
||||
@@ -77,7 +77,7 @@ const component: ClosureComponent = (): Component => {
|
||||
|
||||
const { value, timestamp, parameter } = evaluateParam(
|
||||
vnode.attrs["parameter"],
|
||||
vnode.attrs["device"],
|
||||
device,
|
||||
store.getTimestamp() + store.getClockSkew()
|
||||
);
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@ const component: ClosureComponent = (): Component => {
|
||||
view: (vnode) => {
|
||||
const device = vnode.attrs["device"];
|
||||
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 = [];
|
||||
for (const p of Object.keys(device))
|
||||
|
||||
+17
-4
@@ -87,7 +87,7 @@ i {
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -169,6 +169,7 @@ body {
|
||||
|
||||
& input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0.3em;
|
||||
margin-top: 0.3em;
|
||||
margin-bottom: 0.3em;
|
||||
@@ -291,6 +292,7 @@ body {
|
||||
& > img {
|
||||
margin: 10px;
|
||||
height: 56px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
& > .version {
|
||||
@@ -581,6 +583,7 @@ span.na {
|
||||
.all-parameters {
|
||||
& > input {
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
margin-bottom: 2px;
|
||||
padding: 4px;
|
||||
width: 700px;
|
||||
@@ -588,6 +591,10 @@ span.na {
|
||||
|
||||
& > .parameter-list {
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
& button {
|
||||
font-size: 16px;
|
||||
}
|
||||
overflow-y: scroll;
|
||||
height: 200px;
|
||||
|
||||
@@ -641,7 +648,7 @@ span.na {
|
||||
|
||||
span.tag {
|
||||
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-repeat: no-repeat;
|
||||
border-radius: 0.2em;
|
||||
@@ -725,6 +732,7 @@ span.long-text-overflowed {
|
||||
|
||||
textarea.long-text {
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
min-width: 10vw;
|
||||
min-height: 10vh;
|
||||
}
|
||||
@@ -746,8 +754,7 @@ span.parameter-value {
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
display: block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
fill: none;
|
||||
@@ -797,3 +804,9 @@ span.parameter-value {
|
||||
background-color: #b12d5c;
|
||||
}
|
||||
}
|
||||
|
||||
/* To keep consistent style after changing from Quirks to Standards mode */
|
||||
td,
|
||||
th {
|
||||
line-height: normal;
|
||||
}
|
||||
+6
-9
@@ -268,16 +268,13 @@ export const component: ClosureComponent = (): Component => {
|
||||
}
|
||||
|
||||
function onSortChange(sortedAttrs): void {
|
||||
const _sort = Object.assign({}, sort);
|
||||
for (const [index, direction] of Object.entries(sortedAttrs)) {
|
||||
const param = memoizedGetSortable(attributes[index].parameter);
|
||||
if (param) {
|
||||
// Changing the priority of columns
|
||||
delete _sort[param];
|
||||
_sort[param] = direction;
|
||||
}
|
||||
const _sort = {};
|
||||
for (const index of sortedAttrs) {
|
||||
const param = memoizedGetSortable(
|
||||
attributes[Math.abs(index) - 1].parameter
|
||||
);
|
||||
_sort[param] = Math.sign(index);
|
||||
}
|
||||
|
||||
const ops = { sort: JSON.stringify(_sort) };
|
||||
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
||||
m.route.set("/devices", ops);
|
||||
|
||||
@@ -388,7 +388,7 @@ const component: ClosureComponent = (): Component => {
|
||||
function repositionNotifications(): void {
|
||||
let top = 10;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -408,7 +408,7 @@ const component: ClosureComponent = (): Component => {
|
||||
for (const c of drawerElement.children)
|
||||
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) {
|
||||
|
||||
+3
-7
@@ -107,13 +107,9 @@ export const component: ClosureComponent = (): Component => {
|
||||
}
|
||||
|
||||
function onSortChange(sortAttrs): void {
|
||||
const _sort = Object.assign({}, sort);
|
||||
for (const [index, direction] of Object.entries(sortAttrs)) {
|
||||
// Changing the priority of columns
|
||||
delete _sort[attributes[index].id];
|
||||
_sort[attributes[index].id] = direction;
|
||||
}
|
||||
|
||||
const _sort = {};
|
||||
for (const index of sortAttrs)
|
||||
_sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
|
||||
const ops = { sort: JSON.stringify(_sort) };
|
||||
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
||||
m.route.set("/faults", ops);
|
||||
|
||||
+3
-7
@@ -151,13 +151,9 @@ export const component: ClosureComponent = (): Component => {
|
||||
sortAttributes[i] = sort[attributes[i].id] || 0;
|
||||
|
||||
function onSortChange(sortAttrs): void {
|
||||
const _sort = Object.assign({}, sort);
|
||||
for (const [index, direction] of Object.entries(sortAttrs)) {
|
||||
// Changing the priority of columns
|
||||
delete _sort[attributes[index].id];
|
||||
_sort[attributes[index].id] = direction;
|
||||
}
|
||||
|
||||
const _sort = {};
|
||||
for (const index of sortAttrs)
|
||||
_sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
|
||||
const ops = { sort: JSON.stringify(_sort) };
|
||||
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
||||
m.route.set("/admin/files", ops);
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import { ClosureComponent, Component, Children } from "mithril";
|
||||
import { m } from "./components";
|
||||
import { getIcon } from "./icons";
|
||||
import debounce from "../lib/common/debounce";
|
||||
|
||||
interface Attribute {
|
||||
id?: string;
|
||||
@@ -36,7 +37,7 @@ function renderTable(
|
||||
showMoreCallback: () => void,
|
||||
selected: Set<string>,
|
||||
sortAttributes: Record<string, any>,
|
||||
onSortChange: (obj: Record<string, number>) => void,
|
||||
onSort: (i: number) => void,
|
||||
downloadUrl?: string,
|
||||
valueCallback?: (attr: Attribute, record: Record<string, any>) => Children,
|
||||
actionsCallback?: Children | ((sel: Set<string>) => Children),
|
||||
@@ -79,8 +80,6 @@ function renderTable(
|
||||
continue;
|
||||
}
|
||||
|
||||
let direction = 1;
|
||||
|
||||
let symbol = getIcon("unsorted");
|
||||
if (sortAttributes[i] > 0) symbol = getIcon("sorted-asc");
|
||||
else if (sortAttributes[i] < 0) symbol = getIcon("sorted-dsc");
|
||||
@@ -88,9 +87,9 @@ function renderTable(
|
||||
const sortable = m(
|
||||
"button",
|
||||
{
|
||||
onclick: () => {
|
||||
if (sortAttributes[i] > 0) direction *= -1;
|
||||
return onSortChange({ [i]: direction });
|
||||
onclick: (e) => {
|
||||
e.redraw = false;
|
||||
onSort(i);
|
||||
},
|
||||
},
|
||||
symbol
|
||||
@@ -215,7 +214,10 @@ function renderTable(
|
||||
|
||||
const component: ClosureComponent = (): Component => {
|
||||
let selected = new Set<string>();
|
||||
|
||||
let sortingfunction: (events: number[]) => void;
|
||||
const onSort = debounce((events: number[]) => {
|
||||
sortingfunction(events);
|
||||
}, 500);
|
||||
return {
|
||||
view: (vnode) => {
|
||||
const attributes = vnode.attrs["attributes"];
|
||||
@@ -234,6 +236,20 @@ const component: ClosureComponent = (): Component => {
|
||||
const id = record["_id"] || record["DeviceID.ID"].value[0];
|
||||
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;
|
||||
|
||||
return renderTable(
|
||||
@@ -243,7 +259,7 @@ const component: ClosureComponent = (): Component => {
|
||||
showMoreCallback,
|
||||
selected,
|
||||
sortAttributes,
|
||||
onSortChange,
|
||||
onSort,
|
||||
downloadUrl,
|
||||
valueCallback,
|
||||
actionsCallback,
|
||||
|
||||
@@ -205,13 +205,9 @@ export const component: ClosureComponent = (): Component => {
|
||||
}
|
||||
|
||||
function onSortChange(sortAttrs): void {
|
||||
const _sort = Object.assign({}, sort);
|
||||
for (const [index, direction] of Object.entries(sortAttrs)) {
|
||||
// Changing the priority of columns
|
||||
delete _sort[attributes[index].id];
|
||||
_sort[attributes[index].id] = direction;
|
||||
}
|
||||
|
||||
const _sort = {};
|
||||
for (const index of sortAttrs)
|
||||
_sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
|
||||
const ops = { sort: JSON.stringify(_sort) };
|
||||
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
||||
m.route.set("/admin/permissions", ops);
|
||||
|
||||
+3
-7
@@ -190,13 +190,9 @@ export const component: ClosureComponent = (): Component => {
|
||||
}
|
||||
|
||||
function onSortChange(sortAttrs): void {
|
||||
const _sort = Object.assign({}, sort);
|
||||
for (const [index, direction] of Object.entries(sortAttrs)) {
|
||||
// Changing the priority of columns
|
||||
delete _sort[attributes[index].id];
|
||||
_sort[attributes[index].id] = direction;
|
||||
}
|
||||
|
||||
const _sort = {};
|
||||
for (const index of sortAttrs)
|
||||
_sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
|
||||
const ops = { sort: JSON.stringify(_sort) };
|
||||
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
||||
m.route.set("/admin/presets", ops);
|
||||
|
||||
@@ -173,13 +173,9 @@ export const component: ClosureComponent = (): Component => {
|
||||
sortAttributes[i] = sort[attributes[i].id] || 0;
|
||||
|
||||
function onSortChange(sortAttrs): void {
|
||||
const _sort = Object.assign({}, sort);
|
||||
for (const [index, direction] of Object.entries(sortAttrs)) {
|
||||
// Changing the priority of columns
|
||||
delete _sort[attributes[index].id];
|
||||
_sort[attributes[index].id] = direction;
|
||||
}
|
||||
|
||||
const _sort = {};
|
||||
for (const index of sortAttrs)
|
||||
_sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
|
||||
const ops = { sort: JSON.stringify(_sort) };
|
||||
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
||||
m.route.set("/admin/provisions", ops);
|
||||
|
||||
+3
-7
@@ -186,13 +186,9 @@ export const component: ClosureComponent = (): Component => {
|
||||
}
|
||||
|
||||
function onSortChange(sortAttrs): void {
|
||||
const _sort = Object.assign({}, sort);
|
||||
for (const [index, direction] of Object.entries(sortAttrs)) {
|
||||
// Changing the priority of columns
|
||||
delete _sort[attributes[index].id];
|
||||
_sort[attributes[index].id] = direction;
|
||||
}
|
||||
|
||||
const _sort = {};
|
||||
for (const index of sortAttrs)
|
||||
_sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
|
||||
const ops = { sort: JSON.stringify(_sort) };
|
||||
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
||||
m.route.set("/admin/users", ops);
|
||||
|
||||
@@ -173,13 +173,9 @@ export const component: ClosureComponent = (): Component => {
|
||||
sortAttributes[i] = sort[attributes[i].id] || 0;
|
||||
|
||||
function onSortChange(sortAttrs): void {
|
||||
const _sort = Object.assign({}, sort);
|
||||
for (const [index, direction] of Object.entries(sortAttrs)) {
|
||||
// Changing the priority of columns
|
||||
delete _sort[attributes[index].id];
|
||||
_sort[attributes[index].id] = direction;
|
||||
}
|
||||
|
||||
const _sort = {};
|
||||
for (const index of sortAttrs)
|
||||
_sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
|
||||
const ops = { sort: JSON.stringify(_sort) };
|
||||
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
||||
m.route.set("/admin/virtualParameters", ops);
|
||||
|
||||
Reference in new issue
Block a user