
    6j6                     .   d Z ddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlmZmZ  ej        e          ZdZdefdZ eh d          ZdZed	z  Zdeeef         fd
Zdeeef         ddfdZ e            Zedz  ZdZdZ deee!e         f         fdZ"deee!e         f         ddfdZ# e"            Z$dede%fdZ&deddfdZ'd Z(d Z)dedz  fdZ*de%fdZ+de%fdZ,defdZ-d Z.de%fdZ/d#dZ0dedz  fdZ1de%fd Z2d#d!Z3d#d"Z4dS )$z
Hermes Web UI -- Optional password authentication.
Off by default. Enable by setting HERMES_WEBUI_PASSWORD env var
or configuring a password in the Settings panel.
    N)	STATE_DIRload_settingsi ' returnc                  \   t          j        dd                                          } |                                 r!t	          |           }d|cxk    rdk    rn n|S t                      }|                    d          }t          |t                    rd|cxk    rdk    rn n|S t          S )a  Resolve session TTL from env > settings > default.

    Priority mirrors get_password_hash(): HERMES_WEBUI_SESSION_TTL env var
    first, then settings.json, falling back to ``SESSION_TTL`` (30 days).
    Clamped to [60s, 1 year] to prevent runaway cookies or self-lockout.
    HERMES_WEBUI_SESSION_TTL <   i3session_ttl_seconds)	osgetenvstripisdigitintr   get
isinstanceSESSION_TTL)env_vvalsvs       /root/hermes-webui/api/auth.py_resolve_session_ttlr      s     I0"55;;==E}} %jj#########JA	#$$A!S bA444444444    >   /login/sw.js/health/favicon.ico/manifest.json/api/auth/login/api/auth/status/manifest.webmanifesthermes_sessionz.sessions.jsonc                     	 t                                           rt          j        t                               d                    } t          | t                    st          d          t          j                    fd| 	                                D             S n2# t          $ r%}t                              d|           Y d}~nd}~ww xY wi S )zLoad persisted sessions from STATE_DIR, pruning expired entries.

    Returns an empty dict on any read or parse error so startup is never
    blocked by a corrupt or missing sessions file.
    utf-8encodingu)   malformed sessions file — expected dictc                     i | ]?\  }}t          |t                    r%t          |t          t          f          r	|k    <||@S  )r   strr   float.0texpnows      r   
<dictcomp>z"_load_sessions.<locals>.<dictcomp>G   sa     [ [ [vq#!!S))[.8sEl.K.K[PSVYPYPY sPYPYPYr   z0Failed to load sessions file, starting fresh: %sN)_SESSIONS_FILEexistsjsonloads	read_textr   dict
ValueErrortimeitems	Exceptionloggerdebug)dataer/   s     @r   _load_sessionsr?   ;   s    	L  "" 	[:n666HHIIDdD)) N !LMMM)++C[ [ [ [ [ [ [ [	[  L L LGKKKKKKKKLIs   BB! !
C+CCsessionsc                 (   	 t          j        dd           t          j        t           d          \  }}	 t	          j        |dd          5 }t          j        | |           ddd           n# 1 swxY w Y   t	          j        |d	           t	          j	        |t                     dS # t          $ r( 	 t	          j        |           n# t          $ r Y nw xY w w xY w# t          $ r&}t                              d
|           Y d}~dS d}~ww xY w)zAtomically persist sessions to STATE_DIR/.sessions.json (0600).

    Uses a temp file + os.replace() so a crash mid-write never leaves a
    truncated file.  Mirrors the same pattern as .signing_key persistence.
    Tparentsexist_okz.sessions.tmpdirsuffixwr$   r%   N  zFailed to persist sessions: %s)r   mkdirtempfilemkstempr   fdopenr3   dumpchmodreplacer1   r:   unlinkOSErrorr;   r<   )r@   fdtmpfr>   s        r   _save_sessionsrV   N   sy   :t4444"yIIIC
	2sW555 '	(A&&&' ' ' ' ' ' ' ' ' ' ' ' ' ' 'HS%   JsN+++++ 	 	 		#   	  : : :5q999999999:sp   4C! B, A0$B, 0A44B, 7A482B, ,
C7CC
CCCCC! !
D+DDz.login_attempts.json   r	   c                  &   	 t                                           rt          j        t                               d                    } t          | t                    st          d          t          j                    i }| 	                                D ]E\  }}t          |t                    rt          |t                    s0fd|D             }|r|||<   F|S n2# t          $ r%}t                              d|           Y d}~nd}~ww xY wi S )zFLoad persisted login attempts from STATE_DIR, pruning expired entries.r$   r%   u/   malformed login-attempts file — expected dictc                     g | ]H}t          |t          t          f          r*t          |          z
  t          k     9t          |          IS r(   )r   r   r*   _LOGIN_WINDOWr,   r-   r/   s     r   
