package web import ( "embed" "encoding/json" "io/fs" "log" "net/http" "github.com/duffy/usb-server/internal/config" "github.com/duffy/usb-server/internal/token" ) //go:embed static var staticFiles embed.FS // Handler provides the web UI and API type Handler struct { cfg *config.Config cfgPath string mux *http.ServeMux // Callbacks for device operations GetDevices func() interface{} AttachDevice func(clientID, busID string) error DetachDevice func(clientID, busID string) error InstallService func() error UninstallService func() error GetStatus func() map[string]interface{} } // NewHandler creates a new web handler func NewHandler(cfg *config.Config, cfgPath string) *Handler { h := &Handler{ cfg: cfg, cfgPath: cfgPath, mux: http.NewServeMux(), } h.setupRoutes() return h } func (h *Handler) setupRoutes() { // Static files staticFS, _ := fs.Sub(staticFiles, "static") h.mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS)))) h.mux.HandleFunc("/", h.handleIndex) // API endpoints h.mux.HandleFunc("/api/status", h.handleStatus) h.mux.HandleFunc("/api/devices", h.handleDevices) h.mux.HandleFunc("/api/attach", h.handleAttach) h.mux.HandleFunc("/api/detach", h.handleDetach) h.mux.HandleFunc("/api/config", h.handleConfig) h.mux.HandleFunc("/api/generate-token", h.handleGenerateToken) h.mux.HandleFunc("/api/apply-tokens", h.handleApplyTokens) h.mux.HandleFunc("/api/service/install", h.handleServiceInstall) h.mux.HandleFunc("/api/service/uninstall", h.handleServiceUninstall) } // ServeHTTP implements http.Handler func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.mux.ServeHTTP(w, r) } func (h *Handler) handleIndex(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } data, err := staticFiles.ReadFile("static/index.html") if err != nil { http.Error(w, "Internal error", 500) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write(data) } func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) { status := map[string]interface{}{ "connected": false, "mode": h.cfg.Mode, "name": h.cfg.Name, } if h.GetStatus != nil { status = h.GetStatus() } writeJSON(w, status) } func (h *Handler) handleDevices(w http.ResponseWriter, r *http.Request) { if h.GetDevices == nil { writeJSON(w, map[string]interface{}{"mode": h.cfg.Mode}) return } writeJSON(w, h.GetDevices()) } func (h *Handler) handleAttach(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "Method not allowed", 405) return } var req struct { ClientID string `json:"client_id"` BusID string `json:"bus_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, map[string]interface{}{"ok": false, "error": "invalid request"}) return } if h.AttachDevice == nil { writeJSON(w, map[string]interface{}{"ok": false, "error": "not in use mode"}) return } if err := h.AttachDevice(req.ClientID, req.BusID); err != nil { writeJSON(w, map[string]interface{}{"ok": false, "error": err.Error()}) return } writeJSON(w, map[string]interface{}{"ok": true}) } func (h *Handler) handleDetach(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "Method not allowed", 405) return } var req struct { ClientID string `json:"client_id"` BusID string `json:"bus_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, map[string]interface{}{"ok": false, "error": "invalid request"}) return } if h.DetachDevice == nil { writeJSON(w, map[string]interface{}{"ok": false, "error": "not in use mode"}) return } if err := h.DetachDevice(req.ClientID, req.BusID); err != nil { writeJSON(w, map[string]interface{}{"ok": false, "error": err.Error()}) return } writeJSON(w, map[string]interface{}{"ok": true}) } func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { writeJSON(w, h.cfg) return } if r.Method == "POST" { var updates struct { RelayAddr string `json:"relay_addr"` Mode string `json:"mode"` Name string `json:"name"` WebPort int `json:"web_port"` } if err := json.NewDecoder(r.Body).Decode(&updates); err != nil { writeJSON(w, map[string]interface{}{"ok": false, "error": "invalid request"}) return } if updates.RelayAddr != "" { h.cfg.RelayAddr = updates.RelayAddr } if updates.Mode == "share" || updates.Mode == "use" { h.cfg.Mode = updates.Mode } if updates.Name != "" { h.cfg.Name = updates.Name } if updates.WebPort > 0 { h.cfg.WebPort = updates.WebPort } if err := h.cfg.Save(h.cfgPath); err != nil { writeJSON(w, map[string]interface{}{"ok": false, "error": err.Error()}) return } writeJSON(w, map[string]interface{}{"ok": true}) return } http.Error(w, "Method not allowed", 405) } func (h *Handler) handleGenerateToken(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "Method not allowed", 405) return } tokens, err := token.Generate() if err != nil { writeJSON(w, map[string]interface{}{"error": err.Error()}) return } hash := tokens.Hash() // Save tokens to config h.cfg.Token1 = tokens.Token1 h.cfg.Token2 = tokens.Token2 h.cfg.Token3 = tokens.Token3 h.cfg.Hash = hash h.cfg.Save(h.cfgPath) writeJSON(w, map[string]interface{}{ "token1": tokens.Token1, "token2": tokens.Token2, "token3": tokens.Token3, "hash": hash, }) } func (h *Handler) handleApplyTokens(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "Method not allowed", 405) return } var req struct { Token1 string `json:"token1"` Token2 string `json:"token2"` Token3 string `json:"token3"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, map[string]interface{}{"ok": false, "error": "invalid request"}) return } hash := token.HashFromTokens(req.Token1, req.Token2, req.Token3) h.cfg.Token1 = req.Token1 h.cfg.Token2 = req.Token2 h.cfg.Token3 = req.Token3 h.cfg.Hash = hash h.cfg.Save(h.cfgPath) writeJSON(w, map[string]interface{}{"ok": true, "hash": hash}) } func (h *Handler) handleServiceInstall(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "Method not allowed", 405) return } if h.InstallService == nil { writeJSON(w, map[string]interface{}{"error": "service management not available"}) return } if err := h.InstallService(); err != nil { writeJSON(w, map[string]interface{}{"error": err.Error()}) return } writeJSON(w, map[string]interface{}{"message": "Service installiert und gestartet"}) } func (h *Handler) handleServiceUninstall(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "Method not allowed", 405) return } if h.UninstallService == nil { writeJSON(w, map[string]interface{}{"error": "service management not available"}) return } if err := h.UninstallService(); err != nil { writeJSON(w, map[string]interface{}{"error": err.Error()}) return } writeJSON(w, map[string]interface{}{"message": "Service deinstalliert"}) } func writeJSON(w http.ResponseWriter, v interface{}) { w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(v); err != nil { log.Printf("[web] JSON encode error: %v", err) } }