Просмотр исходного кода

Rename pyopenadr to openleadr
This commit also contains some of the API changes to the signatures functionality, which is not ideal.

Stan Janssen 4 лет назад
Родитель
Сommit
47e6674046
66 измененных файлов с 482 добавлено и 133 удалено
  1. 3 3
      MANIFEST.in
  2. 1 1
      README.md
  3. 0 0
      docs/api/openleadr.rst
  4. 0 62
      docs/api/pyopenadr.service.rst
  5. 1 1
      docs/client.rst
  6. 2 2
      docs/index.rst
  7. 44 0
      docs/message_signing.rst
  8. 2 2
      docs/representations.rst
  9. 1 1
      docs/server.rst
  10. 0 0
      openleadr/__init__.py
  11. 45 29
      openleadr/client.py
  12. 0 0
      openleadr/config.py
  13. 0 0
      openleadr/enums.py
  14. 0 0
      openleadr/errors.py
  15. 18 5
      openleadr/messaging.py
  16. 150 0
      openleadr/objects.py
  17. 0 0
      openleadr/preflight.py
  18. 25 15
      openleadr/server.py
  19. 0 0
      openleadr/service/__init__.py
  20. 0 0
      openleadr/service/event_service.py
  21. 0 0
      openleadr/service/opt_service.py
  22. 0 0
      openleadr/service/poll_service.py
  23. 6 0
      openleadr/service/registration_service.py
  24. 0 0
      openleadr/service/report_service.py
  25. 0 0
      openleadr/service/vtn_service.py
  26. 0 0
      openleadr/templates/oadrCancelOpt.xml
  27. 0 0
      openleadr/templates/oadrCancelPartyRegistration.xml
  28. 0 0
      openleadr/templates/oadrCancelReport.xml
  29. 0 0
      openleadr/templates/oadrCanceledOpt.xml
  30. 0 0
      openleadr/templates/oadrCanceledPartyRegistration.xml
  31. 0 0
      openleadr/templates/oadrCanceledReport.xml
  32. 0 0
      openleadr/templates/oadrCreateOpt.xml
  33. 0 0
      openleadr/templates/oadrCreatePartyRegistration.xml
  34. 0 0
      openleadr/templates/oadrCreateReport.xml
  35. 0 0
      openleadr/templates/oadrCreatedEvent.xml
  36. 0 0
      openleadr/templates/oadrCreatedOpt.xml
  37. 0 0
      openleadr/templates/oadrCreatedPartyRegistration.xml
  38. 0 0
      openleadr/templates/oadrCreatedReport.xml
  39. 0 0
      openleadr/templates/oadrDistributeEvent.xml
  40. 0 0
      openleadr/templates/oadrPayload.xml
  41. 0 0
      openleadr/templates/oadrPoll.xml
  42. 0 0
      openleadr/templates/oadrQueryRegistration.xml
  43. 0 0
      openleadr/templates/oadrRegisterReport.xml
  44. 0 0
      openleadr/templates/oadrRegisteredReport.xml
  45. 0 0
      openleadr/templates/oadrRequestEvent.xml
  46. 0 0
      openleadr/templates/oadrRequestReregistration.xml
  47. 0 0
      openleadr/templates/oadrResponse.xml
  48. 0 0
      openleadr/templates/oadrUpdateReport.xml
  49. 0 0
      openleadr/templates/oadrUpdatedReport.xml
  50. 0 0
      openleadr/templates/parts/eiActivePeriod.xml
  51. 0 0
      openleadr/templates/parts/eiEvent.xml
  52. 4 0
      openleadr/templates/parts/eiEventDescriptor.xml
  53. 0 0
      openleadr/templates/parts/eiEventSignal.xml
  54. 0 0
      openleadr/templates/parts/eiEventTarget.xml
  55. 0 0
      openleadr/templates/parts/eiTarget.xml
  56. 0 0
      openleadr/templates/parts/emixInterface.xml
  57. 0 0
      openleadr/templates/parts/oadrReportDescription.xml
  58. 0 0
      openleadr/templates/parts/oadrReportRequest.xml
  59. 0 0
      openleadr/templates/parts/reportDescriptionEmix.xml
  60. 20 1
      openleadr/utils.py
  61. 1 1
      setup.py
  62. 16 0
      test/__init__.py
  63. 9 2
      test/integration_tests/test_client_registration.py
  64. 82 0
      test/test_failures.py
  65. 50 0
      test/test_objects.py
  66. 2 8
      test/test_signatures.py

+ 3 - 3
MANIFEST.in

