Files
homelab/wlan-setup/store.go
T
claude-botandClaude Opus 5.5 a05f1431ca
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/wlan-setup-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
feat(wlan-setup): Go service for the WLAN setup page
wlan-setup/ is a Go service that will replace the nginx + njs backend of
wlan.renner.uno (switched over in a follow-up once the first image exists).

- Page, /whoami, /healthz; /{android,apple,windows}/install and /callback:
  authentik OAuth code flow (client wlan-setup, client_secret_basic), user
  check against X-authentik-username, per-device app password
  wlan-<platform>-... (29 days), profile per platform: Android Passpoint
  (application/x-wifi-config), Apple .mobileconfig with Passpoint, Windows
  CarrierProvisioning with Passpoint.
- Windows: the profile is signed with XML-DSig in the form of Microsoft's
  samples (enveloped, C14N 1.0, rsa-sha1, chain in KeyInfo) by the key in
  /etc/wlan-signing, kept for 5 minutes under a single-use token, and the
  browser is sent to ms-settings:wifi-provisioning?uri=.../windows/profile/<token>.
- Offline tests against a fake authentik; goxmldsig verifies the signature
  in the tests only.
- CI: wlan-setup-test (gofmt, vet, test -race) on PRs; build-ci-images
  builds gitea.renner.uno/gitea_admin/wlan-setup:YYYY-MM-DD-HHMM on main;
  Flux image automation follows the newest tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 14:39:07 +01:00

71 lines
1.6 KiB
Go

package main
import (
"sync"
"time"
)
// profileTTL is how long a signed Windows profile waits for Windows Settings to fetch it.
const profileTTL = 5 * time.Minute
// maxProfiles bounds the profiles held at once.
const maxProfiles = 1000
type storedProfile struct {
body []byte
expires time.Time
}
// ProfileStore keeps signed Windows profiles in memory, each fetchable once until it expires.
type ProfileStore struct {
mu sync.Mutex
now func() time.Time
profiles map[string]storedProfile
}
func newProfileStore(now func() time.Time) *ProfileStore {
return &ProfileStore{now: now, profiles: map[string]storedProfile{}}
}
func (s *ProfileStore) purge(now time.Time) {
for k, p := range s.profiles {
if !now.Before(p.expires) {
delete(s.profiles, k)
}
}
}
// Put stores body under token; false when the store is full.
func (s *ProfileStore) Put(token string, body []byte) bool {
s.mu.Lock()
defer s.mu.Unlock()
now := s.now()
s.purge(now)
if len(s.profiles) >= maxProfiles {
return false
}
s.profiles[token] = storedProfile{body: body, expires: now.Add(profileTTL)}
return true
}
// Take returns the profile stored under token and deletes it.
func (s *ProfileStore) Take(token string) ([]byte, bool) {
s.mu.Lock()
defer s.mu.Unlock()
s.purge(s.now())
p, ok := s.profiles[token]
if ok {
delete(s.profiles, token)
}
return p.body, ok
}
// Has reports whether a profile is stored under token, without taking it.
func (s *ProfileStore) Has(token string) bool {
s.mu.Lock()
defer s.mu.Unlock()
s.purge(s.now())
_, ok := s.profiles[token]
return ok
}