Files
homelab/wlan-setup/config.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

52 lines
1.6 KiB
Go

package main
import (
"errors"
"net/url"
"strings"
)
// Config is the service configuration, read from the environment.
type Config struct {
ClientSecret string
// AuthentikURL is where the calls go; they carry Host auth.renner.uno.
AuthentikURL string
AuthorizeURL string
PublicURL string
SigningKey string
SigningCert string
Listen string
}
func loadConfig(getenv func(string) string) (Config, error) {
get := func(key, def string) string {
if v := strings.TrimSpace(getenv(key)); v != "" {
return v
}
return def
}
c := Config{
ClientSecret: strings.TrimSpace(getenv("WLAN_SETUP_CLIENT_SECRET")),
AuthentikURL: strings.TrimRight(get("AUTHENTIK_INTERNAL_URL", "http://authentik-server.authentik.svc.cluster.local"), "/"),
AuthorizeURL: get("AUTHENTIK_AUTHORIZE_URL", "https://auth.renner.uno/application/o/authorize/"),
PublicURL: strings.TrimRight(get("PUBLIC_URL", "https://wlan.renner.uno"), "/"),
SigningKey: get("SIGNING_KEY", "/etc/wlan-signing/tls.key"),
SigningCert: get("SIGNING_CERT", "/etc/wlan-signing/tls.crt"),
Listen: get("LISTEN", ":8080"),
}
var errs []error
if c.ClientSecret == "" {
errs = append(errs, errors.New("WLAN_SETUP_CLIENT_SECRET is not set"))
}
for key, v := range map[string]string{
"AUTHENTIK_INTERNAL_URL": c.AuthentikURL,
"AUTHENTIK_AUTHORIZE_URL": c.AuthorizeURL,
"PUBLIC_URL": c.PublicURL,
} {
if u, err := url.Parse(v); err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
errs = append(errs, errors.New(key+" must be an absolute http(s) URL"))
}
}
return c, errors.Join(errs...)
}