From a57679f8375986abc970d22bad52644ba62a4969 Mon Sep 17 00:00:00 2001
From: fatedier <fatedier@gmail.com>
Date: Sun, 8 Dec 2019 21:01:58 +0800
Subject: [PATCH 1/5] support meta info for client and proxy

---
 conf/frpc_full.ini             |  7 +++++++
 models/config/client_common.go |  8 ++++++++
 models/config/proxy.go         | 14 +++++++++++++-
 models/msg/msg.go              | 30 ++++++++++++++++--------------
 models/plugin/http2https.go    |  3 +--
 models/plugin/https2http.go    |  2 +-
 6 files changed, 46 insertions(+), 18 deletions(-)

diff --git a/conf/frpc_full.ini b/conf/frpc_full.ini
index 14ca6ed3..8c86acba 100644
--- a/conf/frpc_full.ini
+++ b/conf/frpc_full.ini
@@ -64,6 +64,10 @@ tls_enable = true
 # heartbeat_interval = 30
 # heartbeat_timeout = 90
 
+# additional meta info for client
+meta_var1 = 123
+meta_var2 = 234
+
 # 'ssh' is the unique proxy name
 # if user in [common] section is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh'
 [ssh]
@@ -92,6 +96,9 @@ health_check_timeout_s = 3
 health_check_max_failed = 3
 # every 10 seconds will do a health check
 health_check_interval_s = 10
+# additional meta info for each proxy
+meta_var1 = 123
+meta_var2 = 234
 
 [ssh_random]
 type = tcp
diff --git a/models/config/client_common.go b/models/config/client_common.go
index fe87e08d..2b5006b4 100644
--- a/models/config/client_common.go
+++ b/models/config/client_common.go
@@ -115,6 +115,8 @@ type ClientCommonConf struct {
 	// before the connection is terminated, in seconds. It is not recommended
 	// to change this value. By default, this value is 90.
 	HeartBeatTimeout int64 `json:"heartbeat_timeout"`
+	// Client meta info
+	Metas map[string]string `json:"metas"`
 }
 
 // GetDefaultClientConf returns a client configuration with default values.
@@ -144,6 +146,7 @@ func GetDefaultClientConf() ClientCommonConf {
 		TLSEnable:         false,
 		HeartBeatInterval: 30,
 		HeartBeatTimeout:  90,
+		Metas:             make(map[string]string),
 	}
 }
 
@@ -294,6 +297,11 @@ func UnmarshalClientConfFromIni(content string) (cfg ClientCommonConf, err error
 			cfg.HeartBeatInterval = v
 		}
 	}
+	for k, v := range conf.Section("common") {
+		if strings.HasPrefix(k, "meta_") {
+			cfg.Metas[strings.TrimPrefix(k, "meta_")] = v
+		}
+	}
 	return
 }
 
diff --git a/models/config/proxy.go b/models/config/proxy.go
index 85c353ed..1efaf38a 100644
--- a/models/config/proxy.go
+++ b/models/config/proxy.go
@@ -130,6 +130,9 @@ type BaseProxyConf struct {
 	// 0 means no limit
 	BandwidthLimit BandwidthQuantity `json:"bandwidth_limit"`
 
+	// meta info for each proxy
+	Metas map[string]string `json:"metas"`
+
 	LocalSvrConf
 	HealthCheckConf
 }
