Files
OwnCord/Server/admin/update_handlers.go
T
J3vbandClaude Opus 4.8 81a0b63e65 feat(e2ee): F3 identity/TOFU + W2-4/W3-3 hardening — checkpoint before F3 UI
WIP save point. Server + W2-4/W3-3 complete and gate-green; F3 voice E2EE
identity keys + TOFU implemented and MITM-verified-closed; the F3 voice-panel
UI (safety-number display, verified/mismatch badge, re-pin modal) is still TODO.

- W2-4 attachment link (coverage confirmed); W3-3a XFF CIDR pre-parse;
  W3-3b update-binary TOCTOU (single-handle verify + O_EXCL staging)
- F3 server: migration 017 identity_public_key, PATCH /users/me persist,
  ready/member_join/user_update carry key, signed voice_e2ee_announce
- F3 client: ECDSA identity keypair (keyring + pin store), publish wired into
  ready, verifyPeerAnnounce pin-before-legacy, rePinPeerIdentity recovery
- Gates: server full CI mirror green (-race/-deadlock/lint/4 build tags);
  client typecheck/lint/format + 3337 vitest green. Rust CI-verify only.

Next: build F3 voice-panel UI, then adversarial review, then finalize commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 21:07:09 +02:00

157 lines
5.5 KiB
Go

package admin
import (
"context"
"log/slog"
"net/http"
"os"
"path/filepath"
"syscall"
"time"
"github.com/owncord/server/updater"
"golang.org/x/mod/semver"
)
// handleCheckUpdate returns the current update status.
func handleCheckUpdate(u *updater.Updater) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if u == nil {
writeErr(w, http.StatusServiceUnavailable, "UPDATE_UNAVAILABLE", "update checking is not configured")
return
}
info, err := u.CheckForUpdate(r.Context())
if err != nil {
slog.Error("update check failed", "err", err)
writeErr(w, http.StatusBadGateway, "UPDATE_CHECK_FAILED", "failed to check for updates — see server logs")
return
}
writeJSON(w, http.StatusOK, info)
}
}
// handleApplyUpdate downloads and applies a server update.
func handleApplyUpdate(u *updater.Updater, hub HubBroadcaster, _ string) http.Handler {
// TODO: maybe disable this endpoint in future docker build type?
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if u == nil {
writeErr(w, http.StatusServiceUnavailable, "UPDATE_UNAVAILABLE", "update checking is not configured")
return
}
// Check for available update.
info, err := u.CheckForUpdate(r.Context())
if err != nil {
slog.Error("update check failed during apply", "err", err)
writeErr(w, http.StatusBadGateway, "UPDATE_CHECK_FAILED", "failed to check for updates — see server logs")
return
}
if !info.UpdateAvailable {
if semver.Compare(info.Current, info.Latest) < 0 && !info.RequiredAssetsPresent {
writeErr(w, http.StatusBadGateway, "MISSING_ASSETS", "release is missing required assets")
return
}
writeErr(w, http.StatusConflict, "NO_UPDATE", "server is already up to date")
return
}
if !info.RequiredAssetsPresent {
writeErr(w, http.StatusBadGateway, "MISSING_ASSETS", "release is missing required assets")
return
}
// Get current executable path.
exePath, err := os.Executable()
if err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "cannot determine executable path")
return
}
exePath, err = filepath.EvalSymlinks(exePath)
if err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "cannot resolve executable path")
return
}
newPath := exePath + ".new"
oldPath := exePath + ".old"
// Download and verify.
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
defer cancel()
// DownloadAndVerify stages the binary and returns its trusted hash
// (bound to the signed release manifest). The apply goroutine below
// re-verifies the staged file against this hash through an open
// handle — never by path — before the rename+spawn.
stagedHash, err := u.DownloadAndVerify(ctx, info.Latest, info.DownloadURL, info.ChecksumURL, info.SignatureURL, info.ManifestURL, info.ManifestSignatureURL, newPath)
if err != nil {
slog.Error("update download/verify failed", "err", err)
writeErr(w, http.StatusBadGateway, "DOWNLOAD_FAILED", "download or verification failed — see server logs")
return
}
// Respond to the client before shutting down.
writeJSON(w, http.StatusOK, map[string]string{
"status": "applying",
"version": info.Latest,
})
// Broadcast restart notification and apply in background.
go func() {
if hub != nil {
hub.BroadcastServerRestart("update", 5)
}
time.Sleep(5 * time.Second)
// TOCTOU guard: open the staged binary once, verify its hash
// through that handle, and commit (rename) that exact file.
// Commit fails if the path was swapped after verification, so
// the bytes verified are the bytes spawned.
staged, err := updater.OpenVerifiedBinary(newPath, stagedHash)
if err != nil {
slog.Error("update: staged binary re-verification failed, aborting update", "error", err)
return
}
defer staged.Close() //nolint:errcheck
// Rename: current -> .old, verified staged binary -> current
_ = os.Remove(oldPath) // remove any stale .old
if err := os.Rename(exePath, oldPath); err != nil {
slog.Error("update: rename current to old failed", "error", err)
return
}
if err := staged.Commit(exePath); err != nil {
slog.Error("update: committing staged binary failed, restoring original binary", "error", err)
// Whatever is at exePath now (if anything) is not the verified
// binary; restoring .old replaces it.
if restoreErr := os.Rename(oldPath, exePath); restoreErr != nil {
slog.Error("update: CRITICAL — recovery rename also failed, server binary may be missing",
"restore_error", restoreErr, "original_error", err,
"old_path", oldPath, "exe_path", exePath)
if hub != nil {
hub.BroadcastServerRestart("update_failed", 0)
}
}
return
}
// Spawn new process.
if err := updater.SpawnDetached(exePath, os.Args[1:]); err != nil {
slog.Error("update: spawn new process failed", "error", err)
return
}
// Signal the process to shut down gracefully before exiting.
// We use SIGTERM on Unix to trigger the graceful shutdown handler
// in main.go. On Windows, os.Exit is unavoidable because the
// process must release its file lock on the binary.
slog.Info("update: new process spawned, shutting down current process")
if p, err := os.FindProcess(os.Getpid()); err == nil {
_ = p.Signal(syscall.SIGTERM)
// Give graceful shutdown a few seconds before force-killing.
time.Sleep(10 * time.Second)
}
os.Exit(0) // fallback if SIGTERM handler didn't exit
}()
})
}