blob: c6203d94d78602388d66606f7d01ffecfb3d90f5 [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
Wei-Yu Chen5cbdfbb2021-12-02 01:10:21 +0800151
Wei-Yu Chen49950b92021-11-08 19:19:18 +0800152 return request_out
153
154 # CPE->ACS RPC calls
155
156 @rpc(
157 models.GetRPCMethods,
158 _returns=models.GetRPCMethodsResponse,
159 _body_style="bare",
160 _operation_name="GetRPCMethods",
161 _out_message_name="GetRPCMethodsResponse",
162 )
163 def get_rpc_methods(ctx, request):
164 """ GetRPCMethods RPC call is terminated here. No need to pass to higher
165 layer """
166 fill_response_header(ctx)
167 resp = AutoConfigServer._handle_tr069_message(ctx, request)
168 return resp
169
170 @rpc(
171 models.Inform,
172 _returns=models.InformResponse,
173 _body_style="bare",
174 _operation_name="Inform",
175 _out_message_name="InformResponse",
176 )
177 def inform(ctx, request):
178 """ Inform response generated locally """
179 fill_response_header(ctx)
180 resp = AutoConfigServer._handle_tr069_message(ctx, request)
181 resp.MaxEnvelopes = AutoConfigServer._max_envelopes
182 return resp
183
184 @rpc(
185 models.TransferComplete,
186 _returns=models.TransferCompleteResponse,
187 _body_style="bare",
188 _operation_name="TransferComplete",
189 _out_message_name="TransferCompleteResponse",
190 )
191 def transfer_complete(ctx, request):
192 fill_response_header(ctx)
193 resp = AutoConfigServer._handle_tr069_message(ctx, request)
194 resp.MaxEnvelopes = AutoConfigServer._max_envelopes
195 return resp
196
197 # Spyne does not handle no input or SimpleModel input for 'bare' function
198 # DummyInput is unused
199 # pylint: disable=unused-argument
200 @rpc(
201 models.DummyInput,
202 _returns=models.AcsToCpeRequests,
203 _out_message_name="EmptyHttp",
204 _body_style='bare',
205 _operation_name="EmptyHttp",
206 )
207 def empty_http(ctx, dummy):
208 # Function to handle empty HTTP request
209 return AutoConfigServer._handle_tr069_message(ctx, dummy)
210
211 # CPE->ACS responses to ACS->CPE RPC calls
212
213 @rpc(
214 models.SetParameterValuesResponse,
215 _returns=models.AcsToCpeRequests,
216 _out_message_name="MessageNameToBeReplaced",
217 _body_style='bare',
218 _operation_name="SetParameterValuesResponse",
219 )
220 def set_parameter_values_response(ctx, response):
221 return AutoConfigServer._handle_tr069_message(ctx, response)
222
223 @rpc(
224 models.GetParameterValuesResponse,
225 _returns=models.AcsToCpeRequests,
226 _out_message_name="MessageNameToBeReplaced",
227 _body_style='bare',
228 _operation_name="GetParameterValuesResponse",
229 )
230 def get_parameter_values_response(ctx, response):
231 return AutoConfigServer._handle_tr069_message(ctx, response)
232
233 @rpc(
234 models.GetParameterNamesResponse,
235 _returns=models.AcsToCpeRequests,
236 _out_message_name="MessageNameToBeReplaced",
237 _body_style='bare',
238 _operation_name="GetParameterNamesResponse",
239 )
240 def get_parameter_names_response(ctx, response):
241 return AutoConfigServer._handle_tr069_message(ctx, response)
242
243 @rpc(
244 models.SetParameterAttributesResponse,
245 _returns=models.AcsToCpeRequests,
246 _out_message_name="MessageNameToBeReplaced",
247 _body_style='bare',
248 _operation_name="SetParameterAttributesResponse",
249 )
250 def set_parameter_attributes_response(ctx, response):
251 return AutoConfigServer._handle_tr069_message(ctx, response)
252
253 @rpc(
254 models.GetParameterAttributesResponse,
255 _returns=models.AcsToCpeRequests,
256 _out_message_name="MessageNameToBeReplaced",
257 _body_style='bare',
258 _operation_name="GetParameterAttributesResponse",
259 )
260 def get_parameter_attributes_response(ctx, response):
261 return AutoConfigServer._handle_tr069_message(ctx, response)
262
263 @rpc(
264 models.AddObjectResponse,
265 _returns=models.AcsToCpeRequests,
266 _out_message_name="MessageNameToBeReplaced",
267 _body_style='bare',
268 _operation_name="AddObjectResponse",
269 )
270 def add_object_response(ctx, response):
271 return AutoConfigServer._handle_tr069_message(ctx, response)
272
273 @rpc(
274 models.DeleteObjectResponse,
275 _returns=models.AcsToCpeRequests,
276 _out_message_name="MessageNameToBeReplaced",
277 _body_style='bare',
278 _operation_name="DeleteObjectResponse",
279 )
280 def delete_object_response(ctx, response):
281 return AutoConfigServer._handle_tr069_message(ctx, response)
282
283 @rpc(
284 models.DownloadResponse,
285 _returns=models.AcsToCpeRequests,
286 _out_message_name="MessageNameToBeReplaced",
287 _body_style='bare',
288 _operation_name="DownloadResponse",
289 )
290 def download_response(ctx, response):
291 return AutoConfigServer._handle_tr069_message(ctx, response)
292
293 @rpc(
294 models.RebootResponse,
295 _returns=models.AcsToCpeRequests,
296 _out_message_name="MessageNameToBeReplaced",
297 _body_style='bare',
298 _operation_name="RebootResponse",
299 )
300 def reboot_response(ctx, response):
301 return AutoConfigServer._handle_tr069_message(ctx, response)
302
303 @rpc(
304 models.Fault,
305 _returns=models.AcsToCpeRequests,
306 _out_message_name="MessageNameToBeReplaced",
307 _body_style='bare',
308 _operation_name="Fault",
309 )
310 def fault(ctx, response):
311 return AutoConfigServer._handle_tr069_message(ctx, response)
312
313
314def on_method_return_string(ctx):
315 """
316 By default, spyne adds a namespace to every single XML element.
317 There isn't a way to change this behavior, and the spyne-recommended way
318 to fix this is by doing string manipulation. The TR-069 spec mandates that
319 only the top-level CWMP elements contain namespaces. Hence this
320 function is to remove namespaces from all elements except top-level CWMP
321 elements (e.g. RPC request/response names, header elements).
322 """
323 # Format strings for XML tags, corresponding to:
324 # 1) Normal start or end tag (without attribute)
325 # 2) Open and close tag (when no attributes or sub-structures exist)
326 # 3) Tag containing attributes
327 # We don't just look for 'cwmp:%s' (with no character after %s) because this
328 # would pick up all tags that start with the tag of interest (e.g
329 # cwmp:SetParameterAttributes would also match
330 # cwmp:SetParameterAttributesStruct)
Wei-Yu Chen5cbdfbb2021-12-02 01:10:21 +0800331
Wei-Yu Chen49950b92021-11-08 19:19:18 +0800332 XML_FORMAT_STRS = [
333 ["cwmp:%s>", "!!!TEMP_MOD!!!:%s>"],
334 ["cwmp:%s/>", "!!!TEMP_MOD!!!:%s/>"],
335 ["cwmp:%s ", "!!!TEMP_MOD!!!:%s "],
336 ]
337 fields_to_preserve_ns = list(RPC_METHODS) + list(RPC_RESPONSES) + \
338 list(CPE_RPC_METHODS) + list(CPE_RPC_RESPONSES) + \
339 list(PSEUDO_RPC_METHODS) + list(TOP_LEVEL_HEADER_ELEMENTS)
340 for field in fields_to_preserve_ns:
341 for formats in XML_FORMAT_STRS:
342 orig_str = formats[0] % field
343 temp_str = formats[1] % field
344 ctx.out_string[0] = ctx.out_string[0].replace(
345 orig_str.encode('ascii'), temp_str.encode('ascii'),
346 )
347
348 # Also preserve namespace inside strings, e.g. for arrayType="cwmp:..."
349 orig_str = "=\"cwmp:"
350 temp_str = "=\"!!!TEMP_MOD!!!:"
351 ctx.out_string[0] = ctx.out_string[0].replace(
352 orig_str.encode('ascii'), temp_str.encode('ascii'),
353 )
354 orig_str = "=\'cwmp:"
355 temp_str = "=\'!!!TEMP_MOD!!!:"
356 ctx.out_string[0] = ctx.out_string[0].replace(
357 orig_str.encode('ascii'), temp_str.encode('ascii'),
358 )
359
360 ctx.out_string[0] = ctx.out_string[0].replace(b'cwmp:', b'')
361 ctx.out_string[0] = ctx.out_string[0].replace(b'!!!TEMP_MOD!!!:', b'cwmp:')
362
363 # Special-case handling so that 'EmptyHttp' RPC will be called using
364 # completely empty HTTP request (not even containing a SOAP envelope), as
365 # per TR-069 spec.
366 if(ctx.descriptor.out_message.Attributes.sub_name == 'EmptyHttp'):
367 ctx.out_string = [b'']
368
Wei-Yu Chen49950b92021-11-08 19:19:18 +0800369AutoConfigServer.event_manager.add_listener(
370 'method_return_string',
371 on_method_return_string,
372)