Skip to content

Commit

Permalink
feat: proxy can capture request body and headers.
Browse files Browse the repository at this point in the history
  • Loading branch information
outofcoffee committed Nov 20, 2024
1 parent 18db7b1 commit 01dd6ca
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 35 deletions.
9 changes: 8 additions & 1 deletion cmd/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ var proxyFlags = struct {
port int
outputDir string
rewrite bool
captureRequestBody bool
captureRequestHeaders bool
ignoreDuplicateRequests bool
recordOnlyResponseHeaders []string
flatResponseFileStructure bool
Expand All @@ -52,6 +54,8 @@ var proxyCmd = &cobra.Command{
outputDir = workingDir
}
options := proxy.RecorderOptions{
CaptureRequestBody: proxyFlags.captureRequestBody,
CaptureRequestHeaders: proxyFlags.captureRequestHeaders,
IgnoreDuplicateRequests: proxyFlags.ignoreDuplicateRequests,
RecordOnlyResponseHeaders: proxyFlags.recordOnlyResponseHeaders,
FlatResponseFileStructure: proxyFlags.flatResponseFileStructure,
Expand All @@ -63,6 +67,8 @@ var proxyCmd = &cobra.Command{
func init() {
proxyCmd.Flags().IntVarP(&proxyFlags.port, "port", "p", 8080, "Port on which to listen")
proxyCmd.Flags().StringVarP(&proxyFlags.outputDir, "output-dir", "o", "", "Directory in which HTTP exchanges are recorded (default: current working directory)")
proxyCmd.Flags().BoolVar(&proxyFlags.captureRequestBody, "capture-request-body", false, "Capture the request body")
proxyCmd.Flags().BoolVar(&proxyFlags.captureRequestHeaders, "capture-request-headers", false, "Capture the request headers")
proxyCmd.Flags().BoolVarP(&proxyFlags.rewrite, "rewrite-urls", "r", false, "Rewrite upstream URL in response body to proxy URL")
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")
Expand All @@ -82,12 +88,13 @@ 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) {
proxy.Handle(upstream, writer, request, func(statusCode int, respBody *[]byte, respHeaders *http.Header) (*[]byte, *http.Header) {
proxy.Handle(upstream, writer, request, func(reqBody *[]byte, statusCode int, respBody *[]byte, respHeaders *http.Header) (*[]byte, *http.Header) {
if rewrite {
respBody = proxy.Rewrite(respHeaders, respBody, upstream, port)
}
recorderC <- proxy.HttpExchange{
Request: request,
RequestBody: reqBody,
StatusCode: statusCode,
ResponseBody: respBody,
ResponseHeaders: respHeaders,
Expand Down
15 changes: 11 additions & 4 deletions impostermodel/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,18 @@ type ResponseConfig struct {
Headers *map[string]string `json:"headers,omitempty"`
}

type RequestBody struct {
Operator string `json:"operator"`
Value string `json:"value"`
}

type Resource struct {
Path string `json:"path"`
Method string `json:"method"`
QueryParams *map[string]string `json:"queryParams,omitempty"`
Response *ResponseConfig `json:"response,omitempty"`
Path string `json:"path"`
Method string `json:"method"`
QueryParams *map[string]string `json:"queryParams,omitempty"`
RequestBody *RequestBody `json:"requestBody,omitempty"`
RequestHeaders *map[string]string `json:"requestHeaders,omitempty"`
Response *ResponseConfig `json:"response,omitempty"`
}

type PluginConfig struct {
Expand Down
36 changes: 36 additions & 0 deletions proxy/content.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package proxy

import (
"mime"
"regexp"
)

// textMediaTypes are the media types that are eligible for
// request capture and response rewriting.
var textMediaTypes = []string{
"text/.+",
"application/javascript",
"application/json",
"application/xml",
"application/x-www-form-urlencoded",
}

// isTextContentType returns true if the media type is eligible for
// request capture and response rewriting.
func isTextContentType(contentType string) bool {
if contentType == "" {
logger.Warnf("missing content type")
return false
}
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil {
logger.Warnf("failed to parse content type: %v: %v", contentType, err)
return false
}
for _, rewriteMediaType := range textMediaTypes {
if matched, _ := regexp.MatchString(rewriteMediaType, mediaType); matched {
return true
}
}
return false
}
5 changes: 3 additions & 2 deletions proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (

type HttpExchange struct {
Request *http.Request
RequestBody *[]byte
StatusCode int
ResponseBody *[]byte
ResponseHeaders *http.Header
Expand Down Expand Up @@ -80,7 +81,7 @@ func Handle(
upstream string,
w http.ResponseWriter,
req *http.Request,
listener func(statusCode int, respBody *[]byte, respHeaders *http.Header) (*[]byte, *http.Header),
listener func(reqBody *[]byte, statusCode int, respBody *[]byte, respHeaders *http.Header) (*[]byte, *http.Header),
) {
startTime := time.Now()

Expand All @@ -101,7 +102,7 @@ func Handle(
return
}

responseBody, respHeaders = listener(statusCode, responseBody, respHeaders)
responseBody, respHeaders = listener(requestBody, statusCode, responseBody, respHeaders)

err = sendResponse(w, respHeaders, statusCode, responseBody, client)
if err != nil {
Expand Down
33 changes: 32 additions & 1 deletion proxy/recorder.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import (
)

type RecorderOptions struct {
CaptureRequestBody bool
CaptureRequestHeaders bool
IgnoreDuplicateRequests bool
RecordOnlyResponseHeaders []string
FlatResponseFileStructure bool
Expand Down Expand Up @@ -162,7 +164,12 @@ func getResponseFile(
}
}

func buildResource(dir string, options RecorderOptions, exchange HttpExchange, respFile string) (impostermodel.Resource, error) {
func buildResource(
dir string,
options RecorderOptions,
exchange HttpExchange,
respFile string,
) (impostermodel.Resource, error) {
req := *exchange.Request
response := &impostermodel.ResponseConfig{
StatusCode: exchange.StatusCode,
Expand All @@ -188,6 +195,30 @@ func buildResource(dir string, options RecorderOptions, exchange HttpExchange, r
}
resource.QueryParams = &queryParams
}
if options.CaptureRequestHeaders && len(req.Header) > 0 {
headers := make(map[string]string)
for headerName, headerValues := range req.Header {
shouldSkip := stringutil.Contains(skipProxyHeaders, headerName) || stringutil.Contains(skipRecordHeaders, headerName)
if !shouldSkip && len(headerValues) > 0 {
headers[headerName] = headerValues[0]
}
}
resource.RequestHeaders = &headers
}
if options.CaptureRequestBody && exchange.RequestBody != nil {
contentType := req.Header.Get("Content-Type")
if !isTextContentType(contentType) {
logger.Debugf("unsupported content type '%s' for capture - skipping request body capture", contentType)
} else {
reqBody := *exchange.RequestBody
if len(reqBody) > 0 {
resource.RequestBody = &impostermodel.RequestBody{
Value: string(reqBody),
Operator: "EqualTo",
}
}
}
}
if len(*exchange.ResponseHeaders) > 0 {
headers := make(map[string]string)
for headerName, headerValues := range *exchange.ResponseHeaders {
Expand Down
29 changes: 2 additions & 27 deletions proxy/rewrite.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,38 +19,13 @@ package proxy
import (
"bytes"
"fmt"
"mime"
"net/http"
"regexp"
)

var rewriteMediaTypes = []string{
"text/.+",
"application/javascript",
"application/json",
"application/xml",
}

func Rewrite(respHeaders *http.Header, respBody *[]byte, upstream string, port int) *[]byte {
contentType := (*respHeaders).Get("Content-Type")
if contentType == "" {
logger.Warnf("no content type - skipping rewrite")
return respBody
}
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil {
logger.Warnf("failed to parse content type - skipping rewrite: %v", err)
return respBody
}
rewrite := false
for _, rewriteMediaType := range rewriteMediaTypes {
if matched, _ := regexp.MatchString(rewriteMediaType, mediaType); matched {
rewrite = true
break
}
}
if !rewrite {
logger.Debugf("unsupported content type %s for rewrite - skipping rewrite", mediaType)
if !isTextContentType(contentType) {
logger.Debugf("unsupported content type '%s' for rewrite - skipping rewrite", contentType)
return respBody
}
rewritten := bytes.ReplaceAll(*respBody, []byte(upstream), []byte(fmt.Sprintf("http://localhost:%d", port)))
Expand Down

0 comments on commit 01dd6ca

Please sign in to comment.