diff --git a/app/modules/domain_management/services.py b/app/modules/domain_management/services.py index 9a89f62..4f504e4 100644 --- a/app/modules/domain_management/services.py +++ b/app/modules/domain_management/services.py @@ -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: diff --git a/requirements.txt b/requirements.txt index e82f580..bd3c841 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,6 @@ alembic==1.14.0 psycopg[binary]==3.2.3 pydantic[email] email-validator +dnspython==2.7.0 openpyxl -dnspython itsdangerous