Skip to content
This repository was archived by the owner on May 15, 2025. It is now read-only.
/ web Public archive

Commit 6ff0f83

Browse files
committed
Revert "First pass"
This reverts commit ee16aad.
1 parent 813e4c3 commit 6ff0f83

File tree

4 files changed

+254
-80
lines changed

4 files changed

+254
-80
lines changed

docker-compose.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,25 @@ services:
1212
- POSTGRES_PASSWORD
1313
- POSTGRES_USER
1414

15+
pdns:
16+
build: pdns
17+
restart: unless-stopped
18+
ports:
19+
- "53:53/udp"
20+
environment:
21+
- LOCALCERT_PDNS_DB_NAME
22+
- LOCALCERT_PDNS_DEFAULT_SOA_CONTENT
23+
- LOCALCERT_PDNS_HOST
24+
- LOCALCERT_PDNS_WEBSERVER_ALLOW_FROM
25+
- LOCALCERT_SHARED_PDNS_API_KEY
26+
- POSTGRES_PASSWORD
27+
- POSTGRES_USER
28+
networks:
29+
localcert-net:
30+
ipv4_address: 10.33.44.2
31+
depends_on:
32+
- db
33+
1534
web:
1635
build: localcert
1736
restart: unless-stopped
@@ -36,6 +55,7 @@ services:
3655
ipv4_address: 10.33.44.3
3756
depends_on:
3857
- db
58+
- pdns
3959
labels:
4060
- "traefik.enable=true"
4161
- "traefik.http.routers.localcert-web.rule=(Host(`console.getlocalcert.net`)) || (Host(`api.getlocalcert.net`) && PathPrefix(`/api/`))"

localcert/domains/pdns.py

Lines changed: 126 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,155 @@
1+
import requests
12
import logging
23

34
from .utils import CustomExceptionServerError
45
from datetime import datetime
56
from django.conf import settings
67
from typing import List
7-
from cloudflare import Cloudflare
88

99

