refactor: restructure API packages into client/http and server/http with typed proxy/visitor models (#5193)

This commit is contained in:
fatedier
2026-03-04 17:38:43 +08:00
committed by GitHub
parent 381245a439
commit fbeb6ca43a
32 changed files with 1704 additions and 727 deletions

64
server/api_router.go Normal file
View File

@@ -0,0 +1,64 @@
// 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 (
"net/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
httppkg "github.com/fatedier/frp/pkg/util/http"
netpkg "github.com/fatedier/frp/pkg/util/net"
adminapi "github.com/fatedier/frp/server/http"
)
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
helper.Router.HandleFunc("/healthz", healthz)
subRouter := helper.Router.NewRoute().Subrouter()
subRouter.Use(helper.AuthMiddleware)
subRouter.Use(httppkg.NewRequestLogger)
// metrics
if svr.cfg.EnablePrometheus {
subRouter.Handle("/metrics", promhttp.Handler())
}
apiController := adminapi.NewController(svr.cfg, svr.clientRegistry, svr.pxyManager)
// apis
subRouter.HandleFunc("/api/serverinfo", httppkg.MakeHTTPHandlerFunc(apiController.APIServerInfo)).Methods("GET")
subRouter.HandleFunc("/api/proxy/{type}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByType)).Methods("GET")
subRouter.HandleFunc("/api/proxy/{type}/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByTypeAndName)).Methods("GET")
subRouter.HandleFunc("/api/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByName)).Methods("GET")
subRouter.HandleFunc("/api/traffic/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyTraffic)).Methods("GET")
subRouter.HandleFunc("/api/clients", httppkg.MakeHTTPHandlerFunc(apiController.APIClientList)).Methods("GET")
subRouter.HandleFunc("/api/clients/{key}", httppkg.MakeHTTPHandlerFunc(apiController.APIClientDetail)).Methods("GET")
subRouter.HandleFunc("/api/proxies", httppkg.MakeHTTPHandlerFunc(apiController.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)
})
}
func healthz(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(200)
}

View File

@@ -12,11 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package api
package http
import (
"cmp"
"encoding/json"
"fmt"
"net/http"
"slices"
@@ -29,6 +28,7 @@ import (
httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/pkg/util/log"
"github.com/fatedier/frp/pkg/util/version"
"github.com/fatedier/frp/server/http/model"
"github.com/fatedier/frp/server/proxy"
"github.com/fatedier/frp/server/registry"
)
@@ -59,7 +59,7 @@ func NewController(
// /api/serverinfo
func (c *Controller) APIServerInfo(ctx *httppkg.Context) (any, error) {
serverStats := mem.StatsCollector.GetServer()
svrResp := ServerInfoResp{
svrResp := model.ServerInfoResp{
Version: version.Full(),
BindPort: c.serverCfg.BindPort,
VhostHTTPPort: c.serverCfg.VhostHTTPPort,
@@ -80,22 +80,6 @@ func (c *Controller) APIServerInfo(ctx *httppkg.Context) (any, error) {
ClientCounts: serverStats.ClientCounts,
ProxyTypeCounts: serverStats.ProxyTypeCounts,
}
// For API that returns struct, we can just return it.
// But current GeneralResponse.Msg in legacy code expects a JSON string.
// Since MakeHTTPHandlerFunc handles struct by encoding to JSON, we can return svrResp directly?
// The original code wraps it in GeneralResponse{Msg: string(json)}.
// If we return svrResp, the response body will be the JSON of svrResp.
// We should check if the frontend expects { "code": 200, "msg": "{...}" } or just {...}.
// Looking at previous code:
// res := GeneralResponse{Code: 200}
// buf, _ := json.Marshal(&svrResp)
// res.Msg = string(buf)
// Response body: {"code": 200, "msg": "{\"version\":...}"}
// Wait, is it double encoded JSON? Yes it seems so!
// Let's check dashboard_api.go original code again.
// Yes: res.Msg = string(buf).
// So the frontend expects { "code": 200, "msg": "JSON_STRING" }.
// This is kind of ugly, but we must preserve compatibility.
return svrResp, nil
}
@@ -112,7 +96,7 @@ func (c *Controller) APIClientList(ctx *httppkg.Context) (any, error) {
statusFilter := strings.ToLower(ctx.Query("status"))
records := c.clientRegistry.List()
items := make([]ClientInfoResp, 0, len(records))
items := make([]model.ClientInfoResp, 0, len(records))
for _, info := range records {
if userFilter != "" && info.User != userFilter {
continue
@@ -129,7 +113,7 @@ func (c *Controller) APIClientList(ctx *httppkg.Context) (any, error) {
items = append(items, buildClientInfoResp(info))
}
slices.SortFunc(items, func(a, b ClientInfoResp) int {
slices.SortFunc(items, func(a, b model.ClientInfoResp) int {
if v := cmp.Compare(a.User, b.User); v != 0 {
return v
}
@@ -165,9 +149,9 @@ func (c *Controller) APIClientDetail(ctx *httppkg.Context) (any, error) {
func (c *Controller) APIProxyByType(ctx *httppkg.Context) (any, error) {
proxyType := ctx.Param("type")
proxyInfoResp := GetProxyInfoResp{}
proxyInfoResp := model.GetProxyInfoResp{}
proxyInfoResp.Proxies = c.getProxyStatsByType(proxyType)
slices.SortFunc(proxyInfoResp.Proxies, func(a, b *ProxyStatsInfo) int {
slices.SortFunc(proxyInfoResp.Proxies, func(a, b *model.ProxyStatsInfo) int {
return cmp.Compare(a.Name, b.Name)
})
@@ -191,7 +175,7 @@ func (c *Controller) APIProxyByTypeAndName(ctx *httppkg.Context) (any, error) {
func (c *Controller) APIProxyTraffic(ctx *httppkg.Context) (any, error) {
name := ctx.Param("name")
trafficResp := GetProxyTrafficResp{}
trafficResp := model.GetProxyTrafficResp{}
trafficResp.Name = name
proxyTrafficInfo := mem.StatsCollector.GetProxyTraffic(name)
@@ -213,7 +197,7 @@ func (c *Controller) APIProxyByName(ctx *httppkg.Context) (any, error) {
return nil, httppkg.NewError(http.StatusNotFound, "no proxy info found")
}
proxyInfo := GetProxyStatsResp{
proxyInfo := model.GetProxyStatsResp{
Name: ps.Name,
User: ps.User,
ClientID: ps.ClientID,
@@ -225,16 +209,7 @@ func (c *Controller) APIProxyByName(ctx *httppkg.Context) (any, error) {
}
if pxy, ok := c.pxyManager.GetByName(name); ok {
content, err := json.Marshal(pxy.GetConfigurer())
if err != nil {
log.Warnf("marshal proxy [%s] conf info error: %v", name, err)
return nil, httppkg.NewError(http.StatusBadRequest, "parse conf error")
}
proxyInfo.Conf = getConfByType(ps.Type)
if err = json.Unmarshal(content, &proxyInfo.Conf); err != nil {
log.Warnf("unmarshal proxy [%s] conf info error: %v", name, err)
return nil, httppkg.NewError(http.StatusBadRequest, "parse conf error")
}
proxyInfo.Conf = getConfFromConfigurer(pxy.GetConfigurer())
proxyInfo.Status = "online"
} else {
proxyInfo.Status = "offline"
@@ -254,25 +229,16 @@ func (c *Controller) DeleteProxies(ctx *httppkg.Context) (any, error) {
return httppkg.GeneralResponse{Code: 200, Msg: "success"}, nil
}
func (c *Controller) getProxyStatsByType(proxyType string) (proxyInfos []*ProxyStatsInfo) {
func (c *Controller) getProxyStatsByType(proxyType string) (proxyInfos []*model.ProxyStatsInfo) {
proxyStats := mem.StatsCollector.GetProxiesByType(proxyType)
proxyInfos = make([]*ProxyStatsInfo, 0, len(proxyStats))
proxyInfos = make([]*model.ProxyStatsInfo, 0, len(proxyStats))
for _, ps := range proxyStats {
proxyInfo := &ProxyStatsInfo{
proxyInfo := &model.ProxyStatsInfo{
User: ps.User,
ClientID: ps.ClientID,
}
if pxy, ok := c.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.Conf = getConfFromConfigurer(pxy.GetConfigurer())
proxyInfo.Status = "online"
} else {
proxyInfo.Status = "offline"
@@ -288,7 +254,7 @@ func (c *Controller) getProxyStatsByType(proxyType string) (proxyInfos []*ProxyS
return
}
func (c *Controller) getProxyStatsByTypeAndName(proxyType string, proxyName string) (proxyInfo GetProxyStatsResp, code int, msg string) {
func (c *Controller) getProxyStatsByTypeAndName(proxyType string, proxyName string) (proxyInfo model.GetProxyStatsResp, code int, msg string) {
proxyInfo.Name = proxyName
ps := mem.StatsCollector.GetProxiesByTypeAndName(proxyType, proxyName)
if ps == nil {
@@ -298,20 +264,7 @@ func (c *Controller) getProxyStatsByTypeAndName(proxyType string, proxyName stri
proxyInfo.User = ps.User
proxyInfo.ClientID = ps.ClientID
if pxy, ok := c.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.Conf = getConfFromConfigurer(pxy.GetConfigurer())
proxyInfo.Status = "online"
} else {
proxyInfo.Status = "offline"
@@ -327,8 +280,8 @@ func (c *Controller) getProxyStatsByTypeAndName(proxyType string, proxyName stri
return
}
func buildClientInfoResp(info registry.ClientInfo) ClientInfoResp {
resp := ClientInfoResp{
func buildClientInfoResp(info registry.ClientInfo) model.ClientInfoResp {
resp := model.ClientInfoResp{
Key: info.Key,
User: info.User,
ClientID: info.ClientID(),
@@ -366,23 +319,37 @@ func matchStatusFilter(online bool, filter string) bool {
}
}
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
func getConfFromConfigurer(cfg v1.ProxyConfigurer) any {
outBase := model.BaseOutConf{ProxyBaseConfig: *cfg.GetBaseConfig()}
switch c := cfg.(type) {
case *v1.TCPProxyConfig:
return &model.TCPOutConf{BaseOutConf: outBase, RemotePort: c.RemotePort}
case *v1.UDPProxyConfig:
return &model.UDPOutConf{BaseOutConf: outBase, RemotePort: c.RemotePort}
case *v1.HTTPProxyConfig:
return &model.HTTPOutConf{
BaseOutConf: outBase,
DomainConfig: c.DomainConfig,
Locations: c.Locations,
HostHeaderRewrite: c.HostHeaderRewrite,
}
case *v1.HTTPSProxyConfig:
return &model.HTTPSOutConf{
BaseOutConf: outBase,
DomainConfig: c.DomainConfig,
}
case *v1.TCPMuxProxyConfig:
return &model.TCPMuxOutConf{
BaseOutConf: outBase,
DomainConfig: c.DomainConfig,
Multiplexer: c.Multiplexer,
RouteByHTTPUser: c.RouteByHTTPUser,
}
case *v1.STCPProxyConfig:
return &model.STCPOutConf{BaseOutConf: outBase}
case *v1.XTCPProxyConfig:
return &model.XTCPOutConf{BaseOutConf: outBase}
}
return outBase
}

View File

@@ -0,0 +1,71 @@
// Copyright 2026 The frp Authors
//
// 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 http
import (
"encoding/json"
"testing"
v1 "github.com/fatedier/frp/pkg/config/v1"
)
func TestGetConfFromConfigurerKeepsPluginFields(t *testing.T) {
cfg := &v1.TCPProxyConfig{
ProxyBaseConfig: v1.ProxyBaseConfig{
Name: "test-proxy",
Type: string(v1.ProxyTypeTCP),
ProxyBackend: v1.ProxyBackend{
Plugin: v1.TypedClientPluginOptions{
Type: v1.PluginHTTPProxy,
ClientPluginOptions: &v1.HTTPProxyPluginOptions{
Type: v1.PluginHTTPProxy,
HTTPUser: "user",
HTTPPassword: "password",
},
},
},
},
RemotePort: 6000,
}
content, err := json.Marshal(getConfFromConfigurer(cfg))
if err != nil {
t.Fatalf("marshal conf failed: %v", err)
}
var out map[string]any
if err := json.Unmarshal(content, &out); err != nil {
t.Fatalf("unmarshal conf failed: %v", err)
}
pluginValue, ok := out["plugin"]
if !ok {
t.Fatalf("plugin field missing in output: %v", out)
}
plugin, ok := pluginValue.(map[string]any)
if !ok {
t.Fatalf("plugin field should be object, got: %#v", pluginValue)
}
if got := plugin["type"]; got != v1.PluginHTTPProxy {
t.Fatalf("plugin type mismatch, want %q got %#v", v1.PluginHTTPProxy, got)
}
if got := plugin["httpUser"]; got != "user" {
t.Fatalf("plugin httpUser mismatch, want %q got %#v", "user", got)
}
if got := plugin["httpPassword"]; got != "password" {
t.Fatalf("plugin httpPassword mismatch, want %q got %#v", "password", got)
}
}

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package api
package model
import (
v1 "github.com/fatedier/frp/pkg/config/v1"

View File

@@ -28,7 +28,6 @@ import (
"github.com/fatedier/golib/crypto"
"github.com/fatedier/golib/net/mux"
fmux "github.com/hashicorp/yamux"
"github.com/prometheus/client_golang/prometheus/promhttp"
quic "github.com/quic-go/quic-go"
"github.com/samber/lo"
@@ -48,7 +47,6 @@ import (
"github.com/fatedier/frp/pkg/util/version"
"github.com/fatedier/frp/pkg/util/vhost"
"github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/server/api"
"github.com/fatedier/frp/server/controller"
"github.com/fatedier/frp/server/group"
"github.com/fatedier/frp/server/metrics"
@@ -690,42 +688,3 @@ func (svr *Service) RegisterVisitorConn(visitorConn net.Conn, newMsg *msg.NewVis
return svr.rc.VisitorManager.NewConn(newMsg.ProxyName, visitorConn, newMsg.Timestamp, newMsg.SignKey,
newMsg.UseEncryption, newMsg.UseCompression, visitorUser)
}
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
helper.Router.HandleFunc("/healthz", healthz)
subRouter := helper.Router.NewRoute().Subrouter()
subRouter.Use(helper.AuthMiddleware)
subRouter.Use(httppkg.NewRequestLogger)
// metrics
if svr.cfg.EnablePrometheus {
subRouter.Handle("/metrics", promhttp.Handler())
}
apiController := api.NewController(svr.cfg, svr.clientRegistry, svr.pxyManager)
// apis
subRouter.HandleFunc("/api/serverinfo", httppkg.MakeHTTPHandlerFunc(apiController.APIServerInfo)).Methods("GET")
subRouter.HandleFunc("/api/proxy/{type}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByType)).Methods("GET")
subRouter.HandleFunc("/api/proxy/{type}/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByTypeAndName)).Methods("GET")
subRouter.HandleFunc("/api/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByName)).Methods("GET")
subRouter.HandleFunc("/api/traffic/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyTraffic)).Methods("GET")
subRouter.HandleFunc("/api/clients", httppkg.MakeHTTPHandlerFunc(apiController.APIClientList)).Methods("GET")
subRouter.HandleFunc("/api/clients/{key}", httppkg.MakeHTTPHandlerFunc(apiController.APIClientDetail)).Methods("GET")
subRouter.HandleFunc("/api/proxies", httppkg.MakeHTTPHandlerFunc(apiController.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)
})
}
func healthz(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(200)
}