First web useful
This commit is contained in:
commit
ddaba6ce62
65
.gitignore
vendored
Normal file
65
.gitignore
vendored
Normal 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
55
go.mod
Normal 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
78
src/aclgit/manager.go
Normal 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
|
||||||
|
}
|
||||||
16
src/entity/clash_config.go
Normal file
16
src/entity/clash_config.go
Normal 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"`
|
||||||
|
}
|
||||||
6
src/entity/experimental.go
Normal file
6
src/entity/experimental.go
Normal 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
10
src/entity/proxy_group.go
Normal 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"`
|
||||||
|
}
|
||||||
77
src/example/example_config.go
Normal file
77
src/example/example_config.go
Normal 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
255
src/main/main.go
Normal 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 提供服务
|
||||||
|
}
|
||||||
351
src/service/generate_config.go
Normal file
351
src/service/generate_config.go
Normal 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
59
src/util/fileutils.go
Normal 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
79
src/util/filewalkers.go
Normal 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
84
src/util/read_proxies.go
Normal 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) {
|
||||||
|
// 调用 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
|
||||||
|
}
|
||||||
BIN
static/background.jpg
Normal file
BIN
static/background.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 180 KiB |
Loading…
x
Reference in New Issue
Block a user