o
    .jQJ                     @   s  d Z ddlZddlmZ ddlmZmZ ddlZddl	Zzddl
ZW n ey/   ddlZY nw ddlmZ eeZG dd deZeG dd	 d	Zd
ededefddZdedeeedB f fddZdededB fddZ	d+dedededeeeeeef  fddZd+dededededB fddZ	d,dedededededB f
d d!Z	"				d-dedededed#ed$ededB fd%d&Z				d.dededed#ed$ededB fd'd(Z 				d.dededed#ed$ededB fd)d*Z!dS )/a3  
RFC 6764 - Locating Services for Calendaring and Contacts (CalDAV/CardDAV)

This module implements DNS-based service discovery for CalDAV and CardDAV
servers as specified in RFC 6764. It allows clients to discover service
endpoints from just a domain name or email address.

Discovery methods (in order of preference):
1. DNS SRV records (_caldavs._tcp / _carddavs._tcp for TLS)
2. DNS TXT records (for path information)
3. Well-Known URIs (/.well-known/caldav or /.well-known/carddav)

SECURITY CONSIDERATIONS:
    DNS-based discovery is vulnerable to attacks if DNS is not secured with DNSSEC:

    - DNS Spoofing: Attackers can provide malicious SRV/TXT records pointing to
      attacker-controlled servers
    - Downgrade Attacks: Malicious DNS can specify non-TLS services, causing
      credentials to be sent in plaintext
    - Man-in-the-Middle: Even with HTTPS, attackers can redirect to their servers

    MITIGATIONS:
    - require_tls=True (DEFAULT): Only accept HTTPS connections, preventing
      downgrade attacks
    - ssl_verify_cert=True (DEFAULT): Verify TLS certificates per RFC 6125
    - Domain validation (RFC 6764 Section 8): Discovered hostnames must be
      in the same domain as the queried domain to prevent redirection attacks
    - Use DNSSEC when possible for DNS integrity
    - Manually verify discovered endpoints for sensitive applications
    - Consider certificate pinning for known domains

    For high-security environments, manual configuration may be preferable to
    automatic discovery.

See: https://datatracker.ietf.org/doc/html/rfc6764
    N)	dataclass)urljoinurlparse)DAVErrorc                   @   s   e Zd ZdZdS )DiscoveryErrorz#Raised when service discovery failsN)__name__
__module____qualname____doc__ r   r   E/home/thesage/.local/lib/python3.10/site-packages/caldav/discovery.pyr   8   s    r   c                   @   s|   e Zd ZU dZeed< eed< eed< eed< eed< dZeed< dZ	eed	< d
Z
eed< dZedB ed< defddZdS )ServiceInfoz5Information about a discovered CalDAV/CardDAV serviceurlhostnameportpathtlsr   priorityweightunknownsourceNusernamereturnc              	   C   s&   d| j  d| j d| j d| j d	S )NzServiceInfo(url=z	, source=z, priority=z, username=))r   r   r   r   )selfr   r   r   __str__L   s   &zServiceInfo.__str__)r   r   r	   r
   str__annotations__intboolr   r   r   r   r   r   r   r   r   r   >   s   
 r   discovered_domainoriginal_domainr   c                 C   s>   |   d}|  d}||krdS |d| rdS dS )a  
    Check if discovered domain is the same as or a subdomain of the original domain.

    This prevents DNS hijacking attacks where malicious DNS records redirect
    to completely different domains (e.g., acme.com -> evil.hackers.are.us).

    Args:
        discovered_domain: The hostname discovered via DNS SRV/TXT or well-known URI
        original_domain: The domain from the user's identifier

    Returns:
        True if discovered_domain is safe (same domain or subdomain), False otherwise

    Examples:
        >>> _is_subdomain_or_same('calendar.example.com', 'example.com')
        True
        >>> _is_subdomain_or_same('example.com', 'example.com')
        True
        >>> _is_subdomain_or_same('evil.com', 'example.com')
        False
        >>> _is_subdomain_or_same('subdomain.calendar.example.com', 'example.com')
        True
        >>> _is_subdomain_or_same('exampleXcom.evil.com', 'example.com')
        False
    .TF)lowerstripendswith)r    r!   
discoveredoriginalr   r   r   _is_subdomain_or_sameP   s   r(   
identifierc                 C   sh   d| v rt | }|jp| dfS d| v r.| d}|d r"|d  nd}|d  }||fS |  dfS )a
  
    Extract domain and optional username from an email address or URL.

    Args:
        identifier: Email address (user@example.com) or domain (example.com)

    Returns:
        A tuple of (domain, username) where username is None if not present

    Examples:
        >>> _extract_domain('user@example.com')
        ('example.com', 'user')
        >>> _extract_domain('example.com')
        ('example.com', None)
        >>> _extract_domain('https://caldav.example.com/path')
        ('caldav.example.com', None)
    ://N@r   )r   r   splitr$   )r)   parsedpartsr   domainr   r   r   _extract_domainz   s   
