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 SetAutoConnect func(vendorID, productID string, enabled bool) error IsAutoConnect func(vendorID, productID string) bool ForceDetachDevice 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/auto-connect", h.handleAutoConnect) h.mux.HandleFunc("/api/force-detach", h.handleForceDetach) 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"` AllowForceDetach *bool `json:"allow_force_detach,omitempty"` } 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 updates.AllowForceDetach != nil { h.cfg.AllowForceDetach = *updates.AllowForceDetach } 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) handleAutoConnect(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "Method not allowed", 405) return } var req struct { VendorID string `json:"vendor_id"` ProductID string `json:"product_id"` Enabled bool `json:"enabled"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, map[string]interface{}{"ok": false, "error": "invalid request"}) return } if h.SetAutoConnect == nil { writeJSON(w, map[string]interface{}{"ok": false, "error": "not in use mode"}) return } if err := h.SetAutoConnect(req.VendorID, req.ProductID, req.Enabled); err != nil { writeJSON(w, map[string]interface{}{"ok": false, "error": err.Error()}) return } writeJSON(w, map[string]interface{}{"ok": true}) } func (h *Handler) handleForceDetach(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.ForceDetachDevice == nil { writeJSON(w, map[string]interface{}{"ok": false, "error": "not in use mode"}) return } if err := h.ForceDetachDevice(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) 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) } }