Repository navigation
Expand file tree
/
Copy pathutils.go
More file actions
211 lines (193 loc) · 6.53 KB
/
Copy pathutils.go
File metadata and controls
211 lines (193 loc) · 6.53 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
package http
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"slices"
"strings"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/httpbody"
"github.com/projectdiscovery/rawhttp"
"github.com/projectdiscovery/retryablehttp-go"
convUtil "github.com/projectdiscovery/utils/conversion"
"github.com/projectdiscovery/utils/errkit"
)
// maxCurlBodySize is the largest request body included in a curl command.
// Larger bodies are left out so the command stays readable.
const maxCurlBodySize = 4 * 1024
// requestDump is a dumped request whose body may be cut short.
type requestDump struct {
// data holds the request line, the headers and at most maxBody bytes
// of the body, followed by a truncation marker when cut short.
data []byte
// truncated is the number of body bytes left out of data.
truncated int
}
// dump creates a dump of the http request in form of a byte slice. Only the
// first maxBody bytes of the body are kept; maxBody <= 0 keeps all of it.
func dump(req *generatedRequest, reqURL string, maxBody int) (requestDump, error) {
if req.request != nil {
dumped, err := dumpRequest(req.request, maxBody)
if err != nil {
return requestDump{}, errkit.Wrapf(err, "could not dump request: %v", req.request.String())
}
return dumped, nil
}
data := req.rawRequest.Data
truncated := 0
if maxBody > 0 && len(data) > maxBody {
truncated = len(data) - maxBody
data = data[:maxBody]
}
rawHttpOptions := &rawhttp.Options{CustomHeaders: req.rawRequest.UnsafeHeaders, CustomRawBytes: req.rawRequest.UnsafeRawBytes}
bin, err := rawhttp.DumpRequestRaw(req.rawRequest.Method, reqURL, req.rawRequest.Path, generators.ExpandMapValues(req.rawRequest.Headers), io.NopCloser(strings.NewReader(data)), rawHttpOptions)
if err != nil {
return requestDump{}, errkit.Wrapf(err, "could not dump request: %v", reqURL)
}
// CustomRawBytes are dumped as given, without the body
if len(req.rawRequest.UnsafeRawBytes) > 0 {
truncated = 0
}
return requestDump{data: appendTruncationMarker(bin, truncated), truncated: truncated}, nil
}
// dumpRequest writes req the way http.Transport sends it, as
// httputil.DumpRequestOut does, but streams the body into a capped buffer
// instead of copying all of it.
func dumpRequest(req *retryablehttp.Request, maxBody int) (requestDump, error) {
body, err := httpbody.View(req)
if err != nil {
return requestDump{}, err
}
// Use a clone to avoid a race condition with the http transport.
clone := req.Request.Clone(req.Context())
if len(body) == 0 {
clone.Body = nil
clone.ContentLength = 0
delete(clone.Header, "Content-length")
} else {
clone.Body = httpbody.New(body)
clone.ContentLength = int64(len(body))
}
// http.Transport writes https requests as http over its connection.
if clone.URL != nil && clone.URL.Scheme == "https" {
clone.URL.Scheme = "http"
}
capture := &cappedWriter{headerEnd: -1, maxBody: maxBody}
if maxBody <= 0 {
capture.buf = make([]byte, 0, len(body)+dumpHeaderRoom)
}
if err := clone.Write(capture); err != nil {
return requestDump{}, err
}
data := capture.buf
// http.Transport asks for gzip unless the caller set an encoding.
if clone.Header.Get("Accept-Encoding") == "" && clone.Header.Get("Range") == "" && clone.Method != http.MethodHead && capture.headerEnd >= 2 {
end := capture.headerEnd - 2
data = slices.Insert(data, end, acceptEncodingGzip...)
}
return requestDump{data: appendTruncationMarker(data, capture.dropped), truncated: capture.dropped}, nil
}
func appendTruncationMarker(data []byte, truncated int) []byte {
if truncated <= 0 {
return data
}
return fmt.Appendf(data, "\n...[truncated %d bytes]", truncated)
}
// capDumpBody keeps at most maxBody bytes of the body in an already built
// request dump. maxBody <= 0 keeps the dump unchanged. Saved output and debug
// use it. Matchers keep the full dump.
func capDumpBody(dump []byte, maxBody int) []byte {
if maxBody <= 0 {
return dump
}
idx := bytes.Index(dump, headerTerminator)
if idx < 0 {
return dump
}
headerEnd := idx + len(headerTerminator)
bodyLen := len(dump) - headerEnd
if bodyLen <= maxBody {
return dump
}
capped := append([]byte(nil), dump[:headerEnd+maxBody]...)
return appendTruncationMarker(capped, bodyLen-maxBody)
}
var headerTerminator = []byte("\r\n\r\n")
var acceptEncodingGzip = []byte("Accept-Encoding: gzip\r\n")
const dumpHeaderRoom = 8 * 1024
// cappedWriter keeps the request line and headers in full and at most
// maxBody bytes of the body that follows, counting the rest.
type cappedWriter struct {
buf []byte
headerEnd int
maxBody int
dropped int
}
func (w *cappedWriter) Write(p []byte) (int, error) {
n := len(p)
if w.headerEnd < 0 {
from := max(len(w.buf)-len(headerTerminator)+1, 0)
w.buf = append(w.buf, p...)
i := bytes.Index(w.buf[from:], headerTerminator)
if i < 0 {
return n, nil
}
w.headerEnd = from + i + len(headerTerminator)
// re-apply the cap to body bytes that came with the headers
p = bytes.Clone(w.buf[w.headerEnd:])
w.buf = w.buf[:w.headerEnd]
}
keep := len(p)
if w.maxBody > 0 {
keep = min(keep, max(w.maxBody-(len(w.buf)-w.headerEnd), 0))
}
w.buf = append(w.buf, p[:keep]...)
w.dropped += len(p) - keep
return n, nil
}
// getHTTPProjectCacheScope builds the project file key for a request. A
// truncated dump does not identify the body, so its hash is added.
func getHTTPProjectCacheScope(requestDump requestDump, body []byte, scheme, host string) []byte {
scheme = strings.ToLower(strings.TrimSpace(scheme))
host = strings.ToLower(strings.TrimSpace(host))
var scoped bytes.Buffer
if scheme != "" && host != "" {
_, _ = scoped.WriteString(scheme)
_, _ = scoped.WriteString("://")
_, _ = scoped.WriteString(host)
_, _ = scoped.WriteString("\n")
}
_, _ = scoped.Write(requestDump.data)
if requestDump.truncated > 0 {
sum := sha256.Sum256(body)
_, _ = scoped.WriteString("\nbody-sha256:")
_, _ = scoped.WriteString(hex.EncodeToString(sum[:]))
}
return scoped.Bytes()
}
func (g *generatedRequest) bodyLen() int64 {
if g.rawRequest != nil {
return int64(len(g.rawRequest.Data))
}
if g.request == nil || g.request.Body == nil {
return 0
}
if body, ok := g.request.Body.(*httpbody.Body); ok {
return int64(body.Len())
}
return g.request.ContentLength
}
// body returns the request body without copying it.
func (g *generatedRequest) body() []byte {
if g.request != nil {
body, _ := httpbody.View(g.request)
return body
}
if g.rawRequest != nil {
return convUtil.Bytes(g.rawRequest.Data)
}
return nil
}