| """ |
| 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. |
| """ |
| |
| from logger import EnodebdLogger as logger |
| from state_machines.enb_acs_manager import StateMachineManager |
| from spyne.decorator import rpc |
| from spyne.model.complex import ComplexModelBase |
| from spyne.server.wsgi import WsgiMethodContext |
| from spyne.service import ServiceBase |
| |
| from . import models |
| |
| # Allow methods without 'self' as first input. Required by spyne |
| # pylint: disable=no-self-argument |
| |
| # RPC methods supported by ACS |
| RPC_METHODS = ['Inform', 'GetRPCMethods', 'TransferComplete'] |
| RPC_RESPONSES = [method + 'Response' for method in RPC_METHODS] |
| # RPC methods supported by CPE |
| CPE_RPC_METHODS = [ |
| 'SetParameterValues', |
| 'GetParameterValues', |
| 'GetParameterNames', |
| 'SetParameterAttributes', |
| 'GetParameterAttributes', |
| 'AddObject', |
| 'DeleteObject', |
| 'Download', |
| 'Reboot', |
| ] |
| CPE_RPC_RESPONSES = [method + 'Response' for method in CPE_RPC_METHODS] |
| # ACS RPC methods that are not explicitly described by the spec (hence shouldn't |
| # be advertised by GetRPCMethods). Note: No responses for these |
| PSEUDO_RPC_METHODS = ['Fault'] |
| # Top-level CWMP header elements. Namespaces should be preserved on these (since |
| # they are not within other CWMP elements) |
| TOP_LEVEL_HEADER_ELEMENTS = ['ID', 'HoldRequests'] |
| |
| |
| def fill_response_header(ctx): |
| """ Echo message ID from input header to output header, when responding to |
| CPE->ACS RPC calls """ |
| ctx.out_header = models.ID(mustUnderstand='1') |
| ctx.out_header.Data = ctx.in_header.Data |
| |
| |
| class AutoConfigServer(ServiceBase): |
| """ TR-069 ACS implementation. The TR-069/CWMP RPC messages are defined, as |
| per cwmp-1-0.xsd schema definition, in the RPC decorators below. These |
| RPC methods are intended to be called by TR-069-compliant customer |
| premesis equipment (CPE), over the SOAP/HTTP interface defined by |
| TR-069. |
| |
| Per spyne documentation, this class is never instantiated, so all RPC |
| functions are implicitly staticmethods. Hence use static class variables |
| to hold state. |
| This also means that only a single thread can be used (since there are |
| no locks). |
| Note that staticmethod decorator can't be used in conjunction with rpc |
| decorator. |
| """ |
| __out_header__ = models.ID |
| __in_header__ = models.ID |
| _acs_to_cpe_queue = None |
| _cpe_to_acs_queue = None |
| |
| """ Set maxEnvelopes to 1, as per TR-069 spec """ |
| _max_envelopes = 1 |
| |
| @classmethod |
| def set_state_machine_manager( |
| cls, |
| state_machine_manager: StateMachineManager, |
| ) -> None: |
| cls.state_machine_manager = state_machine_manager |
| |
| @classmethod |
| def _handle_tr069_message( |
| cls, |
| ctx: WsgiMethodContext, |
| message: ComplexModelBase, |
| ) -> ComplexModelBase: |
| # Log incoming msg |
| if hasattr(message, 'as_dict'): |
| logger.debug('Handling TR069 message: %s', str(type(message))) |
| else: |
| logger.debug('Handling TR069 message.') |
| |
| req = cls._get_tr069_response_from_sm(ctx, message) |
| |
| # Log outgoing msg |
| if hasattr(req, 'as_dict'): |
| logger.debug('Sending TR069 message: %s', str(req.as_dict())) |
| else: |
| logger.debug('Sending TR069 message.') |
| |
| # Set header |
| ctx.out_header = models.ID(mustUnderstand='1') |
| ctx.out_header.Data = 'null' |
| |
| # Set return message name |
| if isinstance(req, models.DummyInput): |
| # Generate 'empty' request to CPE using empty message name |
| ctx.descriptor.out_message.Attributes.sub_name = 'EmptyHttp' |
| return models.AcsToCpeRequests() |
| ctx.descriptor.out_message.Attributes.sub_name = req.__class__.__name__ |
| return cls._generate_acs_to_cpe_request_copy(req) |
| |
| @classmethod |
| def _get_tr069_response_from_sm( |
| cls, |
| ctx: WsgiMethodContext, |
| message: ComplexModelBase, |
| ) -> ComplexModelBase: |
| # We want to blanket-catch all exceptions because a problem with one |
| # tr-069 session shouldn't tank the service for all other enodeB's |
| # being managed |
| try: |
| return cls.state_machine_manager.handle_tr069_message(ctx, message) |
| except Exception: # pylint: disable=broad-except |
| logger.exception( |
| 'Unexpected exception from state machine manager, returning ' |
| 'empty request', |
| ) |
| return models.DummyInput() |
| |
| @staticmethod |
| def _generate_acs_to_cpe_request_copy(request): |
| """ Create an AcsToCpeRequests instance with all the appropriate |
| members set from the input request. AcsToCpeRequests is a union of |
| all request messages, so field names match. |
| """ |
| request_out = models.AcsToCpeRequests() |
| for parameter in request.get_flat_type_info(request.__class__): |
| try: |
| setattr(request_out, parameter, getattr(request, parameter)) |
| except AttributeError: |
| # Allow un-set parameters. If CPE can't handle this, it will |
| # respond with an error message |
| pass |
| |
| return request_out |
| |
| # CPE->ACS RPC calls |
| |
| @rpc( |
| models.GetRPCMethods, |
| _returns=models.GetRPCMethodsResponse, |
| _body_style="bare", |
| _operation_name="GetRPCMethods", |
| _out_message_name="GetRPCMethodsResponse", |
| ) |
| def get_rpc_methods(ctx, request): |
| """ GetRPCMethods RPC call is terminated here. No need to pass to higher |
| layer """ |
| fill_response_header(ctx) |
| resp = AutoConfigServer._handle_tr069_message(ctx, request) |
| return resp |
| |
| @rpc( |
| models.Inform, |
| _returns=models.InformResponse, |
| _body_style="bare", |
| _operation_name="Inform", |
| _out_message_name="InformResponse", |
| ) |
| def inform(ctx, request): |
| """ Inform response generated locally """ |
| fill_response_header(ctx) |
| resp = AutoConfigServer._handle_tr069_message(ctx, request) |
| resp.MaxEnvelopes = AutoConfigServer._max_envelopes |
| return resp |
| |
| @rpc( |
| models.TransferComplete, |
| _returns=models.TransferCompleteResponse, |
| _body_style="bare", |
| _operation_name="TransferComplete", |
| _out_message_name="TransferCompleteResponse", |
| ) |
| def transfer_complete(ctx, request): |
| fill_response_header(ctx) |
| resp = AutoConfigServer._handle_tr069_message(ctx, request) |
| resp.MaxEnvelopes = AutoConfigServer._max_envelopes |
| return resp |
| |
| # Spyne does not handle no input or SimpleModel input for 'bare' function |
| # DummyInput is unused |
| # pylint: disable=unused-argument |
| @rpc( |
| models.DummyInput, |
| _returns=models.AcsToCpeRequests, |
| _out_message_name="EmptyHttp", |
| _body_style='bare', |
| _operation_name="EmptyHttp", |
| ) |
| def empty_http(ctx, dummy): |
| # Function to handle empty HTTP request |
| return AutoConfigServer._handle_tr069_message(ctx, dummy) |
| |
| # CPE->ACS responses to ACS->CPE RPC calls |
| |
| @rpc( |
| models.SetParameterValuesResponse, |
| _returns=models.AcsToCpeRequests, |
| _out_message_name="MessageNameToBeReplaced", |
| _body_style='bare', |
| _operation_name="SetParameterValuesResponse", |
| ) |
| def set_parameter_values_response(ctx, response): |
| return AutoConfigServer._handle_tr069_message(ctx, response) |
| |
| @rpc( |
| models.GetParameterValuesResponse, |
| _returns=models.AcsToCpeRequests, |
| _out_message_name="MessageNameToBeReplaced", |
| _body_style='bare', |
| _operation_name="GetParameterValuesResponse", |
| ) |
| def get_parameter_values_response(ctx, response): |
| return AutoConfigServer._handle_tr069_message(ctx, response) |
| |
| @rpc( |
| models.GetParameterNamesResponse, |
| _returns=models.AcsToCpeRequests, |
| _out_message_name="MessageNameToBeReplaced", |
| _body_style='bare', |
| _operation_name="GetParameterNamesResponse", |
| ) |
| def get_parameter_names_response(ctx, response): |
| return AutoConfigServer._handle_tr069_message(ctx, response) |
| |
| @rpc( |
| models.SetParameterAttributesResponse, |
| _returns=models.AcsToCpeRequests, |
| _out_message_name="MessageNameToBeReplaced", |
| _body_style='bare', |
| _operation_name="SetParameterAttributesResponse", |
| ) |
| def set_parameter_attributes_response(ctx, response): |
| return AutoConfigServer._handle_tr069_message(ctx, response) |
| |
| @rpc( |
| models.GetParameterAttributesResponse, |
| _returns=models.AcsToCpeRequests, |
| _out_message_name="MessageNameToBeReplaced", |
| _body_style='bare', |
| _operation_name="GetParameterAttributesResponse", |
| ) |
| def get_parameter_attributes_response(ctx, response): |
| return AutoConfigServer._handle_tr069_message(ctx, response) |
| |
| @rpc( |
| models.AddObjectResponse, |
| _returns=models.AcsToCpeRequests, |
| _out_message_name="MessageNameToBeReplaced", |
| _body_style='bare', |
| _operation_name="AddObjectResponse", |
| ) |
| def add_object_response(ctx, response): |
| return AutoConfigServer._handle_tr069_message(ctx, response) |
| |
| @rpc( |
| models.DeleteObjectResponse, |
| _returns=models.AcsToCpeRequests, |
| _out_message_name="MessageNameToBeReplaced", |
| _body_style='bare', |
| _operation_name="DeleteObjectResponse", |
| ) |
| def delete_object_response(ctx, response): |
| return AutoConfigServer._handle_tr069_message(ctx, response) |
| |
| @rpc( |
| models.DownloadResponse, |
| _returns=models.AcsToCpeRequests, |
| _out_message_name="MessageNameToBeReplaced", |
| _body_style='bare', |
| _operation_name="DownloadResponse", |
| ) |
| def download_response(ctx, response): |
| return AutoConfigServer._handle_tr069_message(ctx, response) |
| |
| @rpc( |
| models.RebootResponse, |
| _returns=models.AcsToCpeRequests, |
| _out_message_name="MessageNameToBeReplaced", |
| _body_style='bare', |
| _operation_name="RebootResponse", |
| ) |
| def reboot_response(ctx, response): |
| return AutoConfigServer._handle_tr069_message(ctx, response) |
| |
| @rpc( |
| models.Fault, |
| _returns=models.AcsToCpeRequests, |
| _out_message_name="MessageNameToBeReplaced", |
| _body_style='bare', |
| _operation_name="Fault", |
| ) |
| def fault(ctx, response): |
| return AutoConfigServer._handle_tr069_message(ctx, response) |
| |
| |
| def on_method_return_string(ctx): |
| """ |
| By default, spyne adds a namespace to every single XML element. |
| There isn't a way to change this behavior, and the spyne-recommended way |
| to fix this is by doing string manipulation. The TR-069 spec mandates that |
| only the top-level CWMP elements contain namespaces. Hence this |
| function is to remove namespaces from all elements except top-level CWMP |
| elements (e.g. RPC request/response names, header elements). |
| """ |
| # Format strings for XML tags, corresponding to: |
| # 1) Normal start or end tag (without attribute) |
| # 2) Open and close tag (when no attributes or sub-structures exist) |
| # 3) Tag containing attributes |
| # We don't just look for 'cwmp:%s' (with no character after %s) because this |
| # would pick up all tags that start with the tag of interest (e.g |
| # cwmp:SetParameterAttributes would also match |
| # cwmp:SetParameterAttributesStruct) |
| |
| XML_FORMAT_STRS = [ |
| ["cwmp:%s>", "!!!TEMP_MOD!!!:%s>"], |
| ["cwmp:%s/>", "!!!TEMP_MOD!!!:%s/>"], |
| ["cwmp:%s ", "!!!TEMP_MOD!!!:%s "], |
| ] |
| fields_to_preserve_ns = list(RPC_METHODS) + list(RPC_RESPONSES) + \ |
| list(CPE_RPC_METHODS) + list(CPE_RPC_RESPONSES) + \ |
| list(PSEUDO_RPC_METHODS) + list(TOP_LEVEL_HEADER_ELEMENTS) |
| for field in fields_to_preserve_ns: |
| for formats in XML_FORMAT_STRS: |
| orig_str = formats[0] % field |
| temp_str = formats[1] % field |
| ctx.out_string[0] = ctx.out_string[0].replace( |
| orig_str.encode('ascii'), temp_str.encode('ascii'), |
| ) |
| |
| # Also preserve namespace inside strings, e.g. for arrayType="cwmp:..." |
| orig_str = "=\"cwmp:" |
| temp_str = "=\"!!!TEMP_MOD!!!:" |
| ctx.out_string[0] = ctx.out_string[0].replace( |
| orig_str.encode('ascii'), temp_str.encode('ascii'), |
| ) |
| orig_str = "=\'cwmp:" |
| temp_str = "=\'!!!TEMP_MOD!!!:" |
| ctx.out_string[0] = ctx.out_string[0].replace( |
| orig_str.encode('ascii'), temp_str.encode('ascii'), |
| ) |
| |
| ctx.out_string[0] = ctx.out_string[0].replace(b'cwmp:', b'') |
| ctx.out_string[0] = ctx.out_string[0].replace(b'!!!TEMP_MOD!!!:', b'cwmp:') |
| |
| # Special-case handling so that 'EmptyHttp' RPC will be called using |
| # completely empty HTTP request (not even containing a SOAP envelope), as |
| # per TR-069 spec. |
| if(ctx.descriptor.out_message.Attributes.sub_name == 'EmptyHttp'): |
| ctx.out_string = [b''] |
| |
| AutoConfigServer.event_manager.add_listener( |
| 'method_return_string', |
| on_method_return_string, |
| ) |