<listcomp>z(_load_login_attempts.<locals>.<listcomp>{   sV       !!c5\22 8;U1XX~7U7U !HH7U7U7Ur   z6Failed to load login attempts file, starting fresh: %sN)_LOGIN_ATTEMPTS_FILEr2   r3   r4   r5   r   r6   r7   r8   r9   r)   listr:   r;   r<   )r=   attemptsip	raw_timesfreshr>   r/   s         @r   _load_login_attemptsrc   o   sE   R&&(( 	:2<<g<NNOODdD)) T !RSSS)++C/1H!% 	) 	)I!"c** *Y2M2M    &  
  )#(HRLO!	"  R R RMqQQQQQQQQRIs   CC 
D)D		Dr_   c                 H   	 t           j                            dd           t          j        t           j        d          \  }}	 t          j        |dd          5 }t          j        | |           ddd           n# 1 swxY w Y   t          j	        |d	           t          j
        |t                      dS # t          $ r( 	 t          j        |           n# t          $ r Y nw xY w w xY w# t          $ r&}t                              d
|           Y d}~dS d}~ww xY w)zKAtomically persist login attempts to STATE_DIR/.login_attempts.json (0600).TrB   z.login_attempts.tmprE   rH   r$   r%   NrI   z$Failed to persist login attempts: %s)r]   parentrJ   rK   rL   r   rM   r3   rN   rO   rP   r:   rQ   rR   r;   r<   )r_   rS   rT   rU   r>   s        r   _save_login_attemptsrf      s   @#))$)FFF"';'BK`aaaC
	2sW555 '	(A&&&' ' ' ' ' ' ' ' ' ' ' ' ' ' 'HS%   Js011111 	 	 		#   	  @ @ @;Q?????????@sr   AC1 B< B 4B<  BB< B2B< <
