
* 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>
504 lines
14 KiB
Go
504 lines
14 KiB
Go
// Copyright 2017 fatedier, fatedier@gmail.com
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package server
|
|
|
|
import (
|
|
"cmp"
|
|
"encoding/json"
|
|
"math"
|
|
"net/http"
|
|
"slices"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
|
|
"github.com/fatedier/frp/pkg/config/types"
|
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
"github.com/fatedier/frp/pkg/metrics/mem"
|
|
"github.com/fatedier/frp/pkg/msg"
|
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
|
"github.com/fatedier/frp/pkg/util/log"
|
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
|
"github.com/fatedier/frp/pkg/util/version"
|
|
)
|
|
|
|
// TODO(fatedier): add an API to clean status of all offline proxies.
|
|
|
|
type GeneralResponse struct {
|
|
Code int
|
|
Msg string
|
|
}
|
|
|
|
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
|
|
helper.Router.HandleFunc("/healthz", svr.healthz)
|
|
subRouter := helper.Router.NewRoute().Subrouter()
|
|
|
|
subRouter.Use(helper.AuthMiddleware.Middleware)
|
|
|
|
// metrics
|
|
if svr.cfg.EnablePrometheus {
|
|
subRouter.Handle("/metrics", promhttp.Handler())
|
|
}
|
|
|
|
// apis
|
|
subRouter.HandleFunc("/api/sub", svr.apiServerSub)
|
|
subRouter.HandleFunc("/api/serverinfo", svr.apiServerInfo).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}/close", svr.apiCloseProxyByTypeAndName).Methods("POST")
|
|
subRouter.HandleFunc("/api/traffic/{name}", svr.apiProxyTraffic).Methods("GET")
|
|
subRouter.HandleFunc("/api/proxies", svr.deleteProxies).Methods("DELETE")
|
|
|
|
// view
|
|
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
|
|
subRouter.PathPrefix("/static/").Handler(
|
|
netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
|
|
).Methods("GET")
|
|
|
|
subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
|
|
})
|
|
}
|
|
|
|
type serverInfoResp struct {
|
|
Version string `json:"version"`
|
|
BindPort int `json:"bindPort"`
|
|
VhostHTTPPort int `json:"vhostHTTPPort"`
|
|
VhostHTTPSPort int `json:"vhostHTTPSPort"`
|
|
TCPMuxHTTPConnectPort int `json:"tcpmuxHTTPConnectPort"`
|
|
KCPBindPort int `json:"kcpBindPort"`
|
|
QUICBindPort int `json:"quicBindPort"`
|
|
SubdomainHost string `json:"subdomainHost"`
|
|
MaxPoolCount int64 `json:"maxPoolCount"`
|
|
MaxPortsPerClient int64 `json:"maxPortsPerClient"`
|
|
HeartBeatTimeout int64 `json:"heartbeatTimeout"`
|
|
AllowPortsStr string `json:"allowPortsStr,omitempty"`
|
|
TLSForce bool `json:"tlsForce,omitempty"`
|
|
|
|
TotalTrafficIn int64 `json:"totalTrafficIn"`
|
|
TotalTrafficOut int64 `json:"totalTrafficOut"`
|
|
CurConns int64 `json:"curConns"`
|
|
ClientCounts int64 `json:"clientCounts"`
|
|
ProxyTypeCounts map[string]int64 `json:"proxyTypeCount"`
|
|
}
|
|
|
|
// /healthz
|
|
func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
|
|
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
|
|
func (svr *Service) apiServerInfo(w http.ResponseWriter, r *http.Request) {
|
|
res := GeneralResponse{Code: 200}
|
|
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)
|
|
serverStats := mem.StatsCollector.GetServer()
|
|
svrResp := serverInfoResp{
|
|
Version: version.Full(),
|
|
BindPort: svr.cfg.BindPort,
|
|
VhostHTTPPort: svr.cfg.VhostHTTPPort,
|
|
VhostHTTPSPort: svr.cfg.VhostHTTPSPort,
|
|
TCPMuxHTTPConnectPort: svr.cfg.TCPMuxHTTPConnectPort,
|
|
KCPBindPort: svr.cfg.KCPBindPort,
|
|
QUICBindPort: svr.cfg.QUICBindPort,
|
|
SubdomainHost: svr.cfg.SubDomainHost,
|
|
MaxPoolCount: svr.cfg.Transport.MaxPoolCount,
|
|
MaxPortsPerClient: svr.cfg.MaxPortsPerClient,
|
|
HeartBeatTimeout: svr.cfg.Transport.HeartbeatTimeout,
|
|
AllowPortsStr: types.PortsRangeSlice(svr.cfg.AllowPorts).String(),
|
|
TLSForce: svr.cfg.Transport.TLS.Force,
|
|
|
|
TotalTrafficIn: serverStats.TotalTrafficIn,
|
|
TotalTrafficOut: serverStats.TotalTrafficOut,
|
|
CurConns: serverStats.CurConns,
|
|
ClientCounts: serverStats.ClientCounts,
|
|
ProxyTypeCounts: serverStats.ProxyTypeCounts,
|
|
}
|
|
|
|
buf, _ := json.Marshal(&svrResp)
|
|
res.Msg = string(buf)
|
|
}
|
|
|
|
type BaseOutConf struct {
|
|
v1.ProxyBaseConfig
|
|
}
|
|
|
|
type TCPOutConf struct {
|
|
BaseOutConf
|
|
RemotePort int `json:"remotePort"`
|
|
}
|
|
|
|
type TCPMuxOutConf struct {
|
|
BaseOutConf
|
|
v1.DomainConfig
|
|
Multiplexer string `json:"multiplexer"`
|
|
}
|
|
|
|
type UDPOutConf struct {
|
|
BaseOutConf
|
|
RemotePort int `json:"remotePort"`
|
|
}
|
|
|
|
type HTTPOutConf struct {
|
|
BaseOutConf
|
|
v1.DomainConfig
|
|
Locations []string `json:"locations"`
|
|
HostHeaderRewrite string `json:"hostHeaderRewrite"`
|
|
}
|
|
|
|
type HTTPSOutConf struct {
|
|
BaseOutConf
|
|
v1.DomainConfig
|
|
}
|
|
|
|
type STCPOutConf struct {
|
|
BaseOutConf
|
|
}
|
|
|
|
type XTCPOutConf struct {
|
|
BaseOutConf
|
|
}
|
|
|
|
func getConfByType(proxyType string) any {
|
|
switch v1.ProxyType(proxyType) {
|
|
case v1.ProxyTypeTCP:
|
|
return &TCPOutConf{}
|
|
case v1.ProxyTypeTCPMUX:
|
|
return &TCPMuxOutConf{}
|
|
case v1.ProxyTypeUDP:
|
|
return &UDPOutConf{}
|
|
case v1.ProxyTypeHTTP:
|
|
return &HTTPOutConf{}
|
|
case v1.ProxyTypeHTTPS:
|
|
return &HTTPSOutConf{}
|
|
case v1.ProxyTypeSTCP:
|
|
return &STCPOutConf{}
|
|
case v1.ProxyTypeXTCP:
|
|
return &XTCPOutConf{}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Get proxy info.
|
|
type ProxyStatsInfo struct {
|
|
Name string `json:"name"`
|
|
Conf interface{} `json:"conf"`
|
|
ClientVersion string `json:"clientVersion,omitempty"`
|
|
TodayTrafficIn int64 `json:"todayTrafficIn"`
|
|
TodayTrafficOut int64 `json:"todayTrafficOut"`
|
|
CurConns int64 `json:"curConns"`
|
|
LastStartTime string `json:"lastStartTime"`
|
|
LastCloseTime string `json:"lastCloseTime"`
|
|
Status string `json:"status"`
|
|
TrafficOutList []int64 `json:"traffic_out_list"`
|
|
}
|
|
|
|
type GetProxyInfoResp struct {
|
|
Proxies []*ProxyStatsInfo `json:"proxies"`
|
|
}
|
|
|
|
// /api/proxy/:type
|
|
func (svr *Service) apiProxyByType(w http.ResponseWriter, r *http.Request) {
|
|
res := GeneralResponse{Code: 200}
|
|
params := mux.Vars(r)
|
|
proxyType := params["type"]
|
|
|
|
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)
|
|
|
|
proxyInfoResp := GetProxyInfoResp{}
|
|
proxyInfoResp.Proxies = svr.getProxyStatsByType(proxyType)
|
|
slices.SortFunc(proxyInfoResp.Proxies, func(a, b *ProxyStatsInfo) int {
|
|
return cmp.Compare(a.Name, b.Name)
|
|
})
|
|
|
|
buf, _ := json.Marshal(&proxyInfoResp)
|
|
res.Msg = string(buf)
|
|
}
|
|
|
|
func (svr *Service) getProxyStatsByType(proxyType string) (proxyInfos []*ProxyStatsInfo) {
|
|
proxyStats := mem.StatsCollector.GetProxiesByType(proxyType)
|
|
proxyInfos = make([]*ProxyStatsInfo, 0, len(proxyStats))
|
|
for _, ps := range proxyStats {
|
|
proxyInfo := &ProxyStatsInfo{}
|
|
if pxy, ok := svr.pxyManager.GetByName(ps.Name); ok {
|
|
content, err := json.Marshal(pxy.GetConfigurer())
|
|
if err != nil {
|
|
log.Warnf("marshal proxy [%s] conf info error: %v", ps.Name, err)
|
|
continue
|
|
}
|
|
proxyInfo.Conf = getConfByType(ps.Type)
|
|
if err = json.Unmarshal(content, &proxyInfo.Conf); err != nil {
|
|
log.Warnf("unmarshal proxy [%s] conf info error: %v", ps.Name, err)
|
|
continue
|
|
}
|
|
proxyInfo.Status = "online"
|
|
if pxy.GetLoginMsg() != nil {
|
|
proxyInfo.ClientVersion = pxy.GetLoginMsg().Version
|
|
}
|
|
} else {
|
|
proxyInfo.Status = "offline"
|
|
}
|
|
proxyInfo.Name = ps.Name
|
|
proxyInfo.TodayTrafficIn = ps.TodayTrafficIn
|
|
proxyInfo.TodayTrafficOut = ps.TodayTrafficOut
|
|
proxyInfo.CurConns = ps.CurConns
|
|
proxyInfo.LastStartTime = ps.LastStartTime
|
|
proxyInfo.LastCloseTime = ps.LastCloseTime
|
|
proxyInfos = append(proxyInfos, proxyInfo)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Get proxy info by name.
|
|
type GetProxyStatsResp struct {
|
|
Name string `json:"name"`
|
|
Conf interface{} `json:"conf"`
|
|
TodayTrafficIn int64 `json:"todayTrafficIn"`
|
|
TodayTrafficOut int64 `json:"todayTrafficOut"`
|
|
CurConns int64 `json:"curConns"`
|
|
LastStartTime string `json:"lastStartTime"`
|
|
LastCloseTime string `json:"lastCloseTime"`
|
|
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
|
|
func (svr *Service) apiProxyByTypeAndName(w http.ResponseWriter, r *http.Request) {
|
|
res := GeneralResponse{Code: 200}
|
|
params := mux.Vars(r)
|
|
proxyType := params["type"]
|
|
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)
|
|
|
|
var proxyStatsResp GetProxyStatsResp
|
|
proxyStatsResp, res.Code, res.Msg = svr.getProxyStatsByTypeAndName(proxyType, name)
|
|
if res.Code != 200 {
|
|
return
|
|
}
|
|
|
|
buf, _ := json.Marshal(&proxyStatsResp)
|
|
res.Msg = string(buf)
|
|
}
|
|
|
|
func (svr *Service) getProxyStatsByTypeAndName(proxyType string, proxyName string) (proxyInfo GetProxyStatsResp, code int, msg string) {
|
|
proxyInfo.Name = proxyName
|
|
ps := mem.StatsCollector.GetProxiesByTypeAndName(proxyType, proxyName)
|
|
if ps == nil {
|
|
code = 404
|
|
msg = "no proxy info found"
|
|
} else {
|
|
if pxy, ok := svr.pxyManager.GetByName(proxyName); ok {
|
|
content, err := json.Marshal(pxy.GetConfigurer())
|
|
if err != nil {
|
|
log.Warnf("marshal proxy [%s] conf info error: %v", ps.Name, err)
|
|
code = 400
|
|
msg = "parse conf error"
|
|
return
|
|
}
|
|
proxyInfo.Conf = getConfByType(ps.Type)
|
|
if err = json.Unmarshal(content, &proxyInfo.Conf); err != nil {
|
|
log.Warnf("unmarshal proxy [%s] conf info error: %v", ps.Name, err)
|
|
code = 400
|
|
msg = "parse conf error"
|
|
return
|
|
}
|
|
proxyInfo.Status = "online"
|
|
} else {
|
|
proxyInfo.Status = "offline"
|
|
}
|
|
proxyInfo.TodayTrafficIn = ps.TodayTrafficIn
|
|
proxyInfo.TodayTrafficOut = ps.TodayTrafficOut
|
|
proxyInfo.CurConns = ps.CurConns
|
|
proxyInfo.LastStartTime = ps.LastStartTime
|
|
proxyInfo.LastCloseTime = ps.LastCloseTime
|
|
code = 200
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// /api/traffic/:name
|
|
type GetProxyTrafficResp struct {
|
|
Name string `json:"name"`
|
|
TrafficIn []int64 `json:"trafficIn"`
|
|
TrafficOut []int64 `json:"trafficOut"`
|
|
}
|
|
|
|
func (svr *Service) apiProxyTraffic(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)
|
|
|
|
trafficResp := GetProxyTrafficResp{}
|
|
trafficResp.Name = name
|
|
proxyTrafficInfo := mem.StatsCollector.GetProxyTraffic(name)
|
|
|
|
if proxyTrafficInfo == nil {
|
|
res.Code = 404
|
|
res.Msg = "no proxy info found"
|
|
return
|
|
}
|
|
trafficResp.TrafficIn = proxyTrafficInfo.TrafficIn
|
|
trafficResp.TrafficOut = proxyTrafficInfo.TrafficOut
|
|
|
|
buf, _ := json.Marshal(&trafficResp)
|
|
res.Msg = string(buf)
|
|
}
|
|
|
|
// DELETE /api/proxies?status=offline
|
|
func (svr *Service) deleteProxies(w http.ResponseWriter, r *http.Request) {
|
|
res := GeneralResponse{Code: 200}
|
|
|
|
log.Infof("Http request: [%s]", r.URL.Path)
|
|
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))
|
|
}
|
|
}()
|
|
|
|
status := r.URL.Query().Get("status")
|
|
if status != "offline" {
|
|
res.Code = 400
|
|
res.Msg = "status only support offline"
|
|
return
|
|
}
|
|
cleared, total := mem.StatsCollector.ClearOfflineProxies()
|
|
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"`
|
|
}
|