Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# SPDX-License-Identifier: Apache-2.0
3# Copyright 2020 Contributors to OpenLEADR
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
9# http://www.apache.org/licenses/LICENSE-2.0
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
17from asyncio import iscoroutine
18from http import HTTPStatus
19import logging
21from aiohttp import web
22from lxml.etree import XMLSyntaxError
23from signxml.exceptions import InvalidSignature
25from openleadr import enums, errors, hooks, utils
26from openleadr.messaging import parse_message, validate_xml_schema, authenticate_message
28from dataclasses import is_dataclass, asdict
30logger = logging.getLogger('openleadr')
33class VTNService:
34 def __init__(self, vtn_id):
35 self.vtn_id = vtn_id
36 self.handlers = {}
37 for method in [getattr(self, attr) for attr in dir(self) if callable(getattr(self, attr))]:
38 if hasattr(method, '__message_type__'):
39 self.handlers[method.__message_type__] = method
41 async def handler(self, request):
42 """
43 Handle all incoming POST requests.
44 """
45 try:
46 # Check the Content-Type header
47 content_type = request.headers.get('content-type', '')
48 if not content_type.lower().startswith("application/xml"):
49 raise errors.HTTPError(response_code=HTTPStatus.BAD_REQUEST,
50 response_description="The Content-Type header must be application/xml; "
51 f"you provided {request.headers.get('content-type', '')}")
52 content = await request.read()
53 hooks.call('before_parse', content)
55 # Validate the message to the XML Schema
56 message_tree = validate_xml_schema(content)
58 # Parse the message to a type and payload dict
59 message_type, message_payload = parse_message(content)
61 if message_type == 'oadrResponse':
62 raise errors.SendEmptyHTTPResponse()
64 if 'vtn_id' in message_payload \
65 and message_payload['vtn_id'] is not None \
66 and message_payload['vtn_id'] != self.vtn_id:
67 raise errors.InvalidIdError(f"The supplied vtnID is invalid. It should be '{self.vtn_id}', "
68 f"you supplied {message_payload['vtn_id']}.")
70 # Check if we know this VEN, ask for reregistration otherwise
71 if message_type not in ('oadrCreatePartyRegistration', 'oadrQueryRegistration') \
72 and 'ven_id' in message_payload and hasattr(self, 'ven_lookup'):
73 result = await utils.await_if_required(self.ven_lookup(ven_id=message_payload['ven_id']))
74 if result is None or result.get('registration_id', None) is None:
75 raise errors.RequestReregistration(message_payload['ven_id'])
77 # Authenticate the message
78 if request.secure and 'ven_id' in message_payload:
79 if hasattr(self, 'fingerprint_lookup'):
80 await authenticate_message(request, message_tree, message_payload,
81 fingerprint_lookup=self.fingerprint_lookup)
82 elif hasattr(self, 'ven_lookup'):
83 await authenticate_message(request, message_tree, message_payload,
84 ven_lookup=self.ven_lookup)
85 else:
86 logger.error("Could not authenticate this VEN because "
87 "you did not provide a 'ven_lookup' function. Please see "
88 "https://openleadr.org/docs/server.html#signing-messages for info.")
90 # Pass the message off to the handler and get the response type and payload
91 try:
92 # Add the request fingerprint to the message so that the handler can check for it.
93 if request.secure and message_type == 'oadrCreatePartyRegistration':
94 message_payload['fingerprint'] = utils.get_cert_fingerprint_from_request(request)
95 response_type, response_payload = await self.handle_message(message_type,
96 message_payload)
97 except Exception as err:
98 logger.error("An exception occurred during the execution of your "
99 f"{self.__class__.__name__} handler: "
100 f"{err.__class__.__name__}: {err}")
101 raise err
103 if 'response' not in response_payload:
104 response_payload['response'] = {'response_code': 200,
105 'response_description': 'OK',
106 'request_id': message_payload.get('request_id')}
107 response_payload['vtn_id'] = self.vtn_id
108 if 'ven_id' not in response_payload:
109 response_payload['ven_id'] = message_payload.get('ven_id')
110 except errors.RequestReregistration as err:
111 response_type = 'oadrRequestReregistration'
112 response_payload = {'ven_id': err.ven_id}
113 msg = self._create_message(response_type, **response_payload)
114 response = web.Response(text=msg,
115 status=HTTPStatus.OK,
116 content_type='application/xml')
117 except errors.SendEmptyHTTPResponse:
118 response = web.Response(text='',
119 status=HTTPStatus.OK,
120 content_type='application/xml')
121 except errors.ProtocolError as err:
122 # In case of an OpenADR error, return a valid OpenADR message
123 response_type, response_payload = self.error_response(message_type,
124 err.response_code,
125 err.response_description)
126 msg = self._create_message(response_type, **response_payload)
127 response = web.Response(text=msg,
128 status=HTTPStatus.OK,
129 content_type='application/xml')
130 except errors.HTTPError as err:
131 # If we throw a http-related error, deal with it here
132 response = web.Response(text=err.response_description,
133 status=err.response_code)
134 except XMLSyntaxError as err:
135 logger.warning(f"XML schema validation of incoming message failed: {err}.")
136 response = web.Response(text=f'XML failed validation: {err}',
137 status=HTTPStatus.BAD_REQUEST)
138 except errors.FingerprintMismatch as err:
139 logger.warning(err)
140 response = web.Response(text=str(err),
141 status=HTTPStatus.FORBIDDEN)
142 except InvalidSignature:
143 logger.warning("Incoming message had invalid signature, ignoring.")
144 response = web.Response(text='Invalid Signature',
145 status=HTTPStatus.FORBIDDEN)
146 except Exception as err:
147 # In case of some other error, return a HTTP 500
148 logger.error(f"The VTN server encountered an error: {err.__class__.__name__}: {err}")
149 response = web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR)
150 else:
151 # We've successfully handled this message
152 msg = self._create_message(response_type, **response_payload)
153 response = web.Response(text=msg,
154 status=HTTPStatus.OK,
155 content_type='application/xml')
156 hooks.call('before_respond', response.text)
157 return response
159 async def handle_message(self, message_type, message_payload):
160 hooks.call('before_handle', message_type, message_payload)
161 if message_type in self.handlers:
162 handler = self.handlers[message_type]
163 result = handler(message_payload)
164 if iscoroutine(result):
165 result = await result
166 if result is not None:
167 response_type, response_payload = result
168 if is_dataclass(response_payload):
169 response_payload = asdict(response_payload)
170 elif response_payload is None:
171 response_payload = {}
172 else:
173 response_type, response_payload = 'oadrResponse', {}
175 response_payload['vtn_id'] = self.vtn_id
176 if 'ven_id' in message_payload:
177 response_payload['ven_id'] = message_payload['ven_id']
179 response_payload['response'] = {'request_id': message_payload.get('request_id', None),
180 'response_code': 200,
181 'response_description': 'OK'}
182 response_payload['request_id'] = utils.generate_id()
184 else:
185 response_type, response_payload = self.error_response('oadrResponse',
186 enums.STATUS_CODES.COMPLIANCE_ERROR,
187 "A message of type "
188 f"{message_type} should not be "
189 "sent to this endpoint")
190 logger.info(f"Responding to {message_type} with a {response_type} message: {response_payload}.")
191 hooks.call('after_handle', response_type, response_payload)
192 return response_type, response_payload
194 def error_response(self, message_type, error_code, error_description):
195 if message_type == 'oadrCreatePartyRegistration':
196 response_type = 'oadrCreatedPartyRegistration'
197 if message_type == 'oadrRequestEvent':
198 response_type = 'oadrDistributeEvent'
199 else:
200 response_type = 'oadrResponse'
201 response_payload = {'response': {'response_code': error_code,
202 'response_description': error_description}}
203 return response_type, response_payload