1 Commits

Author SHA1 Message Date
urania 9587d11e21 fix: localectl
build-and-release / release (push) Successful in 2m38s
2026-06-22 22:22:36 +02:00
5 changed files with 35 additions and 24 deletions
+1 -2
View File
@@ -239,8 +239,7 @@ func runServer() {
meta.Register(api, mods) meta.Register(api, mods)
meta.RegisterHealth(api, sessions) meta.RegisterHealth(api, sessions)
meta.RegisterWhoami(api, sessions, roles, mods) meta.RegisterWhoami(api, sessions, roles, mods)
meta.ConfigPath = configPath meta.RegisterUpdate(api, configPath)
meta.RegisterUpdate(api)
auth.RegisterLogin(api, sessions, auditStore, cfg.SecureCookie()) auth.RegisterLogin(api, sessions, auditStore, cfg.SecureCookie())
auth.RegisterLogout(api, sessions, cfg.SecureCookie()) auth.RegisterLogout(api, sessions, cfg.SecureCookie())
+3 -2
View File
@@ -1,8 +1,9 @@
package main package main
import ( import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand" "crypto/rand"
"crypto/rsa"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
@@ -24,7 +25,7 @@ func serverCert(certPath, keyPath string) (tls.Certificate, error) {
} }
func generateSelfSignedCert() (tls.Certificate, error) { func generateSelfSignedCert() (tls.Certificate, error) {
priv, err := rsa.GenerateKey(rand.Reader, 2048) priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil { if err != nil {
return tls.Certificate{}, err return tls.Certificate{}, err
} }
+15 -3
View File
@@ -110,13 +110,25 @@ func Load(path string) (*File, error) {
return nil, fmt.Errorf("parse config %s: %w", path, err) return nil, fmt.Errorf("parse config %s: %w", path, err)
} }
// release_repo, when set, is downloaded over the wire and (for /api/update) // release_repo, when set, is downloaded over the wire and (for /api/update)
// executed. Reject http:// at the boundary so /install.sh and the updater // executed. Validate shape + scheme once here so /install.sh and the updater
// never have to re-check. // can use the string directly. Trim any trailing slash so downstream string
// concatenation produces a clean URL.
if f.Server.ReleaseRepo != "" { if f.Server.ReleaseRepo != "" {
f.Server.ReleaseRepo = strings.TrimRight(f.Server.ReleaseRepo, "/")
u, err := url.Parse(f.Server.ReleaseRepo) u, err := url.Parse(f.Server.ReleaseRepo)
if err != nil || u.Scheme != "https" { if err != nil {
return nil, fmt.Errorf("server.release_repo: %w", err)
}
if u.Scheme != "https" {
return nil, fmt.Errorf("server.release_repo must use https:// (got %q)", f.Server.ReleaseRepo) return nil, fmt.Errorf("server.release_repo must use https:// (got %q)", f.Server.ReleaseRepo)
} }
if u.Host == "" {
return nil, fmt.Errorf("server.release_repo missing host: %q", f.Server.ReleaseRepo)
}
parts := strings.Split(strings.Trim(u.Path, "/"), "/")
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return nil, fmt.Errorf("server.release_repo must be https://host/owner/repo, got %q", f.Server.ReleaseRepo)
}
} }
return &f, nil return &f, nil
} }
+8 -9
View File
@@ -12,19 +12,18 @@ import (
"github.com/danielgtaylor/huma/v2" "github.com/danielgtaylor/huma/v2"
) )
// ConfigPath is set at startup so the update handler can re-load config and // RegisterUpdate wires POST /api/update. It runs the equivalent of
// surface release_repo / parse errors to the caller instead of only stderr.
var ConfigPath string
// RegisterUpdate wires POST /api/meta/update. It runs the equivalent of
// `sudo nadir update` in a detached session and returns 202 immediately; the // `sudo nadir update` in a detached session and returns 202 immediately; the
// systemctl restart that ends the updater drops in-flight connections, so the // systemctl restart that ends the updater drops in-flight connections, so the
// caller should poll /api/health to confirm the new version is up. // caller should poll /api/health to confirm the new version is up.
// //
// configPath is re-read by the handler so a missing release_repo (or any other
// config error) surfaces as 4xx/5xx to the caller, not as stderr only.
//
// Authorization: requires (meta, root). Only roles with a wildcard grant // Authorization: requires (meta, root). Only roles with a wildcard grant
// (the default admin role) match, since "meta" isn't a real module with a // (the default admin role) match, since "meta" isn't a real module with a
// declared permission vocabulary. // declared permission vocabulary.
func RegisterUpdate(api huma.API) { func RegisterUpdate(api huma.API, configPath string) {
huma.Register(api, huma.Operation{ huma.Register(api, huma.Operation{
OperationID: "meta-update", OperationID: "meta-update",
Method: "POST", Method: "POST",
@@ -36,13 +35,13 @@ func RegisterUpdate(api huma.API) {
Errors: []int{400, 401, 403, 500}, Errors: []int{400, 401, 403, 500},
DefaultStatus: 202, DefaultStatus: 202,
}, func(ctx context.Context, _ *struct{}) (*oscmd.StatusOutput, error) { }, func(ctx context.Context, _ *struct{}) (*oscmd.StatusOutput, error) {
if ConfigPath != "" { if configPath != "" {
cfg, err := config.Load(ConfigPath) cfg, err := config.Load(configPath)
if err != nil { if err != nil {
return nil, huma.Error500InternalServerError("config load failed", err) return nil, huma.Error500InternalServerError("config load failed", err)
} }
if cfg.Server.ReleaseRepo == "" { if cfg.Server.ReleaseRepo == "" {
return nil, huma.Error400BadRequest("server.release_repo not set in " + ConfigPath) return nil, huma.Error400BadRequest("server.release_repo not set in " + configPath)
} }
} }
exe, err := os.Executable() exe, err := os.Executable()
+8 -8
View File
@@ -160,10 +160,11 @@ func registerLocale(api huma.API) {
Metadata: op("read"), Metadata: op("read"),
Errors: readErrors, Errors: readErrors,
}, func(ctx context.Context, _ *struct{}) (*KeymapsOutput, error) { }, func(ctx context.Context, _ *struct{}) (*KeymapsOutput, error) {
keymaps, err := oscmd.RunLines("localectl", "list-keymaps") // ponytail: minimal servers ship without kbd / /usr/share/keymaps, so
if err != nil { // localectl errors instead of returning empty. Treat that as "no keymaps
return nil, huma.Error500InternalServerError("localectl failed", err) // available" rather than a server fault — set-keymap stays unreachable
} // because nothing will be in the allowlist.
keymaps, _ := oscmd.RunLines("localectl", "list-keymaps")
out := &KeymapsOutput{} out := &KeymapsOutput{}
out.Body.Keymaps = keymaps out.Body.Keymaps = keymaps
return out, nil return out, nil
@@ -185,10 +186,9 @@ func registerLocale(api huma.API) {
if km == "" { if km == "" {
return nil, huma.Error400BadRequest("empty keymap") return nil, huma.Error400BadRequest("empty keymap")
} }
keymaps, err := oscmd.RunLines("localectl", "list-keymaps") // list-keymaps failure means no keymap allowlist on this host (kbd absent);
if err != nil { // fall through to unknown-keymap 400 instead of 500.
return nil, huma.Error500InternalServerError("localectl failed", err) keymaps, _ := oscmd.RunLines("localectl", "list-keymaps")
}
if !slices.Contains(keymaps, km) { if !slices.Contains(keymaps, km) {
return nil, huma.Error400BadRequest("unknown keymap: " + km) return nil, huma.Error400BadRequest("unknown keymap: " + km)
} }