2 Implementation of HTTP protocol specific classes of Tx, Rx, encoder, decoder
3 primitive and related builder
7 # Copyright (c) 2017 Cisco Systems, Inc. and others. All rights reserved.
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
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
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
37 HTTPPROTOCOLNAME = "http"
39 protocol_address = "proto_addr"
40 protocol_port = "proto_port"
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"
47 http_specific_headers = [
48 http_header_content_type.lower(),
49 http_header_content_location.lower(),
50 http_header_content_length.lower(),
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"
65 # TODO add missing element mappings
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)
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
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,
147 class OneM2MHttpTx(IoTTx):
148 """Implementation of HTTP OneM2M Tx channel"""
150 def __init__(self, encoder, decoder):
151 super(OneM2MHttpTx, self).__init__(encoder, decoder)
155 self.session = Session()
162 def send(self, jsonprimitive):
164 message = self.encoder.encode(jsonprimitive)
165 except IoTDataEncodeError:
168 rsp_message = self.session.send(message)
172 rsp_primitive = self.decoder.decode(rsp_message)
173 except IoTDataDecodeError:
179 class OneM2MHttpRx(IoTRx):
180 """Implementation of HTTP OneM2M Rx channel"""
182 def __init__(self, decoder, encoder, port, interface=""):
183 super(OneM2MHttpRx, self).__init__(decoder, encoder)
184 self.interface = interface
186 self.server_address = (interface, port)
190 def _handle_request(self, request):
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.
198 primitive = self.decoder.decode(request)
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
207 request.connection.write_headers(start_line, httputil.HTTPHeaders())
211 encoded = self.encoder.encode(rsp_primitive)
213 headers = httputil.HTTPHeaders()
214 headers.update(encoded.headers)
216 code = encoded.status_code
217 reason = encoded.reason
219 start_line = httputil.ResponseStartLine(
220 version="HTTP/1.1", code=code, reason=reason
222 request.connection.write_headers(start_line, headers)
226 request.connection.write(json.dumps(encoded.content))
231 ioloop.IOLoop.instance().start()
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)
241 ioloop.IOLoop.instance().stop()
245 class OneM2MHttpJsonEncoderRx(IoTDataEncoder):
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)
251 def encode(self, onem2m_primitive):
253 Encodes OneM2M JSON primitive object to Rx specific HTTP message
254 with JSON content type
257 # This is Rx encoder so we use Response
260 params = onem2m_primitive.get_parameters()
261 proto_params = onem2m_primitive.get_protocol_specific_parameters()
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")
267 result_code = proto_params[http_result_code]
269 reason = status_codes._codes[result_code][0]
271 raise IoTDataEncodeError("Invalid result code passed: {}", result_code)
273 msg.status_code = result_code
276 # Headers from protocol specific parameters
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)
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)
290 content = onem2m_primitive.get_content()
292 msg._content = content
297 class OneM2MHttpJsonEncoderTx(IoTDataEncoder):
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)
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",
311 def _encode_operation(self, onem2m_operation):
312 return self.onem2m_oper_to_http_method[onem2m_operation]
314 def _translate_uri_from_onem2m(self, uri):
315 if 0 == uri.find("//"):
316 return "/_/" + uri[2:]
317 if 0 == uri.find("/"):
321 def encode(self, onem2m_primitive):
323 Encodes OneM2M JSON primitive object to Tx specific HTTP message
324 with JSON content type
327 params = onem2m_primitive.get_parameters()
328 proto_params = onem2m_primitive.get_protocol_specific_parameters()
330 # This is Tx encoder so we use Request
335 if OneM2M.short_operation in params:
336 msg.method = self._encode_operation(params[OneM2M.short_operation])
339 if OneM2M.short_to in params:
340 resource_uri = self._translate_uri_from_onem2m(params[OneM2M.short_to])
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])
348 msg.url = "http://" + entity_address + resource_uri
350 # encode headers and query parameters
352 for key, value in params.items():
355 if msg.url and key in http_query_params:
356 msg.url += delimiter + key + "=" + str(value)
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)
365 # Headers from protocol specific parameters
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)
373 content = onem2m_primitive.get_content()
379 class OneM2MHttpDecodeUtils:
380 """Implementation of utility methods for decoder classes"""
383 def translate_http_method_to_onem2m_operation(method, has_resource_type):
386 if has_resource_type:
387 return OneM2M.operation_create
389 return OneM2M.operation_notify
392 return OneM2M.operation_retrieve
395 return OneM2M.operation_update
398 return OneM2M.operation_delete
400 raise IoTDataDecodeError("Unsupported HTTP method: {}".format(method))
403 def translate_uri_to_onem2m(uri):
404 if 0 == uri.find("/_/"):
406 if 0 == uri.find("/~/"):
408 if 0 == uri.find("/"):
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
421 except Exception as e:
422 raise IoTDataDecodeError(
423 "Invalid Content-Length value: {}, error: {}".format(
428 http_specifics[decoded_name] = value
430 if decoded_name is OneM2M.short_response_status_code:
431 # decode as integer value
434 except Exception as e:
435 raise IoTDataDecodeError(
436 "Invalid status code value: {}, error: {}".format(
441 primitive_param_dict[decoded_name] = value
444 class OneM2MHttpJsonDecoderRx(IoTDataDecoder):
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
450 def decode(self, protocol_message):
452 Decodes Tx specific HTTP message with JSON content type to OneM2M JSON
455 builder = OneM2MHttpJsonPrimitiveBuilder().set_communication_protocol(
459 primitive_param_dict = {}
461 OneM2MHttpDecodeUtils.decode_headers(
462 primitive_param_dict, http_specifics, protocol_message.headers
465 builder.set_parameters(primitive_param_dict)
466 builder.set_protocol_specific_parameters(http_specifics)
468 if protocol_message.path:
471 OneM2MHttpDecodeUtils.translate_uri_to_onem2m(protocol_message.path),
474 if protocol_message.body:
475 builder.set_content(protocol_message.body)
477 if protocol_message.query_arguments:
478 for param, value in protocol_message.query_arguments.items():
481 builder.set_param(param, value)
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)
487 builder.set_param(OneM2M.short_operation, operation)
489 return builder.build()
492 class OneM2MHttpJsonDecoderTx(IoTDataDecoder):
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
498 def decode(self, protocol_message):
500 Decodes Rx specific HTTP message with JSON content type to OneM2M JSON
503 builder = OneM2MHttpJsonPrimitiveBuilder().set_communication_protocol(
507 primitive_param_dict = {}
509 OneM2MHttpDecodeUtils.decode_headers(
510 primitive_param_dict, http_specifics, protocol_message.headers
513 # TODO decode query if needed
516 if hasattr(protocol_message, "status_code"):
517 http_specifics[http_result_code] = protocol_message.status_code
519 builder.set_parameters(primitive_param_dict)
520 builder.set_protocol_specific_parameters(http_specifics)
523 if hasattr(protocol_message, "content"):
524 builder.set_content(protocol_message.content)
526 # builder.set_proto_param(original_content_string, protocol_message.content)
527 return builder.build()
530 class OneM2MHttpJsonPrimitive(OneM2MJsonPrimitive):
532 Specialization of OneM2M JSON primitive for HTTP protocol.
533 Extends verification methods of the OneM2MJsonPrimitive with HTTP specific
538 def _check_http_primitive_content(primitive):
539 content = primitive.get_content_str()
544 content_type = primitive.get_proto_param(http_header_content_type)
546 raise AssertionError("HTTP primitive without Content-Type")
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)
554 content_length = primitive.get_proto_param(http_header_content_length)
555 if not content_length:
556 raise AssertionError("HTTP primitive without Content-Length")
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__
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))
573 def _check_request_common(self):
574 op, rqi = super(OneM2MHttpJsonPrimitive, self)._check_request_common()
575 self._check_http_primitive_content(self)
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
582 self._check_http_primitive_content(response_primitive)
584 http_res = response_primitive.get_proto_param(http_result_code)
586 raise AssertionError("HTTP response primitive without Result-Code")
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(
596 expected_http_res = onem2m_to_http_result_codes[response_rsc]
597 except KeyError as e:
599 "Failed to map OneM2M rsc ({}) to HTTP status code: {}".format(
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
612 if response_rsc == OneM2M.result_code_created:
613 content_location = response_primitive.get_proto_param(
614 http_header_content_location
616 if not content_location:
617 raise AssertionError("HTTP response primitive without Content-Location")
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__)
628 class OneM2MHttpJsonPrimitiveBuilder(OneM2MJsonPrimitiveBuilder):
629 """Builder class specialized for OneM2MHttpJsonPrimitive objects"""
632 return OneM2MHttpJsonPrimitive(
633 self.parameters, self.content, self.protocol, self.proto_params