diff --git a/Release.md b/Release.md index 78f9d436..8b137891 100644 --- a/Release.md +++ b/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. diff --git a/conf/frpc_full.ini b/conf/frpc_full.ini index a0e7dca4..9c939678 100644 --- a/conf/frpc_full.ini +++ b/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 diff --git a/conf/frps_full.ini b/conf/frps_full.ini index fecb8bf3..954221b3 100644 --- a/conf/frps_full.ini +++ b/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 diff --git a/pkg/config/proxy.go b/pkg/config/proxy.go index c000bb30..eb73cb37 100644 --- a/pkg/config/proxy.go +++ b/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) { diff --git a/pkg/config/server.go b/pkg/config/server.go index cf298f2e..090708a3 100644 --- a/pkg/config/server.go +++ b/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, diff --git a/pkg/msg/msg.go b/pkg/msg/msg.go index 6e59b7cf..ba6dd63e 100644 --- a/pkg/msg/msg.go +++ b/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"` } diff --git a/pkg/util/tcpmux/httpconnect.go b/pkg/util/tcpmux/httpconnect.go index fcc0a88f..2639c493 100644 --- a/pkg/util/tcpmux/httpconnect.go +++ b/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 } diff --git a/pkg/util/util/http.go b/pkg/util/util/http.go index 988ec179..3b811734 100644 --- a/pkg/util/util/http.go +++ b/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 +} diff --git a/pkg/util/vhost/http.go b/pkg/util/vhost/http.go index b7208627..bfbd16ea 100644 --- a/pkg/util/vhost/http.go +++ b/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 - // if not exist, then check the wildcard_domain such as *.example.com - vr, ok = rp.vhostRouter.Get(domain, location) - if ok { - return - } - - domainSplit := strings.Split(domain, ".") - if len(domainSplit) < 3 { +// 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 := findRouter(domain, location, routeByHTTPUser) + if ok { + return vr, ok + } + + // e.g. domain = test.example.com, try to match wildcard domains. + // *.example.com + // *.com + domainSplit := strings.Split(domain, ".") 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{} diff --git a/pkg/util/vhost/reverseproxy.go b/pkg/util/vhost/reverseproxy.go index 7d711d3b..975f1d14 100644 --- a/pkg/util/vhost/reverseproxy.go +++ b/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 diff --git a/pkg/util/vhost/router.go b/pkg/util/vhost/router.go index dc6a66a3..768420a4 100644 --- a/pkg/util/vhost/router.go +++ b/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 } diff --git a/pkg/util/vhost/vhost.go b/pkg/util/vhost/vhost.go index 4239d113..2957cec4 100644 --- a/pkg/util/vhost/vhost.go +++ b/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, + 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, l) + 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 } diff --git a/server/control.go b/server/control.go index 74a3cfad..197c94de 100644 --- a/server/control.go +++ b/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 diff --git a/server/group/http.go b/server/group/http.go index 0039b8ae..fb00f0c7 100644 --- a/server/group/http.go +++ b/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) -} diff --git a/server/group/tcpmux.go b/server/group/tcpmux.go index f2db8def..fd805d91 100644 --- a/server/group/tcpmux.go +++ b/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 { diff --git a/server/proxy/http.go b/server/proxy/http.go index 86925281..e0e2f23c 100644 --- a/server/proxy/http.go +++ b/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, ",") diff --git a/server/proxy/tcpmux.go b/server/proxy/tcpmux.go index 16cbcff3..b812e601 100644 --- a/server/proxy/tcpmux.go +++ b/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 } diff --git a/server/service.go b/server/service.go index ed663297..c3d398b2 100644 --- a/server/service.go +++ b/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 diff --git a/test/e2e/basic/http.go b/test/e2e/basic/http.go index b8cb448c..8fe73594 100644 --- a/test/e2e/basic/http.go +++ b/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() }) diff --git a/test/e2e/framework/process.go b/test/e2e/framework/process.go index 197cb7de..32ef2f84 100644 --- a/test/e2e/framework/process.go +++ b/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 } diff --git a/test/e2e/pkg/request/request.go b/test/e2e/pkg/request/request.go index 6d264783..cf6352f3 100644 --- a/test/e2e/pkg/request/request.go +++ b/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, diff --git a/test/e2e/plugin/client.go b/test/e2e/plugin/client.go index 03908956..afbd0a2c 100644 --- a/test/e2e/plugin/client.go +++ b/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() })