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_FORMAT: { type: "bool", default: false },
XML_NO_DECL: { type: "bool", default: false }, XML_NO_DECL: { type: "bool", default: false },
XML_NO_EMPTY: { type: "bool", default: false }, XML_NO_EMPTY: { type: "bool", default: false },
XML_LIBXMLJS: { type: "bool", default: false },
// Should probably never be changed // Should probably never be changed
DEVICE_ONLINE_THRESHOLD: { type: "int", default: 4000 } 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 auth from "./auth";
import * as config from "./config"; import * as config from "./config";
import * as common from "./common"; 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 * as session from "./session";
import { evaluateAsync, evaluate, extractParams } from "./common/expression"; import { evaluateAsync, evaluate, extractParams } from "./common/expression";
import * as device from "./device"; import * as device from "./device";
@@ -50,7 +51,13 @@ import { IncomingMessage, ServerResponse } from "http";
import { Readable } from "stream"; import { Readable } from "stream";
import { promisify } from "util"; import { promisify } from "util";
import { TLSSocket } from "tls"; 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 gzipPromisified = promisify(zlib.gzip);
const deflatePromisified = promisify(zlib.deflate); const deflatePromisified = promisify(zlib.deflate);
@@ -70,7 +77,7 @@ const stats = {
async function authenticate( async function authenticate(
sessionContext: SessionContext, sessionContext: SessionContext,
body: string | Buffer body: string
): Promise<boolean> { ): Promise<boolean> {
const authExpression: Expression = localCache.getConfigExpression( const authExpression: Expression = localCache.getConfigExpression(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
@@ -1220,11 +1227,26 @@ async function listenerAsync(
return; 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 = []; const parseWarnings = [];
let rpc; let rpc;
try { try {
rpc = soap.request( rpc = soap.request(
body, bodyStr,
sessionContext ? sessionContext.cwmpVersion : null, sessionContext ? sessionContext.cwmpVersion : null,
parseWarnings parseWarnings
); );
@@ -1246,7 +1268,7 @@ async function listenerAsync(
// Authenticate in case of new connection or unauthenticated session // Authenticate in case of new connection or unauthenticated session
const authenticated = const authenticated =
sessionContext.state === 0 || newConnection sessionContext.state === 0 || newConnection
? await authenticate(sessionContext, body) ? await authenticate(sessionContext, bodyStr)
: true; : true;
if (!authenticated) return responseUnauthorized(sessionContext); 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); if (!authenticated) return responseUnauthorized(_sessionContext);
parsedRpc(_sessionContext, rpc, parseWarnings); 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. * This file is part of GenieACS.
* *
@@ -17,8 +17,14 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { parseXml, Document, Element, ParserOptions } from "libxmljs"; import {
import * as config from "./config"; parseXml,
Element,
parseAttrs,
encodeEntities,
decodeEntities
} from "./xml-parser";
import memoize from "./common/memoize";
import { version as VERSION } from "../package.json"; import { version as VERSION } from "../package.json";
import { import {
CpeGetResponse, 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; let warnings;
const memoizedParseAttrs = memoize(parseAttrs);
function parseBool(v): boolean { function parseBool(v): boolean {
v = "" + v; v = "" + v;
if (v === "true" || v === "TRUE" || v === "True" || v === "1") return true; if (v === "true" || v === "TRUE" || v === "True" || v === "1") return true;
@@ -110,37 +91,29 @@ function parseBool(v): boolean {
else return null; else return null;
} }
function event(xml): string[] { function event(xml: Element): string[] {
return xml return xml.children
.childNodes() .filter(n => n.localName === "EventStruct")
.filter(n => n.name() === "EventStruct") .map(c => c.children.find(n => n.localName === "EventCode").text.trim());
.map(c =>
c
.childNodes()
.find(n => n.name() === "EventCode")
.text()
.trim()
);
} }
function parameterInfoList(xml): [string, boolean][] { function parameterInfoList(xml: Element): [string, boolean][] {
return xml return xml.children
.childNodes() .filter(e => e.localName === "ParameterInfoStruct")
.filter(e => e.name() === "ParameterInfoStruct") .map<[string, boolean]>(e => {
.map(e => { let param: string, value: string;
let param, value; for (const c of e.children) {
for (const c of e.childNodes()) { switch (c.localName) {
switch (c.name()) {
case "Name": case "Name":
param = c.text(); param = c.text;
break; break;
case "Writable": case "Writable":
value = c.text(); value = c.text;
break; break;
} }
} }
let parsed = parseBool(value); let parsed: boolean = parseBool(value);
if (parsed === null) { if (parsed === null) {
warnings.push({ 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( function parameterValueList(
xml xml: Element
): [string, string | number | boolean, string][] { ): [string, string | number | boolean, string][] {
return xml return xml.children
.childNodes() .filter(e => e.localName === "ParameterValueStruct")
.filter(e => e.name() === "ParameterValueStruct") .map<[string, string | number | boolean, string]>(e => {
.map(e => { let valueElement: Element, param: string;
let valueElement, param; for (const c of e.children) {
for (const c of e.childNodes()) { switch (c.localName) {
switch (c.name()) {
case "Name": case "Name":
param = c.text(); param = c.text;
break; break;
case "Value": case "Value":
valueElement = c; valueElement = c;
@@ -173,9 +151,10 @@ function parameterValueList(
} }
} }
const valueType = valueElement.attr("type").value(); const valueType = getValueType(valueElement.attrs);
const value = valueElement.text();
let parsed = value; const value = decodeEntities(valueElement.text);
let parsed: string | number | boolean = value;
if (valueType === "xsd:boolean") { if (valueType === "xsd:boolean") {
parsed = parseBool(value); parsed = parseBool(value);
if (parsed === null) { if (parsed === null) {
@@ -209,51 +188,40 @@ function parameterValueList(
}); });
} }
function GetParameterNames(xml, methodRequest): void { function GetParameterNames(methodRequest): string {
const el = xml.node("cwmp:GetParameterNames"); return `<cwmp:GetParameterNames><ParameterPath>${
el.node("ParameterPath").text(methodRequest.parameterPath); methodRequest.parameterPath
el.node("NextLevel").text(+methodRequest.nextLevel); }</ParameterPath><NextLevel>${+methodRequest.nextLevel}</NextLevel></cwmp:GetParameterNames>`;
} }
function GetParameterNamesResponse(xml): CpeGetResponse { function GetParameterNamesResponse(xml): CpeGetResponse {
return { return {
name: "GetParameterNamesResponse", name: "GetParameterNamesResponse",
parameterList: parameterInfoList( parameterList: parameterInfoList(
xml.childNodes().find(n => n.name() === "ParameterList") xml.children.find(n => n.localName === "ParameterList")
) )
}; };
} }
function GetParameterValues(xml, methodRequest): void { function GetParameterValues(methodRequest): string {
const el = xml.node("cwmp:GetParameterValues").node("ParameterNames"); return `<cwmp:GetParameterValues><ParameterNames soap-enc:arrayType="xsd:string[${
el.attr({ methodRequest.parameterNames.length
"soap-enc:arrayType": `xsd:string[${methodRequest.parameterNames.length}]` }]">${methodRequest.parameterNames
}); .map(p => `<string>${p}</string>`)
.join("")}</ParameterNames></cwmp:GetParameterValues>`;
for (const p of methodRequest.parameterNames) el.node("string").text(p);
} }
function GetParameterValuesResponse(xml): CpeGetResponse { function GetParameterValuesResponse(xml: Element): CpeGetResponse {
return { return {
name: "GetParameterValuesResponse", name: "GetParameterValuesResponse",
parameterList: parameterValueList( parameterList: parameterValueList(
xml.childNodes().find(n => n.name() === "ParameterList") xml.children.find(n => n.localName === "ParameterList")
) )
}; };
} }
function SetParameterValues(xml, methodRequest): void { function SetParameterValues(methodRequest): string {
const el = xml.node("cwmp:SetParameterValues"); const params = methodRequest.parameterList.map(p => {
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]; let val = p[1];
if (p[2] === "xsd:dateTime" && typeof val === "number") { if (p[2] === "xsd:dateTime" && typeof val === "number") {
val = new Date(val).toISOString(); val = new Date(val).toISOString();
@@ -262,43 +230,42 @@ function SetParameterValues(xml, methodRequest): void {
} }
if (p[2] === "xsd:boolean" && typeof val === "boolean") if (p[2] === "xsd:boolean" && typeof val === "boolean")
if (methodRequest.BOOLEAN_LITERAL === false) val = +val; if (methodRequest.BOOLEAN_LITERAL === false) val = +val;
`<ParameterValueStruct><Name>${p[0]}</Name><Value xsi:type="${
p[2]
}">${encodeEntities("" + val)}</Value></ParameterValueStruct>`;
});
pvs return `<cwmp:SetParameterValues><ParameterList soap-enc:arrayType="cwmp:ParameterValueStruct[${
.node("Value") methodRequest.parameterList.length
.attr({ "xsi:type": p[2] }) }]">${params.join(
.text(val); ""
} )}</ParameterList><ParameterKey>${methodRequest.parameterKey ||
""}</ParameterKey></cwmp:SetParameterValues>`;
el.node("ParameterKey").text(methodRequest.parameterKey || "");
} }
function SetParameterValuesResponse(xml): CpeSetResponse { function SetParameterValuesResponse(xml: Element): CpeSetResponse {
return { return {
name: "SetParameterValuesResponse", name: "SetParameterValuesResponse",
status: parseInt( status: parseInt(xml.children.find(n => n.localName === "Status").text)
xml
.childNodes()
.find(n => n.name() === "Status")
.text()
)
}; };
} }
function AddObject(xml, methodRequest): void { function AddObject(methodRequest): string {
const el = xml.node("cwmp:AddObject"); return `<cwmp:AddObject><ObjectName>${
el.node("ObjectName").text(methodRequest.objectName); methodRequest.objectName
el.node("ParameterKey").text(methodRequest.parameterKey || ""); }</ObjectName><ParameterKey>${methodRequest.parameterKey ||
""}</ParameterKey></cwmp:AddObject>`;
} }
function AddObjectResponse(xml): CpeSetResponse { function AddObjectResponse(xml: Element): CpeSetResponse {
let instanceNumber, status; let instanceNumber, status;
for (const c of xml.childNodes()) { for (const c of xml.children) {
switch (c.name()) { switch (c.localName) {
case "InstanceNumber": case "InstanceNumber":
instanceNumber = parseInt(c.text()); instanceNumber = parseInt(c.text);
break; break;
case "Status": case "Status":
status = parseInt(c.text()); status = parseInt(c.text);
break; break;
} }
} }
@@ -310,27 +277,23 @@ function AddObjectResponse(xml): CpeSetResponse {
}; };
} }
function DeleteObject(xml, methodRequest): void { function DeleteObject(methodRequest): string {
const el = xml.node("cwmp:DeleteObject"); return `<cwmp:DeleteObject><ObjectName>${
el.node("ObjectName").text(methodRequest.objectName); methodRequest.objectName
el.node("ParameterKey").text(methodRequest.parameterKey || ""); }</ObjectName><ParameterKey>${methodRequest.parameterKey ||
""}</ParameterKey></cwmp:DeleteObject>`;
} }
function DeleteObjectResponse(xml): CpeSetResponse { function DeleteObjectResponse(xml: Element): CpeSetResponse {
return { return {
name: "DeleteObjectResponse", name: "DeleteObjectResponse",
status: parseInt( status: parseInt(xml.children.find(n => n.localName === "Status").text)
xml
.childNodes()
.find(n => n.name() === "Status")
.text()
)
}; };
} }
function Reboot(xml, methodRequest): void { function Reboot(methodRequest): string {
const el = xml.node("cwmp:Reboot"); return `<cwmp:Reboot><CommandKey>${methodRequest.commandKey ||
el.node("CommandKey").text(methodRequest.commandKey || ""); ""}</CommandKey></cwmp:Reboot>`;
} }
function RebootResponse(): CpeSetResponse { function RebootResponse(): CpeSetResponse {
@@ -339,8 +302,8 @@ function RebootResponse(): CpeSetResponse {
}; };
} }
function FactoryReset(xml): void { function FactoryReset(): string {
xml.node("cwmp:FactoryReset"); return "<cwmp:FactoryReset></cwmp:FactoryReset>";
} }
function FactoryResetResponse(): CpeSetResponse { function FactoryResetResponse(): CpeSetResponse {
@@ -349,32 +312,37 @@ function FactoryResetResponse(): CpeSetResponse {
}; };
} }
function Download(xml, methodRequest): void { function Download(methodRequest): string {
const el = xml.node("cwmp:Download"); return `<cwmp:Download><CommandKey>${methodRequest.commandKey ||
el.node("CommandKey").text(methodRequest.commandKey || ""); ""}</CommandKey><FileType>${methodRequest.fileType}</FileType><URL>${
el.node("FileType").text(methodRequest.fileType); methodRequest.url
el.node("URL").text(methodRequest.url); }</URL><Username>${encodeEntities(
el.node("Username").text(methodRequest.username || ""); methodRequest.username || ""
el.node("Password").text(methodRequest.password || ""); )}</Username><Password>${encodeEntities(
el.node("FileSize").text(methodRequest.fileSize || "0"); methodRequest.password || ""
el.node("TargetFileName").text(methodRequest.targetFileName || ""); )}</Password><FileSize>${methodRequest.fileSize ||
el.node("DelaySeconds").text(methodRequest.delaySeconds || "0"); "0"}</FileSize><TargetFileName>${encodeEntities(
el.node("SuccessURL").text(methodRequest.successUrl || ""); methodRequest.targetFileName || ""
el.node("FailureURL").text(methodRequest.failureUrl || ""); )}</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; let status, startTime, completeTime;
for (const c of xml.childNodes()) { for (const c of xml.children) {
switch (c.name()) { switch (c.localName) {
case "Status": case "Status":
status = parseInt(c.text()); status = parseInt(c.text);
break; break;
case "StartTime": case "StartTime":
startTime = Date.parse(c.text()); startTime = Date.parse(c.text);
break; break;
case "CompleteTime": case "CompleteTime":
completeTime = Date.parse(c.text()); completeTime = Date.parse(c.text);
break; break;
} }
} }
@@ -387,7 +355,7 @@ function DownloadResponse(xml): CpeSetResponse {
}; };
} }
function Inform(xml): InformRequest { function Inform(xml: Element): InformRequest {
let retryCount, evnt, parameterList; let retryCount, evnt, parameterList;
const deviceId = { const deviceId = {
Manufacturer: null, Manufacturer: null,
@@ -396,22 +364,22 @@ function Inform(xml): InformRequest {
SerialNumber: null SerialNumber: null
}; };
for (const c of xml.childNodes()) { for (const c of xml.children) {
switch (c.name()) { switch (c.localName) {
case "ParameterList": case "ParameterList":
parameterList = parameterValueList(c); parameterList = parameterValueList(c);
break; break;
case "DeviceId": case "DeviceId":
for (const cc of c.childNodes()) { for (const cc of c.children) {
const n = cc.name(); const n = cc.localName;
if (n in deviceId) deviceId[n] = cc.text(); if (n in deviceId) deviceId[n] = decodeEntities(cc.text);
} }
break; break;
case "Event": case "Event":
evnt = event(c); evnt = event(c);
break; break;
case "RetryCount": case "RetryCount":
retryCount = parseInt(c.text()); retryCount = parseInt(c.text);
break; break;
} }
} }
@@ -425,41 +393,37 @@ function Inform(xml): InformRequest {
}; };
} }
function InformResponse(xml): void { function InformResponse(): string {
xml return "<cwmp:InformResponse><MaxEnvelopes>1</MaxEnvelopes></cwmp:InformResponse>";
.node("cwmp:InformResponse")
.node("MaxEnvelopes")
.text(1);
} }
function GetRPCMethods(): AcsResponse { function GetRPCMethods(): AcsResponse {
return { name: "GetRPCMethods" }; return { name: "GetRPCMethods" };
} }
function GetRPCMethodsResponse(xml, methodResponse): void { function GetRPCMethodsResponse(methodResponse): string {
const el = xml.node("cwmp:GetRPCMethodsResponse").node("MethodList"); return `<cwmp:GetRPCMethodsResponse><MethodList soap-enc:arrayType="xsd:string[${
el.attr({ methodResponse.methodList.length
"soap-enc:arrayType": `xsd:string[${methodResponse.methodList.length}]` }]">${methodResponse.methodList
}); .map(m => `<string>${m}</string>`)
.join("")}</MethodList></cwmp:GetRPCMethodsResponse>`;
for (const m of methodResponse.methodList) el.node("string").text(m);
} }
function TransferComplete(xml): TransferCompleteRequest { function TransferComplete(xml: Element): TransferCompleteRequest {
let commandKey, _faultStruct, startTime, completeTime; let commandKey, _faultStruct, startTime, completeTime;
for (const c of xml.childNodes()) { for (const c of xml.children) {
switch (c.name()) { switch (c.localName) {
case "CommandKey": case "CommandKey":
commandKey = c.text(); commandKey = c.text;
break; break;
case "FaultStruct": case "FaultStruct":
_faultStruct = faultStruct(c); _faultStruct = faultStruct(c);
break; break;
case "StartTime": case "StartTime":
startTime = Date.parse(c.text()); startTime = Date.parse(c.text);
break; break;
case "CompleteTime": case "CompleteTime":
completeTime = Date.parse(c.text()); completeTime = Date.parse(c.text);
break; break;
} }
} }
@@ -473,46 +437,43 @@ function TransferComplete(xml): TransferCompleteRequest {
}; };
} }
function TransferCompleteResponse(xml): void { function TransferCompleteResponse(): string {
xml.node("cwmp:TransferCompleteResponse").text(""); return "<cwmp:TransferCompleteResponse></cwmp:TransferCompleteResponse>";
} }
function RequestDownload(xml): CpeRequest { function RequestDownload(xml: Element): CpeRequest {
return { return {
name: "RequestDownload", name: "RequestDownload",
fileType: xml fileType: xml.children.find(n => n.localName === "FileType").text
.childNodes()
.find(n => n.name() === "FileType")
.text()
}; };
} }
function RequestDownloadResponse(xml): void { function RequestDownloadResponse(): string {
xml.node("cwmp:RequestDownloadResponse").text(""); return "<cwmp:RequestDownloadResponse></cwmp:RequestDownloadResponse>";
} }
function faultStruct(xml): FaultStruct { function faultStruct(xml: Element): FaultStruct {
let faultCode, faultString, setParameterValuesFault: SpvFault[], pn, fc, fs; let faultCode, faultString, setParameterValuesFault: SpvFault[], pn, fc, fs;
for (const c of xml.childNodes()) { for (const c of xml.children) {
switch (c.name()) { switch (c.localName) {
case "FaultCode": case "FaultCode":
faultCode = c.text(); faultCode = c.text;
break; break;
case "FaultString": case "FaultString":
faultString = c.text(); faultString = decodeEntities(c.text);
break; break;
case "SetParameterValuesFault": case "SetParameterValuesFault":
setParameterValuesFault = setParameterValuesFault || []; setParameterValuesFault = setParameterValuesFault || [];
for (const cc of c.childNodes()) { for (const cc of c.children) {
switch (cc.name()) { switch (cc.localName) {
case "ParameterName": case "ParameterName":
pn = cc.text(); pn = cc.text;
break; break;
case "FaultCode": case "FaultCode":
fc = cc.text(); fc = cc.text;
break; break;
case "FaultString": case "FaultString":
fs = cc.text(); fs = decodeEntities(cc.text);
break; break;
} }
} }
@@ -527,18 +488,18 @@ function faultStruct(xml): FaultStruct {
return { faultCode, faultString, setParameterValuesFault }; return { faultCode, faultString, setParameterValuesFault };
} }
function fault(xml): CpeFault { function fault(xml: Element): CpeFault {
let faultCode, faultString, detail; let faultCode, faultString, detail;
for (const c of xml.childNodes()) { for (const c of xml.children) {
switch (c.name()) { switch (c.localName) {
case "faultcode": case "faultcode":
faultCode = c.text(); faultCode = c.text;
break; break;
case "faultstring": case "faultstring":
faultString = c.text(); faultString = decodeEntities(c.text);
break; break;
case "detail": case "detail":
detail = faultStruct(c.childNodes().find(n => n.name() === "Fault")); detail = faultStruct(c.children.find(n => n.localName === "Fault"));
break; break;
} }
} }
@@ -546,7 +507,7 @@ function fault(xml): CpeFault {
return { faultCode, faultString, detail }; return { faultCode, faultString, detail };
} }
export function request(data, cwmpVersion, warn): SoapMessage { export function request(body: string, cwmpVersion, warn): SoapMessage {
warnings = warn; warnings = warn;
const rpc = { const rpc = {
@@ -558,14 +519,18 @@ export function request(data, cwmpVersion, warn): SoapMessage {
cpeResponse: null 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()) { const envelope = xml.children[0];
switch (c.name()) {
let headerElement: Element, bodyElement: Element;
for (const c of envelope.children) {
switch (c.localName) {
case "Header": case "Header":
headerElement = c; headerElement = c;
break; break;
@@ -576,100 +541,120 @@ export function request(data, cwmpVersion, warn): SoapMessage {
} }
if (headerElement) { if (headerElement) {
for (const c of headerElement.childNodes()) { for (const c of headerElement.children) {
switch (c.name()) { switch (c.localName) {
case "ID": case "ID":
rpc.id = c.text(); rpc.id = decodeEntities(c.text);
break; break;
case "sessionTimeout": case "sessionTimeout":
rpc.sessionTimeout = parseInt(c.text()); rpc.sessionTimeout = parseInt(c.text);
break; break;
} }
} }
} }
for (const methodElement of bodyElement.childNodes()) { const methodElement = bodyElement.children[0];
const methodName = methodElement.name();
if (methodName === "text") continue;
if (!rpc.cwmpVersion && methodName !== "Fault") { if (!rpc.cwmpVersion && methodElement.localName !== "Fault") {
switch (methodElement.namespace().href()) { let namespace, namespaceHref;
case "urn:dslforum-org:cwmp-1-0": for (const e of [methodElement, bodyElement, envelope]) {
rpc.cwmpVersion = "1.0"; namespace = namespace || e.namespace;
break; if (e.attrs) {
case "urn:dslforum-org:cwmp-1-1": const attrs = memoizedParseAttrs(e.attrs);
rpc.cwmpVersion = "1.1"; const attr = namespace
break; ? attrs.find(
case "urn:dslforum-org:cwmp-1-2": s => s.namespace === "xmlns" && s.localName === namespace
if (rpc.sessionTimeout) rpc.cwmpVersion = "1.3"; )
else rpc.cwmpVersion = "1.2"; : attrs.find(s => s.name === "xmlns");
break; if (attr) namespaceHref = attr.value;
case "urn:dslforum-org:cwmp-1-3":
rpc.cwmpVersion = "1.4";
break;
default:
throw new Error("Unrecognized CWMP version");
} }
} }
switch (methodName) { switch (namespaceHref) {
case "Inform": case "urn:dslforum-org:cwmp-1-0":
rpc.cpeRequest = Inform(methodElement); rpc.cwmpVersion = "1.0";
break; break;
case "GetRPCMethods": case "urn:dslforum-org:cwmp-1-1":
rpc.cpeRequest = GetRPCMethods(); rpc.cwmpVersion = "1.1";
break; break;
case "TransferComplete": case "urn:dslforum-org:cwmp-1-2":
rpc.cpeRequest = TransferComplete(methodElement); if (rpc.sessionTimeout) rpc.cwmpVersion = "1.3";
else rpc.cwmpVersion = "1.2";
break; break;
case "RequestDownload": case "urn:dslforum-org:cwmp-1-3":
rpc.cpeRequest = RequestDownload(methodElement); rpc.cwmpVersion = "1.4";
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; break;
default: 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; return rpc;
} }
function createSoapEnv(cwmpVersion: string): Element { const namespacesAttrs = {
const xml = new Document(); "1.0": Object.entries(NAMESPACES["1.0"])
const env = xml.node("soap-env:Envelope"); .map(([k, v]) => `xmlns:${k}="${v}"`)
.join(" "),
for (const prefix of Object.keys(NAMESPACES[cwmpVersion])) "1.1": Object.entries(NAMESPACES["1.1"])
env.defineNamespace(prefix, NAMESPACES[cwmpVersion][prefix]); .map(([k, v]) => `xmlns:${k}="${v}"`)
.join(" "),
return env; "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 } { export function response(rpc): { code: number; headers: {}; data: string } {
const headers = { const headers = {
@@ -679,30 +664,20 @@ export function response(rpc): { code: number; headers: {}; data: string } {
if (!rpc) return { code: 204, headers: headers, data: "" }; if (!rpc) return { code: 204, headers: headers, data: "" };
const env = createSoapEnv(rpc.cwmpVersion); let body;
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) { if (rpc.acsResponse) {
switch (rpc.acsResponse.name) { switch (rpc.acsResponse.name) {
case "InformResponse": case "InformResponse":
InformResponse(body); body = InformResponse();
break; break;
case "GetRPCMethodsResponse": case "GetRPCMethodsResponse":
GetRPCMethodsResponse(body, rpc.acsResponse); body = GetRPCMethodsResponse(rpc.acsResponse);
break; break;
case "TransferCompleteResponse": case "TransferCompleteResponse":
TransferCompleteResponse(body); body = TransferCompleteResponse();
break; break;
case "RequestDownloadResponse": case "RequestDownloadResponse":
RequestDownloadResponse(body); body = RequestDownloadResponse();
break; break;
default: default:
throw new Error(`Unknown method response type ${rpc.acsResponse.name}`); 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) { } else if (rpc.acsRequest) {
switch (rpc.acsRequest.name) { switch (rpc.acsRequest.name) {
case "GetParameterNames": case "GetParameterNames":
GetParameterNames(body, rpc.acsRequest); body = GetParameterNames(rpc.acsRequest);
break; break;
case "GetParameterValues": case "GetParameterValues":
GetParameterValues(body, rpc.acsRequest); body = GetParameterValues(rpc.acsRequest);
break; break;
case "SetParameterValues": case "SetParameterValues":
SetParameterValues(body, rpc.acsRequest); body = SetParameterValues(rpc.acsRequest);
break; break;
case "AddObject": case "AddObject":
AddObject(body, rpc.acsRequest); body = AddObject(rpc.acsRequest);
break; break;
case "DeleteObject": case "DeleteObject":
DeleteObject(body, rpc.acsRequest); body = DeleteObject(rpc.acsRequest);
break; break;
case "Reboot": case "Reboot":
Reboot(body, rpc.acsRequest); body = Reboot(rpc.acsRequest);
break; break;
case "FactoryReset": case "FactoryReset":
FactoryReset(body); body = FactoryReset();
break; break;
case "Download": case "Download":
Download(body, rpc.acsRequest); body = Download(rpc.acsRequest);
break; break;
default: default:
throw new Error(`Unknown method request ${rpc.acsRequest.name}`); throw new Error(`Unknown method request ${rpc.acsRequest.name}`);
@@ -742,6 +717,10 @@ export function response(rpc): { code: number; headers: {}; data: string } {
return { return {
code: 200, code: 200,
headers: headers, 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>`
}; };
} }