Skip to content

Commit 69336b7

Browse files
authored
Merge pull request #68 from acul71/feat/fix-issue-67-DNSADDR-protocol
feat: adding DNSADDR protocol ## Recent Commits - **757aec3** — `test: assert cancellation is handled in test_resolve_cancellation_with_error` Added an explicit assertion to ensure DNS resolution cancellation is handled and tested properly. - **ce716bf** — `fix: implement actual mixed quotes testing in DNS resolver test` Updated the test to actually verify mixed quotes handling in DNS resolution. - **a7d8681** — `refactored the duplicate mock DNS resolution code into a reusable fixture` Reduced code duplication in tests by introducing a fixture for mock DNS resolution. - **b39118f** — `chore: removed repeated code class Resolver` Removed duplicate `Resolver` protocol definition to ensure a single source of truth.
2 parents 6dd3b6b + 757aec3 commit 69336b7

22 files changed

+1489
-591
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,6 @@ readme.html
6363

6464
# PyBuilder
6565
target/
66+
67+
# Editor vscode
68+
.vscode/

README.rst

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
**This project is no longer maintained and has been archived.**
2-
31
py-multiaddr
42
==========================
53

@@ -98,6 +96,37 @@ Multiaddr allows expressing tunnels very nicely.
9896
print(proxyAgain)
9997
# /ip4/10.20.30.40/tcp/443
10098
99+
DNS Resolution
100+
-------------
101+
102+
Multiaddr supports DNS-based address resolution using the DNSADDR protocol.
103+
104+
105+
.. code-block:: python
106+
107+
from multiaddr import Multiaddr
108+
109+
# Create a DNSADDR multiaddr
110+
ma = Multiaddr("/dnsaddr/example.com")
111+
112+
# Resolve to actual IP addresses
113+
resolved = await ma.resolve()
114+
print(resolved)
115+
# [Multiaddr("/ip4/93.184.216.34"), Multiaddr("/ip6/2606:2800:220:1:248:1893:25c8:1946")]
116+
117+
# DNSADDR with peer ID
118+
ma_with_peer = Multiaddr("/dnsaddr/example.com/p2p/QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7wjh53Qk")
119+
resolved_with_peer = await ma_with_peer.resolve()
120+
print(resolved_with_peer)
121+
# [Multiaddr("/ip4/93.184.216.34/p2p/QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7wjh53Qk")]
122+
123+
# Using the DNS resolver directly
124+
from multiaddr.resolvers import DNSResolver
125+
resolver = DNSResolver()
126+
resolved = await resolver.resolve(ma)
127+
print(resolved)
128+
# [Multiaddr("/ip4/93.184.216.34"), Multiaddr("/ip6/2606:2800:220:1:248:1893:25c8:1946")]
129+
101130
Maintainers
102131
===========
103132

