First web useful

This commit is contained in:
huyunfan 2024-12-16 05:34:52 +08:00
commit ddaba6ce62
13 changed files with 1135 additions and 0 deletions

65
.gitignore vendored Normal file
View File

@ -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

55
go.mod Normal file
View File

@ -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
)

78
src/aclgit/manager.go Normal file
View File

@ -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
}

View File

@ -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"`
}

View File

@ -0,0 +1,6 @@
package entity
// ExperimentalOptions represents the experimental section in the Clash config
type ExperimentalOptions struct {
IgnoreResolveFail bool `yaml:"ignore-resolve-fail"`
}

10
src/entity/proxy_group.go Normal file
View File

@ -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"`
}

View File

@ -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",
},
}
}

255
src/main/main.go Normal file
View File

@ -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 = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edit Default List</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
ul {
list-style: none;
padding: 0;
}
li {
display: flex;
margin-bottom: 10px;
}
input[type="text"] {
flex: 1;
padding: 5px;
}
button {
margin-left: 10px;
padding: 5px 10px;
}
#clipboard-message {
color: green;
font-weight: bold;
}
/* 设置页面的样式 */
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font-family: Arial, sans-serif;
}
/* 背景容器 */
.background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: url('static/background.jpg') no-repeat center center;
background-size: cover;
}
/* 半透明白色覆盖层 */
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.5); /* 半透明白色 */
}
/* 页面内容 */
.content {
position: relative;
z-index: 1; /* 确保内容在覆盖层之上 */
text-align: center;
padding: 20px;
}
h1 {
margin: 0;
font-size: 2.5em;
color: black; /* 内容颜色 */
}
p {
font-size: 1.2em;
color: black; /* 内容颜色 */
}
.form-container {
position: relative;
max-width: 600px; /* 表单容器的最大宽度 */
margin: 50px auto; /* 居中 */
padding: 20px;
background: rgba(255, 255, 255, 0.7); /* 半透明白色背景 */
border-radius: 15px; /* 圆角 */
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); /* 添加阴影 */
}
</style>
</head>
<body>
<div class="background"></div>
<!-- 覆盖层 -->
<div class="overlay"></div>
<div class="content">
<h1>Welcome to 404 Clash Config Site !~~</h1>
<div class="form-container">
<form method="POST" action="/update">
<ul id="list">
{{range $index, $line := .lines}}
<li>
<input type="text" name="lines" value="{{$line}}" />
<button type="button" onclick="removeLine(this)">Remove</button>
</li>
{{end}}
</ul>
<button type="button" onclick="addLine()">Add Line</button>
<button type="submit">Save</button>
</form>
<br>
<button onclick="downloadClashConfig()">Download Clash Config</button>
<button onclick="copyToClipboard()">Copy Clash Config to Clipboard</button>
</div>
<p id="clipboard-message" style="display: none;">Clash configuration copied to clipboard!</p>
</div>
<script>
function addLine() {
const list = document.getElementById('list');
const li = document.createElement('li');
li.innerHTML = '<input type="text" name="lines" value="" /><button type="button" onclick="removeLine(this)">Remove</button>';
list.appendChild(li);
}
function removeLine(button) {
const li = button.parentElement;
li.remove();
}
function downloadClashConfig() {
window.location.href = "/download";
}
function copyToClipboard() {
fetch('/clipboard')
.then(response => response.json())
.then(data => {
navigator.clipboard.writeText(data.content).then(() => {
const message = document.getElementById('clipboard-message');
message.style.display = 'block';
setTimeout(() => {
message.style.display = 'none';
}, 2000);
});
})
.catch(err => console.error('Failed to copy:', err));
}
</script>
</body>
</html>
`
// 文件路径
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 提供服务
}

View File

@ -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
}

59
src/util/fileutils.go Normal file
View File

@ -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
}

79
src/util/filewalkers.go Normal file
View File

@ -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
}

84
src/util/read_proxies.go Normal file
View File

@ -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) {
// 调用 ReadAllListFilesaddFileName 设置为 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
}

BIN
static/background.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB