blob: 46315a89ad6e699f8a1f84d1e22d8aaaeaceda88 [file] [log] [blame]
Wei-Yu Chenad55cb82022-02-15 20:07:01 +08001# SPDX-FileCopyrightText: 2020 The Magma Authors.
2# SPDX-FileCopyrightText: 2022 Open Networking Foundation <support@opennetworking.org>
3#
4# SPDX-License-Identifier: BSD-3-Clause
Wei-Yu Chen49950b92021-11-08 19:19:18 +08005
6from logger import EnodebdLogger as logger
7from state_machines.enb_acs_manager import StateMachineManager
8from spyne.decorator import rpc
9from spyne.model.complex import ComplexModelBase
10from spyne.server.wsgi import WsgiMethodContext
11from spyne.service import ServiceBase
12
13from . import models
14
15# Allow methods without 'self' as first input. Required by spyne
16# pylint: disable=no-self-argument
17
18# RPC methods supported by ACS
19RPC_METHODS = ['Inform', 'GetRPCMethods', 'TransferComplete']
20RPC_RESPONSES = [method + 'Response' for method in RPC_METHODS]
21# RPC methods supported by CPE
22CPE_RPC_METHODS = [
23 'SetParameterValues',
24 'GetParameterValues',
25 'GetParameterNames',
26 'SetParameterAttributes',
27 'GetParameterAttributes',
28 'AddObject',
29 'DeleteObject',
30 'Download',
31 'Reboot',
32]
33CPE_RPC_RESPONSES = [method + 'Response' for method in CPE_RPC_METHODS]
34# ACS RPC methods that are not explicitly described by the spec (hence shouldn't
35# be advertised by GetRPCMethods). Note: No responses for these
36PSEUDO_RPC_METHODS = ['Fault']
37# Top-level CWMP header elements. Namespaces should be preserved on these (since
38# they are not within other CWMP elements)
39TOP_LEVEL_HEADER_ELEMENTS = ['ID', 'HoldRequests']
40
41
42def fill_response_header(ctx):
43 """ Echo message ID from input header to output header, when responding to
44 CPE->ACS RPC calls """
45 ctx.out_header = models.ID(mustUnderstand='1')
46 ctx.out_header.Data = ctx.in_header.Data
47
48
49class AutoConfigServer(ServiceBase):
50 """ TR-069 ACS implementation. The TR-069/CWMP RPC messages are defined, as
51 per cwmp-1-0.xsd schema definition, in the RPC decorators below. These
52 RPC methods are intended to be called by TR-069-compliant customer
53 premesis equipment (CPE), over the SOAP/HTTP interface defined by
54 TR-069.
55
56 Per spyne documentation, this class is never instantiated, so all RPC
57 functions are implicitly staticmethods. Hence use static class variables
58 to hold state.
59 This also means that only a single thread can be used (since there are
60 no locks).
61 Note that staticmethod decorator can't be used in conjunction with rpc
62 decorator.
63 """
64 __out_header__ = models.ID
65 __in_header__ = models.ID
66 _acs_to_cpe_queue = None
67 _cpe_to_acs_queue = None
68
69 """ Set maxEnvelopes to 1, as per TR-069 spec """
70 _max_envelopes = 1
71
72 @classmethod
73 def set_state_machine_manager(
74 cls,
75 state_machine_manager: StateMachineManager,
76 ) -> None:
77 cls.state_machine_manager = state_machine_manager
78
79 @classmethod
80 def _handle_tr069_message(
81 cls,
82 ctx: WsgiMethodContext,
83 message: ComplexModelBase,
84 ) -> ComplexModelBase:
85 # Log incoming msg
86 if hasattr(message, 'as_dict'):
87 logger.debug('Handling TR069 message: %s', str(type(message)))
88 else:
89 logger.debug('Handling TR069 message.')
90
91 req = cls._get_tr069_response_from_sm(ctx, message)
92
93 # Log outgoing msg
94 if hasattr(req, 'as_dict'):
95 logger.debug('Sending TR069 message: %s', str(req.as_dict()))
96 else:
97 logger.debug('Sending TR069 message.')
98
99 # Set header
100 ctx.out_header = models.ID(mustUnderstand='1')
101 ctx.out_header.Data = 'null'
102
103 # Set return message name
104 if isinstance(req, models.DummyInput):
105 # Generate 'empty' request to CPE using empty message name
106 ctx.descriptor.out_message.Attributes.sub_name = 'EmptyHttp'
107 return models.AcsToCpeRequests()
108 ctx.descriptor.out_message.Attributes.sub_name = req.__class__.__name__
109 return cls._generate_acs_to_cpe_request_copy(req)
110
111 @classmethod
112 def _get_tr069_response_from_sm(
113 cls,
114 ctx: WsgiMethodContext,
115 message: ComplexModelBase,
116 ) -> ComplexModelBase:
117 # We want to blanket-catch all exceptions because a problem with one
118 # tr-069 session shouldn't tank the service for all other enodeB's
119 # being managed
120 try:
121 return cls.state_machine_manager.handle_tr069_message(ctx, message)
122 except Exception: # pylint: disable=broad-except
123 logger.exception(
124 'Unexpected exception from state machine manager, returning '
125 'empty request',
126 )
127 return models.DummyInput()
128
129 @staticmethod
130 def _generate_acs_to_cpe_request_copy(request):
131 """ Create an AcsToCpeRequests instance with all the appropriate
132 members set from the input request. AcsToCpeRequests is a union of
133 all request messages, so field names match.
134 """
135 request_out = models.AcsToCpeRequests()
136 for parameter in request.get_flat_type_info(request.__class__):
137 try:
138 setattr(request_out, parameter, getattr(request, parameter))
139 except AttributeError:
140 # Allow un-set parameters. If CPE can't handle this, it will
141 # respond with an error message
142 pass
Wei-Yu Chen5cbdfbb2021-12-02 01:10:21 +0800143
Wei-Yu Chen49950b92021-11-08 19:19:18 +0800144 return request_out
145
146 # CPE->ACS RPC calls
147
148 @rpc(
149 models.GetRPCMethods,
150 _returns=models.GetRPCMethodsResponse,
151 _body_style="bare",
152 _operation_name="GetRPCMethods",
153 _out_message_name="GetRPCMethodsResponse",
154 )
155 def get_rpc_methods(ctx, request):
156 """ GetRPCMethods RPC call is terminated here. No need to pass to higher
157 layer """
158 fill_response_header(ctx)
159 resp = AutoConfigServer._handle_tr069_message(ctx, request)
160 return resp
161
162 @rpc(
163 models.Inform,
164 _returns=models.InformResponse,
165 _body_style="bare",
166 _operation_name="Inform",
167 _out_message_name="InformResponse",
168 )
169 def inform(ctx, request):
170 """ Inform response generated locally """
171 fill_response_header(ctx)
172 resp = AutoConfigServer._handle_tr069_message(ctx, request)
173 resp.MaxEnvelopes = AutoConfigServer._max_envelopes
174 return resp
175
176 @rpc(
177 models.TransferComplete,
178 _returns=models.TransferCompleteResponse,
179 _body_style="bare",
180 _operation_name="TransferComplete",
181 _out_message_name="TransferCompleteResponse",
182 )
183 def transfer_complete(ctx, request):
184 fill_response_header(ctx)
185 resp = AutoConfigServer._handle_tr069_message(ctx, request)
186 resp.MaxEnvelopes = AutoConfigServer._max_envelopes
187 return resp
188
189 # Spyne does not handle no input or SimpleModel input for 'bare' function
190 # DummyInput is unused
191 # pylint: disable=unused-argument
192 @rpc(
193 models.DummyInput,
194 _returns=models.AcsToCpeRequests,
195 _out_message_name="EmptyHttp",
196 _body_style='bare',
197 _operation_name="EmptyHttp",
198 )
199 def empty_http(ctx, dummy):
200 # Function to handle empty HTTP request
201 return AutoConfigServer._handle_tr069_message(ctx, dummy)
202
203 # CPE->ACS responses to ACS->CPE RPC calls
204
205 @rpc(
206 models.SetParameterValuesResponse,
207 _returns=models.AcsToCpeRequests,
208 _out_message_name="MessageNameToBeReplaced",
209 _body_style='bare',
210 _operation_name="SetParameterValuesResponse",
211 )
212 def set_parameter_values_response(ctx, response):
213 return AutoConfigServer._handle_tr069_message(ctx, response)
214
215 @rpc(
216 models.GetParameterValuesResponse,
217 _returns=models.AcsToCpeRequests,
218 _out_message_name="MessageNameToBeReplaced",
219 _body_style='bare',
220 _operation_name="GetParameterValuesResponse",
221 )
222 def get_parameter_values_response(ctx, response):
223 return AutoConfigServer._handle_tr069_message(ctx, response)
224
225 @rpc(
226 models.GetParameterNamesResponse,
227 _returns=models.AcsToCpeRequests,
228 _out_message_name="MessageNameToBeReplaced",
229 _body_style='bare',
230 _operation_name="GetParameterNamesResponse",
231 )
232 def get_parameter_names_response(ctx, response):
233 return AutoConfigServer._handle_tr069_message(ctx, response)
234
235 @rpc(
236 models.SetParameterAttributesResponse,
237 _returns=models.AcsToCpeRequests,
238 _out_message_name="MessageNameToBeReplaced",
239 _body_style='bare',
240 _operation_name="SetParameterAttributesResponse",
241 )
242 def set_parameter_attributes_response(ctx, response):
243 return AutoConfigServer._handle_tr069_message(ctx, response)
244
245 @rpc(
246 models.GetParameterAttributesResponse,
247 _returns=models.AcsToCpeRequests,
248 _out_message_name="MessageNameToBeReplaced",
249 _body_style='bare',
250 _operation_name="GetParameterAttributesResponse",
251 )
252 def get_parameter_attributes_response(ctx, response):
253 return AutoConfigServer._handle_tr069_message(ctx, response)
254
255 @rpc(
256 models.AddObjectResponse,
257 _returns=models.AcsToCpeRequests,
258 _out_message_name="MessageNameToBeReplaced",
259 _body_style='bare',
260 _operation_name="AddObjectResponse",
261 )
262 def add_object_response(ctx, response):
263 return AutoConfigServer._handle_tr069_message(ctx, response)
264
265 @rpc(
266 models.DeleteObjectResponse,
267 _returns=models.AcsToCpeRequests,
268 _out_message_name="MessageNameToBeReplaced",
269 _body_style='bare',
270 _operation_name="DeleteObjectResponse",
271 )
272 def delete_object_response(ctx, response):
273 return AutoConfigServer._handle_tr069_message(ctx, response)
274
275 @rpc(
276 models.DownloadResponse,
277 _returns=models.AcsToCpeRequests,
278 _out_message_name="MessageNameToBeReplaced",
279 _body_style='bare',
280 _operation_name="DownloadResponse",
281 )
282 def download_response(ctx, response):
283 return AutoConfigServer._handle_tr069_message(ctx, response)
284
285 @rpc(
286 models.RebootResponse,
287 _returns=models.AcsToCpeRequests,
288 _out_message_name="MessageNameToBeReplaced",
289 _body_style='bare',
290 _operation_name="RebootResponse",
291 )
292 def reboot_response(ctx, response):
293 return AutoConfigServer._handle_tr069_message(ctx, response)
294
295 @rpc(
296 models.Fault,
297 _returns=models.AcsToCpeRequests,
298 _out_message_name="MessageNameToBeReplaced",
299 _body_style='bare',
300 _operation_name="Fault",
301 )
302 def fault(ctx, response):
303 return AutoConfigServer._handle_tr069_message(ctx, response)
304
305
306def on_method_return_string(ctx):
307 """
308 By default, spyne adds a namespace to every single XML element.
309 There isn't a way to change this behavior, and the spyne-recommended way
310 to fix this is by doing string manipulation. The TR-069 spec mandates that
311 only the top-level CWMP elements contain namespaces. Hence this
312 function is to remove namespaces from all elements except top-level CWMP
313 elements (e.g. RPC request/response names, header elements).
314 """
315 # Format strings for XML tags, corresponding to:
316 # 1) Normal start or end tag (without attribute)
317 # 2) Open and close tag (when no attributes or sub-structures exist)
318 # 3) Tag containing attributes
319 # We don't just look for 'cwmp:%s' (with no character after %s) because this
320 # would pick up all tags that start with the tag of interest (e.g
321 # cwmp:SetParameterAttributes would also match
322 # cwmp:SetParameterAttributesStruct)
Wei-Yu Chen5cbdfbb2021-12-02 01:10:21 +0800323
Wei-Yu Chen49950b92021-11-08 19:19:18 +0800324 XML_FORMAT_STRS = [
325 ["cwmp:%s>", "!!!TEMP_MOD!!!:%s>"],
326 ["cwmp:%s/>", "!!!TEMP_MOD!!!:%s/>"],
327 ["cwmp:%s ", "!!!TEMP_MOD!!!:%s "],
328 ]
329 fields_to_preserve_ns = list(RPC_METHODS) + list(RPC_RESPONSES) + \
330 list(CPE_RPC_METHODS) + list(CPE_RPC_RESPONSES) + \
331 list(PSEUDO_RPC_METHODS) + list(TOP_LEVEL_HEADER_ELEMENTS)
332 for field in fields_to_preserve_ns:
333 for formats in XML_FORMAT_STRS:
334 orig_str = formats[0] % field
335 temp_str = formats[1] % field
336 ctx.out_string[0] = ctx.out_string[0].replace(
337 orig_str.encode('ascii'), temp_str.encode('ascii'),
338 )
339
340 # Also preserve namespace inside strings, e.g. for arrayType="cwmp:..."
341 orig_str = "=\"cwmp:"
342 temp_str = "=\"!!!TEMP_MOD!!!:"
343 ctx.out_string[0] = ctx.out_string[0].replace(
344 orig_str.encode('ascii'), temp_str.encode('ascii'),
345 )
346 orig_str = "=\'cwmp:"
347 temp_str = "=\'!!!TEMP_MOD!!!:"
348 ctx.out_string[0] = ctx.out_string[0].replace(
349 orig_str.encode('ascii'), temp_str.encode('ascii'),
350 )
351
352 ctx.out_string[0] = ctx.out_string[0].replace(b'cwmp:', b'')
353 ctx.out_string[0] = ctx.out_string[0].replace(b'!!!TEMP_MOD!!!:', b'cwmp:')
354
355 # Special-case handling so that 'EmptyHttp' RPC will be called using
356 # completely empty HTTP request (not even containing a SOAP envelope), as
357 # per TR-069 spec.
358 if(ctx.descriptor.out_message.Attributes.sub_name == 'EmptyHttp'):
359 ctx.out_string = [b'']
360
Wei-Yu Chen49950b92021-11-08 19:19:18 +0800361AutoConfigServer.event_manager.add_listener(
362 'method_return_string',
363 on_method_return_string,
364)