add proxy manager and monitor (#7)

* GitButler Integration Commit

This is an integration commit for the virtual branches that GitButler is tracking.

Due to GitButler managing multiple virtual branches, you cannot switch back and
forth between git branches and virtual branches easily. 

If you switch to another branch, GitButler will need to be reinitialized.
If you commit on this branch, GitButler will throw it away.

Here are the branches that are currently applied:
 - feat/frpcc (refs/gitbutler/feat/frpcc)
   branch head: e52195a01a6e3432cccc3ba952a8c940ad4d3fc6
   - test/main.go
   - go.sum
   - go.mod
   - server/service.go
For more information about what we're doing here, check out our docs:
https://docs.gitbutler.com/features/virtual-branches/integration-branch

* fix: barry 2024-07-03 15:36:17

* fix: barry 2024-07-03 15:44:12

* fix: barry 2024-07-03 15:46:25

* fix: barry 2024-07-03 16:52:13

* fix: barry 2024-07-03 17:30:53

* fix: barry 2024-07-03 17:42:01

* fix: barry 2024-07-03 18:43:39

* fix: barry 2024-07-03 19:36:06

* fix: barry 2024-07-03 19:43:47

* fix: barry 2024-07-04 10:51:06

* fix: barry 2024-07-04 12:29:01

* fix log and body

* fix rate calc

* fix: barry 2024-07-09 18:20:06

---------

Co-authored-by: GitButler <gitbutler@gitbutler.com>
This commit is contained in:
百里(barry) 2024-07-09 19:20:28 +08:00 committed by GitHub
parent fc15d05955
commit c0b091f139
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 280 additions and 15 deletions

View File

@ -51,6 +51,8 @@ type SessionContext struct {
} }
type Control struct { type Control struct {
svr *Service
// service context // service context
ctx context.Context ctx context.Context
xl *xlog.Logger xl *xlog.Logger
@ -79,9 +81,10 @@ type Control struct {
msgDispatcher *msg.Dispatcher msgDispatcher *msg.Dispatcher
} }
func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, error) { func NewControl(ctx context.Context, sessionCtx *SessionContext, svr *Service) (*Control, error) {
// new xlog instance // new xlog instance
ctl := &Control{ ctl := &Control{
svr: svr,
ctx: ctx, ctx: ctx,
xl: xlog.FromContextSafe(ctx), xl: xlog.FromContextSafe(ctx),
sessionCtx: sessionCtx, sessionCtx: sessionCtx,
@ -182,15 +185,29 @@ func (ctl *Control) handleNatHoleResp(m msg.Message) {
} }
} }
func (ctl *Control) handleCustom(m msg.Message) {
xl := ctl.xl
inMsg := m.(*msg.ClientProxyClose)
xl.Infof("client get close message: %s", inMsg)
ctl.svr.GracefulClose(0)
}
func (ctl *Control) handlePong(m msg.Message) { func (ctl *Control) handlePong(m msg.Message) {
xl := ctl.xl xl := ctl.xl
inMsg := m.(*msg.Pong) inMsg := m.(*msg.Pong)
if inMsg.AuthErr != "" {
xl.Errorf("Pong message contains auth error: %s", inMsg.AuthErr)
ctl.svr.GracefulClose(0)
return
}
if inMsg.Error != "" { if inMsg.Error != "" {
xl.Errorf("Pong message contains error: %s", inMsg.Error) xl.Errorf("Pong message contains error: %s", inMsg.Error)
ctl.closeSession() ctl.closeSession()
return return
} }
ctl.lastPong.Store(time.Now()) ctl.lastPong.Store(time.Now())
xl.Debugf("receive heartbeat from server") xl.Debugf("receive heartbeat from server")
} }
@ -230,6 +247,7 @@ func (ctl *Control) registerMsgHandlers() {
ctl.msgDispatcher.RegisterHandler(&msg.NewProxyResp{}, ctl.handleNewProxyResp) ctl.msgDispatcher.RegisterHandler(&msg.NewProxyResp{}, ctl.handleNewProxyResp)
ctl.msgDispatcher.RegisterHandler(&msg.NatHoleResp{}, ctl.handleNatHoleResp) ctl.msgDispatcher.RegisterHandler(&msg.NatHoleResp{}, ctl.handleNatHoleResp)
ctl.msgDispatcher.RegisterHandler(&msg.Pong{}, ctl.handlePong) ctl.msgDispatcher.RegisterHandler(&msg.Pong{}, ctl.handlePong)
ctl.msgDispatcher.RegisterHandler(&msg.ClientProxyClose{}, ctl.handleCustom)
} }
// headerWorker sends heartbeat to server and check heartbeat timeout. // headerWorker sends heartbeat to server and check heartbeat timeout.

