From f4c9321d4dea2f26f0c809584f4a7cf70eda5e80 Mon Sep 17 00:00:00 2001 From: Zaid Abdulla Date: Tue, 7 May 2019 19:26:32 -0700 Subject: [PATCH] Implement SOAP processor using our new XML parser --- lib/config.ts | 1 + lib/cwmp.ts | 32 +- lib/soap-libxmljs.ts | 747 +++++++++++++++++++++++++++++++++++++++++++ lib/soap.ts | 573 ++++++++++++++++----------------- 4 files changed, 1051 insertions(+), 302 deletions(-) create mode 100644 lib/soap-libxmljs.ts diff --git a/lib/config.ts b/lib/config.ts index 8bdb2a5..d5645b1 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -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 } diff --git a/lib/cwmp.ts b/lib/cwmp.ts index 9fcceb1..5a74201 100644 --- a/lib/cwmp.ts +++ b/lib/cwmp.ts @@ -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 { 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); diff --git a/lib/soap-libxmljs.ts b/lib/soap-libxmljs.ts new file mode 100644 index 0000000..166c9a9 --- /dev/null +++ b/lib/soap-libxmljs.ts @@ -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 . + */ + +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) + }; +} diff --git a/lib/soap.ts b/lib/soap.ts index 166c9a9..282961f 100644 --- a/lib/soap.ts +++ b/lib/soap.ts @@ -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 . */ -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 `${ + methodRequest.parameterPath + }${+methodRequest.nextLevel}`; } 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 `${methodRequest.parameterNames + .map(p => `${p}`) + .join("")}`; } -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; + `${p[0]}${encodeEntities("" + val)}`; + }); - pvs - .node("Value") - .attr({ "xsi:type": p[2] }) - .text(val); - } - - el.node("ParameterKey").text(methodRequest.parameterKey || ""); + return `${params.join( + "" + )}${methodRequest.parameterKey || + ""}`; } -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 `${ + methodRequest.objectName + }${methodRequest.parameterKey || + ""}`; } -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 `${ + methodRequest.objectName + }${methodRequest.parameterKey || + ""}`; } -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 `${methodRequest.commandKey || + ""}`; } function RebootResponse(): CpeSetResponse { @@ -339,8 +302,8 @@ function RebootResponse(): CpeSetResponse { }; } -function FactoryReset(xml): void { - xml.node("cwmp:FactoryReset"); +function FactoryReset(): string { + return ""; } 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 `${methodRequest.commandKey || + ""}${methodRequest.fileType}${ + methodRequest.url + }${encodeEntities( + methodRequest.username || "" + )}${encodeEntities( + methodRequest.password || "" + )}${methodRequest.fileSize || + "0"}${encodeEntities( + methodRequest.targetFileName || "" + )}${methodRequest.delaySeconds || + "0"}${encodeEntities( + methodRequest.successUrl || "" + )}${encodeEntities( + methodRequest.failureUrl || "" + )}`; } -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 "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 GetRPCMethodsResponse(methodResponse): string { + return `${methodResponse.methodList + .map(m => `${m}`) + .join("")}`; } -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 ""; } -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 ""; } -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: `\n${ + rpc.id + }${body}` }; }