Skip to content

Change Capabilities format #6572

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions raiden/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ class Networks(Enum):
class Capabilities(Enum):
"""Capabilities allow for protocol handshake between nodes."""

NO_RECEIVE = "noReceive" # won't proceed with protocol for incoming transfers
NO_MEDIATE = "noMediate" # can't mediate transfers; mediating requires receiving
NO_DELIVERY = "noDelivery" # don't need Delivery messages
WEBRTC = "webRTC"
RECEIVE = "Receive" # handle receiving transfers
MEDIATE = "Mediate" # support for mediating transfers; mediating requires receiving
DELIVERY = "Delivery" # expects and sends Delivery messages
WEBRTC = "webRTC" # supports webRTC messaging


class ServerListType(Enum):
Expand Down
4 changes: 2 additions & 2 deletions raiden/network/transport/matrix/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
)
from raiden.network.utils import get_average_http_response_time
from raiden.storage.serialization.serializer import MessageSerializer
from raiden.utils.capabilities import parse_capabilities, serialize_capabilities
from raiden.utils.capabilities import deserialize_capabilities, serialize_capabilities
from raiden.utils.gevent import spawn_named
from raiden.utils.signer import Signer, recover
from raiden.utils.typing import Address, ChainID, MessageID, PeerCapabilities, Signature
Expand Down Expand Up @@ -346,7 +346,7 @@ def query_capabilities_for_user_id(self, user_id: str) -> PeerCapabilities:
return PeerCapabilities({})
avatar_url = user.get_avatar_url()
if avatar_url is not None:
return PeerCapabilities(parse_capabilities(avatar_url))
return PeerCapabilities(deserialize_capabilities(avatar_url))
else:
return PeerCapabilities({})

Expand Down
6 changes: 3 additions & 3 deletions raiden/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ def get_proportional_imbalance_fee(self, token_address: TokenAddress) -> Proport

@dataclass
class CapabilitiesConfig:
no_receive: bool = False
no_mediate: bool = False
no_delivery: bool = False
receive: bool = True
mediate: bool = True
delivery: bool = True
web_rtc: bool = True


