/** * Copyright 2013-2019 GenieACS Inc. * * 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) }; }