docs/multiaddr.codecs.rst

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
multiaddr.codecs package
2+
========================
3+
4+
Submodules
5+
----------
6+
7+
multiaddr.codecs.cid module
8+
---------------------------
9+
10+
.. automodule:: multiaddr.codecs.cid
11+
:members:
12+
:undoc-members:
13+
:show-inheritance:
14+
15+
multiaddr.codecs.domain module
16+
------------------------------
17+
18+
.. automodule:: multiaddr.codecs.domain
19+
:members:
20+
:undoc-members:
21+
:show-inheritance:
22+
23+
multiaddr.codecs.fspath module
24+
------------------------------
25+
26+
.. automodule:: multiaddr.codecs.fspath
27+
:members:
28+
:undoc-members:
29+
:show-inheritance:
30+
31+
multiaddr.codecs.ip4 module
32+
---------------------------
33+
34+
.. automodule:: multiaddr.codecs.ip4
35+
:members:
36+
:undoc-members:
37+
:show-inheritance:
38+
39+
multiaddr.codecs.ip6 module
40+
---------------------------
41+
42+
.. automodule:: multiaddr.codecs.ip6
43+
:members:
44+
:undoc-members:
45+
:show-inheritance:
46+
47+
multiaddr.codecs.onion module
48+
-----------------------------
49+
50+
.. automodule:: multiaddr.codecs.onion
51+
:members:
52+
:undoc-members:
53+
:show-inheritance:
54+
55+
multiaddr.codecs.onion3 module
56+
------------------------------
57+
58+
.. automodule:: multiaddr.codecs.onion3
59+
:members:
60+
:undoc-members:
61+
:show-inheritance:
62+
63+
multiaddr.codecs.uint16be module
64+
--------------------------------
65+
66+
.. automodule:: multiaddr.codecs.uint16be
67+
:members:
68+
:undoc-members:
69+
:show-inheritance:
70+
71+
multiaddr.codecs.utf8 module
72+
----------------------------
73+
74+
.. automodule:: multiaddr.codecs.utf8
75+
:members:
76+
:undoc-members:
77+
:show-inheritance:
78+
79+
80+
Module contents
81+
---------------
82+
83+
.. automodule:: multiaddr.codecs
84+
:members:
85+
:undoc-members:
86+
:show-inheritance:

docs/multiaddr.resolvers.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
multiaddr.resolvers package
2+
===========================
3+
4+
Submodules
5+
----------
6+
7+
multiaddr.resolvers.base module
8+
-------------------------------
9+
10+
.. automodule:: multiaddr.resolvers.base
11+
:members:
12+
:undoc-members:
13+
:show-inheritance:
14+
15+
multiaddr.resolvers.dns module
16+
------------------------------
17+
18+
.. automodule:: multiaddr.resolvers.dns
19+
:members:
20+
:undoc-members:
21+
:show-inheritance:
22+
23+
24+
Module contents
25+
---------------
26+
27+
.. automodule:: multiaddr.resolvers
28+
:members:
29+
:undoc-members:
30+
:show-inheritance:

docs/multiaddr.rst

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
multiaddr package
22
=================
33

4+
Subpackages
5+
-----------
6+
7+
.. toctree::
8+
9+
multiaddr.codecs
10+
multiaddr.resolvers
11+
412
Submodules
513
----------
614

7-
multiaddr.codec module
8-
----------------------
15+
multiaddr.exceptions module
16+
---------------------------
917

10-
.. automodule:: multiaddr.codec
18+
.. automodule:: multiaddr.exceptions
1119
:members:
1220
:undoc-members:
1321
:show-inheritance:
@@ -28,10 +36,10 @@ multiaddr.protocols module
2836
:undoc-members:
2937
:show-inheritance:
3038

31-
multiaddr.util module
32-
---------------------
39+
multiaddr.transforms module
40+
---------------------------
3341

34-
.. automodule:: multiaddr.util
42+
.. automodule:: multiaddr.transforms
3543
:members:
3644
:undoc-members:
3745
:show-inheritance:

multiaddr/codecs/cid.py

Lines changed: 91 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1-
from typing import Dict, List
1+
import logging
22

33
import base58
44
import cid
55

66
from ..codecs import CodecBase
7+
from ..exceptions import BinaryParseError
78
from . import LENGTH_PREFIXED_VAR_SIZE
89

10+
logger = logging.getLogger(__name__)
11+
912
SIZE = LENGTH_PREFIXED_VAR_SIZE
1013
IS_PATH = False
1114

1215

