diff --git a/pkg/components/handler/http/handler.go b/pkg/components/handler/http/handler.go index 181fb0a..94912fe 100644 --- a/pkg/components/handler/http/handler.go +++ b/pkg/components/handler/http/handler.go @@ -3,10 +3,17 @@ package http import ( "bufio" "context" + "encoding/base64" + "encoding/binary" + "errors" + "hash/crc32" "net" "net/http" "net/http/httputil" + "os" + "strconv" "strings" + "time" "github.com/go-gost/gost/pkg/auth" "github.com/go-gost/gost/pkg/chain" @@ -43,9 +50,9 @@ func (h *Handler) Init(md md.Metadata) error { } func (h *Handler) parseMetadata(md md.Metadata) error { - h.md.proxyAgent = md.GetString(proxyAgent) + h.md.proxyAgent = md.GetString(proxyAgentKey) - if v, _ := md.Get(auths).([]interface{}); len(v) > 0 { + if v, _ := md.Get(authsKey).([]interface{}); len(v) > 0 { authenticator := auth.NewLocalAuthenticator(nil) for _, auth := range v { if s, _ := auth.(string); s != "" { @@ -59,6 +66,17 @@ func (h *Handler) parseMetadata(md md.Metadata) error { } h.md.authenticator = authenticator } + + if v := md.GetString(probeResistKey); v != "" { + if ss := strings.SplitN(v, ":", 2); len(ss) == 2 { + h.md.probeResist = &probeResist{ + Type: ss[0], + Value: ss[1], + Knock: md.GetString(knockKey), + } + } + } + return nil } @@ -66,8 +84,8 @@ func (h *Handler) Handle(ctx context.Context, conn net.Conn) { defer conn.Close() h.logger = h.logger.WithFields(map[string]interface{}{ - "src": conn.RemoteAddr(), - "local": conn.LocalAddr(), + "src": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), }) req, err := http.ReadRequest(bufio.NewReader(conn)) @@ -85,43 +103,32 @@ func (h *Handler) handleRequest(ctx context.Context, conn net.Conn, req *http.Re return } - /* - // try to get the actual host. - if v := req.Header.Get("Gost-Target"); v != "" { - if h, err := decodeServerName(v); err == nil { - req.Host = h - } + // try to get the actual host. + if v := req.Header.Get("Gost-Target"); v != "" { + if h, err := h.decodeServerName(v); err == nil { + req.Host = h } - */ + } + req.Header.Del("Gost-Target") host := req.Host if _, port, _ := net.SplitHostPort(host); port == "" { host = net.JoinHostPort(host, "80") } - h.logger = h.logger.WithFields(map[string]interface{}{ + fields := map[string]interface{}{ "dst": host, - }) + } + if u, _, _ := h.basicProxyAuth(req.Header.Get("Proxy-Authorization")); u != "" { + fields["user"] = u + } + h.logger = h.logger.WithFields(fields) if h.logger.IsLevelEnabled(logger.DebugLevel) { dump, _ := httputil.DumpRequest(req, false) h.logger.Debug(string(dump)) } - /* - u, _, _ := basicProxyAuth(req.Header.Get("Proxy-Authorization")) - if u != "" { - u += "@" - } - log.Logf("[http] %s%s -> %s -> %s", - u, conn.RemoteAddr(), h.options.Node.String(), host) - if Debug { - dump, _ := httputil.DumpRequest(req, false) - log.Logf("[http] %s -> %s\n%s", conn.RemoteAddr(), conn.LocalAddr(), string(dump)) - } - - req.Header.Del("Gost-Target") - */ resp := &http.Response{ ProtoMajor: 1, ProtoMinor: 1, @@ -164,24 +171,20 @@ func (h *Handler) handleRequest(ctx context.Context, conn net.Conn, req *http.Re } */ - /* - if !h.authenticate(conn, req, resp) { - return - } - */ + if !h.authenticate(conn, req, resp) { + return + } if req.Method == "PRI" || (req.Method != http.MethodConnect && req.URL.Scheme != "http") { resp.StatusCode = http.StatusBadRequest - /* - if Debug { - dump, _ := httputil.DumpResponse(resp, false) - log.Logf("[http] %s <- %s\n%s", - conn.RemoteAddr(), conn.LocalAddr(), string(dump)) - } - */ - resp.Write(conn) + + if h.logger.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpResponse(resp, false) + h.logger.Debug(string(dump)) + } + return } @@ -190,14 +193,12 @@ func (h *Handler) handleRequest(ctx context.Context, conn net.Conn, req *http.Re cc, err := h.dial(ctx, host) if err != nil { resp.StatusCode = http.StatusServiceUnavailable - - /* - if Debug { - dump, _ := httputil.DumpResponse(resp, false) - log.Logf("[http] %s <- %s\n%s", conn.RemoteAddr(), conn.LocalAddr(), string(dump)) - } - */ resp.Write(conn) + + if h.logger.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpResponse(resp, false) + h.logger.Debug(string(dump)) + } return } defer cc.Close() @@ -205,11 +206,19 @@ func (h *Handler) handleRequest(ctx context.Context, conn net.Conn, req *http.Re if req.Method == http.MethodConnect { resp.StatusCode = http.StatusOK resp.Status = "200 Connection established" - resp.Write(conn) + + if h.logger.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpResponse(resp, false) + h.logger.Debug(string(dump)) + } + if err = resp.Write(conn); err != nil { + h.logger.Warn(err) + return + } } else { req.Header.Del("Proxy-Connection") - if err = req.Write(cc); err != nil { + h.logger.Warn(err) return } } @@ -252,9 +261,123 @@ func (h *Handler) dial(ctx context.Context, addr string) (conn net.Conn, err err conn, err = route.Dial(ctx, "tcp", addr) if err != nil { + h.logger.Warn("retry:", err) continue } } return } + +func (h *Handler) 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 *Handler) 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 *Handler) authenticate(conn net.Conn, req *http.Request, resp *http.Response) (ok bool) { + u, p, _ := h.basicProxyAuth(req.Header.Get("Proxy-Authorization")) + if h.md.authenticator == nil || h.md.authenticator.Authenticate(u, p) { + return true + } + + pr := h.md.probeResist + // probing resistance is enabled, and knocking host is mismatch. + if pr != nil && (pr.Knock == "" || !strings.EqualFold(req.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 + } + if r, err := http.Get(url); err == nil { + resp = r + defer r.Body.Close() + } + case "host": + cc, err := net.Dial("tcp", pr.Value) + if err == nil { + defer cc.Close() + + req.Write(cc) + handler.Transport(conn, cc) + return + } + case "file": + f, _ := os.Open(pr.Value) + if f != nil { + 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 { + resp.StatusCode = http.StatusProxyAuthRequired + resp.Header.Add("Proxy-Authenticate", "Basic realm=\"gost\"") + if strings.ToLower(req.Header.Get("Proxy-Connection")) == "keep-alive" { + // XXX libcurl will keep sending auth request in same conn + // which we don't supported yet. + resp.Header.Add("Connection", "close") + resp.Header.Add("Proxy-Connection", "close") + } + + h.logger.Info("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 h.logger.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpResponse(resp, false) + h.logger.Debug(string(dump)) + } + + resp.Write(conn) + return +} diff --git a/pkg/components/handler/http/metadata.go b/pkg/components/handler/http/metadata.go index e4086fb..92e8ada 100644 --- a/pkg/components/handler/http/metadata.go +++ b/pkg/components/handler/http/metadata.go @@ -3,9 +3,11 @@ package http import "github.com/go-gost/gost/pkg/auth" const ( - addr = "addr" - proxyAgent = "proxyAgent" - auths = "auths" + addrKey = "addr" + proxyAgentKey = "proxyAgent" + authsKey = "auths" + probeResistKey = "probeResist" + knockKey = "knock" ) type metadata struct { @@ -13,4 +15,11 @@ type metadata struct { authenticator auth.Authenticator proxyAgent string retryCount int + probeResist *probeResist +} + +type probeResist struct { + Type string + Value string + Knock string }