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 }