package api import ( "encoding/base64" "encoding/json" "fmt" "log/slog" "net/http" "go.bigb.es/shroud/internal/awgserver" "go.bigb.es/shroud/internal/config" "go.bigb.es/shroud/internal/metrics" "go.bigb.es/shroud/internal/ssserver" "go.bigb.es/shroud/internal/store" "go.bigb.es/shroud/internal/vless" ) type Handler struct { store store.Store ssServer *ssserver.Server awgServer *awgserver.Server awgConfig *config.AmneziaWGConfig vlessServer *vless.Server tracker *metrics.TransferTracker version string logger *slog.Logger } func NewHandler(s store.Store, ss *ssserver.Server, awg *awgserver.Server, awgCfg *config.AmneziaWGConfig, vl *vless.Server, tracker *metrics.TransferTracker, version string, logger *slog.Logger) *Handler { return &Handler{ store: s, ssServer: ss, awgServer: awg, awgConfig: awgCfg, vlessServer: vl, tracker: tracker, version: version, logger: logger, } } // NewRouter creates an HTTP mux with all API routes under the given secret prefix. func NewRouter(secret string, h *Handler) http.Handler { mux := http.NewServeMux() prefix := "/" + secret // Server endpoints. mux.HandleFunc("GET "+prefix+"/server", h.getServer) mux.HandleFunc("PUT "+prefix+"/name", h.renameServer) // Access key endpoints. mux.HandleFunc("GET "+prefix+"/access-keys", h.listAccessKeys) mux.HandleFunc("POST "+prefix+"/access-keys", h.createAccessKey) mux.HandleFunc("GET "+prefix+"/access-keys/{id}", h.getAccessKey) mux.HandleFunc("PUT "+prefix+"/access-keys/{id}", h.createAccessKeyWithID) mux.HandleFunc("DELETE "+prefix+"/access-keys/{id}", h.deleteAccessKey) mux.HandleFunc("PUT "+prefix+"/access-keys/{id}/name", h.renameAccessKey) // Data limit endpoints. mux.HandleFunc("PUT "+prefix+"/access-keys/{id}/data-limit", h.setKeyDataLimit) mux.HandleFunc("DELETE "+prefix+"/access-keys/{id}/data-limit", h.removeKeyDataLimit) mux.HandleFunc("PUT "+prefix+"/server/access-key-data-limit", h.setDefaultDataLimit) mux.HandleFunc("DELETE "+prefix+"/server/access-key-data-limit", h.removeDefaultDataLimit) // Port and hostname. mux.HandleFunc("PUT "+prefix+"/server/port-for-new-access-keys", h.setDefaultPort) mux.HandleFunc("PUT "+prefix+"/server/hostname-for-access-keys", h.setHostname) // AmneziaWG client config download. mux.HandleFunc("GET "+prefix+"/access-keys/{id}/awg-config", h.getAWGConfig) // Outline Smart Dialer config (AWG as fallback transport). mux.HandleFunc("GET "+prefix+"/access-keys/{id}/outline-config", h.getOutlineConfig) // Metrics endpoints. mux.HandleFunc("GET "+prefix+"/metrics/enabled", h.getMetricsEnabled) mux.HandleFunc("PUT "+prefix+"/metrics/enabled", h.setMetricsEnabled) mux.HandleFunc("GET "+prefix+"/metrics/transfer", h.getTransferMetrics) return corsMiddleware(mux) } func corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } func (h *Handler) syncKeys() error { keys := h.store.ListKeys() if h.ssServer != nil { if err := h.ssServer.SyncKeys(keys); err != nil { return err } } if h.awgServer != nil { if err := h.awgServer.SyncKeys(keys); err != nil { return err } } if h.vlessServer != nil { if err := h.vlessServer.SyncKeys(keys); err != nil { return err } } return nil } func (h *Handler) accessURL(key store.AccessKey) string { srv := h.store.GetServer() hostname := srv.Hostname if hostname == "" { hostname = "localhost" } // SIP002 URI format: ss://base64(method:password)@host:port#tag cred := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString( []byte(fmt.Sprintf("%s:%s", key.Method, key.Password)), ) return fmt.Sprintf("ss://%s@%s:%d#%s", cred, hostname, key.Port, key.Name) } // awgHostname resolves the hostname for AWG client endpoints. // Uses awg.hostname if set; falls back to serverHostname when muxer is enabled; // falls back to "localhost" if still empty. func (h *Handler) awgHostname(serverHostname string) string { hostname := h.awgConfig.AWGHostname(serverHostname) if hostname == "" { hostname = "localhost" } return hostname } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(v) } func writeError(w http.ResponseWriter, status int, msg string) { http.Error(w, msg, status) } func readJSON(r *http.Request, v any) error { defer r.Body.Close() return json.NewDecoder(r.Body).Decode(v) }