Browse Source

support server plugin feature

fatedier 5 years ago
parent
commit
91e46a2c53

+ 1 - 1
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"

+ 1 - 0
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 {

+ 10 - 0
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

+ 24 - 0
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
 }

+ 0 - 0
models/plugin/http2https.go → models/plugin/client/http2https.go


+ 0 - 0
models/plugin/http_proxy.go → models/plugin/client/http_proxy.go


+ 0 - 0
models/plugin/https2http.go → models/plugin/client/https2http.go


+ 0 - 0
models/plugin/plugin.go → models/plugin/client/plugin.go


+ 0 - 0
models/plugin/socks5.go → models/plugin/client/socks5.go


+ 0 - 0
models/plugin/static_file.go → models/plugin/client/static_file.go


+ 0 - 0
models/plugin/unix_domain_socket.go → models/plugin/client/unix_domain_socket.go


+ 104 - 0
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
+}

+ 105 - 0
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
+}

+ 32 - 0
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)
+}

+ 34 - 0
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
+}

+ 46 - 0
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
+}

+ 29 - 4
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,
 				}

+ 24 - 4
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()