380 lines
9.4 KiB
Go
380 lines
9.4 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"rk3588sys/agent/internal/assets"
|
|
"rk3588sys/agent/internal/audit"
|
|
"rk3588sys/agent/internal/metrics"
|
|
"rk3588sys/agent/internal/sysinfo"
|
|
)
|
|
|
|
type configFileStatus struct {
|
|
Exists bool `json:"exists"`
|
|
Path string `json:"path"`
|
|
Sha256 string `json:"sha256,omitempty"`
|
|
Size int64 `json:"size,omitempty"`
|
|
MtimeMS int64 `json:"mtime_ms,omitempty"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
func defaultAuditPath(baseDir string) string {
|
|
if strings.TrimSpace(baseDir) == "" {
|
|
return filepath.Join("logs", "agent_audit.jsonl")
|
|
}
|
|
return filepath.Join(baseDir, "logs", "agent_audit.jsonl")
|
|
}
|
|
|
|
func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
errorJSON(w, http.StatusMethodNotAllowed, "method not allowed")
|
|
return
|
|
}
|
|
if !s.authorize(r, false) {
|
|
errorJSON(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
|
|
now := time.Now()
|
|
resp := map[string]any{
|
|
"timestamp_ms": now.UnixMilli(),
|
|
"uptime_sec": sysinfo.UptimeSec(),
|
|
}
|
|
|
|
if cpu, err := metrics.ReadCPUStat(); err == nil {
|
|
s.cpuMu.Lock()
|
|
usage := 0.0
|
|
intervalMS := int64(0)
|
|
if !s.lastCPUTS.IsZero() {
|
|
usage = metrics.CPUUsage(s.lastCPU, cpu)
|
|
intervalMS = now.Sub(s.lastCPUTS).Milliseconds()
|
|
}
|
|
s.lastCPU = cpu
|
|
s.lastCPUTS = now
|
|
s.cpuMu.Unlock()
|
|
resp["cpu"] = map[string]any{
|
|
"usage_pct": usage,
|
|
"sample_ms": intervalMS,
|
|
"total_ticks": cpu.Total,
|
|
"idle_ticks": cpu.Idle,
|
|
}
|
|
} else {
|
|
resp["cpu"] = map[string]any{"error": err.Error()}
|
|
}
|
|
|
|
if mem, err := metrics.ReadMemInfo(); err == nil {
|
|
resp["memory"] = mem
|
|
} else {
|
|
resp["memory"] = map[string]any{"error": err.Error()}
|
|
}
|
|
|
|
if la, err := metrics.ReadLoadAvg(); err == nil {
|
|
resp["load_avg"] = la
|
|
} else {
|
|
resp["load_avg"] = map[string]any{"error": err.Error()}
|
|
}
|
|
|
|
diskPath := s.baseDir
|
|
if strings.TrimSpace(diskPath) == "" {
|
|
diskPath = "/"
|
|
}
|
|
if du, err := metrics.ReadDiskUsage(diskPath); err == nil {
|
|
resp["disk"] = du
|
|
} else {
|
|
resp["disk"] = map[string]any{"path": diskPath, "error": err.Error()}
|
|
}
|
|
|
|
if nio, err := metrics.ReadNetDev(); err == nil {
|
|
resp["net"] = nio
|
|
} else {
|
|
resp["net"] = map[string]any{"error": err.Error()}
|
|
}
|
|
|
|
if dio, err := metrics.ReadDiskStats(); err == nil {
|
|
resp["disk_io"] = dio
|
|
} else {
|
|
resp["disk_io"] = map[string]any{"error": err.Error()}
|
|
}
|
|
|
|
media := map[string]any{"supported": s.proc != nil && s.proc.Enabled()}
|
|
if s.proc == nil || !s.proc.Enabled() {
|
|
resp["media_server"] = media
|
|
writeJSON(w, http.StatusOK, resp)
|
|
return
|
|
}
|
|
st, err := s.proc.Status()
|
|
if err != nil {
|
|
media["error"] = err.Error()
|
|
} else {
|
|
media["running"] = st.Running
|
|
media["pid"] = st.Pid
|
|
media["config_path"] = st.ConfigPath
|
|
media["started_at_ms"] = st.StartedAtMS
|
|
if st.Running && st.StartedAtMS > 0 {
|
|
media["uptime_sec"] = (now.UnixMilli() - st.StartedAtMS) / 1000
|
|
}
|
|
}
|
|
resp["media_server"] = media
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
func (s *Server) handleVersions(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
errorJSON(w, http.StatusMethodNotAllowed, "method not allowed")
|
|
return
|
|
}
|
|
if !s.authorize(r, false) {
|
|
errorJSON(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
|
|
resp := map[string]any{}
|
|
|
|
agentInfo := map[string]any{
|
|
"version": s.version,
|
|
"build_id": s.buildID,
|
|
"build_type": s.buildType,
|
|
"git_sha": s.gitSHA,
|
|
}
|
|
if bin, err := assets.ExecutableInfo(); err == nil {
|
|
agentInfo["binary"] = bin
|
|
} else {
|
|
agentInfo["binary_error"] = err.Error()
|
|
}
|
|
resp["agent"] = agentInfo
|
|
|
|
mediaInfo := map[string]any{"supported": s.proc != nil && s.proc.Enabled()}
|
|
if s.proc != nil && s.proc.Enabled() {
|
|
if ver, err := s.proc.Version(); err == nil {
|
|
mediaInfo["version"] = ver
|
|
} else {
|
|
mediaInfo["version_error"] = err.Error()
|
|
}
|
|
if bin, err := s.proc.BinaryInfo(); err == nil {
|
|
mediaInfo["binary"] = bin
|
|
} else {
|
|
mediaInfo["binary_error"] = err.Error()
|
|
}
|
|
}
|
|
resp["media_server"] = mediaInfo
|
|
|
|
if m, err := s.store.List(); err == nil {
|
|
resp["models"] = map[string]any{"count": len(m.Items), "items": m.Items}
|
|
} else {
|
|
resp["models"] = map[string]any{"error": err.Error()}
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
func (s *Server) handleConfigStatus(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
errorJSON(w, http.StatusMethodNotAllowed, "method not allowed")
|
|
return
|
|
}
|
|
if !s.authorize(r, false) {
|
|
errorJSON(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
|
|
current := readConfigFileStatus(s.agentCfg.ConfigPath)
|
|
lastGoodPath := s.agentCfg.ConfigPath + ".last_good.json"
|
|
lastGood := readConfigFileStatus(lastGoodPath)
|
|
candidatePath := s.configCandidatePath()
|
|
candidate := readConfigFileStatus(candidatePath)
|
|
|
|
resp := map[string]any{
|
|
"ok": true,
|
|
"config_path": filepath.ToSlash(s.agentCfg.ConfigPath),
|
|
"exists": current.Exists,
|
|
"last_good_path": filepath.ToSlash(lastGoodPath),
|
|
"last_good": lastGood,
|
|
"candidate_path": filepath.ToSlash(candidatePath),
|
|
"candidate": candidate,
|
|
}
|
|
if current.Exists {
|
|
resp["size"] = current.Size
|
|
resp["mtime_ms"] = current.MtimeMS
|
|
resp["sha256"] = current.Sha256
|
|
}
|
|
if len(current.Metadata) > 0 {
|
|
resp["metadata"] = current.Metadata
|
|
}
|
|
if current.Error != "" {
|
|
resp["error"] = current.Error
|
|
}
|
|
|
|
media := map[string]any{"supported": s.proc != nil && s.proc.Enabled()}
|
|
if s.proc != nil && s.proc.Enabled() {
|
|
if st, err := s.proc.Status(); err == nil {
|
|
media["running"] = st.Running
|
|
media["pid"] = st.Pid
|
|
media["config_path"] = filepath.ToSlash(st.ConfigPath)
|
|
media["started_at_ms"] = st.StartedAtMS
|
|
} else {
|
|
media["error"] = err.Error()
|
|
}
|
|
}
|
|
resp["media_server"] = media
|
|
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
func (s *Server) configCandidatePath() string {
|
|
return s.agentCfg.ConfigPath + ".candidate.json"
|
|
}
|
|
|
|
func readConfigFileStatus(path string) configFileStatus {
|
|
out := configFileStatus{Path: filepath.ToSlash(path)}
|
|
if strings.TrimSpace(path) == "" {
|
|
out.Error = "path is empty"
|
|
return out
|
|
}
|
|
|
|
info, err := assets.Info(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return out
|
|
}
|
|
out.Error = err.Error()
|
|
return out
|
|
}
|
|
out.Exists = true
|
|
out.Sha256 = info.Sha256
|
|
out.Size = info.Size
|
|
out.MtimeMS = info.MtimeMS
|
|
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
out.Error = err.Error()
|
|
return out
|
|
}
|
|
var root struct {
|
|
Metadata map[string]any `json:"metadata"`
|
|
}
|
|
if err := json.Unmarshal(b, &root); err != nil {
|
|
out.Error = "config is not valid json: " + err.Error()
|
|
return out
|
|
}
|
|
if len(root.Metadata) > 0 {
|
|
out.Metadata = root.Metadata
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *Server) handleAssets(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
errorJSON(w, http.StatusMethodNotAllowed, "method not allowed")
|
|
return
|
|
}
|
|
if !s.authorize(r, false) {
|
|
errorJSON(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
|
|
resp := map[string]any{}
|
|
if bin, err := assets.ExecutableInfo(); err == nil {
|
|
resp["agent_binary"] = bin
|
|
} else {
|
|
resp["agent_binary_error"] = err.Error()
|
|
}
|
|
if st, err := os.Stat(s.agentCfg.ConfigPath); err == nil && !st.IsDir() {
|
|
if info, err := assets.Info(s.agentCfg.ConfigPath); err == nil {
|
|
resp["config"] = info
|
|
} else {
|
|
resp["config_error"] = err.Error()
|
|
}
|
|
}
|
|
lastGood := s.agentCfg.ConfigPath + ".last_good.json"
|
|
if st, err := os.Stat(lastGood); err == nil && !st.IsDir() {
|
|
if info, err := assets.Info(lastGood); err == nil {
|
|
resp["config_last_good"] = info
|
|
} else {
|
|
resp["config_last_good_error"] = err.Error()
|
|
}
|
|
}
|
|
if s.proc != nil && s.proc.Enabled() {
|
|
if bin, err := s.proc.BinaryInfo(); err == nil {
|
|
resp["media_server_binary"] = bin
|
|
} else {
|
|
resp["media_server_binary_error"] = err.Error()
|
|
}
|
|
}
|
|
if m, err := s.store.List(); err == nil {
|
|
resp["models"] = m
|
|
} else {
|
|
resp["models_error"] = err.Error()
|
|
}
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
func (s *Server) handleTask(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
errorJSON(w, http.StatusMethodNotAllowed, "method not allowed")
|
|
return
|
|
}
|
|
if !s.authorize(r, false) {
|
|
errorJSON(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
id := strings.TrimPrefix(r.URL.Path, "/v1/tasks/")
|
|
if strings.TrimSpace(id) == "" {
|
|
errorJSON(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
t, ok := s.tasks.Get(id)
|
|
if !ok {
|
|
errorJSON(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, t)
|
|
}
|
|
|
|
func (s *Server) recordAudit(r *http.Request, action string, ok bool, detail string) {
|
|
if s.audit == nil {
|
|
return
|
|
}
|
|
tok := strings.TrimSpace(r.Header.Get("X-RK-Token"))
|
|
entry := audit.Entry{
|
|
TimeMS: time.Now().UnixMilli(),
|
|
Action: action,
|
|
Success: ok,
|
|
RemoteIP: remoteIP(r),
|
|
TokenHash: tokenHash(tok),
|
|
Detail: detail,
|
|
RequestID: strings.TrimSpace(r.Header.Get("X-Request-Id")),
|
|
}
|
|
_ = s.audit.Record(entry)
|
|
}
|
|
|
|
func remoteIP(r *http.Request) string {
|
|
if v := strings.TrimSpace(r.Header.Get("X-Forwarded-For")); v != "" {
|
|
parts := strings.Split(v, ",")
|
|
if len(parts) > 0 {
|
|
return strings.TrimSpace(parts[0])
|
|
}
|
|
}
|
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
if err == nil {
|
|
return host
|
|
}
|
|
return r.RemoteAddr
|
|
}
|
|
|
|
func tokenHash(tok string) string {
|
|
if tok == "" {
|
|
return ""
|
|
}
|
|
h := sha256.Sum256([]byte(tok))
|
|
return hex.EncodeToString(h[:])
|
|
}
|