Compare commits

..
27 Commits
Author SHA1 Message Date
Zaid Abdulla 60d8dcb682 1.2.3 2020-10-26 23:40:41 -07:00
Zaid Abdulla e0d648ea3e Changelog for v1.2.3 2020-10-26 23:33:36 -07:00
Zaid Abdulla 6ba67c3fb6 Update dependencies 2020-10-26 23:18:37 -07:00
Zaid Abdulla 04a7c08a20 Fix typos 2020-10-26 15:29:03 -07:00
Zaid Abdulla 92091b52a2 More deprecation fixes 2020-10-26 15:28:22 -07:00
Zaid Abdulla a43bba2405 Fix deprecations 2020-10-25 23:34:05 -07:00
Zaid Abdulla c8a6eb4c72 Show confirm close alert for unsaved changes 2020-10-25 13:37:35 -07:00
Zaid Abdulla 36798cfb5f Fix confusing 404 for unauthorized requests 2020-10-24 10:24:19 -07:00
Zaid Abdulla c52ff6f6ec Support Unicode characters in tags 2020-10-20 15:44:11 -07:00
Zaid Abdulla dfab724515 Add new config cwmp.skipWritableCheck
Some CPEs incorrectly report writable parameters as non-writable. This
config option lets us bypass the check and attempt to modify the
parameter even if it reported as read-only.
2020-10-20 13:35:34 -07:00
Zaid Abdulla 4fd919ccfa Add more XML validations 2020-10-19 13:34:08 -07:00
Zaid Abdulla 62849e8081 Fix connection timeout being set incorrectly 2020-10-18 12:52:01 -07:00
Zaid Abdulla 9f4b421b59 Add Node types to dev dependencies 2020-10-17 20:09:32 -07:00
Zaid Abdulla 44a2fe11ee Fix invalid expression after minimization
In rare edge cases where IS NULL is used the minimization process can
generate an expression that's not equivilant to the original one.
2020-10-16 17:33:29 -07:00
Zaid Abdulla 91d7152ca7 1.2.2 2020-10-03 00:37:35 -07:00
Zaid Abdulla 3fccc1d8a4 Changelog for v1.2.2 2020-10-03 00:36:59 -07:00
Zaid Abdulla 5bb6f60c4d Update dependencies and fix new lint errors 2020-10-02 23:21:07 -07:00
Zaid Abdulla c2f002b95a Better ordering of config YAML 2020-10-02 14:50:17 -07:00
Zaid Abdulla c55a256d52 Show proper login failure message 2020-09-30 15:43:55 -07:00
Zaid Abdulla 3fc0b1740d Limit max page size 2020-09-28 12:47:56 -07:00
Zaid Abdulla 1956441d52 Limit number of tasks in queue 2020-09-28 12:47:15 -07:00
Zaid Abdulla 01e01dae69 Set max height for task queue 2020-09-28 12:40:55 -07:00
Zaid Abdulla f14e6ceb97 Bulk push file from device list page 2020-09-27 11:20:30 -07:00
Zaid Abdulla 82124d471e Fix invalid arguments fault in refresh preset
Happens when upgrading from v1.1 where genieacs-gui creates refresh
configuration with 'age' passed as a string.
2020-09-23 23:15:00 -07:00
Zaid Abdulla 9286bd6aa0 Fix unhandled promise rejections 2020-09-17 17:00:41 -07:00
Zaid Abdulla 328bfa03e0 Allow refreshObject task on root param 2020-09-15 12:50:02 -07:00
Zaid Abdulla de65ca0921 Fix crash when deserializing operation objects 2020-09-14 15:58:47 -07:00
51 changed files with 3310 additions and 4189 deletions

No files matched your search

