Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Git
.git
.gitignore

# Documentation
README.md
CHANGELOG.md
docs/
*.md

# CI/CD
.github/
.goreleaser.yml

# Build artifacts
imposter
*.exe

# Development files
Makefile
since.yaml

# Installation scripts
install/

# Test files
*_test.go
testdata/

# License
LICENSE
31 changes: 31 additions & 0 deletions Dockerfile
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main outofcoffee/imposter Docker images include the CLI already.

You can extend from them and run imposter <some command> within your container.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that, i should remove the self Dockerfile included in project due to is in main outofcoffe/imposter project.

Was not so clear for me how the different projects are joined between them, u usually works in maven projects multimodule or angular "monorepo" solutions, but it is my first experience mergering go parts and kotlin parts.

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
FROM golang:1.23-alpine AS builder

WORKDIR /app

# Copy go mod files first for better caching
COPY go.mod go.sum ./
RUN go mod download

# Copy source code
COPY . .

# Build the binary
RUN go build -tags lambda.norpc -o imposter-cli

# Final stage
FROM alpine:latest

RUN apk --no-cache add ca-certificates curl openjdk17-jre docker-cli

WORKDIR /app

# Copy the binary from builder stage
COPY --from=builder /app/imposter-cli /usr/local/bin/imposter

# Create directory for output
RUN mkdir -p /output

EXPOSE 8080

# Default command shows help
CMD ["imposter", "--help"]
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ Example:

imposter proxy https://example.com

SOAP 1.1/1.2 example:

imposter proxy --soap1.1 http://soap-service.example.com/service

Usage:

```
Expand All @@ -198,10 +202,12 @@ Flags:
--flat Flatten the response file structure
-h, --help help for proxy
-i, --ignore-duplicate-requests Ignore duplicate requests with same method and URI (default true)
--insecure Skip TLS certificate verification for HTTPS upstream servers
-o, --output-dir string Directory in which HTTP exchanges are recorded (default: current working directory)
-p, --port int Port on which to listen (default 8080)
-H, --response-headers strings Record only these response headers
-r, --rewrite-urls Rewrite upstream URL in response body to proxy URL
--soap1.1 Enable SOAP 1.1/1.2 aware mode for capturing requests/responses with action-based differentiation
```

### Pull engine
Expand Down
8 changes: 7 additions & 1 deletion cmd/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ var proxyFlags = struct {
ignoreDuplicateRequests bool
recordOnlyResponseHeaders []string
flatResponseFileStructure bool
soap11Mode bool
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's name this soapMode with the flag --soap since it supports both SOAP 1.1 and 1.2.

insecure bool
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's name this insecureHttps with flag --insecure-https to clarify what we mean by insecure.

}{}

// proxyCmd represents the up command
Expand All @@ -59,6 +61,8 @@ var proxyCmd = &cobra.Command{
IgnoreDuplicateRequests: proxyFlags.ignoreDuplicateRequests,
RecordOnlyResponseHeaders: proxyFlags.recordOnlyResponseHeaders,
FlatResponseFileStructure: proxyFlags.flatResponseFileStructure,
Soap11Mode: proxyFlags.soap11Mode,
Insecure: proxyFlags.insecure,
}
proxyUpstream(upstream, proxyFlags.port, outputDir, proxyFlags.rewrite, options)
},
Expand All @@ -73,6 +77,8 @@ func init() {
proxyCmd.Flags().BoolVarP(&proxyFlags.ignoreDuplicateRequests, "ignore-duplicate-requests", "i", true, "Ignore duplicate requests with same method and URI")
proxyCmd.Flags().StringSliceVarP(&proxyFlags.recordOnlyResponseHeaders, "response-headers", "H", nil, "Record only these response headers")
proxyCmd.Flags().BoolVar(&proxyFlags.flatResponseFileStructure, "flat", false, "Flatten the response file structure")
proxyCmd.Flags().BoolVar(&proxyFlags.soap11Mode, "soap1.1", false, "Enable SOAP 1.1 aware mode for capturing requests/responses with SOAPAction")
proxyCmd.Flags().BoolVar(&proxyFlags.insecure, "insecure", false, "Skip TLS certificate verification for HTTPS upstream servers")
rootCmd.AddCommand(proxyCmd)
}

