diff --git a/internal/pluginhost/support.go b/internal/pluginhost/support.go new file mode 100644 --- /dev/null +++ b/internal/pluginhost/support.go @@ -0,0 +1,6 @@ +package pluginhost + +// SupportPluginHeaderValue reports whether the current binary was built with CGO enabled. +func SupportPluginHeaderValue() string { + return supportPluginValue +} diff --git a/internal/pluginhost/support_cgo.go b/internal/pluginhost/support_cgo.go new file mode 100644 --- /dev/null +++ b/internal/pluginhost/support_cgo.go @@ -0,0 +1,5 @@ +//go:build cgo + +package pluginhost + +const supportPluginValue = "1" diff --git a/internal/pluginhost/support_nocgo.go b/internal/pluginhost/support_nocgo.go new file mode 100644 --- /dev/null +++ b/internal/pluginhost/support_nocgo.go @@ -0,0 +1,5 @@ +//go:build !cgo + +package pluginhost + +const supportPluginValue = "0" diff --git a/internal/api/handlers/management/handler.go b/internal/api/handlers/management/handler.go --- a/internal/api/handlers/management/handler.go +++ b/internal/api/handlers/management/handler.go @@ -171,6 +171,7 @@ c.Header("X-CPA-VERSION", buildinfo.Version) c.Header("X-CPA-COMMIT", buildinfo.Commit) c.Header("X-CPA-BUILD-DATE", buildinfo.BuildDate) + c.Header("X-CPA-SUPPORT-PLUGIN", pluginhost.SupportPluginHeaderValue()) clientIP := c.ClientIP() localClient := clientIP == "127.0.0.1" || clientIP == "::1" diff --git a/internal/api/handlers/management/handler_test.go b/internal/api/handlers/management/handler_test.go --- a/internal/api/handlers/management/handler_test.go +++ b/internal/api/handlers/management/handler_test.go @@ -2,10 +2,13 @@ import ( "net/http" + "net/http/httptest" "strings" "testing" + "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" + "github.com/router-for-me/CLIProxyAPI/v7/internal/pluginhost" ) func TestAuthenticateManagementKey_LocalhostIPBan_BlocksCorrectKeyDuringBan(t *testing.T) { @@ -35,4 +38,52 @@ if !strings.HasPrefix(errMsg, "IP banned due to too many failed attempts. Try again in") { t.Fatalf("unexpected banned message: %q", errMsg) } +} + +func TestMiddlewareSetsSupportPluginHeader(t *testing.T) { + gin.SetMode(gin.TestMode) + + h := &Handler{ + cfg: &config.Config{}, + failedAttempts: make(map[string]*attemptInfo), + envSecret: "test-secret", + } + middleware := h.Middleware() + + t.Run("invalid key", func(t *testing.T) { + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = httptest.NewRequest(http.MethodGet, "/v0/management/config", nil) + c.Request.RemoteAddr = "127.0.0.1:12345" + c.Request.Header.Set("X-Management-Key", "wrong-secret") + + middleware(c) + + if rec.Code != http.StatusUnauthorized { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusUnauthorized) + } + if got := rec.Header().Get("X-CPA-SUPPORT-PLUGIN"); got != pluginhost.SupportPluginHeaderValue() { + t.Fatalf("X-CPA-SUPPORT-PLUGIN = %q, want %q", got, pluginhost.SupportPluginHeaderValue()) + } + }) + + t.Run("valid key", func(t *testing.T) { + engine := gin.New() + engine.GET("/v0/management/config", middleware, func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/v0/management/config", nil) + req.RemoteAddr = "127.0.0.1:12345" + req.Header.Set("X-Management-Key", "test-secret") + engine.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) + } + if got := rec.Header().Get("X-CPA-SUPPORT-PLUGIN"); got != pluginhost.SupportPluginHeaderValue() { + t.Fatalf("X-CPA-SUPPORT-PLUGIN = %q, want %q", got, pluginhost.SupportPluginHeaderValue()) + } + }) }