From c883af78c4f80073a83938e18c356af7d5d4c79c Mon Sep 17 00:00:00 2001 From: 1yam Date: Thu, 13 Jul 2023 11:30:29 +0200 Subject: [PATCH 1/4] Feature : AlephDNS --- setup.cfg | 1 + src/aleph/sdk/conf.py | 6 ++ src/aleph/sdk/domain.py | 126 ++++++++++++++++++++++++++++++++++++ src/aleph/sdk/exceptions.py | 5 ++ tests/unit/test_domains.py | 42 ++++++++++++ 5 files changed, 180 insertions(+) create mode 100644 src/aleph/sdk/domain.py create mode 100644 tests/unit/test_domains.py diff --git a/setup.cfg b/setup.cfg index 85dac9bb..cea57d52 100644 --- a/setup.cfg +++ b/setup.cfg @@ -79,6 +79,7 @@ testing = black isort flake8 + aiodns mqtt = aiomqtt<=0.1.3 certifi diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index 264c8c9f..e78012ce 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -33,6 +33,12 @@ class Settings(BaseSettings): CODE_USES_SQUASHFS: bool = which("mksquashfs") is not None # True if command exists + # Dns resolver + IPFS_DOMAINS = "ipfs.public.aleph.sh" + PROGRAM_DOMAINS = "program.public.aleph.sh" + ROOT_DOMAIN = "static.public.aleph.sh" + RESOLVERS = ["1.1.1.1", "8.8.8.8"] + class Config: env_prefix = "ALEPH_" case_sensitive = False diff --git a/src/aleph/sdk/domain.py b/src/aleph/sdk/domain.py new file mode 100644 index 00000000..7026b433 --- /dev/null +++ b/src/aleph/sdk/domain.py @@ -0,0 +1,126 @@ +import aiodns +import re +from .conf import settings +from typing import Optional +from aleph.sdk.exceptions import DomainConfigurationError + + +class AlephDNS: + def __init__(self): + self.resolver = aiodns.DNSResolver(servers=settings.RESOLVERS) + self.fqdn_matcher = re.compile(r"https?://?") + + async def query(self, name: str, query_type: str): + try: + return await self.resolver.query(name, query_type) + except Exception as e: + print(e) + return None + + def url_to_domain(self, url): + return self.fqdn_matcher.sub("", url).strip().strip("/") + + async def get_ipv6_address(self, url: str): + domain = self.url_to_domain(url) + ipv6 = [] + query = await self.query(domain, "AAAA") + if query: + for entry in query: + ipv6.append(entry.host) + return ipv6 + + async def get_dnslink(self, url: str): + domain = self.url_to_domain(url) + query = await self.query(f"_dnslink.{domain}", "TXT") + if query is not None and len(query) > 0: + return query[0].text + + async def get_control(self, url: str): + domain = self.url_to_domain(url) + query = await self.query(f"_control.{domain}", "TXT") + if query is not None and len(query) > 0: + return query[0].text + + async def check_domain_configured(self, domain, _type, owner): + try: + print("Check...", _type) + return await self.check_domain(domain, _type, owner) + except Exception as error: + raise DomainConfigurationError(error) + + async def check_domain(self, url: str, _type: str, owner: Optional[str] = None): + # if _type.lower() == 'ipfs': + return await self.check_ipfs_domain(url, _type, owner) + # elif _type.lower() == 'program': + # pass + + async def check_ipfs_domain( + self, url: str, _type: str, owner: Optional[str] = None + ): + status = {"cname": True, "owner_proof": False} + + _type = _type.lower() + domain = self.url_to_domain(url) + + if _type == "ipfs": + status["delegation"] = False + + # check1: CNAME value should be ipfs or program + res = await self.query(domain, "CNAME") + if _type.lower() == "ipfs": + expected_value = settings.IPFS_DOMAINS + else: + expected_value = settings.PROGRAM_DOMAINS + + assert_error = ( + f"CNAME record not found: {domain}", + f"Create a CNAME record for {domain} with values {expected_value}", + status, + ) + + assert res is not None, assert_error + assert hasattr(res, "cname"), assert_error + + assert_error = ( + f"{domain} should have a valid CNAME value, {res.cname} provided", + f"Create a CNAME record for {domain} with values {expected_value}", + status, + ) + assert res.cname in expected_value, assert_error + status["cname"] = True + + if _type.lower() == "ipfs": + # check2: CNAME value of _dnslink.__custom_domain__ + # should be _dnslink.__custom_domain__.static.public.aleph.sh + res = await self.query(f"_dnslink.{domain}", "CNAME") + + expected_value = f"_dnslink.{domain}.{settings.ROOT_DOMAIN}" + assert_error = ( + f"CNAME record not found: _dnslink.{domain}", + f"Create a CNAME record for _dnslink.{domain} with value: {expected_value}", + status, + ) + + assert res is not None, assert_error + assert hasattr(res, "cname"), assert_error + assert res.cname == expected_value, assert_error + status["delegation"] = True + + # check3: TXT value of _control.__custom_domain__ should be the address of the owner + owner_address = await self.get_control(domain) + assert_error = ( + f"TXT record not found: _control.{domain}", + f'Create a TXT record for _control.{domain} with value = "owner address"', + status, + ) + assert owner_address is not None, assert_error + + if owner is not None: + assert owner_address == owner, ( + f"Owner address mismatch, got: {owner} expected: {owner_address}", + f"", + status, + ) + status["owner_proof"] = True + + return status diff --git a/src/aleph/sdk/exceptions.py b/src/aleph/sdk/exceptions.py index 51762925..b885da50 100644 --- a/src/aleph/sdk/exceptions.py +++ b/src/aleph/sdk/exceptions.py @@ -50,3 +50,8 @@ class FileTooLarge(Exception): """ pass + + +class DomainConfigurationError(Exception): + "Raised when the domain checks are not satisfied" + pass diff --git a/tests/unit/test_domains.py b/tests/unit/test_domains.py new file mode 100644 index 00000000..1232b1ce --- /dev/null +++ b/tests/unit/test_domains.py @@ -0,0 +1,42 @@ +import pytest +import asyncio + +from aleph.sdk.domain import AlephDNS + + +@pytest.mark.asyncio +async def test_url_to_domain(): + alephdns = AlephDNS() + domain = alephdns.url_to_domain("https://aleph.im") + query = await alephdns.query(domain, "A") + assert query is not None + assert len(query) > 0 + assert hasattr(query[0], "host") + + +@pytest.mark.asyncio +async def test_get_ipv6_adress(): + alephdns = AlephDNS() + url = "https://aleph.im" + ipv6_address = await alephdns.get_ipv6_address(url) + assert ipv6_address is not None + assert len(ipv6_address) > 0 + assert ":" in ipv6_address[0] + + +@pytest.mark.asyncio +async def test_dnslink(): + alephdns = AlephDNS() + url = "https://aleph.im" + dnslink = await alephdns.get_dnslink(url) + assert dnslink is not None + + +""" +@pytest.mark.asyncio +async def test_cname(): + alephdns = AlephDNS() + url = 'https://custom_domain_test.aleph.sh' + check = await alephdns.custom_domain_check(url) + assert check is not None +""" From 11f3d27e8eb867efe902b333df7d9f4fa6c32cc4 Mon Sep 17 00:00:00 2001 From: aliel Date: Thu, 13 Jul 2023 19:30:04 +0200 Subject: [PATCH 2/4] refactor --- src/aleph/sdk/conf.py | 8 ++--- src/aleph/sdk/domain.py | 66 ++++++++++++++++++++------------------ tests/unit/test_domains.py | 22 +++++++++---- 3 files changed, 53 insertions(+), 43 deletions(-) diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index e78012ce..132318ca 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -34,10 +34,10 @@ class Settings(BaseSettings): CODE_USES_SQUASHFS: bool = which("mksquashfs") is not None # True if command exists # Dns resolver - IPFS_DOMAINS = "ipfs.public.aleph.sh" - PROGRAM_DOMAINS = "program.public.aleph.sh" - ROOT_DOMAIN = "static.public.aleph.sh" - RESOLVERS = ["1.1.1.1", "8.8.8.8"] + DNS_IPFS_DOMAIN = "ipfs.public.aleph.sh" + DNS_PROGRAM_DOMAIN = "program.public.aleph.sh" + DNS_ROOT_DOMAIN = "static.public.aleph.sh" + DNS_RESOLVERS = ["1.1.1.1", "1.0.0.1"] class Config: env_prefix = "ALEPH_" diff --git a/src/aleph/sdk/domain.py b/src/aleph/sdk/domain.py index 7026b433..fa7b78ea 100644 --- a/src/aleph/sdk/domain.py +++ b/src/aleph/sdk/domain.py @@ -7,7 +7,7 @@ class AlephDNS: def __init__(self): - self.resolver = aiodns.DNSResolver(servers=settings.RESOLVERS) + self.resolver = aiodns.DNSResolver(servers=settings.DNS_RESOLVERS) self.fqdn_matcher = re.compile(r"https?://?") async def query(self, name: str, query_type: str): @@ -41,69 +41,66 @@ async def get_control(self, url: str): if query is not None and len(query) > 0: return query[0].text - async def check_domain_configured(self, domain, _type, owner): + async def check_domain_configured(self, domain, target, owner): try: - print("Check...", _type) - return await self.check_domain(domain, _type, owner) + print("Check...", target) + return await self.check_domain(domain, target, owner) except Exception as error: raise DomainConfigurationError(error) - async def check_domain(self, url: str, _type: str, owner: Optional[str] = None): - # if _type.lower() == 'ipfs': - return await self.check_ipfs_domain(url, _type, owner) - # elif _type.lower() == 'program': - # pass + async def check_domain(self, url: str, target: str, owner: Optional[str] = None): + return await self.check_ipfs_domain(url, target, owner) + async def check_ipfs_domain( - self, url: str, _type: str, owner: Optional[str] = None + self, url: str, target: str, owner: Optional[str] = None ): status = {"cname": True, "owner_proof": False} - _type = _type.lower() + target = target.lower() domain = self.url_to_domain(url) - if _type == "ipfs": + if target == "ipfs": status["delegation"] = False # check1: CNAME value should be ipfs or program res = await self.query(domain, "CNAME") - if _type.lower() == "ipfs": - expected_value = settings.IPFS_DOMAINS + if target.lower() == "ipfs": + expected_value = settings.DNS_IPFS_DOMAIN else: - expected_value = settings.PROGRAM_DOMAINS + expected_value = settings.DNS_PROGRAM_DOMAIN assert_error = ( f"CNAME record not found: {domain}", f"Create a CNAME record for {domain} with values {expected_value}", status, ) - - assert res is not None, assert_error - assert hasattr(res, "cname"), assert_error + if res is None or not hasattr(res, "cname"): + raise DomainConfigurationError(assert_error) assert_error = ( f"{domain} should have a valid CNAME value, {res.cname} provided", f"Create a CNAME record for {domain} with values {expected_value}", status, ) - assert res.cname in expected_value, assert_error - status["cname"] = True + if res.cname != expected_value: + raise DomainConfigurationError(assert_error) - if _type.lower() == "ipfs": + status["cname"] = True + if target.lower() == "ipfs": # check2: CNAME value of _dnslink.__custom_domain__ # should be _dnslink.__custom_domain__.static.public.aleph.sh res = await self.query(f"_dnslink.{domain}", "CNAME") - expected_value = f"_dnslink.{domain}.{settings.ROOT_DOMAIN}" + expected_value = f"_dnslink.{domain}.{settings.DNS_ROOT_DOMAIN}" assert_error = ( f"CNAME record not found: _dnslink.{domain}", f"Create a CNAME record for _dnslink.{domain} with value: {expected_value}", status, ) + if res is None or not hasattr(res, "cname") or res.cname != expected_value: + raise DomainConfigurationError(assert_error) - assert res is not None, assert_error - assert hasattr(res, "cname"), assert_error - assert res.cname == expected_value, assert_error status["delegation"] = True # check3: TXT value of _control.__custom_domain__ should be the address of the owner @@ -113,14 +110,19 @@ async def check_ipfs_domain( f'Create a TXT record for _control.{domain} with value = "owner address"', status, ) - assert owner_address is not None, assert_error + if owner_address is None: + raise DomainConfigurationError(assert_error) if owner is not None: - assert owner_address == owner, ( - f"Owner address mismatch, got: {owner} expected: {owner_address}", - f"", - status, - ) - status["owner_proof"] = True + if owner == owner_address: + status["owner_proof"] = True + else: + raise DomainConfigurationError( + ( + f"Owner address mismatch, got: {owner} expected: {owner_address}", + f"", + status, + ) + ) return status diff --git a/tests/unit/test_domains.py b/tests/unit/test_domains.py index 1232b1ce..ce78abaa 100644 --- a/tests/unit/test_domains.py +++ b/tests/unit/test_domains.py @@ -2,6 +2,7 @@ import asyncio from aleph.sdk.domain import AlephDNS +from aleph.sdk.exceptions import DomainConfigurationError @pytest.mark.asyncio @@ -15,7 +16,7 @@ async def test_url_to_domain(): @pytest.mark.asyncio -async def test_get_ipv6_adress(): +async def test_get_ipv6_address(): alephdns = AlephDNS() url = "https://aleph.im" ipv6_address = await alephdns.get_ipv6_address(url) @@ -32,11 +33,18 @@ async def test_dnslink(): assert dnslink is not None -""" @pytest.mark.asyncio -async def test_cname(): +async def test_configured_domain(): alephdns = AlephDNS() - url = 'https://custom_domain_test.aleph.sh' - check = await alephdns.custom_domain_check(url) - assert check is not None -""" + url = 'https://custom-domain-unit-test.aleph.sh' + status = await alephdns.check_domain(url, "ipfs", "0xfakeaddress") + assert type(status) is dict + + +@pytest.mark.asyncio +async def test_not_configured_domain(): + alephdns = AlephDNS() + url = 'https://not-configured-domain.aleph.sh' + with pytest.raises(DomainConfigurationError): + status = await alephdns.check_domain(url, "ipfs", "0xfakeaddress") + From e0137a5ce57c809969557846cecc0679b7224e9f Mon Sep 17 00:00:00 2001 From: aliel Date: Wed, 26 Jul 2023 12:47:40 +0200 Subject: [PATCH 3/4] refactor checks --- src/aleph/sdk/domain.py | 151 +++++++++++++++++++++------------------- 1 file changed, 78 insertions(+), 73 deletions(-) diff --git a/src/aleph/sdk/domain.py b/src/aleph/sdk/domain.py index fa7b78ea..b14b4256 100644 --- a/src/aleph/sdk/domain.py +++ b/src/aleph/sdk/domain.py @@ -35,12 +35,6 @@ async def get_dnslink(self, url: str): if query is not None and len(query) > 0: return query[0].text - async def get_control(self, url: str): - domain = self.url_to_domain(url) - query = await self.query(f"_control.{domain}", "TXT") - if query is not None and len(query) > 0: - return query[0].text - async def check_domain_configured(self, domain, target, owner): try: print("Check...", target) @@ -48,81 +42,92 @@ async def check_domain_configured(self, domain, target, owner): except Exception as error: raise DomainConfigurationError(error) - async def check_domain(self, url: str, target: str, owner: Optional[str] = None): - return await self.check_ipfs_domain(url, target, owner) - - - async def check_ipfs_domain( + async def check_domain( self, url: str, target: str, owner: Optional[str] = None ): - status = {"cname": True, "owner_proof": False} + status = {"cname": False, "owner_proof": False} target = target.lower() domain = self.url_to_domain(url) - if target == "ipfs": - status["delegation"] = False + dns_rules = self.get_required_dns_rules(url, target, owner) - # check1: CNAME value should be ipfs or program - res = await self.query(domain, "CNAME") - if target.lower() == "ipfs": - expected_value = settings.DNS_IPFS_DOMAIN - else: - expected_value = settings.DNS_PROGRAM_DOMAIN - - assert_error = ( - f"CNAME record not found: {domain}", - f"Create a CNAME record for {domain} with values {expected_value}", - status, - ) - if res is None or not hasattr(res, "cname"): - raise DomainConfigurationError(assert_error) - - assert_error = ( - f"{domain} should have a valid CNAME value, {res.cname} provided", - f"Create a CNAME record for {domain} with values {expected_value}", - status, - ) - if res.cname != expected_value: - raise DomainConfigurationError(assert_error) - - status["cname"] = True - if target.lower() == "ipfs": - # check2: CNAME value of _dnslink.__custom_domain__ - # should be _dnslink.__custom_domain__.static.public.aleph.sh - res = await self.query(f"_dnslink.{domain}", "CNAME") - - expected_value = f"_dnslink.{domain}.{settings.DNS_ROOT_DOMAIN}" - assert_error = ( - f"CNAME record not found: _dnslink.{domain}", - f"Create a CNAME record for _dnslink.{domain} with value: {expected_value}", - status, - ) - if res is None or not hasattr(res, "cname") or res.cname != expected_value: - raise DomainConfigurationError(assert_error) - - status["delegation"] = True - - # check3: TXT value of _control.__custom_domain__ should be the address of the owner - owner_address = await self.get_control(domain) - assert_error = ( - f"TXT record not found: _control.{domain}", - f'Create a TXT record for _control.{domain} with value = "owner address"', - status, - ) - if owner_address is None: - raise DomainConfigurationError(assert_error) - - if owner is not None: - if owner == owner_address: - status["owner_proof"] = True - else: - raise DomainConfigurationError( - ( - f"Owner address mismatch, got: {owner} expected: {owner_address}", - f"", - status, + for dns_rule in dns_rules: + status[dns_rule["rule_name"]] = False + + record_name = dns_rule["dns"]["name"] + record_type = dns_rule["dns"]["type"] + record_value = dns_rule["dns"]["value"] + + res = await self.query(record_name, record_type.upper()) + + if record_type == "txt": + found = False + + for _res in res: + if hasattr(_res, "text") and _res.text == record_value: + found = True + + if found == False: + raise DomainConfigurationError( + (dns_rule["info"], dns_rule["on_error"], status) ) + + elif res is None or not hasattr(res, record_type) or getattr(res, record_type) != record_value: + raise DomainConfigurationError( + (dns_rule["info"], dns_rule["on_error"], status) ) + status[dns_rule["rule_name"]] = True + return status + + def get_required_dns_rules(self, url, target, owner: Optional[str] = None): + domain = self.url_to_domain(url) + target = target.lower() + dns_rules = [] + + if target == "ipfs": + cname_value = settings.DNS_IPFS_DOMAIN + else: + cname_value = settings.DNS_PROGRAM_DOMAIN + + # cname rule + dns_rules.append({ + "rule_name": "cname", + "dns": { + "type": "cname", + "name": domain, + "value": cname_value + }, + "info": f"Create a CNAME record for {domain} with value {cname_value}", + "on_error": f"CNAME record not found: {domain}" + }) + + if target == "ipfs": + # ipfs rule + dns_rules.append({ + "rule_name": "delegation", + "dns": { + "type": "cname", + "name": f"_dnslink.{domain}", + "value": f"_dnslink.{domain}.{settings.DNS_ROOT_DOMAIN}" + }, + "info": f"Create a CNAME record for _dnslink.{domain} with value _dnslink.{domain}.{settings.DNS_ROOT_DOMAIN}", + "on_error": f"CNAME record not found: _dnslink.{domain}" + }) + + if owner: + # ownership rule + dns_rules.append({ + "rule_name": "owner_proof", + "dns": { + "type": "txt", + "name": f"_control.{domain}", + "value": owner + }, + "info": f"Create a TXT record for _control.{domain} with value = owner address", + "on_error": f"Owner address mismatch" + }) + + return dns_rules From 0779226ec01827ff51e6cdd2831314280d8592a9 Mon Sep 17 00:00:00 2001 From: aliel Date: Wed, 26 Jul 2023 16:28:34 +0200 Subject: [PATCH 4/4] add instances support --- src/aleph/sdk/conf.py | 1 + src/aleph/sdk/domain.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index 132318ca..f35347db 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -36,6 +36,7 @@ class Settings(BaseSettings): # Dns resolver DNS_IPFS_DOMAIN = "ipfs.public.aleph.sh" DNS_PROGRAM_DOMAIN = "program.public.aleph.sh" + DNS_INSTANCE_DOMAIN = "instance.public.aleph.sh" DNS_ROOT_DOMAIN = "static.public.aleph.sh" DNS_RESOLVERS = ["1.1.1.1", "1.0.0.1"] diff --git a/src/aleph/sdk/domain.py b/src/aleph/sdk/domain.py index b14b4256..1d708797 100644 --- a/src/aleph/sdk/domain.py +++ b/src/aleph/sdk/domain.py @@ -89,8 +89,10 @@ def get_required_dns_rules(self, url, target, owner: Optional[str] = None): if target == "ipfs": cname_value = settings.DNS_IPFS_DOMAIN - else: + elif target == "program": cname_value = settings.DNS_PROGRAM_DOMAIN + elif target == "instance": + cname_value = f"{domain}.{settings.DNS_INSTANCE_DOMAIN}" # cname rule dns_rules.append({