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:
481
internal/generator/quantumultx.go
Normal file
481
internal/generator/quantumultx.go
Normal file
@@ -0,0 +1,481 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/subconverter-go/internal/logging"
|
||||
"github.com/subconverter-go/internal/parser"
|
||||
)
|
||||
|
||||
// QuantumultXGenerator Quantumult X格式生成器
|
||||
// 实现Quantumult X代理配置的生成功能
|
||||
type QuantumultXGenerator struct {
|
||||
logger *logging.Logger
|
||||
}
|
||||
|
||||
// NewQuantumultXGenerator 创建新的Quantumult X生成器
|
||||
// 返回初始化好的QuantumultXGenerator实例
|
||||
func NewQuantumultXGenerator(logger *logging.Logger) *QuantumultXGenerator {
|
||||
return &QuantumultXGenerator{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 生成Quantumult X配置
|
||||
func (g *QuantumultXGenerator) Generate(configs []*parser.ProxyConfig, options *GenerationOptions) (string, error) {
|
||||
g.logger.Debugf("Generating Quantumult X configuration")
|
||||
|
||||
// 生成Quantumult X配置文本
|
||||
var builder strings.Builder
|
||||
|
||||
// 生成头部信息
|
||||
g.generateHeader(&builder, options)
|
||||
|
||||
// 生成服务器配置
|
||||
g.generateServers(&builder, configs)
|
||||
|
||||
// 生成远程服务器提供者
|
||||
g.generateRemoteServers(&builder, options)
|
||||
|
||||
// 生成策略组
|
||||
g.generatePolicyGroups(&builder, configs, options)
|
||||
|
||||
// 生成规则
|
||||
g.generateRules(&builder, options)
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
// ValidateOptions 验证Quantumult X生成选项
|
||||
func (g *QuantumultXGenerator) ValidateOptions(options *GenerationOptions) error {
|
||||
g.logger.Debugf("Validating Quantumult X generation options")
|
||||
|
||||
// 验证基本信息
|
||||
if options.Name == "" {
|
||||
return fmt.Errorf("configuration name is required")
|
||||
}
|
||||
|
||||
// 验证端口
|
||||
if options.MixedPort < 0 || options.MixedPort > 65535 {
|
||||
return fmt.Errorf("invalid mixed port: %d", options.MixedPort)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSupportedFormats 获取支持的格式
|
||||
func (g *QuantumultXGenerator) GetSupportedFormats() []string {
|
||||
return []string{"quantumultx"}
|
||||
}
|
||||
|
||||
// generateHeader 生成配置头部信息
|
||||
func (g *QuantumultXGenerator) generateHeader(builder *strings.Builder, options *GenerationOptions) {
|
||||
builder.WriteString("[General]\n")
|
||||
builder.WriteString("network_check_url = http://www.gstatic.com/generate_204\n")
|
||||
builder.WriteString("ipv6 = false\n")
|
||||
builder.WriteString("prefer_ipv6 = false\n")
|
||||
builder.WriteString("wifi_assist = false\n")
|
||||
builder.WriteString("enable_policy = true\n")
|
||||
builder.WriteString("exclude_simple_hostnames = true\n")
|
||||
builder.WriteString("all_proxy_available = false\n")
|
||||
builder.WriteString("dns_server = 119.29.29.29, 223.5.5.5, 1.1.1.1, 8.8.8.8\n")
|
||||
builder.WriteString("fallback_dns_server = 223.5.5.5, 119.29.29.29\n")
|
||||
|
||||
if options.IPv6 {
|
||||
builder.WriteString("ipv6 = true\n")
|
||||
}
|
||||
|
||||
if options.MixedPort > 0 {
|
||||
builder.WriteString(fmt.Sprintf("http_port = %d\n", options.MixedPort))
|
||||
builder.WriteString(fmt.Sprintf("socks5_port = %d\n", options.MixedPort+1))
|
||||
}
|
||||
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
// generateServers 生成服务器配置
|
||||
func (g *QuantumultXGenerator) generateServers(builder *strings.Builder, configs []*parser.ProxyConfig) {
|
||||
builder.WriteString("[Server]\n")
|
||||
|
||||
for i, config := range configs {
|
||||
server := g.convertProxyConfig(config)
|
||||
builder.WriteString(server)
|
||||
|
||||
if i < len(configs)-1 {
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
func (g *QuantumultXGenerator) generateRemoteServers(builder *strings.Builder, options *GenerationOptions) {
|
||||
providers := g.resolveProviders(options)
|
||||
if len(providers) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
builder.WriteString("[Server Remote]\n")
|
||||
for _, def := range providers {
|
||||
builder.WriteString(g.formatRemoteServer(def))
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
// convertProxyConfig 转换代理配置为Quantumult X格式
|
||||
func (g *QuantumultXGenerator) convertProxyConfig(proxyConfig *parser.ProxyConfig) string {
|
||||
switch proxyConfig.Protocol {
|
||||
case "ss":
|
||||
return g.generateSSProxy(proxyConfig)
|
||||
case "ssr":
|
||||
return g.generateSSRProxy(proxyConfig)
|
||||
case "vmess":
|
||||
return g.generateVMessProxy(proxyConfig)
|
||||
case "trojan":
|
||||
return g.generateTrojanProxy(proxyConfig)
|
||||
case "http", "https":
|
||||
return g.generateHTTPProxy(proxyConfig)
|
||||
case "socks5":
|
||||
return g.generateSocks5Proxy(proxyConfig)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// generateSSProxy 生成Shadowsocks代理配置
|
||||
func (g *QuantumultXGenerator) generateSSProxy(config *parser.ProxyConfig) string {
|
||||
method, _ := config.Settings["method"].(string)
|
||||
password, _ := config.Settings["password"].(string)
|
||||
plugin, _ := config.Settings["plugin"].(string)
|
||||
|
||||
if plugin != "" {
|
||||
return fmt.Sprintf("shadowsocks=%s:%d, method=%s, password=%s, plugin=%s, %s",
|
||||
config.Server, config.Port, method, password, plugin, g.generatePluginOpts(config))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("shadowsocks=%s:%d, method=%s, password=%s",
|
||||
config.Server, config.Port, method, password)
|
||||
}
|
||||
|
||||
// generateSSRProxy 生成ShadowsocksR代理配置
|
||||
func (g *QuantumultXGenerator) generateSSRProxy(config *parser.ProxyConfig) string {
|
||||
method, _ := config.Settings["method"].(string)
|
||||
password, _ := config.Settings["password"].(string)
|
||||
protocol, _ := config.Settings["protocol"].(string)
|
||||
protocolParam, _ := config.Settings["protocol-param"].(string)
|
||||
obfs, _ := config.Settings["obfs"].(string)
|
||||
obfsParam, _ := config.Settings["obfs-param"].(string)
|
||||
|
||||
return fmt.Sprintf("shadowsocks=%s:%d, method=%s, password=%s, ssr-protocol=%s, ssr-protocol-param=%s, obfs=%s, obfs-host=%s",
|
||||
config.Server, config.Port, method, password, protocol, protocolParam, obfs, obfsParam)
|
||||
}
|
||||
|
||||
// generateVMessProxy 生成VMess代理配置
|
||||
func (g *QuantumultXGenerator) generateVMessProxy(config *parser.ProxyConfig) string {
|
||||
uuid, _ := config.Settings["uuid"].(string)
|
||||
_, _ = config.Settings["alterId"].(int)
|
||||
network, _ := config.Settings["network"].(string)
|
||||
tls, _ := config.Settings["tls"].(string)
|
||||
host, _ := config.Settings["host"].(string)
|
||||
path, _ := config.Settings["path"].(string)
|
||||
|
||||
var opts []string
|
||||
opts = append(opts, fmt.Sprintf("%s:%d", config.Server, config.Port))
|
||||
opts = append(opts, fmt.Sprintf("method=%s", "chacha20-ietf-poly1305")) // Quantumult X默认加密方式
|
||||
opts = append(opts, fmt.Sprintf("password=%s", uuid))
|
||||
|
||||
if network == "ws" {
|
||||
opts = append(opts, fmt.Sprintf("obfs=ws"))
|
||||
if host != "" {
|
||||
opts = append(opts, fmt.Sprintf("obfs-host=%s", host))
|
||||
}
|
||||
if path != "" {
|
||||
opts = append(opts, fmt.Sprintf("obfs-uri=%s", path))
|
||||
}
|
||||
}
|
||||
|
||||
if tls != "" {
|
||||
opts = append(opts, fmt.Sprintf("tls-host=%s", host))
|
||||
opts = append(opts, fmt.Sprintf("over-tls=true"))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("vmess=%s", strings.Join(opts, ", "))
|
||||
}
|
||||
|
||||
// generateTrojanProxy 生成Trojan代理配置
|
||||
func (g *QuantumultXGenerator) generateTrojanProxy(config *parser.ProxyConfig) string {
|
||||
password, _ := config.Settings["password"].(string)
|
||||
sni, _ := config.Settings["sni"].(string)
|
||||
network, _ := config.Settings["network"].(string)
|
||||
|
||||
var opts []string
|
||||
opts = append(opts, fmt.Sprintf("%s:%d", config.Server, config.Port))
|
||||
opts = append(opts, fmt.Sprintf("password=%s", password))
|
||||
|
||||
if network == "ws" {
|
||||
opts = append(opts, fmt.Sprintf("obfs=ws"))
|
||||
if sni != "" {
|
||||
opts = append(opts, fmt.Sprintf("obfs-host=%s", sni))
|
||||
}
|
||||
}
|
||||
|
||||
if sni != "" {
|
||||
opts = append(opts, fmt.Sprintf("tls-host=%s", sni))
|
||||
opts = append(opts, fmt.Sprintf("over-tls=true"))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("trojan=%s", strings.Join(opts, ", "))
|
||||
}
|
||||
|
||||
// generateHTTPProxy 生成HTTP代理配置
|
||||
func (g *QuantumultXGenerator) generateHTTPProxy(config *parser.ProxyConfig) string {
|
||||
username, _ := config.Settings["username"].(string)
|
||||
password, _ := config.Settings["password"].(string)
|
||||
tls, _ := config.Settings["tls"].(bool)
|
||||
|
||||
if username != "" && password != "" {
|
||||
if tls {
|
||||
return fmt.Sprintf("https=%s:%d, username=%s, password=%s",
|
||||
config.Server, config.Port, username, password)
|
||||
}
|
||||
return fmt.Sprintf("http=%s:%d, username=%s, password=%s",
|
||||
config.Server, config.Port, username, password)
|
||||
}
|
||||
|
||||
if tls {
|
||||
return fmt.Sprintf("https=%s:%d",
|
||||
config.Server, config.Port)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("http=%s:%d",
|
||||
config.Server, config.Port)
|
||||
}
|
||||
|
||||
// generateSocks5Proxy 生成Socks5代理配置
|
||||
func (g *QuantumultXGenerator) generateSocks5Proxy(config *parser.ProxyConfig) string {
|
||||
username, _ := config.Settings["username"].(string)
|
||||
password, _ := config.Settings["password"].(string)
|
||||
|
||||
if username != "" && password != "" {
|
||||
return fmt.Sprintf("socks5=%s:%d, username=%s, password=%s",
|
||||
config.Server, config.Port, username, password)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("socks5=%s:%d",
|
||||
config.Server, config.Port)
|
||||
}
|
||||
|
||||
// generatePluginOpts 生成插件选项
|
||||
func (g *QuantumultXGenerator) generatePluginOpts(config *parser.ProxyConfig) string {
|
||||
// 这里可以根据具体插件生成相应的选项
|
||||
return ""
|
||||
}
|
||||
|
||||
// generatePolicyGroups 生成策略组
|
||||
func (g *QuantumultXGenerator) generatePolicyGroups(builder *strings.Builder, configs []*parser.ProxyConfig, options *GenerationOptions) {
|
||||
builder.WriteString("[Policy]\n")
|
||||
|
||||
// 创建代理名称列表
|
||||
proxyNames := make([]string, 0)
|
||||
for _, config := range configs {
|
||||
proxyNames = append(proxyNames, config.Name)
|
||||
}
|
||||
|
||||
// 直连策略
|
||||
builder.WriteString("static=Direct, direct\n")
|
||||
|
||||
// 拒绝策略
|
||||
builder.WriteString("static=Reject, reject\n")
|
||||
|
||||
// 选择策略
|
||||
builder.WriteString(fmt.Sprintf("static=Proxy, %s\n", strings.Join(proxyNames, ", ")))
|
||||
|
||||
// URL测试策略
|
||||
if options.ProxyTest {
|
||||
testURL := options.ProxyURL
|
||||
if testURL == "" {
|
||||
testURL = "http://www.gstatic.com/generate_204"
|
||||
}
|
||||
builder.WriteString(fmt.Sprintf("url-test=URL-Test, %s, url=%s, interval=300\n",
|
||||
strings.Join(proxyNames, ", "), testURL))
|
||||
}
|
||||
|
||||
// 故障转移策略
|
||||
builder.WriteString(fmt.Sprintf("fallback=Fallback, %s, url=http://www.gstatic.com/generate_204, interval=300\n",
|
||||
strings.Join(proxyNames, ", ")))
|
||||
|
||||
if len(options.GroupDefinitions) > 0 {
|
||||
for _, def := range options.GroupDefinitions {
|
||||
builder.WriteString(g.formatPolicyGroup(def, proxyNames))
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
// generateRules 生成规则
|
||||
func (g *QuantumultXGenerator) generateRules(builder *strings.Builder, options *GenerationOptions) {
|
||||
builder.WriteString("[Rule]\n")
|
||||
|
||||
// 添加默认规则
|
||||
if len(options.Rules) > 0 {
|
||||
builder.WriteString(strings.Join(options.Rules, "\n"))
|
||||
} else {
|
||||
// 添加默认规则集
|
||||
defaultRules := []string{
|
||||
"DOMAIN-SUFFIX,bilibili.com,Direct",
|
||||
"DOMAIN-SUFFIX,baidu.com,Direct",
|
||||
"DOMAIN-SUFFIX,cn,Direct",
|
||||
"DOMAIN-KEYWORD,google,Proxy",
|
||||
"DOMAIN-KEYWORD,github,Proxy",
|
||||
"DOMAIN-KEYWORD,twitter,Proxy",
|
||||
"DOMAIN-KEYWORD,facebook,Proxy",
|
||||
"DOMAIN-KEYWORD,youtube,Proxy",
|
||||
"DOMAIN-KEYWORD,instagram,Proxy",
|
||||
"DOMAIN-KEYWORD,telegram,Proxy",
|
||||
"GEOIP,CN,Direct",
|
||||
"IP-CIDR,127.0.0.0/8,Direct",
|
||||
"IP-CIDR,192.168.0.0/16,Direct",
|
||||
"IP-CIDR,10.0.0.0/8,Direct",
|
||||
"IP-CIDR,172.16.0.0/12,Direct",
|
||||
"FINAL,Proxy",
|
||||
}
|
||||
builder.WriteString(strings.Join(defaultRules, "\n"))
|
||||
}
|
||||
|
||||
// 启用IPv6支持
|
||||
if options.IPv6 {
|
||||
builder.WriteString("\n# IPv6 Rules\n")
|
||||
builder.WriteString("IP-CIDR6,::1/128,Direct\n")
|
||||
builder.WriteString("IP-CIDR6,fc00::/7,Direct\n")
|
||||
}
|
||||
|
||||
// 启用局域网支持
|
||||
if options.EnableLan {
|
||||
builder.WriteString("\n# LAN Rules\n")
|
||||
builder.WriteString("SRC-IP-CIDR,192.168.0.0/16,Direct\n")
|
||||
builder.WriteString("SRC-IP-CIDR,10.0.0.0/8,Direct\n")
|
||||
builder.WriteString("SRC-IP-CIDR,172.16.0.0/12,Direct\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (g *QuantumultXGenerator) resolveProviders(options *GenerationOptions) []*ProviderDefinition {
|
||||
if len(options.Providers) > 0 {
|
||||
return options.Providers
|
||||
}
|
||||
if len(options.CustomProviders) == 0 {
|
||||
return nil
|
||||
}
|
||||
providers := make([]*ProviderDefinition, 0, len(options.CustomProviders))
|
||||
for _, entry := range options.CustomProviders {
|
||||
parsed, err := ParseProviderDefinition(entry)
|
||||
if err != nil {
|
||||
g.logger.WithError(err).Warnf("Failed to parse custom provider: %s", entry)
|
||||
continue
|
||||
}
|
||||
providers = append(providers, parsed)
|
||||
}
|
||||
return providers
|
||||
}
|
||||
|
||||
func (g *QuantumultXGenerator) formatRemoteServer(def *ProviderDefinition) string {
|
||||
parts := []string{def.URL, fmt.Sprintf("tag=%s", def.Name)}
|
||||
parts = append(parts, fmt.Sprintf("path=%s", def.Path))
|
||||
if def.Interval != nil {
|
||||
parts = append(parts, fmt.Sprintf("update-interval=%d", *def.Interval))
|
||||
}
|
||||
parts = append(parts, "opt-parser=true", "enabled=true")
|
||||
|
||||
if len(def.Flags) > 0 {
|
||||
keys := make([]string, 0, len(def.Flags))
|
||||
for k := range def.Flags {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
parts = append(parts, fmt.Sprintf("%s=%t", k, def.Flags[k]))
|
||||
}
|
||||
}
|
||||
if len(def.Fields) > 0 {
|
||||
keys := make([]string, 0, len(def.Fields))
|
||||
for k := range def.Fields {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
parts = append(parts, fmt.Sprintf("%s=%s", k, def.Fields[k]))
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s = %s", def.Name, strings.Join(parts, ", "))
|
||||
}
|
||||
|
||||
func (g *QuantumultXGenerator) formatPolicyGroup(def *GroupDefinition, defaultProxies []string) string {
|
||||
proxies := make([]string, 0, len(def.Proxies))
|
||||
proxies = append(proxies, def.Proxies...)
|
||||
if len(proxies) == 0 && len(def.UseProviders) == 0 {
|
||||
proxies = append(proxies, defaultProxies...)
|
||||
}
|
||||
|
||||
typeToken := g.policyTypeToken(def.Type)
|
||||
items := make([]string, 0, len(proxies))
|
||||
items = append(items, proxies...)
|
||||
|
||||
if len(def.UseProviders) > 0 {
|
||||
items = append(items, fmt.Sprintf("server-tag-regex=%s", g.providerRegex(def.UseProviders)))
|
||||
}
|
||||
if def.URL != "" {
|
||||
items = append(items, fmt.Sprintf("url=%s", def.URL))
|
||||
}
|
||||
if def.Interval != nil {
|
||||
items = append(items, fmt.Sprintf("interval=%d", *def.Interval))
|
||||
}
|
||||
if def.Timeout != nil {
|
||||
items = append(items, fmt.Sprintf("timeout=%d", *def.Timeout))
|
||||
}
|
||||
if def.Tolerance != nil {
|
||||
items = append(items, fmt.Sprintf("tolerance=%d", *def.Tolerance))
|
||||
}
|
||||
if len(def.Extras) > 0 {
|
||||
keys := make([]string, 0, len(def.Extras))
|
||||
for k := range def.Extras {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
items = append(items, fmt.Sprintf("%s=%s", k, def.Extras[k]))
|
||||
}
|
||||
}
|
||||
|
||||
joined := strings.Join(items, ", ")
|
||||
if joined != "" {
|
||||
joined = ", " + joined
|
||||
}
|
||||
return fmt.Sprintf("%s=%s%s", typeToken, def.Name, joined)
|
||||
}
|
||||
|
||||
func (g *QuantumultXGenerator) policyTypeToken(groupType string) string {
|
||||
switch strings.ToLower(groupType) {
|
||||
case "select":
|
||||
return "static"
|
||||
case "url-test":
|
||||
return "url-test"
|
||||
case "fallback":
|
||||
return "fallback"
|
||||
case "load-balance":
|
||||
return "available"
|
||||
default:
|
||||
return "static"
|
||||
}
|
||||
}
|
||||
|
||||
func (g *QuantumultXGenerator) providerRegex(providers []string) string {
|
||||
if len(providers) == 1 {
|
||||
return fmt.Sprintf("^%s$", providers[0])
|
||||
}
|
||||
return fmt.Sprintf("^(%s)$", strings.Join(providers, "|"))
|
||||
}
|
||||
Reference in New Issue
Block a user