Files
genieacs/lib/session.ts
T
Zaid Abdulla bdad2f9a32 Implicit fetch of 'writable' attribute
The writable attribute may not already be present when attempting to
modify parameters included in the inform message. For other parameters
the writable attrbiute is guarenteed to have already been fetched
during discovery.
2021-02-04 22:54:30 -08:00

2949 lines
84 KiB
TypeScript

/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import * as device from "./device";
import * as sandbox from "./sandbox";
import * as localCache from "./local-cache";
import * as defaultProvisions from "./default-provisions";
import { estimateGpnCount } from "./gpn-heuristic";
import Path from "./common/path";
import PathSet from "./common/path-set";
import VersionedMap from "./versioned-map";
import InstanceSet from "./instance-set";
import {
Attributes,
SessionContext,
DeviceData,
VirtualParameterDeclaration,
AttributeTimestamps,
AttributeValues,
Fault,
Declaration,
Clear,
CpeResponse,
CpeFault,
AcsRequest,
AcsResponse,
InformRequest,
TransferCompleteRequest,
Operation,
ScriptResult,
Expression,
GetParameterValues,
GetParameterAttributes,
GetParameterNames,
SetParameterValues,
SetParameterAttributes,
AddObject,
DeleteObject,
Download,
Reboot,
FactoryReset,
AddObjectResponse,
GetParameterValuesResponse,
} from "./types";
import { getRequestOrigin } from "./forwarded";
const VALID_PARAM_TYPES = new Set([
"xsd:int",
"xsd:unsignedInt",
"xsd:boolean",
"xsd:string",
"xsd:dateTime",
"xsd:base64",
"xsd:hexBinary",
]);
function initDeviceData(): DeviceData {
return {
paths: new PathSet(),
timestamps: new VersionedMap(),
attributes: new VersionedMap(),
trackers: new Map(),
changes: new Set(),
};
}
export function init(
deviceId: string,
cwmpVersion: string,
timeout: number
): SessionContext {
const timestamp = Date.now();
const sessionContext: SessionContext = {
timestamp: timestamp,
deviceId: deviceId,
deviceData: initDeviceData(),
cwmpVersion: cwmpVersion,
timeout: timeout,
provisions: [],
channels: {},
virtualParameters: [],
revisions: [0],
rpcCount: 0,
iteration: 0,
cycle: 0,
extensionsCache: {},
declarations: [],
state: 0,
authState: 0,
};
return sessionContext;
}
function generateRpcId(sessionContext: SessionContext): string {
return (
sessionContext.timestamp.toString(16) +
("0" + sessionContext.cycle.toString(16)).slice(-2) +
("0" + sessionContext.rpcCount.toString(16)).slice(-2)
);
}
export function configContextCallback(
sessionContext: SessionContext,
exp: Expression
): Expression {
if (!Array.isArray(exp)) return exp;
if (exp[0] === "PARAM" && typeof exp[1] === "string") {
let name = exp[1];
if (name === "id") name = "DeviceID.ID";
else if (name === "serialNumber") name = "DeviceID.SerialNumber";
else if (name === "productClass") name = "DeviceID.ProductClass";
else if (name === "oui") name = "DeviceID.OUI";
else if (name === "remoteAddress")
return getRequestOrigin(sessionContext.httpRequest).remoteAddress;
const deviceData = sessionContext.deviceData;
const paths = deviceData.paths;
const path = paths.get(Path.parse(name));
if (path) {
const attrs = deviceData.attributes.get(path, 1);
if (attrs && attrs.value && attrs.value[1]) return attrs.value[1][0];
}
} else if (exp[0] === "FUNC") {
if (exp[1] === "REMOTE_ADDRESS")
return getRequestOrigin(sessionContext.httpRequest).remoteAddress;
}
return exp;
}
export async function inform(
sessionContext: SessionContext,
rpcReq: InformRequest
): Promise<AcsResponse> {
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
const params: [Path, number, Attributes][] = [
[
Path.parse("DeviceID.Manufacturer"),
timestamp,
{
object: [timestamp, 0],
writable: [timestamp, 0],
value: [timestamp, [rpcReq.deviceId.Manufacturer, "xsd:string"]],
},
],
[
Path.parse("DeviceID.OUI"),
timestamp,
{
object: [timestamp, 0],
writable: [timestamp, 0],
value: [timestamp, [rpcReq.deviceId.OUI, "xsd:string"]],
},
],
[
Path.parse("DeviceID.ProductClass"),
timestamp,
{
object: [timestamp, 0],
writable: [timestamp, 0],
value: [timestamp, [rpcReq.deviceId.ProductClass, "xsd:string"]],
},
],
[
Path.parse("DeviceID.SerialNumber"),
timestamp,
{
object: [timestamp, 0],
writable: [timestamp, 0],
value: [timestamp, [rpcReq.deviceId.SerialNumber, "xsd:string"]],
},
],
];
for (const p of rpcReq.parameterList) {
const path = p[0];
params.push([
path,
timestamp,
{
object: [timestamp, 0],
value: [timestamp, p.slice(1) as [string | number | boolean, string]],
},
]);
}
params.push([
Path.parse("Events.Inform"),
timestamp,
{
object: [timestamp, 0],
writable: [timestamp, 0],
value: [timestamp, [sessionContext.timestamp, "xsd:dateTime"]],
},
]);
for (const e of rpcReq.event) {
params.push([
Path.parse(`Events.${e.replace(/\s+/g, "_")}`),
timestamp,
{
object: [timestamp, 0],
writable: [timestamp, 0],
value: [timestamp, [sessionContext.timestamp, "xsd:dateTime"]],
},
]);
}
if (sessionContext.new) {
params.push([
Path.parse("DeviceID.ID"),
timestamp,
{
object: [timestamp, 0],
writable: [timestamp, 0],
value: [timestamp, [sessionContext.deviceId, "xsd:string"]],
},
]);
params.push([
Path.parse("Events.Registered"),
timestamp,
{
object: [timestamp, 0],
writable: [timestamp, 0],
value: [timestamp, [sessionContext.timestamp, "xsd:dateTime"]],
},
]);
}
sessionContext.deviceData.timestamps.revision = 1;
sessionContext.deviceData.attributes.revision = 1;
let toClear = null;
for (const p of params) {
// Don't need to clear wildcards for Events
if (p[0].segments[0] === "Events") {
device.set(sessionContext.deviceData, p[0], p[1], p[2]);
} else {
toClear = device.set(
sessionContext.deviceData,
p[0],
p[1],
p[2],
toClear
);
}
}
if (toClear) {
for (const c of toClear)
device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]);
}
return { name: "InformResponse" };
}
export async function transferComplete(
sessionContext: SessionContext,
rpcReq: TransferCompleteRequest
): Promise<{ acsResponse: AcsResponse; operation: Operation; 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: "TransferCompleteResponse" },
operation: null,
fault: null,
};
}
const instance = operation.args.instance;
delete sessionContext.operations[commandKey];
if (!sessionContext.operationsTouched) sessionContext.operationsTouched = {};
sessionContext.operationsTouched[commandKey] = 1;
if (rpcReq.faultStruct && rpcReq.faultStruct.faultCode !== "0") {
revertDownloadParameters(sessionContext, operation.args.instance);
const fault: Fault = {
code: `cwmp.${rpcReq.faultStruct.faultCode}`,
message: rpcReq.faultStruct.faultString,
detail: rpcReq.faultStruct,
timestamp: operation.timestamp,
};
return {
acsResponse: { name: "TransferCompleteResponse" },
operation: operation,
fault: fault,
};
}
let toClear = null;
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
let p;
p = sessionContext.deviceData.paths.add(
Path.parse(`Downloads.${instance}.LastDownload`)
);
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
{ value: [timestamp, [operation.timestamp, "xsd:dateTime"]] },
toClear
);
p = sessionContext.deviceData.paths.add(
Path.parse(`Downloads.${instance}.LastFileType`)
);
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
{ value: [timestamp, [operation.args.fileType, "xsd:string"]] },
toClear
);
p = sessionContext.deviceData.paths.add(
Path.parse(`Downloads.${instance}.LastFileName`)
);
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
{ value: [timestamp, [operation.args.fileName, "xsd:string"]] },
toClear
);
p = sessionContext.deviceData.paths.add(
Path.parse(`Downloads.${instance}.LastTargetFileName`)
);
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
{ value: [timestamp, [operation.args.targetFileName, "xsd:string"]] },
toClear
);
p = sessionContext.deviceData.paths.add(
Path.parse(`Downloads.${instance}.StartTime`)
);
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
{ value: [timestamp, [+rpcReq.startTime, "xsd:dateTime"]] },
toClear
);
p = sessionContext.deviceData.paths.add(
Path.parse(`Downloads.${instance}.CompleteTime`)
);
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
{ value: [timestamp, [+rpcReq.completeTime, "xsd:dateTime"]] },
toClear
);
if (toClear) {
for (const c of toClear)
device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]);
}
return {
acsResponse: { name: "TransferCompleteResponse" },
operation: operation,
fault: null,
};
}
function revertDownloadParameters(
sessionContext: SessionContext,
instance
): void {
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
let p;
p = sessionContext.deviceData.paths.add(
Path.parse(`Downloads.${instance}.LastDownload`)
);
const lastDownload = sessionContext.deviceData.attributes.get(p);
p = sessionContext.deviceData.paths.add(
Path.parse(`Downloads.${instance}.Download`)
);
const toClear = device.set(sessionContext.deviceData, p, timestamp, {
value: [
timestamp,
[
lastDownload && lastDownload.value[1] ? lastDownload.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[] }> {
const revision =
(sessionContext.revisions[sessionContext.revisions.length - 1] || 0) + 1;
sessionContext.deviceData.timestamps.revision = revision;
sessionContext.deviceData.attributes.revision = revision;
const faults = [];
const operations = [];
for (const [commandKey, operation] of Object.entries(
sessionContext.operations
)) {
if (operation.name !== "Download")
throw new Error(`Unknown operation name ${operation.name}`);
const DOWNLOAD_TIMEOUT =
+localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.downloadTimeout",
{},
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e)
) * 1000;
if (sessionContext.timestamp > operation.timestamp + DOWNLOAD_TIMEOUT) {
delete sessionContext.operations[commandKey];
if (!sessionContext.operationsTouched)
sessionContext.operationsTouched = {};
sessionContext.operationsTouched[commandKey] = 1;
faults.push({
code: "timeout",
message: "Download operation timed out",
timestamp: operation.timestamp,
});
operations.push(operation);
revertDownloadParameters(sessionContext, operation.args.instance);
}
}
return { faults, operations };
}
export function addProvisions(
sessionContext: SessionContext,
channel: string,
provisions: (string | number | boolean)[][]
): void {
// Multiply by two because every iteration is two
// phases: read and update
const MAX_ITERATIONS =
+localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.maxCommitIterations",
{},
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e)
) * 2;
delete sessionContext.syncState;
delete sessionContext.rpcRequest;
sessionContext.declarations = [];
sessionContext.provisionsRet = [];
if (sessionContext.revisions[sessionContext.revisions.length - 1] > 0) {
sessionContext.deviceData.timestamps.collapse(1);
sessionContext.deviceData.attributes.collapse(1);
sessionContext.revisions = [0];
sessionContext.extensionsCache = {};
}
if (sessionContext.iteration !== sessionContext.cycle * MAX_ITERATIONS) {
sessionContext.cycle += 1;
sessionContext.rpcCount = 0;
sessionContext.iteration = sessionContext.cycle * MAX_ITERATIONS;
}
sessionContext.channels[channel] |= 0;
for (const provision of provisions) {
const channels = [channel];
// Remove duplicate provisions
const provisionStr = JSON.stringify(provision);
for (const [j, p] of sessionContext.provisions.entries()) {
if (JSON.stringify(p) === provisionStr) {
sessionContext.provisions.splice(j, 1);
for (const c of Object.keys(sessionContext.channels)) {
if (sessionContext.channels[c] & (1 << j)) channels.push(c);
const a = sessionContext.channels[c] >> (j + 1);
sessionContext.channels[c] &= (1 << j) - 1;
sessionContext.channels[c] |= a << j;
}
}
}
for (const c of channels)
sessionContext.channels[c] |= 1 << sessionContext.provisions.length;
sessionContext.provisions.push(provision);
}
}
export function clearProvisions(sessionContext: SessionContext): void {
// Multiply by two because every iteration is two
// phases: read and update
const MAX_ITERATIONS =
+localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.maxCommitIterations",
{},
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e)
) * 2;
if (sessionContext.revisions[sessionContext.revisions.length - 1] > 0) {
sessionContext.deviceData.timestamps.collapse(1);
sessionContext.deviceData.attributes.collapse(1);
}
if (sessionContext.iteration !== sessionContext.cycle * MAX_ITERATIONS) {
sessionContext.cycle += 1;
sessionContext.rpcCount = 0;
sessionContext.iteration = sessionContext.cycle * MAX_ITERATIONS;
}
delete sessionContext.syncState;
delete sessionContext.rpcRequest;
sessionContext.provisions = [];
sessionContext.virtualParameters = [];
sessionContext.channels = {};
sessionContext.declarations = [];
sessionContext.provisionsRet = [];
sessionContext.revisions = [0];
sessionContext.extensionsCache = {};
}
async function runProvisions(
sessionContext: SessionContext,
provisions: any[][],
startRevision: number,
endRevision: number
): Promise<ScriptResult> {
const allProvisions = localCache.getProvisions(sessionContext.cacheSnapshot);
const res = await Promise.all(
provisions.map(async (provision) => {
if (!allProvisions[provision[0]]) {
if (defaultProvisions[provision[0]]) {
const dec = [];
let done = true;
let fault = null;
try {
done = defaultProvisions[provision[0]](
sessionContext,
provision,
dec,
startRevision,
endRevision
);
} catch (err) {
fault = {
code: `script.${err.name}`,
message: err.message,
detail: {
name: err.name,
message: err.message,
stack: `${err.name}: ${err.message}\n at ${provision[0]}`,
},
};
}
return {
fault: fault,
clear: null,
declare: dec,
done: done,
returnValue: null,
};
}
return null;
}
return sandbox.run(
allProvisions[provision[0]].script,
{ args: provision.slice(1) },
sessionContext,
startRevision,
endRevision
);
})
);
let done = true;
let allDeclarations = [];
let allClear = [];
let fault;
for (const r of res) {
if (!r) continue;
done = done && r.done;
if (r.declare) allDeclarations = allDeclarations.concat(r.declare);
if (r.clear) allClear = allClear.concat(r.clear);
fault = r.fault || fault;
}
if (done) for (const d of allDeclarations) d.defer = false;
return {
fault: fault,
clear: allClear,
declare: allDeclarations,
done: done,
returnValue: null,
};
}
async function runVirtualParameters(
sessionContext: SessionContext,
provisions: any[][],
startRevision: number,
endRevision: number
): Promise<ScriptResult> {
const allVirtualParameters = localCache.getVirtualParameters(
sessionContext.cacheSnapshot
);
const res = await Promise.all(
provisions.map(async (provision) => {
const globals = {
args: provision.slice(1),
};
const r = await sandbox.run(
allVirtualParameters[provision[0]].script,
globals,
sessionContext,
startRevision,
endRevision
);
if (r.done && !r.fault) {
if (!r.returnValue) {
r.fault = {
code: "script",
message: "Invalid virtual parameter return value",
};
return r;
}
const ret: {
writable?: boolean;
value?: [string | number | boolean, string?];
} = {};
if (r.returnValue.writable != null) {
ret.writable = !!r.returnValue.writable;
} else if (
provision[1].writable != null ||
provision[2].writable != null
) {
r.fault = {
code: "script",
message: `Virtual parameter '${provision[0]}' must provide 'writable' attribute`,
};
return r;
}
if (r.returnValue.value != null) {
let v: string | number | boolean, t: string;
if (Array.isArray(r.returnValue.value)) [v, t] = r.returnValue.value;
else v = r.returnValue.value;
if (!t) {
if (typeof v === "number") t = "xsd:int";
else if (typeof v === "boolean") t = "xsd:boolean";
else if ((v as any) instanceof Date) t = "xsd:datetime";
else t = "xsd:string";
}
if (v == null || !VALID_PARAM_TYPES.has(t)) {
r.fault = {
code: "script",
message: "Invalid virtual parameter value attribute",
};
return r;
}
ret.value = device.sanitizeParameterValue([v, t]);
} else if (provision[1].value != null || provision[2].value != null) {
r.fault = {
code: "script",
message: `Virtual parameter '${provision[0]}' must provide 'value' attribute`,
};
return r;
}
r.returnValue = ret;
}
return r;
})
);
let done = true;
const virtualParameterUpdates = [];
let allDeclarations = [];
let allClear = [];
let fault;
for (const r of res) {
if (!r) {
virtualParameterUpdates.push(null);
continue;
}
done = done && r.done;
if (r.declare) allDeclarations = allDeclarations.concat(r.declare);
if (r.clear) allClear = allClear.concat(r.clear);
virtualParameterUpdates.push(r.returnValue);
fault = r.fault || fault;
}
if (done) for (const d of allDeclarations) d.defer = false;
return {
fault: fault,
clear: allClear,
declare: allDeclarations,
done: done,
returnValue: done ? virtualParameterUpdates : null,
};
}
function runDeclarations(
sessionContext: SessionContext,
declarations: Declaration[]
): VirtualParameterDeclaration[] {
if (!sessionContext.syncState) {
sessionContext.syncState = {
refreshAttributes: {
exist: new Set(),
object: new Set(),
writable: new Set(),
value: new Set(),
notification: new Set(),
accessList: new Set(),
},
spv: new Map(),
spa: new Map(),
gpn: new Set<Path>(),
gpnPatterns: new Map(),
tags: new Map(),
virtualParameterDeclarations: [],
instancesToDelete: new Map(),
instancesToCreate: new Map(),
downloadsToDelete: new Set(),
downloadsToCreate: new InstanceSet(),
downloadsValues: new Map(),
downloadsDownload: new Map(),
reboot: 0,
factoryReset: 0,
};
}
const allDeclareTimestamps = new Map<Path, number>();
const allDeclareAttributeTimestamps = new Map<Path, AttributeTimestamps>();
const allDeclareAttributeValues = new Map<Path, AttributeValues>();
const allVirtualParameters = localCache.getVirtualParameters(
sessionContext.cacheSnapshot
);
function mergeAttributeTimestamps(p: Path, attrs: AttributeTimestamps): void {
let cur = allDeclareAttributeTimestamps.get(p);
if (!cur) {
allDeclareAttributeTimestamps.set(p, attrs);
} else {
cur = Object.assign({}, cur);
for (const [k, v] of Object.entries(attrs))
cur[k] = Math.max(v, cur[k] || 0);
allDeclareAttributeTimestamps.set(p, cur);
}
}
function mergeAttributeValues(
p: Path,
attrs: AttributeValues,
defer: boolean
): void {
let cur = allDeclareAttributeValues.get(p);
if (!cur) {
if (!defer) allDeclareAttributeValues.set(p, attrs);
} else {
cur = Object.assign({}, cur, attrs);
allDeclareAttributeValues.set(p, cur);
}
}
for (const declaration of declarations) {
let path = declaration.path;
let unpacked: Path[];
// Can't run declarations on root
if (!path.length) continue;
if (
(path.alias | path.wildcard) & 1 ||
path.segments[0] === "VirtualParameters"
) {
sessionContext.deviceData.paths.add(Path.parse("VirtualParameters"));
if ((path.alias | path.wildcard) & 2) {
sessionContext.deviceData.paths.add(Path.parse("VirtualParameters.*"));
for (const k of Object.keys(allVirtualParameters)) {
sessionContext.deviceData.paths.add(
Path.parse(`VirtualParameters.${k}`)
);
}
}
}
if ((path.alias | path.wildcard) & 1 || path.segments[0] === "Reboot")
sessionContext.deviceData.paths.add(Path.parse("Reboot"));
if ((path.alias | path.wildcard) & 1 || path.segments[0] === "FactoryReset")
sessionContext.deviceData.paths.add(Path.parse("FactoryReset"));
if (path.alias) {
const aliasDecs = device.getAliasDeclarations(
path,
declaration.pathGet || 1
);
for (const ad of aliasDecs) {
const p = sessionContext.deviceData.paths.add(ad.path);
allDeclareTimestamps.set(
p,
Math.max(ad.pathGet || 1, allDeclareTimestamps.get(p) || 0)
);
let attrTrackers;
if (ad.attrGet) {
attrTrackers = Object.keys(ad.attrGet);
mergeAttributeTimestamps(p, ad.attrGet);
}
device.track(
sessionContext.deviceData,
p,
"prerequisite",
attrTrackers
);
}
unpacked = device.unpack(sessionContext.deviceData, path);
for (const u of unpacked) {
allDeclareTimestamps.set(
u,
Math.max(declaration.pathGet || 1, allDeclareTimestamps.get(u) || 0)
);
if (declaration.attrGet)
mergeAttributeTimestamps(u, declaration.attrGet);
}
} else {
path = sessionContext.deviceData.paths.add(path);
allDeclareTimestamps.set(
path,
Math.max(declaration.pathGet || 1, allDeclareTimestamps.get(path) || 0)
);
if (declaration.attrGet)
mergeAttributeTimestamps(path, declaration.attrGet);
device.track(sessionContext.deviceData, path, "prerequisite");
}
if (declaration.attrSet) {
if (path.alias | path.wildcard) {
if (!unpacked)
unpacked = device.unpack(sessionContext.deviceData, path);
for (const u of unpacked) {
mergeAttributeValues(u, declaration.attrSet, declaration.defer);
// Ensure writable attr is available
mergeAttributeTimestamps(u, { writable: 1 });
}
} else {
mergeAttributeValues(path, declaration.attrSet, declaration.defer);
// Ensure writable attr is available
mergeAttributeTimestamps(path, { writable: 1 });
}
}
if (declaration.pathSet != null) {
let minInstances, maxInstances;
if (Array.isArray(declaration.pathSet)) {
minInstances = declaration.pathSet[0];
maxInstances = declaration.pathSet[1];
} else {
minInstances = maxInstances = declaration.pathSet;
}
let parent = path.slice(0, -1);
let keys;
if (Array.isArray(path.segments[path.length - 1])) {
keys = {};
for (const [p, v] of path.segments[path.length - 1])
keys[p.toString()] = v;
} else if (path.segments[path.length - 1] === "*") {
keys = {};
}
if (!parent.wildcard && !parent.alias) {
parent = sessionContext.deviceData.paths.add(parent);
if (!unpacked)
unpacked = device.unpack(sessionContext.deviceData, path);
// Ensure writable attr is available
mergeAttributeTimestamps(parent, { writable: 1 });
for (const u of unpacked) mergeAttributeTimestamps(u, { writable: 1 });
processInstances(
sessionContext,
parent,
unpacked,
keys,
minInstances,
maxInstances,
declaration.defer
);
} else {
const parentsUnpacked = device.unpack(
sessionContext.deviceData,
parent
);
for (const par of parentsUnpacked) {
const up = device.unpack(
sessionContext.deviceData,
par.concat(path.slice(-1))
);
// Ensure writable attr is available
mergeAttributeTimestamps(par, { writable: 1 });
for (const u of up) mergeAttributeTimestamps(u, { writable: 1 });
processInstances(
sessionContext,
par,
up,
keys,
minInstances,
maxInstances,
declaration.defer
);
}
}
}
}
return processDeclarations(
sessionContext,
allDeclareTimestamps,
allDeclareAttributeTimestamps,
allDeclareAttributeValues
);
}
export async function rpcRequest(
sessionContext: SessionContext,
_declarations: Declaration[]
): Promise<{ fault: Fault; rpcId: string; rpc: AcsRequest }> {
if (sessionContext.rpcRequest != null) {
return {
fault: null,
rpcId: generateRpcId(sessionContext),
rpc: sessionContext.rpcRequest,
};
}
if (
!sessionContext.virtualParameters.length &&
!sessionContext.declarations.length &&
!(_declarations && _declarations.length) &&
!sessionContext.provisions.length
)
return { fault: null, rpcId: null, rpc: null };
if (
sessionContext.declarations.length <=
sessionContext.virtualParameters.length
) {
const inception = sessionContext.declarations.length;
const revision = (sessionContext.revisions[inception] || 0) + 1;
sessionContext.deviceData.timestamps.revision = revision;
sessionContext.deviceData.attributes.revision = revision;
let run, provisions;
if (inception === 0) {
run = runProvisions;
provisions = sessionContext.provisions;
} else {
run = runVirtualParameters;
provisions = sessionContext.virtualParameters[inception - 1];
}
const {
fault,
clear: toClear,
declare: decs,
done: done,
returnValue: ret,
} = await run(
sessionContext,
provisions,
sessionContext.revisions[inception - 1] || 0,
sessionContext.revisions[inception]
);
if (fault) {
fault.timestamp = sessionContext.timestamp;
return { fault: fault, rpcId: null, rpc: null };
}
// Enforce max clear timestamp
for (const c of toClear) {
if (c[1] > sessionContext.timestamp) c[1] = sessionContext.timestamp;
if (c[2]) {
for (const [k, v] of Object.entries(c[2]))
if (v > sessionContext.timestamp) c[2][k] = sessionContext.timestamp;
}
}
sessionContext.declarations.push(decs);
sessionContext.provisionsRet[inception] = inception ? ret : done;
for (const d of decs) {
// Enforce max timestamp
if (d.pathGet > sessionContext.timestamp)
d.pathGet = sessionContext.timestamp;
if (d.attrGet) {
for (const [k, v] of Object.entries(d.attrGet)) {
if (v > sessionContext.timestamp)
d.attrGet[k] = sessionContext.timestamp;
}
}
}
if (toClear) {
for (const c of toClear)
device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]);
}
return rpcRequest(sessionContext, _declarations);
}
if (_declarations && _declarations.length) {
delete sessionContext.syncState;
if (!sessionContext.declarations[0]) sessionContext.declarations[0] = [];
sessionContext.declarations[0] = sessionContext.declarations[0].concat(
_declarations
);
return rpcRequest(sessionContext, null);
}
if (sessionContext.rpcCount >= 255) {
return {
fault: {
code: "too_many_rpcs",
message: "Too many RPC requests",
timestamp: sessionContext.timestamp,
},
rpcId: null,
rpc: null,
};
}
if (sessionContext.revisions.length >= 8) {
return {
fault: {
code: "deeply_nested_vparams",
message:
"Virtual parameters are referencing other virtual parameters in a deeply nested manner",
timestamp: sessionContext.timestamp,
},
rpcId: null,
rpc: null,
};
}
if (sessionContext.cycle >= 255) {
return {
fault: {
code: "too_many_cycles",
message: "Too many provision cycles",
timestamp: sessionContext.timestamp,
},
rpcId: null,
rpc: null,
};
}
// Multiply by two because every iteration is two
// phases: read and update
const MAX_ITERATIONS =
+localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.maxCommitIterations",
{},
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e)
) * 2;
if (sessionContext.iteration >= MAX_ITERATIONS * (sessionContext.cycle + 1)) {
return {
fault: {
code: "too_many_commits",
message: "Too many commit iterations",
timestamp: sessionContext.timestamp,
},
rpcId: null,
rpc: null,
};
}
if (
!(
sessionContext.syncState &&
sessionContext.syncState.virtualParameterDeclarations &&
sessionContext.syncState.virtualParameterDeclarations.length >=
sessionContext.declarations.length
)
) {
const inception =
sessionContext.syncState &&
sessionContext.syncState.virtualParameterDeclarations
? sessionContext.syncState.virtualParameterDeclarations.length
: 0;
// Avoid unnecessary increment of iteration when using vparams
if (inception === sessionContext.declarations.length - 1)
sessionContext.iteration += 2;
let vpd = runDeclarations(
sessionContext,
sessionContext.declarations[inception]
);
const timestamp = sessionContext.timestamp + sessionContext.iteration;
let toClear;
const allVirtualParameters = localCache.getVirtualParameters(
sessionContext.cacheSnapshot
);
vpd = vpd.filter((declaration) => {
if (Object.keys(allVirtualParameters).length) {
if (declaration[0].length === 1) {
// Avoid setting on every inform as "exist" timestamp
// is not saved in DB
if (!sessionContext.deviceData.attributes.has(declaration[0])) {
toClear = device.set(
sessionContext.deviceData,
declaration[0],
timestamp,
{ object: [timestamp, 1], writable: [timestamp, 0] },
toClear
);
}
return false;
} else if (declaration[0].length === 2) {
if (declaration[0].segments[1] === "*") {
for (const k of Object.keys(allVirtualParameters)) {
toClear = device.set(
sessionContext.deviceData,
Path.parse(`VirtualParameters.${k}`),
timestamp,
{
object: [timestamp, 0],
},
toClear
);
}
toClear = device.set(
sessionContext.deviceData,
declaration[0],
timestamp,
null,
toClear
);
return false;
} else if (
allVirtualParameters[declaration[0].segments[1] as string]
) {
// Avoid setting on every inform as "exist" timestamp
// is not saved in DB
if (!sessionContext.deviceData.attributes.has(declaration[0])) {
toClear = device.set(
sessionContext.deviceData,
declaration[0],
timestamp,
{ object: [timestamp, 0] },
toClear
);
}
return true;
}
}
}
for (const p of sessionContext.deviceData.paths.find(
declaration[0],
false,
true
)) {
if (sessionContext.deviceData.attributes.has(p)) {
if (!toClear) toClear = [];
toClear.push([declaration[0], timestamp]);
break;
}
}
return false;
});
if (toClear) {
for (const c of toClear)
device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]);
}
sessionContext.syncState.virtualParameterDeclarations[inception] = vpd;
return rpcRequest(sessionContext, null);
}
if (!sessionContext.syncState) return { fault: null, rpcId: null, rpc: null };
const inception = sessionContext.declarations.length - 1;
let provisions = generateGetVirtualParameterProvisions(
sessionContext,
sessionContext.syncState.virtualParameterDeclarations[inception]
);
if (!provisions) {
sessionContext.rpcRequest = generateGetRpcRequest(sessionContext);
if (!sessionContext.rpcRequest) {
// Only check after read stage is complete to minimize reprocessing of
// declarations especially during initial discovery of data model
if (sessionContext.deviceData.changes.has("prerequisite")) {
delete sessionContext.syncState;
device.clearTrackers(sessionContext.deviceData, "prerequisite");
return rpcRequest(sessionContext, null);
}
let toClear;
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
// Update tags
for (const [p, v] of sessionContext.syncState.tags) {
const c = sessionContext.deviceData.attributes.get(p);
if (v && !c) {
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
{
object: [timestamp, 0],
writable: [timestamp, 1],
value: [timestamp, [true, "xsd:boolean"]],
},
toClear
);
} else if (c && !v) {
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
null,
toClear
);
}
}
// Downloads
let index;
for (const instance of sessionContext.syncState.downloadsToCreate) {
if (index == null) {
index = 0;
for (const p of sessionContext.deviceData.paths.find(
Path.parse("Downloads.*"),
false,
true
)) {
if (
+p.segments[1] > index &&
sessionContext.deviceData.attributes.has(p)
)
index = +p.segments[1];
}
}
++index;
toClear = device.set(
sessionContext.deviceData,
Path.parse("Downloads"),
timestamp,
{ object: [timestamp, 1], writable: [timestamp, 1] },
toClear
);
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Downloads.${index}`),
timestamp,
{ object: [timestamp, 1], writable: [timestamp, 1] },
toClear
);
const params = {
FileType: {
writable: 1,
value: [instance.FileType || "", "xsd:string"],
},
FileName: {
writable: 1,
value: [instance.FileName || "", "xsd:string"],
},
TargetFileName: {
writable: 1,
value: [instance.TargetFileName || "", "xsd:string"],
},
Download: {
writable: 1,
value: [instance.Download || 0, "xsd:dateTime"],
},
LastFileType: { writable: 0, value: ["", "xsd:string"] },
LastFileName: { writable: 0, value: ["", "xsd:string"] },
LastTargetFileName: { writable: 0, value: ["", "xsd:string"] },
LastDownload: { writable: 0, value: [0, "xsd:dateTime"] },
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(`Downloads.${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(`Downloads.${index}.*`),
timestamp,
null,
toClear
);
}
sessionContext.syncState.downloadsToCreate.clear();
for (const instance of sessionContext.syncState.downloadsToDelete) {
toClear = device.set(
sessionContext.deviceData,
instance,
timestamp,
null,
toClear
);
for (const p of sessionContext.syncState.downloadsValues.keys()) {
if (p.segments[1] === instance.segments[1])
sessionContext.syncState.downloadsValues.delete(p);
}
}
sessionContext.syncState.downloadsToDelete.clear();
for (const [p, v] of sessionContext.syncState.downloadsValues) {
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)
device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]);
}
return rpcRequest(sessionContext, null);
}
provisions = generateSetVirtualParameterProvisions(
sessionContext,
sessionContext.syncState.virtualParameterDeclarations[inception]
);
if (!provisions)
sessionContext.rpcRequest = generateSetRpcRequest(sessionContext);
}
}
if (provisions) {
sessionContext.virtualParameters.push(provisions);
sessionContext.revisions.push(sessionContext.revisions[inception]);
return rpcRequest(sessionContext, null);
}
if (sessionContext.rpcRequest) {
return {
fault: null,
rpcId: generateRpcId(sessionContext),
rpc: sessionContext.rpcRequest,
};
}
++sessionContext.revisions[inception];
sessionContext.declarations.pop();
sessionContext.syncState.virtualParameterDeclarations.pop();
const ret = sessionContext.provisionsRet.splice(inception)[0];
if (!ret) return rpcRequest(sessionContext, null);
sessionContext.revisions.pop();
const rev =
sessionContext.revisions[sessionContext.revisions.length - 1] || 0;
sessionContext.deviceData.timestamps.collapse(rev + 1);
sessionContext.deviceData.attributes.collapse(rev + 1);
sessionContext.deviceData.timestamps.revision = rev + 1;
sessionContext.deviceData.attributes.revision = rev + 1;
for (const k of Object.keys(sessionContext.extensionsCache)) {
if (rev < Number(k.split(":", 1)[0]))
delete sessionContext.extensionsCache[k];
}
const vparams = sessionContext.virtualParameters.pop();
if (!vparams) return { fault: null, rpcId: null, rpc: null };
const timestamp = sessionContext.timestamp + sessionContext.iteration;
let toClear;
for (const [i, vpu] of ret.entries()) {
for (const [k, v] of Object.entries(vpu))
vpu[k] = [timestamp + (vparams[i][2][k] != null ? 1 : 0), v];
toClear = device.set(
sessionContext.deviceData,
Path.parse(`VirtualParameters.${vparams[i][0]}`),
timestamp,
vpu,
toClear
);
}
if (toClear) {
for (const c of toClear)
device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]);
}
return rpcRequest(sessionContext, null);
}
function generateGetRpcRequest(
sessionContext: SessionContext
): GetParameterNames | GetParameterValues | GetParameterAttributes {
const syncState = sessionContext.syncState;
if (!syncState) return null;
for (const path of syncState.refreshAttributes.exist) {
let found = false;
for (const p of sessionContext.deviceData.paths.find(
path,
false,
true,
99
)) {
if (
syncState.refreshAttributes.value.has(p) ||
syncState.refreshAttributes.object.has(p) ||
syncState.refreshAttributes.writable.has(p) ||
syncState.refreshAttributes.notification.has(p) ||
syncState.refreshAttributes.accessList.has(p) ||
syncState.gpn.has(p)
) {
found = true;
break;
}
}
if (!found) {
const p = sessionContext.deviceData.paths.add(path.slice(0, -1));
syncState.gpn.add(p);
const f = 1 << p.length;
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p));
}
}
syncState.refreshAttributes.exist.clear();
for (const path of syncState.refreshAttributes.object) {
let found = false;
for (const p of sessionContext.deviceData.paths.find(
path,
false,
true,
99
)) {
if (
syncState.refreshAttributes.value.has(p) ||
(p.length > path.length &&
(syncState.refreshAttributes.object.has(p) ||
syncState.refreshAttributes.writable.has(p) ||
syncState.refreshAttributes.notification.has(p) ||
syncState.refreshAttributes.accessList.has(p)))
) {
found = true;
break;
}
}
if (!found) {
const p = sessionContext.deviceData.paths.add(path.slice(0, -1));
syncState.gpn.add(p);
const f = 1 << p.length;
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p));
}
}
syncState.refreshAttributes.object.clear();
for (const path of syncState.refreshAttributes.writable) {
const p = sessionContext.deviceData.paths.add(path.slice(0, -1));
syncState.gpn.add(p);
const f = 1 << p.length;
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p));
}
syncState.refreshAttributes.writable.clear();
if (syncState.gpn.size) {
const GPN_NEXT_LEVEL = localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.gpnNextLevel",
{},
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e)
);
const paths = Array.from(syncState.gpn.keys()).sort(
(a, b) => b.length - a.length
);
let path = paths.pop();
while (
path &&
path.length &&
!sessionContext.deviceData.attributes.has(path)
) {
syncState.gpn.delete(path);
path = paths.pop();
}
if (path) {
let nextLevel: boolean;
let est = 0;
if (path.length >= GPN_NEXT_LEVEL) {
const patterns: [Path, number][] = [[path, 0]];
for (const p of sessionContext.deviceData.paths.find(
path,
true,
false,
99
)) {
const v = syncState.gpnPatterns.get(p);
if (v) patterns.push([p, (v >> path.length) << path.length]);
}
est = estimateGpnCount(patterns);
}
if (est < Math.pow(2, Math.max(0, 8 - path.length))) {
nextLevel = true;
syncState.gpn.delete(path);
} else {
nextLevel = false;
for (const p of sessionContext.deviceData.paths.find(
path,
false,
true,
99
))
syncState.gpn.delete(p);
}
return {
name: "GetParameterNames",
parameterPath: path.length ? path.toString() + "." : "",
nextLevel: nextLevel,
};
}
}
if (syncState.refreshAttributes.value.size) {
const GPV_BATCH_SIZE = localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.gpvBatchSize",
{},
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e)
);
const parameterNames: string[] = [];
for (const path of syncState.refreshAttributes.value) {
syncState.refreshAttributes.value.delete(path);
// Need to check in case param is deleted or changed to object
const attrs = sessionContext.deviceData.attributes.get(path);
if (attrs && attrs.object && attrs.object[1] === 0) {
parameterNames.push(path.toString());
if (parameterNames.length >= GPV_BATCH_SIZE) break;
}
}
if (parameterNames.length) {
return {
name: "GetParameterValues",
parameterNames: parameterNames,
};
}
}
if (
syncState.refreshAttributes.notification.size ||
syncState.refreshAttributes.accessList.size
) {
const GPV_BATCH_SIZE = localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.gpvBatchSize",
{},
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e)
);
const parameterNames: string[] = [];
for (const path of syncState.refreshAttributes.notification) {
syncState.refreshAttributes.notification.delete(path);
syncState.refreshAttributes.accessList.delete(path);
// Need to check in case param is deleted
const attrs = sessionContext.deviceData.attributes.get(path);
if (attrs) {
parameterNames.push(path.toString());
if (parameterNames.length >= GPV_BATCH_SIZE) break;
}
}
if (parameterNames.length < GPV_BATCH_SIZE) {
for (const path of syncState.refreshAttributes.accessList) {
syncState.refreshAttributes.accessList.delete(path);
const attrs = sessionContext.deviceData.attributes.get(path);
if (attrs) {
parameterNames.push(path.toString());
if (parameterNames.length >= GPV_BATCH_SIZE) break;
}
}
}
if (parameterNames.length) {
return {
name: "GetParameterAttributes",
parameterNames: parameterNames,
};
}
}
return null;
}
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;
return true;
}
function generateSetRpcRequest(
sessionContext: SessionContext
): (
| SetParameterValues
| SetParameterAttributes
| AddObject
| DeleteObject
| FactoryReset
| Reboot
| Download
) & { next?: string } {
const syncState = sessionContext.syncState;
if (!syncState) return null;
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()) {
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) {
const attrs = sessionContext.deviceData.attributes.get(param);
if (attrs && canWrite(attrs)) {
const instance = instances.values().next().value;
if (instance) {
instances.delete(instance);
return {
name: "AddObject",
objectName: param.toString() + ".",
instanceValues: instance,
next: "getInstanceKeys",
};
}
}
}
// Set values
const GPV_BATCH_SIZE = localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.gpvBatchSize",
{},
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e)
);
const DATETIME_MILLISECONDS = !!localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.datetimeMilliseconds",
{},
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e)
);
const BOOLEAN_LITERAL = !!localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.booleanLiteral",
{},
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e)
);
const parameterValues: [string, string | number | boolean, string][] = [];
for (const [k, v] of syncState.spv) {
syncState.spv.delete(k);
const attrs = sessionContext.deviceData.attributes.get(k);
const curVal = attrs.value ? attrs.value[1] : null;
if (curVal && canWrite(attrs)) {
const val = v.slice() as [string | number | boolean, string];
if (!val[1]) val[1] = curVal[1];
device.sanitizeParameterValue(val);
// Strip milliseconds
if (
val[1] === "xsd:dateTime" &&
!DATETIME_MILLISECONDS &&
typeof val[0] === "number"
)
val[0] -= val[0] % 1000;
if (val[0] !== curVal[0] || val[1] !== curVal[1])
parameterValues.push([k.toString(), val[0], val[1]]);
if (parameterValues.length >= GPV_BATCH_SIZE) break;
}
}
if (parameterValues.length) {
return {
name: "SetParameterValues",
parameterList: parameterValues,
DATETIME_MILLISECONDS: DATETIME_MILLISECONDS,
BOOLEAN_LITERAL: BOOLEAN_LITERAL,
};
}
// Set attributes
const parameterAttributes: [string, number, string[]][] = [];
for (const [k, v] of syncState.spa) {
syncState.spa.delete(k);
const attrs = sessionContext.deviceData.attributes.get(k);
if (
v.notification != null &&
(!attrs.notification || v.notification === attrs.notification[1])
)
v.notification = null;
if (
v.accessList != null &&
(!attrs.accessList ||
compareAccessLists(v.accessList, attrs.accessList[1]))
)
v.accessList = null;
if (v.notification != null || v.accessList != null)
parameterAttributes.push([k.toString(), v.notification, v.accessList]);
if (parameterAttributes.length >= GPV_BATCH_SIZE) break;
}
if (parameterAttributes.length) {
return {
name: "SetParameterAttributes",
parameterList: parameterAttributes,
};
}
// Downloads
for (const [p, t] of syncState.downloadsDownload) {
const attrs = deviceData.attributes.get(p);
if (!(attrs && attrs.value && t <= attrs.value[1][0])) {
const fileTypeAttrs = deviceData.attributes.get(
deviceData.paths.get(p.slice(0, -1).concat(Path.parse("FileType")))
);
const fileNameAttrs = deviceData.attributes.get(
deviceData.paths.get(p.slice(0, -1).concat(Path.parse("FileName")))
);
const targetFileNameAttrs = deviceData.attributes.get(
deviceData.paths.get(
p.slice(0, -1).concat(Path.parse("TargetFileName"))
)
);
return {
name: "Download",
commandKey: generateRpcId(sessionContext),
instance: p.segments[1] as string,
fileType: fileTypeAttrs
? fileTypeAttrs.value
? (fileTypeAttrs.value[1][0] as string)
: null
: null,
fileName: fileNameAttrs
? fileNameAttrs.value
? (fileNameAttrs.value[1][0] as string)
: null
: null,
targetFileName: targetFileNameAttrs
? targetFileNameAttrs.value
? (targetFileNameAttrs.value[1][0] as string)
: null
: null,
};
}
}
// Reboot
if (syncState.reboot) {
const p = sessionContext.deviceData.paths.get(Path.parse("Reboot"));
const attrs = p ? sessionContext.deviceData.attributes.get(p) : null;
if (!(attrs && attrs.value && attrs.value[1][0] >= syncState.reboot)) {
delete syncState.reboot;
return { name: "Reboot" };
}
}
// Factory reset
if (syncState.factoryReset) {
const p = sessionContext.deviceData.paths.get(Path.parse("FactoryReset"));
const attrs = p ? sessionContext.deviceData.attributes.get(p) : null;
if (
!(attrs && attrs.value && attrs.value[1][0] >= syncState.factoryReset)
) {
delete syncState.factoryReset;
return { name: "FactoryReset" };
}
}
return null;
}
function generateGetVirtualParameterProvisions(
sessionContext: SessionContext,
virtualParameterDeclarations: VirtualParameterDeclaration[]
): [
string,
AttributeTimestamps,
AttributeValues,
AttributeTimestamps,
AttributeValues
][] {
let provisions;
if (virtualParameterDeclarations) {
for (const declaration of virtualParameterDeclarations) {
if (declaration[1]) {
const currentTimestamps = {};
const currentValues = {};
const dec = {};
const attrs =
sessionContext.deviceData.attributes.get(declaration[0]) || {};
for (const [k, v] of Object.entries(declaration[1])) {
if (k !== "value" && k !== "writable") continue;
if (!attrs[k] || v > attrs[k][0]) dec[k] = v;
}
for (const [k, v] of Object.entries(attrs)) {
currentTimestamps[k] = v[0];
currentValues[k] = v[1];
}
if (Object.keys(dec).length) {
if (!provisions) provisions = [];
provisions.push([
declaration[0].segments[1],
dec,
{},
currentTimestamps,
currentValues,
]);
}
}
}
}
return provisions;
}
function generateSetVirtualParameterProvisions(
sessionContext: SessionContext,
virtualParameterDeclarations: VirtualParameterDeclaration[]
): [
string,
AttributeTimestamps,
AttributeValues,
AttributeTimestamps,
AttributeValues
][] {
let provisions;
if (virtualParameterDeclarations) {
for (const declaration of virtualParameterDeclarations) {
if (declaration[2] && declaration[2].value != null) {
const attrs = sessionContext.deviceData.attributes.get(declaration[0]);
if (
attrs &&
attrs.writable &&
attrs.writable[1] &&
attrs.value &&
attrs.value[1] != null
) {
const val = declaration[2].value.slice() as [
string | number | boolean,
string
];
if (val[1] == null) val[1] = attrs.value[1][1];
device.sanitizeParameterValue(val);
if (val[0] !== attrs.value[1][0] || val[1] !== attrs.value[1][1]) {
if (!provisions) provisions = [];
const currentTimestamps = {};
const currentValues = {};
for (const [k, v] of Object.entries(attrs)) {
currentTimestamps[k] = v[0];
currentValues[k] = v[1];
}
provisions.push([
declaration[0].segments[1],
{},
{ value: val },
currentTimestamps,
currentValues,
]);
}
}
}
}
}
return provisions;
}
function processDeclarations(
sessionContext: SessionContext,
allDeclareTimestamps,
allDeclareAttributeTimestamps: Map<Path, AttributeTimestamps>,
allDeclareAttributeValues: Map<Path, AttributeValues>
): VirtualParameterDeclaration[] {
const deviceData = sessionContext.deviceData;
const syncState = sessionContext.syncState;
const root = sessionContext.deviceData.paths.add(Path.parse(""));
const paths = deviceData.paths.find(root, false, true, 99);
paths.sort((a, b): number =>
a.wildcard === b.wildcard ? a.length - b.length : a.wildcard - b.wildcard
);
const virtualParameterDeclarations = [] as VirtualParameterDeclaration[];
function func(
leafParam: Path,
leafIsObject: number,
leafTimestamp: number,
_paths: Path[]
): void {
const currentPath = _paths[0];
const children = new Map<string, Path[]>();
let declareTimestamp = 0;
let declareAttributeTimestamps;
let declareAttributeValues;
let currentTimestamp = 0;
let currentAttributes;
if (currentPath.wildcard === 0)
currentAttributes = deviceData.attributes.get(currentPath);
for (const path of _paths) {
if (path.length > currentPath.length) {
const fragment = path.segments[currentPath.length] as string;
let child = children.get(fragment);
if (!child) {
if (path.length > currentPath.length + 1) {
// This is to ensure we don't descend more than one step at a time
const p = path.slice(0, currentPath.length + 1);
child = [p];
} else {
child = [];
}
children.set(fragment, child);
}
child.push(path);
continue;
}
currentTimestamp = Math.max(
currentTimestamp,
deviceData.timestamps.get(path) || 0
);
declareTimestamp = Math.max(
declareTimestamp,
allDeclareTimestamps.get(path) || 0
);
if (currentPath.wildcard === 0) {
const attrs = allDeclareAttributeTimestamps.get(path);
if (attrs) {
if (declareAttributeTimestamps) {
declareAttributeTimestamps = Object.assign(
{},
declareAttributeTimestamps
);
for (const [k, v] of Object.entries(attrs)) {
declareAttributeTimestamps[k] = Math.max(
v,
declareAttributeTimestamps[k] || 0
);
}
} else {
declareAttributeTimestamps = attrs;
}
}
declareAttributeValues =
allDeclareAttributeValues.get(path) || declareAttributeValues;
}
}
if (currentAttributes) {
leafParam = currentPath;
leafIsObject = currentAttributes.object
? currentAttributes.object[1]
: null;
// Possible V8 bug causes null === 0
if (leafIsObject != null && leafIsObject === 0)
leafTimestamp = Math.max(leafTimestamp, currentAttributes.object[0]);
} else {
leafTimestamp = Math.max(leafTimestamp, currentTimestamp);
}
switch (
currentPath.segments[0] !== "*"
? currentPath.segments[0]
: leafParam.segments[0]
) {
case "Reboot":
if (currentPath.length === 1) {
if (declareAttributeValues && declareAttributeValues.value)
syncState.reboot = +new Date(declareAttributeValues.value[0]);
}
break;
case "FactoryReset":
if (currentPath.length === 1) {
if (declareAttributeValues && declareAttributeValues.value)
syncState.factoryReset = +new Date(declareAttributeValues.value[0]);
}
break;
case "Tags":
if (
currentPath.length === 2 &&
currentPath.wildcard === 0 &&
declareAttributeValues &&
declareAttributeValues.value
) {
syncState.tags.set(
currentPath,
device.sanitizeParameterValue([
declareAttributeValues.value[0],
"xsd:boolean",
])[0] as boolean
);
}
break;
case "Events":
case "DeviceID":
// Do nothing
break;
case "Downloads":
if (
currentPath.length === 3 &&
currentPath.wildcard === 0 &&
declareAttributeValues &&
declareAttributeValues.value
) {
if (currentPath.segments[2] === "Download") {
syncState.downloadsDownload.set(
currentPath,
declareAttributeValues.value[0]
);
} else {
syncState.downloadsValues.set(
currentPath,
declareAttributeValues.value[0]
);
}
}
break;
case "VirtualParameters":
if (currentPath.length <= 2) {
let d;
if (!(declareTimestamp <= currentTimestamp)) d = [currentPath];
if (currentPath.wildcard === 0) {
if (declareAttributeTimestamps) {
for (const [attrName, attrTimestamp] of Object.entries(
declareAttributeTimestamps
)) {
if (
!(
currentAttributes &&
currentAttributes[attrName] &&
attrTimestamp <= currentAttributes[attrName][0]
)
) {
if (!d) d = [currentPath];
if (!d[1]) d[1] = {};
d[1][attrName] = attrTimestamp;
}
}
}
if (declareAttributeValues) {
if (!d) d = [currentPath];
d[2] = declareAttributeValues;
}
}
if (d) virtualParameterDeclarations.push(d);
}
break;
default:
if (
declareTimestamp > currentTimestamp &&
declareTimestamp > leafTimestamp
) {
if (currentPath === leafParam) {
syncState.refreshAttributes.exist.add(leafParam);
} else if (leafIsObject) {
syncState.gpn.add(leafParam);
if (leafTimestamp > 0) {
const f = 1 << leafParam.length;
syncState.gpnPatterns.set(
leafParam,
f | syncState.gpnPatterns.get(leafParam)
);
} else {
const f =
((1 << currentPath.length) - 1) ^ ((1 << leafParam.length) - 1);
syncState.gpnPatterns.set(
currentPath,
f | syncState.gpnPatterns.get(currentPath)
);
}
} else {
syncState.refreshAttributes.object.add(leafParam);
if (leafIsObject == null) {
const f =
((1 << syncState.gpnPatterns.size) - 1) ^
((1 << leafParam.length) - 1);
syncState.gpnPatterns.set(
currentPath,
f | syncState.gpnPatterns.get(currentPath)
);
}
}
}
if (currentAttributes) {
if (declareAttributeTimestamps) {
for (const [attrName, attrTimestamp] of Object.entries(
declareAttributeTimestamps
)) {
if (
!(
currentAttributes[attrName] &&
attrTimestamp <= currentAttributes[attrName][0]
)
) {
if (attrName === "value") {
if (
!(
currentAttributes.object &&
currentAttributes.object[1] != null
)
)
syncState.refreshAttributes.object.add(currentPath);
else if (currentAttributes.object[1] === 0)
syncState.refreshAttributes.value.add(currentPath);
} else {
syncState.refreshAttributes[attrName].add(currentPath);
}
}
}
}
if (declareAttributeValues) {
if (declareAttributeValues.value != null)
syncState.spv.set(currentPath, declareAttributeValues.value);
if (declareAttributeValues.notification != null) {
const spa = syncState.spa.get(currentPath);
if (spa) {
spa.notification = declareAttributeValues.notification;
} else {
syncState.spa.set(currentPath, {
notification: declareAttributeValues.notification,
accessList: null,
});
}
}
if (declareAttributeValues.accessList != null) {
const spa = syncState.spa.get(currentPath);
if (spa) {
spa.accessList = declareAttributeValues.accessList;
} else {
syncState.spa.set(currentPath, {
notification: null,
accessList: declareAttributeValues.accessList,
});
}
}
}
}
}
for (let [fragment, child] of children) {
// This fine expression avoids duplicate visits, don't ask.
if (
((currentPath.wildcard ^ child[0].wildcard) &
((1 << currentPath.length) - 1)) >>
leafParam.length ===
0
) {
if (fragment !== "*") {
const wildcardChild = children.get("*");
if (wildcardChild) child = child.concat(wildcardChild);
}
func(leafParam, leafIsObject, leafTimestamp, child);
}
}
}
if (
allDeclareTimestamps.size ||
allDeclareAttributeTimestamps.size ||
allDeclareAttributeValues.size
)
func(root, 1, 0, paths);
return virtualParameterDeclarations;
}
function processInstances(
sessionContext: SessionContext,
parent: Path,
parameters: Path[],
keys: Record<string, string>,
minInstances: number,
maxInstances: number,
defer: boolean
): void {
parent = sessionContext.deviceData.paths.add(parent);
let instancesToCreate: InstanceSet, instancesToDelete: Set<Path>;
if (parent.segments[0] === "Downloads") {
if (parent.length !== 1) return;
instancesToDelete = sessionContext.syncState.downloadsToDelete;
instancesToCreate = sessionContext.syncState.downloadsToCreate;
} else {
instancesToDelete = sessionContext.syncState.instancesToDelete.get(parent);
if (instancesToDelete == null) {
instancesToDelete = new Set();
sessionContext.syncState.instancesToDelete.set(parent, instancesToDelete);
}
instancesToCreate = sessionContext.syncState.instancesToCreate.get(parent);
if (instancesToCreate == null) {
instancesToCreate = new InstanceSet();
sessionContext.syncState.instancesToCreate.set(parent, instancesToCreate);
}
}
if (defer && instancesToCreate.size === 0 && instancesToDelete.size === 0)
return;
let counter = 0;
for (const p of parameters) {
++counter;
if (counter > maxInstances) instancesToDelete.add(p);
else if (counter <= minInstances) instancesToDelete.delete(p);
}
// Key is null if deleting a particular instance rather than use alias
if (!keys) return;
for (const inst of instancesToCreate.superset(keys)) {
++counter;
if (counter > maxInstances) instancesToCreate.delete(inst);
}
for (const inst of instancesToCreate.subset(keys)) {
++counter;
if (counter <= minInstances) {
instancesToCreate.delete(inst);
instancesToCreate.add(JSON.parse(JSON.stringify(keys)));
}
}
while (counter < minInstances) {
++counter;
instancesToCreate.add(JSON.parse(JSON.stringify(keys)));
}
}
export async function rpcResponse(
sessionContext: SessionContext,
id: string,
_rpcRes: CpeResponse
): Promise<void> {
if (id !== generateRpcId(sessionContext))
throw new Error("Request ID not recognized");
++sessionContext.rpcCount;
const rpcRes = _rpcRes;
const rpcReq: typeof sessionContext.rpcRequest & { next?: string } =
sessionContext.rpcRequest;
if (!rpcReq.next) {
sessionContext.rpcRequest = null;
} else if (rpcReq.next === "getInstanceKeys") {
const parameterNames = [];
const instanceValues: Record<string, string> = {};
const req = rpcReq as AddObject;
const res = rpcRes as AddObjectResponse;
for (const [k, v] of Object.entries(req.instanceValues)) {
const n = `${req.objectName}${res.instanceNumber}.${k}`;
parameterNames.push(n);
instanceValues[n] = v;
}
if (!parameterNames.length) {
sessionContext.rpcRequest = null;
} else {
const r: GetParameterValues & {
next: "setInstanceKeys";
instanceValues: Record<string, string>;
} = {
name: "GetParameterValues",
parameterNames: parameterNames,
next: "setInstanceKeys",
instanceValues: instanceValues,
};
sessionContext.rpcRequest = r;
}
} else if (rpcReq.next === "setInstanceKeys") {
const req = rpcReq as GetParameterValues & {
instanceValues: Record<string, string>;
};
const res = rpcRes as GetParameterValuesResponse;
const parameterList: [string, string | number | boolean, string][] = [];
for (const p of res.parameterList) {
if (p[1] !== req.instanceValues[p[0].toString()]) {
const v = device.sanitizeParameterValue([
req.instanceValues[p[0].toString()],
p[2] as string,
]);
parameterList.push([p[0].toString(), v[0], v[1]]);
}
}
if (!parameterList.length) {
sessionContext.rpcRequest = null;
} else {
const DATETIME_MILLISECONDS = !!localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.datetimeMilliseconds",
{},
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e)
);
const BOOLEAN_LITERAL = !!localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.booleanLiteral",
{},
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e)
);
const r: SetParameterValues = {
name: "SetParameterValues",
parameterList: parameterList,
DATETIME_MILLISECONDS: DATETIME_MILLISECONDS,
BOOLEAN_LITERAL: BOOLEAN_LITERAL,
};
sessionContext.rpcRequest = r;
}
}
const timestamp = sessionContext.timestamp + sessionContext.iteration;
const revision =
(sessionContext.revisions[sessionContext.revisions.length - 1] || 0) + 1;
sessionContext.deviceData.timestamps.revision = revision;
sessionContext.deviceData.attributes.revision = revision;
let toClear: Clear[];
if (rpcRes.name === "GetParameterValuesResponse") {
if (rpcReq.name !== "GetParameterValues")
throw new Error("Response name does not match request name");
for (const p of rpcRes.parameterList) {
toClear = device.set(
sessionContext.deviceData,
p[0],
timestamp,
{
object: [timestamp, 0],
value: [timestamp, p.slice(1) as [string | number | boolean, 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) {
toClear = device.set(
sessionContext.deviceData,
p[0],
timestamp,
{
notification: [timestamp, p[1] as number],
accessList: [timestamp, p[2] as string[]],
},
toClear
);
}
} else if (rpcRes.name === "GetParameterNamesResponse") {
if (rpcReq.name !== "GetParameterNames")
throw new Error("Response name does not match request name");
let root: Path;
if (rpcReq.parameterPath.endsWith("."))
root = Path.parse(rpcReq.parameterPath.slice(0, -1));
else root = Path.parse(rpcReq.parameterPath);
// Sort to help fill in missing params
rpcRes.parameterList.sort((a, b) => {
const pa = a[0];
const pb = b[0];
const l = Math.min(pa.length, pb.length);
for (let i = 0; i < l; ++i) {
if (pa.segments[i] > pb.segments[i]) return 1;
if (pa.segments[i] < pb.segments[i]) return -1;
}
return pa.length - pb.length;
});
// Fill in missing unreported parent params
for (let idx = 1; idx < rpcRes.parameterList.length; ++idx) {
const prev = rpcRes.parameterList[idx - 1][0];
const cur = rpcRes.parameterList[idx][0];
let offset = 0;
for (let i = cur.length - 2; i >= 0; --i) {
if (i < prev.length && prev.segments[i] === cur.segments[i]) {
// Set object to true in case CPE didn't indicate that it's an object
if (i === prev.length - 1) rpcRes.parameterList[idx - 1][1] = true;
break;
}
// TODO consider showing a warning
rpcRes.parameterList.splice(idx, 0, [cur.slice(0, i + 1), true, true]);
++offset;
}
idx += offset;
}
if (!root.length) {
for (const n of [
"DeviceID",
"Events",
"Tags",
"Reboot",
"FactoryReset",
"VirtualParameters",
"Downloads",
]) {
const p = sessionContext.deviceData.paths.get(Path.parse(n));
if (p && sessionContext.deviceData.attributes.has(p))
sessionContext.deviceData.timestamps.set(p, timestamp);
}
}
const wildcardPath = Path.parse("*");
const wildcardParams: Path[] = [root.concat(wildcardPath)];
for (const [path, object, writable] of rpcRes.parameterList) {
if (object && !rpcReq.nextLevel)
wildcardParams.push(path.concat(wildcardPath));
toClear = device.set(
sessionContext.deviceData,
path,
timestamp,
{
object: [timestamp, object ? 1 : 0],
writable: [timestamp, writable ? 1 : 0],
},
toClear
);
}
for (const path of wildcardParams) {
toClear = device.set(
sessionContext.deviceData,
path,
timestamp,
null,
toClear
);
}
} else if (rpcRes.name === "SetParameterValuesResponse") {
if (rpcReq.name !== "SetParameterValues")
throw new Error("Response name does not match request name");
for (const p of rpcReq.parameterList) {
toClear = device.set(
sessionContext.deviceData,
Path.parse(p[0]),
timestamp + 1,
{
object: [timestamp + 1, 0],
value: [
timestamp + 1,
p.slice(1) as [string | number | boolean, string],
],
},
toClear
);
}
} else if (rpcRes.name === "SetParameterAttributesResponse") {
if (rpcReq.name !== "SetParameterAttributes")
throw new Error("Response name does not match request name");
for (const p of rpcReq.parameterList) {
let attrs;
if (p[1] != null && p[2] != null) {
attrs = {
notification: [timestamp + 1, p[1]],
accessList: [timestamp + 1, p[2]],
};
} else if (p[1] != null) {
attrs = {
notification: [timestamp + 1, p[1]],
};
} else if (p[2] != null) {
attrs = {
accessList: [timestamp + 1, p[2]],
};
}
toClear = device.set(
sessionContext.deviceData,
Path.parse(p[0]),
timestamp + 1,
attrs,
toClear
);
}
} else if (rpcRes.name === "AddObjectResponse") {
if (rpcReq.name !== "AddObject")
throw new Error("Response name does not match request name");
toClear = device.set(
sessionContext.deviceData,
Path.parse(rpcReq.objectName + rpcRes.instanceNumber),
timestamp + 1,
{ object: [timestamp + 1, 1] },
toClear
);
} else if (rpcRes.name === "DeleteObjectResponse") {
if (rpcReq.name !== "DeleteObject")
throw new Error("Response name does not match request name");
toClear = device.set(
sessionContext.deviceData,
Path.parse(rpcReq.objectName.slice(0, -1)),
timestamp + 1,
null,
toClear
);
} else if (rpcRes.name === "RebootResponse") {
if (rpcReq.name !== "Reboot")
throw new Error("Response name does not match request name");
toClear = device.set(
sessionContext.deviceData,
Path.parse("Reboot"),
timestamp + 1,
{ value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] },
toClear
);
} else if (rpcRes.name === "FactoryResetResponse") {
if (rpcReq.name !== "FactoryReset")
throw new Error("Response name does not match request name");
toClear = device.set(
sessionContext.deviceData,
Path.parse("FactoryReset"),
timestamp + 1,
{ value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] },
toClear
);
} else if (rpcRes.name === "DownloadResponse") {
if (rpcReq.name !== "Download")
throw new Error("Response name does not match request name");
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Downloads.${rpcReq.instance}.Download`),
timestamp + 1,
{ value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] },
toClear
);
if (rpcRes.status === 0) {
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Downloads.${rpcReq.instance}.LastDownload`),
timestamp + 1,
{
value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]],
},
toClear
);
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Downloads.${rpcReq.instance}.LastFileType`),
timestamp + 1,
{ value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] },
toClear
);
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Downloads.${rpcReq.instance}.LastFileName`),
timestamp + 1,
{ value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] },
toClear
);
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Downloads.${rpcReq.instance}.LastTargetFileName`),
timestamp + 1,
{ value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] },
toClear
);
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Downloads.${rpcReq.instance}.StartTime`),
timestamp + 1,
{ value: [timestamp + 1, [+rpcRes.startTime, "xsd:dateTime"]] },
toClear
);
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Downloads.${rpcReq.instance}.CompleteTime`),
timestamp + 1,
{ value: [timestamp + 1, [+rpcRes.completeTime, "xsd:dateTime"]] },
toClear
);
} else {
const operation = {
name: "Download",
timestamp: sessionContext.timestamp,
provisions: sessionContext.provisions,
channels: sessionContext.channels,
retries: {},
args: {
instance: rpcReq.instance,
fileType: rpcReq.fileType,
fileName: rpcReq.fileName,
targetFileName: rpcReq.targetFileName,
},
};
for (const channel of Object.keys(sessionContext.channels)) {
if (sessionContext.retries[channel] != null)
operation.retries[channel] = sessionContext.retries[channel];
}
sessionContext.operations[rpcReq.commandKey] = operation;
if (!sessionContext.operationsTouched)
sessionContext.operationsTouched = {};
sessionContext.operationsTouched[rpcReq.commandKey] = 1;
}
} else {
throw new Error("Response name not recognized");
}
if (toClear) {
for (const c of toClear)
device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]);
}
}
export async function rpcFault(
sessionContext: SessionContext,
id: string,
faultResponse: CpeFault
): Promise<Fault> {
const rpcReq = sessionContext.rpcRequest;
delete sessionContext.syncState;
delete sessionContext.rpcRequest;
++sessionContext.rpcCount;
// Recover from invalid parameter name faults
if (faultResponse.detail.faultCode === "9005") {
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
const revision =
(sessionContext.revisions[sessionContext.revisions.length - 1] || 0) + 1;
sessionContext.deviceData.timestamps.revision = revision;
sessionContext.deviceData.attributes.revision = revision;
let toClear: Clear[];
if (rpcReq.name === "GetParameterNames") {
if (rpcReq.parameterPath) {
toClear = [
[Path.parse(rpcReq.parameterPath.replace(/\.$/, "")), timestamp],
];
}
} else if (rpcReq.name === "GetParameterValues") {
toClear = rpcReq.parameterNames.map(
(p) => [Path.parse(p.replace(/\.$/, "")), timestamp] as Clear
);
} else if (rpcReq.name === "SetParameterValues") {
toClear = (rpcReq.parameterList as [
string,
string | number | boolean,
string
][]).map(
(p) => [Path.parse(p[0].replace(/\.$/, "")), timestamp] as Clear
);
} else if (rpcReq.name === "AddObject") {
toClear = [[Path.parse(rpcReq.objectName.replace(/\.$/, "")), timestamp]];
} else if (rpcReq.name === "DeleteObject") {
toClear = [[Path.parse(rpcReq.objectName.replace(/\.$/, "")), timestamp]];
} else if (rpcReq.name === "GetParameterAttributes") {
toClear = rpcReq.parameterNames.map(
(p) => [Path.parse(p.replace(/\.$/, "")), timestamp] as Clear
);
} else if (rpcReq.name === "SetParameterAttributes") {
toClear = (rpcReq.parameterList as [string, number, string[]][]).map(
(p) => [Path.parse(p[0].replace(/\.$/, "")), timestamp] as Clear
);
}
if (toClear) {
for (const c of toClear)
device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]);
}
return null;
}
const fault: Fault = {
code: `cwmp.${faultResponse.detail.faultCode}`,
message: faultResponse.detail.faultString,
detail: faultResponse.detail,
};
return fault;
}
export async function deserialize(
sessionContextString: string
): Promise<SessionContext> {
const sessionContext = JSON.parse(sessionContextString) as SessionContext;
for (const decs of sessionContext.declarations)
for (const d of decs) d.path = Path.parse((d.path as unknown) as string);
const deviceData = initDeviceData();
for (const r of (sessionContext.deviceData as unknown) as any[]) {
const path = deviceData.paths.add(Path.parse(r[0]));
if (r[1]) deviceData.trackers.set(path, r[1]);
if (r[2]) {
deviceData.timestamps.setRevisions(path, r[2]);
if (r[3]) deviceData.attributes.setRevisions(path, r[3]);
}
}
sessionContext.deviceData = deviceData;
// Ensure cache is populated
await localCache.getCurrentSnapshot();
return sessionContext;
}
export async function serialize(
sessionContext: SessionContext
): Promise<string> {
const deviceData = [];
for (const path of sessionContext.deviceData.paths.find(
Path.parse(""),
false,
false,
99
)) {
const e = [
path.toString(),
sessionContext.deviceData.trackers.get(path) || null,
sessionContext.deviceData.timestamps.getRevisions(path) || null,
sessionContext.deviceData.attributes.getRevisions(path) || null,
];
deviceData.push(e);
}
const declarations = sessionContext.declarations.map((decs) => {
return decs.map((d) => Object.assign({}, d, { path: d.path.toString() }));
});
const jsonSessionContext = Object.assign({}, sessionContext, {
deviceData: deviceData,
declarations: declarations,
syncState: null,
toLoad: null,
httpRequest: null,
httpResponse: null,
});
const sessionContextString = JSON.stringify(jsonSessionContext);
return sessionContextString;
}