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