mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-09 17:43:08 -10:00
212 lines
6.5 KiB
Go
212 lines
6.5 KiB
Go
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
|
|
}
|