4f526c2aee
Split ~850-line sniffer.go into 6 focused files (http, h2, tls, ws, rewrite, test), extract pure helpers (clampBodySize, normalizeHost, effectiveReadTimeout), fix "DeafultSampleRate" typo, add doc comments on all exported symbols, and add 22 unit tests.
190 lines
4.6 KiB
Go
190 lines
4.6 KiB
Go
package sniffing
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"time"
|
|
|
|
"github.com/go-gost/core/logger"
|
|
"github.com/go-gost/core/recorder"
|
|
xhttp "github.com/go-gost/x/internal/net/http"
|
|
xrecorder "github.com/go-gost/x/recorder"
|
|
"golang.org/x/net/http2"
|
|
)
|
|
|
|
// serveH2 handles HTTP/2 connections by reading the client preface and
|
|
// proxying requests through an http2.Transport.
|
|
func (h *Sniffer) serveH2(ctx context.Context, network string, conn net.Conn, ho *HandleOptions) error {
|
|
const expectedBody = "SM\r\n\r\n"
|
|
|
|
buf := make([]byte, len(expectedBody))
|
|
n, err := io.ReadFull(conn, buf)
|
|
if err != nil {
|
|
return fmt.Errorf("h2: error reading client preface: %s", err)
|
|
}
|
|
if string(buf[:n]) != expectedBody {
|
|
return errors.New("h2: invalid client preface")
|
|
}
|
|
|
|
ro := ho.recorderObject
|
|
log := ho.log
|
|
|
|
ro.Time = time.Time{}
|
|
|
|
tr := &http2.Transport{
|
|
DialTLSContext: func(ctx context.Context, nw, addr string, cfg *tls.Config) (net.Conn, error) {
|
|
if dial := ho.dialTLS; dial != nil {
|
|
return dial(ctx, network, addr, cfg)
|
|
}
|
|
|
|
cc, err := (&net.Dialer{}).DialContext(ctx, network, addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log = log.WithFields(map[string]any{"src": cc.LocalAddr().String(), "dst": cc.RemoteAddr().String()})
|
|
ro.SrcAddr = cc.LocalAddr().String()
|
|
ro.DstAddr = cc.RemoteAddr().String()
|
|
|
|
cc = tls.Client(cc, cfg)
|
|
return cc, nil
|
|
},
|
|
}
|
|
defer tr.CloseIdleConnections()
|
|
|
|
(&http2.Server{}).ServeConn(conn, &http2.ServeConnOpts{
|
|
Context: ctx,
|
|
SawClientPreface: true,
|
|
Handler: &h2Handler{
|
|
transport: tr,
|
|
recorder: h.Recorder,
|
|
recorderOptions: h.RecorderOptions,
|
|
recorderObject: ro,
|
|
log: log,
|
|
},
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// h2Handler is an http.Handler that proxies HTTP/2 requests through an
|
|
// http2.Transport while recording request and response metadata.
|
|
type h2Handler struct {
|
|
transport http.RoundTripper
|
|
recorder recorder.Recorder
|
|
recorderOptions *recorder.Options
|
|
recorderObject *xrecorder.HandlerRecorderObject
|
|
log logger.Logger
|
|
}
|
|
|
|
func (h *h2Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
log := h.log
|
|
|
|
ro := &xrecorder.HandlerRecorderObject{}
|
|
*ro = *h.recorderObject
|
|
ro.Time = time.Now()
|
|
|
|
var err error
|
|
log.Infof("%s <-> %s", ro.RemoteAddr, r.Host)
|
|
defer func() {
|
|
ro.Duration = time.Since(ro.Time)
|
|
if err != nil {
|
|
ro.Err = err.Error()
|
|
}
|
|
if rerr := ro.Record(r.Context(), h.recorder); rerr != nil {
|
|
log.Errorf("record: %v", rerr)
|
|
}
|
|
|
|
log.WithFields(map[string]any{
|
|
"duration": time.Since(ro.Time),
|
|
}).Infof("%s >-< %s", ro.RemoteAddr, r.Host)
|
|
}()
|
|
|
|
if clientIP := xhttp.GetClientIP(r); clientIP != nil {
|
|
ro.ClientIP = clientIP.String()
|
|
}
|
|
ro.HTTP = &xrecorder.HTTPRecorderObject{
|
|
Host: r.Host,
|
|
Proto: r.Proto,
|
|
Scheme: "https",
|
|
Method: r.Method,
|
|
URI: r.RequestURI,
|
|
Request: xrecorder.HTTPRequestRecorderObject{
|
|
ContentLength: r.ContentLength,
|
|
Header: r.Header.Clone(),
|
|
},
|
|
}
|
|
|
|
if log.IsLevelEnabled(logger.TraceLevel) {
|
|
dump, _ := httputil.DumpRequest(r, false)
|
|
log.Trace(string(dump))
|
|
}
|
|
|
|
url := r.URL
|
|
url.Scheme = "https"
|
|
url.Host = r.Host
|
|
req := &http.Request{
|
|
Method: r.Method,
|
|
URL: url,
|
|
Host: r.Host,
|
|
Header: r.Header,
|
|
Body: r.Body,
|
|
ContentLength: r.ContentLength,
|
|
Trailer: r.Trailer,
|
|
}
|
|
|
|
var reqBody *xhttp.Body
|
|
if bodySize := clampBodySize(h.recorderOptions); bodySize > 0 && req.Body != nil {
|
|
reqBody = xhttp.NewBody(req.Body, bodySize)
|
|
req.Body = reqBody
|
|
}
|
|
|
|
resp, respErr := h.transport.RoundTrip(req.WithContext(r.Context()))
|
|
if reqBody != nil {
|
|
ro.HTTP.Request.Body = reqBody.Content()
|
|
ro.HTTP.Request.ContentLength = reqBody.Length()
|
|
}
|
|
err = respErr
|
|
if err != nil {
|
|
log.Error(err)
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
ro.HTTP.StatusCode = resp.StatusCode
|
|
ro.HTTP.Response.Header = resp.Header
|
|
ro.HTTP.Response.ContentLength = resp.ContentLength
|
|
|
|
if log.IsLevelEnabled(logger.TraceLevel) {
|
|
dump, _ := httputil.DumpResponse(resp, false)
|
|
log.Trace(string(dump))
|
|
}
|
|
|
|
h.setHeader(w, resp.Header)
|
|
w.WriteHeader(resp.StatusCode)
|
|
|
|
if bodySize := clampBodySize(h.recorderOptions); bodySize > 0 {
|
|
respBody := xhttp.NewBody(resp.Body, bodySize)
|
|
resp.Body = respBody
|
|
io.Copy(w, resp.Body)
|
|
ro.HTTP.Response.Body = respBody.Content()
|
|
ro.HTTP.Response.ContentLength = respBody.Length()
|
|
} else {
|
|
io.Copy(w, resp.Body)
|
|
}
|
|
}
|
|
|
|
func (h *h2Handler) setHeader(w http.ResponseWriter, header http.Header) {
|
|
for k, v := range header {
|
|
for i := range v {
|
|
w.Header().Add(k, v[i])
|
|
}
|
|
}
|
|
}
|