refactor(handler/http2): split handler into 3 focused files with 52 tests
Extract authenticate/probe-resistance to auth.go and roundTrip/forwarding to proxy.go, following the handler/http/ pattern. Export sentinel errors (ErrAuthFailed, ErrWrongConnType) for precise test assertions. Fix flushWriter panic recovery to handle non-string/non-error panics. Default HTTPS port to 443 when no port is specified in the Host header.
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
package http2
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/bypass"
|
||||
"github.com/go-gost/core/limiter"
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/observer/stats"
|
||||
xbypass "github.com/go-gost/x/bypass"
|
||||
xctx "github.com/go-gost/x/ctx"
|
||||
ictx "github.com/go-gost/x/internal/ctx"
|
||||
xio "github.com/go-gost/x/internal/io"
|
||||
xnet "github.com/go-gost/x/internal/net"
|
||||
xhttp "github.com/go-gost/x/internal/net/http"
|
||||
traffic_wrapper "github.com/go-gost/x/limiter/traffic/wrapper"
|
||||
stats_wrapper "github.com/go-gost/x/observer/stats/wrapper"
|
||||
xrecorder "github.com/go-gost/x/recorder"
|
||||
)
|
||||
|
||||
// NOTE: there is an issue (golang/go#43989) will cause the client hangs
|
||||
// when server returns an non-200 status code,
|
||||
// May be fixed in go1.18.
|
||||
func (h *http2Handler) roundTrip(ctx context.Context, w http.ResponseWriter, req *http.Request, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error {
|
||||
if w == nil || req == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to get the actual host.
|
||||
// Compatible with GOST 2.x.
|
||||
if v := req.Header.Get("Gost-Target"); v != "" {
|
||||
if h, err := h.decodeServerName(v); err == nil {
|
||||
req.Host = h
|
||||
}
|
||||
}
|
||||
if v := req.Header.Get("X-Gost-Target"); v != "" {
|
||||
if h, err := h.decodeServerName(v); err == nil {
|
||||
req.Host = h
|
||||
}
|
||||
}
|
||||
|
||||
if clientIP := xhttp.GetClientIP(req); clientIP != nil {
|
||||
ro.ClientIP = clientIP.String()
|
||||
}
|
||||
|
||||
host := req.Host
|
||||
if _, port, _ := net.SplitHostPort(host); port == "" {
|
||||
port := "80"
|
||||
if req.URL.Scheme == "https" || req.TLS != nil {
|
||||
port = "443"
|
||||
}
|
||||
host = net.JoinHostPort(strings.Trim(host, "[]"), port)
|
||||
}
|
||||
ro.Host = host
|
||||
|
||||
fields := map[string]any{
|
||||
"dst": host,
|
||||
"host": host,
|
||||
}
|
||||
if u, _, _ := h.basicProxyAuth(req.Header.Get("Proxy-Authorization")); u != "" {
|
||||
fields["user"] = u
|
||||
ro.ClientID = u
|
||||
}
|
||||
log = log.WithFields(fields)
|
||||
|
||||
if log.IsLevelEnabled(logger.TraceLevel) {
|
||||
dump, _ := httputil.DumpRequest(req, false)
|
||||
log.Trace(string(dump))
|
||||
}
|
||||
log.Debugf("%s >> %s", req.RemoteAddr, host)
|
||||
|
||||
for k := range h.md.header {
|
||||
w.Header().Set(k, h.md.header.Get(k))
|
||||
}
|
||||
|
||||
resp := &http.Response{
|
||||
ProtoMajor: 2,
|
||||
ProtoMinor: 0,
|
||||
Header: w.Header(),
|
||||
Body: io.NopCloser(bytes.NewReader([]byte{})),
|
||||
}
|
||||
|
||||
ro.HTTP = &xrecorder.HTTPRecorderObject{
|
||||
Host: req.Host,
|
||||
Proto: req.Proto,
|
||||
Scheme: req.URL.Scheme,
|
||||
Method: req.Method,
|
||||
URI: req.RequestURI,
|
||||
Request: xrecorder.HTTPRequestRecorderObject{
|
||||
ContentLength: req.ContentLength,
|
||||
Header: req.Header.Clone(),
|
||||
},
|
||||
}
|
||||
defer func() {
|
||||
ro.HTTP.StatusCode = resp.StatusCode
|
||||
ro.HTTP.Response.Header = resp.Header
|
||||
}()
|
||||
|
||||
clientID, ok := h.authenticate(ctx, w, req, resp, log)
|
||||
if !ok {
|
||||
return ErrAuthFailed
|
||||
}
|
||||
|
||||
log = log.WithFields(map[string]any{"clientID": clientID})
|
||||
ro.ClientID = clientID
|
||||
|
||||
ctx = xctx.ContextWithClientID(ctx, xctx.ClientID(clientID))
|
||||
|
||||
if h.options.Bypass != nil && h.options.Bypass.Contains(ctx, "tcp", host, bypass.WithService(h.options.Service)) {
|
||||
resp.StatusCode = http.StatusForbidden
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
log.Debug("bypass: ", host)
|
||||
return xbypass.ErrBypass
|
||||
}
|
||||
|
||||
// delete the proxy related headers.
|
||||
req.Header.Del("Proxy-Authorization")
|
||||
req.Header.Del("Proxy-Connection")
|
||||
req.Header.Del("Gost-Target")
|
||||
req.Header.Del("X-Gost-Target")
|
||||
|
||||
switch h.md.hash {
|
||||
case "host":
|
||||
ctx = xctx.ContextWithHash(ctx, &xctx.Hash{Source: host})
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
cc, err := h.options.Router.Dial(ictx.ContextWithBuffer(ctx, &buf), "tcp", host)
|
||||
ro.Route = buf.String()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
resp.StatusCode = http.StatusServiceUnavailable
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
return err
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
log = log.WithFields(map[string]any{"src": cc.LocalAddr().String(), "dst": cc.RemoteAddr().String()})
|
||||
ro.SrcAddr = cc.LocalAddr().String()
|
||||
ro.DstAddr = cc.RemoteAddr().String()
|
||||
|
||||
if req.Method != http.MethodConnect {
|
||||
start := time.Now()
|
||||
log.Infof("%s <-> %s", req.RemoteAddr, host)
|
||||
err = h.forwardRequest(w, req, cc)
|
||||
log.WithFields(map[string]any{
|
||||
"duration": time.Since(start),
|
||||
}).Infof("%s >-< %s", req.RemoteAddr, host)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
resp.StatusCode = http.StatusOK
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if fw, ok := w.(http.Flusher); ok {
|
||||
fw.Flush()
|
||||
}
|
||||
|
||||
rw := xio.NewReadWriter(req.Body, flushWriter{w})
|
||||
|
||||
// compatible with HTTP1.x
|
||||
if hj, ok := w.(http.Hijacker); ok && req.ProtoMajor == 1 {
|
||||
// we take over the underly connection
|
||||
conn, _, err := hj.Hijack()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
resp.StatusCode = http.StatusInternalServerError
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
rw = conn
|
||||
}
|
||||
|
||||
rw = traffic_wrapper.WrapReadWriter(
|
||||
h.limiter,
|
||||
rw,
|
||||
clientID,
|
||||
limiter.ScopeOption(limiter.ScopeClient),
|
||||
limiter.ServiceOption(h.options.Service),
|
||||
limiter.NetworkOption("tcp"),
|
||||
limiter.AddrOption(host),
|
||||
limiter.ClientOption(clientID),
|
||||
limiter.SrcOption(req.RemoteAddr),
|
||||
)
|
||||
if h.options.Observer != nil {
|
||||
pstats := h.stats.Stats(clientID)
|
||||
pstats.Add(stats.KindTotalConns, 1)
|
||||
pstats.Add(stats.KindCurrentConns, 1)
|
||||
defer pstats.Add(stats.KindCurrentConns, -1)
|
||||
rw = stats_wrapper.WrapReadWriter(rw, pstats)
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
log.Infof("%s <-> %s", req.RemoteAddr, host)
|
||||
// xnet.Transport(rw, cc)
|
||||
xnet.Pipe(ctx, xio.NewReadWriteCloser(rw, rw, req.Body), cc)
|
||||
log.WithFields(map[string]any{
|
||||
"duration": time.Since(start),
|
||||
}).Infof("%s >-< %s", req.RemoteAddr, host)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *http2Handler) decodeServerName(s string) (string, error) {
|
||||
b, err := base64.RawURLEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(b) < 4 {
|
||||
return "", errors.New("invalid name")
|
||||
}
|
||||
v, err := base64.RawURLEncoding.DecodeString(string(b[4:]))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if crc32.ChecksumIEEE(v) != binary.BigEndian.Uint32(b[:4]) {
|
||||
return "", errors.New("invalid name")
|
||||
}
|
||||
return string(v), nil
|
||||
}
|
||||
|
||||
func (h *http2Handler) forwardRequest(w http.ResponseWriter, r *http.Request, rw io.ReadWriter) (err error) {
|
||||
if err = r.Write(rw); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := http.ReadResponse(bufio.NewReader(rw), r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return h.writeResponse(w, resp)
|
||||
}
|
||||
|
||||
func (h *http2Handler) writeResponse(w http.ResponseWriter, resp *http.Response) error {
|
||||
for k, v := range resp.Header {
|
||||
for _, vv := range v {
|
||||
w.Header().Add(k, vv)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
_, err := io.Copy(flushWriter{w}, resp.Body)
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user