Auto-generated patch by python-black
[integration/test.git] / csit / libraries / IoTDM / client_libs / onem2m_http.py
1 """
2  Implementation of HTTP protocol specific classes of Tx, Rx, encoder, decoder
3  primitive and related builder
4 """
5
6 #
7 # Copyright (c) 2017 Cisco Systems, Inc. and others.  All rights reserved.
8 #
9 # This program and the accompanying materials are made available under the
10 # terms of the Eclipse Public License v1.0 which accompanies this distribution,
11 # and is available at http://www.eclipse.org/legal/epl-v10.html
12 #
13
14 from requests import Request
15 from requests import Response
16 from requests import Session
17 from requests import status_codes
18 from tornado import httpserver
19 from tornado import ioloop
20 from tornado import httputil
21 import threading
22 import json
23 import httplib
24
25 from iot_communication_concepts import IoTTx
26 from iot_communication_concepts import IoTRx
27 from iot_data_concepts import IoTDataEncoder
28 from iot_data_concepts import IoTDataDecoder
29 from iot_data_concepts import IoTDataEncodeError
30 from iot_data_concepts import IoTDataDecodeError
31 from onem2m_json_primitive import OneM2MJsonPrimitiveBuilder
32 from onem2m_json_primitive import OneM2MJsonPrimitive
33 from onem2m_primitive import OneM2M
34 from onem2m_primitive import OneM2MEncodeDecodeData
35
36
37 HTTPPROTOCOLNAME = "http"
38
39 protocol_address = "proto_addr"
40 protocol_port = "proto_port"
41
42 http_header_content_type = "Content-Type"
43 http_header_content_location = "Content-Location"
44 http_header_content_length = "Content-Length"
45 http_result_code = "Result-Code"
46
47 http_specific_headers = [
48     http_header_content_type.lower(),
49     http_header_content_location.lower(),
50     http_header_content_length.lower(),
51 ]
52
53 http_header_origin = "X-M2M-Origin"
54 http_header_ri = "X-M2M-RI"
55 http_header_gid = "X-M2M-GID"
56 http_header_rtu = "X-M2M-RTU"
57 http_header_ot = "X-M2M-OT"
58 http_header_rst = "X-M2M-RST"
59 http_header_ret = "X-M2M-RET"
60 http_header_oet = "X-M2M-OET"
61 http_header_ec = "X-M2M-EC"
62 http_header_rsc = "X-M2M-RSC"
63 http_header_ati = "X-M2M-ATI"
64
65 # TODO add missing element mappings
66 http_headers = (
67     OneM2MEncodeDecodeData("HTTPHeaders")
68     .add(http_header_content_type, http_header_content_type)
69     .add(http_header_content_location, http_header_content_location)
70     .add(http_header_content_length, http_header_content_length)
71     .add(OneM2M.short_from, http_header_origin)
72     .add(OneM2M.short_request_identifier, http_header_ri)
73     .add(OneM2M.short_group_request_identifier, http_header_gid)
74     .add(OneM2M.short_originating_timestamp, http_header_ot)
75     .add(OneM2M.short_response_status_code, http_header_rsc)
76 )
77
78 http_query_params = [
79     OneM2M.short_resource_type,
80     OneM2M.short_result_persistence,
81     OneM2M.short_result_content,
82     OneM2M.short_delivery_aggregation,
83     OneM2M.short_discovery_result_type,
84     OneM2M.short_role_ids,
85     OneM2M.short_token_ids,
86     OneM2M.short_local_token_ids,
87     OneM2M.short_token_request_indicator
88     # TODO add filter criteria elements
89 ]
90
91 onem2m_to_http_result_codes = {
92     OneM2M.result_code_accepted: httplib.ACCEPTED,
93     OneM2M.result_code_ok: httplib.OK,
94     OneM2M.result_code_created: httplib.CREATED,
95     OneM2M.result_code_deleted: httplib.OK,
96     OneM2M.result_code_updated: httplib.OK,
97     OneM2M.result_code_bad_request: httplib.BAD_REQUEST,
98     OneM2M.result_code_not_found: httplib.NOT_FOUND,
99     OneM2M.result_code_operation_not_allowed: httplib.METHOD_NOT_ALLOWED,
100     OneM2M.result_code_request_timeout: httplib.REQUEST_TIMEOUT,
101     OneM2M.result_code_subscription_creator_has_no_privilege: httplib.FORBIDDEN,
102     OneM2M.result_code_contents_unacceptable: httplib.BAD_REQUEST,
103     OneM2M.result_code_originator_has_no_privilege: httplib.FORBIDDEN,
104     OneM2M.result_code_group_request_identifier_exists: httplib.CONFLICT,
105     OneM2M.result_code_conflict: httplib.CONFLICT,
106     OneM2M.result_code_originator_has_not_registered: httplib.FORBIDDEN,
107     OneM2M.result_code_security_association_required: httplib.FORBIDDEN,
108     OneM2M.result_code_invalid_child_resource_type: httplib.FORBIDDEN,
109     OneM2M.result_code_no_members: httplib.FORBIDDEN,
110     # OneM2M.result_code_group_member_type_inconsistent: httplib., not supported by HTTP binding spec
111     OneM2M.result_code_esprim_unsupported_option: httplib.FORBIDDEN,
112     OneM2M.result_code_esprim_unknown_key_id: httplib.FORBIDDEN,
113     OneM2M.result_code_esprim_unknown_orig_rand_id: httplib.FORBIDDEN,
114     OneM2M.result_code_esprim_unknown_recv_rand_id: httplib.FORBIDDEN,
115     OneM2M.result_code_esprim_bad_mac: httplib.FORBIDDEN,
116     OneM2M.result_code_internal_server_error: httplib.INTERNAL_SERVER_ERROR,
117     OneM2M.result_code_not_implemened: httplib.NOT_IMPLEMENTED,
118     OneM2M.result_code_target_not_reachable: httplib.NOT_FOUND,
119     OneM2M.result_code_receiver_has_no_privilege: httplib.FORBIDDEN,
120     OneM2M.result_code_already_exists: httplib.FORBIDDEN,
121     OneM2M.result_code_target_not_subscribable: httplib.FORBIDDEN,
122     OneM2M.result_code_subscription_verification_initiation_failed: httplib.INTERNAL_SERVER_ERROR,
123     OneM2M.result_code_subscription_host_has_no_privilege: httplib.FORBIDDEN,
124     OneM2M.result_code_non_blocking_request_not_supported: httplib.NOT_IMPLEMENTED,
125     OneM2M.result_code_not_acceptable: httplib.NOT_ACCEPTABLE,
126     # OneM2M.result_code_discovery_denied_by_ipe: httplib., not supported by HTTP binding spec
127     OneM2M.result_code_group_members_not_responded: httplib.INTERNAL_SERVER_ERROR,
128     OneM2M.result_code_esprim_decryption_error: httplib.INTERNAL_SERVER_ERROR,
129     OneM2M.result_code_esprim_encryption_error: httplib.INTERNAL_SERVER_ERROR,
130     OneM2M.result_code_sparql_update_error: httplib.INTERNAL_SERVER_ERROR,
131     OneM2M.result_code_external_object_not_reachable: httplib.NOT_FOUND,
132     OneM2M.result_code_external_object_not_found: httplib.NOT_FOUND,
133     OneM2M.result_code_max_number_of_member_exceeded: httplib.BAD_REQUEST,
134     OneM2M.result_code_member_type_inconsistent: httplib.BAD_REQUEST,
135     OneM2M.result_code_mgmt_session_cannot_be_established: httplib.INTERNAL_SERVER_ERROR,
136     OneM2M.result_code_mgmt_session_establishment_timeout: httplib.INTERNAL_SERVER_ERROR,
137     OneM2M.result_code_invalid_cmd_type: httplib.BAD_REQUEST,
138     OneM2M.result_code_invalid_arguments: httplib.BAD_REQUEST,
139     OneM2M.result_code_insufficient_argument: httplib.BAD_REQUEST,
140     OneM2M.result_code_mgmt_conversion_error: httplib.INTERNAL_SERVER_ERROR,
141     OneM2M.result_code_mgmt_cancellation_failed: httplib.INTERNAL_SERVER_ERROR,
142     OneM2M.result_code_already_complete: httplib.BAD_REQUEST,
143     OneM2M.result_code_mgmt_command_not_cancellable: httplib.BAD_REQUEST,
144 }
145
146
147 class OneM2MHttpTx(IoTTx):
148     """Implementation of HTTP OneM2M Tx channel"""
149
150     def __init__(self, encoder, decoder):
151         super(OneM2MHttpTx, self).__init__(encoder, decoder)
152         self.session = None
153
154     def _start(self):
155         self.session = Session()
156
157     def _stop(self):
158         if self.session:
159             self.session.close()
160         self.session = None
161
162     def send(self, jsonprimitive):
163         try:
164             message = self.encoder.encode(jsonprimitive)
165         except IoTDataEncodeError:
166             return None
167
168         rsp_message = self.session.send(message)
169
170         rsp_primitive = None
171         try:
172             rsp_primitive = self.decoder.decode(rsp_message)
173         except IoTDataDecodeError:
174             return None
175
176         return rsp_primitive
177
178
179 class OneM2MHttpRx(IoTRx):
180     """Implementation of HTTP OneM2M Rx channel"""
181
182     def __init__(self, decoder, encoder, port, interface=""):
183         super(OneM2MHttpRx, self).__init__(decoder, encoder)
184         self.interface = interface
185         self.port = port
186         self.server_address = (interface, port)
187         self.server = None
188         self.thread = None
189
190     def _handle_request(self, request):
191         """
192         Callback method called directly by the HTTP server. This method
193         decodes received HTTP request and calls provided upper layer
194         receive_cb() method which process decoded primitive and returns another
195         primitive object as result. The resulting primitive object is encoded
196         to HTTP response message and sent back to client.
197         """
198         primitive = self.decoder.decode(request)
199
200         rsp_primitive = self.receive_cb(primitive)
201         if not rsp_primitive:
202             code = httplib.INTERNAL_SERVER_ERROR
203             reason = status_codes._codes[code]
204             start_line = httputil.ResponseStartLine(
205                 version="HTTP/1.1", code=code, reason=reason
206             )
207             request.connection.write_headers(start_line, httputil.HTTPHeaders())
208             request.finish()
209             return
210
211         encoded = self.encoder.encode(rsp_primitive)
212
213         headers = httputil.HTTPHeaders()
214         headers.update(encoded.headers)
215
216         code = encoded.status_code
217         reason = encoded.reason
218
219         start_line = httputil.ResponseStartLine(
220             version="HTTP/1.1", code=code, reason=reason
221         )
222         request.connection.write_headers(start_line, headers)
223
224         # set content
225         if encoded.content:
226             request.connection.write(json.dumps(encoded.content))
227
228         request.finish()
229
230     def _worker(self):
231         ioloop.IOLoop.instance().start()
232
233     def _start(self):
234         self.server = httpserver.HTTPServer(self._handle_request)
235         self.server.listen(self.port, self.interface)
236         # start worker thread which calls blocking ioloop start
237         self.thread = threading.Thread(target=self._worker)
238         self.thread.start()
239
240     def _stop(self):
241         ioloop.IOLoop.instance().stop()
242         self.thread.join
243
244
245 class OneM2MHttpJsonEncoderRx(IoTDataEncoder):
246     """
247     HTTP Rx encoder encodes OneM2M JSON primitive objects to HTTP message
248     objects used by Rx channel (different objects than used by Tx channel)
249     """
250
251     def encode(self, onem2m_primitive):
252         """
253         Encodes OneM2M JSON primitive object to Rx specific HTTP message
254         with JSON content type
255         """
256
257         # This is Rx encoder so we use Response
258         msg = Response()
259
260         params = onem2m_primitive.get_parameters()
261         proto_params = onem2m_primitive.get_protocol_specific_parameters()
262
263         # set result code and reason
264         if http_result_code not in proto_params:
265             raise IoTDataEncodeError("Result code not passed for HTTP response")
266
267         result_code = proto_params[http_result_code]
268         try:
269             reason = status_codes._codes[result_code][0]
270         except KeyError:
271             raise IoTDataEncodeError("Invalid result code passed: {}", result_code)
272
273         msg.status_code = result_code
274         msg.reason = reason
275
276         # Headers from protocol specific parameters
277         if proto_params:
278             for key, value in proto_params.items():
279                 encoded = http_headers.encode_default_ci(key, None)
280                 if None is not encoded:
281                     msg.headers[encoded] = str(value)
282
283         # onem2m parameters
284         for key, value in params.items():
285             encoded = http_headers.encode_default_ci(key, None)
286             if None is not encoded:
287                 msg.headers[encoded] = str(value)
288
289         # Body (content)
290         content = onem2m_primitive.get_content()
291         if content:
292             msg._content = content
293
294         return msg
295
296
297 class OneM2MHttpJsonEncoderTx(IoTDataEncoder):
298     """
299     HTTP Tx encoder encodes OneM2M JSON primitive objects to HTTP message
300     objects used by Tx channel (different objects than used by Rx channel)
301     """
302
303     onem2m_oper_to_http_method = {
304         OneM2M.operation_create: "post",
305         OneM2M.operation_retrieve: "get",
306         OneM2M.operation_update: "put",
307         OneM2M.operation_delete: "delete",
308         OneM2M.operation_notify: "post",
309     }
310
311     def _encode_operation(self, onem2m_operation):
312         return self.onem2m_oper_to_http_method[onem2m_operation]
313
314     def _translate_uri_from_onem2m(self, uri):
315         if 0 == uri.find("//"):
316             return "/_/" + uri[2:]
317         if 0 == uri.find("/"):
318             return "/~" + uri
319         return "/" + uri
320
321     def encode(self, onem2m_primitive):
322         """
323         Encodes OneM2M JSON primitive object to Tx specific HTTP message
324         with JSON content type
325         """
326
327         params = onem2m_primitive.get_parameters()
328         proto_params = onem2m_primitive.get_protocol_specific_parameters()
329
330         # This is Tx encoder so we use Request
331         msg = Request()
332
333         if params:
334             # Method (Operation)
335             if OneM2M.short_operation in params:
336                 msg.method = self._encode_operation(params[OneM2M.short_operation])
337
338             # URL
339             if OneM2M.short_to in params:
340                 resource_uri = self._translate_uri_from_onem2m(params[OneM2M.short_to])
341                 entity_address = ""
342                 if proto_params:
343                     if protocol_address in proto_params:
344                         entity_address = proto_params[protocol_address]
345                         if protocol_port in proto_params:
346                             entity_address += ":" + str(proto_params[protocol_port])
347
348                 msg.url = "http://" + entity_address + resource_uri
349
350             # encode headers and query parameters
351             delimiter = "?"
352             for key, value in params.items():
353
354                 # Query parameters
355                 if msg.url and key in http_query_params:
356                     msg.url += delimiter + key + "=" + str(value)
357                     delimiter = "&"
358                     continue
359
360                 # Headers from primitive parameters
361                 encoded = http_headers.encode_default_ci(key, None)
362                 if None is not encoded:
363                     msg.headers[encoded] = str(value)
364
365         # Headers from protocol specific parameters
366         if proto_params:
367             for key, value in proto_params.items():
368                 encoded = http_headers.encode_default_ci(key, None)
369                 if None is not encoded:
370                     msg.headers[encoded] = str(value)
371
372         # Body (content)
373         content = onem2m_primitive.get_content()
374         if content:
375             msg.json = content
376         return msg.prepare()
377
378
379 class OneM2MHttpDecodeUtils:
380     """Implementation of utility methods for decoder classes"""
381
382     @staticmethod
383     def translate_http_method_to_onem2m_operation(method, has_resource_type):
384         m = method.lower()
385         if "post" == m:
386             if has_resource_type:
387                 return OneM2M.operation_create
388             else:
389                 return OneM2M.operation_notify
390
391         if "get" == m:
392             return OneM2M.operation_retrieve
393
394         if "put" == m:
395             return OneM2M.operation_update
396
397         if "delete" == m:
398             return OneM2M.operation_delete
399
400         raise IoTDataDecodeError("Unsupported HTTP method: {}".format(method))
401
402     @staticmethod
403     def translate_uri_to_onem2m(uri):
404         if 0 == uri.find("/_/"):
405             return "/" + uri[2:]
406         if 0 == uri.find("/~/"):
407             return uri[2:]
408         if 0 == uri.find("/"):
409             return uri[1:]
410
411     @staticmethod
412     def decode_headers(primitive_param_dict, http_specifics, headers):
413         for name, value in headers.items():
414             decoded_name = http_headers.decode_default_ci(name, None)
415             if None is not decoded_name:
416                 if name.lower() in http_specific_headers:
417                     if name.lower() == http_header_content_length.lower():
418                         # decode as integer values
419                         try:
420                             int(value)
421                         except Exception as e:
422                             raise IoTDataDecodeError(
423                                 "Invalid Content-Length value: {}, error: {}".format(
424                                     value, e
425                                 )
426                             )
427
428                     http_specifics[decoded_name] = value
429                 else:
430                     if decoded_name is OneM2M.short_response_status_code:
431                         # decode as integer value
432                         try:
433                             value = int(value)
434                         except Exception as e:
435                             raise IoTDataDecodeError(
436                                 "Invalid status code value: {}, error: {}".format(
437                                     value, e
438                                 )
439                             )
440
441                     primitive_param_dict[decoded_name] = value
442
443
444 class OneM2MHttpJsonDecoderRx(IoTDataDecoder):
445     """
446     HTTP Rx decoder decodes HTTP message objects used by Rx channel (different
447     objects than used by Tx channel) to OneM2M JSON primitive objects
448     """
449
450     def decode(self, protocol_message):
451         """
452         Decodes Tx specific HTTP message with JSON content type to OneM2M JSON
453         primitive object
454         """
455         builder = OneM2MHttpJsonPrimitiveBuilder().set_communication_protocol(
456             HTTPPROTOCOLNAME
457         )
458
459         primitive_param_dict = {}
460         http_specifics = {}
461         OneM2MHttpDecodeUtils.decode_headers(
462             primitive_param_dict, http_specifics, protocol_message.headers
463         )
464
465         builder.set_parameters(primitive_param_dict)
466         builder.set_protocol_specific_parameters(http_specifics)
467
468         if protocol_message.path:
469             builder.set_param(
470                 OneM2M.short_to,
471                 OneM2MHttpDecodeUtils.translate_uri_to_onem2m(protocol_message.path),
472             )
473
474         if protocol_message.body:
475             builder.set_content(protocol_message.body)
476
477         if protocol_message.query_arguments:
478             for param, value in protocol_message.query_arguments.items():
479                 if len(value) == 1:
480                     value = value[0]
481                 builder.set_param(param, value)
482
483         if protocol_message.method:
484             operation = OneM2MHttpDecodeUtils.translate_http_method_to_onem2m_operation(
485                 protocol_message.method, builder.has_param(OneM2M.short_resource_type)
486             )
487             builder.set_param(OneM2M.short_operation, operation)
488
489         return builder.build()
490
491
492 class OneM2MHttpJsonDecoderTx(IoTDataDecoder):
493     """
494     HTTP Tx decoder decodes HTTP message objects used by Tx channel (different
495     objects than used by Rx channel) to OneM2M JSON primitive objects
496     """
497
498     def decode(self, protocol_message):
499         """
500         Decodes Rx specific HTTP message with JSON content type to OneM2M JSON
501         primitive object
502         """
503         builder = OneM2MHttpJsonPrimitiveBuilder().set_communication_protocol(
504             HTTPPROTOCOLNAME
505         )
506
507         primitive_param_dict = {}
508         http_specifics = {}
509         OneM2MHttpDecodeUtils.decode_headers(
510             primitive_param_dict, http_specifics, protocol_message.headers
511         )
512
513         # TODO decode query if needed
514
515         # http result code
516         if hasattr(protocol_message, "status_code"):
517             http_specifics[http_result_code] = protocol_message.status_code
518
519         builder.set_parameters(primitive_param_dict)
520         builder.set_protocol_specific_parameters(http_specifics)
521
522         # set content
523         if hasattr(protocol_message, "content"):
524             builder.set_content(protocol_message.content)
525
526         # builder.set_proto_param(original_content_string, protocol_message.content)
527         return builder.build()
528
529
530 class OneM2MHttpJsonPrimitive(OneM2MJsonPrimitive):
531     """
532     Specialization of OneM2M JSON primitive for HTTP protocol.
533     Extends verification methods of the OneM2MJsonPrimitive with HTTP specific
534     checks
535     """
536
537     @staticmethod
538     def _check_http_primitive_content(primitive):
539         content = primitive.get_content_str()
540         if not content:
541             # nothing to check
542             return
543
544         content_type = primitive.get_proto_param(http_header_content_type)
545         if not content_type:
546             raise AssertionError("HTTP primitive without Content-Type")
547
548         # TODO add support for other content types if needed
549         if "json" not in content_type:
550             raise AssertionError(
551                 "HTTP primitive with unsupported Content-Type: {}".format(content_type)
552             )
553
554         content_length = primitive.get_proto_param(http_header_content_length)
555         if not content_length:
556             raise AssertionError("HTTP primitive without Content-Length")
557
558         if not isinstance(content_length, basestring):
559             raise AssertionError(
560                 "HTTP primitive with Content-Length value of invalid data type: {}, string is expected".format(
561                     content_length.__class__
562                 )
563             )
564
565         # verify length of content if exists
566         # TODO commented out because this fails for primitives built by builder
567         # TODO the correct place to check the value is in encoder/decoder
568         # computed_length = len(content)
569         # if content_length != computed_length:
570         #     raise AssertionError("HTTP primitive Content-Length inconsistency: header value: {}, real length: {}".
571         #                          format(content_length, computed_length))
572
573     def _check_request_common(self):
574         op, rqi = super(OneM2MHttpJsonPrimitive, self)._check_request_common()
575         self._check_http_primitive_content(self)
576         return op, rqi
577
578     def _check_response_common(self, response_primitive, rqi=None, rsc=None):
579         response_rsc = super(OneM2MHttpJsonPrimitive, self)._check_response_common(
580             response_primitive, rqi, rsc
581         )
582         self._check_http_primitive_content(response_primitive)
583
584         http_res = response_primitive.get_proto_param(http_result_code)
585         if not http_res:
586             raise AssertionError("HTTP response primitive without Result-Code")
587
588         if not isinstance(http_res, int):
589             raise AssertionError(
590                 "HTTP response primitive with Result-Code value of invalid data type: {}, expected is integer".format(
591                     http_res.__class__
592                 )
593             )
594
595         try:
596             expected_http_res = onem2m_to_http_result_codes[response_rsc]
597         except KeyError as e:
598             raise RuntimeError(
599                 "Failed to map OneM2M rsc ({}) to HTTP status code: {}".format(
600                     response_rsc, e
601                 )
602             )
603
604         if expected_http_res != http_res:
605             raise AssertionError(
606                 "Incorrect HTTP status code mapped to OneM2M status code {}, http: {}, expected http: {}".format(
607                     response_rsc, http_res, expected_http_res
608                 )
609             )
610
611         # Content-Location
612         if response_rsc == OneM2M.result_code_created:
613             content_location = response_primitive.get_proto_param(
614                 http_header_content_location
615             )
616             if not content_location:
617                 raise AssertionError("HTTP response primitive without Content-Location")
618
619             if not isinstance(content_location, basestring):
620                 raise AssertionError(
621                     "HTTP response primitive with invalid Content-Location value data type: {}, "
622                     + "string is expected".format(content_location.__class__)
623                 )
624
625         return response_rsc
626
627
628 class OneM2MHttpJsonPrimitiveBuilder(OneM2MJsonPrimitiveBuilder):
629     """Builder class specialized for OneM2MHttpJsonPrimitive objects"""
630
631     def build(self):
632         return OneM2MHttpJsonPrimitive(
633             self.parameters, self.content, self.protocol, self.proto_params
634         )