Use dnspython for domain TXT verification
This commit is contained in:
@@ -4,7 +4,6 @@ import re
|
||||
import secrets
|
||||
import socket
|
||||
import ssl
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
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]:
|
||||
"""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:
|
||||
import dns.exception # 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:
|
||||
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:
|
||||
completed = subprocess.run(
|
||||
["nslookup", "-type=TXT", txt_name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=12,
|
||||
check=False,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
return [], "nslookup command is not available on this machine"
|
||||
answers = resolver.resolve(txt_name, "TXT")
|
||||
except dns.resolver.NXDOMAIN:
|
||||
return [], f"TXT name does not exist: {txt_name}"
|
||||
except dns.resolver.NoAnswer:
|
||||
return [], f"No TXT records found for: {txt_name}"
|
||||
except dns.resolver.NoNameservers as exc:
|
||||
return [], f"DNS nameserver failed for {txt_name}: {exc}"
|
||||
except dns.exception.Timeout:
|
||||
return [], f"DNS lookup timed out for: {txt_name}"
|
||||
except Exception as exc:
|
||||
return [], str(exc)
|
||||
|
||||
output = "\n".join([completed.stdout or "", completed.stderr or ""])
|
||||
if completed.returncode != 0 and not output.strip():
|
||||
return [], "DNS lookup failed"
|
||||
return [], f"DNS lookup failed for {txt_name}: {exc}"
|
||||
|
||||
values: list[str] = []
|
||||
for line in output.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
# Windows/Linux nslookup generally prints TXT values inside quotes.
|
||||
quoted = re.findall(r'"([^"]+)"', line)
|
||||
if quoted:
|
||||
values.append("".join(quoted))
|
||||
continue
|
||||
if "text =" in line.lower():
|
||||
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]
|
||||
for answer in answers:
|
||||
try:
|
||||
# dnspython TXT records may be split into multiple byte chunks.
|
||||
chunks = [
|
||||
part.decode("utf-8", errors="ignore") if isinstance(part, bytes) else str(part)
|
||||
for part in getattr(answer, "strings", [])
|
||||
]
|
||||
values.append("".join(chunks) if chunks else answer.to_text().strip())
|
||||
except Exception:
|
||||
values.append(str(answer).strip())
|
||||
return values, 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)
|
||||
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)
|
||||
if values:
|
||||
return [_normalise_txt_value(v) for v in values], None
|
||||
|
||||
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"
|
||||
return [], error or "No TXT record found"
|
||||
|
||||
|
||||
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
|
||||
pydantic[email]
|
||||
email-validator
|
||||
dnspython==2.7.0
|
||||
openpyxl
|
||||
dnspython
|
||||
itsdangerous
|
||||
|
||||
Reference in New Issue
Block a user