Tolerate responses with invalid/missing params

This commit is contained in:
Zaid Abdulla committed 2021-07-24 23:18:51 -07:00
1 parent 08cfe4171c
commit cfe279b511
2 files changed
+126 -29

No files matched your search

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