mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 05:56:38 +08:00
[优化] 增加 FRPS WebUI 支持,添加相关配置和数据库迁移
This commit is contained in:
@@ -45,7 +45,7 @@ func main() {
|
||||
stateStore := state.NewStore(cfg.StatePath)
|
||||
_ = stateStore // In the future we may use stateStore for auth caching
|
||||
|
||||
frpsManager := frps.NewManager(cfg.FrpsPath, cfg.DataDir)
|
||||
frpsManager := frps.NewManager(cfg.FrpsPath, cfg.DataDir, cfg.InitialAuthToken())
|
||||
|
||||
slog.Info("detected frps version", "version", frpsManager.GetVersion())
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@ package frps
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -19,6 +21,7 @@ type Manager struct {
|
||||
frpsPath string
|
||||
dataDir string
|
||||
configPath string
|
||||
agentToken string
|
||||
|
||||
mu sync.RWMutex
|
||||
activeConfig *service.RelayConfig
|
||||
@@ -34,15 +37,18 @@ type RuntimeStatus struct {
|
||||
LastError string
|
||||
Connections int
|
||||
ProxyCount int
|
||||
ClientCount int
|
||||
Proxies []service.RelayProxyStat
|
||||
ProcessAlive bool
|
||||
}
|
||||
|
||||
func NewManager(frpsPath string, dataDir string) *Manager {
|
||||
func NewManager(frpsPath string, dataDir string, agentToken string) *Manager {
|
||||
return &Manager{
|
||||
frpsPath: frpsPath,
|
||||
dataDir: dataDir,
|
||||
configPath: filepath.Join(dataDir, "frps.toml"),
|
||||
status: "unhealthy",
|
||||
agentToken: agentToken,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,15 +71,139 @@ func (m *Manager) GetStatus() string {
|
||||
return m.status
|
||||
}
|
||||
|
||||
type frpsServerInfo struct {
|
||||
Version string `json:"version"`
|
||||
BindPort int `json:"bind_port"`
|
||||
CurConns int `json:"cur_conns"`
|
||||
ClientCounts int `json:"client_counts"`
|
||||
ProxyTypeCount map[string]int `json:"proxy_type_count"`
|
||||
}
|
||||
|
||||
type frpsProxyInfo struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
ClientAddr string `json:"client_addr"`
|
||||
LastStartTime string `json:"last_start_time"`
|
||||
LastCloseTime string `json:"last_close_time"`
|
||||
}
|
||||
|
||||
type frpsProxiesResponse struct {
|
||||
Proxies []frpsProxyInfo `json:"proxies"`
|
||||
}
|
||||
|
||||
func (m *Manager) fetchFrpsTelemetry() (connections int, proxyCount int, clientCount int, proxies []service.RelayProxyStat, err error) {
|
||||
m.mu.RLock()
|
||||
activeConfig := m.activeConfig
|
||||
m.mu.RUnlock()
|
||||
|
||||
if activeConfig == nil {
|
||||
return 0, 0, 0, nil, fmt.Errorf("activeConfig is nil")
|
||||
}
|
||||
|
||||
dashboardPort := activeConfig.BindPort + 500
|
||||
client := &http.Client{
|
||||
Timeout: 2 * time.Second,
|
||||
}
|
||||
|
||||
// 1. Fetch serverinfo
|
||||
infoURL := fmt.Sprintf("http://127.0.0.1:%d/api/serverinfo", dashboardPort)
|
||||
req, err := http.NewRequest(http.MethodGet, infoURL, nil)
|
||||
if err != nil {
|
||||
return 0, 0, 0, nil, err
|
||||
}
|
||||
password := m.agentToken
|
||||
if password == "" {
|
||||
password = "admin"
|
||||
}
|
||||
req.SetBasicAuth("admin", password)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return 0, 0, 0, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return 0, 0, 0, nil, fmt.Errorf("serverinfo returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var info frpsServerInfo
|
||||
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
||||
return 0, 0, 0, nil, err
|
||||
}
|
||||
|
||||
connections = info.CurConns
|
||||
clientCount = info.ClientCounts
|
||||
|
||||
totalProxies := 0
|
||||
for _, count := range info.ProxyTypeCount {
|
||||
totalProxies += count
|
||||
}
|
||||
proxyCount = totalProxies
|
||||
|
||||
// 2. Fetch HTTP proxies list
|
||||
proxiesURL := fmt.Sprintf("http://127.0.0.1:%d/api/proxy/http", dashboardPort)
|
||||
req2, err := http.NewRequest(http.MethodGet, proxiesURL, nil)
|
||||
if err != nil {
|
||||
return connections, proxyCount, clientCount, nil, nil
|
||||
}
|
||||
req2.SetBasicAuth("admin", password)
|
||||
resp2, err := client.Do(req2)
|
||||
if err != nil {
|
||||
return connections, proxyCount, clientCount, nil, nil
|
||||
}
|
||||
defer resp2.Body.Close()
|
||||
|
||||
if resp2.StatusCode == http.StatusOK {
|
||||
var listResp frpsProxiesResponse
|
||||
if err := json.NewDecoder(resp2.Body).Decode(&listResp); err == nil {
|
||||
for _, p := range listResp.Proxies {
|
||||
proxies = append(proxies, service.RelayProxyStat{
|
||||
Name: p.Name,
|
||||
Type: p.Type,
|
||||
Status: p.Status,
|
||||
ClientAddr: p.ClientAddr,
|
||||
LastStartTime: p.LastStartTime,
|
||||
LastCloseTime: p.LastCloseTime,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return connections, proxyCount, clientCount, proxies, nil
|
||||
}
|
||||
|
||||
func (m *Manager) GetRuntimeStatus() RuntimeStatus {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
status := m.status
|
||||
lastError := m.lastError
|
||||
cmd := m.cmd
|
||||
m.mu.RUnlock()
|
||||
|
||||
connections := 0
|
||||
proxyCount := 0
|
||||
clientCount := 0
|
||||
var proxies []service.RelayProxyStat
|
||||
|
||||
if cmd != nil && cmd.Process != nil && status == "healthy" {
|
||||
if conns, pCount, cCount, pxs, err := m.fetchFrpsTelemetry(); err == nil {
|
||||
connections = conns
|
||||
proxyCount = pCount
|
||||
clientCount = cCount
|
||||
proxies = pxs
|
||||
} else {
|
||||
slog.Debug("failed to fetch frps telemetry", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
return RuntimeStatus{
|
||||
Status: m.status,
|
||||
LastError: m.lastError,
|
||||
Connections: 0,
|
||||
ProxyCount: 0,
|
||||
ProcessAlive: m.cmd != nil && m.cmd.Process != nil,
|
||||
Status: status,
|
||||
LastError: lastError,
|
||||
Connections: connections,
|
||||
ProxyCount: proxyCount,
|
||||
ClientCount: clientCount,
|
||||
Proxies: proxies,
|
||||
ProcessAlive: cmd != nil && cmd.Process != nil,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +218,8 @@ func (m *Manager) UpdateConfig(cfg *service.RelayConfig) {
|
||||
if m.activeConfig != nil &&
|
||||
m.activeConfig.BindPort == cfg.BindPort &&
|
||||
m.activeConfig.VhostHTTPPort == cfg.VhostHTTPPort &&
|
||||
m.activeConfig.AuthToken == cfg.AuthToken {
|
||||
m.activeConfig.AuthToken == cfg.AuthToken &&
|
||||
m.activeConfig.WebServerEnabled == cfg.WebServerEnabled {
|
||||
if m.cmd == nil && !m.stopping {
|
||||
slog.Warn("frps config unchanged but process is not running, restarting")
|
||||
if err := m.restartProcess(); err != nil {
|
||||
@@ -136,6 +267,22 @@ func (m *Manager) renderConfig(cfg *service.RelayConfig) error {
|
||||
buf.WriteString(fmt.Sprintf("token = \"%s\"\n", cfg.AuthToken))
|
||||
}
|
||||
|
||||
// WebServer configuration
|
||||
buf.WriteString("\n[webServer]\n")
|
||||
if cfg.WebServerEnabled {
|
||||
buf.WriteString("addr = \"0.0.0.0\"\n")
|
||||
} else {
|
||||
buf.WriteString("addr = \"127.0.0.1\"\n")
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("port = %d\n", cfg.BindPort+500))
|
||||
buf.WriteString("user = \"admin\"\n")
|
||||
|
||||
password := m.agentToken
|
||||
if password == "" {
|
||||
password = "admin"
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("password = \"%s\"\n", password))
|
||||
|
||||
return os.WriteFile(m.configPath, buf.Bytes(), 0644)
|
||||
}
|
||||
|
||||
|
||||
@@ -54,16 +54,18 @@ func (s *Service) doHeartbeat(ctx context.Context) {
|
||||
|
||||
runtimeStatus := s.frpsManager.GetRuntimeStatus()
|
||||
payload := service.RelayHeartbeatPayload{
|
||||
Version: config.Version,
|
||||
ExtVersion: s.frpsManager.GetVersion(),
|
||||
RelayStatus: runtimeStatus.Status,
|
||||
FrpsConnCount: runtimeStatus.Connections,
|
||||
FrpsProxyCount: runtimeStatus.ProxyCount,
|
||||
Name: s.config.NodeName,
|
||||
IP: s.config.NodeIP,
|
||||
Profile: observability.BuildProfile(s.config, s.stateStore),
|
||||
Snapshot: observability.BuildSnapshot(s.config, s.stateStore),
|
||||
HealthEvents: observability.BuildHealthEvents(runtimeStatus),
|
||||
Version: config.Version,
|
||||
ExtVersion: s.frpsManager.GetVersion(),
|
||||
RelayStatus: runtimeStatus.Status,
|
||||
FrpsConnCount: runtimeStatus.Connections,
|
||||
FrpsProxyCount: runtimeStatus.ProxyCount,
|
||||
FrpsClientCount: runtimeStatus.ClientCount,
|
||||
FrpsProxies: runtimeStatus.Proxies,
|
||||
Name: s.config.NodeName,
|
||||
IP: s.config.NodeIP,
|
||||
Profile: observability.BuildProfile(s.config, s.stateStore),
|
||||
Snapshot: observability.BuildSnapshot(s.config, s.stateStore),
|
||||
HealthEvents: observability.BuildHealthEvents(runtimeStatus),
|
||||
}
|
||||
|
||||
resp, err := s.client.Heartbeat(ctx, payload)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type nodeV17 struct{}
|
||||
|
||||
func (nodeV17) TableName() string {
|
||||
return "nodes"
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(V17())
|
||||
}
|
||||
|
||||
func V17() Migration {
|
||||
return Migration{
|
||||
FromVersion: 16,
|
||||
ToVersion: 17,
|
||||
Migrate: migrateV17,
|
||||
Validate: validateV17,
|
||||
}
|
||||
}
|
||||
|
||||
func migrateV17(ctx Context, db *gorm.DB, backend string) error {
|
||||
if err := ctx.ApplyCurrentSchema(db, backend); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateV17(ctx Context, db *gorm.DB, backend string) error {
|
||||
if err := ctx.ValidateDatabaseSchemaVersion(db, backend, 16); err != nil {
|
||||
return err
|
||||
}
|
||||
if db == nil {
|
||||
return fmt.Errorf("database handle is nil")
|
||||
}
|
||||
|
||||
migrator := db.Migrator()
|
||||
if !migrator.HasColumn(&nodeV17{}, "relay_web_server_enabled") {
|
||||
return fmt.Errorf("column nodes.relay_web_server_enabled is missing")
|
||||
}
|
||||
|
||||
// Validate columns on a sharded partition table
|
||||
for _, shard := range []string{"node_observation_frps_00"} {
|
||||
for _, column := range []string{"frps_client_count", "frps_proxies"} {
|
||||
if !migrator.HasColumn(shard, column) {
|
||||
return fmt.Errorf("column %s.%s is missing", shard, column)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -86,6 +86,8 @@ func (databaseSchemaMigrationContext) ValidateDatabaseSchemaVersion(db *gorm.DB,
|
||||
return validateDatabaseSchemaV15(db, backend)
|
||||
case 16:
|
||||
return validateDatabaseSchemaV16(db, backend)
|
||||
case 17:
|
||||
return validateDatabaseSchemaV17(db, backend)
|
||||
default:
|
||||
return fmt.Errorf("database schema validation for v%d is not defined", version)
|
||||
}
|
||||
@@ -1301,6 +1303,31 @@ func validateDatabaseSchemaV16(db *gorm.DB, backend string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDatabaseSchemaV17(db *gorm.DB, backend string) error {
|
||||
if err := validateDatabaseSchemaV16(db, backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if db == nil {
|
||||
return fmt.Errorf("database handle is nil")
|
||||
}
|
||||
|
||||
migrator := db.Migrator()
|
||||
if !migrator.HasColumn(&Node{}, "relay_web_server_enabled") {
|
||||
return fmt.Errorf("column nodes.relay_web_server_enabled is missing")
|
||||
}
|
||||
|
||||
// Validate columns on a sharded partition table
|
||||
for _, shard := range []string{"node_observation_frps_00"} {
|
||||
for _, column := range []string{"frps_client_count", "frps_proxies"} {
|
||||
if !migrator.HasColumn(shard, column) {
|
||||
return fmt.Errorf("column %s.%s is missing", shard, column)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func databaseSchemaMigrations() []databaseSchemaMigration {
|
||||
ctx := databaseSchemaMigrationContext{}
|
||||
migrations := []databaseSchemaMigration{}
|
||||
|
||||
@@ -38,6 +38,7 @@ type Node struct {
|
||||
RelayClientAccessAddr string `json:"relay_client_access_addr" gorm:"size:255"`
|
||||
RelayClientProxyURL string `json:"relay_client_proxy_url" gorm:"size:512"`
|
||||
RelayStatus string `json:"relay_status" gorm:"size:16;not null;default:'unknown'"`
|
||||
RelayWebServerEnabled bool `json:"relay_web_server_enabled" gorm:"not null;default:false"`
|
||||
}
|
||||
|
||||
func ListNodes() (nodes []*Node, err error) {
|
||||
|
||||
@@ -13,6 +13,8 @@ type NodeObservationFrps struct {
|
||||
CapturedAt time.Time `json:"captured_at" gorm:"index"`
|
||||
FrpsConnections int `json:"frps_connections"`
|
||||
FrpsProxyCount int `json:"frps_proxy_count"`
|
||||
FrpsClientCount int `json:"frps_client_count"`
|
||||
FrpsProxies string `json:"frps_proxies" gorm:"type:text"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
|
||||
@@ -171,6 +171,7 @@ type NodeView struct {
|
||||
RelayClientAccessAddr string `json:"relay_client_access_addr"`
|
||||
RelayClientProxyURL string `json:"relay_client_proxy_url"`
|
||||
RelayStatus string `json:"relay_status"`
|
||||
RelayWebServerEnabled bool `json:"relay_web_server_enabled"`
|
||||
}
|
||||
|
||||
func HeartbeatNode(node *model.Node, payload AgentNodePayload) (*HeartbeatResponse, error) {
|
||||
|
||||
@@ -31,6 +31,7 @@ type NodeInput struct {
|
||||
RelayAgentAccessAddr string `json:"relay_agent_access_addr"`
|
||||
RelayClientAccessAddr string `json:"relay_client_access_addr"`
|
||||
RelayClientProxyURL string `json:"relay_client_proxy_url"`
|
||||
RelayWebServerEnabled bool `json:"relay_web_server_enabled"`
|
||||
}
|
||||
|
||||
type NodeAgentUpdateInput struct {
|
||||
@@ -100,6 +101,7 @@ func CreateNode(input NodeInput) (*NodeView, error) {
|
||||
node.RelayAgentAccessAddr = strings.TrimSpace(input.RelayAgentAccessAddr)
|
||||
node.RelayClientAccessAddr = strings.TrimSpace(input.RelayClientAccessAddr)
|
||||
node.RelayClientProxyURL = strings.TrimSpace(input.RelayClientProxyURL)
|
||||
node.RelayWebServerEnabled = input.RelayWebServerEnabled
|
||||
}
|
||||
if !node.GeoManualOverride {
|
||||
applyGeoInfoFromIP(node, node.IP)
|
||||
@@ -137,6 +139,7 @@ func UpdateNode(id uint, input NodeInput) (*NodeView, error) {
|
||||
node.RelayAgentAccessAddr = strings.TrimSpace(input.RelayAgentAccessAddr)
|
||||
node.RelayClientAccessAddr = strings.TrimSpace(input.RelayClientAccessAddr)
|
||||
node.RelayClientProxyURL = strings.TrimSpace(input.RelayClientProxyURL)
|
||||
node.RelayWebServerEnabled = input.RelayWebServerEnabled
|
||||
if input.RelayBindPort > 0 {
|
||||
node.RelayBindPort = input.RelayBindPort
|
||||
}
|
||||
@@ -354,6 +357,7 @@ func buildNodeView(node *model.Node) *NodeView {
|
||||
view.RelayClientAccessAddr = node.RelayClientAccessAddr
|
||||
view.RelayClientProxyURL = node.RelayClientProxyURL
|
||||
view.RelayStatus = node.RelayStatus
|
||||
view.RelayWebServerEnabled = node.RelayWebServerEnabled
|
||||
view.Version = node.Version
|
||||
view.ExtVersion = node.ExtVersion
|
||||
return view
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"openflare/model"
|
||||
"time"
|
||||
@@ -146,24 +147,44 @@ func buildRelayDashboardSnapshot(node *model.Node, obs *model.NodeObservationFrp
|
||||
}
|
||||
totalProxies := 0
|
||||
totalConnections := 0
|
||||
clientCounts := 0
|
||||
proxies := []RelayProxyStat{}
|
||||
|
||||
if obs != nil {
|
||||
totalProxies = obs.FrpsProxyCount
|
||||
totalConnections = obs.FrpsConnections
|
||||
clientCounts = obs.FrpsClientCount
|
||||
if obs.FrpsProxies != "" {
|
||||
var decoded []RelayProxyStat
|
||||
if err := json.Unmarshal([]byte(obs.FrpsProxies), &decoded); err == nil {
|
||||
proxies = decoded
|
||||
}
|
||||
}
|
||||
}
|
||||
if totalProxies < 0 {
|
||||
totalProxies = 0
|
||||
}
|
||||
onlineProxies := totalProxies
|
||||
if node.RelayStatus != "healthy" {
|
||||
onlineProxies = 0
|
||||
onlineProxies := 0
|
||||
for _, p := range proxies {
|
||||
if p.Status == "online" {
|
||||
onlineProxies++
|
||||
}
|
||||
}
|
||||
// Fallback for backward compatibility
|
||||
if len(proxies) == 0 {
|
||||
onlineProxies = totalProxies
|
||||
if node.RelayStatus != "healthy" {
|
||||
onlineProxies = 0
|
||||
}
|
||||
}
|
||||
|
||||
return &RelayDashboardSnapshot{
|
||||
TotalProxies: totalProxies,
|
||||
OnlineProxies: onlineProxies,
|
||||
OfflineProxies: totalProxies - onlineProxies,
|
||||
Proxies: []RelayProxyStat{},
|
||||
Proxies: proxies,
|
||||
TotalConnections: maxInt(totalConnections, 0),
|
||||
ClientCounts: 0,
|
||||
ClientCounts: maxInt(clientCounts, 0),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
@@ -16,26 +17,29 @@ import (
|
||||
|
||||
// RelayHeartbeatPayload is the payload sent by OpenFlareRelay in each heartbeat.
|
||||
type RelayHeartbeatPayload struct {
|
||||
Version string `json:"version"`
|
||||
ExtVersion string `json:"frp_version"`
|
||||
RelayStatus string `json:"relay_status"`
|
||||
FrpsConnCount int `json:"frps_connections"`
|
||||
FrpsProxyCount int `json:"frps_proxy_count"`
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip"`
|
||||
Profile *AgentNodeSystemProfile `json:"profile,omitempty"`
|
||||
Snapshot *AgentNodeMetricSnapshot `json:"snapshot,omitempty"`
|
||||
HealthEvents []AgentNodeHealthEvent `json:"health_events,omitempty"`
|
||||
Version string `json:"version"`
|
||||
ExtVersion string `json:"frp_version"`
|
||||
RelayStatus string `json:"relay_status"`
|
||||
FrpsConnCount int `json:"frps_connections"`
|
||||
FrpsProxyCount int `json:"frps_proxy_count"`
|
||||
FrpsClientCount int `json:"frps_client_count"`
|
||||
FrpsProxies []RelayProxyStat `json:"frps_proxies,omitempty"`
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip"`
|
||||
Profile *AgentNodeSystemProfile `json:"profile,omitempty"`
|
||||
Snapshot *AgentNodeMetricSnapshot `json:"snapshot,omitempty"`
|
||||
HealthEvents []AgentNodeHealthEvent `json:"health_events,omitempty"`
|
||||
}
|
||||
|
||||
const relayFrpsUnhealthyEventType = "frps_unhealthy"
|
||||
|
||||
// RelayConfig is the frps configuration sent to the Relay.
|
||||
type RelayConfig struct {
|
||||
BindPort int `json:"bind_port"`
|
||||
VhostHTTPPort int `json:"vhost_http_port"`
|
||||
AuthToken string `json:"auth_token"`
|
||||
LogLevel string `json:"log_level"`
|
||||
BindPort int `json:"bind_port"`
|
||||
VhostHTTPPort int `json:"vhost_http_port"`
|
||||
AuthToken string `json:"auth_token"`
|
||||
LogLevel string `json:"log_level"`
|
||||
WebServerEnabled bool `json:"web_server_enabled"`
|
||||
}
|
||||
|
||||
// RelaySettings contains runtime settings for the Relay.
|
||||
@@ -148,11 +152,20 @@ func persistRelayHeartbeatObservability(nodeID string, payload RelayHeartbeatPay
|
||||
HealthEvents: payload.HealthEvents,
|
||||
}, reportedAt)
|
||||
|
||||
var proxiesJSON string
|
||||
if len(payload.FrpsProxies) > 0 {
|
||||
if data, err := json.Marshal(payload.FrpsProxies); err == nil {
|
||||
proxiesJSON = string(data)
|
||||
}
|
||||
}
|
||||
|
||||
frpsObs := &model.NodeObservationFrps{
|
||||
NodeID: nodeID,
|
||||
CapturedAt: reportedAt,
|
||||
FrpsConnections: payload.FrpsConnCount,
|
||||
FrpsProxyCount: payload.FrpsProxyCount,
|
||||
FrpsClientCount: payload.FrpsClientCount,
|
||||
FrpsProxies: proxiesJSON,
|
||||
}
|
||||
_ = frpsObs.Insert()
|
||||
}
|
||||
@@ -162,10 +175,11 @@ func buildRelayConfig(node *model.Node) *RelayConfig {
|
||||
return nil
|
||||
}
|
||||
return &RelayConfig{
|
||||
BindPort: node.RelayBindPort,
|
||||
VhostHTTPPort: node.RelayVhostHTTPPort,
|
||||
AuthToken: node.RelayAuthToken,
|
||||
LogLevel: "info",
|
||||
BindPort: node.RelayBindPort,
|
||||
VhostHTTPPort: node.RelayVhostHTTPPort,
|
||||
AuthToken: node.RelayAuthToken,
|
||||
LogLevel: "info",
|
||||
WebServerEnabled: node.RelayWebServerEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ const nodeEditorSchema = z
|
||||
relay_client_access_addr: z.string().trim(),
|
||||
relay_agent_access_addr: z.string().trim(),
|
||||
relay_client_proxy_url: z.string().trim(),
|
||||
relay_web_server_enabled: z.boolean(),
|
||||
geo_manual_override: z.boolean(),
|
||||
geo_region: z.string(),
|
||||
geo_name: z.string().trim().max(128, '位置名不能超过 128 个字符'),
|
||||
@@ -142,6 +143,7 @@ const defaultValues: NodeEditorValues = {
|
||||
relay_client_access_addr: '',
|
||||
relay_agent_access_addr: '',
|
||||
relay_client_proxy_url: '',
|
||||
relay_web_server_enabled: false,
|
||||
geo_manual_override: false,
|
||||
geo_region: '',
|
||||
geo_name: '',
|
||||
@@ -233,6 +235,7 @@ function buildFormValues(node?: Partial<NodeItem> | null): NodeEditorValues {
|
||||
relay_client_access_addr: node.relay_client_access_addr ?? '',
|
||||
relay_agent_access_addr: node.relay_agent_access_addr ?? '',
|
||||
relay_client_proxy_url: node.relay_client_proxy_url ?? '',
|
||||
relay_web_server_enabled: node.relay_web_server_enabled ?? false,
|
||||
geo_manual_override: node.geo_manual_override ?? false,
|
||||
geo_region: node.geo_manual_override ? (node.geo_name ?? '') : '',
|
||||
geo_name: node.geo_name ?? '',
|
||||
@@ -259,6 +262,7 @@ function toPayload(values: NodeEditorValues): NodeMutationPayload {
|
||||
relay_client_access_addr: values.type === 'tunnel_relay' ? values.relay_client_access_addr.trim() : undefined,
|
||||
relay_agent_access_addr: values.type === 'tunnel_relay' ? values.relay_agent_access_addr.trim() : undefined,
|
||||
relay_client_proxy_url: values.type === 'tunnel_relay' ? values.relay_client_proxy_url.trim() : undefined,
|
||||
relay_web_server_enabled: values.type === 'tunnel_relay' ? values.relay_web_server_enabled : undefined,
|
||||
};
|
||||
|
||||
if (!values.geo_manual_override) {
|
||||
@@ -426,6 +430,18 @@ export function NodeEditorModal({
|
||||
>
|
||||
<ResourceInput placeholder="例如: http://10.0.0.1:3128" {...form.register('relay_client_proxy_url')} />
|
||||
</ResourceField>
|
||||
|
||||
<ToggleField
|
||||
label="开启 FRPS WebUI"
|
||||
description="开启后可以通过浏览器直接访问 FRPS 的 Web 界面。安全起见,访问密码固定为当前中继节点的接入 Server Token。"
|
||||
checked={form.watch('relay_web_server_enabled')}
|
||||
onChange={(checked) =>
|
||||
form.setValue('relay_web_server_enabled', checked, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -866,6 +866,21 @@ export function RelayDetailPage({ node }: { node: NodeItem }) {
|
||||
<p>Relay 中继版本:{node.version || 'unknown'}</p>
|
||||
<p>frps 核心版本:{node.ext_version || 'unknown'}</p>
|
||||
<p>中继网络接入:{node.relay_agent_access_addr || '—'}</p>
|
||||
{node.relay_web_server_enabled ? (
|
||||
<p>
|
||||
FRPS WebUI:
|
||||
<a
|
||||
href={`http://${node.ip || '127.0.0.1'}:${node.relay_bind_port + 500}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-[var(--accent-strong)] hover:underline font-medium"
|
||||
>
|
||||
点击打开 Web 界面
|
||||
</a>
|
||||
</p>
|
||||
) : (
|
||||
<p>FRPS WebUI:已禁用</p>
|
||||
)}
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface NodeItem {
|
||||
relay_client_proxy_url: string;
|
||||
relay_auth_token: string;
|
||||
relay_status: string;
|
||||
relay_web_server_enabled: boolean;
|
||||
relay_frps_connections: number;
|
||||
relay_frps_proxy_count: number;
|
||||
geo_name: string;
|
||||
@@ -59,6 +60,7 @@ export interface NodeMutationPayload {
|
||||
relay_client_access_addr?: string;
|
||||
relay_agent_access_addr?: string;
|
||||
relay_client_proxy_url?: string;
|
||||
relay_web_server_enabled?: boolean;
|
||||
auto_update_enabled: boolean;
|
||||
geo_name: string;
|
||||
geo_latitude?: number | null;
|
||||
|
||||
Reference in New Issue
Block a user