package main import ( "context" "encoding/json" "flag" "fmt" "log" "net" "net/http" "os" "os/signal" "path/filepath" "strings" "syscall" "time" ) const Version = "0.2.0" func main() { listen := flag.String("listen", "127.0.0.1:7474", "host:port a escuchar (ej. 10.42.0.10:7474 para mesh wg1)") manifestPath := flag.String("manifest", defaultManifestPath(), "ruta al manifest YAML") auditPath := flag.String("audit", "local_files/audit.db", "ruta al audit.db sqlite") selfTest := flag.Bool("self-test", false, "smoke check, sale 0 si todo OK") flag.Parse() if *selfTest { fmt.Println("device_agent self-test OK") os.Exit(0) } // Cargar manifest. mf, err := LoadManifest(*manifestPath) if err != nil { log.Fatalf("manifest load %s: %v", *manifestPath, err) } log.Printf("manifest loaded device_id=%s capabilities=%d", mf.DeviceID, len(mf.Capabilities)) // Abrir audit DB. _ = os.MkdirAll(filepath.Dir(*auditPath), 0700) audit, err := OpenAudit(*auditPath) if err != nil { log.Fatalf("audit open %s: %v", *auditPath, err) } defer audit.Close() log.Printf("audit db opened %s", *auditPath) // Construir router. mux := http.NewServeMux() mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]any{ "ok": true, "device_id": mf.DeviceID, "version": Version, "ts": time.Now().Unix(), }) }) mux.HandleFunc("/capabilities", func(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]any{ "device_id": mf.DeviceID, "capabilities": mf.Capabilities, }) }) mux.HandleFunc("/capability", capabilityHandler(mf, audit)) srv := &http.Server{ Addr: *listen, Handler: loggingMiddleware(mux), ReadHeaderTimeout: 5 * time.Second, } // Listen explicit para detectar bind issues claro. ln, err := net.Listen("tcp", *listen) if err != nil { log.Fatalf("listen %s: %v", *listen, err) } log.Printf("device_agent v%s listening %s (device_id=%s)", Version, *listen, mf.DeviceID) // Graceful shutdown. ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() go func() { if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed { log.Fatalf("serve: %v", err) } }() <-ctx.Done() log.Println("shutdown signal received, draining...") shutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _ = srv.Shutdown(shutCtx) log.Println("bye") } func defaultManifestPath() string { home, _ := os.UserHomeDir() return filepath.Join(home, ".config", "device_agent", "manifest.yaml") } func writeJSON(w http.ResponseWriter, status int, body any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(body) } func writeError(w http.ResponseWriter, status int, code, msg string) { writeJSON(w, status, map[string]any{ "ok": false, "error": code, "msg": msg, }) } func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() ww := &statusWriter{ResponseWriter: w, status: 200} next.ServeHTTP(ww, r) dur := time.Since(start).Milliseconds() log.Printf("%s %s %d %dms from=%s", r.Method, r.URL.Path, ww.status, dur, clientIP(r)) }) } type statusWriter struct { http.ResponseWriter status int } func (s *statusWriter) WriteHeader(code int) { s.status = code s.ResponseWriter.WriteHeader(code) } func clientIP(r *http.Request) string { host, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { return r.RemoteAddr } if strings.HasPrefix(host, "::") { return host } return host }