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

Commit ee16aad

Browse files
committed
First pass
1 parent 831fc4e commit ee16aad

File tree

4 files changed

+80
-254
lines changed

4 files changed

+80
-254
lines changed

docker-compose.yml

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,6 @@ 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-
3415
web:
3516
build: localcert
3617
restart: unless-stopped
@@ -55,7 +36,6 @@ services:
5536
ipv4_address: 10.33.44.3
5637
depends_on:
5738
- db
58-
- pdns
5939
labels:
6040
- "traefik.enable=true"
6141
- "traefik.http.routers.localcert-web.rule=(Host(`console.getlocalcert.net`)) || (Host(`api.getlocalcert.net`) && PathPrefix(`/api/`))"

localcert/domains/pdns.py

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

43
from .utils import CustomExceptionServerError
54
from datetime import datetime
65
from django.conf import settings
76
from typing import List
7+
from cloudflare import Cloudflare
88

99

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",
10+
ZONE_IDS = {
11+
"localcert.net.": "ab2d04b0ccf31906dd87900f0db11f73",
12+
"localhostcert.net.": "ac1335db9f052915b076c0de09e06443",
1413
}
1514

1615

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

20-
logging.debug(f"[PDNS] Create {zone}")
2118

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()
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}")
3223

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()
57-
58-
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
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 {}
8231

83-
# success
84-
return
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 }
8559

8660

8761
def pdns_replace_rrset(
8862
zone_name: str, rr_name: str, rr_type: str, ttl: int, record_contents: List[str]
8963
):
9064
"""
91-
9265
record_contents - Records from least recently added
9366
"""
9467
assert rr_name.endswith(".")
9568
assert rr_name.endswith(zone_name)
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
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+
)
139101

140102
# success
141103
return
142104

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: 1 addition & 58 deletions
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_create_zone, pdns_replace_rrset
16+
from .pdns import pdns_replace_rrset
1717
from .utils import remove_trailing_dot
1818

1919

@@ -71,8 +71,6 @@ 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-
7674
new_zone = Zone.objects.create(
7775
name=new_fqdn,
7876
owner=None,
@@ -86,58 +84,3 @@ def create_instant_subdomain(is_delegate: bool) -> InstantSubdomainCreatedInfo:
8684
password=secret,
8785
)
8886

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)