diff --git a/lib/session.ts b/lib/session.ts index 7dbc8b8..1164a4d 100644 --- a/lib/session.ts +++ b/lib/session.ts @@ -59,6 +59,7 @@ import { GetParameterValuesResponse, } from "./types"; import { getRequestOrigin } from "./forwarded"; +import * as logger from "./logger"; const VALID_PARAM_TYPES = new Set([ "xsd:int", @@ -2420,8 +2421,15 @@ export async function rpcResponse( id: string, _rpcRes: CpeResponse ): Promise { + function invalidResponse(message: string): Fault { + return { + code: "invalid_response", + message: message, + }; + } + if (id !== generateRpcId(sessionContext)) - throw new Error("Request ID not recognized"); + return invalidResponse("Request ID not recognized"); ++sessionContext.rpcCount; @@ -2512,47 +2520,99 @@ export async function rpcResponse( if (rpcRes.name === "GetParameterValuesResponse") { if (rpcReq.name !== "GetParameterValues") - throw new Error("Response name does not match request name"); + return invalidResponse("Response name does not match request name"); - if (rpcRes.parameterList.length !== rpcReq.parameterNames.length) { - return { - code: `invalid_response`, - message: - "The list of parameters returned in the GetParameterValuesResponse message does not match the requested parameters", - }; - } + const requested = new Set(rpcReq.parameterNames); - for (const p of rpcRes.parameterList) { + for (const [path, value, type] of rpcRes.parameterList) { + if (!requested.delete(path.toString())) { + logger.accessWarn({ + sessionContext: sessionContext, + message: "Unexpected parameter in response", + parameter: path.toString(), + }); + continue; + } toClear = device.set( sessionContext.deviceData, - p[0], + path, timestamp, { object: [timestamp, 0], - value: [timestamp, p.slice(1) as [string | number | boolean, string]], + value: [timestamp, [value, type]], }, toClear ); } + + if (requested.size) { + for (const p of requested) { + logger.accessWarn({ + sessionContext: sessionContext, + message: "Missing parameter in response", + parameter: p, + }); + toClear = device.set( + sessionContext.deviceData, + Path.parse(p), + timestamp, + { + object: [timestamp, 0], + value: [timestamp, ["", "xsd:string"]], + }, + toClear + ); + } + } } else if (rpcRes.name === "GetParameterAttributesResponse") { if (rpcReq.name !== "GetParameterAttributes") throw new Error("Response name does not match request name"); - for (const p of rpcRes.parameterList) { + const requested = new Set(rpcReq.parameterNames); + + for (const [path, notification, accessList] of rpcRes.parameterList) { + if (!requested.delete(path.toString())) { + logger.accessWarn({ + sessionContext: sessionContext, + message: "Unexpected parameter in response", + parameter: path.toString(), + }); + continue; + } toClear = device.set( sessionContext.deviceData, - p[0], + path, timestamp, { - notification: [timestamp, p[1] as number], - accessList: [timestamp, p[2] as string[]], + notification: [timestamp, notification], + accessList: [timestamp, accessList], }, toClear ); } + + if (requested.size) { + for (const p of requested) { + logger.accessWarn({ + sessionContext: sessionContext, + message: "Missing parameter in response", + parameter: p, + }); + toClear = device.set( + sessionContext.deviceData, + Path.parse(p), + timestamp, + { + notification: [timestamp, 0], + accessList: [timestamp, []], + }, + toClear + ); + } + } } else if (rpcRes.name === "GetParameterNamesResponse") { if (rpcReq.name !== "GetParameterNames") - throw new Error("Response name does not match request name"); + return invalidResponse("Response name does not match request name"); let root: Path; if (rpcReq.parameterPath.endsWith(".")) @@ -2609,6 +2669,14 @@ export async function rpcResponse( const wildcardParams: Path[] = [root.concat(wildcardPath)]; for (const [path, object, writable] of rpcRes.parameterList) { + if (!path.toString().startsWith(rpcReq.parameterPath)) { + logger.accessWarn({ + sessionContext: sessionContext, + message: "Unexpected parameter in response", + parameter: path.toString(), + }); + continue; + } if (object && !rpcReq.nextLevel) wildcardParams.push(path.concat(wildcardPath)); @@ -2635,7 +2703,7 @@ export async function rpcResponse( } } else if (rpcRes.name === "SetParameterValuesResponse") { if (rpcReq.name !== "SetParameterValues") - throw new Error("Response name does not match request name"); + return invalidResponse("Response name does not match request name"); for (const p of rpcReq.parameterList) { toClear = device.set( @@ -2654,7 +2722,7 @@ export async function rpcResponse( } } else if (rpcRes.name === "SetParameterAttributesResponse") { if (rpcReq.name !== "SetParameterAttributes") - throw new Error("Response name does not match request name"); + return invalidResponse("Response name does not match request name"); for (const p of rpcReq.parameterList) { let attrs; @@ -2684,7 +2752,7 @@ export async function rpcResponse( } } else if (rpcRes.name === "AddObjectResponse") { if (rpcReq.name !== "AddObject") - throw new Error("Response name does not match request name"); + return invalidResponse("Response name does not match request name"); toClear = device.set( sessionContext.deviceData, @@ -2695,7 +2763,7 @@ export async function rpcResponse( ); } else if (rpcRes.name === "DeleteObjectResponse") { if (rpcReq.name !== "DeleteObject") - throw new Error("Response name does not match request name"); + return invalidResponse("Response name does not match request name"); toClear = device.set( sessionContext.deviceData, @@ -2706,7 +2774,7 @@ export async function rpcResponse( ); } else if (rpcRes.name === "RebootResponse") { if (rpcReq.name !== "Reboot") - throw new Error("Response name does not match request name"); + return invalidResponse("Response name does not match request name"); toClear = device.set( sessionContext.deviceData, @@ -2717,7 +2785,7 @@ export async function rpcResponse( ); } else if (rpcRes.name === "FactoryResetResponse") { if (rpcReq.name !== "FactoryReset") - throw new Error("Response name does not match request name"); + return invalidResponse("Response name does not match request name"); toClear = device.set( sessionContext.deviceData, @@ -2728,7 +2796,7 @@ export async function rpcResponse( ); } else if (rpcRes.name === "DownloadResponse") { if (rpcReq.name !== "Download") - throw new Error("Response name does not match request name"); + return invalidResponse("Response name does not match request name"); toClear = device.set( sessionContext.deviceData, @@ -2814,7 +2882,7 @@ export async function rpcResponse( sessionContext.operationsTouched[rpcReq.commandKey] = 1; } } else { - throw new Error("Response name not recognized"); + return invalidResponse("Response name not recognized"); } if (toClear) { diff --git a/lib/soap.ts b/lib/soap.ts index 9cfd899..8e9fd25 100644 --- a/lib/soap.ts +++ b/lib/soap.ts @@ -109,8 +109,8 @@ function event(xml: Element): string[] { function parameterInfoList(xml: Element): [Path, boolean, boolean][] { return xml.children - .filter((e) => e.localName === "ParameterInfoStruct") .map<[Path, boolean, boolean]>((e) => { + if (e.localName !== "ParameterInfoStruct") return null; let param: string, value: string; for (const c of e.children) { switch (c.localName) { @@ -134,10 +134,20 @@ function parameterInfoList(xml: Element): [Path, boolean, boolean][] { parsed = false; } - if (param && !param.endsWith(".")) - return [Path.parse(param), false, parsed]; - else return [Path.parse(param.slice(0, -1)), true, parsed]; - }); + try { + if (param && !param.endsWith(".")) + return [Path.parse(param), false, parsed]; + else return [Path.parse(param.slice(0, -1)), true, parsed]; + } catch (err) { + warnings.push({ + message: "Missing or invalid XML node", + element: "Name", + parameter: param, + }); + return null; + } + }) + .filter((e) => e != null); } const getValueType = memoize((str: string) => { @@ -151,8 +161,8 @@ function parameterValueList( xml: Element ): [Path, string | number | boolean, string][] { return xml.children - .filter((e) => e.localName === "ParameterValueStruct") .map<[Path, string | number | boolean, string]>((e) => { + if (e.localName !== "ParameterValueStruct") return null; let valueElement: Element, param: string; for (const c of e.children) { switch (c.localName) { @@ -208,15 +218,24 @@ function parameterValueList( parsed = value; } } - - return [Path.parse(param), parsed, valueType]; - }); + try { + return [Path.parse(param), parsed, valueType]; + } catch (err) { + warnings.push({ + message: "Missing or invalid XML node", + element: "Name", + parameter: param, + }); + return null; + } + }) + .filter((e) => e != null); } function parameterAttributeList(xml: Element): [Path, number, string[]][] { return xml.children - .filter((e) => e.localName === "ParameterAttributeStruct") .map<[Path, number, string[]]>((e) => { + if (e.localName !== "ParameterAttributeStruct") return null; let notificationElement: Element, accessListElement: Element, param: string; @@ -248,8 +267,18 @@ function parameterAttributeList(xml: Element): [Path, number, string[]][] { .filter((c) => c.localName === "string") .map((c) => decodeEntities(c.text)); - return [Path.parse(param), notification, accessList]; - }); + try { + return [Path.parse(param), notification, accessList]; + } catch (err) { + warnings.push({ + message: "Missing or invalid XML node", + element: "Name", + parameter: param, + }); + return null; + } + }) + .filter((e) => e != null); } function GetParameterNames(methodRequest): string {