Browse Source

Merge pull request #201 from fatedier/dev

bump version to 0.9.1
fatedier 8 years ago
parent
commit
044bb692dc

+ 32 - 4
README.md

@@ -6,9 +6,9 @@
 
 ## What is frp?
 
-frp is a fast reverse proxy to help you expose a local server behind a NAT or firewall to the internet. Now, it supports tcp, http and https protocol when requests can be forwarded by domains to backward web services.
+frp is a fast reverse proxy to help you expose a local server behind a NAT or firewall to the internet. Now, it supports tcp, udp, http and https protocol when requests can be forwarded by domains to backward web services.
 
-## Catalog
+## Table of Contents
 
 <!-- vim-markdown-toc GFM -->
 * [What can I do with frp?](#what-can-i-do-with-frp)
@@ -29,6 +29,7 @@ frp is a fast reverse proxy to help you expose a local server behind a NAT or fi
     * [Rewriting the Host Header](#rewriting-the-host-header)
     * [Password protecting your web service](#password-protecting-your-web-service)
     * [Custom subdomain names](#custom-subdomain-names)
+    * [URL routing](#url-routing)
     * [Connect frps by HTTP PROXY](#connect-frps-by-http-proxy)
 * [Development Plan](#development-plan)
 * [Contributing](#contributing)
@@ -108,7 +109,7 @@ Put **frpc** and **frpc.ini** to your server in LAN.
 
 Sometimes we want to expose a local web service behind a NAT network to others for testing with your own domain name and unfortunately we can't resolve a domain name to a local ip.
 
-Howerver, we can expose a http or https service using frp.
+However, we can expose a http or https service using frp.
 
 1. Modify frps.ini, configure a http reverse proxy named [web] and set http port as 8080, custom domain as `www.yourdomain.com`:
 
@@ -238,7 +239,7 @@ use_gzip = true
 
 ### Reload configures without frps stopped
 
-If your want to add a new reverse proxy and avoid restarting frps, you can use this function:
+If you want to add a new reverse proxy and avoid restarting frps, you can use this function:
 
 1. `dashboard_port` should be set in frps.ini:
 
@@ -414,6 +415,30 @@ Now you can visit your web service by host `test.frps.com`.
 
 Note that if `subdomain_host` is not empty, `custom_domains` should not be the subdomain of `subdomain_host`.
 
+### URL routing
+
+frp support forward http requests to different backward web services by url routing.
+
+`locations` specify the prefix of URL used for routing. frps first searches for the most specific prefix location given by literal strings regardless of the listed order.
+
+```ini
+# frpc.ini
+[web01]
+privilege_mode = true
+type = http
+local_port = 80
+custom_domains = web.yourdomain.com
+locations = /
+
+[web02]
+privilege_mode = true
+type = http
+local_port = 81
+custom_domains = web.yourdomain.com
+locations = /news,/about
+```
+Http requests with url prefix `/news` and `/about` will be forwarded to **web02** and others to **web01**.
+
 ### Connect frps by HTTP PROXY
 
 frpc can connect frps using HTTP PROXY if you set os environment `HTTP_PROXY` or configure `http_proxy` param in frpc.ini file.
@@ -452,6 +477,8 @@ Interested in getting involved? We would like to help you!
 
 If frp help you a lot, you can support us by:
 
+frp QQ group: 606194980
+
 ### AliPay
 
 ![donation-alipay](/doc/pic/donate-alipay.png)
@@ -469,3 +496,4 @@ Donate money by [paypal](https://www.paypal.me/fatedier) to my account **fatedie
 * [Eric Larssen](https://github.com/ericlarssen)
 * [Damon Zhao](https://github.com/se77en)
 * [Manfred Touron](https://github.com/moul)
+* [xuebing1110](https://github.com/xuebing1110)

+ 30 - 1
README_zh.md

@@ -4,7 +4,7 @@
 
 [README](README.md) | [中文文档](README_zh.md)
 
-frp 是一个高性能的反向代理应用,可以帮助您轻松地进行内网穿透,对外网提供服务,支持 tcp, http, https 等协议类型,并且 web 服务支持根据域名进行路由转发。
+frp 是一个高性能的反向代理应用,可以帮助您轻松地进行内网穿透,对外网提供服务,支持 tcp, udp, http, https 等协议类型,并且 web 服务支持根据域名进行路由转发。
 
 ## 目录
 
@@ -27,6 +27,7 @@ frp 是一个高性能的反向代理应用,可以帮助您轻松地进行内
     * [修改 Host Header](#修改-host-header)
     * [通过密码保护你的 web 服务](#通过密码保护你的-web-服务)
     * [自定义二级域名](#自定义二级域名)
+    * [URL 路由](#url-路由)
     * [通过 HTTP PROXY 连接 frps](#通过-http-proxy-连接-frps)
 * [开发计划](#开发计划)
 * [为 frp 做贡献](#为-frp-做贡献)
@@ -426,6 +427,31 @@ frps 和 fprc 都启动成功后,通过 `test.frps.com` 就可以访问到内
 
 同一个 http 或 https 类型的代理中 `custom_domains`  和 `subdomain` 可以同时配置。
 
+### URL 路由
+
+frp 支持根据请求的 URL 路径路由转发到不同的后端服务。
+
+通过配置文件中的 `locations` 字段指定一个或多个 proxy 能够匹配的 URL 前缀(目前仅支持最大前缀匹配,之后会考虑正则匹配)。例如指定 `locations = /news`,则所有 URL 以 `/news` 开头的请求都会被转发到这个服务。
+
+```ini
+# frpc.ini
+[web01]
+privilege_mode = true
+type = http
+local_port = 80
+custom_domains = web.yourdomain.com
+locations = /
+
+[web02]
+privilege_mode = true
+type = http
+local_port = 81
+custom_domains = web.yourdomain.com
+locations = /news,/about
+```
+
+按照上述的示例配置后,`web.yourdomain.com` 这个域名下所有以 `/news` 以及 `/about` 作为前缀的 URL 请求都会被转发到 web02,其余的请求会被转发到 web01。
+
 ### 通过 HTTP PROXY 连接 frps
 
 在只能通过代理访问外网的环境内,frpc 支持通过 HTTP PROXY 和 frps 进行通信。
@@ -470,6 +496,8 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
 
 如果您觉得 frp 对你有帮助,欢迎给予我们一定的捐助来维持项目的长期发展。
 
+frp 交流群:606194980 (QQ 群号)
+
 ### 支付宝扫码捐赠
 
 ![donate-alipay](/doc/pic/donate-alipay.png)
@@ -487,3 +515,4 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
 * [Eric Larssen](https://github.com/ericlarssen)
 * [Damon Zhao](https://github.com/se77en)
 * [Manfred Touron](https://github.com/moul)
+* [xuebing1110](https://github.com/xuebing1110)

+ 4 - 3
conf/frpc.ini

@@ -30,11 +30,9 @@ type = tcp
 local_ip = 127.0.0.1
 local_port = 22
 # true or false, if true, messages between frps and frpc will be encrypted, default is false
-use_encryption = true
+use_encryption = false
 # default is false
 use_gzip = false
-# connections will be established in advance, default value is zero
-pool_count = 10
 
 [dns]
 type = udp
@@ -47,6 +45,7 @@ type = http
 local_ip = 127.0.0.1
 local_port = 80
 use_gzip = true
+# connections will be established in advance, default value is zero
 pool_count = 20
 # http username and password are safety certification for http protocol
 # if not set, you can access this custom_domains without certification
@@ -77,5 +76,7 @@ local_ip = 127.0.0.1
 local_port = 80
 use_gzip = true
 custom_domains = web03.yourdomain.com
+# locations is only useful for http type
+locations = /,/pic
 host_header_rewrite = example.com
 subdomain = dev

+ 8 - 1
src/assets/static/index.html

@@ -25,6 +25,7 @@
                   <th class="tab_info" ng-click="col='current_conns';desc=!desc">CurCon<i class="iconfont pull-right">&#xe66d;</i></th>
                   <th class="tab_info" ng-click="col='daily[daily.length-1].flow_out';desc=!desc">FlowOut<i class="iconfont pull-right">&#xe66d;</i></th>
                   <th class="tab_info" ng-click="col='daily[daily.length-1].flow_in';desc=!desc">FlowIn<i class="iconfont pull-right">&#xe66d;</i></th>
+                  <th class="tab_info" ng-click="col='daily[daily.length-1].total_accept_conns';desc=!desc">TotalAcceptConns<i class="iconfont pull-right">&#xe66d;</i></th>
                 </tr>
               </thead>
               <tbody id="tab_body">
@@ -38,6 +39,7 @@
                   <td><span ng-bind="x.current_conns"></span></td>
                   <td><span ng-bind="x.daily[x.daily.length-1].flow_out"></span></td>
                   <td><span ng-bind="x.daily[x.daily.length-1].flow_in"></span></td>
+                  <td><span ng-bind="x.daily[x.daily.length-1].total_accept_conns"></span></td>
                 </tr>
               </tbody>
             </table>
@@ -63,7 +65,8 @@
         listen_port: "<<< .ListenPort >>>",
         current_conns: <<< .CurrentConns >>> ,
         domains: [ <<< range.CustomDomains >>> "<<< . >>>", <<< end >>> ],
-        stat: "<<< .Status >>>",
+        locations: [ <<< range.Locations >>> "<<< . >>>", <<< end >>> ],
+        stat: "<<< .Status>>>",
         use_encryption: "<<< .UseEncryption >>>",
         use_gzip: "<<< .UseGzip >>>",
         privilege_mode: "<<< .PrivilegeMode >>>",
@@ -222,6 +225,10 @@
         newrow += "<tr class='info_detail'><td colspan='4'>Domains</td><td colspan='4'>" +
           alldata[index].domains[domainindex] + "</td><tr>";
       }
+      for (var locindex in alldata[index].locations) {
+        newrow += "<tr class='info_detail'><td colspan='4'>Locations</td><td colspan='4'>" +
+          alldata[index].locations[locindex] + "</td><tr>";
+      }
       newrow += "<tr class='info_detail'><td colspan='4'>Ip</td><td colspan='4'>" + alldata[index].bind_addr + "</td><tr>";
       newrow += "<tr class='info_detail'><td colspan='4'>Status</td><td colspan='4'>" + alldata[index].stat + "</td><tr>";
       newrow += "<tr class='info_detail'><td colspan='4'>Encryption</td><td colspan='4'>" + alldata[index].use_encryption + "</td><tr>";

File diff suppressed because it is too large
+ 0 - 0
src/assets/statik/statik.go


+ 1 - 0
src/cmd/frpc/control.go

@@ -159,6 +159,7 @@ func loginToServer(cli *client.ProxyClient) (c *conn.Conn, err error) {
 		privilegeKey := pcrypto.GetAuthKey(cli.Name + client.PrivilegeToken + fmt.Sprintf("%d", nowTime))
 		req.RemotePort = cli.RemotePort
 		req.CustomDomains = cli.CustomDomains
+		req.Locations = cli.Locations
 		req.PrivilegeKey = privilegeKey
 	} else {
 		authKey := pcrypto.GetAuthKey(cli.Name + cli.AuthToken + fmt.Sprintf("%d", nowTime))

+ 5 - 13
src/cmd/frps/control.go

@@ -70,7 +70,7 @@ func controlWorker(c *conn.Conn) {
 	}
 
 	// login when type is NewCtlConn or NewWorkConn
-	ret, info := doLogin(cliReq, c)
+	ret, info, s := doLogin(cliReq, c)
 	// if login type is NewWorkConn, nothing will be send to frpc
 	if cliReq.Type == consts.NewCtlConn {
 		cliRes := &msg.ControlRes{
@@ -94,12 +94,6 @@ func controlWorker(c *conn.Conn) {
 		return
 	}
 
-	s, ok := server.GetProxyServer(cliReq.ProxyName)
-	if !ok {
-		log.Warn("ProxyName [%s] does not exist now", cliReq.ProxyName)
-		return
-	}
-
 	// create a channel for sending messages
 	msgSendChan := make(chan interface{}, 1024)
 	go msgSender(s, c, msgSendChan)
@@ -199,7 +193,7 @@ func msgSender(s *server.ProxyServer, c *conn.Conn, msgSendChan chan interface{}
 // NewCtlConn
 // NewWorkConn
 // NewWorkConnUdp
-func doLogin(req *msg.ControlReq, c *conn.Conn) (ret int64, info string) {
+func doLogin(req *msg.ControlReq, c *conn.Conn) (ret int64, info string, s *server.ProxyServer) {
 	ret = 1
 	// check if PrivilegeMode is enabled
 	if req.PrivilegeMode && !server.PrivilegeMode {
@@ -208,10 +202,7 @@ func doLogin(req *msg.ControlReq, c *conn.Conn) (ret int64, info string) {
 		return
 	}
 
-	var (
-		s  *server.ProxyServer
-		ok bool
-	)
+	var ok bool
 	s, ok = server.GetProxyServer(req.ProxyName)
 	if req.PrivilegeMode && req.Type == consts.NewCtlConn {
 		log.Debug("ProxyName [%s], doLogin and privilege mode is enabled", req.ProxyName)
@@ -297,6 +288,7 @@ func doLogin(req *msg.ControlReq, c *conn.Conn) (ret int64, info string) {
 		}
 
 		// set infomations from frpc
+		s.BindAddr = server.BindAddr
 		s.UseEncryption = req.UseEncryption
 		s.UseGzip = req.UseGzip
 		s.HostHeaderRewrite = req.HostHeaderRewrite
@@ -332,7 +324,7 @@ func doLogin(req *msg.ControlReq, c *conn.Conn) (ret int64, info string) {
 		}
 
 		// update metric's proxy status
-		metric.SetProxyInfo(s.Name, s.Type, s.BindAddr, s.UseEncryption, s.UseGzip, s.PrivilegeMode, s.CustomDomains, s.ListenPort)
+		metric.SetProxyInfo(s.Name, s.Type, s.BindAddr, s.UseEncryption, s.UseGzip, s.PrivilegeMode, s.CustomDomains, s.Locations, s.ListenPort)
 
 		// start proxy and listen for user connections, no block
 		err := s.Start(c)

+ 1 - 0
src/models/client/client.go

@@ -35,6 +35,7 @@ type ProxyClient struct {
 
 	RemotePort    int64
 	CustomDomains []string
+	Locations     []string
 
 	udpTunnel *conn.Conn
 	once      sync.Once

+ 11 - 0
src/models/client/config.go

@@ -166,6 +166,9 @@ func LoadConf(confFile string) (err error) {
 				if ok {
 					proxyClient.HttpPassWord = tmpStr
 				}
+
+			}
+			if proxyClient.Type == "http" || proxyClient.Type == "https" {
 				// subdomain
 				tmpStr, ok = section["subdomain"]
 				if ok {
@@ -227,6 +230,14 @@ func LoadConf(confFile string) (err error) {
 					if !ok && proxyClient.SubDomain == "" {
 						return fmt.Errorf("Parse conf error: proxy [%s] custom_domains and subdomain should set at least one of them when type is http", proxyClient.Name)
 					}
+
+					// locations
+					locations, ok := section["locations"]
+					if ok {
+						proxyClient.Locations = strings.Split(locations, ",")
+					} else {
+						proxyClient.Locations = []string{""}
+					}
 				} else if proxyClient.Type == "https" {
 					// custom_domains
 					domainStr, ok := section["custom_domains"]

+ 3 - 1
src/models/metric/server.go

@@ -34,6 +34,7 @@ type ServerMetric struct {
 	BindAddr      string   `json:"bind_addr"`
 	ListenPort    int64    `json:"listen_port"`
 	CustomDomains []string `json:"custom_domains"`
+	Locations     []string `json:"locations"`
 	Status        string   `json:"status"`
 	UseEncryption bool     `json:"use_encryption"`
 	UseGzip       bool     `json:"use_gzip"`
@@ -112,7 +113,7 @@ func GetProxyMetrics(proxyName string) *ServerMetric {
 
 func SetProxyInfo(proxyName string, proxyType, bindAddr string,
 	useEncryption, useGzip, privilegeMode bool, customDomains []string,
-	listenPort int64) {
+	locations []string, listenPort int64) {
 	smMutex.Lock()
 	info, ok := ServerMetricInfoMap[proxyName]
 	if !ok {
@@ -127,6 +128,7 @@ func SetProxyInfo(proxyName string, proxyType, bindAddr string,
 	info.BindAddr = bindAddr
 	info.ListenPort = listenPort
 	info.CustomDomains = customDomains
+	info.Locations = locations
 	ServerMetricInfoMap[proxyName] = info
 	smMutex.Unlock()
 }

+ 1 - 0
src/models/msg/msg.go

@@ -34,6 +34,7 @@ type ControlReq struct {
 	ProxyType         string   `json:"proxy_type"`
 	RemotePort        int64    `json:"remote_port"`
 	CustomDomains     []string `json:"custom_domains, omitempty"`
+	Locations         []string `json:"locations"`
 	HostHeaderRewrite string   `json:"host_header_rewrite"`
 	HttpUserName      string   `json:"http_username"`
 	HttpPassWord      string   `json:"http_password"`

+ 11 - 3
src/models/server/config.go

@@ -304,6 +304,14 @@ func loadProxyConf(confFile string) (proxyServers map[string]*ProxyServer, err e
 				} else {
 					return proxyServers, fmt.Errorf("Parse conf error: proxy [%s] custom_domains must be set when type is http", proxyServer.Name)
 				}
+
+				// locations
+				locations, ok := section["locations"]
+				if ok {
+					proxyServer.Locations = strings.Split(locations, ",")
+				} else {
+					proxyServer.Locations = []string{""}
+				}
 			} else if proxyServer.Type == "https" {
 				// for https
 				proxyServer.ListenPort = VhostHttpsPort
@@ -332,7 +340,7 @@ func loadProxyConf(confFile string) (proxyServers map[string]*ProxyServer, err e
 	// set metric statistics of all proxies
 	for name, p := range proxyServers {
 		metric.SetProxyInfo(name, p.Type, p.BindAddr, p.UseEncryption, p.UseGzip,
-			p.PrivilegeMode, p.CustomDomains, p.ListenPort)
+			p.PrivilegeMode, p.CustomDomains, p.Locations, p.ListenPort)
 	}
 	return proxyServers, nil
 }
@@ -387,14 +395,14 @@ func CreateProxy(s *ProxyServer) error {
 		if oldServer.Status == consts.Working {
 			return fmt.Errorf("this proxy is already working now")
 		}
-		oldServer.Close()
+		oldServer.Release()
 		if oldServer.PrivilegeMode {
 			delete(ProxyServers, s.Name)
 		}
 	}
 	ProxyServers[s.Name] = s
 	metric.SetProxyInfo(s.Name, s.Type, s.BindAddr, s.UseEncryption, s.UseGzip,
-		s.PrivilegeMode, s.CustomDomains, s.ListenPort)
+		s.PrivilegeMode, s.CustomDomains, s.Locations, s.ListenPort)
 	s.Init()
 	return nil
 }

+ 67 - 14
src/models/server/server.go

@@ -39,6 +39,7 @@ type ProxyServer struct {
 	BindAddr      string
 	ListenPort    int64
 	CustomDomains []string
+	Locations     []string
 
 	Status      int64
 	CtlConn     *conn.Conn // control connection with frpc
@@ -56,6 +57,7 @@ type ProxyServer struct {
 func NewProxyServer() (p *ProxyServer) {
 	p = &ProxyServer{
 		CustomDomains: make([]string, 0),
+		Locations:     make([]string, 0),
 	}
 	return p
 }
@@ -77,6 +79,7 @@ func NewProxyServerFromCtlMsg(req *msg.ControlReq) (p *ProxyServer) {
 		p.ListenPort = VhostHttpsPort
 	}
 	p.CustomDomains = req.CustomDomains
+	p.Locations = req.Locations
 	p.HostHeaderRewrite = req.HostHeaderRewrite
 	p.HttpUserName = req.HttpUserName
 	p.HttpPassWord = req.HttpPassWord
@@ -108,6 +111,15 @@ func (p *ProxyServer) Compare(p2 *ProxyServer) bool {
 			return false
 		}
 	}
+
+	if len(p.Locations) != len(p2.Locations) {
+		return false
+	}
+	for i, _ := range p.Locations {
+		if p.Locations[i] != p2.Locations[i] {
+			return false
+		}
+	}
 	return true
 }
 
@@ -131,26 +143,58 @@ func (p *ProxyServer) Start(c *conn.Conn) (err error) {
 		p.listeners = append(p.listeners, l)
 	} else if p.Type == "http" {
 		for _, domain := range p.CustomDomains {
-			l, err := VhostHttpMuxer.Listen(domain, p.HostHeaderRewrite, p.HttpUserName, p.HttpPassWord)
-			if err != nil {
-				return err
+			if len(p.Locations) == 0 {
+				l, err := VhostHttpMuxer.Listen(domain, "", p.HostHeaderRewrite, p.HttpUserName, p.HttpPassWord)
+				if err != nil {
+					return err
+				}
+				log.Info("ProxyName [%s], type http listen for host [%s] location [%s]", p.Name, domain, "")
+				p.listeners = append(p.listeners, l)
+			} else {
+				for _, location := range p.Locations {
+					l, err := VhostHttpMuxer.Listen(domain, location, p.HostHeaderRewrite, p.HttpUserName, p.HttpPassWord)
+					if err != nil {
+						return err
+					}
+					log.Info("ProxyName [%s], type http listen for host [%s] location [%s]", p.Name, domain, location)
+					p.listeners = append(p.listeners, l)
+				}
 			}
-			p.listeners = append(p.listeners, l)
 		}
 		if p.SubDomain != "" {
-			l, err := VhostHttpMuxer.Listen(p.SubDomain, p.HostHeaderRewrite, p.HttpUserName, p.HttpPassWord)
+			if len(p.Locations) == 0 {
+				l, err := VhostHttpMuxer.Listen(p.SubDomain, "", p.HostHeaderRewrite, p.HttpUserName, p.HttpPassWord)
+				if err != nil {
+					return err
+				}
+				log.Info("ProxyName [%s], type http listen for host [%s] location [%s]", p.Name, p.SubDomain, "")
+				p.listeners = append(p.listeners, l)
+			} else {
+				for _, location := range p.Locations {
+					l, err := VhostHttpMuxer.Listen(p.SubDomain, location, p.HostHeaderRewrite, p.HttpUserName, p.HttpPassWord)
+					if err != nil {
+						return err
+					}
+					log.Info("ProxyName [%s], type http listen for host [%s] location [%s]", p.Name, p.SubDomain, location)
+					p.listeners = append(p.listeners, l)
+				}
+			}
+		}
+	} else if p.Type == "https" {
+		for _, domain := range p.CustomDomains {
+			l, err := VhostHttpsMuxer.Listen(domain, "", p.HostHeaderRewrite, p.HttpUserName, p.HttpPassWord)
 			if err != nil {
 				return err
 			}
+			log.Info("ProxyName [%s], type https listen for host [%s]", p.Name, domain)
 			p.listeners = append(p.listeners, l)
 		}
-
-	} else if p.Type == "https" {
-		for _, domain := range p.CustomDomains {
-			l, err := VhostHttpsMuxer.Listen(domain, p.HostHeaderRewrite, p.HttpUserName, p.HttpPassWord)
+		if p.SubDomain != "" {
+			l, err := VhostHttpsMuxer.Listen(p.SubDomain, "", p.HostHeaderRewrite, p.HttpUserName, p.HttpPassWord)
 			if err != nil {
 				return err
 			}
+			log.Info("ProxyName [%s], type https listen for host [%s]", p.Name, p.SubDomain)
 			p.listeners = append(p.listeners, l)
 		}
 	}
@@ -232,7 +276,20 @@ func (p *ProxyServer) Start(c *conn.Conn) (err error) {
 }
 
 func (p *ProxyServer) Close() {
+	p.Release()
+
+	// if the proxy created by PrivilegeMode, delete it when closed
+	if p.PrivilegeMode {
+		// NOTE: this will take the global ProxyServerMap's lock
+		// if we only want to release resources, use Release() instead
+		DeleteProxy(p.Name)
+	}
+}
+
+func (p *ProxyServer) Release() {
 	p.Lock()
+	defer p.Unlock()
+
 	if p.Status != consts.Closed {
 		p.Status = consts.Closed
 		for _, l := range p.listeners {
@@ -256,11 +313,6 @@ func (p *ProxyServer) Close() {
 		}
 	}
 	metric.SetStatus(p.Name, p.Status)
-	// if the proxy created by PrivilegeMode, delete it when closed
-	if p.PrivilegeMode {
-		DeleteProxy(p.Name)
-	}
-	p.Unlock()
 }
 
 func (p *ProxyServer) WaitUserConn() (closeFlag bool) {
@@ -346,6 +398,7 @@ func (p *ProxyServer) getWorkConn() (workConn *conn.Conn, err error) {
 				err = fmt.Errorf("ProxyName [%s], no work connections available, control is closing", p.Name)
 				return
 			}
+			log.Debug("ProxyName [%s], get work connection from pool", p.Name)
 		default:
 			// no work connections available in the poll, send message to frpc to get more
 			p.ctlMsgChan <- 1

+ 65 - 18
src/utils/conn/conn.go

@@ -16,6 +16,7 @@ package conn
 
 import (
 	"bufio"
+	"bytes"
 	"encoding/base64"
 	"fmt"
 	"io"
@@ -25,6 +26,8 @@ import (
 	"strings"
 	"sync"
 	"time"
+
+	"github.com/fatedier/frp/src/utils/pool"
 )
 
 type Listener struct {
@@ -61,11 +64,7 @@ func Listen(bindAddr string, bindPort int64) (l *Listener, err error) {
 				continue
 			}
 
-			c := &Conn{
-				TcpConn:   conn,
-				closeFlag: false,
-			}
-			c.Reader = bufio.NewReader(c.TcpConn)
+			c := NewConn(conn)
 			l.accept <- c
 		}
 	}()
@@ -95,20 +94,23 @@ func (l *Listener) Close() error {
 type Conn struct {
 	TcpConn   net.Conn
 	Reader    *bufio.Reader
+	buffer    *bytes.Buffer
 	closeFlag bool
-	mutex     sync.RWMutex
+
+	mutex sync.RWMutex
 }
 
 func NewConn(conn net.Conn) (c *Conn) {
-	c = &Conn{}
-	c.TcpConn = conn
+	c = &Conn{
+		TcpConn:   conn,
+		buffer:    nil,
+		closeFlag: false,
+	}
 	c.Reader = bufio.NewReader(c.TcpConn)
-	c.closeFlag = false
-	return c
+	return
 }
 
 func ConnectServer(addr string) (c *Conn, err error) {
-	c = &Conn{}
 	servertAddr, err := net.ResolveTCPAddr("tcp", addr)
 	if err != nil {
 		return
@@ -117,9 +119,7 @@ func ConnectServer(addr string) (c *Conn, err error) {
 	if err != nil {
 		return
 	}
-	c.TcpConn = conn
-	c.Reader = bufio.NewReader(c.TcpConn)
-	c.closeFlag = false
+	c = NewConn(conn)
 	return c, nil
 }
 
@@ -185,7 +185,23 @@ func (c *Conn) GetLocalAddr() (addr string) {
 }
 
 func (c *Conn) Read(p []byte) (n int, err error) {
-	n, err = c.Reader.Read(p)
+	c.mutex.RLock()
+	if c.buffer == nil {
+		c.mutex.RUnlock()
+		return c.Reader.Read(p)
+	}
+	c.mutex.RUnlock()
+
+	n, err = c.buffer.Read(p)
+	if err == io.EOF {
+		c.mutex.Lock()
+		c.buffer = nil
+		c.mutex.Unlock()
+		var n2 int
+		n2, err = c.Reader.Read(p[n:])
+
+		n += n2
+	}
 	return
 }
 
@@ -212,6 +228,16 @@ func (c *Conn) WriteString(content string) (err error) {
 	return err
 }
 
+func (c *Conn) AppendReaderBuffer(content []byte) {
+	c.mutex.Lock()
+	defer c.mutex.Unlock()
+
+	if c.buffer == nil {
+		c.buffer = bytes.NewBuffer(make([]byte, 0, 2048))
+	}
+	c.buffer.Write(content)
+}
+
 func (c *Conn) SetDeadline(t time.Time) error {
 	return c.TcpConn.SetDeadline(t)
 }
@@ -238,22 +264,36 @@ func (c *Conn) IsClosed() (closeFlag bool) {
 }
 
 // when you call this function, you should make sure that
-// remote client won't send any bytes to this socket
+// no bytes were read before
 func (c *Conn) CheckClosed() bool {
 	c.mutex.RLock()
 	if c.closeFlag {
+		c.mutex.RUnlock()
 		return true
 	}
 	c.mutex.RUnlock()
 
+	tmp := pool.GetBuf(2048)
+	defer pool.PutBuf(tmp)
 	err := c.TcpConn.SetReadDeadline(time.Now().Add(time.Millisecond))
 	if err != nil {
 		c.Close()
 		return true
 	}
 
-	var tmp []byte = make([]byte, 1)
-	_, err = c.TcpConn.Read(tmp)
+	n, err := c.TcpConn.Read(tmp)
+	if err == io.EOF {
+		return true
+	}
+
+	var tmp2 []byte = make([]byte, 1)
+	err = c.TcpConn.SetReadDeadline(time.Now().Add(time.Millisecond))
+	if err != nil {
+		c.Close()
+		return true
+	}
+
+	n2, err := c.TcpConn.Read(tmp2)
 	if err == io.EOF {
 		return true
 	}
@@ -263,5 +303,12 @@ func (c *Conn) CheckClosed() bool {
 		c.Close()
 		return true
 	}
+
+	if n > 0 {
+		c.AppendReaderBuffer(tmp[:n])
+	}
+	if n2 > 0 {
+		c.AppendReaderBuffer(tmp2[:n2])
+	}
 	return false
 }

+ 1 - 1
src/utils/version/version.go

@@ -19,7 +19,7 @@ import (
 	"strings"
 )
 
-var version string = "0.9.0"
+var version string = "0.9.1"
 
 func Full() string {
 	return version

+ 2 - 0
src/utils/vhost/http.go

@@ -45,6 +45,8 @@ func GetHttpRequestInfo(c *conn.Conn) (_ net.Conn, _ map[string]string, err erro
 	// hostName
 	tmpArr := strings.Split(request.Host, ":")
 	reqInfoMap["Host"] = tmpArr[0]
+	reqInfoMap["Path"] = request.URL.Path
+	reqInfoMap["Scheme"] = request.URL.Scheme
 
 	// Authorization
 	authStr := request.Header.Get("Authorization")

+ 1 - 0
src/utils/vhost/https.go

@@ -186,5 +186,6 @@ func GetHttpsHostname(c *conn.Conn) (sc net.Conn, _ map[string]string, err error
 		return sc, reqInfoMap, err
 	}
 	reqInfoMap["Host"] = host
+	reqInfoMap["Scheme"] = "https"
 	return sc, reqInfoMap, nil
 }

+ 114 - 0
src/utils/vhost/router.go

@@ -0,0 +1,114 @@
+package vhost
+
+import (
+	"sort"
+	"strings"
+	"sync"
+)
+
+type VhostRouters struct {
+	RouterByDomain map[string][]*VhostRouter
+	mutex          sync.RWMutex
+}
+
+type VhostRouter struct {
+	domain   string
+	location string
+	listener *Listener
+}
+
+func NewVhostRouters() *VhostRouters {
+	return &VhostRouters{
+		RouterByDomain: make(map[string][]*VhostRouter),
+	}
+}
+
+func (r *VhostRouters) Add(domain, location string, l *Listener) {
+	r.mutex.Lock()
+	defer r.mutex.Unlock()
+
+	vrs, found := r.RouterByDomain[domain]
+	if !found {
+		vrs = make([]*VhostRouter, 0, 1)
+	}
+
+	vr := &VhostRouter{
+		domain:   domain,
+		location: location,
+		listener: l,
+	}
+	vrs = append(vrs, vr)
+
+	sort.Sort(sort.Reverse(ByLocation(vrs)))
+	r.RouterByDomain[domain] = vrs
+}
+
+func (r *VhostRouters) Del(domain, location string) {
+	r.mutex.Lock()
+	defer r.mutex.Unlock()
+
+	vrs, found := r.RouterByDomain[domain]
+	if !found {
+		return
+	}
+
+	for i, vr := range vrs {
+		if vr.location == location {
+			if len(vrs) > i+1 {
+				r.RouterByDomain[domain] = append(vrs[:i], vrs[i+1:]...)
+			} else {
+				r.RouterByDomain[domain] = vrs[:i]
+			}
+		}
+	}
+}
+
+func (r *VhostRouters) Get(host, path string) (vr *VhostRouter, exist bool) {
+	r.mutex.RLock()
+	defer r.mutex.RUnlock()
+
+	vrs, found := r.RouterByDomain[host]
+	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 *VhostRouters) Exist(host, path string) (vr *VhostRouter, exist bool) {
+	r.mutex.RLock()
+	defer r.mutex.RUnlock()
+
+	vrs, found := r.RouterByDomain[host]
+	if !found {
+		return
+	}
+
+	for _, vr = range vrs {
+		if path == vr.location {
+			return vr, true
+		}
+	}
+
+	return
+}
+
+// sort by location
+type ByLocation []*VhostRouter
+
+func (a ByLocation) Len() int {
+	return len(a)
+}
+func (a ByLocation) Swap(i, j int) {
+	a[i], a[j] = a[j], a[i]
+}
+func (a ByLocation) Less(i, j int) bool {
+	return strings.Compare(a[i].location, a[j].location) < 0
+}

+ 39 - 32
src/utils/vhost/vhost.go

@@ -29,71 +29,75 @@ type httpAuthFunc func(*conn.Conn, string, string, string) (bool, error)
 type hostRewriteFunc func(*conn.Conn, string) (net.Conn, error)
 
 type VhostMuxer struct {
-	listener    *conn.Listener
-	timeout     time.Duration
-	vhostFunc   muxFunc
-	authFunc    httpAuthFunc
-	rewriteFunc hostRewriteFunc
-	registryMap map[string]*Listener
-	mutex       sync.RWMutex
+	listener       *conn.Listener
+	timeout        time.Duration
+	vhostFunc      muxFunc
+	authFunc       httpAuthFunc
+	rewriteFunc    hostRewriteFunc
+	registryRouter *VhostRouters
+	mutex          sync.RWMutex
 }
 
 func NewVhostMuxer(listener *conn.Listener, vhostFunc muxFunc, authFunc httpAuthFunc, rewriteFunc hostRewriteFunc, timeout time.Duration) (mux *VhostMuxer, err error) {
 	mux = &VhostMuxer{
-		listener:    listener,
-		timeout:     timeout,
-		vhostFunc:   vhostFunc,
-		authFunc:    authFunc,
-		rewriteFunc: rewriteFunc,
-		registryMap: make(map[string]*Listener),
+		listener:       listener,
+		timeout:        timeout,
+		vhostFunc:      vhostFunc,
+		authFunc:       authFunc,
+		rewriteFunc:    rewriteFunc,
+		registryRouter: NewVhostRouters(),
 	}
 	go mux.run()
 	return mux, nil
 }
 
 // listen for a new domain name, if rewriteHost is not empty  and rewriteFunc is not nil, then rewrite the host header to rewriteHost
-func (v *VhostMuxer) Listen(name string, rewriteHost, userName, passWord string) (l *Listener, err error) {
+func (v *VhostMuxer) Listen(name, location, rewriteHost, userName, passWord string) (l *Listener, err error) {
 	v.mutex.Lock()
 	defer v.mutex.Unlock()
-	if _, exist := v.registryMap[name]; exist {
-		return nil, fmt.Errorf("domain name %s is already bound", name)
+
+	_, ok := v.registryRouter.Exist(name, location)
+	if ok {
+		return nil, fmt.Errorf("hostname [%s] location [%s] is already registered", name, location)
 	}
 
 	l = &Listener{
 		name:        name,
+		location:    location,
 		rewriteHost: rewriteHost,
 		userName:    userName,
 		passWord:    passWord,
 		mux:         v,
 		accept:      make(chan *conn.Conn),
 	}
-	v.registryMap[name] = l
+	v.registryRouter.Add(name, location, l)
 	return l, nil
 }
 
-func (v *VhostMuxer) getListener(name string) (l *Listener, exist bool) {
+func (v *VhostMuxer) getListener(name, path string) (l *Listener, exist bool) {
 	v.mutex.RLock()
 	defer v.mutex.RUnlock()
+
 	// first we check the full hostname
 	// if not exist, then check the wildcard_domain such as *.example.com
-	l, exist = v.registryMap[name]
-	if exist {
-		return l, exist
+	vr, found := v.registryRouter.Get(name, path)
+	if found {
+		return vr.listener, true
 	}
+
 	domainSplit := strings.Split(name, ".")
 	if len(domainSplit) < 3 {
 		return l, false
 	}
 	domainSplit[0] = "*"
 	name = strings.Join(domainSplit, ".")
-	l, exist = v.registryMap[name]
-	return l, exist
-}
 
-func (v *VhostMuxer) unRegister(name string) {
-	v.mutex.Lock()
-	defer v.mutex.Unlock()
-	delete(v.registryMap, name)
+	vr, found = v.registryRouter.Get(name, path)
+	if !found {
+		return
+	}
+
+	return vr.listener, true
 }
 
 func (v *VhostMuxer) run() {
@@ -119,8 +123,8 @@ func (v *VhostMuxer) handle(c *conn.Conn) {
 	}
 
 	name := strings.ToLower(reqInfoMap["Host"])
-	// get listener by hostname
-	l, ok := v.getListener(name)
+	path := strings.ToLower(reqInfoMap["Path"])
+	l, ok := v.getListener(name, path)
 	if !ok {
 		c.Close()
 		return
@@ -150,6 +154,7 @@ func (v *VhostMuxer) handle(c *conn.Conn) {
 
 type Listener struct {
 	name        string
+	location    string
 	rewriteHost string
 	userName    string
 	passWord    string
@@ -177,7 +182,7 @@ func (l *Listener) Accept() (*conn.Conn, error) {
 }
 
 func (l *Listener) Close() error {
-	l.mux.unRegister(l.name)
+	l.mux.registryRouter.Del(l.name, l.location)
 	close(l.accept)
 	return nil
 }
@@ -207,16 +212,18 @@ func (sc *sharedConn) Read(p []byte) (n int, err error) {
 		sc.Unlock()
 		return sc.Conn.Read(p)
 	}
+	sc.Unlock()
 	n, err = sc.buff.Read(p)
 
 	if err == io.EOF {
+		sc.Lock()
 		sc.buff = nil
+		sc.Unlock()
 		var n2 int
 		n2, err = sc.Conn.Read(p[n:])
 
 		n += n2
 	}
-	sc.Unlock()
 	return
 }
 

Some files were not shown because too many files changed in this diff