1316
# Spec: https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md#string-representation
14-
CIDv0_PREFIX_TO_LENGTH: Dict[str, List[int]] = {
17+
CIDv0_PREFIX_TO_LENGTH: dict[str, list[int]] = {
1518
# base58btc prefixes for valid lengths 1 - 42 with the identity "hash" function
1619
"12": [5, 12, 19, 23, 30, 41, 52, 56],
1720
"13": [9, 16, 34, 45],
@@ -63,82 +66,111 @@
6366
"Qm": [46],
6467
}
6568

66-
PROTO_NAME_TO_CIDv1_CODEC: Dict[str, str] = {
67-
# The "p2p" multiaddr protocol requires all keys to use the "libp2p-key" multicodec
69+
PROTO_NAME_TO_CIDv1_CODEC = {
6870
"p2p": "libp2p-key",
71+
"ipfs": "dag-pb",
6972
}
7073

7174

75+
def _is_binary_cidv0_multihash(buf: bytes) -> bool:
76+
"""Check if the given bytes represent a CIDv0 multihash."""
77+
try:
78+
# CIDv0 is just a base58btc encoded multihash
79+
# The first byte is the hash function code, second byte is the length
80+
if len(buf) < 2:
81+
return False
82+
hash_code = buf[0]
83+
length = buf[1]
84+
if len(buf) != length + 2: # +2 for the hash code and length bytes
85+
return False
86+
# For CIDv0, we only support sha2-256 (0x12) and identity (0x00)
87+
return hash_code in (0x12, 0x00)
88+
except Exception:
89+
return False
90+
91+
7292
class Codec(CodecBase):
7393
SIZE = SIZE
7494
IS_PATH = IS_PATH
7595

76-
def to_bytes(self, proto, string):
77-
expected_codec = PROTO_NAME_TO_CIDv1_CODEC.get(proto.name)
78-
79-
if len(string) in CIDv0_PREFIX_TO_LENGTH.get(string[0:2], ()): # CIDv0
80-
# Upgrade the wire (binary) representation of any received CIDv0 string
81-
# to CIDv1 if we can determine which multicodec value to use
82-
if expected_codec:
83-
cid_obj = cid.make_cid(1, expected_codec, base58.b58decode(string))
84-
assert isinstance(cid_obj.buffer, bytes)
85-
return cid_obj.buffer
86-
87-
return base58.b58decode(string)
88-
else: # CIDv1+
89-
parsed = cid.from_string(string)
96+
def to_bytes(self, proto, string: str) -> bytes:
97+
"""Convert a CID string to its binary representation."""
98+
if not string:
99+
raise ValueError("CID string cannot be empty")
100+
101+
logger.debug(f"[DEBUG CID to_bytes] Input value: {string}")
102+
103+
# First try to parse as CIDv0 (base58btc encoded multihash)
104+
try:
105+
decoded = base58.b58decode(string)
106+
if _is_binary_cidv0_multihash(decoded):
107+
logger.debug(f"[DEBUG CID to_bytes] Parsed as CIDv0: {decoded.hex()}")
108+
# Do not add length prefix here; the framework handles it
109+
return decoded
110+
except Exception as e:
111+
logger.debug(f"[DEBUG CID to_bytes] Failed to parse as CIDv0: {e}")
112+
113+
# If not CIDv0, try to parse as CIDv1
114+
try:
115+
parsed = cid.make_cid(string)
116+
117+
# Do not add length prefix here; the framework handles it
118+
if not isinstance(parsed.buffer, bytes):
119+
raise ValueError("CID buffer must be bytes")
120+
return parsed.buffer
121+
except ValueError as e:
122+
logger.debug(f"[DEBUG CID to_bytes] Failed to parse as CIDv1: {e}")
123+
raise ValueError(f"Invalid CID: {string}")
90124

91-
# Ensure CID has correct codec for protocol
92-
if expected_codec and parsed.codec != expected_codec:
93-
raise ValueError(
94-
'"{0}" multiaddr CIDs must use the "{1}" multicodec'.format(
95-
proto.name, expected_codec
96-
)
97-
)
125+
def to_string(self, proto, buf: bytes) -> str:
126+
"""Convert a binary CID to its string representation."""
127+
if not buf:
128+
raise ValueError("CID buffer cannot be empty")
98129

99-
return parsed.buffer
130+
logger.debug(f"[DEBUG CID to_string] Input buffer: {buf.hex()}")
131+
logger.debug(f"[DEBUG CID to_string] Protocol: {proto.name}")
100132

101-
def to_string(self, proto, buf):
102133
expected_codec = PROTO_NAME_TO_CIDv1_CODEC.get(proto.name)
134+
logger.debug(f"[DEBUG CID to_string] Expected codec: {expected_codec}")
135+
136+
try:
137+
# First try to parse as CIDv0
138+
if _is_binary_cidv0_multihash(buf):
139+
result = base58.b58encode(buf).decode("ascii")
140+
logger.debug(f"[DEBUG CID to_string] Parsed as CIDv0: {result}")
141+
return result
103142

104-
if _is_binary_cidv0_multihash(buf): # CIDv0
105-
if not expected_codec:
106-
# Simply encode as base58btc as there is nothing better to do
107-
return base58.b58encode(buf).decode("ascii")
108-
109-
# "Implementations SHOULD display peer IDs using the first (raw
110-
# base58btc encoded multihash) format until the second format is
111-
# widely supported."
112-
#
113-
# In the future the following line should instead convert the multihash
114-
# to CIDv1 and with the `expected_codec` and wrap it in base32:
115-
# return cid.make_cid(1, expected_codec, buf).encode("base32").decode("ascii")
116-
return base58.b58encode(buf).decode("ascii")
117-
else: # CIDv1+
143+
# If not CIDv0, try to parse as CIDv1
118144
parsed = cid.from_bytes(buf)
145+
logger.debug(f"[DEBUG CID to_string] Parsed as CIDv1: {parsed}")
119146

120147
# Ensure CID has correct codec for protocol
121148
if expected_codec and parsed.codec != expected_codec:
122149
raise ValueError(
123-
'"{0}" multiaddr CIDs must use the "{1}" multicodec'.format(
150+
'"{}" multiaddr CIDs must use the "{}" multicodec'.format(
124151
proto.name, expected_codec
125152
)
126153
)
127154

128-
# "Implementations SHOULD display peer IDs using the first (raw
129-
# base58btc encoded multihash) format until the second format is
130-
# widely supported."
131-
if expected_codec and _is_binary_cidv0_multihash(parsed.multihash):
132-
return base58.b58encode(parsed.multihash).decode("ascii")
133-
134-
return parsed.encode("base32").decode("ascii")
135-
136-
137-
def _is_binary_cidv0_multihash(buf: bytes) -> bool:
138-
if buf.startswith(b"\x12\x20") and len(buf) == 34: # SHA2-256
139-
return True
140-
141-
if (buf[0] == 0x00 and buf[1] in range(43)) and len(buf) == (buf[1] + 2): # Identity hash
142-
return True
143-
144-
return False
155+
# For peer IDs (p2p/ipfs), always try to use CIDv0 format if possible
156+
if expected_codec:
157+
# Try to convert to CIDv0 format
158+
try:
159+
# Extract the multihash bytes
160+
multihash = parsed.multihash
161+
logger.debug(f"[DEBUG CID to_string] Extracted multihash: {multihash.hex()}")
162+
# Check if it's a valid CIDv0 multihash
163+
if _is_binary_cidv0_multihash(multihash):
164+
result = base58.b58encode(multihash).decode("ascii")
165+
logger.debug(f"[DEBUG CID to_string] Converted to CIDv0: {result}")
166+
return result
167+
except Exception as e:
168+
logger.debug(f"[DEBUG CID to_string] Failed to convert to CIDv0: {e}")
169+
170+
# If we can't convert to CIDv0, use base32 CIDv1 format
171+
result = parsed.encode("base32").decode("ascii")
172+
logger.debug(f"[DEBUG CID to_string] Using CIDv1 format: {result}")
173+
return result
174+
except Exception as e:
175+
logger.debug(f"[DEBUG CID to_string] Error: {e}")
176+
raise BinaryParseError(str(e), buf, proto.name, e) from e

0 commit comments

Comments
 (0)