Hide keyboard shortcuts

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 

2 

3# Copyright 2020 Contributors to OpenLEADR 

4 

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 

8 

9# http://www.apache.org/licenses/LICENSE-2.0 

10 

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. 

16 

17from asyncio import iscoroutine 

18from http import HTTPStatus 

19import logging 

20 

21from aiohttp import web 

22from lxml.etree import XMLSyntaxError 

23from signxml.exceptions import InvalidSignature 

24 

25from openleadr import enums, errors, hooks, utils 

26from openleadr.messaging import parse_message, validate_xml_schema, authenticate_message 

27 

28from dataclasses import is_dataclass, asdict 

29 

30logger = logging.getLogger('openleadr') 

31 

32 

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 

40 

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) 

54 

55 # Validate the message to the XML Schema 

56 message_tree = validate_xml_schema(content) 

57 

58 # Parse the message to a type and payload dict 

59 message_type, message_payload = parse_message(content) 

60 

61 if message_type == 'oadrResponse': 

62 raise errors.SendEmptyHTTPResponse() 

63 

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']}.") 

69 

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']) 

76 

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.") 

89 

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 

102 

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 

158 

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', {} 

174 

175 response_payload['vtn_id'] = self.vtn_id 

176 if 'ven_id' in message_payload: 

177 response_payload['ven_id'] = message_payload['ven_id'] 

178 

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() 

183 

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 

193 

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