blob: 7f191ff169d974adcfb023b3ea928063a504a3f4 [file] [log] [blame]
# SPDX-FileCopyrightText: 2020 The Magma Authors.
# SPDX-FileCopyrightText: 2022 Open Networking Foundation <support@opennetworking.org>
#
# SPDX-License-Identifier: BSD-3-Clause
"""
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'<soapenv:Envelope xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">/n'
b' <soapenv:Body>/n'
b' <cwmp:EmptyHttp/>/n'
b' </soapenv:Body>/n'
b'</soapenv: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'])