mirror of
https://github.com/fatedier/frp.git
synced 2026-02-09 01:59:14 +08:00
refactor: separate API handlers into dedicated packages with improved HTTP utilities (#5127)
This commit is contained in:
@@ -15,44 +15,29 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/fatedier/frp/client/api"
|
||||
"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"
|
||||
"github.com/fatedier/frp/pkg/util/log"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
)
|
||||
|
||||
type GeneralResponse struct {
|
||||
Code int
|
||||
Msg string
|
||||
}
|
||||
|
||||
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.Use(helper.AuthMiddleware.Middleware)
|
||||
|
||||
// api, see admin_api.go
|
||||
subRouter.HandleFunc("/api/reload", svr.apiReload).Methods("GET")
|
||||
subRouter.HandleFunc("/api/stop", svr.apiStop).Methods("POST")
|
||||
subRouter.HandleFunc("/api/status", svr.apiStatus).Methods("GET")
|
||||
subRouter.HandleFunc("/api/config", svr.apiGetConfig).Methods("GET")
|
||||
subRouter.HandleFunc("/api/config", svr.apiPutConfig).Methods("PUT")
|
||||
|
||||
// view
|
||||
subRouter.Use(helper.AuthMiddleware)
|
||||
subRouter.Use(httppkg.NewRequestLogger)
|
||||
subRouter.HandleFunc("/api/reload", httppkg.MakeHTTPHandlerFunc(apiController.Reload)).Methods(http.MethodGet)
|
||||
subRouter.HandleFunc("/api/stop", httppkg.MakeHTTPHandlerFunc(apiController.Stop)).Methods(http.MethodPost)
|
||||
subRouter.HandleFunc("/api/status", httppkg.MakeHTTPHandlerFunc(apiController.Status)).Methods(http.MethodGet)
|
||||
subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.GetConfig)).Methods(http.MethodGet)
|
||||
subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.PutConfig)).Methods(http.MethodPut)
|
||||
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
|
||||
subRouter.PathPrefix("/static/").Handler(
|
||||
netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
|
||||
@@ -62,202 +47,28 @@ func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper)
|
||||
})
|
||||
}
|
||||
|
||||
// /healthz
|
||||
func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
func healthz(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// GET /api/reload
|
||||
func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
|
||||
res := GeneralResponse{Code: 200}
|
||||
strictConfigMode := false
|
||||
strictStr := r.URL.Query().Get("strictConfig")
|
||||
if strictStr != "" {
|
||||
strictConfigMode, _ = strconv.ParseBool(strictStr)
|
||||
}
|
||||
|
||||
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")
|
||||
func newAPIController(svr *Service) *api.Controller {
|
||||
return api.NewController(api.ControllerParams{
|
||||
GetProxyStatus: svr.getAllProxyStatus,
|
||||
ServerAddr: svr.common.ServerAddr,
|
||||
ConfigFilePath: svr.configFilePath,
|
||||
UnsafeFeatures: svr.unsafeFeatures,
|
||||
UpdateConfig: svr.UpdateAllConfigurer,
|
||||
GracefulClose: svr.GracefulClose,
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/stop
|
||||
func (svr *Service) apiStop(w http.ResponseWriter, _ *http.Request) {
|
||||
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)
|
||||
}()
|
||||
|
||||
// getAllProxyStatus returns all proxy statuses.
|
||||
func (svr *Service) getAllProxyStatus() []*proxy.WorkingStatus {
|
||||
svr.ctlMu.RLock()
|
||||
ctl := svr.ctl
|
||||
svr.ctlMu.RUnlock()
|
||||
if ctl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
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 nil
|
||||
}
|
||||
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"`
|
||||
}
|
||||
Reference in New Issue
Block a user