Wei-Yu Chen | ad55cb8 | 2022-02-15 20:07:01 +0800 | [diff] [blame] | 1 | # SPDX-FileCopyrightText: 2020 The Magma Authors. |
| 2 | # SPDX-FileCopyrightText: 2022 Open Networking Foundation <support@opennetworking.org> |
| 3 | # |
| 4 | # SPDX-License-Identifier: BSD-3-Clause |
| 5 | |
Wei-Yu Chen | 49950b9 | 2021-11-08 19:19:18 +0800 | [diff] [blame] | 6 | """ |
Wei-Yu Chen | 49950b9 | 2021-11-08 19:19:18 +0800 | [diff] [blame] | 7 | This file contains modifications of the core spyne functionality. This is done |
| 8 | using child classes and function override to avoid modifying spyne code itself. |
| 9 | Each function below is a modified version of the parent function. These |
| 10 | modifications are required because: |
| 11 | 1) Spyne is not fully python3-compliant |
| 12 | 2) Not all parts of the TR-069 spec are possible through spyne APIs (e.g RPC |
| 13 | calls from server to client in HTTP responses) |
| 14 | 3) Minor enhancements for debug-ability |
| 15 | """ |
| 16 | |
| 17 | from lxml import etree |
| 18 | from logger import EnodebdLogger as logger |
| 19 | from spyne.application import Application |
| 20 | from spyne.interface._base import Interface |
| 21 | from spyne.protocol.soap import Soap11 |
| 22 | from spyne.protocol.xml import XmlDocument |
| 23 | |
| 24 | |
| 25 | class Tr069Interface(Interface): |
| 26 | """ Modified base interface class. """ |
| 27 | |
| 28 | def reset_interface(self): |
| 29 | super(Tr069Interface, self).reset_interface() |
| 30 | # Replace default namespace prefix (may not strictly be |
| 31 | # required, but makes it easier to debug) |
| 32 | del self.nsmap['tns'] |
| 33 | self.nsmap['cwmp'] = self.get_tns() |
| 34 | self.prefmap[self.get_tns()] = 'cwmp' |
| 35 | # To validate against the xsd:<types>, the namespace |
| 36 | # prefix is expected to be the same |
| 37 | del self.nsmap['xs'] |
| 38 | self.nsmap['xsd'] = 'http://www.w3.org/2001/XMLSchema' |
| 39 | self.prefmap['http://www.w3.org/2001/XMLSchema'] = 'xsd' |
| 40 | |
| 41 | |
| 42 | class Tr069Application(Application): |
| 43 | """ Modified spyne application. """ |
| 44 | |
| 45 | def __init__( |
| 46 | self, services, tns, name=None, in_protocol=None, |
| 47 | out_protocol=None, config=None, |
| 48 | ): |
| 49 | super(Tr069Application, self).__init__( |
| 50 | services, tns, name, in_protocol, out_protocol, config, |
| 51 | ) |
| 52 | # Use modified interface class |
| 53 | self.interface = Tr069Interface(self) |
| 54 | |
| 55 | |
| 56 | class Tr069Soap11(Soap11): |
| 57 | """ Modified SOAP protocol. """ |
| 58 | |
| 59 | def __init__(self, *args, **kwargs): |
| 60 | super(Tr069Soap11, self).__init__(*args, **kwargs) |
| 61 | # Disabling type resolution as a workaround for |
| 62 | # https://github.com/arskom/spyne/issues/567 |
| 63 | self.parse_xsi_type = False |
| 64 | # Bug in spyne is cleaning up the default XSD namespace |
| 65 | # and causes validation issues on TR-069 clients |
| 66 | self.cleanup_namespaces = False |
| 67 | |
| 68 | def create_in_document(self, ctx, charset=None): |
| 69 | """ |
| 70 | In TR-069, the ACS (e.g Magma) is an HTTP server, but acts as a client |
| 71 | for SOAP messages. This is done by the CPE (e.g ENodeB) sending an |
| 72 | empty HTTP request, and the ACS responding with a SOAP request in the |
| 73 | HTTP response. This code replaces an empty HTTP request with a string |
| 74 | that gets decoded to a call to the 'EmptyHttp' RPC . |
| 75 | """ |
| 76 | |
| 77 | # Try cp437 as default to ensure that we dont get any decoding errors, |
| 78 | # since it uses 1-byte encoding and has a 'full' char map |
| 79 | if not charset: |
| 80 | charset = 'cp437' |
| 81 | |
| 82 | # Convert from generator to bytes before doing comparison |
| 83 | # Re-encode to chosen charset to remove invalid characters |
| 84 | in_string = b''.join(ctx.in_string).decode(charset, 'ignore') |
| 85 | ctx.in_string = [in_string.encode(charset, 'ignore')] |
| 86 | if ctx.in_string == [b'']: |
| 87 | ctx.in_string = [ |
Wei-Yu Chen | 5cbdfbb | 2021-12-02 01:10:21 +0800 | [diff] [blame] | 88 | b'<soapenv:Envelope xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">/n' |
| 89 | b' <soapenv:Body>/n' |
Wei-Yu Chen | 49950b9 | 2021-11-08 19:19:18 +0800 | [diff] [blame] | 90 | b' <cwmp:EmptyHttp/>/n' |
Wei-Yu Chen | 5cbdfbb | 2021-12-02 01:10:21 +0800 | [diff] [blame] | 91 | b' </soapenv:Body>/n' |
| 92 | b'</soapenv:Envelope>', |
Wei-Yu Chen | 49950b9 | 2021-11-08 19:19:18 +0800 | [diff] [blame] | 93 | ] |
| 94 | |
| 95 | super(Tr069Soap11, self).create_in_document(ctx, charset) |
| 96 | |
| 97 | def decompose_incoming_envelope(self, ctx, message=XmlDocument.REQUEST): |
| 98 | """ |
| 99 | For TR-069, the SOAP fault message (CPE->ACS) contains useful |
| 100 | information, and should not result in another fault response (ACS->CPE). |
| 101 | Strip the outer SOAP fault structure, so that the CWMP fault structure |
| 102 | is treated as a normal RPC call (to the 'Fault' function). |
| 103 | """ |
| 104 | super(Tr069Soap11, self).decompose_incoming_envelope(ctx, message) |
| 105 | |
| 106 | if ctx.in_body_doc.tag == '{%s}Fault' % self.ns_soap_env: |
| 107 | faultstring = ctx.in_body_doc.findtext('faultstring') |
| 108 | if not faultstring or 'CWMP fault' not in faultstring: |
| 109 | # Not a CWMP fault |
| 110 | return |
| 111 | |
| 112 | # Strip SOAP fault structure, leaving inner CWMP fault structure |
| 113 | detail_elem = ctx.in_body_doc.find('detail') |
| 114 | if detail_elem is not None: |
| 115 | detail_children = list(detail_elem) |
| 116 | if len(detail_children): |
| 117 | if len(detail_children) > 1: |
| 118 | logger.warning( |
| 119 | "Multiple detail elements found in SOAP" |
| 120 | " fault - using first one", |
| 121 | ) |
| 122 | ctx.in_body_doc = detail_children[0] |
| 123 | ctx.method_request_string = ctx.in_body_doc.tag |
| 124 | self.validate_body(ctx, message) |
| 125 | |
| 126 | def get_call_handles(self, ctx): |
| 127 | """ |
| 128 | Modified function to fix bug in receiving SOAP fault. In this case, |
| 129 | ctx.method_request_string is None, so 'startswith' errors out. |
| 130 | """ |
| 131 | if ctx.method_request_string is None: |
| 132 | return [] |
| 133 | |
| 134 | return super(Tr069Soap11, self).get_call_handles(ctx) |
| 135 | |
| 136 | def serialize(self, ctx, message): |
| 137 | # Workaround for issue https://github.com/magma/magma/issues/7869 |
| 138 | # Updates to ctx.descriptor.out_message.Attributes.sub_name are taking |
| 139 | # effect on the descriptor. But when puled from _attrcache dictionary, |
| 140 | # it still has a stale value. |
| 141 | # Force repopulation of dictionary by deleting entry |
| 142 | # TODO Remove this code once we have a better fix |
| 143 | if (ctx.descriptor and ctx.descriptor.out_message in self._attrcache): |
| 144 | del self._attrcache[ctx.descriptor.out_message] # noqa: WPS529 |
| 145 | |
| 146 | super(Tr069Soap11, self).serialize(ctx, message) |
| 147 | |
| 148 | # Keep XSD namespace |
| 149 | etree.cleanup_namespaces(ctx.out_document, keep_ns_prefixes=['xsd']) |