Initial commit: core-api service extraction from portainer-core
Build and Push / build (release) Successful in 43s
Build and Push / build (release) Successful in 43s
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
DNS lookup module
|
||||
|
||||
Provides DNS query functionality for the Core API.
|
||||
"""
|
||||
from src.dns.service import DNSService
|
||||
from src.dns.schemas import DNSLookupRequest, DNSLookupResponse, DNSRecord
|
||||
from src.dns.exceptions import DNSQueryError
|
||||
|
||||
__all__ = [
|
||||
"DNSService",
|
||||
"DNSLookupRequest",
|
||||
"DNSLookupResponse",
|
||||
"DNSRecord",
|
||||
"DNSQueryError",
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
DNS-specific exceptions
|
||||
"""
|
||||
|
||||
|
||||
class DNSQueryError(Exception):
|
||||
"""Raised when a DNS query fails"""
|
||||
pass
|
||||
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
Pydantic schemas for DNS lookup module
|
||||
"""
|
||||
from pydantic import Field
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from src.base_schema import BaseSchema
|
||||
|
||||
|
||||
class DNSLookupRequest(BaseSchema):
|
||||
"""Request model for DNS lookup"""
|
||||
|
||||
domain: str = Field(
|
||||
...,
|
||||
description="The domain name to lookup",
|
||||
examples=["example.com", "google.com"],
|
||||
min_length=1,
|
||||
max_length=255
|
||||
)
|
||||
|
||||
record_type: str = Field(
|
||||
default="A",
|
||||
description="DNS record type to query (A, AAAA, MX, TXT, CNAME, NS, SOA, PTR, CAA)",
|
||||
examples=["A", "AAAA", "MX", "TXT", "CNAME"]
|
||||
)
|
||||
|
||||
nameserver: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Optional nameserver to use for the query (e.g., 8.8.8.8, 1.1.1.1)",
|
||||
examples=["8.8.8.8", "1.1.1.1", "9.9.9.9"]
|
||||
)
|
||||
|
||||
|
||||
class DNSRecord(BaseSchema):
|
||||
"""Single DNS record result"""
|
||||
|
||||
value: str = Field(
|
||||
...,
|
||||
description="The DNS record value"
|
||||
)
|
||||
|
||||
ttl: Optional[int] = Field(
|
||||
default=None,
|
||||
description="Time to live in seconds"
|
||||
)
|
||||
|
||||
priority: Optional[int] = Field(
|
||||
default=None,
|
||||
description="Priority (for MX records)"
|
||||
)
|
||||
|
||||
|
||||
class DNSLookupResponse(BaseSchema):
|
||||
"""Response model for DNS lookup"""
|
||||
|
||||
domain: str = Field(
|
||||
...,
|
||||
description="The queried domain name"
|
||||
)
|
||||
|
||||
record_type: str = Field(
|
||||
...,
|
||||
description="DNS record type queried"
|
||||
)
|
||||
|
||||
records: List[DNSRecord] = Field(
|
||||
...,
|
||||
description="List of DNS records found"
|
||||
)
|
||||
|
||||
nameserver_used: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Nameserver used for the query"
|
||||
)
|
||||
|
||||
query_time_ms: float = Field(
|
||||
...,
|
||||
description="Query execution time in milliseconds"
|
||||
)
|
||||
|
||||
queried_at: datetime = Field(
|
||||
...,
|
||||
description="UTC timestamp when query was executed"
|
||||
)
|
||||
|
||||
success: bool = Field(
|
||||
...,
|
||||
description="Whether the query was successful"
|
||||
)
|
||||
|
||||
error_message: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Error message if query failed"
|
||||
)
|
||||
@@ -0,0 +1,216 @@
|
||||
"""
|
||||
DNS Lookup Service
|
||||
|
||||
Provides DNS query functionality using dnspython library.
|
||||
"""
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
import dns.resolver
|
||||
import dns.exception
|
||||
|
||||
from src.logging_config import get_logger
|
||||
from src.dns.schemas import DNSLookupRequest, DNSLookupResponse, DNSRecord
|
||||
from src.dns.exceptions import DNSQueryError
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class DNSService:
|
||||
"""
|
||||
Service for performing DNS lookups
|
||||
|
||||
Uses dnspython for reliable DNS queries with support for
|
||||
various record types and custom nameservers.
|
||||
"""
|
||||
|
||||
# Supported record types
|
||||
SUPPORTED_RECORD_TYPES = [
|
||||
"A", "AAAA", "MX", "TXT", "CNAME", "NS", "SOA", "PTR", "CAA", "SRV"
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize DNS service"""
|
||||
self.resolver = dns.resolver.Resolver()
|
||||
# Set reasonable timeout
|
||||
self.resolver.timeout = 5.0
|
||||
self.resolver.lifetime = 10.0
|
||||
|
||||
async def lookup(self, request: DNSLookupRequest) -> DNSLookupResponse:
|
||||
"""
|
||||
Perform DNS lookup for the specified domain and record type
|
||||
|
||||
Args:
|
||||
request: DNS lookup request with domain, record type, and optional nameserver
|
||||
|
||||
Returns:
|
||||
DNSLookupResponse with query results
|
||||
|
||||
Raises:
|
||||
DNSQueryError: If the DNS query fails
|
||||
"""
|
||||
start_time = time.time()
|
||||
record_type = request.record_type.upper()
|
||||
|
||||
# Validate record type
|
||||
if record_type not in self.SUPPORTED_RECORD_TYPES:
|
||||
raise DNSQueryError(
|
||||
f"Unsupported record type: {record_type}. "
|
||||
f"Supported types: {', '.join(self.SUPPORTED_RECORD_TYPES)}"
|
||||
)
|
||||
|
||||
# Configure nameserver if specified
|
||||
resolver = dns.resolver.Resolver()
|
||||
resolver.timeout = 5.0
|
||||
resolver.lifetime = 10.0
|
||||
|
||||
nameserver_used = None
|
||||
if request.nameserver:
|
||||
resolver.nameservers = [request.nameserver]
|
||||
nameserver_used = request.nameserver
|
||||
logger.info(f"Using custom nameserver: {request.nameserver}")
|
||||
else:
|
||||
nameserver_used = resolver.nameservers[0] if resolver.nameservers else "system"
|
||||
|
||||
try:
|
||||
logger.info(f"Performing DNS lookup: {request.domain} ({record_type})")
|
||||
|
||||
# Perform the DNS query
|
||||
answers = resolver.resolve(request.domain, record_type)
|
||||
|
||||
# Parse results
|
||||
records = []
|
||||
for rdata in answers:
|
||||
record = self._parse_record(rdata, record_type)
|
||||
if record:
|
||||
records.append(record)
|
||||
|
||||
query_time_ms = (time.time() - start_time) * 1000
|
||||
|
||||
logger.info(
|
||||
f"DNS lookup successful: {request.domain} ({record_type}) - "
|
||||
f"Found {len(records)} records in {query_time_ms:.2f}ms"
|
||||
)
|
||||
|
||||
return DNSLookupResponse(
|
||||
domain=request.domain,
|
||||
record_type=record_type,
|
||||
records=records,
|
||||
nameserver_used=nameserver_used,
|
||||
query_time_ms=round(query_time_ms, 2),
|
||||
queried_at=datetime.now(timezone.utc),
|
||||
success=True,
|
||||
error_message=None
|
||||
)
|
||||
|
||||
except dns.resolver.NXDOMAIN:
|
||||
error_msg = f"Domain not found: {request.domain}"
|
||||
logger.warning(error_msg)
|
||||
return self._error_response(request, nameserver_used, start_time, error_msg)
|
||||
|
||||
except dns.resolver.NoAnswer:
|
||||
error_msg = f"No {record_type} records found for {request.domain}"
|
||||
logger.warning(error_msg)
|
||||
return self._error_response(request, nameserver_used, start_time, error_msg)
|
||||
|
||||
except dns.resolver.Timeout:
|
||||
error_msg = f"DNS query timeout for {request.domain}"
|
||||
logger.error(error_msg)
|
||||
return self._error_response(request, nameserver_used, start_time, error_msg)
|
||||
|
||||
except dns.exception.DNSException as e:
|
||||
error_msg = f"DNS error: {str(e)}"
|
||||
logger.error(f"DNS query failed for {request.domain}: {e}")
|
||||
return self._error_response(request, nameserver_used, start_time, error_msg)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unexpected error: {str(e)}"
|
||||
logger.error(f"Unexpected error during DNS lookup: {e}", exc_info=True)
|
||||
return self._error_response(request, nameserver_used, start_time, error_msg)
|
||||
|
||||
def _parse_record(self, rdata, record_type: str) -> Optional[DNSRecord]:
|
||||
"""
|
||||
Parse DNS record data into DNSRecord schema
|
||||
|
||||
Args:
|
||||
rdata: DNS record data from dnspython
|
||||
record_type: Type of DNS record
|
||||
|
||||
Returns:
|
||||
Parsed DNSRecord or None if parsing fails
|
||||
"""
|
||||
try:
|
||||
if record_type == "A" or record_type == "AAAA":
|
||||
return DNSRecord(value=str(rdata), ttl=None)
|
||||
|
||||
elif record_type == "MX":
|
||||
return DNSRecord(
|
||||
value=str(rdata.exchange),
|
||||
priority=rdata.preference,
|
||||
ttl=None
|
||||
)
|
||||
|
||||
elif record_type == "TXT":
|
||||
# TXT records can have multiple strings
|
||||
txt_value = " ".join([s.decode() if isinstance(s, bytes) else str(s) for s in rdata.strings])
|
||||
return DNSRecord(value=txt_value, ttl=None)
|
||||
|
||||
elif record_type in ["CNAME", "NS", "PTR"]:
|
||||
return DNSRecord(value=str(rdata.target), ttl=None)
|
||||
|
||||
elif record_type == "SOA":
|
||||
soa_value = f"mname={rdata.mname} rname={rdata.rname} serial={rdata.serial}"
|
||||
return DNSRecord(value=soa_value, ttl=None)
|
||||
|
||||
elif record_type == "CAA":
|
||||
caa_value = f"{rdata.flags} {rdata.tag.decode() if isinstance(rdata.tag, bytes) else rdata.tag} {rdata.value.decode() if isinstance(rdata.value, bytes) else rdata.value}"
|
||||
return DNSRecord(value=caa_value, ttl=None)
|
||||
|
||||
elif record_type == "SRV":
|
||||
srv_value = f"{rdata.target} port={rdata.port} priority={rdata.priority} weight={rdata.weight}"
|
||||
return DNSRecord(
|
||||
value=srv_value,
|
||||
priority=rdata.priority,
|
||||
ttl=None
|
||||
)
|
||||
|
||||
else:
|
||||
# Fallback for other record types
|
||||
return DNSRecord(value=str(rdata), ttl=None)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse {record_type} record: {e}")
|
||||
return None
|
||||
|
||||
def _error_response(
|
||||
self,
|
||||
request: DNSLookupRequest,
|
||||
nameserver_used: Optional[str],
|
||||
start_time: float,
|
||||
error_message: str
|
||||
) -> DNSLookupResponse:
|
||||
"""
|
||||
Create an error response for failed DNS queries
|
||||
|
||||
Args:
|
||||
request: Original DNS lookup request
|
||||
nameserver_used: Nameserver that was used
|
||||
start_time: Query start time
|
||||
error_message: Error message to include
|
||||
|
||||
Returns:
|
||||
DNSLookupResponse with error details
|
||||
"""
|
||||
query_time_ms = (time.time() - start_time) * 1000
|
||||
|
||||
return DNSLookupResponse(
|
||||
domain=request.domain,
|
||||
record_type=request.record_type.upper(),
|
||||
records=[],
|
||||
nameserver_used=nameserver_used,
|
||||
query_time_ms=round(query_time_ms, 2),
|
||||
queried_at=datetime.now(timezone.utc),
|
||||
success=False,
|
||||
error_message=error_message
|
||||
)
|
||||
Reference in New Issue
Block a user