Compare commits

..
14 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
37 changed files with 837 additions and 3139 deletions

No files matched your search

+19
View File
@@ -1,6 +1,24 @@
# 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.
@@ -9,6 +27,7 @@
from v1.1.
## 1.2.1 (2020-09-08)
- Fix bug causing faults to not be displayed in the UI.
- Fix bug where deleting objects does not get reflected immediately in the UI.
- Improve conversion between filters written in the expression format and
+11 -11
View File
@@ -77,9 +77,16 @@ if (!cluster.worker) {
cluster.stop();
});
} else {
const ssl = {
key: config.get("CWMP_SSL_KEY") as string,
cert: config.get("CWMP_SSL_CERT") as string,
const key = config.get("CWMP_SSL_KEY") as string;
const cert = config.get("CWMP_SSL_CERT") as string;
const options = {
port: SERVICE_PORT,
host: SERVICE_ADDRESS,
ssl: key && cert ? { key, cert } : null,
onConnection: cwmp.onConnection,
timeout: 30000,
keepAliveTimeout: 0,
};
process.on("uncaughtException", (err) => {
@@ -94,14 +101,7 @@ if (!cluster.worker) {
const initPromise = Promise.all([db.connect(), cache.connect()])
.then(() => {
server.start(
SERVICE_PORT,
SERVICE_ADDRESS,
ssl,
cwmp.listener,
cwmp.onConnection,
0
);
server.start(options, cwmp.listener);
})
.catch((err) => {
setTimeout(() => {
+8 -4
View File
@@ -73,9 +73,13 @@ if (!cluster.worker) {
cluster.stop();
});
} else {
const ssl = {
key: config.get("FS_SSL_KEY") as string,
cert: config.get("FS_SSL_CERT") as string,
const key = config.get("FS_SSL_KEY") as string;
const cert = config.get("FS_SSL_CERT") as string;
const options = {
port: SERVICE_PORT,
host: SERVICE_ADDRESS,
ssl: key && cert ? { key, cert } : null,
timeout: 30000,
};
let stopping = false;
@@ -98,7 +102,7 @@ if (!cluster.worker) {
const initPromise = Promise.all([db.connect(), cache.connect()])
.then(() => {
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, _listener);
server.start(options, _listener);
})
.catch((err) => {
setTimeout(() => {
+8 -4
View File
@@ -77,9 +77,13 @@ if (!cluster.worker) {
cluster.stop();
});
} else {
const ssl = {
key: config.get("NBI_SSL_KEY") as string,
cert: config.get("NBI_SSL_CERT") as string,
const key = config.get("NBI_SSL_KEY") as string;
const cert = config.get("NBI_SSL_CERT") as string;
const options = {
port: SERVICE_PORT,
host: SERVICE_ADDRESS,
ssl: key && cert ? { key, cert } : null,
timeout: 30000,
};
let stopping = false;
@@ -102,7 +106,7 @@ if (!cluster.worker) {
const initPromise = Promise.all([db.connect(), cache.connect()])
.then(() => {
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, _listener);
server.start(options, _listener);
})
.catch((err) => {
setTimeout(() => {
+9 -7
View File
@@ -79,9 +79,13 @@ if (!cluster.worker) {
cluster.stop();
});
} else {
const ssl = {
key: config.get("UI_SSL_KEY") as string,
cert: config.get("UI_SSL_CERT") as string,
const key = config.get("UI_SSL_KEY") as string;
const cert = config.get("UI_SSL_CERT") as string;
const options = {
port: SERVICE_PORT,
host: SERVICE_ADDRESS,
ssl: key && cert ? { key, cert } : null,
timeout: 30000,
};
let stopping = false;
@@ -99,14 +103,12 @@ if (!cluster.worker) {
const _listener = (req, res): void => {
if (stopping) res.setHeader("Connection", "close");
listener(req, res).catch((err) => {
throw err;
});
listener(req, res);
};
const initPromise = Promise.all([db2.connect(), cache.connect()])
.then(() => {
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, _listener);
server.start(options, _listener);
})
.catch((err) => {
setTimeout(() => {
+9 -5
View File
@@ -19,14 +19,13 @@
import * as path from "path";
import * as fs from "fs";
import { promisify } from "util";
import { rollup, WarningHandler } from "rollup";
import rollupJson from "@rollup/plugin-json";
import typescript from "@rollup/plugin-typescript";
import { terser } from "rollup-plugin-terser";
import nodeResolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import webpack from "webpack";
import webpack, { Configuration } from "webpack";
import postcss from "postcss";
import postcssImport from "postcss-import";
import postcssPresetEnv from "postcss-preset-env";
@@ -340,8 +339,8 @@ async function generateFrontendJs(): Promise<void> {
file: outputFile,
});
const webpackConf = {
mode: MODE,
const webpackConf: Configuration = {
mode: MODE as "development" | "production",
entry: outputFile,
resolve: {
aliasFields: ["module"],
@@ -362,8 +361,13 @@ async function generateFrontendJs(): Promise<void> {
},
};
const stats = await promisify(webpack)(webpackConf);
return new Promise((resolve, reject) => {
webpack(webpackConf, (err, stats) => {
if (err) return reject(err);
process.stdout.write(stats.toString({ colors: true }) + "\n");
resolve();
});
});
}
async function generateIconsSprite(): Promise<void> {
+14
View File
@@ -47,3 +47,17 @@ export function generateDeviceId(
export function escapeRegExp(str: string): string {
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
}
export function encodeTag(tag: string): string {
return encodeURIComponent(tag)
.replace(
/[!~*'()]/g,
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()
)
.replace(/0x(?=[0-9A-Z]{2})/g, "0%78")
.replace(/%/g, "0x");
}
export function decodeTag(tag: string): string {
return decodeURIComponent(tag.replace(/0x(?=[0-9A-Z]{2})/g, "%"));
}
+1 -2
View File
@@ -1019,8 +1019,7 @@ function boolExprSynthToExpression(boolExpr: BoolExprSynth): Expression {
const { dcSet, isNull } = generateDcSetAndIsNull(variables);
minterms = sanitizeMinterms(minterms, isNull);
const canRaise = (idx: number, set: Set<number>): boolean =>
!isNull.has(idx >> 2) || !(idx & 1) || !set.has(idx ^ 3);
const canRaise = getCanRaiseCallback(isNull);
minterms = espresso(minterms, dcSet, { canRaise });
return sopToExpression(minterms, variables);
}
+22 -19
View File
@@ -90,9 +90,7 @@ async function authenticate(
}
if (authentication && authentication.method === "Digest") {
const sessionNonce = sessionsNonces.get(
sessionContext.httpRequest.connection
);
const sessionNonce = sessionsNonces.get(sessionContext.httpRequest.socket);
if (
!sessionNonce ||
@@ -185,7 +183,7 @@ async function writeResponse(
}
const httpResponse = sessionContext.httpResponse;
const connection = httpResponse.connection;
const connection = httpResponse.socket;
httpResponse.setHeader("Content-Length", Buffer.byteLength(data));
httpResponse.writeHead(res.code, res.headers);
@@ -842,8 +840,11 @@ async function sendAcsRequest(
return writeResponse(sessionContext, res);
}
async function getSession(connection, sessionId): Promise<SessionContext> {
const sessionContext = currentSessions.get(connection);
async function getSession(
connection: Socket,
sessionId: string
): Promise<SessionContext> {
let sessionContext = currentSessions.get(connection);
if (sessionContext) {
currentSessions.delete(connection);
return sessionContext;
@@ -855,7 +856,9 @@ async function getSession(connection, sessionId): Promise<SessionContext> {
const sessionContextString = await cache.pop(`session_${sessionId}`);
if (!sessionContextString) return null;
return session.deserialize(sessionContextString);
sessionContext = await session.deserialize(sessionContextString);
connection.setTimeout(sessionContext.timeout);
return sessionContext;
}
// Only needed to prevent tree shaking from removing the remoteAddress
@@ -988,7 +991,7 @@ async function reportBadState(sessionContext: SessionContext): Promise<void> {
sessionContext: sessionContext,
});
const httpResponse = sessionContext.httpResponse;
currentSessions.delete(httpResponse.connection);
currentSessions.delete(httpResponse.socket);
const body = "Bad session state";
httpResponse.setHeader("Content-Length", Buffer.byteLength(body));
httpResponse.writeHead(400, { Connection: "close" });
@@ -1015,14 +1018,14 @@ async function responseUnauthorized(
resHeaders["WWW-Authenticate"] = `Basic realm="${REALM}"`;
} else {
const nonce = crypto.randomBytes(16).toString("hex");
sessionsNonces.set(sessionContext.httpRequest.connection, nonce);
sessionsNonces.set(sessionContext.httpRequest.socket, nonce);
let d = `Digest realm="${REALM}"`;
d += ',qop="auth,auth-int"';
d += `,nonce="${nonce}"`;
resHeaders["WWW-Authenticate"] = d;
}
currentSessions.set(sessionContext.httpRequest.connection, sessionContext);
currentSessions.set(sessionContext.httpRequest.socket, sessionContext);
}
const httpResponse = sessionContext.httpResponse;
@@ -1042,7 +1045,6 @@ async function processRequest(
): Promise<void> {
for (const w of parseWarnings) {
w.sessionContext = sessionContext;
w.rpc = rpc;
logger.accessWarn(w);
}
@@ -1070,6 +1072,8 @@ async function processRequest(
);
}
sessionContext.httpRequest.socket.setTimeout(sessionContext.timeout * 1000);
if (sessionContext.debug) {
debug.incomingHttpRequest(
sessionContext.httpRequest,
@@ -1210,13 +1214,13 @@ export function listener(
stats.concurrentRequests -= 1;
})
.catch((err) => {
currentSessions.delete(httpResponse.connection);
currentSessions.delete(httpResponse.socket);
stats.concurrentRequests -= 1;
setTimeout(() => {
throw err;
});
try {
httpResponse.connection.unref();
httpResponse.socket.unref();
httpResponse.writeHead(500, { Connection: "close" });
httpResponse.end(`${err.name}: ${err.message}`);
} catch (err) {
@@ -1227,7 +1231,7 @@ export function listener(
function decodeString(buffer: Buffer, charset: string): string {
try {
return buffer.toString(charset);
return buffer.toString(charset as BufferEncoding);
} catch (err) {
if (encodingExists(charset)) return decode(buffer, charset);
}
@@ -1312,9 +1316,9 @@ async function listenerAsync(
// Request aborted
if (!body) return;
const newConnection = !currentSessions.has(httpRequest.connection);
const newConnection = !currentSessions.has(httpRequest.socket);
const sessionContext = await getSession(httpRequest.connection, sessionId);
const sessionContext = await getSession(httpRequest.socket, sessionId);
if (sessionContext) {
sessionContext.httpRequest = httpRequest;
@@ -1352,7 +1356,7 @@ async function listenerAsync(
return;
}
let charset;
let charset: string;
if (httpRequest.headers["content-type"]) {
const m = httpRequest.headers["content-type"].match(
/charset=['"]?([^'"\s]+)/i
@@ -1414,7 +1418,7 @@ async function listenerAsync(
}
const parseWarnings = [];
let rpc;
let rpc: SoapMessage;
try {
rpc = soap.request(
bodyStr,
@@ -1519,7 +1523,6 @@ async function listenerAsync(
_sessionContext.httpRequest = httpRequest;
_sessionContext.httpResponse = httpResponse;
_sessionContext.sessionId = crypto.randomBytes(8).toString("hex");
httpRequest.connection.setTimeout(_sessionContext.timeout * 1000);
const {
tasks: dueTasks,
+3 -4
View File
@@ -19,7 +19,7 @@
import { MongoClient, ObjectID, Collection } from "mongodb";
import { get } from "./config";
import { escapeRegExp } from "./common";
import { encodeTag, escapeRegExp } from "./common";
import { parse } from "./common/expression-parser";
import {
DeviceData,
@@ -234,10 +234,9 @@ export async function fetchDevice(
]);
}
for (let t of v as string[]) {
t = t.replace(/[^a-zA-Z0-9-]+/g, "_");
for (const t of v as string[]) {
res.push([
Path.parse("Tags." + t),
Path.parse("Tags." + encodeTag(t)),
timestamp,
{
object: [timestamp, 0],
+5 -5
View File
@@ -49,7 +49,7 @@ export function incomingHttpRequest(
): void {
if (!DEBUG_FILE) return;
const now = new Date();
const con = httpRequest.connection;
const con = httpRequest.socket;
const msg = {
event: "incoming HTTP request",
timestamp: now,
@@ -77,7 +77,7 @@ export function outgoingHttpResponse(
): void {
if (!DEBUG_FILE) return;
const now = new Date();
const con = httpResponse.connection;
const con = httpResponse.socket;
const msg = {
event: "outgoing HTTP response",
timestamp: now,
@@ -104,7 +104,7 @@ export function outgoingHttpRequest(
): void {
if (!DEBUG_FILE) return;
const now = new Date();
const con = httpRequest.connection;
const con = httpRequest.socket;
const msg = {
event: "outgoing HTTP request",
timestamp: now,
@@ -160,13 +160,13 @@ export function incomingHttpResponse(
): void {
if (!DEBUG_FILE) return;
const now = new Date();
const con = httpResponse.connection;
const con = httpResponse.socket;
const msg = {
event: "incoming HTTP response",
timestamp: now,
remoteAddress: con.remoteAddress,
deviceId: deviceId,
connection: getConnectionTimestamp(httpResponse.connection),
connection: getConnectionTimestamp(httpResponse.socket),
statusCode: httpResponse.statusCode,
headers: httpResponse.headers,
body: body,
+9 -5
View File
@@ -21,6 +21,7 @@ import { ObjectID } from "mongodb";
import { map, parse, stringify, parseList } from "./common/expression-parser";
import { likePatternToRegExp } from "./common/expression";
import { Expression, Fault, Task } from "./types";
import { decodeTag, encodeTag } from "./common";
const isArray = Array.isArray;
@@ -43,7 +44,7 @@ export function processDeviceFilter(filter: Expression): Expression {
exp[1][0] === "PARAM" &&
exp[1][1].startsWith("Tags.")
) {
const t = exp[1][1].slice(5);
const t = decodeTag(exp[1][1].slice(5));
if (exp[0] === "IS NULL") return ["_tags", t, false];
else if (exp[0] === "IS NOT NULL") return ["_tags", t, true];
else if (exp[0] === "=" && exp[2] === true) return ["_tags", t, true];
@@ -406,7 +407,7 @@ export function flattenDevice(device: Record<string, unknown>): FlatDevice {
};
for (const t of tree as string[]) {
output[`Tags.${t}`] = {
output[`Tags.${encodeTag(t)}`] = {
value: [true, "xsd:boolean"],
valueTimestamp: timestamp,
writable: true,
@@ -510,11 +511,11 @@ export function convertOldPrecondition(
if (op === "$ne") {
if (typeof v["$ne"] !== "string")
throw new Error("Only string values are allowed for _tags");
conjs.push(["IS NULL", ["PARAM", `Tags.${val}`]]);
conjs.push(["IS NULL", ["PARAM", `Tags.${encodeTag(val)}`]]);
} else if (op === "$eq") {
if (typeof v["$eq"] !== "string")
throw new Error("Only string values are allowed for _tags");
conjs.push(["IS NOT NULL", ["PARAM", `Tags.${val}`]]);
conjs.push(["IS NOT NULL", ["PARAM", `Tags.${encodeTag(val)}`]]);
} else {
throw new Error(`Invalid tag query`);
}
@@ -522,7 +523,10 @@ export function convertOldPrecondition(
if (conjs.length === 1) expressions.push(conjs[0]);
else if (conjs.length > 1) expressions.push(["AND", ...conjs]);
} else {
expressions.push(["IS NOT NULL", ["PARAM", `Tags.${v}`]]);
expressions.push([
"IS NOT NULL",
["PARAM", `Tags.${encodeTag(v as string)}`],
]);
}
} else if (k.startsWith("Tags.")) {
let exists: boolean;
+23 -15
View File
@@ -52,35 +52,43 @@ function closeServer(timeout, callback): void {
});
}
interface ServerOptions {
port?: number;
host?: string;
ssl?: { key: string; cert: string };
timeout?: number;
keepAliveTimeout?: number;
onConnection?: (socket: Socket) => void;
}
export function start(
port: number,
networkInterface: string,
ssl: { key: string; cert: string },
_listener: http.RequestListener,
onConnection?: (socket: Socket) => void,
keepAliveTimeout = -1
options: ServerOptions,
_listener: http.RequestListener
): void {
listener = _listener;
if (ssl && ssl.key && ssl.cert) {
const options = {
key: ssl.key
if (options.ssl) {
const opts = {
key: options.ssl.key
.split(":")
.map((f) => fs.readFileSync(path.resolve(ROOT_DIR, f.trim()))),
cert: ssl.cert
cert: options.ssl.cert
.split(":")
.map((f) => fs.readFileSync(path.resolve(ROOT_DIR, f.trim()))),
};
server = https.createServer(options, listener);
if (onConnection != null) server.on("secureConnection", onConnection);
server = https.createServer(opts, listener);
if (options.onConnection)
server.on("secureConnection", options.onConnection);
} else {
server = http.createServer(listener);
if (onConnection != null) server.on("connection", onConnection);
if (options.onConnection) server.on("connection", options.onConnection);
}
if (keepAliveTimeout >= 0) server.keepAliveTimeout = keepAliveTimeout;
server.listen(port, networkInterface);
server.timeout = options.timeout || 0;
if (options.keepAliveTimeout != null)
server.keepAliveTimeout = options.keepAliveTimeout;
server.listen({ port: options.port, host: options.host });
}
export function stop(): Promise<void> {
+19 -5
View File
@@ -1734,20 +1734,35 @@ function generateSetRpcRequest(
const deviceData = sessionContext.deviceData;
const SKIP_WRITABLE_CHECK = !!localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.skipWritableCheck",
{},
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e)
);
const canWrite = (attrs: Attributes): boolean =>
SKIP_WRITABLE_CHECK || (attrs.writable && !!attrs.writable[1]);
// Delete instance
for (const instances of syncState.instancesToDelete.values()) {
const instance = instances.values().next().value;
if (instance && sessionContext.deviceData.attributes.has(instance)) {
for (const instance of instances) {
const attrs = sessionContext.deviceData.attributes.get(instance);
if (attrs && canWrite(attrs)) {
instances.delete(instance);
return {
name: "DeleteObject",
objectName: instance.toString() + ".",
};
}
}
}
// Create instance
for (const [param, instances] of syncState.instancesToCreate) {
if (sessionContext.deviceData.attributes.has(param)) {
const attrs = sessionContext.deviceData.attributes.get(param);
if (attrs && canWrite(attrs)) {
const instance = instances.values().next().value;
if (instance) {
instances.delete(instance);
@@ -1791,7 +1806,7 @@ function generateSetRpcRequest(
syncState.spv.delete(k);
const attrs = sessionContext.deviceData.attributes.get(k);
const curVal = attrs.value ? attrs.value[1] : null;
if (curVal && attrs.writable && attrs.writable[1]) {
if (curVal && canWrite(attrs)) {
const val = v.slice() as [string | number | boolean, string];
if (!val[1]) val[1] = curVal[1];
device.sanitizeParameterValue(val);
@@ -2609,7 +2624,6 @@ export async function rpcResponse(
timestamp + 1,
{
object: [timestamp + 1, 0],
writable: [timestamp + 1, 1],
value: [
timestamp + 1,
p.slice(1) as [string | number | boolean, string],
+150 -14
View File
@@ -127,7 +127,8 @@ function parameterInfoList(xml: Element): [Path, boolean, boolean][] {
if (parsed == null) {
warnings.push({
message: "Invalid writable attribute",
message: "Missing or invalid XML node",
element: "Writable",
parameter: param,
});
parsed = false;
@@ -167,7 +168,8 @@ function parameterValueList(
let valueType = getValueType(valueElement.attrs);
if (!valueType) {
warnings.push({
message: "Invalid value type attribute",
message: "Missing or invalid XML node",
attribute: "type",
parameter: param,
});
valueType = "xsd:string";
@@ -179,7 +181,8 @@ function parameterValueList(
parsed = parseBool(value);
if (parsed == null) {
warnings.push({
message: "Invalid value attribute",
message: "Missing or invalid XML node",
element: "Value",
parameter: param,
});
parsed = value;
@@ -188,7 +191,8 @@ function parameterValueList(
parsed = parseInt(value);
if (isNaN(parsed)) {
warnings.push({
message: "Invalid value attribute",
message: "Missing or invalid XML node",
element: "Value",
parameter: param,
});
parsed = value;
@@ -197,7 +201,8 @@ function parameterValueList(
parsed = Date.parse(value);
if (isNaN(parsed)) {
warnings.push({
message: "Invalid value attribute",
message: "Missing or invalid XML node",
element: "Value",
parameter: param,
});
parsed = value;
@@ -232,7 +237,8 @@ function parameterAttributeList(xml: Element): [Path, number, string[]][] {
let notification = parseInt(notificationElement.text);
if (isNaN(notification)) {
warnings.push({
message: "Invalid notification attribute",
message: "Missing or invalid XML node",
element: "Notification",
parameter: param,
});
notification = 0;
@@ -331,7 +337,10 @@ function SetParameterValuesResponse(xml: Element): SetParameterValuesResponse {
}
if (!(status >= 0)) {
warnings.push({ message: "Invalid SetParameterValuesResponse status" });
warnings.push({
message: "Missing or invalid XML node",
element: "Status",
});
status = 0;
}
@@ -392,8 +401,14 @@ function AddObjectResponse(xml: Element): AddObjectResponse {
}
}
if (!/^[0-9]+$/.test(instanceNumber))
throw new Error("Missing or invalid instance number");
if (!(status >= 0)) {
warnings.push({ message: "Invalid AddObjectResponse status" });
warnings.push({
message: "Missing or invalid XML node",
element: "Status",
});
status = 0;
}
@@ -424,7 +439,10 @@ function DeleteObjectResponse(xml: Element): DeleteObjectResponse {
}
if (!(status >= 0)) {
warnings.push({ message: "Invalid DeleteObjectResponse status" });
warnings.push({
message: "Missing or invalid XML node",
element: "Status",
});
status = 0;
}
@@ -495,10 +513,29 @@ function DownloadResponse(xml: Element): DownloadResponse {
}
if (!(status >= 0)) {
warnings.push({ message: "Invalid DownloadResponse status" });
warnings.push({
message: "Missing or invalid XML node",
element: "Status",
});
status = 0;
}
if (startTime == null || isNaN(startTime)) {
warnings.push({
message: "Missing or invalid XML node",
element: "StartTime",
});
startTime = Date.parse("0001-01-01T00:00:00Z");
}
if (completeTime == null || isNaN(completeTime)) {
warnings.push({
message: "Missing or invalid XML node",
element: "CompleteTime",
});
completeTime = Date.parse("0001-01-01T00:00:00Z");
}
return {
name: "DownloadResponse",
status: status,
@@ -537,6 +574,30 @@ function Inform(xml: Element): InformRequest {
}
}
if (!deviceId || !deviceId.SerialNumber || !deviceId.OUI)
throw new Error("Missing or invalid DeviceId element");
if (!parameterList) {
warnings.push({
message: "Missing or invalid XML node",
element: "ParameterList",
});
parameterList = [];
}
if (!evnt) {
warnings.push({ message: "Missing or invalid XML node", element: "Event" });
evnt = [];
}
if (retryCount == null || isNaN(retryCount)) {
warnings.push({
message: "Missing or invalid XML node",
element: "RetryCount",
});
retryCount = 0;
}
return {
name: "Inform",
parameterList: parameterList,
@@ -563,7 +624,10 @@ function GetRPCMethodsResponse(methodResponse): string {
}
function TransferComplete(xml: Element): TransferCompleteRequest {
let commandKey, _faultStruct, startTime, completeTime;
let commandKey: string,
_faultStruct: FaultStruct,
startTime: number,
completeTime: number;
for (const c of xml.children) {
switch (c.localName) {
case "CommandKey":
@@ -581,6 +645,38 @@ function TransferComplete(xml: Element): TransferCompleteRequest {
}
}
if (commandKey == null) {
warnings.push({
message: "Missing or invalid XML node",
element: "CommandKey",
});
commandKey = "";
}
if (!_faultStruct) {
warnings.push({
message: "Missing or invalid XML node",
element: "FaultStruct",
});
_faultStruct = { faultCode: "0", faultString: "" };
}
if (startTime == null || isNaN(startTime)) {
warnings.push({
message: "Missing or invalid XML node",
element: "StartTime",
});
startTime = Date.parse("0001-01-01T00:00:00Z");
}
if (completeTime == null || isNaN(completeTime)) {
warnings.push({
message: "Missing or invalid XML node",
element: "CompleteTime",
});
completeTime = Date.parse("0001-01-01T00:00:00Z");
}
return {
name: "TransferComplete",
commandKey: commandKey,
@@ -606,7 +702,12 @@ function RequestDownloadResponse(): string {
}
function faultStruct(xml: Element): FaultStruct {
let faultCode, faultString, setParameterValuesFault: SpvFault[], pn, fc, fs;
let faultCode: string,
faultString: string,
setParameterValuesFault: SpvFault[],
pn: string,
fc: string,
fs: string;
for (const c of xml.children) {
switch (c.localName) {
case "FaultCode":
@@ -617,6 +718,7 @@ function faultStruct(xml: Element): FaultStruct {
break;
case "SetParameterValuesFault":
setParameterValuesFault = setParameterValuesFault || [];
pn = fc = fs = null;
for (const cc of c.children) {
switch (cc.localName) {
case "ParameterName":
@@ -638,11 +740,27 @@ function faultStruct(xml: Element): FaultStruct {
}
}
if (faultCode == null) {
warnings.push({
message: "Missing or invalid XML node",
element: "FaultCode",
});
faultCode = "";
}
if (faultString == null) {
warnings.push({
message: "Missing or invalid XML node",
element: "FaultString",
});
faultString = "";
}
return { faultCode, faultString, setParameterValuesFault };
}
function fault(xml: Element): CpeFault {
let faultCode, faultString, detail;
let faultCode: string, faultString: string, detail: FaultStruct;
for (const c of xml.children) {
switch (c.localName) {
case "faultcode":
@@ -657,7 +775,25 @@ function fault(xml: Element): CpeFault {
}
}
return { faultCode, faultString, detail };
if (!detail) throw new Error("Missing detail element");
if (faultCode == null) {
warnings.push({
message: "Missing or invalid XML node",
element: "faultcode",
});
faultCode = "Client";
}
if (faultString == null) {
warnings.push({
message: "Missing or invalid XML node",
element: "faultstring",
});
faultString = "CWMP fault";
}
return { faultCode, faultString, detail } as CpeFault;
}
export function request(
+2 -2
View File
@@ -289,8 +289,8 @@ export interface FaultStruct {
}
export interface CpeFault {
faultCode: string;
faultString: string;
faultCode: "Client" | "Server";
faultString: "CWMP fault";
detail?: FaultStruct;
}
+3 -3
View File
@@ -17,7 +17,7 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import { Z_SYNC_FLUSH } from "zlib";
import { constants } from "zlib";
import Koa from "koa";
import Router from "koa-router";
import * as jwt from "jsonwebtoken";
@@ -254,10 +254,10 @@ router.get("/", async (ctx) => {
koa.use(
koaCompress({
gzip: {
flush: Z_SYNC_FLUSH,
flush: constants.Z_SYNC_FLUSH,
},
deflate: {
flush: Z_SYNC_FLUSH,
flush: constants.Z_SYNC_FLUSH,
},
})
);
+31 -30
View File
@@ -31,6 +31,7 @@ import { del } from "../cache";
import Authorizer from "../common/authorizer";
import { ping } from "../ping";
import * as url from "url";
import { decodeTag } from "../common";
const router = new Router();
export default router;
@@ -85,7 +86,7 @@ router.get(`/devices/:id.csv`, async (ctx) => {
if (!authorizer.hasAccess("devices", 2)) {
logUnauthorizedWarning(log);
return void (ctx.status = 404);
return void (ctx.status = 403);
}
const res = await db.query("devices", filter);
@@ -126,7 +127,7 @@ router.get(`/devices/:id.csv`, async (ctx) => {
});
for (const [resource, flags] of Object.entries(resources)) {
router.head(`/${resource}`, async (ctx, next) => {
router.head(`/${resource}`, async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
let filter: Expression = authorizer.getFilter(resource, 1);
if (ctx.request.query.filter)
@@ -141,7 +142,7 @@ for (const [resource, flags] of Object.entries(resources)) {
if (!authorizer.hasAccess(resource, 1)) {
logUnauthorizedWarning(log);
return void next();
return void (ctx.status = 403);
}
// Exclude temporary tasks and faults
@@ -162,7 +163,7 @@ for (const [resource, flags] of Object.entries(resources)) {
logger.accessInfo(log);
});
router.get(`/${resource}`, async (ctx, next) => {
router.get(`/${resource}`, async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const options: QueryOptions = {};
let filter: Expression = authorizer.getFilter(resource, 2);
@@ -190,7 +191,7 @@ for (const [resource, flags] of Object.entries(resources)) {
if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log);
return void next();
return void (ctx.status = 403);
}
// Exclude temporary tasks and faults
@@ -216,7 +217,7 @@ for (const [resource, flags] of Object.entries(resources)) {
});
// CSV download
router.get(`/${resource}.csv`, async (ctx, next) => {
router.get(`/${resource}.csv`, async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const options: QueryOptions = { projection: {} };
let filter: Expression = authorizer.getFilter(resource, 2);
@@ -238,7 +239,7 @@ for (const [resource, flags] of Object.entries(resources)) {
if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log);
return void next();
return void (ctx.status = 403);
}
const columns: Record<string, Expression> = JSON.parse(
@@ -280,7 +281,7 @@ for (const [resource, flags] of Object.entries(resources)) {
if (e[1] === "Tags") {
const tags = [];
for (const p in obj)
if (p.startsWith("Tags.")) tags.push(p.slice(5));
if (p.startsWith("Tags.")) tags.push(decodeTag(p.slice(5)));
return tags.join(", ");
}
@@ -307,7 +308,7 @@ for (const [resource, flags] of Object.entries(resources)) {
logger.accessInfo(log);
});
router.head(`/${resource}/:id`, async (ctx, next) => {
router.head(`/${resource}/:id`, async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const log = {
message: `Count ${resource}`,
@@ -322,18 +323,18 @@ for (const [resource, flags] of Object.entries(resources)) {
]);
if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log);
return void next();
return void (ctx.status = 403);
}
const res = await db.query(resource, filter);
if (!res.length) return void next();
if (!res.length) return void (ctx.status = 404);
logger.accessInfo(log);
ctx.body = "";
});
router.get(`/${resource}/:id`, async (ctx, next) => {
router.get(`/${resource}/:id`, async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const log = {
message: `Query ${resource}`,
@@ -348,19 +349,19 @@ for (const [resource, flags] of Object.entries(resources)) {
]);
if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log);
return void next();
return void (ctx.status = 403);
}
const res = await db.query(resource, filter);
if (!res.length) return void next();
if (!res.length) return void (ctx.status = 404);
logger.accessInfo(log);
ctx.body = res[0];
});
if (flags & RESOURCE_DELETE) {
router.delete(`/${resource}/:id`, async (ctx, next) => {
router.delete(`/${resource}/:id`, async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const log = {
message: `Delete ${resource}`,
@@ -375,10 +376,10 @@ for (const [resource, flags] of Object.entries(resources)) {
]);
if (!authorizer.hasAccess(resource, 3)) {
logUnauthorizedWarning(log);
return void next();
return void (ctx.status = 403);
}
const res = await db.query(resource, filter);
if (!res.length) return void next();
if (!res.length) return void (ctx.status = 404);
const validate = authorizer.getValidator(resource, res[0]);
if (!validate("delete")) {
@@ -395,7 +396,7 @@ for (const [resource, flags] of Object.entries(resources)) {
}
if (flags & RESOURCE_PUT) {
router.put(`/${resource}/:id`, async (ctx, next) => {
router.put(`/${resource}/:id`, async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const id = ctx.params.id;
@@ -407,7 +408,7 @@ for (const [resource, flags] of Object.entries(resources)) {
if (!authorizer.hasAccess(resource, 3)) {
logUnauthorizedWarning(log);
return void next();
return void (ctx.status = 403);
}
const obj = ctx.request.body;
@@ -434,7 +435,7 @@ for (const [resource, flags] of Object.entries(resources)) {
}
}
router.put("/files/:id", async (ctx, next) => {
router.put("/files/:id", async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const resource = "files";
const id = ctx.params.id;
@@ -448,7 +449,7 @@ router.put("/files/:id", async (ctx, next) => {
if (!authorizer.hasAccess(resource, 3)) {
logUnauthorizedWarning(log);
return void next();
return void (ctx.status = 403);
}
const metadata = {
@@ -477,7 +478,7 @@ router.put("/files/:id", async (ctx, next) => {
ctx.body = "";
});
router.post("/devices/:id/tasks", async (ctx, next) => {
router.post("/devices/:id/tasks", async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const log = {
message: "Commit tasks",
@@ -493,10 +494,10 @@ router.post("/devices/:id/tasks", async (ctx, next) => {
]);
if (!authorizer.hasAccess("devices", 3)) {
logUnauthorizedWarning(log);
return void next();
return void (ctx.status = 403);
}
const devices = await db.query("devices", filter);
if (!devices.length) return void next();
if (!devices.length) return void (ctx.status = 404);
const device = devices[0];
const validate = authorizer.getValidator("devices", device);
@@ -545,7 +546,7 @@ router.post("/devices/:id/tasks", async (ctx, next) => {
ctx.body = res.tasks;
});
router.post("/devices/:id/tags", async (ctx, next) => {
router.post("/devices/:id/tags", async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const log = {
message: "Update tags",
@@ -561,10 +562,10 @@ router.post("/devices/:id/tags", async (ctx, next) => {
]);
if (!authorizer.hasAccess("devices", 3)) {
logUnauthorizedWarning(log);
return void next();
return void (ctx.status = 403);
}
const res = await db.query("devices", filter);
if (!res.length) return void next();
if (!res.length) return void (ctx.status = 404);
const validate = authorizer.getValidator("devices", res[0]);
if (!validate("tags", ctx.request.body)) {
@@ -600,7 +601,7 @@ router.get("/ping/:host", async (ctx) => {
});
});
router.put("/users/:id/password", async (ctx, next) => {
router.put("/users/:id/password", async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const username = ctx.params.id;
const log = {
@@ -625,7 +626,7 @@ router.put("/users/:id/password", async (ctx, next) => {
}
} else if (!authorizer.hasAccess("users", 3)) {
logUnauthorizedWarning(log);
return void next();
return void (ctx.status = 403);
}
const filter = and(authorizer.getFilter("users", 3), [
@@ -634,7 +635,7 @@ router.put("/users/:id/password", async (ctx, next) => {
username,
]);
const res = await db.query("users", filter);
if (!res.length) return void next();
if (!res.length) return void (ctx.status = 404);
const newPassword = ctx.request.body.newPassword;
if (ctx.state.user) {
+8 -20
View File
@@ -237,39 +237,27 @@ export function count(resource: string, filter: Expression): Promise<number> {
});
}
export function updateDeviceTags(
export async function updateDeviceTags(
deviceId: string,
tags: Record<string, boolean>
): Promise<void> {
return new Promise((resolve, reject) => {
const add = [];
const pull = [];
const regex = /^[0-9a-zA-Z_]+$/;
for (let [tag, onOff] of Object.entries(tags)) {
tag = tag.trim();
if (onOff) {
if (!tag.match(regex))
return void reject(new Error(`Invalid tag '${tag}'`));
add.push(tag);
} else {
pull.push(tag);
if (onOff) add.push(tag);
else pull.push(tag);
}
}
getClient()
.then((client) => {
const collection = client.db().collection("devices");
const client = await getClient();
const collection = client.db().collection("devices");
const object = {};
if (add && add.length) object["$addToSet"] = { _tags: { $each: add } };
if (pull && pull.length) object["$pullAll"] = { _tags: pull };
collection.updateOne({ _id: deviceId }, object, (err) => {
if (err) return void reject(err);
resolve();
});
})
.catch(reject);
});
await collection.updateOne({ _id: deviceId }, object);
}
function putResource(resource, id, object): Promise<void> {
+2 -1
View File
@@ -53,7 +53,8 @@ export interface Element {
}
export function parseXmlDeclaration(buffer: Buffer): Attribute[] {
for (const enc of ["utf16le", "utf8", "latin1", "ascii"]) {
const encodings: BufferEncoding[] = ["utf16le", "utf8", "latin1", "ascii"];
for (const enc of encodings) {
let str = buffer.toString(enc, 0, 150);
if (str.startsWith("<?xml")) {
str = str.split("\n")[0].trim();
+295 -2884
View File
File diff suppressed because it is too large. Load diff
+18 -17
View File
@@ -1,6 +1,6 @@
{
"name": "genieacs",
"version": "1.2.2",
"version": "1.2.3",
"description": "A TR-069 Auto Configuration Server (ACS)",
"repository": {
"type": "git",
@@ -37,41 +37,42 @@
"yaml": "^1.10.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^15.1.0",
"@rollup/plugin-commonjs": "^16.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^9.0.0",
"@rollup/plugin-node-resolve": "^10.0.0",
"@rollup/plugin-typescript": "^6.0.0",
"@types/jsonwebtoken": "^8.5.0",
"@types/koa": "^2.11.4",
"@types/koa": "^2.11.6",
"@types/koa-compress": "^4.0.1",
"@types/koa-router": "^7.4.1",
"@types/koa-static": "^4.0.1",
"@types/mithril": "^2.0.4",
"@types/mongodb": "^3.5.27",
"@types/mithril": "^2.0.5",
"@types/mongodb": "^3.5.31",
"@types/node": "^14.14.5",
"@types/parsimmon": "^1.10.3",
"@types/seedrandom": "^2.4.28",
"@types/svgo": "^1.3.3",
"@typescript-eslint/eslint-plugin": "^4.3.0",
"@typescript-eslint/parser": "^4.3.0",
"@typescript-eslint/eslint-plugin": "^4.6.0",
"@typescript-eslint/parser": "^4.6.0",
"ava": "^3.13.0",
"codemirror": "^5.58.1",
"codemirror": "^5.58.2",
"cssnano": "^4.1.10",
"eslint": "^7.10.0",
"eslint-config-prettier": "^6.12.0",
"eslint": "^7.12.1",
"eslint-config-prettier": "^6.14.0",
"esm": "^3.2.25",
"jsbi": "^3.1.4",
"mithril": "^2.0.4",
"postcss": "^8.1.1",
"postcss-import": "^12.0.1",
"postcss": "^8.1.4",
"postcss-import": "^13.0.0",
"postcss-preset-env": "^6.7.0",
"prettier": "^2.1.2",
"rollup": "^2.28.2",
"rollup": "^2.32.1",
"rollup-plugin-terser": "^7.0.2",
"source-map-loader": "^1.1.0",
"source-map-loader": "^1.1.1",
"svgo": "^1.3.2",
"ts-node": "^9.0.0",
"typescript": "^4.0.3",
"webpack": "^4.44.2"
"typescript": "^4.0.5",
"webpack": "^5.2.0"
},
"engines": {
"node": ">=12.13.0"
+2 -1
View File
@@ -22,6 +22,7 @@ import { m } from "../components";
import * as notifications from "../notifications";
import * as store from "../store";
import { getIcon } from "../icons";
import { decodeTag } from "../../lib/common";
const component: ClosureComponent = (): Component => {
return {
@@ -32,7 +33,7 @@ const component: ClosureComponent = (): Component => {
const tags = [];
for (const p of Object.keys(device))
if (p.startsWith("Tags.")) tags.push(p.slice(5));
if (p.startsWith("Tags.")) tags.push(decodeTag(p.slice(5)));
tags.sort();
+27 -12
View File
@@ -147,8 +147,8 @@ function renderTable(confsResponse, searchString): Children {
{
title: "Edit config value",
onclick: () => {
const cb = (): Children => {
return m(
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
@@ -176,8 +176,13 @@ function renderTable(confsResponse, searchString): Children {
formData
)
);
};
overlay.open(cb);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
getIcon("edit")
@@ -244,8 +249,8 @@ export const component: ClosureComponent = (): Component => {
{
title: "Create new config",
onclick: () => {
const cb = (): Children => {
return m(
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
@@ -274,8 +279,13 @@ export const component: ClosureComponent = (): Component => {
formData
)
);
};
overlay.open(cb);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
"New config"
@@ -307,8 +317,8 @@ export const component: ClosureComponent = (): Component => {
"button",
{
onclick: () => {
const cb = (): Children => {
return m(
let cb: () => Children = null;
const comp = m(
uiConfigComponent,
Object.assign(
{
@@ -338,8 +348,13 @@ export const component: ClosureComponent = (): Component => {
attrs
)
);
};
overlay.open(cb);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
`Edit ${sub.name}`
+4 -4
View File
@@ -49,10 +49,10 @@ function mval(val): Children {
}
function renderStagingSpv(task: StageTask, queueFunc, cancelFunc): Children {
function keydown(e): void {
if (e.keyCode === 13) queueFunc();
else if (e.keyCode === 27) cancelFunc();
else e.redraw = false;
function keydown(e: KeyboardEvent): void {
if (e.key === "Enter") queueFunc();
else if (e.key === "Escape") cancelFunc();
else e["redraw"] = false;
}
let input;
+1 -1
View File
@@ -51,7 +51,7 @@ export function loadCodeMirror(): Promise<void> {
];
Promise.all(promises)
.then((modules) => {
codeMirror = modules[0];
codeMirror = modules[0].default;
resolve();
})
.catch((err) => {
+9 -4
View File
@@ -221,8 +221,8 @@ export const component: ClosureComponent = (): Component => {
{
title: "Create new file",
onclick: () => {
const cb = (): Children => {
return m(
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
@@ -251,8 +251,13 @@ export const component: ClosureComponent = (): Component => {
formData
)
);
};
overlay.open(cb);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
"New"
+14 -5
View File
@@ -20,16 +20,24 @@
import m, { Children } from "mithril";
type OverlayCallback = () => Children;
type CloseCallback = () => boolean;
let overlayCallback: OverlayCallback = null;
let closeCallback: CloseCallback = null;
export function open(callback: OverlayCallback): void {
export function open(
callback: OverlayCallback,
closeCb: CloseCallback = null
): void {
overlayCallback = callback;
closeCallback = closeCb;
}
export function close(callback: OverlayCallback): boolean {
export function close(callback: OverlayCallback, force = true): boolean {
if (callback === overlayCallback) {
if (!force && closeCallback && !closeCallback()) return false;
overlayCallback = null;
closeCallback = null;
return true;
}
@@ -43,7 +51,7 @@ export function render(): Children {
{
tabindex: 0,
onclick: () => {
close(overlayCallback);
close(overlayCallback, false);
},
style: "opacity: 0",
oncreate: (vnode) => {
@@ -75,9 +83,10 @@ export function render(): Children {
}
document.addEventListener("keydown", (e) => {
if (overlayCallback && e.keyCode === 27 && close(overlayCallback)) m.redraw();
if (overlayCallback && e.key === "Escape" && close(overlayCallback, false))
m.redraw();
});
window.addEventListener("popstate", () => {
if (close(overlayCallback)) m.redraw();
if (close(overlayCallback, false)) m.redraw();
});
+9 -4
View File
@@ -285,8 +285,8 @@ export const component: ClosureComponent = (): Component => {
{
title: "Create new permission",
onclick: () => {
const cb = (): Children => {
return m(
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
@@ -315,8 +315,13 @@ export const component: ClosureComponent = (): Component => {
formData
)
);
};
overlay.open(cb);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
"New"
+26 -15
View File
@@ -287,16 +287,8 @@ export const component: ClosureComponent = (): Component => {
"a",
{
onclick: () => {
const cb = (): Children => {
if (!preset.provision) {
return m(
"div",
{ style: "margin:20px" },
"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 m(
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
@@ -326,8 +318,22 @@ export const component: ClosureComponent = (): Component => {
formData
)
);
cb = (): Children => {
if (!preset.provision) {
return m(
"div",
{ style: "margin:20px" },
"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);
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
"Show"
@@ -343,8 +349,8 @@ export const component: ClosureComponent = (): Component => {
{
title: "Create new preset",
onclick: () => {
const cb = (): Children => {
return m(
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
@@ -373,8 +379,13 @@ export const component: ClosureComponent = (): Component => {
formData
)
);
};
overlay.open(cb);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
"New"
+18 -8
View File
@@ -207,8 +207,8 @@ export const component: ClosureComponent = (): Component => {
"a",
{
onclick: () => {
const cb = (): Children => {
return m(
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
@@ -238,8 +238,13 @@ export const component: ClosureComponent = (): Component => {
formData
)
);
};
overlay.open(cb);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
"Show"
@@ -255,8 +260,8 @@ export const component: ClosureComponent = (): Component => {
{
title: "Create new provision",
onclick: () => {
const cb = (): Children => {
return m(
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
@@ -285,8 +290,13 @@ export const component: ClosureComponent = (): Component => {
formData
)
);
};
overlay.open(cb);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
"New"
+7
View File
@@ -56,6 +56,7 @@ function createField(current, attr, focus): Children {
: null,
onchange: (e) => {
current.object[attr.id] = e.target.value;
current.modified = true;
e.redraw = false;
},
},
@@ -80,6 +81,7 @@ function createField(current, attr, focus): Children {
if (e.target.checked) currentSelected.add(op);
else currentSelected.delete(op);
current.object[attr.id] = Array.from(currentSelected);
current.modified = true;
e.redraw = false;
},
};
@@ -98,6 +100,7 @@ function createField(current, attr, focus): Children {
},
onChange: (value) => {
current.object[attr.id] = value;
current.modified = true;
},
};
return m(codeEditorComponent, attrs);
@@ -112,6 +115,7 @@ function createField(current, attr, focus): Children {
: null,
onchange: (e) => {
current.object[attr.id] = e.target.files;
current.modified = true;
e.redraw = false;
},
});
@@ -132,6 +136,7 @@ function createField(current, attr, focus): Children {
: null,
oninput: (e) => {
current.object[attr.id] = e.target.value;
current.modified = true;
e.redraw = false;
},
onkeypress: (e) => {
@@ -158,6 +163,7 @@ function createField(current, attr, focus): Children {
: null,
oninput: (e) => {
current.object[attr.id] = e.target.value;
current.modified = true;
e.redraw = false;
},
});
@@ -174,6 +180,7 @@ const component: ClosureComponent = (): Component => {
vnode.state["current"] = {
isNew: !base["_id"],
object: Object.assign({}, base),
modified: false,
};
}
+2 -1
View File
@@ -19,6 +19,7 @@
import config from "./config";
import { Expression } from "../lib/types";
import { encodeTag } from "../lib/common";
const resources = {
devices: {},
@@ -138,7 +139,7 @@ function queryMac(param, value): Expression {
}
function queryTag(tag: string): Expression {
const t = tag.replace(/[^a-zA-Z0-9-]+/g, "_");
const t = encodeTag(tag);
return ["IS NOT NULL", ["PARAM", `Tags.${t}`]];
}
+8 -3
View File
@@ -173,10 +173,11 @@ export async function xhrRequest(
// https://mithril.js.org/request.html#error-handling
if (xhr.status !== 304 && Math.floor(xhr.status / 100) !== 2) {
if (xhr.status === 403) throw new Error("Not authorized");
const err = new Error();
err["message"] =
xhr.status === 0
? "Server is unrachable"
? "Server is unreachable"
: `Unexpected response status code ${xhr.status}`;
err["code"] = xhr.status;
err["response"] = xhr.responseText;
@@ -385,8 +386,9 @@ function _fulfill(
filter: memoizedStringify(filter),
}),
extract: (xhr) => {
if (xhr.status === 403) throw new Error("Not authorized");
if (!xhr.status) {
throw new Error("Server is unrachable");
throw new Error("Server is unreachable");
} else if (xhr.status !== 200) {
throw new Error(
`Unexpected response status code ${xhr.status}`
@@ -605,6 +607,8 @@ export function postTasks(
url: `api/devices/${encodeURIComponent(deviceId)}/tasks`,
body: tasks,
extract: (xhr) => {
if (xhr.status === 403) throw new Error("Not authorized");
if (!xhr.status) throw new Error("Server is unreachable");
if (xhr.status !== 200) throw new Error(xhr.response);
const connectionRequestStatus = xhr.getResponseHeader(
"Connection-Request"
@@ -675,7 +679,8 @@ export function resourceExists(resource: string, id: string): Promise<number> {
filter: memoizedStringify(filter),
}),
extract: (xhr) => {
if (!xhr.status) throw new Error("Server is unrachable");
if (xhr.status === 403) throw new Error("Not authorized");
if (!xhr.status) throw new Error("Server is unreachable");
else if (xhr.status !== 200)
throw new Error(`Unexpected response status code ${xhr.status}`);
return +xhr.getResponseHeader("x-total-count");
+2 -6
View File
@@ -102,12 +102,7 @@ const component: ClosureComponent = (): Component => {
const yamlString =
config && Object.values(config).length
? yaml.stringify(config, {
schema: "failsafe",
tags: (args) => {
return args;
},
})
? yaml.stringify(config, { schema: "failsafe" })
: "";
const attrs = {
@@ -120,6 +115,7 @@ const component: ClosureComponent = (): Component => {
},
onChange: (value) => {
vnode.state["updatedYaml"] = value;
vnode.state["modified"] = true;
},
};
+21 -10
View File
@@ -237,9 +237,8 @@ export const component: ClosureComponent = (): Component => {
"a",
{
onclick: () => {
const cb = (): Children => {
const children = [
m(
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
@@ -274,9 +273,10 @@ export const component: ClosureComponent = (): Component => {
attributes: attributes,
}
)
),
];
);
cb = () => {
const children: Children = [comp];
if (canWrite) {
children.push(m("hr"));
const _attrs = {
@@ -292,7 +292,13 @@ export const component: ClosureComponent = (): Component => {
return children;
};
overlay.open(cb);
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
"Show"
@@ -317,8 +323,8 @@ export const component: ClosureComponent = (): Component => {
{
title: "Create new user",
onclick: () => {
const cb = (): Children => {
return m(
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
@@ -347,8 +353,13 @@ export const component: ClosureComponent = (): Component => {
formData
)
);
};
overlay.open(cb);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
"New"
+18 -8
View File
@@ -207,8 +207,8 @@ export const component: ClosureComponent = (): Component => {
"a",
{
onclick: () => {
const cb = (): Children => {
return m(
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
@@ -238,8 +238,13 @@ export const component: ClosureComponent = (): Component => {
formData
)
);
};
overlay.open(cb);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
"Show"
@@ -255,8 +260,8 @@ export const component: ClosureComponent = (): Component => {
{
title: "Create new virtual parameter",
onclick: () => {
const cb = (): Children => {
return m(
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
@@ -285,8 +290,13 @@ export const component: ClosureComponent = (): Component => {
formData
)
);
};
overlay.open(cb);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
"New"