feat(forwarder): support plugin-based rewriter in HTTP body rewrite rules
Each rewriteBody/rewriteResponseBody/rewriteRequestBody rule can now optionally reference a named rewriter plugin (HTTP/gRPC backend) via the rewriter field. When set, body rewriting delegates to the plugin rather than applying pattern/replacement, while content-type filtering still applies. Coincident fixes: - rewriter/plugin/grpc: return nil when conn is nil (avoid nil-ptr panic) - rewriter/plugin/http: drain response body in defer to prevent leaks - config/parsing/rewriter: pass TimeoutOption to gRPC plugin - config/parsing/service: warn when referenced rewriter is not registered
This commit is contained in:
@@ -369,7 +369,7 @@ func (h *Sniffer) httpRoundTrip(ctx context.Context, rw, cc io.ReadWriteCloser,
|
||||
|
||||
// Rewrite request body before wrapping for recording,
|
||||
// so the recorder sees the rewritten content.
|
||||
if err = rewriteReqBody(req, reqBodyRewrites...); err != nil {
|
||||
if err = rewriteReqBody(ctx, req, reqBodyRewrites...); err != nil {
|
||||
log.Errorf("rewrite request body: %v", err)
|
||||
return
|
||||
}
|
||||
@@ -446,7 +446,7 @@ func (h *Sniffer) httpRoundTrip(ctx context.Context, rw, cc io.ReadWriteCloser,
|
||||
resp.Header.Set("Connection", "close")
|
||||
}
|
||||
|
||||
if err = rewriteRespBody(resp, respBodyRewrites...); err != nil {
|
||||
if err = rewriteRespBody(ctx, resp, respBodyRewrites...); err != nil {
|
||||
log.Errorf("rewrite body: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ func (h *Sniffer) handleUpgradeResponse(ctx context.Context, rw, cc io.ReadWrite
|
||||
return xnet.Pipe(ctx, rw, cc)
|
||||
}
|
||||
|
||||
func rewriteRespBody(resp *http.Response, rewrites ...chain.HTTPBodyRewriteSettings) error {
|
||||
func rewriteRespBody(ctx context.Context, resp *http.Response, rewrites ...chain.HTTPBodyRewriteSettings) error {
|
||||
if resp == nil || len(rewrites) == 0 || resp.ContentLength <= 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -65,7 +65,12 @@ func rewriteRespBody(resp *http.Response, rewrites ...chain.HTTPBodyRewriteSetti
|
||||
continue
|
||||
}
|
||||
|
||||
if rewrite.Pattern != nil {
|
||||
if rewrite.Rewriter != nil {
|
||||
body, err = rewrite.Rewriter.Rewrite(ctx, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if rewrite.Pattern != nil {
|
||||
body = rewrite.Pattern.ReplaceAll(body, rewrite.Replacement)
|
||||
}
|
||||
}
|
||||
@@ -90,7 +95,7 @@ func drainBody(b io.ReadCloser) (body []byte, err error) {
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func rewriteReqBody(req *http.Request, rewrites ...chain.HTTPBodyRewriteSettings) error {
|
||||
func rewriteReqBody(ctx context.Context, req *http.Request, rewrites ...chain.HTTPBodyRewriteSettings) error {
|
||||
if req == nil || len(rewrites) == 0 || req.Body == nil || req.ContentLength <= 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -114,7 +119,12 @@ func rewriteReqBody(req *http.Request, rewrites ...chain.HTTPBodyRewriteSettings
|
||||
continue
|
||||
}
|
||||
|
||||
if rewrite.Pattern != nil {
|
||||
if rewrite.Rewriter != nil {
|
||||
body, err = rewrite.Rewriter.Rewrite(ctx, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if rewrite.Pattern != nil {
|
||||
body = rewrite.Pattern.ReplaceAll(body, rewrite.Replacement)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ func TestRewriteReqBody(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.req != nil {
|
||||
_ = rewriteReqBody(tt.req, tt.rewrites...)
|
||||
_ = rewriteReqBody(context.Background(), tt.req, tt.rewrites...)
|
||||
if tt.req.Body != nil {
|
||||
body, _ := io.ReadAll(tt.req.Body)
|
||||
tt.req.Body.Close()
|
||||
@@ -178,7 +178,7 @@ func TestRewriteReqBody(t *testing.T) {
|
||||
t.Errorf("ContentLength = %d, want %d", tt.req.ContentLength, tt.wantCL)
|
||||
}
|
||||
} else {
|
||||
_ = rewriteReqBody(nil) // should not panic
|
||||
_ = rewriteReqBody(context.Background(), nil) // should not panic
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -344,8 +344,8 @@ func TestDrainBody(t *testing.T) {
|
||||
|
||||
func TestRewriteRespBody(t *testing.T) {
|
||||
t.Run("nil response", func(t *testing.T) {
|
||||
if err := rewriteRespBody(nil); err != nil {
|
||||
t.Errorf("rewriteRespBody(nil) = %v, want nil", err)
|
||||
if err := rewriteRespBody(context.Background(), nil); err != nil {
|
||||
t.Errorf("rewriteRespBody(context.Background(), nil) = %v, want nil", err)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -354,7 +354,7 @@ func TestRewriteRespBody(t *testing.T) {
|
||||
ContentLength: 100,
|
||||
Body: io.NopCloser(strings.NewReader("original")),
|
||||
}
|
||||
_ = rewriteRespBody(resp)
|
||||
_ = rewriteRespBody(context.Background(), resp)
|
||||
// Body unchanged
|
||||
got, _ := io.ReadAll(resp.Body)
|
||||
if string(got) != "original" {
|
||||
@@ -367,7 +367,7 @@ func TestRewriteRespBody(t *testing.T) {
|
||||
ContentLength: 0,
|
||||
Body: io.NopCloser(strings.NewReader("original")),
|
||||
}
|
||||
_ = rewriteRespBody(resp, chain.HTTPBodyRewriteSettings{
|
||||
_ = rewriteRespBody(context.Background(), resp, chain.HTTPBodyRewriteSettings{
|
||||
Pattern: regexp.MustCompile("."),
|
||||
Type: "*",
|
||||
Replacement: []byte("replaced"),
|
||||
@@ -385,7 +385,7 @@ func TestRewriteRespBody(t *testing.T) {
|
||||
ContentLength: 100,
|
||||
Body: io.NopCloser(strings.NewReader("original")),
|
||||
}
|
||||
_ = rewriteRespBody(resp, chain.HTTPBodyRewriteSettings{
|
||||
_ = rewriteRespBody(context.Background(), resp, chain.HTTPBodyRewriteSettings{
|
||||
Type: "*",
|
||||
Replacement: []byte("replaced"),
|
||||
})
|
||||
@@ -413,7 +413,7 @@ func TestRewriteRespBodyContentTypeFilter(t *testing.T) {
|
||||
ContentLength: 100,
|
||||
Body: io.NopCloser(strings.NewReader("original")),
|
||||
}
|
||||
_ = rewriteRespBody(resp, makeRewrite("text/html", "replaced"))
|
||||
_ = rewriteRespBody(context.Background(), resp, makeRewrite("text/html", "replaced"))
|
||||
got, _ := io.ReadAll(resp.Body)
|
||||
if string(got) != "replaced" {
|
||||
t.Errorf("body = %q, want %q", string(got), "replaced")
|
||||
@@ -426,7 +426,7 @@ func TestRewriteRespBodyContentTypeFilter(t *testing.T) {
|
||||
ContentLength: 100,
|
||||
Body: io.NopCloser(strings.NewReader("hello")),
|
||||
}
|
||||
_ = rewriteRespBody(resp, makeRewrite("*", "world"))
|
||||
_ = rewriteRespBody(context.Background(), resp, makeRewrite("*", "world"))
|
||||
got, _ := io.ReadAll(resp.Body)
|
||||
if string(got) != "world" {
|
||||
t.Errorf("body = %q, want %q", string(got), "world")
|
||||
@@ -439,7 +439,7 @@ func TestRewriteRespBodyContentTypeFilter(t *testing.T) {
|
||||
ContentLength: 100,
|
||||
Body: io.NopCloser(strings.NewReader("original")),
|
||||
}
|
||||
_ = rewriteRespBody(resp, makeRewrite("text/html", "replaced"))
|
||||
_ = rewriteRespBody(context.Background(), resp, makeRewrite("text/html", "replaced"))
|
||||
got, _ := io.ReadAll(resp.Body)
|
||||
if string(got) != "original" {
|
||||
t.Errorf("body = %q, want %q (unchanged)", string(got), "original")
|
||||
@@ -454,7 +454,7 @@ func TestRewriteRespBodyContentTypeFilter(t *testing.T) {
|
||||
}
|
||||
// Empty type defaults to "text/html", and response has no Content-Type -> "" containing "text/html"? No.
|
||||
// strings.Contains("text/html", "") is always true, so rewrite should happen.
|
||||
_ = rewriteRespBody(resp, makeRewrite("", "replaced"))
|
||||
_ = rewriteRespBody(context.Background(), resp, makeRewrite("", "replaced"))
|
||||
got, _ := io.ReadAll(resp.Body)
|
||||
if string(got) != "replaced" {
|
||||
t.Errorf("body = %q, want %q (should be rewritten)", string(got), "replaced")
|
||||
@@ -1146,7 +1146,7 @@ func TestRewriteRespBody_PatternReplacement(t *testing.T) {
|
||||
ContentLength: 100,
|
||||
Body: io.NopCloser(strings.NewReader("<title>Old Title</title>")),
|
||||
}
|
||||
err := rewriteRespBody(resp, chain.HTTPBodyRewriteSettings{
|
||||
err := rewriteRespBody(context.Background(), resp, chain.HTTPBodyRewriteSettings{
|
||||
Pattern: regexp.MustCompile("Old"),
|
||||
Type: "text/html",
|
||||
Replacement: []byte("New"),
|
||||
@@ -1169,7 +1169,7 @@ func TestRewriteRespBody_MultipleRewrites(t *testing.T) {
|
||||
ContentLength: 100,
|
||||
Body: io.NopCloser(strings.NewReader("hello")),
|
||||
}
|
||||
err := rewriteRespBody(resp,
|
||||
err := rewriteRespBody(context.Background(), resp,
|
||||
chain.HTTPBodyRewriteSettings{
|
||||
Pattern: regexp.MustCompile("h.*o"),
|
||||
Type: "text/html",
|
||||
@@ -1196,7 +1196,7 @@ func TestRewriteRespBody_NilPattern(t *testing.T) {
|
||||
ContentLength: 100,
|
||||
Body: io.NopCloser(strings.NewReader("original")),
|
||||
}
|
||||
err := rewriteRespBody(resp, chain.HTTPBodyRewriteSettings{
|
||||
err := rewriteRespBody(context.Background(), resp, chain.HTTPBodyRewriteSettings{
|
||||
Pattern: nil,
|
||||
Type: "*",
|
||||
Replacement: []byte("replaced"),
|
||||
@@ -1215,7 +1215,7 @@ func TestRewriteRespBody_EmptyContentTypeUsesDefault(t *testing.T) {
|
||||
ContentLength: 100,
|
||||
Body: io.NopCloser(strings.NewReader("original")),
|
||||
}
|
||||
err := rewriteRespBody(resp, chain.HTTPBodyRewriteSettings{
|
||||
err := rewriteRespBody(context.Background(), resp, chain.HTTPBodyRewriteSettings{
|
||||
Pattern: regexp.MustCompile(".*"),
|
||||
Type: "",
|
||||
Replacement: []byte("rewritten"),
|
||||
@@ -1235,7 +1235,7 @@ func TestRewriteRespBody_NegativeContentLength(t *testing.T) {
|
||||
ContentLength: -1,
|
||||
Body: io.NopCloser(strings.NewReader("original")),
|
||||
}
|
||||
err := rewriteRespBody(resp, chain.HTTPBodyRewriteSettings{
|
||||
err := rewriteRespBody(context.Background(), resp, chain.HTTPBodyRewriteSettings{
|
||||
Pattern: regexp.MustCompile(".*"),
|
||||
Type: "*",
|
||||
Replacement: []byte("replaced"),
|
||||
|
||||
Reference in New Issue
Block a user