Files
backend/config/config.go
NanamiAdmin 1432651a14 fix(watchdog): fix watchdog unable to send webhook and simplify the parsing of json config
- Change webhook config headers and body from map to string format
- Move command parsing logic to separate function in command.go
- Add debug logging for webhook operations
- Improve exception handling with proper JSON parsing
- Simplify config loading logic with default values
2026-04-28 20:48:54 +08:00

433 lines
11 KiB
Go

package config
import (
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"super-frpc/global"
"super-frpc/postLog"
"github.com/BurntSushi/toml"
)
type InstanceInfo struct {
Name string `json:"name"`
ServerAddr string `json:"serverAddr"`
ServerPort string `json:"serverPort"`
AuthMethod string `json:"auth_method"`
RunUser string `json:"runUser"`
Additional map[string]interface{} `json:"additionalProperties"`
}
type FrpcProxyInfo struct {
Name string `json:"name"`
Type string `json:"type"`
LocalIP string `json:"local_ip"`
LocalPort int `json:"local_port"`
RemotePort int `json:"remote_port"`
}
type CreateInstanceRequest struct {
InstanceInfo InstanceInfo `json:"instanceInfo"`
BootAtStart bool `json:"bootAtStart"`
RunUser string `json:"runUser"`
Additional map[string]interface{} `json:"additionalProperties"`
}
type CreateProxyRequest struct {
InstanceID string `json:"instanceID"`
ProxyInfo FrpcProxyInfo `json:"proxyInfo"`
}
type FrpcConfig struct {
Global map[string]interface{} `toml:"-"`
Proxies []map[string]interface{} `toml:"proxies"`
}
func LoadConfig(configPath string, getInitSystem func() string) error {
data, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("failed to read config file: %w", err)
}
var fileConfig global.Config
if err := json.Unmarshal(data, &fileConfig); err != nil {
return fmt.Errorf("failed to parse config file: %w", err)
}
global.CurrentConfig.ListenAddr = fileConfig.ListenAddr
global.CurrentConfig.ListenPort = fileConfig.ListenPort
global.CurrentConfig.FrpcPath = fileConfig.FrpcPath
global.CurrentConfig.InstancePath = fileConfig.InstancePath
global.CurrentConfig.Debug = fileConfig.Debug
global.CurrentConfig.Watchdog.Enabled = fileConfig.Watchdog.Enabled
global.CurrentConfig.Watchdog.Port = fileConfig.Watchdog.Port
global.CurrentConfig.Notification.Enabled = fileConfig.Notification.Enabled
global.CurrentConfig.Notification.Method = fileConfig.Notification.Method
global.CurrentConfig.Webhook.Method = fileConfig.Webhook.Method
global.CurrentConfig.Webhook.URL = fileConfig.Webhook.URL
global.CurrentConfig.Webhook.Headers = fileConfig.Webhook.Headers
global.CurrentConfig.Webhook.Body = fileConfig.Webhook.Body
if fileConfig.ListenAddr == "" {
global.CurrentConfig.ListenAddr = "0.0.0.0"
}
if fileConfig.ListenPort == "" {
global.CurrentConfig.ListenPort = "7000"
}
if fileConfig.FrpcPath == "" {
if getInitSystem() == "windows" {
global.CurrentConfig.FrpcPath = "frp_client/frpc.exe"
} else {
global.CurrentConfig.FrpcPath = "/usr/bin/frpc"
}
}
if fileConfig.InstancePath == "" {
global.CurrentConfig.InstancePath = "./configs"
}
if fileConfig.Watchdog.Port == 0 {
global.CurrentConfig.Watchdog.Port = 12380
}
if err := os.MkdirAll(global.CurrentConfig.InstancePath, 0755); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}
postLog.Debug(fmt.Sprintf("Loaded config: %v", global.CurrentConfig))
return nil
}
func GetConfig() *global.Config {
return &global.CurrentConfig
}
func SaveConfig() error {
data, err := json.MarshalIndent(global.CurrentConfig, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
if err := os.WriteFile(*global.ConfigPath, data, 0644); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
return nil
}
func DecodeFrpcConfig(configContent string) (FrpcConfig, error) {
var config FrpcConfig
var rawMap map[string]interface{}
meta, err := toml.Decode(configContent, &rawMap)
if err != nil {
return config, fmt.Errorf("failed to parse config: %w", err)
}
config.Global = make(map[string]interface{})
config.Proxies = make([]map[string]interface{}, 0)
getNestedValue := func(m map[string]interface{}, key string) (interface{}, bool) {
parts := strings.Split(key, ".")
current := m
for i, part := range parts {
if i == len(parts)-1 {
val, ok := current[part]
return val, ok
}
next, ok := current[part].(map[string]interface{})
if !ok {
return nil, false
}
current = next
}
return nil, false
}
for _, key := range meta.Keys() {
keyStr := key.String()
if keyStr == "proxies" {
if proxies, ok := rawMap["proxies"].([]map[string]interface{}); ok {
config.Proxies = proxies
} else if proxies, ok := rawMap["proxies"].([]interface{}); ok {
for _, p := range proxies {
if pm, ok := p.(map[string]interface{}); ok {
config.Proxies = append(config.Proxies, pm)
}
}
}
} else if !strings.HasPrefix(keyStr, "proxies.") {
if val, ok := getNestedValue(rawMap, keyStr); ok {
config.Global[keyStr] = val
}
}
}
return config, nil
}
func EncodeFrpcConfig(config FrpcConfig) (string, error) {
var buf strings.Builder
if len(config.Global) > 0 {
for key, value := range config.Global {
switch v := value.(type) {
case string:
buf.WriteString(fmt.Sprintf("%s = %q\n", key, v))
case int:
buf.WriteString(fmt.Sprintf("%s = %d\n", key, v))
case int64:
buf.WriteString(fmt.Sprintf("%s = %d\n", key, v))
case float64:
buf.WriteString(fmt.Sprintf("%s = %v\n", key, v))
case bool:
buf.WriteString(fmt.Sprintf("%s = %v\n", key, v))
default:
data, err := json.Marshal(v)
if err != nil {
return "", fmt.Errorf("failed to marshal value for key %s: %w", key, err)
}
buf.WriteString(fmt.Sprintf("%s = %s\n", key, string(data)))
}
}
buf.WriteString("\n")
}
if len(config.Proxies) > 0 {
for _, proxy := range config.Proxies {
buf.WriteString("[[proxies]]\n")
for key, value := range proxy {
switch v := value.(type) {
case string:
buf.WriteString(fmt.Sprintf("%s = %q\n", key, v))
case int:
buf.WriteString(fmt.Sprintf("%s = %d\n", key, v))
case int64:
buf.WriteString(fmt.Sprintf("%s = %d\n", key, v))
case float64:
buf.WriteString(fmt.Sprintf("%s = %v\n", key, v))
case bool:
buf.WriteString(fmt.Sprintf("%s = %v\n", key, v))
default:
data, err := json.Marshal(v)
if err != nil {
return "", fmt.Errorf("failed to marshal value for key %s: %w", key, err)
}
buf.WriteString(fmt.Sprintf("%s = %s\n", key, string(data)))
}
}
buf.WriteString("\n")
}
}
return strings.TrimSuffix(buf.String(), "\n"), nil
}
func HandleConfigFileCreate(configPath string, info InstanceInfo, setKeyText func(configPath, key, section, value string) error) error {
config := FrpcConfig{
Global: make(map[string]interface{}),
}
result, err := EncodeFrpcConfig(config)
if err != nil {
return fmt.Errorf("failed to encode empty config: %w", err)
}
if err := os.WriteFile(configPath, []byte(result), 0644); err != nil {
return fmt.Errorf("failed to create config file: %w", err)
}
configData := make(map[string]interface{})
configData["serverAddr"] = info.ServerAddr
configData["serverPort"] = info.ServerPort
configData["auth.method"] = info.AuthMethod
if authToken, ok := info.Additional["auth_token"]; ok {
configData["auth.token"] = authToken
}
for key, value := range info.Additional {
if key == "auth_token" || key == "auth_method" {
continue
}
configData[key] = value
}
for key, value := range configData {
var configValue string
if key == "serverPort" {
switch v := value.(type) {
case float64:
configValue = strconv.Itoa(int(v))
case int:
configValue = strconv.Itoa(v)
case string:
var intVal int
if _, err := fmt.Sscanf(v, "%d", &intVal); err == nil {
configValue = strconv.Itoa(intVal)
} else {
configValue = v
}
default:
configValue = fmt.Sprintf("%v", value)
}
} else {
configValue = fmt.Sprintf("%v", value)
}
if err := setKeyText(configPath, key, "", configValue); err != nil {
return fmt.Errorf("failed to set key %s: %w", key, err)
}
}
return nil
}
func AddFrpcProxy(configContent string, info FrpcProxyInfo) (string, error) {
config, err := DecodeFrpcConfig(configContent)
if err != nil {
return "", fmt.Errorf("failed to parse config: %w", err)
}
proxy := map[string]interface{}{
"name": info.Name,
"type": info.Type,
"localIP": info.LocalIP,
"localPort": info.LocalPort,
"remotePort": info.RemotePort,
}
config.Proxies = append(config.Proxies, proxy)
result, err := EncodeFrpcConfig(config)
if err != nil {
return "", fmt.Errorf("failed to write config: %w", err)
}
return result, nil
}
func RemoveFrpcProxy(configContent string, proxyName string) (string, error) {
config, err := DecodeFrpcConfig(configContent)
if err != nil {
return "", fmt.Errorf("failed to parse config: %w", err)
}
var found bool
var newProxies []map[string]interface{}
for _, proxy := range config.Proxies {
if name, ok := proxy["name"].(string); ok && name == proxyName {
found = true
continue
}
newProxies = append(newProxies, proxy)
}
if !found {
return "", fmt.Errorf("proxy %s not found", proxyName)
}
config.Proxies = newProxies
result, err := EncodeFrpcConfig(config)
if err != nil {
return "", fmt.Errorf("failed to write config: %w", err)
}
return result, nil
}
func ModifyFrpcProxy(configContent string, info FrpcProxyInfo) (string, error) {
config, err := DecodeFrpcConfig(configContent)
if err != nil {
return "", fmt.Errorf("failed to parse config: %w", err)
}
var found bool
for i, proxy := range config.Proxies {
if name, ok := proxy["name"].(string); ok && name == info.Name {
config.Proxies[i] = map[string]interface{}{
"name": info.Name,
"type": info.Type,
"localIP": info.LocalIP,
"localPort": info.LocalPort,
"remotePort": info.RemotePort,
}
found = true
break
}
}
if !found {
return "", fmt.Errorf("proxy %s not found", info.Name)
}
result, err := EncodeFrpcConfig(config)
if err != nil {
return "", fmt.Errorf("failed to write config: %w", err)
}
return result, nil
}
func GetKeyText(configPath, key, section string, readFile func(string) ([]byte, error)) (value string, err error) {
configContent, err := readFile(configPath)
if err != nil {
return "", fmt.Errorf("failed to read config file: %w", err)
}
config, err := DecodeFrpcConfig(string(configContent))
if err != nil {
return "", fmt.Errorf("failed to parse config: %w", err)
}
if config.Global == nil {
return "", fmt.Errorf("config file has no global fields")
}
if v, ok := config.Global[key]; ok {
value = fmt.Sprintf("%v", v)
}
return value, nil
}
func SetKeyText(configPath, key, section string, value string, readFile func(string) ([]byte, error), writeFile func(string, []byte, os.FileMode) error) error {
configContent, err := readFile(configPath)
if err != nil {
return fmt.Errorf("failed to read config file: %w", err)
}
config, err := DecodeFrpcConfig(string(configContent))
if err != nil {
return fmt.Errorf("failed to parse config: %w", err)
}
if config.Global == nil {
config.Global = make(map[string]interface{})
}
if intVal, err := strconv.Atoi(value); err == nil {
config.Global[key] = intVal
} else {
config.Global[key] = value
}
result, err := EncodeFrpcConfig(config)
if err != nil {
return fmt.Errorf("failed to write config: %w", err)
}
if err := writeFile(configPath, []byte(result), 0644); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
return nil
}