Files
rengine/tools/dnsx/parser.py
T
Yogesh Ojha 69239333bc Refactor validation and DNS tools, implementation of onboarding wizard
- Added `clean_name` function to `validation.py` for name validation with max length.
- Simplified docstrings in `dnsx` client, parser, and service files for clarity.
- Enhanced error handling in `ViewDNSService` to prevent disabling keys on rate limits.
- Improved WHOIS service to handle concurrent inserts gracefully with integrity checks.
- Updated `Settings` class in worker config to inherit from `BaseAppSettings`.
- Added new dependencies in `pyproject.toml` for `apprise` and `cryptography`.
2026-06-21 21:56:20 +05:30

177 lines
6.1 KiB
Python

from __future__ import annotations
from shared.logging import get_logger
from shared.utils.coerce import safe_str_list, strip_trailing_dot
from tools.dnsx.models import (
DnsxCAAEntry,
DnsxMXEntry,
DnsxReconResponse,
DnsxRecord,
DnsxSOAEntry,
DnsxSRVEntry,
)
logger = get_logger(__name__)
def parse_mx_entries(raw_list: list[str] | None) -> list[DnsxMXEntry]:
"""Parse MX records from dnsx string format."""
entries = []
for raw in safe_str_list(raw_list):
parts = raw.split(None, 1)
if len(parts) == 2: # noqa: PLR2004
try:
priority = int(parts[0])
host = strip_trailing_dot(parts[1])
entries.append(DnsxMXEntry(priority=priority, host=host))
except ValueError:
entries.append(DnsxMXEntry(priority=0, host=strip_trailing_dot(raw)))
elif parts:
entries.append(DnsxMXEntry(priority=0, host=strip_trailing_dot(parts[0])))
return entries
def parse_srv_entries(raw_list: list[str] | None) -> list[DnsxSRVEntry]:
"""Parse SRV records from dnsx string format (priority weight port target)."""
entries = []
for raw in safe_str_list(raw_list):
parts = raw.split(None, 3)
if len(parts) == 4: # noqa: PLR2004
try:
entries.append(
DnsxSRVEntry(
priority=int(parts[0]),
weight=int(parts[1]),
port=int(parts[2]),
target=strip_trailing_dot(parts[3]),
)
)
except ValueError:
logger.debug(f"Skipping malformed SRV record: {raw}")
else:
logger.debug(f"Skipping SRV record with unexpected format: {raw}")
return entries
def parse_soa_entries(raw_list: list | None) -> list[DnsxSOAEntry]:
"""Parse SOA records from dnsx output (dicts or whitespace-delimited strings)."""
if not raw_list:
return []
entries = []
for item in raw_list:
if isinstance(item, dict):
entries.append(
DnsxSOAEntry(
name=strip_trailing_dot(str(item.get("name", ""))),
ns=strip_trailing_dot(str(item.get("ns", ""))),
mailbox=str(item.get("mailbox", "")),
serial=_safe_int(item.get("serial")),
refresh=_safe_int(item.get("refresh")),
retry=_safe_int(item.get("retry")),
expire=_safe_int(item.get("expire")),
minttl=_safe_int(item.get("minttl")),
)
)
elif isinstance(item, str) and item.strip():
parts = item.split()
if len(parts) >= 7: # noqa: PLR2004
entries.append(
DnsxSOAEntry(
ns=strip_trailing_dot(parts[0]),
mailbox=parts[1],
serial=_safe_int(parts[2]),
refresh=_safe_int(parts[3]),
retry=_safe_int(parts[4]),
expire=_safe_int(parts[5]),
minttl=_safe_int(parts[6]),
)
)
else:
logger.debug(f"Skipping SOA with unexpected format: {item}")
return entries
def parse_caa_entries(raw_list: list | None) -> list[DnsxCAAEntry]:
"""Parse CAA records from dnsx output (dicts or whitespace-delimited strings)."""
if not raw_list:
return []
entries = []
for item in raw_list:
if isinstance(item, dict):
entries.append(
DnsxCAAEntry(
flag=_safe_int(item.get("flag")),
tag=str(item.get("tag", "")),
value=str(item.get("value", "")),
)
)
elif isinstance(item, str) and item.strip():
parts = item.split(None, 2)
if len(parts) >= 3: # noqa: PLR2004
entries.append(
DnsxCAAEntry(
flag=_safe_int(parts[0]),
tag=parts[1],
value=parts[2],
)
)
elif len(parts) == 2: # noqa: PLR2004
entries.append(DnsxCAAEntry(tag=parts[0], value=parts[1]))
return entries
def _safe_int(value) -> int:
"""Safely convert to int, defaulting to 0."""
if value is None:
return 0
try:
return int(value)
except (ValueError, TypeError):
return 0
def parse_dnsx_record(raw: dict) -> DnsxReconResponse:
"""Parse a single raw dnsx JSON dict into a typed DnsxReconResponse."""
host = str(raw.get("host", "")).strip()
if not host:
msg = "dnsx record missing 'host' field"
raise ValueError(msg)
if isinstance(raw.get("axfr"), dict):
raw["axfr"] = None
record = DnsxRecord.model_validate(raw)
return DnsxReconResponse(
host=record.host,
a=safe_str_list(record.a),
aaaa=safe_str_list(record.aaaa),
cname=[strip_trailing_dot(c) for c in safe_str_list(record.cname)],
ns=[strip_trailing_dot(n) for n in safe_str_list(record.ns)],
txt=safe_str_list(record.txt),
ptr=[strip_trailing_dot(p) for p in safe_str_list(record.ptr)],
mx=parse_mx_entries(record.mx_raw),
srv=parse_srv_entries(record.srv_raw),
soa=parse_soa_entries(record.soa_raw),
caa=parse_caa_entries(record.caa_raw),
axfr=safe_str_list(record.axfr),
cdn=record.cdn,
cdn_name=record.cdn_name,
status_code=record.status_code,
timestamp=record.timestamp,
)
def parse_dnsx_jsonl(json_records: list[dict]) -> list[DnsxReconResponse]:
"""Parse multiple dnsx JSON records; invalid records are logged and skipped."""
results = []
for i, raw in enumerate(json_records):
try:
results.append(parse_dnsx_record(raw))
except (ValueError, Exception) as e:
host = raw.get("host", "unknown")
logger.warning(f"Failed to parse dnsx record #{i} (host={host}): {e}")
return results