diff --git a/audit/internal/webui/api.go b/audit/internal/webui/api.go index fccbaf6..1c21425 100644 --- a/audit/internal/webui/api.go +++ b/audit/internal/webui/api.go @@ -1483,6 +1483,61 @@ func (h *handler) handleAPISystemShutdown(w http.ResponseWriter, r *http.Request writeJSON(w, map[string]string{"status": "shutting down"}) } +// timezoneNameRE matches IANA timezone identifiers like "Europe/Moscow" or "UTC". +var timezoneNameRE = regexp.MustCompile(`^[A-Za-z0-9_+\-]+(/[A-Za-z0-9_+\-]+)*$`) + +func validTimezoneName(tz string) bool { + if tz == "" || !timezoneNameRE.MatchString(tz) { + return false + } + _, err := os.Stat(filepath.Join("/usr/share/zoneinfo", tz)) + return err == nil +} + +// handleAPISystemTimeSync sets the host's timezone and wall-clock time from +// values supplied by the client's browser (used when the appliance has no +// network/NTP access to keep its own clock in sync). +func (h *handler) handleAPISystemTimeSync(w http.ResponseWriter, r *http.Request) { + var req struct { + Timezone string `json:"timezone"` + EpochMS int64 `json:"epoch_ms"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "invalid request body") + return + } + if req.EpochMS <= 0 { + writeError(w, http.StatusBadRequest, "epoch_ms required") + return + } + + var out strings.Builder + + if req.Timezone != "" { + if !validTimezoneName(req.Timezone) { + writeError(w, http.StatusBadRequest, "invalid timezone") + return + } + if b, err := exec.Command("timedatectl", "set-timezone", req.Timezone).CombinedOutput(); err != nil { + writeError(w, http.StatusInternalServerError, "set-timezone failed: "+strings.TrimSpace(string(b))) + return + } + fmt.Fprintf(&out, "timezone set to %s\n", req.Timezone) + } + + // Manual time only sticks if NTP sync is off. + _ = exec.Command("timedatectl", "set-ntp", "false").Run() + + sec := req.EpochMS / 1000 + if b, err := exec.Command("date", "-u", "-s", "@"+strconv.FormatInt(sec, 10)).CombinedOutput(); err != nil { + writeError(w, http.StatusInternalServerError, "set-time failed: "+strings.TrimSpace(string(b))) + return + } + out.WriteString("system clock synced\n") + + writeJSON(w, map[string]string{"status": "ok", "output": out.String()}) +} + // ── Tools ───────────────────────────────────────────────────────────────────── var standardTools = []string{ diff --git a/audit/internal/webui/server.go b/audit/internal/webui/server.go index b8e71b2..4acc1cb 100644 --- a/audit/internal/webui/server.go +++ b/audit/internal/webui/server.go @@ -346,6 +346,7 @@ func NewHandler(opts HandlerOptions) http.Handler { mux.HandleFunc("POST /api/system/install-to-ram", h.handleAPIInstallToRAM) mux.HandleFunc("POST /api/system/reboot", h.handleAPISystemReboot) mux.HandleFunc("POST /api/system/shutdown", h.handleAPISystemShutdown) + mux.HandleFunc("POST /api/system/time-sync", h.handleAPISystemTimeSync) // Preflight mux.HandleFunc("GET /api/preflight", h.handleAPIPreflight)