Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84de431872 | ||
|
|
716f2c6df3 | ||
|
|
6ce91df127 | ||
|
|
1a53a320da | ||
|
|
cfe279b511 | ||
|
|
08cfe4171c | ||
|
|
6c37e88ab1 | ||
|
|
8b27794cc7 | ||
|
|
9cf18503ee | ||
|
|
3b36cefbca | ||
|
|
02e09dfdab | ||
|
|
63a75cc765 | ||
|
|
630697a0cb | ||
|
|
f6aa444a23 | ||
|
|
139dc5410e |
No files matched your search
+18
-12
@@ -145,15 +145,24 @@ export async function connectionRequest(
|
||||
|
||||
const debug = !!getConfig(snapshot, "cwmp.debug", {}, now, evalCallback);
|
||||
|
||||
let udpProm;
|
||||
let udpProm = Promise.resolve(false);
|
||||
if (udpConnectionRequestAddress) {
|
||||
udpProm = udpConnectionRequest(
|
||||
udpConnectionRequestAddress,
|
||||
authExp,
|
||||
UDP_CONNECTION_REQUEST_PORT,
|
||||
debug,
|
||||
deviceId
|
||||
);
|
||||
try {
|
||||
const u = new URL("udp://" + udpConnectionRequestAddress);
|
||||
udpProm = udpConnectionRequest(
|
||||
u.hostname,
|
||||
parseInt(u.port || "80"),
|
||||
authExp,
|
||||
UDP_CONNECTION_REQUEST_PORT,
|
||||
debug,
|
||||
deviceId
|
||||
).then(
|
||||
() => true,
|
||||
() => false
|
||||
);
|
||||
} catch (err) {
|
||||
// Ignore invalid address
|
||||
}
|
||||
}
|
||||
|
||||
const status = await httpConnectionRequest(
|
||||
@@ -165,10 +174,7 @@ export async function connectionRequest(
|
||||
deviceId
|
||||
);
|
||||
|
||||
if (udpProm) {
|
||||
await udpProm;
|
||||
return "";
|
||||
}
|
||||
if (await udpProm) return "";
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ export function escapeRegExp(str: string): string {
|
||||
export function encodeTag(tag: string): string {
|
||||
return encodeURIComponent(tag)
|
||||
.replace(
|
||||
/[!~*'()]/g,
|
||||
/[!~*'().]/g,
|
||||
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()
|
||||
)
|
||||
.replace(/0x(?=[0-9A-Z]{2})/g, "0%78")
|
||||
|
||||
+104
-49
@@ -23,6 +23,31 @@ type Alias = [Path, string][];
|
||||
let cache1 = new Map<string, Path>();
|
||||
let cache2 = new Map<string, Path>();
|
||||
|
||||
const CHAR_SPACE = 32;
|
||||
const CHAR_BACKSLASH = 92;
|
||||
const CHAR_COLON = 58;
|
||||
const CHAR_QUOTE = 34;
|
||||
const CHAR_OPEN_BRACKET = 91;
|
||||
const CHAR_CLOSE_BRACKET = 93;
|
||||
const CHAR_ASTERISK = 42;
|
||||
const CHAR_COMMA = 44;
|
||||
const CHAR_DOT = 46;
|
||||
|
||||
function charCodeAt(str: string, index: number): number {
|
||||
if (index >= str.length) return 0;
|
||||
return str.charCodeAt(index);
|
||||
}
|
||||
|
||||
function legalChar(c: number): boolean {
|
||||
return (
|
||||
(c >= 97 && c <= 122) ||
|
||||
(c >= 65 && c <= 90) ||
|
||||
(c >= 48 && c <= 57) ||
|
||||
c === 95 ||
|
||||
c === 45
|
||||
);
|
||||
}
|
||||
|
||||
export default class Path {
|
||||
public readonly segments: Segments;
|
||||
public readonly wildcard: number;
|
||||
@@ -30,37 +55,70 @@ export default class Path {
|
||||
protected _string: string;
|
||||
protected _stringIndex: number[];
|
||||
|
||||
protected static parseAliasValue(
|
||||
pattern: string,
|
||||
index: number
|
||||
): { index: number; value: string } {
|
||||
let i = index;
|
||||
while (charCodeAt(pattern, i) === CHAR_SPACE) ++i;
|
||||
if (charCodeAt(pattern, i) === CHAR_QUOTE) {
|
||||
for (let j = i + 1; j < pattern.length; ++j) {
|
||||
if (
|
||||
pattern.charCodeAt(j) === CHAR_QUOTE &&
|
||||
pattern.charCodeAt(j - 1) !== CHAR_BACKSLASH
|
||||
) {
|
||||
try {
|
||||
++j;
|
||||
const v = JSON.parse(pattern.slice(i, j));
|
||||
return { index: j, value: v };
|
||||
} catch {
|
||||
return { index, value: null };
|
||||
}
|
||||
}
|
||||
}
|
||||
return { index, value: null };
|
||||
}
|
||||
|
||||
for (; i < pattern.length; ++i) {
|
||||
const c = pattern.charCodeAt(i);
|
||||
if (c === CHAR_CLOSE_BRACKET || c === CHAR_COMMA) break;
|
||||
}
|
||||
|
||||
return { index: i, value: pattern.slice(index, i).trim() };
|
||||
}
|
||||
|
||||
protected static parseAliasPath(
|
||||
pattern: string,
|
||||
index: number
|
||||
): { index: number; path: Path } {
|
||||
let i = index;
|
||||
while (charCodeAt(pattern, i) === CHAR_SPACE) ++i;
|
||||
const { index: idx, segments } = Path.parsePath(pattern, i);
|
||||
if (idx === i) return { index, path: null };
|
||||
i = idx;
|
||||
while (charCodeAt(pattern, i) === CHAR_SPACE) ++i;
|
||||
if (charCodeAt(pattern, i) !== CHAR_COLON) return { index, path: null };
|
||||
return { index: i + 1, path: new Path(segments) };
|
||||
}
|
||||
|
||||
protected static parseAlias(
|
||||
pattern: string,
|
||||
index: number
|
||||
): { index: number; alias: Alias } {
|
||||
const alias: Alias = [];
|
||||
while (index < pattern.length && pattern[index] !== "]") {
|
||||
const { index: idx, segments } = Path.parsePath(pattern, index);
|
||||
let j = (index = idx + 1);
|
||||
while (pattern[j] !== "]" && pattern[j] !== ",") {
|
||||
if (pattern[j] === '"' && index === j) {
|
||||
++j;
|
||||
while (pattern[j] !== '"' || pattern[j - 1] === "\\") {
|
||||
if (++j >= pattern.length)
|
||||
throw new Error("Invalid alias expression");
|
||||
}
|
||||
}
|
||||
if (++j >= pattern.length) throw new Error("Invalid alias expression");
|
||||
}
|
||||
let i = index;
|
||||
for (;;) {
|
||||
let path: Path;
|
||||
let value: string;
|
||||
({ index: i, path } = Path.parseAliasPath(pattern, i));
|
||||
if (!path) break;
|
||||
({ index: i, value } = Path.parseAliasValue(pattern, i));
|
||||
if (value == null) break;
|
||||
alias.push([path, value]);
|
||||
index = i;
|
||||
|
||||
let value = pattern.slice(index, j).trim();
|
||||
index = j;
|
||||
if (value[0] === '"') {
|
||||
try {
|
||||
value = JSON.parse(value);
|
||||
} catch (error) {
|
||||
throw new Error("Invalid alias expression");
|
||||
}
|
||||
}
|
||||
|
||||
alias.push([new Path(segments), value]);
|
||||
if (pattern[index] === ",") ++index;
|
||||
if (charCodeAt(pattern, index) !== CHAR_COMMA) break;
|
||||
++i;
|
||||
}
|
||||
|
||||
// Ensure identical expressions have idential string representation
|
||||
@@ -81,30 +139,26 @@ export default class Path {
|
||||
index: number
|
||||
): { index: number; segments: Segments } {
|
||||
const segments = [];
|
||||
// Colon separator is needed for parseAlias
|
||||
if (index < pattern.length && pattern[index] !== ":") {
|
||||
for (;;) {
|
||||
if (pattern[index] === "[") {
|
||||
const { index: idx, alias } = Path.parseAlias(pattern, index + 1);
|
||||
index = idx + 1;
|
||||
segments.push(alias);
|
||||
} else {
|
||||
const j = index;
|
||||
while (
|
||||
index < pattern.length &&
|
||||
pattern[index] !== ":" &&
|
||||
pattern[index] !== "."
|
||||
)
|
||||
++index;
|
||||
const s = pattern.slice(j, index).trim();
|
||||
if (!s) throw new Error("Invalid parameter path");
|
||||
segments.push(s);
|
||||
}
|
||||
|
||||
if (index >= pattern.length || pattern[index] === ":") break;
|
||||
else if (pattern[index] !== ".")
|
||||
throw new Error("Invalid alias expression");
|
||||
++index;
|
||||
let segStart = index;
|
||||
let wildcard = -1;
|
||||
for (let i = index; i <= pattern.length; ++i) {
|
||||
const c = charCodeAt(pattern, i);
|
||||
if (c === CHAR_OPEN_BRACKET && i === segStart) {
|
||||
const { index: idx, alias } = Path.parseAlias(pattern, i + 1);
|
||||
if (charCodeAt(pattern, idx) !== CHAR_CLOSE_BRACKET) break;
|
||||
segments.push(alias);
|
||||
index = i = idx + 1;
|
||||
segStart = i + 1;
|
||||
} else if (c === CHAR_ASTERISK && i === segStart) {
|
||||
wildcard = i + 1;
|
||||
} else if (!legalChar(c) || i === wildcard) {
|
||||
if (i === segStart) break;
|
||||
const s = pattern.slice(segStart, i);
|
||||
segments.push(s);
|
||||
index = i;
|
||||
if (c !== CHAR_DOT) break;
|
||||
segStart = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +197,8 @@ export default class Path {
|
||||
if (!path) {
|
||||
path = cache2.get(str);
|
||||
if (!path) {
|
||||
const { segments } = Path.parsePath(str, 0);
|
||||
const { index, segments } = Path.parsePath(str, 0);
|
||||
if (index < str.length) throw new Error("Invalid parameter path");
|
||||
path = new Path(segments);
|
||||
if (path.toString() !== str) cache1.set(path.toString(), path);
|
||||
}
|
||||
|
||||
@@ -169,9 +169,13 @@ export async function httpConnectionRequest(
|
||||
if (res.statusCode === 503) return "Device is offline";
|
||||
|
||||
if (res.statusCode === 401 && res.headers["www-authenticate"]) {
|
||||
authHeader = auth.parseWwwAuthenticateHeader(
|
||||
res.headers["www-authenticate"]
|
||||
);
|
||||
try {
|
||||
authHeader = auth.parseWwwAuthenticateHeader(
|
||||
res.headers["www-authenticate"]
|
||||
);
|
||||
} catch (err) {
|
||||
return "Connection request error: Error parsing www-authenticate header";
|
||||
}
|
||||
[username, password, authExp] = await extractAuth(authExp, false);
|
||||
} else {
|
||||
return `Connection request error: Unexpected status code ${res.statusCode}`;
|
||||
@@ -182,14 +186,13 @@ export async function httpConnectionRequest(
|
||||
}
|
||||
|
||||
export async function udpConnectionRequest(
|
||||
address: string,
|
||||
host: string,
|
||||
port: number,
|
||||
authExp: Expression,
|
||||
sourcePort = 0,
|
||||
_debug: boolean,
|
||||
deviceId: string
|
||||
): Promise<void> {
|
||||
const [host, portStr] = address.split(":", 2);
|
||||
const port = portStr ? parseInt(portStr) : 80;
|
||||
const now = Date.now();
|
||||
|
||||
const client = dgram.createSocket({ type: "udp4", reuseAddr: true });
|
||||
@@ -216,8 +219,8 @@ export async function udpConnectionRequest(
|
||||
.createHmac("sha1", password)
|
||||
.update(`${ts}${id}${username}${cn}`)
|
||||
.digest("hex");
|
||||
const uri = `http://${address}?ts=${ts}&id=${id}&un=${username}&cn=${cn}&sig=${sig}`;
|
||||
const msg = `GET ${uri} HTTP/1.1\r\nHost: ${address}\r\n\r\n`;
|
||||
const uri = `http://${host}:${port}?ts=${ts}&id=${id}&un=${username}&cn=${cn}&sig=${sig}`;
|
||||
const msg = `GET ${uri} HTTP/1.1\r\nHost: ${host}:${port}\r\n\r\n`;
|
||||
const message = Buffer.from(msg);
|
||||
|
||||
for (let i = 0; i < 3; ++i) {
|
||||
|
||||
+53
-6
@@ -84,9 +84,13 @@ async function authenticate(
|
||||
let authentication;
|
||||
|
||||
if (sessionContext.httpRequest.headers["authorization"]) {
|
||||
authentication = auth.parseAuthorizationHeader(
|
||||
sessionContext.httpRequest.headers["authorization"]
|
||||
);
|
||||
try {
|
||||
authentication = auth.parseAuthorizationHeader(
|
||||
sessionContext.httpRequest.headers["authorization"]
|
||||
);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (authentication && authentication.method === "Digest") {
|
||||
@@ -208,7 +212,7 @@ async function writeResponse(
|
||||
sessionContext.extendLock = now + 10000;
|
||||
await cache.acquireLock(
|
||||
`cwmp_session_${sessionContext.deviceId}`,
|
||||
sessionContext.timeout + 15000,
|
||||
sessionContext.timeout * 1000 + 15000,
|
||||
0,
|
||||
sessionContext.sessionId
|
||||
);
|
||||
@@ -357,6 +361,39 @@ async function transferComplete(sessionContext, rpc): Promise<void> {
|
||||
return writeResponse(sessionContext, res);
|
||||
}
|
||||
|
||||
async function duStateChangeComplete(sessionContext, rpc): Promise<void> {
|
||||
const { acsResponse, operation, fault } = await session.duStateChangeComplete(
|
||||
sessionContext,
|
||||
rpc.cpeRequest
|
||||
);
|
||||
|
||||
if (!operation) {
|
||||
logger.accessWarn({
|
||||
sessionContext: sessionContext,
|
||||
message: "Unrecognized command key",
|
||||
rpc: rpc,
|
||||
});
|
||||
}
|
||||
|
||||
if (fault) {
|
||||
Object.assign(sessionContext.retries, operation.retries);
|
||||
recordFault(
|
||||
sessionContext,
|
||||
fault,
|
||||
operation.provisions,
|
||||
operation.channels
|
||||
);
|
||||
}
|
||||
|
||||
const res = soap.response({
|
||||
id: rpc.id,
|
||||
acsResponse: acsResponse,
|
||||
cwmpVersion: sessionContext.cwmpVersion,
|
||||
});
|
||||
|
||||
return writeResponse(sessionContext, res);
|
||||
}
|
||||
|
||||
// Append provisions and remove duplicates
|
||||
function appendProvisions(original, toAppend): boolean {
|
||||
let modified = false;
|
||||
@@ -891,7 +928,7 @@ async function getSession(
|
||||
const sessionContextString = await cache.pop(`session_${sessionId}`);
|
||||
if (!sessionContextString) return null;
|
||||
sessionContext = await session.deserialize(sessionContextString);
|
||||
connection.setTimeout(sessionContext.timeout);
|
||||
connection.setTimeout(sessionContext.timeout * 1000);
|
||||
return sessionContext;
|
||||
}
|
||||
|
||||
@@ -1146,7 +1183,7 @@ async function processRequest(
|
||||
sessionContext.extendLock = sessionContext.timestamp + 10000;
|
||||
await cache.acquireLock(
|
||||
`cwmp_session_${sessionContext.deviceId}`,
|
||||
sessionContext.timeout + 15000,
|
||||
sessionContext.timeout * 1000 + 15000,
|
||||
0,
|
||||
sessionContext.sessionId
|
||||
);
|
||||
@@ -1235,6 +1272,16 @@ async function processRequest(
|
||||
cwmpVersion: sessionContext.cwmpVersion,
|
||||
});
|
||||
return writeResponse(sessionContext, res);
|
||||
} else if (rpc.cpeRequest.name === "DUStateChangeComplete") {
|
||||
if (sessionContext.state !== 1) return reportBadState(sessionContext);
|
||||
|
||||
logger.accessInfo({
|
||||
sessionContext: sessionContext,
|
||||
message: "CPE request",
|
||||
rpc: rpc,
|
||||
});
|
||||
|
||||
return duStateChangeComplete(sessionContext, rpc);
|
||||
} else {
|
||||
if (sessionContext.state !== 1 || rpc.cpeRequest.name === "Inform")
|
||||
return void reportBadState(sessionContext);
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
import { MongoClient, ObjectID, Collection, Db } from "mongodb";
|
||||
import { get } from "./config";
|
||||
import { encodeTag, escapeRegExp } from "./common";
|
||||
import { decodeTag, encodeTag, escapeRegExp } from "./common";
|
||||
import { parse } from "./common/expression-parser";
|
||||
import {
|
||||
DeviceData,
|
||||
@@ -46,6 +46,8 @@ export let tasksCollection: Collection,
|
||||
|
||||
let clientPromise: Promise<MongoClient>;
|
||||
|
||||
const INVALID_PATH_SUFFIX = "__invalid";
|
||||
|
||||
function compareAccessLists(list1: string[], list2: string[]): boolean {
|
||||
if (list1.length !== list2.length) return false;
|
||||
for (const [i, v] of list1.entries()) if (v !== list2[i]) return false;
|
||||
@@ -167,7 +169,19 @@ export async function fetchDevice(
|
||||
if (obj["_accessList"] != null)
|
||||
attrs.accessList = [obj["_attributesTimestamp"] || 1, obj["_accessList"]];
|
||||
|
||||
res.push([Path.parse(path), t, attrs]);
|
||||
try {
|
||||
res.push([Path.parse(path), t, attrs]);
|
||||
} catch (err) {
|
||||
// The path parser is now more strict so we might be in a situation where
|
||||
// the database contains invalid paths from before this change So here we
|
||||
// encode the invalid characters.
|
||||
const splits = path.split(".");
|
||||
splits[splits.length - 1] =
|
||||
encodeTag(splits[splits.length - 1]) + INVALID_PATH_SUFFIX;
|
||||
path = splits.join(".");
|
||||
res.push([Path.parse(path), t, attrs]);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
if (!k.startsWith("_")) {
|
||||
@@ -449,7 +463,16 @@ export async function saveDevice(
|
||||
break;
|
||||
default:
|
||||
if (!diff[2]) {
|
||||
update["$unset"][path.toString()] = 1;
|
||||
let pathStr = path.toString();
|
||||
// Paths with that suffix are encoded and need to be decoded
|
||||
if (pathStr.endsWith(INVALID_PATH_SUFFIX)) {
|
||||
const splits = pathStr.split(".");
|
||||
splits[splits.length - 1] = decodeTag(
|
||||
splits[splits.length - 1].slice(0, 0 - INVALID_PATH_SUFFIX.length)
|
||||
);
|
||||
pathStr = splits.join(".");
|
||||
}
|
||||
update["$unset"][pathStr] = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
+10
-9
@@ -30,14 +30,6 @@ const TIMEOUT = +config.get("EXT_TIMEOUT");
|
||||
const processes: { [script: string]: ChildProcess } = {};
|
||||
const jobs = new Map();
|
||||
|
||||
function messageHandler(message): void {
|
||||
const func = jobs.get(message[0]);
|
||||
if (func) {
|
||||
jobs.delete(message[0]);
|
||||
func({ fault: message[1], value: message[2] });
|
||||
}
|
||||
}
|
||||
|
||||
export function run(args: string[]): Promise<{ fault: Fault; value: any }> {
|
||||
return new Promise((resolve) => {
|
||||
const scriptName = args[0];
|
||||
@@ -70,7 +62,16 @@ export function run(args: string[]): Promise<{ fault: Fault; value: any }> {
|
||||
if (processes[scriptName] === p) delete processes[scriptName];
|
||||
});
|
||||
|
||||
p.on("message", messageHandler);
|
||||
p.on("message", (message) => {
|
||||
const func = jobs.get(message[0]);
|
||||
if (func) {
|
||||
jobs.delete(message[0]);
|
||||
// Wait for any disconnect even to fire
|
||||
setTimeout(() => {
|
||||
func({ fault: message[1], value: message[2] });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const rlstdout = readline.createInterface(p.stdout);
|
||||
rlstdout.on("line", (line) => {
|
||||
|
||||
+512
-27
@@ -57,8 +57,13 @@ import {
|
||||
FactoryReset,
|
||||
AddObjectResponse,
|
||||
GetParameterValuesResponse,
|
||||
ChangeDUStateOperation,
|
||||
DownloadOperation,
|
||||
DUStateChangeCompleteRequest,
|
||||
ChangeDUState,
|
||||
} from "./types";
|
||||
import { getRequestOrigin } from "./forwarded";
|
||||
import * as logger from "./logger";
|
||||
|
||||
const VALID_PARAM_TYPES = new Set([
|
||||
"xsd:int",
|
||||
@@ -283,6 +288,8 @@ export async function transferComplete(
|
||||
sessionContext.deviceData.attributes.revision = revision;
|
||||
const commandKey = rpcReq.commandKey;
|
||||
const operation = sessionContext.operations[commandKey];
|
||||
if (operation.name !== "Download")
|
||||
throw new Error("Unexpected operation name");
|
||||
|
||||
if (!operation) {
|
||||
return {
|
||||
@@ -432,6 +439,142 @@ function revertDownloadParameters(
|
||||
}
|
||||
}
|
||||
|
||||
export async function duStateChangeComplete(
|
||||
sessionContext: SessionContext,
|
||||
rpcReq: DUStateChangeCompleteRequest
|
||||
): Promise<{
|
||||
acsResponse: AcsResponse;
|
||||
operation: ChangeDUStateOperation;
|
||||
fault: Fault;
|
||||
}> {
|
||||
const revision =
|
||||
(sessionContext.revisions[sessionContext.revisions.length - 1] || 0) + 1;
|
||||
sessionContext.deviceData.timestamps.revision = revision;
|
||||
sessionContext.deviceData.attributes.revision = revision;
|
||||
const commandKey = rpcReq.commandKey;
|
||||
const operation = sessionContext.operations[commandKey];
|
||||
|
||||
if (!operation) {
|
||||
return {
|
||||
acsResponse: { name: "DUStateChangeCompleteResponse" },
|
||||
operation: null,
|
||||
fault: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (operation.name !== "ChangeDUState")
|
||||
throw new Error("Unexpected operation name");
|
||||
|
||||
if (operation.args.operations.length !== rpcReq.results.length) {
|
||||
return {
|
||||
acsResponse: { name: "DUStateChangeCompleteResponse" },
|
||||
operation: operation,
|
||||
fault: {
|
||||
code: "invalid_response",
|
||||
message: "Unexpected number of results in DUStateChangeComplete",
|
||||
timestamp: operation.timestamp,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
delete sessionContext.operations[commandKey];
|
||||
if (!sessionContext.operationsTouched) sessionContext.operationsTouched = {};
|
||||
sessionContext.operationsTouched[commandKey] = 1;
|
||||
|
||||
let toClear = null;
|
||||
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
|
||||
|
||||
let fault: Fault;
|
||||
for (const [i, res] of rpcReq.results.entries()) {
|
||||
const op = operation.args.operations[i];
|
||||
if (res.fault && res.fault.faultCode !== "0") {
|
||||
revertChangeDuStateParameters(sessionContext, op.instance);
|
||||
fault = {
|
||||
code: `cwmp.${res.fault.faultCode}`,
|
||||
message: res.fault.faultString,
|
||||
timestamp: operation.timestamp,
|
||||
detail: res.fault,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
const toSet: Record<string, [string | number | boolean, string]> = {
|
||||
LastChangeDUState: [operation.timestamp, "xsd:dateTime"],
|
||||
LastOperationType: [op.operationType, "xsd:string"],
|
||||
LastURL: [op.url, "xsd:string"],
|
||||
LastUUID: [op.uuid, "xsd:string"],
|
||||
LastExecutionEnvRef: [op.executionEnvRef, "xsd:string"],
|
||||
StartTime: [+res.startTime, "xsd:dateTime"],
|
||||
CompleteTime: [+res.completeTime, "xsd:dateTime"],
|
||||
CurrentState: [res.currentState, "xsd:string"],
|
||||
DeploymentUnitRef: [res.deploymentUnitRef, "xsd:string"],
|
||||
Version: [res.version, "xsd:string"],
|
||||
Resolved: [res.resolved, "xsd:string"],
|
||||
ExecutionUnitRefList: [res.executionUnitRefList, "xsd:string"],
|
||||
};
|
||||
|
||||
for (const [k, v] of Object.entries(toSet)) {
|
||||
const p = sessionContext.deviceData.paths.add(
|
||||
Path.parse(`DUStates.${op.instance}.${k}`)
|
||||
);
|
||||
toClear = device.set(
|
||||
sessionContext.deviceData,
|
||||
p,
|
||||
timestamp,
|
||||
{ value: [timestamp, v] },
|
||||
toClear
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (toClear) {
|
||||
for (const c of toClear)
|
||||
device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]);
|
||||
}
|
||||
|
||||
return {
|
||||
acsResponse: { name: "DUStateChangeCompleteResponse" },
|
||||
operation: operation,
|
||||
fault: fault,
|
||||
};
|
||||
}
|
||||
|
||||
function revertChangeDuStateParameters(
|
||||
sessionContext: SessionContext,
|
||||
instance
|
||||
): void {
|
||||
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
|
||||
|
||||
let p;
|
||||
|
||||
p = sessionContext.deviceData.paths.add(
|
||||
Path.parse(`DUStates.${instance}.LastChangeDUState`)
|
||||
);
|
||||
|
||||
const lastChangeDuState = sessionContext.deviceData.attributes.get(p);
|
||||
|
||||
p = sessionContext.deviceData.paths.add(
|
||||
Path.parse(`DUStates.${instance}.ChangeDUState`)
|
||||
);
|
||||
|
||||
const toClear = device.set(sessionContext.deviceData, p, timestamp, {
|
||||
value: [
|
||||
timestamp,
|
||||
[
|
||||
lastChangeDuState && lastChangeDuState.value[1]
|
||||
? lastChangeDuState.value[1][0]
|
||||
: 0,
|
||||
"xsd:dateTime",
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
if (toClear) {
|
||||
for (const c of toClear)
|
||||
device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]);
|
||||
}
|
||||
}
|
||||
|
||||
export async function timeoutOperations(
|
||||
sessionContext: SessionContext
|
||||
): Promise<{ faults: Fault[]; operations: Operation[] }> {
|
||||
@@ -793,6 +936,10 @@ function runDeclarations(
|
||||
downloadsToCreate: new InstanceSet(),
|
||||
downloadsValues: new Map(),
|
||||
downloadsDownload: new Map(),
|
||||
duStatesToDelete: new Set(),
|
||||
duStatesToCreate: new InstanceSet(),
|
||||
duStatesValues: new Map(),
|
||||
duStatesChangeDUState: new Map(),
|
||||
reboot: 0,
|
||||
factoryReset: 0,
|
||||
};
|
||||
@@ -1438,6 +1585,147 @@ export async function rpcRequest(
|
||||
}
|
||||
}
|
||||
|
||||
// DUState
|
||||
index = null;
|
||||
for (const instance of sessionContext.syncState.duStatesToCreate) {
|
||||
if (index == null) {
|
||||
index = 0;
|
||||
for (const p of sessionContext.deviceData.paths.find(
|
||||
Path.parse("DUStates.*"),
|
||||
false,
|
||||
true
|
||||
)) {
|
||||
if (
|
||||
+p.segments[1] > index &&
|
||||
sessionContext.deviceData.attributes.has(p)
|
||||
)
|
||||
index = +p.segments[1];
|
||||
}
|
||||
}
|
||||
|
||||
++index;
|
||||
|
||||
toClear = device.set(
|
||||
sessionContext.deviceData,
|
||||
Path.parse("DUStates"),
|
||||
timestamp,
|
||||
{ object: [timestamp, 1], writable: [timestamp, 1] },
|
||||
toClear
|
||||
);
|
||||
|
||||
toClear = device.set(
|
||||
sessionContext.deviceData,
|
||||
Path.parse(`DUStates.${index}`),
|
||||
timestamp,
|
||||
{ object: [timestamp, 1], writable: [timestamp, 1] },
|
||||
toClear
|
||||
);
|
||||
|
||||
const params = {
|
||||
URL: {
|
||||
writable: 1,
|
||||
value: [instance.URL || "", "xsd:string"],
|
||||
},
|
||||
UUID: {
|
||||
writable: 1,
|
||||
value: [instance.UUID || "", "xsd:string"],
|
||||
},
|
||||
ExecutionEnvRef: {
|
||||
writable: 1,
|
||||
value: [instance.ExecutionEnvRef || "", "xsd:string"],
|
||||
},
|
||||
Username: {
|
||||
writable: 1,
|
||||
value: [instance.Username || "", "xsd:string"],
|
||||
},
|
||||
Password: {
|
||||
writable: 1,
|
||||
value: [instance.Password || "", "xsd:string"],
|
||||
},
|
||||
ChangeDUState: {
|
||||
writable: 1,
|
||||
value: [instance.ChangeDUState || "", "xsd:dateTime"],
|
||||
},
|
||||
OperationType: {
|
||||
writable: 1,
|
||||
value: [instance.OperationType || "", "xsd:string"],
|
||||
},
|
||||
LastURL: { writable: 0, value: ["", "xsd:string"] },
|
||||
LastUUID: { writable: 0, value: ["", "xsd:string"] },
|
||||
LastExecutionEnvRef: { writable: 0, value: ["", "xsd:string"] },
|
||||
LastChangeDUState: { writable: 0, value: [0, "xsd:dateTime"] },
|
||||
LastOperationType: { writable: 0, value: [0, "xsd:string"] },
|
||||
Version: { writable: 0, value: [0, "xsd:string"] },
|
||||
CurrentState: { writable: 0, value: [0, "xsd:string"] },
|
||||
ExecutionUnitRefList: { writable: 0, value: [0, "xsd:string"] },
|
||||
DeploymentUnitRef: { writable: 0, value: [0, "xsd:string"] },
|
||||
Resolved: { writable: 0, value: [0, "xsd:boolean"] },
|
||||
StartTime: { writable: 0, value: [0, "xsd:dateTime"] },
|
||||
CompleteTime: { writable: 0, value: [0, "xsd:dateTime"] },
|
||||
};
|
||||
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
toClear = device.set(
|
||||
sessionContext.deviceData,
|
||||
Path.parse(`DUStates.${index}.${k}`),
|
||||
timestamp,
|
||||
{
|
||||
object: [timestamp, 0],
|
||||
writable: [timestamp, v.writable as 0 | 1],
|
||||
value: [
|
||||
timestamp,
|
||||
v.value as [string | number | boolean, string],
|
||||
],
|
||||
},
|
||||
toClear
|
||||
);
|
||||
}
|
||||
|
||||
toClear = device.set(
|
||||
sessionContext.deviceData,
|
||||
Path.parse(`DUStates.${index}.*`),
|
||||
timestamp,
|
||||
null,
|
||||
toClear
|
||||
);
|
||||
}
|
||||
|
||||
sessionContext.syncState.duStatesToCreate.clear();
|
||||
|
||||
for (const instance of sessionContext.syncState.duStatesToDelete) {
|
||||
toClear = device.set(
|
||||
sessionContext.deviceData,
|
||||
instance,
|
||||
timestamp,
|
||||
null,
|
||||
toClear
|
||||
);
|
||||
for (const p of sessionContext.syncState.duStatesValues.keys()) {
|
||||
if (p.segments[1] === instance.segments[1])
|
||||
sessionContext.syncState.duStatesValues.delete(p);
|
||||
}
|
||||
}
|
||||
|
||||
sessionContext.syncState.duStatesToDelete.clear();
|
||||
|
||||
for (const [p, v] of sessionContext.syncState.duStatesValues) {
|
||||
const attrs = sessionContext.deviceData.attributes.get(p);
|
||||
if (attrs) {
|
||||
if (attrs.writable && attrs.writable[1] && attrs.value) {
|
||||
const val = device.sanitizeParameterValue([v, attrs.value[1][1]]);
|
||||
if (val[0] !== attrs.value[1][0]) {
|
||||
toClear = device.set(
|
||||
sessionContext.deviceData,
|
||||
p,
|
||||
timestamp,
|
||||
{ value: [timestamp, val] },
|
||||
toClear
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toClear || sessionContext.deviceData.changes.has("prerequisite")) {
|
||||
if (toClear) {
|
||||
for (const c of toClear)
|
||||
@@ -1740,6 +2028,7 @@ function generateSetRpcRequest(
|
||||
| FactoryReset
|
||||
| Reboot
|
||||
| Download
|
||||
| ChangeDUState
|
||||
) & { next?: string } {
|
||||
const syncState = sessionContext.syncState;
|
||||
if (!syncState) return null;
|
||||
@@ -1918,6 +2207,76 @@ function generateSetRpcRequest(
|
||||
}
|
||||
}
|
||||
|
||||
// ChangeDUState
|
||||
const duStateOps: ChangeDUState["operations"] = [];
|
||||
for (const [p, t] of syncState.duStatesChangeDUState) {
|
||||
const attrs = deviceData.attributes.get(p);
|
||||
if (attrs && attrs.value && t <= attrs.value[1][0]) continue;
|
||||
const urlAttrs = deviceData.attributes.get(
|
||||
deviceData.paths.get(p.slice(0, -1).concat(Path.parse("URL")))
|
||||
);
|
||||
const operationTypeAttrs = deviceData.attributes.get(
|
||||
deviceData.paths.get(p.slice(0, -1).concat(Path.parse("OperationType")))
|
||||
);
|
||||
const uuidAttrs = deviceData.attributes.get(
|
||||
deviceData.paths.get(p.slice(0, -1).concat(Path.parse("UUID")))
|
||||
);
|
||||
const executionEnvRefAttrs = deviceData.attributes.get(
|
||||
deviceData.paths.get(p.slice(0, -1).concat(Path.parse("ExecutionEnvRef")))
|
||||
);
|
||||
const versionAttrs = deviceData.attributes.get(
|
||||
deviceData.paths.get(p.slice(0, -1).concat(Path.parse("Version")))
|
||||
);
|
||||
const usernameAttrs = deviceData.attributes.get(
|
||||
deviceData.paths.get(p.slice(0, -1).concat(Path.parse("Username")))
|
||||
);
|
||||
const passwordAttrs = deviceData.attributes.get(
|
||||
deviceData.paths.get(p.slice(0, -1).concat(Path.parse("Password")))
|
||||
);
|
||||
|
||||
const operationType = operationTypeAttrs
|
||||
? (operationTypeAttrs.value[1][0] as string)
|
||||
: null;
|
||||
const url = urlAttrs ? (urlAttrs.value[1][0] as string) : null;
|
||||
const uuid = uuidAttrs ? (uuidAttrs.value[1][0] as string) : null;
|
||||
const executionEnvRef = executionEnvRefAttrs
|
||||
? (executionEnvRefAttrs.value[1][0] as string)
|
||||
: null;
|
||||
const version = versionAttrs ? (versionAttrs.value[1][0] as string) : null;
|
||||
const username = usernameAttrs
|
||||
? (usernameAttrs.value[1][0] as string)
|
||||
: null;
|
||||
const password = passwordAttrs
|
||||
? (passwordAttrs.value[1][0] as string)
|
||||
: null;
|
||||
|
||||
if (
|
||||
operationType !== "install" &&
|
||||
operationType !== "update" &&
|
||||
operationType !== "uninstall"
|
||||
)
|
||||
continue;
|
||||
|
||||
duStateOps.push({
|
||||
operationType: operationType,
|
||||
instance: p.segments[1] as string,
|
||||
url: url,
|
||||
uuid: uuid,
|
||||
executionEnvRef: executionEnvRef,
|
||||
version: version,
|
||||
username: username,
|
||||
password: password,
|
||||
});
|
||||
}
|
||||
|
||||
if (duStateOps.length) {
|
||||
return {
|
||||
name: "ChangeDUState",
|
||||
commandKey: generateRpcId(sessionContext),
|
||||
operations: duStateOps,
|
||||
};
|
||||
}
|
||||
|
||||
// Reboot
|
||||
if (syncState.reboot) {
|
||||
const p = sessionContext.deviceData.paths.get(Path.parse("Reboot"));
|
||||
@@ -2199,6 +2558,26 @@ function processDeclarations(
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "DUStates":
|
||||
if (
|
||||
currentPath.length === 3 &&
|
||||
currentPath.wildcard === 0 &&
|
||||
declareAttributeValues &&
|
||||
declareAttributeValues.value
|
||||
) {
|
||||
if (currentPath.segments[2] === "ChangeDUState") {
|
||||
syncState.duStatesChangeDUState.set(
|
||||
currentPath,
|
||||
declareAttributeValues.value[0]
|
||||
);
|
||||
} else {
|
||||
syncState.duStatesValues.set(
|
||||
currentPath,
|
||||
declareAttributeValues.value[0]
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "VirtualParameters":
|
||||
if (currentPath.length <= 2) {
|
||||
let d;
|
||||
@@ -2290,7 +2669,7 @@ function processDeclarations(
|
||||
syncState.refreshAttributes.object.add(currentPath);
|
||||
else if (currentAttributes.object[1] === 0)
|
||||
syncState.refreshAttributes.value.add(currentPath);
|
||||
} else {
|
||||
} else if (attrName in syncState.refreshAttributes) {
|
||||
syncState.refreshAttributes[attrName].add(currentPath);
|
||||
}
|
||||
}
|
||||
@@ -2369,6 +2748,10 @@ function processInstances(
|
||||
if (parent.length !== 1) return;
|
||||
instancesToDelete = sessionContext.syncState.downloadsToDelete;
|
||||
instancesToCreate = sessionContext.syncState.downloadsToCreate;
|
||||
} else if (parent.segments[0] === "DUStates") {
|
||||
if (parent.length !== 1) return;
|
||||
instancesToDelete = sessionContext.syncState.duStatesToDelete;
|
||||
instancesToCreate = sessionContext.syncState.duStatesToCreate;
|
||||
} else {
|
||||
instancesToDelete = sessionContext.syncState.instancesToDelete.get(parent);
|
||||
if (instancesToDelete == null) {
|
||||
@@ -2420,8 +2803,15 @@ export async function rpcResponse(
|
||||
id: string,
|
||||
_rpcRes: CpeResponse
|
||||
): Promise<Fault> {
|
||||
function invalidResponse(message: string): Fault {
|
||||
return {
|
||||
code: "invalid_response",
|
||||
message: message,
|
||||
};
|
||||
}
|
||||
|
||||
if (id !== generateRpcId(sessionContext))
|
||||
throw new Error("Request ID not recognized");
|
||||
return invalidResponse("Request ID not recognized");
|
||||
|
||||
++sessionContext.rpcCount;
|
||||
|
||||
@@ -2512,47 +2902,99 @@ export async function rpcResponse(
|
||||
|
||||
if (rpcRes.name === "GetParameterValuesResponse") {
|
||||
if (rpcReq.name !== "GetParameterValues")
|
||||
throw new Error("Response name does not match request name");
|
||||
return invalidResponse("Response name does not match request name");
|
||||
|
||||
if (rpcRes.parameterList.length !== rpcReq.parameterNames.length) {
|
||||
return {
|
||||
code: `invalid_response`,
|
||||
message:
|
||||
"The list of parameters returned in the GetParameterValuesResponse message does not match the requested parameters",
|
||||
};
|
||||
}
|
||||
const requested = new Set(rpcReq.parameterNames);
|
||||
|
||||
for (const p of rpcRes.parameterList) {
|
||||
for (const [path, value, type] of rpcRes.parameterList) {
|
||||
if (!requested.delete(path.toString())) {
|
||||
logger.accessWarn({
|
||||
sessionContext: sessionContext,
|
||||
message: "Unexpected parameter in response",
|
||||
parameter: path.toString(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
toClear = device.set(
|
||||
sessionContext.deviceData,
|
||||
p[0],
|
||||
path,
|
||||
timestamp,
|
||||
{
|
||||
object: [timestamp, 0],
|
||||
value: [timestamp, p.slice(1) as [string | number | boolean, string]],
|
||||
value: [timestamp, [value, type]],
|
||||
},
|
||||
toClear
|
||||
);
|
||||
}
|
||||
|
||||
if (requested.size) {
|
||||
for (const p of requested) {
|
||||
logger.accessWarn({
|
||||
sessionContext: sessionContext,
|
||||
message: "Missing parameter in response",
|
||||
parameter: p,
|
||||
});
|
||||
toClear = device.set(
|
||||
sessionContext.deviceData,
|
||||
Path.parse(p),
|
||||
timestamp,
|
||||
{
|
||||
object: [timestamp, 0],
|
||||
value: [timestamp, ["", "xsd:string"]],
|
||||
},
|
||||
toClear
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (rpcRes.name === "GetParameterAttributesResponse") {
|
||||
if (rpcReq.name !== "GetParameterAttributes")
|
||||
throw new Error("Response name does not match request name");
|
||||
|
||||
for (const p of rpcRes.parameterList) {
|
||||
const requested = new Set(rpcReq.parameterNames);
|
||||
|
||||
for (const [path, notification, accessList] of rpcRes.parameterList) {
|
||||
if (!requested.delete(path.toString())) {
|
||||
logger.accessWarn({
|
||||
sessionContext: sessionContext,
|
||||
message: "Unexpected parameter in response",
|
||||
parameter: path.toString(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
toClear = device.set(
|
||||
sessionContext.deviceData,
|
||||
p[0],
|
||||
path,
|
||||
timestamp,
|
||||
{
|
||||
notification: [timestamp, p[1] as number],
|
||||
accessList: [timestamp, p[2] as string[]],
|
||||
notification: [timestamp, notification],
|
||||
accessList: [timestamp, accessList],
|
||||
},
|
||||
toClear
|
||||
);
|
||||
}
|
||||
|
||||
if (requested.size) {
|
||||
for (const p of requested) {
|
||||
logger.accessWarn({
|
||||
sessionContext: sessionContext,
|
||||
message: "Missing parameter in response",
|
||||
parameter: p,
|
||||
});
|
||||
toClear = device.set(
|
||||
sessionContext.deviceData,
|
||||
Path.parse(p),
|
||||
timestamp,
|
||||
{
|
||||
notification: [timestamp, 0],
|
||||
accessList: [timestamp, []],
|
||||
},
|
||||
toClear
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (rpcRes.name === "GetParameterNamesResponse") {
|
||||
if (rpcReq.name !== "GetParameterNames")
|
||||
throw new Error("Response name does not match request name");
|
||||
return invalidResponse("Response name does not match request name");
|
||||
|
||||
let root: Path;
|
||||
if (rpcReq.parameterPath.endsWith("."))
|
||||
@@ -2598,6 +3040,7 @@ export async function rpcResponse(
|
||||
"FactoryReset",
|
||||
"VirtualParameters",
|
||||
"Downloads",
|
||||
"DUStates",
|
||||
]) {
|
||||
const p = sessionContext.deviceData.paths.get(Path.parse(n));
|
||||
if (p && sessionContext.deviceData.attributes.has(p))
|
||||
@@ -2609,6 +3052,14 @@ export async function rpcResponse(
|
||||
const wildcardParams: Path[] = [root.concat(wildcardPath)];
|
||||
|
||||
for (const [path, object, writable] of rpcRes.parameterList) {
|
||||
if (!path.toString().startsWith(rpcReq.parameterPath)) {
|
||||
logger.accessWarn({
|
||||
sessionContext: sessionContext,
|
||||
message: "Unexpected parameter in response",
|
||||
parameter: path.toString(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (object && !rpcReq.nextLevel)
|
||||
wildcardParams.push(path.concat(wildcardPath));
|
||||
|
||||
@@ -2635,7 +3086,7 @@ export async function rpcResponse(
|
||||
}
|
||||
} else if (rpcRes.name === "SetParameterValuesResponse") {
|
||||
if (rpcReq.name !== "SetParameterValues")
|
||||
throw new Error("Response name does not match request name");
|
||||
return invalidResponse("Response name does not match request name");
|
||||
|
||||
for (const p of rpcReq.parameterList) {
|
||||
toClear = device.set(
|
||||
@@ -2654,7 +3105,7 @@ export async function rpcResponse(
|
||||
}
|
||||
} else if (rpcRes.name === "SetParameterAttributesResponse") {
|
||||
if (rpcReq.name !== "SetParameterAttributes")
|
||||
throw new Error("Response name does not match request name");
|
||||
return invalidResponse("Response name does not match request name");
|
||||
|
||||
for (const p of rpcReq.parameterList) {
|
||||
let attrs;
|
||||
@@ -2684,7 +3135,7 @@ export async function rpcResponse(
|
||||
}
|
||||
} else if (rpcRes.name === "AddObjectResponse") {
|
||||
if (rpcReq.name !== "AddObject")
|
||||
throw new Error("Response name does not match request name");
|
||||
return invalidResponse("Response name does not match request name");
|
||||
|
||||
toClear = device.set(
|
||||
sessionContext.deviceData,
|
||||
@@ -2695,7 +3146,7 @@ export async function rpcResponse(
|
||||
);
|
||||
} else if (rpcRes.name === "DeleteObjectResponse") {
|
||||
if (rpcReq.name !== "DeleteObject")
|
||||
throw new Error("Response name does not match request name");
|
||||
return invalidResponse("Response name does not match request name");
|
||||
|
||||
toClear = device.set(
|
||||
sessionContext.deviceData,
|
||||
@@ -2706,7 +3157,7 @@ export async function rpcResponse(
|
||||
);
|
||||
} else if (rpcRes.name === "RebootResponse") {
|
||||
if (rpcReq.name !== "Reboot")
|
||||
throw new Error("Response name does not match request name");
|
||||
return invalidResponse("Response name does not match request name");
|
||||
|
||||
toClear = device.set(
|
||||
sessionContext.deviceData,
|
||||
@@ -2717,7 +3168,7 @@ export async function rpcResponse(
|
||||
);
|
||||
} else if (rpcRes.name === "FactoryResetResponse") {
|
||||
if (rpcReq.name !== "FactoryReset")
|
||||
throw new Error("Response name does not match request name");
|
||||
return invalidResponse("Response name does not match request name");
|
||||
|
||||
toClear = device.set(
|
||||
sessionContext.deviceData,
|
||||
@@ -2728,7 +3179,7 @@ export async function rpcResponse(
|
||||
);
|
||||
} else if (rpcRes.name === "DownloadResponse") {
|
||||
if (rpcReq.name !== "Download")
|
||||
throw new Error("Response name does not match request name");
|
||||
return invalidResponse("Response name does not match request name");
|
||||
|
||||
toClear = device.set(
|
||||
sessionContext.deviceData,
|
||||
@@ -2789,7 +3240,7 @@ export async function rpcResponse(
|
||||
toClear
|
||||
);
|
||||
} else {
|
||||
const operation = {
|
||||
const operation: DownloadOperation = {
|
||||
name: "Download",
|
||||
timestamp: sessionContext.timestamp,
|
||||
provisions: sessionContext.provisions,
|
||||
@@ -2813,8 +3264,42 @@ export async function rpcResponse(
|
||||
sessionContext.operationsTouched = {};
|
||||
sessionContext.operationsTouched[rpcReq.commandKey] = 1;
|
||||
}
|
||||
} else if (rpcRes.name === "ChangeDUStateResponse") {
|
||||
if (rpcReq.name !== "ChangeDUState")
|
||||
return invalidResponse("Response name does not match request name");
|
||||
|
||||
for (const op of rpcReq.operations) {
|
||||
toClear = device.set(
|
||||
sessionContext.deviceData,
|
||||
Path.parse(`DUStates.${op.instance}.ChangeDUState`),
|
||||
timestamp + 1,
|
||||
{ value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] },
|
||||
toClear
|
||||
);
|
||||
}
|
||||
|
||||
const operation = {
|
||||
name: "ChangeDUState",
|
||||
timestamp: sessionContext.timestamp,
|
||||
provisions: sessionContext.provisions,
|
||||
channels: sessionContext.channels,
|
||||
retries: {},
|
||||
args: {
|
||||
operations: rpcReq.operations,
|
||||
},
|
||||
};
|
||||
|
||||
for (const channel of Object.keys(sessionContext.channels)) {
|
||||
if (sessionContext.retries[channel] != null)
|
||||
operation.retries[channel] = sessionContext.retries[channel];
|
||||
}
|
||||
|
||||
sessionContext.operations[rpcReq.commandKey] = operation as Operation;
|
||||
if (!sessionContext.operationsTouched)
|
||||
sessionContext.operationsTouched = {};
|
||||
sessionContext.operationsTouched[rpcReq.commandKey] = 1;
|
||||
} else {
|
||||
throw new Error("Response name not recognized");
|
||||
return invalidResponse("Response name not recognized");
|
||||
}
|
||||
|
||||
if (toClear) {
|
||||
|
||||
+170
-12
@@ -48,6 +48,9 @@ import {
|
||||
GetRPCMethodsRequest,
|
||||
RequestDownloadRequest,
|
||||
AcsResponse,
|
||||
ChangeDUState,
|
||||
ChangeDUStateResponse,
|
||||
DUStateChangeCompleteRequest,
|
||||
} from "./types";
|
||||
import Path from "./common/path";
|
||||
|
||||
@@ -109,8 +112,8 @@ function event(xml: Element): string[] {
|
||||
|
||||
function parameterInfoList(xml: Element): [Path, boolean, boolean][] {
|
||||
return xml.children
|
||||
.filter((e) => e.localName === "ParameterInfoStruct")
|
||||
.map<[Path, boolean, boolean]>((e) => {
|
||||
if (e.localName !== "ParameterInfoStruct") return null;
|
||||
let param: string, value: string;
|
||||
for (const c of e.children) {
|
||||
switch (c.localName) {
|
||||
@@ -134,10 +137,20 @@ function parameterInfoList(xml: Element): [Path, boolean, boolean][] {
|
||||
parsed = false;
|
||||
}
|
||||
|
||||
if (param && !param.endsWith("."))
|
||||
return [Path.parse(param), false, parsed];
|
||||
else return [Path.parse(param.slice(0, -1)), true, parsed];
|
||||
});
|
||||
try {
|
||||
if (param && !param.endsWith("."))
|
||||
return [Path.parse(param), false, parsed];
|
||||
else return [Path.parse(param.slice(0, -1)), true, parsed];
|
||||
} catch (err) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
element: "Name",
|
||||
parameter: param,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((e) => e != null);
|
||||
}
|
||||
|
||||
const getValueType = memoize((str: string) => {
|
||||
@@ -151,8 +164,8 @@ function parameterValueList(
|
||||
xml: Element
|
||||
): [Path, string | number | boolean, string][] {
|
||||
return xml.children
|
||||
.filter((e) => e.localName === "ParameterValueStruct")
|
||||
.map<[Path, string | number | boolean, string]>((e) => {
|
||||
if (e.localName !== "ParameterValueStruct") return null;
|
||||
let valueElement: Element, param: string;
|
||||
for (const c of e.children) {
|
||||
switch (c.localName) {
|
||||
@@ -208,15 +221,24 @@ function parameterValueList(
|
||||
parsed = value;
|
||||
}
|
||||
}
|
||||
|
||||
return [Path.parse(param), parsed, valueType];
|
||||
});
|
||||
try {
|
||||
return [Path.parse(param), parsed, valueType];
|
||||
} catch (err) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
element: "Name",
|
||||
parameter: param,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((e) => e != null);
|
||||
}
|
||||
|
||||
function parameterAttributeList(xml: Element): [Path, number, string[]][] {
|
||||
return xml.children
|
||||
.filter((e) => e.localName === "ParameterAttributeStruct")
|
||||
.map<[Path, number, string[]]>((e) => {
|
||||
if (e.localName !== "ParameterAttributeStruct") return null;
|
||||
let notificationElement: Element,
|
||||
accessListElement: Element,
|
||||
param: string;
|
||||
@@ -248,8 +270,18 @@ function parameterAttributeList(xml: Element): [Path, number, string[]][] {
|
||||
.filter((c) => c.localName === "string")
|
||||
.map((c) => decodeEntities(c.text));
|
||||
|
||||
return [Path.parse(param), notification, accessList];
|
||||
});
|
||||
try {
|
||||
return [Path.parse(param), notification, accessList];
|
||||
} catch (err) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
element: "Name",
|
||||
parameter: param,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((e) => e != null);
|
||||
}
|
||||
|
||||
function GetParameterNames(methodRequest): string {
|
||||
@@ -544,6 +576,29 @@ function DownloadResponse(xml: Element): DownloadResponse {
|
||||
};
|
||||
}
|
||||
|
||||
function ChangeDUState(methodRequest: ChangeDUState): string {
|
||||
const operations = methodRequest.operations.map((op) => {
|
||||
if (op.operationType === "install")
|
||||
return `<Operations xsi:type="cwmp:InstallOpStruct"><UUID>${op.uuid}</UUID><URL>${op.url}</URL><ExecutionEnvRef>${op.executionEnvRef}</ExecutionEnvRef><Username>${op.username}</Username><Password>${op.password}</Password></Operations>`;
|
||||
else if (op.operationType === "update")
|
||||
return `<Operations xsi:type="cwmp:UpdateOpStruct"><UUID>${op.uuid}</UUID><URL>${op.url}</URL><Version>${op.version}</Version><Username>${op.username}</Username><Password>${op.password}</Password></Operations>`;
|
||||
else if (op.operationType === "uninstall")
|
||||
return `<Operations xsi:type="cwmp:UninstallOpStruct"><UUID>${op.uuid}</UUID><Version>${op.version}</Version><ExecutionEnvRef>${op.executionEnvRef}</ExecutionEnvRef></Operations>`;
|
||||
|
||||
return "";
|
||||
});
|
||||
|
||||
return `<cwmp:ChangeDUState><CommandKey>${
|
||||
methodRequest.commandKey || ""
|
||||
}</CommandKey>${operations.join("")}</cwmp:ChangeDUState>`;
|
||||
}
|
||||
|
||||
function ChangeDUStateResponse(): ChangeDUStateResponse {
|
||||
return {
|
||||
name: "ChangeDUStateResponse",
|
||||
};
|
||||
}
|
||||
|
||||
function Inform(xml: Element): InformRequest {
|
||||
let retryCount: number, evnt: string[];
|
||||
let parameterList: [Path, string | number | boolean, string][];
|
||||
@@ -690,6 +745,97 @@ function TransferCompleteResponse(): string {
|
||||
return "<cwmp:TransferCompleteResponse></cwmp:TransferCompleteResponse>";
|
||||
}
|
||||
|
||||
function DUStateChangeComplete(xml: Element): DUStateChangeCompleteRequest {
|
||||
let commandKey: string,
|
||||
results: DUStateChangeCompleteRequest["results"],
|
||||
res: DUStateChangeCompleteRequest["results"][0];
|
||||
for (const c of xml.children) {
|
||||
switch (c.localName) {
|
||||
case "CommandKey":
|
||||
commandKey = c.text;
|
||||
break;
|
||||
case "Results":
|
||||
if (!results) results = [];
|
||||
res = {
|
||||
uuid: null,
|
||||
deploymentUnitRef: null,
|
||||
version: null,
|
||||
currentState: null,
|
||||
resolved: true,
|
||||
executionUnitRefList: null,
|
||||
startTime: null,
|
||||
completeTime: null,
|
||||
fault: null,
|
||||
};
|
||||
for (const cc of c.children) {
|
||||
switch (cc.localName) {
|
||||
case "UUID":
|
||||
res.uuid = cc.text;
|
||||
break;
|
||||
case "DeploymentUnitRef":
|
||||
res.deploymentUnitRef = cc.text;
|
||||
break;
|
||||
case "Version":
|
||||
res.version = cc.text;
|
||||
break;
|
||||
case "CurrentState":
|
||||
res.currentState = cc.text;
|
||||
break;
|
||||
case "Resolved":
|
||||
res.resolved = parseBool(cc.text);
|
||||
break;
|
||||
case "ExecutionUnitRefList":
|
||||
res.executionUnitRefList = cc.text;
|
||||
break;
|
||||
case "StartTime":
|
||||
if (c.text) res.startTime = Date.parse(c.text);
|
||||
break;
|
||||
case "CompleteTime":
|
||||
if (c.text) res.completeTime = Date.parse(c.text);
|
||||
break;
|
||||
case "Fault":
|
||||
res.fault = {
|
||||
faultCode: cc.children.find((n) => n.localName === "FaultCode")
|
||||
.text,
|
||||
faultString: cc.children.find(
|
||||
(n) => n.localName === "FaultString"
|
||||
).text,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
results.push(res);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (commandKey == null) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
element: "CommandKey",
|
||||
});
|
||||
commandKey = "";
|
||||
}
|
||||
|
||||
if (!results) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
element: "Results",
|
||||
});
|
||||
results = [];
|
||||
}
|
||||
|
||||
return {
|
||||
name: "DUStateChangeComplete",
|
||||
commandKey: commandKey,
|
||||
results: results,
|
||||
};
|
||||
}
|
||||
|
||||
function DUStateChangeCompleteResponse(): string {
|
||||
return "<cwmp:DUStateChangeCompleteResponse></cwmp:DUStateChangeCompleteResponse>";
|
||||
}
|
||||
|
||||
function RequestDownload(xml: Element): RequestDownloadRequest {
|
||||
return {
|
||||
name: "RequestDownload",
|
||||
@@ -894,6 +1040,9 @@ export function request(
|
||||
case "TransferComplete":
|
||||
rpc.cpeRequest = TransferComplete(methodElement);
|
||||
break;
|
||||
case "DUStateChangeComplete":
|
||||
rpc.cpeRequest = DUStateChangeComplete(methodElement);
|
||||
break;
|
||||
case "RequestDownload":
|
||||
rpc.cpeRequest = RequestDownload(methodElement);
|
||||
break;
|
||||
@@ -927,6 +1076,9 @@ export function request(
|
||||
case "DownloadResponse":
|
||||
rpc.cpeResponse = DownloadResponse(methodElement);
|
||||
break;
|
||||
case "ChangeDUStateResponse":
|
||||
rpc.cpeResponse = ChangeDUStateResponse();
|
||||
break;
|
||||
case "Fault":
|
||||
rpc.cpeFault = fault(methodElement);
|
||||
break;
|
||||
@@ -980,6 +1132,9 @@ export function response(rpc: {
|
||||
case "TransferCompleteResponse":
|
||||
body = TransferCompleteResponse();
|
||||
break;
|
||||
case "DUStateChangeCompleteResponse":
|
||||
body = DUStateChangeCompleteResponse();
|
||||
break;
|
||||
case "RequestDownloadResponse":
|
||||
body = RequestDownloadResponse();
|
||||
break;
|
||||
@@ -1022,6 +1177,9 @@ export function response(rpc: {
|
||||
case "Download":
|
||||
body = Download(rpc.acsRequest);
|
||||
break;
|
||||
case "ChangeDUState":
|
||||
body = ChangeDUState(rpc.acsRequest);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown method request ${(rpc.acsRequest as AcsRequest).name}`
|
||||
|
||||
+65
-5
@@ -121,6 +121,10 @@ export interface SyncState {
|
||||
downloadsToCreate: InstanceSet;
|
||||
downloadsValues: Map<Path, string | number>;
|
||||
downloadsDownload: Map<Path, number>;
|
||||
duStatesToDelete: Set<Path>;
|
||||
duStatesToCreate: InstanceSet;
|
||||
duStatesValues: Map<Path, string | number>;
|
||||
duStatesChangeDUState: Map<Path, number>;
|
||||
reboot: number;
|
||||
factoryReset: number;
|
||||
}
|
||||
@@ -183,12 +187,17 @@ export interface Task {
|
||||
provisions?: (string | number | boolean)[][];
|
||||
}
|
||||
|
||||
export interface Operation {
|
||||
interface OperationBase {
|
||||
name: string;
|
||||
timestamp: number;
|
||||
provisions: string[][];
|
||||
channels: { [channel: string]: number };
|
||||
retries: { [channel: string]: number };
|
||||
args: any;
|
||||
}
|
||||
|
||||
export interface DownloadOperation extends OperationBase {
|
||||
name: "Download";
|
||||
args: {
|
||||
instance: string;
|
||||
fileType: string;
|
||||
@@ -197,6 +206,15 @@ export interface Operation {
|
||||
};
|
||||
}
|
||||
|
||||
export interface ChangeDUStateOperation extends OperationBase {
|
||||
name: "ChangeDUState";
|
||||
args: {
|
||||
operations: ChangeDUState["operations"];
|
||||
};
|
||||
}
|
||||
|
||||
export type Operation = DownloadOperation | ChangeDUStateOperation;
|
||||
|
||||
export type AcsRequest =
|
||||
| GetParameterNames
|
||||
| GetParameterValues
|
||||
@@ -207,7 +225,8 @@ export type AcsRequest =
|
||||
| DeleteObject
|
||||
| FactoryReset
|
||||
| Reboot
|
||||
| Download;
|
||||
| Download
|
||||
| ChangeDUState;
|
||||
|
||||
export interface GetParameterNames {
|
||||
name: "GetParameterNames";
|
||||
@@ -277,6 +296,21 @@ export interface Download {
|
||||
failureUrl?: string;
|
||||
}
|
||||
|
||||
export interface ChangeDUState {
|
||||
name: "ChangeDUState";
|
||||
commandKey: string;
|
||||
operations: {
|
||||
operationType: "install" | "update" | "uninstall";
|
||||
instance: string;
|
||||
url: string;
|
||||
uuid: string;
|
||||
version: string;
|
||||
executionEnvRef: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface SpvFault {
|
||||
parameterName: string;
|
||||
faultCode: string;
|
||||
@@ -305,7 +339,8 @@ export type CpeResponse =
|
||||
| DeleteObjectResponse
|
||||
| RebootResponse
|
||||
| FactoryResetResponse
|
||||
| DownloadResponse;
|
||||
| DownloadResponse
|
||||
| ChangeDUStateResponse;
|
||||
|
||||
export interface GetParameterNamesResponse {
|
||||
name: "GetParameterNamesResponse";
|
||||
@@ -342,6 +377,10 @@ export interface DeleteObjectResponse {
|
||||
status: number;
|
||||
}
|
||||
|
||||
export interface ChangeDUStateResponse {
|
||||
name: "ChangeDUStateResponse";
|
||||
}
|
||||
|
||||
export interface RebootResponse {
|
||||
name: "RebootResponse";
|
||||
}
|
||||
@@ -361,7 +400,8 @@ export type CpeRequest =
|
||||
| InformRequest
|
||||
| TransferCompleteRequest
|
||||
| GetRPCMethodsRequest
|
||||
| RequestDownloadRequest;
|
||||
| RequestDownloadRequest
|
||||
| DUStateChangeCompleteRequest;
|
||||
|
||||
export interface InformRequest {
|
||||
name: "Inform";
|
||||
@@ -384,6 +424,22 @@ export interface TransferCompleteRequest {
|
||||
completeTime?: number;
|
||||
}
|
||||
|
||||
export interface DUStateChangeCompleteRequest {
|
||||
name: "DUStateChangeComplete";
|
||||
commandKey: string;
|
||||
results: {
|
||||
uuid: string;
|
||||
deploymentUnitRef: string;
|
||||
version: string;
|
||||
currentState: string;
|
||||
resolved: boolean;
|
||||
executionUnitRefList: string;
|
||||
fault?: { faultCode: string; faultString: string };
|
||||
startTime?: number;
|
||||
completeTime?: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface GetRPCMethodsRequest {
|
||||
name: "GetRPCMethods";
|
||||
}
|
||||
@@ -397,7 +453,8 @@ export type AcsResponse =
|
||||
| InformResponse
|
||||
| GetRPCMethodsResponse
|
||||
| TransferCompleteResponse
|
||||
| RequestDownloadResponse;
|
||||
| RequestDownloadResponse
|
||||
| DUStateChangeCompleteResponse;
|
||||
|
||||
export interface InformResponse {
|
||||
name: "InformResponse";
|
||||
@@ -415,6 +472,9 @@ export interface TransferCompleteResponse {
|
||||
export interface RequestDownloadResponse {
|
||||
name: "RequestDownloadResponse";
|
||||
}
|
||||
export interface DUStateChangeCompleteResponse {
|
||||
name: "DUStateChangeCompleteResponse";
|
||||
}
|
||||
|
||||
export interface QueryOptions {
|
||||
projection?: any;
|
||||
|
||||
@@ -59,8 +59,10 @@ const getAuthorizer = memoize(
|
||||
}
|
||||
);
|
||||
|
||||
koa.on("error", async (err) => {
|
||||
koa.on("error", async (err, ctx) => {
|
||||
setTimeout(() => {
|
||||
// Ignored errors resulting from aborted requests
|
||||
if (ctx && ctx.req.aborted) return;
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -659,7 +659,7 @@ router.get("/ping/:host", async (ctx) => {
|
||||
ctx.body = parsed;
|
||||
} else {
|
||||
ctx.status = 500;
|
||||
ctx.body = `${err.name}: ${err.message}`;
|
||||
ctx.body = err ? `${err.name}: ${err.message}` : "Unknown error";
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
import ava from "ava";
|
||||
import Path from "../lib/common/path";
|
||||
|
||||
ava("parse", (t) => {
|
||||
t.throws(() => Path.parse("."));
|
||||
t.throws(() => Path.parse("a "));
|
||||
t.throws(() => Path.parse(".a"));
|
||||
t.throws(() => Path.parse("a."));
|
||||
t.throws(() => Path.parse("a..b"));
|
||||
t.throws(() => Path.parse("b*"));
|
||||
t.throws(() => Path.parse("*b"));
|
||||
t.throws(() => Path.parse("a.b c.d"));
|
||||
t.throws(() => Path.parse("a["));
|
||||
t.throws(() => Path.parse("a[b"));
|
||||
t.throws(() => Path.parse("a[b:"));
|
||||
t.throws(() => Path.parse('a[b:"waef]'));
|
||||
t.notThrows(() => Path.parse("[]"));
|
||||
t.notThrows(() => Path.parse("[a:]"));
|
||||
t.notThrows(() => Path.parse("*"));
|
||||
t.notThrows(() => Path.parse(""));
|
||||
});
|
||||
|
||||
ava("toString", (t) => {
|
||||
const path1 = Path.parse('abc.[ abc : 123 , 123 : abc , 123: " abc "].123');
|
||||
const path2 = Path.parse('abc.[123: " abc ",abc:123,123:abc].123');
|
||||
|
||||
@@ -78,7 +78,7 @@ const component: ClosureComponent = (): Component => {
|
||||
const { value, timestamp, parameter } = evaluateParam(
|
||||
vnode.attrs["parameter"],
|
||||
vnode.attrs["device"],
|
||||
store.getTimestamp()
|
||||
store.getTimestamp() + store.getClockSkew()
|
||||
);
|
||||
|
||||
if (value == null) return null;
|
||||
@@ -117,7 +117,7 @@ const component: ClosureComponent = (): Component => {
|
||||
e.redraw = false;
|
||||
// Don't update any child element
|
||||
if (e.target === (el as VnodeDOM).dom) {
|
||||
const now = Date.now();
|
||||
const now = Date.now() + store.getClockSkew();
|
||||
const localeString = new Date(timestamp).toLocaleString();
|
||||
e.target.title = `${localeString} (${timeAgo(now - timestamp)})`;
|
||||
}
|
||||
|
||||
@@ -787,3 +787,13 @@ span.parameter-value {
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
background-color: #eee;
|
||||
overflow: hidden;
|
||||
height: 3px;
|
||||
& > .progress-bar {
|
||||
height: 100%;
|
||||
background-color: #b12d5c;
|
||||
}
|
||||
}
|
||||
+88
-79
@@ -73,58 +73,32 @@ const unpackSmartQuery = memoize((query) => {
|
||||
});
|
||||
});
|
||||
|
||||
interface ValidationErrors {
|
||||
[prop: string]: string;
|
||||
}
|
||||
|
||||
function putActionHandler(action, _object): Promise<ValidationErrors> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const object = Object.assign({}, _object);
|
||||
if (action === "save") {
|
||||
const file = object["file"] ? object["file"][0] : null;
|
||||
delete object["file"];
|
||||
|
||||
if (!file) return void resolve({ file: "File not selected" });
|
||||
|
||||
const id = file.name;
|
||||
|
||||
store
|
||||
.resourceExists("files", id)
|
||||
.then((exists) => {
|
||||
if (exists) {
|
||||
store.setTimestamp(Date.now());
|
||||
return void resolve({ file: "File already exists" });
|
||||
}
|
||||
const headers = Object.assign(
|
||||
{
|
||||
"Content-Type": "application/octet-stream",
|
||||
Accept: "application/octet-stream",
|
||||
},
|
||||
object
|
||||
);
|
||||
|
||||
store
|
||||
.xhrRequest({
|
||||
method: "PUT",
|
||||
headers: headers,
|
||||
url: `api/files/${encodeURIComponent(id)}`,
|
||||
serialize: (body) => body, // Identity function to prevent JSON.parse on blob data
|
||||
body: file,
|
||||
})
|
||||
.then(() => {
|
||||
notifications.push(
|
||||
"success",
|
||||
`File ${exists ? "updated" : "created"}`
|
||||
);
|
||||
store.setTimestamp(Date.now());
|
||||
resolve(null);
|
||||
})
|
||||
.catch(reject);
|
||||
})
|
||||
.catch(reject);
|
||||
} else {
|
||||
reject(new Error("Undefined action"));
|
||||
}
|
||||
function upload(
|
||||
file: File,
|
||||
headers: Record<string, string>,
|
||||
abortSignal?: AbortSignal,
|
||||
progressListener?: (e: ProgressEvent) => void
|
||||
): Promise<void> {
|
||||
headers = Object.assign(
|
||||
{
|
||||
"Content-Type": "application/octet-stream",
|
||||
},
|
||||
headers
|
||||
);
|
||||
return store.xhrRequest({
|
||||
method: "PUT",
|
||||
headers: headers,
|
||||
url: `api/files/${encodeURIComponent(file.name)}`,
|
||||
serialize: (body) => body, // Identity function to prevent JSON.parse on blob data
|
||||
body: file,
|
||||
config: (xhr) => {
|
||||
if (progressListener)
|
||||
xhr.upload.addEventListener("progress", progressListener);
|
||||
if (abortSignal) {
|
||||
if (abortSignal.aborted) xhr.abort();
|
||||
abortSignal.addEventListener("abort", () => xhr.abort());
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -224,42 +198,77 @@ export const component: ClosureComponent = (): Component => {
|
||||
title: "Create new file",
|
||||
onclick: () => {
|
||||
let cb: () => Children = null;
|
||||
const abortController = new AbortController();
|
||||
let progress = -1;
|
||||
const comp = m(
|
||||
putFormComponent,
|
||||
Object.assign(
|
||||
{
|
||||
actionHandler: (action, object) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
putActionHandler(action, object)
|
||||
.then((errors) => {
|
||||
const errorList = errors
|
||||
? Object.values(errors)
|
||||
: [];
|
||||
if (errorList.length) {
|
||||
for (const err of errorList)
|
||||
notifications.push("error", err);
|
||||
} else {
|
||||
overlay.close(cb);
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
notifications.push("error", err.message);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
actionHandler: async (action, _object) => {
|
||||
if (action !== "save")
|
||||
throw new Error("Undefined action");
|
||||
const object = Object.assign({}, _object);
|
||||
const file = object["file"]
|
||||
? object["file"][0]
|
||||
: null;
|
||||
delete object["file"];
|
||||
if (!file) {
|
||||
notifications.push("error", "File not selected");
|
||||
return;
|
||||
}
|
||||
|
||||
if (await store.resourceExists("files", file.name)) {
|
||||
store.setTimestamp(Date.now());
|
||||
notifications.push("error", "File already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
const progressListener = (e: ProgressEvent): void => {
|
||||
progress = e.loaded / e.total;
|
||||
m.redraw();
|
||||
};
|
||||
|
||||
progress = 0;
|
||||
try {
|
||||
await upload(
|
||||
file,
|
||||
object,
|
||||
abortController.signal,
|
||||
progressListener
|
||||
);
|
||||
store.setTimestamp(Date.now());
|
||||
notifications.push("success", "File created");
|
||||
overlay.close(cb);
|
||||
} catch (err) {
|
||||
notifications.push("error", err.message);
|
||||
}
|
||||
progress = -1;
|
||||
},
|
||||
},
|
||||
formData
|
||||
)
|
||||
);
|
||||
cb = () => comp;
|
||||
overlay.open(
|
||||
cb,
|
||||
() =>
|
||||
!comp.state["current"]["modified"] ||
|
||||
confirm("You have unsaved changes. Close anyway?")
|
||||
);
|
||||
cb = () => {
|
||||
if (progress < 0) return [null, comp];
|
||||
return [
|
||||
m(
|
||||
"div.progress",
|
||||
m("div.progress-bar", {
|
||||
style: `width: ${Math.trunc(progress * 100)}%`,
|
||||
})
|
||||
),
|
||||
comp,
|
||||
];
|
||||
};
|
||||
overlay.open(cb, () => {
|
||||
if (
|
||||
comp.state["current"]["modified"] &&
|
||||
!confirm("You have unsaved changes. Close anyway?")
|
||||
)
|
||||
return false;
|
||||
abortController.abort();
|
||||
return true;
|
||||
});
|
||||
},
|
||||
},
|
||||
"New"
|
||||
|
||||
+35
-6
@@ -34,6 +34,8 @@ const memoizedEvaluate = memoize(evaluate);
|
||||
let fulfillTimestamp = 0;
|
||||
let connectionNotification, configNotification, versionNotification;
|
||||
|
||||
let clockSkew = 0;
|
||||
|
||||
const queries = {
|
||||
filter: new WeakMap(),
|
||||
bookmark: new WeakMap(),
|
||||
@@ -93,11 +95,13 @@ class QueryResponse {
|
||||
}
|
||||
|
||||
function checkConnection(): void {
|
||||
const now1 = Date.now();
|
||||
m.request({
|
||||
url: "status",
|
||||
method: "GET",
|
||||
background: true,
|
||||
extract: (xhr) => {
|
||||
const now2 = Date.now();
|
||||
if (xhr.status !== 200) {
|
||||
if (!connectionNotification) {
|
||||
connectionNotification = notifications.push(
|
||||
@@ -112,6 +116,21 @@ function checkConnection(): void {
|
||||
connectionNotification = null;
|
||||
}
|
||||
|
||||
try {
|
||||
const nowAvg = Math.trunc((now1 + now2) / 2);
|
||||
const skew = Date.parse(xhr.getResponseHeader("Date")) - nowAvg;
|
||||
if (Math.abs(skew - clockSkew) > 5000 && now2 - now1 < 1000) {
|
||||
clockSkew = skew;
|
||||
console.warn(
|
||||
`System and server clocks are out of sync. Adding ${clockSkew}ms offset to any client-side time relative calculations.`
|
||||
);
|
||||
setTimestamp(now2);
|
||||
m.redraw();
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore in case of missing or invalid Date header
|
||||
}
|
||||
|
||||
const configChanged =
|
||||
xhr.getResponseHeader("x-config-snapshot") !== configSnapshot;
|
||||
const versionChanged =
|
||||
@@ -209,7 +228,7 @@ export async function xhrRequest(
|
||||
|
||||
export function unpackExpression(exp: Expression): Expression {
|
||||
if (!Array.isArray(exp)) return exp;
|
||||
const e = memoizedEvaluate(exp, null, fulfillTimestamp);
|
||||
const e = memoizedEvaluate(exp, null, fulfillTimestamp + clockSkew);
|
||||
return e;
|
||||
}
|
||||
|
||||
@@ -295,7 +314,7 @@ function compareFunction(sort: {
|
||||
function findMatches(resourceType, filter, sort, limit): any[] {
|
||||
let value = [];
|
||||
for (const obj of resources[resourceType].objects.values())
|
||||
if (evaluate(filter, obj, fulfillTimestamp)) value.push(obj);
|
||||
if (evaluate(filter, obj, fulfillTimestamp + clockSkew)) value.push(obj);
|
||||
|
||||
value = value.sort(compareFunction(sort));
|
||||
if (limit) value = value.slice(0, limit);
|
||||
@@ -461,7 +480,7 @@ export function fulfill(accessTimestamp: number): void {
|
||||
|
||||
for (const queryResponse of toFetch) {
|
||||
let filter = queries.filter.get(queryResponse);
|
||||
filter = memoizedEvaluate(filter, null, fulfillTimestamp);
|
||||
filter = memoizedEvaluate(filter, null, fulfillTimestamp + clockSkew);
|
||||
const bookmark = queries.bookmark.get(queryResponse);
|
||||
const sort = queries.sort.get(queryResponse);
|
||||
if (bookmark) filter = limitFilter(filter, sort, bookmark);
|
||||
@@ -476,7 +495,11 @@ export function fulfill(accessTimestamp: number): void {
|
||||
if (!diff) {
|
||||
for (const queryResponse of toFetch) {
|
||||
let filter = queries.filter.get(queryResponse);
|
||||
filter = memoizedEvaluate(filter, null, fulfillTimestamp);
|
||||
filter = memoizedEvaluate(
|
||||
filter,
|
||||
null,
|
||||
fulfillTimestamp + clockSkew
|
||||
);
|
||||
const limit = queries.limit.get(queryResponse);
|
||||
const bookmark = queries.bookmark.get(queryResponse);
|
||||
const sort = queries.sort.get(queryResponse);
|
||||
@@ -521,7 +544,9 @@ export function fulfill(accessTimestamp: number): void {
|
||||
|
||||
for (const d of deleted) {
|
||||
const obj = resources[resourceType].objects.get(d);
|
||||
if (evaluate(combinedFilterDiff, obj, fulfillTimestamp))
|
||||
if (
|
||||
evaluate(combinedFilterDiff, obj, fulfillTimestamp + clockSkew)
|
||||
)
|
||||
resources[resourceType].objects.delete(d);
|
||||
}
|
||||
|
||||
@@ -563,6 +588,10 @@ export function setTimestamp(t: number): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function getClockSkew(): number {
|
||||
return clockSkew;
|
||||
}
|
||||
|
||||
export function postTasks(
|
||||
deviceId: string,
|
||||
tasks: QueueTask[]
|
||||
@@ -664,7 +693,7 @@ export function evaluateExpression(
|
||||
obj: Record<string, unknown>
|
||||
): Expression {
|
||||
if (!Array.isArray(exp)) return exp;
|
||||
return memoizedEvaluate(exp, obj, fulfillTimestamp);
|
||||
return memoizedEvaluate(exp, obj, fulfillTimestamp + clockSkew);
|
||||
}
|
||||
|
||||
export function changePassword(
|
||||
|
||||
Reference in new issue
Block a user