Implement SOAP processor using our new XML parser

This commit is contained in:
Zaid Abdulla committed 2019-06-05 00:17:36 -07:00
1 parent bec74a4995
commit f4c9321d4d
4 files changed
+1051 -302

No files matched your search

+1
View File
@@ -84,6 +84,7 @@ const options = {
XML_FORMAT: { type: "bool", default: false },
XML_NO_DECL: { type: "bool", default: false },
XML_NO_EMPTY: { type: "bool", default: false },
XML_LIBXMLJS: { type: "bool", default: false },
// Should probably never be changed
DEVICE_ONLINE_THRESHOLD: { type: "int", default: 4000 }
+27 -5
View File
@@ -24,7 +24,8 @@ import { Socket } from "net";
import * as auth from "./auth";
import * as config from "./config";
import * as common from "./common";
import * as soap from "./soap";
import * as soapCustom from "./soap";
import * as soapLibxmljs from "./soap-libxmljs";
import * as session from "./session";
import { evaluateAsync, evaluate, extractParams } from "./common/expression";
import * as device from "./device";
@@ -50,7 +51,13 @@ import { IncomingMessage, ServerResponse } from "http";
import { Readable } from "stream";
import { promisify } from "util";
import { TLSSocket } from "tls";
import { dependencies as DEPENDENCIES } from "../package.json";
import { parseXmlDeclaration } from "./xml-parser";
const soap =
DEPENDENCIES["libxmljs"] && config.get("XML_LIBXMLJS")
? soapLibxmljs
: soapCustom;
const gzipPromisified = promisify(zlib.gzip);
const deflatePromisified = promisify(zlib.deflate);
@@ -70,7 +77,7 @@ const stats = {
async function authenticate(
sessionContext: SessionContext,
body: string | Buffer
body: string
): Promise<boolean> {
const authExpression: Expression = localCache.getConfigExpression(
sessionContext.cacheSnapshot,
@@ -1220,11 +1227,26 @@ async function listenerAsync(
return;
}
let charset;
if (httpRequest.headers["content-type"]) {
const m = httpRequest.headers["content-type"].match(
/charset=['"]?([^'"\s]+)/i
);
if (m) charset = m[1].toLowerCase();
}
if (!charset) {
const e = parseXmlDeclaration(body).find(s => s.localName === "encoding");
charset = e ? e.value : "utf8";
}
const bodyStr = body.toString(charset);
const parseWarnings = [];
let rpc;
try {
rpc = soap.request(
body,
bodyStr,
sessionContext ? sessionContext.cwmpVersion : null,
parseWarnings
);
@@ -1246,7 +1268,7 @@ async function listenerAsync(
// Authenticate in case of new connection or unauthenticated session
const authenticated =
sessionContext.state === 0 || newConnection
? await authenticate(sessionContext, body)
? await authenticate(sessionContext, bodyStr)
: true;
if (!authenticated) return responseUnauthorized(sessionContext);
@@ -1328,7 +1350,7 @@ async function listenerAsync(
}
}
const authenticated = await authenticate(_sessionContext, body);
const authenticated = await authenticate(_sessionContext, bodyStr);
if (!authenticated) return responseUnauthorized(_sessionContext);
parsedRpc(_sessionContext, rpc, parseWarnings);
+747
View File
@@ -0,0 +1,747 @@
/**
* Copyright 2013-2018 Zaid Abdulla
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import { parseXml, Document, Element, ParserOptions } from "libxmljs";
import * as config from "./config";
import { version as VERSION } from "../package.json";
import {
CpeGetResponse,
CpeSetResponse,
InformRequest,
AcsResponse,
CpeRequest,
FaultStruct,
SpvFault,
CpeFault,
SoapMessage,
TransferCompleteRequest
} from "./types";
const SERVER_NAME = `GenieACS/${VERSION}`;
const NAMESPACES = {
"1.0": {
"soap-enc": "http://schemas.xmlsoap.org/soap/encoding/",
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
xsd: "http://www.w3.org/2001/XMLSchema",
xsi: "http://www.w3.org/2001/XMLSchema-instance",
cwmp: "urn:dslforum-org:cwmp-1-0"
},
"1.1": {
"soap-enc": "http://schemas.xmlsoap.org/soap/encoding/",
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
xsd: "http://www.w3.org/2001/XMLSchema",
xsi: "http://www.w3.org/2001/XMLSchema-instance",
cwmp: "urn:dslforum-org:cwmp-1-1"
},
"1.2": {
"soap-enc": "http://schemas.xmlsoap.org/soap/encoding/",
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
xsd: "http://www.w3.org/2001/XMLSchema",
xsi: "http://www.w3.org/2001/XMLSchema-instance",
cwmp: "urn:dslforum-org:cwmp-1-2"
},
"1.3": {
"soap-enc": "http://schemas.xmlsoap.org/soap/encoding/",
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
xsd: "http://www.w3.org/2001/XMLSchema",
xsi: "http://www.w3.org/2001/XMLSchema-instance",
cwmp: "urn:dslforum-org:cwmp-1-2"
},
"1.4": {
"soap-enc": "http://schemas.xmlsoap.org/soap/encoding/",
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
xsd: "http://www.w3.org/2001/XMLSchema",
xsi: "http://www.w3.org/2001/XMLSchema-instance",
cwmp: "urn:dslforum-org:cwmp-1-3"
}
};
const LIBXMLJS_PARSE_OPTIONS: ParserOptions = {
nocdata: true,
recover: !!config.get("XML_RECOVER"),
/* eslint-disable-next-line @typescript-eslint/camelcase */
ignore_enc: !!config.get("XML_IGNORE_ENC")
};
interface WriterOptions {
declaration?: boolean;
format?: boolean;
selfCloseEmpty?: boolean;
whitespace?: boolean;
type?: "XML" | "HTML" | "XHTML";
}
declare module "libxmljs" {
interface Document {
toString(formatting: boolean | WriterOptions): string;
}
}
const LIBXMLJS_SAVE_OPTIONS: WriterOptions = {
format: !!config.get("XML_FORMAT"),
declaration: !config.get("XML_NO_DECL"),
selfCloseEmpty: !config.get("XML_NO_EMPTY")
};
let warnings;
function parseBool(v): boolean {
v = "" + v;
if (v === "true" || v === "TRUE" || v === "True" || v === "1") return true;
else if (v === "false" || v === "FALSE" || v === "False" || v === "0")
return false;
else return null;
}
function event(xml): string[] {
return xml
.childNodes()
.filter(n => n.name() === "EventStruct")
.map(c =>
c
.childNodes()
.find(n => n.name() === "EventCode")
.text()
.trim()
);
}
function parameterInfoList(xml): [string, boolean][] {
return xml
.childNodes()
.filter(e => e.name() === "ParameterInfoStruct")
.map(e => {
let param, value;
for (const c of e.childNodes()) {
switch (c.name()) {
case "Name":
param = c.text();
break;
case "Writable":
value = c.text();
break;
}
}
let parsed = parseBool(value);
if (parsed === null) {
warnings.push({
message: "Invalid writable attribute",
parameter: param
});
parsed = false;
}
return [param, parsed];
});
}
function parameterValueList(
xml
): [string, string | number | boolean, string][] {
return xml
.childNodes()
.filter(e => e.name() === "ParameterValueStruct")
.map(e => {
let valueElement, param;
for (const c of e.childNodes()) {
switch (c.name()) {
case "Name":
param = c.text();
break;
case "Value":
valueElement = c;
break;
}
}
const valueType = valueElement.attr("type").value();
const value = valueElement.text();
let parsed = value;
if (valueType === "xsd:boolean") {
parsed = parseBool(value);
if (parsed === null) {
warnings.push({
message: "Invalid value attribute",
parameter: param
});
parsed = value;
}
} else if (valueType === "xsd:int" || valueType === "xsd:unsignedInt") {
parsed = parseInt(value);
if (isNaN(parsed)) {
warnings.push({
message: "Invalid value attribute",
parameter: param
});
parsed = value;
}
} else if (valueType === "xsd:dateTime") {
parsed = Date.parse(value);
if (isNaN(parsed)) {
warnings.push({
message: "Invalid value attribute",
parameter: param
});
parsed = value;
}
}
return [param, parsed, valueType];
});
}
function GetParameterNames(xml, methodRequest): void {
const el = xml.node("cwmp:GetParameterNames");
el.node("ParameterPath").text(methodRequest.parameterPath);
el.node("NextLevel").text(+methodRequest.nextLevel);
}
function GetParameterNamesResponse(xml): CpeGetResponse {
return {
name: "GetParameterNamesResponse",
parameterList: parameterInfoList(
xml.childNodes().find(n => n.name() === "ParameterList")
)
};
}
function GetParameterValues(xml, methodRequest): void {
const el = xml.node("cwmp:GetParameterValues").node("ParameterNames");
el.attr({
"soap-enc:arrayType": `xsd:string[${methodRequest.parameterNames.length}]`
});
for (const p of methodRequest.parameterNames) el.node("string").text(p);
}
function GetParameterValuesResponse(xml): CpeGetResponse {
return {
name: "GetParameterValuesResponse",
parameterList: parameterValueList(
xml.childNodes().find(n => n.name() === "ParameterList")
)
};
}
function SetParameterValues(xml, methodRequest): void {
const el = xml.node("cwmp:SetParameterValues");
const paramList = el.node("ParameterList");
paramList.attr({
"soap-enc:arrayType": `cwmp:ParameterValueStruct[${
methodRequest.parameterList.length
}]`
});
for (const p of methodRequest.parameterList) {
const pvs = paramList.node("ParameterValueStruct");
pvs.node("Name").text(p[0]);
let val = p[1];
if (p[2] === "xsd:dateTime" && typeof val === "number") {
val = new Date(val).toISOString();
if (methodRequest.DATETIME_MILLISECONDS === false)
val = val.replace(".000", "");
}
if (p[2] === "xsd:boolean" && typeof val === "boolean")
if (methodRequest.BOOLEAN_LITERAL === false) val = +val;
pvs
.node("Value")
.attr({ "xsi:type": p[2] })
.text(val);
}
el.node("ParameterKey").text(methodRequest.parameterKey || "");
}
function SetParameterValuesResponse(xml): CpeSetResponse {
return {
name: "SetParameterValuesResponse",
status: parseInt(
xml
.childNodes()
.find(n => n.name() === "Status")
.text()
)
};
}
function AddObject(xml, methodRequest): void {
const el = xml.node("cwmp:AddObject");
el.node("ObjectName").text(methodRequest.objectName);
el.node("ParameterKey").text(methodRequest.parameterKey || "");
}
function AddObjectResponse(xml): CpeSetResponse {
let instanceNumber, status;
for (const c of xml.childNodes()) {
switch (c.name()) {
case "InstanceNumber":
instanceNumber = parseInt(c.text());
break;
case "Status":
status = parseInt(c.text());
break;
}
}
return {
name: "AddObjectResponse",
instanceNumber: instanceNumber,
status: status
};
}
function DeleteObject(xml, methodRequest): void {
const el = xml.node("cwmp:DeleteObject");
el.node("ObjectName").text(methodRequest.objectName);
el.node("ParameterKey").text(methodRequest.parameterKey || "");
}
function DeleteObjectResponse(xml): CpeSetResponse {
return {
name: "DeleteObjectResponse",
status: parseInt(
xml
.childNodes()
.find(n => n.name() === "Status")
.text()
)
};
}
function Reboot(xml, methodRequest): void {
const el = xml.node("cwmp:Reboot");
el.node("CommandKey").text(methodRequest.commandKey || "");
}
function RebootResponse(): CpeSetResponse {
return {
name: "RebootResponse"
};
}
function FactoryReset(xml): void {
xml.node("cwmp:FactoryReset");
}
function FactoryResetResponse(): CpeSetResponse {
return {
name: "FactoryResetResponse"
};
}
function Download(xml, methodRequest): void {
const el = xml.node("cwmp:Download");
el.node("CommandKey").text(methodRequest.commandKey || "");
el.node("FileType").text(methodRequest.fileType);
el.node("URL").text(methodRequest.url);
el.node("Username").text(methodRequest.username || "");
el.node("Password").text(methodRequest.password || "");
el.node("FileSize").text(methodRequest.fileSize || "0");
el.node("TargetFileName").text(methodRequest.targetFileName || "");
el.node("DelaySeconds").text(methodRequest.delaySeconds || "0");
el.node("SuccessURL").text(methodRequest.successUrl || "");
el.node("FailureURL").text(methodRequest.failureUrl || "");
}
function DownloadResponse(xml): CpeSetResponse {
let status, startTime, completeTime;
for (const c of xml.childNodes()) {
switch (c.name()) {
case "Status":
status = parseInt(c.text());
break;
case "StartTime":
startTime = Date.parse(c.text());
break;
case "CompleteTime":
completeTime = Date.parse(c.text());
break;
}
}
return {
name: "DownloadResponse",
status: status,
startTime: startTime,
completeTime: completeTime
};
}
function Inform(xml): InformRequest {
let retryCount, evnt, parameterList;
const deviceId = {
Manufacturer: null,
OUI: null,
ProductClass: null,
SerialNumber: null
};
for (const c of xml.childNodes()) {
switch (c.name()) {
case "ParameterList":
parameterList = parameterValueList(c);
break;
case "DeviceId":
for (const cc of c.childNodes()) {
const n = cc.name();
if (n in deviceId) deviceId[n] = cc.text();
}
break;
case "Event":
evnt = event(c);
break;
case "RetryCount":
retryCount = parseInt(c.text());
break;
}
}
return {
name: "Inform",
parameterList: parameterList,
deviceId: deviceId,
event: evnt,
retryCount: retryCount
};
}
function InformResponse(xml): void {
xml
.node("cwmp:InformResponse")
.node("MaxEnvelopes")
.text(1);
}
function GetRPCMethods(): AcsResponse {
return { name: "GetRPCMethods" };
}
function GetRPCMethodsResponse(xml, methodResponse): void {
const el = xml.node("cwmp:GetRPCMethodsResponse").node("MethodList");
el.attr({
"soap-enc:arrayType": `xsd:string[${methodResponse.methodList.length}]`
});
for (const m of methodResponse.methodList) el.node("string").text(m);
}
function TransferComplete(xml): TransferCompleteRequest {
let commandKey, _faultStruct, startTime, completeTime;
for (const c of xml.childNodes()) {
switch (c.name()) {
case "CommandKey":
commandKey = c.text();
break;
case "FaultStruct":
_faultStruct = faultStruct(c);
break;
case "StartTime":
startTime = Date.parse(c.text());
break;
case "CompleteTime":
completeTime = Date.parse(c.text());
break;
}
}
return {
name: "TransferComplete",
commandKey: commandKey,
faultStruct: _faultStruct,
startTime: startTime,
completeTime: completeTime
};
}
function TransferCompleteResponse(xml): void {
xml.node("cwmp:TransferCompleteResponse").text("");
}
function RequestDownload(xml): CpeRequest {
return {
name: "RequestDownload",
fileType: xml
.childNodes()
.find(n => n.name() === "FileType")
.text()
};
}
function RequestDownloadResponse(xml): void {
xml.node("cwmp:RequestDownloadResponse").text("");
}
function faultStruct(xml): FaultStruct {
let faultCode, faultString, setParameterValuesFault: SpvFault[], pn, fc, fs;
for (const c of xml.childNodes()) {
switch (c.name()) {
case "FaultCode":
faultCode = c.text();
break;
case "FaultString":
faultString = c.text();
break;
case "SetParameterValuesFault":
setParameterValuesFault = setParameterValuesFault || [];
for (const cc of c.childNodes()) {
switch (cc.name()) {
case "ParameterName":
pn = cc.text();
break;
case "FaultCode":
fc = cc.text();
break;
case "FaultString":
fs = cc.text();
break;
}
}
setParameterValuesFault.push({
parameterName: pn,
faultCode: fc,
faultString: fs
});
}
}
return { faultCode, faultString, setParameterValuesFault };
}
function fault(xml): CpeFault {
let faultCode, faultString, detail;
for (const c of xml.childNodes()) {
switch (c.name()) {
case "faultcode":
faultCode = c.text();
break;
case "faultstring":
faultString = c.text();
break;
case "detail":
detail = faultStruct(c.childNodes().find(n => n.name() === "Fault"));
break;
}
}
return { faultCode, faultString, detail };
}
export function request(data, cwmpVersion, warn): SoapMessage {
warnings = warn;
const rpc = {
id: null,
cwmpVersion: cwmpVersion,
sessionTimeout: null,
cpeRequest: null,
cpeFault: null,
cpeResponse: null
};
if (!data.length) return rpc;
const xml = parseXml(data, LIBXMLJS_PARSE_OPTIONS);
let headerElement, bodyElement;
for (const c of xml.childNodes()) {
switch (c.name()) {
case "Header":
headerElement = c;
break;
case "Body":
bodyElement = c;
break;
}
}
if (headerElement) {
for (const c of headerElement.childNodes()) {
switch (c.name()) {
case "ID":
rpc.id = c.text();
break;
case "sessionTimeout":
rpc.sessionTimeout = parseInt(c.text());
break;
}
}
}
for (const methodElement of bodyElement.childNodes()) {
const methodName = methodElement.name();
if (methodName === "text") continue;
if (!rpc.cwmpVersion && methodName !== "Fault") {
switch (methodElement.namespace().href()) {
case "urn:dslforum-org:cwmp-1-0":
rpc.cwmpVersion = "1.0";
break;
case "urn:dslforum-org:cwmp-1-1":
rpc.cwmpVersion = "1.1";
break;
case "urn:dslforum-org:cwmp-1-2":
if (rpc.sessionTimeout) rpc.cwmpVersion = "1.3";
else rpc.cwmpVersion = "1.2";
break;
case "urn:dslforum-org:cwmp-1-3":
rpc.cwmpVersion = "1.4";
break;
default:
throw new Error("Unrecognized CWMP version");
}
}
switch (methodName) {
case "Inform":
rpc.cpeRequest = Inform(methodElement);
break;
case "GetRPCMethods":
rpc.cpeRequest = GetRPCMethods();
break;
case "TransferComplete":
rpc.cpeRequest = TransferComplete(methodElement);
break;
case "RequestDownload":
rpc.cpeRequest = RequestDownload(methodElement);
break;
case "GetParameterNamesResponse":
rpc.cpeResponse = GetParameterNamesResponse(methodElement);
break;
case "GetParameterValuesResponse":
rpc.cpeResponse = GetParameterValuesResponse(methodElement);
break;
case "SetParameterValuesResponse":
rpc.cpeResponse = SetParameterValuesResponse(methodElement);
break;
case "AddObjectResponse":
rpc.cpeResponse = AddObjectResponse(methodElement);
break;
case "DeleteObjectResponse":
rpc.cpeResponse = DeleteObjectResponse(methodElement);
break;
case "RebootResponse":
rpc.cpeResponse = RebootResponse();
break;
case "FactoryResetResponse":
rpc.cpeResponse = FactoryResetResponse();
break;
case "DownloadResponse":
rpc.cpeResponse = DownloadResponse(methodElement);
break;
case "Fault":
rpc.cpeFault = fault(methodElement);
break;
default:
throw new Error(`8000 Method not supported ${methodName}`);
}
}
return rpc;
}
function createSoapEnv(cwmpVersion: string): Element {
const xml = new Document();
const env = xml.node("soap-env:Envelope");
for (const prefix of Object.keys(NAMESPACES[cwmpVersion]))
env.defineNamespace(prefix, NAMESPACES[cwmpVersion][prefix]);
return env;
}
export function response(rpc): { code: number; headers: {}; data: string } {
const headers = {
Server: SERVER_NAME,
SOAPServer: SERVER_NAME
};
if (!rpc) return { code: 204, headers: headers, data: "" };
const env = createSoapEnv(rpc.cwmpVersion);
const header = env.node("soap-env:Header");
header
.node("cwmp:ID")
.attr({
"soap-env:mustUnderstand": "1"
})
.text(rpc.id);
const body = env.node("soap-env:Body");
if (rpc.acsResponse) {
switch (rpc.acsResponse.name) {
case "InformResponse":
InformResponse(body);
break;
case "GetRPCMethodsResponse":
GetRPCMethodsResponse(body, rpc.acsResponse);
break;
case "TransferCompleteResponse":
TransferCompleteResponse(body);
break;
case "RequestDownloadResponse":
RequestDownloadResponse(body);
break;
default:
throw new Error(`Unknown method response type ${rpc.acsResponse.name}`);
}
} else if (rpc.acsRequest) {
switch (rpc.acsRequest.name) {
case "GetParameterNames":
GetParameterNames(body, rpc.acsRequest);
break;
case "GetParameterValues":
GetParameterValues(body, rpc.acsRequest);
break;
case "SetParameterValues":
SetParameterValues(body, rpc.acsRequest);
break;
case "AddObject":
AddObject(body, rpc.acsRequest);
break;
case "DeleteObject":
DeleteObject(body, rpc.acsRequest);
break;
case "Reboot":
Reboot(body, rpc.acsRequest);
break;
case "FactoryReset":
FactoryReset(body);
break;
case "Download":
Download(body, rpc.acsRequest);
break;
default:
throw new Error(`Unknown method request ${rpc.acsRequest.name}`);
}
}
headers["Content-Type"] = 'text/xml; charset="utf-8"';
return {
code: 200,
headers: headers,
data: env.doc().toString(LIBXMLJS_SAVE_OPTIONS)
};
}
+276 -297
View File
@@ -1,5 +1,5 @@
/**
* Copyright 2013-2018 Zaid Abdulla
* Copyright 2013-2019 Zaid Abdulla
*
* This file is part of GenieACS.
*
@@ -17,8 +17,14 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import { parseXml, Document, Element, ParserOptions } from "libxmljs";
import * as config from "./config";
import {
parseXml,
Element,
parseAttrs,
encodeEntities,
decodeEntities
} from "./xml-parser";
import memoize from "./common/memoize";
import { version as VERSION } from "../package.json";
import {
CpeGetResponse,
@@ -73,35 +79,10 @@ const NAMESPACES = {
}
};
const LIBXMLJS_PARSE_OPTIONS: ParserOptions = {
nocdata: true,
recover: !!config.get("XML_RECOVER"),
/* eslint-disable-next-line @typescript-eslint/camelcase */
ignore_enc: !!config.get("XML_IGNORE_ENC")
};
interface WriterOptions {
declaration?: boolean;
format?: boolean;
selfCloseEmpty?: boolean;
whitespace?: boolean;
type?: "XML" | "HTML" | "XHTML";
}
declare module "libxmljs" {
interface Document {
toString(formatting: boolean | WriterOptions): string;
}
}
const LIBXMLJS_SAVE_OPTIONS: WriterOptions = {
format: !!config.get("XML_FORMAT"),
declaration: !config.get("XML_NO_DECL"),
selfCloseEmpty: !config.get("XML_NO_EMPTY")
};
let warnings;
const memoizedParseAttrs = memoize(parseAttrs);
function parseBool(v): boolean {
v = "" + v;
if (v === "true" || v === "TRUE" || v === "True" || v === "1") return true;
@@ -110,37 +91,29 @@ function parseBool(v): boolean {
else return null;
}
function event(xml): string[] {
return xml
.childNodes()
.filter(n => n.name() === "EventStruct")
.map(c =>
c
.childNodes()
.find(n => n.name() === "EventCode")
.text()
.trim()
);
function event(xml: Element): string[] {
return xml.children
.filter(n => n.localName === "EventStruct")
.map(c => c.children.find(n => n.localName === "EventCode").text.trim());
}
function parameterInfoList(xml): [string, boolean][] {
return xml
.childNodes()
.filter(e => e.name() === "ParameterInfoStruct")
.map(e => {
let param, value;
for (const c of e.childNodes()) {
switch (c.name()) {
function parameterInfoList(xml: Element): [string, boolean][] {
return xml.children
.filter(e => e.localName === "ParameterInfoStruct")
.map<[string, boolean]>(e => {
let param: string, value: string;
for (const c of e.children) {
switch (c.localName) {
case "Name":
param = c.text();
param = c.text;
break;
case "Writable":
value = c.text();
value = c.text;
break;
}
}
let parsed = parseBool(value);
let parsed: boolean = parseBool(value);
if (parsed === null) {
warnings.push({
@@ -154,18 +127,23 @@ function parameterInfoList(xml): [string, boolean][] {
});
}
const getValueType = memoize(str =>
parseAttrs(str)
.find(s => s.localName === "type")
.value.trim()
);
function parameterValueList(
xml
xml: Element
): [string, string | number | boolean, string][] {
return xml
.childNodes()
.filter(e => e.name() === "ParameterValueStruct")
.map(e => {
let valueElement, param;
for (const c of e.childNodes()) {
switch (c.name()) {
return xml.children
.filter(e => e.localName === "ParameterValueStruct")
.map<[string, string | number | boolean, string]>(e => {
let valueElement: Element, param: string;
for (const c of e.children) {
switch (c.localName) {
case "Name":
param = c.text();
param = c.text;
break;
case "Value":
valueElement = c;
@@ -173,9 +151,10 @@ function parameterValueList(
}
}
const valueType = valueElement.attr("type").value();
const value = valueElement.text();
let parsed = value;
const valueType = getValueType(valueElement.attrs);
const value = decodeEntities(valueElement.text);
let parsed: string | number | boolean = value;
if (valueType === "xsd:boolean") {
parsed = parseBool(value);
if (parsed === null) {
@@ -209,51 +188,40 @@ function parameterValueList(
});
}
function GetParameterNames(xml, methodRequest): void {
const el = xml.node("cwmp:GetParameterNames");
el.node("ParameterPath").text(methodRequest.parameterPath);
el.node("NextLevel").text(+methodRequest.nextLevel);
function GetParameterNames(methodRequest): string {
return `<cwmp:GetParameterNames><ParameterPath>${
methodRequest.parameterPath
}</ParameterPath><NextLevel>${+methodRequest.nextLevel}</NextLevel></cwmp:GetParameterNames>`;
}
function GetParameterNamesResponse(xml): CpeGetResponse {
return {
name: "GetParameterNamesResponse",
parameterList: parameterInfoList(
xml.childNodes().find(n => n.name() === "ParameterList")
xml.children.find(n => n.localName === "ParameterList")
)
};
}
function GetParameterValues(xml, methodRequest): void {
const el = xml.node("cwmp:GetParameterValues").node("ParameterNames");
el.attr({
"soap-enc:arrayType": `xsd:string[${methodRequest.parameterNames.length}]`
});
for (const p of methodRequest.parameterNames) el.node("string").text(p);
function GetParameterValues(methodRequest): string {
return `<cwmp:GetParameterValues><ParameterNames soap-enc:arrayType="xsd:string[${
methodRequest.parameterNames.length
}]">${methodRequest.parameterNames
.map(p => `<string>${p}</string>`)
.join("")}</ParameterNames></cwmp:GetParameterValues>`;
}
function GetParameterValuesResponse(xml): CpeGetResponse {
function GetParameterValuesResponse(xml: Element): CpeGetResponse {
return {
name: "GetParameterValuesResponse",
parameterList: parameterValueList(
xml.childNodes().find(n => n.name() === "ParameterList")
xml.children.find(n => n.localName === "ParameterList")
)
};
}
function SetParameterValues(xml, methodRequest): void {
const el = xml.node("cwmp:SetParameterValues");
const paramList = el.node("ParameterList");
paramList.attr({
"soap-enc:arrayType": `cwmp:ParameterValueStruct[${
methodRequest.parameterList.length
}]`
});
for (const p of methodRequest.parameterList) {
const pvs = paramList.node("ParameterValueStruct");
pvs.node("Name").text(p[0]);
function SetParameterValues(methodRequest): string {
const params = methodRequest.parameterList.map(p => {
let val = p[1];
if (p[2] === "xsd:dateTime" && typeof val === "number") {
val = new Date(val).toISOString();
@@ -262,43 +230,42 @@ function SetParameterValues(xml, methodRequest): void {
}
if (p[2] === "xsd:boolean" && typeof val === "boolean")
if (methodRequest.BOOLEAN_LITERAL === false) val = +val;
`<ParameterValueStruct><Name>${p[0]}</Name><Value xsi:type="${
p[2]
}">${encodeEntities("" + val)}</Value></ParameterValueStruct>`;
});
pvs
.node("Value")
.attr({ "xsi:type": p[2] })
.text(val);
}
el.node("ParameterKey").text(methodRequest.parameterKey || "");
return `<cwmp:SetParameterValues><ParameterList soap-enc:arrayType="cwmp:ParameterValueStruct[${
methodRequest.parameterList.length
}]">${params.join(
""
)}</ParameterList><ParameterKey>${methodRequest.parameterKey ||
""}</ParameterKey></cwmp:SetParameterValues>`;
}
function SetParameterValuesResponse(xml): CpeSetResponse {
function SetParameterValuesResponse(xml: Element): CpeSetResponse {
return {
name: "SetParameterValuesResponse",
status: parseInt(
xml
.childNodes()
.find(n => n.name() === "Status")
.text()
)
status: parseInt(xml.children.find(n => n.localName === "Status").text)
};
}
function AddObject(xml, methodRequest): void {
const el = xml.node("cwmp:AddObject");
el.node("ObjectName").text(methodRequest.objectName);
el.node("ParameterKey").text(methodRequest.parameterKey || "");
function AddObject(methodRequest): string {
return `<cwmp:AddObject><ObjectName>${
methodRequest.objectName
}</ObjectName><ParameterKey>${methodRequest.parameterKey ||
""}</ParameterKey></cwmp:AddObject>`;
}
function AddObjectResponse(xml): CpeSetResponse {
function AddObjectResponse(xml: Element): CpeSetResponse {
let instanceNumber, status;
for (const c of xml.childNodes()) {
switch (c.name()) {
for (const c of xml.children) {
switch (c.localName) {
case "InstanceNumber":
instanceNumber = parseInt(c.text());
instanceNumber = parseInt(c.text);
break;
case "Status":
status = parseInt(c.text());
status = parseInt(c.text);
break;
}
}
@@ -310,27 +277,23 @@ function AddObjectResponse(xml): CpeSetResponse {
};
}
function DeleteObject(xml, methodRequest): void {
const el = xml.node("cwmp:DeleteObject");
el.node("ObjectName").text(methodRequest.objectName);
el.node("ParameterKey").text(methodRequest.parameterKey || "");
function DeleteObject(methodRequest): string {
return `<cwmp:DeleteObject><ObjectName>${
methodRequest.objectName
}</ObjectName><ParameterKey>${methodRequest.parameterKey ||
""}</ParameterKey></cwmp:DeleteObject>`;
}
function DeleteObjectResponse(xml): CpeSetResponse {
function DeleteObjectResponse(xml: Element): CpeSetResponse {
return {
name: "DeleteObjectResponse",
status: parseInt(
xml
.childNodes()
.find(n => n.name() === "Status")
.text()
)
status: parseInt(xml.children.find(n => n.localName === "Status").text)
};
}
function Reboot(xml, methodRequest): void {
const el = xml.node("cwmp:Reboot");
el.node("CommandKey").text(methodRequest.commandKey || "");
function Reboot(methodRequest): string {
return `<cwmp:Reboot><CommandKey>${methodRequest.commandKey ||
""}</CommandKey></cwmp:Reboot>`;
}
function RebootResponse(): CpeSetResponse {
@@ -339,8 +302,8 @@ function RebootResponse(): CpeSetResponse {
};
}
function FactoryReset(xml): void {
xml.node("cwmp:FactoryReset");
function FactoryReset(): string {
return "<cwmp:FactoryReset></cwmp:FactoryReset>";
}
function FactoryResetResponse(): CpeSetResponse {
@@ -349,32 +312,37 @@ function FactoryResetResponse(): CpeSetResponse {
};
}
function Download(xml, methodRequest): void {
const el = xml.node("cwmp:Download");
el.node("CommandKey").text(methodRequest.commandKey || "");
el.node("FileType").text(methodRequest.fileType);
el.node("URL").text(methodRequest.url);
el.node("Username").text(methodRequest.username || "");
el.node("Password").text(methodRequest.password || "");
el.node("FileSize").text(methodRequest.fileSize || "0");
el.node("TargetFileName").text(methodRequest.targetFileName || "");
el.node("DelaySeconds").text(methodRequest.delaySeconds || "0");
el.node("SuccessURL").text(methodRequest.successUrl || "");
el.node("FailureURL").text(methodRequest.failureUrl || "");
function Download(methodRequest): string {
return `<cwmp:Download><CommandKey>${methodRequest.commandKey ||
""}</CommandKey><FileType>${methodRequest.fileType}</FileType><URL>${
methodRequest.url
}</URL><Username>${encodeEntities(
methodRequest.username || ""
)}</Username><Password>${encodeEntities(
methodRequest.password || ""
)}</Password><FileSize>${methodRequest.fileSize ||
"0"}</FileSize><TargetFileName>${encodeEntities(
methodRequest.targetFileName || ""
)}</TargetFileName><DelaySeconds>${methodRequest.delaySeconds ||
"0"}</DelaySeconds><SuccessURL>${encodeEntities(
methodRequest.successUrl || ""
)}</SuccessURL><FailureURL>${encodeEntities(
methodRequest.failureUrl || ""
)}</FailureURL></cwmp:Download>`;
}
function DownloadResponse(xml): CpeSetResponse {
function DownloadResponse(xml: Element): CpeSetResponse {
let status, startTime, completeTime;
for (const c of xml.childNodes()) {
switch (c.name()) {
for (const c of xml.children) {
switch (c.localName) {
case "Status":
status = parseInt(c.text());
status = parseInt(c.text);
break;
case "StartTime":
startTime = Date.parse(c.text());
startTime = Date.parse(c.text);
break;
case "CompleteTime":
completeTime = Date.parse(c.text());
completeTime = Date.parse(c.text);
break;
}
}
@@ -387,7 +355,7 @@ function DownloadResponse(xml): CpeSetResponse {
};
}
function Inform(xml): InformRequest {
function Inform(xml: Element): InformRequest {
let retryCount, evnt, parameterList;
const deviceId = {
Manufacturer: null,
@@ -396,22 +364,22 @@ function Inform(xml): InformRequest {
SerialNumber: null
};
for (const c of xml.childNodes()) {
switch (c.name()) {
for (const c of xml.children) {
switch (c.localName) {
case "ParameterList":
parameterList = parameterValueList(c);
break;
case "DeviceId":
for (const cc of c.childNodes()) {
const n = cc.name();
if (n in deviceId) deviceId[n] = cc.text();
for (const cc of c.children) {
const n = cc.localName;
if (n in deviceId) deviceId[n] = decodeEntities(cc.text);
}
break;
case "Event":
evnt = event(c);
break;
case "RetryCount":
retryCount = parseInt(c.text());
retryCount = parseInt(c.text);
break;
}
}
@@ -425,41 +393,37 @@ function Inform(xml): InformRequest {
};
}
function InformResponse(xml): void {
xml
.node("cwmp:InformResponse")
.node("MaxEnvelopes")
.text(1);
function InformResponse(): string {
return "<cwmp:InformResponse><MaxEnvelopes>1</MaxEnvelopes></cwmp:InformResponse>";
}
function GetRPCMethods(): AcsResponse {
return { name: "GetRPCMethods" };
}
function GetRPCMethodsResponse(xml, methodResponse): void {
const el = xml.node("cwmp:GetRPCMethodsResponse").node("MethodList");
el.attr({
"soap-enc:arrayType": `xsd:string[${methodResponse.methodList.length}]`
});
for (const m of methodResponse.methodList) el.node("string").text(m);
function GetRPCMethodsResponse(methodResponse): string {
return `<cwmp:GetRPCMethodsResponse><MethodList soap-enc:arrayType="xsd:string[${
methodResponse.methodList.length
}]">${methodResponse.methodList
.map(m => `<string>${m}</string>`)
.join("")}</MethodList></cwmp:GetRPCMethodsResponse>`;
}
function TransferComplete(xml): TransferCompleteRequest {
function TransferComplete(xml: Element): TransferCompleteRequest {
let commandKey, _faultStruct, startTime, completeTime;
for (const c of xml.childNodes()) {
switch (c.name()) {
for (const c of xml.children) {
switch (c.localName) {
case "CommandKey":
commandKey = c.text();
commandKey = c.text;
break;
case "FaultStruct":
_faultStruct = faultStruct(c);
break;
case "StartTime":
startTime = Date.parse(c.text());
startTime = Date.parse(c.text);
break;
case "CompleteTime":
completeTime = Date.parse(c.text());
completeTime = Date.parse(c.text);
break;
}
}
@@ -473,46 +437,43 @@ function TransferComplete(xml): TransferCompleteRequest {
};
}
function TransferCompleteResponse(xml): void {
xml.node("cwmp:TransferCompleteResponse").text("");
function TransferCompleteResponse(): string {
return "<cwmp:TransferCompleteResponse></cwmp:TransferCompleteResponse>";
}
function RequestDownload(xml): CpeRequest {
function RequestDownload(xml: Element): CpeRequest {
return {
name: "RequestDownload",
fileType: xml
.childNodes()
.find(n => n.name() === "FileType")
.text()
fileType: xml.children.find(n => n.localName === "FileType").text
};
}
function RequestDownloadResponse(xml): void {
xml.node("cwmp:RequestDownloadResponse").text("");
function RequestDownloadResponse(): string {
return "<cwmp:RequestDownloadResponse></cwmp:RequestDownloadResponse>";
}
function faultStruct(xml): FaultStruct {
function faultStruct(xml: Element): FaultStruct {
let faultCode, faultString, setParameterValuesFault: SpvFault[], pn, fc, fs;
for (const c of xml.childNodes()) {
switch (c.name()) {
for (const c of xml.children) {
switch (c.localName) {
case "FaultCode":
faultCode = c.text();
faultCode = c.text;
break;
case "FaultString":
faultString = c.text();
faultString = decodeEntities(c.text);
break;
case "SetParameterValuesFault":
setParameterValuesFault = setParameterValuesFault || [];
for (const cc of c.childNodes()) {
switch (cc.name()) {
for (const cc of c.children) {
switch (cc.localName) {
case "ParameterName":
pn = cc.text();
pn = cc.text;
break;
case "FaultCode":
fc = cc.text();
fc = cc.text;
break;
case "FaultString":
fs = cc.text();
fs = decodeEntities(cc.text);
break;
}
}
@@ -527,18 +488,18 @@ function faultStruct(xml): FaultStruct {
return { faultCode, faultString, setParameterValuesFault };
}
function fault(xml): CpeFault {
function fault(xml: Element): CpeFault {
let faultCode, faultString, detail;
for (const c of xml.childNodes()) {
switch (c.name()) {
for (const c of xml.children) {
switch (c.localName) {
case "faultcode":
faultCode = c.text();
faultCode = c.text;
break;
case "faultstring":
faultString = c.text();
faultString = decodeEntities(c.text);
break;
case "detail":
detail = faultStruct(c.childNodes().find(n => n.name() === "Fault"));
detail = faultStruct(c.children.find(n => n.localName === "Fault"));
break;
}
}
@@ -546,7 +507,7 @@ function fault(xml): CpeFault {
return { faultCode, faultString, detail };
}
export function request(data, cwmpVersion, warn): SoapMessage {
export function request(body: string, cwmpVersion, warn): SoapMessage {
warnings = warn;
const rpc = {
@@ -558,14 +519,18 @@ export function request(data, cwmpVersion, warn): SoapMessage {
cpeResponse: null
};
if (!data.length) return rpc;
if (!body.length) return rpc;
const xml = parseXml(data, LIBXMLJS_PARSE_OPTIONS);
const xml = parseXml(body);
let headerElement, bodyElement;
if (!xml.children.length) return rpc;
for (const c of xml.childNodes()) {
switch (c.name()) {
const envelope = xml.children[0];
let headerElement: Element, bodyElement: Element;
for (const c of envelope.children) {
switch (c.localName) {
case "Header":
headerElement = c;
break;
@@ -576,100 +541,120 @@ export function request(data, cwmpVersion, warn): SoapMessage {
}
if (headerElement) {
for (const c of headerElement.childNodes()) {
switch (c.name()) {
for (const c of headerElement.children) {
switch (c.localName) {
case "ID":
rpc.id = c.text();
rpc.id = decodeEntities(c.text);
break;
case "sessionTimeout":
rpc.sessionTimeout = parseInt(c.text());
rpc.sessionTimeout = parseInt(c.text);
break;
}
}
}
for (const methodElement of bodyElement.childNodes()) {
const methodName = methodElement.name();
if (methodName === "text") continue;
const methodElement = bodyElement.children[0];
if (!rpc.cwmpVersion && methodName !== "Fault") {
switch (methodElement.namespace().href()) {
case "urn:dslforum-org:cwmp-1-0":
rpc.cwmpVersion = "1.0";
break;
case "urn:dslforum-org:cwmp-1-1":
rpc.cwmpVersion = "1.1";
break;
case "urn:dslforum-org:cwmp-1-2":
if (rpc.sessionTimeout) rpc.cwmpVersion = "1.3";
else rpc.cwmpVersion = "1.2";
if (!rpc.cwmpVersion && methodElement.localName !== "Fault") {
let namespace, namespaceHref;
for (const e of [methodElement, bodyElement, envelope]) {
namespace = namespace || e.namespace;
if (e.attrs) {
const attrs = memoizedParseAttrs(e.attrs);
const attr = namespace
? attrs.find(
s => s.namespace === "xmlns" && s.localName === namespace
)
: attrs.find(s => s.name === "xmlns");
break;
case "urn:dslforum-org:cwmp-1-3":
rpc.cwmpVersion = "1.4";
break;
default:
throw new Error("Unrecognized CWMP version");
if (attr) namespaceHref = attr.value;
}
}
switch (methodName) {
case "Inform":
rpc.cpeRequest = Inform(methodElement);
switch (namespaceHref) {
case "urn:dslforum-org:cwmp-1-0":
rpc.cwmpVersion = "1.0";
break;
case "GetRPCMethods":
rpc.cpeRequest = GetRPCMethods();
case "urn:dslforum-org:cwmp-1-1":
rpc.cwmpVersion = "1.1";
break;
case "TransferComplete":
rpc.cpeRequest = TransferComplete(methodElement);
case "urn:dslforum-org:cwmp-1-2":
if (rpc.sessionTimeout) rpc.cwmpVersion = "1.3";
else rpc.cwmpVersion = "1.2";
break;
case "RequestDownload":
rpc.cpeRequest = RequestDownload(methodElement);
break;
case "GetParameterNamesResponse":
rpc.cpeResponse = GetParameterNamesResponse(methodElement);
break;
case "GetParameterValuesResponse":
rpc.cpeResponse = GetParameterValuesResponse(methodElement);
break;
case "SetParameterValuesResponse":
rpc.cpeResponse = SetParameterValuesResponse(methodElement);
break;
case "AddObjectResponse":
rpc.cpeResponse = AddObjectResponse(methodElement);
break;
case "DeleteObjectResponse":
rpc.cpeResponse = DeleteObjectResponse(methodElement);
break;
case "RebootResponse":
rpc.cpeResponse = RebootResponse();
break;
case "FactoryResetResponse":
rpc.cpeResponse = FactoryResetResponse();
break;
case "DownloadResponse":
rpc.cpeResponse = DownloadResponse(methodElement);
break;
case "Fault":
rpc.cpeFault = fault(methodElement);
case "urn:dslforum-org:cwmp-1-3":
rpc.cwmpVersion = "1.4";
break;
default:
throw new Error(`8000 Method not supported ${methodName}`);
throw new Error("Unrecognized CWMP version");
}
}
switch (methodElement.localName) {
case "Inform":
rpc.cpeRequest = Inform(methodElement);
break;
case "GetRPCMethods":
rpc.cpeRequest = GetRPCMethods();
break;
case "TransferComplete":
rpc.cpeRequest = TransferComplete(methodElement);
break;
case "RequestDownload":
rpc.cpeRequest = RequestDownload(methodElement);
break;
case "GetParameterNamesResponse":
rpc.cpeResponse = GetParameterNamesResponse(methodElement);
break;
case "GetParameterValuesResponse":
rpc.cpeResponse = GetParameterValuesResponse(methodElement);
break;
case "SetParameterValuesResponse":
rpc.cpeResponse = SetParameterValuesResponse(methodElement);
break;
case "AddObjectResponse":
rpc.cpeResponse = AddObjectResponse(methodElement);
break;
case "DeleteObjectResponse":
rpc.cpeResponse = DeleteObjectResponse(methodElement);
break;
case "RebootResponse":
rpc.cpeResponse = RebootResponse();
break;
case "FactoryResetResponse":
rpc.cpeResponse = FactoryResetResponse();
break;
case "DownloadResponse":
rpc.cpeResponse = DownloadResponse(methodElement);
break;
case "Fault":
rpc.cpeFault = fault(methodElement);
break;
default:
throw new Error(`8000 Method not supported ${methodElement.localName}`);
}
return rpc;
}
function createSoapEnv(cwmpVersion: string): Element {
const xml = new Document();
const env = xml.node("soap-env:Envelope");
for (const prefix of Object.keys(NAMESPACES[cwmpVersion]))
env.defineNamespace(prefix, NAMESPACES[cwmpVersion][prefix]);
return env;
}
const namespacesAttrs = {
"1.0": Object.entries(NAMESPACES["1.0"])
.map(([k, v]) => `xmlns:${k}="${v}"`)
.join(" "),
"1.1": Object.entries(NAMESPACES["1.1"])
.map(([k, v]) => `xmlns:${k}="${v}"`)
.join(" "),
"1.2": Object.entries(NAMESPACES["1.2"])
.map(([k, v]) => `xmlns:${k}="${v}"`)
.join(" "),
"1.3": Object.entries(NAMESPACES["1.3"])
.map(([k, v]) => `xmlns:${k}="${v}"`)
.join(" "),
"1.4": Object.entries(NAMESPACES["1.4"])
.map(([k, v]) => `xmlns:${k}="${v}"`)
.join(" ")
};
export function response(rpc): { code: number; headers: {}; data: string } {
const headers = {
@@ -679,30 +664,20 @@ export function response(rpc): { code: number; headers: {}; data: string } {
if (!rpc) return { code: 204, headers: headers, data: "" };
const env = createSoapEnv(rpc.cwmpVersion);
const header = env.node("soap-env:Header");
header
.node("cwmp:ID")
.attr({
"soap-env:mustUnderstand": "1"
})
.text(rpc.id);
const body = env.node("soap-env:Body");
let body;
if (rpc.acsResponse) {
switch (rpc.acsResponse.name) {
case "InformResponse":
InformResponse(body);
body = InformResponse();
break;
case "GetRPCMethodsResponse":
GetRPCMethodsResponse(body, rpc.acsResponse);
body = GetRPCMethodsResponse(rpc.acsResponse);
break;
case "TransferCompleteResponse":
TransferCompleteResponse(body);
body = TransferCompleteResponse();
break;
case "RequestDownloadResponse":
RequestDownloadResponse(body);
body = RequestDownloadResponse();
break;
default:
throw new Error(`Unknown method response type ${rpc.acsResponse.name}`);
@@ -710,28 +685,28 @@ export function response(rpc): { code: number; headers: {}; data: string } {
} else if (rpc.acsRequest) {
switch (rpc.acsRequest.name) {
case "GetParameterNames":
GetParameterNames(body, rpc.acsRequest);
body = GetParameterNames(rpc.acsRequest);
break;
case "GetParameterValues":
GetParameterValues(body, rpc.acsRequest);
body = GetParameterValues(rpc.acsRequest);
break;
case "SetParameterValues":
SetParameterValues(body, rpc.acsRequest);
body = SetParameterValues(rpc.acsRequest);
break;
case "AddObject":
AddObject(body, rpc.acsRequest);
body = AddObject(rpc.acsRequest);
break;
case "DeleteObject":
DeleteObject(body, rpc.acsRequest);
body = DeleteObject(rpc.acsRequest);
break;
case "Reboot":
Reboot(body, rpc.acsRequest);
body = Reboot(rpc.acsRequest);
break;
case "FactoryReset":
FactoryReset(body);
body = FactoryReset();
break;
case "Download":
Download(body, rpc.acsRequest);
body = Download(rpc.acsRequest);
break;
default:
throw new Error(`Unknown method request ${rpc.acsRequest.name}`);
@@ -742,6 +717,10 @@ export function response(rpc): { code: number; headers: {}; data: string } {
return {
code: 200,
headers: headers,
data: env.doc().toString(LIBXMLJS_SAVE_OPTIONS)
data: `<?xml version="1.0" encoding="UTF-8"?>\n<soap-env:Envelope ${
namespacesAttrs[rpc.cwmpVersion]
}><soap-env:Header><cwmp:ID soap-env:mustUnderstand="1">${
rpc.id
}</cwmp:ID></soap-env:Header><soap-env:Body>${body}</soap-env:Body></soap-env:Envelope>`
};
}