Use dnspython for domain TXT verification

This commit is contained in:
A R R R Associates
2026-07-09 11:08:58 +05:30
parent 94aa8292e2
commit b7c772f6e1
2 changed files with 35 additions and 52 deletions
+34 -51
View File
@@ -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
View File
@@ -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