10-
ZONE_IDS = {
11-
"localcert.net.": "ab2d04b0ccf31906dd87900f0db11f73",
12-
"localhostcert.net.": "ac1335db9f052915b076c0de09e06443",
10+
PDNS_API_BASE_URL = f"http://{settings.LOCALCERT_PDNS_SERVER_IP}:{settings.LOCALCERT_PDNS_API_PORT}/api/v1"
11+
PDNS_HEADERS = {
12+
"X-API-Key": settings.LOCALCERT_PDNS_API_KEY,
13+
"accept": "application/json",
1314
}
1415

1516

16-
client = Cloudflare(api_token=os.environ.get("CLOUDFLARE_TOKEN"))
17+
def pdns_create_zone(zone: str):
18+
assert zone.endswith(".")
1719

20+
logging.debug(f"[PDNS] Create {zone}")
1821

19-
# TODO: Some records are set by wildcard, hardcode these
20-
def pdns_describe_domain(domain: str) -> dict:
21-
assert domain.endswith(".")
22-
logging.debug(f"[PDNS] Describe {domain}")
22+
# Create zone in pdns
23+
resp = requests.post(
24+
PDNS_API_BASE_URL + "/servers/localhost/zones",
25+
headers=PDNS_HEADERS,
26+
json={
27+
"name": zone,
28+
"kind": "Native",
29+
},
30+
)
31+
json_resp = resp.json()
32+
33+
if "error" in json_resp.keys():
34+
raise CustomExceptionServerError(json_resp["error"]) # pragma: no cover
35+
36+
# success
37+
return
38+
39+
40+
# TODO use the targeted name/type
41+
def pdns_describe_domain(zone_name: str) -> dict:
42+
assert zone_name.endswith(".")
43+
44+
logging.debug(f"[PDNS] Describe {zone_name}")
45+
46+
# TODO: newer pdns versions can filter by name/type
47+
resp = requests.get(
48+
f"{PDNS_API_BASE_URL}/servers/localhost/zones/{zone_name}",
49+
headers=PDNS_HEADERS,
50+
)
51+
if resp.status_code != requests.codes.ok:
52+
raise CustomExceptionServerError(
53+
f"Unable to describe domain, PDNS error code: {resp.status_code}"
54+
) # pragma: no cover
55+
56+
return resp.json()
2357

24-
for k, v in ZONE_IDS.items():
25-
if domain.endswith(f".{k}")
26-
zone_id = v
27-
break
28-
else:
29-
# Ooops
30-
return {}
3158

32-
# CF doesn't use trailing dot
33-
domain = domain[:-1]
34-
35-
# Two lookups:
36-
# <domain>.<zone> (exact)
37-
# *.<domain>.<zone> (endswith)
38-
results = client.dns.records.list(
39-
zone_id=zone_id,
40-
name={"endswith": f".{domain}"},
41-
type="TXT",
42-
).result
43-
r2 = client.dns.records.list(
44-
zone_id=zone_id,
45-
name={"exact": domain},
46-
type="TXT",
47-
).result
48-
results.extend(r2)
49-
50-
rrsets = []
51-
for result in results:
52-
rrset.append({
53-
"type": "TXT",
54-
"name": result.name,
55-
"content": result.content,
56-
"ttl": result.ttl,
57-
})
58-
return { "rrsets": rrsets }
59+
def pdns_delete_rrset(zone_name: str, rr_name: str, rrtype: str):
60+
assert zone_name.endswith(".")
61+
assert rr_name.endswith(zone_name)
62+
assert rrtype == "TXT"
63+
64+
logging.debug(f"[PDNS] Delete {zone_name} {rr_name} {rrtype}")
65+
66+
resp = requests.patch(
67+
f"{PDNS_API_BASE_URL}/servers/localhost/zones/{zone_name}",
68+
headers=PDNS_HEADERS,
69+
json={
70+
"rrsets": [
71+
{
72+
"name": rr_name,
73+
"type": "TXT",
74+
"changetype": "DELETE",
75+
},
76+
],
77+
},
78+
)
79+
80+
if resp.status_code != requests.codes.no_content:
81+
raise CustomExceptionServerError(f"{resp.status_code}") # pragma: no cover
82+
83+
# success
84+
return
5985

6086

6187
def pdns_replace_rrset(
6288
zone_name: str, rr_name: str, rr_type: str, ttl: int, record_contents: List[str]
6389
):
6490
"""
91+
6592
record_contents - Records from least recently added
6693
"""
6794
assert rr_name.endswith(".")
6895
assert rr_name.endswith(zone_name)
69-
assert rr_type == "TXT"
70-
71-
# CF doesn't use trailing dot
72-
rr_name = rr_name[:-1]
73-
74-
# Collect the existing content
75-
zone_id = ZONE_IDS[zone_name]
76-
results = client.dns.records.list(
77-
zone_id=zone_id,
78-
name=rr_name,
79-
type=rr_type,
80-
).result
81-
82-
for record in results:
83-
if record.content not in record_contents:
84-
# Delete records that are no longer needed
85-
client.dns.records.delete(
86-
zone_id=zone_id,
87-
dns_record_id=record.id,
88-
)
89-
else:
90-
# Don't alter records that already exist
91-
record_contents.remove(record.content)
92-
93-
for content in record_contents:
94-
# Create anything that's new
95-
client.dns.records.create(
96-
zone_id=zone_id,
97-
name=rr_name,
98-
type=rr_type,
99-
content=content,
100-
)
96+
assert rr_type in ["TXT", "A", "MX", "NS", "SOA"]
97+
98+
logging.debug(
99+
f"[PDNS] Replace {zone_name} {rr_name} {rr_type} {ttl} {record_contents}"
100+
)
101+
102+
records = [
103+
{
104+
"content": content,
105+
"disabled": False,
106+
}
107+
for content in record_contents
108+
]
109+
comments = [
110+
{
111+
"content": f"{record_contents[idx]} : {idx}",
112+
"account": "",
113+
"modified_at": int(datetime.now().timestamp()),
114+
}
115+
for idx in range(len(record_contents))
116+
]
117+
118+
resp = requests.patch(
119+
f"{PDNS_API_BASE_URL}/servers/localhost/zones/{zone_name}",
120+
headers=PDNS_HEADERS,
121+
json={
122+
"rrsets": [
123+
{
124+
"name": rr_name,
125+
"type": rr_type,
126+
"changetype": "REPLACE",
127+
"ttl": ttl,
128+
"records": records,
129+
"comments": comments,
130+
},
131+
],
132+
},
133+
)
134+
135+
if resp.status_code != requests.codes.no_content:
136+
raise CustomExceptionServerError(
137+
f"{resp.status_code}: {resp.content.decode('utf-8')}"
138+
) # pragma: no cover
101139

102140
# success
103141
return
104142

143+
144+
def pdns_get_stats():
145+
resp = requests.get(
146+
f"{PDNS_API_BASE_URL}/servers/localhost/statistics",
147+
headers=PDNS_HEADERS,
148+
)
149+
150+
if resp.status_code != 200: # pragma: no cover
151+
logging.error(f"{resp.status_code}: {resp.content.decode('utf-8')}")
152+
return {}
153+
154+
# success
155+
return resp.json()

localcert/domains/subdomain_utils.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
DEFAULT_SPF_POLICY,
1414
)
1515
from .models import Zone, ZoneApiKey
16-
from .pdns import pdns_replace_rrset
16+
from .pdns import pdns_create_zone, pdns_replace_rrset
1717
from .utils import remove_trailing_dot
1818

