From 91e46a2c5318b66088b169d9ea91b5bd261e5bf5 Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 20 Dec 2019 20:28:28 +0800 Subject: [PATCH] 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()