refactor: separate API handlers into dedicated packages with improved HTTP utilities (#5127)
This commit is contained in:
parent
3370bd53f5
commit
ed13141c56
@ -40,7 +40,6 @@ frp is an open source project with its ongoing development made possible entirel
|
|||||||
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
## Recall.ai - API for meeting recordings
|
## Recall.ai - API for meeting recordings
|
||||||
|
|||||||
@ -15,44 +15,29 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cmp"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/client/api"
|
||||||
"github.com/fatedier/frp/client/proxy"
|
"github.com/fatedier/frp/client/proxy"
|
||||||
"github.com/fatedier/frp/pkg/config"
|
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GeneralResponse struct {
|
|
||||||
Code int
|
|
||||||
Msg string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
|
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
|
||||||
helper.Router.HandleFunc("/healthz", svr.healthz)
|
apiController := newAPIController(svr)
|
||||||
|
|
||||||
|
// Healthz endpoint without auth
|
||||||
|
helper.Router.HandleFunc("/healthz", healthz)
|
||||||
|
|
||||||
|
// API routes and static files with auth
|
||||||
subRouter := helper.Router.NewRoute().Subrouter()
|
subRouter := helper.Router.NewRoute().Subrouter()
|
||||||
|
subRouter.Use(helper.AuthMiddleware)
|
||||||
subRouter.Use(helper.AuthMiddleware.Middleware)
|
subRouter.Use(httppkg.NewRequestLogger)
|
||||||
|
subRouter.HandleFunc("/api/reload", httppkg.MakeHTTPHandlerFunc(apiController.Reload)).Methods(http.MethodGet)
|
||||||
// api, see admin_api.go
|
subRouter.HandleFunc("/api/stop", httppkg.MakeHTTPHandlerFunc(apiController.Stop)).Methods(http.MethodPost)
|
||||||
subRouter.HandleFunc("/api/reload", svr.apiReload).Methods("GET")
|
subRouter.HandleFunc("/api/status", httppkg.MakeHTTPHandlerFunc(apiController.Status)).Methods(http.MethodGet)
|
||||||
subRouter.HandleFunc("/api/stop", svr.apiStop).Methods("POST")
|
subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.GetConfig)).Methods(http.MethodGet)
|
||||||
subRouter.HandleFunc("/api/status", svr.apiStatus).Methods("GET")
|
subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.PutConfig)).Methods(http.MethodPut)
|
||||||
subRouter.HandleFunc("/api/config", svr.apiGetConfig).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/config", svr.apiPutConfig).Methods("PUT")
|
|
||||||
|
|
||||||
// view
|
|
||||||
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
|
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
|
||||||
subRouter.PathPrefix("/static/").Handler(
|
subRouter.PathPrefix("/static/").Handler(
|
||||||
netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
|
netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
|
||||||
@ -62,202 +47,28 @@ func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// /healthz
|
func healthz(w http.ResponseWriter, _ *http.Request) {
|
||||||
func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
|
w.WriteHeader(http.StatusOK)
|
||||||
w.WriteHeader(200)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/reload
|
func newAPIController(svr *Service) *api.Controller {
|
||||||
func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
|
return api.NewController(api.ControllerParams{
|
||||||
res := GeneralResponse{Code: 200}
|
GetProxyStatus: svr.getAllProxyStatus,
|
||||||
strictConfigMode := false
|
ServerAddr: svr.common.ServerAddr,
|
||||||
strictStr := r.URL.Query().Get("strictConfig")
|
ConfigFilePath: svr.configFilePath,
|
||||||
if strictStr != "" {
|
UnsafeFeatures: svr.unsafeFeatures,
|
||||||
strictConfigMode, _ = strconv.ParseBool(strictStr)
|
UpdateConfig: svr.UpdateAllConfigurer,
|
||||||
}
|
GracefulClose: svr.GracefulClose,
|
||||||
|
})
|
||||||
log.Infof("api request [/api/reload]")
|
|
||||||
defer func() {
|
|
||||||
log.Infof("api response [/api/reload], code [%d]", res.Code)
|
|
||||||
w.WriteHeader(res.Code)
|
|
||||||
if len(res.Msg) > 0 {
|
|
||||||
_, _ = w.Write([]byte(res.Msg))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(svr.configFilePath, strictConfigMode)
|
|
||||||
if err != nil {
|
|
||||||
res.Code = 400
|
|
||||||
res.Msg = err.Error()
|
|
||||||
log.Warnf("reload frpc proxy config error: %s", res.Msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, svr.unsafeFeatures); err != nil {
|
|
||||||
res.Code = 400
|
|
||||||
res.Msg = err.Error()
|
|
||||||
log.Warnf("reload frpc proxy config error: %s", res.Msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := svr.UpdateAllConfigurer(proxyCfgs, visitorCfgs); err != nil {
|
|
||||||
res.Code = 500
|
|
||||||
res.Msg = err.Error()
|
|
||||||
log.Warnf("reload frpc proxy config error: %s", res.Msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Infof("success reload conf")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/stop
|
// getAllProxyStatus returns all proxy statuses.
|
||||||
func (svr *Service) apiStop(w http.ResponseWriter, _ *http.Request) {
|
func (svr *Service) getAllProxyStatus() []*proxy.WorkingStatus {
|
||||||
res := GeneralResponse{Code: 200}
|
|
||||||
|
|
||||||
log.Infof("api request [/api/stop]")
|
|
||||||
defer func() {
|
|
||||||
log.Infof("api response [/api/stop], code [%d]", res.Code)
|
|
||||||
w.WriteHeader(res.Code)
|
|
||||||
if len(res.Msg) > 0 {
|
|
||||||
_, _ = w.Write([]byte(res.Msg))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go svr.GracefulClose(100 * time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
type StatusResp map[string][]ProxyStatusResp
|
|
||||||
|
|
||||||
type ProxyStatusResp struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Err string `json:"err"`
|
|
||||||
LocalAddr string `json:"local_addr"`
|
|
||||||
Plugin string `json:"plugin"`
|
|
||||||
RemoteAddr string `json:"remote_addr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewProxyStatusResp(status *proxy.WorkingStatus, serverAddr string) ProxyStatusResp {
|
|
||||||
psr := ProxyStatusResp{
|
|
||||||
Name: status.Name,
|
|
||||||
Type: status.Type,
|
|
||||||
Status: status.Phase,
|
|
||||||
Err: status.Err,
|
|
||||||
}
|
|
||||||
baseCfg := status.Cfg.GetBaseConfig()
|
|
||||||
if baseCfg.LocalPort != 0 {
|
|
||||||
psr.LocalAddr = net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort))
|
|
||||||
}
|
|
||||||
psr.Plugin = baseCfg.Plugin.Type
|
|
||||||
|
|
||||||
if status.Err == "" {
|
|
||||||
psr.RemoteAddr = status.RemoteAddr
|
|
||||||
if slices.Contains([]string{"tcp", "udp"}, status.Type) {
|
|
||||||
psr.RemoteAddr = serverAddr + psr.RemoteAddr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return psr
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/status
|
|
||||||
func (svr *Service) apiStatus(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
var (
|
|
||||||
buf []byte
|
|
||||||
res StatusResp = make(map[string][]ProxyStatusResp)
|
|
||||||
)
|
|
||||||
|
|
||||||
log.Infof("http request [/api/status]")
|
|
||||||
defer func() {
|
|
||||||
log.Infof("http response [/api/status]")
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
buf, _ = json.Marshal(&res)
|
|
||||||
_, _ = w.Write(buf)
|
|
||||||
}()
|
|
||||||
|
|
||||||
svr.ctlMu.RLock()
|
svr.ctlMu.RLock()
|
||||||
ctl := svr.ctl
|
ctl := svr.ctl
|
||||||
svr.ctlMu.RUnlock()
|
svr.ctlMu.RUnlock()
|
||||||
if ctl == nil {
|
if ctl == nil {
|
||||||
return
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
ps := ctl.pm.GetAllProxyStatus()
|
|
||||||
for _, status := range ps {
|
|
||||||
res[status.Type] = append(res[status.Type], NewProxyStatusResp(status, svr.common.ServerAddr))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, arrs := range res {
|
|
||||||
if len(arrs) <= 1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
slices.SortFunc(arrs, func(a, b ProxyStatusResp) int {
|
|
||||||
return cmp.Compare(a.Name, b.Name)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/config
|
|
||||||
func (svr *Service) apiGetConfig(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
res := GeneralResponse{Code: 200}
|
|
||||||
|
|
||||||
log.Infof("http get request [/api/config]")
|
|
||||||
defer func() {
|
|
||||||
log.Infof("http get response [/api/config], code [%d]", res.Code)
|
|
||||||
w.WriteHeader(res.Code)
|
|
||||||
if len(res.Msg) > 0 {
|
|
||||||
_, _ = w.Write([]byte(res.Msg))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if svr.configFilePath == "" {
|
|
||||||
res.Code = 400
|
|
||||||
res.Msg = "frpc has no config file path"
|
|
||||||
log.Warnf("%s", res.Msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
content, err := os.ReadFile(svr.configFilePath)
|
|
||||||
if err != nil {
|
|
||||||
res.Code = 400
|
|
||||||
res.Msg = err.Error()
|
|
||||||
log.Warnf("load frpc config file error: %s", res.Msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
res.Msg = string(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PUT /api/config
|
|
||||||
func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) {
|
|
||||||
res := GeneralResponse{Code: 200}
|
|
||||||
|
|
||||||
log.Infof("http put request [/api/config]")
|
|
||||||
defer func() {
|
|
||||||
log.Infof("http put response [/api/config], code [%d]", res.Code)
|
|
||||||
w.WriteHeader(res.Code)
|
|
||||||
if len(res.Msg) > 0 {
|
|
||||||
_, _ = w.Write([]byte(res.Msg))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// get new config content
|
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
res.Code = 400
|
|
||||||
res.Msg = fmt.Sprintf("read request body error: %v", err)
|
|
||||||
log.Warnf("%s", res.Msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(body) == 0 {
|
|
||||||
res.Code = 400
|
|
||||||
res.Msg = "body can't be empty"
|
|
||||||
log.Warnf("%s", res.Msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.WriteFile(svr.configFilePath, body, 0o600); err != nil {
|
|
||||||
res.Code = 500
|
|
||||||
res.Msg = fmt.Sprintf("write content to frpc config file error: %v", err)
|
|
||||||
log.Warnf("%s", res.Msg)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
return ctl.pm.GetAllProxyStatus()
|
||||||
}
|
}
|
||||||
|
|||||||
189
client/api/controller.go
Normal file
189
client/api/controller.go
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
// Copyright 2025 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 api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/client/proxy"
|
||||||
|
"github.com/fatedier/frp/pkg/config"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Controller handles HTTP API requests for frpc.
|
||||||
|
type Controller struct {
|
||||||
|
// getProxyStatus returns the current proxy status.
|
||||||
|
// Returns nil if the control connection is not established.
|
||||||
|
getProxyStatus func() []*proxy.WorkingStatus
|
||||||
|
|
||||||
|
// serverAddr is the frps server address for display.
|
||||||
|
serverAddr string
|
||||||
|
|
||||||
|
// configFilePath is the path to the configuration file.
|
||||||
|
configFilePath string
|
||||||
|
|
||||||
|
// unsafeFeatures is used for validation.
|
||||||
|
unsafeFeatures *security.UnsafeFeatures
|
||||||
|
|
||||||
|
// updateConfig updates proxy and visitor configurations.
|
||||||
|
updateConfig func(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error
|
||||||
|
|
||||||
|
// gracefulClose gracefully stops the service.
|
||||||
|
gracefulClose func(d time.Duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ControllerParams contains parameters for creating an APIController.
|
||||||
|
type ControllerParams struct {
|
||||||
|
GetProxyStatus func() []*proxy.WorkingStatus
|
||||||
|
ServerAddr string
|
||||||
|
ConfigFilePath string
|
||||||
|
UnsafeFeatures *security.UnsafeFeatures
|
||||||
|
UpdateConfig func(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error
|
||||||
|
GracefulClose func(d time.Duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewController creates a new Controller.
|
||||||
|
func NewController(params ControllerParams) *Controller {
|
||||||
|
return &Controller{
|
||||||
|
getProxyStatus: params.GetProxyStatus,
|
||||||
|
serverAddr: params.ServerAddr,
|
||||||
|
configFilePath: params.ConfigFilePath,
|
||||||
|
unsafeFeatures: params.UnsafeFeatures,
|
||||||
|
updateConfig: params.UpdateConfig,
|
||||||
|
gracefulClose: params.GracefulClose,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload handles GET /api/reload
|
||||||
|
func (c *Controller) Reload(ctx *httppkg.Context) (any, error) {
|
||||||
|
strictConfigMode := false
|
||||||
|
strictStr := ctx.Query("strictConfig")
|
||||||
|
if strictStr != "" {
|
||||||
|
strictConfigMode, _ = strconv.ParseBool(strictStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(c.configFilePath, strictConfigMode)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("reload frpc proxy config error: %s", err.Error())
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, c.unsafeFeatures); err != nil {
|
||||||
|
log.Warnf("reload frpc proxy config error: %s", err.Error())
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.updateConfig(proxyCfgs, visitorCfgs); err != nil {
|
||||||
|
log.Warnf("reload frpc proxy config error: %s", err.Error())
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("success reload conf")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop handles POST /api/stop
|
||||||
|
func (c *Controller) Stop(ctx *httppkg.Context) (any, error) {
|
||||||
|
go c.gracefulClose(100 * time.Millisecond)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status handles GET /api/status
|
||||||
|
func (c *Controller) Status(ctx *httppkg.Context) (any, error) {
|
||||||
|
res := make(StatusResp)
|
||||||
|
ps := c.getProxyStatus()
|
||||||
|
if ps == nil {
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, status := range ps {
|
||||||
|
res[status.Type] = append(res[status.Type], c.buildProxyStatusResp(status))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, arrs := range res {
|
||||||
|
if len(arrs) <= 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
slices.SortFunc(arrs, func(a, b ProxyStatusResp) int {
|
||||||
|
return cmp.Compare(a.Name, b.Name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfig handles GET /api/config
|
||||||
|
func (c *Controller) GetConfig(ctx *httppkg.Context) (any, error) {
|
||||||
|
if c.configFilePath == "" {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, "frpc has no config file path")
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(c.configFilePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("load frpc config file error: %s", err.Error())
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
return string(content), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutConfig handles PUT /api/config
|
||||||
|
func (c *Controller) PutConfig(ctx *httppkg.Context) (any, error) {
|
||||||
|
body, err := ctx.Body()
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read request body error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(body) == 0 {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, "body can't be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(c.configFilePath, body, 0o600); err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, fmt.Sprintf("write content to frpc config file error: %v", err))
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildProxyStatusResp creates a ProxyStatusResp from proxy.WorkingStatus
|
||||||
|
func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) ProxyStatusResp {
|
||||||
|
psr := ProxyStatusResp{
|
||||||
|
Name: status.Name,
|
||||||
|
Type: status.Type,
|
||||||
|
Status: status.Phase,
|
||||||
|
Err: status.Err,
|
||||||
|
}
|
||||||
|
baseCfg := status.Cfg.GetBaseConfig()
|
||||||
|
if baseCfg.LocalPort != 0 {
|
||||||
|
psr.LocalAddr = net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort))
|
||||||
|
}
|
||||||
|
psr.Plugin = baseCfg.Plugin.Type
|
||||||
|
|
||||||
|
if status.Err == "" {
|
||||||
|
psr.RemoteAddr = status.RemoteAddr
|
||||||
|
if slices.Contains([]string{"tcp", "udp"}, status.Type) {
|
||||||
|
psr.RemoteAddr = c.serverAddr + psr.RemoteAddr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return psr
|
||||||
|
}
|
||||||
29
client/api/types.go
Normal file
29
client/api/types.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Copyright 2025 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 api
|
||||||
|
|
||||||
|
// StatusResp is the response for GET /api/status
|
||||||
|
type StatusResp map[string][]ProxyStatusResp
|
||||||
|
|
||||||
|
// ProxyStatusResp contains proxy status information
|
||||||
|
type ProxyStatusResp struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Err string `json:"err"`
|
||||||
|
LocalAddr string `json:"local_addr"`
|
||||||
|
Plugin string `json:"plugin"`
|
||||||
|
RemoteAddr string `json:"remote_addr"`
|
||||||
|
}
|
||||||
@ -11,7 +11,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatedier/frp/client"
|
"github.com/fatedier/frp/client/api"
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ func (c *Client) SetAuth(user, pwd string) {
|
|||||||
c.authPwd = pwd
|
c.authPwd = pwd
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetProxyStatus(ctx context.Context, name string) (*client.ProxyStatusResp, error) {
|
func (c *Client) GetProxyStatus(ctx context.Context, name string) (*api.ProxyStatusResp, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -41,7 +41,7 @@ func (c *Client) GetProxyStatus(ctx context.Context, name string) (*client.Proxy
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
allStatus := make(client.StatusResp)
|
allStatus := make(api.StatusResp)
|
||||||
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
||||||
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
|
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
|
||||||
}
|
}
|
||||||
@ -55,7 +55,7 @@ func (c *Client) GetProxyStatus(ctx context.Context, name string) (*client.Proxy
|
|||||||
return nil, fmt.Errorf("no proxy status found")
|
return nil, fmt.Errorf("no proxy status found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetAllProxyStatus(ctx context.Context) (client.StatusResp, error) {
|
func (c *Client) GetAllProxyStatus(ctx context.Context) (api.StatusResp, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -64,7 +64,7 @@ func (c *Client) GetAllProxyStatus(ctx context.Context) (client.StatusResp, erro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
allStatus := make(client.StatusResp)
|
allStatus := make(api.StatusResp)
|
||||||
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
||||||
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
|
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
|
||||||
}
|
}
|
||||||
|
|||||||
57
pkg/util/http/context.go
Normal file
57
pkg/util/http/context.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// Copyright 2025 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"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Context struct {
|
||||||
|
Req *http.Request
|
||||||
|
Resp http.ResponseWriter
|
||||||
|
vars map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewContext(w http.ResponseWriter, r *http.Request) *Context {
|
||||||
|
return &Context{
|
||||||
|
Req: r,
|
||||||
|
Resp: w,
|
||||||
|
vars: mux.Vars(r),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Param(key string) string {
|
||||||
|
return c.vars[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Query(key string) string {
|
||||||
|
return c.Req.URL.Query().Get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) BindJSON(obj any) error {
|
||||||
|
body, err := io.ReadAll(c.Req.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.Unmarshal(body, obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Body() ([]byte, error) {
|
||||||
|
return io.ReadAll(c.Req.Body)
|
||||||
|
}
|
||||||
33
pkg/util/http/error.go
Normal file
33
pkg/util/http/error.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// Copyright 2025 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 "fmt"
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
Code int
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) Error() string {
|
||||||
|
return e.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewError(code int, msg string) *Error {
|
||||||
|
return &Error{
|
||||||
|
Code: code,
|
||||||
|
Err: fmt.Errorf("%s", msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
66
pkg/util/http/handler.go
Normal file
66
pkg/util/http/handler.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// Copyright 2025 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"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GeneralResponse struct {
|
||||||
|
Code int
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIHandler is a handler function that returns a response object or an error.
|
||||||
|
type APIHandler func(ctx *Context) (any, error)
|
||||||
|
|
||||||
|
// MakeHTTPHandlerFunc turns a normal APIHandler into a http.HandlerFunc.
|
||||||
|
func MakeHTTPHandlerFunc(handler APIHandler) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := NewContext(w, r)
|
||||||
|
res, err := handler(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("http response [%s]: error: %v", r.URL.Path, err)
|
||||||
|
code := http.StatusInternalServerError
|
||||||
|
if e, ok := err.(*Error); ok {
|
||||||
|
code = e.Code
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(code)
|
||||||
|
_ = json.NewEncoder(w).Encode(GeneralResponse{Code: code, Msg: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if res == nil {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := res.(type) {
|
||||||
|
case []byte:
|
||||||
|
_, _ = w.Write(v)
|
||||||
|
case string:
|
||||||
|
_, _ = w.Write([]byte(v))
|
||||||
|
default:
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_ = json.NewEncoder(w).Encode(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
pkg/util/http/middleware.go
Normal file
40
pkg/util/http/middleware.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// Copyright 2025 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 (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type responseWriter struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
code int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rw *responseWriter) WriteHeader(code int) {
|
||||||
|
rw.code = code
|
||||||
|
rw.ResponseWriter.WriteHeader(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRequestLogger(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Infof("http request: [%s]", r.URL.Path)
|
||||||
|
rw := &responseWriter{ResponseWriter: w, code: http.StatusOK}
|
||||||
|
next.ServeHTTP(rw, r)
|
||||||
|
log.Infof("http response [%s]: code [%d]", r.URL.Path, rw.code)
|
||||||
|
})
|
||||||
|
}
|
||||||
346
server/api/controller.go
Normal file
346
server/api/controller.go
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
// Copyright 2025 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 api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/config/types"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/metrics/mem"
|
||||||
|
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/proxy"
|
||||||
|
"github.com/fatedier/frp/server/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Controller struct {
|
||||||
|
// dependencies
|
||||||
|
serverCfg *v1.ServerConfig
|
||||||
|
clientRegistry *registry.ClientRegistry
|
||||||
|
pxyManager ProxyManager
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProxyManager interface {
|
||||||
|
GetByName(name string) (proxy.Proxy, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewController(
|
||||||
|
serverCfg *v1.ServerConfig,
|
||||||
|
clientRegistry *registry.ClientRegistry,
|
||||||
|
pxyManager ProxyManager,
|
||||||
|
) *Controller {
|
||||||
|
return &Controller{
|
||||||
|
serverCfg: serverCfg,
|
||||||
|
clientRegistry: clientRegistry,
|
||||||
|
pxyManager: pxyManager,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// /api/serverinfo
|
||||||
|
func (c *Controller) APIServerInfo(ctx *httppkg.Context) (any, error) {
|
||||||
|
serverStats := mem.StatsCollector.GetServer()
|
||||||
|
svrResp := ServerInfoResp{
|
||||||
|
Version: version.Full(),
|
||||||
|
BindPort: c.serverCfg.BindPort,
|
||||||
|
VhostHTTPPort: c.serverCfg.VhostHTTPPort,
|
||||||
|
VhostHTTPSPort: c.serverCfg.VhostHTTPSPort,
|
||||||
|
TCPMuxHTTPConnectPort: c.serverCfg.TCPMuxHTTPConnectPort,
|
||||||
|
KCPBindPort: c.serverCfg.KCPBindPort,
|
||||||
|
QUICBindPort: c.serverCfg.QUICBindPort,
|
||||||
|
SubdomainHost: c.serverCfg.SubDomainHost,
|
||||||
|
MaxPoolCount: c.serverCfg.Transport.MaxPoolCount,
|
||||||
|
MaxPortsPerClient: c.serverCfg.MaxPortsPerClient,
|
||||||
|
HeartBeatTimeout: c.serverCfg.Transport.HeartbeatTimeout,
|
||||||
|
AllowPortsStr: types.PortsRangeSlice(c.serverCfg.AllowPorts).String(),
|
||||||
|
TLSForce: c.serverCfg.Transport.TLS.Force,
|
||||||
|
|
||||||
|
TotalTrafficIn: serverStats.TotalTrafficIn,
|
||||||
|
TotalTrafficOut: serverStats.TotalTrafficOut,
|
||||||
|
CurConns: serverStats.CurConns,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// /api/clients
|
||||||
|
func (c *Controller) APIClientList(ctx *httppkg.Context) (any, error) {
|
||||||
|
if c.clientRegistry == nil {
|
||||||
|
return nil, fmt.Errorf("client registry unavailable")
|
||||||
|
}
|
||||||
|
|
||||||
|
userFilter := ctx.Query("user")
|
||||||
|
clientIDFilter := ctx.Query("clientId")
|
||||||
|
runIDFilter := ctx.Query("runId")
|
||||||
|
statusFilter := strings.ToLower(ctx.Query("status"))
|
||||||
|
|
||||||
|
records := c.clientRegistry.List()
|
||||||
|
items := make([]ClientInfoResp, 0, len(records))
|
||||||
|
for _, info := range records {
|
||||||
|
if userFilter != "" && info.User != userFilter {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if clientIDFilter != "" && info.ClientID != clientIDFilter {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if runIDFilter != "" && info.RunID != runIDFilter {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !matchStatusFilter(info.Online, statusFilter) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
items = append(items, buildClientInfoResp(info))
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.SortFunc(items, func(a, b ClientInfoResp) int {
|
||||||
|
if v := cmp.Compare(a.User, b.User); v != 0 {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
if v := cmp.Compare(a.ClientID, b.ClientID); v != 0 {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return cmp.Compare(a.Key, b.Key)
|
||||||
|
})
|
||||||
|
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// /api/clients/{key}
|
||||||
|
func (c *Controller) APIClientDetail(ctx *httppkg.Context) (any, error) {
|
||||||
|
key := ctx.Param("key")
|
||||||
|
if key == "" {
|
||||||
|
return nil, fmt.Errorf("missing client key")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.clientRegistry == nil {
|
||||||
|
return nil, fmt.Errorf("client registry unavailable")
|
||||||
|
}
|
||||||
|
|
||||||
|
info, ok := c.clientRegistry.GetByKey(key)
|
||||||
|
if !ok {
|
||||||
|
return nil, httppkg.NewError(http.StatusNotFound, fmt.Sprintf("client %s not found", key))
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildClientInfoResp(info), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// /api/proxy/:type
|
||||||
|
func (c *Controller) APIProxyByType(ctx *httppkg.Context) (any, error) {
|
||||||
|
proxyType := ctx.Param("type")
|
||||||
|
|
||||||
|
proxyInfoResp := GetProxyInfoResp{}
|
||||||
|
proxyInfoResp.Proxies = c.getProxyStatsByType(proxyType)
|
||||||
|
slices.SortFunc(proxyInfoResp.Proxies, func(a, b *ProxyStatsInfo) int {
|
||||||
|
return cmp.Compare(a.Name, b.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
return proxyInfoResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// /api/proxy/:type/:name
|
||||||
|
func (c *Controller) APIProxyByTypeAndName(ctx *httppkg.Context) (any, error) {
|
||||||
|
proxyType := ctx.Param("type")
|
||||||
|
name := ctx.Param("name")
|
||||||
|
|
||||||
|
proxyStatsResp, code, msg := c.getProxyStatsByTypeAndName(proxyType, name)
|
||||||
|
if code != 200 {
|
||||||
|
return nil, httppkg.NewError(code, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxyStatsResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// /api/traffic/:name
|
||||||
|
func (c *Controller) APIProxyTraffic(ctx *httppkg.Context) (any, error) {
|
||||||
|
name := ctx.Param("name")
|
||||||
|
|
||||||
|
trafficResp := GetProxyTrafficResp{}
|
||||||
|
trafficResp.Name = name
|
||||||
|
proxyTrafficInfo := mem.StatsCollector.GetProxyTraffic(name)
|
||||||
|
|
||||||
|
if proxyTrafficInfo == nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusNotFound, "no proxy info found")
|
||||||
|
}
|
||||||
|
trafficResp.TrafficIn = proxyTrafficInfo.TrafficIn
|
||||||
|
trafficResp.TrafficOut = proxyTrafficInfo.TrafficOut
|
||||||
|
|
||||||
|
return trafficResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/proxies?status=offline
|
||||||
|
func (c *Controller) DeleteProxies(ctx *httppkg.Context) (any, error) {
|
||||||
|
status := ctx.Query("status")
|
||||||
|
if status != "offline" {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, "status only support offline")
|
||||||
|
}
|
||||||
|
cleared, total := mem.StatsCollector.ClearOfflineProxies()
|
||||||
|
log.Infof("cleared [%d] offline proxies, total [%d] proxies", cleared, total)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) 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 := 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.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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) 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 := 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.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
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildClientInfoResp(info registry.ClientInfo) ClientInfoResp {
|
||||||
|
resp := ClientInfoResp{
|
||||||
|
Key: info.Key,
|
||||||
|
User: info.User,
|
||||||
|
ClientID: info.ClientID,
|
||||||
|
RunID: info.RunID,
|
||||||
|
Hostname: info.Hostname,
|
||||||
|
ClientIP: info.IP,
|
||||||
|
FirstConnectedAt: toUnix(info.FirstConnectedAt),
|
||||||
|
LastConnectedAt: toUnix(info.LastConnectedAt),
|
||||||
|
Online: info.Online,
|
||||||
|
}
|
||||||
|
if !info.DisconnectedAt.IsZero() {
|
||||||
|
resp.DisconnectedAt = info.DisconnectedAt.Unix()
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func toUnix(t time.Time) int64 {
|
||||||
|
if t.IsZero() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return t.Unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchStatusFilter(online bool, filter string) bool {
|
||||||
|
switch strings.ToLower(filter) {
|
||||||
|
case "", "all":
|
||||||
|
return true
|
||||||
|
case "online":
|
||||||
|
return online
|
||||||
|
case "offline":
|
||||||
|
return !online
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
131
server/api/types.go
Normal file
131
server/api/types.go
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// Copyright 2025 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 api
|
||||||
|
|
||||||
|
import (
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientInfoResp struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
User string `json:"user"`
|
||||||
|
ClientID string `json:"clientID"`
|
||||||
|
RunID string `json:"runID"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
ClientIP string `json:"clientIP,omitempty"`
|
||||||
|
FirstConnectedAt int64 `json:"firstConnectedAt"`
|
||||||
|
LastConnectedAt int64 `json:"lastConnectedAt"`
|
||||||
|
DisconnectedAt int64 `json:"disconnectedAt,omitempty"`
|
||||||
|
Online bool `json:"online"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseOutConf struct {
|
||||||
|
v1.ProxyBaseConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type TCPOutConf struct {
|
||||||
|
BaseOutConf
|
||||||
|
RemotePort int `json:"remotePort"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TCPMuxOutConf struct {
|
||||||
|
BaseOutConf
|
||||||
|
v1.DomainConfig
|
||||||
|
Multiplexer string `json:"multiplexer"`
|
||||||
|
RouteByHTTPUser string `json:"routeByHTTPUser"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get proxy info.
|
||||||
|
type ProxyStatsInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Conf any `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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetProxyInfoResp struct {
|
||||||
|
Proxies []*ProxyStatsInfo `json:"proxies"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get proxy info by name.
|
||||||
|
type GetProxyStatsResp struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Conf any `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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// /api/traffic/:name
|
||||||
|
type GetProxyTrafficResp struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
TrafficIn []int64 `json:"trafficIn"`
|
||||||
|
TrafficOut []int64 `json:"trafficOut"`
|
||||||
|
}
|
||||||
@ -40,6 +40,7 @@ import (
|
|||||||
"github.com/fatedier/frp/server/controller"
|
"github.com/fatedier/frp/server/controller"
|
||||||
"github.com/fatedier/frp/server/metrics"
|
"github.com/fatedier/frp/server/metrics"
|
||||||
"github.com/fatedier/frp/server/proxy"
|
"github.com/fatedier/frp/server/proxy"
|
||||||
|
"github.com/fatedier/frp/server/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ControlManager struct {
|
type ControlManager struct {
|
||||||
@ -147,7 +148,7 @@ type Control struct {
|
|||||||
// Server configuration information
|
// Server configuration information
|
||||||
serverCfg *v1.ServerConfig
|
serverCfg *v1.ServerConfig
|
||||||
|
|
||||||
clientRegistry *ClientRegistry
|
clientRegistry *registry.ClientRegistry
|
||||||
|
|
||||||
xl *xlog.Logger
|
xl *xlog.Logger
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
|||||||
@ -1,563 +0,0 @@
|
|||||||
// 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"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"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"
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
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/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/traffic/{name}", svr.apiProxyTraffic).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/clients", svr.apiClientList).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/clients/{key}", svr.apiClientDetail).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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type clientInfoResp struct {
|
|
||||||
Key string `json:"key"`
|
|
||||||
User string `json:"user"`
|
|
||||||
ClientID string `json:"clientID"`
|
|
||||||
RunID string `json:"runID"`
|
|
||||||
Hostname string `json:"hostname"`
|
|
||||||
ClientIP string `json:"clientIP,omitempty"`
|
|
||||||
FirstConnectedAt int64 `json:"firstConnectedAt"`
|
|
||||||
LastConnectedAt int64 `json:"lastConnectedAt"`
|
|
||||||
DisconnectedAt int64 `json:"disconnectedAt,omitempty"`
|
|
||||||
Online bool `json:"online"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// /healthz
|
|
||||||
func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
w.WriteHeader(200)
|
|
||||||
}
|
|
||||||
|
|
||||||
// /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)
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// /api/clients
|
|
||||||
func (svr *Service) apiClientList(w http.ResponseWriter, r *http.Request) {
|
|
||||||
res := GeneralResponse{Code: 200}
|
|
||||||
defer func() {
|
|
||||||
log.Infof("http response [%s]: code [%d]", r.URL.RequestURI(), res.Code)
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(res.Code)
|
|
||||||
if len(res.Msg) > 0 {
|
|
||||||
_, _ = w.Write([]byte(res.Msg))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
log.Infof("http request: [%s]", r.URL.RequestURI())
|
|
||||||
|
|
||||||
if svr.clientRegistry == nil {
|
|
||||||
res.Code = http.StatusInternalServerError
|
|
||||||
res.Msg = "client registry unavailable"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
query := r.URL.Query()
|
|
||||||
userFilter := query.Get("user")
|
|
||||||
clientIDFilter := query.Get("clientId")
|
|
||||||
runIDFilter := query.Get("runId")
|
|
||||||
statusFilter := strings.ToLower(query.Get("status"))
|
|
||||||
|
|
||||||
records := svr.clientRegistry.List()
|
|
||||||
items := make([]clientInfoResp, 0, len(records))
|
|
||||||
for _, info := range records {
|
|
||||||
if userFilter != "" && info.User != userFilter {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if clientIDFilter != "" && info.ClientID != clientIDFilter {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if runIDFilter != "" && info.RunID != runIDFilter {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !matchStatusFilter(info.Online, statusFilter) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
items = append(items, buildClientInfoResp(info))
|
|
||||||
}
|
|
||||||
|
|
||||||
slices.SortFunc(items, func(a, b clientInfoResp) int {
|
|
||||||
if v := cmp.Compare(a.User, b.User); v != 0 {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
if v := cmp.Compare(a.ClientID, b.ClientID); v != 0 {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return cmp.Compare(a.Key, b.Key)
|
|
||||||
})
|
|
||||||
|
|
||||||
buf, _ := json.Marshal(items)
|
|
||||||
res.Msg = string(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
// /api/clients/{key}
|
|
||||||
func (svr *Service) apiClientDetail(w http.ResponseWriter, r *http.Request) {
|
|
||||||
res := GeneralResponse{Code: 200}
|
|
||||||
defer func() {
|
|
||||||
log.Infof("http response [%s]: code [%d]", r.URL.RequestURI(), res.Code)
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(res.Code)
|
|
||||||
if len(res.Msg) > 0 {
|
|
||||||
_, _ = w.Write([]byte(res.Msg))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
log.Infof("http request: [%s]", r.URL.RequestURI())
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
key := vars["key"]
|
|
||||||
if key == "" {
|
|
||||||
res.Code = http.StatusBadRequest
|
|
||||||
res.Msg = "missing client key"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if svr.clientRegistry == nil {
|
|
||||||
res.Code = http.StatusInternalServerError
|
|
||||||
res.Msg = "client registry unavailable"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
info, ok := svr.clientRegistry.GetByKey(key)
|
|
||||||
if !ok {
|
|
||||||
res.Code = http.StatusNotFound
|
|
||||||
res.Msg = fmt.Sprintf("client %s not found", key)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
buf, _ := json.Marshal(buildClientInfoResp(info))
|
|
||||||
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"`
|
|
||||||
RouteByHTTPUser string `json:"routeByHTTPUser"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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 any `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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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.Header().Set("Content-Type", "application/json")
|
|
||||||
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 any `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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// /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.Header().Set("Content-Type", "application/json")
|
|
||||||
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.Header().Set("Content-Type", "application/json")
|
|
||||||
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 buildClientInfoResp(info ClientInfo) clientInfoResp {
|
|
||||||
resp := clientInfoResp{
|
|
||||||
Key: info.Key,
|
|
||||||
User: info.User,
|
|
||||||
ClientID: info.ClientID,
|
|
||||||
RunID: info.RunID,
|
|
||||||
Hostname: info.Hostname,
|
|
||||||
ClientIP: info.IP,
|
|
||||||
FirstConnectedAt: toUnix(info.FirstConnectedAt),
|
|
||||||
LastConnectedAt: toUnix(info.LastConnectedAt),
|
|
||||||
Online: info.Online,
|
|
||||||
}
|
|
||||||
if !info.DisconnectedAt.IsZero() {
|
|
||||||
resp.DisconnectedAt = info.DisconnectedAt.Unix()
|
|
||||||
}
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
func toUnix(t time.Time) int64 {
|
|
||||||
if t.IsZero() {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return t.Unix()
|
|
||||||
}
|
|
||||||
|
|
||||||
func matchStatusFilter(online bool, filter string) bool {
|
|
||||||
switch strings.ToLower(filter) {
|
|
||||||
case "", "all":
|
|
||||||
return true
|
|
||||||
case "online":
|
|
||||||
return online
|
|
||||||
case "offline":
|
|
||||||
return !online
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +1,18 @@
|
|||||||
package server
|
// Copyright 2025 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 registry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/fatedier/golib/crypto"
|
"github.com/fatedier/golib/crypto"
|
||||||
"github.com/fatedier/golib/net/mux"
|
"github.com/fatedier/golib/net/mux"
|
||||||
fmux "github.com/hashicorp/yamux"
|
fmux "github.com/hashicorp/yamux"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
quic "github.com/quic-go/quic-go"
|
quic "github.com/quic-go/quic-go"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
|
||||||
@ -47,11 +48,13 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/util/version"
|
"github.com/fatedier/frp/pkg/util/version"
|
||||||
"github.com/fatedier/frp/pkg/util/vhost"
|
"github.com/fatedier/frp/pkg/util/vhost"
|
||||||
"github.com/fatedier/frp/pkg/util/xlog"
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
|
"github.com/fatedier/frp/server/api"
|
||||||
"github.com/fatedier/frp/server/controller"
|
"github.com/fatedier/frp/server/controller"
|
||||||
"github.com/fatedier/frp/server/group"
|
"github.com/fatedier/frp/server/group"
|
||||||
"github.com/fatedier/frp/server/metrics"
|
"github.com/fatedier/frp/server/metrics"
|
||||||
"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/registry"
|
||||||
"github.com/fatedier/frp/server/visitor"
|
"github.com/fatedier/frp/server/visitor"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -97,7 +100,7 @@ type Service struct {
|
|||||||
ctlManager *ControlManager
|
ctlManager *ControlManager
|
||||||
|
|
||||||
// Track logical clients keyed by user.clientID.
|
// Track logical clients keyed by user.clientID.
|
||||||
clientRegistry *ClientRegistry
|
clientRegistry *registry.ClientRegistry
|
||||||
|
|
||||||
// Manage all proxies
|
// Manage all proxies
|
||||||
pxyManager *proxy.Manager
|
pxyManager *proxy.Manager
|
||||||
@ -159,7 +162,7 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
|
|||||||
|
|
||||||
svr := &Service{
|
svr := &Service{
|
||||||
ctlManager: NewControlManager(),
|
ctlManager: NewControlManager(),
|
||||||
clientRegistry: NewClientRegistry(),
|
clientRegistry: registry.NewClientRegistry(),
|
||||||
pxyManager: proxy.NewManager(),
|
pxyManager: proxy.NewManager(),
|
||||||
pluginManager: plugin.NewManager(),
|
pluginManager: plugin.NewManager(),
|
||||||
rc: &controller.ResourceController{
|
rc: &controller.ResourceController{
|
||||||
@ -687,3 +690,41 @@ func (svr *Service) RegisterVisitorConn(visitorConn net.Conn, newMsg *msg.NewVis
|
|||||||
return svr.rc.VisitorManager.NewConn(newMsg.ProxyName, visitorConn, newMsg.Timestamp, newMsg.SignKey,
|
return svr.rc.VisitorManager.NewConn(newMsg.ProxyName, visitorConn, newMsg.Timestamp, newMsg.SignKey,
|
||||||
newMsg.UseEncryption, newMsg.UseCompression, visitorUser)
|
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/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)
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user