Skip to content

Commit 7cf8740

Browse files
committed
Change capabilities sedes according to #6503
This changes the (de-)serialization of capabilities that are broadcast via synapse `avatar_url` to be url query parameters. The new capabilities serialization format in detail: Capabilities are serialized to `avatar_url` as mxc://raiden.network/cap?{capabilities_url_encoded} The spec for `{capabilities_url_encoded}` is as follows: - `bool` values are encoded as `'1' == True` and `'0' == False` - other values are encoded as strings - the strings `'0'` and `'1'` are deserialized as `bool` (see above). - other strings are kept as they are - multiple entries / list values are allowed - final interpretation of any value is up to the client Changes in detail: - hand rolled serialization is removed - truncation of falsy entries is dropped / all entries are explicit - unknown capabilities will be preserved - `parse_capabilities` is renamed to `deserialize_capabilities` - deserialization errors are treated as empty capabilities - adjusted unit tests
1 parent 00a68e7 commit 7cf8740

File tree

5 files changed

+65
-51
lines changed

5 files changed

+65
-51
lines changed

raiden/network/transport/matrix/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
)
5757
from raiden.network.utils import get_average_http_response_time
5858
from raiden.storage.serialization.serializer import MessageSerializer
59-
from raiden.utils.capabilities import parse_capabilities, serialize_capabilities
59+
from raiden.utils.capabilities import deserialize_capabilities, serialize_capabilities
6060
from raiden.utils.gevent import spawn_named
6161
from raiden.utils.signer import Signer, recover
6262
from raiden.utils.typing import Address, ChainID, MessageID, PeerCapabilities, Signature
@@ -346,7 +346,7 @@ def query_capabilities_for_user_id(self, user_id: str) -> PeerCapabilities:
346346
return PeerCapabilities({})
347347
avatar_url = user.get_avatar_url()
348348
if avatar_url is not None:
349-
return PeerCapabilities(parse_capabilities(avatar_url))
349+
return PeerCapabilities(deserialize_capabilities(avatar_url))
350350
else:
351351
return PeerCapabilities({})
352352

raiden/tests/integration/fixtures/raiden_network.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,8 @@ def adhoc_capability():
198198
@pytest.fixture
199199
def capabilities(adhoc_capability) -> CapabilitiesConfig:
200200
config = CapabilitiesConfig()
201-
config.adhoc_capability = adhoc_capability # type: ignore
201+
if adhoc_capability:
202+
config.adhoc_capability = adhoc_capability # type: ignore
202203
return config
203204

204205

