commit ddaba6ce62b8f3e9e60f070d8a97cdf946a2dfca Author: huyunfan Date: Mon Dec 16 05:34:52 2024 +0800 First web useful diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7d615af --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +# Binary files +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out + +# Go build artifacts +/_build/ +/_build/ +/dist/ +/bin/ +/tmp/ + +# Ignore log files and debugging output +*.log +*.trace + +# Environment files +.env +.env.* + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*.bak +*.tmp + +# Dependency directories +vendor/ +Gopkg.lock +Gopkg.toml + +# Go modules +go.sum +go.work +go.work.sum + +# Test output +*.coverprofile +*.test +test-results/ + +# OS-specific files +.DS_Store +Thumbs.db + +# Coverage files +coverage.out + +# Other temporary files +*.bak +*.tmp +*.old +*.backup +!.keep +src/proxies/* +src/rules/* +acl4ssr_repo +lufxy.yaml +result.yaml \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..30ce3a7 --- /dev/null +++ b/go.mod @@ -0,0 +1,55 @@ +module proxyrules + +go 1.22 + +require ( + github.com/go-git/go-git/v5 v5.12.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v1.0.0 // indirect + github.com/bytedance/sonic v1.12.5 // indirect + github.com/bytedance/sonic/loader v0.2.1 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.7 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.10.0 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.23.0 // indirect + github.com/goccy/go-json v0.10.4 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.2.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/arch v0.12.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.32.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + google.golang.org/protobuf v1.35.2 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect +) diff --git a/src/aclgit/manager.go b/src/aclgit/manager.go new file mode 100644 index 0000000..5fe4e94 --- /dev/null +++ b/src/aclgit/manager.go @@ -0,0 +1,78 @@ +package aclgit + +import ( + "fmt" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "os" + "path/filepath" +) + +// AclGitManager 封装了克隆、分支切换和文件读取功能 +type AclGitManager struct { + RepoURL string + TargetDir string + Repository *git.Repository +} + +// NewAclGitManager 创建一个新的 AclGitManager 实例 +func NewAclGitManager(repoURL, targetDir string) *AclGitManager { + return &AclGitManager{ + RepoURL: repoURL, + TargetDir: targetDir, + } +} + +// Clone 克隆远程仓库到本地的 rules 文件夹,并默认指定分支 +func (a *AclGitManager) Clone(branchName string) error { + // 确定克隆到 rules 文件夹 + cloneDir := filepath.Join(a.TargetDir, "rules") + + // 如果目标目录已存在,删除它 + if _, err := os.Stat(cloneDir); err == nil { + if err := os.RemoveAll(cloneDir); err != nil { + return fmt.Errorf("failed to cleanup existing directory: %v", err) + } + } + + // 克隆仓库,并指定分支 + repo, err := git.PlainClone(cloneDir, false, &git.CloneOptions{ + URL: a.RepoURL, + Progress: os.Stdout, + ReferenceName: plumbing.NewBranchReferenceName(branchName), // 指定克隆的分支 + }) + if err != nil { + return fmt.Errorf("failed to clone repository: %v", err) + } + + a.Repository = repo + return nil +} + +// CheckoutBranch 切换到指定分支 +func (a *AclGitManager) CheckoutBranch(branchName string) error { + if a.Repository == nil { + return fmt.Errorf("repository is not initialized, call Clone() first") + } + + worktree, err := a.Repository.Worktree() + if err != nil { + return fmt.Errorf("failed to get worktree: %v", err) + } + + // 确保分支存在 + ref := plumbing.NewBranchReferenceName(branchName) + if _, err := a.Repository.Reference(ref, true); err != nil { + return fmt.Errorf("branch '%s' does not exist: %v", branchName, err) + } + + // 切换到指定分支 + err = worktree.Checkout(&git.CheckoutOptions{ + Branch: ref, + }) + if err != nil { + return fmt.Errorf("failed to checkout branch '%s': %v", branchName, err) + } + + return nil +} diff --git a/src/entity/clash_config.go b/src/entity/clash_config.go new file mode 100644 index 0000000..e3746e7 --- /dev/null +++ b/src/entity/clash_config.go @@ -0,0 +1,16 @@ +package entity + +// ClashConfig represents the main structure of the Clash configuration file +type ClashConfig struct { + Port int `yaml:"port"` + SocksPort int `yaml:"socks-port"` + AllowLan bool `yaml:"allow-lan"` + Mode string `yaml:"mode"` + LogLevel string `yaml:"log-level"` + ExternalController string `yaml:"external-controller"` + Experimental ExperimentalOptions `yaml:"experimental"` + Hosts map[string]string `yaml:"hosts"` + Proxies []string `yaml:"proxies"` + ProxyGroups []ProxyGroup `yaml:"proxy-groups"` + Rules []string `yaml:"rules"` +} diff --git a/src/entity/experimental.go b/src/entity/experimental.go new file mode 100644 index 0000000..a511618 --- /dev/null +++ b/src/entity/experimental.go @@ -0,0 +1,6 @@ +package entity + +// ExperimentalOptions represents the experimental section in the Clash config +type ExperimentalOptions struct { + IgnoreResolveFail bool `yaml:"ignore-resolve-fail"` +} diff --git a/src/entity/proxy_group.go b/src/entity/proxy_group.go new file mode 100644 index 0000000..13b9df2 --- /dev/null +++ b/src/entity/proxy_group.go @@ -0,0 +1,10 @@ +package entity + +// ProxyGroup represents a proxy group in the Clash configuration +type ProxyGroup struct { + Name string `yaml:"name"` + Type string `yaml:"type"` + Proxies []string `yaml:"proxies"` + URL string `yaml:"url,omitempty"` + Interval int `yaml:"interval,omitempty"` +} diff --git a/src/example/example_config.go b/src/example/example_config.go new file mode 100644 index 0000000..81fc66c --- /dev/null +++ b/src/example/example_config.go @@ -0,0 +1,77 @@ +package example + +import ( + "proxyrules/src/entity" +) + +// GetExampleConfig generates a sample ClashConfig instance +func GetExampleConfig() entity.ClashConfig { + return entity.ClashConfig{ + Port: 7890, + SocksPort: 7891, + AllowLan: false, + Mode: "Rule", + LogLevel: "info", + ExternalController: "127.0.0.1:9090", + Experimental: entity.ExperimentalOptions{ + IgnoreResolveFail: true, + }, + Hosts: map[string]string{ + "mtalk.google.com": "108.177.125.188", + }, + Proxies: nil, + ProxyGroups: []entity.ProxyGroup{ + { + Name: "自动选择快速节点", + Type: "url-test", + Proxies: []string{"自由扩散", "协助扩散", "freebsd"}, + URL: "http://www.gstatic.com/generate_204", + Interval: 300, + }, + { + Name: "PROXY", + Type: "select", + Proxies: []string{"自动选择快速节点", "自由扩散", "协助扩散", "freebsd", "Tokyo-该节点流量计费!", "Dallas", "Neon", "Neon-v6", "HK-v6", "保加利亚", "保加利亚-v6", "FR-hy2-v6", "FR-v2ray-v6", "FR-v2ray-cdn"}, + }, + { + Name: "Final", + Type: "select", + Proxies: []string{"DIRECT", "PROXY", "freebsd"}, + }, + { + Name: "Apple", + Type: "select", + Proxies: []string{"DIRECT", "PROXY", "freebsd"}, + }, + { + Name: "GlobalMedia", + Type: "select", + Proxies: []string{"自由扩散", "协助扩散", "freebsd"}, + }, + { + Name: "HKMTMedia", + Type: "select", + Proxies: []string{"DIRECT", "自由扩散", "协助扩散", "freebsd"}, + }, + }, + Rules: []string{ + "IP-CIDR,11.11.0.0/16,DIRECT", + "DOMAIN-SUFFIX,*.hit.edu.cn,DIRECT", + "DOMAIN-SUFFIX,hit.edu.cn,DIRECT", + "DOMAIN-SUFFIX,*.fr.cherr.cc,DIRECT", + "DOMAIN-SUFFIX,fr.cherr.cc,DIRECT", + "DOMAIN-SUFFIX,aaronhu.cn,DIRECT", + "DOMAIN-SUFFIX,*.aaronhu.cn,DIRECT", + "DOMAIN-SUFFIX,metacubex.one,PROXY", + "DOMAIN-SUFFIX,*.metacubex.one,PROXY", + "DOMAIN-SUFFIX,cmliussss.com,PROXY", + "DOMAIN-SUFFIX,*.cmliussss.com,PROXY", + "DOMAIN-SUFFIX,hysteria.network,PROXY", + "DOMAIN-SUFFIX,*.hysteria.network,PROXY", + "DOMAIN-SUFFIX,googletraveladservices.com,DIRECT", + "DOMAIN,dl.google.com,DIRECT", + "DOMAIN,mtalk.google.com,DIRECT", + "DOMAIN-SUFFIX,17gouwuba.com,REJECT", + }, + } +} diff --git a/src/main/main.go b/src/main/main.go new file mode 100644 index 0000000..372228d --- /dev/null +++ b/src/main/main.go @@ -0,0 +1,255 @@ +package main + +import ( + "fmt" + "github.com/gin-gonic/gin" + "html/template" + "io/ioutil" + "net/http" + "os" + "proxyrules/src/service" + "strings" +) + +const htmlTemplate = ` + + + + + + Edit Default List + + + +
+ + +
+
+ +

