mirror of
https://github.com/OpenMTC/OpenMTC.git
synced 2025-05-02 08:42:50 +00:00
235 lines
7.9 KiB
Python
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
|