r1   txt_datac                 C   sF   |   D ]}d|v r | dd\}}|  dkr |   S qdS )a  
    Parse TXT record data to extract the path attribute.

    According to RFC 6764, TXT records contain attribute=value pairs.
    We're looking for the 'path' attribute.

    Args:
        txt_data: TXT record data (e.g., "path=/caldav/")

    Returns:
        The path value or None if not found

    Examples:
        >>> _parse_txt_record('path=/caldav/')
        '/caldav/'
        >>> _parse_txt_record('path=/caldav/ other=value')
        '/caldav/'
    =   r   N)r-   r$   r#   )r2   pairkeyvaluer   r   r   _parse_txt_record   s   r8   Tr0   service_typeuse_tlsc                 C   s  |rdnd}d| | d|  }t d|  zKtj|d}g }|D ]4}t|jd}t|j	}	t|j
}
t|j}t d| d	|	 d
|
 d| d	 |||	|
|f q$|jdd d |W S  tjjtjjtjjfy } zt d| d|  g W  Y d}~S d}~ww )an  
    Perform DNS SRV record lookup.

    Args:
        domain: The domain to query
        service_type: Either 'caldav' or 'carddav'
        use_tls: If True, query for TLS service (_caldavs), else non-TLS (_caldav)

    Returns:
        List of tuples: (hostname, port, priority, weight)
        Sorted by priority (lower is better), then randomized by weight
    s _._tcp.zPerforming SRV lookup for SRVr"   zFound SRV record: :z (priority=z	, weight=r   c                 S   s   | d | d  fS )N      r   )xr   r   r   <lambda>   s    z_srv_lookup.<locals>.<lambda>)r6   zSRV lookup failed for : N)logdebugdnsresolverresolver   targetrstripr   r   r   r   appendsortNXDOMAINNoAnswer	exceptionDNSException)r0   r9   r:   service_suffixsrv_nameanswersresultsrdatar   r   r   r   er   r   r   _srv_lookup   s0   


$rY   c           
   
   C   s   |rdnd}d| | d|  }t d|  z-tj|d}|D ] }ddd |jD }t d	|  t|}|rB|  W S q"W dS  tjjtjj	tj
jfyl }	 zt d
| d|	  W Y d}	~	dS d}	~	ww )a?  
    Perform DNS TXT record lookup to find the service path.

    Args:
        domain: The domain to query
        service_type: Either 'caldav' or 'carddav'
        use_tls: If True, query for TLS service (_caldavs), else non-TLS (_caldav)

    Returns:
        The path from the TXT record, or None if not found
    r;   r<   r=   r>   zPerforming TXT lookup for TXTc                 S   s$   g | ]}t |tr|d n|qS )zutf-8)
isinstancebytesdecode).0r;   r   r   r   
<listcomp>   s   $ z_txt_lookup.<locals>.<listcomp>zFound TXT record: zTXT lookup failed for rE   N)rF   rG   rH   rI   rJ   joinstringsr8   rO   rP   rQ   rR   )
r0   r9   r:   rS   txt_namerU   rW   r2   r   rX   r   r   r   _txt_lookup   s2   
 rc   
   timeoutssl_verify_certc              
   C   sP  d| }d|  | }t d|  zvtj|||dd}|jdv rp|jd}|rpt d|  t||}t|}	|	jp@| }
t	|
| sUt 
d	|  d
|
 d W dS t||
|	jpc|	jdkrbdnd|	jpgd|	jdkddW S |jdkrt d|  t|| d|dddW S W dS  tjjy } zt d|  W Y d}~dS d}~ww )a8  
    Try to discover service via Well-Known URI (RFC 5785).

    According to RFC 6764, if SRV/TXT lookup fails, clients should try:
    - https://domain/.well-known/caldav
    - https://domain/.well-known/carddav

    Security: Redirects to different domains are validated per RFC 6764 Section 8.

    Args:
        domain: The domain to query
        service_type: Either 'caldav' or 'carddav'
        timeout: Request timeout in seconds
        ssl_verify_cert: Whether to verify SSL certificates

    Returns:
        ServiceInfo if successful, None otherwise
    z/.well-known/zhttps://zTrying well-known URI: F)re   verifyallow_redirects)i-  i.  i/  i3  i4  LocationzWell-known URI redirected to: zVRFC 6764 Security: Rejecting well-known redirect to different domain. Queried domain: z, Redirect target: z&. Ignoring this redirect for security.Nhttps  P   /z
well-known)r   r   r   r   r   r      z(Well-known URI is the service endpoint: TzWell-known URI lookup failed: )rF   rG   requestsgetstatus_codeheadersr   r   r   r(   warningr   r   schemer   
exceptionsRequestException)r0   r9   re   rf   well_known_pathr   responselocation	final_urlr.   redirect_hostnamerX   r   r   r   _well_known_lookup  sf   






