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
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)
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
89 onem2m_to_http_result_codes = {
90 OneM2M.result_code_accepted: httplib.ACCEPTED,
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,
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,
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,
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
149 class OneM2MHttpTx(IoTTx):
150 """Implementation of HTTP OneM2M Tx channel"""
152 def __init__(self, encoder, decoder):
153 super(OneM2MHttpTx, self).__init__(encoder, decoder)
157 self.session = Session()
164 def send(self, jsonprimitive):
166 message = self.encoder.encode(jsonprimitive)
167 except IoTDataEncodeError as e:
170 rsp_message = self.session.send(message)
174 rsp_primitive = self.decoder.decode(rsp_message)
175 except IoTDataDecodeError as e:
181 class OneM2MHttpRx(IoTRx):
182 """Implementation of HTTP OneM2M Rx channel"""
184 def __init__(self, decoder, encoder, port, interface=""):
185 super(OneM2MHttpRx, self).__init__(decoder, encoder)
186 self.interface = interface
188 self.server_address = (interface, port)
192 def _handle_request(self, request):
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.
200 primitive = self.decoder.decode(request)
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())
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(version='HTTP/1.1', code=code, reason=reason)
220 request.connection.write_headers(start_line, headers)
224 request.connection.write(json.dumps(encoded.content))
229 ioloop.IOLoop.instance().start()
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)
239 ioloop.IOLoop.instance().stop()
243 class OneM2MHttpJsonEncoderRx(IoTDataEncoder):
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)
249 def encode(self, onem2m_primitive):
251 Encodes OneM2M JSON primitive object to Rx specific HTTP message
252 with JSON content type
255 # This is Rx encoder so we use Response
258 params = onem2m_primitive.get_parameters()
259 proto_params = onem2m_primitive.get_protocol_specific_parameters()
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")
265 result_code = proto_params[http_result_code]
267 reason = status_codes._codes[result_code][0]
269 raise IoTDataEncodeError("Invalid result code passed: {}", result_code)
271 msg.status_code = result_code
274 # Headers from protocol specific parameters
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)
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)
288 content = onem2m_primitive.get_content()
290 msg._content = content
295 class OneM2MHttpJsonEncoderTx(IoTDataEncoder):
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)
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"
309 def _encode_operation(self, onem2m_operation):
310 return self.onem2m_oper_to_http_method[onem2m_operation]
312 def _translate_uri_from_onem2m(self, uri):
313 if 0 == uri.find("//"):
314 return "/_/" + uri[2:]
315 if 0 == uri.find("/"):
319 def encode(self, onem2m_primitive):
321 Encodes OneM2M JSON primitive object to Tx specific HTTP message
322 with JSON content type
325 params = onem2m_primitive.get_parameters()
326 proto_params = onem2m_primitive.get_protocol_specific_parameters()
328 # This is Tx encoder so we use Request
333 if OneM2M.short_operation in params:
334 msg.method = self._encode_operation(params[OneM2M.short_operation])
337 if OneM2M.short_to in params:
338 resource_uri = self._translate_uri_from_onem2m(params[OneM2M.short_to])
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]))
346 msg.url = "http://" + entity_address + resource_uri
348 # encode headers and query parameters
350 for key, value in params.items():
353 if msg.url and key in http_query_params:
354 msg.url += (delimiter + key + "=" + str(value))
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)
363 # Headers from protocol specific parameters
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)
371 content = onem2m_primitive.get_content()
377 class OneM2MHttpDecodeUtils:
378 """Implementation of utility methods for decoder classes"""
381 def translate_http_method_to_onem2m_operation(method, has_resource_type):
384 if has_resource_type:
385 return OneM2M.operation_create
387 return OneM2M.operation_notify
390 return OneM2M.operation_retrieve
393 return OneM2M.operation_update
396 return OneM2M.operation_delete
398 raise IoTDataDecodeError("Unsupported HTTP method: {}".format(method))
401 def translate_uri_to_onem2m(uri):
402 if 0 == uri.find("/_/"):
404 if 0 == uri.find("/~/"):
406 if 0 == uri.find("/"):
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
419 except Exception as e:
420 raise IoTDataDecodeError("Invalid Content-Length value: {}, error: {}".format(value, e))
422 http_specifics[decoded_name] = value
424 if decoded_name is OneM2M.short_response_status_code:
425 # decode as integer value
428 except Exception as e:
429 raise IoTDataDecodeError("Invalid status code value: {}, error: {}".format(value, e))
431 primitive_param_dict[decoded_name] = value
434 class OneM2MHttpJsonDecoderRx(IoTDataDecoder):
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
440 def decode(self, protocol_message):
442 Decodes Tx specific HTTP message with JSON content type to OneM2M JSON
445 builder = OneM2MHttpJsonPrimitiveBuilder() \
446 .set_communication_protocol(HTTPPROTOCOLNAME)
448 primitive_param_dict = {}
450 OneM2MHttpDecodeUtils.decode_headers(primitive_param_dict, http_specifics, protocol_message.headers)
452 builder.set_parameters(primitive_param_dict)
453 builder.set_protocol_specific_parameters(http_specifics)
455 if protocol_message.path:
456 builder.set_param(OneM2M.short_to, OneM2MHttpDecodeUtils.translate_uri_to_onem2m(protocol_message.path))
458 if protocol_message.body:
459 builder.set_content(protocol_message.body)
461 if protocol_message.query_arguments:
462 for param, value in protocol_message.query_arguments.items():
465 builder.set_param(param, value)
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)
472 return builder.build()
475 class OneM2MHttpJsonDecoderTx(IoTDataDecoder):
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
481 def decode(self, protocol_message):
483 Decodes Rx specific HTTP message with JSON content type to OneM2M JSON
486 builder = OneM2MHttpJsonPrimitiveBuilder() \
487 .set_communication_protocol(HTTPPROTOCOLNAME)
489 primitive_param_dict = {}
491 OneM2MHttpDecodeUtils.decode_headers(primitive_param_dict, http_specifics, protocol_message.headers)
493 # TODO decode query if needed
496 if hasattr(protocol_message, "status_code"):
497 http_specifics[http_result_code] = protocol_message.status_code
499 builder.set_parameters(primitive_param_dict)
500 builder.set_protocol_specific_parameters(http_specifics)
503 if hasattr(protocol_message, "content"):
504 builder.set_content(protocol_message.content)
506 # builder.set_proto_param(original_content_string, protocol_message.content)
507 return builder.build()
510 class OneM2MHttpJsonPrimitive(OneM2MJsonPrimitive):
512 Specialization of OneM2M JSON primitive for HTTP protocol.
513 Extends verification methods of the OneM2MJsonPrimitive with HTTP specific
518 def _check_http_primitive_content(primitive):
519 content = primitive.get_content_str()
524 content_type = primitive.get_proto_param(http_header_content_type)
526 raise AssertionError("HTTP primitive without Content-Type")
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))
532 content_length = primitive.get_proto_param(http_header_content_length)
533 if not content_length:
534 raise AssertionError("HTTP primitive without Content-Length")
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__))
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))
549 def _check_request_common(self):
550 op, rqi = super(OneM2MHttpJsonPrimitive, self)._check_request_common()
551 self._check_http_primitive_content(self)
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)
558 http_res = response_primitive.get_proto_param(http_result_code)
560 raise AssertionError("HTTP response primitive without Result-Code")
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(
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))
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))
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")
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__))
591 class OneM2MHttpJsonPrimitiveBuilder(OneM2MJsonPrimitiveBuilder):
592 """Builder class specialized for OneM2MHttpJsonPrimitive objects"""
595 return OneM2MHttpJsonPrimitive(self.parameters, self.content, self.protocol, self.proto_params)