Skip to content

Commit 86df08f

Browse files
1yamaliel
andauthored
Feature : AlephDNS (#47)
* Feature : AlephDNS add instances support add ipfs support add program support --------- Co-authored-by: aliel <[email protected]>
1 parent 6705f95 commit 86df08f

File tree

5 files changed

+198
-0
lines changed

5 files changed

+198
-0
lines changed

setup.cfg

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ testing =
7979
black
8080
isort
8181
flake8
82+
aiodns
8283
mqtt =
8384
aiomqtt<=0.1.3
8485
certifi

src/aleph/sdk/conf.py

+7
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ class Settings(BaseSettings):
3333

3434
CODE_USES_SQUASHFS: bool = which("mksquashfs") is not None # True if command exists
3535

36+
# Dns resolver
37+
DNS_IPFS_DOMAIN = "ipfs.public.aleph.sh"
38+
DNS_PROGRAM_DOMAIN = "program.public.aleph.sh"
39+
DNS_INSTANCE_DOMAIN = "instance.public.aleph.sh"
40+
DNS_ROOT_DOMAIN = "static.public.aleph.sh"
41+
DNS_RESOLVERS = ["1.1.1.1", "1.0.0.1"]
42+
3643
class Config:
3744
env_prefix = "ALEPH_"
3845
case_sensitive = False

src/aleph/sdk/domain.py

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import aiodns
2+
import re
3+
from .conf import settings
4+
from typing import Optional
5+
from aleph.sdk.exceptions import DomainConfigurationError
6+
7+
8+
class AlephDNS:
9+
def __init__(self):
10+
self.resolver = aiodns.DNSResolver(servers=settings.DNS_RESOLVERS)
11+
self.fqdn_matcher = re.compile(r"https?://?")
12+
13+
async def query(self, name: str, query_type: str):
14+
try:
15+
return await self.resolver.query(name, query_type)
16+
except Exception as e:
17+
print(e)
18+
return None
19+
20+
def url_to_domain(self, url):
21+
return self.fqdn_matcher.sub("", url).strip().strip("/")
22+
23+
async def get_ipv6_address(self, url: str):
24+
domain = self.url_to_domain(url)
25+
ipv6 = []
26+
query = await self.query(domain, "AAAA")
27+
if query:
28+
for entry in query:
29+
ipv6.append(entry.host)
30+
return ipv6
31+
32+
async def get_dnslink(self, url: str):
33+
domain = self.url_to_domain(url)
34+
query = await self.query(f"_dnslink.{domain}", "TXT")
35+
if query is not None and len(query) > 0:
36+
return query[0].text
37+
38+
async def check_domain_configured(self, domain, target, owner):
39+
try:
40+
print("Check...", target)
41+
return await self.check_domain(domain, target, owner)
42+
except Exception as error:
43+
raise DomainConfigurationError(error)
44+
45+
async def check_domain(
46+
self, url: str, target: str, owner: Optional[str] = None
47+
):
48+
status = {"cname": False, "owner_proof": False}
49+
50+
target = target.lower()
51+
domain = self.url_to_domain(url)
52+
53+
dns_rules = self.get_required_dns_rules(url, target, owner)
54+
55+
for dns_rule in dns_rules:
56+
status[dns_rule["rule_name"]] = False
57+
58+
record_name = dns_rule["dns"]["name"]
59+
record_type = dns_rule["dns"]["type"]
60+
record_value = dns_rule["dns"]["value"]
61+
62+
res = await self.query(record_name, record_type.upper())
63+
64+
if record_type == "txt":
65+
found = False
66+
67+
for _res in res:
68+
if hasattr(_res, "text") and _res.text == record_value:
69+
found = True
70+
71+
if found == False:
72+
raise DomainConfigurationError(
73+
(dns_rule["info"], dns_rule["on_error"], status)
74+
)
75+
76+
elif res is None or not hasattr(res, record_type) or getattr(res, record_type) != record_value:
77+
raise DomainConfigurationError(
78+
(dns_rule["info"], dns_rule["on_error"], status)
79+
)
80+
81+
status[dns_rule["rule_name"]] = True
82+
83+
return status
84+
85+
def get_required_dns_rules(self, url, target, owner: Optional[str] = None):
86+
domain = self.url_to_domain(url)
87+
target = target.lower()
88+
dns_rules = []
89+
90+
if target == "ipfs":
91+
cname_value = settings.DNS_IPFS_DOMAIN
92+
elif target == "program":
93+
cname_value = settings.DNS_PROGRAM_DOMAIN
94+
elif target == "instance":
95+
cname_value = f"{domain}.{settings.DNS_INSTANCE_DOMAIN}"
96+
97+
# cname rule
98+
dns_rules.append({
99+
"rule_name": "cname",
100+
"dns": {
101+
"type": "cname",
102+
"name": domain,
103+
"value": cname_value
104+
},
105+
"info": f"Create a CNAME record for {domain} with value {cname_value}",
106+
"on_error": f"CNAME record not found: {domain}"
107+
})
108+
109+
if target == "ipfs":
110+
# ipfs rule
111+
dns_rules.append({
112+
"rule_name": "delegation",
113+
"dns": {
114+
"type": "cname",
115+
"name": f"_dnslink.{domain}",
116+
"value": f"_dnslink.{domain}.{settings.DNS_ROOT_DOMAIN}"
117+
},
118+
"info": f"Create a CNAME record for _dnslink.{domain} with value _dnslink.{domain}.{settings.DNS_ROOT_DOMAIN}",
119+
"on_error": f"CNAME record not found: _dnslink.{domain}"
120+
})
121+
122+
if owner:
123+
# ownership rule
124+
dns_rules.append({
125+
"rule_name": "owner_proof",
126+
"dns": {
127+
"type": "txt",
128+
"name": f"_control.{domain}",
129+
"value": owner
130+
},
131+
"info": f"Create a TXT record for _control.{domain} with value = owner address",
132+
"on_error": f"Owner address mismatch"
133+
})
134+
135+
return dns_rules

src/aleph/sdk/exceptions.py

+5
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,8 @@ class FileTooLarge(Exception):
5050
"""
5151

5252
pass
53+
54+
55+
class DomainConfigurationError(Exception):
56+
"Raised when the domain checks are not satisfied"
57+
pass

tests/unit/test_domains.py

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import pytest
2+
import asyncio
3+
4+
from aleph.sdk.domain import AlephDNS
5+
from aleph.sdk.exceptions import DomainConfigurationError
6+
7+
8+
@pytest.mark.asyncio
9+
async def test_url_to_domain():
10+
alephdns = AlephDNS()
11+
domain = alephdns.url_to_domain("https://aleph.im")
12+
query = await alephdns.query(domain, "A")
13+
assert query is not None
14+
assert len(query) > 0
15+
assert hasattr(query[0], "host")
16+
17+
18+
@pytest.mark.asyncio
19+
async def test_get_ipv6_address():
20+
alephdns = AlephDNS()
21+
url = "https://aleph.im"
22+
ipv6_address = await alephdns.get_ipv6_address(url)
23+
assert ipv6_address is not None
24+
assert len(ipv6_address) > 0
25+
assert ":" in ipv6_address[0]
26+
27+
28+
@pytest.mark.asyncio
29+
async def test_dnslink():
30+
alephdns = AlephDNS()
31+
url = "https://aleph.im"
32+
dnslink = await alephdns.get_dnslink(url)
33+
assert dnslink is not None
34+
35+
36+
@pytest.mark.asyncio
37+
async def test_configured_domain():
38+
alephdns = AlephDNS()
39+
url = 'https://custom-domain-unit-test.aleph.sh'
40+
status = await alephdns.check_domain(url, "ipfs", "0xfakeaddress")
41+
assert type(status) is dict
42+
43+
44+
@pytest.mark.asyncio
45+
async def test_not_configured_domain():
46+
alephdns = AlephDNS()
47+
url = 'https://not-configured-domain.aleph.sh'
48+
with pytest.raises(DomainConfigurationError):
49+
status = await alephdns.check_domain(url, "ipfs", "0xfakeaddress")
50+

0 commit comments

Comments
 (0)