raiden/tests/integration/network/transport/test_matrix_transport.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
from raiden.transfer.identifiers import CANONICAL_IDENTIFIER_UNORDERED_QUEUE, QueueIdentifier
5757
from raiden.transfer.state import NetworkState
5858
from raiden.transfer.state_change import ActionChannelClose
59-
from raiden.utils.capabilities import capconfig_to_dict, parse_capabilities
59+
from raiden.utils.capabilities import capconfig_to_dict, deserialize_capabilities
6060
from raiden.utils.formatting import to_checksum_address
6161
from raiden.utils.typing import Address, Dict, List, PeerCapabilities, TokenNetworkAddress, cast
6262
from raiden.waiting import wait_for_network_state
@@ -1343,18 +1343,15 @@ def test_transport_capabilities(raiden_network: List[RaidenService], capabilitie
13431343
wait_for_network_state(app0, app1.address, NetworkState.REACHABLE, retry_timeout)
13441344
wait_for_network_state(app1, app0.address, NetworkState.REACHABLE, retry_timeout)
13451345

1346-
# only True values are set in the avatar_url (opt_in)
1347-
expected_capabilities = {
1348-
key: value for key, value in capconfig_to_dict(capabilities).items() if value
1349-
}
1346+
expected_capabilities = capconfig_to_dict(capabilities)
13501347

13511348
app1_user_ids = app0.transport.get_user_ids_for_address(app1.address)
13521349
assert len(app1_user_ids) == 1, "app1 should have exactly one user_id"
13531350
app1_user = app0.transport._client.get_user(app1_user_ids.pop())
13541351
app1_avatar_url = app1_user.get_avatar_url()
1355-
assert "adhoc_capability" in app1_avatar_url, "avatar_url not set for app1"
1356-
msg = "capabilities could not be parsed"
1357-
assert parse_capabilities(app1_avatar_url) == expected_capabilities, msg
1352+
assert len(app1_avatar_url), "avatar_url not set for app1"
1353+
app1_capabilities = deserialize_capabilities(app1_avatar_url)
1354+
assert "adhoc_capability" in app1_capabilities, "capabilities could not be parsed correctly"
13581355

13591356
msg = "capabilities were not collected in transport client"
13601357
collected_capabilities = app0.transport._address_mgr.get_address_capabilities(app1.address)

raiden/tests/unit/test_utils.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from raiden.utils.capabilities import (
1212
capconfig_to_dict,
1313
capdict_to_config,
14-
parse_capabilities,
14+
deserialize_capabilities,
1515
serialize_capabilities,
1616
)
1717
from raiden.utils.keys import privatekey_to_publickey
@@ -97,26 +97,25 @@ def test_get_http_rtt_ignore_failing(requests_responses):
9797
assert get_average_http_response_time(url="http://url3", method="get") is None
9898

9999

100-
def test_parse_capabilities():
101-
capstring = 'foo,toad,bar="max",form,agar'
102-
parsed = parse_capabilities(capstring)
100+
def test_deserialize_capabilities():
101+
capstring = "mxc://raiden.network/cap?foo=1&toad=1&bar=max&form=1&agar=1&nottrue=0&l=one&l=2"
102+
parsed = deserialize_capabilities(capstring)
103103
assert parsed.get("foo") is True
104104
assert parsed.get("toad") is True
105105
assert parsed.get("bar") == "max"
106-
assert parsed.get("form") is True
107106
assert parsed.get("agar") is True
107+
assert parsed.get("nottrue") is False
108+
assert parsed.get("l") == ["one", "2"]
108109
assert not parsed.get("nothing")
109110

110-
assert serialize_capabilities(parsed) == f"mxc://{capstring}"
111+
assert serialize_capabilities(parsed) == f"{capstring}"
111112

112113
parsed["false"] = False
113114

114-
assert serialize_capabilities(parsed) == f"mxc://{capstring}"
115+
# Explicit new value changes the serialization format
116+
assert serialize_capabilities(parsed) != f"mxc://{capstring}"
115117

116-
parsed["nothing"] = None
117-
assert 'nothing="None"' in serialize_capabilities(parsed)
118-
119-
assert parse_capabilities("") == dict()
118+
assert deserialize_capabilities("") == dict()
120119

121120
assert serialize_capabilities({}) == "mxc://"
122121

raiden/utils/capabilities.py

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,61 @@
1-
from typing import Any, Dict, Optional
1+
from typing import Any, Dict, Optional, Union
2+
3+
import structlog
4+
from werkzeug.urls import url_decode, url_encode
25

36
from raiden.constants import Capabilities
47
from raiden.settings import CapabilitiesConfig
58

9+
log = structlog.get_logger(__name__)
10+
11+
12+
def _bool_to_binary(value: Any) -> str:
13+
if isinstance(value, bool):
14+
return "1" if value is True else "0"
15+
return value
16+
17+
18+
def _serialize(capdict: Optional[Dict[str, Any]]) -> str:
19+
if capdict is None:
20+
capdict = {}
21+
for key in capdict:
22+
capdict[key] = _bool_to_binary(capdict[key])
23+
return url_encode(capdict)
24+
25+
26+
def serialize_capabilities(capdict: Optional[Dict[str, Any]]) -> str:
27+
if not capdict:
28+
return "mxc://"
29+
return f"mxc://raiden.network/cap?{_serialize(capdict)}"
30+
631

7-
def parse_capabilities(capstring: str) -> Dict[str, Any]:
32+
def deserialize_capabilities(capstring: str) -> Dict[str, Any]:
833
if capstring.startswith("mxc://"):
934
capstring = capstring[6:]
10-
elif "/" in capstring:
35+
if "/" in capstring:
1136
capstring = capstring[capstring.rindex("/") + 1 :]
12-
result: Dict[str, Any] = {}
13-
if len(capstring) == 0:
14-
return result
15-
for token in capstring.split(","):
16-
if "=" in token:
17-
key, value = token.split("=")
18-
value = value.strip('"')
19-
result[key] = value
37+
if "?" in capstring:
38+
capstring = capstring[capstring.rindex("?") + 1 :]
39+
capdict = url_decode(capstring.encode("utf-8"))
40+
capabilities: Dict[str, Any] = dict()
41+
for key in capdict:
42+
value = capdict.getlist(key, type=int_bool)
43+
# reduce lists with one entry to just their element
44+
if len(value) == 1:
45+
capabilities[key] = value.pop()
2046
else:
21-
result[token] = True
22-
return result
47+
capabilities[key] = value
48+
return capabilities
2349

2450

25-
def serialize_capabilities(capdict: Optional[Dict[str, Any]]) -> str:
26-
if not capdict:
27-
return "mxc://"
28-
for key in capdict.keys():
29-
if "/" in str(key):
30-
raise ValueError(f"Key {key} is malformed, '/' not allowed")
31-
32-
entries = []
33-
for key, value in capdict.items():
34-
if isinstance(value, bool):
35-
if value:
36-
entries.append(key)
51+
def int_bool(value: str) -> Union[bool, str]:
52+
try:
53+
if int(value) in {0, 1}:
54+
return bool(int(value))
3755
else:
38-
entries.append(f'{key}="{value}"')
39-
if len(entries):
40-
return f"mxc://{','.join(entries)}"
41-
return "mxc://"
56+
return value
57+
except ValueError:
58+
return value
4259

4360

4461
def capdict_to_config(capdict: Dict[str, Any]) -> CapabilitiesConfig:

0 commit comments

Comments
 (0)