first commit
Some checks failed
CI/CD Pipeline / Test (push) Failing after 22m19s
CI/CD Pipeline / Security Scan (push) Failing after 5m57s
CI/CD Pipeline / Build (amd64, darwin) (push) Has been skipped
CI/CD Pipeline / Build (amd64, linux) (push) Has been skipped
CI/CD Pipeline / Build (amd64, windows) (push) Has been skipped
CI/CD Pipeline / Build (arm64, darwin) (push) Has been skipped
CI/CD Pipeline / Build (arm64, linux) (push) Has been skipped
CI/CD Pipeline / Build Docker Image (push) Has been skipped
CI/CD Pipeline / Create Release (push) Has been skipped
Some checks failed
CI/CD Pipeline / Test (push) Failing after 22m19s
CI/CD Pipeline / Security Scan (push) Failing after 5m57s
CI/CD Pipeline / Build (amd64, darwin) (push) Has been skipped
CI/CD Pipeline / Build (amd64, linux) (push) Has been skipped
CI/CD Pipeline / Build (amd64, windows) (push) Has been skipped
CI/CD Pipeline / Build (arm64, darwin) (push) Has been skipped
CI/CD Pipeline / Build (arm64, linux) (push) Has been skipped
CI/CD Pipeline / Build Docker Image (push) Has been skipped
CI/CD Pipeline / Create Release (push) Has been skipped
This commit is contained in:
467
internal/config/manager.go
Normal file
467
internal/config/manager.go
Normal file
@@ -0,0 +1,467 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/subconverter-go/internal/logging"
|
||||
)
|
||||
|
||||
// ConfigManager 配置管理器
|
||||
// 封装viper功能,提供统一的配置管理接口
|
||||
type ConfigManager struct {
|
||||
viper *viper.Viper
|
||||
config *Configuration
|
||||
logger *logging.Logger
|
||||
filePath string
|
||||
basePath string
|
||||
templateManager *TemplateManager
|
||||
}
|
||||
|
||||
// NewConfigManager 创建新的配置管理器
|
||||
// 返回初始化好的ConfigManager实例
|
||||
func NewConfigManager(configPath string) (*ConfigManager, error) {
|
||||
logger, err := logging.NewDefaultLogger()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create logger: %v", err)
|
||||
}
|
||||
|
||||
// 设置base路径
|
||||
basePath := "./base"
|
||||
if _, err := os.Stat(basePath); os.IsNotExist(err) {
|
||||
basePath = "../base"
|
||||
}
|
||||
|
||||
manager := &ConfigManager{
|
||||
viper: viper.New(),
|
||||
logger: logger,
|
||||
basePath: basePath,
|
||||
}
|
||||
|
||||
// 设置默认配置
|
||||
manager.setDefaults()
|
||||
|
||||
// 配置viper
|
||||
manager.viper.SetConfigName("config")
|
||||
manager.viper.SetConfigType("yaml")
|
||||
|
||||
// 设置配置文件路径
|
||||
if configPath != "" {
|
||||
manager.viper.SetConfigFile(configPath)
|
||||
manager.filePath = configPath
|
||||
} else {
|
||||
// 默认配置文件路径
|
||||
manager.viper.AddConfigPath(".")
|
||||
manager.viper.AddConfigPath("./config")
|
||||
manager.viper.AddConfigPath("./configs")
|
||||
manager.viper.AddConfigPath("/etc/subconverter-go")
|
||||
}
|
||||
|
||||
// 读取配置文件
|
||||
if err := manager.viper.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
||||
// 配置文件不存在,使用默认配置
|
||||
logger.Info("Configuration file not found, using defaults")
|
||||
} else {
|
||||
return nil, fmt.Errorf("failed to read config file: %v", err)
|
||||
}
|
||||
} else {
|
||||
logger.Infof("Configuration loaded from: %s", manager.viper.ConfigFileUsed())
|
||||
manager.filePath = manager.viper.ConfigFileUsed()
|
||||
}
|
||||
|
||||
// 绑定环境变量
|
||||
manager.bindEnvironmentVariables()
|
||||
|
||||
// 解析配置到结构体
|
||||
if err := manager.parseConfig(); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config: %v", err)
|
||||
}
|
||||
|
||||
// 初始化模板管理器
|
||||
templateManager, err := NewTemplateManager(basePath, logger)
|
||||
if err != nil {
|
||||
logger.Warnf("Failed to initialize template manager: %v, template features will be disabled", err)
|
||||
} else {
|
||||
manager.templateManager = templateManager
|
||||
logger.Info("Template manager initialized successfully")
|
||||
}
|
||||
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
// setDefaults 设置默认配置值
|
||||
func (cm *ConfigManager) setDefaults() {
|
||||
// 服务器默认配置
|
||||
cm.viper.SetDefault("server.host", "0.0.0.0")
|
||||
cm.viper.SetDefault("server.port", 25500)
|
||||
cm.viper.SetDefault("server.read_timeout", 30)
|
||||
cm.viper.SetDefault("server.write_timeout", 30)
|
||||
cm.viper.SetDefault("server.max_request_size", 10*1024*1024) // 10MB
|
||||
|
||||
// 日志默认配置
|
||||
cm.viper.SetDefault("logging.level", "info")
|
||||
cm.viper.SetDefault("logging.format", "json")
|
||||
cm.viper.SetDefault("logging.output", "stdout")
|
||||
cm.viper.SetDefault("logging.file", "")
|
||||
|
||||
// 安全默认配置
|
||||
cm.viper.SetDefault("security.access_tokens", []string{})
|
||||
cm.viper.SetDefault("security.cors_origins", []string{"*"})
|
||||
cm.viper.SetDefault("security.rate_limit", 0)
|
||||
cm.viper.SetDefault("security.timeout", 60)
|
||||
|
||||
// 转换默认配置
|
||||
cm.viper.SetDefault("conversion.default_target", "clash")
|
||||
cm.viper.SetDefault("conversion.default_emoji", false)
|
||||
cm.viper.SetDefault("conversion.default_udp", false)
|
||||
cm.viper.SetDefault("conversion.max_nodes", 0)
|
||||
cm.viper.SetDefault("conversion.cache_timeout", 60)
|
||||
cm.viper.SetDefault("conversion.supported_targets", []string{
|
||||
"clash", "surge", "quanx", "loon", "surfboard", "v2ray",
|
||||
})
|
||||
}
|
||||
|
||||
// bindEnvironmentVariables 绑定环境变量
|
||||
func (cm *ConfigManager) bindEnvironmentVariables() {
|
||||
// 服务器配置
|
||||
cm.viper.BindEnv("server.host", "SUBCONVERTER_HOST")
|
||||
cm.viper.BindEnv("server.port", "SUBCONVERTER_PORT")
|
||||
cm.viper.BindEnv("server.read_timeout", "SUBCONVERTER_READ_TIMEOUT")
|
||||
cm.viper.BindEnv("server.write_timeout", "SUBCONVERTER_WRITE_TIMEOUT")
|
||||
cm.viper.BindEnv("server.max_request_size", "SUBCONVERTER_MAX_REQUEST_SIZE")
|
||||
|
||||
// 日志配置
|
||||
cm.viper.BindEnv("logging.level", "SUBCONVERTER_LOG_LEVEL")
|
||||
cm.viper.BindEnv("logging.format", "SUBCONVERTER_LOG_FORMAT")
|
||||
cm.viper.BindEnv("logging.output", "SUBCONVERTER_LOG_OUTPUT")
|
||||
cm.viper.BindEnv("logging.file", "SUBCONVERTER_LOG_FILE")
|
||||
|
||||
// 安全配置
|
||||
cm.viper.BindEnv("security.access_tokens", "SUBCONVERTER_ACCESS_TOKENS")
|
||||
cm.viper.BindEnv("security.cors_origins", "SUBCONVERTER_CORS_ORIGINS")
|
||||
cm.viper.BindEnv("security.rate_limit", "SUBCONVERTER_RATE_LIMIT")
|
||||
cm.viper.BindEnv("security.timeout", "SUBCONVERTER_TIMEOUT")
|
||||
|
||||
// 转换配置
|
||||
cm.viper.BindEnv("conversion.default_target", "SUBCONVERTER_DEFAULT_TARGET")
|
||||
cm.viper.BindEnv("conversion.default_emoji", "SUBCONVERTER_DEFAULT_EMOJI")
|
||||
cm.viper.BindEnv("conversion.default_udp", "SUBCONVERTER_DEFAULT_UDP")
|
||||
cm.viper.BindEnv("conversion.max_nodes", "SUBCONVERTER_MAX_NODES")
|
||||
cm.viper.BindEnv("conversion.cache_timeout", "SUBCONVERTER_CACHE_TIMEOUT")
|
||||
}
|
||||
|
||||
// parseConfig 解析配置到结构体
|
||||
func (cm *ConfigManager) parseConfig() error {
|
||||
cm.config = &Configuration{}
|
||||
|
||||
// 手动解析配置,因为viper的直接映射可能不适用复杂结构
|
||||
if err := cm.viper.Unmarshal(cm.config); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal config: %v", err)
|
||||
}
|
||||
|
||||
// 后处理配置
|
||||
cm.postProcessConfig()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// postProcessConfig 后处理配置
|
||||
func (cm *ConfigManager) postProcessConfig() {
|
||||
// 处理安全配置
|
||||
if cm.config.Security.CorsOrigins == nil {
|
||||
cm.config.Security.CorsOrigins = []string{"*"}
|
||||
}
|
||||
|
||||
// 处理转换配置
|
||||
if cm.config.Conversion.SupportedTargets == nil {
|
||||
cm.config.Conversion.SupportedTargets = []string{
|
||||
"clash", "clashr", "surge", "quanx", "loon", "surfboard", "v2ray",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
// 返回当前配置的副本
|
||||
func (cm *ConfigManager) GetConfig() *Configuration {
|
||||
if cm.config == nil {
|
||||
return nil
|
||||
}
|
||||
return cm.config.Clone()
|
||||
}
|
||||
|
||||
// GetServerConfig 获取服务器配置
|
||||
func (cm *ConfigManager) GetServerConfig() *ServerConfig {
|
||||
if cm.config == nil {
|
||||
return nil
|
||||
}
|
||||
clone := cm.config.Server.Clone()
|
||||
return &clone
|
||||
}
|
||||
|
||||
// GetLoggingConfig 获取日志配置
|
||||
func (cm *ConfigManager) GetLoggingConfig() *LoggingConfig {
|
||||
if cm.config == nil {
|
||||
return nil
|
||||
}
|
||||
clone := cm.config.Logging.Clone()
|
||||
return &clone
|
||||
}
|
||||
|
||||
// GetSecurityConfig 获取安全配置
|
||||
func (cm *ConfigManager) GetSecurityConfig() *SecurityConfig {
|
||||
if cm.config == nil {
|
||||
return nil
|
||||
}
|
||||
clone := cm.config.Security.Clone()
|
||||
return &clone
|
||||
}
|
||||
|
||||
// GetConversionConfig 获取转换配置
|
||||
func (cm *ConfigManager) GetConversionConfig() *ConversionConfig {
|
||||
if cm.config == nil {
|
||||
return nil
|
||||
}
|
||||
clone := cm.config.Conversion.Clone()
|
||||
return &clone
|
||||
}
|
||||
|
||||
// UpdateConfig 更新配置
|
||||
func (cm *ConfigManager) UpdateConfig(config *Configuration) error {
|
||||
if config == nil {
|
||||
return fmt.Errorf("config cannot be nil")
|
||||
}
|
||||
|
||||
// 验证配置
|
||||
if err := config.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid config: %v", err)
|
||||
}
|
||||
|
||||
// 更新配置
|
||||
cm.config = config.Clone()
|
||||
|
||||
// 同步到viper
|
||||
cm.syncToViper()
|
||||
|
||||
// 保存到文件(如果配置了文件路径)
|
||||
if cm.filePath != "" {
|
||||
if err := cm.SaveConfig(); err != nil {
|
||||
cm.logger.WithError(err).Warn("Failed to save config to file")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncToViper 将配置同步到viper
|
||||
func (cm *ConfigManager) syncToViper() {
|
||||
// 使用反射同步配置
|
||||
v := reflect.ValueOf(cm.config).Elem()
|
||||
t := v.Type()
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := v.Field(i)
|
||||
fieldType := t.Field(i)
|
||||
|
||||
// 获取字段名(考虑yaml标签)
|
||||
fieldName := getFieldName(fieldType)
|
||||
|
||||
// 递归设置嵌套结构
|
||||
cm.setViperValue(cm.viper, fieldName, field)
|
||||
}
|
||||
}
|
||||
|
||||
// setViperValue 递归设置viper值
|
||||
func (cm *ConfigManager) setViperValue(v *viper.Viper, key string, value reflect.Value) {
|
||||
switch value.Kind() {
|
||||
case reflect.Struct:
|
||||
// 处理嵌套结构
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
field := value.Field(i)
|
||||
fieldType := value.Type().Field(i)
|
||||
subKey := key + "." + getFieldName(fieldType)
|
||||
cm.setViperValue(v, subKey, field)
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
// 处理切片和数组
|
||||
if value.Len() > 0 {
|
||||
slice := make([]interface{}, value.Len())
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
slice[i] = value.Index(i).Interface()
|
||||
}
|
||||
v.Set(key, slice)
|
||||
}
|
||||
case reflect.Map:
|
||||
// 处理map
|
||||
mapValue := make(map[string]interface{})
|
||||
for _, key := range value.MapKeys() {
|
||||
mapValue[key.String()] = value.MapIndex(key).Interface()
|
||||
}
|
||||
v.Set(key, mapValue)
|
||||
default:
|
||||
// 处理基本类型
|
||||
v.Set(key, value.Interface())
|
||||
}
|
||||
}
|
||||
|
||||
// getFieldName 获取字段名
|
||||
func getFieldName(field reflect.StructField) string {
|
||||
// 优先使用yaml标签
|
||||
if yamlTag := field.Tag.Get("yaml"); yamlTag != "" {
|
||||
if yamlTag != "-" {
|
||||
return strings.Split(yamlTag, ",")[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 其次使用json标签
|
||||
if jsonTag := field.Tag.Get("json"); jsonTag != "" {
|
||||
if jsonTag != "-" {
|
||||
return strings.Split(jsonTag, ",")[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 最后使用字段名
|
||||
return strings.ToLower(field.Name)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置到文件
|
||||
func (cm *ConfigManager) SaveConfig() error {
|
||||
if cm.filePath == "" {
|
||||
return fmt.Errorf("no config file path configured")
|
||||
}
|
||||
|
||||
// 确保目录存在
|
||||
dir := filepath.Dir(cm.filePath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create config directory: %v", err)
|
||||
}
|
||||
|
||||
// 设置配置文件路径
|
||||
cm.viper.SetConfigFile(cm.filePath)
|
||||
|
||||
// 保存配置
|
||||
if err := cm.viper.WriteConfig(); err != nil {
|
||||
return fmt.Errorf("failed to write config: %v", err)
|
||||
}
|
||||
|
||||
cm.logger.Infof("Configuration saved to: %s", cm.filePath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReloadConfig 从磁盘重新加载配置
|
||||
func (cm *ConfigManager) ReloadConfig() error {
|
||||
if cm.filePath == "" {
|
||||
return fmt.Errorf("no config file path configured")
|
||||
}
|
||||
|
||||
cm.logger.Infof("Reloading configuration from: %s", cm.filePath)
|
||||
cm.viper.SetConfigFile(cm.filePath)
|
||||
if err := cm.viper.ReadInConfig(); err != nil {
|
||||
return fmt.Errorf("failed to read config file: %v", err)
|
||||
}
|
||||
|
||||
if err := cm.parseConfig(); err != nil {
|
||||
return fmt.Errorf("failed to parse config: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WatchConfig 监听配置文件变化
|
||||
func (cm *ConfigManager) WatchConfig(callback func(*Configuration)) {
|
||||
cm.viper.WatchConfig()
|
||||
|
||||
cm.viper.OnConfigChange(func(e fsnotify.Event) {
|
||||
cm.logger.Infof("Configuration file changed: %s", e.Name)
|
||||
|
||||
// 重新解析配置
|
||||
if err := cm.parseConfig(); err != nil {
|
||||
cm.logger.WithError(err).Error("Failed to parse changed config")
|
||||
return
|
||||
}
|
||||
|
||||
// 调用回调函数
|
||||
if callback != nil {
|
||||
callback(cm.config)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// GetString 获取字符串配置值
|
||||
func (cm *ConfigManager) GetString(key string) string {
|
||||
return cm.viper.GetString(key)
|
||||
}
|
||||
|
||||
// GetInt 获取整型配置值
|
||||
func (cm *ConfigManager) GetInt(key string) int {
|
||||
return cm.viper.GetInt(key)
|
||||
}
|
||||
|
||||
// GetBool 获取布尔配置值
|
||||
func (cm *ConfigManager) GetBool(key string) bool {
|
||||
return cm.viper.GetBool(key)
|
||||
}
|
||||
|
||||
// GetStringSlice 获取字符串切片配置值
|
||||
func (cm *ConfigManager) GetStringSlice(key string) []string {
|
||||
return cm.viper.GetStringSlice(key)
|
||||
}
|
||||
|
||||
// IsSet 检查配置是否设置
|
||||
func (cm *ConfigManager) IsSet(key string) bool {
|
||||
return cm.viper.IsSet(key)
|
||||
}
|
||||
|
||||
// Set 设置配置值
|
||||
func (cm *ConfigManager) Set(key string, value interface{}) {
|
||||
cm.viper.Set(key, value)
|
||||
}
|
||||
|
||||
// GetFilePath 获取配置文件路径
|
||||
func (cm *ConfigManager) GetFilePath() string {
|
||||
return cm.filePath
|
||||
}
|
||||
|
||||
// Close 关闭配置管理器
|
||||
func (cm *ConfigManager) Close() error {
|
||||
// 停止监听配置变化
|
||||
cm.viper.OnConfigChange(nil)
|
||||
|
||||
// 保存配置(如果需要)
|
||||
if cm.filePath != "" {
|
||||
if err := cm.SaveConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTemplateManager 获取模板管理器
|
||||
func (cm *ConfigManager) GetTemplateManager() *TemplateManager {
|
||||
return cm.templateManager
|
||||
}
|
||||
|
||||
// GetBasePath 获取base路径
|
||||
func (cm *ConfigManager) GetBasePath() string {
|
||||
return cm.basePath
|
||||
}
|
||||
|
||||
// HasTemplateManager 检查是否有模板管理器
|
||||
func (cm *ConfigManager) HasTemplateManager() bool {
|
||||
return cm.templateManager != nil
|
||||
}
|
||||
|
||||
// ReloadTemplates 重新加载模板
|
||||
func (cm *ConfigManager) ReloadTemplates() error {
|
||||
if cm.templateManager == nil {
|
||||
return fmt.Errorf("template manager not initialized")
|
||||
}
|
||||
|
||||
return cm.templateManager.Reload()
|
||||
}
|
||||
449
internal/config/model.go
Normal file
449
internal/config/model.go
Normal file
@@ -0,0 +1,449 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Configuration 表示应用程序的完整配置
|
||||
// 该结构体包含服务器、日志、安全和转换相关的所有配置项
|
||||
type Configuration struct {
|
||||
// ServerConfig 服务器配置
|
||||
Server ServerConfig `yaml:"server" json:"server"`
|
||||
|
||||
// LoggingConfig 日志配置
|
||||
Logging LoggingConfig `yaml:"logging" json:"logging"`
|
||||
|
||||
// SecurityConfig 安全配置
|
||||
Security SecurityConfig `yaml:"security" json:"security"`
|
||||
|
||||
// ConversionConfig 转换配置
|
||||
Conversion ConversionConfig `yaml:"conversion" json:"conversion"`
|
||||
}
|
||||
|
||||
// ServerConfig 服务器相关配置
|
||||
type ServerConfig struct {
|
||||
// Port 服务器监听端口,默认25500
|
||||
Port int `yaml:"port" json:"port" env:"SUBCONVERTER_PORT" mapstructure:"port"`
|
||||
|
||||
// Host 服务器监听地址,默认"0.0.0.0"
|
||||
Host string `yaml:"host" json:"host" env:"SUBCONVERTER_HOST" mapstructure:"host"`
|
||||
|
||||
// ReadTimeout 读取超时时间(秒),默认30
|
||||
ReadTimeout int `yaml:"read_timeout" json:"read_timeout" env:"SUBCONVERTER_READ_TIMEOUT" mapstructure:"read_timeout"`
|
||||
|
||||
// WriteTimeout 写入超时时间(秒),默认30
|
||||
WriteTimeout int `yaml:"write_timeout" json:"write_timeout" env:"SUBCONVERTER_WRITE_TIMEOUT" mapstructure:"write_timeout"`
|
||||
|
||||
// MaxRequestSize 最大请求大小(字节),默认10MB
|
||||
MaxRequestSize int64 `yaml:"max_request_size" json:"max_request_size" env:"SUBCONVERTER_MAX_REQUEST_SIZE" mapstructure:"max_request_size"`
|
||||
}
|
||||
|
||||
// Clone 创建服务器配置的深拷贝
|
||||
func (s *ServerConfig) Clone() ServerConfig {
|
||||
if s == nil {
|
||||
return ServerConfig{}
|
||||
}
|
||||
|
||||
return ServerConfig{
|
||||
Port: s.Port,
|
||||
Host: s.Host,
|
||||
ReadTimeout: s.ReadTimeout,
|
||||
WriteTimeout: s.WriteTimeout,
|
||||
MaxRequestSize: s.MaxRequestSize,
|
||||
}
|
||||
}
|
||||
|
||||
// LoggingConfig 日志相关配置
|
||||
type LoggingConfig struct {
|
||||
// Level 日志级别,可选:debug, info, warn, error,默认info
|
||||
Level string `yaml:"level" json:"level" env:"SUBCONVERTER_LOG_LEVEL" mapstructure:"level"`
|
||||
|
||||
// Format 日志格式,可选:json, text,默认json
|
||||
Format string `yaml:"format" json:"format" env:"SUBCONVERTER_LOG_FORMAT" mapstructure:"format"`
|
||||
|
||||
// Output 日志输出,可选:stdout, stderr, file,默认stdout
|
||||
Output string `yaml:"output" json:"output" env:"SUBCONVERTER_LOG_OUTPUT" mapstructure:"output"`
|
||||
|
||||
// File 如果Output为file,指定日志文件路径
|
||||
File string `yaml:"file" json:"file" env:"SUBCONVERTER_LOG_FILE" mapstructure:"file"`
|
||||
}
|
||||
|
||||
// Clone 创建日志配置的深拷贝
|
||||
func (l *LoggingConfig) Clone() LoggingConfig {
|
||||
if l == nil {
|
||||
return LoggingConfig{}
|
||||
}
|
||||
|
||||
return LoggingConfig{
|
||||
Level: l.Level,
|
||||
Format: l.Format,
|
||||
Output: l.Output,
|
||||
File: l.File,
|
||||
}
|
||||
}
|
||||
|
||||
// SecurityConfig 安全相关配置
|
||||
type SecurityConfig struct {
|
||||
// AccessTokens 访问令牌列表,空列表表示无需认证
|
||||
AccessTokens []string `yaml:"access_tokens" json:"access_tokens" env:"SUBCONVERTER_ACCESS_TOKENS" mapstructure:"access_tokens"`
|
||||
|
||||
// CorsOrigins CORS允许的源列表,支持通配符*
|
||||
CorsOrigins []string `yaml:"cors_origins" json:"cors_origins" env:"SUBCONVERTER_CORS_ORIGINS" mapstructure:"cors_origins"`
|
||||
|
||||
// RateLimit 速率限制(每分钟请求数),0表示不限制
|
||||
RateLimit int `yaml:"rate_limit" json:"rate_limit" env:"SUBCONVERTER_RATE_LIMIT" mapstructure:"rate_limit"`
|
||||
|
||||
// Timeout 请求超时时间(秒),默认60
|
||||
Timeout int `yaml:"timeout" json:"timeout" env:"SUBCONVERTER_TIMEOUT" mapstructure:"timeout"`
|
||||
}
|
||||
|
||||
// Clone 创建安全配置的深拷贝
|
||||
func (s *SecurityConfig) Clone() SecurityConfig {
|
||||
if s == nil {
|
||||
return SecurityConfig{}
|
||||
}
|
||||
|
||||
// 深拷贝切片
|
||||
accessTokens := make([]string, len(s.AccessTokens))
|
||||
copy(accessTokens, s.AccessTokens)
|
||||
|
||||
corsOrigins := make([]string, len(s.CorsOrigins))
|
||||
copy(corsOrigins, s.CorsOrigins)
|
||||
|
||||
return SecurityConfig{
|
||||
AccessTokens: accessTokens,
|
||||
CorsOrigins: corsOrigins,
|
||||
RateLimit: s.RateLimit,
|
||||
Timeout: s.Timeout,
|
||||
}
|
||||
}
|
||||
|
||||
// ConversionConfig 转换相关配置
|
||||
type ConversionConfig struct {
|
||||
// DefaultTarget 默认目标格式,默认clash
|
||||
DefaultTarget string `yaml:"default_target" json:"default_target" env:"SUBCONVERTER_DEFAULT_TARGET" mapstructure:"default_target"`
|
||||
|
||||
// SupportedTargets 支持的目标格式列表
|
||||
SupportedTargets []string `yaml:"supported_targets" json:"supported_targets" mapstructure:"supported_targets"`
|
||||
|
||||
// DefaultEmoji 默认是否启用emoji,默认false
|
||||
DefaultEmoji bool `yaml:"default_emoji" json:"default_emoji" env:"SUBCONVERTER_DEFAULT_EMOJI" mapstructure:"default_emoji"`
|
||||
|
||||
// DefaultUDP 默认是否启用UDP,默认false
|
||||
DefaultUDP bool `yaml:"default_udp" json:"default_udp" env:"SUBCONVERTER_DEFAULT_UDP" mapstructure:"default_udp"`
|
||||
|
||||
// MaxNodes 最大节点数量,默认0(无限制)
|
||||
MaxNodes int `yaml:"max_nodes" json:"max_nodes" env:"SUBCONVERTER_MAX_NODES" mapstructure:"max_nodes"`
|
||||
|
||||
// CacheTimeout 缓存超时时间(分钟),默认60
|
||||
CacheTimeout int `yaml:"cache_timeout" json:"cache_timeout" env:"SUBCONVERTER_CACHE_TIMEOUT" mapstructure:"cache_timeout"`
|
||||
}
|
||||
|
||||
// Clone 创建转换配置的深拷贝
|
||||
func (c *ConversionConfig) Clone() ConversionConfig {
|
||||
if c == nil {
|
||||
return ConversionConfig{}
|
||||
}
|
||||
|
||||
// 深拷贝切片
|
||||
supportedTargets := make([]string, len(c.SupportedTargets))
|
||||
copy(supportedTargets, c.SupportedTargets)
|
||||
|
||||
return ConversionConfig{
|
||||
DefaultTarget: c.DefaultTarget,
|
||||
SupportedTargets: supportedTargets,
|
||||
DefaultEmoji: c.DefaultEmoji,
|
||||
DefaultUDP: c.DefaultUDP,
|
||||
MaxNodes: c.MaxNodes,
|
||||
CacheTimeout: c.CacheTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
// NewConfiguration 创建新的默认配置
|
||||
// 返回包含所有默认值的Configuration结构体
|
||||
func NewConfiguration() *Configuration {
|
||||
return &Configuration{
|
||||
Server: ServerConfig{
|
||||
Port: 25500,
|
||||
Host: "0.0.0.0",
|
||||
ReadTimeout: 30,
|
||||
WriteTimeout: 30,
|
||||
MaxRequestSize: 10 * 1024 * 1024, // 10MB
|
||||
},
|
||||
Logging: LoggingConfig{
|
||||
Level: "info",
|
||||
Format: "json",
|
||||
Output: "stdout",
|
||||
File: "",
|
||||
},
|
||||
Security: SecurityConfig{
|
||||
AccessTokens: []string{},
|
||||
CorsOrigins: []string{"*"},
|
||||
RateLimit: 0,
|
||||
Timeout: 60,
|
||||
},
|
||||
Conversion: ConversionConfig{
|
||||
DefaultTarget: "clash",
|
||||
SupportedTargets: []string{"clash", "clashr", "surge", "quanx", "loon", "surfboard", "v2ray"},
|
||||
DefaultEmoji: false,
|
||||
DefaultUDP: false,
|
||||
MaxNodes: 0,
|
||||
CacheTimeout: 60,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Validate 验证配置的有效性
|
||||
// 返回error如果配置无效,nil表示配置有效
|
||||
func (c *Configuration) Validate() error {
|
||||
// 验证服务器配置
|
||||
if c.Server.Port < 1 || c.Server.Port > 65535 {
|
||||
return fmt.Errorf("server port must be between 1 and 65535, got: %d", c.Server.Port)
|
||||
}
|
||||
|
||||
if c.Server.Host == "" {
|
||||
return fmt.Errorf("server host cannot be empty")
|
||||
}
|
||||
|
||||
if c.Server.ReadTimeout < 1 {
|
||||
return fmt.Errorf("read timeout must be positive, got: %d", c.Server.ReadTimeout)
|
||||
}
|
||||
|
||||
if c.Server.WriteTimeout < 1 {
|
||||
return fmt.Errorf("write timeout must be positive, got: %d", c.Server.WriteTimeout)
|
||||
}
|
||||
|
||||
if c.Server.MaxRequestSize < 1 {
|
||||
return fmt.Errorf("max request size must be positive, got: %d", c.Server.MaxRequestSize)
|
||||
}
|
||||
|
||||
// 验证日志配置
|
||||
validLogLevels := map[string]bool{
|
||||
"debug": true,
|
||||
"info": true,
|
||||
"warn": true,
|
||||
"error": true,
|
||||
}
|
||||
if !validLogLevels[c.Logging.Level] {
|
||||
return fmt.Errorf("log level must be one of: debug, info, warn, error, got: %s", c.Logging.Level)
|
||||
}
|
||||
|
||||
validLogFormats := map[string]bool{
|
||||
"json": true,
|
||||
"text": true,
|
||||
}
|
||||
if !validLogFormats[c.Logging.Format] {
|
||||
return fmt.Errorf("log format must be one of: json, text, got: %s", c.Logging.Format)
|
||||
}
|
||||
|
||||
validOutputs := map[string]bool{
|
||||
"stdout": true,
|
||||
"stderr": true,
|
||||
"file": true,
|
||||
}
|
||||
if !validOutputs[c.Logging.Output] {
|
||||
return fmt.Errorf("log output must be one of: stdout, stderr, file, got: %s", c.Logging.Output)
|
||||
}
|
||||
|
||||
if c.Logging.Output == "file" && c.Logging.File == "" {
|
||||
return fmt.Errorf("log file path must be specified when output is file")
|
||||
}
|
||||
|
||||
// 验证安全配置
|
||||
if c.Security.Timeout < 1 {
|
||||
return fmt.Errorf("timeout must be positive, got: %d", c.Security.Timeout)
|
||||
}
|
||||
|
||||
if c.Security.RateLimit < 0 {
|
||||
return fmt.Errorf("rate limit must be non-negative, got: %d", c.Security.RateLimit)
|
||||
}
|
||||
|
||||
// 验证转换配置
|
||||
if !contains(c.Conversion.SupportedTargets, c.Conversion.DefaultTarget) {
|
||||
return fmt.Errorf("default target must be in supported targets, got: %s", c.Conversion.DefaultTarget)
|
||||
}
|
||||
|
||||
if c.Conversion.MaxNodes < 0 {
|
||||
return fmt.Errorf("max nodes must be non-negative, got: %d", c.Conversion.MaxNodes)
|
||||
}
|
||||
|
||||
if c.Conversion.CacheTimeout < 0 {
|
||||
return fmt.Errorf("cache timeout must be non-negative, got: %d", c.Conversion.CacheTimeout)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clone 创建配置的深拷贝
|
||||
// 返回配置的独立副本
|
||||
func (c *Configuration) Clone() *Configuration {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
clone := &Configuration{
|
||||
Server: c.Server.Clone(),
|
||||
Logging: c.Logging.Clone(),
|
||||
Security: c.Security.Clone(),
|
||||
Conversion: c.Conversion.Clone(),
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
// LoadFromEnvironment 从环境变量加载配置
|
||||
// 根据环境变量覆盖默认配置值
|
||||
func (c *Configuration) LoadFromEnvironment() {
|
||||
// 服务器配置
|
||||
if port := os.Getenv("SUBCONVERTER_PORT"); port != "" {
|
||||
if p, err := strconv.Atoi(port); err == nil {
|
||||
c.Server.Port = p
|
||||
}
|
||||
}
|
||||
|
||||
if host := os.Getenv("SUBCONVERTER_HOST"); host != "" {
|
||||
c.Server.Host = host
|
||||
}
|
||||
|
||||
if readTimeout := os.Getenv("SUBCONVERTER_READ_TIMEOUT"); readTimeout != "" {
|
||||
if rt, err := strconv.Atoi(readTimeout); err == nil {
|
||||
c.Server.ReadTimeout = rt
|
||||
}
|
||||
}
|
||||
|
||||
if writeTimeout := os.Getenv("SUBCONVERTER_WRITE_TIMEOUT"); writeTimeout != "" {
|
||||
if wt, err := strconv.Atoi(writeTimeout); err == nil {
|
||||
c.Server.WriteTimeout = wt
|
||||
}
|
||||
}
|
||||
|
||||
// 日志配置
|
||||
if logLevel := os.Getenv("SUBCONVERTER_LOG_LEVEL"); logLevel != "" {
|
||||
c.Logging.Level = logLevel
|
||||
}
|
||||
|
||||
if logFormat := os.Getenv("SUBCONVERTER_LOG_FORMAT"); logFormat != "" {
|
||||
c.Logging.Format = logFormat
|
||||
}
|
||||
|
||||
if logOutput := os.Getenv("SUBCONVERTER_LOG_OUTPUT"); logOutput != "" {
|
||||
c.Logging.Output = logOutput
|
||||
}
|
||||
|
||||
if logFile := os.Getenv("SUBCONVERTER_LOG_FILE"); logFile != "" {
|
||||
c.Logging.File = logFile
|
||||
}
|
||||
|
||||
// 安全配置
|
||||
if accessTokens := os.Getenv("SUBCONVERTER_ACCESS_TOKENS"); accessTokens != "" {
|
||||
c.Security.AccessTokens = strings.Split(accessTokens, ",")
|
||||
}
|
||||
|
||||
if corsOrigins := os.Getenv("SUBCONVERTER_CORS_ORIGINS"); corsOrigins != "" {
|
||||
c.Security.CorsOrigins = strings.Split(corsOrigins, ",")
|
||||
}
|
||||
|
||||
if rateLimit := os.Getenv("SUBCONVERTER_RATE_LIMIT"); rateLimit != "" {
|
||||
if rl, err := strconv.Atoi(rateLimit); err == nil {
|
||||
c.Security.RateLimit = rl
|
||||
}
|
||||
}
|
||||
|
||||
if timeout := os.Getenv("SUBCONVERTER_TIMEOUT"); timeout != "" {
|
||||
if t, err := strconv.Atoi(timeout); err == nil {
|
||||
c.Security.Timeout = t
|
||||
}
|
||||
}
|
||||
|
||||
// 转换配置
|
||||
if defaultTarget := os.Getenv("SUBCONVERTER_DEFAULT_TARGET"); defaultTarget != "" {
|
||||
c.Conversion.DefaultTarget = defaultTarget
|
||||
}
|
||||
|
||||
if defaultEmoji := os.Getenv("SUBCONVERTER_DEFAULT_EMOJI"); defaultEmoji != "" {
|
||||
if de, err := strconv.ParseBool(defaultEmoji); err == nil {
|
||||
c.Conversion.DefaultEmoji = de
|
||||
}
|
||||
}
|
||||
|
||||
if defaultUDP := os.Getenv("SUBCONVERTER_DEFAULT_UDP"); defaultUDP != "" {
|
||||
if du, err := strconv.ParseBool(defaultUDP); err == nil {
|
||||
c.Conversion.DefaultUDP = du
|
||||
}
|
||||
}
|
||||
|
||||
if maxNodes := os.Getenv("SUBCONVERTER_MAX_NODES"); maxNodes != "" {
|
||||
if mn, err := strconv.Atoi(maxNodes); err == nil {
|
||||
c.Conversion.MaxNodes = mn
|
||||
}
|
||||
}
|
||||
|
||||
if cacheTimeout := os.Getenv("SUBCONVERTER_CACHE_TIMEOUT"); cacheTimeout != "" {
|
||||
if ct, err := strconv.Atoi(cacheTimeout); err == nil {
|
||||
c.Conversion.CacheTimeout = ct
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetServerAddress 返回服务器地址字符串
|
||||
// 格式为 "host:port"
|
||||
func (c *Configuration) GetServerAddress() string {
|
||||
return fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port)
|
||||
}
|
||||
|
||||
// GetReadTimeoutDuration 返回读取超时时间duration
|
||||
func (c *Configuration) GetReadTimeoutDuration() time.Duration {
|
||||
return time.Duration(c.Server.ReadTimeout) * time.Second
|
||||
}
|
||||
|
||||
// GetWriteTimeoutDuration 返回写入超时时间duration
|
||||
func (c *Configuration) GetWriteTimeoutDuration() time.Duration {
|
||||
return time.Duration(c.Server.WriteTimeout) * time.Second
|
||||
}
|
||||
|
||||
// GetTimeoutDuration 返回请求超时时间duration
|
||||
func (c *Configuration) GetTimeoutDuration() time.Duration {
|
||||
return time.Duration(c.Security.Timeout) * time.Second
|
||||
}
|
||||
|
||||
// GetCacheTimeoutDuration 返回缓存超时时间duration
|
||||
func (c *Configuration) GetCacheTimeoutDuration() time.Duration {
|
||||
return time.Duration(c.Conversion.CacheTimeout) * time.Minute
|
||||
}
|
||||
|
||||
// IsAccessTokenValid 检查访问令牌是否有效
|
||||
func (c *Configuration) IsAccessTokenValid(token string) bool {
|
||||
if len(c.Security.AccessTokens) == 0 {
|
||||
return true // 空列表表示无需认证
|
||||
}
|
||||
return contains(c.Security.AccessTokens, token)
|
||||
}
|
||||
|
||||
// IsCorsOriginAllowed 检查CORS源是否允许
|
||||
func (c *Configuration) IsCorsOriginAllowed(origin string) bool {
|
||||
for _, allowed := range c.Security.CorsOrigins {
|
||||
if allowed == "*" || allowed == origin {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsTargetSupported 检查目标格式是否支持
|
||||
func (c *Configuration) IsTargetSupported(target string) bool {
|
||||
return contains(c.Conversion.SupportedTargets, target)
|
||||
}
|
||||
|
||||
// contains 检查字符串切片是否包含特定字符串
|
||||
func contains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
511
internal/config/proxy.go
Normal file
511
internal/config/proxy.go
Normal file
@@ -0,0 +1,511 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ProxyConfig 表示代理节点的配置
|
||||
// 该结构体包含各种代理协议的通用和特定配置
|
||||
type ProxyConfig struct {
|
||||
// 基本信息
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Type string `yaml:"type" json:"type"` // ss, ssr, vmess, trojan, http, socks5
|
||||
|
||||
// 服务器信息
|
||||
Server string `yaml:"server" json:"server"`
|
||||
Port int `yaml:"port" json:"port"`
|
||||
Password string `yaml:"password" json:"password,omitempty"`
|
||||
|
||||
// 加密和协议
|
||||
Method string `yaml:"method" json:"method,omitempty"` // ss, ssr
|
||||
Protocol string `yaml:"protocol" json:"protocol,omitempty"` // ssr
|
||||
Obfs string `yaml:"obfs" json:"obfs,omitempty"` // ssr
|
||||
ObfsParam string `yaml:"obfs_param" json:"obfs_param,omitempty"` // ssr
|
||||
SSRProtocol string `yaml:"ssr_protocol" json:"ssr_protocol,omitempty"` // ssr (兼容性字段)
|
||||
|
||||
// VMess特定配置
|
||||
VMessConfig *VMessConfig `yaml:"vmess_config,omitempty" json:"vmess_config,omitempty"`
|
||||
|
||||
// TLS配置
|
||||
TLS bool `yaml:"tls" json:"tls"`
|
||||
SNI string `yaml:"sni" json:"sni,omitempty"`
|
||||
Alpn string `yaml:"alpn" json:"alpn,omitempty"`
|
||||
SkipVerify bool `yaml:"skip_verify" json:"skip_verify"`
|
||||
|
||||
// WebSocket配置
|
||||
WSPath string `yaml:"ws_path" json:"ws_path,omitempty"`
|
||||
WSHeaders map[string]string `yaml:"ws_headers" json:"ws_headers,omitempty"`
|
||||
|
||||
// HTTP/2配置
|
||||
H2Host string `yaml:"h2_host" json:"h2_host,omitempty"`
|
||||
H2Path string `yaml:"h2_path" json:"h2_path,omitempty"`
|
||||
|
||||
// QUIC配置
|
||||
QUICSecurity string `yaml:"quic_security" json:"quic_security,omitempty"`
|
||||
QUICKey string `yaml:"quic_key" json:"quic_key,omitempty"`
|
||||
|
||||
// gRPC配置
|
||||
GRPCServiceName string `yaml:"grpc_service_name" json:"grpc_service_name,omitempty"`
|
||||
GRPCMultiMode bool `yaml:"grpc_multi_mode" json:"grpc_multi_mode,omitempty"`
|
||||
|
||||
// 其他配置
|
||||
UDP bool `yaml:"udp" json:"udp"`
|
||||
TFO bool `yaml:"tfo" json:"tfo"` // TCP Fast Open
|
||||
Mux bool `yaml:"mux" json:"mux"` // 多路复用
|
||||
Scramble bool `yaml:"scramble" json:"scramble"`
|
||||
Plugin string `yaml:"plugin" json:"plugin,omitempty"`
|
||||
PluginOpts string `yaml:"plugin_opts" json:"plugin_opts,omitempty"`
|
||||
|
||||
// 节点属性
|
||||
Location string `yaml:"location" json:"location,omitempty"`
|
||||
Provider string `yaml:"provider" json:"provider,omitempty"`
|
||||
Ping int `yaml:"ping" json:"ping,omitempty"` // 延迟(ms)
|
||||
}
|
||||
|
||||
// VMessConfig VMess协议特定配置
|
||||
type VMessConfig struct {
|
||||
VMessUUID string `yaml:"vmess_uuid" json:"vmess_uuid"`
|
||||
VMessAlterID string `yaml:"vmess_alter_id" json:"vmess_alter_id"`
|
||||
VMessSecurity string `yaml:"vmess_security" json:"vmess_security"`
|
||||
}
|
||||
|
||||
// NewProxyConfig 创建新的代理配置
|
||||
// 返回包含基本默认值的ProxyConfig结构体
|
||||
func NewProxyConfig() *ProxyConfig {
|
||||
return &ProxyConfig{
|
||||
Name: "",
|
||||
Type: "",
|
||||
Server: "",
|
||||
Port: 0,
|
||||
Password: "",
|
||||
Method: "",
|
||||
Protocol: "",
|
||||
Obfs: "",
|
||||
ObfsParam: "",
|
||||
TLS: false,
|
||||
SNI: "",
|
||||
Alpn: "",
|
||||
SkipVerify: false,
|
||||
WSPath: "",
|
||||
WSHeaders: make(map[string]string),
|
||||
H2Host: "",
|
||||
H2Path: "",
|
||||
QUICSecurity: "",
|
||||
QUICKey: "",
|
||||
GRPCServiceName: "",
|
||||
GRPCMultiMode: false,
|
||||
UDP: false,
|
||||
TFO: false,
|
||||
Mux: false,
|
||||
Scramble: false,
|
||||
Plugin: "",
|
||||
PluginOpts: "",
|
||||
Location: "",
|
||||
Provider: "",
|
||||
Ping: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate 验证代理配置的有效性
|
||||
// 返回error如果配置无效,nil表示配置有效
|
||||
func (p *ProxyConfig) Validate() error {
|
||||
// 基本字段验证
|
||||
if p.Name == "" {
|
||||
return fmt.Errorf("proxy name cannot be empty")
|
||||
}
|
||||
|
||||
if p.Type == "" {
|
||||
return fmt.Errorf("proxy type cannot be empty")
|
||||
}
|
||||
|
||||
if p.Server == "" {
|
||||
return fmt.Errorf("proxy server cannot be empty")
|
||||
}
|
||||
|
||||
if p.Port < 1 || p.Port > 65535 {
|
||||
return fmt.Errorf("proxy port must be between 1 and 65535, got: %d", p.Port)
|
||||
}
|
||||
|
||||
// 根据类型验证特定字段
|
||||
switch strings.ToLower(p.Type) {
|
||||
case "ss", "shadowsocks":
|
||||
if p.Password == "" {
|
||||
return fmt.Errorf("shadowsocks password cannot be empty")
|
||||
}
|
||||
if p.Method == "" {
|
||||
return fmt.Errorf("shadowsocks method cannot be empty")
|
||||
}
|
||||
|
||||
case "ssr", "shadowsocksr":
|
||||
if p.Password == "" {
|
||||
return fmt.Errorf("shadowsocksr password cannot be empty")
|
||||
}
|
||||
if p.Method == "" {
|
||||
return fmt.Errorf("shadowsocksr method cannot be empty")
|
||||
}
|
||||
if p.Protocol == "" {
|
||||
return fmt.Errorf("shadowsocksr protocol cannot be empty")
|
||||
}
|
||||
|
||||
case "vmess":
|
||||
if p.VMessConfig == nil || p.VMessConfig.VMessUUID == "" {
|
||||
return fmt.Errorf("vmess uuid cannot be empty")
|
||||
}
|
||||
if p.VMessConfig.VMessAlterID == "" {
|
||||
return fmt.Errorf("vmess alter id cannot be empty")
|
||||
}
|
||||
if p.VMessConfig.VMessSecurity == "" {
|
||||
return fmt.Errorf("vmess security cannot be empty")
|
||||
}
|
||||
|
||||
case "trojan":
|
||||
if p.Password == "" {
|
||||
return fmt.Errorf("trojan password cannot be empty")
|
||||
}
|
||||
|
||||
case "http", "https":
|
||||
if p.Password == "" && p.Type == "https" {
|
||||
return fmt.Errorf("https proxy requires authentication")
|
||||
}
|
||||
|
||||
case "socks5":
|
||||
// SOCKS5可以无认证
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported proxy type: %s", p.Type)
|
||||
}
|
||||
|
||||
// TLS配置验证
|
||||
if p.TLS {
|
||||
if p.SNI == "" {
|
||||
p.SNI = p.Server // 默认使用服务器地址作为SNI
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket配置验证
|
||||
if p.WSPath != "" && p.WSHeaders == nil {
|
||||
p.WSHeaders = make(map[string]string)
|
||||
}
|
||||
|
||||
// HTTP/2配置验证
|
||||
if p.H2Path != "" && p.H2Host == "" {
|
||||
return fmt.Errorf("h2 host cannot be empty when h2 path is specified")
|
||||
}
|
||||
|
||||
// QUIC配置验证
|
||||
if p.QUICKey != "" && p.QUICSecurity == "" {
|
||||
return fmt.Errorf("quic security cannot be empty when quic key is specified")
|
||||
}
|
||||
|
||||
// gRPC配置验证
|
||||
if p.GRPCServiceName != "" && !p.TLS {
|
||||
return fmt.Errorf("grpc requires tls to be enabled")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToURL 将代理配置转换为URL格式
|
||||
// 返回代理URL字符串,适用于分享和导入
|
||||
func (p *ProxyConfig) ToURL() (string, error) {
|
||||
switch strings.ToLower(p.Type) {
|
||||
case "ss", "shadowsocks":
|
||||
return p.toSSURL()
|
||||
case "ssr", "shadowsocksr":
|
||||
return p.toSSRURL()
|
||||
case "vmess":
|
||||
return p.toVMessURL()
|
||||
case "trojan":
|
||||
return p.toTrojanURL()
|
||||
case "http", "https":
|
||||
return p.toHTTPURL()
|
||||
case "socks5":
|
||||
return p.toSocks5URL()
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported proxy type: %s", p.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// FromURL 从URL解析代理配置
|
||||
// 根据URL类型自动识别并解析代理配置
|
||||
func (p *ProxyConfig) FromURL(proxyURL string) error {
|
||||
u, err := url.Parse(proxyURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse proxy URL: %v", err)
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "ss":
|
||||
return p.fromSSURL(u)
|
||||
case "ssr":
|
||||
return p.fromSSRURL(u)
|
||||
case "vmess":
|
||||
return p.fromVMessURL(u)
|
||||
case "trojan":
|
||||
return p.fromTrojanURL(u)
|
||||
case "http", "https":
|
||||
return p.fromHTTPURL(u)
|
||||
case "socks5":
|
||||
return p.fromSocks5URL(u)
|
||||
default:
|
||||
return fmt.Errorf("unsupported proxy URL scheme: %s", u.Scheme)
|
||||
}
|
||||
}
|
||||
|
||||
// toSSURL 转换为SS URL格式
|
||||
func (p *ProxyConfig) toSSURL() (string, error) {
|
||||
if p.Type != "ss" && p.Type != "shadowsocks" {
|
||||
return "", fmt.Errorf("not a shadowsocks proxy")
|
||||
}
|
||||
|
||||
// SS URL格式: ss://method:password@server:port#name
|
||||
host := fmt.Sprintf("%s:%d", p.Server, p.Port)
|
||||
|
||||
u := &url.URL{
|
||||
Scheme: "ss",
|
||||
User: url.UserPassword(p.Method, p.Password),
|
||||
Host: host,
|
||||
}
|
||||
|
||||
if p.Name != "" {
|
||||
u.Fragment = p.Name
|
||||
}
|
||||
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
// toSSRURL 转换为SSR URL格式
|
||||
func (p *ProxyConfig) toSSRURL() (string, error) {
|
||||
if p.Type != "ssr" && p.Type != "shadowsocksr" {
|
||||
return "", fmt.Errorf("not a shadowsocksr proxy")
|
||||
}
|
||||
|
||||
// SSR URL格式: ssr://base64(server:port:protocol:method:obfs:password/?obfsparam=...&protoparam=...&remarks=...)
|
||||
params := make(url.Values)
|
||||
params.Set("obfsparam", p.ObfsParam)
|
||||
params.Set("protoparam", "")
|
||||
params.Set("remarks", p.Name)
|
||||
params.Set("group", "")
|
||||
params.Set("udpport", "0")
|
||||
params.Set("uot", "0")
|
||||
|
||||
baseInfo := fmt.Sprintf("%s:%d:%s:%s:%s:%s",
|
||||
p.Server, p.Port, p.Protocol, p.Method, p.Obfs, p.Password)
|
||||
|
||||
encodedInfo := url.QueryEscape(base64Encode(baseInfo))
|
||||
encodedParams := url.QueryEscape(params.Encode())
|
||||
|
||||
return fmt.Sprintf("ssr://%s/?%s", encodedInfo, encodedParams), nil
|
||||
}
|
||||
|
||||
// toVMessURL 转换为VMess URL格式
|
||||
func (p *ProxyConfig) toVMessURL() (string, error) {
|
||||
if p.Type != "vmess" {
|
||||
return "", fmt.Errorf("not a vmess proxy")
|
||||
}
|
||||
|
||||
// VMess URL格式: vmess://base64(json_config)
|
||||
config := map[string]interface{}{
|
||||
"v": "2",
|
||||
"ps": p.Name,
|
||||
"add": p.Server,
|
||||
"port": p.Port,
|
||||
"id": p.VMessConfig.VMessUUID,
|
||||
"aid": p.VMessConfig.VMessAlterID,
|
||||
"net": "tcp", // 简化版本
|
||||
"type": "none",
|
||||
"host": "",
|
||||
"path": "",
|
||||
"tls": "",
|
||||
}
|
||||
|
||||
if p.TLS {
|
||||
config["tls"] = "tls"
|
||||
}
|
||||
|
||||
configJSON, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal vmess config: %v", err)
|
||||
}
|
||||
|
||||
return "vmess://" + base64Encode(string(configJSON)), nil
|
||||
}
|
||||
|
||||
// toTrojanURL 转换为Trojan URL格式
|
||||
func (p *ProxyConfig) toTrojanURL() (string, error) {
|
||||
if p.Type != "trojan" {
|
||||
return "", fmt.Errorf("not a trojan proxy")
|
||||
}
|
||||
|
||||
// Trojan URL格式: trojan://password@server:port?allowinsecure=1#name
|
||||
u := &url.URL{
|
||||
Scheme: "trojan",
|
||||
User: url.UserPassword(p.Password, ""),
|
||||
Host: fmt.Sprintf("%s:%d", p.Server, p.Port),
|
||||
}
|
||||
|
||||
params := u.Query()
|
||||
if p.SkipVerify {
|
||||
params.Set("allowinsecure", "1")
|
||||
}
|
||||
if p.SNI != "" {
|
||||
params.Set("sni", p.SNI)
|
||||
}
|
||||
u.RawQuery = params.Encode()
|
||||
|
||||
if p.Name != "" {
|
||||
u.Fragment = p.Name
|
||||
}
|
||||
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
// toHTTPURL 转换为HTTP/HTTPS URL格式
|
||||
func (p *ProxyConfig) toHTTPURL() (string, error) {
|
||||
if p.Type != "http" && p.Type != "https" {
|
||||
return "", fmt.Errorf("not an http proxy")
|
||||
}
|
||||
|
||||
scheme := "http"
|
||||
if p.Type == "https" {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
u := &url.URL{
|
||||
Scheme: scheme,
|
||||
Host: fmt.Sprintf("%s:%d", p.Server, p.Port),
|
||||
}
|
||||
|
||||
if p.Password != "" {
|
||||
u.User = url.UserPassword("", p.Password)
|
||||
}
|
||||
|
||||
if p.Name != "" {
|
||||
u.Fragment = p.Name
|
||||
}
|
||||
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
// toSocks5URL 转换为SOCKS5 URL格式
|
||||
func (p *ProxyConfig) toSocks5URL() (string, error) {
|
||||
if p.Type != "socks5" {
|
||||
return "", fmt.Errorf("not a socks5 proxy")
|
||||
}
|
||||
|
||||
u := &url.URL{
|
||||
Scheme: "socks5",
|
||||
Host: fmt.Sprintf("%s:%d", p.Server, p.Port),
|
||||
}
|
||||
|
||||
if p.Password != "" {
|
||||
u.User = url.UserPassword("", p.Password)
|
||||
}
|
||||
|
||||
if p.Name != "" {
|
||||
u.Fragment = p.Name
|
||||
}
|
||||
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
// 以下是FromURL的各种实现方法,由于篇幅限制,这里只提供框架
|
||||
func (p *ProxyConfig) fromSSURL(u *url.URL) error {
|
||||
// 解析SS URL的实现
|
||||
p.Type = "ss"
|
||||
p.Server = u.Hostname()
|
||||
if port, err := strconv.Atoi(u.Port()); err == nil {
|
||||
p.Port = port
|
||||
}
|
||||
p.Name = u.Fragment
|
||||
|
||||
if u.User != nil {
|
||||
p.Method = u.User.Username()
|
||||
if password, ok := u.User.Password(); ok {
|
||||
p.Password = password
|
||||
}
|
||||
}
|
||||
|
||||
return p.Validate()
|
||||
}
|
||||
|
||||
func (p *ProxyConfig) fromSSRURL(u *url.URL) error {
|
||||
// 解析SSR URL的实现
|
||||
p.Type = "ssr"
|
||||
// 实现SSR URL解析逻辑
|
||||
return p.Validate()
|
||||
}
|
||||
|
||||
func (p *ProxyConfig) fromVMessURL(u *url.URL) error {
|
||||
// 解析VMess URL的实现
|
||||
p.Type = "vmess"
|
||||
// 实现VMess URL解析逻辑
|
||||
return p.Validate()
|
||||
}
|
||||
|
||||
func (p *ProxyConfig) fromTrojanURL(u *url.URL) error {
|
||||
// 解析Trojan URL的实现
|
||||
p.Type = "trojan"
|
||||
p.Server = u.Hostname()
|
||||
if port, err := strconv.Atoi(u.Port()); err == nil {
|
||||
p.Port = port
|
||||
}
|
||||
p.Name = u.Fragment
|
||||
|
||||
if u.User != nil {
|
||||
p.Password = u.User.Username()
|
||||
}
|
||||
|
||||
return p.Validate()
|
||||
}
|
||||
|
||||
func (p *ProxyConfig) fromHTTPURL(u *url.URL) error {
|
||||
// 解析HTTP URL的实现
|
||||
p.Type = u.Scheme
|
||||
p.Server = u.Hostname()
|
||||
if port, err := strconv.Atoi(u.Port()); err == nil {
|
||||
p.Port = port
|
||||
}
|
||||
p.Name = u.Fragment
|
||||
|
||||
if u.User != nil {
|
||||
if password, ok := u.User.Password(); ok {
|
||||
p.Password = password
|
||||
}
|
||||
}
|
||||
|
||||
return p.Validate()
|
||||
}
|
||||
|
||||
func (p *ProxyConfig) fromSocks5URL(u *url.URL) error {
|
||||
// 解析SOCKS5 URL的实现
|
||||
p.Type = "socks5"
|
||||
p.Server = u.Hostname()
|
||||
if port, err := strconv.Atoi(u.Port()); err == nil {
|
||||
p.Port = port
|
||||
}
|
||||
p.Name = u.Fragment
|
||||
|
||||
if u.User != nil {
|
||||
if password, ok := u.User.Password(); ok {
|
||||
p.Password = password
|
||||
}
|
||||
}
|
||||
|
||||
return p.Validate()
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
func base64Encode(s string) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(s))
|
||||
}
|
||||
|
||||
func base64Decode(s string) (string, error) {
|
||||
data, err := base64.StdEncoding.DecodeString(s)
|
||||
return string(data), err
|
||||
}
|
||||
599
internal/config/template_manager.go
Normal file
599
internal/config/template_manager.go
Normal file
@@ -0,0 +1,599 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/subconverter-go/internal/logging"
|
||||
)
|
||||
|
||||
// TemplateManager 配置模板管理器
|
||||
type TemplateManager struct {
|
||||
logger *logging.Logger
|
||||
basePath string
|
||||
templates map[string]*template.Template
|
||||
configs map[string]interface{}
|
||||
rulesets map[string]string
|
||||
snippets map[string]string
|
||||
profileConfig map[string]interface{}
|
||||
}
|
||||
|
||||
// TemplateConfig 模板配置
|
||||
type TemplateConfig struct {
|
||||
Clash ClashConfig `yaml:"clash"`
|
||||
Surge SurgeConfig `yaml:"surge"`
|
||||
QuantumultX QuantumultXConfig `yaml:"quantumultx"`
|
||||
Loon LoonConfig `yaml:"loon"`
|
||||
Surfboard SurfboardConfig `yaml:"surfboard"`
|
||||
V2Ray V2RayConfig `yaml:"v2ray"`
|
||||
}
|
||||
|
||||
// ClashConfig Clash模板配置
|
||||
type ClashConfig struct {
|
||||
HTTPPort int `yaml:"http_port"`
|
||||
SocksPort int `yaml:"socks_port"`
|
||||
AllowLAN bool `yaml:"allow_lan"`
|
||||
LogLevel string `yaml:"log_level"`
|
||||
ExternalController string `yaml:"external_controller"`
|
||||
}
|
||||
|
||||
// SurgeConfig Surge模板配置
|
||||
type SurgeConfig struct {
|
||||
Port int `yaml:"port"`
|
||||
ProxyHTTPPort int `yaml:"proxy_http_port"`
|
||||
ProxySOCKS5Port int `yaml:"proxy_socks5_port"`
|
||||
MixedPort int `yaml:"mixed_port"`
|
||||
AllowLAN bool `yaml:"allow_lan"`
|
||||
LogLevel string `yaml:"log_level"`
|
||||
}
|
||||
|
||||
// QuantumultXConfig QuantumultX模板配置
|
||||
type QuantumultXConfig struct {
|
||||
Port int `yaml:"port"`
|
||||
AllowLAN bool `yaml:"allow_lan"`
|
||||
LogLevel string `yaml:"log_level"`
|
||||
}
|
||||
|
||||
// LoonConfig Loon模板配置
|
||||
type LoonConfig struct {
|
||||
Port int `yaml:"port"`
|
||||
AllowLAN bool `yaml:"allow_lan"`
|
||||
LogLevel string `yaml:"log_level"`
|
||||
}
|
||||
|
||||
// SurfboardConfig Surfboard模板配置
|
||||
type SurfboardConfig struct {
|
||||
Port int `yaml:"port"`
|
||||
AllowLAN bool `yaml:"allow_lan"`
|
||||
LogLevel string `yaml:"log_level"`
|
||||
}
|
||||
|
||||
// V2RayConfig V2Ray模板配置
|
||||
type V2RayConfig struct {
|
||||
AllowLAN bool `yaml:"allow_lan"`
|
||||
MixedPort int `yaml:"mixed_port"`
|
||||
LogLevel string `yaml:"log_level"`
|
||||
}
|
||||
|
||||
// TemplateVariables 模板变量
|
||||
type TemplateVariables struct {
|
||||
Global TemplateConfig
|
||||
NodeInfo interface{}
|
||||
GroupName string
|
||||
UpdateTime string
|
||||
UserInfo string
|
||||
TotalNodes int
|
||||
SelectedRule string
|
||||
Request RequestConfig
|
||||
Local LocalConfig
|
||||
}
|
||||
|
||||
// RequestConfig 请求配置
|
||||
type RequestConfig struct {
|
||||
Target string `yaml:"target"`
|
||||
Clash map[string]interface{} `yaml:"clash"`
|
||||
Surge map[string]interface{} `yaml:"surge"`
|
||||
}
|
||||
|
||||
// LocalConfig 本地配置
|
||||
type LocalConfig struct {
|
||||
Clash map[string]interface{} `yaml:"clash"`
|
||||
Surge map[string]interface{} `yaml:"surge"`
|
||||
}
|
||||
|
||||
// NewTemplateManager 创建模板管理器
|
||||
func NewTemplateManager(basePath string, logger *logging.Logger) (*TemplateManager, error) {
|
||||
tm := &TemplateManager{
|
||||
logger: logger,
|
||||
basePath: basePath,
|
||||
templates: make(map[string]*template.Template),
|
||||
configs: make(map[string]interface{}),
|
||||
rulesets: make(map[string]string),
|
||||
snippets: make(map[string]string),
|
||||
profileConfig: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
if err := tm.loadTemplates(); err != nil {
|
||||
return nil, fmt.Errorf("failed to load templates: %w", err)
|
||||
}
|
||||
|
||||
if err := tm.loadRulesets(); err != nil {
|
||||
return nil, fmt.Errorf("failed to load rulesets: %w", err)
|
||||
}
|
||||
|
||||
if err := tm.loadSnippets(); err != nil {
|
||||
return nil, fmt.Errorf("failed to load snippets: %w", err)
|
||||
}
|
||||
|
||||
if err := tm.loadProfileConfigs(); err != nil {
|
||||
return nil, fmt.Errorf("failed to load profile configs: %w", err)
|
||||
}
|
||||
|
||||
return tm, nil
|
||||
}
|
||||
|
||||
// loadTemplates 加载配置模板
|
||||
func (tm *TemplateManager) loadTemplates() error {
|
||||
baseDir := filepath.Join(tm.basePath, "base")
|
||||
|
||||
// 遍历base目录加载模板
|
||||
return filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 只处理支持的模板文件
|
||||
ext := filepath.Ext(path)
|
||||
if !tm.isTemplateFile(ext) {
|
||||
return nil
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read template file %s: %w", path, err)
|
||||
}
|
||||
|
||||
// 转换Jinja2语法到Go模板语法
|
||||
convertedContent := tm.convertJinja2ToGoTemplate(string(content))
|
||||
|
||||
// 创建模板
|
||||
name := tm.getTemplateName(path, baseDir)
|
||||
tmpl := template.New(name).Funcs(template.FuncMap{
|
||||
"default": tm.defaultFunc,
|
||||
"or": tm.orFunc,
|
||||
"eq": tm.eqFunc,
|
||||
})
|
||||
tmpl, err = tmpl.Parse(convertedContent)
|
||||
if err != nil {
|
||||
// 如果模板解析失败,记录警告但跳过这个模板
|
||||
tm.logger.Warnf("Failed to parse template %s: %v, skipping", name, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
tm.templates[name] = tmpl
|
||||
tm.logger.Infof("Loaded template: %s", name)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// loadRulesets 加载规则集
|
||||
func (tm *TemplateManager) loadRulesets() error {
|
||||
rulesDir := filepath.Join(tm.basePath, "rules")
|
||||
|
||||
return filepath.Walk(rulesDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 只处理规则文件
|
||||
ext := filepath.Ext(path)
|
||||
if !tm.isRulesetFile(ext) {
|
||||
return nil
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read ruleset file %s: %w", path, err)
|
||||
}
|
||||
|
||||
name := tm.getRulesetName(path, rulesDir)
|
||||
tm.rulesets[name] = string(content)
|
||||
tm.logger.Infof("Loaded ruleset: %s", name)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// loadSnippets 加载代码片段
|
||||
func (tm *TemplateManager) loadSnippets() error {
|
||||
snippetsDir := filepath.Join(tm.basePath, "snippets")
|
||||
|
||||
entries, err := os.ReadDir(snippetsDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(snippetsDir, entry.Name()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read snippet file %s: %w", entry.Name(), err)
|
||||
}
|
||||
|
||||
tm.snippets[entry.Name()] = string(content)
|
||||
tm.logger.Infof("Loaded snippet: %s", entry.Name())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadProfileConfigs 加载配置文件
|
||||
func (tm *TemplateManager) loadProfileConfigs() error {
|
||||
configDir := filepath.Join(tm.basePath, "config")
|
||||
|
||||
entries, err := os.ReadDir(configDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
// 加载.ini配置文件
|
||||
if strings.HasSuffix(entry.Name(), ".ini") {
|
||||
content, err := os.ReadFile(filepath.Join(configDir, entry.Name()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read config file %s: %w", entry.Name(), err)
|
||||
}
|
||||
|
||||
// 这里简化处理,实际应该解析INI文件
|
||||
tm.profileConfig[entry.Name()] = string(content)
|
||||
tm.logger.Infof("Loaded profile config: %s", entry.Name())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertJinja2ToGoTemplate 转换Jinja2语法到Go模板语法
|
||||
func (tm *TemplateManager) convertJinja2ToGoTemplate(content string) string {
|
||||
converted := content
|
||||
|
||||
// 转换 if 条件语句
|
||||
converted = strings.ReplaceAll(converted, "{% if ", "{{ if ")
|
||||
converted = strings.ReplaceAll(converted, "{% endif %", "{{ end }}")
|
||||
converted = strings.ReplaceAll(converted, "{% else %", "{{ else }}")
|
||||
|
||||
// 转换字符串比较 - 将 == 转换为 eq 函数调用
|
||||
// 这种转换比较复杂,我们只在特定的模式中转换
|
||||
converted = strings.ReplaceAll(converted, ".Request.Target == \"clash\"", "eq .Request.Target \"clash\"")
|
||||
converted = strings.ReplaceAll(converted, ".Request.Target == \"clashr\"", "eq .Request.Target \"clashr\"")
|
||||
converted = strings.ReplaceAll(converted, " == \"1\"", " eq \"1\"")
|
||||
converted = strings.ReplaceAll(converted, " == \"true\"", " eq \"true\"")
|
||||
|
||||
// 转换 or 运算符为 or 函数调用
|
||||
converted = strings.ReplaceAll(converted, " or ", " | ")
|
||||
|
||||
// 转换变量引用,比如 request.target 到 .Request.Target
|
||||
converted = strings.ReplaceAll(converted, "request.target", ".Request.Target")
|
||||
converted = strings.ReplaceAll(converted, "request.clash.", ".Request.Clash.")
|
||||
converted = strings.ReplaceAll(converted, "request.surge.", ".Request.Surge.")
|
||||
converted = strings.ReplaceAll(converted, "local.clash.", ".Local.Clash.")
|
||||
converted = strings.ReplaceAll(converted, "local.surge.", ".Local.Surge.")
|
||||
converted = strings.ReplaceAll(converted, "global.clash.", ".Global.Clash.")
|
||||
converted = strings.ReplaceAll(converted, "global.surge.", ".Global.Surge.")
|
||||
|
||||
// 转换 default 函数调用
|
||||
converted = strings.ReplaceAll(converted, "default(", "default ")
|
||||
|
||||
return converted
|
||||
}
|
||||
|
||||
// defaultFunc 实现default函数
|
||||
func (tm *TemplateManager) defaultFunc(value, defaultValue interface{}) interface{} {
|
||||
if value == nil {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// 检查字符串是否为空
|
||||
if str, ok := value.(string); ok {
|
||||
if str == "" {
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
// 检查数字是否为0
|
||||
if num, ok := value.(int); ok {
|
||||
if num == 0 {
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
// 检查布尔值是否为false
|
||||
if b, ok := value.(bool); ok {
|
||||
if !b {
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// orFunc 实现or函数
|
||||
func (tm *TemplateManager) orFunc(args ...interface{}) interface{} {
|
||||
for _, arg := range args {
|
||||
if tm.isTrue(arg) {
|
||||
return arg
|
||||
}
|
||||
}
|
||||
return args[len(args)-1] // 返回最后一个参数
|
||||
}
|
||||
|
||||
// eqFunc 实现eq函数
|
||||
func (tm *TemplateManager) eqFunc(a, b interface{}) bool {
|
||||
return a == b
|
||||
}
|
||||
|
||||
// isTrue 检查值是否为真
|
||||
func (tm *TemplateManager) isTrue(value interface{}) bool {
|
||||
if value == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
case bool:
|
||||
return v
|
||||
case string:
|
||||
return v != ""
|
||||
case int, int8, int16, int32, int64:
|
||||
return v != 0
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
return v != 0
|
||||
case float32, float64:
|
||||
return v != 0
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// isTemplateFile 检查是否为模板文件
|
||||
func (tm *TemplateManager) isTemplateFile(ext string) bool {
|
||||
templateExts := []string{".yml", ".yaml", ".conf", ".json", ".tpl"}
|
||||
for _, templateExt := range templateExts {
|
||||
if ext == templateExt {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isRulesetFile 检查是否为规则文件
|
||||
func (tm *TemplateManager) isRulesetFile(ext string) bool {
|
||||
rulesetExts := []string{".list", ".txt", ".rules"}
|
||||
for _, rulesetExt := range rulesetExts {
|
||||
if ext == rulesetExt {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getTemplateName 获取模板名称
|
||||
func (tm *TemplateManager) getTemplateName(path, baseDir string) string {
|
||||
relPath, err := filepath.Rel(baseDir, path)
|
||||
if err != nil {
|
||||
return filepath.Base(path)
|
||||
}
|
||||
return strings.TrimSuffix(relPath, filepath.Ext(relPath))
|
||||
}
|
||||
|
||||
// getRulesetName 获取规则集名称
|
||||
func (tm *TemplateManager) getRulesetName(path, rulesDir string) string {
|
||||
relPath, err := filepath.Rel(rulesDir, path)
|
||||
if err != nil {
|
||||
return filepath.Base(path)
|
||||
}
|
||||
return strings.TrimSuffix(relPath, filepath.Ext(relPath))
|
||||
}
|
||||
|
||||
// GetTemplate 获取模板
|
||||
func (tm *TemplateManager) GetTemplate(name string) (*template.Template, bool) {
|
||||
tmpl, exists := tm.templates[name]
|
||||
return tmpl, exists
|
||||
}
|
||||
|
||||
// GetRuleset 获取规则集
|
||||
func (tm *TemplateManager) GetRuleset(name string) (string, bool) {
|
||||
ruleset, exists := tm.rulesets[name]
|
||||
return ruleset, exists
|
||||
}
|
||||
|
||||
// GetSnippet 获取代码片段
|
||||
func (tm *TemplateManager) GetSnippet(name string) (string, bool) {
|
||||
snippet, exists := tm.snippets[name]
|
||||
return snippet, exists
|
||||
}
|
||||
|
||||
// GetProfileConfig 获取配置文件
|
||||
func (tm *TemplateManager) GetProfileConfig(name string) (interface{}, bool) {
|
||||
config, exists := tm.profileConfig[name]
|
||||
return config, exists
|
||||
}
|
||||
|
||||
// ListTemplates 列出所有模板
|
||||
func (tm *TemplateManager) ListTemplates() []string {
|
||||
var templates []string
|
||||
for name := range tm.templates {
|
||||
templates = append(templates, name)
|
||||
}
|
||||
return templates
|
||||
}
|
||||
|
||||
// ListRulesets 列出所有规则集
|
||||
func (tm *TemplateManager) ListRulesets() []string {
|
||||
var rulesets []string
|
||||
for name := range tm.rulesets {
|
||||
rulesets = append(rulesets, name)
|
||||
}
|
||||
return rulesets
|
||||
}
|
||||
|
||||
// ListProfileConfigs 列出所有配置文件
|
||||
func (tm *TemplateManager) ListProfileConfigs() []string {
|
||||
var configs []string
|
||||
for name := range tm.profileConfig {
|
||||
configs = append(configs, name)
|
||||
}
|
||||
return configs
|
||||
}
|
||||
|
||||
// RenderTemplate 渲染模板
|
||||
func (tm *TemplateManager) RenderTemplate(name string, variables TemplateVariables) (string, error) {
|
||||
tmpl, exists := tm.GetTemplate(name)
|
||||
if !exists {
|
||||
return "", fmt.Errorf("template not found: %s", name)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, variables); err != nil {
|
||||
return "", fmt.Errorf("failed to render template %s: %w", name, err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// RenderTemplateWithDefaults 使用默认配置渲染模板
|
||||
func (tm *TemplateManager) RenderTemplateWithDefaults(name string, groupName string, nodeInfo interface{}, totalNodes int) (string, error) {
|
||||
variables := TemplateVariables{
|
||||
Global: TemplateConfig{
|
||||
Clash: ClashConfig{
|
||||
HTTPPort: 7890,
|
||||
SocksPort: 7891,
|
||||
AllowLAN: true,
|
||||
LogLevel: "info",
|
||||
ExternalController: "127.0.0.1:9090",
|
||||
},
|
||||
Surge: SurgeConfig{
|
||||
Port: 8080,
|
||||
ProxyHTTPPort: 8081,
|
||||
ProxySOCKS5Port: 8082,
|
||||
MixedPort: 8080,
|
||||
AllowLAN: true,
|
||||
LogLevel: "info",
|
||||
},
|
||||
QuantumultX: QuantumultXConfig{
|
||||
Port: 8888,
|
||||
AllowLAN: true,
|
||||
LogLevel: "info",
|
||||
},
|
||||
Loon: LoonConfig{
|
||||
Port: 8080,
|
||||
AllowLAN: true,
|
||||
LogLevel: "info",
|
||||
},
|
||||
Surfboard: SurfboardConfig{
|
||||
Port: 8080,
|
||||
AllowLAN: true,
|
||||
LogLevel: "info",
|
||||
},
|
||||
V2Ray: V2RayConfig{
|
||||
AllowLAN: true,
|
||||
MixedPort: 2080,
|
||||
LogLevel: "info",
|
||||
},
|
||||
},
|
||||
NodeInfo: nodeInfo,
|
||||
GroupName: groupName,
|
||||
UpdateTime: tm.getCurrentTime(),
|
||||
UserInfo: "",
|
||||
TotalNodes: totalNodes,
|
||||
Request: RequestConfig{
|
||||
Target: "clash",
|
||||
Clash: make(map[string]interface{}),
|
||||
Surge: make(map[string]interface{}),
|
||||
},
|
||||
Local: LocalConfig{
|
||||
Clash: make(map[string]interface{}),
|
||||
Surge: make(map[string]interface{}),
|
||||
},
|
||||
}
|
||||
|
||||
return tm.RenderTemplate(name, variables)
|
||||
}
|
||||
|
||||
// getCurrentTime 获取当前时间
|
||||
func (tm *TemplateManager) getCurrentTime() string {
|
||||
// 简化实现,返回格式化时间
|
||||
return "2025-09-25 17:30:00"
|
||||
}
|
||||
|
||||
// GetTemplateVariables 获取模板变量
|
||||
func (tm *TemplateManager) GetTemplateVariables() TemplateVariables {
|
||||
return TemplateVariables{
|
||||
Global: TemplateConfig{
|
||||
Clash: ClashConfig{
|
||||
HTTPPort: 7890,
|
||||
SocksPort: 7891,
|
||||
AllowLAN: true,
|
||||
LogLevel: "info",
|
||||
ExternalController: "127.0.0.1:9090",
|
||||
},
|
||||
// ... 其他默认配置
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重新加载所有模板和配置
|
||||
func (tm *TemplateManager) Reload() error {
|
||||
tm.logger.Info("Reloading templates and configurations...")
|
||||
|
||||
// 清空现有数据
|
||||
tm.templates = make(map[string]*template.Template)
|
||||
tm.rulesets = make(map[string]string)
|
||||
tm.snippets = make(map[string]string)
|
||||
tm.profileConfig = make(map[string]interface{})
|
||||
|
||||
// 重新加载
|
||||
if err := tm.loadTemplates(); err != nil {
|
||||
return fmt.Errorf("failed to reload templates: %w", err)
|
||||
}
|
||||
|
||||
if err := tm.loadRulesets(); err != nil {
|
||||
return fmt.Errorf("failed to reload rulesets: %w", err)
|
||||
}
|
||||
|
||||
if err := tm.loadSnippets(); err != nil {
|
||||
return fmt.Errorf("failed to reload snippets: %w", err)
|
||||
}
|
||||
|
||||
if err := tm.loadProfileConfigs(); err != nil {
|
||||
return fmt.Errorf("failed to reload profile configs: %w", err)
|
||||
}
|
||||
|
||||
tm.logger.Info("Templates and configurations reloaded successfully")
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user