Skip to content

Commit 24b0c84

Browse files
authored
Merge pull request #392 from rosahaj/master
Add client.SetMultipartBoundaryFunc and port Blink/WebKit/Firefox implementations
2 parents 5f9bf49 + 5e4950e commit 24b0c84

File tree

6 files changed

+138
-9
lines changed

6 files changed

+138
-9
lines changed

client.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ type Client struct {
5959
jsonUnmarshal func(data []byte, v interface{}) error
6060
xmlMarshal func(v interface{}) ([]byte, error)
6161
xmlUnmarshal func(data []byte, v interface{}) error
62+
multipartBoundaryFunc func() string
6263
outputDirectory string
6364
scheme string
6465
log Logger
@@ -239,6 +240,17 @@ func (c *Client) SetCommonFormData(data map[string]string) *Client {
239240
return c
240241
}
241242

243+
// SetMultipartBoundaryFunc overrides the default function used to generate
244+
// boundary delimiters for "multipart/form-data" requests with a customized one,
245+
// which returns a boundary delimiter (without the two leading hyphens).
246+
//
247+
// Boundary delimiter may only contain certain ASCII characters, and must be
248+
// non-empty and at most 70 bytes long (see RFC 2046, Section 5.1.1).
249+
func (c *Client) SetMultipartBoundaryFunc(fn func() string) *Client {
250+
c.multipartBoundaryFunc = fn
251+
return c
252+
}
253+
242254
// SetBaseURL set the default base URL, will be used if request URL is
243255
// a relative URL.
244256
func (c *Client) SetBaseURL(u string) *Client {

client_impersonate.go

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,56 @@
11
package req
22

33
import (
4+
"crypto/rand"
5+
"encoding/binary"
6+
"math/big"
7+
"strconv"
8+
"strings"
9+
410
"github.com/imroc/req/v3/http2"
511
utls "github.com/refraction-networking/utls"
612
)
713

14+
// Identical for both Blink-based browsers (Chrome, Chromium, etc.) and WebKit-based browsers (Safari, etc.)
15+
// Blink implementation: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/network/form_data_encoder.cc;drc=1d694679493c7b2f7b9df00e967b4f8699321093;l=130
16+
// WebKit implementation: https://github.com/WebKit/WebKit/blob/47eea119fe9462721e5cc75527a4280c6d5f5214/Source/WebCore/platform/network/FormDataBuilder.cpp#L120
17+
func webkitMultipartBoundaryFunc() string {
18+
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789AB"
19+
20+
sb := strings.Builder{}
21+
sb.WriteString("----WebKitFormBoundary")
22+
23+
for i := 0; i < 16; i++ {
24+
index, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters)-1)))
25+
if err != nil {
26+
panic(err)
27+
}
28+
29+
sb.WriteByte(letters[index.Int64()])
30+
}
31+
32+
return sb.String()
33+
}
34+
35+
// Firefox implementation: https://searchfox.org/mozilla-central/source/dom/html/HTMLFormSubmission.cpp#355
36+
func firefoxMultipartBoundaryFunc() string {
37+
sb := strings.Builder{}
38+
sb.WriteString("-------------------------")
39+
40+
for i := 0; i < 3; i++ {
41+
var b [8]byte
42+
if _, err := rand.Read(b[:]); err != nil {
43+
panic(err)
44+
}
45+
u32 := binary.LittleEndian.Uint32(b[:])
46+
s := strconv.FormatUint(uint64(u32), 10)
47+
48+
sb.WriteString(s)
49+
}
50+
51+
return sb.String()
52+
}
53+
854
var (
955
chromeHttp2Settings = []http2.Setting{
1056
{
@@ -71,6 +117,7 @@ var (
71117
"sec-fetch-dest": "document",
72118
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7,it;q=0.6",
73119
}
120+
74121
chromeHeaderPriority = http2.PriorityParam{
75122
StreamDep: 0,
76123
Exclusive: true,
@@ -87,7 +134,8 @@ func (c *Client) ImpersonateChrome() *Client {
87134
SetCommonPseudoHeaderOder(chromePseudoHeaderOrder...).
88135
SetCommonHeaderOrder(chromeHeaderOrder...).
89136
SetCommonHeaders(chromeHeaders).
90-
SetHTTP2HeaderPriority(chromeHeaderPriority)
137+
SetHTTP2HeaderPriority(chromeHeaderPriority).
138+
SetMultipartBoundaryFunc(webkitMultipartBoundaryFunc)
91139
return c
92140
}
93141

@@ -106,6 +154,7 @@ var (
106154
Val: 16384,
107155
},
108156
}
157+
109158
firefoxPriorityFrames = []http2.PriorityFrame{
110159
{
111160
StreamID: 3,
@@ -156,12 +205,14 @@ var (
156205
},
157206
},
158207
}
208+
159209
firefoxPseudoHeaderOrder = []string{
160210
":method",
161211
":path",
162212
":authority",
163213
":scheme",
164214
}
215+
165216
firefoxHeaderOrder = []string{
166217
"user-agent",
167218
"accept",
@@ -176,6 +227,7 @@ var (
176227
"sec-fetch-user",
177228
"te",
178229
}
230+
179231
firefoxHeaders = map[string]string{
180232
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0",
181233
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
@@ -187,6 +239,7 @@ var (
187239
"sec-fetch-user": "?1",
188240
//"te": "trailers",
189241
}
242+
190243
firefoxHeaderPriority = http2.PriorityParam{
191244
StreamDep: 13,
192245
Exclusive: false,
@@ -204,7 +257,8 @@ func (c *Client) ImpersonateFirefox() *Client {
204257
SetCommonPseudoHeaderOder(firefoxPseudoHeaderOrder...).
205258
SetCommonHeaderOrder(firefoxHeaderOrder...).
206259
SetCommonHeaders(firefoxHeaders).
207-
SetHTTP2HeaderPriority(firefoxHeaderPriority)
260+
SetHTTP2HeaderPriority(firefoxHeaderPriority).
261+
SetMultipartBoundaryFunc(firefoxMultipartBoundaryFunc)
208262
return c
209263
}
210264

@@ -264,6 +318,7 @@ func (c *Client) ImpersonateSafari() *Client {
264318
SetCommonPseudoHeaderOder(safariPseudoHeaderOrder...).
265319
SetCommonHeaderOrder(safariHeaderOrder...).
266320
SetCommonHeaders(safariHeaders).
267-
SetHTTP2HeaderPriority(safariHeaderPriority)
321+
SetHTTP2HeaderPriority(safariHeaderPriority).
322+
SetMultipartBoundaryFunc(webkitMultipartBoundaryFunc)
268323
return c
269324
}

client_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import (
55
"context"
66
"crypto/tls"
77
"errors"
8+
"fmt"
89
"io"
910
"net"
1011
"net/http"
1112
"net/http/cookiejar"
1213
"net/url"
1314
"os"
15+
"regexp"
1416
"strings"
1517
"testing"
1618
"time"
@@ -468,6 +470,35 @@ func TestSetCommonFormData(t *testing.T) {
468470
tests.AssertEqual(t, "test", form.Get("test"))
469471
}
470472

473+
func TestSetMultipartBoundaryFunc(t *testing.T) {
474+
delimiter := "test-delimiter"
475+
expectedContentType := fmt.Sprintf("multipart/form-data; boundary=%s", delimiter)
476+
resp, err := tc().
477+
SetMultipartBoundaryFunc(func() string {
478+
return delimiter
479+
}).R().
480+
EnableForceMultipart().
481+
SetFormData(
482+
map[string]string{
483+
"test": "test",
484+
}).
485+
Post("/content-type")
486+
assertSuccess(t, resp, err)
487+
tests.AssertEqual(t, expectedContentType, resp.String())
488+
}
489+
490+
func TestFirefoxMultipartBoundaryFunc(t *testing.T) {
491+
r := regexp.MustCompile(`^-------------------------\d{1,10}\d{1,10}\d{1,10}$`)
492+
b := firefoxMultipartBoundaryFunc()
493+
tests.AssertEqual(t, true, r.MatchString(b))
494+
}
495+
496+
func TestWebkitMultipartBoundaryFunc(t *testing.T) {
497+
r := regexp.MustCompile(`^----WebKitFormBoundary[0-9a-zA-Z]{16}$`)
498+
b := webkitMultipartBoundaryFunc()
499+
tests.AssertEqual(t, true, r.MatchString(b))
500+
}
501+
471502
func TestClientClone(t *testing.T) {
472503
c1 := tc().DevMode().
473504
SetCommonHeader("test", "test").

client_wrapper.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ package req
33
import (
44
"context"
55
"crypto/tls"
6-
"github.com/imroc/req/v3/http2"
7-
utls "github.com/refraction-networking/utls"
86
"io"
97
"net"
108
"net/http"
119
"net/url"
1210
"time"
11+
12+
"github.com/imroc/req/v3/http2"
13+
utls "github.com/refraction-networking/utls"
1314
)
1415

1516
// WrapRoundTrip is a global wrapper methods which delegated
@@ -56,6 +57,12 @@ func SetCommonFormData(data map[string]string) *Client {
5657
return defaultClient.SetCommonFormData(data)
5758
}
5859

60+
// SetMultipartBoundaryFunc is a global wrapper methods which delegated
61+
// to the default client's Client.SetMultipartBoundaryFunc.
62+
func SetMultipartBoundaryFunc(fn func() string) *Client {
63+
return defaultClient.SetMultipartBoundaryFunc(fn)
64+
}
65+
5966
// SetBaseURL is a global wrapper methods which delegated
6067
// to the default client's Client.SetBaseURL.
6168
func SetBaseURL(u string) *Client {
@@ -482,6 +489,18 @@ func ImpersonateChrome() *Client {
482489
return defaultClient.ImpersonateChrome()
483490
}
484491

492+
// ImpersonateChrome is a global wrapper methods which delegated
493+
// to the default client's Client.ImpersonateChrome.
494+
func ImpersonateFirefox() *Client {
495+
return defaultClient.ImpersonateFirefox()
496+
}
497+
498+
// ImpersonateChrome is a global wrapper methods which delegated
499+
// to the default client's Client.ImpersonateChrome.
500+
func ImpersonateSafari() *Client {
501+
return defaultClient.ImpersonateFirefox()
502+
}
503+
485504
// SetCommonContentType is a global wrapper methods which delegated
486505
// to the default client's Client.SetCommonContentType.
487506
func SetCommonContentType(ct string) *Client {

middleware.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,21 @@ func writeMultiPart(r *Request, w *multipart.Writer) {
148148
}
149149
}
150150

151-
func handleMultiPart(r *Request) (err error) {
151+
func handleMultiPart(c *Client, r *Request) (err error) {
152+
var b string
153+
if c.multipartBoundaryFunc != nil {
154+
b = c.multipartBoundaryFunc()
155+
}
156+
152157
if r.forceChunkedEncoding {
153158
pr, pw := io.Pipe()
154159
r.GetBody = func() (io.ReadCloser, error) {
155160
return pr, nil
156161
}
157162
w := multipart.NewWriter(pw)
163+
if len(b) > 0 {
164+
w.SetBoundary(b)
165+
}
158166
r.SetContentType(w.FormDataContentType())
159167
go func() {
160168
writeMultiPart(r, w)
@@ -163,6 +171,9 @@ func handleMultiPart(r *Request) (err error) {
163171
} else {
164172
buf := new(bytes.Buffer)
165173
w := multipart.NewWriter(buf)
174+
if len(b) > 0 {
175+
w.SetBoundary(b)
176+
}
166177
writeMultiPart(r, w)
167178
r.GetBody = func() (io.ReadCloser, error) {
168179
return io.NopCloser(bytes.NewReader(buf.Bytes())), nil
@@ -242,7 +253,7 @@ func parseRequestBody(c *Client, r *Request) (err error) {
242253
}
243254
// handle multipart
244255
if r.isMultiPart {
245-
return handleMultiPart(r)
256+
return handleMultiPart(c, r)
246257
}
247258

248259
// handle form data

response.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
package req
22

33
import (
4-
"github.com/imroc/req/v3/internal/header"
5-
"github.com/imroc/req/v3/internal/util"
64
"io"
75
"net/http"
86
"strings"
97
"time"
8+
9+
"github.com/imroc/req/v3/internal/header"
10+
"github.com/imroc/req/v3/internal/util"
1011
)
1112

1213
// Response is the http response.

0 commit comments

Comments
 (0)