aba7e2a256
Split ~1250-line sniffer.go into sniffer_http.go (HTTP handling, dial, round-trip), sniffer_tls.go (TLS MITM, termination), sniffer_h2.go (HTTP/2), sniffer_ws.go (WebSocket frame copy/record), and sniffer_rewrite.go (upgrade, body rewrite). Extract clampBodySize, normalizeHost, effectiveReadTimeout as package-level functions to simplify testing. Add 22 tests covering pure functions, option setters, HTTP proxy integration, WebSocket frame copy, and edge cases.
185 lines
4.5 KiB
Go
185 lines
4.5 KiB
Go
package forwarder
|
|
|
|
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, 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, network, addr string, cfg *tls.Config) (net.Conn, error) {
|
|
node, cc, err := h.dialTLS(ctx, addr, ho)
|
|
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()
|
|
log.Debugf("connected to node %s(%s)", node.Name, node.Addr)
|
|
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])
|
|
}
|
|
}
|
|
}
|