mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-03-22 02:04:25 +00:00
284 lines
9.0 KiB
Go
284 lines
9.0 KiB
Go
package surf
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"time"
|
|
|
|
"github.com/enetx/g"
|
|
"github.com/enetx/http"
|
|
"github.com/enetx/surf/header"
|
|
"github.com/enetx/surf/internal/drainbody"
|
|
"github.com/enetx/surf/profiles/chrome"
|
|
"github.com/enetx/surf/profiles/firefox"
|
|
)
|
|
|
|
// Request represents an HTTP request with additional surf-specific functionality.
|
|
// It wraps the standard http.Request and provides enhanced features like middleware support,
|
|
// retry capabilities, remote address tracking, and structured error handling.
|
|
type Request struct {
|
|
request *http.Request // The underlying standard HTTP request
|
|
cli *Client // The associated surf client for this request
|
|
werr *error // Pointer to error encountered during request writing/preparation
|
|
err error // General error associated with the request (validation, setup, etc.)
|
|
remoteAddr net.Addr // Remote server address captured during connection
|
|
body io.ReadCloser // Request body reader (for retry support and body preservation)
|
|
}
|
|
|
|
// GetRequest returns the underlying standard http.Request.
|
|
// Provides access to the wrapped HTTP request for advanced use cases.
|
|
func (req *Request) GetRequest() *http.Request { return req.request }
|
|
|
|
// Do executes the HTTP request and returns a Response wrapped in a Result type.
|
|
// This is the main method that performs the actual HTTP request with full surf functionality:
|
|
// - Applies request middleware (authentication, headers, tracing, etc.)
|
|
// - Preserves request body for potential retries
|
|
// - Implements retry logic with configurable status codes and delays
|
|
// - Measures request timing for performance analysis
|
|
// - Handles request preparation errors and write errors
|
|
func (req *Request) Do() g.Result[*Response] {
|
|
// Return early if request has preparation errors
|
|
if req.err != nil {
|
|
return g.Err[*Response](req.err)
|
|
}
|
|
|
|
// Apply all configured request middleware
|
|
if err := req.cli.applyReqMW(req); err != nil {
|
|
return g.Err[*Response](err)
|
|
}
|
|
|
|
// Preserve request body for retries (except HEAD requests which have no body)
|
|
if req.request.Method != http.MethodHead {
|
|
req.body, req.request.Body, req.err = drainbody.DrainBody(req.request.Body)
|
|
if req.err != nil {
|
|
return g.Err[*Response](req.err)
|
|
}
|
|
}
|
|
|
|
var (
|
|
resp *http.Response
|
|
attempts int
|
|
err error
|
|
)
|
|
|
|
start := time.Now()
|
|
cli := req.cli.cli
|
|
|
|
builder := req.cli.builder
|
|
|
|
// Execute request with retry logic
|
|
retry:
|
|
resp, err = cli.Do(req.request)
|
|
if err != nil {
|
|
return g.Err[*Response](err)
|
|
}
|
|
|
|
// Check if retry is needed based on status code and retry configuration
|
|
if builder != nil && builder.retryMax != 0 && attempts < builder.retryMax && builder.retryCodes.NotEmpty() &&
|
|
builder.retryCodes.Contains(resp.StatusCode) {
|
|
attempts++
|
|
|
|
time.Sleep(builder.retryWait)
|
|
goto retry
|
|
}
|
|
|
|
// Check for write errors that occurred during request preparation
|
|
if req.werr != nil && *req.werr != nil {
|
|
return g.Err[*Response](*req.werr)
|
|
}
|
|
|
|
response := &Response{
|
|
Attempts: attempts,
|
|
Time: time.Since(start),
|
|
Client: req.cli,
|
|
ContentLength: resp.ContentLength,
|
|
Cookies: resp.Cookies(),
|
|
Headers: Headers(resp.Header),
|
|
Proto: g.String(resp.Proto),
|
|
StatusCode: StatusCode(resp.StatusCode),
|
|
URL: resp.Request.URL,
|
|
UserAgent: g.String(req.request.UserAgent()),
|
|
remoteAddr: req.remoteAddr,
|
|
request: req,
|
|
response: resp,
|
|
}
|
|
|
|
if req.request.Method != http.MethodHead {
|
|
response.Body = new(Body)
|
|
response.Body.Reader = resp.Body
|
|
response.Body.cache = builder != nil && builder.cacheBody
|
|
response.Body.contentType = resp.Header.Get(header.CONTENT_TYPE)
|
|
response.Body.limit = -1
|
|
}
|
|
|
|
if err := req.cli.applyRespMW(response); err != nil {
|
|
return g.Err[*Response](err)
|
|
}
|
|
|
|
return g.Ok(response)
|
|
}
|
|
|
|
// WithContext associates a context with the request for cancellation and deadlines.
|
|
// The context can be used to cancel the request, set timeouts, or pass request-scoped values.
|
|
// Returns the request for method chaining. If ctx is nil, the request is unchanged.
|
|
func (req *Request) WithContext(ctx context.Context) *Request {
|
|
if ctx != nil {
|
|
req.request = req.request.WithContext(ctx)
|
|
}
|
|
|
|
return req
|
|
}
|
|
|
|
// AddCookies adds one or more HTTP cookies to the request.
|
|
// Cookies are added to the request headers and will be sent with the HTTP request.
|
|
// Returns the request for method chaining.
|
|
func (req *Request) AddCookies(cookies ...*http.Cookie) *Request {
|
|
for _, cookie := range cookies {
|
|
req.request.AddCookie(cookie)
|
|
}
|
|
|
|
return req
|
|
}
|
|
|
|
// SetHeaders sets HTTP headers for the request, replacing any existing headers with the same name.
|
|
// Supports multiple input formats:
|
|
// - Two arguments: key, value (string or g.String)
|
|
// - Single argument: http.Header, Headers, map types, or g.Map types
|
|
// Maintains header order for fingerprinting purposes when using g.MapOrd.
|
|
// Returns the request for method chaining.
|
|
func (req *Request) SetHeaders(headers ...any) *Request {
|
|
if req.request == nil || headers == nil {
|
|
return req
|
|
}
|
|
|
|
req.applyHeaders(headers, func(h http.Header, k, v string) { h.Set(k, v) })
|
|
|
|
return req
|
|
}
|
|
|
|
// AddHeaders adds HTTP headers to the request, appending to any existing headers with the same name.
|
|
// Unlike SetHeaders, this method preserves existing headers and adds new values.
|
|
// Supports the same input formats as SetHeaders.
|
|
// Returns the request for method chaining.
|
|
func (req *Request) AddHeaders(headers ...any) *Request {
|
|
if req.request == nil || headers == nil {
|
|
return req
|
|
}
|
|
|
|
req.applyHeaders(headers, func(h http.Header, k, v string) { h.Add(k, v) })
|
|
|
|
return req
|
|
}
|
|
|
|
// applyHeaders is a helper function that processes various header input formats and applies them to an HTTP request.
|
|
// It handles type checking, conversion, and delegation to the provided setOrAdd function for actual header manipulation.
|
|
// Supports ordered header maps for fingerprinting and maintains compatibility with multiple map and header types.
|
|
func (req *Request) applyHeaders(
|
|
rawHeaders []any,
|
|
setOrAdd func(h http.Header, key, value string),
|
|
) {
|
|
r := req.request
|
|
if len(rawHeaders) >= 2 {
|
|
var key, value string
|
|
|
|
switch k := rawHeaders[0].(type) {
|
|
case string:
|
|
key = k
|
|
case g.String:
|
|
key = k.Std()
|
|
default:
|
|
panic(fmt.Sprintf("unsupported key type: expected 'string' or 'String', got %T", rawHeaders[0]))
|
|
}
|
|
|
|
switch v := rawHeaders[1].(type) {
|
|
case string:
|
|
value = v
|
|
case g.String:
|
|
value = v.Std()
|
|
default:
|
|
panic(fmt.Sprintf("unsupported value type: expected 'string' or 'String', got %T", rawHeaders[1]))
|
|
}
|
|
|
|
setOrAdd(r.Header, key, value)
|
|
return
|
|
}
|
|
|
|
switch h := rawHeaders[0].(type) {
|
|
case http.Header:
|
|
for key, values := range h {
|
|
for _, value := range values {
|
|
setOrAdd(r.Header, key, value)
|
|
}
|
|
}
|
|
case Headers:
|
|
for key, values := range h {
|
|
for _, value := range values {
|
|
setOrAdd(r.Header, key, value)
|
|
}
|
|
}
|
|
case map[string]string:
|
|
for key, value := range h {
|
|
setOrAdd(r.Header, key, value)
|
|
}
|
|
case map[g.String]g.String:
|
|
for key, value := range h {
|
|
setOrAdd(r.Header, key.Std(), value.Std())
|
|
}
|
|
case g.Map[string, string]:
|
|
for key, value := range h {
|
|
setOrAdd(r.Header, key, value)
|
|
}
|
|
case g.Map[g.String, g.String]:
|
|
for key, value := range h {
|
|
setOrAdd(r.Header, key.Std(), value.Std())
|
|
}
|
|
case g.MapOrd[string, string]:
|
|
updated := updateRequestHeaderOrder(req, h)
|
|
updated.Iter().ForEach(func(key, value string) { setOrAdd(r.Header, key, value) })
|
|
case g.MapOrd[g.String, g.String]:
|
|
updated := updateRequestHeaderOrder(req, h)
|
|
updated.Iter().ForEach(func(key, value g.String) { setOrAdd(r.Header, key.Std(), value.Std()) })
|
|
default:
|
|
panic(fmt.Sprintf("unsupported headers type: expected 'http.Header', 'surf.Headers', 'map[~string]~string', 'Map[~string, ~string]', or 'MapOrd[~string, ~string]', got %T", rawHeaders[0]))
|
|
}
|
|
}
|
|
|
|
// updateRequestHeaderOrder processes ordered headers for HTTP/2 and HTTP/3 fingerprinting.
|
|
// It maintains the specific order of headers which is crucial for browser fingerprinting.
|
|
// Separates regular headers from pseudo-headers (starting with ':') and sets the appropriate
|
|
// header order keys for the transport layer to use. Returns a filtered map containing only
|
|
// non-pseudo headers with non-empty values.
|
|
func updateRequestHeaderOrder[T ~string](r *Request, h g.MapOrd[T, T]) g.MapOrd[T, T] {
|
|
hclone := h.Clone()
|
|
|
|
if r.cli.builder != nil {
|
|
switch r.cli.builder.browser {
|
|
case chromeBrowser:
|
|
chrome.Headers(&hclone, r.request.Method)
|
|
case firefoxBrowser:
|
|
firefox.Headers(&hclone, r.request.Method)
|
|
}
|
|
}
|
|
|
|
headersKeys := g.TransformSlice(hclone.Iter().
|
|
Keys().
|
|
Map(func(s T) T { return T(g.String(s).Lower()) }).
|
|
Collect(), func(t T) string { return string(t) })
|
|
|
|
headers, pheaders := headersKeys.Iter().Partition(func(v string) bool { return v[0] != ':' })
|
|
|
|
if headers.NotEmpty() {
|
|
r.request.Header[http.HeaderOrderKey] = headers
|
|
}
|
|
|
|
if pheaders.NotEmpty() {
|
|
r.request.Header[http.PHeaderOrderKey] = pheaders
|
|
}
|
|
|
|
return hclone.Iter().
|
|
Filter(func(header, data T) bool { return header[0] != ':' && len(data) != 0 }).
|
|
Collect()
|
|
}
|