a9d3ffa496
Wire pStats into sub-handlers to capture real byte counts from QUIC streams instead of the dummy listener conn. Resolve target address and validate capsule-protocol after authentication instead of before 200 response to prevent dead tunnels and unauthenticated probing. Merge pending observer events rather than replacing on consecutive failures. Add idle read timeout to TCP tunnel relay. Add doc comments on exported symbols and NewHandler.
636 lines
17 KiB
Go
636 lines
17 KiB
Go
package masque
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-gost/core/auth"
|
|
"github.com/go-gost/core/bypass"
|
|
"github.com/go-gost/core/handler"
|
|
"github.com/go-gost/core/limiter"
|
|
"github.com/go-gost/core/limiter/traffic"
|
|
"github.com/go-gost/core/logger"
|
|
md "github.com/go-gost/core/metadata"
|
|
"github.com/go-gost/core/observer"
|
|
"github.com/go-gost/core/observer/stats"
|
|
"github.com/go-gost/core/recorder"
|
|
xbypass "github.com/go-gost/x/bypass"
|
|
xctx "github.com/go-gost/x/ctx"
|
|
ictx "github.com/go-gost/x/internal/ctx"
|
|
xnet "github.com/go-gost/x/internal/net"
|
|
"github.com/go-gost/x/internal/net/udp"
|
|
masque_util "github.com/go-gost/x/internal/util/masque"
|
|
stats_util "github.com/go-gost/x/internal/util/stats"
|
|
rate_limiter "github.com/go-gost/x/limiter/rate"
|
|
cache_limiter "github.com/go-gost/x/limiter/traffic/cache"
|
|
traffic_wrapper "github.com/go-gost/x/limiter/traffic/wrapper"
|
|
metrics "github.com/go-gost/x/metrics/wrapper"
|
|
xstats "github.com/go-gost/x/observer/stats"
|
|
stats_wrapper "github.com/go-gost/x/observer/stats/wrapper"
|
|
xrecorder "github.com/go-gost/x/recorder"
|
|
"github.com/go-gost/x/registry"
|
|
"github.com/quic-go/quic-go/http3"
|
|
)
|
|
|
|
func init() {
|
|
registry.HandlerRegistry().Register("masque", NewHandler)
|
|
}
|
|
|
|
var (
|
|
// ErrBadRequest indicates the request was malformed or invalid.
|
|
ErrBadRequest = errors.New("masque: bad request")
|
|
// ErrMethodNotAllowed indicates the request method is not CONNECT.
|
|
ErrMethodNotAllowed = errors.New("masque: method not allowed")
|
|
// ErrCapsuleRequired indicates the Capsule-Protocol header is missing or invalid.
|
|
ErrCapsuleRequired = errors.New("masque: capsule-protocol header required")
|
|
// ErrDatagramNotSupport indicates that the QUIC connection does not support HTTP/3 datagrams.
|
|
ErrDatagramNotSupport = errors.New("masque: datagrams not supported")
|
|
)
|
|
|
|
type masqueHandler struct {
|
|
md metadata
|
|
options handler.Options
|
|
stats *stats_util.HandlerStats
|
|
limiter traffic.TrafficLimiter
|
|
cancel context.CancelFunc
|
|
recorder recorder.RecorderObject
|
|
}
|
|
|
|
// NewHandler creates a MASQUE protocol handler that supports CONNECT-UDP
|
|
// (RFC 9298) and CONNECT for TCP tunneling over HTTP/3.
|
|
func NewHandler(opts ...handler.Option) handler.Handler {
|
|
options := handler.Options{}
|
|
for _, opt := range opts {
|
|
opt(&options)
|
|
}
|
|
|
|
return &masqueHandler{
|
|
options: options,
|
|
}
|
|
}
|
|
|
|
func (h *masqueHandler) Init(md md.Metadata) error {
|
|
if err := h.parseMetadata(md); err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
h.cancel = cancel
|
|
|
|
if h.options.Observer != nil {
|
|
h.stats = stats_util.NewHandlerStats(h.options.Service, h.md.observerResetTraffic)
|
|
go h.observeStats(ctx)
|
|
}
|
|
|
|
if h.options.Limiter != nil {
|
|
h.limiter = cache_limiter.NewCachedTrafficLimiter(h.options.Limiter,
|
|
cache_limiter.RefreshIntervalOption(h.md.limiterRefreshInterval),
|
|
cache_limiter.CleanupIntervalOption(h.md.limiterCleanupInterval),
|
|
cache_limiter.ScopeOption(limiter.ScopeClient),
|
|
)
|
|
}
|
|
|
|
for _, ro := range h.options.Recorders {
|
|
if ro.Record == xrecorder.RecorderServiceHandler {
|
|
h.recorder = ro
|
|
break
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (h *masqueHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) (err error) {
|
|
defer conn.Close()
|
|
|
|
start := time.Now()
|
|
|
|
ro := &xrecorder.HandlerRecorderObject{
|
|
Network: "",
|
|
Service: h.options.Service,
|
|
RemoteAddr: conn.RemoteAddr().String(),
|
|
LocalAddr: conn.LocalAddr().String(),
|
|
Proto: "masque",
|
|
Time: start,
|
|
SID: xctx.SidFromContext(ctx).String(),
|
|
}
|
|
|
|
if srcAddr := xctx.SrcAddrFromContext(ctx); srcAddr != nil {
|
|
ro.ClientAddr = srcAddr.String()
|
|
}
|
|
|
|
log := h.options.Logger.WithFields(map[string]any{
|
|
"network": ro.Network,
|
|
"remote": conn.RemoteAddr().String(),
|
|
"local": conn.LocalAddr().String(),
|
|
"client": ro.ClientAddr,
|
|
"sid": ro.SID,
|
|
})
|
|
log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr())
|
|
|
|
pStats := &xstats.Stats{}
|
|
|
|
defer func() {
|
|
if err != nil {
|
|
ro.Err = err.Error()
|
|
}
|
|
ro.InputBytes = pStats.Get(stats.KindInputBytes)
|
|
ro.OutputBytes = pStats.Get(stats.KindOutputBytes)
|
|
ro.Duration = time.Since(start)
|
|
if h.recorder.Recorder != nil {
|
|
if err := ro.Record(ctx, h.recorder.Recorder); err != nil {
|
|
log.Errorf("record: %v", err)
|
|
}
|
|
}
|
|
|
|
log.WithFields(map[string]any{
|
|
"duration": time.Since(start),
|
|
"inputBytes": ro.InputBytes,
|
|
"outputBytes": ro.OutputBytes,
|
|
}).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr())
|
|
}()
|
|
|
|
if !h.checkRateLimit(conn.RemoteAddr()) {
|
|
return rate_limiter.ErrRateLimit
|
|
}
|
|
|
|
// Extract request and response from context metadata
|
|
ctxMd := ictx.MetadataFromContext(ctx)
|
|
if ctxMd == nil {
|
|
err := errors.New("masque: wrong connection type, requires HTTP/3")
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
|
|
w, _ := ctxMd.Get("w").(http.ResponseWriter)
|
|
r, _ := ctxMd.Get("r").(*http.Request)
|
|
|
|
if w == nil {
|
|
return ErrBadRequest
|
|
}
|
|
if r == nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return ErrBadRequest
|
|
}
|
|
|
|
// Validate CONNECT method
|
|
if r.Method != http.MethodConnect {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
log.Error(ErrMethodNotAllowed)
|
|
return ErrMethodNotAllowed
|
|
}
|
|
|
|
// Dispatch based on :protocol pseudo-header
|
|
// In quic-go, the :protocol pseudo-header is stored in r.Proto
|
|
switch r.Proto {
|
|
case "connect-udp":
|
|
// Extended CONNECT for UDP (RFC 9298)
|
|
ro.Network = "udp"
|
|
return h.handleConnectUDP(ctx, w, r, conn.LocalAddr(), ro, log, pStats)
|
|
case "HTTP/3.0", "":
|
|
// Standard CONNECT for TCP (RFC 9114)
|
|
// r.Proto is "HTTP/3.0" for standard HTTP/3 CONNECT (no :protocol header)
|
|
ro.Network = "tcp"
|
|
return h.handleConnectTCP(ctx, w, r, conn.LocalAddr(), ro, log, pStats)
|
|
default:
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
log.Errorf("masque: unsupported protocol: %s", r.Proto)
|
|
return ErrBadRequest
|
|
}
|
|
}
|
|
|
|
func (h *masqueHandler) Close() error {
|
|
if h.cancel != nil {
|
|
h.cancel()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (h *masqueHandler) handleConnectUDP(ctx context.Context, w http.ResponseWriter, r *http.Request, laddr net.Addr, ro *xrecorder.HandlerRecorderObject, log logger.Logger, pStats *xstats.Stats) error {
|
|
// Extract user for logging
|
|
if u, _, _ := h.basicProxyAuth(r.Header.Get("Proxy-Authorization")); u != "" {
|
|
log = log.WithFields(map[string]any{"user": u})
|
|
ro.ClientID = u
|
|
}
|
|
|
|
// Authenticate
|
|
clientID, ok := h.authenticate(ctx, w, r, log)
|
|
if !ok {
|
|
return errors.New("authentication failed")
|
|
}
|
|
|
|
if clientID != "" {
|
|
log = log.WithFields(map[string]any{"clientID": clientID})
|
|
ro.ClientID = clientID
|
|
}
|
|
ctx = xctx.ContextWithClientID(ctx, xctx.ClientID(clientID))
|
|
|
|
// Parse target from path
|
|
host, port, err := masque_util.ParseMasquePath(r.URL.Path)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
log.Error("masque: invalid path: ", err)
|
|
return err
|
|
}
|
|
|
|
targetAddr := fmt.Sprintf("%s:%d", host, port)
|
|
ro.Host = targetAddr
|
|
log = log.WithFields(map[string]any{
|
|
"target": targetAddr,
|
|
})
|
|
log.Debug("connect-udp request to ", targetAddr)
|
|
|
|
// Check bypass
|
|
if h.options.Bypass != nil &&
|
|
h.options.Bypass.Contains(ctx, "udp", targetAddr, bypass.WithService(h.options.Service)) {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
log.Debug("bypass: ", targetAddr)
|
|
return xbypass.ErrBypass
|
|
}
|
|
|
|
// Validate capsule-protocol header (required for CONNECT-UDP per RFC 9298)
|
|
if r.Header.Get("Capsule-Protocol") != "?1" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
log.Error(ErrCapsuleRequired)
|
|
return ErrCapsuleRequired
|
|
}
|
|
|
|
// Resolve target address
|
|
raddr, err := net.ResolveUDPAddr("udp", targetAddr)
|
|
if err != nil {
|
|
log.Error("masque: failed to resolve target address: ", err)
|
|
return err
|
|
}
|
|
|
|
// Get HTTP/3 stream for datagrams
|
|
streamer, ok := w.(http3.HTTPStreamer)
|
|
if !ok {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
log.Error(ErrDatagramNotSupport)
|
|
return ErrDatagramNotSupport
|
|
}
|
|
|
|
// Get the underlying HTTP/3 stream
|
|
stream := streamer.HTTPStream()
|
|
|
|
// Send success response with capsule-protocol header
|
|
w.Header().Set("Capsule-Protocol", "?1")
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
// Flush the response headers
|
|
if flusher, ok := w.(http.Flusher); ok {
|
|
flusher.Flush()
|
|
}
|
|
|
|
// Create datagram connection wrapping the HTTP/3 stream (client side)
|
|
datagramConn := masque_util.NewDatagramConn(stream, laddr, raddr)
|
|
defer datagramConn.Close()
|
|
|
|
// Wrap with recorder stats and traffic limiter
|
|
var clientPC net.PacketConn = datagramConn
|
|
clientPC = stats_wrapper.WrapPacketConn(clientPC, pStats)
|
|
clientPC = traffic_wrapper.WrapPacketConn(
|
|
clientPC,
|
|
h.limiter,
|
|
clientID,
|
|
limiter.ServiceOption(h.options.Service),
|
|
limiter.ScopeOption(limiter.ScopeClient),
|
|
limiter.NetworkOption("udp"),
|
|
limiter.AddrOption(targetAddr),
|
|
limiter.ClientOption(clientID),
|
|
limiter.SrcOption(ro.RemoteAddr),
|
|
)
|
|
|
|
// Track per-client connection stats
|
|
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)
|
|
clientPC = stats_wrapper.WrapPacketConn(clientPC, pstats)
|
|
}
|
|
|
|
// Get target connection - either through router/chain or direct
|
|
var targetPC net.PacketConn
|
|
|
|
switch h.md.hash {
|
|
case "host":
|
|
ctx = xctx.ContextWithHash(ctx, &xctx.Hash{Source: targetAddr})
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
|
|
if h.options.Router != nil {
|
|
// Use router to dial through chain (for forwarding through upstream proxy)
|
|
c, err := h.options.Router.Dial(ictx.ContextWithBuffer(ctx, &buf), "udp", targetAddr)
|
|
ro.Route = buf.String()
|
|
if err != nil {
|
|
log.Error("masque: failed to dial through router: ", err)
|
|
return err
|
|
}
|
|
defer c.Close()
|
|
|
|
ro.SrcAddr = c.LocalAddr().String()
|
|
|
|
// The connection from router should be a PacketConn (e.g., from masque connector)
|
|
if pc, ok := c.(net.PacketConn); ok {
|
|
targetPC = pc
|
|
log.Debugf("relaying UDP to %s via chain", targetAddr)
|
|
} else {
|
|
// Wrap as PacketConn if it's a regular Conn
|
|
targetPC = &connPacketConn{Conn: c, raddr: raddr}
|
|
log.Debugf("relaying UDP to %s via chain (wrapped)", targetAddr)
|
|
}
|
|
} else {
|
|
// Direct connection - create local UDP socket
|
|
directConn, err := net.ListenPacket("udp", "")
|
|
if err != nil {
|
|
log.Error("masque: failed to create UDP socket: ", err)
|
|
return err
|
|
}
|
|
defer directConn.Close()
|
|
|
|
ro.SrcAddr = directConn.LocalAddr().String()
|
|
|
|
// Wrap with fixed target address
|
|
targetPC = &fixedTargetPacketConn{
|
|
PacketConn: directConn,
|
|
target: raddr,
|
|
}
|
|
log.Debugf("relaying UDP to %s directly via %s", raddr, directConn.LocalAddr())
|
|
}
|
|
|
|
// Wrap target with metrics
|
|
targetPC = metrics.WrapPacketConn(h.options.Service, targetPC)
|
|
|
|
// Relay UDP packets between client and target
|
|
relay := udp.NewRelay(clientPC, targetPC).
|
|
WithService(h.options.Service).
|
|
WithLogger(log).
|
|
WithBufferSize(h.md.bufferSize)
|
|
|
|
return relay.Run(ctx)
|
|
}
|
|
|
|
func (h *masqueHandler) handleConnectTCP(ctx context.Context, w http.ResponseWriter, r *http.Request, laddr net.Addr, ro *xrecorder.HandlerRecorderObject, log logger.Logger, pStats *xstats.Stats) error {
|
|
// Extract user for logging
|
|
if u, _, _ := h.basicProxyAuth(r.Header.Get("Proxy-Authorization")); u != "" {
|
|
log = log.WithFields(map[string]any{"user": u})
|
|
ro.ClientID = u
|
|
}
|
|
|
|
// Authenticate
|
|
clientID, ok := h.authenticate(ctx, w, r, log)
|
|
if !ok {
|
|
return errors.New("authentication failed")
|
|
}
|
|
|
|
if clientID != "" {
|
|
log = log.WithFields(map[string]any{"clientID": clientID})
|
|
ro.ClientID = clientID
|
|
}
|
|
ctx = xctx.ContextWithClientID(ctx, xctx.ClientID(clientID))
|
|
|
|
// Target address comes from :authority (r.Host) for standard CONNECT
|
|
targetAddr := r.Host
|
|
if targetAddr == "" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
log.Error("masque: missing target address in :authority")
|
|
return ErrBadRequest
|
|
}
|
|
|
|
// Ensure port is present
|
|
if _, port, _ := net.SplitHostPort(targetAddr); port == "" {
|
|
targetAddr = net.JoinHostPort(strings.Trim(targetAddr, "[]"), "443")
|
|
}
|
|
|
|
ro.Host = targetAddr
|
|
log = log.WithFields(map[string]any{
|
|
"target": targetAddr,
|
|
})
|
|
log.Debug("connect-tcp request to ", targetAddr)
|
|
|
|
// Check bypass
|
|
if h.options.Bypass != nil &&
|
|
h.options.Bypass.Contains(ctx, "tcp", targetAddr, bypass.WithService(h.options.Service)) {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
log.Debug("bypass: ", targetAddr)
|
|
return xbypass.ErrBypass
|
|
}
|
|
|
|
// Resolve target address for StreamConn
|
|
raddr, err := net.ResolveTCPAddr("tcp", targetAddr)
|
|
if err != nil {
|
|
log.Error("masque: failed to resolve target address: ", err)
|
|
return err
|
|
}
|
|
|
|
// Dial target connection
|
|
switch h.md.hash {
|
|
case "host":
|
|
ctx = xctx.ContextWithHash(ctx, &xctx.Hash{Source: targetAddr})
|
|
}
|
|
|
|
var cc net.Conn
|
|
var buf bytes.Buffer
|
|
|
|
if h.options.Router != nil {
|
|
cc, err = h.options.Router.Dial(ictx.ContextWithBuffer(ctx, &buf), "tcp", targetAddr)
|
|
} else {
|
|
cc, err = net.Dial("tcp", targetAddr)
|
|
}
|
|
ro.Route = buf.String()
|
|
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
log.Error("masque: failed to dial target: ", err)
|
|
return err
|
|
}
|
|
defer cc.Close()
|
|
|
|
ro.SrcAddr = cc.LocalAddr().String()
|
|
ro.DstAddr = cc.RemoteAddr().String()
|
|
|
|
// Get HTTP/3 stream
|
|
streamer, ok := w.(http3.HTTPStreamer)
|
|
if !ok {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
log.Error("masque: failed to get HTTP/3 streamer")
|
|
return errors.New("masque: HTTP/3 streamer not available")
|
|
}
|
|
|
|
// Get the underlying HTTP/3 stream
|
|
stream := streamer.HTTPStream()
|
|
|
|
// Send 200 OK response
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
// Flush the response headers
|
|
if flusher, ok := w.(http.Flusher); ok {
|
|
flusher.Flush()
|
|
}
|
|
|
|
// Create stream connection wrapping the HTTP/3 stream
|
|
streamConn := masque_util.NewStreamConn(stream, laddr, raddr)
|
|
defer streamConn.Close()
|
|
|
|
// Wrap with recorder stats and traffic limiter
|
|
var clientConn net.Conn = streamConn
|
|
clientConn = stats_wrapper.WrapConn(clientConn, pStats)
|
|
clientConn = traffic_wrapper.WrapConn(
|
|
clientConn,
|
|
h.limiter,
|
|
clientID,
|
|
limiter.ServiceOption(h.options.Service),
|
|
limiter.ScopeOption(limiter.ScopeClient),
|
|
limiter.NetworkOption("tcp"),
|
|
limiter.AddrOption(targetAddr),
|
|
limiter.ClientOption(clientID),
|
|
limiter.SrcOption(ro.RemoteAddr),
|
|
)
|
|
|
|
// Track per-client connection stats
|
|
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)
|
|
clientConn = stats_wrapper.WrapConn(clientConn, pstats)
|
|
}
|
|
|
|
// Wrap target with metrics
|
|
cc = metrics.WrapConn(h.options.Service, cc)
|
|
|
|
log.Infof("%s <-> %s", ro.RemoteAddr, targetAddr)
|
|
|
|
// Bidirectional relay
|
|
xnet.Pipe(ctx, clientConn, cc, xnet.WithReadTimeout(h.md.idleTimeout))
|
|
|
|
log.WithFields(map[string]any{
|
|
"duration": time.Since(ro.Time),
|
|
}).Infof("%s >-< %s", ro.RemoteAddr, targetAddr)
|
|
|
|
return nil
|
|
}
|
|
|
|
// fixedTargetPacketConn wraps a PacketConn and always reads/writes to a fixed target address
|
|
type fixedTargetPacketConn struct {
|
|
net.PacketConn
|
|
target net.Addr
|
|
}
|
|
|
|
func (c *fixedTargetPacketConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) {
|
|
n, _, err = c.PacketConn.ReadFrom(b)
|
|
return n, c.target, err
|
|
}
|
|
|
|
func (c *fixedTargetPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
|
|
return c.PacketConn.WriteTo(b, c.target)
|
|
}
|
|
|
|
// connPacketConn wraps a net.Conn as a net.PacketConn for use with the UDP relay.
|
|
// This is used when the router returns a stream-based connection (like from masque connector).
|
|
type connPacketConn struct {
|
|
net.Conn
|
|
raddr net.Addr
|
|
}
|
|
|
|
func (c *connPacketConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) {
|
|
n, err = c.Conn.Read(b)
|
|
return n, c.raddr, err
|
|
}
|
|
|
|
func (c *connPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
|
|
return c.Conn.Write(b)
|
|
}
|
|
|
|
func (h *masqueHandler) checkRateLimit(addr net.Addr) bool {
|
|
if h.options.RateLimiter == nil {
|
|
return true
|
|
}
|
|
host, _, _ := net.SplitHostPort(addr.String())
|
|
if limiter := h.options.RateLimiter.Limiter(host); limiter != nil {
|
|
return limiter.Allow(1)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (h *masqueHandler) observeStats(ctx context.Context) {
|
|
if h.options.Observer == nil {
|
|
return
|
|
}
|
|
|
|
var events []observer.Event
|
|
|
|
ticker := time.NewTicker(h.md.observerPeriod)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
if len(events) > 0 {
|
|
if err := h.options.Observer.Observe(ctx, events); err == nil {
|
|
events = nil
|
|
}
|
|
break
|
|
}
|
|
|
|
evs := h.stats.Events()
|
|
if err := h.options.Observer.Observe(ctx, evs); err != nil {
|
|
events = append(events, evs...)
|
|
}
|
|
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *masqueHandler) basicProxyAuth(proxyAuth string) (username, password string, ok bool) {
|
|
if proxyAuth == "" {
|
|
return
|
|
}
|
|
|
|
if !strings.HasPrefix(proxyAuth, "Basic ") {
|
|
return
|
|
}
|
|
c, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(proxyAuth, "Basic "))
|
|
if err != nil {
|
|
return
|
|
}
|
|
cs := string(c)
|
|
s := strings.IndexByte(cs, ':')
|
|
if s < 0 {
|
|
return
|
|
}
|
|
|
|
return cs[:s], cs[s+1:], true
|
|
}
|
|
|
|
func (h *masqueHandler) authenticate(ctx context.Context, w http.ResponseWriter, r *http.Request, log logger.Logger) (id string, ok bool) {
|
|
u, p, _ := h.basicProxyAuth(r.Header.Get("Proxy-Authorization"))
|
|
if h.options.Auther == nil {
|
|
return "", true
|
|
}
|
|
if id, ok = h.options.Auther.Authenticate(ctx, u, p, auth.WithService(h.options.Service)); ok {
|
|
return
|
|
}
|
|
|
|
realm := defaultRealm
|
|
if h.md.authBasicRealm != "" {
|
|
realm = h.md.authBasicRealm
|
|
}
|
|
w.Header().Set("Proxy-Authenticate", fmt.Sprintf("Basic realm=\"%s\"", realm))
|
|
w.WriteHeader(http.StatusProxyAuthRequired)
|
|
log.Debug("proxy authentication required")
|
|
return
|
|
}
|