1919

@@ -71,6 +71,8 @@ def create_instant_subdomain(is_delegate: bool) -> InstantSubdomainCreatedInfo:
7171
new_fqdn = f"{subdomain_name}.{parent_name}"
7272

7373
logging.info(f"Creating instant domain {new_fqdn} for anonymous user")
74+
set_up_pdns_for_zone(new_fqdn, parent_name)
75+
7476
new_zone = Zone.objects.create(
7577
name=new_fqdn,
7678
owner=None,
@@ -84,3 +86,58 @@ def create_instant_subdomain(is_delegate: bool) -> InstantSubdomainCreatedInfo:
8486
password=secret,
8587
)
8688

89+
90+
def set_up_pdns_for_zone(zone_name: str, parent_zone: str):
91+
assert zone_name.endswith("." + parent_zone)
92+
93+
pdns_create_zone(zone_name)
94+
95+
# localhostcert.net has predefined A records locked to localhost
96+
if parent_zone == "localhostcert.net.":
97+
pdns_replace_rrset(zone_name, zone_name, "A", 86400, ["127.0.0.1"])
98+
else:
99+
# Others don't have default A records
100+
assert parent_zone == "localcert.net."
101+
102+
pdns_replace_rrset(zone_name, zone_name, "TXT", 1, [DEFAULT_SPF_POLICY])
103+
pdns_replace_rrset(
104+
zone_name, f"_dmarc.{zone_name}", "TXT", 86400, [DEFAULT_DMARC_POLICY]
105+
)
106+
pdns_replace_rrset(
107+
zone_name, f"*._domainkey.{zone_name}", "TXT", 86400, [DEFAULT_DKIM_POLICY]
108+
)
109+
pdns_replace_rrset(zone_name, zone_name, "MX", 86400, [DEFAULT_MX_RECORD])
110+
111+
pdns_replace_rrset(
112+
zone_name,
113+
zone_name,
114+
"NS",
115+
60,
116+
[
117+
settings.LOCALCERT_PDNS_NS1,
118+
settings.LOCALCERT_PDNS_NS2,
119+
],
120+
)
121+
122+
pdns_replace_rrset(
123+
zone_name,
124+
zone_name,
125+
"SOA",
126+
60,
127+
[
128+
settings.LOCALCERT_PDNS_NS1
129+
+ " soa-admin.robalexdev.com. 0 10800 3600 604800 3600",
130+
],
131+
)
132+
133+
# Delegation from parent zone
134+
pdns_replace_rrset(
135+
parent_zone,
136+
zone_name,
137+
"NS",
138+
60,
139+
[
140+
settings.LOCALCERT_PDNS_NS1,
141+
settings.LOCALCERT_PDNS_NS2,
142+
],
143+
)

0 commit comments

Comments
 (0)