Browse Source

release v0.5.0 (#24)

[new] Optimize for http services.Support virtual host and custom domain binding.
[new] Support max days of keeping log files.
[fix] Fix a bug when reconnecting.
fatedier 8 years ago
parent
commit
f804330dbf

+ 0 - 14
Dockerfile

@@ -1,14 +0,0 @@
-FROM golang:1.5
-
-MAINTAINER fatedier
-
-RUN echo "[common]\nbind_addr = 0.0.0.0\nbind_port = 7000\n[test]\npasswd = 123\nbind_addr = 0.0.0.0\nlisten_port = 80" > /usr/share/frps.ini
-
-ADD ./ /usr/share/frp/
-
-RUN cd /usr/share/frp && make
-
-EXPOSE 80
-EXPOSE 7000
-
-CMD ["/usr/share/frp/bin/frps", "-c", "/usr/share/frps.ini"]

+ 11 - 7
README.md

@@ -8,6 +8,12 @@
 
 frp is a fast reverse proxy to help you expose a local server behind a NAT or firewall to the internet.
 
+## What can I do with frp?
+
+* Expose any http service behind a NAT or firewall to the internet by a server with public IP address(Name-based Virtual Host Support).
+* Expose any tcp service behind a NAT or firewall to the internet by a server with public IP address.
+* Inspect all http requests/responses that are transmitted over the tunnel(future).
+
 ## Status
 
 frp is under development and you can try it with latest release version.Master branch for releasing stable version when dev branch for developing.
@@ -16,17 +22,15 @@ frp is under development and you can try it with latest release version.Master b
 
 ## Quick Start
 
-Read the [QuickStart](doc/quick_start_en.md) | [使用文档](doc/quick_start_zh.md)
+Read the [QuickStart](/doc/quick_start_en.md)
 
-## Architecture
+[Tcp port forwarding](/doc/quick_start_en.md#tcp-port-forwarding)
 
-![architecture](doc/pic/architecture.png)
+[Http port forwarding and Custom domain binding](/doc/quick_start_en.md#http-port-forwarding-and-custom-domains-binding)
 
-## What can I do with frp?
+## Architecture
 
-* Expose any http service behind a NAT or firewall to the internet by a server with public IP address.
-* Expose any tcp service behind a NAT or firewall to the internet by a server with public IP address.
-* Inspect all http requests/responses that are transmitted over the tunnel(future).
+![architecture](/doc/pic/architecture.png)
 
 ## Contributing
 

+ 15 - 10
README_zh.md

@@ -4,31 +4,36 @@
 
 [README](README.md) | [中文文档](README_zh.md)
 
->frp 是一个高性能的反向代理应用,可以帮助你轻松的进行内网穿透,对外网提供服务。
+>frp 是一个高性能的反向代理应用,可以帮助你轻松的进行内网穿透,对外网提供服务,对于 http 服务还支持虚拟主机功能,访问80端口,可以根据域名路由到后端不同的 http 服务。
+
+## frp 的作用?
+
+* 利用处于内网或防火墙后的机器,对外网环境提供 http 服务。
+* 对于 http 服务支持基于域名的虚拟主机,支持自定义域名绑定,使多个域名可以共用一个80端口。
+* 利用处于内网或防火墙后的机器,对外网环境提供 tcp 服务,例如在家里通过 ssh 访问公司局域网内的主机。
+* 可查看通过代理的所有 http 请求和响应的详细信息。(待开发)
 
 ## 开发状态
 
 frp 目前正在前期开发阶段,master分支用于发布稳定版本,dev分支用于开发,您可以尝试下载最新的 release 版本进行测试。
 
-**在 1.x 版本以前,交互协议都可能会被改变,不能保证向后兼容。**
+**在 1.0 版本以前,交互协议都可能会被改变,不能保证向后兼容。**
 
 ## 快速开始
 
-[QuickStart](doc/quick_start_en.md) | [使用文档](doc/quick_start_zh.md)
+[使用文档](/doc/quick_start_zh.md)
 
-## 架构
+[tcp 端口转发](/doc/quick_start_zh.md#tcp-端口转发)
 
-![architecture](doc/pic/architecture.png)
+[http 端口转发,自定义域名绑定](/doc/quick_start_zh.md#http-端口转发自定义域名绑定)
 
-## frp 的作用?
+## 架构
 
-* 利用处于内网或防火墙后的机器,对外网环境提供http服务。(针对http的优化正在开发中)
-* 利用处于内网或防火墙后的机器,对外网环境提供tcp服务。
-* 可查看通过代理的所有http请求和响应信息。(待开发)
+![architecture](/doc/pic/architecture.png)
 
 ## 贡献代码
 
-如果您对这个项目感兴趣,并且想要参与其中,我们非常欢迎!
+如果您对这个项目感兴趣,我们非常欢迎您参与其中
 
 * 如果您需要提交问题,可以通过 [issues](https://github.com/fatedier/frp/issues) 来完成。
 * 如果您有新的功能需求,可以反馈至 fatedier@gmail.com 共同讨论。

+ 17 - 2
conf/frpc.ini

@@ -6,12 +6,27 @@ server_port = 7000
 log_file = ./frpc.log
 # debug, info, warn, error
 log_level = info
+log_max_days = 3
 # for authentication
 auth_token = 123
 
-# test1 is the proxy name same as server's configuration
-[test1]
+# ssh is the proxy name same as server's configuration
+[ssh]
+# tcp | http, default is tcp
+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
+
+# Resolve your domain names to [server_addr] so you can use http://web01.yourdomain.com to browse web01 and http://web02.yourdomain.com to browse web02, the domains are set in frps.ini
+[web01]
+type = http
+local_ip = 127.0.0.1
+local_port = 80
+use_encryption = true
+
+[web02]
+type = http
+local_ip = 127.0.0.1
+local_port = 8000

+ 17 - 2
conf/frps.ini

@@ -2,13 +2,28 @@
 [common]
 bind_addr = 0.0.0.0
 bind_port = 7000
+# optional
+vhost_http_port = 80
 # console or real logFile path like ./frps.log
 log_file = ./frps.log
 # debug, info, warn, error
 log_level = info
+log_max_days = 3
 
-# test1 is the proxy name, client will use this name and auth_token to connect to server
-[test1]
+# ssh is the proxy name, client will use this name and auth_token to connect to server
+[ssh]
+type = tcp
 auth_token = 123
 bind_addr = 0.0.0.0
 listen_port = 6000
+
+[web01]
+type = http
+auth_token = 123
+# if proxy type equals http, custom_domains must be set separated by commas
+custom_domains = web01.yourdomain.com,web01.yourdomain2.com
+
+[web02]
+type = http
+auth_token = 123
+custom_domains = web02.yourdomain.com

+ 75 - 11
doc/quick_start_en.md

@@ -2,7 +2,10 @@
 
 frp is easier to use compared with other similar projects.
 
-We will use a simple demo to demonstrate how to create a connection to server A's ssh port by server B with public IP address x.x.x.x(replace to the real IP address of your server).
+We will use two simple demo to demonstrate how to use frp.
+
+1. How to create a connection to **server A**'s **ssh port** by **server B** with **public IP address** x.x.x.x(replace to the real IP address of your server).
+2. How to visit web service in **server A**'s **8000 port** and **8001 port** by **web01.yourdomain.com** and **web02.yourdomain.com** through **server B** with public ID address.
 
 ### Download SourceCode
 
@@ -10,6 +13,8 @@ We will use a simple demo to demonstrate how to create a connection to server A'
 
 Or you can use `git clone https://github.com/fatedier/frp.git $GOPATH/src/github.com/fatedier/frp`.
 
+If you want to try it quickly, download the compiled program and configuration files from [https://github.com/fatedier/frp/releases](https://github.com/fatedier/frp/releases).
+
 ### Compile
 
 Enter the root directory and execute `make`, then wait until finished.
@@ -19,16 +24,18 @@ Enter the root directory and execute `make`, then wait until finished.
 ### Pre-requirement
 
 * Go environment. Version of go >= 1.4.
-* Godep (if not exist, go get will be executed to download godep when compiling)
+* Godep (if not exist, `go get` will be executed to download godep when compiling)
 
 ### Deploy
 
-1. Move `./bin/frps` and `./conf/frps.ini` to any directory of server B.
-2. Move `./bin/frpc` and `./conf/frpc.ini` to any directory of server A.
+1. Move `./bin/frps` and `./conf/frps.ini` to any directory of **server B**.
+2. Move `./bin/frpc` and `./conf/frpc.ini` to any directory of **server A**.
 3. Modify all configuration files, details in next paragraph.
-4. Execute `nohup ./frps &` or `nohup ./frps -c ./frps.ini &` in server B.
-5. Execute `nohup ./frpc &` or `nohup ./frpc -c ./frpc.ini &` in server A.
-6. Use `ssh -oPort=6000 {user}@x.x.x.x` to test if frp is work(replace {user} to real username in server A).
+4. Execute `nohup ./frps &` or `nohup ./frps -c ./frps.ini &` in **server B**.
+5. Execute `nohup ./frpc &` or `nohup ./frpc -c ./frpc.ini &` in **server A**.
+6. Use `ssh -oPort=6000 {user}@x.x.x.x` to test if frp is work(replace {user} to real username in **server A**), or visit custom domains by browser.
+
+## Tcp port forwarding
 
 ### Configuration files
 
@@ -42,8 +49,8 @@ bind_port = 7000
 log_file = ./frps.log
 log_level = info
 
-# test is the custom name of proxy and there can be many proxies with unique name in one configure file
-[test]
+# ssh is the custom name of proxy and there can be many proxies with unique name in one configure file
+[ssh]
 auth_token = 123
 bind_addr = 0.0.0.0
 # finally we connect to server A by this port
@@ -62,10 +69,67 @@ log_level = info
 # for authentication
 auth_token = 123
 
-# test is proxy name same with configure in frps.ini
-[test]
+# ssh is proxy name same with configure in frps.ini
+[ssh]
 # local port which need to be transferred
 local_port = 22
 # if use_encryption equals true, messages between frpc and frps will be encrypted, default is false
 use_encryption = true
 ```
+
+## Http port forwarding and Custom domains binding
+
+If you only want to forward port one by one, you just need refer to [Tcp port forwarding](/doc/quick_start_en.md#Tcp-port-forwarding).If you want to visit different web pages deployed in different web servers by **server B**'s **80 port**, you should specify the type as **http**.
+
+You also need to resolve your **A record** of your custom domain to [server_addr], or resolve your **CNAME record** to [server_addr] if [server_addr] is a domain.
+
+After that, you can visit your web pages in local server by custom domains.
+
+### Configuration files
+
+#### frps.ini
+
+```ini
+[common]
+bind_addr = 0.0.0.0
+bind_port = 7000
+# if you want to support vhost, specify one port for http services
+vhost_http_port = 80
+log_file = ./frps.log
+log_level = info
+
+[web01]
+type = http
+auth_token = 123
+# # if proxy type equals http, custom_domains must be set separated by commas
+custom_domains = web01.yourdomain.com
+
+[web02]
+type = http
+auth_token = 123
+custom_domains = web02.yourdomain.com
+```
+
+#### frpc.ini
+
+```ini
+[common]
+server_addr = x.x.x.x
+server_port = 7000
+log_file = ./frpc.log
+log_level = info
+auth_token = 123 
+
+# custom domains are set in frps.ini
+[web01]
+type = http
+local_ip = 127.0.0.1
+local_port = 8000
+# encryption is optional, default is false
+use_encryption = true
+
+[web02]
+type = http
+local_ip = 127.0.0.1
+local_port = 8001
+```

+ 80 - 12
doc/quick_start_zh.md

@@ -1,6 +1,9 @@
 # frp 使用文档
 
-frp 相比于其他项目而言非常易于部署和使用,这里我们用一个简单的示例演示如何通过一台拥有公网IP地址的服务器B,访问处于内网环境中的服务器A的ssh端口,服务器B的IP地址为 x.x.x.x(测试时替换为真实的IP地址)。
+相比于其他项目而言 frp 更易于部署和使用,这里我们用两个简单的示例来演示 frp 的使用过程。
+
+1. 如何通过一台拥有公网IP地址的**服务器B**,访问处于公司内部网络环境中的**服务器A**的**ssh**端口,**服务器B**的IP地址为 x.x.x.x(测试时替换为真实的IP地址)。
+2. 如何利用一台拥有公网IP地址的**服务器B**,使通过 **web01.yourdomain.com** 可以访问内网环境中**服务器A**上**8000端口**的web服务,**web02.yourdomain.com** 可以访问**服务器A**上**8001端口**的web服务。
 
 ### 下载源码
 
@@ -8,6 +11,8 @@ frp 相比于其他项目而言非常易于部署和使用,这里我们用一
 
 或者可以使用 `git clone https://github.com/fatedier/frp.git $GOPATH/src/github.com/fatedier/frp` 拷贝到相应目录下。
 
+如果您想快速进行测试,也可以根据您服务器的操作系统及架构直接下载编译好的程序及示例配置文件,[https://github.com/fatedier/frp/releases](https://github.com/fatedier/frp/releases)。
+
 ### 编译
 
 进入下载后的源码根目录,执行 `make` 命令,等待编译完成。
@@ -17,16 +22,20 @@ frp 相比于其他项目而言非常易于部署和使用,这里我们用一
 ### 依赖
 
 * go 1.4 以上版本
-* godep (如果检查不存在,编译时会通过 go get 命令安装)
+* godep (如果检查不存在,编译时会通过 `go get` 命令安装)
 
 ### 部署
 
-1. 将 ./bin/frps 和 ./conf/frps.ini 拷贝至服务器B任意目录。
-2. 将 ./bin/frpc 和 ./conf/frpc.ini 拷贝至服务器A任意目录。
-3. 修改两边的配置文件,见下一节说明。
+1. 将 ./bin/frps 和 ./conf/frps.ini 拷贝至**服务器B**任意目录。
+2. 将 ./bin/frpc 和 ./conf/frpc.ini 拷贝至**服务器A**任意目录。
+3. 根据要实现的功能修改两边的配置文件,详细内容见后续章节说明。
 4. 在服务器B执行 `nohup ./frps &` 或者 `nohup ./frps -c ./frps.ini &`。
 5. 在服务器A执行 `nohup ./frpc &` 或者 `nohup ./frpc -c ./frpc.ini &`。
-6. 通过 `ssh -oPort=6000 {user}@x.x.x.x` 测试是否能够成功连接服务器A({user}替换为服务器A上存在的真实用户)。
+6. 通过 `ssh -oPort=6000 {user}@x.x.x.x` 测试是否能够成功连接**服务器A**({user}替换为**服务器A**上存在的真实用户),或通过浏览器访问自定义域名验证 http 服务是否转发成功。
+
+## tcp 端口转发
+
+转发 tcp 端口需要按照需求修改 frps 和 frpc 的配置文件。
 
 ### 配置文件
 
@@ -40,9 +49,9 @@ bind_port = 7000
 log_file = ./frps.log
 log_level = info
 
-# test 为代理的自定义名称,可以有多个,不能重复,和frpc中名称对应
-[test]
-auth_token = 123
+# ssh 为代理的自定义名称,可以有多个,不能重复,和frpc中名称对应
+[ssh]
+auth_token = 123 
 bind_addr = 0.0.0.0
 # 最后将通过此端口访问后端服务
 listen_port = 6000
@@ -58,12 +67,71 @@ server_port = 7000
 log_file = ./frpc.log
 log_level = info
 # 用于身份验证
-auth_token = 123
+auth_token = 123 
 
-# test需要和 frps.ini 中配置一致
-[test]
+# ssh 需要和 frps.ini 中配置一致
+[ssh]
 # 需要转发的本地端口
 local_port = 22
 # 启用加密,frpc与frps之间通信加密,默认为 false
 use_encryption = true
 ```
+
+## http 端口转发,自定义域名绑定
+
+如果只需要一对一的转发,例如**服务器B**的**80端口**转发**服务器A**的**8000端口**,则只需要配置 [tcp 端口转发](/doc/quick_start_zh.md#tcp-端口转发) 即可,如果需要使**服务器B**的**80端口**可以转发至**多个**web服务端口,则需要指定代理的类型为 http,并且在 frps 的配置文件中配置用于提供 http 转发服务的端口。
+
+按照如下的内容修改配置文件后,需要将自定义域名的 **A 记录**解析到 [server_addr],如果 [server_addr] 是域名也可以将自定义域名的 **CNAME 记录**解析到 [server_addr]。
+
+之后就可以通过自定义域名访问到本地的多个 web 服务。
+
+### 配置文件
+
+#### frps.ini
+
+```ini
+[common]
+bind_addr = 0.0.0.0
+bind_port = 7000
+# 如果需要支持http类型的代理则需要指定一个端口
+vhost_http_port = 80
+log_file = ./frps.log
+log_level = info
+
+[web01]
+# type 默认为 tcp,这里需要特别指定为 http
+type = http
+auth_token = 123
+# 自定义域名绑定,如果需要同时绑定多个以英文逗号分隔
+custom_domains = web01.yourdomain.com
+
+[web02]
+type = http
+auth_token = 123
+custom_domains = web02.yourdomain.com
+```
+
+#### frpc.ini
+
+```ini
+[common]
+server_addr = x.x.x.x
+server_port = 7000
+log_file = ./frpc.log
+log_level = info
+auth_token = 123 
+
+
+# 自定义域名在 frps.ini 中配置,方便做统一管理
+[web01]
+type = http
+local_ip = 127.0.0.1
+local_port = 8000
+# 可选是否加密
+use_encryption = true
+
+[web02]
+type = http
+local_ip = 127.0.0.1
+local_port = 8001
+```

+ 1 - 1
src/frp/cmd/frpc/main.go

@@ -88,7 +88,7 @@ func main() {
 		client.ServerPort = serverPort
 	}
 
-	log.InitLog(client.LogWay, client.LogFile, client.LogLevel)
+	log.InitLog(client.LogWay, client.LogFile, client.LogLevel, client.LogMaxDays)
 
 	// wait until all control goroutine exit
 	var wait sync.WaitGroup

+ 1 - 1
src/frp/cmd/frps/control.go

@@ -30,7 +30,7 @@ import (
 
 func ProcessControlConn(l *conn.Listener) {
 	for {
-		c, err := l.GetConn()
+		c, err := l.Accept()
 		if err != nil {
 			return
 		}

+ 18 - 3
src/frp/cmd/frps/main.go

@@ -19,6 +19,7 @@ import (
 	"os"
 	"strconv"
 	"strings"
+	"time"
 
 	docopt "github.com/docopt/docopt-go"
 
@@ -26,6 +27,7 @@ import (
 	"frp/utils/conn"
 	"frp/utils/log"
 	"frp/utils/version"
+	"frp/utils/vhost"
 )
 
 var (
@@ -88,12 +90,25 @@ func main() {
 		server.BindPort = bindPort
 	}
 
-	log.InitLog(server.LogWay, server.LogFile, server.LogLevel)
+	log.InitLog(server.LogWay, server.LogFile, server.LogLevel, server.LogMaxDays)
 
 	l, err := conn.Listen(server.BindAddr, server.BindPort)
 	if err != nil {
-		log.Error("Create listener error, %v", err)
-		os.Exit(-1)
+		log.Error("Create server listener error, %v", err)
+		os.Exit(1)
+	}
+
+	// create vhost if VhostHttpPort != 0
+	if server.VhostHttpPort != 0 {
+		vhostListener, err := conn.Listen(server.BindAddr, server.VhostHttpPort)
+		if err != nil {
+			log.Error("Create vhost http listener error, %v", err)
+			os.Exit(1)
+		}
+		server.VhostMuxer, err = vhost.NewHttpMuxer(vhostListener, 30*time.Second)
+		if err != nil {
+			log.Error("Create vhost httpMuxer error, %v", err)
+		}
 	}
 
 	log.Info("Start frps success")

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

@@ -31,6 +31,7 @@ type ProxyClient struct {
 	AuthToken     string
 	LocalIp       string
 	LocalPort     int64
+	Type          string
 	UseEncryption bool
 }
 

+ 16 - 0
src/frp/models/client/config.go

@@ -28,6 +28,7 @@ var (
 	LogFile           string = "console"
 	LogWay            string = "console"
 	LogLevel          string = "info"
+	LogMaxDays        int64  = 3
 	HeartBeatInterval int64  = 20
 	HeartBeatTimeout  int64  = 90
 )
@@ -69,6 +70,11 @@ func LoadConf(confFile string) (err error) {
 		LogLevel = tmpStr
 	}
 
+	tmpStr, ok = conf.Get("common", "log_max_days")
+	if ok {
+		LogMaxDays, _ = strconv.ParseInt(tmpStr, 10, 64)
+	}
+
 	var authToken string
 	tmpStr, ok = conf.Get("common", "auth_token")
 	if ok {
@@ -105,6 +111,16 @@ func LoadConf(confFile string) (err error) {
 				return fmt.Errorf("Parse ini file error: proxy [%s] local_port not found", proxyClient.Name)
 			}
 
+			// type
+			proxyClient.Type = "tcp"
+			typeStr, ok := section["type"]
+			if ok {
+				if typeStr != "tcp" && typeStr != "http" {
+					return fmt.Errorf("Parse ini file error: proxy [%s] type error", proxyClient.Name)
+				}
+				proxyClient.Type = typeStr
+			}
+
 			// use_encryption
 			proxyClient.UseEncryption = false
 			useEncryptionStr, ok := section["use_encryption"]

+ 56 - 11
src/frp/models/server/config.go

@@ -17,19 +17,26 @@ package server
 import (
 	"fmt"
 	"strconv"
+	"strings"
 
 	ini "github.com/vaughan0/go-ini"
+
+	"frp/utils/vhost"
 )
 
 // common config
 var (
 	BindAddr         string = "0.0.0.0"
 	BindPort         int64  = 7000
+	VhostHttpPort    int64  = 0 // if VhostHttpPort equals 0, do not listen a public port for http
 	LogFile          string = "console"
 	LogWay           string = "console" // console or file
 	LogLevel         string = "info"
+	LogMaxDays       int64  = 3
 	HeartBeatTimeout int64  = 90
 	UserConnTimeout  int64  = 10
+
+	VhostMuxer *vhost.HttpMuxer
 )
 
 var ProxyServers map[string]*ProxyServer = make(map[string]*ProxyServer)
@@ -54,6 +61,13 @@ func LoadConf(confFile string) (err error) {
 		BindPort, _ = strconv.ParseInt(tmpStr, 10, 64)
 	}
 
+	tmpStr, ok = conf.Get("common", "vhost_http_port")
+	if ok {
+		VhostHttpPort, _ = strconv.ParseInt(tmpStr, 10, 64)
+	} else {
+		VhostHttpPort = 0
+	}
+
 	tmpStr, ok = conf.Get("common", "log_file")
 	if ok {
 		LogFile = tmpStr
@@ -69,30 +83,61 @@ func LoadConf(confFile string) (err error) {
 		LogLevel = tmpStr
 	}
 
+	tmpStr, ok = conf.Get("common", "log_max_days")
+	if ok {
+		LogMaxDays, _ = strconv.ParseInt(tmpStr, 10, 64)
+	}
+
 	// servers
 	for name, section := range conf {
 		if name != "common" {
 			proxyServer := &ProxyServer{}
+			proxyServer.CustomDomains = make([]string, 0)
 			proxyServer.Name = name
 
+			proxyServer.Type, ok = section["type"]
+			if ok {
+				if proxyServer.Type != "tcp" && proxyServer.Type != "http" {
+					return fmt.Errorf("Parse ini file error: proxy [%s] type error", proxyServer.Name)
+				}
+			} else {
+				proxyServer.Type = "tcp"
+			}
+
 			proxyServer.AuthToken, ok = section["auth_token"]
 			if !ok {
 				return fmt.Errorf("Parse ini file error: proxy [%s] no auth_token found", proxyServer.Name)
 			}
 
-			proxyServer.BindAddr, ok = section["bind_addr"]
-			if !ok {
-				proxyServer.BindAddr = "0.0.0.0"
-			}
+			// for tcp
+			if proxyServer.Type == "tcp" {
+				proxyServer.BindAddr, ok = section["bind_addr"]
+				if !ok {
+					proxyServer.BindAddr = "0.0.0.0"
+				}
 
-			portStr, ok := section["listen_port"]
-			if ok {
-				proxyServer.ListenPort, err = strconv.ParseInt(portStr, 10, 64)
-				if err != nil {
-					return fmt.Errorf("Parse ini file error: proxy [%s] listen_port error", proxyServer.Name)
+				portStr, ok := section["listen_port"]
+				if ok {
+					proxyServer.ListenPort, err = strconv.ParseInt(portStr, 10, 64)
+					if err != nil {
+						return fmt.Errorf("Parse ini file error: proxy [%s] listen_port error", proxyServer.Name)
+					}
+				} else {
+					return fmt.Errorf("Parse ini file error: proxy [%s] listen_port not found", proxyServer.Name)
+				}
+			} else if proxyServer.Type == "http" {
+				// for http
+				domainStr, ok := section["custom_domains"]
+				if ok {
+					var suffix string
+					if VhostHttpPort != 80 {
+						suffix = fmt.Sprintf(":%d", VhostHttpPort)
+					}
+					proxyServer.CustomDomains = strings.Split(domainStr, ",")
+					for i, domain := range proxyServer.CustomDomains {
+						proxyServer.CustomDomains[i] = strings.ToLower(strings.TrimSpace(domain)) + suffix
+					}
 				}
-			} else {
-				return fmt.Errorf("Parse ini file error: proxy [%s] listen_port not found", proxyServer.Name)
 			}
 
 			proxyServer.Init()

+ 71 - 45
src/frp/models/server/server.go

@@ -24,15 +24,22 @@ import (
 	"frp/utils/log"
 )
 
+type Listener interface {
+	Accept() (*conn.Conn, error)
+	Close() error
+}
+
 type ProxyServer struct {
 	Name          string
 	AuthToken     string
-	UseEncryption bool
+	Type          string
 	BindAddr      string
 	ListenPort    int64
-	Status        int64
+	UseEncryption bool
+	CustomDomains []string
 
-	listener     *conn.Listener  // accept new connection from remote users
+	Status       int64
+	listeners    []Listener      // accept new connection from remote users
 	ctlMsgChan   chan int64      // every time accept a new user conn, put "1" to the channel
 	workConnChan chan *conn.Conn // get new work conns from control goroutine
 	userConnList *list.List      // store user conns
@@ -44,6 +51,7 @@ func (p *ProxyServer) Init() {
 	p.workConnChan = make(chan *conn.Conn)
 	p.ctlMsgChan = make(chan int64)
 	p.userConnList = list.New()
+	p.listeners = make([]Listener, 0)
 }
 
 func (p *ProxyServer) Lock() {
@@ -57,57 +65,71 @@ func (p *ProxyServer) Unlock() {
 // start listening for user conns
 func (p *ProxyServer) Start() (err error) {
 	p.Init()
-	p.listener, err = conn.Listen(p.BindAddr, p.ListenPort)
-	if err != nil {
-		return err
+	if p.Type == "tcp" {
+		l, err := conn.Listen(p.BindAddr, p.ListenPort)
+		if err != nil {
+			return err
+		}
+		p.listeners = append(p.listeners, l)
+	} else if p.Type == "http" {
+		for _, domain := range p.CustomDomains {
+			l, err := VhostMuxer.Listen(domain)
+			if err != nil {
+				return err
+			}
+			p.listeners = append(p.listeners, l)
+		}
 	}
 
 	p.Status = consts.Working
 
 	// start a goroutine for listener to accept user connection
-	go func() {
-		for {
-			// block
-			// if listener is closed, err returned
-			c, err := p.listener.GetConn()
-			if err != nil {
-				log.Info("ProxyName [%s], listener is closed", p.Name)
-				return
-			}
-			log.Debug("ProxyName [%s], get one new user conn [%s]", p.Name, c.GetRemoteAddr())
-
-			// insert into list
-			p.Lock()
-			if p.Status != consts.Working {
-				log.Debug("ProxyName [%s] is not working, new user conn close", p.Name)
-				c.Close()
-				p.Unlock()
-				return
-			}
-			p.userConnList.PushBack(c)
-			p.Unlock()
-
-			// put msg to control conn
-			p.ctlMsgChan <- 1
+	for _, listener := range p.listeners {
+		go func(l Listener) {
+			for {
+				// block
+				// if listener is closed, err returned
+				c, err := l.Accept()
+				if err != nil {
+					log.Info("ProxyName [%s], listener is closed", p.Name)
+					return
+				}
+				log.Debug("ProxyName [%s], get one new user conn [%s]", p.Name, c.GetRemoteAddr())
 
-			// set timeout
-			time.AfterFunc(time.Duration(UserConnTimeout)*time.Second, func() {
+				// insert into list
 				p.Lock()
-				defer p.Unlock()
-				element := p.userConnList.Front()
-				if element == nil {
+				if p.Status != consts.Working {
+					log.Debug("ProxyName [%s] is not working, new user conn close", p.Name)
+					c.Close()
+					p.Unlock()
 					return
 				}
+				p.userConnList.PushBack(c)
+				p.Unlock()
 
-				userConn := element.Value.(*conn.Conn)
-				if userConn == c {
-					log.Warn("ProxyName [%s], user conn [%s] timeout", p.Name, c.GetRemoteAddr())
-				}
-			})
-		}
-	}()
+				// put msg to control conn
+				p.ctlMsgChan <- 1
+
+				// set timeout
+				time.AfterFunc(time.Duration(UserConnTimeout)*time.Second, func() {
+					p.Lock()
+					element := p.userConnList.Front()
+					p.Unlock()
+					if element == nil {
+						return
+					}
+
+					userConn := element.Value.(*conn.Conn)
+					if userConn == c {
+						log.Warn("ProxyName [%s], user conn [%s] timeout", p.Name, c.GetRemoteAddr())
+						userConn.Close()
+					}
+				})
+			}
+		}(listener)
+	}
 
-	// start another goroutine for join two conns from client and user
+	// start another goroutine for join two conns from frpc and user
 	go func() {
 		for {
 			workConn, ok := <-p.workConnChan
@@ -149,8 +171,12 @@ func (p *ProxyServer) Close() {
 	p.Lock()
 	if p.Status != consts.Closed {
 		p.Status = consts.Closed
-		if p.listener != nil {
-			p.listener.Close()
+		if len(p.listeners) != 0 {
+			for _, l := range p.listeners {
+				if l != nil {
+					l.Close()
+				}
+			}
 		}
 		close(p.ctlMsgChan)
 		close(p.workConnChan)

+ 23 - 9
src/frp/utils/conn/conn.go

@@ -20,6 +20,7 @@ import (
 	"io"
 	"net"
 	"sync"
+	"time"
 
 	"frp/utils/log"
 	"frp/utils/pcrypto"
@@ -28,7 +29,7 @@ import (
 type Listener struct {
 	addr      net.Addr
 	l         *net.TCPListener
-	conns     chan *Conn
+	accept    chan *Conn
 	closeFlag bool
 }
 
@@ -42,7 +43,7 @@ func Listen(bindAddr string, bindPort int64) (l *Listener, err error) {
 	l = &Listener{
 		addr:      listener.Addr(),
 		l:         listener,
-		conns:     make(chan *Conn),
+		accept:    make(chan *Conn),
 		closeFlag: false,
 	}
 
@@ -61,7 +62,7 @@ func Listen(bindAddr string, bindPort int64) (l *Listener, err error) {
 				closeFlag: false,
 			}
 			c.Reader = bufio.NewReader(c.TcpConn)
-			l.conns <- c
+			l.accept <- c
 		}
 	}()
 	return l, err
@@ -69,30 +70,38 @@ func Listen(bindAddr string, bindPort int64) (l *Listener, err error) {
 
 // wait util get one new connection or listener is closed
 // if listener is closed, err returned
-func (l *Listener) GetConn() (conn *Conn, err error) {
-	var ok bool
-	conn, ok = <-l.conns
+func (l *Listener) Accept() (*Conn, error) {
+	conn, ok := <-l.accept
 	if !ok {
 		return conn, fmt.Errorf("channel close")
 	}
 	return conn, nil
 }
 
-func (l *Listener) Close() {
+func (l *Listener) Close() error {
 	if l.l != nil && l.closeFlag == false {
 		l.closeFlag = true
 		l.l.Close()
-		close(l.conns)
+		close(l.accept)
 	}
+	return nil
 }
 
 // wrap for TCPConn
 type Conn struct {
-	TcpConn   *net.TCPConn
+	TcpConn   net.Conn
 	Reader    *bufio.Reader
 	closeFlag bool
 }
 
+func NewConn(conn net.Conn) (c *Conn) {
+	c = &Conn{}
+	c.TcpConn = conn
+	c.Reader = bufio.NewReader(c.TcpConn)
+	c.closeFlag = false
+	return c
+}
+
 func ConnectServer(host string, port int64) (c *Conn, err error) {
 	c = &Conn{}
 	servertAddr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("%s:%d", host, port))
@@ -131,6 +140,11 @@ func (c *Conn) Write(content string) (err error) {
 
 }
 
+func (c *Conn) SetDeadline(t time.Time) error {
+	err := c.TcpConn.SetDeadline(t)
+	return err
+}
+
 func (c *Conn) Close() {
 	if c.TcpConn != nil && c.closeFlag == false {
 		c.closeFlag = true

+ 7 - 7
src/frp/utils/log/log.go

@@ -15,6 +15,7 @@
 package log
 
 import (
+	"fmt"
 	"github.com/astaxie/beego/logs"
 )
 
@@ -26,24 +27,24 @@ func init() {
 	Log.SetLogFuncCallDepth(Log.GetLogFuncCallDepth() + 1)
 }
 
-func InitLog(logWay string, logFile string, logLevel string) {
-	SetLogFile(logWay, logFile)
+func InitLog(logWay string, logFile string, logLevel string, maxdays int64) {
+	SetLogFile(logWay, logFile, maxdays)
 	SetLogLevel(logLevel)
 }
 
-// logWay: such as file or console
-func SetLogFile(logWay string, logFile string) {
+// logWay: file or console
+func SetLogFile(logWay string, logFile string, maxdays int64) {
 	if logWay == "console" {
 		Log.SetLogger("console", "")
 	} else {
-		Log.SetLogger("file", `{"filename": "`+logFile+`"}`)
+		params := fmt.Sprintf(`{"filename": "%s", "maxdays": %d}`, logFile, maxdays)
+		Log.SetLogger("file", params)
 	}
 }
 
 // value: error, warning, info, debug
 func SetLogLevel(logLevel string) {
 	level := 4 // warning
-
 	switch logLevel {
 	case "error":
 		level = 3
@@ -56,7 +57,6 @@ func SetLogLevel(logLevel string) {
 	default:
 		level = 4
 	}
-
 	Log.SetLevel(level)
 }
 

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

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

+ 193 - 0
src/frp/utils/vhost/vhost.go

@@ -0,0 +1,193 @@
+// Copyright 2016 fatedier, fatedier@gmail.com
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package vhost
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+	"strings"
+	"sync"
+	"time"
+
+	"frp/utils/conn"
+)
+
+type muxFunc func(*conn.Conn) (net.Conn, string, error)
+
+type VhostMuxer struct {
+	listener    *conn.Listener
+	timeout     time.Duration
+	vhostFunc   muxFunc
+	registryMap map[string]*Listener
+	mutex       sync.RWMutex
+}
+
+func NewVhostMuxer(listener *conn.Listener, vhostFunc muxFunc, timeout time.Duration) (mux *VhostMuxer, err error) {
+	mux = &VhostMuxer{
+		listener:    listener,
+		timeout:     timeout,
+		vhostFunc:   vhostFunc,
+		registryMap: make(map[string]*Listener),
+	}
+	go mux.run()
+	return mux, nil
+}
+
+func (v *VhostMuxer) Listen(name string) (l *Listener, err error) {
+	v.mutex.Lock()
+	defer v.mutex.Unlock()
+	if _, exist := v.registryMap[name]; exist {
+		return nil, fmt.Errorf("name %s is already bound", name)
+	}
+
+	l = &Listener{
+		name:   name,
+		mux:    v,
+		accept: make(chan *conn.Conn),
+	}
+	v.registryMap[name] = l
+	return l, nil
+}
+
+func (v *VhostMuxer) getListener(name string) (l *Listener, exist bool) {
+	v.mutex.RLock()
+	defer v.mutex.RUnlock()
+	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)
+}
+
+func (v *VhostMuxer) run() {
+	for {
+		conn, err := v.listener.Accept()
+		if err != nil {
+			return
+		}
+		go v.handle(conn)
+	}
+}
+
+func (v *VhostMuxer) handle(c *conn.Conn) {
+	if err := c.SetDeadline(time.Now().Add(v.timeout)); err != nil {
+		return
+	}
+
+	sConn, name, err := v.vhostFunc(c)
+	if err != nil {
+		return
+	}
+
+	name = strings.ToLower(name)
+
+	l, ok := v.getListener(name)
+	if !ok {
+		return
+	}
+
+	if err = sConn.SetDeadline(time.Time{}); err != nil {
+		return
+	}
+	c.TcpConn = sConn
+
+	l.accept <- c
+}
+
+type HttpMuxer struct {
+	*VhostMuxer
+}
+
+func GetHttpHostname(c *conn.Conn) (_ net.Conn, routerName string, err error) {
+	sc, rd := newShareConn(c.TcpConn)
+
+	request, err := http.ReadRequest(bufio.NewReader(rd))
+	if err != nil {
+		return sc, "", err
+	}
+	routerName = request.Host
+	request.Body.Close()
+
+	return sc, routerName, nil
+}
+
+func NewHttpMuxer(listener *conn.Listener, timeout time.Duration) (*HttpMuxer, error) {
+	mux, err := NewVhostMuxer(listener, GetHttpHostname, timeout)
+	return &HttpMuxer{mux}, err
+}
+
+type Listener struct {
+	name   string
+	mux    *VhostMuxer // for closing VhostMuxer
+	accept chan *conn.Conn
+}
+
+func (l *Listener) Accept() (*conn.Conn, error) {
+	conn, ok := <-l.accept
+	if !ok {
+		return nil, fmt.Errorf("Listener closed")
+	}
+	return conn, nil
+}
+
+func (l *Listener) Close() error {
+	l.mux.unRegister(l.name)
+	close(l.accept)
+	return nil
+}
+
+func (l *Listener) Name() string {
+	return l.name
+}
+
+type sharedConn struct {
+	net.Conn
+	sync.Mutex
+	buff *bytes.Buffer
+}
+
+func newShareConn(conn net.Conn) (*sharedConn, io.Reader) {
+	sc := &sharedConn{
+		Conn: conn,
+		buff: bytes.NewBuffer(make([]byte, 0, 1024)),
+	}
+	return sc, io.TeeReader(conn, sc.buff)
+}
+
+func (sc *sharedConn) Read(p []byte) (n int, err error) {
+	sc.Lock()
+	if sc.buff == nil {
+		sc.Unlock()
+		return sc.Conn.Read(p)
+	}
+	n, err = sc.buff.Read(p)
+
+	if err == io.EOF {
+		sc.buff = nil
+		var n2 int
+		n2, err = sc.Conn.Read(p[n:])
+
+		n += n2
+	}
+	sc.Unlock()
+	return
+}