2019-01-11 17:46:03 +01:00

235 lines
7.9 KiB
Python

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