mirror of
https://github.com/go-gost/x.git
synced 2026-10-19 16:16:32 +00:00
fix(forwarder): authenticate HTTP node with Proxy-Authorization
Node auth on the HTTP forwarder path read the origin credential (Authorization) and challenged with 401/WWW-Authenticate. When the forwarded backend also used Basic auth, the node consumed the credential meant for the backend, so the two could not coexist. Read the proxy credential (Proxy-Authorization) instead and challenge with 407/Proxy-Authenticate. Authorization is left untouched so it reaches the backend, and Proxy-Authorization is stripped before the upstream write since it is hop-by-hop (RFC 9110 11.7.2).
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
@@ -43,6 +44,28 @@ func borrowBodyPrefix(body io.ReadCloser, n int) (prefix []byte, restored io.Rea
|
||||
return
|
||||
}
|
||||
|
||||
// basicProxyAuth extracts the username and password from an HTTP Basic
|
||||
// Proxy-Authorization header value. It mirrors net/http's Request.BasicAuth,
|
||||
// but for the proxy credential (RFC 9110 §11.7.2) instead of the origin
|
||||
// credential. It returns ok=false if the value is empty, does not use the
|
||||
// Basic scheme, or is not valid base64; the password may contain colons since
|
||||
// the username/password separator is the first colon.
|
||||
func basicProxyAuth(proxyAuth string) (username, password string, ok bool) {
|
||||
const prefix = "Basic "
|
||||
if len(proxyAuth) < len(prefix) || !strings.EqualFold(proxyAuth[:len(prefix)], prefix) {
|
||||
return "", "", false
|
||||
}
|
||||
c, err := base64.StdEncoding.DecodeString(proxyAuth[len(prefix):])
|
||||
if err != nil {
|
||||
return "", "", false
|
||||
}
|
||||
username, password, ok = strings.Cut(string(c), ":")
|
||||
if !ok {
|
||||
return "", "", false
|
||||
}
|
||||
return username, password, true
|
||||
}
|
||||
|
||||
// HandleHTTP sniffs and proxies an HTTP connection. It reads the initial
|
||||
// request, performs node selection via the configured hop, and forwards the
|
||||
// request with HTTP keep-alive support.
|
||||
@@ -454,13 +477,20 @@ func (h *Sniffer) httpRoundTrip(ctx context.Context, rw, cc io.ReadWriteCloser,
|
||||
var respHeaderRewrites []chain.HTTPHeaderRewriteSettings
|
||||
if httpSettings != nil {
|
||||
if auther := httpSettings.Auther; auther != nil {
|
||||
username, password, _ := req.BasicAuth()
|
||||
// Node auth is proxy auth, so it reads the proxy credential
|
||||
// (Proxy-Authorization) rather than the origin credential
|
||||
// (Authorization). The two are separate fields precisely so a client
|
||||
// can authenticate to this node and to the backend independently;
|
||||
// consuming Authorization here would swallow the credentials the
|
||||
// client sent for a backend that also uses Basic auth (e.g. a
|
||||
// reverse-proxied site behind an authenticated node).
|
||||
username, password, _ := basicProxyAuth(req.Header.Get("Proxy-Authorization"))
|
||||
id, ok := auther.Authenticate(ctx, username, password, auth.WithService(ho.Service))
|
||||
if !ok {
|
||||
res.StatusCode = http.StatusUnauthorized
|
||||
res.StatusCode = http.StatusProxyAuthRequired
|
||||
ro.HTTP.StatusCode = res.StatusCode
|
||||
res.Header.Set("WWW-Authenticate", "Basic")
|
||||
log.Warnf("node %s(%s) 401 unauthorized", node.Name, node.Addr)
|
||||
res.Header.Set("Proxy-Authenticate", "Basic")
|
||||
log.Warnf("node %s(%s) 407 unauthorized", node.Name, node.Addr)
|
||||
res.Write(rw)
|
||||
err = errors.New("unauthorized")
|
||||
return
|
||||
@@ -526,6 +556,10 @@ func (h *Sniffer) httpRoundTrip(ctx context.Context, rw, cc io.ReadWriteCloser,
|
||||
respHeaderRewrites = httpSettings.RewriteResponseHeader
|
||||
}
|
||||
|
||||
// Proxy-Authorization authenticates the client to this node and is
|
||||
// hop-by-hop (RFC 9110 §11.7.2): never forward it to the upstream backend.
|
||||
req.Header.Del("Proxy-Authorization")
|
||||
|
||||
// Snapshot the original request body before rewriting, restoring it via
|
||||
// MultiReader so the body is read from the wire only once. Only when body
|
||||
// recording is enabled and request bodies are actually rewritten.
|
||||
|
||||
Reference in New Issue
Block a user