From 455a85e6a037e30fb753ab47c2d2e6b6f5d3ad3e Mon Sep 17 00:00:00 2001 From: yusing Date: Thu, 8 May 2025 20:59:32 +0800 Subject: [PATCH] feat(docker): add Docker socket proxy support and related configurations - Introduced Docker socket proxy handling in the agent. - Added environment variables for Docker socket configuration. - Implemented new Docker handler with endpoint permissions based on environment settings. - Added tests for Docker handler functionality. - Updated go.mod to include gorilla/mux for routing. --- agent/cmd/main.go | 13 + agent/go.mod | 1 + agent/go.sum | 2 + agent/pkg/agent/templates/agent.compose.yml | 30 ++ agent/pkg/env/env.go | 80 +++- agent/pkg/handler/docker_handler_test.go | 414 ++++++++++++++++++++ agent/pkg/handler/docker_socket.go | 7 + agent/pkg/handler/handler.go | 153 ++++++++ internal/common/env.go | 8 +- 9 files changed, 701 insertions(+), 7 deletions(-) create mode 100644 agent/pkg/handler/docker_handler_test.go diff --git a/agent/cmd/main.go b/agent/cmd/main.go index 42507d5..adcccd3 100644 --- a/agent/cmd/main.go +++ b/agent/cmd/main.go @@ -5,11 +5,13 @@ import ( "github.com/yusing/go-proxy/agent/pkg/agent" "github.com/yusing/go-proxy/agent/pkg/env" + "github.com/yusing/go-proxy/agent/pkg/handler" "github.com/yusing/go-proxy/agent/pkg/server" "github.com/yusing/go-proxy/internal/gperr" "github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging/memlogger" "github.com/yusing/go-proxy/internal/metrics/systeminfo" + httpServer "github.com/yusing/go-proxy/internal/net/gphttp/server" "github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/pkg" ) @@ -55,6 +57,17 @@ Tips: } server.StartAgentServer(t, opts) + + if env.DockerSocketAddr != "" { + logging.Info().Msgf("Docker socket listening on: %s", env.DockerSocketAddr) + opts := httpServer.Options{ + Name: "docker", + HTTPAddr: env.DockerSocketAddr, + Handler: handler.NewDockerHandler(), + } + httpServer.StartServer(t, opts) + } + systeminfo.Poller.Start() task.WaitExit(3) diff --git a/agent/go.mod b/agent/go.mod index e57e1c0..9d614b2 100644 --- a/agent/go.mod +++ b/agent/go.mod @@ -7,6 +7,7 @@ replace github.com/yusing/go-proxy => .. require ( github.com/coder/websocket v1.8.13 github.com/docker/docker v28.1.1+incompatible + github.com/gorilla/mux v1.8.1 github.com/rs/zerolog v1.34.0 github.com/stretchr/testify v1.10.0 github.com/yusing/go-proxy v0.12.3 diff --git a/agent/go.sum b/agent/go.sum index a760f51..f1d3591 100644 --- a/agent/go.sum +++ b/agent/go.sum @@ -84,6 +84,8 @@ github.com/google/pprof v0.0.0-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gotify/server/v2 v2.6.3 h1:2sLDRsQ/No1+hcFwFDvjNtwKepfCSIR8L3BkXl/Vz1I= diff --git a/agent/pkg/agent/templates/agent.compose.yml b/agent/pkg/agent/templates/agent.compose.yml index 1397640..0167d62 100644 --- a/agent/pkg/agent/templates/agent.compose.yml +++ b/agent/pkg/agent/templates/agent.compose.yml @@ -9,6 +9,36 @@ services: AGENT_PORT: "{{.Port}}" AGENT_CA_CERT: "{{.CACert}}" AGENT_SSL_CERT: "{{.SSLCert}}" + # docker socket proxy + # unset DOCKER_SOCKET_ADDR to disable + DOCKER_SOCKET_ADDR: 127.0.0.1:2375 + POST: false + ALLOW_RESTARTS: false + ALLOW_START: false + ALLOW_STOP: false + AUTH: false + BUILD: false + COMMIT: false + CONFIGS: false + CONTAINERS: false + DISTRIBUTIONS: false + EVENTS: true + EXEC: false + GRPC: false + IMAGES: false + INFO: false + NETWORKS: false + NODES: false + PING: true + PLUGINS: false + SECRETS: false + SERVICES: false + SESSION: false + SWARM: false + SYSTEM: false + TASKS: false + VERSION: true + VOLUMES: false volumes: - /var/run/docker.sock:/var/run/docker.sock - ./data:/app/data diff --git a/agent/pkg/env/env.go b/agent/pkg/env/env.go index cc27328..f15fd88 100644 --- a/agent/pkg/env/env.go +++ b/agent/pkg/env/env.go @@ -15,10 +15,82 @@ func DefaultAgentName() string { } var ( - AgentName = common.GetEnvString("AGENT_NAME", DefaultAgentName()) - AgentPort = common.GetEnvInt("AGENT_PORT", 8890) + AgentName string + AgentPort int + AgentSkipClientCertCheck bool + AgentCACert string + AgentSSLCert string + + DockerSocketAddr string + DockerPost bool + DockerRestarts bool + DockerStart bool + DockerStop bool + DockerAuth bool + DockerBuild bool + DockerCommit bool + DockerConfigs bool + DockerContainers bool + DockerDistributions bool + DockerEvents bool + DockerExec bool + DockerGrpc bool + DockerImages bool + DockerInfo bool + DockerNetworks bool + DockerNodes bool + DockerPing bool + DockerPlugins bool + DockerSecrets bool + DockerServices bool + DockerSession bool + DockerSwarm bool + DockerSystem bool + DockerTasks bool + DockerVersion bool + DockerVolumes bool +) + +func init() { + Load() +} + +func Load() { + AgentName = common.GetEnvString("AGENT_NAME", DefaultAgentName()) + AgentPort = common.GetEnvInt("AGENT_PORT", 8890) AgentSkipClientCertCheck = common.GetEnvBool("AGENT_SKIP_CLIENT_CERT_CHECK", false) - AgentCACert = common.GetEnvString("AGENT_CA_CERT", "") + AgentCACert = common.GetEnvString("AGENT_CA_CERT", "") AgentSSLCert = common.GetEnvString("AGENT_SSL_CERT", "") -) + + // docker socket proxy + DockerSocketAddr = common.GetEnvString("DOCKER_SOCKET_ADDR", "127.0.0.1:2375") + + DockerPost = common.GetEnvBool("POST", false) + DockerRestarts = common.GetEnvBool("ALLOW_RESTARTS", false) + DockerStart = common.GetEnvBool("ALLOW_START", false) + DockerStop = common.GetEnvBool("ALLOW_STOP", false) + DockerAuth = common.GetEnvBool("AUTH", false) + DockerBuild = common.GetEnvBool("BUILD", false) + DockerCommit = common.GetEnvBool("COMMIT", false) + DockerConfigs = common.GetEnvBool("CONFIGS", false) + DockerContainers = common.GetEnvBool("CONTAINERS", false) + DockerDistributions = common.GetEnvBool("DISTRIBUTIONS", false) + DockerEvents = common.GetEnvBool("EVENTS", true) + DockerExec = common.GetEnvBool("EXEC", false) + DockerGrpc = common.GetEnvBool("GRPC", false) + DockerImages = common.GetEnvBool("IMAGES", false) + DockerInfo = common.GetEnvBool("INFO", false) + DockerNetworks = common.GetEnvBool("NETWORKS", false) + DockerNodes = common.GetEnvBool("NODES", false) + DockerPing = common.GetEnvBool("PING", true) + DockerPlugins = common.GetEnvBool("PLUGINS", false) + DockerSecrets = common.GetEnvBool("SECRETS", false) + DockerServices = common.GetEnvBool("SERVICES", false) + DockerSession = common.GetEnvBool("SESSION", false) + DockerSwarm = common.GetEnvBool("SWARM", false) + DockerSystem = common.GetEnvBool("SYSTEM", false) + DockerTasks = common.GetEnvBool("TASKS", false) + DockerVersion = common.GetEnvBool("VERSION", true) + DockerVolumes = common.GetEnvBool("VOLUMES", false) +} diff --git a/agent/pkg/handler/docker_handler_test.go b/agent/pkg/handler/docker_handler_test.go new file mode 100644 index 0000000..df9dfcd --- /dev/null +++ b/agent/pkg/handler/docker_handler_test.go @@ -0,0 +1,414 @@ +package handler + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/yusing/go-proxy/agent/pkg/env" +) + +func TestNewDockerHandler(t *testing.T) { + tests := []struct { + name string + method string + path string + envSetup func() + wantStatusCode int + }{ + { + name: "GET _ping allowed by default", + method: http.MethodGet, + path: "/_ping", + envSetup: func() {}, + wantStatusCode: http.StatusOK, + }, + { + name: "GET version allowed by default", + method: http.MethodGet, + path: "/version", + envSetup: func() {}, + wantStatusCode: http.StatusOK, + }, + { + name: "GET containers allowed when enabled", + method: http.MethodGet, + path: "/containers", + envSetup: func() { + env.DockerContainers = true + }, + wantStatusCode: http.StatusOK, + }, + { + name: "GET containers not allowed when disabled", + method: http.MethodGet, + path: "/containers", + envSetup: func() { + env.DockerContainers = false + }, + wantStatusCode: http.StatusForbidden, + }, + { + name: "POST not allowed by default", + method: http.MethodPost, + path: "/_ping", + envSetup: func() { + env.DockerPost = false + }, + wantStatusCode: http.StatusMethodNotAllowed, + }, + { + name: "POST allowed when enabled", + method: http.MethodPost, + path: "/_ping", + envSetup: func() { + env.DockerPost = true + env.DockerPing = true + }, + wantStatusCode: http.StatusOK, + }, + { + name: "Container restart not allowed when disabled", + method: http.MethodPost, + path: "/containers/test-container/restart", + envSetup: func() { + env.DockerPost = true + env.DockerContainers = true + env.DockerRestarts = false + }, + wantStatusCode: http.StatusForbidden, + }, + { + name: "Container restart allowed when enabled", + method: http.MethodPost, + path: "/containers/test-container/restart", + envSetup: func() { + env.DockerPost = true + env.DockerContainers = true + env.DockerRestarts = true + }, + wantStatusCode: http.StatusOK, + }, + { + name: "Container start not allowed when disabled", + method: http.MethodPost, + path: "/containers/test-container/start", + envSetup: func() { + env.DockerPost = true + env.DockerContainers = true + env.DockerStart = false + }, + wantStatusCode: http.StatusForbidden, + }, + { + name: "Container start allowed when enabled", + method: http.MethodPost, + path: "/containers/test-container/start", + envSetup: func() { + env.DockerPost = true + env.DockerContainers = true + env.DockerStart = true + }, + wantStatusCode: http.StatusOK, + }, + { + name: "Container stop not allowed when disabled", + method: http.MethodPost, + path: "/containers/test-container/stop", + envSetup: func() { + env.DockerPost = true + env.DockerContainers = true + env.DockerStop = false + }, + wantStatusCode: http.StatusForbidden, + }, + { + name: "Container stop allowed when enabled", + method: http.MethodPost, + path: "/containers/test-container/stop", + envSetup: func() { + env.DockerPost = true + env.DockerContainers = true + env.DockerStop = true + }, + wantStatusCode: http.StatusOK, + }, + { + name: "Versioned API paths work", + method: http.MethodGet, + path: "/v1.41/version", + envSetup: func() { + env.DockerVersion = true + }, + wantStatusCode: http.StatusOK, + }, + { + name: "PUT method not allowed", + method: http.MethodPut, + path: "/version", + envSetup: func() { + env.DockerVersion = true + }, + wantStatusCode: http.StatusMethodNotAllowed, + }, + { + name: "DELETE method not allowed", + method: http.MethodDelete, + path: "/version", + envSetup: func() { + env.DockerVersion = true + }, + wantStatusCode: http.StatusMethodNotAllowed, + }, + } + + // Save original env values to restore after tests + originalContainers := env.DockerContainers + originalRestarts := env.DockerRestarts + originalStart := env.DockerStart + originalStop := env.DockerStop + originalPost := env.DockerPost + originalPing := env.DockerPing + originalVersion := env.DockerVersion + + defer func() { + // Restore original values + env.DockerContainers = originalContainers + env.DockerRestarts = originalRestarts + env.DockerStart = originalStart + env.DockerStop = originalStop + env.DockerPost = originalPost + env.DockerPing = originalPing + env.DockerVersion = originalVersion + }() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup environment for this test + tt.envSetup() + + // Create test handler that will record the response for verification + dockerHandler := NewDockerHandler() + + // Test server to capture the response + recorder := httptest.NewRecorder() + + // Create request + req, err := http.NewRequest(tt.method, tt.path, nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + // Process the request + dockerHandler.ServeHTTP(recorder, req) + + // Check response + if recorder.Code != tt.wantStatusCode { + t.Errorf("Expected status code %d, got %d", + tt.wantStatusCode, recorder.Code) + } + }) + } +} + +// This test focuses on checking that all the path prefix handling works correctly +func TestNewDockerHandler_PathHandling(t *testing.T) { + tests := []struct { + name string + path string + envVarName string + envVarValue bool + method string + wantAllowed bool + }{ + {"Container path", "/containers/json", "DockerContainers", true, http.MethodGet, true}, + {"Container path disabled", "/containers/json", "DockerContainers", false, http.MethodGet, false}, + + {"Auth path", "/auth", "DockerAuth", true, http.MethodGet, true}, + {"Auth path disabled", "/auth", "DockerAuth", false, http.MethodGet, false}, + + {"Build path", "/build", "DockerBuild", true, http.MethodGet, true}, + {"Build path disabled", "/build", "DockerBuild", false, http.MethodGet, false}, + + {"Commit path", "/commit", "DockerCommit", true, http.MethodGet, true}, + {"Commit path disabled", "/commit", "DockerCommit", false, http.MethodGet, false}, + + {"Configs path", "/configs", "DockerConfigs", true, http.MethodGet, true}, + {"Configs path disabled", "/configs", "DockerConfigs", false, http.MethodGet, false}, + + {"Distributions path", "/distributions", "DockerDistributions", true, http.MethodGet, true}, + {"Distributions path disabled", "/distributions", "DockerDistributions", false, http.MethodGet, false}, + + {"Events path", "/events", "DockerEvents", true, http.MethodGet, true}, + {"Events path disabled", "/events", "DockerEvents", false, http.MethodGet, false}, + + {"Exec path", "/exec", "DockerExec", true, http.MethodGet, true}, + {"Exec path disabled", "/exec", "DockerExec", false, http.MethodGet, false}, + + {"Grpc path", "/grpc", "DockerGrpc", true, http.MethodGet, true}, + {"Grpc path disabled", "/grpc", "DockerGrpc", false, http.MethodGet, false}, + + {"Images path", "/images", "DockerImages", true, http.MethodGet, true}, + {"Images path disabled", "/images", "DockerImages", false, http.MethodGet, false}, + + {"Info path", "/info", "DockerInfo", true, http.MethodGet, true}, + {"Info path disabled", "/info", "DockerInfo", false, http.MethodGet, false}, + + {"Networks path", "/networks", "DockerNetworks", true, http.MethodGet, true}, + {"Networks path disabled", "/networks", "DockerNetworks", false, http.MethodGet, false}, + + {"Nodes path", "/nodes", "DockerNodes", true, http.MethodGet, true}, + {"Nodes path disabled", "/nodes", "DockerNodes", false, http.MethodGet, false}, + + {"Plugins path", "/plugins", "DockerPlugins", true, http.MethodGet, true}, + {"Plugins path disabled", "/plugins", "DockerPlugins", false, http.MethodGet, false}, + + {"Secrets path", "/secrets", "DockerSecrets", true, http.MethodGet, true}, + {"Secrets path disabled", "/secrets", "DockerSecrets", false, http.MethodGet, false}, + + {"Services path", "/services", "DockerServices", true, http.MethodGet, true}, + {"Services path disabled", "/services", "DockerServices", false, http.MethodGet, false}, + + {"Session path", "/session", "DockerSession", true, http.MethodGet, true}, + {"Session path disabled", "/session", "DockerSession", false, http.MethodGet, false}, + + {"Swarm path", "/swarm", "DockerSwarm", true, http.MethodGet, true}, + {"Swarm path disabled", "/swarm", "DockerSwarm", false, http.MethodGet, false}, + + {"System path", "/system", "DockerSystem", true, http.MethodGet, true}, + {"System path disabled", "/system", "DockerSystem", false, http.MethodGet, false}, + + {"Tasks path", "/tasks", "DockerTasks", true, http.MethodGet, true}, + {"Tasks path disabled", "/tasks", "DockerTasks", false, http.MethodGet, false}, + + {"Volumes path", "/volumes", "DockerVolumes", true, http.MethodGet, true}, + {"Volumes path disabled", "/volumes", "DockerVolumes", false, http.MethodGet, false}, + + // Test versioned paths + {"Versioned auth", "/v1.41/auth", "DockerAuth", true, http.MethodGet, true}, + {"Versioned auth disabled", "/v1.41/auth", "DockerAuth", false, http.MethodGet, false}, + } + + defer func() { + // Restore original env values + env.Load() + }() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Reset all Docker* env vars to false for this test + env.Load() + + // Enable POST for all these tests + env.DockerPost = true + + // Set the specific env var for this test + switch tt.envVarName { + case "DockerContainers": + env.DockerContainers = tt.envVarValue + case "DockerRestarts": + env.DockerRestarts = tt.envVarValue + case "DockerStart": + env.DockerStart = tt.envVarValue + case "DockerStop": + env.DockerStop = tt.envVarValue + case "DockerAuth": + env.DockerAuth = tt.envVarValue + case "DockerBuild": + env.DockerBuild = tt.envVarValue + case "DockerCommit": + env.DockerCommit = tt.envVarValue + case "DockerConfigs": + env.DockerConfigs = tt.envVarValue + case "DockerDistributions": + env.DockerDistributions = tt.envVarValue + case "DockerEvents": + env.DockerEvents = tt.envVarValue + case "DockerExec": + env.DockerExec = tt.envVarValue + case "DockerGrpc": + env.DockerGrpc = tt.envVarValue + case "DockerImages": + env.DockerImages = tt.envVarValue + case "DockerInfo": + env.DockerInfo = tt.envVarValue + case "DockerNetworks": + env.DockerNetworks = tt.envVarValue + case "DockerNodes": + env.DockerNodes = tt.envVarValue + case "DockerPlugins": + env.DockerPlugins = tt.envVarValue + case "DockerSecrets": + env.DockerSecrets = tt.envVarValue + case "DockerServices": + env.DockerServices = tt.envVarValue + case "DockerSession": + env.DockerSession = tt.envVarValue + case "DockerSwarm": + env.DockerSwarm = tt.envVarValue + case "DockerSystem": + env.DockerSystem = tt.envVarValue + case "DockerTasks": + env.DockerTasks = tt.envVarValue + case "DockerVolumes": + env.DockerVolumes = tt.envVarValue + default: + t.Fatalf("Unknown env var: %s", tt.envVarName) + } + + // Create test handler + dockerHandler := NewDockerHandler() + + // Test server to capture the response + recorder := httptest.NewRecorder() + + // Create request + req, err := http.NewRequest(tt.method, tt.path, nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + // Process the request + dockerHandler.ServeHTTP(recorder, req) + + // Check if the status indicates if the path is allowed or not + isAllowed := recorder.Code != http.StatusForbidden + if isAllowed != tt.wantAllowed { + t.Errorf("Path %s with env %s=%v: got allowed=%v, want allowed=%v (status=%d)", + tt.path, tt.envVarName, tt.envVarValue, isAllowed, tt.wantAllowed, recorder.Code) + } + }) + } +} + +// TestNewDockerHandlerWithMockDocker mocks the Docker API to test the actual HTTP handler behavior +// This is a more comprehensive test that verifies the full request/response chain +func TestNewDockerHandlerWithMockDocker(t *testing.T) { + // Set up environment + env.DockerContainers = true + env.DockerPost = true + + // Create the handler + handler := NewDockerHandler() + + // Test a valid request + req, _ := http.NewRequest(http.MethodGet, "/containers", nil) + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + + if recorder.Code != http.StatusOK { + t.Errorf("Expected status OK for /containers, got %d", recorder.Code) + } + + // Test a disallowed path + env.DockerContainers = false + handler = NewDockerHandler() // recreate with new env + + req, _ = http.NewRequest(http.MethodGet, "/containers", nil) + recorder = httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + + if recorder.Code != http.StatusForbidden { + t.Errorf("Expected status Forbidden for /containers when disabled, got %d", recorder.Code) + } +} diff --git a/agent/pkg/handler/docker_socket.go b/agent/pkg/handler/docker_socket.go index 27bedf2..36fe928 100644 --- a/agent/pkg/handler/docker_socket.go +++ b/agent/pkg/handler/docker_socket.go @@ -16,6 +16,13 @@ func serviceUnavailable(w http.ResponseWriter, r *http.Request) { http.Error(w, "docker socket is not available", http.StatusServiceUnavailable) } +func mockDockerSocketHandler() http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("mock docker response")) + }) +} + func DockerSocketHandler() http.HandlerFunc { dockerClient, err := docker.NewClient(common.DockerHostFromEnv) if err != nil { diff --git a/agent/pkg/handler/handler.go b/agent/pkg/handler/handler.go index 48af218..20b4da7 100644 --- a/agent/pkg/handler/handler.go +++ b/agent/pkg/handler/handler.go @@ -4,9 +4,12 @@ import ( "fmt" "io" "net/http" + "strings" + "github.com/gorilla/mux" "github.com/yusing/go-proxy/agent/pkg/agent" "github.com/yusing/go-proxy/agent/pkg/env" + "github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/logging/memlogger" "github.com/yusing/go-proxy/internal/metrics/systeminfo" "github.com/yusing/go-proxy/internal/utils/strutils" @@ -47,3 +50,153 @@ func NewAgentHandler() http.Handler { mux.ServeMux.HandleFunc("/", DockerSocketHandler()) return mux } + +func endpointNotAllowed(w http.ResponseWriter, _ *http.Request) { + http.Error(w, "Endpoint not allowed", http.StatusForbidden) +} + +// ref: https://github.com/Tecnativa/docker-socket-proxy/blob/master/haproxy.cfg +func NewDockerHandler() http.Handler { + r := mux.NewRouter() + var socketHandler http.HandlerFunc + if common.IsTest { + socketHandler = mockDockerSocketHandler() + } else { + socketHandler = DockerSocketHandler() + } + + const apiVersionPrefix = `/{version:(?:v[\d\.]+)?}` + const containerPath = "/containers/{id:[a-zA-Z0-9_.-]+}" + + allowedPaths := []string{} + deniedPaths := []string{} + + if env.DockerContainers { + allowedPaths = append(allowedPaths, "/containers") + if !env.DockerRestarts { + deniedPaths = append(deniedPaths, containerPath+"/stop") + deniedPaths = append(deniedPaths, containerPath+"/restart") + deniedPaths = append(deniedPaths, containerPath+"/kill") + } + if !env.DockerStart { + deniedPaths = append(deniedPaths, containerPath+"/start") + } + if !env.DockerStop && env.DockerRestarts { + deniedPaths = append(deniedPaths, containerPath+"/stop") + } + } + if env.DockerAuth { + allowedPaths = append(allowedPaths, "/auth") + } + if env.DockerBuild { + allowedPaths = append(allowedPaths, "/build") + } + if env.DockerCommit { + allowedPaths = append(allowedPaths, "/commit") + } + if env.DockerConfigs { + allowedPaths = append(allowedPaths, "/configs") + } + if env.DockerDistributions { + allowedPaths = append(allowedPaths, "/distributions") + } + if env.DockerEvents { + allowedPaths = append(allowedPaths, "/events") + } + if env.DockerExec { + allowedPaths = append(allowedPaths, "/exec") + } + if env.DockerGrpc { + allowedPaths = append(allowedPaths, "/grpc") + } + if env.DockerImages { + allowedPaths = append(allowedPaths, "/images") + } + if env.DockerInfo { + allowedPaths = append(allowedPaths, "/info") + } + if env.DockerNetworks { + allowedPaths = append(allowedPaths, "/networks") + } + if env.DockerNodes { + allowedPaths = append(allowedPaths, "/nodes") + } + if env.DockerPing { + allowedPaths = append(allowedPaths, "/_ping") + } + if env.DockerPlugins { + allowedPaths = append(allowedPaths, "/plugins") + } + if env.DockerSecrets { + allowedPaths = append(allowedPaths, "/secrets") + } + if env.DockerServices { + allowedPaths = append(allowedPaths, "/services") + } + if env.DockerSession { + allowedPaths = append(allowedPaths, "/session") + } + if env.DockerSwarm { + allowedPaths = append(allowedPaths, "/swarm") + } + if env.DockerSystem { + allowedPaths = append(allowedPaths, "/system") + } + if env.DockerTasks { + allowedPaths = append(allowedPaths, "/tasks") + } + if env.DockerVersion { + allowedPaths = append(allowedPaths, "/version") + } + if env.DockerVolumes { + allowedPaths = append(allowedPaths, "/volumes") + } + + // Helper to determine if a path should be treated as a prefix + isPrefixPath := func(path string) bool { + return strings.Count(path, "/") == 1 + } + + // 1. Register Denied Paths (specific) + for _, path := range deniedPaths { + // Handle with version prefix + r.HandleFunc(apiVersionPrefix+path, endpointNotAllowed) + // Handle without version prefix + r.HandleFunc(path, endpointNotAllowed) + } + + // 2. Register Allowed Paths + for _, p := range allowedPaths { + fullPathWithVersion := apiVersionPrefix + p + if isPrefixPath(p) { + r.PathPrefix(fullPathWithVersion).Handler(socketHandler) + r.PathPrefix(p).Handler(socketHandler) + } else { + r.HandleFunc(fullPathWithVersion, socketHandler) + r.HandleFunc(p, socketHandler) + } + } + + // 3. Add fallback for any other routes + r.PathPrefix("/").HandlerFunc(endpointNotAllowed) + + // HTTP method filtering + if !env.DockerPost { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + switch req.Method { + case http.MethodGet: + r.ServeHTTP(w, req) + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } + }) + } + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + switch req.Method { + case http.MethodPost, http.MethodGet: + r.ServeHTTP(w, req) + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } + }) +} diff --git a/internal/common/env.go b/internal/common/env.go index ccfda82..5512c1c 100644 --- a/internal/common/env.go +++ b/internal/common/env.go @@ -82,10 +82,12 @@ func GetEnv[T any](key string, defaultValue T, parser func(string) (T, error)) T return defaultValue } +func stringstring(s string) (string, error) { + return s, nil +} + func GetEnvString(key string, defaultValue string) string { - return GetEnv(key, defaultValue, func(s string) (string, error) { - return s, nil - }) + return GetEnv(key, defaultValue, stringstring) } func GetEnvBool(key string, defaultValue bool) bool {