Expand Down
3 changes: 2 additions & 1 deletion raiden/tests/integration/fixtures/raiden_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@ def adhoc_capability():
@pytest.fixture
def capabilities(adhoc_capability) -> CapabilitiesConfig:
config = CapabilitiesConfig()
config.adhoc_capability = adhoc_capability # type: ignore
if adhoc_capability:
config.adhoc_capability = adhoc_capability # type: ignore
return config


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
from raiden.transfer.identifiers import CANONICAL_IDENTIFIER_UNORDERED_QUEUE, QueueIdentifier
from raiden.transfer.state import NetworkState
from raiden.transfer.state_change import ActionChannelClose
from raiden.utils.capabilities import capconfig_to_dict, parse_capabilities
from raiden.utils.capabilities import capconfig_to_dict, deserialize_capabilities
from raiden.utils.formatting import to_checksum_address
from raiden.utils.typing import Address, Dict, List, PeerCapabilities, TokenNetworkAddress, cast
from raiden.waiting import wait_for_network_state
Expand Down Expand Up @@ -1343,18 +1343,15 @@ def test_transport_capabilities(raiden_network: List[RaidenService], capabilitie
wait_for_network_state(app0, app1.address, NetworkState.REACHABLE, retry_timeout)
wait_for_network_state(app1, app0.address, NetworkState.REACHABLE, retry_timeout)

# only True values are set in the avatar_url (opt_in)
expected_capabilities = {
key: value for key, value in capconfig_to_dict(capabilities).items() if value
}
expected_capabilities = capconfig_to_dict(capabilities)

app1_user_ids = app0.transport.get_user_ids_for_address(app1.address)
assert len(app1_user_ids) == 1, "app1 should have exactly one user_id"
app1_user = app0.transport._client.get_user(app1_user_ids.pop())
app1_avatar_url = app1_user.get_avatar_url()
assert "adhoc_capability" in app1_avatar_url, "avatar_url not set for app1"
msg = "capabilities could not be parsed"
assert parse_capabilities(app1_avatar_url) == expected_capabilities, msg
assert len(app1_avatar_url), "avatar_url not set for app1"
app1_capabilities = deserialize_capabilities(app1_avatar_url)
assert "adhoc_capability" in app1_capabilities, "capabilities could not be parsed correctly"

msg = "capabilities were not collected in transport client"
collected_capabilities = app0.transport._address_mgr.get_address_capabilities(app1.address)
Expand Down
21 changes: 10 additions & 11 deletions raiden/tests/unit/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from raiden.utils.capabilities import (
capconfig_to_dict,
capdict_to_config,
parse_capabilities,
deserialize_capabilities,
serialize_capabilities,
)
from raiden.utils.keys import privatekey_to_publickey
Expand Down Expand Up @@ -97,26 +97,25 @@ def test_get_http_rtt_ignore_failing(requests_responses):
assert get_average_http_response_time(url="http://url3", method="get") is None


def test_parse_capabilities():
capstring = 'foo,toad,bar="max",form,agar'
parsed = parse_capabilities(capstring)
def test_deserialize_capabilities():
capstring = "mxc://raiden.network/cap?foo=1&toad=1&bar=max&form=1&agar=1&nottrue=0&l=one&l=2"
parsed = deserialize_capabilities(capstring)
assert parsed.get("foo") is True
assert parsed.get("toad") is True
assert parsed.get("bar") == "max"
assert parsed.get("form") is True
assert parsed.get("agar") is True
assert parsed.get("nottrue") is False
assert parsed.get("l") == ["one", "2"]
assert not parsed.get("nothing")

assert serialize_capabilities(parsed) == f"mxc://{capstring}"
assert serialize_capabilities(parsed) == f"{capstring}"

parsed["false"] = False

assert serialize_capabilities(parsed) == f"mxc://{capstring}"
# Explicit new value changes the serialization format
assert serialize_capabilities(parsed) != f"mxc://{capstring}"

parsed["nothing"] = None
assert 'nothing="None"' in serialize_capabilities(parsed)

assert parse_capabilities("") == dict()
assert deserialize_capabilities("") == dict()

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

Expand Down
94 changes: 57 additions & 37 deletions raiden/utils/capabilities.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,71 @@
from typing import Any, Dict, Optional
from typing import Any, Dict, Optional, Union

import structlog
from werkzeug.urls import url_decode, url_encode

from raiden.constants import Capabilities
from raiden.settings import CapabilitiesConfig

log = structlog.get_logger(__name__)

def parse_capabilities(capstring: str) -> Dict[str, Any]:
if capstring.startswith("mxc://"):
capstring = capstring[6:]
elif "/" in capstring:
capstring = capstring[capstring.rindex("/") + 1 :]
result: Dict[str, Any] = {}
if len(capstring) == 0:
return result
for token in capstring.split(","):
if "=" in token:
key, value = token.split("=")
value = value.strip('"')
result[key] = value
else:
result[token] = True
return result

def _bool_to_binary(value: Any) -> str:
if isinstance(value, bool):
return "1" if value is True else "0"
return value


def _serialize(capdict: Optional[Dict[str, Any]]) -> str:
if capdict is None:
capdict = {}
for key in capdict:
capdict[key] = _bool_to_binary(capdict[key])
return url_encode(capdict)


def serialize_capabilities(capdict: Optional[Dict[str, Any]]) -> str:
if not capdict:
return "mxc://"
for key in capdict.keys():
if "/" in str(key):
raise ValueError(f"Key {key} is malformed, '/' not allowed")

entries = []
for key, value in capdict.items():
if isinstance(value, bool):
if value:
entries.append(key)
return f"mxc://raiden.network/cap?{_serialize(capdict)}"


def _strip_capstring(capstring: str) -> str:
if capstring.startswith("mxc://"):
capstring = capstring[6:]
_, _, capstring = capstring.rpartition("/")
_, _, capstring = capstring.rpartition("?")
return capstring


def deserialize_capabilities(capstring: str) -> Dict[str, Any]:
capstring = _strip_capstring(capstring)
capdict = url_decode(capstring.encode())
capabilities: Dict[str, Any] = dict()
for key in capdict:
value = capdict.getlist(key, type=int_bool)
# reduce lists with one entry to just their element
if len(value) == 1:
capabilities[key] = value.pop()
else:
capabilities[key] = value
return capabilities


def int_bool(value: str) -> Union[bool, str]:
try:
if int(value) in {0, 1}:
return bool(int(value))
else:
entries.append(f'{key}="{value}"')
if len(entries):
return f"mxc://{','.join(entries)}"
return "mxc://"
return value
except ValueError:
return value


def capdict_to_config(capdict: Dict[str, Any]) -> CapabilitiesConfig:
config = CapabilitiesConfig(
no_receive=capdict.get(Capabilities.NO_RECEIVE.value, False),
no_mediate=capdict.get(Capabilities.NO_MEDIATE.value, False),
no_delivery=capdict.get(Capabilities.NO_DELIVERY.value, False),
receive=capdict.get(Capabilities.RECEIVE.value, True),
mediate=capdict.get(Capabilities.MEDIATE.value, True),
delivery=capdict.get(Capabilities.DELIVERY.value, True),
web_rtc=capdict.get(Capabilities.WEBRTC.value, False),
)
for key in capdict.keys():
Expand All @@ -56,15 +76,15 @@ def capdict_to_config(capdict: Dict[str, Any]) -> CapabilitiesConfig:

def capconfig_to_dict(config: CapabilitiesConfig) -> Dict[str, Any]:
result = {
Capabilities.NO_RECEIVE.value: config.no_receive,
Capabilities.NO_MEDIATE.value: config.no_mediate,
Capabilities.NO_DELIVERY.value: config.no_delivery,
Capabilities.RECEIVE.value: config.receive,
Capabilities.MEDIATE.value: config.mediate,
Capabilities.DELIVERY.value: config.delivery,
Capabilities.WEBRTC.value: config.web_rtc,
}
other_keys = [
key
for key in config.__dict__.keys()
if key not in ["no_receive", "no_mediate", "no_delivery", "web_rtc"]
if key not in ["receive", "mediate", "delivery", "web_rtc"]
]
for key in other_keys:
if key not in [_.value for _ in Capabilities]:
Expand Down