Browse Source

Merge pull request #344 from fatedier/dev

bump version to 0.11.0
fatedier 7 years ago
parent
commit
712afed0ab

+ 1 - 2
.travis.yml

@@ -2,8 +2,7 @@ sudo: false
 language: go
 
 go:
-    - 1.7.5
-    - 1.8
+    - 1.8.x
 
 install:
     - make

+ 1 - 1
Dockerfile

@@ -1,4 +1,4 @@
-FROM golang:1.6
+FROM golang:1.8
 
 COPY . /go/src/github.com/fatedier/frp
 

+ 1 - 1
Makefile

@@ -42,7 +42,7 @@ alltest: gotest
 clean:
 	rm -f ./bin/frpc
 	rm -f ./bin/frps
-	cd ./test && ./clean_test.sh && cd -
+	cd ./tests && ./clean_test.sh && cd -
 
 save:
 	godep save ./...

+ 1 - 1
assets/static/index.html

@@ -1 +1 @@
-<!DOCTYPE html> <html lang=en> <head> <meta charset=utf-8> <title>frps dashboard</title> <link rel="shortcut icon" href="favicon.ico"></head> <body> <div id=app></div> <script type="text/javascript" src="manifest.js?b52826060da73c6b5a10"></script><script type="text/javascript" src="vendor.js?66dfcf2d1c500e900413"></script><script type="text/javascript" src="index.js?ceb589f1be7a87112dbd"></script></body> </html> 
+<!DOCTYPE html> <html lang=en> <head> <meta charset=utf-8> <title>frps dashboard</title> <link rel="shortcut icon" href="favicon.ico"></head> <body> <div id=app></div> <script type="text/javascript" src="manifest.js?5217927b66cc446ebfd3"></script><script type="text/javascript" src="vendor.js?66dfcf2d1c500e900413"></script><script type="text/javascript" src="index.js?bf962cded96400bef9a0"></script></body> </html> 

File diff suppressed because it is too large
+ 0 - 0
assets/static/index.js


+ 1 - 1
assets/static/manifest.js

@@ -1 +1 @@
-!function(e){function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}var n=window.webpackJsonp;window.webpackJsonp=function(t,c,u){for(var i,a,f,l=0,s=[];l<t.length;l++)a=t[l],o[a]&&s.push(o[a][0]),o[a]=0;for(i in c)Object.prototype.hasOwnProperty.call(c,i)&&(e[i]=c[i]);for(n&&n(t,c,u);s.length;)s.shift()();if(u)for(l=0;l<u.length;l++)f=r(r.s=u[l]);return f};var t={},o={2:0};r.e=function(e){function n(){u.onerror=u.onload=null,clearTimeout(i);var r=o[e];0!==r&&(r&&r[1](new Error("Loading chunk "+e+" failed.")),o[e]=void 0)}if(0===o[e])return Promise.resolve();if(o[e])return o[e][2];var t=new Promise(function(r,n){o[e]=[r,n]});o[e][2]=t;var c=document.getElementsByTagName("head")[0],u=document.createElement("script");u.type="text/javascript",u.charset="utf-8",u.async=!0,u.timeout=12e4,r.nc&&u.setAttribute("nonce",r.nc),u.src=r.p+""+e+".js?"+{0:"ceb589f1be7a87112dbd",1:"66dfcf2d1c500e900413"}[e];var i=setTimeout(n,12e4);return u.onerror=u.onload=n,c.appendChild(u),t},r.m=e,r.c=t,r.i=function(e){return e},r.d=function(e,n,t){r.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:t})},r.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(n,"a",n),n},r.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},r.p="",r.oe=function(e){throw console.error(e),e}}([]);
+!function(e){function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}var n=window.webpackJsonp;window.webpackJsonp=function(t,c,u){for(var i,a,f,l=0,s=[];l<t.length;l++)a=t[l],o[a]&&s.push(o[a][0]),o[a]=0;for(i in c)Object.prototype.hasOwnProperty.call(c,i)&&(e[i]=c[i]);for(n&&n(t,c,u);s.length;)s.shift()();if(u)for(l=0;l<u.length;l++)f=r(r.s=u[l]);return f};var t={},o={2:0};r.e=function(e){function n(){u.onerror=u.onload=null,clearTimeout(i);var r=o[e];0!==r&&(r&&r[1](new Error("Loading chunk "+e+" failed.")),o[e]=void 0)}if(0===o[e])return Promise.resolve();if(o[e])return o[e][2];var t=new Promise(function(r,n){o[e]=[r,n]});o[e][2]=t;var c=document.getElementsByTagName("head")[0],u=document.createElement("script");u.type="text/javascript",u.charset="utf-8",u.async=!0,u.timeout=12e4,r.nc&&u.setAttribute("nonce",r.nc),u.src=r.p+""+e+".js?"+{0:"bf962cded96400bef9a0",1:"66dfcf2d1c500e900413"}[e];var i=setTimeout(n,12e4);return u.onerror=u.onload=n,c.appendChild(u),t},r.m=e,r.c=t,r.i=function(e){return e},r.d=function(e,n,t){r.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:t})},r.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(n,"a",n),n},r.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},r.p="",r.oe=function(e){throw console.error(e),e}}([]);

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


+ 15 - 4
client/control.go

