users need always login
This commit is contained in:
+18
-15
@@ -37,7 +37,7 @@ there is no browser-side persistence or independent playback state.
|
|||||||
flowchart TB
|
flowchart TB
|
||||||
Main[main.rkt<br/>configuration and lifecycle]
|
Main[main.rkt<br/>configuration and lifecycle]
|
||||||
Server[private/server.rkt<br/>HTTP adapter]
|
Server[private/server.rkt<br/>HTTP adapter]
|
||||||
Users[private/users.rkt<br/>users, networks and sessions]
|
Users[private/users.rkt<br/>users, trusted proxies and sessions]
|
||||||
Player[private/player.rkt<br/>application state and commands]
|
Player[private/player.rkt<br/>application state and commands]
|
||||||
Playlists[private/playlists.rkt<br/>durable playlist tabs]
|
Playlists[private/playlists.rkt<br/>durable playlist tabs]
|
||||||
DLNAAdapter[private/dlna-playback.rkt<br/>playlist transition orchestration]
|
DLNAAdapter[private/dlna-playback.rkt<br/>playlist transition orchestration]
|
||||||
@@ -241,16 +241,19 @@ on every render.
|
|||||||
### 3.7 User authentication
|
### 3.7 User authentication
|
||||||
|
|
||||||
Authentication is enabled by adding Argon2id password hashes under `[users]`.
|
Authentication is enabled by adding Argon2id password hashes under `[users]`.
|
||||||
Requests whose effective client address belongs to a configured local network
|
All browser clients then authenticate, including clients on the local network.
|
||||||
bypass login. A forwarded address is accepted only when the direct peer belongs
|
A forwarded address is accepted only when the direct peer belongs to
|
||||||
to `[authentication] trusted-proxies`; the rightmost `X-Forwarded-For` value is
|
`[authentication] trusted-proxies`; the rightmost `X-Forwarded-For` value is
|
||||||
used so an untrusted client cannot prepend a local address.
|
used so an untrusted client cannot prepend a forged address.
|
||||||
|
|
||||||
Successful logins create opaque 256-bit session tokens. Only the token is sent
|
Successful logins create opaque 256-bit session tokens. Only the token is sent
|
||||||
to the browser in a `Secure`, `HttpOnly`, `SameSite=Strict` cookie; server-side
|
to the browser in a `Secure`, `HttpOnly`, `SameSite=Strict` cookie; server-side
|
||||||
session state has an idle timeout and is intentionally volatile. Login failures
|
session state has an idle timeout and is intentionally volatile. Login failures
|
||||||
are rate-limited per effective client address. Agent endpoints are outside user
|
are rate-limited per effective client address. Authenticated requests move the
|
||||||
sessions and retain their separate application-ID authorization.
|
server-side idle deadline. Once half the configured lifetime has elapsed, the
|
||||||
|
cookie is reissued with a fresh lifetime; this avoids a `Set-Cookie` header on
|
||||||
|
every one-second state poll. Agent endpoints are outside user sessions and
|
||||||
|
retain their separate application-ID authorization.
|
||||||
|
|
||||||
## 4. Key runtime flows
|
## 4. Key runtime flows
|
||||||
|
|
||||||
@@ -334,7 +337,7 @@ file:
|
|||||||
setting remains supported);
|
setting remains supported);
|
||||||
- allowed 256-bit playback-agent IDs under `[playback-agents]`;
|
- allowed 256-bit playback-agent IDs under `[playback-agents]`;
|
||||||
- the optional playlist-keystore override under `[player]`;
|
- the optional playlist-keystore override under `[player]`;
|
||||||
- Argon2id user hashes, local networks, trusted proxies, and session timeout.
|
- Argon2id user hashes, trusted proxies, and session timeout.
|
||||||
|
|
||||||
Command-line network settings override INI values. Library paths from both
|
Command-line network settings override INI values. Library paths from both
|
||||||
sources are combined and de-duplicated.
|
sources are combined and de-duplicated.
|
||||||
@@ -345,13 +348,13 @@ and sessions still reset when the process restarts.
|
|||||||
|
|
||||||
## 7. Security and operational boundaries
|
## 7. Security and operational boundaries
|
||||||
|
|
||||||
The browser API has optional user authentication but no TLS termination or
|
The browser API has optional user authentication but no TLS termination.
|
||||||
per-user player state. Authentication must be enabled before exposing it to an
|
Authentication must be enabled before exposing it to an untrusted network, and
|
||||||
untrusted network, and a reverse proxy must provide HTTPS because session
|
a reverse proxy must provide HTTPS because session cookies are always marked
|
||||||
cookies are always marked `Secure`. Local-network bypass and forwarded client
|
`Secure`. Forwarded client addresses are security-sensitive configuration:
|
||||||
addresses are security-sensitive configuration: only known reverse-proxy peers
|
only known reverse-proxy peers may be trusted, and the application port should
|
||||||
may be trusted, and the application port should remain firewalled from the
|
remain firewalled from the internet. Playlists are isolated by username, while
|
||||||
internet.
|
renderer selection and transport state remain shared.
|
||||||
|
|
||||||
Playback-agent registration and polling are authorized against a default-deny
|
Playback-agent registration and polling are authorized against a default-deny
|
||||||
INI allowlist. Media URLs additionally contain an opaque per-track token. The
|
INI allowlist. Media URLs additionally contain an opaque per-track token. The
|
||||||
|
|||||||
@@ -41,9 +41,8 @@ podcasts=D:\Podcasts
|
|||||||
7b4776ef27104e8eb9f7ea2c622ce76ca23de4260b0f94e6880d321017b32a0e4=true
|
7b4776ef27104e8eb9f7ea2c622ce76ca23de4260b0f94e6880d321017b32a0e4=true
|
||||||
|
|
||||||
[authentication]
|
[authentication]
|
||||||
local-networks=127.0.0.0/8;::1/128;10.0.0.0/8;172.16.0.0/12;192.168.0.0/16
|
|
||||||
trusted-proxies=127.0.0.0/8;::1/128
|
trusted-proxies=127.0.0.0/8;::1/128
|
||||||
session-seconds=43200
|
session-seconds=604800
|
||||||
|
|
||||||
[users]
|
[users]
|
||||||
hans=$argon2id$v=19$m=19456,t=2,p=1$...
|
hans=$argon2id$v=19$m=19456,t=2,p=1$...
|
||||||
@@ -57,8 +56,8 @@ ondersteund. Playlisttabs worden standaard opgeslagen in de keystore
|
|||||||
`playlist-keystore=...` onder `[player]` kan desgewenst een ander pad worden
|
`playlist-keystore=...` onder `[player]` kan desgewenst een ander pad worden
|
||||||
gebruikt.
|
gebruikt.
|
||||||
|
|
||||||
Zodra `[users]` minstens één gebruiker bevat, toont de webinterface voor
|
Zodra `[users]` minstens één gebruiker bevat, moeten alle browserclients
|
||||||
niet-lokale clients een eigen loginvenster. Wachtwoorden staan uitsluitend als
|
inloggen, zowel lokaal als via internet. Wachtwoorden staan uitsluitend als
|
||||||
Argon2id-hash in de INI. Maak zo'n hash vanuit Racket:
|
Argon2id-hash in de INI. Maak zo'n hash vanuit Racket:
|
||||||
|
|
||||||
```racket
|
```racket
|
||||||
@@ -69,7 +68,6 @@ Argon2id-hash in de INI. Maak zo'n hash vanuit Racket:
|
|||||||
(displayln (make-password-hash "een lang en uniek wachtwoord"))
|
(displayln (make-password-hash "een lang en uniek wachtwoord"))
|
||||||
```
|
```
|
||||||
|
|
||||||
`local-networks` bepaalt welke clients zonder login mogen werken.
|
|
||||||
`trusted-proxies` bepaalt uitsluitend van welke directe peers de laatste
|
`trusted-proxies` bepaalt uitsluitend van welke directe peers de laatste
|
||||||
`X-Forwarded-For`-waarde wordt geaccepteerd. Laat die lijst zo klein mogelijk;
|
`X-Forwarded-For`-waarde wordt geaccepteerd. Laat die lijst zo klein mogelijk;
|
||||||
bij Apache op dezelfde machine zijn loopbackadressen voldoende. Zonder
|
bij Apache op dezelfde machine zijn loopbackadressen voldoende. Zonder
|
||||||
@@ -108,21 +106,25 @@ tab verdwijnt daarbij ook uit de keystore. Voor iedere gebruiker bevat de key
|
|||||||
GUID-key staan de naam en tracks van die playlist. Tracks uit verschillende
|
GUID-key staan de naam en tracks van die playlist. Tracks uit verschillende
|
||||||
geconfigureerde libraries mogen in dezelfde playlist staan; ontbrekende of
|
geconfigureerde libraries mogen in dezelfde playlist staan; ontbrekende of
|
||||||
buiten de libraries gelegen bestanden worden bij het laden overgeslagen.
|
buiten de libraries gelegen bestanden worden bij het laden overgeslagen.
|
||||||
`local`, `anonymous` en iedere aangemelde gebruiker hebben daarbij een eigen
|
Iedere aangemelde gebruiker heeft daarbij een eigen playlistverzameling. Als
|
||||||
playlistverzameling. Het fysieke afspeelpunt en de transportbediening blijven
|
authenticatie is uitgeschakeld, wordt de verzameling van `anonymous` gebruikt.
|
||||||
gedeeld; een playlistcommando van een andere gebruiker neemt die speler over.
|
Het fysieke afspeelpunt en de transportbediening blijven gedeeld; een
|
||||||
|
playlistcommando van een andere gebruiker neemt die speler over. Een oudere
|
||||||
|
`playlists-for-local`-verzameling blijft in de keystore staan, maar wordt niet
|
||||||
|
automatisch aan een gebruiker toegewezen.
|
||||||
|
|
||||||
De server luistert standaard alleen op localhost. Geef alleen bewust een
|
De server luistert standaard alleen op localhost. Geef alleen bewust een
|
||||||
LAN-adres aan `--listen-ip`. Configureer gebruikersauthenticatie voordat de
|
LAN-adres aan `--listen-ip`. Configureer gebruikersauthenticatie voordat de
|
||||||
webinterface via een publiek bereikbare reverse proxy wordt aangeboden.
|
webinterface via een publiek bereikbare reverse proxy wordt aangeboden.
|
||||||
|
|
||||||
Externe gebruikers krijgen na succesvolle aanmelding een willekeurige 256-bit
|
Gebruikers krijgen na succesvolle aanmelding een willekeurige 256-bit
|
||||||
sessiecookie met `Secure`, `HttpOnly` en `SameSite=Strict`. Sessies verlopen na
|
sessiecookie met `Secure`, `HttpOnly` en `SameSite=Strict`. De standaard
|
||||||
de ingestelde inactiviteitsduur en worden niet over een serverherstart heen
|
inactiviteitsduur is zeven dagen. Geldig gebruik verschuift de servertermijn;
|
||||||
bewaard. Na vijf mislukte pogingen vanaf hetzelfde clientadres wordt aanmelden
|
halverwege de termijn wordt ook de browsercookie opnieuw voor zeven dagen
|
||||||
vijf minuten geblokkeerd. De `/api/agent/*`-routes gebruiken geen
|
uitgegeven. Sessies worden niet over een serverherstart heen bewaard. Na vijf
|
||||||
gebruikerssessie: daarvoor blijft de afzonderlijke playback-agent-allowlist
|
mislukte pogingen vanaf hetzelfde clientadres wordt aanmelden vijf minuten
|
||||||
gelden.
|
geblokkeerd. De `/api/agent/*`-routes gebruiken geen gebruikerssessie: daarvoor
|
||||||
|
blijft de afzonderlijke playback-agent-allowlist gelden.
|
||||||
|
|
||||||
## Windows playback agent
|
## Windows playback agent
|
||||||
|
|
||||||
|
|||||||
@@ -55,14 +55,9 @@
|
|||||||
(define/contract (run-web-player music-paths
|
(define/contract (run-web-player music-paths
|
||||||
#:allowed-agent-ids [allowed-agent-ids '()]
|
#:allowed-agent-ids [allowed-agent-ids '()]
|
||||||
#:users [users '()]
|
#:users [users '()]
|
||||||
#:local-networks
|
|
||||||
[local-networks
|
|
||||||
'("127.0.0.0/8" "::1/128"
|
|
||||||
"10.0.0.0/8" "172.16.0.0/12"
|
|
||||||
"192.168.0.0/16")]
|
|
||||||
#:trusted-proxies
|
#:trusted-proxies
|
||||||
[trusted-proxies '("127.0.0.0/8" "::1/128")]
|
[trusted-proxies '("127.0.0.0/8" "::1/128")]
|
||||||
#:session-seconds [session-seconds 43200]
|
#:session-seconds [session-seconds 604800]
|
||||||
#:listen-ip [listen-ip "127.0.0.1"]
|
#:listen-ip [listen-ip "127.0.0.1"]
|
||||||
#:port [port 8080]
|
#:port [port 8080]
|
||||||
#:dlna-port [dlna-port 8734]
|
#:dlna-port [dlna-port 8734]
|
||||||
@@ -72,7 +67,6 @@
|
|||||||
(->* ((listof library-spec/c))
|
(->* ((listof library-spec/c))
|
||||||
(#:allowed-agent-ids (listof string?)
|
(#:allowed-agent-ids (listof string?)
|
||||||
#:users (listof (cons/c string? string?))
|
#:users (listof (cons/c string? string?))
|
||||||
#:local-networks (listof string?)
|
|
||||||
#:trusted-proxies (listof string?)
|
#:trusted-proxies (listof string?)
|
||||||
#:session-seconds exact-positive-integer?
|
#:session-seconds exact-positive-integer?
|
||||||
#:listen-ip string?
|
#:listen-ip string?
|
||||||
@@ -88,7 +82,6 @@
|
|||||||
#:dlna-port dlna-port))
|
#:dlna-port dlna-port))
|
||||||
(auth-manager
|
(auth-manager
|
||||||
(make-auth-manager users
|
(make-auth-manager users
|
||||||
#:local-networks local-networks
|
|
||||||
#:trusted-proxies trusted-proxies
|
#:trusted-proxies trusted-proxies
|
||||||
#:session-seconds session-seconds)))
|
#:session-seconds session-seconds)))
|
||||||
(info-rkt-web-player
|
(info-rkt-web-player
|
||||||
@@ -179,19 +172,13 @@
|
|||||||
#:when (string? (cdr entry)))
|
#:when (string? (cdr entry)))
|
||||||
entry))
|
entry))
|
||||||
|
|
||||||
(define local-networks
|
|
||||||
(configuration-list
|
|
||||||
(ini-get config 'authentication 'local-networks "")
|
|
||||||
'("127.0.0.0/8" "::1/128"
|
|
||||||
"10.0.0.0/8" "172.16.0.0/12" "192.168.0.0/16")))
|
|
||||||
|
|
||||||
(define trusted-proxies
|
(define trusted-proxies
|
||||||
(configuration-list
|
(configuration-list
|
||||||
(ini-get config 'authentication 'trusted-proxies "")
|
(ini-get config 'authentication 'trusted-proxies "")
|
||||||
'("127.0.0.0/8" "::1/128")))
|
'("127.0.0.0/8" "::1/128")))
|
||||||
|
|
||||||
(define session-seconds
|
(define session-seconds
|
||||||
(ini-get config 'authentication 'session-seconds 43200))
|
(ini-get config 'authentication 'session-seconds 604800))
|
||||||
|
|
||||||
(define all-libraries
|
(define all-libraries
|
||||||
(append configured-libraries
|
(append configured-libraries
|
||||||
@@ -203,7 +190,6 @@
|
|||||||
all-libraries
|
all-libraries
|
||||||
#:allowed-agent-ids allowed-agent-ids
|
#:allowed-agent-ids allowed-agent-ids
|
||||||
#:users configured-users
|
#:users configured-users
|
||||||
#:local-networks local-networks
|
|
||||||
#:trusted-proxies trusted-proxies
|
#:trusted-proxies trusted-proxies
|
||||||
#:session-seconds session-seconds
|
#:session-seconds session-seconds
|
||||||
#:listen-ip (or listen-ip
|
#:listen-ip (or listen-ip
|
||||||
|
|||||||
+10
-10
@@ -1126,7 +1126,7 @@
|
|||||||
(string-downcase app-id)))
|
(string-downcase app-id)))
|
||||||
(define store (open-playlist-store playlist-keystore))
|
(define store (open-playlist-store playlist-keystore))
|
||||||
(define stored-tabs
|
(define stored-tabs
|
||||||
(load-user-playlists store "local" libraries))
|
(load-user-playlists store "anonymous" libraries))
|
||||||
(let* ((library (and (pair? libraries) (car libraries)))
|
(let* ((library (and (pair? libraries) (car libraries)))
|
||||||
(browser-entries
|
(browser-entries
|
||||||
(if library
|
(if library
|
||||||
@@ -1142,7 +1142,7 @@
|
|||||||
(selected-index 0)
|
(selected-index 0)
|
||||||
(contexts (make-hash))
|
(contexts (make-hash))
|
||||||
(initial-context (playlist-context tabs selected-index)))
|
(initial-context (playlist-context tabs selected-index)))
|
||||||
(hash-set! contexts "local" initial-context)
|
(hash-set! contexts "anonymous" initial-context)
|
||||||
(define value
|
(define value
|
||||||
(player libraries
|
(player libraries
|
||||||
(remove-duplicates normalized-agent-ids string=?)
|
(remove-duplicates normalized-agent-ids string=?)
|
||||||
@@ -1175,7 +1175,7 @@
|
|||||||
(make-hash)
|
(make-hash)
|
||||||
store
|
store
|
||||||
contexts
|
contexts
|
||||||
"local"
|
"anonymous"
|
||||||
dlna-port))
|
dlna-port))
|
||||||
value))
|
value))
|
||||||
|
|
||||||
@@ -1185,7 +1185,7 @@
|
|||||||
; post : Cached DLNA playback information has been incorporated.
|
; post : Cached DLNA playback information has been incorporated.
|
||||||
; result : A JSON-compatible hash.
|
; result : A JSON-compatible hash.
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
(define (player-state->jsexpr value #:username [username "local"])
|
(define (player-state->jsexpr value #:username [username "anonymous"])
|
||||||
(define normalized (normal-playlist-username username))
|
(define normalized (normal-playlist-username username))
|
||||||
(call-with-semaphore
|
(call-with-semaphore
|
||||||
(player-command-lock value)
|
(player-command-lock value)
|
||||||
@@ -1264,7 +1264,7 @@
|
|||||||
"playlist-clear" "tab-add" "tab-select" "tab-rename"
|
"playlist-clear" "tab-add" "tab-select" "tab-rename"
|
||||||
"tab-delete" "play"))
|
"tab-delete" "play"))
|
||||||
|
|
||||||
(define (player-command! value command data #:username [username "local"])
|
(define (player-command! value command data #:username [username "anonymous"])
|
||||||
(define normalized (normal-playlist-username username))
|
(define normalized (normal-playlist-username username))
|
||||||
(call-with-semaphore
|
(call-with-semaphore
|
||||||
(player-command-lock value)
|
(player-command-lock value)
|
||||||
@@ -1488,7 +1488,7 @@
|
|||||||
; post : Player state remains unchanged.
|
; post : Player state remains unchanged.
|
||||||
; result : Artwork bytes and MIME type, or #f when unavailable.
|
; result : Artwork bytes and MIME type, or #f when unavailable.
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
(define (player-track-artwork value artwork-id #:username [username "local"])
|
(define (player-track-artwork value artwork-id #:username [username "anonymous"])
|
||||||
(define item
|
(define item
|
||||||
(call-with-semaphore
|
(call-with-semaphore
|
||||||
(player-command-lock value)
|
(player-command-lock value)
|
||||||
@@ -1757,10 +1757,10 @@
|
|||||||
"tab-rename"
|
"tab-rename"
|
||||||
(hasheq 'index 1 'name "Hans favorieten")
|
(hasheq 'index 1 'name "Hans favorieten")
|
||||||
#:username "hans")
|
#:username "hans")
|
||||||
(define local-after-hans
|
(define anonymous-after-hans
|
||||||
(player-state->jsexpr example-player #:username "local"))
|
(player-state->jsexpr example-player #:username "anonymous"))
|
||||||
(check-equal? (length (hash-ref local-after-hans 'tabs)) 1)
|
(check-equal? (length (hash-ref anonymous-after-hans 'tabs)) 1)
|
||||||
(check-equal? (length (hash-ref local-after-hans 'tracks)) 2)
|
(check-equal? (length (hash-ref anonymous-after-hans 'tracks)) 2)
|
||||||
(player-close! example-player)
|
(player-close! example-player)
|
||||||
|
|
||||||
(define restored-player
|
(define restored-player
|
||||||
|
|||||||
+19
-2
@@ -54,7 +54,6 @@
|
|||||||
(let ((user (auth-request-user current-auth request)))
|
(let ((user (auth-request-user current-auth request)))
|
||||||
(json-response
|
(json-response
|
||||||
(hasheq 'enabled (auth-enabled? current-auth)
|
(hasheq 'enabled (auth-enabled? current-auth)
|
||||||
'local (auth-request-local? current-auth request)
|
|
||||||
'authenticated (and user #t)
|
'authenticated (and user #t)
|
||||||
'username (or user 'null)))))
|
'username (or user 'null)))))
|
||||||
|
|
||||||
@@ -215,6 +214,24 @@
|
|||||||
(regexp-match? #px"^/api/(?:auth|agent)(?:/|$)"
|
(regexp-match? #px"^/api/(?:auth|agent)(?:/|$)"
|
||||||
(request-path request)))
|
(request-path request)))
|
||||||
|
|
||||||
|
(define (response-add-header value extra-header)
|
||||||
|
(response (response-code value)
|
||||||
|
(response-message value)
|
||||||
|
(response-seconds value)
|
||||||
|
(response-mime value)
|
||||||
|
(cons extra-header (response-headers value))
|
||||||
|
(response-output value)))
|
||||||
|
|
||||||
|
(define (dispatch-api request)
|
||||||
|
(define value (api-dispatch request))
|
||||||
|
(define renewed-cookie
|
||||||
|
(and (not (regexp-match? #px"^/api/agent(?:/|$)"
|
||||||
|
(request-path request)))
|
||||||
|
(auth-renewal-cookie current-auth request)))
|
||||||
|
(if renewed-cookie
|
||||||
|
(response-add-header value (header #"Set-Cookie" renewed-cookie))
|
||||||
|
value))
|
||||||
|
|
||||||
(define (dispatch request)
|
(define (dispatch request)
|
||||||
(cond
|
(cond
|
||||||
((and (bytes=? (request-method request) #"POST")
|
((and (bytes=? (request-method request) #"POST")
|
||||||
@@ -225,7 +242,7 @@
|
|||||||
#:code 415))
|
#:code 415))
|
||||||
((or (public-api-request? request)
|
((or (public-api-request? request)
|
||||||
(auth-request-user current-auth request))
|
(auth-request-user current-auth request))
|
||||||
(api-dispatch request))
|
(dispatch-api request))
|
||||||
(else
|
(else
|
||||||
(json-response
|
(json-response
|
||||||
(hasheq 'error "Aanmelden is vereist"
|
(hasheq 'error "Aanmelden is vereist"
|
||||||
|
|||||||
+54
-31
@@ -14,18 +14,20 @@
|
|||||||
make-auth-manager
|
make-auth-manager
|
||||||
auth-manager?
|
auth-manager?
|
||||||
auth-enabled?
|
auth-enabled?
|
||||||
auth-request-local?
|
|
||||||
auth-request-user
|
auth-request-user
|
||||||
auth-login!
|
auth-login!
|
||||||
auth-logout!
|
auth-logout!
|
||||||
auth-session-cookie
|
auth-session-cookie
|
||||||
|
auth-renewal-cookie
|
||||||
auth-expired-cookie)
|
auth-expired-cookie)
|
||||||
|
|
||||||
(struct ip-network (address prefix) #:transparent)
|
(struct ip-network (address prefix) #:transparent)
|
||||||
(struct session (username [last-seen #:mutable]) #:transparent)
|
(struct session
|
||||||
|
(username [last-seen #:mutable] [last-cookie-renewal #:mutable])
|
||||||
|
#:transparent)
|
||||||
(struct failures ([attempts #:mutable] [started #:mutable]) #:transparent)
|
(struct failures ([attempts #:mutable] [started #:mutable]) #:transparent)
|
||||||
(struct auth-manager
|
(struct auth-manager
|
||||||
(users local-networks trusted-proxies session-seconds sessions failed lock)
|
(users trusted-proxies session-seconds sessions failed lock)
|
||||||
#:transparent)
|
#:transparent)
|
||||||
|
|
||||||
(define password-kdf
|
(define password-kdf
|
||||||
@@ -136,11 +138,6 @@
|
|||||||
(define (auth-enabled? manager)
|
(define (auth-enabled? manager)
|
||||||
(positive? (hash-count (auth-manager-users manager))))
|
(positive? (hash-count (auth-manager-users manager))))
|
||||||
|
|
||||||
(define (auth-request-local? manager request)
|
|
||||||
(ormap (lambda (network)
|
|
||||||
(network-contains? network (request-address manager request)))
|
|
||||||
(auth-manager-local-networks manager)))
|
|
||||||
|
|
||||||
(define (request-session-token request)
|
(define (request-session-token request)
|
||||||
(for/or ((cookie (in-list (request-cookies request))))
|
(for/or ((cookie (in-list (request-cookies request))))
|
||||||
(and (string=? (client-cookie-name cookie) session-cookie-name)
|
(and (string=? (client-cookie-name cookie) session-cookie-name)
|
||||||
@@ -156,7 +153,6 @@
|
|||||||
(define (auth-request-user manager request)
|
(define (auth-request-user manager request)
|
||||||
(cond
|
(cond
|
||||||
((not (auth-enabled? manager)) "anonymous")
|
((not (auth-enabled? manager)) "anonymous")
|
||||||
((auth-request-local? manager request) "local")
|
|
||||||
(else
|
(else
|
||||||
(let ((token (request-session-token request))
|
(let ((token (request-session-token request))
|
||||||
(now (current-seconds)))
|
(now (current-seconds)))
|
||||||
@@ -215,7 +211,7 @@
|
|||||||
(prune-sessions! manager now)
|
(prune-sessions! manager now)
|
||||||
(hash-set! (auth-manager-sessions manager)
|
(hash-set! (auth-manager-sessions manager)
|
||||||
token
|
token
|
||||||
(session normalized now))
|
(session normalized now now))
|
||||||
token)
|
token)
|
||||||
(begin
|
(begin
|
||||||
(record-failure! manager address now)
|
(record-failure! manager address now)
|
||||||
@@ -237,6 +233,30 @@
|
|||||||
token
|
token
|
||||||
(auth-manager-session-seconds manager))))
|
(auth-manager-session-seconds manager))))
|
||||||
|
|
||||||
|
;; Return a refreshed cookie at most once per half session lifetime. The
|
||||||
|
;; server-side inactivity timer is updated on every authenticated request, but
|
||||||
|
;; limiting Set-Cookie avoids rewriting it for every one-second player poll.
|
||||||
|
(define (auth-renewal-cookie manager request)
|
||||||
|
(and (auth-enabled? manager)
|
||||||
|
(let ((token (request-session-token request))
|
||||||
|
(now (current-seconds)))
|
||||||
|
(and token
|
||||||
|
(call-with-semaphore
|
||||||
|
(auth-manager-lock manager)
|
||||||
|
(lambda ()
|
||||||
|
(prune-sessions! manager now)
|
||||||
|
(define value
|
||||||
|
(hash-ref (auth-manager-sessions manager) token #f))
|
||||||
|
(and value
|
||||||
|
(>= (- now (session-last-cookie-renewal value))
|
||||||
|
(max 1
|
||||||
|
(quotient
|
||||||
|
(auth-manager-session-seconds manager)
|
||||||
|
2)))
|
||||||
|
(begin
|
||||||
|
(set-session-last-cookie-renewal! value now)
|
||||||
|
(auth-session-cookie manager token)))))))))
|
||||||
|
|
||||||
(define (auth-expired-cookie)
|
(define (auth-expired-cookie)
|
||||||
(string->bytes/utf-8
|
(string->bytes/utf-8
|
||||||
(format
|
(format
|
||||||
@@ -244,14 +264,9 @@
|
|||||||
session-cookie-name)))
|
session-cookie-name)))
|
||||||
|
|
||||||
(define (make-auth-manager user-pairs
|
(define (make-auth-manager user-pairs
|
||||||
#:local-networks
|
|
||||||
[local-network-values
|
|
||||||
'("127.0.0.0/8" "::1/128"
|
|
||||||
"10.0.0.0/8" "172.16.0.0/12"
|
|
||||||
"192.168.0.0/16")]
|
|
||||||
#:trusted-proxies
|
#:trusted-proxies
|
||||||
[trusted-proxy-values '("127.0.0.0/8" "::1/128")]
|
[trusted-proxy-values '("127.0.0.0/8" "::1/128")]
|
||||||
#:session-seconds [session-seconds 43200])
|
#:session-seconds [session-seconds 604800])
|
||||||
(unless (exact-positive-integer? session-seconds)
|
(unless (exact-positive-integer? session-seconds)
|
||||||
(raise-argument-error 'make-auth-manager "exact-positive-integer?"
|
(raise-argument-error 'make-auth-manager "exact-positive-integer?"
|
||||||
session-seconds))
|
session-seconds))
|
||||||
@@ -277,7 +292,6 @@
|
|||||||
(hash-set! users (string-downcase (string-trim (car entry)))
|
(hash-set! users (string-downcase (string-trim (car entry)))
|
||||||
(cdr entry)))
|
(cdr entry)))
|
||||||
(auth-manager users
|
(auth-manager users
|
||||||
(map parse-network local-network-values)
|
|
||||||
(map parse-network trusted-proxy-values)
|
(map parse-network trusted-proxy-values)
|
||||||
session-seconds
|
session-seconds
|
||||||
(make-hash)
|
(make-hash)
|
||||||
@@ -297,30 +311,35 @@
|
|||||||
(define manager
|
(define manager
|
||||||
(make-auth-manager
|
(make-auth-manager
|
||||||
(list (cons "Hans" test-hash))
|
(list (cons "Hans" test-hash))
|
||||||
#:local-networks '("192.168.1.0/24")
|
|
||||||
#:trusted-proxies '("127.0.0.1/32")))
|
#:trusted-proxies '("127.0.0.1/32")))
|
||||||
|
|
||||||
(define (test-request peer [headers '()])
|
(define (test-request peer [headers '()])
|
||||||
(request #"GET" (string->url "http://example.test/api/state")
|
(request #"GET" (string->url "http://example.test/api/state")
|
||||||
headers (delay '()) #f "127.0.0.1" 80 peer))
|
headers (delay '()) #f "127.0.0.1" 80 peer))
|
||||||
|
|
||||||
(check-true (auth-request-local? manager (test-request "192.168.1.42")))
|
|
||||||
(check-false (auth-request-local? manager (test-request "192.168.2.42")))
|
|
||||||
(check-true
|
|
||||||
(auth-request-local?
|
|
||||||
manager
|
|
||||||
(test-request "127.0.0.1"
|
|
||||||
(list (header #"X-Forwarded-For" #"198.51.100.2, 192.168.1.8")))))
|
|
||||||
(check-false
|
|
||||||
(auth-request-local?
|
|
||||||
manager
|
|
||||||
(test-request "198.51.100.2"
|
|
||||||
(list (header #"X-Forwarded-For" #"192.168.1.8")))))
|
|
||||||
|
|
||||||
(define remote (test-request "198.51.100.2"))
|
(define remote (test-request "198.51.100.2"))
|
||||||
|
;; Local and remote browser requests follow the same login path.
|
||||||
|
(check-false (auth-request-user manager (test-request "127.0.0.1")))
|
||||||
|
(check-equal?
|
||||||
|
(request-address
|
||||||
|
manager
|
||||||
|
(test-request
|
||||||
|
"127.0.0.1"
|
||||||
|
(list (header #"X-Forwarded-For"
|
||||||
|
#"198.51.100.8, 203.0.113.9"))))
|
||||||
|
"203.0.113.9")
|
||||||
|
(check-equal?
|
||||||
|
(request-address
|
||||||
|
manager
|
||||||
|
(test-request
|
||||||
|
"198.51.100.2"
|
||||||
|
(list (header #"X-Forwarded-For" #"203.0.113.9"))))
|
||||||
|
"198.51.100.2")
|
||||||
(define token
|
(define token
|
||||||
(auth-login! manager remote "hans" "correct horse battery staple"))
|
(auth-login! manager remote "hans" "correct horse battery staple"))
|
||||||
(check-true (string? token))
|
(check-true (string? token))
|
||||||
|
(check-true
|
||||||
|
(regexp-match? #rx#"Max-Age=604800" (auth-session-cookie manager token)))
|
||||||
(define authenticated
|
(define authenticated
|
||||||
(test-request
|
(test-request
|
||||||
"198.51.100.2"
|
"198.51.100.2"
|
||||||
@@ -329,6 +348,10 @@
|
|||||||
(string->bytes/utf-8
|
(string->bytes/utf-8
|
||||||
(format "~a=~a" session-cookie-name token))))))
|
(format "~a=~a" session-cookie-name token))))))
|
||||||
(check-equal? (auth-request-user manager authenticated) "hans")
|
(check-equal? (auth-request-user manager authenticated) "hans")
|
||||||
|
(define stored-session (hash-ref (auth-manager-sessions manager) token))
|
||||||
|
(set-session-last-cookie-renewal! stored-session 0)
|
||||||
|
(check-true (bytes? (auth-renewal-cookie manager authenticated)))
|
||||||
|
(check-false (auth-renewal-cookie manager authenticated))
|
||||||
(auth-logout! manager authenticated)
|
(auth-logout! manager authenticated)
|
||||||
(check-false (auth-request-user manager authenticated))
|
(check-false (auth-request-user manager authenticated))
|
||||||
(check-false (auth-login! manager remote "hans" "wrong password")))
|
(check-false (auth-login! manager remote "hans" "wrong password")))
|
||||||
|
|||||||
+2
-2
@@ -101,8 +101,8 @@ function hideLogin() {
|
|||||||
async function refreshAuth() {
|
async function refreshAuth() {
|
||||||
try {
|
try {
|
||||||
const auth = await api("/api/auth/status");
|
const auth = await api("/api/auth/status");
|
||||||
elements.logout.hidden = !auth.enabled || auth.local || !auth.authenticated;
|
elements.logout.hidden = !auth.enabled || !auth.authenticated;
|
||||||
if (auth.enabled && !auth.local && !auth.authenticated) showLogin();
|
if (auth.enabled && !auth.authenticated) showLogin();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus(`Authenticatiestatus onbekend: ${error.message}`);
|
setStatus(`Authenticatiestatus onbekend: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,10 @@ dlna-port=8734
|
|||||||
; 7b4776ef27104e8eb9f7ea2c622ce76ca23de4260b0f94e6880d321017b32a0e4=true
|
; 7b4776ef27104e8eb9f7ea2c622ce76ca23de4260b0f94e6880d321017b32a0e4=true
|
||||||
|
|
||||||
[authentication]
|
[authentication]
|
||||||
; Local clients do not need to log in. Separate networks with semicolons.
|
|
||||||
local-networks=127.0.0.0/8;::1/128;10.0.0.0/8;172.16.0.0/12;192.168.0.0/16
|
|
||||||
; Only these direct peers may supply X-Forwarded-For.
|
; Only these direct peers may supply X-Forwarded-For.
|
||||||
trusted-proxies=127.0.0.0/8;::1/128
|
trusted-proxies=127.0.0.0/8;::1/128
|
||||||
session-seconds=43200
|
; Seven-day sliding idle timeout. Active cookies are renewed halfway through.
|
||||||
|
session-seconds=604800
|
||||||
|
|
||||||
[users]
|
[users]
|
||||||
; Generate a hash with (make-password-hash "a long password") from
|
; Generate a hash with (make-password-hash "a long password") from
|
||||||
|
|||||||
@@ -21,9 +21,8 @@ browser by Racket's web server.
|
|||||||
(list/c string? path-string?)))]
|
(list/c string? path-string?)))]
|
||||||
[#:allowed-agent-ids allowed-agent-ids (listof string?) null]
|
[#:allowed-agent-ids allowed-agent-ids (listof string?) null]
|
||||||
[#:users users (listof (cons/c string? string?)) null]
|
[#:users users (listof (cons/c string? string?)) null]
|
||||||
[#:local-networks local-networks (listof string?)]
|
|
||||||
[#:trusted-proxies trusted-proxies (listof string?)]
|
[#:trusted-proxies trusted-proxies (listof string?)]
|
||||||
[#:session-seconds session-seconds exact-positive-integer? 43200]
|
[#:session-seconds session-seconds exact-positive-integer? 604800]
|
||||||
[#:listen-ip listen-ip string? "127.0.0.1"]
|
[#:listen-ip listen-ip string? "127.0.0.1"]
|
||||||
[#:port port exact-positive-integer? 8080]
|
[#:port port exact-positive-integer? 8080]
|
||||||
[#:dlna-port dlna-port exact-positive-integer? 8734]
|
[#:dlna-port dlna-port exact-positive-integer? 8734]
|
||||||
@@ -45,6 +44,10 @@ network renderer. Playlist tabs are atomically persisted in
|
|||||||
resources are closed when the web server exits. The default is
|
resources are closed when the web server exits. The default is
|
||||||
@tt{data/playlists.keystore} below the installed rkt-web-player collection.
|
@tt{data/playlists.keystore} below the installed rkt-web-player collection.
|
||||||
Each username owns an ordered GUID index and separate playlist values.
|
Each username owns an ordered GUID index and separate playlist values.
|
||||||
|
When @racket[users] is non-empty, every browser client must authenticate.
|
||||||
|
Sessions have a sliding idle timeout; an active browser cookie is renewed once
|
||||||
|
half of @racket[session-seconds] has elapsed. Playback-agent endpoints continue
|
||||||
|
to use their separate application-ID allowlist.
|
||||||
|
|
||||||
The default listen address only exposes the interface to the local computer.
|
The default listen address only exposes the interface to the local computer.
|
||||||
Use a LAN address deliberately if other devices should control the player.
|
Use a LAN address deliberately if other devices should control the player.
|
||||||
|
|||||||
Reference in New Issue
Block a user