Expand All @@ -88,7 +94,7 @@ func proxyUpstream(upstream string, port int, dir string, rewrite bool, options
_, _ = fmt.Fprintf(writer, "ok\n")
})
mux.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
proxy2.Handle(upstream, writer, request, func(reqBody *[]byte, statusCode int, respBody *[]byte, respHeaders *http.Header) (*[]byte, *http.Header) {
proxy2.Handle(upstream, writer, request, options.Insecure, func(reqBody *[]byte, statusCode int, respBody *[]byte, respHeaders *http.Header) (*[]byte, *http.Header) {
if rewrite {
respBody = proxy2.Rewrite(respHeaders, respBody, upstream, port)
}
Expand Down
70 changes: 67 additions & 3 deletions cmd/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"net/http"
"os"
"path"
"strings"
"testing"
"time"
)
Expand Down Expand Up @@ -61,6 +62,16 @@ func Test_proxyUpstream(t *testing.T) {
},
},
},
{
name: "proxy SOAP 1.1 service with SOAPAction",
args: args{
rewrite: false,
options: proxy.RecorderOptions{
FlatResponseFileStructure: false,
Soap11Mode: true,
},
},
},
}
for _, tt := range tests {
server, upstream, upstreamPort, err := startUpstream()
Expand All @@ -85,14 +96,29 @@ func Test_proxyUpstream(t *testing.T) {
t.Fatalf("proxy did not come up on port %d", port)
}

if err := sendRequestToProxy(port); err != nil {
t.Fatal(err)
// Send appropriate request based on test mode
if tt.args.options.Soap11Mode {
if err := sendSoapRequestToProxy(port); err != nil {
t.Fatal(err)
}
} else {
if err := sendRequestToProxy(port); err != nil {
t.Fatal(err)
}
}

upstreamHostAndPort := fmt.Sprintf("localhost-%d", upstreamPort)
cfgFileName := upstreamHostAndPort + "-config.yaml"
var indexFileName string
if tt.args.options.FlatResponseFileStructure {

if tt.args.options.Soap11Mode {
// SOAP mode: expect SOAPAction in filename
if tt.args.options.FlatResponseFileStructure {
indexFileName = upstreamHostAndPort + "-POST-index_http___example_com_service_GetUser.txt"
} else {
indexFileName = "POST-index_http___example_com_service_GetUser.txt"
}
} else if tt.args.options.FlatResponseFileStructure {
indexFileName = upstreamHostAndPort + "-GET-index.txt"
} else {
indexFileName = "GET-index.txt"
Expand Down Expand Up @@ -156,3 +182,41 @@ func sendRequestToProxy(port int) error {
}
return nil
}

func sendSoapRequestToProxy(port int) error {
client := http.Client{
Timeout: 2 * time.Second,
}
url := fmt.Sprintf("http://localhost:%d", port)

soapBody := `<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetUser xmlns="http://example.com/service">
<userId>123</userId>
</GetUser>
</soap:Body>
</soap:Envelope>`

req, err := http.NewRequest("POST", url, strings.NewReader(soapBody))
if err != nil {
return fmt.Errorf("failed to create SOAP request: %s", err)
}

req.Header.Set("Content-Type", "text/xml; charset=utf-8")
req.Header.Set("SOAPAction", "http://example.com/service/GetUser")

resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("SOAP request failed for proxy at %s: %s", url, err)
}
if _, err := io.ReadAll(resp.Body); err != nil {
return fmt.Errorf("body read failed for proxy at %s: %s", url, err)
}
_ = resp.Body.Close()
if resp.StatusCode == 200 {
logger.Tracef("SOAP proxy up at %s", url)
return nil
}
return nil
}
132 changes: 132 additions & 0 deletions docs/soap_proxy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# SOAP 1.1/1.2 Proxy Support

The imposter-cli proxy command supports both SOAP 1.1 and SOAP 1.2 services with the `--soap1.1` flag, enabling action-aware recording and mock generation.

## Usage

```bash
imposter proxy --soap1.1 [URL] [flags]
```

### Flags

- `--soap1.1` - Enable SOAP 1.1/1.2 aware mode for capturing requests/responses with action-based differentiation
- `--insecure` - Skip TLS certificate verification for HTTPS upstream servers (useful for self-signed certificates)

### Example

```bash
imposter proxy --soap1.1 http://soap-service.example.com/service
```

For HTTPS services with self-signed certificates:

```bash
imposter proxy --soap1.1 --insecure https://soap-service.example.com/service
```

## Features

When `--soap1.1` is enabled:

1. **SOAP Action Detection** - Supports both SOAP 1.1 and SOAP 1.2 action specifications:
- **SOAP 1.1**: SOAPAction header (`SOAPAction: "http://example.com/GetUser"`)
- **SOAP 1.2**: Content-Type action parameter (`Content-Type: application/soap+xml;action="http://example.com/GetUser"`)

2. **Operation-Specific File Naming** - Files include action in names for same-endpoint differentiation

3. **Automatic Configuration** - Generated configs include proper header matching based on SOAP version

## Action Detection

### SOAP 1.1 Style (SOAPAction Header)
```http
POST /service HTTP/1.1
Content-Type: text/xml; charset=utf-8
SOAPAction: "http://example.com/GetUser"
```

### SOAP 1.2 Style (Content-Type Action Parameter)
```http
POST /service HTTP/1.1
Content-Type: application/soap+xml;charset=UTF-8;action="http://example.com/GetUser"
```

## File Naming

- Standard: `POST-endpoint.xml`
- SOAP 1.1/1.2: `POST-endpoint_http___example_com_GetUser.xml` (for action "http://example.com/GetUser")

## Generated Configuration

### SOAP 1.1 Configuration
```yaml
plugin: rest
path: /service
resources:
- method: POST
requestHeaders:
SOAPAction: "http://example.com/GetUser"
response:
file: POST-endpoint_http___example_com_GetUser.xml
```

### SOAP 1.2 Configuration
```yaml
plugin: rest
path: /service
resources:
- method: POST
requestHeaders:
Content-Type: "application/soap+xml;charset=UTF-8;action=\"http://example.com/GetUser\""
response:
file: POST-endpoint_http___example_com_GetUser.xml
```

## Docker Usage

Run the proxy in a Docker container:

```bash
# Basic usage
docker run -d --name imposter-soap-proxy -p 8080:8080 -v $PWD:/output \
nexus.bcn.crealogix.net:18080/imposter-cli-soap11:latest \
proxy --soap1.1 --capture-request-body --capture-request-headers --output-dir /output https://soap-service.example.com/service

# For HTTPS with self-signed certificates
docker run -d --name imposter-soap-proxy -p 8080:8080 -v $PWD:/output \
nexus.bcn.crealogix.net:18080/imposter-cli-soap11:latest \
proxy --soap1.1 --insecure --capture-request-body --capture-request-headers --output-dir /output https://soap-service.example.com/service
```

### Docker Container Notes

The `nexus.bcn.crealogix.net:18080/imposter-cli-soap11:latest` image includes:

- **Full engine support**: Supports `-t docker`, `-t jvm`, and `-t golang` engine types
- **Java Runtime**: OpenJDK 17 for JVM engine operations
- **Docker Client**: For Docker engine operations (requires socket mapping)
- **SOAP 1.1/1.2**: Enhanced proxy functionality for SOAP services

**Engine Usage Examples:**

```bash
# Using JVM engine (recommended)
docker run --rm -v $PWD:/config -p 8080:8080 \
nexus.bcn.crealogix.net:18080/imposter-cli-soap11:latest \
up -t jvm /config

# Using Docker engine (requires socket mapping and privileges)
docker run --rm --privileged -v /var/run/docker.sock:/var/run/docker.sock \
-v $PWD:/config -p 8080:8080 \
nexus.bcn.crealogix.net:18080/imposter-cli-soap11:latest \
up -t docker /config
```

## Compatibility

- **SOAP 1.1**: Full support via SOAPAction header
- **SOAP 1.2**: Full support via Content-Type action parameter
- **Mixed environments**: Automatically detects and handles both formats
- **Backward compatibility**: Maintains full compatibility with existing proxy functionality
- **Enterprise HTTPS**: Supports self-signed certificates with `--insecure` flag
1 change: 1 addition & 0 deletions internal/proxy/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var textMediaTypes = []string{
"application/javascript",
"application/json",
"application/xml",
"application/soap\\+xml",
"application/x-www-form-urlencoded",
}

Expand Down
Loading
Loading