@@ -106,9 +106,20 @@ func NewControl(svr *Service, pxyCfgs map[string]config.ProxyConf) *Control {
 // 7. In controler(): start new reader(), writer(), manager()
 // controler() will keep running
 func (ctl *Control) Run() error {
-	err := ctl.login()
-	if err != nil {
-		return err
+	for {
+		err := ctl.login()
+		if err != nil {
+			// if login_fail_exit is true, just exit this program
+			// otherwise sleep a while and continues relogin to server
+			if config.ClientCommonCfg.LoginFailExit {
+				return err
+			} else {
+				ctl.Warn("login to server fail: %v", err)
+				time.Sleep(30 * time.Second)
+			}
+		} else {
+			break
+		}
 	}
 
 	go ctl.controler()
@@ -166,7 +177,7 @@ func (ctl *Control) NewWorkConn() {
 
 	// dispatch this work connection to related proxy
 	if pxy, ok := ctl.proxies[startMsg.ProxyName]; ok {
-		workConn.Info("start a new work connection, localAddr: %s remoteAddr: %s", workConn.LocalAddr().String(), workConn.RemoteAddr().String())
+		workConn.Debug("start a new work connection, localAddr: %s remoteAddr: %s", workConn.LocalAddr().String(), workConn.RemoteAddr().String())
 		go pxy.InWorkConn(workConn)
 	} else {
 		workConn.Close()

+ 62 - 20
client/proxy.go

@@ -23,6 +23,7 @@ import (
 
 	"github.com/fatedier/frp/models/config"
 	"github.com/fatedier/frp/models/msg"
+	"github.com/fatedier/frp/models/plugin"
 	"github.com/fatedier/frp/models/proto/tcp"
 	"github.com/fatedier/frp/models/proto/udp"
 	"github.com/fatedier/frp/utils/errors"
@@ -81,57 +82,84 @@ type BaseProxy struct {
 type TcpProxy struct {
 	BaseProxy
 
-	cfg *config.TcpProxyConf
+	cfg         *config.TcpProxyConf
+	proxyPlugin plugin.Plugin
 }
 
 func (pxy *TcpProxy) Run() (err error) {
+	if pxy.cfg.Plugin != "" {
+		pxy.proxyPlugin, err = plugin.Create(pxy.cfg.Plugin, pxy.cfg.PluginParams)
+		if err != nil {
+			return
+		}
+	}
 	return
 }
 
 func (pxy *TcpProxy) Close() {
+	if pxy.proxyPlugin != nil {
+		pxy.proxyPlugin.Close()
+	}
 }
 
 func (pxy *TcpProxy) InWorkConn(conn frpNet.Conn) {
-	defer conn.Close()
-	HandleTcpWorkConnection(&pxy.cfg.LocalSvrConf, &pxy.cfg.BaseProxyConf, conn)
+	HandleTcpWorkConnection(&pxy.cfg.LocalSvrConf, pxy.proxyPlugin, &pxy.cfg.BaseProxyConf, conn)
 }
 
 // HTTP
 type HttpProxy struct {
 	BaseProxy
 
-	cfg *config.HttpProxyConf
+	cfg         *config.HttpProxyConf
+	proxyPlugin plugin.Plugin
 }
 
 func (pxy *HttpProxy) Run() (err error) {
+	if pxy.cfg.Plugin != "" {
+		pxy.proxyPlugin, err = plugin.Create(pxy.cfg.Plugin, pxy.cfg.PluginParams)
+		if err != nil {
+			return
+		}
+	}
 	return
 }
 
 func (pxy *HttpProxy) Close() {
+	if pxy.proxyPlugin != nil {
+		pxy.proxyPlugin.Close()
+	}
 }
 
 func (pxy *HttpProxy) InWorkConn(conn frpNet.Conn) {
-	defer conn.Close()
-	HandleTcpWorkConnection(&pxy.cfg.LocalSvrConf, &pxy.cfg.BaseProxyConf, conn)
+	HandleTcpWorkConnection(&pxy.cfg.LocalSvrConf, pxy.proxyPlugin, &pxy.cfg.BaseProxyConf, conn)
 }
 
 // HTTPS
 type HttpsProxy struct {
 	BaseProxy
 
-	cfg *config.HttpsProxyConf
+	cfg         *config.HttpsProxyConf
+	proxyPlugin plugin.Plugin
 }
 
 func (pxy *HttpsProxy) Run() (err error) {
+	if pxy.cfg.Plugin != "" {
+		pxy.proxyPlugin, err = plugin.Create(pxy.cfg.Plugin, pxy.cfg.PluginParams)
+		if err != nil {
+			return
+		}
+	}
 	return
 }
 
 func (pxy *HttpsProxy) Close() {
+	if pxy.proxyPlugin != nil {
+		pxy.proxyPlugin.Close()
+	}
 }
 
 func (pxy *HttpsProxy) InWorkConn(conn frpNet.Conn) {
-	defer conn.Close()
-	HandleTcpWorkConnection(&pxy.cfg.LocalSvrConf, &pxy.cfg.BaseProxyConf, conn)
+	HandleTcpWorkConnection(&pxy.cfg.LocalSvrConf, pxy.proxyPlugin, &pxy.cfg.BaseProxyConf, conn)
 }
 
 // UDP
@@ -240,14 +268,13 @@ func (pxy *UdpProxy) InWorkConn(conn frpNet.Conn) {
 }
 
 // Common handler for tcp work connections.
-func HandleTcpWorkConnection(localInfo *config.LocalSvrConf, baseInfo *config.BaseProxyConf, workConn frpNet.Conn) {
-	localConn, err := frpNet.ConnectTcpServer(fmt.Sprintf("%s:%d", localInfo.LocalIp, localInfo.LocalPort))
-	if err != nil {
-		workConn.Error("connect to local service [%s:%d] error: %v", localInfo.LocalIp, localInfo.LocalPort, err)
-		return
-	}
+func HandleTcpWorkConnection(localInfo *config.LocalSvrConf, proxyPlugin plugin.Plugin,
+	baseInfo *config.BaseProxyConf, workConn frpNet.Conn) {
 
-	var remote io.ReadWriteCloser
+	var (
+		remote io.ReadWriteCloser
+		err    error
+	)
 	remote = workConn
 	if baseInfo.UseEncryption {
 		remote, err = tcp.WithEncryption(remote, []byte(config.ClientCommonCfg.PrivilegeToken))
@@ -259,8 +286,23 @@ func HandleTcpWorkConnection(localInfo *config.LocalSvrConf, baseInfo *config.Ba
 	if baseInfo.UseCompression {
 		remote = tcp.WithCompression(remote)
 	}
-	workConn.Debug("join connections, localConn(l[%s] r[%s]) workConn(l[%s] r[%s])", localConn.LocalAddr().String(),
-		localConn.RemoteAddr().String(), workConn.LocalAddr().String(), workConn.RemoteAddr().String())
-	tcp.Join(localConn, remote)
-	workConn.Debug("join connections closed")
+
+	if proxyPlugin != nil {
+		// if plugin is set, let plugin handle connections first
+		workConn.Debug("handle by plugin: %s", proxyPlugin.Name())
+		proxyPlugin.Handle(remote)
+		workConn.Debug("handle by plugin finished")
+		return
+	} else {
+		localConn, err := frpNet.ConnectTcpServer(fmt.Sprintf("%s:%d", localInfo.LocalIp, localInfo.LocalPort))
+		if err != nil {
+			workConn.Error("connect to local service [%s:%d] error: %v", localInfo.LocalIp, localInfo.LocalPort, err)
+			return
+		}
+
+		workConn.Debug("join connections, localConn(l[%s] r[%s]) workConn(l[%s] r[%s])", localConn.LocalAddr().String(),
+			localConn.RemoteAddr().String(), workConn.LocalAddr().String(), workConn.RemoteAddr().String())
+		tcp.Join(localConn, remote)
+		workConn.Debug("join connections closed")
+	}
 }

+ 2 - 2
cmd/frpc/main.go

@@ -46,7 +46,7 @@ Options:
     --log-level=<log_level>     set log level: debug, info, warn, error
     --server-addr=<server_addr> addr which frps is listening for, example: 0.0.0.0:7000
     -h --help                   show this screen
-    --version                   show version
+    -v --version                show version
 `
 
 func main() {
@@ -106,7 +106,7 @@ func main() {
 		}
 	}
 
-	pxyCfgs, err := config.LoadProxyConfFromFile(conf)
+	pxyCfgs, err := config.LoadProxyConfFromFile(config.ClientCommonCfg.User, conf, config.ClientCommonCfg.Start)
 	if err != nil {
 		fmt.Println(err)
 		os.Exit(1)

+ 2 - 76
conf/frpc.ini

@@ -1,83 +1,9 @@
-# [common] is integral section
 [common]
-# A literal address or host name for IPv6 must be enclosed
-# in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80"
-server_addr = 0.0.0.0
+server_addr = 127.0.0.1
 server_port = 7000
 
-# if you want to connect frps by http proxy, you can set http_proxy here or in global environment variables
-# http_proxy = http://user:pwd@192.168.1.128:8080
-
-# console or real logFile path like ./frpc.log
-log_file = ./frpc.log
-
-# trace, debug, info, warn, error
-log_level = info
-
-log_max_days = 3
-
-# for authentication
-privilege_token = 12345678
-
-# connections will be established in advance, default value is zero
-pool_count = 5
-
-# if tcp stream multiplexing is used, default is true, it must be same with frps
-tcp_mux = true
-
-# your proxy name will be changed to {user}.{proxy}
-user = your_name
-
-# heartbeat configure, it's not recommended to modify the default value
-# the default value of heartbeat_interval is 10 and heartbeat_timeout is 90
-# heartbeat_interval = 30
-# heartbeat_timeout = 90
-
-# ssh is the proxy name same as server's configuration
-# if user in [common] section is not empty, it will be changed to {user}.{proxy} such as your_name.ssh
 [ssh]
-# tcp | udp | http | https, 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 = false
-# if true, message will be compressed
-use_compression = false
-# remote port listen by frps
-remote_port = 6001
-
-[dns]
-type = udp
-local_ip = 114.114.114.114
-local_port = 53
-remote_port = 6002
-use_encryption = false
-use_compression = false
-
-# 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
-[web01]
-type = http
-local_ip = 127.0.0.1
-local_port = 80
-use_encryption = false
-use_compression = true
-# http username and password are safety certification for http protocol
-# if not set, you can access this custom_domains without certification
-http_user = admin
-http_pwd = admin
-# if domain for frps is frps.com, then you can access [web01] proxy by URL http://test.frps.com
-subdomain = web01
-custom_domains = web02.yourdomain.com
-# locations is only useful for http type
-locations = /,/pic
-host_header_rewrite = example.com
-
-[web02]
-type = https
-local_ip = 127.0.0.1
-local_port = 8000
-use_encryption = false
-use_compression = false 
-subdomain = web01
-custom_domains = web02.yourdomain.com
+remote_port = 6000

+ 107 - 0
conf/frpc_full.ini

@@ -0,0 +1,107 @@
+# [common] is integral section
+[common]
+# A literal address or host name for IPv6 must be enclosed
+# in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80"
+server_addr = 0.0.0.0
+server_port = 7000
+
+# if you want to connect frps by http proxy, you can set http_proxy here or in global environment variables
+# http_proxy = http://user:pwd@192.168.1.128:8080
+
+# console or real logFile path like ./frpc.log
+log_file = ./frpc.log
+
+# trace, debug, info, warn, error
+log_level = info
+
+log_max_days = 3
+
+# for authentication
+privilege_token = 12345678
+
+# connections will be established in advance, default value is zero
+pool_count = 5
+
+# if tcp stream multiplexing is used, default is true, it must be same with frps
+tcp_mux = true
+
+# your proxy name will be changed to {user}.{proxy}
+user = your_name
+
+# decide if exit program when first login failed, otherwise continuous relogin to frps
+# default is true
+login_fail_exit = true
+
+# proxy names you want to start divided by ','
+# default is empty, means all proxies
+# start = ssh,dns
+
+# heartbeat configure, it's not recommended to modify the default value
+# the default value of heartbeat_interval is 10 and heartbeat_timeout is 90
+# heartbeat_interval = 30
+# heartbeat_timeout = 90
+
+# ssh is the proxy name same as server's configuration
+# if user in [common] section is not empty, it will be changed to {user}.{proxy} such as your_name.ssh
+[ssh]
+# tcp | udp | http | https, 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 = false
+# if true, message will be compressed
+use_compression = false
+# remote port listen by frps
+remote_port = 6001
+
+[dns]
+type = udp
+local_ip = 114.114.114.114
+local_port = 53
+remote_port = 6002
+use_encryption = false
+use_compression = false
+
+# 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
+[web01]
+type = http
+local_ip = 127.0.0.1
+local_port = 80
+use_encryption = false
+use_compression = true
+# http username and password are safety certification for http protocol
+# if not set, you can access this custom_domains without certification
+http_user = admin
+http_pwd = admin
+# if domain for frps is frps.com, then you can access [web01] proxy by URL http://test.frps.com
+subdomain = web01
+custom_domains = web02.yourdomain.com
+# locations is only useful for http type
+locations = /,/pic
+host_header_rewrite = example.com
+
+[web02]
+type = https
+local_ip = 127.0.0.1
+local_port = 8000
+use_encryption = false
+use_compression = false 
+subdomain = web01
+custom_domains = web02.yourdomain.com
+
+[plugin_unix_domain_socket]
+type = tcp
+remote_port = 6003
+# if plugin is defined, local_ip and local_port is useless
+# plugin will handle connections got from frps
+plugin = unix_domain_socket
+# params set with prefix "plugin_" that plugin needed
+plugin_unix_path = /var/run/docker.sock
+
+[plugin_http_proxy]
+type = tcp
+remote_port = 6004
+plugin = http_proxy
+plugin_http_user = abc
+plugin_http_passwd = abc

+ 0 - 10
conf/frpc_min.ini

@@ -1,10 +0,0 @@
-[common]
-server_addr = 0.0.0.0
-server_port = 7000
-#privilege_token = 12345678
-
-[ssh]
-type = tcp
-local_ip = 127.0.0.1
-local_port = 22
-remote_port = 6000

+ 0 - 49
conf/frps.ini

@@ -1,51 +1,2 @@
-# [common] is integral section
 [common]
-# A literal address or host name for IPv6 must be enclosed
-# in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80"
-bind_addr = 0.0.0.0
 bind_port = 7000
-
-# if you want to support virtual host, you must set the http port for listening (optional)
-vhost_http_port = 80
-vhost_https_port = 443
-
-# if you want to configure or reload frps by dashboard, dashboard_port must be set
-dashboard_port = 7500
-
-# dashboard user and pwd for basic auth protect, if not set, both default value is admin
-dashboard_user = admin
-dashboard_pwd = admin
-
-# dashboard assets directory(only for debug mode)
-# assets_dir = ./static
-# console or real logFile path like ./frps.log
-log_file = ./frps.log
-
-# trace, debug, info, warn, error
-log_level = info
-
-log_max_days = 3
-
-# privilege mode is the only supported mode since v0.10.0
-privilege_token = 12345678
-
-# heartbeat configure, it's not recommended to modify the default value
-# the default value of heartbeat_timeout is 90
-# heartbeat_timeout = 90
-
-# only allow frpc to bind ports you list, if you set nothing, there won't be any limit
-privilege_allow_ports = 2000-3000,3001,3003,4000-50000
-
-# pool_count in each proxy will change to max_pool_count if they exceed the maximum value
-max_pool_count = 5
-
-# authentication_timeout means the timeout interval (seconds) when the frpc connects frps
-# if authentication_timeout is zero, the time is not verified, default is 900s
-authentication_timeout = 900
-
-# if subdomain_host is not empty, you can set subdomain when type is http or https in frpc's configure file
-# when subdomain is test, the host used by routing is test.frps.com
-subdomain_host = frps.com
-
-# if tcp stream multiplexing is used, default is true
-tcp_mux = true

+ 51 - 0
conf/frps_full.ini

@@ -0,0 +1,51 @@
+# [common] is integral section
+[common]
+# A literal address or host name for IPv6 must be enclosed
+# in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80"
+bind_addr = 0.0.0.0
+bind_port = 7000
+
+# if you want to support virtual host, you must set the http port for listening (optional)
+vhost_http_port = 80
+vhost_https_port = 443
+
+# if you want to configure or reload frps by dashboard, dashboard_port must be set
+dashboard_port = 7500
+
+# dashboard user and pwd for basic auth protect, if not set, both default value is admin
+dashboard_user = admin
+dashboard_pwd = admin
+
+# dashboard assets directory(only for debug mode)
+# assets_dir = ./static
+# console or real logFile path like ./frps.log
+log_file = ./frps.log
+
+# trace, debug, info, warn, error
+log_level = info
+
+log_max_days = 3
+
+# privilege mode is the only supported mode since v0.10.0
+privilege_token = 12345678
+
+# heartbeat configure, it's not recommended to modify the default value
+# the default value of heartbeat_timeout is 90
+# heartbeat_timeout = 90
+
+# only allow frpc to bind ports you list, if you set nothing, there won't be any limit
+privilege_allow_ports = 2000-3000,3001,3003,4000-50000
+
+# pool_count in each proxy will change to max_pool_count if they exceed the maximum value
+max_pool_count = 5
+
+# authentication_timeout means the timeout interval (seconds) when the frpc connects frps
+# if authentication_timeout is zero, the time is not verified, default is 900s
+authentication_timeout = 900
+
+# if subdomain_host is not empty, you can set subdomain when type is http or https in frpc's configure file
+# when subdomain is test, the host used by routing is test.frps.com
+subdomain_host = frps.com
+
+# if tcp stream multiplexing is used, default is true
+tcp_mux = true

+ 0 - 7
conf/frps_min.ini

@@ -1,7 +0,0 @@
-[common]
-bind_addr = 0.0.0.0
-bind_port = 7000
-vhost_http_port = 80
-vhost_https_port = 443
-dashboard_port = 7500
-#privilege_token = 12345678

+ 20 - 0
models/config/client_common.go

@@ -18,6 +18,7 @@ import (
 	"fmt"
 	"os"
 	"strconv"
+	"strings"
 
 	ini "github.com/vaughan0/go-ini"
 )
@@ -38,6 +39,8 @@ type ClientCommonConf struct {
 	PoolCount         int
 	TcpMux            bool
 	User              string
+	LoginFailExit     bool
+	Start             map[string]struct{}
 	HeartBeatInterval int64
 	HeartBeatTimeout  int64
 }
@@ -56,6 +59,8 @@ func GetDeaultClientCommonConf() *ClientCommonConf {
 		PoolCount:         1,
 		TcpMux:            true,
 		User:              "",
+		LoginFailExit:     true,
+		Start:             make(map[string]struct{}),
 		HeartBeatInterval: 30,
 		HeartBeatTimeout:  90,
 	}
@@ -134,6 +139,21 @@ func LoadClientCommonConf(conf ini.File) (cfg *ClientCommonConf, err error) {
 		cfg.User = tmpStr
 	}
 
+	tmpStr, ok = conf.Get("common", "start")
+	if ok {
+		proxyNames := strings.Split(tmpStr, ",")
+		for _, name := range proxyNames {
+			cfg.Start[name] = struct{}{}
+		}
+	}
+
+	tmpStr, ok = conf.Get("common", "login_fail_exit")
+	if ok && tmpStr == "false" {
+		cfg.LoginFailExit = false
+	} else {
+		cfg.LoginFailExit = true
+	}
+
 	tmpStr, ok = conf.Get("common", "heartbeat_timeout")
 	if ok {
 		v, err = strconv.ParseInt(tmpStr, 10, 64)

+ 42 - 7
models/config/proxy.go

@@ -239,6 +239,7 @@ func (cfg *DomainConf) check() (err error) {
 	return nil
 }
 
+// Local service info
 type LocalSvrConf struct {
 	LocalIp   string `json:"-"`
 	LocalPort int    `json:"-"`
@@ -259,12 +260,34 @@ func (cfg *LocalSvrConf) LoadFromFile(name string, section ini.Section) (err err
 	return nil
 }
 
+type PluginConf struct {
+	Plugin       string            `json:"-"`
+	PluginParams map[string]string `json:"-"`
+}
+
+func (cfg *PluginConf) LoadFromFile(name string, section ini.Section) (err error) {
+	cfg.Plugin = section["plugin"]
+	cfg.PluginParams = make(map[string]string)
+	if cfg.Plugin != "" {
+		// get params begin with "plugin_"
+		for k, v := range section {
+			if strings.HasPrefix(k, "plugin_") {
+				cfg.PluginParams[k] = v
+			}
+		}
+	} else {
+		return fmt.Errorf("Parse conf error: proxy [%s] no plugin info found", name)
+	}
+	return
+}
+
 // TCP
 type TcpProxyConf struct {
 	BaseProxyConf
 	BindInfoConf
 
 	LocalSvrConf
+	PluginConf
 }
 
 func (cfg *TcpProxyConf) LoadFromMsg(pMsg *msg.NewProxy) {
@@ -279,8 +302,11 @@ func (cfg *TcpProxyConf) LoadFromFile(name string, section ini.Section) (err err
 	if err = cfg.BindInfoConf.LoadFromFile(name, section); err != nil {
 		return
 	}
-	if err = cfg.LocalSvrConf.LoadFromFile(name, section); err != nil {
-		return
+
+	if err = cfg.PluginConf.LoadFromFile(name, section); err != nil {
+		if err = cfg.LocalSvrConf.LoadFromFile(name, section); err != nil {
+			return
+		}
 	}
 	return
 }
@@ -337,6 +363,7 @@ type HttpProxyConf struct {
 	DomainConf
 
 	LocalSvrConf
+	PluginConf
 
 	Locations         []string `json:"locations"`
 	HostHeaderRewrite string   `json:"host_header_rewrite"`
@@ -405,6 +432,7 @@ type HttpsProxyConf struct {
 	DomainConf
 
 	LocalSvrConf
+	PluginConf
 }
 
 func (cfg *HttpsProxyConf) LoadFromMsg(pMsg *msg.NewProxy) {
@@ -438,14 +466,21 @@ func (cfg *HttpsProxyConf) Check() (err error) {
 	return
 }
 
-func LoadProxyConfFromFile(conf ini.File) (proxyConfs map[string]ProxyConf, err error) {
-	var prefix string
-	if ClientCommonCfg.User != "" {
-		prefix = ClientCommonCfg.User + "."
+// if len(startProxy) is 0, start all
+// otherwise just start proxies in startProxy map
+func LoadProxyConfFromFile(prefix string, conf ini.File, startProxy map[string]struct{}) (proxyConfs map[string]ProxyConf, err error) {
+	if prefix != "" {
+		prefix += "."
+	}
+
+	startAll := true
+	if len(startProxy) > 0 {
+		startAll = false
 	}
 	proxyConfs = make(map[string]ProxyConf)
 	for name, section := range conf {
-		if name != "common" {
+		_, shouldStart := startProxy[name]
+		if name != "common" && (startAll || shouldStart) {
 			cfg, err := NewProxyConfFromFile(name, section)
 			if err != nil {
 				return proxyConfs, err

+ 227 - 0
models/plugin/http_proxy.go

@@ -0,0 +1,227 @@
+// Copyright 2017 frp team
+//
+// 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 plugin
+
+import (
+	"encoding/base64"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+	"strings"
+	"sync"
+
+	"github.com/fatedier/frp/models/proto/tcp"
+	"github.com/fatedier/frp/utils/errors"
+	frpNet "github.com/fatedier/frp/utils/net"
+)
+
+const PluginHttpProxy = "http_proxy"
+
+func init() {
+	Register(PluginHttpProxy, NewHttpProxyPlugin)
+}
+
+type Listener struct {
+	conns  chan net.Conn
+	closed bool
+	mu     sync.Mutex
+}
+
+func NewProxyListener() *Listener {
+	return &Listener{
+		conns: make(chan net.Conn, 64),
+	}
+}
+
+func (l *Listener) Accept() (net.Conn, error) {
+	conn, ok := <-l.conns
+	if !ok {
+		return nil, fmt.Errorf("listener closed")
+	}
+	return conn, nil
+}
+
+func (l *Listener) PutConn(conn net.Conn) error {
+	err := errors.PanicToError(func() {
+		l.conns <- conn
+	})
+	return err
+}
+
+func (l *Listener) Close() error {
+	l.mu.Lock()
+	defer l.mu.Unlock()
+	if !l.closed {
+		close(l.conns)
+		l.closed = true
+	}
+	return nil
+}
+
+func (l *Listener) Addr() net.Addr {
+	return (*net.TCPAddr)(nil)
+}
+
+type HttpProxy struct {
+	l          *Listener
+	s          *http.Server
+	AuthUser   string
+	AuthPasswd string
+}
+
+func NewHttpProxyPlugin(params map[string]string) (Plugin, error) {
+	user := params["plugin_http_user"]
+	passwd := params["plugin_http_passwd"]
+	listener := NewProxyListener()
+
+	hp := &HttpProxy{
+		l:          listener,
+		AuthUser:   user,
+		AuthPasswd: passwd,
+	}
+
+	hp.s = &http.Server{
+		Handler: hp,
+	}
+
+	go hp.s.Serve(listener)
+	return hp, nil
+}
+
+func (hp *HttpProxy) Name() string {
+	return PluginHttpProxy
+}
+
+func (hp *HttpProxy) Handle(conn io.ReadWriteCloser) {
+	var wrapConn net.Conn
+	if realConn, ok := conn.(net.Conn); ok {
+		wrapConn = realConn
+	} else {
+		wrapConn = frpNet.WrapReadWriteCloserToConn(conn)
+	}
+
+	hp.l.PutConn(wrapConn)
+	return
+}
+
+func (hp *HttpProxy) Close() error {
+	hp.s.Close()
+	hp.l.Close()
+	return nil
+}
+
+func (hp *HttpProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
+	if ok := hp.Auth(rw, req); !ok {
+		rw.Header().Set("Proxy-Authenticate", "Basic")
+		rw.WriteHeader(http.StatusProxyAuthRequired)
+		return
+	}
+
+	if req.Method == "CONNECT" {
+		hp.ConnectHandler(rw, req)
+	} else {
+		hp.HttpHandler(rw, req)
+	}
+}
+
+func (hp *HttpProxy) HttpHandler(rw http.ResponseWriter, req *http.Request) {
+	removeProxyHeaders(req)
+
+	resp, err := http.DefaultTransport.RoundTrip(req)
+	if err != nil {
+		http.Error(rw, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	defer resp.Body.Close()
+
+	copyHeaders(rw.Header(), resp.Header)
+	rw.WriteHeader(resp.StatusCode)
+
+	_, err = io.Copy(rw, resp.Body)
+	if err != nil && err != io.EOF {
+		return
+	}
+}
+
+func (hp *HttpProxy) 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
+	}
+
+	remote, err := net.Dial("tcp", req.URL.Host)
+	if err != nil {
+		http.Error(rw, "Failed", http.StatusBadRequest)
+		client.Close()
+		return
+	}
+	client.Write([]byte("HTTP/1.0 200 OK\r\n\r\n"))
+
+	go tcp.Join(remote, client)
+}
+
+func (hp *HttpProxy) Auth(rw http.ResponseWriter, req *http.Request) bool {
+	if hp.AuthUser == "" && hp.AuthPasswd == "" {
+		return true
+	}
+
+	s := strings.SplitN(req.Header.Get("Proxy-Authorization"), " ", 2)
+	if len(s) != 2 {
+		return false
+	}
+
+	b, err := base64.StdEncoding.DecodeString(s[1])
+	if err != nil {
+		return false
+	}
+
+	pair := strings.SplitN(string(b), ":", 2)
+	if len(pair) != 2 {
+		return false
+	}
+
+	if pair[0] != hp.AuthUser || pair[1] != hp.AuthPasswd {
+		return false
+	}
+	return true
+}
+
+func copyHeaders(dst, src http.Header) {
+	for key, values := range src {
+		for _, value := range values {
+			dst.Add(key, value)
+		}
+	}
+}
+
+func removeProxyHeaders(req *http.Request) {
+	req.RequestURI = ""
+	req.Header.Del("Proxy-Connection")
+	req.Header.Del("Connection")
+	req.Header.Del("Proxy-Authenticate")
+	req.Header.Del("Proxy-Authorization")
+	req.Header.Del("TE")
+	req.Header.Del("Trailers")
+	req.Header.Del("Transfer-Encoding")
+	req.Header.Del("Upgrade")
+}

+ 45 - 0
models/plugin/plugin.go

@@ -0,0 +1,45 @@
+// Copyright 2017 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 plugin
+
+import (
+	"fmt"
+	"io"
+)
+
+// Creators is used for create plugins to handle connections.
+var creators = make(map[string]CreatorFn)
+
+// params has prefix "plugin_"
+type CreatorFn func(params map[string]string) (Plugin, error)
+
+func Register(name string, fn CreatorFn) {
+	creators[name] = fn
+}
+
+func Create(name string, params map[string]string) (p Plugin, err error) {
+	if fn, ok := creators[name]; ok {
+		p, err = fn(params)
+	} else {
+		err = fmt.Errorf("plugin [%s] is not registered", name)
+	}
+	return
+}
+
+type Plugin interface {
+	Name() string
+	Handle(conn io.ReadWriteCloser)
+	Close() error
+}

+ 69 - 0
models/plugin/unix_domain_socket.go

@@ -0,0 +1,69 @@
+// Copyright 2017 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 plugin
+
+import (
+	"fmt"
+	"io"
+	"net"
+
+	"github.com/fatedier/frp/models/proto/tcp"
+)
+
+const PluginUnixDomainSocket = "unix_domain_socket"
+
+func init() {
+	Register(PluginUnixDomainSocket, NewUnixDomainSocketPlugin)
+}
+
+type UnixDomainSocketPlugin struct {
+	UnixAddr *net.UnixAddr
+}
+
+func NewUnixDomainSocketPlugin(params map[string]string) (p Plugin, err error) {
+	unixPath, ok := params["plugin_unix_path"]
+	if !ok {
+		err = fmt.Errorf("plugin_unix_path not found")
+		return
+	}
+
+	unixAddr, errRet := net.ResolveUnixAddr("unix", unixPath)
+	if errRet != nil {
+		err = errRet
+		return
+	}
+
+	p = &UnixDomainSocketPlugin{
+		UnixAddr: unixAddr,
+	}
+	return
+}
+
+func (uds *UnixDomainSocketPlugin) Handle(conn io.ReadWriteCloser) {
+	localConn, err := net.DialUnix("unix", nil, uds.UnixAddr)
+	if err != nil {
+		return
+	}
+
+	tcp.Join(localConn, conn)
+}
+
+func (uds *UnixDomainSocketPlugin) Name() string {
+	return PluginUnixDomainSocket
+}
+
+func (uds *UnixDomainSocketPlugin) Close() error {
+	return nil
+}

+ 1 - 1
server/control.go

@@ -268,7 +268,7 @@ func (ctl *Control) stoper() {
 	for _, pxy := range ctl.proxies {
 		pxy.Close()
 		ctl.svr.DelProxy(pxy.GetName())
-		StatsCloseProxy(pxy.GetConf().GetBaseInfo().ProxyType)
+		StatsCloseProxy(pxy.GetName(), pxy.GetConf().GetBaseInfo().ProxyType)
 	}
 
 	ctl.allShutdown.Done()

+ 38 - 4
server/dashboard.go

@@ -15,9 +15,12 @@
 package server
 
 import (
+	"compress/gzip"
 	"fmt"
+	"io"
 	"net"
 	"net/http"
+	"strings"
 	"time"
 
 	"github.com/fatedier/frp/assets"
@@ -45,7 +48,7 @@ func RunDashboardServer(addr string, port int64) (err error) {
 
 	// view
 	router.Handler("GET", "/favicon.ico", http.FileServer(assets.FileSystem))
-	router.Handler("GET", "/static/*filepath", basicAuthWraper(http.StripPrefix("/static/", http.FileServer(assets.FileSystem))))
+	router.Handler("GET", "/static/*filepath", MakeGzipHandler(basicAuthWraper(http.StripPrefix("/static/", http.FileServer(assets.FileSystem)))))
 	router.HandlerFunc("GET", "/", basicAuth(func(w http.ResponseWriter, r *http.Request) {
 		http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
 	}))
@@ -84,7 +87,7 @@ type AuthWraper struct {
 
 func (aw *AuthWraper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	user, passwd, hasAuth := r.BasicAuth()
-	if (aw.user == "" && aw.passwd == "") || (hasAuth && user == aw.user || passwd == aw.passwd) {
+	if (aw.user == "" && aw.passwd == "") || (hasAuth && user == aw.user && passwd == aw.passwd) {
 		aw.h.ServeHTTP(w, r)
 	} else {
 		w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
@@ -104,7 +107,7 @@ func basicAuth(h http.HandlerFunc) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		user, passwd, hasAuth := r.BasicAuth()
 		if (config.ServerCommonCfg.DashboardUser == "" && config.ServerCommonCfg.DashboardPwd == "") ||
-			(hasAuth && user == config.ServerCommonCfg.DashboardUser || passwd == config.ServerCommonCfg.DashboardPwd) {
+			(hasAuth && user == config.ServerCommonCfg.DashboardUser && passwd == config.ServerCommonCfg.DashboardPwd) {
 			h.ServeHTTP(w, r)
 		} else {
 			w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
@@ -117,7 +120,7 @@ func httprouterBasicAuth(h httprouter.Handle) httprouter.Handle {
 	return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
 		user, passwd, hasAuth := r.BasicAuth()
 		if (config.ServerCommonCfg.DashboardUser == "" && config.ServerCommonCfg.DashboardPwd == "") ||
-			(hasAuth && user == config.ServerCommonCfg.DashboardUser || passwd == config.ServerCommonCfg.DashboardPwd) {
+			(hasAuth && user == config.ServerCommonCfg.DashboardUser && passwd == config.ServerCommonCfg.DashboardPwd) {
 			h(w, r, ps)
 		} else {
 			w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
@@ -125,3 +128,34 @@ func httprouterBasicAuth(h httprouter.Handle) httprouter.Handle {
 		}
 	}
 }
+
+type GzipWraper struct {
+	h http.Handler
+}
+
+func (gw *GzipWraper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
+		gw.h.ServeHTTP(w, r)
+		return
+	}
+	w.Header().Set("Content-Encoding", "gzip")
+	gz := gzip.NewWriter(w)
+	defer gz.Close()
+	gzr := gzipResponseWriter{Writer: gz, ResponseWriter: w}
+	gw.h.ServeHTTP(gzr, r)
+}
+
+func MakeGzipHandler(h http.Handler) http.Handler {
+	return &GzipWraper{
+		h: h,
+	}
+}
+
+type gzipResponseWriter struct {
+	io.Writer
+	http.ResponseWriter
+}
+
+func (w gzipResponseWriter) Write(b []byte) (int, error) {
+	return w.Writer.Write(b)
+}

+ 8 - 1
server/dashboard_api.go

@@ -21,6 +21,7 @@ import (
 	"github.com/fatedier/frp/models/config"
 	"github.com/fatedier/frp/models/consts"
 	"github.com/fatedier/frp/utils/log"
+	"github.com/fatedier/frp/utils/version"
 
 	"github.com/julienschmidt/httprouter"
 )
@@ -34,6 +35,7 @@ type GeneralResponse struct {
 type ServerInfoResp struct {
 	GeneralResponse
 
+	Version          string `json:"version"`
 	VhostHttpPort    int64  `json:"vhost_http_port"`
 	VhostHttpsPort   int64  `json:"vhost_https_port"`
 	AuthTimeout      int64  `json:"auth_timeout"`
@@ -61,6 +63,7 @@ func apiServerInfo(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
 	cfg := config.ServerCommonCfg
 	serverStats := StatsGetServer()
 	res = ServerInfoResp{
+		Version:          version.Full(),
 		VhostHttpPort:    cfg.VhostHttpPort,
 		VhostHttpsPort:   cfg.VhostHttpsPort,
 		AuthTimeout:      cfg.AuthTimeout,
@@ -86,6 +89,8 @@ type ProxyStatsInfo struct {
 	TodayTrafficIn  int64            `json:"today_traffic_in"`
 	TodayTrafficOut int64            `json:"today_traffic_out"`
 	CurConns        int64            `json:"cur_conns"`
+	LastStartTime   string           `json:"last_start_time"`
+	LastCloseTime   string           `json:"last_close_time"`
 	Status          string           `json:"status"`
 }
 
@@ -173,10 +178,12 @@ func getProxyStatsByType(proxyType string) (proxyInfos []*ProxyStatsInfo) {
 		} else {
 			proxyInfo.Status = consts.Offline
 		}
+		proxyInfo.Name = ps.Name
 		proxyInfo.TodayTrafficIn = ps.TodayTrafficIn
 		proxyInfo.TodayTrafficOut = ps.TodayTrafficOut
 		proxyInfo.CurConns = ps.CurConns
-		proxyInfo.Name = ps.Name
+		proxyInfo.LastStartTime = ps.LastStartTime
+		proxyInfo.LastCloseTime = ps.LastCloseTime
 		proxyInfos = append(proxyInfos, proxyInfo)
 	}
 	return

+ 44 - 5
server/metric.go

@@ -16,8 +16,10 @@ package server
 
 import (
 	"sync"
+	"time"
 
 	"github.com/fatedier/frp/models/config"
+	"github.com/fatedier/frp/utils/log"
 	"github.com/fatedier/frp/utils/metric"
 )
 
@@ -46,10 +48,13 @@ type ServerStatistics struct {
 }
 
 type ProxyStatistics struct {
-	ProxyType  string
-	TrafficIn  metric.DateCounter
-	TrafficOut metric.DateCounter
-	CurConns   metric.Counter
+	Name          string
+	ProxyType     string
+	TrafficIn     metric.DateCounter
+	TrafficOut    metric.DateCounter
+	CurConns      metric.Counter
+	LastStartTime time.Time
+	LastCloseTime time.Time
 }
 
 func init() {
@@ -63,6 +68,27 @@ func init() {
 
 		ProxyStatistics: make(map[string]*ProxyStatistics),
 	}
+
+	go func() {
+		for {
+			time.Sleep(12 * time.Hour)
+			log.Debug("start to clear useless proxy statistics data...")
+			StatsClearUselessInfo()
+			log.Debug("finish to clear useless proxy statistics data")
+		}
+	}()
+}
+
+func StatsClearUselessInfo() {
+	// To check if there are proxies that closed than 7 days and drop them.
+	globalStats.mu.Lock()
+	defer globalStats.mu.Unlock()
+	for name, data := range globalStats.ProxyStatistics {
+		if !data.LastCloseTime.IsZero() && time.Since(data.LastCloseTime) > time.Duration(7*24)*time.Hour {
+			delete(globalStats.ProxyStatistics, name)
+			log.Trace("clear proxy [%s]'s statistics data, lastCloseTime: [%s]", name, data.LastCloseTime.String())
+		}
+	}
 }
 
 func StatsNewClient() {
@@ -91,6 +117,7 @@ func StatsNewProxy(name string, proxyType string) {
 		proxyStats, ok := globalStats.ProxyStatistics[name]
 		if !(ok && proxyStats.ProxyType == proxyType) {
 			proxyStats = &ProxyStatistics{
+				Name:       name,
 				ProxyType:  proxyType,
 				CurConns:   metric.NewCounter(),
 				TrafficIn:  metric.NewDateCounter(ReserveDays),
@@ -98,16 +125,20 @@ func StatsNewProxy(name string, proxyType string) {
 			}
 			globalStats.ProxyStatistics[name] = proxyStats
 		}
+		proxyStats.LastStartTime = time.Now()
 	}
 }
 
-func StatsCloseProxy(proxyType string) {
+func StatsCloseProxy(proxyName string, proxyType string) {
 	if config.ServerCommonCfg.DashboardPort != 0 {
 		globalStats.mu.Lock()
 		defer globalStats.mu.Unlock()
 		if counter, ok := globalStats.ProxyTypeCounts[proxyType]; ok {
 			counter.Dec(1)
 		}
+		if proxyStats, ok := globalStats.ProxyStatistics[proxyName]; ok {
+			proxyStats.LastCloseTime = time.Now()
+		}
 	}
 }
 
@@ -199,6 +230,8 @@ type ProxyStats struct {
 	Type            string
 	TodayTrafficIn  int64
 	TodayTrafficOut int64
+	LastStartTime   string
+	LastCloseTime   string
 	CurConns        int64
 }
 
@@ -219,6 +252,12 @@ func StatsGetProxiesByType(proxyType string) []*ProxyStats {
 			TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
 			CurConns:        proxyStats.CurConns.Count(),
 		}
+		if !proxyStats.LastStartTime.IsZero() {
+			ps.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05")
+		}
+		if !proxyStats.LastCloseTime.IsZero() {
+			ps.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05")
+		}
 		res = append(res, ps)
 	}
 	return res

+ 6 - 0
tests/conf/auto_test_frpc.ini

@@ -27,3 +27,9 @@ type = udp
 local_ip = 127.0.0.1
 local_port = 10703
 remote_port = 10712
+
+[unix_domain]
+type = tcp
+remote_port = 10704
+plugin = unix_domain_socket
+plugin_unix_path = /tmp/frp_echo_server.sock

+ 27 - 3
tests/echo_server.go

@@ -4,12 +4,15 @@ import (
 	"bufio"
 	"fmt"
 	"io"
+	"net"
+	"os"
+	"syscall"
 
-	"github.com/fatedier/frp/utils/net"
+	frpNet "github.com/fatedier/frp/utils/net"
 )
 
 func StartEchoServer() {
-	l, err := net.ListenTcp("127.0.0.1", 10701)
+	l, err := frpNet.ListenTcp("127.0.0.1", 10701)
 	if err != nil {
 		fmt.Printf("echo server listen error: %v\n", err)
 		return
@@ -27,7 +30,7 @@ func StartEchoServer() {
 }
 
 func StartUdpEchoServer() {
-	l, err := net.ListenUDP("127.0.0.1", 10703)
+	l, err := frpNet.ListenUDP("127.0.0.1", 10703)
 	if err != nil {
 		fmt.Printf("udp echo server listen error: %v\n", err)
 		return
@@ -44,6 +47,27 @@ func StartUdpEchoServer() {
 	}
 }
 
+func StartUnixDomainServer() {
+	unixPath := "/tmp/frp_echo_server.sock"
+	os.Remove(unixPath)
+	syscall.Umask(0)
+	l, err := net.Listen("unix", unixPath)
+	if err != nil {
+		fmt.Printf("unix domain server listen error: %v\n", err)
+		return
+	}
+
+	for {
+		c, err := l.Accept()
+		if err != nil {
+			fmt.Printf("unix domain server accept error: %v\n", err)
+			return
+		}
+
+		go echoWorker(c)
+	}
+}
+
 func echoWorker(c net.Conn) {
 	br := bufio.NewReader(c)
 	for {

+ 22 - 0
tests/func_test.go

@@ -26,6 +26,7 @@ func init() {
 	go StartEchoServer()
 	go StartUdpEchoServer()
 	go StartHttpServer()
+	go StartUnixDomainServer()
 	time.Sleep(500 * time.Millisecond)
 }
 
@@ -95,3 +96,24 @@ func TestUdpEchoServer(t *testing.T) {
 		t.Fatalf("message got from udp server error, get %s", string(data[:n-1]))
 	}
 }
+
+func TestUnixDomainServer(t *testing.T) {
+	c, err := frpNet.ConnectTcpServer(fmt.Sprintf("127.0.0.1:%d", 10704))
+	if err != nil {
+		t.Fatalf("connect to echo server error: %v", err)
+	}
+	timer := time.Now().Add(time.Duration(5) * time.Second)
+	c.SetDeadline(timer)
+
+	c.Write([]byte(ECHO_TEST_STR + "\n"))
+
+	br := bufio.NewReader(c)
+	buf, err := br.ReadString('\n')
+	if err != nil {
+		t.Fatalf("read from echo server error: %v", err)
+	}
+
+	if ECHO_TEST_STR != buf {
+		t.Fatalf("content error, send [%s], get [%s]", strings.Trim(ECHO_TEST_STR, "\n"), strings.Trim(buf, "\n"))
+	}
+}

+ 35 - 1
utils/net/conn.go

@@ -15,7 +15,9 @@
 package net
 
 import (
+	"io"
 	"net"
+	"time"
 
 	"github.com/fatedier/frp/utils/log"
 )
@@ -32,12 +34,44 @@ type WrapLogConn struct {
 }
 
 func WrapConn(c net.Conn) Conn {
-	return WrapLogConn{
+	return &WrapLogConn{
 		Conn:   c,
 		Logger: log.NewPrefixLogger(""),
 	}
 }
 
+type WrapReadWriteCloserConn struct {
+	io.ReadWriteCloser
+	log.Logger
+}
+
+func (conn *WrapReadWriteCloserConn) LocalAddr() net.Addr {
+	return (*net.TCPAddr)(nil)
+}
+
+func (conn *WrapReadWriteCloserConn) RemoteAddr() net.Addr {
+	return (*net.TCPAddr)(nil)
+}
+
+func (conn *WrapReadWriteCloserConn) SetDeadline(t time.Time) error {
+	return nil
+}
+
+func (conn *WrapReadWriteCloserConn) SetReadDeadline(t time.Time) error {
+	return nil
+}
+
+func (conn *WrapReadWriteCloserConn) SetWriteDeadline(t time.Time) error {
+	return nil
+}
+
+func WrapReadWriteCloserToConn(rwc io.ReadWriteCloser) Conn {
+	return &WrapReadWriteCloserConn{
+		ReadWriteCloser: rwc,
+		Logger:          log.NewPrefixLogger(""),
+	}
+}
+
 type Listener interface {
 	Accept() (Conn, error)
 	Close() error

+ 3 - 1
utils/net/tcp.go

@@ -128,7 +128,9 @@ func ConnectTcpServerByHttpProxy(httpProxy string, serverAddr string) (c Conn, e
 
 	var proxyAuth string
 	if proxyUrl.User != nil {
-		proxyAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(proxyUrl.User.String()))
+		username := proxyUrl.User.Username()
+		passwd, _ := proxyUrl.User.Password()
+		proxyAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+passwd))
 	}
 
 	if proxyUrl.Scheme != "http" {

+ 3 - 3
utils/version/version.go

@@ -19,7 +19,7 @@ import (
 	"strings"
 )
 
-var version string = "0.10.0"
+var version string = "0.11.0"
 
 func Full() string {
 	return version
@@ -54,8 +54,8 @@ func Minor(v string) int64 {
 
 // add every case there if server will not accept client's protocol and return false
 func Compat(client string) (ok bool, msg string) {
-	if LessThan(client, version) {
-		return false, "Please upgrade your frpc version to 0.10.0"
+	if LessThan(client, "0.10.0") {
+		return false, "Please upgrade your frpc version to at least 0.10.0"
 	}
 	return true, ""
 }

+ 6 - 1
web/frps/src/components/Overview.vue

@@ -4,6 +4,9 @@
             <el-col :md="12">
                 <div class="source">
                     <el-form label-position="left" class="server_info">
+                        <el-form-item label="Version">
+                          <span>{{ version }}</span>
+                        </el-form-item>
                         <el-form-item label="Http Port">
                           <span>{{ vhost_http_port }}</span>
                         </el-form-item>
@@ -25,7 +28,7 @@
                         <el-form-item label="Client Counts">
                           <span>{{ client_counts }}</span>
                         </el-form-item>
-                        <el-form-item label="Current Conns">
+                        <el-form-item label="Current Connections">
                           <span>{{ cur_conns }}</span>
                         </el-form-item>
                         <el-form-item label="Proxy Counts">
@@ -47,6 +50,7 @@
     export default {
         data() {
             return {
+                version: '',
                 vhost_http_port: '',
                 vhost_https_port: '',
                 auth_timeout: '',
@@ -70,6 +74,7 @@
               .then(res => {
                 return res.json()
               }).then(json => {
+                this.version = json.version
                 this.vhost_http_port = json.vhost_http_port
                 if (this.vhost_http_port == 0) {
                     this.vhost_http_port = "disable"

+ 6 - 0
web/frps/src/components/ProxiesHttp.vue

@@ -39,6 +39,12 @@
             <el-form-item label="Compression">
               <span>{{ props.row.compression }}</span>
             </el-form-item>
+            <el-form-item label="Last Start">
+              <span>{{ props.row.last_start_time }}</span>
+            </el-form-item>
+            <el-form-item label="Last Close">
+              <span>{{ props.row.last_close_time }}</span>
+            </el-form-item>
         </el-form>
     </template>
     </el-table-column>

+ 6 - 0
web/frps/src/components/ProxiesHttps.vue

@@ -33,6 +33,12 @@
             <el-form-item label="Compression">
               <span>{{ props.row.compression }}</span>
             </el-form-item>
+            <el-form-item label="Last Start">
+              <span>{{ props.row.last_start_time }}</span>
+            </el-form-item>
+            <el-form-item label="Last Close">
+              <span>{{ props.row.last_close_time }}</span>
+            </el-form-item>
         </el-form>
     </template>
     </el-table-column>

+ 6 - 0
web/frps/src/components/ProxiesTcp.vue

@@ -30,6 +30,12 @@
             <el-form-item label="Compression">
               <span>{{ props.row.compression }}</span>
             </el-form-item>
+            <el-form-item label="Last Start">
+              <span>{{ props.row.last_start_time }}</span>
+            </el-form-item>
+            <el-form-item label="Last Close">
+              <span>{{ props.row.last_close_time }}</span>
+            </el-form-item>
         </el-form>
         </template>
     </el-table-column>

+ 6 - 0
web/frps/src/components/ProxiesUdp.vue

@@ -30,6 +30,12 @@
             <el-form-item label="Compression">
               <span>{{ props.row.compression }}</span>
             </el-form-item>
+            <el-form-item label="Last Start">
+              <span>{{ props.row.last_start_time }}</span>
+            </el-form-item>
+            <el-form-item label="Last Close">
+              <span>{{ props.row.last_close_time }}</span>
+            </el-form-item>
         </el-form>
     </template>
     </el-table-column>

+ 2 - 0
web/frps/src/utils/proxy.js

@@ -11,6 +11,8 @@ class BaseProxy {
         this.conns = proxyStats.cur_conns
         this.traffic_in = proxyStats.today_traffic_in
         this.traffic_out = proxyStats.today_traffic_out
+        this.last_start_time = proxyStats.last_start_time
+        this.last_close_time = proxyStats.last_close_time
         this.status = proxyStats.status
     }
 }

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