Skip to content

Commit

Permalink
Revert "First pass"
Browse files Browse the repository at this point in the history
This reverts commit ee16aad.
  • Loading branch information
robalexdev committed Nov 23, 2024
1 parent 813e4c3 commit 6ff0f83
Show file tree
Hide file tree
Showing 4 changed files with 254 additions and 80 deletions.
20 changes: 20 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,25 @@ services:
- POSTGRES_PASSWORD
- POSTGRES_USER

pdns:
build: pdns
restart: unless-stopped
ports:
- "53:53/udp"
environment:
- LOCALCERT_PDNS_DB_NAME
- LOCALCERT_PDNS_DEFAULT_SOA_CONTENT
- LOCALCERT_PDNS_HOST
- LOCALCERT_PDNS_WEBSERVER_ALLOW_FROM
- LOCALCERT_SHARED_PDNS_API_KEY
- POSTGRES_PASSWORD
- POSTGRES_USER
networks:
localcert-net:
ipv4_address: 10.33.44.2
depends_on:
- db

web:
build: localcert
restart: unless-stopped
Expand All @@ -36,6 +55,7 @@ services:
ipv4_address: 10.33.44.3
depends_on:
- db
- pdns
labels:
- "traefik.enable=true"
- "traefik.http.routers.localcert-web.rule=(Host(`console.getlocalcert.net`)) || (Host(`api.getlocalcert.net`) && PathPrefix(`/api/`))"
Expand Down
201 changes: 126 additions & 75 deletions localcert/domains/pdns.py
Original file line number Diff line number Diff line change
@@ -1,104 +1,155 @@
import requests
import logging

from .utils import CustomExceptionServerError
from datetime import datetime
from django.conf import settings
from typing import List
from cloudflare import Cloudflare


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


client = Cloudflare(api_token=os.environ.get("CLOUDFLARE_TOKEN"))
def pdns_create_zone(zone: str):
assert zone.endswith(".")

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

# TODO: Some records are set by wildcard, hardcode these
def pdns_describe_domain(domain: str) -> dict:
assert domain.endswith(".")
logging.debug(f"[PDNS] Describe {domain}")
# Create zone in pdns
resp = requests.post(
PDNS_API_BASE_URL + "/servers/localhost/zones",
headers=PDNS_HEADERS,
json={
"name": zone,
"kind": "Native",
},
)
json_resp = resp.json()

if "error" in json_resp.keys():
raise CustomExceptionServerError(json_resp["error"]) # pragma: no cover

# success
return


# TODO use the targeted name/type
def pdns_describe_domain(zone_name: str) -> dict:
assert zone_name.endswith(".")

logging.debug(f"[PDNS] Describe {zone_name}")

# TODO: newer pdns versions can filter by name/type
resp = requests.get(
f"{PDNS_API_BASE_URL}/servers/localhost/zones/{zone_name}",
headers=PDNS_HEADERS,
)
if resp.status_code != requests.codes.ok:
raise CustomExceptionServerError(
f"Unable to describe domain, PDNS error code: {resp.status_code}"
) # pragma: no cover

return resp.json()

for k, v in ZONE_IDS.items():
if domain.endswith(f".{k}")
zone_id = v
break
else:
# Ooops
return {}

# CF doesn't use trailing dot
domain = domain[:-1]

# Two lookups:
# <domain>.<zone> (exact)
# *.<domain>.<zone> (endswith)
results = client.dns.records.list(
zone_id=zone_id,
name={"endswith": f".{domain}"},
type="TXT",
).result
r2 = client.dns.records.list(
zone_id=zone_id,
name={"exact": domain},
type="TXT",
).result
results.extend(r2)

rrsets = []
for result in results:
rrset.append({
"type": "TXT",
"name": result.name,
"content": result.content,
"ttl": result.ttl,
})
return { "rrsets": rrsets }
def pdns_delete_rrset(zone_name: str, rr_name: str, rrtype: str):
assert zone_name.endswith(".")
assert rr_name.endswith(zone_name)
assert rrtype == "TXT"

logging.debug(f"[PDNS] Delete {zone_name} {rr_name} {rrtype}")

resp = requests.patch(
f"{PDNS_API_BASE_URL}/servers/localhost/zones/{zone_name}",
headers=PDNS_HEADERS,
json={
"rrsets": [
{
"name": rr_name,
"type": "TXT",
"changetype": "DELETE",
},
],
},
)

if resp.status_code != requests.codes.no_content:
raise CustomExceptionServerError(f"{resp.status_code}") # pragma: no cover

# success
return


