Skip to content
Open
72 changes: 67 additions & 5 deletions plugins/wasm-go/extensions/transformer/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ package main

import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"mime"
"mime/multipart"
"net/textproto"
"net/url"
"sort"
"strconv"
Expand Down Expand Up @@ -136,14 +139,27 @@ func parseBody(contentType string, body []byte) (interface{}, error) {
}
formName := p.FormName()
fileName := p.FileName()
if formName == "" || fileName != "" {
contentType := p.Header.Get("Content-Type") // 获取文件的 MIME 类型

if formName == "" {
continue
}
formValue, err := io.ReadAll(p)
if err != nil {
return nil, err
}
ret[formName] = append(ret[formName], string(formValue))

if fileName == "" {
ret[formName] = append(ret[formName], string(formValue))
} else {
// 文件字段:记录文件名和 base64 内容
ret[formName+".filename"] = append(ret[formName+".filename"], fileName)
ret[formName+".content-type"] = append(ret[formName+".content-type"], contentType) // 保存 MIME 类型
encoded := base64.StdEncoding.EncodeToString(formValue)
ret[formName+".content"] = append(ret[formName+".content"], encoded)
ret[formName] = append(ret[formName], "")
}

}
return ret, nil

Expand Down Expand Up @@ -188,10 +204,56 @@ func constructBody(contentType string, body interface{}) ([]byte, error) {
if err = w.SetBoundary(params["boundary"]); err != nil {
return nil, err
}

// 先处理文件字段(因为需要 .filename 和 .content)
processed := make(map[string]bool) // 避免重复处理

for k, vs := range bd {
for _, v := range vs {
if err = w.WriteField(k, v); err != nil {
return nil, err
if strings.HasSuffix(k, ".filename") || strings.HasSuffix(k, ".content") || strings.HasSuffix(k, ".content-type") {
continue
}

// 判断是否是文件字段
filenames, hasFilename := bd[k+".filename"]
contents, hasContent := bd[k+".content"]

if hasFilename && hasContent {
// 是文件字段
processed[k] = true
for i := 0; i < len(filenames) && i < len(contents); i++ {
filename := filenames[i]
content64 := contents[i]
// 获取对应的 MIME 类型
var contentType string
if contentTypes, exists := bd[k+".content-type"]; exists && i < len(contentTypes) {
contentType = contentTypes[i]
} else {
contentType = "application/octet-stream" // 默认值
}
content, err := base64.StdEncoding.DecodeString(content64)
if err != nil {
return nil, errors.Wrap(err, "decode base64 content failed")
}

// 创建带 Content-Type 的表单文件
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, k, filename))
h.Set("Content-Type", contentType)

fw, err := w.CreatePart(h)
if err != nil {
return nil, err
}
_, err = fw.Write(content)
if err != nil {
return nil, err
}
}
} else {
for _, v := range vs {
if err = w.WriteField(k, v); err != nil {
return nil, err
}
}
}
}
Expand Down
60 changes: 60 additions & 0 deletions test/e2e/conformance/tests/go-wasm-transformer.go
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,66 @@ var WasmPluginsTransformer = suite.ConformanceTest{
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 19: request multipart/form-data body with file",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo19.com",
Path: "/post",
Method: "POST",
ContentType: "multipart/form-data; boundary=--------------------------180978275079165582161528",
Body: []byte(`
----------------------------180978275079165582161528
Content-Disposition: form-data; name="file1"; filename="test.txt"
Content-Type: text/plain

这是一个txt文件
----------------------------180978275079165582161528
Content-Disposition: form-data; name="file2"; filename="test.txt"
Content-Type: text/plain

这是一个txt文件
----------------------------180978275079165582161528--

`),
},
ExpectedRequest: &http.ExpectedRequest{
Request: http.Request{
Host: "foo19.com",
Path: "/post",
Method: "POST",
ContentType: "multipart/form-data; boundary=--------------------------180978275079165582161528",
Body: []byte(`
----------------------------180978275079165582161528
Content-Disposition: form-data; name="file2"; filename="test.txt"
Content-Type: text/plain

这是一个txt文件
----------------------------180978275079165582161528
Content-Disposition: form-data; name="file1"; filename="test.txt"
Content-Type: text/plain

这是一个txt文件
----------------------------180978275079165582161528
Content-Disposition: form-data; name="x-process"

wasm
----------------------------180978275079165582161528--

`),
},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
},
},
},
}
t.Run("WasmPlugin transformer", func(t *testing.T) {
for _, testcase := range testcases {
Expand Down
31 changes: 31 additions & 0 deletions test/e2e/conformance/tests/go-wasm-transformer.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,26 @@ spec:
port:
number: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
name: wasmplugin-transform-request-body-multipart-formdata-file
namespace: higress-conformance-infra
spec:
ingressClassName: higress
rules:
- host: "foo19.com"
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: infra-backend-v1
port:
number: 8080
---
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
Expand Down Expand Up @@ -891,4 +911,15 @@ spec:
strategy: SPLIT_AND_RETAIN_FIRST
- key: X-split-dedupe-last
strategy: SPLIT_AND_RETAIN_LAST

- ingress:
- wasmplugin-transform-request-body-multipart-formdata-file
configDisable: false
config:
reqRules:
- operate: add
body:
- key: X-process
value: wasm

url: file:///opt/plugins/wasm-go/extensions/transformer/plugin.wasm
Loading