Welcome to 404 Clash Config Site !~~

+
+
+
    + {{range $index, $line := .lines}} +
  • + + +
  • + {{end}} +
+ + +
+
+ + +
+ +
+ + + + +` + +// 文件路径 +const defaultFilePath = "src/proxies/default.list" +const clashConfigPath = "lufxy.yaml" + +// 读取默认列表文件 +func readDefaultList() ([]string, error) { + content, err := ioutil.ReadFile(defaultFilePath) + if err != nil { + return nil, err + } + lines := strings.Split(string(content), "\n") + // 去除空行 + var result []string + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed != "" { + result = append(result, trimmed) + } + } + return result, nil +} + +// 写入到默认列表文件 +func writeDefaultList(lines []string) error { + content := strings.Join(lines, "\n") + return ioutil.WriteFile(defaultFilePath, []byte(content), 0644) +} + +// 读取 Clash 配置文件 +func readClashConfig() (string, error) { + content, err := ioutil.ReadFile(clashConfigPath) + if err != nil { + return "", err + } + return string(content), nil +} + +func main() { + service.GenerateConfig() + r := gin.Default() + r.Static("/static", "./static") // 将 static 目录映射到 /static 路径 + + // 加载 HTML 模版 + r.SetHTMLTemplate(template.Must(template.New("index").Parse(htmlTemplate))) + + // 显示文件内容和操作界面 + r.GET("/", func(c *gin.Context) { + lines, err := readDefaultList() + if err != nil { + c.String(http.StatusInternalServerError, fmt.Sprintf("Error reading file: %v", err)) + return + } + c.HTML(http.StatusOK, "index", gin.H{"lines": lines}) + }) + + // 提交更新后的文件内容 + r.POST("/update", func(c *gin.Context) { + formLines := c.PostFormArray("lines") + if err := writeDefaultList(formLines); err != nil { + c.String(http.StatusInternalServerError, fmt.Sprintf("Error writing file: %v", err)) + return + } + c.Redirect(http.StatusFound, "/") + }) + + // 下载 Clash 配置 + r.GET("/download", func(c *gin.Context) { + if _, err := os.Stat(clashConfigPath); os.IsNotExist(err) { + c.String(http.StatusNotFound, "Clash configuration file not found") + return + } + c.FileAttachment(clashConfigPath, "lufxy.yaml") + }) + + // 将 Clash 配置放入剪贴板 + r.GET("/clipboard", func(c *gin.Context) { + config, err := readClashConfig() + if err != nil { + c.String(http.StatusInternalServerError, fmt.Sprintf("Error reading Clash config: %v", err)) + return + } + c.JSON(http.StatusOK, gin.H{"content": config}) + }) + + // 启动服务 + r.Run(":8080") // 在 http://localhost:8080 提供服务 +} diff --git a/src/service/generate_config.go b/src/service/generate_config.go new file mode 100644 index 0000000..31ed222 --- /dev/null +++ b/src/service/generate_config.go @@ -0,0 +1,351 @@ +package service + +import ( + "bufio" + "fmt" + "log" + "os" + "path/filepath" + "proxyrules/src/aclgit" + "regexp" + "strings" + "time" + + "gopkg.in/yaml.v3" // YAML 序列化库 + "proxyrules/src/entity" + "proxyrules/src/example" + "proxyrules/src/util" +) + +func GenerateConfig() { + repoManager := aclgit.NewAclGitManager("https://github.com/ACL4SSR/ACL4SSR.git", "./src/rules") + + // 最大尝试次数 + maxAttempts := 3 + var err error + + // 尝试克隆仓库 + for attempts := 1; attempts <= maxAttempts; attempts++ { + fmt.Printf("Attempt %d to clone repository...\n", attempts) + err = repoManager.Clone("master") + if err == nil { + fmt.Println("Clone completed successfully!") + break + } + fmt.Printf("Clone attempt %d failed: %v\n", attempts, err) + + // 如果还没到最大尝试次数,等待一段时间后再尝试 + if attempts < maxAttempts { + fmt.Println("Retrying in 3 seconds...") + time.Sleep(3 * time.Second) + } + } + + // 如果克隆失败,打印错误日志,但继续执行后续操作 + if err != nil { + log.Printf("Failed to clone repository after %d attempts: %v", maxAttempts, err) + } else { + fmt.Println("Repository cloned successfully after retries!") + } + + // 无论克隆是否成功,继续执行后续操作 + fmt.Println("Proceeding with the next steps...") + // 2. proxies阶段,读proxies,读取 proxies 文件夹中的 .list 文件 + proxyDir := filepath.Join("./src", "proxies") + proxies, _, err := util.ReadAllListFiles(proxyDir, false) + if err != nil { + log.Fatalf("Failed to read .list files from rules: %v", err) + } + + // 将所有 Proxies 的值(每个文件的内容)合并为一个切片 + var proxyList []string + for _, values := range proxies { + proxyList = append(proxyList, values...) + } + // 将所有 Proxies 的值(每个文件的内容)合并为一个切片 + // 打印合并后的 Proxies 切片 + fmt.Println("Proxies:", proxies) + + // 3. rules阶段,读rules文件夹下的所有.list文件 + rulesDir := filepath.Join("./src", "rules") + rules, requireChoseList, err := util.ReadAllListFiles(rulesDir, true) + if err != nil { + log.Fatalf("Failed to read .list files from rules: %v", err) + } + + // 将所有 Rules 的值(每个文件的内容)合并为一个切片 + var rulesList []string + for _, values := range rules { + rulesList = append(rulesList, values...) + } + rulesList = dealNoResolve(rulesList) + rulesList = dealUserAgent(rulesList) + + // 打印合并后的 Rules 切片 + //fmt.Println("Rules:", rulesList) + + // 使用 example 包中的示例配置 + config := example.GetExampleConfig() + config.Proxies = proxyList + // 获取 Rules + // 创建 ProxyGroup + var proxyGroups []entity.ProxyGroup + proxyNameList := ExtractProxyNames(proxyList) + proxyGroups = append(proxyGroups, entity.ProxyGroup{ + Name: "AutoSelect", + Type: "url-test", + Proxies: proxyNameList, + URL: "http://www.gstatic.com/generate_204", + Interval: 300, + }) + + proxyGroups = append(proxyGroups, entity.ProxyGroup{ + Name: "PROXY", + Type: "select", + Proxies: append([]string{"AutoSelect"}, proxyNameList...), + }) + proxyGroups = append(proxyGroups, entity.ProxyGroup{ + Name: "Final", + Type: "select", + Proxies: []string{"DIRECT", "PROXY"}, + }) + defaultDirect := []string{"Apple", "ChinaCompanyIp", "ChinaDomain", "ChinaIp", "ChinaIpV6", "ChinaMedia", "Download", "GoogleCN", "LocalAreaNetwork", "Microsoft", "OneDrive", "UnBan", "Xbox", "360", "4399", "58", "AccelerateDirectSites", "Alibaba", "Baidu", "Bilibili", "ByteDance", "CCTV", "CN", "ChinaDNS", "ChinaNet", "ChinaOneKeyLogin", "DiDi", "Douyu", "GoogleCNProxyIP", "Heytap", "HuaWei", "Iflytek", "Iqiyi", "JD", "Kingsoft", "Kuaishou", "Letv", "MGTVTV", "MI", "MOO", "Marketing", "Meitu", "NetEase", "NetEaseMusic", "PDD", "PPTV", "PrivateTracker", "PublicDirectCDN", "Samsung", "RemoteDesktop", "Sina", "SohuSogo", "SteamCN", "SteamRegionCheck", "Tencent", "TencentLolm", "TencentVideo", "Vip", "Wechat", "Ximalaya", "Xunlei", "Youku", "CherrDirect", "VPN"} + defaultBAN := []string{"BanAD", "BanEasyList", "BanEasyListChina", "BanEasyPrivacy", "BanProgramAD", "MIUIPrivacy"} + addedDirectProxyList := append([]string{"DIRECT", "PROXY"}, proxyNameList...) + addedProxyList := append([]string{"PROXY", "DIRECT"}, proxyNameList...) + banProxyList := append([]string{"REJECT", "PROXY", "DIRECT"}, proxyNameList...) + // 添加 "DIRECT" 和 "PROXY" + + requireChoseList = RemoveDuplicates(requireChoseList) + for _, requirement := range requireChoseList { + var proxyGroup entity.ProxyGroup + if Contains(defaultDirect, requirement) { + proxyGroup = entity.ProxyGroup{ + Name: requirement, + Type: "select", + Proxies: addedDirectProxyList, + } + } else if Contains(defaultBAN, requirement) { + proxyGroup = entity.ProxyGroup{ + Name: requirement, + Type: "select", + Proxies: banProxyList, + } + } else { + proxyGroup = entity.ProxyGroup{ + Name: requirement, + Type: "select", + Proxies: addedProxyList, + } + } + proxyGroups = append(proxyGroups, proxyGroup) + } + orderedGroupNames := []string{"AutoSelect", "PROXY", "Final", "OpenAi", "Claude", "ClaudeAI", "Gemini", "Telegram", "Bilibili", "BilibiliHMT", "Steam", "SteamCN", "SteamRegionCheck", "Porn", "Pornhub", "Pixiv", "JetBrains", "PrivateTracker", "Microsoft", "Bing", "Apple", "AppleNews", "AppleTV", "Github", "Google", "GoogleCNProxyIP", "GoogleEarth", "GoogleFCM", "Youtube", "YoutubeMusic", "Tiktok", "Instagram", "Line", "LineTV", "Wikipedia", "Zoom", "Epic", "MIUIPrivacy", "MI"} + proxyGroups = ReorderProxyGroups(proxyGroups, orderedGroupNames) + // 更新配置中的 ProxyGroups + config.ProxyGroups = proxyGroups + rulesList = append(rulesList, "GEOIP,CN,DIRECT") + rulesList = append(rulesList, "MATCH,Final") + config.Rules = rulesList + + // 打印生成的 ClashConfig(包括 ProxyGroups) + //fmt.Println("Generated ClashConfig:") + //printClashConfigAsYAML(config) // 打印 YAML + // 将结果写入 result.yaml + err = writeConfigToFile(config, "lufxy.yaml") + if err != nil { + log.Fatalf("Failed to write config to file: %v", err) + } + fmt.Println("Configuration written to lufxy.yaml successfully!") +} + +// splitRule 分割规则字符串,例如 "RULE-TYPE,pattern,action" -> ["RULE-TYPE", "pattern", "action"] +func splitRule(rule string) []string { + return strings.Split(rule, ",") +} + +// printClashConfigAsYAML 序列化 ClashConfig 并打印为 YAML 格式 +func printClashConfigAsYAML(config entity.ClashConfig) { + data, err := yaml.Marshal(&config) + if err != nil { + log.Fatalf("Failed to serialize ClashConfig to YAML: %v", err) + } + fmt.Println(string(data)) +} +func writeConfigToFile(config entity.ClashConfig, fileName string) error { + data, err := yaml.Marshal(&config) + if err != nil { + return fmt.Errorf("failed to serialize config to YAML: %w", err) + } + + // 打开文件(如果文件不存在则创建) + file, err := os.Create(fileName) + if err != nil { + return fmt.Errorf("failed to create file %s: %w", fileName, err) + } + defer file.Close() + + // 写入 YAML 数据到文件 + _, err = file.Write(data) + if err != nil { + return fmt.Errorf("failed to write data to file %s: %w", fileName, err) + } + ProcessFile(fileName) + + return nil +} + +func ProcessFile(filePath string) error { + // 打开文件(只读模式) + file, err := os.Open(filePath) + if err != nil { + return fmt.Errorf("无法打开文件: %v", err) + } + defer file.Close() + + // 正则表达式,匹配单引号包裹的大括号内容 + re := regexp.MustCompile(`'(\{.*?\})'`) + + // 用于存储处理后的文件内容 + var processedLines []string + + // 逐行读取文件 + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + // 替换匹配的内容,去掉两边的单引号 + processedLine := re.ReplaceAllString(line, `$1`) + processedLines = append(processedLines, processedLine) + } + + // 检查读取过程中是否发生错误 + if err := scanner.Err(); err != nil { + return fmt.Errorf("读取文件时发生错误: %v", err) + } + + // 关闭当前文件以便后续写入 + file.Close() + + // 打开同一个文件(写模式,覆盖内容) + outputFile, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("无法写入文件: %v", err) + } + defer outputFile.Close() + + // 将处理后的内容写回文件 + writer := bufio.NewWriter(outputFile) + for _, line := range processedLines { + _, err := writer.WriteString(line + "\n") + if err != nil { + return fmt.Errorf("写入文件时发生错误: %v", err) + } + } + writer.Flush() + + return nil +} + +func ExtractProxyNames(data []string) []string { + // 定义正则表达式,匹配 name: "value" + re := regexp.MustCompile(`name:\s*"(.*?)"`) + var names []string + + // 遍历每一行数据并匹配 + for _, line := range data { + matches := re.FindStringSubmatch(line) + if len(matches) > 1 { // matches[1] 是第一个捕获组 + names = append(names, matches[1]) + } + } + + return names +} + +func RemoveDuplicates(input []string) []string { + // 使用 map 来记录已经存在的字符串 + seen := make(map[string]bool) + var result []string + + for _, value := range input { + // 如果字符串没有出现过,则添加到结果切片 + if !seen[value] { + result = append(result, value) + seen[value] = true + } + } + + return result +} + +// dealNoResolve 对输入的字符串切片处理,将倒数第二项是 "no-resolve" 的项与倒数第一项调换位置 +func dealNoResolve(input []string) []string { + // 遍历每一个字符串 + for i, str := range input { + // 按逗号分隔,并去除每一项的前后空格 + parts := strings.Split(str, ",") + for j := range parts { + parts[j] = strings.TrimSpace(parts[j]) + } + + // 检查是否至少有两项,且倒数第二项为 "no-resolve" + if len(parts) >= 2 && parts[len(parts)-2] == "no-resolve" { + // 调换倒数第二项和倒数第一项的位置 + parts[len(parts)-2], parts[len(parts)-1] = parts[len(parts)-1], parts[len(parts)-2] + // 将修改后的字符串重新组合,并替换原来的字符串 + input[i] = strings.Join(parts, ",") + } + } + + return input +} +func dealUserAgent(input []string) []string { + output := []string{} + // 遍历每一个字符串 + for i, str := range input { + // 按逗号分隔,并去除每一项的前后空格 + parts := strings.Split(str, ",") + for j := range parts { + parts[j] = strings.TrimSpace(parts[j]) + } + + if len(parts) >= 1 && parts[0] != "USER-AGENT" && parts[0] != "URL-REGEX" { + output = append(output, input[i]) + } + } + return output +} +func Contains(slice []string, item string) bool { + for _, element := range slice { + if element == item { + return true + } + } + return false +} +func ReorderProxyGroups(proxyGroups []entity.ProxyGroup, nameOrder []string) []entity.ProxyGroup { + // 创建一个用于存储新的 ProxyGroup 切片 + var orderedGroups []entity.ProxyGroup + // 使用 map 记录已经处理过的 ProxyGroup 名称 + processed := make(map[string]bool) + + // 按照 nameOrder 的顺序将对应的 ProxyGroup 添加到 orderedGroups + for _, name := range nameOrder { + for _, group := range proxyGroups { + if group.Name == name { + orderedGroups = append(orderedGroups, group) + processed[name] = true + break + } + } + } + + // 将剩余未处理的 ProxyGroup 添加到 orderedGroups + for _, group := range proxyGroups { + if !processed[group.Name] { + orderedGroups = append(orderedGroups, group) + } + } + + return orderedGroups +} diff --git a/src/util/fileutils.go b/src/util/fileutils.go new file mode 100644 index 0000000..6783c9a --- /dev/null +++ b/src/util/fileutils.go @@ -0,0 +1,59 @@ +package util + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strings" +) + +// ReadFileAndAppendFileName 读取文件内容并将文件名追加到每行末尾 +func ReadFileAndAppendFileName(path string, fileName string) ([]string, error) { + file, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("failed to open file: %v", err) + } + defer file.Close() + + var lines []string + scanner := bufio.NewScanner(file) + + // 获取文件名(去掉扩展名部分) + fileBaseName := strings.TrimSuffix(fileName, filepath.Ext(fileName)) + for scanner.Scan() { + line := scanner.Text() + // 如果行不为空,添加文件名 + if strings.TrimSpace(line) != "" { + lines = append(lines, fmt.Sprintf("%s,%s", line, fileBaseName)) + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("failed to read file content: %v", err) + } + + return lines, nil +} + +// ReadFile 读取文件内容并返回每一行的字符串切片 +func ReadFile(path string) ([]string, error) { + file, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("failed to open file: %v", err) + } + defer file.Close() + + var lines []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + lines = append(lines, line) + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("failed to read file content: %v", err) + } + + return lines, nil +} diff --git a/src/util/filewalkers.go b/src/util/filewalkers.go new file mode 100644 index 0000000..3221e88 --- /dev/null +++ b/src/util/filewalkers.go @@ -0,0 +1,79 @@ +package util + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strings" +) + +// ReadAllListFiles 遍历指定根目录下的所有 .list 文件,将文件内容按行读取并返回。 +// 每行会追加文件名(去掉扩展名),忽略以 # 开头的行和空行。 +func ReadAllListFiles(rootDir string, addFileName bool) (map[string][]string, []string, error) { + listFiles := make(map[string][]string) + fileNames := []string{} + + // 遍历根目录下的所有文件 + err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // 只处理 .list 文件 + if !info.IsDir() && strings.HasSuffix(info.Name(), ".list") { + content, err := readFilteredFile(path, info.Name(), addFileName) + if err != nil { + return fmt.Errorf("failed to read file '%s': %v", path, err) + } + + // 将文件内容存入 map,键为文件名,值为过滤后的文件内容 + listFiles[info.Name()] = content + + // 保存文件名到切片(去掉扩展名部分) + fileNames = append(fileNames, strings.TrimSuffix(info.Name(), filepath.Ext(info.Name()))) + } + return nil + }) + if err != nil { + return nil, nil, fmt.Errorf("failed to walk directory: %v", err) + } + + return listFiles, fileNames, nil +} + +// readFilteredFile 读取文件内容,忽略以 # 开头的行和空行,并将文件名追加到每行末尾 +func readFilteredFile(filePath string, fileName string, addFilename bool) ([]string, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, fmt.Errorf("failed to open file: %v", err) + } + defer file.Close() + + var lines []string + scanner := bufio.NewScanner(file) + + // 获取文件名(去掉扩展名部分) + fileBaseName := strings.TrimSuffix(fileName, filepath.Ext(fileName)) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) // 去除前后空格 + + // 忽略空行和以 # 开头的行 + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + if addFilename { + // 将文件名追加到行末尾 + lines = append(lines, fmt.Sprintf("%s,%s", line, fileBaseName)) + } else { + lines = append(lines, fmt.Sprintf("%s", line)) + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("failed to read file content: %v", err) + } + + return lines, nil +} diff --git a/src/util/read_proxies.go b/src/util/read_proxies.go new file mode 100644 index 0000000..5e6e11c --- /dev/null +++ b/src/util/read_proxies.go @@ -0,0 +1,84 @@ +package util + +import ( + "errors" + "fmt" + "regexp" + "strings" +) + +// ReadProxies 读取并处理 proxies 文件夹下的所有 .list 文件。 +// 返回一个 map[string]interface{},键为文件名,值为解析后的内容。 +func ReadProxies(proxiesDir string) (map[string]interface{}, error) { + // 调用 ReadAllListFiles,addFileName 设置为 false + filesContent, _, err := ReadAllListFiles(proxiesDir, false) + if err != nil { + return nil, fmt.Errorf("failed to read proxies files: %v", err) + } + + // 用于存储最终的 proxies 数据 + proxiesMap := make(map[string]interface{}) + + // 遍历每个文件的内容 + for fileName, lines := range filesContent { + var fileProxies []map[string]interface{} + + for _, line := range lines { + // 将行内容解析为 JSON 格式 + var proxy map[string]interface{} + err := parseProxyConfig(line, &proxy) + if err != nil { + return nil, fmt.Errorf("failed to parse line in file '%s': %v", fileName, err) + } + + // 将解析后的数据添加到当前文件的代理列表中 + fileProxies = append(fileProxies, proxy) + } + + // 将文件的代理列表添加到最终结果中 + proxiesMap[fileName] = fileProxies + } + + return proxiesMap, nil +} + +// parseProxyConfig 将字符串解析为 map[string]interface{} +func parseProxyConfig(proxyStr string, out *map[string]interface{}) error { + // 检查字符串是否以 "{" 开头和以 "}" 结尾 + proxyStr = strings.TrimSpace(proxyStr) + if !strings.HasPrefix(proxyStr, "{") || !strings.HasSuffix(proxyStr, "}") { + return errors.New("invalid proxy string format, must start with '{' and end with '}'") + } + + // 使用正则表达式处理键值对 + re := regexp.MustCompile(`(\w+):\s*([^,{}]+|{[^}]*})`) + matches := re.FindAllStringSubmatch(proxyStr, -1) + + if matches == nil { + return errors.New("failed to parse proxy string, no matches found") + } + + // 构造 map + result := make(map[string]interface{}) + for _, match := range matches { + key := match[1] + value := strings.TrimSpace(match[2]) + + // 如果值是嵌套对象(以 { 开头和以 } 结尾),递归解析 + if strings.HasPrefix(value, "{") && strings.HasSuffix(value, "}") { + var nested map[string]interface{} + err := parseProxyConfig(value, &nested) + if err != nil { + return fmt.Errorf("failed to parse nested object for key '%s': %v", key, err) + } + result[key] = nested + } else { + // 如果值是字符串,移除可能的多余引号 + value = strings.Trim(value, `"`) + result[key] = value + } + } + + *out = result + return nil +} diff --git a/static/background.jpg b/static/background.jpg new file mode 100644 index 0000000..750c095 Binary files /dev/null and b/static/background.jpg differ