Resolve PEP8 in IoTDM
[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 = OneM2MEncodeDecodeData("HTTPHeaders")\
67     .add(http_header_content_type, http_header_content_type)\
68     .add(http_header_content_location, http_header_content_location)\
69     .add(http_header_content_length, http_header_content_length)\
70     .add(OneM2M.short_from, http_header_origin)\
71     .add(OneM2M.short_request_identifier, http_header_ri)\
72     .add(OneM2M.short_group_request_identifier, http_header_gid)\
73     .add(OneM2M.short_originating_timestamp, http_header_ot)\
74     .add(OneM2M.short_response_status_code, http_header_rsc)
75
76 http_query_params = [
77     OneM2M.short_resource_type,
78     OneM2M.short_result_persistence,
79     OneM2M.short_result_content,
80     OneM2M.short_delivery_aggregation,
81     OneM2M.short_discovery_result_type,
82     OneM2M.short_role_ids,
83     OneM2M.short_token_ids,
84     OneM2M.short_local_token_ids,
85     OneM2M.short_token_request_indicator
86     # TODO add filter criteria elements
87 ]
88
89 onem2m_to_http_result_codes = {
90     OneM2M.result_code_accepted: httplib.ACCEPTED,
91
92     OneM2M.result_code_ok: httplib.OK,
93     OneM2M.result_code_created: httplib.CREATED,
94     OneM2M.result_code_deleted: httplib.OK,
95     OneM2M.result_code_updated: httplib.OK,
96
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
117     OneM2M.result_code_internal_server_error: httplib.INTERNAL_SERVER_ERROR,
118     OneM2M.result_code_not_implemened: httplib.NOT_IMPLEMENTED,
119     OneM2M.result_code_target_not_reachable: httplib.NOT_FOUND,
120     OneM2M.result_code_receiver_has_no_privilege: httplib.FORBIDDEN,
121     OneM2M.result_code_already_exists: httplib.FORBIDDEN,
122     OneM2M.result_code_target_not_subscribable: httplib.FORBIDDEN,
123     OneM2M.result_code_subscription_verification_initiation_failed: httplib.INTERNAL_SERVER_ERROR,
124     OneM2M.result_code_subscription_host_has_no_privilege: httplib.FORBIDDEN,
125     OneM2M.result_code_non_blocking_request_not_supported: httplib.NOT_IMPLEMENTED,
126     OneM2M.result_code_not_acceptable: httplib.NOT_ACCEPTABLE,
127     # OneM2M.result_code_discovery_denied_by_ipe: httplib., not supported by HTTP binding spec
128     OneM2M.result_code_group_members_not_responded: httplib.INTERNAL_SERVER_ERROR,
129     OneM2M.result_code_esprim_decryption_error: httplib.INTERNAL_SERVER_ERROR,
130     OneM2M.result_code_esprim_encryption_error: httplib.INTERNAL_SERVER_ERROR,
131     OneM2M.result_code_sparql_update_error: httplib.INTERNAL_SERVER_ERROR,
132
133     OneM2M.result_code_external_object_not_reachable: httplib.NOT_FOUND,
134     OneM2M.result_code_external_object_not_found: httplib.NOT_FOUND,
135     OneM2M.result_code_max_number_of_member_exceeded: httplib.BAD_REQUEST,
136     OneM2M.result_code_member_type_inconsistent: httplib.BAD_REQUEST,
137     OneM2M.result_code_mgmt_session_cannot_be_established: httplib.INTERNAL_SERVER_ERROR,
138     OneM2M.result_code_mgmt_session_establishment_timeout: httplib.INTERNAL_SERVER_ERROR,
139     OneM2M.result_code_invalid_cmd_type: httplib.BAD_REQUEST,
140     OneM2M.result_code_invalid_arguments: httplib.BAD_REQUEST,
141     OneM2M.result_code_insufficient_argument: httplib.BAD_REQUEST,
142     OneM2M.result_code_mgmt_conversion_error: httplib.INTERNAL_SERVER_ERROR,
143     OneM2M.result_code_mgmt_cancellation_failed: httplib.INTERNAL_SERVER_ERROR,
144     OneM2M.result_code_already_complete: httplib.BAD_REQUEST,
145     OneM2M.result_code_mgmt_command_not_cancellable: httplib.BAD_REQUEST
146 }
147
148
149 class OneM2MHttpTx(IoTTx):
150     """Implementation of HTTP OneM2M Tx channel"""
151
152     def __init__(self, encoder, decoder):
153         super(OneM2MHttpTx, self).__init__(encoder, decoder)
154         self.session = None
155
156     def _start(self):
157         self.session = Session()
158
159     def _stop(self):
160         if self.session:
161             self.session.close()
162         self.session = None
163
164     def send(self, jsonprimitive):
165         try:
166             message = self.encoder.encode(jsonprimitive)
167         except IoTDataEncodeError:
168             return None
169
170         rsp_message = self.session.send(message)
171
172         rsp_primitive = None
173         try:
174             rsp_primitive = self.decoder.decode(rsp_message)
175         except IoTDataDecodeError:
176             return None
177
178         return rsp_primitive
179
180
181 class OneM2MHttpRx(IoTRx):
182     """Implementation of HTTP OneM2M Rx channel"""
183
184     def __init__(self, decoder, encoder, port, interface=""):
185         super(OneM2MHttpRx, self).__init__(decoder, encoder)
186         self.interface = interface
187         self.port = port
188         self.server_address = (interface, port)
189         self.server = None
190         self.thread = None
191
192     def _handle_request(self, request):
193         """
194         Callback method called directly by the HTTP server. This method
195         decodes received HTTP request and calls provided upper layer
196         receive_cb() method which process decoded primitive and returns another
197         primitive object as result. The resulting primitive object is encoded
198         to HTTP response message and sent back to client.
199         """
200         primitive = self.decoder.decode(request)
201
202         rsp_primitive = self.receive_cb(primitive)
203         if not rsp_primitive:
204             code = httplib.INTERNAL_SERVER_ERROR
205             reason = status_codes._codes[code]
206             start_line = httputil.ResponseStartLine(version='HTTP/1.1', code=code, reason=reason)
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(version='HTTP/1.1', code=code, reason=reason)
220         request.connection.write_headers(start_line, headers)
221
222         # set content
223         if encoded.content:
224             request.connection.write(json.dumps(encoded.content))
225
226         request.finish()
227
228     def _worker(self):
229         ioloop.IOLoop.instance().start()
230
231     def _start(self):
232         self.server = httpserver.HTTPServer(self._handle_request)
233         self.server.listen(self.port, self.interface)
234         # start worker thread which calls blocking ioloop start
235         self.thread = threading.Thread(target=self._worker)
236         self.thread.start()
237
238     def _stop(self):
239         ioloop.IOLoop.instance().stop()
240         self.thread.join
241
242
243 class OneM2MHttpJsonEncoderRx(IoTDataEncoder):
244     """
245     HTTP Rx encoder encodes OneM2M JSON primitive objects to HTTP message
246     objects used by Rx channel (different objects than used by Tx channel)
247     """
248
249     def encode(self, onem2m_primitive):
250         """
251         Encodes OneM2M JSON primitive object to Rx specific HTTP message
252         with JSON content type
253         """
254
255         # This is Rx encoder so we use Response
256         msg = Response()
257
258         params = onem2m_primitive.get_parameters()
259         proto_params = onem2m_primitive.get_protocol_specific_parameters()
260
261         # set result code and reason
262         if http_result_code not in proto_params:
263             raise IoTDataEncodeError("Result code not passed for HTTP response")
264
265         result_code = proto_params[http_result_code]
266         try:
267             reason = status_codes._codes[result_code][0]
268         except KeyError:
269             raise IoTDataEncodeError("Invalid result code passed: {}", result_code)
270
271         msg.status_code = result_code
272         msg.reason = reason
273
274         # Headers from protocol specific parameters
275         if proto_params:
276             for key, value in proto_params.items():
277                 encoded = http_headers.encode_default_ci(key, None)
278                 if None is not encoded:
279                     msg.headers[encoded] = str(value)
280
281         # onem2m parameters
282         for key, value in params.items():
283             encoded = http_headers.encode_default_ci(key, None)
284             if None is not encoded:
285                 msg.headers[encoded] = str(value)
286
287         # Body (content)
288         content = onem2m_primitive.get_content()
289         if content:
290             msg._content = content
291
292         return msg
293
294
295 class OneM2MHttpJsonEncoderTx(IoTDataEncoder):
296     """
297     HTTP Tx encoder encodes OneM2M JSON primitive objects to HTTP message
298     objects used by Tx channel (different objects than used by Rx channel)
299     """
300
301     onem2m_oper_to_http_method = {
302         OneM2M.operation_create: "post",
303         OneM2M.operation_retrieve: "get",
304         OneM2M.operation_update: "put",
305         OneM2M.operation_delete: "delete",
306         OneM2M.operation_notify: "post"
307     }
308
309     def _encode_operation(self, onem2m_operation):
310         return self.onem2m_oper_to_http_method[onem2m_operation]
311
312     def _translate_uri_from_onem2m(self, uri):
313         if 0 == uri.find("//"):
314             return "/_/" + uri[2:]
315         if 0 == uri.find("/"):
316             return "/~" + uri
317         return "/" + uri
318
319     def encode(self, onem2m_primitive):
320         """
321         Encodes OneM2M JSON primitive object to Tx specific HTTP message
322         with JSON content type
323         """
324
325         params = onem2m_primitive.get_parameters()
326         proto_params = onem2m_primitive.get_protocol_specific_parameters()
327
328         # This is Tx encoder so we use Request
329         msg = Request()
330
331         if params:
332             # Method (Operation)
333             if OneM2M.short_operation in params:
334                 msg.method = self._encode_operation(params[OneM2M.short_operation])
335
336             # URL
337             if OneM2M.short_to in params:
338                 resource_uri = self._translate_uri_from_onem2m(params[OneM2M.short_to])
339                 entity_address = ""
340                 if proto_params:
341                     if protocol_address in proto_params:
342                         entity_address = proto_params[protocol_address]
343                         if protocol_port in proto_params:
344                             entity_address += (":" + str(proto_params[protocol_port]))
345
346                 msg.url = "http://" + entity_address + resource_uri
347
348             # encode headers and query parameters
349             delimiter = "?"
350             for key, value in params.items():
351
352                 # Query parameters
353                 if msg.url and key in http_query_params:
354                     msg.url += (delimiter + key + "=" + str(value))
355                     delimiter = "&"
356                     continue
357
358                 # Headers from primitive parameters
359                 encoded = http_headers.encode_default_ci(key, None)
360                 if None is not encoded:
361                     msg.headers[encoded] = str(value)
362
363         # Headers from protocol specific parameters
364         if proto_params:
365             for key, value in proto_params.items():
366                 encoded = http_headers.encode_default_ci(key, None)
367                 if None is not encoded:
368                     msg.headers[encoded] = str(value)
369
370         # Body (content)
371         content = onem2m_primitive.get_content()
372         if content:
373             msg.json = content
374         return msg.prepare()
375
376
377 class OneM2MHttpDecodeUtils:
378     """Implementation of utility methods for decoder classes"""
379
380     @staticmethod
381     def translate_http_method_to_onem2m_operation(method, has_resource_type):
382         m = method.lower()
383         if "post" == m:
384             if has_resource_type:
385                 return OneM2M.operation_create
386             else:
387                 return OneM2M.operation_notify
388
389         if "get" == m:
390             return OneM2M.operation_retrieve
391
392         if "put" == m:
393             return OneM2M.operation_update
394
395         if "delete" == m:
396             return OneM2M.operation_delete
397
398         raise IoTDataDecodeError("Unsupported HTTP method: {}".format(method))
399
400     @staticmethod
401     def translate_uri_to_onem2m(uri):
402         if 0 == uri.find("/_/"):
403             return "/" + uri[2:]
404         if 0 == uri.find("/~/"):
405             return uri[2:]
406         if 0 == uri.find("/"):
407             return uri[1:]
408
409     @staticmethod
410     def decode_headers(primitive_param_dict, http_specifics, headers):
411         for name, value in headers.items():
412             decoded_name = http_headers.decode_default_ci(name, None)
413             if None is not decoded_name:
414                 if name.lower() in http_specific_headers:
415                     if name.lower() == http_header_content_length.lower():
416                         # decode as integer values
417                         try:
418                             int(value)
419                         except Exception as e:
420                             raise IoTDataDecodeError("Invalid Content-Length value: {}, error: {}".format(value, e))
421
422                     http_specifics[decoded_name] = value
423                 else:
424                     if decoded_name is OneM2M.short_response_status_code:
425                         # decode as integer value
426                         try:
427                             value = int(value)
428                         except Exception as e:
429                             raise IoTDataDecodeError("Invalid status code value: {}, error: {}".format(value, e))
430
431                     primitive_param_dict[decoded_name] = value
432
433
434 class OneM2MHttpJsonDecoderRx(IoTDataDecoder):
435     """
436     HTTP Rx decoder decodes HTTP message objects used by Rx channel (different
437     objects than used by Tx channel) to OneM2M JSON primitive objects
438     """
439
440     def decode(self, protocol_message):
441         """
442         Decodes Tx specific HTTP message with JSON content type to OneM2M JSON
443         primitive object
444         """
445         builder = OneM2MHttpJsonPrimitiveBuilder() \
446             .set_communication_protocol(HTTPPROTOCOLNAME)
447
448         primitive_param_dict = {}
449         http_specifics = {}
450         OneM2MHttpDecodeUtils.decode_headers(primitive_param_dict, http_specifics, protocol_message.headers)
451
452         builder.set_parameters(primitive_param_dict)
453         builder.set_protocol_specific_parameters(http_specifics)
454
455         if protocol_message.path:
456             builder.set_param(OneM2M.short_to, OneM2MHttpDecodeUtils.translate_uri_to_onem2m(protocol_message.path))
457
458         if protocol_message.body:
459             builder.set_content(protocol_message.body)
460
461         if protocol_message.query_arguments:
462             for param, value in protocol_message.query_arguments.items():
463                 if len(value) == 1:
464                     value = value[0]
465                 builder.set_param(param, value)
466
467         if protocol_message.method:
468             operation = OneM2MHttpDecodeUtils.translate_http_method_to_onem2m_operation(
469                 protocol_message.method, builder.has_param(OneM2M.short_resource_type))
470             builder.set_param(OneM2M.short_operation, operation)
471
472         return builder.build()
473
474
475 class OneM2MHttpJsonDecoderTx(IoTDataDecoder):
476     """
477     HTTP Tx decoder decodes HTTP message objects used by Tx channel (different
478     objects than used by Rx channel) to OneM2M JSON primitive objects
479     """
480
481     def decode(self, protocol_message):
482         """
483         Decodes Rx specific HTTP message with JSON content type to OneM2M JSON
484         primitive object
485         """
486         builder = OneM2MHttpJsonPrimitiveBuilder() \
487             .set_communication_protocol(HTTPPROTOCOLNAME)
488
489         primitive_param_dict = {}
490         http_specifics = {}
491         OneM2MHttpDecodeUtils.decode_headers(primitive_param_dict, http_specifics, protocol_message.headers)
492
493         # TODO decode query if needed
494
495         # http result code
496         if hasattr(protocol_message, "status_code"):
497             http_specifics[http_result_code] = protocol_message.status_code
498
499         builder.set_parameters(primitive_param_dict)
500         builder.set_protocol_specific_parameters(http_specifics)
501
502         # set content
503         if hasattr(protocol_message, "content"):
504             builder.set_content(protocol_message.content)
505
506         # builder.set_proto_param(original_content_string, protocol_message.content)
507         return builder.build()
508
509
510 class OneM2MHttpJsonPrimitive(OneM2MJsonPrimitive):
511     """
512     Specialization of OneM2M JSON primitive for HTTP protocol.
513     Extends verification methods of the OneM2MJsonPrimitive with HTTP specific
514     checks
515     """
516
517     @staticmethod
518     def _check_http_primitive_content(primitive):
519         content = primitive.get_content_str()
520         if not content:
521             # nothing to check
522             return
523
524         content_type = primitive.get_proto_param(http_header_content_type)
525         if not content_type:
526             raise AssertionError("HTTP primitive without Content-Type")
527
528         # TODO add support for other content types if needed
529         if "json" not in content_type:
530             raise AssertionError("HTTP primitive with unsupported Content-Type: {}".format(content_type))
531
532         content_length = primitive.get_proto_param(http_header_content_length)
533         if not content_length:
534             raise AssertionError("HTTP primitive without Content-Length")
535
536         if not isinstance(content_length, basestring):
537             raise AssertionError(
538                 "HTTP primitive with Content-Length value of invalid data type: {}, string is expected".format(
539                     content_length.__class__))
540
541         # verify length of content if exists
542         # TODO commented out because this fails for primitives built by builder
543         # TODO the correct place to check the value is in encoder/decoder
544         # computed_length = len(content)
545         # if content_length != computed_length:
546         #     raise AssertionError("HTTP primitive Content-Length inconsistency: header value: {}, real length: {}".
547         #                          format(content_length, computed_length))
548
549     def _check_request_common(self):
550         op, rqi = super(OneM2MHttpJsonPrimitive, self)._check_request_common()
551         self._check_http_primitive_content(self)
552         return op, rqi
553
554     def _check_response_common(self, response_primitive, rqi=None, rsc=None):
555         response_rsc = super(OneM2MHttpJsonPrimitive, self)._check_response_common(response_primitive, rqi, rsc)
556         self._check_http_primitive_content(response_primitive)
557
558         http_res = response_primitive.get_proto_param(http_result_code)
559         if not http_res:
560             raise AssertionError("HTTP response primitive without Result-Code")
561
562         if not isinstance(http_res, int):
563             raise AssertionError(
564                 "HTTP response primitive with Result-Code value of invalid data type: {}, expected is integer".format(
565                     http_res.__class__))
566
567         try:
568             expected_http_res = onem2m_to_http_result_codes[response_rsc]
569         except KeyError as e:
570             raise RuntimeError("Failed to map OneM2M rsc ({}) to HTTP status code: {}".format(response_rsc, e))
571
572         if expected_http_res != http_res:
573             raise AssertionError(
574                 "Incorrect HTTP status code mapped to OneM2M status code {}, http: {}, expected http: {}".format(
575                     response_rsc, http_res, expected_http_res))
576
577         # Content-Location
578         if response_rsc == OneM2M.result_code_created:
579             content_location = response_primitive.get_proto_param(http_header_content_location)
580             if not content_location:
581                 raise AssertionError("HTTP response primitive without Content-Location")
582
583             if not isinstance(content_location, basestring):
584                 raise AssertionError(
585                     "HTTP response primitive with invalid Content-Location value data type: {}, " +
586                     "string is expected".format(content_location.__class__))
587
588         return response_rsc
589
590
591 class OneM2MHttpJsonPrimitiveBuilder(OneM2MJsonPrimitiveBuilder):
592     """Builder class specialized for OneM2MHttpJsonPrimitive objects"""
593
594     def build(self):
595         return OneM2MHttpJsonPrimitive(self.parameters, self.content, self.protocol, self.proto_params)