Use dnspython for domain TXT verification
This commit is contained in:
@@ -4,7 +4,6 @@ import re
|
|||||||
import secrets
|
import secrets
|
||||||
import socket
|
import socket
|
||||||
import ssl
|
import ssl
|
||||||
import subprocess
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from email.utils import parsedate_to_datetime
|
from email.utils import parsedate_to_datetime
|
||||||
@@ -598,64 +597,53 @@ def _normalise_txt_value(value: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _resolve_txt_with_dnspython(txt_name: str) -> tuple[list[str], str | None]:
|
def _resolve_txt_with_dnspython(txt_name: str) -> tuple[list[str], str | None]:
|
||||||
|
"""Resolve TXT records using the Python DNS resolver.
|
||||||
|
|
||||||
|
Production containers should not depend on OS tools such as nslookup/dig.
|
||||||
|
The package dependency is declared in requirements.txt as dnspython.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
|
import dns.exception # type: ignore
|
||||||
import dns.resolver # type: ignore
|
import dns.resolver # type: ignore
|
||||||
except Exception:
|
|
||||||
return [], "dnspython is not installed"
|
|
||||||
try:
|
|
||||||
answers = dns.resolver.resolve(txt_name, "TXT")
|
|
||||||
values: list[str] = []
|
|
||||||
for answer in answers:
|
|
||||||
try:
|
|
||||||
chunks = [part.decode("utf-8", errors="ignore") if isinstance(part, bytes) else str(part) for part in answer.strings]
|
|
||||||
values.append("".join(chunks))
|
|
||||||
except Exception:
|
|
||||||
values.append(str(answer).strip())
|
|
||||||
return values, None
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return [], str(exc)
|
return [], f"dnspython package is not installed or not importable: {exc}"
|
||||||
|
|
||||||
|
resolver = dns.resolver.Resolver(configure=True)
|
||||||
|
resolver.lifetime = 12.0
|
||||||
|
resolver.timeout = 4.0
|
||||||
|
|
||||||
def _resolve_txt_with_nslookup(txt_name: str) -> tuple[list[str], str | None]:
|
|
||||||
try:
|
try:
|
||||||
completed = subprocess.run(
|
answers = resolver.resolve(txt_name, "TXT")
|
||||||
["nslookup", "-type=TXT", txt_name],
|
except dns.resolver.NXDOMAIN:
|
||||||
capture_output=True,
|
return [], f"TXT name does not exist: {txt_name}"
|
||||||
text=True,
|
except dns.resolver.NoAnswer:
|
||||||
timeout=12,
|
return [], f"No TXT records found for: {txt_name}"
|
||||||
check=False,
|
except dns.resolver.NoNameservers as exc:
|
||||||
)
|
return [], f"DNS nameserver failed for {txt_name}: {exc}"
|
||||||
except FileNotFoundError:
|
except dns.exception.Timeout:
|
||||||
return [], "nslookup command is not available on this machine"
|
return [], f"DNS lookup timed out for: {txt_name}"
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return [], str(exc)
|
return [], f"DNS lookup failed for {txt_name}: {exc}"
|
||||||
|
|
||||||
output = "\n".join([completed.stdout or "", completed.stderr or ""])
|
|
||||||
if completed.returncode != 0 and not output.strip():
|
|
||||||
return [], "DNS lookup failed"
|
|
||||||
|
|
||||||
values: list[str] = []
|
values: list[str] = []
|
||||||
for line in output.splitlines():
|
for answer in answers:
|
||||||
line = line.strip()
|
try:
|
||||||
if not line:
|
# dnspython TXT records may be split into multiple byte chunks.
|
||||||
continue
|
chunks = [
|
||||||
# Windows/Linux nslookup generally prints TXT values inside quotes.
|
part.decode("utf-8", errors="ignore") if isinstance(part, bytes) else str(part)
|
||||||
quoted = re.findall(r'"([^"]+)"', line)
|
for part in getattr(answer, "strings", [])
|
||||||
if quoted:
|
]
|
||||||
values.append("".join(quoted))
|
values.append("".join(chunks) if chunks else answer.to_text().strip())
|
||||||
continue
|
except Exception:
|
||||||
if "text =" in line.lower():
|
values.append(str(answer).strip())
|
||||||
values.append(line.split("=", 1)[1].strip())
|
|
||||||
if not values and output.strip():
|
|
||||||
# Keep a short diagnostic without storing full command noise.
|
|
||||||
return [], output.strip().splitlines()[-1][:240]
|
|
||||||
return values, None
|
return values, None
|
||||||
|
|
||||||
|
|
||||||
def lookup_dns_txt_values(txt_name: str) -> tuple[list[str], str | None]:
|
def lookup_dns_txt_values(txt_name: str) -> tuple[list[str], str | None]:
|
||||||
"""Return TXT records for a DNS name using dnspython when available, else nslookup.
|
"""Return TXT records for a DNS name using dnspython only.
|
||||||
|
|
||||||
No new dependency is required. On Windows, nslookup is normally available by default.
|
This avoids failures in minimal Docker/Coolify containers where the nslookup
|
||||||
|
command is not installed. Install dependencies with `pip install -r requirements.txt`.
|
||||||
"""
|
"""
|
||||||
txt_name = normalize_domain(txt_name)
|
txt_name = normalize_domain(txt_name)
|
||||||
if not txt_name:
|
if not txt_name:
|
||||||
@@ -664,12 +652,7 @@ def lookup_dns_txt_values(txt_name: str) -> tuple[list[str], str | None]:
|
|||||||
values, error = _resolve_txt_with_dnspython(txt_name)
|
values, error = _resolve_txt_with_dnspython(txt_name)
|
||||||
if values:
|
if values:
|
||||||
return [_normalise_txt_value(v) for v in values], None
|
return [_normalise_txt_value(v) for v in values], None
|
||||||
|
return [], error or "No TXT record found"
|
||||||
ns_values, ns_error = _resolve_txt_with_nslookup(txt_name)
|
|
||||||
if ns_values:
|
|
||||||
return [_normalise_txt_value(v) for v in ns_values], None
|
|
||||||
|
|
||||||
return [], ns_error or error or "No TXT record found"
|
|
||||||
|
|
||||||
|
|
||||||
def verify_domain_dns_txt(db: Session, mapping: DomainMapping, *, user_id: int | None) -> DomainDnsVerificationResult:
|
def verify_domain_dns_txt(db: Session, mapping: DomainMapping, *, user_id: int | None) -> DomainDnsVerificationResult:
|
||||||
|
|||||||
+1
-1
@@ -12,6 +12,6 @@ alembic==1.14.0
|
|||||||
psycopg[binary]==3.2.3
|
psycopg[binary]==3.2.3
|
||||||
pydantic[email]
|
pydantic[email]
|
||||||
email-validator
|
email-validator
|
||||||
|
dnspython==2.7.0
|
||||||
openpyxl
|
openpyxl
|
||||||
dnspython
|
|
||||||
itsdangerous
|
itsdangerous
|
||||||
|
|||||||
Reference in New Issue
Block a user