package app import ( "context" "crypto/hmac" "crypto/sha256" "database/sql" "encoding/hex" "encoding/json" "errors" "io" "net/http" "regexp" "strings" "time" ) const fleetCredentialPrefix = "Bearer " var ( installationIDPattern = regexp.MustCompile( `^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`, ) hexCredentialPattern = regexp.MustCompile(`^[0-9a-f]{64}$`) fleetValuePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._+~,:/() -]*$`) errorCodePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]*$`) ) type fleetRequest struct { SchemaVersion int `json:"schema_version"` InstallationID string `json:"installation_id"` Credential string `json:"credential,omitempty"` Event string `json:"event,omitempty"` Result string `json:"result,omitempty"` ProxMenuVersion string `json:"proxmenu_version,omitempty"` GitCommit string `json:"git_commit,omitempty"` PVEVersion string `json:"pve_version,omitempty"` OSVersion string `json:"os_version,omitempty"` KernelVersion string `json:"kernel_version,omitempty"` Architecture string `json:"architecture,omitempty"` Clustered bool `json:"clustered"` ErrorCode string `json:"error_code,omitempty"` DurationSeconds int `json:"duration_seconds,omitempty"` } type fleetHostRecord struct { InstallationID string VerifiedAt sql.NullTime FirstSeenAt time.Time LastSeenAt time.Time LastSuccessAt sql.NullTime LastEvent string LastResult string LastErrorCode string ProxMenuVersion string GitCommit string PVEVersion string OSVersion string KernelVersion string Architecture string Clustered bool } func (s *Server) handleFleetRegister(w http.ResponseWriter, r *http.Request) { request, ok := decodeFleetRequest(w, r, true) if !ok { return } sourceHash := s.fleetSourceHash(s.clientIP(r)) var recent int if err := s.db.QueryRowContext( r.Context(), `SELECT COUNT(*) FROM fleet_hosts WHERE registration_source_hash = ? AND first_seen_at > datetime('now', '-1 day')`, sourceHash, ).Scan(&recent); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "unable to register host"}) return } credentialHash := hashValue(request.Credential) var existingHash []byte err := s.db.QueryRowContext( r.Context(), `SELECT credential_hash FROM fleet_hosts WHERE installation_id = ?`, request.InstallationID, ).Scan(&existingHash) switch { case errors.Is(err, sql.ErrNoRows): if recent >= 25 { w.Header().Set("Retry-After", "86400") writeJSON(w, http.StatusTooManyRequests, map[string]string{"error": "registration limit reached"}) return } if err := s.insertFleetHost(r.Context(), request, credentialHash[:], sourceHash); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "unable to register host"}) return } writeJSON(w, http.StatusCreated, map[string]string{"status": "registered"}) case err != nil: writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "unable to register host"}) case !hmac.Equal(existingHash, credentialHash[:]): writeJSON(w, http.StatusForbidden, map[string]string{"error": "host credential is invalid"}) default: if err := s.updateFleetHost(r.Context(), request, false); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "unable to update host"}) return } writeJSON(w, http.StatusOK, map[string]string{"status": "registered"}) } } func (s *Server) handleFleetEvent(w http.ResponseWriter, r *http.Request) { request, ok := decodeFleetRequest(w, r, false) if !ok { return } authorization := strings.TrimSpace(r.Header.Get("Authorization")) if !strings.HasPrefix(authorization, fleetCredentialPrefix) { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "host credential required"}) return } credential := strings.TrimSpace(strings.TrimPrefix(authorization, fleetCredentialPrefix)) if !hexCredentialPattern.MatchString(credential) { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "host credential required"}) return } credentialHash := hashValue(credential) var authorized int if err := s.db.QueryRowContext( r.Context(), `SELECT COUNT(*) FROM fleet_hosts WHERE installation_id = ? AND credential_hash = ?`, request.InstallationID, credentialHash[:], ).Scan(&authorized); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "unable to authenticate host"}) return } if authorized != 1 { writeJSON(w, http.StatusForbidden, map[string]string{"error": "host credential is invalid"}) return } if err := s.updateFleetHost(r.Context(), request, true); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "unable to record event"}) return } w.WriteHeader(http.StatusNoContent) } func decodeFleetRequest(w http.ResponseWriter, r *http.Request, registration bool) (fleetRequest, bool) { var request fleetRequest r.Body = http.MaxBytesReader(w, r.Body, 16<<10) decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(&request); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"}) return request, false } if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"}) return request, false } request.InstallationID = strings.ToLower(strings.TrimSpace(request.InstallationID)) request.Credential = strings.ToLower(strings.TrimSpace(request.Credential)) request.Event = strings.TrimSpace(request.Event) request.Result = strings.TrimSpace(request.Result) request.ErrorCode = strings.TrimSpace(request.ErrorCode) if request.SchemaVersion != 1 || !installationIDPattern.MatchString(request.InstallationID) || (registration && !hexCredentialPattern.MatchString(request.Credential)) || !validFleetMetadata(request) { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid host data"}) return request, false } if registration { request.Event = "installed" request.Result = "success" } else if !validFleetEvent(request.Event, request.Result, request.ErrorCode) { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid event data"}) return request, false } return request, true } func validFleetMetadata(request fleetRequest) bool { values := []struct { value string limit int }{ {request.ProxMenuVersion, 64}, {request.GitCommit, 64}, {request.PVEVersion, 128}, {request.OSVersion, 128}, {request.KernelVersion, 128}, {request.Architecture, 32}, } for _, candidate := range values { if len(candidate.value) > candidate.limit || (candidate.value != "" && !fleetValuePattern.MatchString(candidate.value)) { return false } } return request.DurationSeconds >= 0 && request.DurationSeconds <= 31*24*60*60 } func validFleetEvent(event, result, errorCode string) bool { switch event { case "run_started", "run_completed", "run_failed", "upgraded": default: return false } switch result { case "started", "success", "warning", "failure": default: return false } return errorCode == "" || (len(errorCode) <= 64 && errorCodePattern.MatchString(errorCode)) } func (s *Server) insertFleetHost( ctx context.Context, request fleetRequest, credentialHash []byte, sourceHash []byte, ) error { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() if _, err := tx.ExecContext( ctx, `INSERT INTO fleet_hosts (installation_id, credential_hash, registration_source_hash, proxmenu_version, git_commit, pve_version, os_version, kernel_version, architecture, clustered) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, request.InstallationID, credentialHash, sourceHash, request.ProxMenuVersion, request.GitCommit, request.PVEVersion, request.OSVersion, request.KernelVersion, request.Architecture, request.Clustered, ); err != nil { return err } if _, err := tx.ExecContext( ctx, `INSERT INTO fleet_events (installation_id, event_type, result, proxmenu_version) VALUES (?, 'installed', 'success', ?)`, request.InstallationID, request.ProxMenuVersion, ); err != nil { return err } return tx.Commit() } func (s *Server) updateFleetHost(ctx context.Context, request fleetRequest, recordEvent bool) error { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() result, err := tx.ExecContext( ctx, `UPDATE fleet_hosts SET last_seen_at = CURRENT_TIMESTAMP, last_success_at = CASE WHEN ? = 'run_completed' AND ? = 'success' THEN CURRENT_TIMESTAMP ELSE last_success_at END, last_event = CASE WHEN ? THEN ? ELSE last_event END, last_result = CASE WHEN ? THEN ? ELSE last_result END, last_error_code = CASE WHEN ? THEN ? ELSE last_error_code END, proxmenu_version = ?, git_commit = ?, pve_version = ?, os_version = ?, kernel_version = ?, architecture = ?, clustered = ?, updated_at = CURRENT_TIMESTAMP WHERE installation_id = ?`, request.Event, request.Result, recordEvent, request.Event, recordEvent, request.Result, recordEvent, request.ErrorCode, request.ProxMenuVersion, request.GitCommit, request.PVEVersion, request.OSVersion, request.KernelVersion, request.Architecture, request.Clustered, request.InstallationID, ) if err != nil { return err } affected, _ := result.RowsAffected() if affected != 1 { return sql.ErrNoRows } if recordEvent { if _, err := tx.ExecContext( ctx, `INSERT INTO fleet_events (installation_id, event_type, result, proxmenu_version, error_code, duration_seconds) VALUES (?, ?, ?, ?, ?, ?)`, request.InstallationID, request.Event, request.Result, request.ProxMenuVersion, request.ErrorCode, request.DurationSeconds, ); err != nil { return err } } return tx.Commit() } func (s *Server) fleetSourceHash(sourceIP string) []byte { mac := hmac.New(sha256.New, s.cfg.CookieSecret) _, _ = mac.Write([]byte("fleet-registration:")) _, _ = mac.Write([]byte(sourceIP)) return mac.Sum(nil) } func (s *Server) verifyFleetInstallation(ctx context.Context, installationID string) { installationID = strings.ToLower(strings.TrimSpace(installationID)) if !installationIDPattern.MatchString(installationID) { return } _, _ = s.db.ExecContext( ctx, `UPDATE fleet_hosts SET verified_at = COALESCE(verified_at, CURRENT_TIMESTAMP), last_seen_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE installation_id = ?`, installationID, ) } func (s *Server) listFleetHosts(r *http.Request) ([]fleetHostRecord, error) { rows, err := s.db.QueryContext( r.Context(), `SELECT installation_id, verified_at, first_seen_at, last_seen_at, last_success_at, last_event, last_result, last_error_code, proxmenu_version, git_commit, pve_version, os_version, kernel_version, architecture, clustered FROM fleet_hosts ORDER BY last_seen_at DESC LIMIT 500`, ) if err != nil { return nil, err } defer rows.Close() var records []fleetHostRecord for rows.Next() { var record fleetHostRecord if err := rows.Scan( &record.InstallationID, &record.VerifiedAt, &record.FirstSeenAt, &record.LastSeenAt, &record.LastSuccessAt, &record.LastEvent, &record.LastResult, &record.LastErrorCode, &record.ProxMenuVersion, &record.GitCommit, &record.PVEVersion, &record.OSVersion, &record.KernelVersion, &record.Architecture, &record.Clustered, ); err != nil { return nil, err } records = append(records, record) } return records, rows.Err() } func (s *Server) handleFleetPortal(w http.ResponseWriter, r *http.Request) { tech, err := s.currentTechnician(r) if err != nil { http.Redirect(w, r, "/", http.StatusSeeOther) return } hosts, err := s.listFleetHosts(r) if err != nil { http.Error(w, "unable to list hosts", http.StatusInternalServerError) return } s.render(w, "hosts.html", pageData{ Title: "ProxMenu Hosts", Technician: tech, CSRFToken: tech.CSRFToken, FleetHosts: hosts, CurrentView: "hosts", }) } func shortInstallationID(value string) string { if len(value) <= 8 { return value } return value[:8] } func shortCommit(value string) string { value = strings.TrimSpace(value) if len(value) <= 12 { return value } if _, err := hex.DecodeString(value[:12]); err != nil { return value } return value[:12] }