Browse Source

type http/tcpmux proxy support route_by_http_user, tcpmux support passthourgh mode (#2932)

fatedier 2 years ago
parent
commit
4af85da0c2

+ 0 - 6
Release.md

@@ -1,7 +1 @@
-### New
 
-* Added new parameter `config_dir` in frpc to run multiple client instances in one process.
-
-### Fix
-
-* Equal sign in environment variables causes parsing error.

+ 3 - 0
conf/frpc_full.ini

@@ -216,6 +216,8 @@ subdomain = web01
 custom_domains = web01.yourdomain.com
 # locations is only available for http type
 locations = /,/pic
+# route requests to this service if http basic auto user is abc
+# route_by_http_user = abc
 host_header_rewrite = example.com
 # params with prefix "header_" will be used to update http request headers
 header_X-From-Where = frp
@@ -348,3 +350,4 @@ multiplexer = httpconnect
 local_ip = 127.0.0.1
 local_port = 10701
 custom_domains = tunnel1
+# route_by_http_user = user1

+ 3 - 0
conf/frps_full.ini

@@ -30,6 +30,9 @@ vhost_https_port = 443
 # HTTP CONNECT requests. By default, this value is 0.
 # tcpmux_httpconnect_port = 1337
 
+# If tcpmux_passthrough is true, frps won't do any update on traffic.
+# tcpmux_passthrough = false
+
 # set dashboard_addr and dashboard_port to view dashboard of frps
 # dashboard_addr's default value is same with bind_addr
 # dashboard is available only if dashboard_port is set

+ 10 - 3
pkg/config/proxy.go

@@ -162,6 +162,7 @@ type HTTPProxyConf struct {
 	HTTPPwd           string            `ini:"http_pwd" json:"http_pwd"`
 	HostHeaderRewrite string            `ini:"host_header_rewrite" json:"host_header_rewrite"`
 	Headers           map[string]string `ini:"-" json:"headers"`
+	RouteByHTTPUser   string            `ini:"route_by_http_user" json:"route_by_http_user"`
 }
 
 // HTTPS
@@ -178,8 +179,9 @@ type TCPProxyConf struct {
 
 // TCPMux
 type TCPMuxProxyConf struct {
-	BaseProxyConf `ini:",extends"`
-	DomainConf    `ini:",extends"`
+	BaseProxyConf   `ini:",extends"`
+	DomainConf      `ini:",extends"`
+	RouteByHTTPUser string `ini:"route_by_http_user" json:"route_by_http_user"`
 
 	Multiplexer string `ini:"multiplexer"`
 }
@@ -576,7 +578,7 @@ func (cfg *TCPMuxProxyConf) Compare(cmp ProxyConf) bool {
 		return false
 	}
 
-	if cfg.Multiplexer != cmpConf.Multiplexer {
+	if cfg.Multiplexer != cmpConf.Multiplexer || cfg.RouteByHTTPUser != cmpConf.RouteByHTTPUser {
 		return false
 	}
 
@@ -601,6 +603,7 @@ func (cfg *TCPMuxProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) {
 	cfg.CustomDomains = pMsg.CustomDomains
 	cfg.SubDomain = pMsg.SubDomain
 	cfg.Multiplexer = pMsg.Multiplexer
+	cfg.RouteByHTTPUser = pMsg.RouteByHTTPUser
 }
 
 func (cfg *TCPMuxProxyConf) MarshalToMsg(pMsg *msg.NewProxy) {
@@ -610,6 +613,7 @@ func (cfg *TCPMuxProxyConf) MarshalToMsg(pMsg *msg.NewProxy) {
 	pMsg.CustomDomains = cfg.CustomDomains
 	pMsg.SubDomain = cfg.SubDomain
 	pMsg.Multiplexer = cfg.Multiplexer
+	pMsg.RouteByHTTPUser = cfg.RouteByHTTPUser
 }
 
 func (cfg *TCPMuxProxyConf) CheckForCli() (err error) {
@@ -724,6 +728,7 @@ func (cfg *HTTPProxyConf) Compare(cmp ProxyConf) bool {
 		cfg.HTTPUser != cmpConf.HTTPUser ||
 		cfg.HTTPPwd != cmpConf.HTTPPwd ||
 		cfg.HostHeaderRewrite != cmpConf.HostHeaderRewrite ||
+		cfg.RouteByHTTPUser != cmpConf.RouteByHTTPUser ||
 		!reflect.DeepEqual(cfg.Headers, cmpConf.Headers) {
 		return false
 	}
@@ -754,6 +759,7 @@ func (cfg *HTTPProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) {
 	cfg.HTTPUser = pMsg.HTTPUser
 	cfg.HTTPPwd = pMsg.HTTPPwd
 	cfg.Headers = pMsg.Headers
+	cfg.RouteByHTTPUser = pMsg.RouteByHTTPUser
 }
 
 func (cfg *HTTPProxyConf) MarshalToMsg(pMsg *msg.NewProxy) {
@@ -767,6 +773,7 @@ func (cfg *HTTPProxyConf) MarshalToMsg(pMsg *msg.NewProxy) {
 	pMsg.HTTPUser = cfg.HTTPUser
 	pMsg.HTTPPwd = cfg.HTTPPwd
 	pMsg.Headers = cfg.Headers
+	pMsg.RouteByHTTPUser = cfg.RouteByHTTPUser
 }
 
 func (cfg *HTTPProxyConf) CheckForCli() (err error) {

+ 3 - 0
pkg/config/server.go

@@ -62,6 +62,8 @@ type ServerCommonConf struct {
 	// requests on one single port. If it's not - it will listen on this value for
 	// HTTP CONNECT requests. By default, this value is 0.
 	TCPMuxHTTPConnectPort int `ini:"tcpmux_httpconnect_port" json:"tcpmux_httpconnect_port" validate:"gte=0,lte=65535"`
+	// If TCPMuxPassthrough is true, frps won't do any update on traffic.
+	TCPMuxPassthrough bool `ini:"tcpmux_passthrough" json:"tcpmux_passthrough"`
 	// VhostHTTPTimeout specifies the response header timeout for the Vhost
 	// HTTP server, in seconds. By default, this value is 60.
 	VhostHTTPTimeout int64 `ini:"vhost_http_timeout" json:"vhost_http_timeout"`
@@ -188,6 +190,7 @@ func GetDefaultServerConf() ServerCommonConf {
 		VhostHTTPPort:           0,
 		VhostHTTPSPort:          0,
 		TCPMuxHTTPConnectPort:   0,
+		TCPMuxPassthrough:       false,
 		VhostHTTPTimeout:        60,
 		DashboardAddr:           "0.0.0.0",
 		DashboardPort:           0,

+ 68 - 67
pkg/msg/msg.go

@@ -62,133 +62,134 @@ var (
 
 // When frpc start, client send this message to login to server.
 type Login struct {
-	Version      string            `json:"version"`
-	Hostname     string            `json:"hostname"`
-	Os           string            `json:"os"`
-	Arch         string            `json:"arch"`
-	User         string            `json:"user"`
-	PrivilegeKey string            `json:"privilege_key"`
-	Timestamp    int64             `json:"timestamp"`
-	RunID        string            `json:"run_id"`
-	Metas        map[string]string `json:"metas"`
+	Version      string            `json:"version,omitempty"`
+	Hostname     string            `json:"hostname,omitempty"`
+	Os           string            `json:"os,omitempty"`
+	Arch         string            `json:"arch,omitempty"`
+	User         string            `json:"user,omitempty"`
+	PrivilegeKey string            `json:"privilege_key,omitempty"`
+	Timestamp    int64             `json:"timestamp,omitempty"`
+	RunID        string            `json:"run_id,omitempty"`
+	Metas        map[string]string `json:"metas,omitempty"`
 
 	// Some global configures.
-	PoolCount int `json:"pool_count"`
+	PoolCount int `json:"pool_count,omitempty"`
 }
 
 type LoginResp struct {
-	Version       string `json:"version"`
-	RunID         string `json:"run_id"`
-	ServerUDPPort int    `json:"server_udp_port"`
-	Error         string `json:"error"`
+	Version       string `json:"version,omitempty"`
+	RunID         string `json:"run_id,omitempty"`
+	ServerUDPPort int    `json:"server_udp_port,omitempty"`
+	Error         string `json:"error,omitempty"`
 }
 
 // When frpc login success, send this message to frps for running a new proxy.
 type NewProxy struct {
-	ProxyName      string            `json:"proxy_name"`
-	ProxyType      string            `json:"proxy_type"`
-	UseEncryption  bool              `json:"use_encryption"`
-	UseCompression bool              `json:"use_compression"`
-	Group          string            `json:"group"`
-	GroupKey       string            `json:"group_key"`
-	Metas          map[string]string `json:"metas"`
+	ProxyName      string            `json:"proxy_name,omitempty"`
+	ProxyType      string            `json:"proxy_type,omitempty"`
+	UseEncryption  bool              `json:"use_encryption,omitempty"`
+	UseCompression bool              `json:"use_compression,omitempty"`
+	Group          string            `json:"group,omitempty"`
+	GroupKey       string            `json:"group_key,omitempty"`
+	Metas          map[string]string `json:"metas,omitempty"`
 
 	// tcp and udp only
-	RemotePort int `json:"remote_port"`
+	RemotePort int `json:"remote_port,omitempty"`
 
 	// http and https only
-	CustomDomains     []string          `json:"custom_domains"`
-	SubDomain         string            `json:"subdomain"`
-	Locations         []string          `json:"locations"`
-	HTTPUser          string            `json:"http_user"`
-	HTTPPwd           string            `json:"http_pwd"`
-	HostHeaderRewrite string            `json:"host_header_rewrite"`
-	Headers           map[string]string `json:"headers"`
+	CustomDomains     []string          `json:"custom_domains,omitempty"`
+	SubDomain         string            `json:"subdomain,omitempty"`
+	Locations         []string          `json:"locations,omitempty"`
+	HTTPUser          string            `json:"http_user,omitempty"`
+	HTTPPwd           string            `json:"http_pwd,omitempty"`
+	HostHeaderRewrite string            `json:"host_header_rewrite,omitempty"`
+	Headers           map[string]string `json:"headers,omitempty"`
+	RouteByHTTPUser   string            `json:"route_by_http_user,omitempty"`
 
 	// stcp
-	Sk string `json:"sk"`
+	Sk string `json:"sk,omitempty"`
 
 	// tcpmux
-	Multiplexer string `json:"multiplexer"`
+	Multiplexer string `json:"multiplexer,omitempty"`
 }
 
 type NewProxyResp struct {
-	ProxyName  string `json:"proxy_name"`
-	RemoteAddr string `json:"remote_addr"`
-	Error      string `json:"error"`
+	ProxyName  string `json:"proxy_name,omitempty"`
+	RemoteAddr string `json:"remote_addr,omitempty"`
+	Error      string `json:"error,omitempty"`
 }
 
 type CloseProxy struct {
-	ProxyName string `json:"proxy_name"`
+	ProxyName string `json:"proxy_name,omitempty"`
 }
 
 type NewWorkConn struct {
-	RunID        string `json:"run_id"`
-	PrivilegeKey string `json:"privilege_key"`
-	Timestamp    int64  `json:"timestamp"`
+	RunID        string `json:"run_id,omitempty"`
+	PrivilegeKey string `json:"privilege_key,omitempty"`
+	Timestamp    int64  `json:"timestamp,omitempty"`
 }
 
 type ReqWorkConn struct {
 }
 
 type StartWorkConn struct {
-	ProxyName string `json:"proxy_name"`
-	SrcAddr   string `json:"src_addr"`
-	DstAddr   string `json:"dst_addr"`
-	SrcPort   uint16 `json:"src_port"`
-	DstPort   uint16 `json:"dst_port"`
-	Error     string `json:"error"`
+	ProxyName string `json:"proxy_name,omitempty"`
+	SrcAddr   string `json:"src_addr,omitempty"`
+	DstAddr   string `json:"dst_addr,omitempty"`
+	SrcPort   uint16 `json:"src_port,omitempty"`
+	DstPort   uint16 `json:"dst_port,omitempty"`
+	Error     string `json:"error,omitempty"`
 }
 
 type NewVisitorConn struct {
-	ProxyName      string `json:"proxy_name"`
-	SignKey        string `json:"sign_key"`
-	Timestamp      int64  `json:"timestamp"`
-	UseEncryption  bool   `json:"use_encryption"`
-	UseCompression bool   `json:"use_compression"`
+	ProxyName      string `json:"proxy_name,omitempty"`
+	SignKey        string `json:"sign_key,omitempty"`
+	Timestamp      int64  `json:"timestamp,omitempty"`
+	UseEncryption  bool   `json:"use_encryption,omitempty"`
+	UseCompression bool   `json:"use_compression,omitempty"`
 }
 
 type NewVisitorConnResp struct {
-	ProxyName string `json:"proxy_name"`
-	Error     string `json:"error"`
+	ProxyName string `json:"proxy_name,omitempty"`
+	Error     string `json:"error,omitempty"`
 }
 
 type Ping struct {
-	PrivilegeKey string `json:"privilege_key"`
-	Timestamp    int64  `json:"timestamp"`
+	PrivilegeKey string `json:"privilege_key,omitempty"`
+	Timestamp    int64  `json:"timestamp,omitempty"`
 }
 
 type Pong struct {
-	Error string `json:"error"`
+	Error string `json:"error,omitempty"`
 }
 
 type UDPPacket struct {
-	Content    string       `json:"c"`
-	LocalAddr  *net.UDPAddr `json:"l"`
-	RemoteAddr *net.UDPAddr `json:"r"`
+	Content    string       `json:"c,omitempty"`
+	LocalAddr  *net.UDPAddr `json:"l,omitempty"`
+	RemoteAddr *net.UDPAddr `json:"r,omitempty"`
 }
 
 type NatHoleVisitor struct {
-	ProxyName string `json:"proxy_name"`
-	SignKey   string `json:"sign_key"`
-	Timestamp int64  `json:"timestamp"`
+	ProxyName string `json:"proxy_name,omitempty"`
+	SignKey   string `json:"sign_key,omitempty"`
+	Timestamp int64  `json:"timestamp,omitempty"`
 }
 
 type NatHoleClient struct {
-	ProxyName string `json:"proxy_name"`
-	Sid       string `json:"sid"`
+	ProxyName string `json:"proxy_name,omitempty"`
+	Sid       string `json:"sid,omitempty"`
 }
 
 type NatHoleResp struct {
-	Sid         string `json:"sid"`
-	VisitorAddr string `json:"visitor_addr"`
-	ClientAddr  string `json:"client_addr"`
-	Error       string `json:"error"`
+	Sid         string `json:"sid,omitempty"`
+	VisitorAddr string `json:"visitor_addr,omitempty"`
+	ClientAddr  string `json:"client_addr,omitempty"`
+	Error       string `json:"error,omitempty"`
 }
 
 type NatHoleClientDetectOK struct {
 }
 
 type NatHoleSid struct {
-	Sid string `json:"sid"`
+	Sid string `json:"sid,omitempty"`
 }

+ 34 - 8
pkg/util/tcpmux/httpconnect.go

@@ -24,18 +24,24 @@ import (
 
 	"github.com/fatedier/frp/pkg/util/util"
 	"github.com/fatedier/frp/pkg/util/vhost"
+	gnet "github.com/fatedier/golib/net"
 )
 
 type HTTPConnectTCPMuxer struct {
 	*vhost.Muxer
+
+	passthrough  bool
+	authRequired bool // Not supported until we really need this.
 }
 
-func NewHTTPConnectTCPMuxer(listener net.Listener, timeout time.Duration) (*HTTPConnectTCPMuxer, error) {
-	mux, err := vhost.NewMuxer(listener, getHostFromHTTPConnect, nil, sendHTTPOk, nil, timeout)
-	return &HTTPConnectTCPMuxer{mux}, err
+func NewHTTPConnectTCPMuxer(listener net.Listener, passthrough bool, timeout time.Duration) (*HTTPConnectTCPMuxer, error) {
+	ret := &HTTPConnectTCPMuxer{passthrough: passthrough, authRequired: false}
+	mux, err := vhost.NewMuxer(listener, ret.getHostFromHTTPConnect, nil, ret.sendConnectResponse, nil, timeout)
+	ret.Muxer = mux
+	return ret, err
 }
 
-func readHTTPConnectRequest(rd io.Reader) (host string, err error) {
+func (muxer *HTTPConnectTCPMuxer) readHTTPConnectRequest(rd io.Reader) (host string, httpUser string, err error) {
 	bufioReader := bufio.NewReader(rd)
 
 	req, err := http.ReadRequest(bufioReader)
@@ -49,20 +55,40 @@ func readHTTPConnectRequest(rd io.Reader) (host string, err error) {
 	}
 
 	host, _ = util.CanonicalHost(req.Host)
+	proxyAuth := req.Header.Get("Proxy-Authorization")
+	if proxyAuth != "" {
+		httpUser, _, _ = util.ParseBasicAuth(proxyAuth)
+	}
 	return
 }
 
-func sendHTTPOk(c net.Conn) error {
+func (muxer *HTTPConnectTCPMuxer) sendConnectResponse(c net.Conn, reqInfo map[string]string) error {
+	if muxer.passthrough {
+		return nil
+	}
 	return util.OkResponse().Write(c)
 }
 
-func getHostFromHTTPConnect(c net.Conn) (_ net.Conn, _ map[string]string, err error) {
+func (muxer *HTTPConnectTCPMuxer) getHostFromHTTPConnect(c net.Conn) (net.Conn, map[string]string, error) {
 	reqInfoMap := make(map[string]string, 0)
-	host, err := readHTTPConnectRequest(c)
+	sc, rd := gnet.NewSharedConn(c)
+
+	host, httpUser, err := muxer.readHTTPConnectRequest(rd)
 	if err != nil {
 		return nil, reqInfoMap, err
 	}
+
 	reqInfoMap["Host"] = host
 	reqInfoMap["Scheme"] = "tcp"
-	return c, reqInfoMap, nil
+	reqInfoMap["HTTPUser"] = httpUser
+
+	var outConn net.Conn = c
+	if muxer.passthrough {
+		outConn = sc
+		if muxer.authRequired && httpUser == "" {
+			util.ProxyUnauthorizedResponse().Write(c)
+			outConn = c
+		}
+	}
+	return outConn, reqInfoMap, nil
 }

+ 33 - 0
pkg/util/util/http.go

@@ -15,6 +15,7 @@
 package util
 
 import (
+	"encoding/base64"
 	"net"
 	"net/http"
 	"strings"
@@ -34,6 +35,20 @@ func OkResponse() *http.Response {
 	return res
 }
 
+func ProxyUnauthorizedResponse() *http.Response {
+	header := make(http.Header)
+	header.Set("Proxy-Authenticate", `Basic realm="Restricted"`)
+	res := &http.Response{
+		Status:     "Proxy Authentication Required",
+		StatusCode: 407,
+		Proto:      "HTTP/1.1",
+		ProtoMajor: 1,
+		ProtoMinor: 1,
+		Header:     header,
+	}
+	return res
+}
+
 // canonicalHost strips port from host if present and returns the canonicalized
 // host name.
 func CanonicalHost(host string) (string, error) {
@@ -64,3 +79,21 @@ func hasPort(host string) bool {
 	}
 	return host[0] == '[' && strings.Contains(host, "]:")
 }
+
+func ParseBasicAuth(auth string) (username, password string, ok bool) {
+	const prefix = "Basic "
+	// Case insensitive prefix match. See Issue 22736.
+	if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) {
+		return
+	}
+	c, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
+	if err != nil {
+		return
+	}
+	cs := string(c)
+	s := strings.IndexByte(cs, ':')
+	if s < 0 {
+		return
+	}
+	return cs[:s], cs[s+1:], true
+}

+ 131 - 32
pkg/util/vhost/http.go

@@ -23,17 +23,19 @@ import (
 	"log"
 	"net"
 	"net/http"
+	"net/url"
 	"strings"
 	"time"
 
 	frpLog "github.com/fatedier/frp/pkg/util/log"
 	"github.com/fatedier/frp/pkg/util/util"
+	frpIo "github.com/fatedier/golib/io"
 
 	"github.com/fatedier/golib/pool"
 )
 
 var (
-	ErrNoDomain = errors.New("no such domain")
+	ErrNoRouteFound = errors.New("no route found")
 )
 
 type HTTPReverseProxyOptions struct {
@@ -56,17 +58,22 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
 		vhostRouter:           vhostRouter,
 	}
 	proxy := &ReverseProxy{
+		// Modify incoming requests by route policies.
 		Director: func(req *http.Request) {
 			req.URL.Scheme = "http"
 			url := req.Context().Value(RouteInfoURL).(string)
+			routeByHTTPUser := req.Context().Value(RouteInfoHTTPUser).(string)
 			oldHost, _ := util.CanonicalHost(req.Context().Value(RouteInfoHost).(string))
-			rc := rp.GetRouteConfig(oldHost, url)
+			rc := rp.GetRouteConfig(oldHost, url, routeByHTTPUser)
 			if rc != nil {
 				if rc.RewriteHost != "" {
 					req.Host = rc.RewriteHost
 				}
-				// Set {domain}.{location} as URL host here to let http transport reuse connections.
-				req.URL.Host = rc.Domain + "." + base64.StdEncoding.EncodeToString([]byte(rc.Location))
+				// Set {domain}.{location}.{routeByHTTPUser} as URL host here to let http transport reuse connections.
+				// TODO(fatedier): use proxy name instead?
+				req.URL.Host = rc.Domain + "." +
+					base64.StdEncoding.EncodeToString([]byte(rc.Location)) + "." +
+					base64.StdEncoding.EncodeToString([]byte(rc.RouteByHTTPUser))
 
 				for k, v := range rc.Headers {
 					req.Header.Set(k, v)
@@ -76,14 +83,30 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
 			}
 
 		},
+		// Create a connection to one proxy routed by route policy.
 		Transport: &http.Transport{
 			ResponseHeaderTimeout: rp.responseHeaderTimeout,
 			IdleConnTimeout:       60 * time.Second,
 			DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
 				url := ctx.Value(RouteInfoURL).(string)
 				host, _ := util.CanonicalHost(ctx.Value(RouteInfoHost).(string))
+				routerByHTTPUser := ctx.Value(RouteInfoHTTPUser).(string)
 				remote := ctx.Value(RouteInfoRemote).(string)
-				return rp.CreateConnection(host, url, remote)
+				return rp.CreateConnection(host, url, routerByHTTPUser, remote)
+			},
+			Proxy: func(req *http.Request) (*url.URL, error) {
+				// Use proxy mode if there is host in HTTP first request line.
+				// GET http://example.com/ HTTP/1.1
+				// Host: example.com
+				//
+				// Normal:
+				// GET / HTTP/1.1
+				// Host: example.com
+				urlHost := req.Context().Value(RouteInfoURLHost).(string)
+				if urlHost != "" {
+					return req.URL, nil
+				}
+				return nil, nil
 			},
 		},
 		BufferPool: newWrapPool(),
@@ -101,7 +124,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
 // Register register the route config to reverse proxy
 // reverse proxy will use CreateConnFn from routeCfg to create a connection to the remote service
 func (rp *HTTPReverseProxy) Register(routeCfg RouteConfig) error {
-	err := rp.vhostRouter.Add(routeCfg.Domain, routeCfg.Location, &routeCfg)
+	err := rp.vhostRouter.Add(routeCfg.Domain, routeCfg.Location, routeCfg.RouteByHTTPUser, &routeCfg)
 	if err != nil {
 		return err
 	}
@@ -109,28 +132,29 @@ func (rp *HTTPReverseProxy) Register(routeCfg RouteConfig) error {
 }
 
 // UnRegister unregister route config by domain and location
-func (rp *HTTPReverseProxy) UnRegister(domain string, location string) {
-	rp.vhostRouter.Del(domain, location)
+func (rp *HTTPReverseProxy) UnRegister(routeCfg RouteConfig) {
+	rp.vhostRouter.Del(routeCfg.Domain, routeCfg.Location, routeCfg.RouteByHTTPUser)
 }
 
-func (rp *HTTPReverseProxy) GetRouteConfig(domain string, location string) *RouteConfig {
-	vr, ok := rp.getVhost(domain, location)
+func (rp *HTTPReverseProxy) GetRouteConfig(domain, location, routeByHTTPUser string) *RouteConfig {
+	vr, ok := rp.getVhost(domain, location, routeByHTTPUser)
 	if ok {
+		frpLog.Debug("get new HTTP request host [%s] path [%s] httpuser [%s]", domain, location, routeByHTTPUser)
 		return vr.payload.(*RouteConfig)
 	}
 	return nil
 }
 
-func (rp *HTTPReverseProxy) GetRealHost(domain string, location string) (host string) {
-	vr, ok := rp.getVhost(domain, location)
+func (rp *HTTPReverseProxy) GetRealHost(domain, location, routeByHTTPUser string) (host string) {
+	vr, ok := rp.getVhost(domain, location, routeByHTTPUser)
 	if ok {
 		host = vr.payload.(*RouteConfig).RewriteHost
 	}
 	return
 }
 
-func (rp *HTTPReverseProxy) GetHeaders(domain string, location string) (headers map[string]string) {
-	vr, ok := rp.getVhost(domain, location)
+func (rp *HTTPReverseProxy) GetHeaders(domain, location, routeByHTTPUser string) (headers map[string]string) {
+	vr, ok := rp.getVhost(domain, location, routeByHTTPUser)
 	if ok {
 		headers = vr.payload.(*RouteConfig).Headers
 	}
@@ -138,19 +162,19 @@ func (rp *HTTPReverseProxy) GetHeaders(domain string, location string) (headers
 }
 
 // CreateConnection create a new connection by route config
-func (rp *HTTPReverseProxy) CreateConnection(domain string, location string, remoteAddr string) (net.Conn, error) {
-	vr, ok := rp.getVhost(domain, location)
+func (rp *HTTPReverseProxy) CreateConnection(domain, location, routeByHTTPUser string, remoteAddr string) (net.Conn, error) {
+	vr, ok := rp.getVhost(domain, location, routeByHTTPUser)
 	if ok {
 		fn := vr.payload.(*RouteConfig).CreateConnFn
 		if fn != nil {
 			return fn(remoteAddr)
 		}
 	}
-	return nil, fmt.Errorf("%v: %s %s", ErrNoDomain, domain, location)
+	return nil, fmt.Errorf("%v: %s %s %s", ErrNoRouteFound, domain, location, routeByHTTPUser)
 }
 
-func (rp *HTTPReverseProxy) CheckAuth(domain, location, user, passwd string) bool {
-	vr, ok := rp.getVhost(domain, location)
+func (rp *HTTPReverseProxy) CheckAuth(domain, location, routeByHTTPUser, user, passwd string) bool {
+	vr, ok := rp.getVhost(domain, location, routeByHTTPUser)
 	if ok {
 		checkUser := vr.payload.(*RouteConfig).Username
 		checkPasswd := vr.payload.(*RouteConfig).Password
@@ -161,45 +185,120 @@ func (rp *HTTPReverseProxy) CheckAuth(domain, location, user, passwd string) boo
 	return true
 }
 
-// getVhost get vhost router by domain and location
-func (rp *HTTPReverseProxy) getVhost(domain string, location string) (vr *Router, ok bool) {
-	// first we check the full hostname
+// getVhost trys to get vhost router by route policy.
+func (rp *HTTPReverseProxy) getVhost(domain, location, routeByHTTPUser string) (*Router, bool) {
+	findRouter := func(inDomain, inLocation, inRouteByHTTPUser string) (*Router, bool) {
+		vr, ok := rp.vhostRouter.Get(inDomain, inLocation, inRouteByHTTPUser)
+		if ok {
+			return vr, ok
+		}
+		// Try to check if there is one proxy that doesn't specify routerByHTTPUser, it means match all.
+		vr, ok = rp.vhostRouter.Get(inDomain, inLocation, "")
+		if ok {
+			return vr, ok
+		}
+		return nil, false
+	}
+
+	// First we check the full hostname
 	// if not exist, then check the wildcard_domain such as *.example.com
-	vr, ok = rp.vhostRouter.Get(domain, location)
+	vr, ok := findRouter(domain, location, routeByHTTPUser)
 	if ok {
-		return
+		return vr, ok
 	}
 
+	// e.g. domain = test.example.com, try to match wildcard domains.
+	// *.example.com
+	// *.com
 	domainSplit := strings.Split(domain, ".")
-	if len(domainSplit) < 3 {
-		return nil, false
-	}
-
 	for {
 		if len(domainSplit) < 3 {
-			return nil, false
+			break
 		}
 
 		domainSplit[0] = "*"
 		domain = strings.Join(domainSplit, ".")
-		vr, ok = rp.vhostRouter.Get(domain, location)
+		vr, ok = findRouter(domain, location, routeByHTTPUser)
 		if ok {
 			return vr, true
 		}
 		domainSplit = domainSplit[1:]
 	}
+
+	// Finally, try to check if there is one proxy that domain is "*" means match all domains.
+	vr, ok = findRouter("*", location, routeByHTTPUser)
+	if ok {
+		return vr, true
+	}
+	return nil, false
+}
+
+func (rp *HTTPReverseProxy) connectHandler(rw http.ResponseWriter, req *http.Request) {
+	hj, ok := rw.(http.Hijacker)
+	if !ok {
+		rw.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+
+	client, _, err := hj.Hijack()
+	if err != nil {
+		rw.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+
+	url := req.Context().Value(RouteInfoURL).(string)
+	routeByHTTPUser := req.Context().Value(RouteInfoHTTPUser).(string)
+	domain, _ := util.CanonicalHost(req.Context().Value(RouteInfoHost).(string))
+	remoteAddr := req.Context().Value(RouteInfoRemote).(string)
+
+	remote, err := rp.CreateConnection(domain, url, routeByHTTPUser, remoteAddr)
+	if err != nil {
+		http.Error(rw, "Failed", http.StatusBadRequest)
+		client.Close()
+		return
+	}
+	req.Write(remote)
+	go frpIo.Join(remote, client)
+}
+
+func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Request {
+	newctx := req.Context()
+	newctx = context.WithValue(newctx, RouteInfoURL, req.URL.Path)
+	newctx = context.WithValue(newctx, RouteInfoHost, req.Host)
+	newctx = context.WithValue(newctx, RouteInfoURLHost, req.URL.Host)
+
+	user := ""
+	// If url host isn't empty, it's a proxy request. Get http user from Proxy-Authorization header.
+	if req.URL.Host != "" {
+		proxyAuth := req.Header.Get("Proxy-Authorization")
+		if proxyAuth != "" {
+			user, _, _ = parseBasicAuth(proxyAuth)
+		}
+	}
+	if user == "" {
+		user, _, _ = req.BasicAuth()
+	}
+	newctx = context.WithValue(newctx, RouteInfoHTTPUser, user)
+	newctx = context.WithValue(newctx, RouteInfoRemote, req.RemoteAddr)
+	return req.Clone(newctx)
 }
 
 func (rp *HTTPReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	domain, _ := util.CanonicalHost(req.Host)
 	location := req.URL.Path
 	user, passwd, _ := req.BasicAuth()
-	if !rp.CheckAuth(domain, location, user, passwd) {
+	if !rp.CheckAuth(domain, location, user, user, passwd) {
 		rw.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
 		http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
 		return
 	}
-	rp.proxy.ServeHTTP(rw, req)
+
+	newreq := rp.injectRequestInfoToCtx(req)
+	if req.Method == http.MethodConnect {
+		rp.connectHandler(rw, newreq)
+	} else {
+		rp.proxy.ServeHTTP(rw, newreq)
+	}
 }
 
 type wrapPool struct{}

+ 19 - 7
pkg/util/vhost/reverseproxy.go

@@ -8,6 +8,7 @@ package vhost
 
 import (
 	"context"
+	"encoding/base64"
 	"fmt"
 	"io"
 	"log"
@@ -209,6 +210,24 @@ func (p *ReverseProxy) modifyResponse(rw http.ResponseWriter, res *http.Response
 	return true
 }
 
+func parseBasicAuth(auth string) (username, password string, ok bool) {
+	const prefix = "Basic "
+	// Case insensitive prefix match. See Issue 22736.
+	if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) {
+		return
+	}
+	c, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
+	if err != nil {
+		return
+	}
+	cs := string(c)
+	s := strings.IndexByte(cs, ':')
+	if s < 0 {
+		return
+	}
+	return cs[:s], cs[s+1:], true
+}
+
 func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	transport := p.Transport
 	if transport == nil {
@@ -238,13 +257,6 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 		outreq.Header = make(http.Header) // Issue 33142: historical behavior was to always allocate
 	}
 
-	// =============================
-	// Modified for frp
-	outreq = outreq.Clone(context.WithValue(outreq.Context(), RouteInfoURL, req.URL.Path))
-	outreq = outreq.Clone(context.WithValue(outreq.Context(), RouteInfoHost, req.Host))
-	outreq = outreq.Clone(context.WithValue(outreq.Context(), RouteInfoRemote, req.RemoteAddr))
-	// =============================
-
 	p.Director(outreq)
 	outreq.Close = false
 

+ 43 - 21
pkg/util/vhost/router.go

@@ -11,33 +11,42 @@ var (
 	ErrRouterConfigConflict = errors.New("router config conflict")
 )
 
+type routerByHTTPUser map[string][]*Router
+
 type Routers struct {
-	RouterByDomain map[string][]*Router
-	mutex          sync.RWMutex
+	indexByDomain map[string]routerByHTTPUser
+
+	mutex sync.RWMutex
 }
 
 type Router struct {
 	domain   string
 	location string
+	httpUser string
 
+	// store any object here
 	payload interface{}
 }
 
 func NewRouters() *Routers {
 	return &Routers{
-		RouterByDomain: make(map[string][]*Router),
+		indexByDomain: make(map[string]routerByHTTPUser),
 	}
 }
 
-func (r *Routers) Add(domain, location string, payload interface{}) error {
+func (r *Routers) Add(domain, location, httpUser string, payload interface{}) error {
 	r.mutex.Lock()
 	defer r.mutex.Unlock()
 
-	if _, exist := r.exist(domain, location); exist {
+	if _, exist := r.exist(domain, location, httpUser); exist {
 		return ErrRouterConfigConflict
 	}
 
-	vrs, found := r.RouterByDomain[domain]
+	routersByHTTPUser, found := r.indexByDomain[domain]
+	if !found {
+		routersByHTTPUser = make(map[string][]*Router)
+	}
+	vrs, found := routersByHTTPUser[httpUser]
 	if !found {
 		vrs = make([]*Router, 0, 1)
 	}
@@ -45,20 +54,27 @@ func (r *Routers) Add(domain, location string, payload interface{}) error {
 	vr := &Router{
 		domain:   domain,
 		location: location,
+		httpUser: httpUser,
 		payload:  payload,
 	}
 	vrs = append(vrs, vr)
-
 	sort.Sort(sort.Reverse(ByLocation(vrs)))
-	r.RouterByDomain[domain] = vrs
+
+	routersByHTTPUser[httpUser] = vrs
+	r.indexByDomain[domain] = routersByHTTPUser
 	return nil
 }
 
-func (r *Routers) Del(domain, location string) {
+func (r *Routers) Del(domain, location, httpUser string) {
 	r.mutex.Lock()
 	defer r.mutex.Unlock()
 
-	vrs, found := r.RouterByDomain[domain]
+	routersByHTTPUser, found := r.indexByDomain[domain]
+	if !found {
+		return
+	}
+
+	vrs, found := routersByHTTPUser[httpUser]
 	if !found {
 		return
 	}
@@ -68,40 +84,46 @@ func (r *Routers) Del(domain, location string) {
 			newVrs = append(newVrs, vr)
 		}
 	}
-	r.RouterByDomain[domain] = newVrs
+	routersByHTTPUser[httpUser] = newVrs
 }
 
-func (r *Routers) Get(host, path string) (vr *Router, exist bool) {
+func (r *Routers) Get(host, path, httpUser string) (vr *Router, exist bool) {
 	r.mutex.RLock()
 	defer r.mutex.RUnlock()
 
-	vrs, found := r.RouterByDomain[host]
+	routersByHTTPUser, found := r.indexByDomain[host]
+	if !found {
+		return
+	}
+
+	vrs, found := routersByHTTPUser[httpUser]
 	if !found {
 		return
 	}
 
-	// can't support load balance, will to do
 	for _, vr = range vrs {
 		if strings.HasPrefix(path, vr.location) {
 			return vr, true
 		}
 	}
-
 	return
 }
 
-func (r *Routers) exist(host, path string) (vr *Router, exist bool) {
-	vrs, found := r.RouterByDomain[host]
+func (r *Routers) exist(host, path, httpUser string) (route *Router, exist bool) {
+	routersByHTTPUser, found := r.indexByDomain[host]
+	if !found {
+		return
+	}
+	routers, found := routersByHTTPUser[httpUser]
 	if !found {
 		return
 	}
 
-	for _, vr = range vrs {
-		if path == vr.location {
-			return vr, true
+	for _, route = range routers {
+		if path == route.location {
+			return route, true
 		}
 	}
-
 	return
 }
 

+ 78 - 47
pkg/util/vhost/vhost.go

@@ -29,16 +29,19 @@ import (
 type RouteInfo string
 
 const (
-	RouteInfoURL    RouteInfo = "url"
-	RouteInfoHost   RouteInfo = "host"
-	RouteInfoRemote RouteInfo = "remote"
+	RouteInfoURL      RouteInfo = "url"
+	RouteInfoHost     RouteInfo = "host"
+	RouteInfoHTTPUser RouteInfo = "httpUser"
+	RouteInfoRemote   RouteInfo = "remote"
+	RouteInfoURLHost  RouteInfo = "urlHost"
 )
 
 type muxFunc func(net.Conn) (net.Conn, map[string]string, error)
 type httpAuthFunc func(net.Conn, string, string, string) (bool, error)
 type hostRewriteFunc func(net.Conn, string) (net.Conn, error)
-type successFunc func(net.Conn) error
+type successFunc func(net.Conn, map[string]string) error
 
+// Muxer is only used for https and tcpmux proxy.
 type Muxer struct {
 	listener       net.Listener
 	timeout        time.Duration
@@ -49,7 +52,15 @@ type Muxer struct {
 	registryRouter *Routers
 }
 
-func NewMuxer(listener net.Listener, vhostFunc muxFunc, authFunc httpAuthFunc, successFunc successFunc, rewriteFunc hostRewriteFunc, timeout time.Duration) (mux *Muxer, err error) {
+func NewMuxer(
+	listener net.Listener,
+	vhostFunc muxFunc,
+	authFunc httpAuthFunc,
+	successFunc successFunc,
+	rewriteFunc hostRewriteFunc,
+	timeout time.Duration,
+) (mux *Muxer, err error) {
+
 	mux = &Muxer{
 		listener:       listener,
 		timeout:        timeout,
@@ -67,12 +78,13 @@ type CreateConnFunc func(remoteAddr string) (net.Conn, error)
 
 // RouteConfig is the params used to match HTTP requests
 type RouteConfig struct {
-	Domain      string
-	Location    string
-	RewriteHost string
-	Username    string
-	Password    string
-	Headers     map[string]string
+	Domain          string
+	Location        string
+	RewriteHost     string
+	Username        string
+	Password        string
+	Headers         map[string]string
+	RouteByHTTPUser string
 
 	CreateConnFn CreateConnFunc
 }
@@ -81,49 +93,66 @@ type RouteConfig struct {
 // then rewrite the host header to rewriteHost
 func (v *Muxer) Listen(ctx context.Context, cfg *RouteConfig) (l *Listener, err error) {
 	l = &Listener{
-		name:        cfg.Domain,
-		location:    cfg.Location,
-		rewriteHost: cfg.RewriteHost,
-		userName:    cfg.Username,
-		passWord:    cfg.Password,
-		mux:         v,
-		accept:      make(chan net.Conn),
-		ctx:         ctx,
-	}
-	err = v.registryRouter.Add(cfg.Domain, cfg.Location, l)
+		name:            cfg.Domain,
+		location:        cfg.Location,
+		routeByHTTPUser: cfg.RouteByHTTPUser,
+		rewriteHost:     cfg.RewriteHost,
+		userName:        cfg.Username,
+		passWord:        cfg.Password,
+		mux:             v,
+		accept:          make(chan net.Conn),
+		ctx:             ctx,
+	}
+	err = v.registryRouter.Add(cfg.Domain, cfg.Location, cfg.RouteByHTTPUser, l)
 	if err != nil {
 		return
 	}
 	return l, nil
 }
 
-func (v *Muxer) getListener(name, path string) (l *Listener, exist bool) {
+func (v *Muxer) getListener(name, path, httpUser string) (*Listener, bool) {
+
+	findRouter := func(inName, inPath, inHTTPUser string) (*Listener, bool) {
+		vr, ok := v.registryRouter.Get(inName, inPath, httpUser)
+		if ok {
+			return vr.payload.(*Listener), true
+		}
+		// Try to check if there is one proxy that doesn't specify routerByHTTPUser, it means match all.
+		vr, ok = v.registryRouter.Get(inName, inPath, "")
+		if ok {
+			return vr.payload.(*Listener), true
+		}
+		return nil, false
+	}
+
 	// first we check the full hostname
 	// if not exist, then check the wildcard_domain such as *.example.com
-	vr, found := v.registryRouter.Get(name, path)
-	if found {
-		return vr.payload.(*Listener), true
+	l, ok := findRouter(name, path, httpUser)
+	if ok {
+		return l, true
 	}
 
 	domainSplit := strings.Split(name, ".")
-	if len(domainSplit) < 3 {
-		return
-	}
-
 	for {
 		if len(domainSplit) < 3 {
-			return
+			break
 		}
 
 		domainSplit[0] = "*"
 		name = strings.Join(domainSplit, ".")
 
-		vr, found = v.registryRouter.Get(name, path)
-		if found {
-			return vr.payload.(*Listener), true
+		l, ok = findRouter(name, path, httpUser)
+		if ok {
+			return l, true
 		}
 		domainSplit = domainSplit[1:]
 	}
+	// Finally, try to check if there is one proxy that domain is "*" means match all domains.
+	l, ok = findRouter("*", path, httpUser)
+	if ok {
+		return l, true
+	}
+	return nil, false
 }
 
 func (v *Muxer) run() {
@@ -151,25 +180,26 @@ func (v *Muxer) handle(c net.Conn) {
 
 	name := strings.ToLower(reqInfoMap["Host"])
 	path := strings.ToLower(reqInfoMap["Path"])
-	l, ok := v.getListener(name, path)
+	httpUser := reqInfoMap["HTTPUser"]
+	l, ok := v.getListener(name, path, httpUser)
 	if !ok {
 		res := notFoundResponse()
 		res.Write(c)
-		log.Debug("http request for host [%s] path [%s] not found", name, path)
+		log.Debug("http request for host [%s] path [%s] httpUser [%s] not found", name, path, httpUser)
 		c.Close()
 		return
 	}
 
 	xl := xlog.FromContextSafe(l.ctx)
 	if v.successFunc != nil {
-		if err := v.successFunc(c); err != nil {
+		if err := v.successFunc(c, reqInfoMap); err != nil {
 			xl.Info("success func failure on vhost connection: %v", err)
 			c.Close()
 			return
 		}
 	}
 
-	// if authFunc is exist and userName/password is set
+	// if authFunc is exist and username/password is set
 	// then verify user access
 	if l.mux.authFunc != nil && l.userName != "" && l.passWord != "" {
 		bAccess, err := l.mux.authFunc(c, l.userName, l.passWord, reqInfoMap["Authorization"])
@@ -188,7 +218,7 @@ func (v *Muxer) handle(c net.Conn) {
 	}
 	c = sConn
 
-	xl.Debug("get new http request host [%s] path [%s]", name, path)
+	xl.Debug("new request host [%s] path [%s] httpUser [%s]", name, path, httpUser)
 	err = errors.PanicToError(func() {
 		l.accept <- c
 	})
@@ -198,14 +228,15 @@ func (v *Muxer) handle(c net.Conn) {
 }
 
 type Listener struct {
-	name        string
-	location    string
-	rewriteHost string
-	userName    string
-	passWord    string
-	mux         *Muxer // for closing Muxer
-	accept      chan net.Conn
-	ctx         context.Context
+	name            string
+	location        string
+	routeByHTTPUser string
+	rewriteHost     string
+	userName        string
+	passWord        string
+	mux             *Muxer // for closing Muxer
+	accept          chan net.Conn
+	ctx             context.Context
 }
 
 func (l *Listener) Accept() (net.Conn, error) {
@@ -231,7 +262,7 @@ func (l *Listener) Accept() (net.Conn, error) {
 }
 
 func (l *Listener) Close() error {
-	l.mux.registryRouter.Del(l.name, l.location)
+	l.mux.registryRouter.Del(l.name, l.location, l.routeByHTTPUser)
 	close(l.accept)
 	return nil
 }

+ 2 - 2
server/control.go

@@ -458,11 +458,11 @@ func (ctl *Control) manager() {
 					ProxyName: m.ProxyName,
 				}
 				if err != nil {
-					xl.Warn("new proxy [%s] error: %v", m.ProxyName, err)
+					xl.Warn("new proxy [%s] type [%s] error: %v", m.ProxyName, m.ProxyType, err)
 					resp.Error = util.GenerateResponseErrorString(fmt.Sprintf("new proxy [%s] error", m.ProxyName), err, ctl.serverCfg.DetailedErrorsToClient)
 				} else {
 					resp.RemoteAddr = remoteAddr
-					xl.Info("new proxy [%s] success", m.ProxyName)
+					xl.Info("new proxy [%s] type [%s] success", m.ProxyName, m.ProxyType)
 					metrics.Server.NewProxy(m.ProxyName, m.ProxyType)
 				}
 				ctl.sendCh <- resp

+ 28 - 19
server/group/http.go

@@ -10,8 +10,11 @@ import (
 )
 
 type HTTPGroupController struct {
+	// groups by indexKey
 	groups map[string]*HTTPGroup
 
+	// register createConn for each group to vhostRouter.
+	// createConn will get a connection from one proxy of the group
 	vhostRouter *vhost.Routers
 
 	mu sync.Mutex
@@ -24,10 +27,12 @@ func NewHTTPGroupController(vhostRouter *vhost.Routers) *HTTPGroupController {
 	}
 }
 
-func (ctl *HTTPGroupController) Register(proxyName, group, groupKey string,
-	routeConfig vhost.RouteConfig) (err error) {
+func (ctl *HTTPGroupController) Register(
+	proxyName, group, groupKey string,
+	routeConfig vhost.RouteConfig,
+) (err error) {
 
-	indexKey := httpGroupIndex(group, routeConfig.Domain, routeConfig.Location)
+	indexKey := group
 	ctl.mu.Lock()
 	g, ok := ctl.groups[indexKey]
 	if !ok {
@@ -39,8 +44,8 @@ func (ctl *HTTPGroupController) Register(proxyName, group, groupKey string,
 	return g.Register(proxyName, group, groupKey, routeConfig)
 }
 
-func (ctl *HTTPGroupController) UnRegister(proxyName, group, domain, location string) {
-	indexKey := httpGroupIndex(group, domain, location)
+func (ctl *HTTPGroupController) UnRegister(proxyName, group string, routeConfig vhost.RouteConfig) {
+	indexKey := group
 	ctl.mu.Lock()
 	defer ctl.mu.Unlock()
 	g, ok := ctl.groups[indexKey]
@@ -55,11 +60,13 @@ func (ctl *HTTPGroupController) UnRegister(proxyName, group, domain, location st
 }
 
 type HTTPGroup struct {
-	group    string
-	groupKey string
-	domain   string
-	location string
+	group           string
+	groupKey        string
+	domain          string
+	location        string
+	routeByHTTPUser string
 
+	// CreateConnFuncs indexed by echo proxy name
 	createFuncs map[string]vhost.CreateConnFunc
 	pxyNames    []string
 	index       uint64
@@ -75,8 +82,10 @@ func NewHTTPGroup(ctl *HTTPGroupController) *HTTPGroup {
 	}
 }
 
-func (g *HTTPGroup) Register(proxyName, group, groupKey string,
-	routeConfig vhost.RouteConfig) (err error) {
+func (g *HTTPGroup) Register(
+	proxyName, group, groupKey string,
+	routeConfig vhost.RouteConfig,
+) (err error) {
 
 	g.mu.Lock()
 	defer g.mu.Unlock()
@@ -84,7 +93,7 @@ func (g *HTTPGroup) Register(proxyName, group, groupKey string,
 		// the first proxy in this group
 		tmp := routeConfig // copy object
 		tmp.CreateConnFn = g.createConn
-		err = g.ctl.vhostRouter.Add(routeConfig.Domain, routeConfig.Location, &tmp)
+		err = g.ctl.vhostRouter.Add(routeConfig.Domain, routeConfig.Location, routeConfig.RouteByHTTPUser, &tmp)
 		if err != nil {
 			return
 		}
@@ -93,8 +102,10 @@ func (g *HTTPGroup) Register(proxyName, group, groupKey string,
 		g.groupKey = groupKey
 		g.domain = routeConfig.Domain
 		g.location = routeConfig.Location
+		g.routeByHTTPUser = routeConfig.RouteByHTTPUser
 	} else {
-		if g.group != group || g.domain != routeConfig.Domain || g.location != routeConfig.Location {
+		if g.group != group || g.domain != routeConfig.Domain ||
+			g.location != routeConfig.Location || g.routeByHTTPUser != routeConfig.RouteByHTTPUser {
 			err = ErrGroupParamsInvalid
 			return
 		}
@@ -125,7 +136,7 @@ func (g *HTTPGroup) UnRegister(proxyName string) (isEmpty bool) {
 
 	if len(g.createFuncs) == 0 {
 		isEmpty = true
-		g.ctl.vhostRouter.Del(g.domain, g.location)
+		g.ctl.vhostRouter.Del(g.domain, g.location, g.routeByHTTPUser)
 	}
 	return
 }
@@ -138,6 +149,7 @@ func (g *HTTPGroup) createConn(remoteAddr string) (net.Conn, error) {
 	group := g.group
 	domain := g.domain
 	location := g.location
+	routeByHTTPUser := g.routeByHTTPUser
 	if len(g.pxyNames) > 0 {
 		name := g.pxyNames[int(newIndex)%len(g.pxyNames)]
 		f, _ = g.createFuncs[name]
@@ -145,12 +157,9 @@ func (g *HTTPGroup) createConn(remoteAddr string) (net.Conn, error) {
 	g.mu.RUnlock()
 
 	if f == nil {
-		return nil, fmt.Errorf("no CreateConnFunc for http group [%s], domain [%s], location [%s]", group, domain, location)
+		return nil, fmt.Errorf("no CreateConnFunc for http group [%s], domain [%s], location [%s], routeByHTTPUser [%s]",
+			group, domain, location, routeByHTTPUser)
 	}
 
 	return f(remoteAddr)
 }
-
-func httpGroupIndex(group, domain, location string) string {
-	return fmt.Sprintf("%s_%s_%s", group, domain, location)
-}

+ 21 - 14
server/group/tcpmux.go

@@ -46,8 +46,11 @@ func NewTCPMuxGroupCtl(tcpMuxHTTPConnectMuxer *tcpmux.HTTPConnectTCPMuxer) *TCPM
 
 // Listen is the wrapper for TCPMuxGroup's Listen
 // If there are no group, we will create one here
-func (tmgc *TCPMuxGroupCtl) Listen(ctx context.Context, multiplexer string, group string, groupKey string,
-	domain string) (l net.Listener, err error) {
+func (tmgc *TCPMuxGroupCtl) Listen(
+	ctx context.Context,
+	multiplexer, group, groupKey string,
+	routeConfig vhost.RouteConfig,
+) (l net.Listener, err error) {
 
 	tmgc.mu.Lock()
 	tcpMuxGroup, ok := tmgc.groups[group]
@@ -59,7 +62,7 @@ func (tmgc *TCPMuxGroupCtl) Listen(ctx context.Context, multiplexer string, grou
 
 	switch multiplexer {
 	case consts.HTTPConnectTCPMultiplexer:
-		return tcpMuxGroup.HTTPConnectListen(ctx, group, groupKey, domain)
+		return tcpMuxGroup.HTTPConnectListen(ctx, group, groupKey, routeConfig)
 	default:
 		err = fmt.Errorf("unknown multiplexer [%s]", multiplexer)
 		return
@@ -75,9 +78,10 @@ func (tmgc *TCPMuxGroupCtl) RemoveGroup(group string) {
 
 // TCPMuxGroup route connections to different proxies
 type TCPMuxGroup struct {
-	group    string
-	groupKey string
-	domain   string
+	group           string
+	groupKey        string
+	domain          string
+	routeByHTTPUser string
 
 	acceptCh chan net.Conn
 	index    uint64
@@ -99,15 +103,17 @@ func NewTCPMuxGroup(ctl *TCPMuxGroupCtl) *TCPMuxGroup {
 // Listen will return a new TCPMuxGroupListener
 // if TCPMuxGroup already has a listener, just add a new TCPMuxGroupListener to the queues
 // otherwise, listen on the real address
-func (tmg *TCPMuxGroup) HTTPConnectListen(ctx context.Context, group string, groupKey string, domain string) (ln *TCPMuxGroupListener, err error) {
+func (tmg *TCPMuxGroup) HTTPConnectListen(
+	ctx context.Context,
+	group, groupKey string,
+	routeConfig vhost.RouteConfig,
+) (ln *TCPMuxGroupListener, err error) {
+
 	tmg.mu.Lock()
 	defer tmg.mu.Unlock()
 	if len(tmg.lns) == 0 {
 		// the first listener, listen on the real address
-		routeConfig := &vhost.RouteConfig{
-			Domain: domain,
-		}
-		tcpMuxLn, errRet := tmg.ctl.tcpMuxHTTPConnectMuxer.Listen(ctx, routeConfig)
+		tcpMuxLn, errRet := tmg.ctl.tcpMuxHTTPConnectMuxer.Listen(ctx, &routeConfig)
 		if errRet != nil {
 			return nil, errRet
 		}
@@ -115,7 +121,8 @@ func (tmg *TCPMuxGroup) HTTPConnectListen(ctx context.Context, group string, gro
 
 		tmg.group = group
 		tmg.groupKey = groupKey
-		tmg.domain = domain
+		tmg.domain = routeConfig.Domain
+		tmg.routeByHTTPUser = routeConfig.RouteByHTTPUser
 		tmg.tcpMuxLn = tcpMuxLn
 		tmg.lns = append(tmg.lns, ln)
 		if tmg.acceptCh == nil {
@@ -123,8 +130,8 @@ func (tmg *TCPMuxGroup) HTTPConnectListen(ctx context.Context, group string, gro
 		}
 		go tmg.worker()
 	} else {
-		// domain in the same group must be equal
-		if tmg.group != group || tmg.domain != domain {
+		// route config in the same group must be equal
+		if tmg.group != group || tmg.domain != routeConfig.Domain || tmg.routeByHTTPUser != routeConfig.RouteByHTTPUser {
 			return nil, ErrGroupParamsInvalid
 		}
 		if tmg.groupKey != groupKey {

+ 19 - 16
server/proxy/http.go

@@ -38,11 +38,12 @@ type HTTPProxy struct {
 func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
 	xl := pxy.xl
 	routeConfig := vhost.RouteConfig{
-		RewriteHost:  pxy.cfg.HostHeaderRewrite,
-		Headers:      pxy.cfg.Headers,
-		Username:     pxy.cfg.HTTPUser,
-		Password:     pxy.cfg.HTTPPwd,
-		CreateConnFn: pxy.GetRealConn,
+		RewriteHost:     pxy.cfg.HostHeaderRewrite,
+		RouteByHTTPUser: pxy.cfg.RouteByHTTPUser,
+		Headers:         pxy.cfg.Headers,
+		Username:        pxy.cfg.HTTPUser,
+		Password:        pxy.cfg.HTTPPwd,
+		CreateConnFn:    pxy.GetRealConn,
 	}
 
 	locations := pxy.cfg.Locations
@@ -65,8 +66,8 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
 		routeConfig.Domain = domain
 		for _, location := range locations {
 			routeConfig.Location = location
-			tmpDomain := routeConfig.Domain
-			tmpLocation := routeConfig.Location
+
+			tmpRouteConfig := routeConfig
 
 			// handle group
 			if pxy.cfg.Group != "" {
@@ -76,7 +77,7 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
 				}
 
 				pxy.closeFuncs = append(pxy.closeFuncs, func() {
-					pxy.rc.HTTPGroupCtl.UnRegister(pxy.name, pxy.cfg.Group, tmpDomain, tmpLocation)
+					pxy.rc.HTTPGroupCtl.UnRegister(pxy.name, pxy.cfg.Group, tmpRouteConfig)
 				})
 			} else {
 				// no group
@@ -85,11 +86,12 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
 					return
 				}
 				pxy.closeFuncs = append(pxy.closeFuncs, func() {
-					pxy.rc.HTTPReverseProxy.UnRegister(tmpDomain, tmpLocation)
+					pxy.rc.HTTPReverseProxy.UnRegister(tmpRouteConfig)
 				})
 			}
 			addrs = append(addrs, util.CanonicalAddr(routeConfig.Domain, int(pxy.serverCfg.VhostHTTPPort)))
-			xl.Info("http proxy listen for host [%s] location [%s] group [%s]", routeConfig.Domain, routeConfig.Location, pxy.cfg.Group)
+			xl.Info("http proxy listen for host [%s] location [%s] group [%s], routeByHTTPUser [%s]",
+				routeConfig.Domain, routeConfig.Location, pxy.cfg.Group, pxy.cfg.RouteByHTTPUser)
 		}
 	}
 
@@ -97,8 +99,8 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
 		routeConfig.Domain = pxy.cfg.SubDomain + "." + pxy.serverCfg.SubDomainHost
 		for _, location := range locations {
 			routeConfig.Location = location
-			tmpDomain := routeConfig.Domain
-			tmpLocation := routeConfig.Location
+
+			tmpRouteConfig := routeConfig
 
 			// handle group
 			if pxy.cfg.Group != "" {
@@ -108,7 +110,7 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
 				}
 
 				pxy.closeFuncs = append(pxy.closeFuncs, func() {
-					pxy.rc.HTTPGroupCtl.UnRegister(pxy.name, pxy.cfg.Group, tmpDomain, tmpLocation)
+					pxy.rc.HTTPGroupCtl.UnRegister(pxy.name, pxy.cfg.Group, tmpRouteConfig)
 				})
 			} else {
 				err = pxy.rc.HTTPReverseProxy.Register(routeConfig)
@@ -116,12 +118,13 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
 					return
 				}
 				pxy.closeFuncs = append(pxy.closeFuncs, func() {
-					pxy.rc.HTTPReverseProxy.UnRegister(tmpDomain, tmpLocation)
+					pxy.rc.HTTPReverseProxy.UnRegister(tmpRouteConfig)
 				})
 			}
-			addrs = append(addrs, util.CanonicalAddr(tmpDomain, pxy.serverCfg.VhostHTTPPort))
+			addrs = append(addrs, util.CanonicalAddr(tmpRouteConfig.Domain, pxy.serverCfg.VhostHTTPPort))
 
-			xl.Info("http proxy listen for host [%s] location [%s] group [%s]", routeConfig.Domain, routeConfig.Location, pxy.cfg.Group)
+			xl.Info("http proxy listen for host [%s] location [%s] group [%s], routeByHTTPUser [%s]",
+				routeConfig.Domain, routeConfig.Location, pxy.cfg.Group, pxy.cfg.RouteByHTTPUser)
 		}
 	}
 	remoteAddr = strings.Join(addrs, ",")

+ 11 - 8
server/proxy/tcpmux.go

@@ -30,20 +30,23 @@ type TCPMuxProxy struct {
 	cfg *config.TCPMuxProxyConf
 }
 
-func (pxy *TCPMuxProxy) httpConnectListen(domain string, addrs []string) (_ []string, err error) {
+func (pxy *TCPMuxProxy) httpConnectListen(domain, routeByHTTPUser string, addrs []string) ([]string, error) {
 	var l net.Listener
+	var err error
+	routeConfig := &vhost.RouteConfig{
+		Domain:          domain,
+		RouteByHTTPUser: routeByHTTPUser,
+	}
 	if pxy.cfg.Group != "" {
-		l, err = pxy.rc.TCPMuxGroupCtl.Listen(pxy.ctx, pxy.cfg.Multiplexer, pxy.cfg.Group, pxy.cfg.GroupKey, domain)
+		l, err = pxy.rc.TCPMuxGroupCtl.Listen(pxy.ctx, pxy.cfg.Multiplexer, pxy.cfg.Group, pxy.cfg.GroupKey, *routeConfig)
 	} else {
-		routeConfig := &vhost.RouteConfig{
-			Domain: domain,
-		}
 		l, err = pxy.rc.TCPMuxHTTPConnectMuxer.Listen(pxy.ctx, routeConfig)
 	}
 	if err != nil {
 		return nil, err
 	}
-	pxy.xl.Info("tcpmux httpconnect multiplexer listens for host [%s]", domain)
+	pxy.xl.Info("tcpmux httpconnect multiplexer listens for host [%s], group [%s] routeByHTTPUser [%s]",
+		domain, pxy.cfg.Group, pxy.cfg.RouteByHTTPUser)
 	pxy.listeners = append(pxy.listeners, l)
 	return append(addrs, util.CanonicalAddr(domain, pxy.serverCfg.TCPMuxHTTPConnectPort)), nil
 }
@@ -55,14 +58,14 @@ func (pxy *TCPMuxProxy) httpConnectRun() (remoteAddr string, err error) {
 			continue
 		}
 
-		addrs, err = pxy.httpConnectListen(domain, addrs)
+		addrs, err = pxy.httpConnectListen(domain, pxy.cfg.RouteByHTTPUser, addrs)
 		if err != nil {
 			return "", err
 		}
 	}
 
 	if pxy.cfg.SubDomain != "" {
-		addrs, err = pxy.httpConnectListen(pxy.cfg.SubDomain+"."+pxy.serverCfg.SubDomainHost, addrs)
+		addrs, err = pxy.httpConnectListen(pxy.cfg.SubDomain+"."+pxy.serverCfg.SubDomainHost, pxy.cfg.RouteByHTTPUser, addrs)
 		if err != nil {
 			return "", err
 		}

+ 2 - 2
server/service.go

@@ -131,12 +131,12 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) {
 			return
 		}
 
-		svr.rc.TCPMuxHTTPConnectMuxer, err = tcpmux.NewHTTPConnectTCPMuxer(l, vhostReadWriteTimeout)
+		svr.rc.TCPMuxHTTPConnectMuxer, err = tcpmux.NewHTTPConnectTCPMuxer(l, cfg.TCPMuxPassthrough, vhostReadWriteTimeout)
 		if err != nil {
 			err = fmt.Errorf("Create vhost tcpMuxer error, %v", err)
 			return
 		}
-		log.Info("tcpmux httpconnect multiplexer listen on %s", address)
+		log.Info("tcpmux httpconnect multiplexer listen on %s, passthough: %v", address, cfg.TCPMuxPassthrough)
 	}
 
 	// Init all plugins

+ 67 - 17
test/e2e/basic/http.go

@@ -10,7 +10,6 @@ import (
 	"github.com/fatedier/frp/test/e2e/framework/consts"
 	"github.com/fatedier/frp/test/e2e/mock/server/httpserver"
 	"github.com/fatedier/frp/test/e2e/pkg/request"
-	"github.com/fatedier/frp/test/e2e/pkg/utils"
 
 	"github.com/gorilla/websocket"
 	. "github.com/onsi/ginkgo"
@@ -59,28 +58,83 @@ var _ = Describe("[Feature: HTTP]", func() {
 
 		f.RunProcesses([]string{serverConf}, []string{clientConf})
 
-		// foo path
-		framework.NewRequestExpect(f).Explain("foo path").Port(vhostHTTPPort).
+		tests := []struct {
+			path       string
+			expectResp string
+			desc       string
+		}{
+			{path: "/foo", expectResp: "foo", desc: "foo path"},
+			{path: "/bar", expectResp: "bar", desc: "bar path"},
+			{path: "/other", expectResp: "foo", desc: "other path"},
+		}
+
+		for _, test := range tests {
+			framework.NewRequestExpect(f).Explain(test.desc).Port(vhostHTTPPort).
+				RequestModify(func(r *request.Request) {
+					r.HTTP().HTTPHost("normal.example.com").HTTPPath(test.path)
+				}).
+				ExpectResp([]byte(test.expectResp)).
+				Ensure()
+		}
+	})
+
+	It("HTTP route by HTTP user", func() {
+		vhostHTTPPort := f.AllocPort()
+		serverConf := getDefaultServerConf(vhostHTTPPort)
+
+		fooPort := f.AllocPort()
+		f.RunServer("", newHTTPServer(fooPort, "foo"))
+
+		barPort := f.AllocPort()
+		f.RunServer("", newHTTPServer(barPort, "bar"))
+
+		otherPort := f.AllocPort()
+		f.RunServer("", newHTTPServer(otherPort, "other"))
+
+		clientConf := consts.DefaultClientConfig
+		clientConf += fmt.Sprintf(`
+			[foo]
+			type = http
+			local_port = %d
+			custom_domains = normal.example.com
+			route_by_http_user = user1
+
+			[bar]
+			type = http
+			local_port = %d
+			custom_domains = normal.example.com
+			route_by_http_user = user2
+
+			[catchAll]
+			type = http
+			local_port = %d
+			custom_domains = normal.example.com
+			`, fooPort, barPort, otherPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		// user1
+		framework.NewRequestExpect(f).Explain("user1").Port(vhostHTTPPort).
 			RequestModify(func(r *request.Request) {
-				r.HTTP().HTTPHost("normal.example.com").HTTPPath("/foo")
+				r.HTTP().HTTPHost("normal.example.com").HTTPAuth("user1", "")
 			}).
 			ExpectResp([]byte("foo")).
 			Ensure()
 
-		// bar path
-		framework.NewRequestExpect(f).Explain("bar path").Port(vhostHTTPPort).
+		// user2
+		framework.NewRequestExpect(f).Explain("user2").Port(vhostHTTPPort).
 			RequestModify(func(r *request.Request) {
-				r.HTTP().HTTPHost("normal.example.com").HTTPPath("/bar")
+				r.HTTP().HTTPHost("normal.example.com").HTTPAuth("user2", "")
 			}).
 			ExpectResp([]byte("bar")).
 			Ensure()
 
-		// other path
-		framework.NewRequestExpect(f).Explain("other path").Port(vhostHTTPPort).
+		// other user
+		framework.NewRequestExpect(f).Explain("other user").Port(vhostHTTPPort).
 			RequestModify(func(r *request.Request) {
-				r.HTTP().HTTPHost("normal.example.com").HTTPPath("/other")
+				r.HTTP().HTTPHost("normal.example.com").HTTPAuth("user3", "")
 			}).
-			ExpectResp([]byte("foo")).
+			ExpectResp([]byte("other")).
 			Ensure()
 	})
 
@@ -110,18 +164,14 @@ var _ = Describe("[Feature: HTTP]", func() {
 		// set incorrect auth header
 		framework.NewRequestExpect(f).Port(vhostHTTPPort).
 			RequestModify(func(r *request.Request) {
-				r.HTTP().HTTPHost("normal.example.com").HTTPHeaders(map[string]string{
-					"Authorization": utils.BasicAuth("test", "invalid"),
-				})
+				r.HTTP().HTTPHost("normal.example.com").HTTPAuth("test", "invalid")
 			}).
 			Ensure(framework.ExpectResponseCode(401))
 
 		// set correct auth header
 		framework.NewRequestExpect(f).Port(vhostHTTPPort).
 			RequestModify(func(r *request.Request) {
-				r.HTTP().HTTPHost("normal.example.com").HTTPHeaders(map[string]string{
-					"Authorization": utils.BasicAuth("test", "test"),
-				})
+				r.HTTP().HTTPHost("normal.example.com").HTTPAuth("test", "test")
 			}).
 			Ensure()
 	})

+ 1 - 1
test/e2e/framework/process.go

@@ -60,7 +60,7 @@ func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []str
 		ExpectNoError(err)
 		time.Sleep(500 * time.Millisecond)
 	}
-	time.Sleep(500 * time.Millisecond)
+	time.Sleep(time.Second)
 
 	return currentServerProcesses, currentClientProcesses
 }

+ 28 - 8
test/e2e/pkg/request/request.go

@@ -13,6 +13,7 @@ import (
 	"time"
 
 	"github.com/fatedier/frp/test/e2e/pkg/rpc"
+	"github.com/fatedier/frp/test/e2e/pkg/utils"
 	libdial "github.com/fatedier/golib/net/dial"
 )
 
@@ -20,10 +21,11 @@ type Request struct {
 	protocol string
 
 	// for all protocol
-	addr    string
-	port    int
-	body    []byte
-	timeout time.Duration
+	addr     string
+	port     int
+	body     []byte
+	timeout  time.Duration
+	resolver *net.Resolver
 
 	// for http or https
 	method    string
@@ -32,6 +34,8 @@ type Request struct {
 	headers   map[string]string
 	tlsConfig *tls.Config
 
+	authValue string
+
 	proxyURL string
 }
 
@@ -40,8 +44,9 @@ func New() *Request {
 		protocol: "tcp",
 		addr:     "127.0.0.1",
 
-		method: "GET",
-		path:   "/",
+		method:  "GET",
+		path:    "/",
+		headers: map[string]string{},
 	}
 }
 
@@ -108,6 +113,11 @@ func (r *Request) HTTPHeaders(headers map[string]string) *Request {
 	return r
 }
 
+func (r *Request) HTTPAuth(user, password string) *Request {
+	r.authValue = utils.BasicAuth(user, password)
+	return r
+}
+
 func (r *Request) TLSConfig(tlsConfig *tls.Config) *Request {
 	r.tlsConfig = tlsConfig
 	return r
@@ -123,6 +133,11 @@ func (r *Request) Body(content []byte) *Request {
 	return r
 }
 
+func (r *Request) Resolver(resolver *net.Resolver) *Request {
+	r.resolver = resolver
+	return r
+}
+
 func (r *Request) Do() (*Response, error) {
 	var (
 		conn net.Conn
@@ -150,11 +165,12 @@ func (r *Request) Do() (*Response, error) {
 			return nil, err
 		}
 	} else {
+		dialer := &net.Dialer{Resolver: r.resolver}
 		switch r.protocol {
 		case "tcp":
-			conn, err = net.Dial("tcp", addr)
+			conn, err = dialer.Dial("tcp", addr)
 		case "udp":
-			conn, err = net.Dial("udp", addr)
+			conn, err = dialer.Dial("udp", addr)
 		default:
 			return nil, fmt.Errorf("invalid protocol")
 		}
@@ -198,11 +214,15 @@ func (r *Request) sendHTTPRequest(method, urlstr string, host string, headers ma
 	for k, v := range headers {
 		req.Header.Set(k, v)
 	}
+	if r.authValue != "" {
+		req.Header.Set("Authorization", r.authValue)
+	}
 	tr := &http.Transport{
 		DialContext: (&net.Dialer{
 			Timeout:   time.Second,
 			KeepAlive: 30 * time.Second,
 			DualStack: true,
+			Resolver:  r.resolver,
 		}).DialContext,
 		MaxIdleConns:          100,
 		IdleConnTimeout:       90 * time.Second,

+ 1 - 4
test/e2e/plugin/client.go

@@ -12,7 +12,6 @@ import (
 	"github.com/fatedier/frp/test/e2e/pkg/cert"
 	"github.com/fatedier/frp/test/e2e/pkg/port"
 	"github.com/fatedier/frp/test/e2e/pkg/request"
-	"github.com/fatedier/frp/test/e2e/pkg/utils"
 
 	. "github.com/onsi/ginkgo"
 )
@@ -181,9 +180,7 @@ var _ = Describe("[Feature: Client-Plugins]", func() {
 
 		// from http proxy with auth
 		framework.NewRequestExpect(f).Request(
-			framework.NewHTTPRequest().HTTPHost("other.example.com").HTTPPath("/test_static_file").Port(vhostPort).HTTPHeaders(map[string]string{
-				"Authorization": utils.BasicAuth("abc", "123"),
-			}),
+			framework.NewHTTPRequest().HTTPHost("other.example.com").HTTPPath("/test_static_file").Port(vhostPort).HTTPAuth("abc", "123"),
 		).ExpectResp([]byte("foo")).Ensure()
 	})