-
-
Notifications
You must be signed in to change notification settings - Fork 2k
Expand file tree
/
Copy pathhooks.go
More file actions
357 lines (303 loc) · 9.1 KB
/
Copy pathhooks.go
File metadata and controls
357 lines (303 loc) · 9.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
package client
import (
"crypto/rand"
"errors"
"fmt"
"io"
"mime/multipart"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
utils "github.com/gofiber/utils/v2"
"github.com/valyala/fasthttp"
)
var protocolCheck = regexp.MustCompile(`^https?://.*$`)
var fileBufPool = sync.Pool{
New: func() any {
b := make([]byte, 1<<20) // 1MB buffer
return &b
},
}
const (
headerAccept = "Accept"
applicationJSON = "application/json"
applicationCBOR = "application/cbor"
applicationXML = "application/xml"
applicationForm = "application/x-www-form-urlencoded"
multipartFormData = "multipart/form-data"
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
)
// unsafeRandString returns a random string of length n.
// An error is returned if the random source fails.
func unsafeRandString(n int) (string, error) {
inputLength := byte(len(letterBytes))
// Compute the largest multiple of inputLength ≤ 256 to avoid modulo bias.
// Any byte ≥ max will be rejected and re‑read.
maxLength := byte(256 - (256 % int(inputLength)))
out := make([]byte, n)
buf := make([]byte, n)
// Read n raw bytes in one shot
if _, err := rand.Read(buf); err != nil {
return "", fmt.Errorf("rand.Read failed: %w", err)
}
for i, b := range buf {
// Reject values ≥ maxLength
for b >= maxLength {
if _, err := rand.Read(buf[i : i+1]); err != nil {
return "", fmt.Errorf("rand.Read failed: %w", err)
}
b = buf[i]
}
out[i] = letterBytes[b%inputLength]
}
return utils.UnsafeString(out), nil
}
// parserRequestURL sets options for the hostclient and normalizes the URL.
// It merges the baseURL with the request URI if needed and applies query and path parameters.
func parserRequestURL(c *Client, req *Request) error {
splitURL := strings.Split(req.url, "?")
// Ensure splitURL has at least two elements.
splitURL = append(splitURL, "")
// If the URL doesn't start with http/https, prepend the baseURL.
uri := splitURL[0]
if !protocolCheck.MatchString(uri) {
uri = c.baseURL + uri
if !protocolCheck.MatchString(uri) {
return ErrURLFormat
}
}
// Set path parameters from the request and client.
for key, val := range req.path.All() {
uri = strings.ReplaceAll(uri, ":"+key, val)
}
for key, val := range c.path.All() {
uri = strings.ReplaceAll(uri, ":"+key, val)
}
// Set the URI in the raw request.
disablePathNormalizing := c.DisablePathNormalizing() || req.DisablePathNormalizing()
req.RawRequest.SetRequestURI(uri)
req.RawRequest.URI().DisablePathNormalizing = disablePathNormalizing
if disablePathNormalizing {
req.RawRequest.URI().SetPathBytes(req.RawRequest.URI().PathOriginal())
}
// Merge query parameters.
hashSplit := strings.Split(splitURL[1], "#")
hashSplit = append(hashSplit, "")
args := fasthttp.AcquireArgs()
defer fasthttp.ReleaseArgs(args)
args.Parse(hashSplit[0])
for key, value := range c.params.All() {
args.AddBytesKV(key, value)
}
for key, value := range req.params.All() {
args.AddBytesKV(key, value)
}
req.RawRequest.URI().SetQueryStringBytes(utils.CopyBytes(args.QueryString()))
req.RawRequest.URI().SetHash(hashSplit[1])
return nil
}
// parserRequestHeader merges client and request headers, and sets headers automatically based on the request data.
// It also sets the User-Agent and Referer headers, and applies any cookies from the cookie jar.
func parserRequestHeader(c *Client, req *Request) error {
// Set HTTP method.
req.RawRequest.Header.SetMethod(req.Method())
// Merge headers from the client.
for key, value := range c.header.All() {
req.RawRequest.Header.AddBytesKV(key, value)
}
// Merge headers from the request.
for key, value := range req.header.All() {
req.RawRequest.Header.AddBytesKV(key, value)
}
// Set Content-Type and Accept headers based on the request body type.
switch req.bodyType {
case jsonBody:
req.RawRequest.Header.SetContentType(applicationJSON)
req.RawRequest.Header.Set(headerAccept, applicationJSON)
case xmlBody:
req.RawRequest.Header.SetContentType(applicationXML)
case cborBody:
req.RawRequest.Header.SetContentType(applicationCBOR)
case formBody:
req.RawRequest.Header.SetContentType(applicationForm)
case filesBody:
req.RawRequest.Header.SetContentType(multipartFormData)
// If boundary is default, append a random string to it.
if req.boundary == boundary {
randStr, err := unsafeRandString(16)
if err != nil {
return fmt.Errorf("boundary generation: %w", err)
}
req.boundary += randStr
}
req.RawRequest.Header.SetMultipartFormBoundary(req.boundary)
default:
// noBody or rawBody do not require special handling here.
}
// Set User-Agent header.
req.RawRequest.Header.SetUserAgent(defaultUserAgent)
if c.userAgent != "" {
req.RawRequest.Header.SetUserAgent(c.userAgent)
}
if req.userAgent != "" {
req.RawRequest.Header.SetUserAgent(req.userAgent)
}
// Set Referer header.
req.RawRequest.Header.SetReferer(c.referer)
if req.referer != "" {
req.RawRequest.Header.SetReferer(req.referer)
}
// Set cookies from the cookie jar if available.
if c.cookieJar != nil {
c.cookieJar.dumpCookiesToReq(req.RawRequest)
}
// Set cookies from the client.
for key, val := range c.cookies.All() {
req.RawRequest.Header.SetCookie(key, val)
}
// Set cookies from the request.
for key, val := range req.cookies.All() {
req.RawRequest.Header.SetCookie(key, val)
}
return nil
}
// parserRequestBody serializes the request body based on its type and sets it into the RawRequest.
func parserRequestBody(c *Client, req *Request) error {
switch req.bodyType {
case jsonBody:
body, err := c.jsonMarshal(req.body)
if err != nil {
return err
}
req.RawRequest.SetBody(body)
case xmlBody:
body, err := c.xmlMarshal(req.body)
if err != nil {
return err
}
req.RawRequest.SetBody(body)
case cborBody:
body, err := c.cborMarshal(req.body)
if err != nil {
return err
}
req.RawRequest.SetBody(body)
case formBody:
req.RawRequest.SetBody(req.formData.QueryString())
case filesBody:
return parserRequestBodyFile(req)
case rawBody:
if body, ok := req.body.([]byte); ok { //nolint:revive // ignore simplicity
req.RawRequest.SetBody(body)
} else {
return ErrBodyType
}
case noBody:
// No body to set.
return nil
}
return nil
}
// parserRequestBodyFile handles the case where the request contains files to be uploaded.
func parserRequestBodyFile(req *Request) error {
mw := multipart.NewWriter(req.RawRequest.BodyWriter())
err := mw.SetBoundary(req.boundary)
if err != nil {
return fmt.Errorf("set boundary error: %w", err)
}
defer func() {
e := mw.Close()
if e != nil {
// Close errors are typically ignored.
return
}
}()
// Add form data.
for key, value := range req.formData.All() {
err = mw.WriteField(utils.UnsafeString(key), utils.UnsafeString(value))
if err != nil {
break
}
}
if err != nil {
return fmt.Errorf("write formdata error: %w", err)
}
// Add files.
fileBuf, ok := fileBufPool.Get().(*[]byte)
if !ok {
return errors.New("failed to retrieve buffer from a sync.Pool")
}
defer fileBufPool.Put(fileBuf)
for i, f := range req.files {
if f.name == "" && f.path == "" {
return ErrFileNoName
}
// Set the file name if not provided.
if f.name == "" && f.path != "" {
f.path = filepath.Clean(f.path)
f.name = filepath.Base(f.path)
}
// Set the field name if not provided.
if f.fieldName == "" {
f.fieldName = "file" + strconv.Itoa(i+1)
}
if err := addFormFile(mw, f, fileBuf); err != nil {
return err
}
}
return nil
}
func addFormFile(mw *multipart.Writer, f *File, fileBuf *[]byte) error {
// If reader is not set, open the file.
if f.reader == nil {
var err error
f.reader, err = os.Open(f.path)
if err != nil {
return fmt.Errorf("open file error: %w", err)
}
}
// Ensure the file reader is always closed after copying.
defer f.reader.Close() //nolint:errcheck // not needed
// Create form file and copy the content.
w, err := mw.CreateFormFile(f.fieldName, f.name)
if err != nil {
return fmt.Errorf("create file error: %w", err)
}
if _, err := io.CopyBuffer(w, f.reader, *fileBuf); err != nil {
return fmt.Errorf("failed to copy file data: %w", err)
}
return nil
}
// parserResponseCookie parses the Set-Cookie headers from the response and stores them.
func parserResponseCookie(c *Client, resp *Response, req *Request) error {
var err error
for key, value := range resp.RawResponse.Header.Cookies() {
cookie := fasthttp.AcquireCookie()
if err = cookie.ParseBytes(value); err != nil {
fasthttp.ReleaseCookie(cookie)
break
}
cookie.SetKeyBytes(key)
resp.cookie = append(resp.cookie, cookie)
}
if err != nil {
return err
}
// Store cookies in the cookie jar if available.
if c.cookieJar != nil {
c.cookieJar.parseCookiesFromResp(req.RawRequest.URI().Host(), req.RawRequest.URI().Path(), resp.RawResponse)
}
return nil
}
// logger is a response hook that logs request and response data if debug mode is enabled.
func logger(c *Client, resp *Response, req *Request) error {
if !c.debug {
return nil
}
c.logger.Debugf("%s\n", req.RawRequest.String())
c.logger.Debugf("%s\n", resp.RawResponse.String())
return nil
}