def pdns_replace_rrset(
zone_name: str, rr_name: str, rr_type: str, ttl: int, record_contents: List[str]
):
"""
record_contents - Records from least recently added
"""
assert rr_name.endswith(".")
assert rr_name.endswith(zone_name)
assert rr_type == "TXT"

# CF doesn't use trailing dot
rr_name = rr_name[:-1]

# Collect the existing content
zone_id = ZONE_IDS[zone_name]
results = client.dns.records.list(
zone_id=zone_id,
name=rr_name,
type=rr_type,
).result

for record in results:
if record.content not in record_contents:
# Delete records that are no longer needed
client.dns.records.delete(
zone_id=zone_id,
dns_record_id=record.id,
)
else:
# Don't alter records that already exist
record_contents.remove(record.content)

for content in record_contents:
# Create anything that's new
client.dns.records.create(
zone_id=zone_id,
name=rr_name,
type=rr_type,
content=content,
)
assert rr_type in ["TXT", "A", "MX", "NS", "SOA"]

logging.debug(
f"[PDNS] Replace {zone_name} {rr_name} {rr_type} {ttl} {record_contents}"
)

records = [
{
"content": content,
"disabled": False,
}
for content in record_contents
]
comments = [
{
"content": f"{record_contents[idx]} : {idx}",
"account": "",
"modified_at": int(datetime.now().timestamp()),
}
for idx in range(len(record_contents))
]

resp = requests.patch(
f"{PDNS_API_BASE_URL}/servers/localhost/zones/{zone_name}",
headers=PDNS_HEADERS,
json={
"rrsets": [
{
"name": rr_name,
"type": rr_type,
"changetype": "REPLACE",
"ttl": ttl,
"records": records,
"comments": comments,
},
],
},
)

if resp.status_code != requests.codes.no_content:
raise CustomExceptionServerError(
f"{resp.status_code}: {resp.content.decode('utf-8')}"
) # pragma: no cover

# success
return


def pdns_get_stats():
resp = requests.get(
f"{PDNS_API_BASE_URL}/servers/localhost/statistics",
headers=PDNS_HEADERS,
)

if resp.status_code != 200: # pragma: no cover
logging.error(f"{resp.status_code}: {resp.content.decode('utf-8')}")
return {}

# success
return resp.json()
59 changes: 58 additions & 1 deletion localcert/domains/subdomain_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
DEFAULT_SPF_POLICY,
)
from .models import Zone, ZoneApiKey
from .pdns import pdns_replace_rrset
from .pdns import pdns_create_zone, pdns_replace_rrset
from .utils import remove_trailing_dot


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

logging.info(f"Creating instant domain {new_fqdn} for anonymous user")
set_up_pdns_for_zone(new_fqdn, parent_name)

new_zone = Zone.objects.create(
name=new_fqdn,
owner=None,
Expand All @@ -84,3 +86,58 @@ def create_instant_subdomain(is_delegate: bool) -> InstantSubdomainCreatedInfo:
password=secret,
)


def set_up_pdns_for_zone(zone_name: str, parent_zone: str):
assert zone_name.endswith("." + parent_zone)

pdns_create_zone(zone_name)

# localhostcert.net has predefined A records locked to localhost
if parent_zone == "localhostcert.net.":
pdns_replace_rrset(zone_name, zone_name, "A", 86400, ["127.0.0.1"])
else:
# Others don't have default A records
assert parent_zone == "localcert.net."

pdns_replace_rrset(zone_name, zone_name, "TXT", 1, [DEFAULT_SPF_POLICY])
pdns_replace_rrset(
zone_name, f"_dmarc.{zone_name}", "TXT", 86400, [DEFAULT_DMARC_POLICY]
)
pdns_replace_rrset(
zone_name, f"*._domainkey.{zone_name}", "TXT", 86400, [DEFAULT_DKIM_POLICY]
)
pdns_replace_rrset(zone_name, zone_name, "MX", 86400, [DEFAULT_MX_RECORD])

pdns_replace_rrset(
zone_name,
zone_name,
"NS",
60,
[
settings.LOCALCERT_PDNS_NS1,
settings.LOCALCERT_PDNS_NS2,
],
)

pdns_replace_rrset(
zone_name,
zone_name,
"SOA",
60,
[
settings.LOCALCERT_PDNS_NS1
+ " soa-admin.robalexdev.com. 0 10800 3600 604800 3600",
],
)

# Delegation from parent zone
pdns_replace_rrset(
parent_zone,
zone_name,
"NS",
60,
[
settings.LOCALCERT_PDNS_NS1,
settings.LOCALCERT_PDNS_NS2,
],
)
Loading

0 comments on commit 6ff0f83

Please sign in to comment.