+27
View File
@@ -1,6 +1,33 @@
# Change Log
## 1.2.3 (2020-10-26)
- New config option 'cwmp.skipWritableCheck' for when some CPEs incorrectly
report writable parameters as non-writable. When set to true, the scripts will
no longer respect the 'writable' attribute of the CPE parameters and will send
a SetParamteerValues, AddObject, or DeleteObject request anyway.
- Tags no longer restrict what characters are allowed. Any character other than
alphanumeric characters, hyphen, or underscore is now encoded in the data
model (i.e. Tags.\<tag>) using its hex value preceded by "0x".
- Ask for a confirmation before closing a pop-up dialog with unsaved changes.
- Better XML validation to avoid crashes caused by invalid CPE requests.
- Fix confusing 404 error message when the user attempts to modify a resource
when they don't have the necessary permissions.
- Fix a rare issue where genieacs-cwmp stops accepting new connections after
running for a few weeks.
- Fix exception when IS NULL operator is used in certain situations.
## 1.2.2 (2020-10-03)
- Added button to push files to selected devices from device listing page.
- A few minor UI improvements.
- Fix exception that can happen and persist after a Download request.
- Fix validation bug preventing running refreshObject task on data model root.
- Fix invalid arguments fault in refresh preset configuration when upgrading
from v1.1.
## 1.2.1 (2020-09-08)
- Fix bug causing faults to not be displayed in the UI.
- Fix bug where deleting objects does not get reflected immediately in the UI.
- Improve conversion between filters written in the expression format and
+11 -11
View File
@@ -77,9 +77,16 @@ if (!cluster.worker) {
cluster.stop();
});
} else {
const ssl = {
key: config.get("CWMP_SSL_KEY") as string,
cert: config.get("CWMP_SSL_CERT") as string,
const key = config.get("CWMP_SSL_KEY") as string;
const cert = config.get("CWMP_SSL_CERT") as string;
const options = {
port: SERVICE_PORT,
host: SERVICE_ADDRESS,
ssl: key && cert ? { key, cert } : null,
onConnection: cwmp.onConnection,
timeout: 30000,
keepAliveTimeout: 0,
};
process.on("uncaughtException", (err) => {
@@ -94,14 +101,7 @@ if (!cluster.worker) {
const initPromise = Promise.all([db.connect(), cache.connect()])
.then(() => {
server.start(
SERVICE_PORT,
SERVICE_ADDRESS,
ssl,
cwmp.listener,
cwmp.onConnection,
0
);
server.start(options, cwmp.listener);
})
.catch((err) => {
setTimeout(() => {
+8 -4
View File
@@ -73,9 +73,13 @@ if (!cluster.worker) {
cluster.stop();
});
} else {
const ssl = {
key: config.get("FS_SSL_KEY") as string,
cert: config.get("FS_SSL_CERT") as string,
const key = config.get("FS_SSL_KEY") as string;
const cert = config.get("FS_SSL_CERT") as string;
const options = {
port: SERVICE_PORT,
host: SERVICE_ADDRESS,
ssl: key && cert ? { key, cert } : null,
timeout: 30000,
};
let stopping = false;
@@ -98,7 +102,7 @@ if (!cluster.worker) {
const initPromise = Promise.all([db.connect(), cache.connect()])
.then(() => {
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, _listener);
server.start(options, _listener);
})
.catch((err) => {
setTimeout(() => {
+8 -4
View File
@@ -77,9 +77,13 @@ if (!cluster.worker) {
cluster.stop();
});
} else {
const ssl = {
key: config.get("NBI_SSL_KEY") as string,
cert: config.get("NBI_SSL_CERT") as string,
const key = config.get("NBI_SSL_KEY") as string;
const cert = config.get("NBI_SSL_CERT") as string;
const options = {
port: SERVICE_PORT,
host: SERVICE_ADDRESS,
ssl: key && cert ? { key, cert } : null,
timeout: 30000,
};
let stopping = false;
@@ -102,7 +106,7 @@ if (!cluster.worker) {
const initPromise = Promise.all([db.connect(), cache.connect()])
.then(() => {
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, _listener);
server.start(options, _listener);
})
.catch((err) => {
setTimeout(() => {
+9 -7
View File
@@ -79,9 +79,13 @@ if (!cluster.worker) {
cluster.stop();
});
} else {
const ssl = {
key: config.get("UI_SSL_KEY") as string,
cert: config.get("UI_SSL_CERT") as string,
const key = config.get("UI_SSL_KEY") as string;
const cert = config.get("UI_SSL_CERT") as string;
const options = {
port: SERVICE_PORT,
host: SERVICE_ADDRESS,
ssl: key && cert ? { key, cert } : null,
timeout: 30000,
};
let stopping = false;
@@ -99,14 +103,12 @@ if (!cluster.worker) {
const _listener = (req, res): void => {
if (stopping) res.setHeader("Connection", "close");
listener(req, res).catch((err) => {
throw err;
});
listener(req, res);
};
const initPromise = Promise.all([db2.connect(), cache.connect()])
.then(() => {
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, _listener);
server.start(options, _listener);
})
.catch((err) => {
setTimeout(() => {
+10 -6
View File
@@ -19,14 +19,13 @@
import * as path from "path";
import * as fs from "fs";
import { promisify } from "util";
import { rollup, WarningHandler } from "rollup";
import rollupJson from "@rollup/plugin-json";
import typescript from "@rollup/plugin-typescript";
import { terser } from "rollup-plugin-terser";
import nodeResolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import webpack from "webpack";
import webpack, { Configuration } from "webpack";
import postcss from "postcss";
import postcssImport from "postcss-import";
import postcssPresetEnv from "postcss-preset-env";
@@ -340,8 +339,8 @@ async function generateFrontendJs(): Promise<void> {
file: outputFile,
});
const webpackConf = {
mode: MODE,
const webpackConf: Configuration = {
mode: MODE as "development" | "production",
entry: outputFile,
resolve: {
aliasFields: ["module"],
@@ -362,8 +361,13 @@ async function generateFrontendJs(): Promise<void> {
},
};
const stats = await promisify(webpack)(webpackConf);
process.stdout.write(stats.toString({ colors: true }) + "\n");
return new Promise((resolve, reject) => {
webpack(webpackConf, (err, stats) => {
if (err) return reject(err);
process.stdout.write(stats.toString({ colors: true }) + "\n");
resolve();
});
});
}
async function generateIconsSprite(): Promise<void> {
+4
View File
@@ -236,6 +236,10 @@ function sanitizeTask(task): void {
break;
case "refreshObject":
if (typeof task.objectName !== "string")
throw new Error("Missing 'objectName' property");
break;
case "deleteObject":
if (typeof task.objectName !== "string" || !task.objectName.length)
throw new Error("Missing 'objectName' property");
+14
View File
@@ -47,3 +47,17 @@ export function generateDeviceId(
export function escapeRegExp(str: string): string {
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
}
export function encodeTag(tag: string): string {
return encodeURIComponent(tag)
.replace(
/[!~*'()]/g,
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()
)
.replace(/0x(?=[0-9A-Z]{2})/g, "0%78")
.replace(/%/g, "0x");
}
export function decodeTag(tag: string): string {
return decodeURIComponent(tag.replace(/0x(?=[0-9A-Z]{2})/g, "%"));
}
+1 -2
View File
@@ -1019,8 +1019,7 @@ function boolExprSynthToExpression(boolExpr: BoolExprSynth): Expression {
const { dcSet, isNull } = generateDcSetAndIsNull(variables);
minterms = sanitizeMinterms(minterms, isNull);
const canRaise = (idx: number, set: Set<number>): boolean =>
!isNull.has(idx >> 2) || !(idx & 1) || !set.has(idx ^ 3);
const canRaise = getCanRaiseCallback(isNull);
minterms = espresso(minterms, dcSet, { canRaise });
return sopToExpression(minterms, variables);
}
+2 -2
View File
@@ -42,7 +42,7 @@ function interpretEscapes(str): string {
export function map(
exp: Expression,
callback: (exp: Expression) => Expression
callback: (e: Expression) => Expression
): Expression {
if (!Array.isArray(exp)) return callback(exp);
@@ -60,7 +60,7 @@ export function map(
export async function mapAsync(
exp: Expression,
callback: (exp: Expression) => Promise<Expression>
callback: (e: Expression) => Promise<Expression>
): Promise<Expression> {
if (!Array.isArray(exp)) return callback(exp);
+9 -9
View File
@@ -264,21 +264,21 @@ export function evaluateCallback(exp: Expression): Expression {
export function evaluate(
exp: Expression,
obj: Record<string, unknown> | ((exp: string) => Expression),
obj: Record<string, unknown> | ((e: string) => Expression),
now: number,
cb?: (exp: Expression) => Expression
cb?: (e: Expression) => Expression
): string | number | boolean | null;
export function evaluate(
exp: Expression,
obj?: Record<string, unknown> | ((exp: string) => Expression),
obj?: Record<string, unknown> | ((e: string) => Expression),
now?: number,
cb?: (exp: Expression) => Expression
cb?: (e: Expression) => Expression
): Expression;
export function evaluate(
exp: Expression,
obj?: Record<string, unknown> | ((exp: string) => Expression),
obj?: Record<string, unknown> | ((e: string) => Expression),
now?: number,
cb?: (exp: Expression) => Expression
cb?: (e: Expression) => Expression
): Expression {
return map(exp, (e) => {
if (cb) e = cb(e);
@@ -306,19 +306,19 @@ export async function evaluateAsync(
exp: Expression,
obj: Record<string, unknown>,
now: number,
cb?: (exp: Expression) => Promise<Expression>
cb?: (e: Expression) => Promise<Expression>
): Promise<string | number | boolean | null>;
export async function evaluateAsync(
exp: Expression,
obj?: Record<string, unknown>,
now?: number,
cb?: (exp: Expression) => Promise<Expression>
cb?: (e: Expression) => Promise<Expression>
): Promise<Expression>;
export async function evaluateAsync(
exp: Expression,
obj?: Record<string, unknown>,
now?: number,
cb?: (exp: Expression) => Promise<Expression>
cb?: (e: Expression) => Promise<Expression>
): Promise<Expression> {
return mapAsync(exp, async (e) => {
if (cb) e = await cb(e);
+28 -20
View File
@@ -90,9 +90,7 @@ async function authenticate(
}
if (authentication && authentication.method === "Digest") {
const sessionNonce = sessionsNonces.get(
sessionContext.httpRequest.connection
);
const sessionNonce = sessionsNonces.get(sessionContext.httpRequest.socket);
if (
!sessionNonce ||
@@ -185,7 +183,7 @@ async function writeResponse(
}
const httpResponse = sessionContext.httpResponse;
const connection = httpResponse.connection;
const connection = httpResponse.socket;
httpResponse.setHeader("Content-Length", Buffer.byteLength(data));
httpResponse.writeHead(res.code, res.headers);
@@ -842,8 +840,11 @@ async function sendAcsRequest(
return writeResponse(sessionContext, res);
}
async function getSession(connection, sessionId): Promise<SessionContext> {
const sessionContext = currentSessions.get(connection);
async function getSession(
connection: Socket,
sessionId: string
): Promise<SessionContext> {
let sessionContext = currentSessions.get(connection);
if (sessionContext) {
currentSessions.delete(connection);
return sessionContext;
@@ -855,7 +856,9 @@ async function getSession(connection, sessionId): Promise<SessionContext> {
const sessionContextString = await cache.pop(`session_${sessionId}`);
if (!sessionContextString) return null;
return session.deserialize(sessionContextString);
sessionContext = await session.deserialize(sessionContextString);
connection.setTimeout(sessionContext.timeout);
return sessionContext;
}
// Only needed to prevent tree shaking from removing the remoteAddress
@@ -988,7 +991,7 @@ async function reportBadState(sessionContext: SessionContext): Promise<void> {
sessionContext: sessionContext,
});
const httpResponse = sessionContext.httpResponse;
currentSessions.delete(httpResponse.connection);
currentSessions.delete(httpResponse.socket);
const body = "Bad session state";
httpResponse.setHeader("Content-Length", Buffer.byteLength(body));
httpResponse.writeHead(400, { Connection: "close" });
@@ -1015,14 +1018,14 @@ async function responseUnauthorized(
resHeaders["WWW-Authenticate"] = `Basic realm="${REALM}"`;
} else {
const nonce = crypto.randomBytes(16).toString("hex");
sessionsNonces.set(sessionContext.httpRequest.connection, nonce);
sessionsNonces.set(sessionContext.httpRequest.socket, nonce);
let d = `Digest realm="${REALM}"`;
d += ',qop="auth,auth-int"';
d += `,nonce="${nonce}"`;
resHeaders["WWW-Authenticate"] = d;
}
currentSessions.set(sessionContext.httpRequest.connection, sessionContext);
currentSessions.set(sessionContext.httpRequest.socket, sessionContext);
}
const httpResponse = sessionContext.httpResponse;
@@ -1042,7 +1045,6 @@ async function processRequest(
): Promise<void> {
for (const w of parseWarnings) {
w.sessionContext = sessionContext;
w.rpc = rpc;
logger.accessWarn(w);
}
@@ -1070,6 +1072,8 @@ async function processRequest(
);
}
sessionContext.httpRequest.socket.setTimeout(sessionContext.timeout * 1000);
if (sessionContext.debug) {
debug.incomingHttpRequest(
sessionContext.httpRequest,
@@ -1210,19 +1214,24 @@ export function listener(
stats.concurrentRequests -= 1;
})
.catch((err) => {
currentSessions.delete(httpResponse.connection);
httpResponse.writeHead(500, { Connection: "close" });
httpResponse.end(`${err.name}: ${err.message}`);
currentSessions.delete(httpResponse.socket);
stats.concurrentRequests -= 1;
setTimeout(() => {
throw err;
});
try {
httpResponse.socket.unref();
httpResponse.writeHead(500, { Connection: "close" });
httpResponse.end(`${err.name}: ${err.message}`);
} catch (err) {
// Ignore
}
});
}
function decodeString(buffer: Buffer, charset: string): string {
try {
return buffer.toString(charset);
return buffer.toString(charset as BufferEncoding);
} catch (err) {
if (encodingExists(charset)) return decode(buffer, charset);
}
@@ -1307,9 +1316,9 @@ async function listenerAsync(
// Request aborted
if (!body) return;
const newConnection = !currentSessions.has(httpRequest.connection);
const newConnection = !currentSessions.has(httpRequest.socket);
const sessionContext = await getSession(httpRequest.connection, sessionId);
const sessionContext = await getSession(httpRequest.socket, sessionId);
if (sessionContext) {
sessionContext.httpRequest = httpRequest;
@@ -1347,7 +1356,7 @@ async function listenerAsync(
return;
}
let charset;
let charset: string;
if (httpRequest.headers["content-type"]) {
const m = httpRequest.headers["content-type"].match(
/charset=['"]?([^'"\s]+)/i
@@ -1409,7 +1418,7 @@ async function listenerAsync(
}
const parseWarnings = [];
let rpc;
let rpc: SoapMessage;
try {
rpc = soap.request(
bodyStr,
@@ -1514,7 +1523,6 @@ async function listenerAsync(
_sessionContext.httpRequest = httpRequest;
_sessionContext.httpResponse = httpResponse;
_sessionContext.sessionId = crypto.randomBytes(8).toString("hex");
httpRequest.connection.setTimeout(_sessionContext.timeout * 1000);
const {
tasks: dueTasks,
+9 -5
View File
@@ -19,7 +19,7 @@
import { MongoClient, ObjectID, Collection } from "mongodb";
import { get } from "./config";
import { escapeRegExp } from "./common";
import { encodeTag, escapeRegExp } from "./common";
import { parse } from "./common/expression-parser";
import {
DeviceData,
@@ -234,10 +234,9 @@ export async function fetchDevice(
]);
}
for (let t of v as string[]) {
t = t.replace(/[^a-zA-Z0-9-]+/g, "_");
for (const t of v as string[]) {
res.push([
Path.parse("Tags." + t),
Path.parse("Tags." + encodeTag(t)),
timestamp,
{
object: [timestamp, 0],
@@ -691,6 +690,11 @@ export async function getOperations(
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;
continue;
}
r.timestamp = +r.timestamp;
if (r.args) r.args = JSON.parse(r.args);
r.provisions = JSON.parse(r.provisions);
@@ -712,7 +716,7 @@ export async function saveOperation(
o["provisions"] = JSON.stringify(operation.provisions);
o["retries"] = JSON.stringify(operation.retries);
o["args"] = JSON.stringify(operation.args);
await operationsCollection.replaceOne({ _id: id }, operation, {
await operationsCollection.replaceOne({ _id: id }, o, {
upsert: true,
});
}
+5 -5
View File
@@ -49,7 +49,7 @@ export function incomingHttpRequest(
): void {
if (!DEBUG_FILE) return;
const now = new Date();
const con = httpRequest.connection;
const con = httpRequest.socket;
const msg = {
event: "incoming HTTP request",
timestamp: now,
@@ -77,7 +77,7 @@ export function outgoingHttpResponse(
): void {
if (!DEBUG_FILE) return;
const now = new Date();
const con = httpResponse.connection;
const con = httpResponse.socket;
const msg = {
event: "outgoing HTTP response",
timestamp: now,
@@ -104,7 +104,7 @@ export function outgoingHttpRequest(
): void {
if (!DEBUG_FILE) return;
const now = new Date();
const con = httpRequest.connection;
const con = httpRequest.socket;
const msg = {
event: "outgoing HTTP request",
timestamp: now,
@@ -160,13 +160,13 @@ export function incomingHttpResponse(
): void {
if (!DEBUG_FILE) return;
const now = new Date();
const con = httpResponse.connection;
const con = httpResponse.socket;
const msg = {
event: "incoming HTTP response",
timestamp: now,
remoteAddress: con.remoteAddress,
deviceId: deviceId,
connection: getConnectionTimestamp(httpResponse.connection),
connection: getConnectionTimestamp(httpResponse.socket),
statusCode: httpResponse.statusCode,
headers: httpResponse.headers,
body: body,
+2 -2
View File
@@ -188,7 +188,7 @@ async function fetchPresets(): Promise<Preset[]> {
for (const c of preset["configurations"]) {
switch (c.type) {
case "age":
_provisions.push(["refresh", c.name, c.age]);
_provisions.push(["refresh", c.name, +c.age]);
break;
case "value":
@@ -475,7 +475,7 @@ export function getConfig(
key: string,
context: Record<string, unknown>,
now: number,
cb?: (Expression) => Expression
cb?: (e: Expression) => Expression
): string | number | boolean | null {
const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist");
+9 -5
View File
@@ -21,6 +21,7 @@ import { ObjectID } from "mongodb";
import { map, parse, stringify, parseList } from "./common/expression-parser";
import { likePatternToRegExp } from "./common/expression";
import { Expression, Fault, Task } from "./types";
import { decodeTag, encodeTag } from "./common";
const isArray = Array.isArray;
@@ -43,7 +44,7 @@ export function processDeviceFilter(filter: Expression): Expression {
exp[1][0] === "PARAM" &&
exp[1][1].startsWith("Tags.")
) {
const t = exp[1][1].slice(5);
const t = decodeTag(exp[1][1].slice(5));
if (exp[0] === "IS NULL") return ["_tags", t, false];
else if (exp[0] === "IS NOT NULL") return ["_tags", t, true];
else if (exp[0] === "=" && exp[2] === true) return ["_tags", t, true];
@@ -406,7 +407,7 @@ export function flattenDevice(device: Record<string, unknown>): FlatDevice {
};
for (const t of tree as string[]) {
output[`Tags.${t}`] = {
output[`Tags.${encodeTag(t)}`] = {
value: [true, "xsd:boolean"],
valueTimestamp: timestamp,
writable: true,
@@ -510,11 +511,11 @@ export function convertOldPrecondition(
if (op === "$ne") {
if (typeof v["$ne"] !== "string")
throw new Error("Only string values are allowed for _tags");
conjs.push(["IS NULL", ["PARAM", `Tags.${val}`]]);
conjs.push(["IS NULL", ["PARAM", `Tags.${encodeTag(val)}`]]);
} else if (op === "$eq") {
if (typeof v["$eq"] !== "string")
throw new Error("Only string values are allowed for _tags");
conjs.push(["IS NOT NULL", ["PARAM", `Tags.${val}`]]);
conjs.push(["IS NOT NULL", ["PARAM", `Tags.${encodeTag(val)}`]]);
} else {
throw new Error(`Invalid tag query`);
}
@@ -522,7 +523,10 @@ export function convertOldPrecondition(
if (conjs.length === 1) expressions.push(conjs[0]);
else if (conjs.length > 1) expressions.push(["AND", ...conjs]);
} else {
expressions.push(["IS NOT NULL", ["PARAM", `Tags.${v}`]]);
expressions.push([
"IS NOT NULL",
["PARAM", `Tags.${encodeTag(v as string)}`],
]);
}
} else if (k.startsWith("Tags.")) {
let exists: boolean;
+23 -15
View File
@@ -52,35 +52,43 @@ function closeServer(timeout, callback): void {
});
}
interface ServerOptions {
port?: number;
host?: string;
ssl?: { key: string; cert: string };
timeout?: number;
keepAliveTimeout?: number;
onConnection?: (socket: Socket) => void;
}
export function start(
port: number,
networkInterface: string,
ssl: { key: string; cert: string },
_listener: http.RequestListener,
onConnection?: (socket: Socket) => void,
keepAliveTimeout = -1
options: ServerOptions,
_listener: http.RequestListener
): void {
listener = _listener;
if (ssl && ssl.key && ssl.cert) {
const options = {
key: ssl.key
if (options.ssl) {
const opts = {
key: options.ssl.key
.split(":")
.map((f) => fs.readFileSync(path.resolve(ROOT_DIR, f.trim()))),
cert: ssl.cert
cert: options.ssl.cert
.split(":")
.map((f) => fs.readFileSync(path.resolve(ROOT_DIR, f.trim()))),
};
server = https.createServer(options, listener);
if (onConnection != null) server.on("secureConnection", onConnection);
server = https.createServer(opts, listener);
if (options.onConnection)
server.on("secureConnection", options.onConnection);
} else {
server = http.createServer(listener);
if (onConnection != null) server.on("connection", onConnection);
if (options.onConnection) server.on("connection", options.onConnection);
}
if (keepAliveTimeout >= 0) server.keepAliveTimeout = keepAliveTimeout;
server.listen(port, networkInterface);
server.timeout = options.timeout || 0;
if (options.keepAliveTimeout != null)
server.keepAliveTimeout = options.keepAliveTimeout;
server.listen({ port: options.port, host: options.host });
}
export function stop(): Promise<void> {
+23 -9
View File
@@ -1734,20 +1734,35 @@ function generateSetRpcRequest(
const deviceData = sessionContext.deviceData;
const SKIP_WRITABLE_CHECK = !!localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.skipWritableCheck",
{},
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e)
);
const canWrite = (attrs: Attributes): boolean =>
SKIP_WRITABLE_CHECK || (attrs.writable && !!attrs.writable[1]);
// Delete instance
for (const instances of syncState.instancesToDelete.values()) {
const instance = instances.values().next().value;
if (instance && sessionContext.deviceData.attributes.has(instance)) {
return {
name: "DeleteObject",
objectName: instance.toString() + ".",
};
for (const instance of instances) {
const attrs = sessionContext.deviceData.attributes.get(instance);
if (attrs && canWrite(attrs)) {
instances.delete(instance);
return {
name: "DeleteObject",
objectName: instance.toString() + ".",
};
}
}
}
// Create instance
for (const [param, instances] of syncState.instancesToCreate) {
if (sessionContext.deviceData.attributes.has(param)) {
const attrs = sessionContext.deviceData.attributes.get(param);
if (attrs && canWrite(attrs)) {
const instance = instances.values().next().value;
if (instance) {
instances.delete(instance);
@@ -1791,7 +1806,7 @@ function generateSetRpcRequest(
syncState.spv.delete(k);
const attrs = sessionContext.deviceData.attributes.get(k);
const curVal = attrs.value ? attrs.value[1] : null;
if (curVal && attrs.writable && attrs.writable[1]) {
if (curVal && canWrite(attrs)) {
const val = v.slice() as [string | number | boolean, string];
if (!val[1]) val[1] = curVal[1];
device.sanitizeParameterValue(val);
@@ -2609,7 +2624,6 @@ export async function rpcResponse(
timestamp + 1,
{
object: [timestamp + 1, 0],
writable: [timestamp + 1, 1],
value: [
timestamp + 1,
p.slice(1) as [string | number | boolean, string],
+150 -14
View File
@@ -127,7 +127,8 @@ function parameterInfoList(xml: Element): [Path, boolean, boolean][] {
if (parsed == null) {
warnings.push({
message: "Invalid writable attribute",
message: "Missing or invalid XML node",
element: "Writable",
parameter: param,
});
parsed = false;
@@ -167,7 +168,8 @@ function parameterValueList(
let valueType = getValueType(valueElement.attrs);
if (!valueType) {
warnings.push({
message: "Invalid value type attribute",
message: "Missing or invalid XML node",
attribute: "type",
parameter: param,
});
valueType = "xsd:string";
@@ -179,7 +181,8 @@ function parameterValueList(
parsed = parseBool(value);
if (parsed == null) {
warnings.push({
message: "Invalid value attribute",
message: "Missing or invalid XML node",
element: "Value",
parameter: param,
});
parsed = value;
@@ -188,7 +191,8 @@ function parameterValueList(
parsed = parseInt(value);
if (isNaN(parsed)) {
warnings.push({
message: "Invalid value attribute",
message: "Missing or invalid XML node",
element: "Value",
parameter: param,
});
parsed = value;
@@ -197,7 +201,8 @@ function parameterValueList(
parsed = Date.parse(value);
if (isNaN(parsed)) {
warnings.push({
message: "Invalid value attribute",
message: "Missing or invalid XML node",
element: "Value",
parameter: param,
});
parsed = value;
@@ -232,7 +237,8 @@ function parameterAttributeList(xml: Element): [Path, number, string[]][] {
let notification = parseInt(notificationElement.text);
if (isNaN(notification)) {
warnings.push({
message: "Invalid notification attribute",
message: "Missing or invalid XML node",
element: "Notification",
parameter: param,
});
notification = 0;
@@ -331,7 +337,10 @@ function SetParameterValuesResponse(xml: Element): SetParameterValuesResponse {
}
if (!(status >= 0)) {
warnings.push({ message: "Invalid SetParameterValuesResponse status" });
warnings.push({
message: "Missing or invalid XML node",
element: "Status",
});
status = 0;
}
@@ -392,8 +401,14 @@ function AddObjectResponse(xml: Element): AddObjectResponse {
}
}
if (!/^[0-9]+$/.test(instanceNumber))
throw new Error("Missing or invalid instance number");
if (!(status >= 0)) {
warnings.push({ message: "Invalid AddObjectResponse status" });
warnings.push({
message: "Missing or invalid XML node",
element: "Status",
});
status = 0;
}
@@ -424,7 +439,10 @@ function DeleteObjectResponse(xml: Element): DeleteObjectResponse {
}
if (!(status >= 0)) {
warnings.push({ message: "Invalid DeleteObjectResponse status" });
warnings.push({
message: "Missing or invalid XML node",
element: "Status",
});
status = 0;
}
@@ -495,10 +513,29 @@ function DownloadResponse(xml: Element): DownloadResponse {
}
if (!(status >= 0)) {
warnings.push({ message: "Invalid DownloadResponse status" });
warnings.push({
message: "Missing or invalid XML node",
element: "Status",
});
status = 0;
}
if (startTime == null || isNaN(startTime)) {
warnings.push({
message: "Missing or invalid XML node",
element: "StartTime",
});
startTime = Date.parse("0001-01-01T00:00:00Z");
}
if (completeTime == null || isNaN(completeTime)) {
warnings.push({
message: "Missing or invalid XML node",
element: "CompleteTime",
});
completeTime = Date.parse("0001-01-01T00:00:00Z");
}
return {
name: "DownloadResponse",
status: status,
@@ -537,6 +574,30 @@ function Inform(xml: Element): InformRequest {
}
}
if (!deviceId || !deviceId.SerialNumber || !deviceId.OUI)
throw new Error("Missing or invalid DeviceId element");
if (!parameterList) {
warnings.push({
message: "Missing or invalid XML node",
element: "ParameterList",
});
parameterList = [];
}
if (!evnt) {
warnings.push({ message: "Missing or invalid XML node", element: "Event" });
evnt = [];
}
if (retryCount == null || isNaN(retryCount)) {
warnings.push({
message: "Missing or invalid XML node",
element: "RetryCount",
});
retryCount = 0;
}
return {
name: "Inform",
parameterList: parameterList,
@@ -563,7 +624,10 @@ function GetRPCMethodsResponse(methodResponse): string {
}
function TransferComplete(xml: Element): TransferCompleteRequest {
let commandKey, _faultStruct, startTime, completeTime;
let commandKey: string,
_faultStruct: FaultStruct,
startTime: number,
completeTime: number;
for (const c of xml.children) {
switch (c.localName) {
case "CommandKey":
@@ -581,6 +645,38 @@ function TransferComplete(xml: Element): TransferCompleteRequest {
}
}
if (commandKey == null) {
warnings.push({
message: "Missing or invalid XML node",
element: "CommandKey",
});
commandKey = "";
}
if (!_faultStruct) {
warnings.push({
message: "Missing or invalid XML node",
element: "FaultStruct",
});
_faultStruct = { faultCode: "0", faultString: "" };
}
if (startTime == null || isNaN(startTime)) {
warnings.push({
message: "Missing or invalid XML node",
element: "StartTime",
});
startTime = Date.parse("0001-01-01T00:00:00Z");
}
if (completeTime == null || isNaN(completeTime)) {
warnings.push({
message: "Missing or invalid XML node",
element: "CompleteTime",
});
completeTime = Date.parse("0001-01-01T00:00:00Z");
}
return {
name: "TransferComplete",
commandKey: commandKey,
@@ -606,7 +702,12 @@ function RequestDownloadResponse(): string {
}
function faultStruct(xml: Element): FaultStruct {
let faultCode, faultString, setParameterValuesFault: SpvFault[], pn, fc, fs;
let faultCode: string,
faultString: string,
setParameterValuesFault: SpvFault[],
pn: string,
fc: string,
fs: string;
for (const c of xml.children) {
switch (c.localName) {
case "FaultCode":
@@ -617,6 +718,7 @@ function faultStruct(xml: Element): FaultStruct {
break;
case "SetParameterValuesFault":
setParameterValuesFault = setParameterValuesFault || [];
pn = fc = fs = null;
for (const cc of c.children) {
switch (cc.localName) {
case "ParameterName":
@@ -638,11 +740,27 @@ function faultStruct(xml: Element): FaultStruct {
}
}
if (faultCode == null) {
warnings.push({
message: "Missing or invalid XML node",
element: "FaultCode",
});
faultCode = "";
}
if (faultString == null) {
warnings.push({
message: "Missing or invalid XML node",
element: "FaultString",
});
faultString = "";
}
return { faultCode, faultString, setParameterValuesFault };
}
function fault(xml: Element): CpeFault {
let faultCode, faultString, detail;
let faultCode: string, faultString: string, detail: FaultStruct;
for (const c of xml.children) {
switch (c.localName) {
case "faultcode":
@@ -657,7 +775,25 @@ function fault(xml: Element): CpeFault {
}
}
return { faultCode, faultString, detail };
if (!detail) throw new Error("Missing detail element");
if (faultCode == null) {
warnings.push({
message: "Missing or invalid XML node",
element: "faultcode",
});
faultCode = "Client";
}
if (faultString == null) {
warnings.push({
message: "Missing or invalid XML node",
element: "faultstring",
});
faultString = "CWMP fault";
}
return { faultCode, faultString, detail } as CpeFault;
}
export function request(
+2 -2
View File
@@ -289,8 +289,8 @@ export interface FaultStruct {
}
export interface CpeFault {
faultCode: string;
faultString: string;
faultCode: "Client" | "Server";
faultString: "CWMP fault";
detail?: FaultStruct;
}
+6 -4
View File
@@ -17,7 +17,7 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import { Z_SYNC_FLUSH } from "zlib";
import { constants } from "zlib";
import Koa from "koa";
import Router from "koa-router";
import * as jwt from "jsonwebtoken";
@@ -60,7 +60,9 @@ const getAuthorizer = memoize(
);
koa.on("error", async (err) => {
throw err;
setTimeout(() => {
throw err;
});
});
koa.use(async (ctx, next) => {
@@ -252,10 +254,10 @@ router.get("/", async (ctx) => {
koa.use(
koaCompress({
gzip: {
flush: Z_SYNC_FLUSH,
flush: constants.Z_SYNC_FLUSH,
},
deflate: {
flush: Z_SYNC_FLUSH,
flush: constants.Z_SYNC_FLUSH,
},
})
);
-10
View File
@@ -145,16 +145,6 @@ export async function postTasks(
return { connectionRequest: "OK", tasks: statuses };
}
interface PingResponse {
packetsTransmitted: number;
packetsReceived: number;
packetLoss: string;
min: number;
avg: number;
max: number;
mdev: number;
}
export async function putResource(
resource: string,
id: string,
+31 -30
View File
@@ -31,6 +31,7 @@ import { del } from "../cache";
import Authorizer from "../common/authorizer";
import { ping } from "../ping";
import * as url from "url";
import { decodeTag } from "../common";
const router = new Router();
export default router;
@@ -85,7 +86,7 @@ router.get(`/devices/:id.csv`, async (ctx) => {
if (!authorizer.hasAccess("devices", 2)) {
logUnauthorizedWarning(log);
return void (ctx.status = 404);
return void (ctx.status = 403);
}
const res = await db.query("devices", filter);
@@ -126,7 +127,7 @@ router.get(`/devices/:id.csv`, async (ctx) => {
});
for (const [resource, flags] of Object.entries(resources)) {
router.head(`/${resource}`, async (ctx, next) => {
router.head(`/${resource}`, async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
let filter: Expression = authorizer.getFilter(resource, 1);
if (ctx.request.query.filter)
@@ -141,7 +142,7 @@ for (const [resource, flags] of Object.entries(resources)) {
if (!authorizer.hasAccess(resource, 1)) {
logUnauthorizedWarning(log);
return void next();
return void (ctx.status = 403);
}
// Exclude temporary tasks and faults
@@ -162,7 +163,7 @@ for (const [resource, flags] of Object.entries(resources)) {
logger.accessInfo(log);
});
router.get(`/${resource}`, async (ctx, next) => {
router.get(`/${resource}`, async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const options: QueryOptions = {};
let filter: Expression = authorizer.getFilter(resource, 2);
@@ -190,7 +191,7 @@ for (const [resource, flags] of Object.entries(resources)) {
if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log);
return void next();
return void (ctx.status = 403);
}
// Exclude temporary tasks and faults
@@ -216,7 +217,7 @@ for (const [resource, flags] of Object.entries(resources)) {
});
// CSV download
router.get(`/${resource}.csv`, async (ctx, next) => {
router.get(`/${resource}.csv`, async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const options: QueryOptions = { projection: {} };
let filter: Expression = authorizer.getFilter(resource, 2);
@@ -238,7 +239,7 @@ for (const [resource, flags] of Object.entries(resources)) {
if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log);
return void next();
return void (ctx.status = 403);
}
const columns: Record<string, Expression> = JSON.parse(
@@ -280,7 +281,7 @@ for (const [resource, flags] of Object.entries(resources)) {
if (e[1] === "Tags") {
const tags = [];
for (const p in obj)
if (p.startsWith("Tags.")) tags.push(p.slice(5));
if (p.startsWith("Tags.")) tags.push(decodeTag(p.slice(5)));
return tags.join(", ");
}
@@ -307,7 +308,7 @@ for (const [resource, flags] of Object.entries(resources)) {
logger.accessInfo(log);
});
router.head(`/${resource}/:id`, async (ctx, next) => {
router.head(`/${resource}/:id`, async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const log = {
message: `Count ${resource}`,
@@ -322,18 +323,18 @@ for (const [resource, flags] of Object.entries(resources)) {
]);
if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log);
return void next();
return void (ctx.status = 403);
}
const res = await db.query(resource, filter);
if (!res.length) return void next();
if (!res.length) return void (ctx.status = 404);
logger.accessInfo(log);
ctx.body = "";
});
router.get(`/${resource}/:id`, async (ctx, next) => {
router.get(`/${resource}/:id`, async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const log = {
message: `Query ${resource}`,
@@ -348,19 +349,19 @@ for (const [resource, flags] of Object.entries(resources)) {
]);
if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log);
return void next();
return void (ctx.status = 403);
}
const res = await db.query(resource, filter);
if (!res.length) return void next();
if (!res.length) return void (ctx.status = 404);
logger.accessInfo(log);
ctx.body = res[0];
});
if (flags & RESOURCE_DELETE) {
router.delete(`/${resource}/:id`, async (ctx, next) => {
router.delete(`/${resource}/:id`, async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const log = {
message: `Delete ${resource}`,
@@ -375,10 +376,10 @@ for (const [resource, flags] of Object.entries(resources)) {
]);
if (!authorizer.hasAccess(resource, 3)) {
logUnauthorizedWarning(log);
return void next();
return void (ctx.status = 403);
}
const res = await db.query(resource, filter);
if (!res.length) return void next();
if (!res.length) return void (ctx.status = 404);
const validate = authorizer.getValidator(resource, res[0]);
if (!validate("delete")) {
@@ -395,7 +396,7 @@ for (const [resource, flags] of Object.entries(resources)) {
}
if (flags & RESOURCE_PUT) {
router.put(`/${resource}/:id`, async (ctx, next) => {
router.put(`/${resource}/:id`, async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const id = ctx.params.id;
@@ -407,7 +408,7 @@ for (const [resource, flags] of Object.entries(resources)) {
if (!authorizer.hasAccess(resource, 3)) {
logUnauthorizedWarning(log);
return void next();
return void (ctx.status = 403);
}
const obj = ctx.request.body;
@@ -434,7 +435,7 @@ for (const [resource, flags] of Object.entries(resources)) {
}
}
router.put("/files/:id", async (ctx, next) => {
router.put("/files/:id", async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const resource = "files";
const id = ctx.params.id;
@@ -448,7 +449,7 @@ router.put("/files/:id", async (ctx, next) => {
if (!authorizer.hasAccess(resource, 3)) {
logUnauthorizedWarning(log);
return void next();
return void (ctx.status = 403);
}
const metadata = {
@@ -477,7 +478,7 @@ router.put("/files/:id", async (ctx, next) => {
ctx.body = "";
});
router.post("/devices/:id/tasks", async (ctx, next) => {
router.post("/devices/:id/tasks", async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const log = {
message: "Commit tasks",
@@ -493,10 +494,10 @@ router.post("/devices/:id/tasks", async (ctx, next) => {
]);
if (!authorizer.hasAccess("devices", 3)) {
logUnauthorizedWarning(log);
return void next();
return void (ctx.status = 403);
}
const devices = await db.query("devices", filter);
if (!devices.length) return void next();
if (!devices.length) return void (ctx.status = 404);
const device = devices[0];
const validate = authorizer.getValidator("devices", device);
@@ -545,7 +546,7 @@ router.post("/devices/:id/tasks", async (ctx, next) => {
ctx.body = res.tasks;
});
router.post("/devices/:id/tags", async (ctx, next) => {
router.post("/devices/:id/tags", async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const log = {
message: "Update tags",
@@ -561,10 +562,10 @@ router.post("/devices/:id/tags", async (ctx, next) => {
]);
if (!authorizer.hasAccess("devices", 3)) {
logUnauthorizedWarning(log);
return void next();
return void (ctx.status = 403);
}
const res = await db.query("devices", filter);
if (!res.length) return void next();
if (!res.length) return void (ctx.status = 404);
const validate = authorizer.getValidator("devices", res[0]);
if (!validate("tags", ctx.request.body)) {
@@ -600,7 +601,7 @@ router.get("/ping/:host", async (ctx) => {
});
});
router.put("/users/:id/password", async (ctx, next) => {
router.put("/users/:id/password", async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const username = ctx.params.id;
const log = {
@@ -625,7 +626,7 @@ router.put("/users/:id/password", async (ctx, next) => {
}
} else if (!authorizer.hasAccess("users", 3)) {
logUnauthorizedWarning(log);
return void next();
return void (ctx.status = 403);
}
const filter = and(authorizer.getFilter("users", 3), [
@@ -634,7 +635,7 @@ router.put("/users/:id/password", async (ctx, next) => {
username,
]);
const res = await db.query("users", filter);
if (!res.length) return void next();
if (!res.length) return void (ctx.status = 404);
const newPassword = ctx.request.body.newPassword;
if (ctx.state.user) {
+16 -28
View File
@@ -237,39 +237,27 @@ export function count(resource: string, filter: Expression): Promise<number> {
});
}
export function updateDeviceTags(
export async function updateDeviceTags(
deviceId: string,
tags: Record<string, boolean>
): Promise<void> {
return new Promise((resolve, reject) => {
const add = [];
const pull = [];
const add = [];
const pull = [];
const regex = /^[0-9a-zA-Z_]+$/;
for (let [tag, onOff] of Object.entries(tags)) {
tag = tag.trim();
if (onOff) {
if (!tag.match(regex))
return void reject(new Error(`Invalid tag '${tag}'`));
add.push(tag);
} else {
pull.push(tag);
}
}
getClient()
.then((client) => {
const collection = client.db().collection("devices");
for (let [tag, onOff] of Object.entries(tags)) {
tag = tag.trim();
if (onOff) add.push(tag);
else pull.push(tag);
}
const object = {};
if (add && add.length) object["$addToSet"] = { _tags: { $each: add } };
if (pull && pull.length) object["$pullAll"] = { _tags: pull };
collection.updateOne({ _id: deviceId }, object, (err) => {
if (err) return void reject(err);
resolve();
});
})
.catch(reject);
});
const client = await getClient();
const collection = client.db().collection("devices");
const object = {};
if (add && add.length) object["$addToSet"] = { _tags: { $each: add } };
if (pull && pull.length) object["$pullAll"] = { _tags: pull };
await collection.updateOne({ _id: deviceId }, object);
}
function putResource(resource, id, object): Promise<void> {
+2 -1
View File
@@ -53,7 +53,8 @@ export interface Element {
}
export function parseXmlDeclaration(buffer: Buffer): Attribute[] {
for (const enc of ["utf16le", "utf8", "latin1", "ascii"]) {
const encodings: BufferEncoding[] = ["utf16le", "utf8", "latin1", "ascii"];
for (const enc of encodings) {
let str = buffer.toString(enc, 0, 150);
if (str.startsWith("<?xml")) {
str = str.split("\n")[0].trim();
+2186 -3474
View File
File diff suppressed because it is too large. Load diff
+26 -25
View File
@@ -1,6 +1,6 @@
{
"name": "genieacs",
"version": "1.2.1",
"version": "1.2.3",
"description": "A TR-069 Auto Configuration Server (ACS)",
"repository": {
"type": "git",
@@ -31,47 +31,48 @@
"koa-router": "^9.4.0",
"koa-static": "^5.0.0",
"later": "^1.2.0",
"mongodb": "^3.6.1",
"mongodb": "^3.6.2",
"parsimmon": "^1.16.0",
"seedrandom": "^3.0.5",
"yaml": "^1.10.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^15.0.0",
"@rollup/plugin-commonjs": "^16.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^9.0.0",
"@rollup/plugin-typescript": "^5.0.2",
"@rollup/plugin-node-resolve": "^10.0.0",
"@rollup/plugin-typescript": "^6.0.0",
"@types/jsonwebtoken": "^8.5.0",
"@types/koa": "^2.11.4",
"@types/koa-compress": "^4.0.0",
"@types/koa": "^2.11.6",
"@types/koa-compress": "^4.0.1",
"@types/koa-router": "^7.4.1",
"@types/koa-static": "^4.0.1",
"@types/mithril": "^2.0.3",
"@types/mongodb": "^3.5.27",
"@types/parsimmon": "^1.10.2",
"@types/mithril": "^2.0.5",
"@types/mongodb": "^3.5.31",
"@types/node": "^14.14.5",
"@types/parsimmon": "^1.10.3",
"@types/seedrandom": "^2.4.28",
"@types/svgo": "^1.3.3",
"@typescript-eslint/eslint-plugin": "^3.10.1",
"@typescript-eslint/parser": "^3.10.1",
"ava": "^3.12.1",
"codemirror": "^5.57.0",
"@typescript-eslint/eslint-plugin": "^4.6.0",
"@typescript-eslint/parser": "^4.6.0",
"ava": "^3.13.0",
"codemirror": "^5.58.2",
"cssnano": "^4.1.10",
"eslint": "^7.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint": "^7.12.1",
"eslint-config-prettier": "^6.14.0",
"esm": "^3.2.25",
"jsbi": "^3.1.3",
"jsbi": "^3.1.4",
"mithril": "^2.0.4",
"postcss": "^7.0.32",
"postcss-import": "^12.0.1",
"postcss": "^8.1.4",
"postcss-import": "^13.0.0",
"postcss-preset-env": "^6.7.0",
"prettier": "^2.1.1",
"rollup": "^2.26.9",
"rollup-plugin-terser": "^7.0.1",
"source-map-loader": "^1.1.0",
"prettier": "^2.1.2",
"rollup": "^2.32.1",
"rollup-plugin-terser": "^7.0.2",
"source-map-loader": "^1.1.1",
"svgo": "^1.3.2",
"ts-node": "^9.0.0",
"typescript": "^4.0.2",
"webpack": "^4.44.1"
"typescript": "^4.0.5",
"webpack": "^5.2.0"
},
"engines": {
"node": ">=12.13.0"
+1 -1
View File
@@ -70,7 +70,7 @@ const component: ClosureComponent = (): Component => {
onclick: () => {
taskQueue.stageDownload({
name: "download",
device: device["DeviceID.ID"].value[0],
devices: [device["DeviceID.ID"].value[0]],
});
},
},
+1 -1
View File
@@ -90,7 +90,7 @@ const component: ClosureComponent = (): Component => {
onclick: () => {
taskQueue.stageSpv({
name: "setParameterValues",
device: device["DeviceID.ID"].value[0],
devices: [device["DeviceID.ID"].value[0]],
parameterValues: [
[
parameter,
+2 -1
View File
@@ -81,7 +81,8 @@ const component: ClosureComponent = (): Component => {
store.fulfill(0, Date.now());
})
.catch((err) => {
throw err;
e.target.disabled = false;
notifications.push("error", err.message);
});
},
},
+2 -1
View File
@@ -22,6 +22,7 @@ import { m } from "../components";
import * as notifications from "../notifications";
import * as store from "../store";
import { getIcon } from "../icons";
import { decodeTag } from "../../lib/common";
const component: ClosureComponent = (): Component => {
return {
@@ -32,7 +33,7 @@ const component: ClosureComponent = (): Component => {
const tags = [];
for (const p of Object.keys(device))
if (p.startsWith("Tags.")) tags.push(p.slice(5));
if (p.startsWith("Tags.")) tags.push(decodeTag(p.slice(5)));
tags.sort();
+32 -1
View File
@@ -42,6 +42,35 @@ export function flattenConfig(config: Record<string, unknown>): any {
return flatten;
}
// Order keys such that nested objects come last
function orderKeys(config: any): number {
let res = 1;
if (typeof config !== "object") return res;
if (Array.isArray(config)) {
for (const c of config) res += orderKeys(c);
return res;
}
const weights: [string, number][] = Object.entries(config).map(([k, v]) => [
k,
orderKeys(v),
]);
weights.sort((a, b) => {
if (a[1] !== b[1]) return a[1] - b[1];
if (b[0] > a[0]) return -1;
else return 1;
});
for (const [k, w] of weights) {
res += w;
const v = config[k];
delete config[k];
config[k] = v;
}
return res;
}
export function structureConfig(config: Config[]): any {
config.sort((a, b) => (a._id > b._id ? 1 : a._id < b._id ? -1 : 0));
const _config = {};
@@ -93,7 +122,9 @@ export function structureConfig(config: Config[]): any {
return object;
};
return toArray(_config);
const res = toArray(_config);
orderKeys(res);
return res;
}
export function diffConfig(
+107 -92
View File
@@ -147,37 +147,42 @@ function renderTable(confsResponse, searchString): Children {
{
title: "Edit config value",
onclick: () => {
const cb = (): Children => {
return m(
putFormComponent,
Object.assign(
{
base: conf,
actionHandler: (action, object) => {
return new Promise((resolve) => {
putActionHandler(action, object, false)
.then((errors) => {
const ErrorList = errors ? Object.values(errors) : [];
if (ErrorList.length) {
for (const err of ErrorList)
notifications.push("error", err);
} else {
overlay.close(cb);
}
resolve();
})
.catch((err) => {
notifications.push("error", err.message);
resolve();
});
});
},
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
base: conf,
actionHandler: (action, object) => {
return new Promise((resolve) => {
putActionHandler(action, object, false)
.then((errors) => {
const ErrorList = errors ? Object.values(errors) : [];
if (ErrorList.length) {
for (const err of ErrorList)
notifications.push("error", err);
} else {
overlay.close(cb);
}
resolve();
})
.catch((err) => {
notifications.push("error", err.message);
resolve();
});
});
},
formData
)
);
};
overlay.open(cb);
},
formData
)
);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
getIcon("edit")
@@ -244,38 +249,43 @@ export const component: ClosureComponent = (): Component => {
{
title: "Create new config",
onclick: () => {
const cb = (): Children => {
return m(
putFormComponent,
Object.assign(
{
actionHandler: (action, object) => {
return new Promise((resolve) => {
putActionHandler(action, object, true)
.then((errors) => {
const errorList = errors
? Object.values(errors)
: [];
if (errorList.length) {
for (const err of errorList)
notifications.push("error", err);
} else {
overlay.close(cb);
}
resolve();
})
.catch((err) => {
notifications.push("error", err.message);
resolve();
});
});
},
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
actionHandler: (action, object) => {
return new Promise((resolve) => {
putActionHandler(action, object, true)
.then((errors) => {
const errorList = errors
? Object.values(errors)
: [];
if (errorList.length) {
for (const err of errorList)
notifications.push("error", err);
} else {
overlay.close(cb);
}
resolve();
})
.catch((err) => {
notifications.push("error", err.message);
resolve();
});
});
},
formData
)
);
};
overlay.open(cb);
},
formData
)
);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
"New config"
@@ -307,39 +317,44 @@ export const component: ClosureComponent = (): Component => {
"button",
{
onclick: () => {
const cb = (): Children => {
return m(
uiConfigComponent,
Object.assign(
{
onUpdate: (errs: string[]) => {
const errors = errs ? Object.values(errs) : [];
if (errors.length) {
for (const err of errors)
notifications.push("error", err);
} else {
notifications.push(
"success",
`${sub.name.replace(
/^[a-z]/,
sub.name[0].toUpperCase()
)} config updated`
);
overlay.close(cb);
}
store.fulfill(0, Date.now());
},
onError: (err) => {
notifications.push("error", err.message);
store.fulfill(0, Date.now());
let cb: () => Children = null;
const comp = m(
uiConfigComponent,
Object.assign(
{
onUpdate: (errs: string[]) => {
const errors = errs ? Object.values(errs) : [];
if (errors.length) {
for (const err of errors)
notifications.push("error", err);
} else {
notifications.push(
"success",
`${sub.name.replace(
/^[a-z]/,
sub.name[0].toUpperCase()
)} config updated`
);
overlay.close(cb);
},
}
store.fulfill(0, Date.now());
},
attrs
)
);
};
overlay.open(cb);
onError: (err) => {
notifications.push("error", err.message);
store.fulfill(0, Date.now());
overlay.close(cb);
},
},
attrs
)
);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
`Edit ${sub.name}`
+2
View File
@@ -178,6 +178,8 @@ body {
}
& > .queue {
max-height: 400px;
overflow-y: auto;
& > .pending {
animation: 0.5s linear 0s infinite alternate pending-blink;
border-radius: 0.4em;
+28 -13
View File
@@ -23,7 +23,7 @@ import config from "./config";
import indexTableComponent from "./index-table-component";
import filterComponent from "./filter-component";
import * as store from "./store";
import * as taskQueue from "./task-queue";
import { queueTask, stageDownload } from "./task-queue";
import * as notifications from "./notifications";
import { parse, stringify } from "../lib/common/expression-parser";
import { evaluate, extractParams } from "../lib/common/expression";
@@ -91,12 +91,11 @@ function renderActions(selected: Set<string>): Children {
title: "Reboot selected devices",
disabled: !selected.size,
onclick: () => {
for (const d of selected) {
taskQueue.queueTask({
name: "reboot",
device: d,
});
}
const tasks = [...selected].map((s) => ({
name: "reboot",
device: s,
}));
queueTask(...tasks);
},
},
"Reboot"
@@ -110,18 +109,34 @@ function renderActions(selected: Set<string>): Children {
title: "Factory reset selected devices",
disabled: !selected.size,
onclick: () => {
for (const d of selected) {
taskQueue.queueTask({
name: "factoryReset",
device: d,
});
}
const tasks = [...selected].map((s) => ({
name: "factoryReset",
device: s,
}));
queueTask(...tasks);
},
},
"Reset"
)
);
buttons.push(
m(
"button.critical",
{
title: "Push a firmware or a config file",
disabled: !selected.size,
onclick: () => {
stageDownload({
name: "download",
devices: [...selected],
});
},
},
"Push file"
)
);
buttons.push(
m(
"button.primary",
+87 -62
View File
@@ -24,10 +24,21 @@ import m, {
Component,
VnodeDOM,
} from "mithril";
import * as taskQueue from "./task-queue";
import * as store from "./store";
import * as notifications from "./notifications";
import { getIcon } from "./icons";
import {
clear,
commit,
deleteTask,
getQueue,
getStaging,
QueueTask,
queueTask,
StageTask,
} from "./task-queue";
const invalid: WeakSet<StageTask> = new WeakSet();
function mparam(param): Children {
return m("span.parameter", { title: param }, `${param}`);
@@ -37,11 +48,11 @@ function mval(val): Children {
return m("span.value", { title: val }, `${val}`);
}
function renderStagingSpv(task, queueFunc, cancelFunc): Children {
function keydown(e): void {
if (e.keyCode === 13) queueFunc();
else if (e.keyCode === 27) cancelFunc();
else e.redraw = false;
function renderStagingSpv(task: StageTask, queueFunc, cancelFunc): Children {
function keydown(e: KeyboardEvent): void {
if (e.key === "Enter") queueFunc();
else if (e.key === "Escape") cancelFunc();
else e["redraw"] = false;
}
let input;
@@ -90,13 +101,24 @@ function renderStagingSpv(task, queueFunc, cancelFunc): Children {
return [m("span", "Editing ", mparam(task.parameterValues[0][0])), input];
}
function renderStagingDownload(task): Children {
task.invalid = !task.fileName || !task.fileType;
function renderStagingDownload(task: StageTask): Children {
if (!task.fileName || !task.fileType) invalid.add(task);
else invalid.delete(task);
const files = store.fetch("files", true);
const idParts = task.device.split("-");
const oui = decodeURIComponent(idParts[0]);
const productClass =
idParts.length === 3 ? decodeURIComponent(idParts[1]) : "";
let oui = "";
let productClass = "";
for (const d of task.devices) {
const parts = d.split("-");
if (oui === "") oui = parts[0];
else if (oui !== parts[0]) oui = null;
if (parts.length === 3) {
if (productClass === "") productClass = parts[1];
else if (productClass !== parts[1]) productClass = null;
}
}
if (oui) oui = decodeURIComponent(oui);
if (productClass) productClass = decodeURIComponent(productClass);
const typesList = [
"",
@@ -162,13 +184,17 @@ function renderStagingDownload(task): Children {
];
}
function renderStaging(staging): Child[] {
function renderStaging(staging: Set<StageTask>): Child[] {
const elements: Child[] = [];
for (const s of staging) {
const queueFunc = (): void => {
staging.delete(s);
taskQueue.queueTask(s);
for (const d of s.devices) {
const t = Object.assign({ device: d }, s);
delete t.devices;
queueTask(t);
}
};
const cancelFunc = (): void => {
staging.delete(s);
@@ -181,7 +207,7 @@ function renderStaging(staging): Child[] {
const queue = m(
"button.primary",
{ title: "Queue task", onclick: queueFunc, disabled: s.invalid },
{ title: "Queue task", onclick: queueFunc, disabled: invalid.has(s) },
"Queue"
);
const cancel = m(
@@ -195,7 +221,7 @@ function renderStaging(staging): Child[] {
return elements;
}
function renderQueue(queue): Child[] {
function renderQueue(queue: Set<QueueTask>): Child[] {
const details: Child[] = [];
const devices: { [deviceId: string]: any[] } = {};
for (const t of queue) {
@@ -215,7 +241,7 @@ function renderQueue(queue): Child[] {
{
title: "Retry this task",
onclick: () => {
taskQueue.queueTask(t);
queueTask(t);
},
},
getIcon("retry")
@@ -229,7 +255,7 @@ function renderQueue(queue): Child[] {
{
title: "Remove this task",
onclick: () => {
taskQueue.deleteTask(t);
deleteTask(t);
},
},
getIcon("remove")
@@ -347,8 +373,8 @@ function renderNotifications(notifs): Child[] {
const component: ClosureComponent = (): Component => {
return {
view: (vnode) => {
const queue = taskQueue.getQueue();
const staging = taskQueue.getStaging();
const queue = getQueue();
const staging = getStaging();
const notifs = notifications.getNotifications();
let drawerElement, statusElement;
@@ -394,56 +420,55 @@ const component: ClosureComponent = (): Component => {
title: "Commit queued tasks",
disabled: !statusCount.queued,
onclick: () => {
const tasks = Array.from(taskQueue.getQueue()).filter(
const tasks = Array.from(getQueue()).filter(
(t) => t["status"] === "queued"
);
taskQueue
.commit(
tasks,
(deviceId, err, connectionRequestStatus, tasks2) => {
if (err) {
notifications.push(
"error",
`${deviceId}: ${err.message}`
);
return;
}
if (connectionRequestStatus !== "OK") {
notifications.push(
"error",
`${deviceId}: ${connectionRequestStatus}`
);
return;
}
for (const t of tasks2) {
if (t.status === "stale") {
notifications.push(
"error",
`${deviceId}: No contact from device`
);
return;
} else if (t.status === "fault") {
notifications.push(
"error",
`${deviceId}: Task(s) faulted`
);
return;
}
}
commit(
tasks,
(deviceId, err, connectionRequestStatus, tasks2) => {
if (err) {
notifications.push(
"success",
`${deviceId}: Task(s) committed`
"error",
`${deviceId}: ${err.message}`
);
return;
}
)
if (connectionRequestStatus !== "OK") {
notifications.push(
"error",
`${deviceId}: ${connectionRequestStatus}`
);
return;
}
for (const t of tasks2) {
if (t.status === "stale") {
notifications.push(
"error",
`${deviceId}: No contact from device`
);
return;
} else if (t.status === "fault") {
notifications.push(
"error",
`${deviceId}: Task(s) faulted`
);
return;
}
}
notifications.push(
"success",
`${deviceId}: Task(s) committed`
);
}
)
.then(() => {
store.fulfill(0, Date.now());
})
.catch((err) => {
throw err;
notifications.push("error", err.message);
});
},
},
@@ -453,7 +478,7 @@ const component: ClosureComponent = (): Component => {
"button",
{
title: "Clear tasks",
onclick: taskQueue.clear,
onclick: clear,
disabled: !queueElements.length,
},
"Clear"
+1 -1
View File
@@ -51,7 +51,7 @@ export function loadCodeMirror(): Promise<void> {
];
Promise.all(promises)
.then((modules) => {
codeMirror = modules[0];
codeMirror = modules[0].default;
resolve();
})
.catch((err) => {
+36 -31
View File
@@ -221,38 +221,43 @@ export const component: ClosureComponent = (): Component => {
{
title: "Create new file",
onclick: () => {
const cb = (): Children => {
return m(
putFormComponent,
Object.assign(
{
actionHandler: (action, object) => {
return new Promise((resolve) => {
putActionHandler(action, object)
.then((errors) => {
const errorList = errors
? Object.values(errors)
: [];
if (errorList.length) {
for (const err of errorList)
notifications.push("error", err);
} else {
overlay.close(cb);
}
resolve();
})
.catch((err) => {
notifications.push("error", err.message);
resolve();
});
});
},
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
actionHandler: (action, object) => {
return new Promise((resolve) => {
putActionHandler(action, object)
.then((errors) => {
const errorList = errors
? Object.values(errors)
: [];
if (errorList.length) {
for (const err of errorList)
notifications.push("error", err);
} else {
overlay.close(cb);
}
resolve();
})
.catch((err) => {
notifications.push("error", err.message);
resolve();
});
});
},
formData
)
);
};
overlay.open(cb);
},
formData
)
);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
"New"
+5 -2
View File
@@ -27,6 +27,8 @@ interface Attribute {
type?: string;
}
const MAX_PAGE_SIZE = 200;
function renderTable(
attributes: Attribute[],
data: Record<string, any>[],
@@ -37,7 +39,7 @@ function renderTable(
onSortChange: (obj: Record<string, number>) => void,
downloadUrl?: string,
valueCallback?: (attr: Attribute, record: Record<string, any>) => Children,
actionsCallback?: Children | ((selected: Set<string>) => Children),
actionsCallback?: Children | ((sel: Set<string>) => Children),
recordActionsCallback?: Children | ((record: Record<string, any>) => Children)
): Children {
const records = data || [];
@@ -180,7 +182,8 @@ function renderTable(
{
title: "Show more records",
onclick: showMoreCallback,
disabled: !data.length || records.length >= total,
disabled:
!data.length || records.length >= Math.min(MAX_PAGE_SIZE, total),
},
"More"
)
+1 -1
View File
@@ -81,7 +81,7 @@ export const component: ClosureComponent = (): Component => {
location.reload();
})
.catch((err) => {
notifications.push("error", err.message);
notifications.push("error", err.response || err.message);
e.target.disabled = false;
});
return false;
+14 -5
View File
@@ -20,16 +20,24 @@
import m, { Children } from "mithril";
type OverlayCallback = () => Children;
type CloseCallback = () => boolean;
let overlayCallback: OverlayCallback = null;
let closeCallback: CloseCallback = null;
export function open(callback: OverlayCallback): void {
export function open(
callback: OverlayCallback,
closeCb: CloseCallback = null
): void {
overlayCallback = callback;
closeCallback = closeCb;
}
export function close(callback: OverlayCallback): boolean {
export function close(callback: OverlayCallback, force = true): boolean {
if (callback === overlayCallback) {
if (!force && closeCallback && !closeCallback()) return false;
overlayCallback = null;
closeCallback = null;
return true;
}
@@ -43,7 +51,7 @@ export function render(): Children {
{
tabindex: 0,
onclick: () => {
close(overlayCallback);
close(overlayCallback, false);
},
style: "opacity: 0",
oncreate: (vnode) => {
@@ -75,9 +83,10 @@ export function render(): Children {
}
document.addEventListener("keydown", (e) => {
if (overlayCallback && e.keyCode === 27 && close(overlayCallback)) m.redraw();
if (overlayCallback && e.key === "Escape" && close(overlayCallback, false))
m.redraw();
});
window.addEventListener("popstate", () => {
if (close(overlayCallback)) m.redraw();
if (close(overlayCallback, false)) m.redraw();
});
+36 -31
View File
@@ -285,38 +285,43 @@ export const component: ClosureComponent = (): Component => {
{
title: "Create new permission",
onclick: () => {
const cb = (): Children => {
return m(
putFormComponent,
Object.assign(
{
actionHandler: (action, object) => {
return new Promise((resolve) => {
putActionHandler(action, object)
.then((errors) => {
const errorList = errors
? Object.values(errors)
: [];
if (errorList.length) {
for (const err of errorList)
notifications.push("error", err);
} else {
overlay.close(cb);
}
resolve();
})
.catch((err) => {
notifications.push("error", err.message);
resolve();
});
});
},
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
actionHandler: (action, object) => {
return new Promise((resolve) => {
putActionHandler(action, object)
.then((errors) => {
const errorList = errors
? Object.values(errors)
: [];
if (errorList.length) {
for (const err of errorList)
notifications.push("error", err);
} else {
overlay.close(cb);
}
resolve();
})
.catch((err) => {
notifications.push("error", err.message);
resolve();
});
});
},
formData
)
);
};
overlay.open(cb);
},
formData
)
);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
"New"
+64 -53
View File
@@ -287,7 +287,38 @@ export const component: ClosureComponent = (): Component => {
"a",
{
onclick: () => {
const cb = (): Children => {
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
base: preset,
actionHandler: (action, object) => {
return new Promise((resolve) => {
putActionHandler(action, object, false)
.then((errors) => {
const errorList = errors
? Object.values(errors)
: [];
if (errorList.length) {
for (const err of errorList)
notifications.push("error", err);
} else {
overlay.close(cb);
}
resolve();
})
.catch((err) => {
notifications.push("error", err.message);
resolve();
});
});
},
},
formData
)
);
cb = (): Children => {
if (!preset.provision) {
return m(
"div",
@@ -295,15 +326,37 @@ export const component: ClosureComponent = (): Component => {
"This UI only supports presets with a single 'provision' configuraiton. If this preset was originally created from the old UI (genieacs-gui), you must edit it there."
);
}
return comp;
};
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
"Show"
),
];
};
return m(
if (window.authorizer.hasAccess("presets", 3)) {
attrs["actionsCallback"] = (selected: Set<string>): Children => {
return [
m(
"button.primary",
{
title: "Create new preset",
onclick: () => {
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
base: preset,
actionHandler: (action, object) => {
return new Promise((resolve) => {
putActionHandler(action, object, false)
putActionHandler(action, object, true)
.then((errors) => {
const errorList = errors
? Object.values(errors)
@@ -326,55 +379,13 @@ export const component: ClosureComponent = (): Component => {
formData
)
);
};
overlay.open(cb);
},
},
"Show"
),
];
};
if (window.authorizer.hasAccess("presets", 3)) {
attrs["actionsCallback"] = (selected: Set<string>): Children => {
return [
m(
"button.primary",
{
title: "Create new preset",
onclick: () => {
const cb = (): Children => {
return m(
putFormComponent,
Object.assign(
{
actionHandler: (action, object) => {
return new Promise((resolve) => {
putActionHandler(action, object, true)
.then((errors) => {
const errorList = errors
? Object.values(errors)
: [];
if (errorList.length) {
for (const err of errorList)
notifications.push("error", err);
} else {
overlay.close(cb);
}
resolve();
})
.catch((err) => {
notifications.push("error", err.message);
resolve();
});
});
},
},
formData
)
);
};
overlay.open(cb);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
"New"
+63 -53
View File
@@ -207,15 +207,67 @@ export const component: ClosureComponent = (): Component => {
"a",
{
onclick: () => {
const cb = (): Children => {
return m(
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
base: provision,
actionHandler: (action, object) => {
return new Promise((resolve) => {
putActionHandler(action, object, false)
.then((errors) => {
const errorList = errors
? Object.values(errors)
: [];
if (errorList.length) {
for (const err of errorList)
notifications.push("error", err);
} else {
overlay.close(cb);
}
resolve();
})
.catch((err) => {
notifications.push("error", err.message);
resolve();
});
});
},
},
formData
)
);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
"Show"
),
];
};
if (window.authorizer.hasAccess("provisions", 3)) {
attrs["actionsCallback"] = (selected: Set<string>): Children => {
return [
m(
"button.primary",
{
title: "Create new provision",
onclick: () => {
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
base: provision,
actionHandler: (action, object) => {
return new Promise((resolve) => {
putActionHandler(action, object, false)
putActionHandler(action, object, true)
.then((errors) => {
const errorList = errors
? Object.values(errors)
@@ -238,55 +290,13 @@ export const component: ClosureComponent = (): Component => {
formData
)
);
};
overlay.open(cb);
},
},
"Show"
),
];
};
if (window.authorizer.hasAccess("provisions", 3)) {
attrs["actionsCallback"] = (selected: Set<string>): Children => {
return [
m(
"button.primary",
{
title: "Create new provision",
onclick: () => {
const cb = (): Children => {
return m(
putFormComponent,
Object.assign(
{
actionHandler: (action, object) => {
return new Promise((resolve) => {
putActionHandler(action, object, true)
.then((errors) => {
const errorList = errors
? Object.values(errors)
: [];
if (errorList.length) {
for (const err of errorList)
notifications.push("error", err);
} else {
overlay.close(cb);
}
resolve();
})
.catch((err) => {
notifications.push("error", err.message);
resolve();
});
});
},
},
formData
)
);
};
overlay.open(cb);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
"New"
+7
View File
@@ -56,6 +56,7 @@ function createField(current, attr, focus): Children {
: null,
onchange: (e) => {
current.object[attr.id] = e.target.value;
current.modified = true;
e.redraw = false;
},
},
@@ -80,6 +81,7 @@ function createField(current, attr, focus): Children {
if (e.target.checked) currentSelected.add(op);
else currentSelected.delete(op);
current.object[attr.id] = Array.from(currentSelected);
current.modified = true;
e.redraw = false;
},
};
@@ -98,6 +100,7 @@ function createField(current, attr, focus): Children {
},
onChange: (value) => {
current.object[attr.id] = value;
current.modified = true;
},
};
return m(codeEditorComponent, attrs);
@@ -112,6 +115,7 @@ function createField(current, attr, focus): Children {
: null,
onchange: (e) => {
current.object[attr.id] = e.target.files;
current.modified = true;
e.redraw = false;
},
});
@@ -132,6 +136,7 @@ function createField(current, attr, focus): Children {
: null,
oninput: (e) => {
current.object[attr.id] = e.target.value;
current.modified = true;
e.redraw = false;
},
onkeypress: (e) => {
@@ -158,6 +163,7 @@ function createField(current, attr, focus): Children {
: null,
oninput: (e) => {
current.object[attr.id] = e.target.value;
current.modified = true;
e.redraw = false;
},
});
@@ -174,6 +180,7 @@ const component: ClosureComponent = (): Component => {
vnode.state["current"] = {
isNew: !base["_id"],
object: Object.assign({}, base),
modified: false,
};
}
+2 -1
View File
@@ -19,6 +19,7 @@
import config from "./config";
import { Expression } from "../lib/types";
import { encodeTag } from "../lib/common";
const resources = {
devices: {},
@@ -138,7 +139,7 @@ function queryMac(param, value): Expression {
}
function queryTag(tag: string): Expression {
const t = tag.replace(/[^a-zA-Z0-9-]+/g, "_");
const t = encodeTag(tag);
return ["IS NOT NULL", ["PARAM", `Tags.${t}`]];
}
+8 -3
View File
@@ -173,10 +173,11 @@ export async function xhrRequest(
// https://mithril.js.org/request.html#error-handling
if (xhr.status !== 304 && Math.floor(xhr.status / 100) !== 2) {
if (xhr.status === 403) throw new Error("Not authorized");
const err = new Error();
err["message"] =
xhr.status === 0
? "Server is unrachable"
? "Server is unreachable"
: `Unexpected response status code ${xhr.status}`;
err["code"] = xhr.status;
err["response"] = xhr.responseText;
@@ -385,8 +386,9 @@ function _fulfill(
filter: memoizedStringify(filter),
}),
extract: (xhr) => {
if (xhr.status === 403) throw new Error("Not authorized");
if (!xhr.status) {
throw new Error("Server is unrachable");
throw new Error("Server is unreachable");
} else if (xhr.status !== 200) {
throw new Error(
`Unexpected response status code ${xhr.status}`
@@ -605,6 +607,8 @@ export function postTasks(
url: `api/devices/${encodeURIComponent(deviceId)}/tasks`,
body: tasks,
extract: (xhr) => {
if (xhr.status === 403) throw new Error("Not authorized");
if (!xhr.status) throw new Error("Server is unreachable");
if (xhr.status !== 200) throw new Error(xhr.response);
const connectionRequestStatus = xhr.getResponseHeader(
"Connection-Request"
@@ -675,7 +679,8 @@ export function resourceExists(resource: string, id: string): Promise<number> {
filter: memoizedStringify(filter),
}),
extract: (xhr) => {
if (!xhr.status) throw new Error("Server is unrachable");
if (xhr.status === 403) throw new Error("Not authorized");
if (!xhr.status) throw new Error("Server is unreachable");
else if (xhr.status !== 200)
throw new Error(`Unexpected response status code ${xhr.status}`);
return +xhr.getResponseHeader("x-total-count");
+44 -13
View File
@@ -19,23 +19,41 @@
import * as store from "./store";
import { Task } from "../lib/types";
import * as notifications from "./notifications";
export interface QueueTask extends Task {
status?: string;
device?: string;
device: string;
}
const queue: Set<QueueTask> = new Set();
const staging: Set<QueueTask> = new Set();
export interface StageTask extends Task {
devices: string[];
}
export function queueTask(task: QueueTask): void {
staging.delete(task);
task.status = "queued";
queue.add(task);
const MAX_QUEUE = 100;
const queue: Set<QueueTask> = new Set();
const staging: Set<StageTask> = new Set();
function canQueue(tasks: QueueTask[]): boolean {
let count = queue.size;
for (const task of tasks) if (!queue.has(task)) ++count;
return count <= MAX_QUEUE;
}
export function queueTask(...tasks: QueueTask[]): void {
if (!canQueue(tasks)) {
notifications.push("error", "Too many tasks in queue");
return;
}
for (const task of tasks) {
task.status = "queued";
queue.add(task);
}
}
export function deleteTask(task: QueueTask): void {
staging.delete(task);
queue.delete(task);
}
@@ -47,7 +65,7 @@ export function clear(): void {
queue.clear();
}
export function getStaging(): Set<QueueTask> {
export function getStaging(): Set<StageTask> {
return staging;
}
@@ -55,11 +73,19 @@ export function clearStaging(): void {
staging.clear();
}
export function stageSpv(task: QueueTask): void {
export function stageSpv(task: StageTask): void {
if (queue.size + task.devices.length > MAX_QUEUE) {
notifications.push("error", "Too many tasks in queue");
return;
}
staging.add(task);
}
export function stageDownload(task: QueueTask): void {
export function stageDownload(task: StageTask): void {
if (queue.size + task.devices.length > MAX_QUEUE) {
notifications.push("error", "Too many tasks in queue");
return;
}
staging.add(task);
}
@@ -69,14 +95,19 @@ export function commit(
deviceId: string,
err: Error,
conReqStatus: string,
tasks: QueueTask[]
_tasks: QueueTask[]
) => void
): Promise<void> {
const devices: { [deviceId: string]: any[] } = {};
if (!canQueue(tasks))
return Promise.reject(new Error("Too many tasks in queue"));
for (const t of tasks) {
devices[t.device] = devices[t.device] || [];
devices[t.device].push(t);
queueTask(t);
t.status = "queued";
queue.add(t);
}
return new Promise((resolve) => {
+2 -6
View File
@@ -102,12 +102,7 @@ const component: ClosureComponent = (): Component => {
const yamlString =
config && Object.values(config).length
? yaml.stringify(config, {
schema: "failsafe",
tags: (args) => {
return args;
},
})
? yaml.stringify(config, { schema: "failsafe" })
: "";
const attrs = {
@@ -120,6 +115,7 @@ const component: ClosureComponent = (): Component => {
},
onChange: (value) => {
vnode.state["updatedYaml"] = value;
vnode.state["modified"] = true;
},
};
+81 -70
View File
@@ -237,46 +237,46 @@ export const component: ClosureComponent = (): Component => {
"a",
{
onclick: () => {
const cb = (): Children => {
const children = [
m(
putFormComponent,
Object.assign(
{
base: {
_id: user._id,
roles: user.roles.split(","),
},
actionHandler: (action, object) => {
return new Promise((resolve) => {
putActionHandler(action, object, false)
.then((errors) => {
const errorList = errors
? Object.values(errors)
: [];
if (errorList.length) {
for (const err of errorList)
notifications.push("error", err);
} else {
overlay.close(cb);
}
resolve();
})
.catch((err) => {
notifications.push("error", err.message);
resolve();
});
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
base: {
_id: user._id,
roles: user.roles.split(","),
},
actionHandler: (action, object) => {
return new Promise((resolve) => {
putActionHandler(action, object, false)
.then((errors) => {
const errorList = errors
? Object.values(errors)
: [];
if (errorList.length) {
for (const err of errorList)
notifications.push("error", err);
} else {
overlay.close(cb);
}
resolve();
})
.catch((err) => {
notifications.push("error", err.message);
resolve();
});
},
},
{
resource: "users",
attributes: attributes,
}
)
),
];
});
},
},
{
resource: "users",
attributes: attributes,
}
)
);
cb = () => {
const children: Children = [comp];
if (canWrite) {
children.push(m("hr"));
const _attrs = {
@@ -292,7 +292,13 @@ export const component: ClosureComponent = (): Component => {
return children;
};
overlay.open(cb);
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
"Show"
@@ -317,38 +323,43 @@ export const component: ClosureComponent = (): Component => {
{
title: "Create new user",
onclick: () => {
const cb = (): Children => {
return m(
putFormComponent,
Object.assign(
{
actionHandler: (action, object) => {
return new Promise((resolve) => {
putActionHandler(action, object, true)
.then((errors) => {
const errorList = errors
? Object.values(errors)
: [];
if (errorList.length) {
for (const err of errorList)
notifications.push("error", err);
} else {
overlay.close(cb);
}
resolve();
})
.catch((err) => {
notifications.push("error", err.message);
resolve();
});
});
},
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
actionHandler: (action, object) => {
return new Promise((resolve) => {
putActionHandler(action, object, true)
.then((errors) => {
const errorList = errors
? Object.values(errors)
: [];
if (errorList.length) {
for (const err of errorList)
notifications.push("error", err);
} else {
overlay.close(cb);
}
resolve();
})
.catch((err) => {
notifications.push("error", err.message);
resolve();
});
});
},
formData
)
);
};
overlay.open(cb);
},
formData
)
);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
"New"
+63 -53
View File
@@ -207,15 +207,67 @@ export const component: ClosureComponent = (): Component => {
"a",
{
onclick: () => {
const cb = (): Children => {
return m(
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
base: virtualParameter,
actionHandler: (action, object) => {
return new Promise((resolve) => {
putActionHandler(action, object, false)
.then((errors) => {
const errorList = errors
? Object.values(errors)
: [];
if (errorList.length) {
for (const err of errorList)
notifications.push("error", err);
} else {
overlay.close(cb);
}
resolve();
})
.catch((err) => {
notifications.push("error", err.message);
resolve();
});
});
},
},
formData
)
);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
"Show"
),
];
};
if (window.authorizer.hasAccess("virtualParameters", 3)) {
attrs["actionsCallback"] = (selected: Set<string>): Children => {
return [
m(
"button.primary",
{
title: "Create new virtual parameter",
onclick: () => {
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
base: virtualParameter,
actionHandler: (action, object) => {
return new Promise((resolve) => {
putActionHandler(action, object, false)
putActionHandler(action, object, true)
.then((errors) => {
const errorList = errors
? Object.values(errors)
@@ -238,55 +290,13 @@ export const component: ClosureComponent = (): Component => {
formData
)
);
};
overlay.open(cb);
},
},
"Show"
),
];
};
if (window.authorizer.hasAccess("virtualParameters", 3)) {
attrs["actionsCallback"] = (selected: Set<string>): Children => {
return [
m(
"button.primary",
{
title: "Create new virtual parameter",
onclick: () => {
const cb = (): Children => {
return m(
putFormComponent,
Object.assign(
{
actionHandler: (action, object) => {
return new Promise((resolve) => {
putActionHandler(action, object, true)
.then((errors) => {
const errorList = errors
? Object.values(errors)
: [];
if (errorList.length) {
for (const err of errorList)
notifications.push("error", err);
} else {
overlay.close(cb);
}
resolve();
})
.catch((err) => {
notifications.push("error", err.message);
resolve();
});
});
},
},
formData
)
);
};
overlay.open(cb);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
"New"