Files
backend/config.go
NanamiAdmin 92a0e24db7 refactor(frpc): restructure instance management and config handling
- Move instance-related structs and functions to config.go
- Remove serverAddr, serverPort, and authMethod from database schema
- Implement new config parsing and encoding with nested key support
- Update service management to use instanceID instead of username/name
- Add GetServiceNameByInstanceID helper function
- Update API documentation for auth.method field change
2026-03-25 20:00:34 +08:00

351 lines
8.5 KiB
Go

package main
import (
"encoding/json"
"errors"
"fmt"
"os"
"strconv"
"strings"
"github.com/BurntSushi/toml"
)
type Config struct {
ListenAddr string `json:"listenAddr"`
ListenPort string `json:"listenPort"`
FrpcPath string `json:"frpcPath"`
InstancePath string `json:"instancePath"`
Debug bool `json:"debug"`
}
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 string `json:"local_port"`
RemotePort string `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"`
}
var globalConfig *Config
func LoadConfig(configPath string) (*Config, error) {
data, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse config file: %w", err)
}
if config.ListenAddr == "" {
config.ListenAddr = "0.0.0.0"
}
if config.ListenPort == "" {
config.ListenPort = "8080"
}
if config.FrpcPath == "" {
config.FrpcPath = "/usr/bin/frpc"
}
if config.InstancePath == "" {
config.InstancePath = "./configs"
}
if err := os.MkdirAll(config.InstancePath, 0755); err != nil {
return nil, fmt.Errorf("failed to create config directory: %w", err)
}
globalConfig = &config
return &config, nil
}
func GetConfig() (*Config, error) {
if globalConfig == nil {
return nil, errors.New("config not loaded")
}
return globalConfig, nil
}
func SaveConfig(configPath string, config *Config) error {
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
if err := os.WriteFile(configPath, data, 0644); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
globalConfig = config
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 generateFrpcConfig(info InstanceInfo) string {
config := FrpcConfig{
Global: make(map[string]interface{}),
}
config.Global["serverAddr"] = info.ServerAddr
config.Global["serverPort"] = info.ServerPort
config.Global["auth.method"] = info.AuthMethod
for key, value := range info.Additional {
config.Global[key] = value
}
result, err := encodeFrpcConfig(config)
if err != nil {
return ""
}
return result
}
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 getKeyText(configPath, key, section string) (value string, err error) {
configContent, err := os.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) error {
configContent, err := os.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 := os.WriteFile(configPath, []byte(result), 0644); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
return nil
}