import urllib.request import urllib.parse import urllib.error import ssl from socket import ( gaierror, error as socket_error, ) from time import time from urllib.parse import urlparse from aplus import Promise from futile.caching import LRUCache from geventhttpclient.client import HTTPClient from geventhttpclient.response import HTTPResponse from openmtc.exc import ( OpenMTCNetworkError, ConnectionFailed, ) from openmtc_onem2m.exc import ( get_error_class, get_response_status, ERROR_MIN, ) from openmtc_onem2m.model import ( ResourceTypeE, get_short_attribute_name, get_short_member_name, ) from openmtc_onem2m.serializer.util import ( decode_onem2m_content, encode_onem2m_content, ) from openmtc_onem2m.transport import ( OneM2MOperation, OneM2MResponse, OneM2MErrorResponse, ) from . import ( OneM2MClient, normalize_path, ) _method_map_to_http = { OneM2MOperation.create: 'POST', OneM2MOperation.retrieve: 'GET', OneM2MOperation.update: 'PUT', OneM2MOperation.delete: 'DELETE', OneM2MOperation.notify: 'POST', } _clients = LRUCache(threadsafe=False) _query_params = frozenset(['rt', 'rp', 'rcn', 'da', 'drt', 'rids', 'tids', 'ltids', 'tqi']) _header_to_field_map = { 'X-M2M-ORIGIN': 'originator', 'X-M2M-RI': 'rqi', 'X-M2M-GID': 'gid', 'X-M2M-OT': 'ot', 'X-M2M-RST': 'rset', 'X-M2M-RET': 'rqet', 'X-M2M-OET': 'oet', 'X-M2M-EC': 'ec', 'X-M2M-RVI': 'rvi', 'X-M2M-VSI': 'vsi', } def get_client(m2m_ep, use_xml=False, ca_certs=None, cert_file=None, key_file=None, insecure=False): try: return _clients[(m2m_ep, use_xml)] except KeyError: # TODO: make connection_timeout and concurrency configurable client = _clients[(m2m_ep, use_xml)] = OneM2MHTTPClient( m2m_ep, use_xml, ca_certs, cert_file, key_file, insecure) return client class OneM2MHTTPClient(OneM2MClient): # defaults DEF_SSL_VERSION = ssl.PROTOCOL_TLSv1_2 def __init__(self, m2m_ep, use_xml, ca_certs=None, cert_file=None, key_file=None, insecure=False): super(OneM2MHTTPClient, self).__init__() self.parsed_url = urlparse(m2m_ep) is_https = self.parsed_url.scheme[-1].lower() == "s" port = self.parsed_url.port or (is_https and 443 or 80) host = self.parsed_url.hostname self.path = self.parsed_url.path.rstrip('/') if self.path and not self.path.endswith('/'): self.path += '/' # TODO(rst): handle IPv6 host here # geventhttpclient sets incorrect host header # i.e "host: ::1:8000" instead of "host: [::1]:8000 if is_https: ssl_options = { 'ssl_version': self.DEF_SSL_VERSION } if ca_certs: ssl_options['ca_certs'] = ca_certs if cert_file and key_file: ssl_options['certfile'] = cert_file ssl_options['keyfile'] = key_file else: ssl_options = None def get_http_client(): return HTTPClient(host, port, connection_timeout=120.0, concurrency=50, ssl=is_https, ssl_options=ssl_options, insecure=insecure) self._get_client = get_http_client self.content_type = 'application/' + ('xml' if use_xml else 'json') def _handle_network_error(self, exc, p, http_request, t, exc_class=OpenMTCNetworkError): error_str = str(exc) if error_str in ("", "''"): error_str = repr(exc) method = http_request["method"] path = http_request["request_uri"] log_path = "%s://%s/%s" % (self.parsed_url.scheme, self.parsed_url.netloc, path) error_msg = "Error during HTTP request: %s. " \ "Request was: %s %s (%.4fs)" % (error_str, method, log_path, time() - t) p.reject(exc_class(error_msg)) def map_onem2m_request_to_http_request(self, onem2m_request): """ Maps a OneM2M request to a HTTP request :param onem2m_request: OneM2M request to be mapped :return: request: the resulting HTTP request """ self.logger.debug("Mapping OneM2M request to generic request: %s", onem2m_request) params = { param: getattr(onem2m_request, param) for param in _query_params if getattr(onem2m_request, param) is not None } if onem2m_request.fc is not None: filter_criteria = onem2m_request.fc params.update({ (get_short_attribute_name(name) or get_short_member_name(name)): val for name, val in filter_criteria.get_values(True).items() }) if onem2m_request.ae_notifying: path = '' else: path = normalize_path(onem2m_request.to) if params: path += '?' + urllib.parse.urlencode(params, True) content_type, data = encode_onem2m_content(onem2m_request.content, self.content_type, path=path) # TODO(rst): check again # set resource type if onem2m_request.operation == OneM2MOperation.create: content_type += '; ty=' + str(ResourceTypeE[onem2m_request.resource_type.typename]) headers = { header: getattr(onem2m_request, field) for header, field in _header_to_field_map.items() if getattr(onem2m_request, field) is not None } headers['content-type'] = content_type headers['accept'] = self.content_type self.logger.debug("Added request params: %s", params) return { 'method': _method_map_to_http[onem2m_request.operation], 'request_uri': self.path + path, 'body': data, 'headers': headers, } def map_http_response_to_onem2m_response(self, onem2m_request, response): """ Maps HTTP response to OneM2M response :param onem2m_request: the OneM2M request that created the response :param response: the HTTP response :return: resulting OneM2MResponse or OneM2MErrorResponse """ if not isinstance(response, HTTPResponse): self.logger.error("Not a valid response: %s", response) # return OneM2MErrorResponse(STATUS_INTERNAL_SERVER_ERROR) self.logger.debug("Mapping HTTP response for OneM2M response: %s", response) rsc = response.get("x-m2m-rsc", 5000) if int(rsc) >= ERROR_MIN: return OneM2MErrorResponse( get_error_class(rsc).response_status_code, onem2m_request) return OneM2MResponse( get_response_status(rsc), request=onem2m_request, rsc=rsc, pc=decode_onem2m_content(response.read(), response.get("content-type")) ) def send_onem2m_request(self, onem2m_request): with Promise() as p: http_request = self.map_onem2m_request_to_http_request(onem2m_request) t = time() client = self._get_client() try: response = client.request(**http_request) except (socket_error, gaierror) as exc: self._handle_network_error(exc, p, http_request, t, ConnectionFailed) except Exception as exc: self.logger.exception("Error in HTTP request") self._handle_network_error(exc, p, http_request, t) else: try: onem2m_response = self.map_http_response_to_onem2m_response(onem2m_request, response) if isinstance(onem2m_response, OneM2MErrorResponse): p.reject(onem2m_response) else: p.fulfill(onem2m_response) finally: response.release() finally: client.close() return p