e543c0b0fd
- Wrap non-CONNECT upstream with traffic_wrapper and stats_wrapper (was only on CONNECT path) - Write 503 error response on forwardRequest failure instead of silent connection close - Decouple resp.Header from w.Header() to prevent metadata header leakage/doubling in 407 responses - Return pipeTo signal from authenticate instead of dialing inline (matches http handler pattern) - Add idleTimeout metadata field and pass to xnet.Pipe for CONNECT tunnels - Add 4 tests for probe resistance host forwarding and knock bypass
136 lines
3.3 KiB
Go
136 lines
3.3 KiB
Go
package http2
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/go-gost/core/auth"
|
|
"github.com/go-gost/core/logger"
|
|
)
|
|
|
|
func (h *http2Handler) 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 *http2Handler) authenticate(ctx context.Context, w http.ResponseWriter, r *http.Request, resp *http.Response, log logger.Logger) (id string, ok bool, pipeTo string) {
|
|
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
|
|
}
|
|
|
|
pr := h.md.probeResistance
|
|
// probing resistance is enabled, and knocking host is mismatch.
|
|
if pr != nil && (pr.Knock == "" || !knockMatch(r.URL.Hostname(), pr.Knock)) {
|
|
resp.StatusCode = http.StatusServiceUnavailable // default status code
|
|
switch pr.Type {
|
|
case "code":
|
|
resp.StatusCode, _ = strconv.Atoi(pr.Value)
|
|
case "web":
|
|
url := pr.Value
|
|
if !strings.HasPrefix(url, "http") {
|
|
url = "http://" + url
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
log.Error(err)
|
|
break
|
|
}
|
|
r, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
log.Error(err)
|
|
break
|
|
}
|
|
resp = r
|
|
defer resp.Body.Close()
|
|
case "host":
|
|
return "", false, pr.Value
|
|
case "file":
|
|
f, _ := os.Open(pr.Value)
|
|
if f != nil {
|
|
defer f.Close()
|
|
|
|
resp.StatusCode = http.StatusOK
|
|
if finfo, _ := f.Stat(); finfo != nil {
|
|
resp.ContentLength = finfo.Size()
|
|
}
|
|
resp.Header.Set("Content-Type", "text/html")
|
|
resp.Body = f
|
|
}
|
|
}
|
|
}
|
|
|
|
if resp.StatusCode == 0 {
|
|
realm := defaultRealm
|
|
if h.md.authBasicRealm != "" {
|
|
realm = h.md.authBasicRealm
|
|
}
|
|
resp.StatusCode = http.StatusProxyAuthRequired
|
|
resp.Header.Add("Proxy-Authenticate", fmt.Sprintf("Basic realm=\"%s\"", realm))
|
|
if strings.ToLower(r.Header.Get("Proxy-Connection")) == "keep-alive" {
|
|
// XXX libcurl will keep sending auth request in same conn
|
|
// which we don't supported yet.
|
|
resp.Header.Set("Connection", "close")
|
|
resp.Header.Set("Proxy-Connection", "close")
|
|
}
|
|
|
|
log.Debug("proxy authentication required")
|
|
} else {
|
|
resp.Header = http.Header{}
|
|
// resp.Header.Set("Server", "nginx/1.20.1")
|
|
// resp.Header.Set("Date", time.Now().Format(http.TimeFormat))
|
|
if resp.StatusCode == http.StatusOK {
|
|
resp.Header.Set("Connection", "keep-alive")
|
|
}
|
|
}
|
|
|
|
if log.IsLevelEnabled(logger.TraceLevel) {
|
|
dump, _ := httputil.DumpResponse(resp, false)
|
|
log.Trace(string(dump))
|
|
}
|
|
|
|
h.writeResponse(w, resp)
|
|
|
|
return
|
|
}
|
|
|
|
// knockMatch reports whether hostname matches any entry in the
|
|
// comma-separated knock list. Matching is case-insensitive. An empty
|
|
// knock string returns false (no match).
|
|
func knockMatch(hostname, knock string) bool {
|
|
if knock == "" {
|
|
return false
|
|
}
|
|
for _, h := range strings.Split(knock, ",") {
|
|
if strings.EqualFold(hostname, strings.TrimSpace(h)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|