Init commit for standalone enodebd

Change-Id: I88eeef5135dd7ba8551ddd9fb6a0695f5325337b
diff --git a/tr069/spyne_mods.py b/tr069/spyne_mods.py
new file mode 100644
index 0000000..0f0e918
--- /dev/null
+++ b/tr069/spyne_mods.py
@@ -0,0 +1,155 @@
+"""
+Copyright 2020 The Magma Authors.
+
+This source code is licensed under the BSD-style license found in the
+LICENSE file in the root directory of this source tree.
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+This file contains modifications of the core spyne functionality. This is done
+using child classes and function override to avoid modifying spyne code itself.
+Each function below is a modified version of the parent function. These
+modifications are required because:
+1) Spyne is not fully python3-compliant
+2) Not all parts of the TR-069 spec are possible through spyne APIs (e.g RPC
+   calls from server to client in HTTP responses)
+3) Minor enhancements for debug-ability
+"""
+
+from lxml import etree
+from logger import EnodebdLogger as logger
+from spyne.application import Application
+from spyne.interface._base import Interface
+from spyne.protocol.soap import Soap11
+from spyne.protocol.xml import XmlDocument
+
+
+class Tr069Interface(Interface):
+    """ Modified base interface class. """
+
+    def reset_interface(self):
+        super(Tr069Interface, self).reset_interface()
+        # Replace default namespace prefix (may not strictly be
+        # required, but makes it easier to debug)
+        del self.nsmap['tns']
+        self.nsmap['cwmp'] = self.get_tns()
+        self.prefmap[self.get_tns()] = 'cwmp'
+        # To validate against the xsd:<types>, the namespace
+        # prefix is expected to be the same
+        del self.nsmap['xs']
+        self.nsmap['xsd'] = 'http://www.w3.org/2001/XMLSchema'
+        self.prefmap['http://www.w3.org/2001/XMLSchema'] = 'xsd'
+
+
+class Tr069Application(Application):
+    """ Modified spyne application. """
+
+    def __init__(
+        self, services, tns, name=None, in_protocol=None,
+        out_protocol=None, config=None,
+    ):
+        super(Tr069Application, self).__init__(
+            services, tns, name, in_protocol, out_protocol, config,
+        )
+        # Use modified interface class
+        self.interface = Tr069Interface(self)
+
+
+class Tr069Soap11(Soap11):
+    """ Modified SOAP protocol. """
+
+    def __init__(self, *args, **kwargs):
+        super(Tr069Soap11, self).__init__(*args, **kwargs)
+        # Disabling type resolution as a workaround for
+        # https://github.com/arskom/spyne/issues/567
+        self.parse_xsi_type = False
+        # Bug in spyne is cleaning up the default XSD namespace
+        # and causes validation issues on TR-069 clients
+        self.cleanup_namespaces = False
+
+    def create_in_document(self, ctx, charset=None):
+        """
+        In TR-069, the ACS (e.g Magma) is an HTTP server, but acts as a client
+        for SOAP messages. This is done by the CPE (e.g ENodeB) sending an
+        empty HTTP request, and the ACS responding with a SOAP request in the
+        HTTP response. This code replaces an empty HTTP request with a string
+        that gets decoded to a call to the 'EmptyHttp' RPC .
+        """
+
+        # Try cp437 as default to ensure that we dont get any decoding errors,
+        #  since it uses 1-byte encoding and has a 'full' char map
+        if not charset:
+            charset = 'cp437'
+
+        # Convert from generator to bytes before doing comparison
+        # Re-encode to chosen charset to remove invalid characters
+        in_string = b''.join(ctx.in_string).decode(charset, 'ignore')
+        ctx.in_string = [in_string.encode(charset, 'ignore')]
+        if ctx.in_string == [b'']:
+            ctx.in_string = [
+                b'<soap11env:Envelope xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:soap11env="http://schemas.xmlsoap.org/soap/envelope/">/n'
+                b'   <soap11env:Body>/n'
+                b'       <cwmp:EmptyHttp/>/n'
+                b'   </soap11env:Body>/n'
+                b'</soap11env:Envelope>',
+            ]
+
+        super(Tr069Soap11, self).create_in_document(ctx, charset)
+
+    def decompose_incoming_envelope(self, ctx, message=XmlDocument.REQUEST):
+        """
+        For TR-069, the SOAP fault message (CPE->ACS) contains useful
+        information, and should not result in another fault response (ACS->CPE).
+        Strip the outer SOAP fault structure, so that the CWMP fault structure
+        is treated as a normal RPC call (to the 'Fault' function).
+        """
+        super(Tr069Soap11, self).decompose_incoming_envelope(ctx, message)
+
+        if ctx.in_body_doc.tag == '{%s}Fault' % self.ns_soap_env:
+            faultstring = ctx.in_body_doc.findtext('faultstring')
+            if not faultstring or 'CWMP fault' not in faultstring:
+                # Not a CWMP fault
+                return
+
+            # Strip SOAP fault structure, leaving inner CWMP fault structure
+            detail_elem = ctx.in_body_doc.find('detail')
+            if detail_elem is not None:
+                detail_children = list(detail_elem)
+                if len(detail_children):
+                    if len(detail_children) > 1:
+                        logger.warning(
+                            "Multiple detail elements found in SOAP"
+                            " fault - using first one",
+                        )
+                    ctx.in_body_doc = detail_children[0]
+                    ctx.method_request_string = ctx.in_body_doc.tag
+                    self.validate_body(ctx, message)
+
+    def get_call_handles(self, ctx):
+        """
+        Modified function to fix bug in receiving SOAP fault. In this case,
+        ctx.method_request_string is None, so 'startswith' errors out.
+        """
+        if ctx.method_request_string is None:
+            return []
+
+        return super(Tr069Soap11, self).get_call_handles(ctx)
+
+    def serialize(self, ctx, message):
+        # Workaround for issue https://github.com/magma/magma/issues/7869
+        # Updates to ctx.descriptor.out_message.Attributes.sub_name are taking
+        # effect on the descriptor. But when puled from _attrcache dictionary,
+        # it still has a stale value.
+        # Force repopulation of dictionary by deleting entry
+        # TODO Remove this code once we have a better fix
+        if (ctx.descriptor and ctx.descriptor.out_message in self._attrcache):
+            del self._attrcache[ctx.descriptor.out_message]  # noqa: WPS529
+
+        super(Tr069Soap11, self).serialize(ctx, message)
+
+        # Keep XSD namespace
+        etree.cleanup_namespaces(ctx.out_document, keep_ns_prefixes=['xsd'])