C.CC.
C)&C.(C))C..C1 1
D!;DD!r`   c                 $   t          j                     t                              | g           }fd|D             }|r|t          | <   nt                              | d           t	          t                     t          |          t          k     S )z2Return True if the IP is allowed to attempt login.c                 0    g | ]}|z
  t           k     |S r(   )rZ   r[   s     r   r\   z%_check_login_rate.<locals>.<listcomp>   s'    ???asQw'>'>'>'>'>r   N)r8   _login_attemptsr   poprf   len_LOGIN_MAX_ATTEMPTS)r`   r_   r/   s     @r   _check_login_raterm      s    
)++C""2r**H????8???H &&B%%%)))x==...r   c                     t          j                     }t                              | g           }|                    |           |t          | <   t	          t                     d S )N)r8   ri   r   appendrf   )r`   r/   r_   s      r   _record_login_attemptrp      sR    
)++C""2r**HOOC"OB)))))r   c                     t           dz  } 	 |                                 r1|                                 }t          |          dk    r
|dd         S n*# t          $ r t
                              d           Y nw xY wt          j        d          }	 t          j	        dd           | 
                    |           |                     d           n*# t          $ r t
                              d           Y nw xY w|S )	zIReturn a random signing key, generating and persisting one on first call.z.signing_key    Nz>Failed to read or access signing key file, using in-memory keyTrB   rI   z7Failed to persist signing key, using in-memory key only)r   r2   
read_bytesrk   r:   r;   r<   secretstoken_bytesrJ   write_bytesrO   )key_filerawkeys      r   _signing_keyrz      s   >)HW?? 	 %%''C3xx2~~3B3x W W WUVVVVVW 
b
!
!CPt4444S!!!u P P PNOOOOOPJs%   AA $A98A9A C $C98C9c                     t                      }t          j        d|                                 |d          }|                                S )aS  PBKDF2-SHA256 with 600k iterations (OWASP recommendation).
    Salt is the persisted random signing key, which is secret and unique per
    installation. This keeps the stored hash format a plain hex string
    (no format change to settings.json) while replacing the predictable
    STATE_DIR-derived salt from the original implementation.sha256i'	 )rz   hashlibpbkdf2_hmacencodehex)passwordsaltdks      r   _hash_passwordr      s:     >>D		Xx'8'8$	H	HB6688Or   c                      t          j        dd                                          } | rt          |           S t	                      }|                    d          pdS )zdReturn the active password hash, or None if auth is disabled.
    Priority: env var > settings.json.HERMES_WEBUI_PASSWORDr   password_hashN)r   r   r   r   r   r   )env_pwsettingss     r   get_password_hashr      sX     Y.3399;;F &f%%%H<<((0D0r   c                  "    t                      duS )z7True if a password is configured (env var or settings).N)r   r(   r   r   is_auth_enabledr      s    d**r   c                 j    t                      }|sdS t          j        t          |           |          S )z4Verify a plaintext password against the stored hash.F)r   hmaccompare_digestr   )plainexpecteds     r   verify_passwordr      s5     ""H u~e44h???r   c                  d   t          j        d          } t          j                    t                      z   t          | <   t          t                     t          j        t                      | 	                                t          j                                                  dd         }|  d| S )z7Create a new auth session. Returns signed cookie value.rr   N.)rt   	token_hexr8   r   	_sessionsrV   r   newrz   r   r}   r|   	hexdigest)tokensigs     r   create_sessionr      s    b!!Ey{{%9%;%;;Ie9
(<>>5<<>>7>
B
B
L
L
N
NsPRs
SCcr   c                      t          j                     fdt                                          D             } | r6| D ]}t                              |d           t	          t                     dS dS )zFRemove all expired session entries to prevent unbounded memory growth.c                 &    g | ]\  }}|k    |S r(   r(   r+   s      r   r\   z+_prune_expired_sessions.<locals>.<listcomp>   s"    >>>VQC#IIqIIIr   N)r8   r   r9   rj   rV   )expiredr   r/   s     @r   _prune_expired_sessionsr      s}    
)++C>>>>y00>>>G " 	' 	'EMM%&&&&y!!!!!" "r   c                    | rd| vrdS t                       |                     dd          \  }}t          j        t	                      |                                t          j                                                  dd         }t          j	        ||          sdS t                              |          }|rt          j                    |k    rt                              |d           dS dS )zFVerify a signed session cookie. Returns True if valid and not expired.r   F   Nrr   T)r   rsplitr   r   rz   r   r}   r|   r   r   r   r   r8   rj   )cookie_valuer   r   expected_sigexpirys        r   verify_sessionr      s     3l22u$$S!,,JE38LNNELLNNGNKKUUWWX[Y[X[\LsL11 u]]5!!F TY[[6))eT"""u4r   c                     | rZd| v rX|                      dd          d         }|t          v r5t                              |d           t          t                     dS dS dS dS )zRemove a session token.r   r   r   N)r   r   rj   rV   )r   r   s     r   invalidate_sessionr     sw     &|++##C++A.IMM%&&&9%%%%%	& &++r   c                 *   | j                             dd          }|sdS t          j                                        }	 |                    |           n# t          j        j        $ r Y dS w xY w|                    t                    }|r|j        ndS )z1Extract the auth cookie from the request headers.Cookier   N)	headersr   httpcookiesSimpleCookieloadCookieErrorCOOKIE_NAMEvalue)handlercookie_headercookiemorsels       r   parse_cookier     s    O''"55M t\&&((FM""""<#   ttZZ$$F!+6<<t+s   A A-,A-c                    t                      sdS |j        t          v s4|j                            d          s|j                            d          rdS t	          |           }|rt          |          rdS |j                            d          rZ|                     d           |                     dd           |                                  | j	        
                    d           n||                     d	           d
dlm} |j        pd}|j        r|d|j        z   z  }|                    |d          }|                     dd|z              |                                  dS )zCheck if request is authorized. Returns True if OK.
    If not authorized, sends 401 (API) or 302 redirect (page) and returns False.Tz/static/z/session/static/z/api/i  zContent-Typezapplication/jsons#   {"error":"Authentication required"}i.  r   N/?)safeLocationzlogin?next=F)r   pathPUBLIC_PATHS
startswithr   r   send_responsesend_headerend_headerswfilewriteurllib.parseparsequeryquote)r   parsed
cookie_val	_urlparse_path_with_query_nexts         r   
check_authr   &  s     t{l""fk&<&<Z&H&H"FKLbLbcuLvLv"tg&&J nZ00 t{g&& "c"""N,>???BCCCCc"""( 	)(((((!;-#< 	3fl 22  0s;;J(=>>>5r   c                    t           j                                        }||t          <   d|t                   d<   d|t                   d<   d|t                   d<   t	          t                                |t                   d<   t          | j        dd	          | j        	                    d
d          dk    rd|t                   d<   | 
                    d|t                                                              d	S )z$Set the auth cookie on the response.ThttponlyLaxsamesiter   r   max-agegetpeercertNzX-Forwarded-Protor   httpssecure
Set-Cookie)r   r   r   r   r)   r   getattrrequestr   r   r   OutputString)r   r   r   s      r   set_auth_cookier   Y  s    \&&((F&F;&*F;
#&+F;
#"%F;%()=)?)?%@%@F;	"wt44@GODWDWXkmoDpDpt{D{D{(,{H%f[&9&F&F&H&HIIIIIr   c                    t           j                                        }d|t          <   d|t                   d<   d|t                   d<   d|t                   d<   |                     d|t                                                              d	S )
z&Clear the auth cookie on the response.r   Tr   r   r   0r   r   N)r   r   r   r   r   r   )r   r   s     r   clear_auth_cookier   g  sy    \&&((FF;&*F;
#"%F;%(F;	"f[&9&F&F&H&HIIIIIr   )r   N)5__doc__r}   r   http.cookiesr   r3   loggingr   rt   rK   r8   
api.configr   r   	getLogger__name__r;   r   r   r   	frozensetr   r   r1   r6   r)   r*   r?   rV   r   r]   rl   rZ   r^   rc   rf   ri   boolrm   rp   rz   r   r   r   r   r   r   r   r   r   r   r   r   r(   r   r   <module>r      sW   
         				    / / / / / / / /		8	$	$ c    ( y      --S%Z(    &:T#u*- :$ : : : :2 N	 !#99  d3U#34    2@4T%[(8#9 @d @ @ @ @( '&((/# /$ / / / /*c *d * * * *  *  13: 1 1 1 1+ + + + +
@d @ @ @ @    " " "D     & & & &,S4Z , , , ,04 0 0 0 0fJ J J JJ J J J J Jr   