r|   caldav
prefer_tlsrequire_tlsc                 C   s  |dvrt d| ddt| \}}td| d|  |r(td|  |r3dg}td	 n|r9dd
gnd
dg}td |D ]s}	t|||	}
|
r|
d \}}}}t||shtd| d| d qDt|||	}|swtd d}|	r{dnd}|	rdnd}||kr| d| d| | }n	| d| | }td| d|  t	|||||	||d|d	  S qDtd t
||||}|r||_td| d|j  |S td| d|  d S )!a  
    Discover CalDAV or CardDAV service for a domain or email address.

    This is the main entry point for RFC 6764 service discovery.
    It tries multiple methods in order:
    1. DNS SRV records (with TLS preferred)
    2. DNS TXT records for path information
    3. Well-Known URIs as fallback

    SECURITY WARNING:
        RFC 6764 discovery relies on DNS, which can be spoofed if not using DNSSEC.
        An attacker controlling DNS could:
        - Redirect connections to a malicious server
        - Downgrade from HTTPS to HTTP to capture credentials
        - Perform man-in-the-middle attacks

        By default, require_tls=True prevents HTTP downgrade attacks.
        For production use, consider:
        - Using DNSSEC-validated domains
        - Manual verification of discovered endpoints
        - Pinning certificates for known domains

    Args:
        identifier: Domain name (example.com) or email address (user@example.com)
        service_type: Either 'caldav' or 'carddav'
        timeout: Timeout for HTTP requests in seconds
        ssl_verify_cert: Whether to verify SSL certificates
        prefer_tls: If True, try TLS services first (only used if require_tls=False)
        require_tls: If True (default), ONLY accept TLS connections. This prevents
                     DNS-based downgrade attacks to plaintext HTTP. Set to False
                     only if you explicitly need to support non-TLS servers and
                     trust your DNS infrastructure.

    Returns:
        ServiceInfo object with discovered service details, or None if discovery fails

    Raises:
        DiscoveryError: If service_type is invalid

    Examples:
        >>> info = discover_service('user@example.com', 'caldav')
        >>> if info:
        ...     print(f"Service URL: {info.url}")

        >>> # Allow non-TLS (INSECURE - only for testing)
        >>> info = discover_service('user@example.com', 'caldav', require_tls=False)
    )r}   carddavzInvalid service_type: z. Must be 'caldav' or 'carddav')reasonzDiscovering z service for domain: z$Username extracted from identifier: Tz/require_tls=True: Only attempting TLS discoveryFz:require_tls=False: Allowing non-TLS connections (INSECURE)r   zVRFC 6764 Security: Rejecting SRV record pointing to different domain. Queried domain: z, Target FQDN: z6. This may indicate DNS hijacking or misconfiguration.z$No TXT record found, using root pathrm   rj   httprk   rl   r*   r@   zDiscovered z service via SRV: srv)	r   r   r   r   r   r   r   r   r   z(SRV lookup failed, trying well-known URIz service via well-known URI: zFailed to discover z service for N)r   r1   rF   inforG   rs   rY   r(   rc   r   r|   r   r   )r)   r9   re   rf   r~   r   r0   r   tls_optionsr:   srv_recordsr   r   r   r   r   rt   default_portr   well_known_infor   r   r   discover_serviced  sp   7





.r   c                 C      t | d||||dS )a  
    Convenience function to discover CalDAV service.

    Args:
        identifier: Domain name or email address
        timeout: Timeout for HTTP requests in seconds
        ssl_verify_cert: Whether to verify SSL certificates
        prefer_tls: If True, try TLS services first
        require_tls: If True (default), only accept TLS connections

    Returns:
        ServiceInfo object or None
    r}   r)   r9   re   rf   r~   r   r   r)   re   rf   r~   r   r   r   r   discover_caldav     r   c                 C   r   )a  
    Convenience function to discover CardDAV service.

    Args:
        identifier: Domain name or email address
        timeout: Timeout for HTTP requests in seconds
        ssl_verify_cert: Whether to verify SSL certificates
        prefer_tls: If True, try TLS services first
        require_tls: If True (default), only accept TLS connections

    Returns:
        ServiceInfo object or None
    r   r   r   r   r   r   r   discover_carddav  r   r   )T)rd   T)r}   rd   TTT)rd   TTT)"r
   loggingdataclassesr   urllib.parser   r   dns.exceptionrH   dns.resolverniquestsro   ImportErrorcaldav.lib.errorr   	getLoggerr   rF   r   r   r   r   r(   tupler1   r8   listr   rY   rc   r|   r   r   r   r   r   r   r   <module>   s   %
*"
 0*
U
 
 