/** * Copyright 2013-2019 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, Element, parseAttrs, encodeEntities, decodeEntities } from "./xml-parser"; import memoize from "./common/memoize"; 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" } }; let warnings; const memoizedParseAttrs = memoize(parseAttrs); 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: Element): string[] { return xml.children .filter(n => n.localName === "EventStruct") .map(c => c.children.find(n => n.localName === "EventCode").text.trim()); } 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; break; case "Writable": value = c.text; break; } } let parsed: boolean = parseBool(value); if (parsed === null) { warnings.push({ message: "Invalid writable attribute", parameter: param }); parsed = false; } return [param, parsed]; }); } const getValueType = memoize((str: string) => parseAttrs(str) .find(s => s.localName === "type") .value.trim() ); function parameterValueList( xml: Element ): [string, string | number | boolean, string][] { 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; break; case "Value": valueElement = c; break; } } 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) { 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(methodRequest): string { return `${ methodRequest.parameterPath }${+methodRequest.nextLevel}`; } function GetParameterNamesResponse(xml): CpeGetResponse { return { name: "GetParameterNamesResponse", parameterList: parameterInfoList( xml.children.find(n => n.localName === "ParameterList") ) }; } function GetParameterValues(methodRequest): string { return `${methodRequest.parameterNames .map(p => `${p}`) .join("")}`; } function GetParameterValuesResponse(xml: Element): CpeGetResponse { return { name: "GetParameterValuesResponse", parameterList: parameterValueList( xml.children.find(n => n.localName === "ParameterList") ) }; } 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(); if (methodRequest.DATETIME_MILLISECONDS === false) val = val.replace(".000", ""); } if (p[2] === "xsd:boolean" && typeof val === "boolean") if (methodRequest.BOOLEAN_LITERAL === false) val = +val; `${p[0]}${encodeEntities("" + val)}`; }); return `${params.join( "" )}${methodRequest.parameterKey || ""}`; } function SetParameterValuesResponse(xml: Element): CpeSetResponse { return { name: "SetParameterValuesResponse", status: parseInt(xml.children.find(n => n.localName === "Status").text) }; } function AddObject(methodRequest): string { return `${ methodRequest.objectName }${methodRequest.parameterKey || ""}`; } function AddObjectResponse(xml: Element): CpeSetResponse { let instanceNumber, status; for (const c of xml.children) { switch (c.localName) { case "InstanceNumber": instanceNumber = parseInt(c.text); break; case "Status": status = parseInt(c.text); break; } } return { name: "AddObjectResponse", instanceNumber: instanceNumber, status: status }; } function DeleteObject(methodRequest): string { return `${ methodRequest.objectName }${methodRequest.parameterKey || ""}`; } function DeleteObjectResponse(xml: Element): CpeSetResponse { return { name: "DeleteObjectResponse", status: parseInt(xml.children.find(n => n.localName === "Status").text) }; } function Reboot(methodRequest): string { return `${methodRequest.commandKey || ""}`; } function RebootResponse(): CpeSetResponse { return { name: "RebootResponse" }; } function FactoryReset(): string { return ""; } function FactoryResetResponse(): CpeSetResponse { return { name: "FactoryResetResponse" }; } 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: Element): CpeSetResponse { let status, startTime, completeTime; for (const c of xml.children) { switch (c.localName) { 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: Element): InformRequest { let retryCount, evnt, parameterList; const deviceId = { Manufacturer: null, OUI: null, ProductClass: null, SerialNumber: null }; for (const c of xml.children) { switch (c.localName) { case "ParameterList": parameterList = parameterValueList(c); break; case "DeviceId": 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); break; } } return { name: "Inform", parameterList: parameterList, deviceId: deviceId, event: evnt, retryCount: retryCount }; } function InformResponse(): string { return "1"; } function GetRPCMethods(): AcsResponse { return { name: "GetRPCMethods" }; } function GetRPCMethodsResponse(methodResponse): string { return `${methodResponse.methodList .map(m => `${m}`) .join("")}`; } function TransferComplete(xml: Element): TransferCompleteRequest { let commandKey, _faultStruct, startTime, completeTime; for (const c of xml.children) { switch (c.localName) { 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(): string { return ""; } function RequestDownload(xml: Element): CpeRequest { return { name: "RequestDownload", fileType: xml.children.find(n => n.localName === "FileType").text }; } function RequestDownloadResponse(): string { return ""; } function faultStruct(xml: Element): FaultStruct { let faultCode, faultString, setParameterValuesFault: SpvFault[], pn, fc, fs; for (const c of xml.children) { switch (c.localName) { case "FaultCode": faultCode = c.text; break; case "FaultString": faultString = decodeEntities(c.text); break; case "SetParameterValuesFault": setParameterValuesFault = setParameterValuesFault || []; for (const cc of c.children) { switch (cc.localName) { case "ParameterName": pn = cc.text; break; case "FaultCode": fc = cc.text; break; case "FaultString": fs = decodeEntities(cc.text); break; } } setParameterValuesFault.push({ parameterName: pn, faultCode: fc, faultString: fs }); } } return { faultCode, faultString, setParameterValuesFault }; } function fault(xml: Element): CpeFault { let faultCode, faultString, detail; for (const c of xml.children) { switch (c.localName) { case "faultcode": faultCode = c.text; break; case "faultstring": faultString = decodeEntities(c.text); break; case "detail": detail = faultStruct(c.children.find(n => n.localName === "Fault")); break; } } return { faultCode, faultString, detail }; } export function request(body: string, cwmpVersion, warn): SoapMessage { warnings = warn; const rpc = { id: null, cwmpVersion: cwmpVersion, sessionTimeout: null, cpeRequest: null, cpeFault: null, cpeResponse: null }; if (!body.length) return rpc; const xml = parseXml(body); if (!xml.children.length) return rpc; const envelope = xml.children[0]; let headerElement: Element, bodyElement: Element; for (const c of envelope.children) { switch (c.localName) { case "Header": headerElement = c; break; case "Body": bodyElement = c; break; } } if (headerElement) { for (const c of headerElement.children) { switch (c.localName) { case "ID": rpc.id = decodeEntities(c.text); break; case "sessionTimeout": rpc.sessionTimeout = parseInt(c.text); break; } } } const methodElement = bodyElement.children[0]; 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"); if (attr) namespaceHref = attr.value; } } switch (namespaceHref) { 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 (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; } 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 = { Server: SERVER_NAME, SOAPServer: SERVER_NAME }; if (!rpc) return { code: 204, headers: headers, data: "" }; let body; if (rpc.acsResponse) { switch (rpc.acsResponse.name) { case "InformResponse": body = InformResponse(); break; case "GetRPCMethodsResponse": body = GetRPCMethodsResponse(rpc.acsResponse); break; case "TransferCompleteResponse": body = TransferCompleteResponse(); break; case "RequestDownloadResponse": body = RequestDownloadResponse(); break; default: throw new Error(`Unknown method response type ${rpc.acsResponse.name}`); } } else if (rpc.acsRequest) { switch (rpc.acsRequest.name) { case "GetParameterNames": body = GetParameterNames(rpc.acsRequest); break; case "GetParameterValues": body = GetParameterValues(rpc.acsRequest); break; case "SetParameterValues": body = SetParameterValues(rpc.acsRequest); break; case "AddObject": body = AddObject(rpc.acsRequest); break; case "DeleteObject": body = DeleteObject(rpc.acsRequest); break; case "Reboot": body = Reboot(rpc.acsRequest); break; case "FactoryReset": body = FactoryReset(); break; case "Download": body = Download(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: `\n${ rpc.id }${body}` }; }