
    jp                        d Z ddlmZ ddlZddlZddlZddlmZ  ej	        e
          Z ed          Zdd
ZddZddZddZddZdS )uz  Helpers for X-Forwarded-Prefix support.

Mission-control style deploys reverse-proxy the dashboard at a path
prefix (e.g. ``mission-control.tilos.com/hermes/*`` -> dashboard on
:9119), injecting ``X-Forwarded-Prefix: /hermes`` so the backend can
reconstruct prefixed URLs (Location: headers, OAuth redirect_uri,
cookie Path attributes, SPA asset URLs).

This module is also the home of the ``HERMES_DASHBOARD_PUBLIC_URL`` /
``dashboard.public_url`` resolution — when the operator declares a
complete public URL (scheme + host + optional path prefix), we use
that directly for the OAuth ``redirect_uri`` and skip the
X-Forwarded-Prefix reconstruction. Relief valve for deploys where the
proxy header chain isn't reliable.

The single source of truth for both helpers lives here so the gate
middleware, the OAuth routes, the cookie helpers, and the SPA mount
all agree on validation rules.
    )annotationsN)Optional)"'<> 
	rawOptional[str]returnstrc                   | sdS |                                  sdS                     d          sdz                       d          dv s$dv s t          fdt          D                       rdS t                    dk    rdS S )aT  Normalise an X-Forwarded-Prefix header value.

    Returns a string like ``"/hermes"`` (no trailing slash) or ``""``
    when no prefix is set / the header is malformed. We deliberately
    reject anything containing ``..`` or non-printable bytes so a
    hostile proxy can't inject HTML or path-traversal sequences via the
    prefix.
     /z//z..c              3      K   | ]}|v V  	d S N ).0cps     ?/usr/local/lib/hermes-agent/hermes_cli/dashboard_auth/prefix.py	<genexpr>z#normalise_prefix.<locals>.<genexpr>7   s'      --!qAv------    @   )strip
startswithrstripany_REJECT_CHARSlen)r   r   s    @r   normalise_prefixr$   #   s      r		A r<< !G	A		199----}-----  r
1vv{{rHr   c                P    t          | j                            d                    S )zConvenience wrapper that reads the header off a Starlette/FastAPI
    Request and normalises it. Returns ``""`` when no prefix.
    zx-forwarded-prefix)r$   headersget)requests    r   prefix_from_requestr)   ?   s#     GO//0DEEFFFr   c                6   | sdS |                                  sdS t          fdt          D                       rdS 	 t          j                                      }n# t          $ r Y dS w xY w|j        dvrdS |j        sdS 	                    d          S )u  Normalise a ``dashboard.public_url`` value.

    Returns the cleaned URL (scheme://netloc[/path], trailing slash
    removed) on success, or ``""`` when the value is empty, malformed,
    or contains characters that suggest header injection. The caller
    must treat ``""`` as "fall back to request reconstruction" — never
    as "the user explicitly chose no public URL", because the two are
    indistinguishable from an empty env var.
    r   c              3      K   | ]}|v V  	d S r   r   )r   r   urls     r   r   z(_normalise_public_url.<locals>.<genexpr>^   s'      
+
+18
+
+
+
+
+
+r   >   httphttpsr   )
r   r!   r"   urllibparseurlparse
ValueErrorschemenetlocr    )r   parsedr,   s     @r   _normalise_public_urlr6   K   s      r
))++C r
 
+
+
+
+]
+
+
+++ r&&s++   rr}---r= r ::c??s   A! !
A/.A/dictc                 @   	 ddl m}  n# t          $ r i cY S w xY w	  |             }n4# t          $ r'}t                              d|           i cY d}~S d}~ww xY wt          |t                    r|                    d          nd}t          |t                    r|ni S )aY  Return the ``dashboard`` block from ``config.yaml`` if it exists
    and is a dict; otherwise an empty dict.

    Robust to (a) load_config() raising (malformed YAML, IO error,
    config.yaml absent), and (b) ``dashboard`` being absent or non-dict.
    Both shapes fall through to ``{}`` so the caller can rely on
    ``.get(...)`` access.
    r   )load_configzVdashboard-auth.prefix: load_config() raised %s; falling back to env-only configurationN	dashboard)hermes_cli.configr9   	Exception_logdebug
isinstancer7   r'   )r9   cfgexcsections       r   _load_dashboard_sectionrC   m   s    1111111   			kmm   

5	
 	
 	

 						 '1d&;&;Ecggk"""G $//777R7s&   	 
' 
AAAAc                     t           j                            dd          } t          |           }|r|S t	                                          dd          }t          t          |                    S )u  Resolve the operator-declared dashboard public URL.

    Precedence (mirrors ``dashboard.oauth.client_id``):

      1. ``HERMES_DASHBOARD_PUBLIC_URL`` env var (when non-empty after
         strip — empty values are treated as unset so a provisioned-but-
         not-populated Fly secret can't shadow a valid config.yaml entry).
      2. ``dashboard.public_url`` in ``config.yaml``.
      3. Empty string — signals "no override, reconstruct from request"
         to the caller.

    Each candidate value is run through :func:`_normalise_public_url`.
    A malformed env var falls through to the config.yaml entry; a
    malformed config entry falls through to ``""``. This means a typo
    in one surface doesn't prevent the other from working.
    HERMES_DASHBOARD_PUBLIC_URLr   
public_url)osenvironr'   r6   rC   r   )env_raw	env_cleancfg_raws      r   resolve_public_urlrL      sd    " jnn:B??G%g..I %''++L"==G W...r   )r   r   r   r   )r   r   )r   r7   )__doc__
__future__r   loggingrG   urllib.parser/   typingr   	getLogger__name__r=   	frozensetr"   r$   r)   r6   rC   rL   r   r   r   <module>rU      s    & # " " " " "  				          w""
 	EFF   8G G G G   D8 8 8 84/ / / / / /r   