@@ -1,3 +1,3 @@
-include pyopenadr/templates/*.xml
-include pyopenadr/templates/parts/*.xml
-include pyopenadr/templates/signatures/*.xml
+include openleadr/templates/*.xml
+include openleadr/templates/parts/*.xml
+include openleadr/templates/signatures/*.xml

+ 1 - 1
README.md

@@ -18,7 +18,7 @@ new bug reports and pull requests are most welcome.
 
 ```bash
 git clone https://github.com/openleadr/openleadr-python
-cd pyopenadr-python
+cd openleadr-python
 python3 -m venv python_env
 ./python_env/bin/pip3 install -e .
 ```

+ 0 - 0
docs/api/pyopenadr.rst → docs/api/openleadr.rst


+ 0 - 62
docs/api/pyopenadr.service.rst

@@ -1,62 +0,0 @@
-openleadr.service package
-=========================
-
-Submodules
-----------
-
-openleadr.service.event\_service module
----------------------------------------
-
-.. automodule:: openleadr.service.event_service
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-openleadr.service.opt\_service module
--------------------------------------
-
-.. automodule:: openleadr.service.opt_service
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-openleadr.service.poll\_service module
---------------------------------------
-
-.. automodule:: openleadr.service.poll_service
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-openleadr.service.registration\_service module
-----------------------------------------------
-
-.. automodule:: openleadr.service.registration_service
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-openleadr.service.report\_service module
-----------------------------------------
-
-.. automodule:: openleadr.service.report_service
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-openleadr.service.vtn\_service module
--------------------------------------
-
-.. automodule:: openleadr.service.vtn_service
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-
-Module contents
----------------
-
-.. automodule:: openleadr.service
-   :members:
-   :undoc-members:
-   :show-inheritance:

+ 1 - 1
docs/client.rst

@@ -64,7 +64,7 @@ And you configure this report in OpenLEADR using an :ref:`oadrReportDescription`
         report_description = {''}
         client.add_report({'report'})
 
-The only thing you need to provide is the current value for the item you are reporting. PyOpenADR will format the complete :ref:`oadrReport` message for you.
+The only thing you need to provide is the current value for the item you are reporting. OpenLEADR will format the complete :ref:`oadrReport` message for you.
 
 
 

+ 2 - 2
docs/index.rst

@@ -1,4 +1,4 @@
-.. pyOpenAdr documentation master file, created by
+.. OpenLEADR documentation master file, created by
    sphinx-quickstart on Thu Jul  9 14:09:27 2020.
    You can adapt this file completely to your liking, but it should at least
    contain the root `toctree` directive.
@@ -101,7 +101,7 @@ Table of Contents
 Representations of OpenADR payloads
 ===================================
 
-PyOpenADR uses Python dictionaries and vanilla Python types (like datetime and timedelta) to represent the OpenADR payloads. These pages serve as a reference to these representations.
+OpenLEADR uses Python dictionaries and vanilla Python types (like datetime and timedelta) to represent the OpenADR payloads. These pages serve as a reference to these representations.
 
 For example, this XML payload:
 

+ 44 - 0
docs/message_signing.rst

@@ -0,0 +1,44 @@
+.. _message_signing:
+
+===============
+Message Signing
+===============
+
+OpenADR messages should ideally be signed using an X509 certificate keypair. This allows both parties to verify that the message came from the correct party, and that it was not tampered with in transport. It does not provide message encryption; for that, a transport-level encryption (TLS) should be used.
+
+Overview
+--------
+
+The high level overview is this:
+
+1. The VTN creates an X509 certificate and an associated private key. It shares a fingerprint of the certificate with the VEN it connects to (this fingerprint will be printed to your console on startup).
+2. The VEN receives a signed (not encrypted) message from the VTN. Each message to the VEN includes the complete X509 Certificate that can be used to verify the signature. The VEN verifies that the message signature is correct, and it verifies that the certificate fingerprint matches the fingerprint that it received from the VEN.
+
+The same process applies with the parties reversed.
+
+For a VEN (client), which talks to one VTN, you simply provide the path to the signing certificate and private key, and the private key passphrase and the VTN's fingerprint on initialization. See: :ref:`client_signing_messages` and :ref:`client_validating_messages`.
+
+For a VTN (server), which talks to multiple VENs, you provide the signing certificate, private key, private key passphrase and a handler function that can look up the certificate fingerprint when given a ven_id. See: :ref:`server_signing_messages`.
+
+
+Generating certificates
+-----------------------
+
+To generate a certificate + private key pair, you can use the following command on Linux:
+
+.. code-block:: text
+
+    openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365
+
+This will generate two files (key.pem and cert.pem) and require you to input a passphrase that encrypts/decrypts the private key.
+
+You can provide paths to these files, as well as the passphrase, in your Client or Server initialization. See the examples referred to above.
+
+
+Replay Protection
+-----------------
+
+To prevent an attacker from simple re-playing a previous message (for instance, an Event), unmodified, each signed message contains a ReplayProtect property. This contains a random string (nonce) and a timestamp. Upon validation of the message, it is verified that the timestamp is not older then some preconfigured value (default: 5 seconds), and that the random string has not already been seen during that time window. A cache of the nonces is kept automatically to verify this.
+
+OpenLEADR automatically generates and validates these portions of the signature. Signed messages that do not contain a ReplayProtect element are rejected, as required by the OpenADR specification.
+

+ 2 - 2
docs/representations.rst

@@ -4,13 +4,13 @@
 Payload Representations
 =======================
 
-In PyOpenADR, the complex hierarchies of the OpenADR XML-payloads are represented as Python dictionaries. These have been simplified as much as possible, allowing for a more natural and more readable experience.
+In OpenLEADR, the complex hierarchies of the OpenADR XML-payloads are represented as Python dictionaries. These have been simplified as much as possible, allowing for a more natural and more readable experience.
 
 This means that you don't have to instantiate objects and sub-objects and sub-sub-objects, but that you can define the entire object in a single, declarative statement. This kan keep a simple implementation very compact. The downside is that there is little help from your IDE and there is little discoverability for what contents can be provided in the messages. This page can be used as a reference for that information.
 
 To help you, all outgong messages are validated against the XML schema, and you will receive warnings if your messages don't comply to the schema.
 
-The following general principles have been applied to representing OpenADR objects in PyOpenADR:
+The following general principles have been applied to representing OpenADR objects in OpenLEADR:
 
 - All property names are represented in snake_case instead of CamelCase or mixedCase names. For example: ``requestID`` becomes ``request_id``.
 - For all properties, the ``oadr*`` and ``Ei*`` prefixes have been stripped away. For example: ``eiResponse`` becomes ``response`` and ``oadrResponse`` becomes ``response``.

+ 1 - 1
docs/server.rst

@@ -64,7 +64,7 @@ The VTN must determine when VENs are relevant and which Events to send to them.
 
 .. admonition:: Implementation Checklist
 
-    In your application, the creation of Events is completely up to you. PyOpenADR will only call your ``on_poll`` handler with a ``ven_id``. This handler must be able to either retrieve the next event for this VEN out of some storage or queue, or make up the Event in real time.
+    In your application, the creation of Events is completely up to you. OpenLEADR will only call your ``on_poll`` handler with a ``ven_id``. This handler must be able to either retrieve the next event for this VEN out of some storage or queue, or make up the Event in real time.
 
     - ``on_created_event(payload)`` handler is called whenever the VEN sends an :ref:`oadrCreatedEvent` message, probably informing you of what they intend to do with the event you gave them.
     - ``on_request_event(ven_id)``: this should return the next event (if any) that you have for the VEN. If you return ``None``, a blank :ref:`oadrResponse` will be returned to the VEN.

+ 0 - 0
pyopenadr/__init__.py → openleadr/__init__.py


+ 45 - 29
pyopenadr/client.py → openleadr/client.py

@@ -30,6 +30,7 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
 import asyncio
 from asyncio import iscoroutine
 from functools import partial
+import warnings
 
 MEASURANDS = {'power_real': 'power_quantity',
               'power_reactive': 'power_quantity',
@@ -43,7 +44,7 @@ class OpenADRClient:
     Main client class. Most of these methods will be called automatically, but
     you can always choose to call them manually.
     """
-    def __init__(self, ven_name, vtn_url, debug=False, cert=None, key=None, passphrase=None, verification_cert=None):
+    def __init__(self, ven_name, vtn_url, debug=False, cert=None, key=None, passphrase=None, vtn_fingerprint=None):
         """
         Initializes a new OpenADR Client (Virtual End Node)
 
@@ -52,7 +53,7 @@ class OpenADRClient:
         :param bool debug: Whether or not to print debugging messages
         :param str cert: The path to a PEM-formatted Certificate file to use for signing messages
         :param str key: The path to a PEM-formatted Private Key file to use for signing messages
-        :param str verification_cert: The path to a PEM-formatted Certificate file to use for verifying incoming messages.
+        :param str fingerprint: The fingerprint for the VTN's certificate to verify incomnig messages
         """
 
         self.ven_name = ven_name
@@ -71,24 +72,26 @@ class OpenADRClient:
                 cert = file.read()
             with open(key, 'rb') as file:
                 key = file.read()
+            print("*" * 80)
+            print("Your VEN Certificate Fingerprint is", certificate_fingerprint(cert))
+            print("Please deliver this fingerprint to the VTN you are connecting to.")
+            print("You do not need to keep this a secret.")
+            print("*" * 80)
 
         self._create_message = partial(create_message,
                                        cert=cert,
                                        key=key,
                                        passphrase=passphrase)
-        if verification_cert:
-            with open(verification_cert, "rb") as file:
-                verification_cert = file.read()
         self._parse_message = partial(parse_message,
-                                      cert=verification_cert)
+                                      fingerprint=vtn_fingerprint)
 
 
     async def run(self):
         """
         Run the client in full-auto mode.
         """
-        if not hasattr(self, 'on_event') or not hasattr(self, 'on_report'):
-            raise NotImplementedError("You must implement both the on_event and and_report functions or coroutines.")
+        if not hasattr(self, 'on_event'):
+            raise NotImplementedError("You must implement an on_event function or coroutine.")
 
         await self.create_party_registration()
 
@@ -210,6 +213,8 @@ class OpenADRClient:
             payload['ven_id'] = ven_id
         message = self._create_message('oadrCreatePartyRegistration', request_id=new_request_id(), **payload)
         response_type, response_payload = await self._perform_request(service, message)
+        if response_type is None:
+            return
         if response_payload['response']['response_code'] != 200:
             status_code = response_payload['response']['response_code']
             status_description = response_payload['response']['response_description']
@@ -253,7 +258,6 @@ class OpenADRClient:
                                         'opt_type': opt_type}]}
         message = self._create_message('oadrCreatedEvent', **payload)
         response_type, response_payload = await self._perform_request(service, message)
-        return response_type, response_payload
 
     async def register_report(self):
         """
@@ -317,23 +321,34 @@ class OpenADRClient:
         service = 'EiReport'
         message = self._create_message('oadrUpdateReport', report)
         response_type, response_payload = self._perform_request(service, message)
-
-        # We might get a oadrCancelReport message in this thing:
-        if 'cancel_report' in response.payload:
-            print("TODO: cancel this report")
+        if response_type is not None:
+            # We might get a oadrCancelReport message in this thing:
+            if 'cancel_report' in response_payload:
+                print("TODO: cancel this report")
 
 
     async def _perform_request(self, service, message):
         if self.debug:
             print(f"Client is sending {message}")
         url = f"{self.vtn_url}/{service}"
-        async with self.client_session.post(url, data=message) as req:
-            if req.status != HTTPStatus.OK:
-                raise Exception(f"Received non-OK status in request: {req.status}")
-            content = await req.read()
-            if self.debug:
-                print(content.decode('utf-8'))
-        return self._parse_message(content)
+        try:
+            async with self.client_session.post(url, data=message) as req:
+                if req.status != HTTPStatus.OK:
+                    warnings.warn(f"Non-OK status when performing a request to {url} with data {message}: {req.status}")
+                    return None, {}
+                content = await req.read()
+                if self.debug:
+                    print(content.decode('utf-8'))
+        except:
+            # Could not connect to server
+            warnings.warn(f"Could not connect to server with URL {self.vtn_url}")
+            return None, {}
+        try:
+            message_type, message_payload = self._parse_message(content)
+        except:
+            warnings.warn(f"The incoming message could not be parsed or validated: {content}.")
+            return None, {}
+        return message_type, message_payload
 
     async def _on_event(self, message):
         if self.debug:
@@ -349,27 +364,28 @@ class OpenADRClient:
         await self.created_event(request_id, event_id, result)
         return
 
-    async def _on_report(self, message):
-        result = self.on_report(message)
-        if iscoroutine(result):
-            result = await result
-        return result
-
     async def _poll(self):
+        print("Now polling")
         response_type, response_payload = await self.poll()
+        if response_type is None:
+            raise Exception("NO RESPONSE")
+            return
+
         if response_type == 'oadrResponse':
             print("No events or reports available")
             return
 
         if response_type == 'oadrRequestReregistration':
-            result = await self.create_party_registration()
+            await self.create_party_registration()
 
         if response_type == 'oadrDistributeEvent':
-            result = await self._on_event(response_payload)
+            await self._on_event(response_payload)
 
         elif response_type == 'oadrUpdateReport':
-            result = await self._on_report(response_payload)
+            await self._on_report(response_payload)
 
         else:
             print(f"No handler implemented for message type {response_type}, ignoring.")
+
+        # Immediately poll again, because there might be more messages
         await self._poll()

+ 0 - 0
pyopenadr/config.py → openleadr/config.py


+ 0 - 0
pyopenadr/enums.py → openleadr/enums.py


+ 0 - 0
pyopenadr/errors.py → openleadr/errors.py


+ 18 - 5
pyopenadr/messaging.py → openleadr/messaging.py

@@ -28,16 +28,14 @@ SIGNER = XMLSigner(method=methods.detached,
                    c14n_algorithm="http://www.w3.org/2001/10/xml-exc-c14n#")
 VERIFIER = XMLVerifier()
 
-def parse_message(data, cert=None):
+def parse_message(data, fingerprint=None, fingerprint_lookup=None):
     """
     Parse a message and distill its usable parts. Returns a message type and payload.
     """
     message_dict = xmltodict.parse(data, process_namespaces=True, namespaces=NAMESPACES)
     message_type, message_payload = message_dict['oadrPayload']['oadrSignedObject'].popitem()
-    if cert:
-        tree = etree.fromstring(ensure_bytes(data))
-        VERIFIER.verify(tree, x509_cert=cert, expect_references=2)
-        _verify_replay_protect(message_dict)
+    if 'ven_id' in message_payload:
+        validate_and_authenticate_message(data, message_dict, fingerprint, fingerprint_lookup)
     return message_type, normalize_dict(message_payload)
 
 def create_message(message_type, cert=None, key=None, passphrase=None, **message_payload):
@@ -64,6 +62,21 @@ def create_message(message_type, cert=None, key=None, passphrase=None, **message
                           signed_object=signed_object)
     return msg
 
+def validate_and_authenticate_message(data, message_dict, fingerprint=None, fingerprint_lookup=None):
+    if not fingerprint and not fingerprint_lookup:
+        return
+    tree = etree.fromstring(ensure_bytes(data))
+    cert = extract_pem_cert(tree)
+    ven_id = tree.find('.//{http://docs.oasis-open.org/ns/energyinterop/201110}venID').text
+    cert_fingerprint = certificate_fingerprint(cert)
+    if not fingerprint:
+        fingerprint = fingerprint_lookup(ven_id)
+
+    if fingerprint != certificate_fingerprint(cert):
+        raise ValueError("The fingerprint does not match")
+    VERIFIER.verify(tree, x509_cert=ensure_bytes(cert), expect_references=2)
+    _verify_replay_protect(message_dict)
+
 def _create_replay_protect():
     dt_element = Element("{http://openadr.org/oadr-2.0b/2012/07/xmldsig-properties}timestamp")
     dt_element.text = datetimeformat(datetime.now(timezone.utc))

+ 150 - 0
openleadr/objects.py

@@ -0,0 +1,150 @@
+# SPDX-License-Identifier: Apache-2.0
+
+# Copyright 2020 Contributors to OpenLEADR
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from dataclasses import dataclass
+from typing import List
+from datetime import datetime, timezone, timedelta
+
+class objdict(dict):
+    def __getattr__(self, name):
+        if name in self:
+            return self[name]
+        else:
+            raise AttributeError("No such attribute: " + name)
+
+    def __setattr__(self, name, value):
+        self[name] = value
+
+    def __delattr__(self, name):
+        if name in self:
+            del self[name]
+        else:
+            raise AttributeError("No such attribute: " + name)
+
+@dataclass
+class AggregatedPNode(objdict):
+    node: str
+
+@dataclass
+class EndDeviceAsset(objdict):
+    mrid: str
+
+@dataclass
+class MeterAsset(objdict):
+    mrid: str
+
+@dataclass
+class PNode(objdict):
+    node: str
+
+@dataclass
+class FeatureCollection(objdict):
+    id: str
+    location: dict
+
+@dataclass
+class ServiceArea(objdict):
+    feature_collection: FeatureCollection
+
+@dataclass
+class ServiceDeliveryPoint(objdict):
+    node: str
+
+@dataclass
+class ServiceLocation(objdict):
+    node: str
+
+@dataclass
+class TransportInterface(objdict):
+    point_of_receipt: str
+    point_of_delivery: str
+
+@dataclass
+class Target(objdict):
+    aggregated_p_node: AggregatedPNode = None
+    end_device_asset: EndDeviceAsset = None
+    meter_asset: MeterAsset = None
+    p_node: PNode = None
+    service_area: ServiceArea = None
+    service_delivery_point: ServiceDeliveryPoint = None
+    service_location: ServiceLocation = None
+    transport_interface: TransportInterface = None
+    group_id: str = None
+    group_name: str = None
+    resource_id: str = None
+    ven_id: str = None
+    party_id: str = None
+
+@dataclass
+class EventDescriptor(objdict):
+    event_id: int
+    modification_number: int
+    market_context: str
+    event_status: str
+
+    created_date_time: datetime = None
+    modification_date: datetime = None
+    priority: int = 0
+    test_event: bool = False
+    vtn_comment: str = None
+
+    def __post_init__(self):
+        if self.modification_date is None:
+            self.modification_date = datetime.now(timezone.utc)
+        if self.created_date_time is None:
+            self.created_date_time = datetime.now(timezone.utc)
+        if self.modification_number is None:
+            self.modification_number = 0
+        if not isinstance(self.test_event, bool):
+            self.test_event = False
+
+@dataclass
+class ActivePeriod(objdict):
+    dtstart: datetime
+    duration: timedelta
+    tolerance: dict = None
+    notification: dict = None
+    ramp_up: dict = None
+    recovery: dict = None
+
+@dataclass
+class Interval(objdict):
+    dtstart: datetime
+    duration: timedelta
+    signal_payload: float
+    uid: int = None
+
+@dataclass
+class EventSignal(objdict):
+    intervals: List[Interval]
+    target: Target
+    signal_name: str
+    signal_type: str
+    signal_id: str
+    current_value: float
+
+@dataclass
+class Event(objdict):
+    event_descriptor: EventDescriptor
+    active_period: ActivePeriod
+    event_signals: EventSignal
+    target: Target
+
+@dataclass
+class Response(objdict):
+    response_code: int
+    response_description: str
+    request_id: str

+ 0 - 0
pyopenadr/preflight.py → openleadr/preflight.py


+ 25 - 15
pyopenadr/server.py → openleadr/server.py

@@ -21,21 +21,31 @@ from functools import partial
 
 class OpenADRServer:
     _MAP = {'on_created_event': EventService,
-           'on_request_event': EventService,
+            'on_request_event': EventService,
 
-           'on_register_report': ReportService,
-           'on_create_report': ReportService,
-           'on_created_report': ReportService,
-           'on_request_report': ReportService,
-           'on_update_report': ReportService,
+            'on_register_report': ReportService,
+            'on_create_report': ReportService,
+            'on_created_report': ReportService,
+            'on_request_report': ReportService,
+            'on_update_report': ReportService,
 
-           'on_poll': PollService,
+            'on_poll': PollService,
 
-           'on_query_registration': RegistrationService,
-           'on_create_party_registration': RegistrationService,
-           'on_cancel_party_registration': RegistrationService}
+            'on_query_registration': RegistrationService,
+            'on_create_party_registration': RegistrationService,
+            'on_cancel_party_registration': RegistrationService}
 
-    def __init__(self, vtn_id, cert=None, key=None, passphrase=None, verification_cert=None):
+    def __init__(self, vtn_id, cert=None, key=None, passphrase=None, fingerprint_lookup=None):
+        """
+        Create a new OpenADR VTN (Server).
+
+        :param vtn_id string: An identifier string for this VTN. This is how you identify yourself to the VENs that talk to you.
+        :param cert string: Path to the PEM-formatted certificate file that is used to sign outgoing messages
+        :param key string: Path to the PEM-formatted private key file that is used to sign outgoing messages
+        :param passphrase string: The passphrase used to decrypt the private key file
+        :param fingerprint_lookup callable: A callable that receives a ven_id and should return the registered fingerprint for that VEN.
+                                            You should receive these fingerprints outside of OpenADR and configure them manually.
+        """
         self.app = web.Application()
         self.services = {'event_service': EventService(vtn_id),
                          'report_service': ReportService(vtn_id),
@@ -50,11 +60,8 @@ class OpenADRServer:
                 cert = file.read()
             with open(key, "rb") as file:
                 key = file.read()
-        if verification_cert:
-            with open(verification_cert, "rb") as file:
-                verification_cert = file.read()
         VTNService._create_message = partial(create_message, cert=cert, key=key, passphrase=passphrase)
-        VTNService._parse_message = partial(parse_message, cert=verification_cert)
+        VTNService._parse_message = partial(parse_message, fingerprint_lookup=fingerprint_lookup)
 
         self.__setattr__ = self.add_handler
 
@@ -70,6 +77,9 @@ class OpenADRServer:
     def add_handler(self, name, func):
         """
         Add a handler to the OpenADRServer.
+
+        :param name string: The name for this handler. Should be one of: on_created_event, on_request_event, on_register_report, on_create_report, on_created_report, on_request_report, on_update_report, on_poll, on_query_registration, on_create_party_registration, on_cancel_party_registration.
+        :param func coroutine: A coroutine that handles this event. It receives the message, and should return the contents of a response.
         """
         print("Called add_handler", name, func)
         if name in self._MAP:

+ 0 - 0
pyopenadr/service/__init__.py → openleadr/service/__init__.py


+ 0 - 0
pyopenadr/service/event_service.py → openleadr/service/event_service.py


+ 0 - 0
pyopenadr/service/opt_service.py → openleadr/service/opt_service.py


+ 0 - 0
pyopenadr/service/poll_service.py → openleadr/service/poll_service.py


+ 6 - 0
pyopenadr/service/registration_service.py → openleadr/service/registration_service.py

@@ -89,6 +89,12 @@ class RegistrationService(VTNService):
         result = self.on_create_party_registration(payload)
         if iscoroutine(result):
             result = await result
+
+        response_type, response_payload = result
+        response_payload['reponse'] = {'response_code': 200,
+                                       'response_description': 'OK',
+                                       'request_id': payload['request_id']}
+        response_payload['vtn_id'] = self.vtn_id
         return result
 
     @handler('oadrCancelPartyRegistration')

+ 0 - 0
pyopenadr/service/report_service.py → openleadr/service/report_service.py


+ 0 - 0
pyopenadr/service/vtn_service.py → openleadr/service/vtn_service.py


+ 0 - 0
pyopenadr/templates/oadrCancelOpt.xml → openleadr/templates/oadrCancelOpt.xml


+ 0 - 0
pyopenadr/templates/oadrCancelPartyRegistration.xml → openleadr/templates/oadrCancelPartyRegistration.xml


+ 0 - 0
pyopenadr/templates/oadrCancelReport.xml → openleadr/templates/oadrCancelReport.xml


+ 0 - 0
pyopenadr/templates/oadrCanceledOpt.xml → openleadr/templates/oadrCanceledOpt.xml


+ 0 - 0
pyopenadr/templates/oadrCanceledPartyRegistration.xml → openleadr/templates/oadrCanceledPartyRegistration.xml


+ 0 - 0
pyopenadr/templates/oadrCanceledReport.xml → openleadr/templates/oadrCanceledReport.xml


+ 0 - 0
pyopenadr/templates/oadrCreateOpt.xml → openleadr/templates/oadrCreateOpt.xml


+ 0 - 0
pyopenadr/templates/oadrCreatePartyRegistration.xml → openleadr/templates/oadrCreatePartyRegistration.xml


+ 0 - 0
pyopenadr/templates/oadrCreateReport.xml → openleadr/templates/oadrCreateReport.xml


+ 0 - 0
pyopenadr/templates/oadrCreatedEvent.xml → openleadr/templates/oadrCreatedEvent.xml


+ 0 - 0
pyopenadr/templates/oadrCreatedOpt.xml → openleadr/templates/oadrCreatedOpt.xml


+ 0 - 0
pyopenadr/templates/oadrCreatedPartyRegistration.xml → openleadr/templates/oadrCreatedPartyRegistration.xml


+ 0 - 0
pyopenadr/templates/oadrCreatedReport.xml → openleadr/templates/oadrCreatedReport.xml


+ 0 - 0
pyopenadr/templates/oadrDistributeEvent.xml → openleadr/templates/oadrDistributeEvent.xml


+ 0 - 0
pyopenadr/templates/oadrPayload.xml → openleadr/templates/oadrPayload.xml


+ 0 - 0
pyopenadr/templates/oadrPoll.xml → openleadr/templates/oadrPoll.xml


+ 0 - 0
pyopenadr/templates/oadrQueryRegistration.xml → openleadr/templates/oadrQueryRegistration.xml


+ 0 - 0
pyopenadr/templates/oadrRegisterReport.xml → openleadr/templates/oadrRegisterReport.xml


+ 0 - 0
pyopenadr/templates/oadrRegisteredReport.xml → openleadr/templates/oadrRegisteredReport.xml


+ 0 - 0
pyopenadr/templates/oadrRequestEvent.xml → openleadr/templates/oadrRequestEvent.xml


+ 0 - 0
pyopenadr/templates/oadrRequestReregistration.xml → openleadr/templates/oadrRequestReregistration.xml


+ 0 - 0
pyopenadr/templates/oadrResponse.xml → openleadr/templates/oadrResponse.xml


+ 0 - 0
pyopenadr/templates/oadrUpdateReport.xml → openleadr/templates/oadrUpdateReport.xml


+ 0 - 0
pyopenadr/templates/oadrUpdatedReport.xml → openleadr/templates/oadrUpdatedReport.xml


+ 0 - 0
pyopenadr/templates/parts/eiActivePeriod.xml → openleadr/templates/parts/eiActivePeriod.xml


+ 0 - 0
pyopenadr/templates/parts/eiEvent.xml → openleadr/templates/parts/eiEvent.xml


+ 4 - 0
pyopenadr/templates/parts/eiEventDescriptor.xml → openleadr/templates/parts/eiEventDescriptor.xml

@@ -1,8 +1,12 @@
 <ei:eventDescriptor>
     <ei:eventID>{{ event.event_descriptor.event_id }}</ei:eventID>
     <ei:modificationNumber>{{ event.event_descriptor.modification_number }}</ei:modificationNumber>
+    {% if event.event_descriptor.modification_date_time is not none %}
     <ei:modificationDateTime>{{ event.event_descriptor.modification_date_time|datetimeformat }}</ei:modificationDateTime>
+    {% endif %}
+    {%if event.event_descriptor.modification_date_time is not none %}
     <ei:priority>{{ event.event_descriptor.priority }}</ei:priority>
+    {% endif %}
     <ei:eiMarketContext>
         <marketContext xmlns="http://docs.oasis-open.org/ns/emix/2011/06">{{ event.event_descriptor.market_context }}</marketContext>
     </ei:eiMarketContext>

+ 0 - 0
pyopenadr/templates/parts/eiEventSignal.xml → openleadr/templates/parts/eiEventSignal.xml


+ 0 - 0
pyopenadr/templates/parts/eiEventTarget.xml → openleadr/templates/parts/eiEventTarget.xml


+ 0 - 0
pyopenadr/templates/parts/eiTarget.xml → openleadr/templates/parts/eiTarget.xml


+ 0 - 0
pyopenadr/templates/parts/emixInterface.xml → openleadr/templates/parts/emixInterface.xml


+ 0 - 0
pyopenadr/templates/parts/oadrReportDescription.xml → openleadr/templates/parts/oadrReportDescription.xml


+ 0 - 0
pyopenadr/templates/parts/oadrReportRequest.xml → openleadr/templates/parts/oadrReportRequest.xml


+ 0 - 0
pyopenadr/templates/parts/reportDescriptionEmix.xml → openleadr/templates/parts/reportDescriptionEmix.xml


+ 20 - 1
pyopenadr/utils.py → openleadr/utils.py

@@ -21,6 +21,8 @@ import string
 from collections import OrderedDict
 import itertools
 import re
+import ssl
+import hashlib
 
 from openleadr import config
 
@@ -345,4 +347,21 @@ def ensure_bytes(obj):
     if isinstance(obj, str):
         return bytes(obj, 'utf-8')
     else:
-        raise TypeError("Must be bytes or str")
+        raise TypeError("Must be bytes or str")
+
+def ensure_str(obj):
+    if isinstance(obj, str):
+        return obj
+    if isinstance(obj, bytes):
+        return obj.decode('utf-8')
+    else:
+        raise TypeError("Must be bytes or str")
+
+def certificate_fingerprint(certificate_str):
+    der_cert = ssl.PEM_cert_to_DER_cert(ensure_str(certificate_str))
+    hash = hashlib.sha256(der_cert).digest().hex()
+    return ":".join([hash[i-2:i].upper() for i in range(-20, 0, 2)])
+
+def extract_pem_cert(tree):
+    cert = tree.find('.//{http://www.w3.org/2000/09/xmldsig#}X509Certificate').text
+    return "-----BEGIN CERTIFICATE-----\n" + cert + "-----END CERTIFICATE-----\n"

+ 1 - 1
setup.py

@@ -17,7 +17,7 @@
 from setuptools import setup
 import os
 
-with open("README.md", "r") as fh:
+with open('README.md', 'r') as fh:
     long_description = fh.read()
 
 with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'VERSION')) as file:

+ 16 - 0
test/__init__.py

@@ -0,0 +1,16 @@
+# SPDX-License-Identifier: Apache-2.0
+
+# Copyright 2020 Contributors to OpenLEADR
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+

+ 9 - 2
test/integration_tests/test_client_registration.py

@@ -61,7 +61,7 @@ async def start_server():
 
 @pytest.fixture
 async def start_server_with_signatures():
-    server = OpenADRServer(vtn_id=VTN_ID, cert=CERTFILE, key=KEYFILE, passphrase='openadr', verification_cert=CERTFILE)
+    server = OpenADRServer(vtn_id=VTN_ID, cert=CERTFILE, key=KEYFILE, passphrase='openadr', fingerprint_lookup=fingerprint_lookup)
     server.add_handler('on_create_party_registration', _on_create_party_registration)
 
     runner = web.AppRunner(server.app)
@@ -91,11 +91,18 @@ async def test_create_party_registration(start_server):
     assert response_payload['ven_id'] == VEN_ID
 
 
+def fingerprint_lookup(ven_id):
+    with open(CERTFILE) as file:
+        cert = file.read()
+    return certificate_fingerprint(cert)
+
 @pytest.mark.asyncio
 async def test_create_party_registration_with_signatures(start_server_with_signatures):
+    with open(CERTFILE) as file:
+        cert = file.read()
     client = OpenADRClient(ven_name=VEN_NAME,
                            vtn_url=f"http://localhost:{SERVER_PORT}/OpenADR2/Simple/2.0b",
-                           cert=CERTFILE, key=KEYFILE, passphrase='openadr', verification_cert=CERTFILE)
+                           cert=CERTFILE, key=KEYFILE, passphrase='openadr', vtn_fingerprint=certificate_fingerprint(cert))
 
     response_type, response_payload = await client.create_party_registration()
     assert response_type == 'oadrCreatedPartyRegistration'

+ 82 - 0
test/test_failures.py

@@ -0,0 +1,82 @@
+from openleadr import OpenADRClient, OpenADRServer
+from openleadr.utils import generate_id
+import pytest
+from aiohttp import web
+import os
+import asyncio
+from datetime import timedelta
+
+@pytest.mark.asyncio
+async def test_http_level_error(start_server):
+    client = OpenADRClient(vtn_url="http://this.is.an.error", ven_name=VEN_NAME)
+    client.on_event = _client_on_event
+    await client.run()
+
+@pytest.mark.asyncio
+async def test_openadr_error(start_server):
+    client = OpenADRClient(vtn_url=f"http://localhost:{SERVER_PORT}/OpenADR2/Simple/2.0b", ven_name=VEN_NAME)
+    client.on_event = _client_on_event
+    await client.run()
+
+
+@pytest.mark.asyncio
+async def test_signature_error(start_server_with_signatures):
+    client = OpenADRClient(vtn_url=f"http://localhost:{SERVER_PORT}/OpenADR2/Simple/2.0b", ven_name=VEN_NAME,
+                           vtn_fingerprint="INVALID")
+    client.on_event = _client_on_event
+    await client.run()
+    await asyncio.sleep(3)
+
+##########################################################################################
+
+SERVER_PORT = 8001
+VEN_NAME = 'myven'
+VEN_ID = '1234abcd'
+VTN_ID = "TestVTN"
+
+CERTFILE = os.path.join(os.path.dirname(__file__), "cert.pem")
+KEYFILE =  os.path.join(os.path.dirname(__file__), "key.pem")
+
+
+async def _on_create_party_registration(payload):
+    registration_id = generate_id()
+    payload = {'response': {'response_code': 200,
+                            'response_description': 'OK',
+                            'request_id': payload['request_id']},
+               'ven_id': VEN_ID,
+               'registration_id': registration_id,
+               'profiles': [{'profile_name': '2.0b',
+                             'transports': {'transport_name': 'simpleHttp'}}],
+               'requested_oadr_poll_freq': timedelta(seconds=1)}
+    return 'oadrCreatedPartyRegistration', payload
+
+async def _client_on_event(event):
+    pass
+
+async def _client_on_report(report):
+    pass
+
+@pytest.fixture
+async def start_server():
+    server = OpenADRServer(vtn_id=VTN_ID)
+    server.add_handler('on_create_party_registration', _on_create_party_registration)
+
+    runner = web.AppRunner(server.app)
+    await runner.setup()
+    site = web.TCPSite(runner, 'localhost', SERVER_PORT)
+    await site.start()
+    yield
+    await runner.cleanup()
+
+@pytest.fixture
+async def start_server_with_signatures():
+    server = OpenADRServer(vtn_id=VTN_ID, cert=CERTFILE, key=KEYFILE, passphrase='openadr')
+    server.add_handler('on_create_party_registration', _on_create_party_registration)
+
+    runner = web.AppRunner(server.app)
+    await runner.setup()
+    site = web.TCPSite(runner, 'localhost', SERVER_PORT)
+    await site.start()
+    yield
+    await runner.cleanup()
+

+ 50 - 0
test/test_objects.py

@@ -0,0 +1,50 @@
+from openleadr import objects, enums
+from datetime import datetime, timedelta
+from openleadr.messaging import create_message, parse_message
+from pprint import pprint
+
+def test_oadr_event():
+    event = objects.Event(
+        event_descriptor=objects.EventDescriptor(
+            event_id=1,
+            modification_number=0,
+            market_context='MarketContext1',
+            event_status=enums.EVENT_STATUS.NEAR),
+        active_period=objects.ActivePeriod(
+            dtstart=datetime.now(),
+            duration=timedelta(minutes=10)),
+        event_signals=[objects.EventSignal(
+            intervals=[
+                objects.Interval(
+                    dtstart=datetime.now(),
+                    duration=timedelta(minutes=5),
+                    uid=0,
+                    signal_payload=1),
+                objects.Interval(
+                    dtstart=datetime.now(),
+                    duration=timedelta(minutes=5),
+                    uid=1,
+                    signal_payload=2)],
+            target=objects.Target(
+                ven_id='1234'
+            ),
+            signal_name=enums.SIGNAL_NAME.LOAD_CONTROL,
+            signal_type=enums.SIGNAL_TYPE.LEVEL,
+            signal_id=1,
+            current_value=0
+        )],
+        target=objects.Target(
+            ven_id='1234'
+        )
+    )
+
+    response = objects.Response(response_code=200,
+                                response_description='OK',
+                                request_id='1234')
+    pprint(event)
+    msg = create_message('oadrDistributeEvent', response=response, events=[event])
+    pprint(msg)
+    message_type, message_payload = parse_message(msg)
+
+
+

+ 2 - 8
test/test_signatures.py

@@ -30,7 +30,7 @@ TEST_KEY_PASSWORD = 'openadr'
 
 def test_message_validation():
     msg = create_message('oadrPoll', ven_id='123', cert=TEST_CERT, key=TEST_KEY, passphrase='openadr')
-    parsed_type, parsed_message = parse_message(msg, cert=TEST_CERT)
+    parsed_type, parsed_message = parse_message(msg, fingerprint=certificate_fingerprint(TEST_CERT))
     assert parsed_type == 'oadrPoll'
 
 
@@ -80,10 +80,4 @@ def test_message_validation_complex():
                          cert=TEST_CERT,
                          key=TEST_KEY,
                          passphrase='openadr')
-    parsed_type, parsed_msg = parse_message(msg, cert=TEST_CERT)
-
-if __name__ == "__main__":
-    msg = create_message('oadrPoll', ven_id='123', signing_certificate=TEST_CERT, signing_key=TEST_KEY, signing_key_passphrase=b'openadr')
-    parsed_type, parsed_message = parse_message(msg)
-    validate_message(msg, public_key=TEST_CERT)
-    print(msg)
+    parsed_type, parsed_msg = parse_message(msg, fingerprint=certificate_fingerprint(TEST_CERT))