diff --git a/lib/db.ts b/lib/db.ts index b50062d..3a994c5 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -47,6 +47,12 @@ export let tasksCollection: Collection, let clientPromise: Promise; export let client: MongoClient; +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; +} + export async function connect(): Promise { clientPromise = MongoClient.connect("" + get("MONGODB_CONNECTION_URL"), { useNewUrlParser: true @@ -118,6 +124,8 @@ export async function fetchDevice( function storeParams(obj, path: string, pathLength: number, ts): void { if (obj["_timestamp"]) obj["_timestamp"] = +obj["_timestamp"]; + if (obj["_attributesTimestamp"]) + obj["_attributesTimestamp"] = +obj["_attributesTimestamp"]; const attrs: Attributes = {}; let t = obj["_timestamp"] || 1; @@ -135,6 +143,16 @@ export async function fetchDevice( if (obj["_object"] != null) attrs.object = [t, obj["_object"] ? 1 : 0]; + if (obj["_notification"] != null) { + attrs.notification = [ + obj["_attributesTimestamp"] || 1, + obj["_notification"] + ]; + } + + if (obj["_accessList"] != null) + attrs.accessList = [obj["_attributesTimestamp"] || 1, obj["_accessList"]]; + res.push([Path.parse(path.slice(0, -1)), t, attrs]); for (const [k, v] of Object.entries(obj)) { @@ -331,6 +349,12 @@ export async function saveDevice( const object2 = ((diff[2] || {}).object || [])[1]; const writable2 = ((diff[2] || {}).writable || [])[1]; const writable1 = ((diff[1] || {}).writable || [])[1]; + const attributesTimestamp1 = ((diff[1] || {}).notification || [])[0]; + const attributesTimestamp2 = ((diff[2] || {}).notification || [])[0]; + const notification1 = ((diff[1] || {}).notification || [])[1]; + const notification2 = ((diff[2] || {}).notification || [])[1]; + const accessList1 = ((diff[1] || {}).accessList || [])[1]; + const accessList2 = ((diff[2] || {}).accessList || [])[1]; switch (path.segments[0]) { case "Events": @@ -457,6 +481,46 @@ export async function saveDevice( path.length ? path.toString() + "._writable" : "_writable" ] = !!writable2; } + + break; + case "notification": + if ( + !diff[1] || + !diff[1].notification || + notification2 !== notification1 + ) { + update["$set"][ + path.length + ? path.toString() + "._notification" + : "_notification" + ] = notification2; + } + + if (attributesTimestamp2 !== attributesTimestamp1) { + update["$set"][ + path.toString() + "._attributesTimestamp" + ] = new Date(attributesTimestamp2); + } + + break; + case "accessList": + if ( + !diff[1] || + !diff[1].accessList || + !compareAccessLists(accessList2, accessList1) + ) { + update["$set"][ + path.length + ? path.toString() + "._accessList" + : "_accessList" + ] = accessList2; + } + + if (attributesTimestamp2 !== attributesTimestamp1) { + update["$set"][ + path.toString() + "._attributesTimestamp" + ] = new Date(attributesTimestamp2); + } } } } @@ -472,6 +536,12 @@ export async function saveDevice( if (attrName === "value") { update["$unset"][p + "_type"] = 1; update["$unset"][p + "_timestamp"] = 1; + } else if (attrName === "notification") { + if (accessList2 == null) + update["$unset"][`${p}_attributesTimestamp`] = 1; + } else if (attrName === "accessList") { + if (notification2 == null) + update["$unset"][`${p}_attributesTimestamp`] = 1; } } } diff --git a/lib/device.ts b/lib/device.ts index a42a09b..1822f3d 100644 --- a/lib/device.ts +++ b/lib/device.ts @@ -23,7 +23,9 @@ import { DeviceData, Declaration, Clear } from "./types"; const CHANGE_FLAGS = { object: 2, writable: 4, - value: 8 + value: 8, + notification: 16, + accessList: 32 }; function parseBool(v): boolean { diff --git a/lib/session.ts b/lib/session.ts index b3a2336..931bff2 100644 --- a/lib/session.ts +++ b/lib/session.ts @@ -729,9 +729,12 @@ function runDeclarations( exist: new Set(), object: new Set(), writable: new Set(), - value: 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(), @@ -1463,6 +1466,8 @@ function generateGetRpcRequest(sessionContext: SessionContext): GetAcsRequest { 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; @@ -1491,7 +1496,9 @@ function generateGetRpcRequest(sessionContext: SessionContext): GetAcsRequest { syncState.refreshAttributes.value.has(p) || (p.length > path.length && (syncState.refreshAttributes.object.has(p) || - syncState.refreshAttributes.writable.has(p))) + syncState.refreshAttributes.writable.has(p) || + syncState.refreshAttributes.notification.has(p) || + syncState.refreshAttributes.accessList.has(p))) ) { found = true; break; @@ -1599,9 +1606,57 @@ function generateGetRpcRequest(sessionContext: SessionContext): GetAcsRequest { }; } } + + if ( + syncState.refreshAttributes.notification.size || + syncState.refreshAttributes.accessList.size + ) { + const GPV_BATCH_SIZE = localCache.getConfig( + sessionContext.cacheSnapshot, + "cwmp.gpvBatchSize", + sessionContext.configContext + ); + + 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): SetAcsRequest { const syncState = sessionContext.syncState; if (!syncState) return null; @@ -1654,7 +1709,7 @@ function generateSetRpcRequest(sessionContext: SessionContext): SetAcsRequest { sessionContext.configContext ); - const parameterValues: [Path, string | number | boolean, string][] = []; + 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); @@ -1673,7 +1728,7 @@ function generateSetRpcRequest(sessionContext: SessionContext): SetAcsRequest { val[0] -= val[0] % 1000; if (val[0] !== curVal[0] || val[1] !== curVal[1]) - parameterValues.push([k, val[0], val[1]]); + parameterValues.push([k.toString(), val[0], val[1]]); if (parameterValues.length >= GPV_BATCH_SIZE) break; } @@ -1682,19 +1737,44 @@ function generateSetRpcRequest(sessionContext: SessionContext): SetAcsRequest { if (parameterValues.length) { return { name: "SetParameterValues", - parameterList: parameterValues.map( - p => - [p[0].toString(), p[1], p[2]] as [ - string, - string | number | boolean, - string - ] - ), + 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); @@ -2109,8 +2189,34 @@ function processDeclarations( } } } - if (declareAttributeValues && declareAttributeValues.value != null) - syncState.spv.set(currentPath, declareAttributeValues.value); + 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 + }); + } + } + } } } @@ -2243,7 +2349,7 @@ export async function rpcResponse( if (p[1] !== rpcReq.instanceValues[p[0]]) { const v = device.sanitizeParameterValue([ rpcReq.instanceValues[p[0]], - p[2] + p[2] as string ]); parameterList.push([p[0], v[0], v[1]]); } @@ -2288,6 +2394,21 @@ export async function rpcResponse( ); } + break; + case "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, + Path.parse(p[0]), + timestamp, + { notification: [timestamp, p[1]], accessList: [timestamp, p[2]] }, + toClear + ); + } + break; case "GetParameterNamesResponse": if (rpcReq.name !== "GetParameterNames") @@ -2404,6 +2525,38 @@ export async function rpcResponse( ); } + break; + case "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 + ); + } + break; case "AddObjectResponse": toClear = device.set( @@ -2566,9 +2719,11 @@ export async function rpcFault( p => [Path.parse(p.replace(/\.$/, "")), timestamp] as Clear ); } else if (rpcReq.name === "SetParameterValues") { - toClear = rpcReq.parameterList.map( - p => [Path.parse(p[0].replace(/\.$/, "")), timestamp] as Clear - ); + 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") { diff --git a/lib/soap.ts b/lib/soap.ts index f7c4389..0144125 100644 --- a/lib/soap.ts +++ b/lib/soap.ts @@ -188,6 +188,45 @@ function parameterValueList( }); } +function parameterAttributeList(xml: Element): [string, number, string[]][] { + return xml.children + .filter(e => e.localName === "ParameterAttributeStruct") + .map<[string, number, string[]]>(e => { + let notificationElement: Element, + accessListElement: Element, + param: string; + for (const c of e.children) { + switch (c.localName) { + case "Name": + param = c.text; + break; + case "Notification": + notificationElement = c; + break; + case "AccessList": + accessListElement = c; + break; + } + } + + const notification = decodeEntities(notificationElement.text); + const notif = +notification; + + if (notif !== parseInt(notification)) { + warnings.push({ + message: "Invalid notification attribute", + parameter: param + }); + } + + const accessList = accessListElement.children + .filter(c => c.localName === "string") + .map(c => decodeEntities(c.text)); + + return [param, notif, accessList]; + }); +} + function GetParameterNames(methodRequest): string { return `${ methodRequest.parameterPath @@ -220,6 +259,23 @@ function GetParameterValuesResponse(xml: Element): CpeGetResponse { }; } +function GetParameterAttributes(methodRequest): string { + return `${methodRequest.parameterNames + .map(p => `${p}`) + .join("")}`; +} + +function GetParameterAttributesResponse(xml: Element): CpeGetResponse { + return { + name: "GetParameterAttributesResponse", + parameterList: parameterAttributeList( + xml.children.find(n => n.localName === "ParameterList") + ) + }; +} + function SetParameterValues(methodRequest): string { const params = methodRequest.parameterList.map(p => { let val = p[1]; @@ -250,6 +306,34 @@ function SetParameterValuesResponse(xml: Element): CpeSetResponse { }; } +function SetParameterAttributes(methodRequest): string { + const params = methodRequest.parameterList.map(p => { + return `${ + p[0] + }${ + p[1] == null ? "false" : "true" + }${ + p[1] == null ? "" : p[1] + }${ + p[2] == null ? "false" : "true" + }${ + p[2] == null + ? "" + : p[2].map(s => `${encodeEntities(s)}`).join("") + }`; + }); + + return `${params.join("")}`; +} + +function SetParameterAttributesResponse(): CpeSetResponse { + return { + name: "SetParameterAttributesResponse" + }; +} + function AddObject(methodRequest): string { return `${ methodRequest.objectName @@ -610,9 +694,15 @@ export function request(body: string, cwmpVersion, warn): SoapMessage { case "GetParameterValuesResponse": rpc.cpeResponse = GetParameterValuesResponse(methodElement); break; + case "GetParameterAttributesResponse": + rpc.cpeResponse = GetParameterAttributesResponse(methodElement); + break; case "SetParameterValuesResponse": rpc.cpeResponse = SetParameterValuesResponse(methodElement); break; + case "SetParameterAttributesResponse": + rpc.cpeResponse = SetParameterAttributesResponse(); + break; case "AddObjectResponse": rpc.cpeResponse = AddObjectResponse(methodElement); break; @@ -690,9 +780,15 @@ export function response(rpc): { code: number; headers: {}; data: string } { case "GetParameterValues": body = GetParameterValues(rpc.acsRequest); break; + case "GetParameterAttributes": + body = GetParameterAttributes(rpc.acsRequest); + break; case "SetParameterValues": body = SetParameterValues(rpc.acsRequest); break; + case "SetParameterAttributes": + body = SetParameterAttributes(rpc.acsRequest); + break; case "AddObject": body = AddObject(rpc.acsRequest); break; diff --git a/lib/types.ts b/lib/types.ts index e743139..f96cac6 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -51,18 +51,24 @@ export interface Attributes { object?: [number, 1 | 0]; writable?: [number, 1 | 0]; value?: [number, [string | number | boolean, string]]; + notification?: [number, number]; + accessList?: [number, string[]]; } export interface AttributeTimestamps { object?: number; writable?: number; value?: number; + notification?: number; + accessList?: number; } export interface AttributeValues { object?: boolean; writable?: boolean; value?: [string | number | boolean, string?]; + notification?: number; + accessList?: string[]; } export interface DeviceData { @@ -75,12 +81,21 @@ export interface DeviceData { export type VirtualParameterDeclaration = [ Path, - { path?: number; object?: number; writable?: number; value?: number }?, + { + path?: number; + object?: number; + writable?: number; + value?: number; + notification?: number; + accessList?: number; + }?, { path?: [number, number]; object?: boolean; writable?: boolean; value?: [string | number | boolean, string?]; + notification?: number; + accessList?: string[]; }? ]; @@ -90,8 +105,11 @@ export interface SyncState { object: Set; writable: Set; value: Set; + notification: Set; + accessList: Set; }; spv: Map; + spa: Map; gpn: Set; gpnPatterns: Map; tags: Map; @@ -182,7 +200,7 @@ export interface AcsRequest { } export interface GetAcsRequest extends AcsRequest { - name: "GetParameterNames" | "GetParameterValues"; + name: "GetParameterNames" | "GetParameterValues" | "GetParameterAttributes"; objectName?: string; parameterNames?: string[]; parameterPath?: string; @@ -193,12 +211,15 @@ export interface GetAcsRequest extends AcsRequest { export interface SetAcsRequest extends AcsRequest { name: | "SetParameterValues" + | "SetParameterAttributes" | "AddObject" | "DeleteObject" | "FactoryReset" | "Reboot" | "Download"; - parameterList?: [string, string | number | boolean, string][]; + parameterList?: + | [string, string | number | boolean, string][] + | [string, number, string[]][]; instanceValues?: { [param: string]: string | number | boolean }; objectName?: string; DATETIME_MILLISECONDS?: boolean; @@ -235,15 +256,20 @@ export interface CpeFault { } export interface CpeGetResponse extends CpeResponse { - name: "GetParameterNamesResponse" | "GetParameterValuesResponse"; + name: + | "GetParameterNamesResponse" + | "GetParameterValuesResponse" + | "GetParameterAttributesResponse"; parameterList?: | [string, boolean][] - | [string, string | number | boolean, string][]; + | [string, string | number | boolean, string][] + | [string, number, string[]][]; } export interface CpeSetResponse extends CpeResponse { name: | "SetParameterValuesResponse" + | "SetParameterAttributesResponse" | "AddObjectResponse" | "DeleteObjectResponse" | "RebootResponse" @@ -300,11 +326,19 @@ export interface Declaration { path: Path; pathGet: number; pathSet?: [number, number]; - attrGet?: { object?: number; writable?: number; value?: number }; + attrGet?: { + object?: number; + writable?: number; + value?: number; + notification?: number; + accessList?: number; + }; attrSet?: { object?: boolean; writable?: boolean; value?: [string | number | boolean, string?]; + notification?: number; + accessList?: string[]; }; defer: boolean; } @@ -312,7 +346,13 @@ export interface Declaration { export type Clear = [ Path, number, - { object?: number; writable?: number; value?: number }?, + { + object?: number; + writable?: number; + value?: number; + notification?: number; + accessList?: number; + }?, number? ];