vtn_service.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. # SPDX-License-Identifier: Apache-2.0
  2. # Copyright 2020 Contributors to OpenLEADR
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. # Unless required by applicable law or agreed to in writing, software
  8. # distributed under the License is distributed on an "AS IS" BASIS,
  9. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. # See the License for the specific language governing permissions and
  11. # limitations under the License.
  12. from asyncio import iscoroutine
  13. from http import HTTPStatus
  14. import logging
  15. from aiohttp import web
  16. from lxml.etree import XMLSyntaxError
  17. from signxml.exceptions import InvalidSignature
  18. from .. import errors
  19. from ..enums import STATUS_CODES
  20. from ..messaging import parse_message, validate_xml_schema, authenticate_message
  21. from ..utils import generate_id, get_cert_fingerprint_from_request
  22. from dataclasses import is_dataclass, asdict
  23. logger = logging.getLogger('openleadr')
  24. class VTNService:
  25. def __init__(self, vtn_id):
  26. self.vtn_id = vtn_id
  27. self.handlers = {}
  28. for method in [getattr(self, attr) for attr in dir(self) if callable(getattr(self, attr))]:
  29. if hasattr(method, '__message_type__'):
  30. self.handlers[method.__message_type__] = method
  31. async def handler(self, request):
  32. """
  33. Handle all incoming POST requests.
  34. """
  35. try:
  36. # Check the Content-Type header
  37. content_type = request.headers.get('content-type', '')
  38. if not content_type.lower().startswith("application/xml"):
  39. raise errors.HTTPError(response_code=HTTPStatus.BAD_REQUEST,
  40. response_description="The Content-Type header must be application/xml, "
  41. "you provided {request.headers.get('content-type', '')}")
  42. content = await request.read()
  43. # Validate the message to the XML Schema
  44. message_tree = validate_xml_schema(content)
  45. # Parse the message to a type and payload dict
  46. message_type, message_payload = parse_message(content)
  47. if 'vtn_id' in message_payload \
  48. and message_payload['vtn_id'] is not None \
  49. and message_payload['vtn_id'] != self.vtn_id:
  50. raise errors.InvalidIdError(f"The supplied vtnID is invalid. It should be '{self.vtn_id}', "
  51. f"you supplied {message_payload['vtn_id']}.")
  52. # Authenticate the message
  53. if request.secure and 'ven_id' in message_payload:
  54. await authenticate_message(request, message_tree, message_payload,
  55. self.fingerprint_lookup)
  56. # Pass the message off to the handler and get the response type and payload
  57. try:
  58. # Add the request fingerprint to the message so that the handler can check for it.
  59. if request.secure and message_type == 'oadrCreatePartyRegistration':
  60. message_payload['fingerprint'] = get_cert_fingerprint_from_request(request)
  61. response_type, response_payload = await self.handle_message(message_type,
  62. message_payload)
  63. except Exception as err:
  64. logger.error("An exception occurred during the execution of your "
  65. f"{self.__class__.__name__} handler: "
  66. f"{err.__class__.__name__}: {err}")
  67. raise err
  68. if 'response' not in response_payload:
  69. response_payload['response'] = {'response_code': 200,
  70. 'response_description': 'OK',
  71. 'request_id': message_payload.get('request_id')}
  72. response_payload['vtn_id'] = self.vtn_id
  73. if 'ven_id' not in response_payload:
  74. response_payload['ven_id'] = message_payload.get('ven_id')
  75. except errors.ProtocolError as err:
  76. # In case of an OpenADR error, return a valid OpenADR message
  77. response_type, response_payload = self.error_response(message_type,
  78. err.response_code,
  79. err.response_description)
  80. msg = self._create_message(response_type, **response_payload)
  81. response = web.Response(text=msg,
  82. status=HTTPStatus.OK,
  83. content_type='application/xml')
  84. except errors.HTTPError as err:
  85. # If we throw a http-related error, deal with it here
  86. response = web.Response(text=err.response_description,
  87. status=err.response_code)
  88. except XMLSyntaxError as err:
  89. logger.warning(f"XML schema validation of incoming message failed: {err}.")
  90. response = web.Response(text=f'XML failed validation: {err}',
  91. status=HTTPStatus.BAD_REQUEST)
  92. except errors.FingerprintMismatch as err:
  93. logger.warning(err)
  94. response = web.Response(text=str(err),
  95. status=HTTPStatus.FORBIDDEN)
  96. except InvalidSignature:
  97. logger.warning("Incoming message had invalid signature, ignoring.")
  98. response = web.Response(text='Invalid Signature',
  99. status=HTTPStatus.FORBIDDEN)
  100. except Exception as err:
  101. # In case of some other error, return a HTTP 500
  102. logger.error(f"The VTN server encountered an error: {err.__class__.__name__}: {err}")
  103. response = web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR)
  104. else:
  105. # We've successfully handled this message
  106. msg = self._create_message(response_type, **response_payload)
  107. response = web.Response(text=msg,
  108. status=HTTPStatus.OK,
  109. content_type='application/xml')
  110. return response
  111. async def handle_message(self, message_type, message_payload):
  112. if message_type in self.handlers:
  113. handler = self.handlers[message_type]
  114. result = handler(message_payload)
  115. if iscoroutine(result):
  116. result = await result
  117. if result is not None:
  118. response_type, response_payload = result
  119. if is_dataclass(response_payload):
  120. response_payload = asdict(response_payload)
  121. elif response_payload is None:
  122. response_payload = {}
  123. else:
  124. response_type, response_payload = 'oadrResponse', {}
  125. response_payload['vtn_id'] = self.vtn_id
  126. if 'ven_id' in message_payload:
  127. response_payload['ven_id'] = message_payload['ven_id']
  128. response_payload['response'] = {'request_id': message_payload.get('request_id', None),
  129. 'response_code': 200,
  130. 'response_description': 'OK'}
  131. response_payload['request_id'] = generate_id()
  132. else:
  133. response_type, response_payload = self.error_response('oadrResponse',
  134. STATUS_CODES.COMPLIANCE_ERROR,
  135. "A message of type "
  136. f"{message_type} should not be "
  137. "sent to this endpoint")
  138. logger.info(f"Responding to {message_type} with a {response_type} message: {response_payload}.")
  139. return response_type, response_payload
  140. def error_response(self, message_type, error_code, error_description):
  141. if message_type == 'oadrCreatePartyRegistration':
  142. response_type = 'oadrCreatedPartyRegistration'
  143. if message_type == 'oadrRequestEvent':
  144. response_type = 'oadrDistributeEvent'
  145. else:
  146. response_type = 'oadrResponse'
  147. response_payload = {'response': {'response_code': error_code,
  148. 'response_description': error_description}}
  149. return response_type, response_payload