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