Skip to content

Commit b53cdf3

Browse files
authored
Merge pull request #20 from Indicio-tech/feat/forward-pack
Feature: forward pack message
2 parents 57a42f0 + 50328b1 commit b53cdf3

5 files changed

Lines changed: 146 additions & 26 deletions

File tree

didcomm_messaging/__init__.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""DIDComm Messaging."""
22
from dataclasses import dataclass
33
import json
4-
from typing import Generic, Optional
4+
from typing import Generic, Optional, List
55

66
from pydid.service import DIDCommV2Service
77

@@ -16,7 +16,23 @@ class PackResult:
1616
"""Result of packing a message."""
1717

1818
message: bytes
19-
target: str
19+
target_services: List[DIDCommV2Service]
20+
21+
def get_endpoint(self, protocol: str) -> str:
22+
"""Get the first matching endpoint to send the message to."""
23+
return self.get_service(protocol).service_endpoint.uri
24+
25+
def get_service(self, protocol: str) -> DIDCommV2Service:
26+
"""Get the first matching service to send the message to."""
27+
return self.filter_services_by_protocol(protocol)[0]
28+
29+
def filter_services_by_protocol(self, protocol: str) -> List[DIDCommV2Service]:
30+
"""Get all services that start with a specific uri protocol."""
31+
return [
32+
service
33+
for service in self.target_services
34+
if service.service_endpoint.uri.startswith(protocol)
35+
]
2036

2137

2238
@dataclass
@@ -68,8 +84,8 @@ async def pack(self, message: dict, to: str, frm: Optional[str] = None, **option
6884
json.dumps(message).encode(), [to], frm, **options
6985
)
7086

71-
forward, service = await self.routing.prepare_forward(to, encoded_message)
72-
return PackResult(forward, self.service_to_target(service))
87+
forward, services = await self.routing.prepare_forward(to, encoded_message)
88+
return PackResult(forward, services)
7389

7490
async def unpack(self, encoded_message: bytes, **options) -> UnpackResult:
7591
"""Unpack a message."""

didcomm_messaging/resolver/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ class DIDResolver(ABC):
2424
async def resolve(self, did: str) -> dict:
2525
"""Resolve a DID."""
2626

27+
@abstractmethod
28+
async def is_resolvable(self, did: str) -> bool:
29+
"""Check to see if a DID is resolvable."""
30+
2731
async def resolve_and_parse(self, did: str) -> DIDDocument:
2832
"""Resolve a DID and parse the DID document."""
2933
doc = await self.resolve(did)
@@ -56,6 +60,13 @@ def __init__(self, resolvers: Dict[str, DIDResolver]):
5660
"""Initialize the resolver."""
5761
self.resolvers = resolvers
5862

63+
async def is_resolvable(self, did: str) -> bool:
64+
"""Check to see if a DID is resolvable."""
65+
for prefix, resolver in self.resolvers.items():
66+
if did.startswith(prefix):
67+
return await resolver.is_resolvable(did)
68+
return False
69+
5970
async def resolve(self, did: str) -> dict:
6071
"""Resolve a DID."""
6172
for prefix, resolver in self.resolvers.items():

didcomm_messaging/resolver/peer.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
try:
66
from did_peer_2 import resolve as resolve_peer_2
7+
from did_peer_2 import PATTERN as peer_2_pattern
78
from did_peer_4 import resolve as resolve_peer_4
9+
from did_peer_4 import LONG_PATTERN as peer_4_pattern_long
10+
from did_peer_4 import SHORT_PATTERN as peer_4_pattern_short
811
except ImportError:
912
raise ImportError(
1013
"did-peer-2 and did-peer-4 are required for did:peer resolution; "
@@ -15,6 +18,10 @@
1518
class Peer2(DIDResolver):
1619
"""did:peer:2 resolver."""
1720

21+
async def is_resolvable(self, did: str) -> bool:
22+
"""Check to see if a DID is resolvable."""
23+
return peer_2_pattern.match(did)
24+
1825
async def resolve(self, did: str) -> dict:
1926
"""Resolve a did:peer:2 DID."""
2027
return resolve_peer_2(did)
@@ -23,6 +30,10 @@ async def resolve(self, did: str) -> dict:
2330
class Peer4(DIDResolver):
2431
"""did:peer:4 resolver."""
2532

33+
async def is_resolvable(self, did: str) -> bool:
34+
"""Check to see if a DID is resolvable."""
35+
return peer_4_pattern_short.match(did) or peer_4_pattern_long.match(did)
36+
2637
async def resolve(self, did: str) -> dict:
2738
"""Resolve a did:peer:4 DID."""
2839
return resolve_peer_4(did)

didcomm_messaging/routing.py

Lines changed: 101 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
"""RoutingService interface."""
22

3-
from typing import Tuple
3+
import json
4+
import uuid
5+
6+
from typing import Tuple, List, Dict, Any
47
from pydid.service import DIDCommV2Service
58
from didcomm_messaging.packaging import PackagingService
69
from didcomm_messaging.resolver import DIDResolver
@@ -18,24 +21,45 @@ def __init__(self, packaging: PackagingService, resolver: DIDResolver):
1821
self.packaging = packaging
1922
self.resolver = resolver
2023

21-
async def _resolve_service(self, to: str) -> DIDCommV2Service:
22-
"""Resolve the service endpoint for a given DID."""
23-
doc = await self.resolver.resolve_and_parse(to)
24-
if not doc.service:
25-
raise RoutingServiceError(f"No service endpoint found for {to}")
26-
27-
first_didcomm_service = next(
28-
(
29-
service
30-
for service in doc.service
31-
if isinstance(service, DIDCommV2Service)
32-
),
33-
None,
34-
)
35-
if not first_didcomm_service:
36-
raise RoutingServiceError(f"No DIDCommV2 service endpoint found for {to}")
24+
async def _resolve_services(self, to: str) -> List[DIDCommV2Service]:
25+
if not await self.resolver.is_resolvable(to):
26+
return []
27+
did_doc = await self.resolver.resolve_and_parse(to)
28+
services = []
29+
if did_doc.service: # service is not guaranteed to exist
30+
for did_service in did_doc.service:
31+
if "didcomm/v2" in did_service.service_endpoint.accept:
32+
services.append(did_service)
33+
if not services:
34+
return []
35+
return services
36+
37+
async def is_forwardable_service(self, service: DIDCommV2Service) -> bool:
38+
"""Determine if the uri of a service is a service we should forward to."""
39+
endpoint = service.service_endpoint.uri
40+
found_forwardable_service = await self.resolver.is_resolvable(endpoint)
41+
return found_forwardable_service
3742

38-
return first_didcomm_service
43+
def _create_forward_message(
44+
self, to: str, next_target: str, message: str
45+
) -> Dict[Any, Any]:
46+
return {
47+
"typ": "application/didcomm-plain+json",
48+
"type": "https://didcomm.org/routing/2.0/forward",
49+
"id": str(uuid.uuid4()),
50+
"to": [to],
51+
# "expires_time": 123456, # time to expire the forward message, in epoch time
52+
"body": {"next": next_target},
53+
"attachments": [
54+
{
55+
"id": str(uuid.uuid4()),
56+
"media_type": "application/didcomm-encrypted+json",
57+
"data": {
58+
"json": json.loads(message),
59+
},
60+
},
61+
],
62+
}
3963

4064
async def prepare_forward(
4165
self, to: str, encoded_message: bytes
@@ -47,8 +71,63 @@ async def prepare_forward(
4771
encoded_message (bytes): The encoded message.
4872
4973
Returns:
50-
The encoded message, and the service endpoint to forward to.
74+
The encoded message, and the services to forward to.
5175
"""
52-
service = await self._resolve_service(to)
53-
# TODO Do the stuff
54-
return encoded_message, service
76+
77+
# Get the initial service
78+
services = await self._resolve_services(to)
79+
chain = [
80+
{
81+
"did": to,
82+
"service": services,
83+
}
84+
]
85+
86+
# Loop through service DIDs until we run out of DIDs to forward to
87+
to_did = services[0].service_endpoint.uri
88+
found_forwardable_service = await self.is_forwardable_service(services[0])
89+
while found_forwardable_service:
90+
services = await self._resolve_services(to_did)
91+
if services:
92+
chain.append(
93+
{
94+
"did": to_did,
95+
"service": services,
96+
}
97+
)
98+
to_did = services[0].service_endpoint.uri
99+
found_forwardable_service = (
100+
await self.is_forwardable_service(services[0]) if services else False
101+
)
102+
103+
if not chain[-1]["service"]:
104+
raise RoutingServiceError(f"No DIDCommV2 service endpoint found for {to}")
105+
106+
# Grab our target to pack the initial message to, then pack the message
107+
# for the DID target
108+
next_target = chain.pop(0)["did"]
109+
packed_message = encoded_message
110+
111+
# Loop through the entire services chain and pack the message for each
112+
# layer of mediators
113+
for service in chain:
114+
# https://identity.foundation/didcomm-messaging/spec/#sender-process-to-enable-forwarding
115+
# Respect routing keys by adding the current DID to the front of
116+
# the list, then wrapping message following routing key order
117+
routing_keys = service["service"][0].service_endpoint.routing_keys
118+
routing_keys.insert(0, service["did"]) # prepend did
119+
120+
# Pack for each key
121+
while routing_keys:
122+
key = routing_keys.pop() # pop from end of list (reverse order)
123+
packed_message = await self.packaging.pack(
124+
json.dumps(
125+
self._create_forward_message(key, next_target, packed_message)
126+
),
127+
[key],
128+
)
129+
next_target = key
130+
131+
# Return the forward-packed message as well as the last service in the
132+
# chain, which is the destination of the top-level forward message.
133+
return (packed_message, chain[-1]["service"])

tests/test_didresolver.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99

1010
class TestResolver(DIDResolver):
11+
async def is_resolvable(self, did: str) -> bool:
12+
return True
13+
1114
async def resolve(self, did: str) -> dict:
1215
return {"did": did}
1316

0 commit comments

Comments
 (0)