View File

@ -317,7 +317,7 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
AuthSetter: svr.authSetter, AuthSetter: svr.authSetter,
Connector: connector, Connector: connector,
} }
ctl, err := NewControl(svr.ctx, sessionCtx) ctl, err := NewControl(svr.ctx, sessionCtx, svr)
if err != nil { if err != nil {
conn.Close() conn.Close()
xl.Errorf("NewControl error: %v", err) xl.Errorf("NewControl error: %v", err)

2
go.mod
View File

@ -18,6 +18,7 @@ require (
github.com/pires/go-proxyproto v0.7.0 github.com/pires/go-proxyproto v0.7.0
github.com/prometheus/client_golang v1.19.0 github.com/prometheus/client_golang v1.19.0
github.com/quic-go/quic-go v0.42.0 github.com/quic-go/quic-go v0.42.0
github.com/r3labs/sse/v2 v2.10.0
github.com/rodaine/table v1.2.0 github.com/rodaine/table v1.2.0
github.com/samber/lo v1.39.0 github.com/samber/lo v1.39.0
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.8.0
@ -74,6 +75,7 @@ require (
golang.org/x/tools v0.17.0 // indirect golang.org/x/tools v0.17.0 // indirect
google.golang.org/appengine v1.6.8 // indirect google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.33.0 // indirect google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect

6
go.sum
View File

@ -114,6 +114,8 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/quic-go/quic-go v0.42.0 h1:uSfdap0eveIl8KXnipv9K7nlwZ5IqLlYOpJ58u5utpM= github.com/quic-go/quic-go v0.42.0 h1:uSfdap0eveIl8KXnipv9K7nlwZ5IqLlYOpJ58u5utpM=
github.com/quic-go/quic-go v0.42.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M= github.com/quic-go/quic-go v0.42.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0=
github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rodaine/table v1.2.0 h1:38HEnwK4mKSHQJIkavVj+bst1TEY7j9zhLMWu4QJrMA= github.com/rodaine/table v1.2.0 h1:38HEnwK4mKSHQJIkavVj+bst1TEY7j9zhLMWu4QJrMA=
@ -132,6 +134,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
@ -181,6 +184,7 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@ -266,6 +270,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y=
gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@ -17,6 +17,8 @@ package msg
import ( import (
"io" "io"
"reflect" "reflect"
"github.com/fatedier/golib/log"
) )
func AsyncHandler(f func(Message)) func(Message) { func AsyncHandler(f func(Message)) func(Message) {
@ -69,6 +71,7 @@ func (d *Dispatcher) readLoop() {
return return
} }
log.Debugf("receive message from server: %s, exists: %v", reflect.TypeOf(m), d.msgHandlers[reflect.TypeOf(m)] != nil)
if handler, ok := d.msgHandlers[reflect.TypeOf(m)]; ok { if handler, ok := d.msgHandlers[reflect.TypeOf(m)]; ok {
handler(m) handler(m)
} else if d.defaultHandler != nil { } else if d.defaultHandler != nil {

View File

@ -38,6 +38,7 @@ const (
TypeNatHoleResp = 'm' TypeNatHoleResp = 'm'
TypeNatHoleSid = '5' TypeNatHoleSid = '5'
TypeNatHoleReport = '6' TypeNatHoleReport = '6'
TypeClientProxyClose = '7'
) )
var msgTypeMap = map[byte]interface{}{ var msgTypeMap = map[byte]interface{}{
@ -59,6 +60,7 @@ var msgTypeMap = map[byte]interface{}{
TypeNatHoleResp: NatHoleResp{}, TypeNatHoleResp: NatHoleResp{},
TypeNatHoleSid: NatHoleSid{}, TypeNatHoleSid: NatHoleSid{},
TypeNatHoleReport: NatHoleReport{}, TypeNatHoleReport: NatHoleReport{},
TypeClientProxyClose: ClientProxyClose{},
} }
var TypeNameNatHoleResp = reflect.TypeOf(&NatHoleResp{}).Elem().Name() var TypeNameNatHoleResp = reflect.TypeOf(&NatHoleResp{}).Elem().Name()
@ -178,7 +180,8 @@ type Ping struct {
} }
type Pong struct { type Pong struct {
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
AuthErr string `json:"auth_err,omitempty"`
} }
type UDPPacket struct { type UDPPacket struct {
@ -243,3 +246,7 @@ type NatHoleReport struct {
Sid string `json:"sid,omitempty"` Sid string `json:"sid,omitempty"`
Success bool `json:"success,omitempty"` Success bool `json:"success,omitempty"`
} }
type ClientProxyClose struct {
Name string
}

View File

@ -93,6 +93,25 @@ func (r *Routers) Del(domain, location, httpUser string) {
routersByHTTPUser[httpUser] = newVrs routersByHTTPUser[httpUser] = newVrs
} }
func (r *Routers) GetAll() (rr []map[string]any) {
r.mutex.RLock()
defer r.mutex.RUnlock()
for _, v := range r.indexByDomain {
for _, v1 := range v {
for _, v2 := range v1 {
rr = append(rr, map[string]any{
"domain": v2.domain,
"location": v2.location,
"httpUser": v2.httpUser,
"payload": v2.payload,
})
}
}
}
return rr
}
func (r *Routers) Get(host, path, httpUser string) (vr *Router, exist bool) { func (r *Routers) Get(host, path, httpUser string) (vr *Router, exist bool) {
host = strings.ToLower(host) host = strings.ToLower(host)

View File

@ -367,6 +367,7 @@ func (ctl *Control) worker() {
func (ctl *Control) registerMsgHandlers() { func (ctl *Control) registerMsgHandlers() {
ctl.msgDispatcher.RegisterHandler(&msg.NewProxy{}, ctl.handleNewProxy) ctl.msgDispatcher.RegisterHandler(&msg.NewProxy{}, ctl.handleNewProxy)
ctl.msgDispatcher.RegisterHandler(&msg.Ping{}, ctl.handlePing) ctl.msgDispatcher.RegisterHandler(&msg.Ping{}, ctl.handlePing)
ctl.msgDispatcher.RegisterHandler(&msg.ClientProxyClose{}, ctl.handleCustom)
ctl.msgDispatcher.RegisterHandler(&msg.NatHoleVisitor{}, msg.AsyncHandler(ctl.handleNatHoleVisitor)) ctl.msgDispatcher.RegisterHandler(&msg.NatHoleVisitor{}, msg.AsyncHandler(ctl.handleNatHoleVisitor))
ctl.msgDispatcher.RegisterHandler(&msg.NatHoleClient{}, msg.AsyncHandler(ctl.handleNatHoleClient)) ctl.msgDispatcher.RegisterHandler(&msg.NatHoleClient{}, msg.AsyncHandler(ctl.handleNatHoleClient))
ctl.msgDispatcher.RegisterHandler(&msg.NatHoleReport{}, msg.AsyncHandler(ctl.handleNatHoleReport)) ctl.msgDispatcher.RegisterHandler(&msg.NatHoleReport{}, msg.AsyncHandler(ctl.handleNatHoleReport))
@ -408,6 +409,14 @@ func (ctl *Control) handleNewProxy(m msg.Message) {
_ = ctl.msgDispatcher.Send(resp) _ = ctl.msgDispatcher.Send(resp)
} }
func (ctl *Control) handleCustom(m msg.Message) {
inMsg := m.(*msg.ClientProxyClose)
xl := ctl.xl
xl.Debugf("handle custom request")
_ = ctl.msgDispatcher.Send(inMsg)
}
func (ctl *Control) handlePing(m msg.Message) { func (ctl *Control) handlePing(m msg.Message) {
xl := ctl.xl xl := ctl.xl
inMsg := m.(*msg.Ping) inMsg := m.(*msg.Ping)
@ -421,14 +430,19 @@ func (ctl *Control) handlePing(m msg.Message) {
Ping: *inMsg, Ping: *inMsg,
} }
retContent, err := ctl.pluginManager.Ping(content) retContent, err := ctl.pluginManager.Ping(content)
var authErr string
if err == nil { if err == nil {
inMsg = &retContent.Ping inMsg = &retContent.Ping
err = ctl.authVerifier.VerifyPing(inMsg) err = ctl.authVerifier.VerifyPing(inMsg)
if err != nil {
authErr = err.Error()
}
} }
if err != nil { if err != nil {
xl.Warnf("received invalid ping: %v", err) xl.Warnf("received invalid ping: %v", err)
_ = ctl.msgDispatcher.Send(&msg.Pong{ _ = ctl.msgDispatcher.Send(&msg.Pong{
Error: util.GenerateResponseErrorString("invalid ping", err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient)), Error: util.GenerateResponseErrorString("invalid ping", err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient)),
AuthErr: authErr,
}) })
return return
} }

View File

@ -17,6 +17,7 @@ package server
import ( import (
"cmp" "cmp"
"encoding/json" "encoding/json"
"math"
"net/http" "net/http"
"slices" "slices"
@ -26,6 +27,7 @@ import (
"github.com/fatedier/frp/pkg/config/types" "github.com/fatedier/frp/pkg/config/types"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/metrics/mem" "github.com/fatedier/frp/pkg/metrics/mem"
"github.com/fatedier/frp/pkg/msg"
httppkg "github.com/fatedier/frp/pkg/util/http" httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/pkg/util/log"
netpkg "github.com/fatedier/frp/pkg/util/net" netpkg "github.com/fatedier/frp/pkg/util/net"
@ -51,9 +53,11 @@ func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper)
} }
// apis // apis
subRouter.HandleFunc("/api/sub", svr.apiServerSub)
subRouter.HandleFunc("/api/serverinfo", svr.apiServerInfo).Methods("GET") subRouter.HandleFunc("/api/serverinfo", svr.apiServerInfo).Methods("GET")
subRouter.HandleFunc("/api/proxy/{type}", svr.apiProxyByType).Methods("GET") subRouter.HandleFunc("/api/proxy/{type}", svr.apiProxyByType).Methods("GET")
subRouter.HandleFunc("/api/proxy/{type}/{name}", svr.apiProxyByTypeAndName).Methods("GET") subRouter.HandleFunc("/api/proxy/{type}/{name}", svr.apiProxyByTypeAndName).Methods("GET")
subRouter.HandleFunc("/api/proxy/{type}/{name}/close", svr.apiCloseProxyByTypeAndName).Methods("POST")
subRouter.HandleFunc("/api/traffic/{name}", svr.apiProxyTraffic).Methods("GET") subRouter.HandleFunc("/api/traffic/{name}", svr.apiProxyTraffic).Methods("GET")
subRouter.HandleFunc("/api/proxies", svr.deleteProxies).Methods("DELETE") subRouter.HandleFunc("/api/proxies", svr.deleteProxies).Methods("DELETE")
@ -95,6 +99,15 @@ func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(200) w.WriteHeader(200)
} }
func (svr *Service) apiServerSub(w http.ResponseWriter, r *http.Request) {
go func() {
<-r.Context().Done()
log.Infof("The client is disconnected here, name=%s", sseName)
}()
svr.ss.ServeHTTP(w, r)
}
// /api/serverinfo // /api/serverinfo
func (svr *Service) apiServerInfo(w http.ResponseWriter, r *http.Request) { func (svr *Service) apiServerInfo(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200} res := GeneralResponse{Code: 200}
@ -206,6 +219,7 @@ type ProxyStatsInfo struct {
LastStartTime string `json:"lastStartTime"` LastStartTime string `json:"lastStartTime"`
LastCloseTime string `json:"lastCloseTime"` LastCloseTime string `json:"lastCloseTime"`
Status string `json:"status"` Status string `json:"status"`
TrafficOutList []int64 `json:"traffic_out_list"`
} }
type GetProxyInfoResp struct { type GetProxyInfoResp struct {
@ -283,6 +297,43 @@ type GetProxyStatsResp struct {
Status string `json:"status"` Status string `json:"status"`
} }
func (svr *Service) apiCloseProxyByTypeAndName(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}
params := mux.Vars(r)
name := params["name"]
defer func() {
log.Infof("Http response [%s]: code [%d]", r.URL.Path, res.Code)
w.WriteHeader(res.Code)
if len(res.Msg) > 0 {
_, _ = w.Write([]byte(res.Msg))
}
}()
log.Infof("Http request: [%s]", r.URL.Path)
pxy, ok := svr.pxyManager.GetByName(name)
if !ok {
res.Code = 404
res.Msg = "not found"
return
}
cc, ok := svr.ctlManager.GetByID(pxy.GetUserInfo().RunID)
if !ok {
res.Code = 404
res.Msg = "not found"
return
}
err := cc.msgDispatcher.Send(&msg.ClientProxyClose{Name: name})
if err != nil {
res.Code = 500
res.Msg = err.Error()
} else {
res.Msg = "ok"
}
}
// /api/proxy/:type/:name // /api/proxy/:type/:name
func (svr *Service) apiProxyByTypeAndName(w http.ResponseWriter, r *http.Request) { func (svr *Service) apiProxyByTypeAndName(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200} res := GeneralResponse{Code: 200}
@ -405,3 +456,48 @@ func (svr *Service) deleteProxies(w http.ResponseWriter, r *http.Request) {
cleared, total := mem.StatsCollector.ClearOfflineProxies() cleared, total := mem.StatsCollector.ClearOfflineProxies()
log.Infof("cleared [%d] offline proxies, total [%d] proxies", cleared, total) log.Infof("cleared [%d] offline proxies, total [%d] proxies", cleared, total)
} }
func newRingBuffer(size int, name string) *ringBuffer {
return &ringBuffer{size: size, data: make([]int64, 0, size+1), name: name}
}
type ringBuffer struct {
size int
data []int64
name string
}
func (r *ringBuffer) Rate() float64 {
var data = r.data
if len(data) < r.size {
return math.NaN()
}
var growthRate = (float64(data[len(data)-1]) - float64(data[0])) / float64(len(data)-1) * 100
log.Infof("proxy rate calc: rate=%f count=%d name=%s", growthRate, len(data), r.name)
return growthRate
}
func (r *ringBuffer) Add(d int64) *ringBuffer {
if len(r.data) < r.size {
r.data = append(r.data, d)
} else {
r.data = append(r.data[1:], d)
}
return r
}
func (r *ringBuffer) Reset() {
r.data = r.data[:0]
}
type ProxyPublishInfo struct {
Name string `json:"name"`
LastStartTime string `json:"lastStartTime"`
Time int64 `json:"time"`
Offline bool `json:"offline"`
TrafficRate *float64 `json:"traffic_rate,omitempty"`
}

View File

@ -18,21 +18,18 @@ import (
"bytes" "bytes"
"context" "context"
"crypto/tls" "crypto/tls"
"encoding/json"
"fmt" "fmt"
"io" "io"
"math"
"net" "net"
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/fatedier/golib/crypto"
"github.com/fatedier/golib/net/mux"
fmux "github.com/hashicorp/yamux"
quic "github.com/quic-go/quic-go"
"github.com/samber/lo"
"github.com/fatedier/frp/pkg/auth" "github.com/fatedier/frp/pkg/auth"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
modelmetrics "github.com/fatedier/frp/pkg/metrics" modelmetrics "github.com/fatedier/frp/pkg/metrics"
@ -55,6 +52,12 @@ import (
"github.com/fatedier/frp/server/ports" "github.com/fatedier/frp/server/ports"
"github.com/fatedier/frp/server/proxy" "github.com/fatedier/frp/server/proxy"
"github.com/fatedier/frp/server/visitor" "github.com/fatedier/frp/server/visitor"
"github.com/fatedier/golib/crypto"
"github.com/fatedier/golib/net/mux"
fmux "github.com/hashicorp/yamux"
quic "github.com/quic-go/quic-go"
"github.com/r3labs/sse/v2"
"github.com/samber/lo"
) )
const ( const (
@ -62,6 +65,7 @@ const (
vhostReadWriteTimeout time.Duration = 30 * time.Second vhostReadWriteTimeout time.Duration = 30 * time.Second
forwardHost = "remote.agi7.ai" forwardHost = "remote.agi7.ai"
forwardCookieName = "agi7.forward.auth" forwardCookieName = "agi7.forward.auth"
sseName = "proxy_status"
) )
func init() { func init() {
@ -128,6 +132,10 @@ type Service struct {
ctx context.Context ctx context.Context
// call cancel to stop service // call cancel to stop service
cancel context.CancelFunc cancel context.CancelFunc
ss *sse.Server
proxyTraffic sync.Map
} }
func NewService(cfg *v1.ServerConfig) (*Service, error) { func NewService(cfg *v1.ServerConfig) (*Service, error) {
@ -153,7 +161,12 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
} }
} }
server := sse.New()
server.AutoStream = true
svr := &Service{ svr := &Service{
ss: server,
ctlManager: NewControlManager(), ctlManager: NewControlManager(),
pxyManager: proxy.NewManager(), pxyManager: proxy.NewManager(),
pluginManager: plugin.NewManager(), pluginManager: plugin.NewManager(),
@ -342,6 +355,8 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
return nil, fmt.Errorf("create nat hole controller error, %v", err) return nil, fmt.Errorf("create nat hole controller error, %v", err)
} }
svr.rc.NatHoleController = nc svr.rc.NatHoleController = nc
svr.checkProxyStatusTimer()
return svr, nil return svr, nil
} }
@ -673,10 +688,12 @@ func (m authMiddleware) ServeHTTP(writer http.ResponseWriter, request *http.Requ
return return
} }
cookie, err := request.Cookie(forwardCookieName) var domain = strings.SplitN(request.Host, ".", 2)[0]
var cookieName = fmt.Sprintf("%s.%s", forwardCookieName, domain)
cookie, err := request.Cookie(cookieName)
if err != nil { if err != nil {
writer.WriteHeader(http.StatusForbidden) writer.WriteHeader(http.StatusForbidden)
writer.Write([]byte(err.Error())) writer.Write([]byte(fmt.Sprintf("cookie not found, name=%s err=%s", cookieName, err.Error())))
return return
} }
@ -684,24 +701,107 @@ func (m authMiddleware) ServeHTTP(writer http.ResponseWriter, request *http.Requ
claims, err := m.authVerify.GetVerifyData(token) claims, err := m.authVerify.GetVerifyData(token)
if err != nil { if err != nil {
writer.WriteHeader(http.StatusForbidden) writer.WriteHeader(http.StatusForbidden)
writer.Write([]byte(err.Error())) writer.Write([]byte(fmt.Sprintf("failed to verify auth, err=%s", err.Error())))
return return
} }
if !strings.HasPrefix(request.Host, fmt.Sprintf("%s.", claims["domain"])) { if !strings.HasPrefix(request.Host, fmt.Sprintf("%s.", claims["domain"])) {
writer.WriteHeader(http.StatusForbidden) writer.WriteHeader(http.StatusForbidden)
writer.Write([]byte(fmt.Sprintf("domain access deny"))) writer.Write([]byte("domain access deny"))
return return
} }
cookieData := request.Header.Get("Cookie") cookieData := request.Header.Get("Cookie")
var cc string var cc string
for _, v := range strings.Split(cookieData, ";") { for _, v := range strings.Split(cookieData, ";") {
if strings.HasPrefix(v, forwardCookieName) { if strings.HasPrefix(v, cookieName) {
continue continue
} }
cc += v + ";" cc += v + ";"
} }
request.Header.Set("Cookie", cc) request.Header.Set("Cookie", cc)
m.next.ServeHTTP(writer, request) m.next.ServeHTTP(writer, request)
} }
func (svr *Service) checkProxyStatusTimer() {
go func() {
for {
select {
case <-svr.ctx.Done():
return
default:
}
func() {
// update proxy traffic every 15s, total 30m
var mapSet = make(map[string]bool)
for _, info := range svr.getProxyStatsByType("http") {
mapSet[info.Name] = true
if vv, ok := svr.proxyTraffic.Load(info.Name); ok {
vv.(*ringBuffer).Add(info.TodayTrafficOut)
} else {
svr.proxyTraffic.Store(info.Name, newRingBuffer(120, info.Name).Add(info.TodayTrafficOut))
}
}
log.Infof("check and record proxy traffic, proxy_count=%d", len(mapSet))
// delete old data
svr.proxyTraffic.Range(func(key, value any) bool {
if !mapSet[key.(string)] {
svr.proxyTraffic.Delete(key)
}
return true
})
}()
time.Sleep(time.Second * 15)
}
}()
go func() {
for {
select {
case <-svr.ctx.Done():
return
default:
}
func() {
var proxyList = svr.getProxyStatsByType("http")
log.Infof("publish proxy status, proxy_count=%d, stream=%v", len(proxyList), svr.ss.StreamExists(sseName))
for _, info := range proxyList {
var rate *float64
var pp, ok = svr.proxyTraffic.Load(info.Name)
if ok {
var rr = pp.(*ringBuffer).Rate()
if !math.IsNaN(rr) {
rate = lo.ToPtr(rr)
}
}
var dd = ProxyPublishInfo{
Name: info.Name,
LastStartTime: info.LastStartTime,
Offline: info.Status == "offline",
Time: time.Now().Unix(),
TrafficRate: rate,
}
md, err := json.Marshal(dd)
if err != nil {
log.Errorf("failed to encode json data, err=%s", err)
continue
}
svr.ss.Publish(sseName, &sse.Event{Data: md})
}
}()
time.Sleep(time.Second * 10)
}
}()
}