/** * 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 . */ 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, Upload, 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 { 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, }; } delete sessionContext.operations[commandKey]; if (!sessionContext.operationsTouched) sessionContext.operationsTouched = {}; sessionContext.operationsTouched[commandKey] = 1; if (rpcReq.faultStruct && rpcReq.faultStruct.faultCode !== "0") { if (operation.name === "Download") revertDownloadParameters(sessionContext, operation.args.instance); else if (operation.name === "Upload") revertUploadParameters(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; if (operation.name === "Download") { toClear = commitDownloadOperation( sessionContext, operation, rpcReq, timestamp, toClear ); } else if (operation.name === "Upload") { toClear = commitUploadOperation( sessionContext, operation, rpcReq, timestamp, 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 commitDownloadOperation( sessionContext: SessionContext, operation: Operation, rpcReq: TransferCompleteRequest, timestamp: number, toClear: Clear[] ): Clear[] { const instance = operation.args.instance; 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 ); return toClear; } function commitUploadOperation( sessionContext: SessionContext, operation: Operation, rpcReq: TransferCompleteRequest, timestamp: number, toClear: Clear[] ): Clear[] { const instance = operation.args.instance; let p; p = sessionContext.deviceData.paths.add( Path.parse(`Uploads.${instance}.LastUpload`) ); toClear = device.set( sessionContext.deviceData, p, timestamp, { value: [timestamp, [operation.timestamp, "xsd:dateTime"]] }, toClear ); p = sessionContext.deviceData.paths.add( Path.parse(`Uploads.${instance}.LastFileType`) ); toClear = device.set( sessionContext.deviceData, p, timestamp, { value: [timestamp, [operation.args.fileType, "xsd:string"]] }, toClear ); p = sessionContext.deviceData.paths.add( Path.parse(`Uploads.${instance}.LastFileName`) ); toClear = device.set( sessionContext.deviceData, p, timestamp, { value: [timestamp, [operation.args.fileName, "xsd:string"]] }, toClear ); p = sessionContext.deviceData.paths.add( Path.parse(`Uploads.${instance}.StartTime`) ); toClear = device.set( sessionContext.deviceData, p, timestamp, { value: [timestamp, [+rpcReq.startTime, "xsd:dateTime"]] }, toClear ); p = sessionContext.deviceData.paths.add( Path.parse(`Uploads.${instance}.CompleteTime`) ); toClear = device.set( sessionContext.deviceData, p, timestamp, { value: [timestamp, [+rpcReq.completeTime, "xsd:dateTime"]] }, toClear ); return toClear; } function revertUploadParameters( sessionContext: SessionContext, instance ): void { const timestamp = sessionContext.timestamp + sessionContext.iteration + 1; let p; p = sessionContext.deviceData.paths.add( Path.parse(`Uploads.${instance}.LastUpload`) ); const lastUpload = sessionContext.deviceData.attributes.get(p); p = sessionContext.deviceData.paths.add( Path.parse(`Uploads.${instance}.Upload`) ); const toClear = device.set(sessionContext.deviceData, p, timestamp, { value: [ timestamp, [ lastUpload && lastUpload.value[1] ? lastUpload.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]); } } 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") { 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); } } else if (operation.name === "Upload") { const UPLOAD_TIMEOUT = +( localCache.getConfig( sessionContext.cacheSnapshot, "cwmp.uploadTimeout", {}, sessionContext.timestamp, (e) => configContextCallback(sessionContext, e) ) || 3600 ) * 1000; if (sessionContext.timestamp > operation.timestamp + UPLOAD_TIMEOUT) { delete sessionContext.operations[commandKey]; if (!sessionContext.operationsTouched) sessionContext.operationsTouched = {}; sessionContext.operationsTouched[commandKey] = 1; faults.push({ code: "timeout", message: "Upload operation timed out", timestamp: operation.timestamp, }); operations.push(operation); revertUploadParameters(sessionContext, operation.args.instance); } } else { throw new Error(`Unknown operation name ${operation.name}`); } } 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 { 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 { 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(), 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(), uploadsToDelete: new Set(), uploadsToCreate: new InstanceSet(), uploadsValues: new Map(), uploadsUpload: new Map(), reboot: 0, factoryReset: 0, }; } const allDeclareTimestamps = new Map(); const allDeclareAttributeTimestamps = new Map(); const allDeclareAttributeValues = new Map(); 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); } else { mergeAttributeValues(path, declaration.attrSet, declaration.defer); } } 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 ( ((path.wildcard | path.alias) & ((1 << (path.length - 1)) - 1)) === 0 ) { parent = sessionContext.deviceData.paths.add(parent); if (!unpacked) unpacked = device.unpack(sessionContext.deviceData, path); processInstances( sessionContext, parent, unpacked, keys, minInstances, maxInstances, declaration.defer ); } else { const parentsUnpacked = device.unpack( sessionContext.deviceData, parent ); for (const par of parentsUnpacked) { processInstances( sessionContext, par, device.unpack( sessionContext.deviceData, par.concat(path.slice(-1)) ), 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: number; 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 ); } } } } // Uploads toClear = syncUploads(sessionContext, timestamp, 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 syncUploads( sessionContext: SessionContext, timestamp: number, toClear: Clear[] ): Clear[] { let index: number; for (const instance of sessionContext.syncState.uploadsToCreate) { if (index == null) { index = 0; for (const p of sessionContext.deviceData.paths.find( Path.parse("Uploads.*"), false, true )) { if ( +p.segments[1] > index && sessionContext.deviceData.attributes.has(p) ) index = +p.segments[1]; } } ++index; toClear = device.set( sessionContext.deviceData, Path.parse("Uploads"), timestamp, { object: [timestamp, 1], writable: [timestamp, 1] }, toClear ); toClear = device.set( sessionContext.deviceData, Path.parse(`Uploads.${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"], }, Upload: { writable: 1, value: [instance.Upload || 0, "xsd:dateTime"], }, LastFileType: { writable: 0, value: ["", "xsd:string"] }, LastFileName: { writable: 0, value: ["", "xsd:string"] }, LastTargetFileName: { writable: 0, value: ["", "xsd:string"] }, LastUpload: { 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(`Uploads.${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(`Uploads.${index}.*`), timestamp, null, toClear ); } sessionContext.syncState.uploadsToCreate.clear(); for (const instance of sessionContext.syncState.uploadsToDelete) { toClear = device.set( sessionContext.deviceData, instance, timestamp, null, toClear ); for (const p of sessionContext.syncState.uploadsValues.keys()) { if (p.segments[1] === instance.segments[1]) sessionContext.syncState.uploadsValues.delete(p); } } sessionContext.syncState.uploadsToDelete.clear(); for (const [p, v] of sessionContext.syncState.uploadsValues) { 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 ); } } } } return toClear; } 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 | Upload ) & { 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, }; } } // Uploads for (const [p, t] of syncState.uploadsUpload) { 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"))) ); if (fileNameAttrs && fileNameAttrs.value) { return { name: "Upload", commandKey: generateRpcId(sessionContext), instance: p.segments[1] as string, fileType: fileTypeAttrs ? fileTypeAttrs.value ? (fileTypeAttrs.value[1][0] as string) : null : null, fileName: sessionContext.deviceId + "/" + (fileNameAttrs.value[1][0] as string), }; } } } // 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, allDeclareAttributeValues: Map ): 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(); 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 "Uploads": if ( currentPath.length === 3 && currentPath.wildcard === 0 && declareAttributeValues && declareAttributeValues.value ) { if (currentPath.segments[2] === "Upload") { syncState.uploadsUpload.set( currentPath, declareAttributeValues.value[0] ); } else { syncState.uploadsValues.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, minInstances: number, maxInstances: number, defer: boolean ): void { parent = sessionContext.deviceData.paths.add(parent); let instancesToCreate: InstanceSet, instancesToDelete: Set; if (parent.segments[0] === "Downloads") { if (parent.length !== 1) return; instancesToDelete = sessionContext.syncState.downloadsToDelete; instancesToCreate = sessionContext.syncState.downloadsToCreate; } else if (parent.segments[0] === "Uploads") { if (parent.length !== 1) return; instancesToDelete = sessionContext.syncState.uploadsToDelete; instancesToCreate = sessionContext.syncState.uploadsToCreate; } 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 { 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 = {}; 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; } = { name: "GetParameterValues", parameterNames: parameterNames, next: "setInstanceKeys", instanceValues: instanceValues, }; sessionContext.rpcRequest = r; } } else if (rpcReq.next === "setInstanceKeys") { const req = rpcReq as GetParameterValues & { instanceValues: Record; }; 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", "Uploads", ]) { 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 if (rpcRes.name === "UploadResponse") { if (rpcReq.name !== "Upload") throw new Error("Response name does not match request name"); toClear = device.set( sessionContext.deviceData, Path.parse(`Uploads.${rpcReq.instance}.Upload`), timestamp + 1, { value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] }, toClear ); if (rpcRes.status === 0) { toClear = device.set( sessionContext.deviceData, Path.parse(`Uploads.${rpcReq.instance}.LastUpload`), timestamp + 1, { value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]], }, toClear ); toClear = device.set( sessionContext.deviceData, Path.parse(`Uploads.${rpcReq.instance}.LastFileType`), timestamp + 1, { value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] }, toClear ); toClear = device.set( sessionContext.deviceData, Path.parse(`Uploads.${rpcReq.instance}.LastFileName`), timestamp + 1, { value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] }, toClear ); toClear = device.set( sessionContext.deviceData, Path.parse(`Uploads.${rpcReq.instance}.StartTime`), timestamp + 1, { value: [timestamp + 1, [+rpcRes.startTime, "xsd:dateTime"]] }, toClear ); toClear = device.set( sessionContext.deviceData, Path.parse(`Uploads.${rpcReq.instance}.CompleteTime`), timestamp + 1, { value: [timestamp + 1, [+rpcRes.completeTime, "xsd:dateTime"]] }, toClear ); } else { const operation = { name: "Upload", timestamp: sessionContext.timestamp, provisions: sessionContext.provisions, channels: sessionContext.channels, retries: {}, args: { instance: rpcReq.instance, fileType: rpcReq.fileType, fileName: rpcReq.fileName, }, }; 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 { 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 { 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 { 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; }