diff --git a/internal/config/env_validation.go b/internal/config/env_validation.go index 6139cd1b..65c75b4a 100644 --- a/internal/config/env_validation.go +++ b/internal/config/env_validation.go @@ -207,59 +207,68 @@ func validateContainerID(containerID string) error { return nil } -// checkPortMapping uses docker inspect to verify that the specified port is mapped -func checkPortMapping(containerID, port string) (bool, error) { +// runDockerInspect is a helper function that executes docker inspect with a given format template. +// It validates the container ID before running the command and returns the output as a string. +// +// Security Note: This is an internal helper function that should only be called with +// hardcoded format templates defined within this package. The formatTemplate parameter +// is not validated as it is never exposed to user input. +// +// Parameters: +// - containerID: The Docker container ID to inspect (validated before use) +// - formatTemplate: The Go template format string for docker inspect (e.g., "{{.Config.OpenStdin}}") +// +// Returns: +// - output: The trimmed output from docker inspect +// - error: Any validation or command execution error +func runDockerInspect(containerID, formatTemplate string) (string, error) { if err := validateContainerID(containerID); err != nil { - return false, err + return "", err } - // Use docker inspect to get port bindings - cmd := exec.Command("docker", "inspect", "--format", "{{json .NetworkSettings.Ports}}", containerID) + cmd := exec.Command("docker", "inspect", "--format", formatTemplate, containerID) output, err := cmd.Output() if err != nil { - return false, fmt.Errorf("docker inspect failed: %w", err) + return "", fmt.Errorf("docker inspect failed: %w", err) + } + + return strings.TrimSpace(string(output)), nil +} + +// checkPortMapping uses docker inspect to verify that the specified port is mapped +func checkPortMapping(containerID, port string) (bool, error) { + output, err := runDockerInspect(containerID, "{{json .NetworkSettings.Ports}}") + if err != nil { + return false, err } // Parse the port from the output portKey := fmt.Sprintf("%s/tcp", port) - outputStr := string(output) // Check if the port is in the output with a host binding // The format is like: {"8000/tcp":[{"HostIp":"0.0.0.0","HostPort":"8000"}]} - return strings.Contains(outputStr, portKey) && strings.Contains(outputStr, "HostPort"), nil + return strings.Contains(output, portKey) && strings.Contains(output, "HostPort"), nil } // checkStdinInteractive uses docker inspect to verify the container was started with -i flag func checkStdinInteractive(containerID string) bool { - if err := validateContainerID(containerID); err != nil { - return false - } - - // Use docker inspect to check stdin_open - cmd := exec.Command("docker", "inspect", "--format", "{{.Config.OpenStdin}}", containerID) - output, err := cmd.Output() + output, err := runDockerInspect(containerID, "{{.Config.OpenStdin}}") if err != nil { return false } - return strings.TrimSpace(string(output)) == "true" + return output == "true" } // checkLogDirMounted uses docker inspect to verify the log directory is mounted func checkLogDirMounted(containerID, logDir string) bool { - if err := validateContainerID(containerID); err != nil { - return false - } - - // Use docker inspect to get mounts - cmd := exec.Command("docker", "inspect", "--format", "{{json .Mounts}}", containerID) - output, err := cmd.Output() + output, err := runDockerInspect(containerID, "{{json .Mounts}}") if err != nil { return false } // Check if the log directory is in the mounts - return strings.Contains(string(output), logDir) + return strings.Contains(output, logDir) } // GetGatewayPortFromEnv returns the MCP_GATEWAY_PORT value, parsed as int diff --git a/internal/config/env_validation_test.go b/internal/config/env_validation_test.go index abbd6058..045e3ece 100644 --- a/internal/config/env_validation_test.go +++ b/internal/config/env_validation_test.go @@ -492,3 +492,50 @@ func TestValidateExecutionEnvironment(t *testing.T) { } }) } + +func TestRunDockerInspect(t *testing.T) { + tests := []struct { + name string + containerID string + formatTemplate string + shouldError bool + }{ + { + name: "empty container ID", + containerID: "", + formatTemplate: "{{.Config.OpenStdin}}", + shouldError: true, + }, + { + name: "invalid container ID - too short", + containerID: "abc123", + formatTemplate: "{{.Config.OpenStdin}}", + shouldError: true, + }, + { + name: "invalid container ID - special chars", + containerID: "abc;def123456", + formatTemplate: "{{.Config.OpenStdin}}", + shouldError: true, + }, + { + name: "valid container ID format - command will fail without docker", + containerID: "abcdef123456", + formatTemplate: "{{.Config.OpenStdin}}", + shouldError: true, // Will fail because container doesn't exist + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + output, err := runDockerInspect(tt.containerID, tt.formatTemplate) + + if tt.shouldError { + assert.Error(t, err, "Expected error but got none") + assert.Empty(t, output, "Expected empty output on error") + } else { + assert.NoError(t, err, "Unexpected error") + } + }) + } +}