@@ -146,7 +149,8 @@ func (cfg *BaseProxyConf) compare(cmp *BaseProxyConf) bool {
 		cfg.Group != cmp.Group ||
 		cfg.GroupKey != cmp.GroupKey ||
 		cfg.ProxyProtocolVersion != cmp.ProxyProtocolVersion ||
-		cfg.BandwidthLimit.Equal(&cmp.BandwidthLimit) {
+		cfg.BandwidthLimit.Equal(&cmp.BandwidthLimit) ||
+		!reflect.DeepEqual(cfg.Metas, cmp.Metas) {
 		return false
 	}
 	if !cfg.LocalSvrConf.compare(&cmp.LocalSvrConf) {
@@ -165,6 +169,7 @@ func (cfg *BaseProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) {
 	cfg.UseCompression = pMsg.UseCompression
 	cfg.Group = pMsg.Group
 	cfg.GroupKey = pMsg.GroupKey
+	cfg.Metas = pMsg.Metas
 }
 
 func (cfg *BaseProxyConf) UnmarshalFromIni(prefix string, name string, section ini.Section) error {
@@ -212,6 +217,12 @@ func (cfg *BaseProxyConf) UnmarshalFromIni(prefix string, name string, section i
 		}
 		cfg.HealthCheckUrl = s + cfg.HealthCheckUrl
 	}
+
+	for k, v := range section {
+		if strings.HasPrefix(k, "meta_") {
+			cfg.Metas[strings.TrimPrefix(k, "meta_")] = v
+		}
+	}
 	return nil
 }
 
@@ -222,6 +233,7 @@ func (cfg *BaseProxyConf) MarshalToMsg(pMsg *msg.NewProxy) {
 	pMsg.UseCompression = cfg.UseCompression
 	pMsg.Group = cfg.Group
 	pMsg.GroupKey = cfg.GroupKey
+	pMsg.Metas = cfg.Metas
 }
 
 func (cfg *BaseProxyConf) checkForCli() (err error) {
diff --git a/models/msg/msg.go b/models/msg/msg.go
index 11d2542f..ce41c9ec 100644
--- a/models/msg/msg.go
+++ b/models/msg/msg.go
@@ -62,14 +62,15 @@ var (
 
 // When frpc start, client send this message to login to server.
 type Login struct {
-	Version      string `json:"version"`
-	Hostname     string `json:"hostname"`
-	Os           string `json:"os"`
-	Arch         string `json:"arch"`
-	User         string `json:"user"`
-	PrivilegeKey string `json:"privilege_key"`
-	Timestamp    int64  `json:"timestamp"`
-	RunId        string `json:"run_id"`
+	Version      string            `json:"version"`
+	Hostname     string            `json:"hostname"`
+	Os           string            `json:"os"`
+	Arch         string            `json:"arch"`
+	User         string            `json:"user"`
+	PrivilegeKey string            `json:"privilege_key"`
+	Timestamp    int64             `json:"timestamp"`
+	RunId        string            `json:"run_id"`
+	Metas        map[string]string `json:"metas"`
 
 	// Some global configures.
 	PoolCount int `json:"pool_count"`
@@ -84,12 +85,13 @@ type LoginResp struct {
 
 // When frpc login success, send this message to frps for running a new proxy.
 type NewProxy struct {
-	ProxyName      string `json:"proxy_name"`
-	ProxyType      string `json:"proxy_type"`
-	UseEncryption  bool   `json:"use_encryption"`
-	UseCompression bool   `json:"use_compression"`
-	Group          string `json:"group"`
-	GroupKey       string `json:"group_key"`
+	ProxyName      string            `json:"proxy_name"`
+	ProxyType      string            `json:"proxy_type"`
+	UseEncryption  bool              `json:"use_encryption"`
+	UseCompression bool              `json:"use_compression"`
+	Group          string            `json:"group"`
+	GroupKey       string            `json:"group_key"`
+	Metas          map[string]string `json:"metas"`
 
 	// tcp and udp only
 	RemotePort int `json:"remote_port"`
diff --git a/models/plugin/http2https.go b/models/plugin/http2https.go
index 6f5965e6..570b3f9c 100644
--- a/models/plugin/http2https.go
+++ b/models/plugin/http2https.go
@@ -94,7 +94,6 @@ func NewHTTP2HTTPSPlugin(params map[string]string) (Plugin, error) {
 	return p, nil
 }
 
-
 func (p *HTTP2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, extraBufToLocal []byte) {
 	wrapConn := frpNet.WrapReadWriteCloserToConn(conn, realConn)
 	p.l.PutConn(wrapConn)
@@ -105,7 +104,7 @@ func (p *HTTP2HTTPSPlugin) Name() string {
 }
 
 func (p *HTTP2HTTPSPlugin) Close() error {
-	if err := p.s.Close();err != nil {
+	if err := p.s.Close(); err != nil {
 		return err
 	}
 	return nil
diff --git a/models/plugin/https2http.go b/models/plugin/https2http.go
index af5b6af9..093a74f4 100644
--- a/models/plugin/https2http.go
+++ b/models/plugin/https2http.go
@@ -126,7 +126,7 @@ func (p *HTTPS2HTTPPlugin) Name() string {
 }
 
 func (p *HTTPS2HTTPPlugin) Close() error {
-	if err := p.s.Close();err != nil {
+	if err := p.s.Close(); err != nil {
 		return err
 	}
 	return nil

From 91e46a2c5318b66088b169d9ea91b5bd261e5bf5 Mon Sep 17 00:00:00 2001
From: fatedier <fatedier@gmail.com>
Date: Fri, 20 Dec 2019 20:28:28 +0800
Subject: [PATCH 2/5] support server plugin feature

---
 client/proxy/proxy.go                         |   2 +-
 client/service.go                             |   1 +
 conf/frps_full.ini                            |  10 ++
 models/config/server_common.go                |  24 ++++
 models/plugin/{ => client}/http2https.go      |   0
 models/plugin/{ => client}/http_proxy.go      |   0
 models/plugin/{ => client}/https2http.go      |   0
 models/plugin/{ => client}/plugin.go          |   0
 models/plugin/{ => client}/socks5.go          |   0
 models/plugin/{ => client}/static_file.go     |   0
 .../plugin/{ => client}/unix_domain_socket.go |   0
 models/plugin/server/http.go                  | 104 +++++++++++++++++
 models/plugin/server/manager.go               | 105 ++++++++++++++++++
 models/plugin/server/plugin.go                |  32 ++++++
 models/plugin/server/tracer.go                |  34 ++++++
 models/plugin/server/types.go                 |  46 ++++++++
 server/control.go                             |  33 +++++-
 server/service.go                             |  28 ++++-
 18 files changed, 410 insertions(+), 9 deletions(-)
 rename models/plugin/{ => client}/http2https.go (100%)
 rename models/plugin/{ => client}/http_proxy.go (100%)
 rename models/plugin/{ => client}/https2http.go (100%)
 rename models/plugin/{ => client}/plugin.go (100%)
 rename models/plugin/{ => client}/socks5.go (100%)
 rename models/plugin/{ => client}/static_file.go (100%)
 rename models/plugin/{ => client}/unix_domain_socket.go (100%)
 create mode 100644 models/plugin/server/http.go
 create mode 100644 models/plugin/server/manager.go
 create mode 100644 models/plugin/server/plugin.go
 create mode 100644 models/plugin/server/tracer.go
 create mode 100644 models/plugin/server/types.go

diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go
index 268b317d..c51364f8 100644
--- a/client/proxy/proxy.go
+++ b/client/proxy/proxy.go
@@ -28,7 +28,7 @@ import (
 
 	"github.com/fatedier/frp/models/config"
 	"github.com/fatedier/frp/models/msg"
-	"github.com/fatedier/frp/models/plugin"
+	plugin "github.com/fatedier/frp/models/plugin/client"
 	"github.com/fatedier/frp/models/proto/udp"
 	"github.com/fatedier/frp/utils/limit"
 	frpNet "github.com/fatedier/frp/utils/net"
diff --git a/client/service.go b/client/service.go
index 095df0aa..5ad08855 100644
--- a/client/service.go
+++ b/client/service.go
@@ -222,6 +222,7 @@ func (svr *Service) login() (conn net.Conn, session *fmux.Session, err error) {
 		PrivilegeKey: util.GetAuthKey(svr.cfg.Token, now),
 		Timestamp:    now,
 		RunId:        svr.runId,
+		Metas:        svr.cfg.Metas,
 	}
 
 	if err = msg.WriteMsg(conn, loginMsg); err != nil {
diff --git a/conf/frps_full.ini b/conf/frps_full.ini
index ed507cef..030a3b3a 100644
--- a/conf/frps_full.ini
+++ b/conf/frps_full.ini
@@ -71,3 +71,13 @@ tcp_mux = true
 
 # custom 404 page for HTTP requests
 # custom_404_page = /path/to/404.html
+
+[plugin.user-manager]
+addr = 127.0.0.1:9000
+path = /handler
+ops = Login
+
+[plugin.port-manager]
+addr = 127.0.0.1:9001
+path = /handler
+ops = NewProxy
diff --git a/models/config/server_common.go b/models/config/server_common.go
index a190a61a..df6b7a10 100644
--- a/models/config/server_common.go
+++ b/models/config/server_common.go
@@ -21,6 +21,7 @@ import (
 
 	ini "github.com/vaughan0/go-ini"
 
+	plugin "github.com/fatedier/frp/models/plugin/server"
 	"github.com/fatedier/frp/utils/util"
 )
 
@@ -134,6 +135,8 @@ type ServerCommonConf struct {
 	// UserConnTimeout specifies the maximum time to wait for a work
 	// connection. By default, this value is 10.
 	UserConnTimeout int64 `json:"user_conn_timeout"`
+	// HTTPPlugins specify the server plugins support HTTP protocol.
+	HTTPPlugins map[string]plugin.HTTPPluginOptions `json:"http_plugins"`
 }
 
 // GetDefaultServerConf returns a server configuration with reasonable
@@ -167,6 +170,7 @@ func GetDefaultServerConf() ServerCommonConf {
 		HeartBeatTimeout:  90,
 		UserConnTimeout:   10,
 		Custom404Page:     "",
+		HTTPPlugins:       make(map[string]plugin.HTTPPluginOptions),
 	}
 }
 
@@ -181,6 +185,8 @@ func UnmarshalServerConfFromIni(content string) (cfg ServerCommonConf, err error
 		return ServerCommonConf{}, err
 	}
 
+	UnmarshalPluginsFromIni(conf, &cfg)
+
 	var (
 		tmpStr string
 		ok     bool
@@ -375,6 +381,24 @@ func UnmarshalServerConfFromIni(content string) (cfg ServerCommonConf, err error
 	return
 }
 
+func UnmarshalPluginsFromIni(sections ini.File, cfg *ServerCommonConf) {
+	for name, section := range sections {
+		if strings.HasPrefix(name, "plugin.") {
+			name = strings.TrimSpace(strings.TrimPrefix(name, "plugin."))
+			options := plugin.HTTPPluginOptions{
+				Name: name,
+				Addr: section["addr"],
+				Path: section["path"],
+				Ops:  strings.Split(section["ops"], ","),
+			}
+			for i, _ := range options.Ops {
+				options.Ops[i] = strings.TrimSpace(options.Ops[i])
+			}
+			cfg.HTTPPlugins[name] = options
+		}
+	}
+}
+
 func (cfg *ServerCommonConf) Check() (err error) {
 	return
 }
diff --git a/models/plugin/http2https.go b/models/plugin/client/http2https.go
similarity index 100%
rename from models/plugin/http2https.go
rename to models/plugin/client/http2https.go
diff --git a/models/plugin/http_proxy.go b/models/plugin/client/http_proxy.go
similarity index 100%
rename from models/plugin/http_proxy.go
rename to models/plugin/client/http_proxy.go
diff --git a/models/plugin/https2http.go b/models/plugin/client/https2http.go
similarity index 100%
rename from models/plugin/https2http.go
rename to models/plugin/client/https2http.go
diff --git a/models/plugin/plugin.go b/models/plugin/client/plugin.go
similarity index 100%
rename from models/plugin/plugin.go
rename to models/plugin/client/plugin.go
diff --git a/models/plugin/socks5.go b/models/plugin/client/socks5.go
similarity index 100%
rename from models/plugin/socks5.go
rename to models/plugin/client/socks5.go
diff --git a/models/plugin/static_file.go b/models/plugin/client/static_file.go
similarity index 100%
rename from models/plugin/static_file.go
rename to models/plugin/client/static_file.go
diff --git a/models/plugin/unix_domain_socket.go b/models/plugin/client/unix_domain_socket.go
similarity index 100%
rename from models/plugin/unix_domain_socket.go
rename to models/plugin/client/unix_domain_socket.go
diff --git a/models/plugin/server/http.go b/models/plugin/server/http.go
new file mode 100644
index 00000000..155c470a
--- /dev/null
+++ b/models/plugin/server/http.go
@@ -0,0 +1,104 @@
+// Copyright 2019 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 (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"reflect"
+)
+
+type HTTPPluginOptions struct {
+	Name string
+	Addr string
+	Path string
+	Ops  []string
+}
+
+type httpPlugin struct {
+	options HTTPPluginOptions
+
+	url    string
+	client *http.Client
+}
+
+func NewHTTPPluginOptions(options HTTPPluginOptions) Plugin {
+	return &httpPlugin{
+		options: options,
+		url:     fmt.Sprintf("http://%s%s", options.Addr, options.Path),
+		client:  &http.Client{},
+	}
+}
+
+func (p *httpPlugin) Name() string {
+	return p.options.Name
+}
+
+func (p *httpPlugin) IsSupport(op string) bool {
+	for _, v := range p.options.Ops {
+		if v == op {
+			return true
+		}
+	}
+	return false
+}
+
+func (p *httpPlugin) Handle(ctx context.Context, op string, content interface{}) (*Response, interface{}, error) {
+	r := &Request{
+		Version: APIVersion,
+		Op:      op,
+		Content: content,
+	}
+	var res Response
+	res.Content = reflect.New(reflect.TypeOf(content)).Interface()
+	if err := p.do(ctx, r, &res); err != nil {
+		return nil, nil, err
+	}
+	return &res, res.Content, nil
+}
+
+func (p *httpPlugin) do(ctx context.Context, r *Request, res *Response) error {
+	buf, err := json.Marshal(r)
+	if err != nil {
+		return err
+	}
+	req, err := http.NewRequest("POST", p.url, bytes.NewReader(buf))
+	if err != nil {
+		return err
+	}
+	req = req.WithContext(ctx)
+	req.Header.Set("X-Frp-Reqid", GetReqidFromContext(ctx))
+	resp, err := p.client.Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return fmt.Errorf("do http request error code: %d", resp.StatusCode)
+	}
+	buf, err = ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return err
+	}
+	if err = json.Unmarshal(buf, res); err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/models/plugin/server/manager.go b/models/plugin/server/manager.go
new file mode 100644
index 00000000..94642932
--- /dev/null
+++ b/models/plugin/server/manager.go
@@ -0,0 +1,105 @@
+// Copyright 2019 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 (
+	"context"
+	"errors"
+	"fmt"
+
+	"github.com/fatedier/frp/utils/util"
+	"github.com/fatedier/frp/utils/xlog"
+)
+
+type Manager struct {
+	loginPlugins    []Plugin
+	newProxyPlugins []Plugin
+}
+
+func NewManager() *Manager {
+	return &Manager{
+		loginPlugins:    make([]Plugin, 0),
+		newProxyPlugins: make([]Plugin, 0),
+	}
+}
+
+func (m *Manager) Register(p Plugin) {
+	if p.IsSupport(OpLogin) {
+		m.loginPlugins = append(m.loginPlugins, p)
+	}
+	if p.IsSupport(OpNewProxy) {
+		m.newProxyPlugins = append(m.newProxyPlugins, p)
+	}
+}
+
+func (m *Manager) Login(content *LoginContent) (*LoginContent, error) {
+	var (
+		res = &Response{
+			Reject:   false,
+			Unchange: true,
+		}
+		retContent interface{}
+		err        error
+	)
+	reqid, _ := util.RandId()
+	xl := xlog.New().AppendPrefix("reqid: " + reqid)
+	ctx := xlog.NewContext(context.Background(), xl)
+	ctx = NewReqidContext(ctx, reqid)
+
+	for _, p := range m.loginPlugins {
+		res, retContent, err = p.Handle(ctx, OpLogin, *content)
+		if err != nil {
+			xl.Warn("send Login request to plugin [%s] error: %v", p.Name(), err)
+			return nil, errors.New("send Login request to plugin error")
+		}
+		if res.Reject {
+			return nil, fmt.Errorf("%s", res.RejectReason)
+		}
+		if !res.Unchange {
+			content = retContent.(*LoginContent)
+		}
+	}
+	return content, nil
+}
+
+func (m *Manager) NewProxy(content *NewProxyContent) (*NewProxyContent, error) {
+	var (
+		res = &Response{
+			Reject:   false,
+			Unchange: true,
+		}
+		retContent interface{}
+		err        error
+	)
+	reqid, _ := util.RandId()
+	xl := xlog.New().AppendPrefix("reqid: " + reqid)
+	ctx := xlog.NewContext(context.Background(), xl)
+	ctx = NewReqidContext(ctx, reqid)
+
+	for _, p := range m.newProxyPlugins {
+		res, retContent, err = p.Handle(ctx, OpNewProxy, *content)
+		if err != nil {
+			xl.Warn("send NewProxy request to plugin [%s] error: %v", p.Name(), err)
+			return nil, errors.New("send NewProxy request to plugin error")
+		}
+		if res.Reject {
+			return nil, fmt.Errorf("%s", res.RejectReason)
+		}
+		if !res.Unchange {
+			content = retContent.(*NewProxyContent)
+		}
+	}
+	return content, nil
+}
diff --git a/models/plugin/server/plugin.go b/models/plugin/server/plugin.go
new file mode 100644
index 00000000..fd16b145
--- /dev/null
+++ b/models/plugin/server/plugin.go
@@ -0,0 +1,32 @@
+// Copyright 2019 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 (
+	"context"
+)
+
+const (
+	APIVersion = "0.1.0"
+
+	OpLogin    = "Login"
+	OpNewProxy = "NewProxy"
+)
+
+type Plugin interface {
+	Name() string
+	IsSupport(op string) bool
+	Handle(ctx context.Context, op string, content interface{}) (res *Response, retContent interface{}, err error)
+}
diff --git a/models/plugin/server/tracer.go b/models/plugin/server/tracer.go
new file mode 100644
index 00000000..2f4f2ccc
--- /dev/null
+++ b/models/plugin/server/tracer.go
@@ -0,0 +1,34 @@
+// Copyright 2019 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 (
+	"context"
+)
+
+type key int
+
+const (
+	reqidKey key = 0
+)
+
+func NewReqidContext(ctx context.Context, reqid string) context.Context {
+	return context.WithValue(ctx, reqidKey, reqid)
+}
+
+func GetReqidFromContext(ctx context.Context) string {
+	ret, _ := ctx.Value(reqidKey).(string)
+	return ret
+}
diff --git a/models/plugin/server/types.go b/models/plugin/server/types.go
new file mode 100644
index 00000000..4e392b71
--- /dev/null
+++ b/models/plugin/server/types.go
@@ -0,0 +1,46 @@
+// Copyright 2019 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 (
+	"github.com/fatedier/frp/models/msg"
+)
+
+type Request struct {
+	Version string      `json:"version"`
+	Op      string      `json:"op"`
+	Content interface{} `json:"content"`
+}
+
+type Response struct {
+	Reject       bool        `json:"reject"`
+	RejectReason string      `json:"reject_reason"`
+	Unchange     bool        `json:"unchange"`
+	Content      interface{} `json:"content"`
+}
+
+type LoginContent struct {
+	msg.Login
+}
+
+type UserInfo struct {
+	User  string            `json:"user"`
+	Metas map[string]string `json:"metas"`
+}
+
+type NewProxyContent struct {
+	User UserInfo `json:"user"`
+	msg.NewProxy
+}
diff --git a/server/control.go b/server/control.go
index 0db61987..e5e4901c 100644
--- a/server/control.go
+++ b/server/control.go
@@ -27,6 +27,7 @@ import (
 	"github.com/fatedier/frp/models/consts"
 	frpErr "github.com/fatedier/frp/models/errors"
 	"github.com/fatedier/frp/models/msg"
+	plugin "github.com/fatedier/frp/models/plugin/server"
 	"github.com/fatedier/frp/server/controller"
 	"github.com/fatedier/frp/server/proxy"
 	"github.com/fatedier/frp/server/stats"
@@ -86,6 +87,9 @@ type Control struct {
 	// proxy manager
 	pxyManager *proxy.ProxyManager
 
+	// plugin manager
+	pluginManager *plugin.Manager
+
 	// stats collector to store stats info of clients and proxies
 	statsCollector stats.Collector
 
@@ -138,9 +142,16 @@ type Control struct {
 	ctx context.Context
 }
 
-func NewControl(ctx context.Context, rc *controller.ResourceController, pxyManager *proxy.ProxyManager,
-	statsCollector stats.Collector, ctlConn net.Conn, loginMsg *msg.Login,
-	serverCfg config.ServerCommonConf) *Control {
+func NewControl(
+	ctx context.Context,
+	rc *controller.ResourceController,
+	pxyManager *proxy.ProxyManager,
+	pluginManager *plugin.Manager,
+	statsCollector stats.Collector,
+	ctlConn net.Conn,
+	loginMsg *msg.Login,
+	serverCfg config.ServerCommonConf,
+) *Control {
 
 	poolCount := loginMsg.PoolCount
 	if poolCount > int(serverCfg.MaxPoolCount) {
@@ -149,6 +160,7 @@ func NewControl(ctx context.Context, rc *controller.ResourceController, pxyManag
 	return &Control{
 		rc:              rc,
 		pxyManager:      pxyManager,
+		pluginManager:   pluginManager,
 		statsCollector:  statsCollector,
 		conn:            ctlConn,
 		loginMsg:        loginMsg,
@@ -407,8 +419,21 @@ func (ctl *Control) manager() {
 
 			switch m := rawMsg.(type) {
 			case *msg.NewProxy:
+				content := &plugin.NewProxyContent{
+					User: plugin.UserInfo{
+						User:  ctl.loginMsg.User,
+						Metas: ctl.loginMsg.Metas,
+					},
+					NewProxy: *m,
+				}
+				var remoteAddr string
+				retContent, err := ctl.pluginManager.NewProxy(content)
+				if err == nil {
+					m = &retContent.NewProxy
+					remoteAddr, err = ctl.RegisterProxy(m)
+				}
+
 				// register proxy in this control
-				remoteAddr, err := ctl.RegisterProxy(m)
 				resp := &msg.NewProxyResp{
 					ProxyName: m.ProxyName,
 				}
diff --git a/server/service.go b/server/service.go
index a4d2e9df..122555af 100644
--- a/server/service.go
+++ b/server/service.go
@@ -33,6 +33,7 @@ import (
 	"github.com/fatedier/frp/models/config"
 	"github.com/fatedier/frp/models/msg"
 	"github.com/fatedier/frp/models/nathole"
+	plugin "github.com/fatedier/frp/models/plugin/server"
 	"github.com/fatedier/frp/server/controller"
 	"github.com/fatedier/frp/server/group"
 	"github.com/fatedier/frp/server/ports"
@@ -76,6 +77,9 @@ type Service struct {
 	// Manage all proxies
 	pxyManager *proxy.ProxyManager
 
+	// Manage all plugins
+	pluginManager *plugin.Manager
+
 	// HTTP vhost router
 	httpVhostRouter *vhost.VhostRouters
 
@@ -92,8 +96,9 @@ type Service struct {
 
 func NewService(cfg config.ServerCommonConf) (svr *Service, err error) {
 	svr = &Service{
-		ctlManager: NewControlManager(),
-		pxyManager: proxy.NewProxyManager(),
+		ctlManager:    NewControlManager(),
+		pxyManager:    proxy.NewProxyManager(),
+		pluginManager: plugin.NewManager(),
 		rc: &controller.ResourceController{
 			VisitorManager: controller.NewVisitorManager(),
 			TcpPortManager: ports.NewPortManager("tcp", cfg.ProxyBindAddr, cfg.AllowPorts),
@@ -104,6 +109,12 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) {
 		cfg:             cfg,
 	}
 
+	// Init all plugins
+	for name, options := range cfg.HTTPPlugins {
+		svr.pluginManager.Register(plugin.NewHTTPPluginOptions(options))
+		log.Info("plugin [%s] has been registered", name)
+	}
+
 	// Init group controller
 	svr.rc.TcpGroupCtl = group.NewTcpGroupCtl(svr.rc.TcpPortManager)
 
@@ -295,7 +306,16 @@ func (svr *Service) HandleListener(l net.Listener) {
 
 				switch m := rawMsg.(type) {
 				case *msg.Login:
-					err = svr.RegisterControl(conn, m)
+					// server plugin hook
+					content := &plugin.LoginContent{
+						Login: *m,
+					}
+					retContent, err := svr.pluginManager.Login(content)
+					if err == nil {
+						m = &retContent.Login
+						err = svr.RegisterControl(conn, m)
+					}
+
 					// If login failed, send error message there.
 					// Otherwise send success message in control's work goroutine.
 					if err != nil {
@@ -384,7 +404,7 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) (err
 		return
 	}
 
-	ctl := NewControl(ctx, svr.rc, svr.pxyManager, svr.statsCollector, ctlConn, loginMsg, svr.cfg)
+	ctl := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, svr.statsCollector, ctlConn, loginMsg, svr.cfg)
 
 	if oldCtl := svr.ctlManager.Add(loginMsg.RunId, ctl); oldCtl != nil {
 		oldCtl.allShutdown.WaitDone()

From 31e2cb76bb3a420b114851064ad9bc84f993bb75 Mon Sep 17 00:00:00 2001
From: fatedier <fatedier@gmail.com>
Date: Mon, 23 Dec 2019 20:00:59 +0800
Subject: [PATCH 3/5] bump version

---
 utils/version/version.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/utils/version/version.go b/utils/version/version.go
index dde6de96..27eb88a4 100644
--- a/utils/version/version.go
+++ b/utils/version/version.go
@@ -19,7 +19,7 @@ import (
 	"strings"
 )
 
-var version string = "0.30.0"
+var version string = "0.31.0"
 
 func Full() string {
 	return version

From e91c9473be33a1c9fccdcb20b78e113c8872f5fa Mon Sep 17 00:00:00 2001
From: fatedier <fatedier@gmail.com>
Date: Fri, 3 Jan 2020 11:35:12 +0800
Subject: [PATCH 4/5] add server manage plugin doc

---
 README.md               |   9 ++-
 README_zh.md            |  11 ++-
 doc/server_plugin.md    | 171 ++++++++++++++++++++++++++++++++++++++++
 doc/server_plugin_zh.md | 171 ++++++++++++++++++++++++++++++++++++++++
 4 files changed, 357 insertions(+), 5 deletions(-)
 create mode 100644 doc/server_plugin.md
 create mode 100644 doc/server_plugin_zh.md

diff --git a/README.md b/README.md
index 2890d645..9ca3a4d5 100644
--- a/README.md
+++ b/README.md
@@ -54,7 +54,8 @@ frp also has a P2P connect mode.
     * [URL routing](#url-routing)
     * [Connecting to frps via HTTP PROXY](#connecting-to-frps-via-http-proxy)
     * [Range ports mapping](#range-ports-mapping)
-    * [Plugins](#plugins)
+    * [Client Plugins](#client-plugins)
+    * [Server Manage Plugins](#server-manage-plugins)
 * [Development Plan](#development-plan)
 * [Contributing](#contributing)
 * [Donation](#donation)
@@ -806,7 +807,7 @@ remote_port = 6000-6006,6007
 
 frpc will generate 8 proxies like `test_tcp_0`, `test_tcp_1`, ..., `test_tcp_7`.
 
-### Plugins
+### Client Plugins
 
 frpc only forwards requests to local TCP or UDP ports by default.
 
@@ -828,6 +829,10 @@ plugin_http_passwd = abc
 
 `plugin_http_user` and `plugin_http_passwd` are configuration parameters used in `http_proxy` plugin.
 
+### Server Manage Plugins
+
+Read the [document](/doc/server_plugin.md).
+
 ## Development Plan
 
 * Log HTTP request information in frps.
diff --git a/README_zh.md b/README_zh.md
index 6f62e387..a7c45242 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -50,7 +50,8 @@ frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp
     * [URL 路由](#url-路由)
     * [通过代理连接 frps](#通过代理连接-frps)
     * [范围端口映射](#范围端口映射)
-    * [插件](#插件)
+    * [客户端插件](#客户端插件)
+    * [服务端管理插件](#服务端管理插件)
 * [开发计划](#开发计划)
 * [为 frp 做贡献](#为-frp-做贡献)
 * [捐助](#捐助)
@@ -858,11 +859,11 @@ remote_port = 6000-6006,6007
 
 实际连接成功后会创建 8 个 proxy,命名为 `test_tcp_0, test_tcp_1 ... test_tcp_7`。
 
-### 插件
+### 客户端插件
 
 默认情况下,frpc 只会转发请求到本地 tcp 或 udp 端口。
 
-插件模式是为了在客户端提供更加丰富的功能,目前内置的插件有 `unix_domain_socket`、`http_proxy`、`socks5`、`static_file`。具体使用方式请查看[使用示例](#使用示例)。
+客户端插件模式是为了在客户端提供更加丰富的功能,目前内置的插件有 `unix_domain_socket`、`http_proxy`、`socks5`、`static_file`。具体使用方式请查看[使用示例](#使用示例)。
 
 通过 `plugin` 指定需要使用的插件,插件的配置参数都以 `plugin_` 开头。使用插件后 `local_ip` 和 `local_port` 不再需要配置。
 
@@ -880,6 +881,10 @@ plugin_http_passwd = abc
 
 `plugin_http_user` 和 `plugin_http_passwd` 即为 `http_proxy` 插件可选的配置参数。
 
+### 服务端管理插件
+
+[使用说明](/doc/server_plugin_zh.md)
+
 ## 开发计划
 
 计划在后续版本中加入的功能与优化,排名不分先后,如果有其他功能建议欢迎在 [issues](https://github.com/fatedier/frp/issues) 中反馈。
diff --git a/doc/server_plugin.md b/doc/server_plugin.md
new file mode 100644
index 00000000..7d9be120
--- /dev/null
+++ b/doc/server_plugin.md
@@ -0,0 +1,171 @@
+### Manage Plugin
+
+frp manage plugin is aim to extend frp's ability without modifing self code.
+
+It runs as a process and listen on a port to provide RPC interface. Before frps doing some operations, frps will send RPC requests to manage plugin and do operations by it's response.
+
+### RPC request
+
+Support HTTP first.
+
+When manage plugin accept the operation request, it can give three different responses.
+
+* Reject operation and return the reason.
+* Allow operation and keep original content.
+* Allow operation and return modified content.
+
+### Interface
+
+HTTP path can be configured for each manage plugin in frps. Assume here is `/handler`.
+
+Request
+
+```
+POST /handler
+{
+    "version": "0.1.0",
+    "op": "Login",
+    "content": {
+        ... // Operation info
+    }
+}
+
+Request Header
+X-Frp-Reqid: for tracing
+```
+
+Response
+
+Error if not return 200 http code.
+
+Reject opeartion
+
+```
+{
+    "reject": true,
+    "reject_reason": "invalid user"
+}
+```
+
+Allow operation and keep original content
+
+```
+{
+    "reject": false,
+    "unchange": true
+}
+```
+
+Allow opeartion and modify content
+
+```
+{
+    "unchange": "false",
+    "content": {
+        ... // Replaced content
+    }
+}
+```
+
+### Operation
+
+Now it supports `Login` and `NewProxy`.
+
+#### Login
+
+Client login operation
+
+```
+{
+    "content": {
+        "version": <string>,
+        "hostname": <string>,
+        "os": <string>,
+        "arch": <string>,
+        "user": <string>,
+        "timestamp": <int64>,
+        "privilege_key": <string>,
+        "run_id": <string>,
+        "pool_count": <int>,
+        "metas": map<string>string
+    }
+}
+```
+
+#### NewProxy
+
+Create new proxy
+
+```
+{
+    "content": {
+        "user": {
+            "user": <string>,
+            "metas": map<string>string
+        },
+        "proxy_name": <string>,
+        "proxy_type": <string>,
+        "use_encryption": <bool>,
+        "use_compression": <bool>,
+        "group": <string>,
+        "group_key": <string>,
+
+        // tcp and udp only
+        "remote_port": <int>,
+
+        // http and https only
+        "custom_domains": []<string>,
+        "subdomain": <string>,
+        "locations": <string>,
+        "http_user": <string>,
+        "http_pwd": <string>,
+        "host_header_rewrite": <string>,
+        "headers": map<string>string,
+
+        "metas": map<string>string
+    }
+}
+```
+
+### manage plugin configure
+
+```ini
+[common]
+bind_port = 7000
+
+[plugin.user-manager]
+addr = 127.0.0.1:9000
+path = /handler
+ops = Login
+
+[plugin.port-manager]
+addr = 127.0.0.1:9001
+path = /handler
+ops = NewProxy
+```
+
+addr: plugin listen on.
+path: http request url path.
+ops: opeartions plugin needs handle.
+
+### meta data
+
+Meta data will be sent to manage plugin in each RCP request.
+
+Meta data start with `meta_`. It can be configured in `common` and each proxy.
+
+```
+# frpc.ini
+[common]
+server_addr = 127.0.0.1
+server_port = 7000
+user = fake
+meta_token = fake
+meta_version = 1.0.0
+
+[ssh]
+type = tcp
+local_port = 22
+remote_port = 6000
+meta_id = 123
+```
diff --git a/doc/server_plugin_zh.md b/doc/server_plugin_zh.md
new file mode 100644
index 00000000..78b71a32
--- /dev/null
+++ b/doc/server_plugin_zh.md
@@ -0,0 +1,171 @@
+### 服务端管理插件
+
+frp 管理插件的作用是在不侵入自身代码的前提下,扩展 frp 服务端的能力。
+
+frp 管理插件会以单独进程的形式运行,并且监听在一个端口上,对外提供 RPC 接口,响应 frps 的请求。
+
+frps 在执行某些操作前,会根据配置向管理插件发送 RPC 请求,根据管理插件的响应来执行相应的操作。
+
+### RPC 请求
+
+管理插件接收到操作请求后,可以给出三种回应。
+
+* 拒绝操作,需要返回拒绝操作的原因。
+* 允许操作,不需要修改操作内容。
+* 允许操作,对操作请求进行修改后,返回修改后的内容。
+
+### 接口
+
+接口路径可以在 frps 配置中为每个插件单独配置,这里以 `/handler` 为例。
+
+Request
+
+```
+POST /handler
+{
+    "version": "0.1.0",
+    "op": "Login",
+    "content": {
+        ... // 具体的操作信息
+    }
+}
+
+请求 Header
+X-Frp-Reqid: 用于追踪请求
+```
+
+Response
+
+非 200 的返回都认为是请求异常。
+
+拒绝执行操作
+
+```
+{
+   "reject": true,
+   "reject_reason": "invalid user"
+}
+```
+
+允许且内容不需要变动
+
+```
+{
+    "reject": false,
+    "unchange": true
+}
+```
+
+允许且需要替换操作内容
+
+```
+{
+    "unchange": "false",
+    "content": {
+        ... // 替换后的操作信息,格式必须和请求时的一致
+    }
+}
+```
+
+### 操作类型
+
+目前插件支持管理的操作类型有 `Login` 和 `NewProxy`。
+
+#### Login
+
+用户登录操作信息
+
+```
+{
+    "content": {
+        "version": <string>,
+        "hostname": <string>,
+        "os": <string>,
+        "arch": <string>,
+        "user": <string>,
+        "timestamp": <int64>,
+        "privilege_key": <string>,
+        "run_id": <string>,
+        "pool_count": <int>,
+        "metas": map<string>string
+    }
+}
+```
+
+#### NewProxy
+
+创建代理的相关信息
+
+```
+{
+    "content": {
+        "user": {
+            "user": <string>,
+            "metas": map<string>string
+        },
+        "proxy_name": <string>,
+        "proxy_type": <string>,
+        "use_encryption": <bool>,
+        "use_compression": <bool>,
+        "group": <string>,
+        "group_key": <string>,
+
+        // tcp and udp only
+        "remote_port": <int>,
+
+        // http and https only
+        "custom_domains": []<string>,
+        "subdomain": <string>,
+        "locations": <string>,
+        "http_user": <string>,
+        "http_pwd": <string>,
+        "host_header_rewrite": <string>,
+        "headers": map<string>string,
+
+        "metas": map<string>string
+    }
+}
+```
+
+### frps 中插件配置
+
+```ini
+[common]
+bind_port = 7000
+
+[plugin.user-manager]
+addr = 127.0.0.1:9000
+path = /handler
+ops = Login
+
+[plugin.port-manager]
+addr = 127.0.0.1:9001
+path = /handler
+ops = NewProxy
+```
+
+addr: 插件监听的网络地址。
+path: 插件监听的 HTTP 请求路径。
+ops: 插件需要处理的操作列表,多个 op 以英文逗号分隔。
+
+### 元数据
+
+为了减少 frps 的代码修改,同时提高管理插件的扩展能力,在 frpc 的配置文件中引入自定义元数据的概念。元数据会在调用 RPC 请求时发送给插件。
+
+元数据以 `meta_` 开头,可以配置多个,元数据分为两种,一种配置在 `common` 下,一种配置在各个 proxy 中。
+
+```
+# frpc.ini
+[common]
+server_addr = 127.0.0.1
+server_port = 7000
+user = fake
+meta_token = fake
+meta_version = 1.0.0
+
+[ssh]
+type = tcp
+local_port = 22
+remote_port = 6000
+meta_id = 123
+```

From 42014eea2321ac6df900553d1edefb34f9045871 Mon Sep 17 00:00:00 2001
From: fatedier <fatedier@gmail.com>
Date: Fri, 3 Jan 2020 11:39:44 +0800
Subject: [PATCH 5/5] improve xtcp, fix #1585

---
 client/proxy/proxy.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go
index c51364f8..c9ef8dd0 100644
--- a/client/proxy/proxy.go
+++ b/client/proxy/proxy.go
@@ -349,7 +349,7 @@ func (pxy *XtcpProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
 
 	lConn.WriteToUDP(sidBuf[:n], uAddr)
 
-	kcpConn, err := frpNet.NewKcpConnFromUdp(lConn, false, natHoleRespMsg.VisitorAddr)
+	kcpConn, err := frpNet.NewKcpConnFromUdp(lConn, false, uAddr.String())
 	if err != nil {
 		xl.Error("create kcp connection from udp connection error: %v", err)
 		return