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>
71 lines
1.6 KiB
Go
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
|
|
}
|