YimoEx commited on
Commit
bb9df9e
·
1 Parent(s): 41a2e38
This view is limited to 50 files because it contains too many changes.   See raw diff
.dockerignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .DS_Store
2
+ *.DS_Store
3
+ db
4
+ logs
5
+ webs
6
+ README.md
7
+ README.en-US.md
8
+ *.sh
9
+ sublink_amd64
10
+ sublink_arm64
11
+ sublink.exe
12
+ install.sh
.github/workflows/docker-image.yml ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Build and Push Multi-Arch Docker Image
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+ branches:
9
+ - main
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+
15
+ steps:
16
+ # 检出代码
17
+ - name: Checkout code
18
+ uses: actions/checkout@v2
19
+
20
+ # 设置 QEMU 以支持跨架构构建
21
+ - name: Set up QEMU
22
+ uses: docker/setup-qemu-action@v2
23
+
24
+ # 设置 Docker Buildx
25
+ - name: Set up Docker Buildx
26
+ uses: docker/setup-buildx-action@v2
27
+
28
+ # 登录到 Docker Hub
29
+ - name: Log in to Docker Hub
30
+ uses: docker/login-action@v2
31
+ with:
32
+ username: ${{ secrets.DOCKER_USERNAME }}
33
+ password: ${{ secrets.DOCKER_PASSWORD }}
34
+
35
+ # 构建并推送多平台 Docker 镜像
36
+ - name: Build and Push Multi-Arch Docker Image
37
+ run: |
38
+ docker buildx build --platform linux/amd64,linux/arm64 \
39
+ -t jaaksi/sublinkx:latest \
40
+ --push .
.gitignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ .DS_Store
2
+ *.DS_Store
3
+ db
4
+ logs
5
+ sublink_amd64
6
+ sublink_arm64
7
+ sublink.exe
Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Build stage
2
+ FROM golang:1.22.2-alpine AS builder
3
+ WORKDIR /app
4
+ COPY . .
5
+ RUN go mod download
6
+ RUN go build -o sublinkX
7
+
8
+ # Final stage
9
+ FROM alpine:latest
10
+ WORKDIR /app
11
+
12
+ # 设置时区为 Asia/Shanghai
13
+ ENV TZ=Asia/Shanghai
14
+
15
+ COPY --from=builder /app/sublinkX /app/sublinkX
16
+ EXPOSE 8000
17
+ CMD ["/app/sublinkX"]
18
+
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2024-SublinkX
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.en-US.md ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center"><img src="webs/src/assets/logo.png" width="150px" height="150px" /></div>
2
+
3
+ <div align="center"> <img src="https://img.shields.io/badge/Vue-5.0.8-brightgreen.svg"/> <img src="https://img.shields.io/badge/Go-1.22.0-green.svg"/> <img src="https://img.shields.io/badge/Element Plus-2.6.1-blue.svg"/> <img src="https://img.shields.io/badge/license-MIT-green.svg"/> <a href="https://t.me/+u6gLWF0yP5NiZWQ1" target="_blank"> <img src="https://img.shields.io/badge/TG-交流群-orange.svg"/> </a> <div align="center">Chinese|<a href="README.en-US.md">English</div></div>
4
+
5
+ ## [Project Profile]
6
+
7
+ Project based on sublink project secondary development: github.com/jaaksii/sublink
8
+
9
+ Front end based on github.com/youlaitech/vue3-element-admin
10
+
11
+ Go+gin+gorm on the backend
12
+
13
+ Default account admin password 123456 modify yourself
14
+
15
+ Because rewriting has a lot of layout structure and a little less functionality
16
+
17
+ ## [Project Features]
18
+
19
+ High degree of freedom and security, ability to log access subscriptions, easy configuration
20
+
21
+ Binary compilation without Docker containers
22
+
23
+ Currently only supported clients: v2ray crash surge
24
+
25
+ v2ray is base64 generic format
26
+
27
+ Clash Support Protocol:ss ssr trojan vmess vless hy hy2 tuic
28
+
29
+ surge Support Protocol:ss trojan vmess hy2 tuic
30
+
31
+ ## [Project Preview]
32
+
33
+ ! [1712594176714](webs/src/assets/1.png)! [1712594176714](webs/src/assets/2.png)
34
+
35
+ ## [update description]
36
+
37
+ Add specified client
38
+
39
+ Fix menu update function does not update menu version
40
+
41
+ ## [Installation Instructions]### Linux Mode:
42
+ ```curl -s -H "Cache-Control: no-cache" -H "Pragma: no-cache" https://raw.githubusercontent.com/gooaclok819/sublinkX/main/install.sh | sudo bash ```
43
+
44
+ ```sublink``outgoing menu
45
+
46
+ Then enter the installation script
47
+
48
+ ### Docker method:
49
+
50
+ Create a directory where you need it, such as mkdir sublinkx
51
+
52
+ Then cd enters this directory, enter the following command and the data will be mounted.
53
+
54
+ DB and template ```docker run --name sublinkx -p 8000:8000 \-v $PWD/db:/app/db \-v $PWD/template:/app/template \-v $PWD/logs:/app/logs \-d jaaksi/sublinkx ```
55
+
56
+ ## Stargazers over time [! [Stargazers over time](https://starchart.cc/gooaclok819/sublinkX.svg? variant=adaptive)](https://starchart.cc/gooaclok819/sublinkX)
README.md CHANGED
@@ -1,11 +1,81 @@
1
- ---
2
- title: Sublinker
3
- emoji: 🦀
4
- colorFrom: blue
5
- colorTo: red
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+ <img src="webs/src/assets/logo.png" width="150px" height="150px" />
3
+ </div>
4
+
5
+ <div align="center">
6
+ <img src="https://img.shields.io/badge/Vue-5.0.8-brightgreen.svg"/>
7
+ <img src="https://img.shields.io/badge/Go-1.22.0-green.svg"/>
8
+ <img src="https://img.shields.io/badge/Element Plus-2.6.1-blue.svg"/>
9
+ <img src="https://img.shields.io/badge/license-MIT-green.svg"/>
10
+ <a href="https://t.me/+u6gLWF0yP5NiZWQ1" target="_blank">
11
+ <img src="https://img.shields.io/badge/TG-交流群-orange.svg"/>
12
+ </a>
13
+ <div align="center"> 中文 | <a href="README.en-US.md">English</div>
14
+ </div>
15
+
16
+ ## [项目简介]
17
+
18
+ 项目基于sublink项目二次开发:https://github.com/jaaksii/sublink
19
+
20
+ 前端基于:https://github.com/youlaitech/vue3-element-admin
21
+
22
+ 后端采用go+gin+gorm
23
+
24
+ 默认账号admin 密码123456 自行修改
25
+
26
+ 因为重写目前还有很多布局结构以及功能稍少
27
+
28
+ ## [项目特色]
29
+
30
+ 自由度和安全性较高,能够记录访问订阅,配置轻松
31
+
32
+ 二进制编译无需Docker容器
33
+
34
+ 目前仅支持客户端:v2ray clash surge
35
+
36
+ v2ray为base64通用格式
37
+
38
+ clash支持协议:ss ssr trojan vmess vless hy hy2 tuic
39
+
40
+ surge支持协议:ss trojan vmess hy2 tuic
41
+
42
+ ## [项目预览]
43
+
44
+ ![1712594176714](webs/src/assets/1.png)
45
+ ![1712594176714](webs/src/assets/2.png)
46
+
47
+ ## [更新说明]
48
+
49
+ 新增指定客户端
50
+
51
+ 修复菜单更新功能没更新菜单版本
52
+
53
+ ## [安装说明]
54
+ ### linux方式:
55
+ ```
56
+ curl -s -H "Cache-Control: no-cache" -H "Pragma: no-cache" https://raw.githubusercontent.com/gooaclok819/sublinkX/main/install.sh | sudo bash
57
+ ```
58
+
59
+ ```sublink``` 呼出菜单
60
+
61
+ 然后输入安装脚本即可
62
+
63
+ ### docker方式:
64
+
65
+ 在自己需要的位置创建一个目录比如mkdir sublinkx
66
+
67
+ 然后cd进入这个目录,输入下面指令之后数据就挂载过来
68
+
69
+ 需要备份的就是db和template
70
+ ```
71
+ docker run --name sublinkx -p 8000:8000 \
72
+ -v $PWD/db:/app/db \
73
+ -v $PWD/template:/app/template \
74
+ -v $PWD/logs:/app/logs \
75
+ -d jaaksi/sublinkx
76
+ ```
77
+
78
+ ## Stargazers over time
79
+ [![Stargazers over time](https://starchart.cc/gooaclok819/sublinkX.svg?variant=adaptive)](https://starchart.cc/gooaclok819/sublinkX)
80
+
81
+
api/auth.go ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package api
2
+
3
+ import (
4
+ "log"
5
+ "sublink/middlewares"
6
+ "sublink/models"
7
+ "sublink/utils"
8
+ "time"
9
+
10
+ "github.com/dgrijalva/jwt-go"
11
+ "github.com/gin-gonic/gin"
12
+ )
13
+
14
+ // 获取token
15
+ func GetToken(username string) (string, error) {
16
+ c := &middlewares.JwtClaims{
17
+ Username: username,
18
+ StandardClaims: jwt.StandardClaims{
19
+ ExpiresAt: time.Now().Add(time.Hour * 24 * 14).Unix(), // 设置14天过期
20
+ IssuedAt: time.Now().Unix(), // 签发时间
21
+ Subject: username, // 用户
22
+ },
23
+ }
24
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
25
+ return token.SignedString(middlewares.Secret)
26
+ }
27
+
28
+ // 获取captcha图形验证码
29
+ func GetCaptcha(c *gin.Context) {
30
+ id, bs4, _, err := utils.GetCaptcha()
31
+ if err != nil {
32
+ log.Println("获取验证码失败")
33
+ c.JSON(400, gin.H{
34
+ "msg": "获取验证码失败",
35
+ })
36
+ return
37
+ }
38
+ c.JSON(200, gin.H{
39
+ "code": "00000",
40
+ "data": gin.H{
41
+ "captchaKey": id,
42
+ "captchaBase64": bs4,
43
+ },
44
+ "msg": "获取验证码成功",
45
+ })
46
+
47
+ }
48
+
49
+ // 用户登录
50
+ func UserLogin(c *gin.Context) {
51
+ username := c.PostForm("username")
52
+ password := c.PostForm("password")
53
+ captchaCode := c.PostForm("captchaCode")
54
+ captchaKey := c.PostForm("captchaKey")
55
+ // 验证验证码
56
+ if !utils.VerifyCaptcha(captchaKey, captchaCode) {
57
+ log.Println("验证码错误")
58
+ c.JSON(400, gin.H{
59
+ "msg": "验证码错误",
60
+ })
61
+ return
62
+ }
63
+ user := &models.User{Username: username, Password: password}
64
+ err := user.Verify()
65
+ if err != nil {
66
+ log.Println("账号或者密码错误")
67
+ c.JSON(400, gin.H{
68
+ "msg": "账号或者密码错误",
69
+ })
70
+ return
71
+ }
72
+ // 生成token
73
+ token, err := GetToken(username)
74
+ if err != nil {
75
+ log.Println("获取token失败")
76
+ c.JSON(400, gin.H{
77
+ "msg": "获取token失败",
78
+ })
79
+ return
80
+ }
81
+ // 登录成功返回token
82
+ c.JSON(200, gin.H{
83
+ "code": "00000",
84
+ "data": gin.H{
85
+ "accessToken": token,
86
+ "tokenType": "Bearer",
87
+ "refreshToken": nil,
88
+ "expires": nil,
89
+ },
90
+ "msg": "登录成功",
91
+ })
92
+ }
93
+ func UserOut(c *gin.Context) {
94
+ // 拿到jwt中的username
95
+ if _, Is := c.Get("username"); Is {
96
+ c.JSON(200, gin.H{
97
+ "code": "00000",
98
+ "msg": "退出成功",
99
+ })
100
+ }
101
+ }
api/clients.go ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package api
2
+
3
+ import (
4
+ "crypto/md5"
5
+ "encoding/hex"
6
+ "encoding/json"
7
+ "fmt"
8
+ "io"
9
+ "log"
10
+ "net/http"
11
+ "net/url"
12
+ "strings"
13
+ "sublink/models"
14
+ "sublink/node"
15
+
16
+ "github.com/gin-gonic/gin"
17
+ )
18
+
19
+ var SunName string
20
+
21
+ // md5加密
22
+ func Md5(src string) string {
23
+ m := md5.New()
24
+ m.Write([]byte(src))
25
+ res := hex.EncodeToString(m.Sum(nil))
26
+ return res
27
+ }
28
+ func GetClient(c *gin.Context) {
29
+ // 获取协议头
30
+ token := c.Query("token")
31
+ ClientIndex := c.Query("client") // 客户端标识
32
+ if token == "" {
33
+ log.Println("token为空")
34
+ c.Writer.WriteString("token为空")
35
+ return
36
+ }
37
+ // fmt.Println(c.Query("token"))
38
+ Sub := new(models.Subcription)
39
+ // 获取所有订阅
40
+ list, _ := Sub.List()
41
+ // 查找订阅是否包含此名字
42
+ for _, sub := range list {
43
+ // 数据库订阅名字赋值变量
44
+ SunName = sub.Name
45
+ //查找token的md5是否匹配并且转换成小写
46
+ if Md5(SunName) == strings.ToLower(token) {
47
+ // 判断是否带客户端参数
48
+ switch ClientIndex {
49
+ case "clash":
50
+ GetClash(c)
51
+ return
52
+ case "surge":
53
+ GetSurge(c)
54
+ return
55
+ case "v2ray":
56
+ GetV2ray(c)
57
+ return
58
+ }
59
+ // 自动识别客户端
60
+ ClientList := []string{"clash", "surge"}
61
+ for k, v := range c.Request.Header {
62
+ if k == "User-Agent" {
63
+ for _, UserAgent := range v {
64
+ if UserAgent == "" {
65
+ fmt.Println("User-Agent为空")
66
+ }
67
+ // fmt.Println("协议头:", UserAgent)
68
+ // 遍历客户端列表
69
+ // SunName = sub.Name
70
+ for _, client := range ClientList {
71
+ // fmt.Println(strings.ToLower(UserAgent), strings.ToLower(client))
72
+ // fmt.Println(strings.Contains(strings.ToLower(UserAgent), strings.ToLower(client)))
73
+ if strings.Contains(strings.ToLower(UserAgent), strings.ToLower(client)) {
74
+ // fmt.Println("客户端", client)
75
+ switch client {
76
+ case "clash":
77
+ GetClash(c)
78
+ return
79
+ case "surge":
80
+ GetSurge(c)
81
+ return
82
+ default:
83
+ fmt.Println("未知客户端") // 这个应该是不能达到的,因为已经在上面列出所有情况
84
+ }
85
+ // 找到匹配的客户端后退出循环
86
+
87
+ }
88
+ }
89
+ GetV2ray(c)
90
+ }
91
+
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ }
98
+ func GetV2ray(c *gin.Context) {
99
+ var sub models.Subcription
100
+ if SunName == "" {
101
+ c.Writer.WriteString("订阅名为空")
102
+ return
103
+ }
104
+ // subname := c.Param("subname")
105
+ // subname := SunName
106
+ // subname = node.Base64Decode(subname)
107
+ sub.Name = SunName
108
+ err := sub.Find()
109
+ if err != nil {
110
+ c.Writer.WriteString("找不到这个订阅:" + SunName)
111
+ return
112
+ }
113
+ err = sub.GetSub()
114
+ if err != nil {
115
+ c.Writer.WriteString("读取错误")
116
+ return
117
+ }
118
+ baselist := ""
119
+ for _, v := range sub.Nodes {
120
+ switch {
121
+ // 如果包含多条节点
122
+ case strings.Contains(v.Link, ","):
123
+ links := strings.Split(v.Link, ",")
124
+ baselist += strings.Join(links, "\n") + "\n"
125
+ continue
126
+ //如果是订阅转换
127
+ case strings.Contains(v.Link, "http://") || strings.Contains(v.Link, "https://"):
128
+ resp, err := http.Get(v.Link)
129
+ if err != nil {
130
+ log.Println(err)
131
+ return
132
+ }
133
+ defer resp.Body.Close()
134
+ body, _ := io.ReadAll(resp.Body)
135
+ nodes := node.Base64Decode(string(body))
136
+ baselist += nodes + "\n"
137
+ // 默认
138
+ default:
139
+ baselist += v.Link + "\n"
140
+ }
141
+ }
142
+ c.Set("subname", SunName)
143
+ filename := fmt.Sprintf("%s.txt", SunName)
144
+ encodedFilename := url.QueryEscape(filename)
145
+ c.Writer.Header().Set("Content-Disposition", "inline; filename*=utf-8''"+encodedFilename)
146
+ c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
147
+ c.Writer.WriteString(node.Base64Encode(baselist))
148
+ }
149
+ func GetClash(c *gin.Context) {
150
+ var sub models.Subcription
151
+ // subname := c.Param("subname")
152
+ // subname := node.Base64Decode(SunName)
153
+ sub.Name = SunName
154
+ err := sub.Find()
155
+ if err != nil {
156
+ c.Writer.WriteString("找不到这个订阅:" + SunName)
157
+ return
158
+ }
159
+ err = sub.GetSub()
160
+ if err != nil {
161
+ c.Writer.WriteString("读取错误")
162
+ return
163
+ }
164
+ urls := []string{}
165
+ for _, v := range sub.Nodes {
166
+ switch {
167
+ // 如果包含多条节点
168
+ case strings.Contains(v.Link, ","):
169
+ links := strings.Split(v.Link, ",")
170
+ urls = append(urls, links...)
171
+ continue
172
+ //如果是订阅转换
173
+ case strings.Contains(v.Link, "http://") || strings.Contains(v.Link, "https://"):
174
+ resp, err := http.Get(v.Link)
175
+ if err != nil {
176
+ log.Println(err)
177
+ return
178
+ }
179
+ defer resp.Body.Close()
180
+ body, _ := io.ReadAll(resp.Body)
181
+ nodes := node.Base64Decode(string(body))
182
+ links := strings.Split(nodes, "\n")
183
+ urls = append(urls, links...)
184
+ // 默认
185
+ default:
186
+ urls = append(urls, v.Link)
187
+ }
188
+ }
189
+
190
+ var configs node.SqlConfig
191
+ err = json.Unmarshal([]byte(sub.Config), &configs)
192
+ if err != nil {
193
+ c.Writer.WriteString("配置读取错误")
194
+ return
195
+ }
196
+ DecodeClash, err := node.EncodeClash(urls, configs)
197
+ if err != nil {
198
+ c.Writer.WriteString(err.Error())
199
+ return
200
+ }
201
+ c.Set("subname", SunName)
202
+ filename := fmt.Sprintf("%s.yaml", SunName)
203
+ encodedFilename := url.QueryEscape(filename)
204
+ c.Writer.Header().Set("Content-Disposition", "inline; filename*=utf-8''"+encodedFilename)
205
+ c.Writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
206
+ c.Writer.WriteString(string(DecodeClash))
207
+ }
208
+ func GetSurge(c *gin.Context) {
209
+ var sub models.Subcription
210
+ // subname := c.Param("subname")
211
+ // subname := node.Base64Decode(SunName)
212
+ sub.Name = SunName
213
+ err := sub.Find()
214
+ if err != nil {
215
+ c.Writer.WriteString("找不到这个订阅:" + SunName)
216
+ return
217
+ }
218
+ err = sub.GetSub()
219
+ if err != nil {
220
+ c.Writer.WriteString("读取错误")
221
+ return
222
+ }
223
+ urls := []string{}
224
+ for _, v := range sub.Nodes {
225
+ switch {
226
+ // 如果包含多条节点
227
+ case strings.Contains(v.Link, ","):
228
+ links := strings.Split(v.Link, ",")
229
+ urls = append(urls, links...)
230
+ continue
231
+ //如果是订阅转换
232
+ case strings.Contains(v.Link, "http://") || strings.Contains(v.Link, "https://"):
233
+ resp, err := http.Get(v.Link)
234
+ if err != nil {
235
+ log.Println(err)
236
+ return
237
+ }
238
+ defer resp.Body.Close()
239
+ body, _ := io.ReadAll(resp.Body)
240
+ nodes := node.Base64Decode(string(body))
241
+ links := strings.Split(nodes, "\n")
242
+ urls = append(urls, links...)
243
+ // 默认
244
+ default:
245
+ urls = append(urls, v.Link)
246
+ }
247
+ }
248
+
249
+ var configs node.SqlConfig
250
+ err = json.Unmarshal([]byte(sub.Config), &configs)
251
+ if err != nil {
252
+ c.Writer.WriteString("配置读取错误")
253
+ return
254
+ }
255
+ // log.Println("surge路径:", configs)
256
+ DecodeClash, err := node.EncodeSurge(urls, configs)
257
+ if err != nil {
258
+ c.Writer.WriteString(err.Error())
259
+ return
260
+ }
261
+ c.Set("subname", SunName)
262
+ filename := fmt.Sprintf("%s.conf", SunName)
263
+ encodedFilename := url.QueryEscape(filename)
264
+ c.Writer.Header().Set("Content-Disposition", "inline; filename*=utf-8''"+encodedFilename)
265
+ c.Writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
266
+ host := c.Request.Host
267
+ url := c.Request.URL.String()
268
+ // 如果包含头部更新信息
269
+ if strings.Contains(DecodeClash, "#!MANAGED-CONFIG") {
270
+ c.Writer.WriteString(DecodeClash)
271
+ return
272
+ }
273
+ // 否则就插入头部更新信息
274
+ interval := fmt.Sprintf("#!MANAGED-CONFIG %s interval=86400 strict=false", host+url)
275
+ c.Writer.WriteString(string(interval + "\n" + DecodeClash))
276
+ }
api/mentu.go ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package api
2
+
3
+ import (
4
+ "github.com/gin-gonic/gin"
5
+ )
6
+
7
+ type Meta struct {
8
+ Title string `json:"title"`
9
+ Icon string `json:"icon"`
10
+ Hidden bool `json:"hidden"`
11
+ Roles []string `json:"roles"`
12
+ KeepAlive bool `json:"keepAlive,omitempty"`
13
+ }
14
+
15
+ type Child struct {
16
+ Path string `json:"path"`
17
+ Component string `json:"component"`
18
+ Name string `json:"name"`
19
+ Meta Meta `json:"meta"`
20
+ }
21
+
22
+ type Menu struct {
23
+ Path string `json:"path"`
24
+ Component string `json:"component"`
25
+ Redirect string `json:"redirect"`
26
+ Name string `json:"name"`
27
+ Meta Meta `json:"meta"`
28
+ Children []Child `json:"children"`
29
+ }
30
+
31
+ func GetMenus(c *gin.Context) {
32
+ menus := []Menu{
33
+ {
34
+ Path: "/system",
35
+ Component: "Layout",
36
+ // Redirect: "/system/user",
37
+ Name: "system",
38
+ Meta: Meta{
39
+ Title: "system",
40
+ Icon: "system",
41
+ Hidden: true,
42
+ Roles: []string{"ADMIN"},
43
+ },
44
+ Children: []Child{
45
+ {
46
+ Path: "user/set",
47
+ Component: "system/user/set",
48
+ Name: "Userset",
49
+ Meta: Meta{
50
+ Title: "userset",
51
+ Icon: "role",
52
+ Hidden: true,
53
+ Roles: []string{"ADMIN"},
54
+ KeepAlive: true,
55
+ },
56
+ },
57
+ },
58
+ },
59
+ // 订阅管理
60
+ {
61
+ Path: "/subcription",
62
+ Component: "Layout",
63
+ Redirect: "/subcription/subs",
64
+ Name: "subcription",
65
+ Meta: Meta{
66
+ Title: "subcription",
67
+ Icon: "client",
68
+ Hidden: false,
69
+ Roles: []string{"ADMIN"},
70
+ },
71
+ Children: []Child{
72
+ {
73
+ Path: "subs",
74
+ Component: "subcription/subs",
75
+ Name: "Subs",
76
+ Meta: Meta{
77
+ Title: "sublist",
78
+ Icon: "link",
79
+ Hidden: false,
80
+ Roles: []string{"ADMIN"},
81
+ KeepAlive: true,
82
+ },
83
+ },
84
+ {
85
+ Path: "nodes",
86
+ Component: "subcription/nodes",
87
+ Name: "Nodes",
88
+ Meta: Meta{
89
+ Title: "nodelist",
90
+ Icon: "publish",
91
+ Hidden: false,
92
+ Roles: []string{"ADMIN"},
93
+ KeepAlive: true,
94
+ },
95
+ },
96
+ {
97
+ Path: "template",
98
+ Component: "subcription/template",
99
+ Name: "Template",
100
+ Meta: Meta{
101
+ Title: "templatelist",
102
+ Icon: "document",
103
+ Hidden: false,
104
+ Roles: []string{"ADMIN"},
105
+ KeepAlive: true,
106
+ },
107
+ },
108
+ },
109
+ },
110
+ }
111
+ c.JSON(200, gin.H{
112
+ "code": "00000",
113
+ "data": menus,
114
+ "msg": "获取成功",
115
+ })
116
+ }
api/node.go ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package api
2
+
3
+ import (
4
+ "log"
5
+ "net/url"
6
+ "strconv"
7
+ "strings"
8
+ "sublink/models"
9
+ "sublink/node"
10
+ "time"
11
+
12
+ "github.com/gin-gonic/gin"
13
+ )
14
+
15
+ func NodeUpdadte(c *gin.Context) {
16
+ var node models.Node
17
+ name := c.PostForm("name")
18
+ oldname := c.PostForm("oldname")
19
+ oldlink := c.PostForm("oldlink")
20
+ link := c.PostForm("link")
21
+ if name == "" || link == "" {
22
+ c.JSON(400, gin.H{
23
+ "msg": "节点名称 or 备注不能为空",
24
+ })
25
+ return
26
+ }
27
+ // 查找旧节点
28
+ node.Name = oldname
29
+ node.Link = oldlink
30
+ err := node.Find()
31
+ if err != nil {
32
+ c.JSON(400, gin.H{
33
+ "msg": err.Error(),
34
+ })
35
+ return
36
+ }
37
+ node.Name = name
38
+ node.Link = link
39
+ err = node.Update()
40
+ if err != nil {
41
+ c.JSON(400, gin.H{
42
+ "msg": "更新失败",
43
+ })
44
+ return
45
+ }
46
+ c.JSON(200, gin.H{
47
+ "code": "00000",
48
+ "msg": "更新成功",
49
+ })
50
+ }
51
+
52
+ // 获取节点列表
53
+ func NodeGet(c *gin.Context) {
54
+ var Node models.Node
55
+ nodes, err := Node.List()
56
+ if err != nil {
57
+ c.JSON(500, gin.H{
58
+ "msg": "node list error",
59
+ })
60
+ return
61
+ }
62
+ c.JSON(200, gin.H{
63
+ "code": "00000",
64
+ "data": nodes,
65
+ "msg": "node get",
66
+ })
67
+ }
68
+
69
+ // 添加节点
70
+ func NodeAdd(c *gin.Context) {
71
+ var Node models.Node
72
+ link := c.PostForm("link")
73
+ name := c.PostForm("name")
74
+ if link == "" {
75
+ c.JSON(400, gin.H{
76
+ "msg": "link 不能为空",
77
+ })
78
+ return
79
+ }
80
+ if !strings.Contains(link, "://") {
81
+ c.JSON(400, gin.H{
82
+ "msg": "link 必须包含 ://",
83
+ })
84
+ return
85
+ }
86
+ Node.Name = name
87
+ if name == "" {
88
+ u, err := url.Parse(link)
89
+ if err != nil {
90
+ log.Println(err)
91
+ return
92
+ }
93
+ switch {
94
+ case u.Scheme == "ss":
95
+ ss, err := node.DecodeSSURL(link)
96
+ if err != nil {
97
+ log.Println(err)
98
+ return
99
+ }
100
+ Node.Name = ss.Name
101
+ case u.Scheme == "ssr":
102
+ ssr, err := node.DecodeSSRURL(link)
103
+ if err != nil {
104
+ log.Println(err)
105
+ return
106
+ }
107
+ Node.Name = ssr.Qurey.Remarks
108
+ case u.Scheme == "trojan":
109
+ trojan, err := node.DecodeTrojanURL(link)
110
+ if err != nil {
111
+ log.Println(err)
112
+ return
113
+ }
114
+ Node.Name = trojan.Name
115
+ case u.Scheme == "vmess":
116
+ vmess, err := node.DecodeVMESSURL(link)
117
+ if err != nil {
118
+ log.Println(err)
119
+ return
120
+ }
121
+ Node.Name = vmess.Ps
122
+ case u.Scheme == "vless":
123
+ vless, err := node.DecodeVLESSURL(link)
124
+ if err != nil {
125
+ log.Println(err)
126
+ return
127
+ }
128
+ Node.Name = vless.Name
129
+ case u.Scheme == "hy" || u.Scheme == "hysteria":
130
+ hy, err := node.DecodeHYURL(link)
131
+ if err != nil {
132
+ log.Println(err)
133
+ return
134
+ }
135
+ Node.Name = hy.Name
136
+ case u.Scheme == "hy2" || u.Scheme == "hysteria2":
137
+ hy2, err := node.DecodeHY2URL(link)
138
+ if err != nil {
139
+ log.Println(err)
140
+ return
141
+ }
142
+ Node.Name = hy2.Name
143
+ case u.Scheme == "tuic":
144
+ tuic, err := node.DecodeTuicURL(link)
145
+ if err != nil {
146
+ log.Println(err)
147
+ return
148
+ }
149
+ Node.Name = tuic.Name
150
+ }
151
+ }
152
+ Node.Link = link
153
+ Node.CreateDate = time.Now().Format("2006-01-02 15:04:05")
154
+ err := Node.Find()
155
+ // 如果找到记录说明重复
156
+ if err == nil {
157
+ Node.Name = name + " " + time.Now().Format("2006-01-02 15:04:05")
158
+ }
159
+ err = Node.Add()
160
+ if err != nil {
161
+ c.JSON(400, gin.H{
162
+ "msg": "添加失败检查一下是否节点重复",
163
+ })
164
+ return
165
+ }
166
+ c.JSON(200, gin.H{
167
+ "code": "00000",
168
+ "msg": "添加成功",
169
+ })
170
+ }
171
+
172
+ // 删除节点
173
+ func NodeDel(c *gin.Context) {
174
+ var Node models.Node
175
+ id := c.Query("id")
176
+ if id == "" {
177
+ c.JSON(400, gin.H{
178
+ "msg": "id 不能为空",
179
+ })
180
+ return
181
+ }
182
+ x, _ := strconv.Atoi(id)
183
+ Node.ID = x
184
+ err := Node.Del()
185
+ if err != nil {
186
+ c.JSON(400, gin.H{
187
+ "msg": "删除失败",
188
+ })
189
+ return
190
+ }
191
+ c.JSON(200, gin.H{
192
+ "code": "00000",
193
+ "msg": "删除成功",
194
+ })
195
+ }
196
+
197
+ // 节点统计
198
+ func NodesTotal(c *gin.Context) {
199
+ var Node models.Node
200
+ nodes, err := Node.List()
201
+ count := len(nodes)
202
+ if err != nil {
203
+ c.JSON(500, gin.H{
204
+ "msg": "获取不到节点统计",
205
+ })
206
+ return
207
+ }
208
+ c.JSON(200, gin.H{
209
+ "code": "00000",
210
+ "data": count,
211
+ "msg": "取得节点统计",
212
+ })
213
+ }
api/sub.go ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package api
2
+
3
+ import (
4
+ "strconv"
5
+ "strings"
6
+ "sublink/models"
7
+ "time"
8
+
9
+ "github.com/gin-gonic/gin"
10
+ )
11
+
12
+ func SubTotal(c *gin.Context) {
13
+ var Sub models.Subcription
14
+ subs, err := Sub.List()
15
+ count := len(subs)
16
+ if err != nil {
17
+ c.JSON(500, gin.H{
18
+ "msg": "取得订阅总数失败",
19
+ })
20
+ return
21
+ }
22
+ c.JSON(200, gin.H{
23
+ "code": "00000",
24
+ "data": count,
25
+ "msg": "取得订阅总数",
26
+ })
27
+ }
28
+
29
+ // 获取订阅列表
30
+ func SubGet(c *gin.Context) {
31
+ var Sub models.Subcription
32
+ Subs, err := Sub.List()
33
+ if err != nil {
34
+ c.JSON(500, gin.H{
35
+ "msg": "node list error",
36
+ })
37
+ return
38
+ }
39
+ c.JSON(200, gin.H{
40
+ "code": "00000",
41
+ "data": Subs,
42
+ "msg": "node get",
43
+ })
44
+ }
45
+
46
+ // 添加节点
47
+ func SubAdd(c *gin.Context) {
48
+ var sub models.Subcription
49
+ name := c.PostForm("name")
50
+ config := c.PostForm("config")
51
+ nodes := c.PostForm("nodes")
52
+ if name == "" || nodes == "" {
53
+ c.JSON(400, gin.H{
54
+ "msg": "订阅名称 or 节点不能为空",
55
+ })
56
+ return
57
+ }
58
+ sub.Nodes = []models.Node{}
59
+ for _, v := range strings.Split(nodes, ",") {
60
+ var node models.Node
61
+ node.Name = v
62
+ err := node.Find()
63
+ if err != nil {
64
+ continue
65
+ }
66
+ sub.Nodes = append(sub.Nodes, node)
67
+ }
68
+
69
+ sub.Config = config
70
+ sub.Name = name
71
+ sub.CreateDate = time.Now().Format("2006-01-02 15:04:05")
72
+
73
+ err := sub.Add()
74
+ if err != nil {
75
+ c.JSON(400, gin.H{
76
+ "msg": "添加失败",
77
+ })
78
+ return
79
+ }
80
+ err = sub.AddNode() //创建多对多关系
81
+ if err != nil {
82
+ c.JSON(400, gin.H{
83
+ "msg": err.Error(),
84
+ })
85
+ return
86
+ }
87
+ c.JSON(200, gin.H{
88
+ "code": "00000",
89
+ "msg": "添加成功",
90
+ })
91
+ }
92
+
93
+ // 更新节点
94
+ func SubUpdate(c *gin.Context) {
95
+ var sub models.Subcription
96
+ name := c.PostForm("name")
97
+ oldname := c.PostForm("oldname")
98
+ config := c.PostForm("config")
99
+ nodes := c.PostForm("nodes")
100
+ if name == "" || nodes == "" {
101
+ c.JSON(400, gin.H{
102
+ "msg": "订阅名称 or 节点不能为空",
103
+ })
104
+ return
105
+ }
106
+ // 查找旧节点
107
+ sub.Name = oldname
108
+ err := sub.Find()
109
+ if err != nil {
110
+ c.JSON(400, gin.H{
111
+ "msg": err.Error(),
112
+ })
113
+ return
114
+ }
115
+ // 更新节点
116
+ sub.Config = config
117
+ sub.Name = name
118
+ sub.CreateDate = time.Now().Format("2006-01-02 15:04:05")
119
+ sub.Nodes = []models.Node{}
120
+ for _, v := range strings.Split(nodes, ",") {
121
+ var node models.Node
122
+ node.Name = v
123
+ err := node.Find()
124
+ if err != nil {
125
+ continue
126
+ }
127
+ sub.Nodes = append(sub.Nodes, node)
128
+ }
129
+
130
+ err = sub.Update()
131
+ if err != nil {
132
+ c.JSON(400, gin.H{
133
+ "msg": "更新失败",
134
+ })
135
+ return
136
+ }
137
+
138
+ err = sub.UpdateNodes() //更新多对多关系
139
+ if err != nil {
140
+ c.JSON(400, gin.H{
141
+ "msg": err.Error(),
142
+ })
143
+ return
144
+ }
145
+ c.JSON(200, gin.H{
146
+ "code": "00000",
147
+ "msg": "更新成功",
148
+ })
149
+ }
150
+
151
+ // 删除节点
152
+ func SubDel(c *gin.Context) {
153
+ var sub models.Subcription
154
+ id := c.Query("id")
155
+ if id == "" {
156
+ c.JSON(400, gin.H{
157
+ "msg": "id 不能为空",
158
+ })
159
+ return
160
+ }
161
+ x, _ := strconv.Atoi(id)
162
+ sub.ID = x
163
+ err := sub.Find()
164
+ if err != nil {
165
+ c.JSON(400, gin.H{
166
+ "msg": "查找失败",
167
+ })
168
+ return
169
+ }
170
+ err = sub.Del()
171
+ if err != nil {
172
+ c.JSON(400, gin.H{
173
+ "msg": "删除失败",
174
+ })
175
+ return
176
+ }
177
+ c.JSON(200, gin.H{
178
+ "code": "00000",
179
+ "msg": "删除成功",
180
+ })
181
+ }
api/template.go ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package api
2
+
3
+ import (
4
+ "log"
5
+ "os"
6
+
7
+ "github.com/gin-gonic/gin"
8
+ )
9
+
10
+ type Temp struct {
11
+ File string `json:"file"`
12
+ Text string `json:"text"`
13
+ CreateDate string `json:"create_date"`
14
+ }
15
+
16
+ func GetTempS(c *gin.Context) {
17
+ files, err := os.ReadDir("./template")
18
+ if err != nil {
19
+ c.JSON(400, gin.H{
20
+ "msg": err.Error(),
21
+ })
22
+ return
23
+ }
24
+ var temps []Temp
25
+ for _, file := range files {
26
+ info, _ := file.Info()
27
+ time := info.ModTime().Format("2006-01-02 15:04:05")
28
+ text, _ := os.ReadFile("./template/" + file.Name())
29
+ temp := Temp{
30
+ File: file.Name(),
31
+ Text: string(text),
32
+ CreateDate: time,
33
+ }
34
+ temps = append(temps, temp)
35
+ }
36
+ if len(temps) == 0 {
37
+ c.JSON(200, gin.H{
38
+ "code": "00000",
39
+ "data": []string{},
40
+ "msg": "ok",
41
+ })
42
+ return
43
+ }
44
+ c.JSON(200, gin.H{
45
+ "code": "00000",
46
+ "data": temps,
47
+ "msg": "ok",
48
+ })
49
+ }
50
+ func UpdateTemp(c *gin.Context) {
51
+ filename := c.PostForm("filename")
52
+ oldname := c.PostForm("oldname")
53
+ text := c.PostForm("text")
54
+ err := os.Rename("./template/"+oldname, "./template/"+filename)
55
+ if err != nil {
56
+ log.Println(err)
57
+ c.JSON(400, gin.H{
58
+ "msg": "改名失败",
59
+ })
60
+ return
61
+ }
62
+ err = os.WriteFile("./template/"+filename, []byte(text), 0666)
63
+ if err != nil {
64
+ log.Println(err)
65
+ c.JSON(400, gin.H{
66
+ "msg": "修改失败",
67
+ })
68
+ return
69
+ }
70
+ c.JSON(200, gin.H{
71
+ "code": "00000",
72
+ "msg": "修改成功",
73
+ })
74
+
75
+ }
76
+ func AddTemp(c *gin.Context) {
77
+ filename := c.PostForm("filename")
78
+ text := c.PostForm("text")
79
+ if filename == "" || text == "" {
80
+ c.JSON(400, gin.H{
81
+ "msg": "文件名或者类型或内容不能为空",
82
+ })
83
+ return
84
+ }
85
+ // 检查文件是否存在
86
+ _, err := os.ReadFile("./template/" + filename)
87
+ if err == nil {
88
+ log.Println(err)
89
+ c.JSON(400, gin.H{
90
+ "msg": "文件已存在",
91
+ })
92
+ return
93
+ }
94
+ // 检查目录是否创建
95
+ _, err = os.Stat("./template/")
96
+ if err != nil {
97
+ if os.IsNotExist(err) {
98
+ os.Mkdir("./template/", os.ModePerm)
99
+ }
100
+ }
101
+ err = os.WriteFile("./template/"+filename, []byte(text), 0666)
102
+ if err != nil {
103
+ log.Println(err)
104
+ c.JSON(400, gin.H{
105
+ "msg": "上传失败",
106
+ })
107
+ return
108
+ }
109
+ c.JSON(200, gin.H{
110
+ "code": "00000",
111
+ "msg": "上传成功",
112
+ })
113
+ }
114
+ func DelTemp(c *gin.Context) {
115
+ filename := c.PostForm("filename")
116
+ _, err := os.ReadFile("./template/" + filename)
117
+ if err != nil {
118
+ log.Println(err)
119
+ c.JSON(400, gin.H{
120
+ "msg": "文件不存在",
121
+ })
122
+ return
123
+ }
124
+ err = os.Remove("./template/" + filename)
125
+ if err != nil {
126
+ log.Println(err)
127
+ c.JSON(400, gin.H{
128
+ "msg": "删除失败",
129
+ })
130
+ return
131
+ }
132
+ c.JSON(200, gin.H{
133
+ "code": "00000",
134
+ "msg": "删除成功",
135
+ })
136
+ }
api/user.go ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package api
2
+
3
+ import (
4
+ "log"
5
+ "sublink/models"
6
+
7
+ "github.com/gin-gonic/gin"
8
+ )
9
+
10
+ type User struct {
11
+ ID int
12
+ Username string
13
+ Nickname string
14
+ Avatar string
15
+ Mobile string
16
+ Email string
17
+ }
18
+
19
+ // 新增用户
20
+ func UserAdd(c *gin.Context) {
21
+ user := &models.User{
22
+ Username: "test",
23
+ Password: "test",
24
+ }
25
+ err := user.Create()
26
+ if err != nil {
27
+ log.Println("创建用户失败")
28
+ }
29
+ c.String(200, "创建用户成功")
30
+ }
31
+
32
+ // 获取用户信息
33
+ func UserMe(c *gin.Context) {
34
+ // 获取jwt中的username
35
+ // 返回用户信息
36
+ username, _ := c.Get("username")
37
+ user := &models.User{Username: username.(string)}
38
+ err := user.Find()
39
+ if err != nil {
40
+ c.JSON(400, gin.H{
41
+ "code": "00000",
42
+ "msg": err,
43
+ })
44
+ return
45
+ }
46
+ c.JSON(200, gin.H{
47
+ "code": "00000",
48
+ "data": gin.H{
49
+ "avatar": "static/avatar.gif",
50
+ "nickname": user.Nickname,
51
+ "userId": user.ID,
52
+ "username": user.Username,
53
+ "roles": []string{"ADMIN"},
54
+ // "perms": []string{
55
+ // "sys:menu:delete", "sys:dept:edit", "sys:dict_type:add",
56
+ // "sys:dict:edit", "sys:dict:delete", "sys:dict_type:edit",
57
+ // "sys:menu:add", "sys:user:add", "sys:role:edit",
58
+ // "sys:dept:delete", "sys:user:password_reset", "sys:user:edit",
59
+ // "sys:user:delete", "sys:dept:add", "sys:role:delete",
60
+ // "sys:dict_type:delete", "sys:menu:edit", "sys:dict:add",
61
+ // "sys:role:add",
62
+ // },
63
+ },
64
+ "msg": "获取用户信息成功",
65
+ })
66
+ }
67
+
68
+ // 获取所有用户
69
+ func UserPages(c *gin.Context) {
70
+ // 获取jwt中的username
71
+ // 返回用户信息
72
+ username, _ := c.Get("username")
73
+ user := &models.User{Username: username.(string)}
74
+ users, err := user.All()
75
+ if err != nil {
76
+ log.Println("获取用户信息失败")
77
+ }
78
+ list := []*User{}
79
+ for i := range users {
80
+ list = append(list, &User{
81
+ ID: users[i].ID,
82
+ Username: users[i].Username,
83
+ Nickname: users[i].Nickname,
84
+ Avatar: "static/avatar.gif",
85
+ })
86
+ }
87
+ c.JSON(200, gin.H{
88
+ "code": "00000",
89
+ "data": gin.H{
90
+ "list": list,
91
+ },
92
+ "msg": "获取用户信息成功",
93
+ })
94
+ }
95
+
96
+ // 更新用户信息
97
+
98
+ func UserSet(c *gin.Context) {
99
+ NewUsername := c.Param("username")
100
+ NewPassword := c.Param("password")
101
+ log.Println(NewUsername, NewPassword)
102
+ if NewUsername == "" || NewPassword == "" {
103
+ c.JSON(400, gin.H{
104
+ "code": "00001",
105
+ "msg": "用户名或密码不能为空",
106
+ })
107
+ return
108
+ }
109
+ username, _ := c.Get("username")
110
+ user := &models.User{Username: username.(string)}
111
+ err := user.Set(&models.User{
112
+ Username: NewUsername,
113
+ Password: NewPassword,
114
+ })
115
+ if err != nil {
116
+ log.Println(err)
117
+ c.JSON(400, gin.H{
118
+ "code": "00000",
119
+ "msg": err,
120
+ })
121
+ return
122
+ }
123
+ // 修改成功
124
+ c.JSON(200, gin.H{
125
+ "code": "00000",
126
+ "msg": "修改成功",
127
+ })
128
+
129
+ }
build.sh ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o sublink_amd64 main.go
2
+ GOOS=linux GOARCH=arm64 go build -ldflags="-w -s" -o sublink_arm64 main.go
demo.sh ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ cd G:\sublinkX\sublinkX
2
+ red=$(go run main.go --version)
3
+ echo "版本号:$red"
4
+ read -p "回车"
docker-compose.yml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8' # 使用 Docker Compose 文件的版本
2
+
3
+ services:
4
+ sublink:
5
+ build:
6
+ context: . # 指定 Dockerfile 的上下文(当前目录)
7
+ dockerfile: Dockerfile # 指定 Dockerfile 的名称,如果是默认的 "Dockerfile",可以省略此行
8
+ ports:
9
+ - "8000:8000" # 将容器的 8000 端口映射到主机的 8000 端口
10
+ volumes:
11
+ - .:/app # 将当前目录挂载到容器的 /app 目录,便于开发时实时更新
12
+ environment:
13
+ - ENV_VAR_NAME=value # 可选,您可以在此处设置其他环境变量
14
+ - TZ=Asia/Shanghai # 设置时区为 Asia/Shanghai
15
+ restart: unless-stopped # 设置容器重启策略
go.mod ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module sublink
2
+
3
+ go 1.22.0
4
+
5
+ require (
6
+ github.com/bytedance/sonic v1.11.3 // indirect
7
+ github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
8
+ github.com/chenzhuoyu/iasm v0.9.1 // indirect
9
+ github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
10
+ github.com/dustin/go-humanize v1.0.1 // indirect
11
+ github.com/gabriel-vasile/mimetype v1.4.3 // indirect
12
+ github.com/gin-contrib/sse v0.1.0 // indirect
13
+ github.com/gin-gonic/gin v1.9.1 // indirect
14
+ github.com/glebarez/go-sqlite v1.22.0 // indirect
15
+ github.com/glebarez/sqlite v1.11.0 // indirect
16
+ github.com/go-playground/locales v0.14.1 // indirect
17
+ github.com/go-playground/universal-translator v0.18.1 // indirect
18
+ github.com/go-playground/validator/v10 v10.19.0 // indirect
19
+ github.com/goccy/go-json v0.10.2 // indirect
20
+ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
21
+ github.com/google/uuid v1.6.0 // indirect
22
+ github.com/jinzhu/inflection v1.0.0 // indirect
23
+ github.com/jinzhu/now v1.1.5 // indirect
24
+ github.com/json-iterator/go v1.1.12 // indirect
25
+ github.com/klauspost/cpuid/v2 v2.2.7 // indirect
26
+ github.com/leodido/go-urn v1.4.0 // indirect
27
+ github.com/mattn/go-isatty v0.0.20 // indirect
28
+ github.com/mattn/go-sqlite3 v1.14.22 // indirect
29
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
30
+ github.com/modern-go/reflect2 v1.0.2 // indirect
31
+ github.com/mojocn/base64Captcha v1.3.6 // indirect
32
+ github.com/ncruces/go-strftime v0.1.9 // indirect
33
+ github.com/pelletier/go-toml/v2 v2.2.0 // indirect
34
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
35
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
36
+ github.com/ugorji/go/codec v1.2.12 // indirect
37
+ golang.org/x/arch v0.7.0 // indirect
38
+ golang.org/x/crypto v0.21.0 // indirect
39
+ golang.org/x/image v0.15.0 // indirect
40
+ golang.org/x/net v0.22.0 // indirect
41
+ golang.org/x/sys v0.19.0 // indirect
42
+ golang.org/x/text v0.14.0 // indirect
43
+ google.golang.org/protobuf v1.33.0 // indirect
44
+ gopkg.in/yaml.v3 v3.0.1 // indirect
45
+ gorm.io/driver/sqlite v1.5.5 // indirect
46
+ gorm.io/gorm v1.25.9 // indirect
47
+ modernc.org/libc v1.49.3 // indirect
48
+ modernc.org/mathutil v1.6.0 // indirect
49
+ modernc.org/memory v1.8.0 // indirect
50
+ modernc.org/sqlite v1.29.6 // indirect
51
+ )
go.sum ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
2
+ github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
3
+ github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
4
+ github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
5
+ github.com/bytedance/sonic v1.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA=
6
+ github.com/bytedance/sonic v1.11.3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
7
+ github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
8
+ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
9
+ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
10
+ github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
11
+ github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
12
+ github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
13
+ github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
14
+ github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
15
+ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
16
+ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
17
+ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
18
+ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
19
+ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
20
+ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
21
+ github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
22
+ github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
23
+ github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
24
+ github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
25
+ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
26
+ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
27
+ github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
28
+ github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
29
+ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
30
+ github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
31
+ github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
32
+ github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
33
+ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
34
+ github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
35
+ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
36
+ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
37
+ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
38
+ github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
39
+ github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
40
+ github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
41
+ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
42
+ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
43
+ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
44
+ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
45
+ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
46
+ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
47
+ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
48
+ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
49
+ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
50
+ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
51
+ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
52
+ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
53
+ github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
54
+ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
55
+ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
56
+ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
57
+ github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
58
+ github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
59
+ github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
60
+ github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
61
+ github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
62
+ github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
63
+ github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
64
+ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
65
+ github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
66
+ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
67
+ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
68
+ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
69
+ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
70
+ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
71
+ github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
72
+ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
73
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
74
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
75
+ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
76
+ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
77
+ github.com/mojocn/base64Captcha v1.3.6 h1:gZEKu1nsKpttuIAQgWHO+4Mhhls8cAKyiV2Ew03H+Tw=
78
+ github.com/mojocn/base64Captcha v1.3.6/go.mod h1:i5CtHvm+oMbj1UzEPXaA8IH/xHFZ3DGY3Wh3dBpZ28E=
79
+ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
80
+ github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
81
+ github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
82
+ github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
83
+ github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
84
+ github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
85
+ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
86
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
87
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
88
+ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
89
+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
90
+ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
91
+ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
92
+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
93
+ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
94
+ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
95
+ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
96
+ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
97
+ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
98
+ github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
99
+ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
100
+ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
101
+ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
102
+ github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
103
+ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
104
+ github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
105
+ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
106
+ github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
107
+ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
108
+ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
109
+ golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
110
+ golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
111
+ golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
112
+ golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
113
+ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
114
+ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
115
+ golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
116
+ golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
117
+ golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
118
+ golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
119
+ golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk=
120
+ golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
121
+ golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
122
+ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
123
+ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
124
+ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
125
+ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
126
+ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
127
+ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
128
+ golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
129
+ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
130
+ golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
131
+ golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
132
+ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
133
+ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
134
+ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
135
+ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
136
+ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
137
+ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
138
+ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
139
+ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
140
+ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
141
+ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
142
+ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
143
+ golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
144
+ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
145
+ golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
146
+ golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
147
+ golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
148
+ golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
149
+ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
150
+ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
151
+ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
152
+ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
153
+ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
154
+ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
155
+ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
156
+ golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
157
+ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
158
+ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
159
+ golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
160
+ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
161
+ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
162
+ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
163
+ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
164
+ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
165
+ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
166
+ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
167
+ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
168
+ google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
169
+ google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
170
+ google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
171
+ google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
172
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
173
+ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
174
+ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
175
+ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
176
+ gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
177
+ gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
178
+ gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8=
179
+ gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
180
+ modernc.org/libc v1.49.3 h1:j2MRCRdwJI2ls/sGbeSk0t2bypOG/uvPZUsGQFDulqg=
181
+ modernc.org/libc v1.49.3/go.mod h1:yMZuGkn7pXbKfoT/M35gFJOAEdSKdxL0q64sF7KqCDo=
182
+ modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
183
+ modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
184
+ modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
185
+ modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
186
+ modernc.org/sqlite v1.29.6 h1:0lOXGrycJPptfHDuohfYgNqoe4hu+gYuN/pKgY5XjS4=
187
+ modernc.org/sqlite v1.29.6/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U=
188
+ nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
189
+ rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
install.sh ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # 检查用户是否为root
3
+ if [ "$(id -u)" != "0" ]; then
4
+ echo -e "${RED}该脚本必须以root身份运行。${NC}"
5
+ exit 1
6
+ fi
7
+
8
+ # 创建一个程序目录
9
+ INSTALL_DIR="/usr/local/bin/sublink"
10
+
11
+ if [ ! -d "$INSTALL_DIR" ]; then
12
+ mkdir -p "$INSTALL_DIR"
13
+ fi
14
+
15
+ # 获取最新的发行版标签
16
+ latest_release=$(curl --silent "https://api.github.com/repos/gooaclok819/sublinkX/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
17
+ echo "最新版本: $latest_release"
18
+
19
+ # 检测机器类型
20
+ machine_type=$(uname -m)
21
+
22
+ if [ "$machine_type" = "x86_64" ]; then
23
+ file_name="sublink_amd64"
24
+ elif [ "$machine_type" = "aarch64" ]; then
25
+ file_name="sublink_arm64"
26
+ else
27
+ echo "不支持的机器类型: $machine_type"
28
+ exit 1
29
+ fi
30
+
31
+ # 下载文件
32
+ cd ~
33
+ curl -LO "https://github.com/gooaclok819/sublinkX/releases/download/$latest_release/$file_name"
34
+
35
+ # 设置文件为可执行
36
+ chmod +x $file_name
37
+
38
+ # 移动文件到指定目录
39
+ mv $file_name "$INSTALL_DIR/sublink"
40
+
41
+ # 创建systemctl服务
42
+ echo "[Unit]
43
+ Description=Sublink Service
44
+
45
+ [Service]
46
+ ExecStart=$INSTALL_DIR/sublink
47
+ WorkingDirectory=$INSTALL_DIR
48
+ [Install]
49
+ WantedBy=multi-user.target" | tee /etc/systemd/system/sublink.service
50
+
51
+ # 重新加载systemd守护进程
52
+ systemctl daemon-reload
53
+
54
+ # 启动并启用服务
55
+ systemctl start sublink
56
+ systemctl enable sublink
57
+ echo "服务已启动并已设置为开机启动"
58
+ echo "默认账号admin密码123456 默认端口8000"
59
+ echo "安装完成已经启动输入sublink可以呼出菜单"
60
+
61
+
62
+ # 下载menu.sh并设置权限
63
+ curl -o /usr/bin/sublink -H "Cache-Control: no-cache" -H "Pragma: no-cache" https://raw.githubusercontent.com/gooaclok819/sublinkX/main/menu.sh
64
+ chmod 755 "/usr/bin/sublink"
main.go ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package main
2
+
3
+ import (
4
+ "embed"
5
+ "flag"
6
+ "fmt"
7
+ "io/fs"
8
+ "log"
9
+ "net/http"
10
+ "os"
11
+ "sublink/middlewares"
12
+ "sublink/models"
13
+ "sublink/routers"
14
+ "sublink/settings"
15
+ "sublink/utils"
16
+
17
+ "github.com/gin-gonic/gin"
18
+ )
19
+
20
+ //go:embed static/js/*
21
+ //go:embed static/css/*
22
+ //go:embed static/img/*
23
+ //go:embed static/*
24
+ var embeddedFiles embed.FS
25
+
26
+ //go:embed template
27
+ var Template embed.FS
28
+
29
+ var version string
30
+
31
+ func Templateinit() {
32
+ // 设置template路径
33
+ // 检查目录是否创建
34
+ subFS, err := fs.Sub(Template, "template")
35
+ if err != nil {
36
+ log.Println(err)
37
+ return // 如果出错,直接返回
38
+ }
39
+ entries, err := fs.ReadDir(subFS, ".")
40
+ if err != nil {
41
+ log.Println(err)
42
+ return // 如果出错,直接返回
43
+ }
44
+ // 创建template目录
45
+ _, err = os.Stat("./template")
46
+ if os.IsNotExist(err) {
47
+ err = os.Mkdir("./template", 0666)
48
+ if err != nil {
49
+ log.Println(err)
50
+ return
51
+ }
52
+ }
53
+ // 写入默认模板
54
+ for _, entry := range entries {
55
+ _, err := os.Stat("./template/" + entry.Name())
56
+ //如果文件不存在则写入默认模板
57
+ if os.IsNotExist(err) {
58
+ data, err := fs.ReadFile(subFS, entry.Name())
59
+ if err != nil {
60
+ log.Println(err)
61
+ continue
62
+ }
63
+ err = os.WriteFile("./template/"+entry.Name(), data, 0666)
64
+ if err != nil {
65
+ log.Println(err)
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ func main() {
72
+ var port int
73
+ // 获取版本号
74
+ var Isversion bool
75
+ version = "1.8"
76
+ flag.BoolVar(&Isversion, "version", false, "显示版本号")
77
+ flag.Parse()
78
+ if Isversion {
79
+ fmt.Println(version)
80
+ return
81
+ }
82
+ // 初始化数据库
83
+ models.InitSqlite()
84
+ // 获取命令行参数
85
+ args := os.Args
86
+ // 如果长度小于2则没有接收到任何参数
87
+ if len(args) < 2 {
88
+ port = 8000
89
+ Run(port)
90
+ return
91
+ }
92
+ // 命令行参数选择
93
+ settingCmd := flag.NewFlagSet("setting", flag.ExitOnError)
94
+ var username, password string
95
+ settingCmd.StringVar(&username, "username", "", "设置账号")
96
+ settingCmd.StringVar(&password, "password", "", "设置密码")
97
+ settingCmd.IntVar(&port, "port", 8000, "修改端口")
98
+ switch args[1] {
99
+ // 解析setting命令标志
100
+ case "setting":
101
+ settingCmd.Parse(args[2:])
102
+ fmt.Println(username, password)
103
+ settings.ResetUser(username, password)
104
+ return
105
+ case "run":
106
+ settingCmd.Parse(args[2:])
107
+ Run(port)
108
+ default:
109
+ return
110
+
111
+ }
112
+ }
113
+
114
+ func Run(port int) {
115
+ // 初始化gin框架
116
+ r := gin.Default()
117
+ // 初始化日志配置
118
+ utils.Loginit()
119
+ // 初始化模板
120
+ Templateinit()
121
+ // 安装中间件
122
+ r.Use(middlewares.AuthorToken) // jwt验证token
123
+ // 设置静态资源路径
124
+ staticFiles, err := fs.Sub(embeddedFiles, "static")
125
+ if err != nil {
126
+ log.Println(err)
127
+ }
128
+ r.StaticFS("/static", http.FS(staticFiles))
129
+ // 设置模板路径
130
+ r.GET("/", func(c *gin.Context) {
131
+ data, err := fs.ReadFile(staticFiles, "index.html")
132
+ if err != nil {
133
+ c.Error(err)
134
+ return
135
+
136
+ }
137
+ c.Data(200, "text/html", data)
138
+ })
139
+ // 注册路由
140
+ routers.User(r)
141
+ routers.Mentus(r)
142
+ routers.Subcription(r)
143
+ routers.Nodes(r)
144
+ routers.Clients(r)
145
+ routers.Total(r)
146
+ routers.Templates(r)
147
+ routers.Version(r, version)
148
+ // 启动服务
149
+ r.Run(fmt.Sprintf("0.0.0.0:%d", port))
150
+ }
menu.sh ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ function Up {
3
+ # 获取最新的发行版标签
4
+ latest_release=$(curl --silent "https://api.github.com/repos/gooaclok819/sublinkX/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
5
+ echo "最新版本: $latest_release"
6
+ # 检测机器类型
7
+ machine_type=$(uname -m)
8
+
9
+ if [ "$machine_type" = "x86_64" ]; then
10
+ file_name="sublink_amd64"
11
+ elif [ "$machine_type" = "aarch64" ]; then
12
+ file_name="sublink_arm64"
13
+ else
14
+ echo "不支持的机器类型: $machine_type"
15
+ exit 1
16
+ fi
17
+
18
+ # 下载文件
19
+ curl -LO "https://github.com/gooaclok819/sublinkX/releases/download/$latest_release/$file_name"
20
+
21
+ # 设置文件为可执行
22
+ chmod +x $file_name
23
+
24
+ # 移动文件到指定目录
25
+ mv $file_name "$INSTALL_DIR/sublink"
26
+ echo "更新完成"
27
+
28
+ }
29
+ function Select {
30
+ # 获取最新的发行版标签
31
+ latest_release=$(curl --silent "https://api.github.com/repos/gooaclok819/sublinkX/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
32
+ # 获取服务状态
33
+ cd /usr/local/bin/sublink # 进入sublink目录
34
+ status=$(systemctl is-active sublink)
35
+ version=$(./sublink --version)
36
+ echo "最新版本:$latest_release"
37
+ echo "当前版本:$version"
38
+ # 判断服务状态并打印
39
+ if [ "$status" = "active" ]; then
40
+ echo "当前运行状态: 已运行"
41
+ else
42
+ echo "当前运行状态: 未运行"
43
+ fi
44
+ echo "1. 启动服务"
45
+ echo "2. 停止服务"
46
+ echo "3. 卸载安装"
47
+ echo "4. 查看服务状态"
48
+ echo "5. 查看运行目录"
49
+ echo "6. 修改端口"
50
+ echo "7. 更新"
51
+ echo "8. 重置账号密码"
52
+ echo "0. 退出"
53
+ echo -n "请选择一个选项: "
54
+ read option
55
+
56
+ case $option in
57
+ 1)
58
+ systemctl start sublink
59
+ systemctl daemon-reload
60
+ ;;
61
+ 2)
62
+ systemctl stop sublink
63
+ systemctl daemon-reload
64
+ ;;
65
+ 3)
66
+ # 停止服务之前检查服务是否存在
67
+ if systemctl is-active --quiet sublink; then
68
+ systemctl stop sublink
69
+ fi
70
+ if systemctl is-enabled --quiet sublink; then
71
+ systemctl disable sublink
72
+ fi
73
+ # 删除服务文件
74
+ read -p "是否删除systemd服务文件(包含端口设置)(y/n): " isDelSystemd
75
+ if [ "$isDelSystemd" = "y" ]; then
76
+ sudo rm /etc/systemd/system/sublink.service
77
+ fi
78
+ # 删除相关文件和目录
79
+ sudo rm -r /usr/local/bin/sublink/sublink
80
+ sudo rm -r /usr/bin/sublink
81
+ read -p "是否删除模板文件和数据库(y/n): " isDelete
82
+ if [ "$isDelete" = "y" ]; then
83
+ sudo rm -r /usr/local/bin/sublink/db
84
+ sudo rm -r /usr/local/bin/sublink/template
85
+ sudo rm -r /usr/local/bin/sublink/logs
86
+ fi
87
+ echo "卸载完成"
88
+ ;;
89
+ 4)
90
+ systemctl status sublink
91
+ ;;
92
+ 5)
93
+ echo "运行目录: /usr/local/bin/sublink"
94
+ echo "需要备份的目录为db,template目录为模版文件可备份可不备份"
95
+ cd /usr/local/bin/sublink
96
+ ;;
97
+ 6)
98
+ SERVICE_FILE="/etc/systemd/system/sublink.service"
99
+ read -p "请输入新的端口号: " Port
100
+ echo "新的端口号: $Port"
101
+ PARAMETER="run --port $Port"
102
+ # 检查服务文件是否存在
103
+ if [ ! -f "$SERVICE_FILE" ]; then
104
+ echo "服务文件不存在: $SERVICE_FILE"
105
+ exit 1
106
+ fi
107
+
108
+ # 检查 ExecStart 是否已经包含该参数
109
+ if grep -q "run --port" "$SERVICE_FILE"; then
110
+ echo "参数已存在,正在替换..."
111
+ # 使用 sed 替换 ExecStart 行中的 -port 参数
112
+ sudo sed -i "s/-port [0-9]\+/-port $Port/" "$SERVICE_FILE"
113
+ else
114
+ # 如果没有 -port 参数,添加新参数
115
+ # 使用 sed 替换 ExecStart 行,添加启动参数
116
+ sudo sed -i "/^ExecStart=/ s|$| $PARAMETER|" "$SERVICE_FILE"
117
+ echo "参数已添加到 ExecStart 行: $PARAMETER"
118
+ fi
119
+
120
+ # 重新加载 systemd 守护进程
121
+ sudo systemctl daemon-reload
122
+ # 重启 sublink 服务
123
+ sudo systemctl restart sublink
124
+
125
+ echo "服务已重启。"
126
+
127
+ ;;
128
+ 7)
129
+ # 停止服务之前检查服务是否存在
130
+ if systemctl is-active --quiet sublink; then
131
+ systemctl stop sublink
132
+ fi
133
+ # 检查是否为最新版本
134
+ if [[ $version = $latest_release ]]; then
135
+ echo "当前已经是最新版本"
136
+ else
137
+ Up
138
+ fi
139
+ # 更新菜单
140
+ sudo rm /usr/bin/sublink
141
+ curl -o /usr/bin/sublink -H "Cache-Control: no-cache" -H "Pragma: no-cache" https://raw.githubusercontent.com/gooaclok819/sublinkX/main/menu.sh
142
+ chmod 755 "/usr/bin/sublink"
143
+ ;;
144
+ 8)
145
+ read -p "请输入新的账号: " User
146
+ read -p "请输入新的密码: " Password
147
+ # 运行二进制文件并传递启动参数,放在后台运行
148
+ cd /usr/local/bin/sublink
149
+ ./sublink setting --username "$User" --password "$Password" &
150
+ # 获取该程序的PID
151
+ pid=$!
152
+ # 等待程序完成
153
+ wait $pid
154
+ # 如果需要可以在此处进行清理
155
+ systemctl restart sublink
156
+ ;;
157
+ 0)
158
+ exit 0
159
+ ;;
160
+ *)
161
+ echo "无效的选项,请重新选择"
162
+ Select
163
+ ;;
164
+ esac
165
+ }
166
+ Select
middlewares/ip.go ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package middlewares
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "io"
7
+ "log"
8
+ "net/http"
9
+ "sublink/models"
10
+ "time"
11
+
12
+ "github.com/gin-gonic/gin"
13
+ "golang.org/x/text/encoding/simplifiedchinese"
14
+ )
15
+
16
+ func GetIp(c *gin.Context) {
17
+ c.Next()
18
+ func() {
19
+ subname, _ := c.Get("subname")
20
+
21
+ ip := c.ClientIP()
22
+ resp, err := http.Get(fmt.Sprintf("https://whois.pconline.com.cn/ipJson.jsp?ip=%s&json=true", ip))
23
+ if err != nil {
24
+ log.Println(err)
25
+ return
26
+ }
27
+ defer resp.Body.Close()
28
+ body, _ := io.ReadAll(resp.Body)
29
+ utf8Body, _ := simplifiedchinese.GBK.NewDecoder().Bytes(body)
30
+ type IpInfo struct {
31
+ Addr string `json:"addr"`
32
+ Ip string `json:"ip"`
33
+ }
34
+ ipinfo := IpInfo{}
35
+ err = json.Unmarshal(utf8Body, &ipinfo)
36
+ if err != nil {
37
+ log.Println(err)
38
+ return
39
+ }
40
+ var sub models.Subcription
41
+ if subname, ok := subname.(string); ok {
42
+ sub.Name = subname
43
+ }
44
+ err = sub.Find()
45
+ if err != nil {
46
+ log.Println(err)
47
+ return
48
+ }
49
+ var iplog models.SubLogs
50
+ iplog.IP = ip
51
+ err = iplog.Find(sub.ID)
52
+ // 如果没有找到记录
53
+ if err != nil {
54
+ iploga := []models.SubLogs{
55
+ {IP: ip,
56
+ Addr: ipinfo.Addr,
57
+ SubcriptionID: sub.ID,
58
+ Date: time.Now().Format("2006-01-02 15:04:05"),
59
+ Count: 1,
60
+ },
61
+ }
62
+ sub.SubLogs = iploga
63
+ err = sub.Update()
64
+ if err != nil {
65
+ log.Println(err)
66
+ return
67
+ }
68
+ } else {
69
+ // 更新访问次数
70
+ iplog.Count++
71
+ iplog.Date = time.Now().Format("2006-01-02 15:04:05")
72
+ err = iplog.Update()
73
+ if err != nil {
74
+ log.Println(err)
75
+ return
76
+ }
77
+ }
78
+ }()
79
+
80
+ }
middlewares/jwt.go ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package middlewares
2
+
3
+ import (
4
+ "errors"
5
+ "net/http"
6
+ "strings"
7
+
8
+ "github.com/dgrijalva/jwt-go"
9
+ "github.com/gin-gonic/gin"
10
+ )
11
+
12
+ var Secret = []byte("sublink") // 秘钥
13
+
14
+ // JwtClaims jwt声明
15
+ type JwtClaims struct {
16
+ Username string `json:"username"`
17
+ jwt.StandardClaims
18
+ }
19
+
20
+ // AuthorToken 验证token中间件
21
+ func AuthorToken(c *gin.Context) {
22
+ // 定义白名单
23
+ list := []string{"/static", "/api/v1/auth/login", "/api/v1/auth/captcha", "/c/", "/api/v1/version"}
24
+ // 如果是首页直接跳过
25
+ if c.Request.URL.Path == "/" {
26
+ c.Next()
27
+ return
28
+ }
29
+ // 如果是白名单直接跳过
30
+ for _, v := range list {
31
+ if strings.HasPrefix(c.Request.URL.Path, v) {
32
+ c.Next()
33
+ return
34
+ }
35
+ }
36
+ token := c.Request.Header.Get("Authorization")
37
+ if token == "" {
38
+ c.JSON(400, gin.H{"msg": "请求未携带token"})
39
+ c.Abort()
40
+ return
41
+ }
42
+ parts := strings.Split(token, ".")
43
+ if len(parts) != 3 {
44
+ c.JSON(400, gin.H{"msg": "token格式错误"})
45
+ c.Abort()
46
+ return
47
+ }
48
+ // 去掉Bearer前缀
49
+ token = strings.Replace(token, "Bearer ", "", -1)
50
+ mc, err := ParseToken(token)
51
+ if err != nil {
52
+ c.JSON(http.StatusUnauthorized, gin.H{
53
+ "code": 401,
54
+ "msg": err.Error(),
55
+ })
56
+ c.Abort()
57
+ return
58
+ }
59
+ c.Set("username", mc.Username)
60
+ c.Next()
61
+ }
62
+
63
+ // ParseToken 解析JWT
64
+ func ParseToken(tokenString string) (*JwtClaims, error) {
65
+ // 解析token
66
+ token, err := jwt.ParseWithClaims(tokenString, &JwtClaims{}, func(token *jwt.Token) (i interface{}, err error) {
67
+ return Secret, nil
68
+ })
69
+ if err != nil {
70
+ return nil, err
71
+ }
72
+ if claims, ok := token.Claims.(*JwtClaims); ok && token.Valid { // 校验token
73
+ return claims, nil
74
+ }
75
+ return nil, errors.New("invalid token")
76
+ }
models/config.go ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ package models
2
+
3
+ type Config struct {
4
+ ID int
5
+ Key string
6
+ Value string
7
+ }
models/iplogs.go ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package models
2
+
3
+ type SubLogs struct {
4
+ ID int
5
+ IP string
6
+ Date string
7
+ Addr string
8
+ Count int
9
+ SubcriptionID int
10
+ }
11
+
12
+ // Add 添加IP
13
+ func (iplog *SubLogs) Add() error {
14
+ return DB.Create(iplog).Error
15
+ }
16
+
17
+ // 查找IP
18
+ func (iplog *SubLogs) Find(id int) error {
19
+ return DB.Where("ip = ? and subcription_id = ?", iplog.IP, id).First(iplog).Error
20
+ }
21
+
22
+ // Update 更新IP
23
+ func (iplog *SubLogs) Update() error {
24
+ return DB.Where("id = ? or ip = ?", iplog.ID, iplog.IP).Updates(iplog).Error
25
+ }
26
+
27
+ // List 获取IP列表
28
+ func (iplog *SubLogs) List() ([]SubLogs, error) {
29
+ var iplogs []SubLogs
30
+ err := DB.Find(&iplogs).Error
31
+ if err != nil {
32
+ return nil, err
33
+ }
34
+ return iplogs, nil
35
+ }
36
+
37
+ // Del 删除IP
38
+ func (iplog *SubLogs) Del() error {
39
+ return DB.Delete(iplog).Error
40
+ }
models/node.go ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package models
2
+
3
+ type Node struct {
4
+ ID int
5
+ Link string
6
+ Name string
7
+ CreateDate string
8
+ }
9
+
10
+ // Add 添加节点
11
+ func (node *Node) Add() error {
12
+ return DB.Create(node).Error
13
+ }
14
+
15
+ // 更新节点
16
+ func (node *Node) Update() error {
17
+ return DB.Model(node).Updates(node).Error
18
+ }
19
+
20
+ // 查找节点是否重复
21
+ func (node *Node) Find() error {
22
+ return DB.Where("link = ? or name = ?", node.Link, node.Name).First(node).Error
23
+ }
24
+
25
+ // 节点列表
26
+ func (node *Node) List() ([]Node, error) {
27
+ var nodes []Node
28
+ err := DB.Find(&nodes).Error
29
+ if err != nil {
30
+ return nil, err
31
+ }
32
+ return nodes, nil
33
+ }
34
+
35
+ // 删除节点
36
+ func (node *Node) Del() error {
37
+ return DB.Delete(node).Error
38
+ }
models/sqlite.go ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package models
2
+
3
+ import (
4
+ "log"
5
+ "os"
6
+
7
+ "github.com/glebarez/sqlite"
8
+ "gorm.io/gorm"
9
+ )
10
+
11
+ var DB *gorm.DB
12
+ var isInitialized bool
13
+
14
+ func InitSqlite() {
15
+ // 检查目录是否创建
16
+ _, err := os.Stat("./db")
17
+ if err != nil {
18
+ if os.IsNotExist(err) {
19
+ os.Mkdir("./db", os.ModePerm)
20
+ }
21
+ }
22
+ // 连接数据库
23
+ db, err := gorm.Open(sqlite.Open("./db/sublink.db"), &gorm.Config{})
24
+ if err != nil {
25
+ log.Println("连接数据库失败")
26
+ }
27
+ DB = db
28
+ // 检查是否已经初始化
29
+ if isInitialized {
30
+ log.Println("数据库已经初始化,无需重复初始化")
31
+ return
32
+ }
33
+ err = db.AutoMigrate(&User{}, &Subcription{}, &Node{}, &SubLogs{})
34
+ if err != nil {
35
+ log.Println("数据表迁移失败")
36
+ }
37
+ // 初始化用户数据
38
+ err = db.First(&User{}).Error
39
+ if err == gorm.ErrRecordNotFound {
40
+ admin := &User{
41
+ Username: "admin",
42
+ Password: "123456",
43
+ Role: "admin",
44
+ Nickname: "管理员",
45
+ }
46
+ err = admin.Create()
47
+ if err != nil {
48
+ log.Println("初始化添加用户数据失败")
49
+ }
50
+ }
51
+ // 设置初始化标志为 true
52
+ isInitialized = true
53
+ log.Println("数据库初始化成功") // 只有在没有任何错误时才会打印这个日志
54
+ }
models/subcription.go ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package models
2
+
3
+ type Subcription struct {
4
+ ID int
5
+ Name string
6
+ Config string `gorm:"embedded"`
7
+ Nodes []Node `gorm:"many2many:subcription_nodes;"` // 多对多关系
8
+ SubLogs []SubLogs `gorm:"foreignKey:SubcriptionID;"` // 一对多关系 约束父表被删除子表记录跟着删除
9
+ CreateDate string
10
+ }
11
+
12
+ // Add 添加订阅
13
+ func (sub *Subcription) Add() error {
14
+ return DB.Create(sub).Error
15
+ }
16
+
17
+ // 添加节点列表建立多对多关系
18
+ func (sub *Subcription) AddNode() error {
19
+ return DB.Model(sub).Association("Nodes").Append(sub.Nodes)
20
+ }
21
+
22
+ // 更新订阅
23
+ func (sub *Subcription) Update() error {
24
+ return DB.Where("id = ? or name = ?", sub.ID, sub.Name).Updates(sub).Error
25
+ }
26
+
27
+ // 更新节点列表建立多对多关系
28
+ func (sub *Subcription) UpdateNodes() error {
29
+ return DB.Model(sub).Association("Nodes").Replace(sub.Nodes)
30
+ }
31
+
32
+ // 查找订阅
33
+ func (sub *Subcription) Find() error {
34
+ return DB.Where("id = ? or name = ?", sub.ID, sub.Name).First(sub).Error
35
+ }
36
+
37
+ // 读取订阅
38
+ func (sub *Subcription) GetSub() error {
39
+ // err := DB.Find(sub).Error
40
+ // if err != nil {
41
+ // return err
42
+ // }
43
+ return DB.Model(sub).Association("Nodes").Find(&sub.Nodes)
44
+ }
45
+
46
+ // 订阅列表
47
+ func (sub *Subcription) List() ([]Subcription, error) {
48
+ var subs []Subcription
49
+ err := DB.Find(&subs).Error
50
+ if err != nil {
51
+ return nil, err
52
+ }
53
+ for i := range subs {
54
+ DB.Model(&subs[i]).Association("Nodes").Find(&subs[i].Nodes)
55
+ DB.Model(&subs[i]).Association("SubLogs").Find(&subs[i].SubLogs)
56
+ }
57
+ return subs, nil
58
+ }
59
+
60
+ func (sub *Subcription) IPlogUpdate() error {
61
+ return DB.Model(sub).Association("SubLogs").Replace(&sub.SubLogs)
62
+ }
63
+
64
+ // 删除订阅
65
+ func (sub *Subcription) Del() error {
66
+ err := DB.Model(sub).Association("Nodes").Clear()
67
+ if err != nil {
68
+ return err
69
+ }
70
+ return DB.Delete(sub).Error
71
+ }
models/user.go ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package models
2
+
3
+ type User struct {
4
+ ID int
5
+ Username string
6
+ Password string
7
+ Role string
8
+ Nickname string
9
+ }
10
+
11
+ func (user *User) Create() error { // 创建用户
12
+ return DB.Create(user).Error
13
+ }
14
+ func (user *User) Set(UpdateUser *User) error { // 设置用户
15
+ return DB.Where("username = ?", user.Username).Updates(UpdateUser).Error
16
+ }
17
+ func (user *User) Verify() error { // 验证用户
18
+ return DB.Where("username = ? AND password = ?", user.Username, user.Password).First(user).Error
19
+ }
20
+
21
+ func (user *User) Find() error { // 查找用户
22
+ return DB.Where("username = ? ", user.Username).First(user).Error
23
+ }
24
+
25
+ func (user *User) All() ([]User, error) { // 获取所有用户
26
+ var users []User
27
+ err := DB.Find(&users).Error
28
+ return users, err
29
+ }
30
+
31
+ func (user *User) Del() error { // 删除用户
32
+ return DB.Delete(user).Error
33
+ }
node/clash.go ADDED
@@ -0,0 +1,429 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package node
2
+
3
+ import (
4
+ "fmt"
5
+ "io"
6
+ "log"
7
+ "net/http"
8
+ "os"
9
+ "strconv"
10
+ "strings"
11
+
12
+ "gopkg.in/yaml.v3"
13
+ )
14
+
15
+ type Proxy struct {
16
+ Name string `yaml:"name,omitempty"`
17
+ Type string `yaml:"type,omitempty"`
18
+ Server string `yaml:"server,omitempty"`
19
+ Port int `yaml:"port,omitempty"`
20
+ Cipher string `yaml:"cipher,omitempty"`
21
+ Password string `yaml:"password,omitempty"`
22
+ Client_fingerprint string `yaml:"client-fingerprint,omitempty"`
23
+ Tfo bool `yaml:"tfo,omitempty"`
24
+ Udp bool `yaml:"udp,omitempty"`
25
+ Skip_cert_verify bool `yaml:"skip-cert-verify,omitempty"`
26
+ Tls bool `yaml:"tls,omitempty"`
27
+ Servername string `yaml:"servername,omitempty"`
28
+ Flow string `yaml:"flow,omitempty"`
29
+ AlterId string `yaml:"alterId,omitempty"`
30
+ Network string `yaml:"network,omitempty"`
31
+ Reality_opts map[string]interface{} `yaml:"reality-opts,omitempty"`
32
+ Ws_opts map[string]interface{} `yaml:"ws-opts,omitempty"`
33
+ Auth_str string `yaml:"auth_str,omitempty"`
34
+ Auth string `yaml:"auth,omitempty"`
35
+ Up int `yaml:"up,omitempty"`
36
+ Down int `yaml:"down,omitempty"`
37
+ Alpn []string `yaml:"alpn,omitempty"`
38
+ Sni string `yaml:"sni,omitempty"`
39
+ Obfs string `yaml:"obfs,omitempty"`
40
+ Obfs_password string `yaml:"obfs-password,omitempty"`
41
+ Protocol string `yaml:"protocol,omitempty"`
42
+ Uuid string `yaml:"uuid,omitempty"`
43
+ Peer string `yaml:"peer,omitempty"`
44
+ Congestion_control string `yaml:"congestion_control,omitempty"`
45
+ Udp_relay_mode string `yaml:"udp_relay_mode,omitempty"`
46
+ Disable_sni bool `yaml:"disable_sni,omitempty"`
47
+ }
48
+
49
+ type ProxyGroup struct {
50
+ Proxies []string `yaml:"proxies"`
51
+ }
52
+ type Config struct {
53
+ Proxies []Proxy `yaml:"proxies"`
54
+ Proxy_groups []ProxyGroup `yaml:"proxy-groups"`
55
+ }
56
+
57
+ // 删除opts中的空值
58
+ func DeleteOpts(opts map[string]interface{}) {
59
+ for k, v := range opts {
60
+ switch v := v.(type) {
61
+ case string:
62
+ if v == "" {
63
+ delete(opts, k)
64
+ }
65
+ case map[string]interface{}:
66
+ DeleteOpts(v)
67
+ if len(v) == 0 {
68
+ delete(opts, k)
69
+ }
70
+ }
71
+ }
72
+ }
73
+ func convertToInt(value interface{}) (int, error) {
74
+ switch v := value.(type) {
75
+ case int:
76
+ return v, nil
77
+ case float64:
78
+ return int(v), nil
79
+ case string:
80
+ return strconv.Atoi(v)
81
+ default:
82
+ return 0, fmt.Errorf("unexpected type %T", v)
83
+ }
84
+ }
85
+
86
+ // EncodeClash 用于生成 Clash 配置文件
87
+ func EncodeClash(urls []string, sqlconfig SqlConfig) ([]byte, error) {
88
+ // 传入urls,解析urls,生成proxys
89
+ // yamlfile 为模板文件
90
+ var proxys []Proxy
91
+
92
+ for _, link := range urls {
93
+ Scheme := strings.Split(link, "://")[0]
94
+ switch {
95
+ case Scheme == "ss":
96
+ ss, err := DecodeSSURL(link)
97
+ if err != nil {
98
+ log.Println(err)
99
+ continue
100
+ }
101
+ // 如果没有名字,就用服务器地址作为名字
102
+ if ss.Name == "" {
103
+ ss.Name = fmt.Sprintf("%s:%d", ss.Server, ss.Port)
104
+ }
105
+ ssproxy := Proxy{
106
+ Name: ss.Name,
107
+ Type: "ss",
108
+ Server: ss.Server,
109
+ Port: ss.Port,
110
+ Cipher: ss.Param.Cipher,
111
+ Password: ss.Param.Password,
112
+ Udp: sqlconfig.Udp,
113
+ Skip_cert_verify: sqlconfig.Cert,
114
+ }
115
+ proxys = append(proxys, ssproxy)
116
+ case Scheme == "ssr":
117
+ ssr, err := DecodeSSRURL(link)
118
+ if err != nil {
119
+ log.Println(err)
120
+ }
121
+ // 如果没有名字,就用服务器地址作为名字
122
+ if ssr.Qurey.Remarks == "" {
123
+ ssr.Qurey.Remarks = fmt.Sprintf("%s:%d", ssr.Server, ssr.Port)
124
+ }
125
+ ssrproxy := Proxy{
126
+ Name: ssr.Qurey.Remarks,
127
+ Type: "ssr",
128
+ Server: ssr.Server,
129
+ Port: ssr.Port,
130
+ Cipher: ssr.Method,
131
+ Password: ssr.Password,
132
+ Obfs: ssr.Obfs,
133
+ Obfs_password: ssr.Qurey.Obfsparam,
134
+ Protocol: ssr.Protocol,
135
+ Udp: sqlconfig.Udp,
136
+ Skip_cert_verify: sqlconfig.Cert,
137
+ }
138
+ proxys = append(proxys, ssrproxy)
139
+ case Scheme == "trojan":
140
+ trojan, err := DecodeTrojanURL(link)
141
+ if err != nil {
142
+ log.Println(err)
143
+ continue
144
+ }
145
+ // 如果没有名字,就用服务器地址作为名字
146
+ if trojan.Name == "" {
147
+ trojan.Name = fmt.Sprintf("%s:%d", trojan.Hostname, trojan.Port)
148
+ }
149
+ ws_opts := map[string]interface{}{
150
+ "path": trojan.Query.Path,
151
+ "headers": map[string]interface{}{
152
+ "Host": trojan.Query.Host,
153
+ },
154
+ }
155
+ DeleteOpts(ws_opts)
156
+ trojanproxy := Proxy{
157
+ Name: trojan.Name,
158
+ Type: "trojan",
159
+ Server: trojan.Hostname,
160
+ Port: trojan.Port,
161
+ Password: trojan.Password,
162
+ Client_fingerprint: trojan.Query.Fp,
163
+ Sni: trojan.Query.Sni,
164
+ Network: trojan.Query.Type,
165
+ Flow: trojan.Query.Flow,
166
+ Alpn: trojan.Query.Alpn,
167
+ Ws_opts: ws_opts,
168
+ Udp: sqlconfig.Udp,
169
+ Skip_cert_verify: sqlconfig.Cert,
170
+ }
171
+ proxys = append(proxys, trojanproxy)
172
+ case Scheme == "vmess":
173
+ vmess, err := DecodeVMESSURL(link)
174
+ if err != nil {
175
+ log.Println(err)
176
+ continue
177
+ }
178
+ // 如果没有名字,就用服务器地址作为名字
179
+ if vmess.Ps == "" {
180
+ vmess.Ps = fmt.Sprintf("%s:%s", vmess.Add, vmess.Port)
181
+ }
182
+ ws_opts := map[string]interface{}{
183
+ "path": vmess.Path,
184
+ "headers": map[string]interface{}{
185
+ "Host": vmess.Host,
186
+ },
187
+ }
188
+ DeleteOpts(ws_opts)
189
+ tls := false
190
+ if vmess.Tls != "none" && vmess.Tls != "" {
191
+ tls = true
192
+ }
193
+ port, _ := convertToInt(vmess.Port)
194
+ aid, _ := convertToInt(vmess.Aid)
195
+ vmessproxy := Proxy{
196
+ Name: vmess.Ps,
197
+ Type: "vmess",
198
+ Server: vmess.Add,
199
+ Port: port,
200
+ Cipher: vmess.Scy,
201
+ Uuid: vmess.Id,
202
+ AlterId: strconv.Itoa(aid),
203
+ Network: vmess.Net,
204
+ Tls: tls,
205
+ Ws_opts: ws_opts,
206
+ Udp: sqlconfig.Udp,
207
+ Skip_cert_verify: sqlconfig.Cert,
208
+ }
209
+ proxys = append(proxys, vmessproxy)
210
+ case Scheme == "vless":
211
+ vless, err := DecodeVLESSURL(link)
212
+ if err != nil {
213
+ log.Println(err)
214
+ continue
215
+ }
216
+ // 如果没有名字,就用服务器地址作为名字
217
+ if vless.Name == "" {
218
+ vless.Name = fmt.Sprintf("%s:%d", vless.Server, vless.Port)
219
+ }
220
+ ws_opts := map[string]interface{}{
221
+ "path": vless.Query.Path,
222
+ "headers": map[string]interface{}{
223
+ "Host": vless.Query.Host,
224
+ },
225
+ }
226
+ reality_opts := map[string]interface{}{
227
+ "public-key": vless.Query.Pbk,
228
+ "short-id": vless.Query.Sid,
229
+ }
230
+ DeleteOpts(ws_opts)
231
+ DeleteOpts(reality_opts)
232
+ tls := false
233
+ if vless.Query.Security != "" {
234
+ tls = true
235
+ }
236
+ if vless.Query.Security == "none" {
237
+ tls = false
238
+ }
239
+ vlessproxy := Proxy{
240
+ Name: vless.Name,
241
+ Type: "vless",
242
+ Server: vless.Server,
243
+ Port: vless.Port,
244
+ Servername: vless.Query.Sni,
245
+ Uuid: vless.Uuid,
246
+ Client_fingerprint: vless.Query.Fp,
247
+ Network: vless.Query.Type,
248
+ Flow: vless.Query.Flow,
249
+ Alpn: vless.Query.Alpn,
250
+ Ws_opts: ws_opts,
251
+ Reality_opts: reality_opts,
252
+ Udp: sqlconfig.Udp,
253
+ Skip_cert_verify: sqlconfig.Cert,
254
+ Tls: tls,
255
+ }
256
+ proxys = append(proxys, vlessproxy)
257
+ case Scheme == "hy" || Scheme == "hysteria":
258
+ hy, err := DecodeHYURL(link)
259
+ if err != nil {
260
+ log.Println(err)
261
+ continue
262
+ }
263
+ // 如果没有名字,就用服务器地址作为名字
264
+ if hy.Name == "" {
265
+ hy.Name = fmt.Sprintf("%s:%d", hy.Host, hy.Port)
266
+ }
267
+ hyproxy := Proxy{
268
+ Name: hy.Name,
269
+ Type: "hysteria",
270
+ Server: hy.Host,
271
+ Port: hy.Port,
272
+ Auth_str: hy.Auth,
273
+ Up: hy.UpMbps,
274
+ Down: hy.DownMbps,
275
+ Alpn: hy.ALPN,
276
+ Peer: hy.Peer,
277
+ Udp: sqlconfig.Udp,
278
+ Skip_cert_verify: sqlconfig.Cert,
279
+ }
280
+ proxys = append(proxys, hyproxy)
281
+ case Scheme == "hy2" || Scheme == "hysteria2":
282
+ hy2, err := DecodeHY2URL(link)
283
+ if err != nil {
284
+ log.Println(err)
285
+ continue
286
+ }
287
+ // 如果没有名字,就用服务器地址作为名字
288
+ if hy2.Name == "" {
289
+ hy2.Name = fmt.Sprintf("%s:%d", hy2.Host, hy2.Port)
290
+ }
291
+ hyproxy2 := Proxy{
292
+ Name: hy2.Name,
293
+ Type: "hysteria2",
294
+ Server: hy2.Host,
295
+ Port: hy2.Port,
296
+ Auth_str: hy2.Auth,
297
+ Sni: hy2.Sni,
298
+ Alpn: hy2.ALPN,
299
+ Obfs: hy2.Obfs,
300
+ Password: hy2.Password,
301
+ Obfs_password: hy2.ObfsPassword,
302
+ Udp: sqlconfig.Udp,
303
+ Skip_cert_verify: sqlconfig.Cert,
304
+ }
305
+ proxys = append(proxys, hyproxy2)
306
+ case Scheme == "tuic":
307
+ tuic, err := DecodeTuicURL(link)
308
+ if err != nil {
309
+ log.Println(err)
310
+ continue
311
+ }
312
+ // 如果没有名字,就用服务器地址作为名字
313
+ if tuic.Name == "" {
314
+ tuic.Name = fmt.Sprintf("%s:%d", tuic.Host, tuic.Port)
315
+ }
316
+ disable_sni := false
317
+ if tuic.Disable_sni == 1 {
318
+ disable_sni = true
319
+ }
320
+ tuicproxy := Proxy{
321
+ Name: tuic.Name,
322
+ Type: "tuic",
323
+ Server: tuic.Host,
324
+ Port: tuic.Port,
325
+ Password: tuic.Password,
326
+ Uuid: tuic.Uuid,
327
+ Congestion_control: tuic.Congestion_control,
328
+ Alpn: tuic.Alpn,
329
+ Udp_relay_mode: tuic.Udp_relay_mode,
330
+ Disable_sni: disable_sni,
331
+ Sni: tuic.Sni,
332
+ Udp: sqlconfig.Udp,
333
+ Skip_cert_verify: sqlconfig.Cert,
334
+ }
335
+ proxys = append(proxys, tuicproxy)
336
+ }
337
+ }
338
+ // 生成Clash配置文件
339
+ return DecodeClash(proxys, sqlconfig.Clash)
340
+ }
341
+
342
+ // DecodeClash 用于解析 Clash 配置文件
343
+ func DecodeClash(proxys []Proxy, yamlfile string) ([]byte, error) {
344
+ // 读取 YAML 文件
345
+ var data []byte
346
+ var err error
347
+ if strings.Contains(yamlfile, "://") {
348
+ resp, err := http.Get(yamlfile)
349
+ if err != nil {
350
+ log.Println("http.Get error", err)
351
+ return nil, err
352
+ }
353
+ defer resp.Body.Close()
354
+ data, err = io.ReadAll(resp.Body)
355
+ if err != nil {
356
+ log.Printf("error: %v", err)
357
+ return nil, err
358
+ }
359
+ } else {
360
+ data, err = os.ReadFile(yamlfile)
361
+ if err != nil {
362
+ log.Printf("error: %v", err)
363
+ return nil, err
364
+ }
365
+ }
366
+ // 解析 YAML 文件
367
+ config := make(map[interface{}]interface{})
368
+ err = yaml.Unmarshal(data, &config)
369
+ if err != nil {
370
+ log.Printf("error: %v", err)
371
+ return nil, err
372
+ }
373
+
374
+ // 检查 "proxies" 键是否存在于 config 中
375
+ proxies, ok := config["proxies"].([]interface{})
376
+ if !ok {
377
+ // 如果 "proxies" 键不存在,创建一个新的切片
378
+ proxies = []interface{}{}
379
+ }
380
+ // 定义一个代理列表名字
381
+ ProxiesNameList := []string{}
382
+ // 添加新代理
383
+ for _, p := range proxys {
384
+ ProxiesNameList = append(ProxiesNameList, p.Name)
385
+ proxies = append(proxies, p)
386
+ }
387
+ // proxies = append(proxies, newProxy)
388
+ config["proxies"] = proxies
389
+ // 往ProxyGroup中插入代理列表
390
+ // ProxiesNameList := []string{"newProxy", "ceshi"}
391
+ proxyGroups := config["proxy-groups"].([]interface{})
392
+ for i, pg := range proxyGroups {
393
+ proxyGroup, ok := pg.(map[string]interface{})
394
+ if !ok {
395
+ continue
396
+ }
397
+ // 如果 proxyGroup["proxies"] 是 nil,初始化它为一个空的切片
398
+ if proxyGroup["proxies"] == nil {
399
+ proxyGroup["proxies"] = []interface{}{}
400
+ }
401
+ // 如果为链式代理的话则不插入返回
402
+ // log.Print("代理类型为:", proxyGroup["type"])
403
+ if proxyGroup["type"] == "relay" {
404
+ break
405
+ }
406
+ // 清除 nil 值
407
+ var validProxies []interface{}
408
+ for _, p := range proxyGroup["proxies"].([]interface{}) {
409
+ if p != nil {
410
+ validProxies = append(validProxies, p)
411
+ }
412
+ }
413
+ // 添加新代理
414
+ for _, newProxy := range ProxiesNameList {
415
+ validProxies = append(validProxies, newProxy)
416
+ }
417
+ proxyGroup["proxies"] = validProxies
418
+ proxyGroups[i] = proxyGroup
419
+ }
420
+
421
+ config["proxy-groups"] = proxyGroups
422
+
423
+ // 将修改后的内容写回文件
424
+ newData, err := yaml.Marshal(config)
425
+ if err != nil {
426
+ log.Printf("error: %v", err)
427
+ }
428
+ return newData, nil
429
+ }
node/common.go ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package node
2
+
3
+ import (
4
+ "encoding/base64"
5
+ "fmt"
6
+ "os"
7
+ "regexp"
8
+ "strings"
9
+ )
10
+
11
+ type SqlConfig struct {
12
+ Clash string `json:"clash"`
13
+ Surge string `json:"surge"`
14
+ Udp bool `json:"udp"`
15
+ Cert bool `json:"cert"`
16
+ }
17
+
18
+ // ipv6地址匹配规则
19
+ func ValRetIPv6Addr(s string) string {
20
+ pattern := `\[([0-9a-fA-F:]+)\]`
21
+ re := regexp.MustCompile(pattern)
22
+ match := re.FindStringSubmatch(s)
23
+ if len(match) > 0 {
24
+ return match[1]
25
+ } else {
26
+ return s
27
+ }
28
+ }
29
+
30
+ // 判断是否需要补全
31
+ func IsBase64makeup(s string) string {
32
+ l := len(s)
33
+ if l%4 != 0 {
34
+ return s + strings.Repeat("=", 4-l%4)
35
+ }
36
+ return s
37
+ }
38
+
39
+ // base64编码
40
+ func Base64Encode(s string) string {
41
+ return base64.StdEncoding.EncodeToString([]byte(s))
42
+ }
43
+
44
+ // base64解码
45
+ func Base64Decode(s string) string {
46
+ // 去除空格
47
+ s = strings.ReplaceAll(s, " ", "")
48
+ // 判断是否有特殊字符来判断是标准base64还是url base64
49
+ match, err := regexp.MatchString(`[_-]`, s)
50
+ if err != nil {
51
+ fmt.Println(err)
52
+ }
53
+ if !match {
54
+ // 默认使用标准解码
55
+ encoded := IsBase64makeup(s)
56
+ decoded, err := base64.StdEncoding.DecodeString(encoded)
57
+ if err != nil {
58
+ return s // 返回原字符串
59
+ }
60
+ decoded_str := string(decoded)
61
+ return decoded_str
62
+
63
+ } else {
64
+ // 如果有特殊字符则使用URL解码
65
+ encoded := IsBase64makeup(s)
66
+ decoded, err := base64.URLEncoding.DecodeString(encoded)
67
+ if err != nil {
68
+ return s // 返回原字符串
69
+ }
70
+ decoded_str := string(decoded)
71
+ return decoded_str
72
+ }
73
+ }
74
+
75
+ // base64解码不自动补齐
76
+ func Base64Decode2(s string) string {
77
+ // 去除空格
78
+ s = strings.ReplaceAll(s, " ", "")
79
+ // 判断是否有特殊字符来判断是标准base64还是url base64
80
+ match, err := regexp.MatchString(`[_-]`, s)
81
+ if err != nil {
82
+ fmt.Println(err)
83
+ }
84
+ if !match {
85
+ // 默认使用标准解码
86
+ decoded, err := base64.StdEncoding.DecodeString(s)
87
+ if err != nil {
88
+ return s // 返回原字符串
89
+ }
90
+ decoded_str := string(decoded)
91
+ return decoded_str
92
+
93
+ } else {
94
+ // 如果有特殊字符则使用URL解码
95
+ decoded, err := base64.URLEncoding.DecodeString(s)
96
+ if err != nil {
97
+ return s // 返回原字符串
98
+ }
99
+ decoded_str := string(decoded)
100
+ return decoded_str
101
+ }
102
+ }
103
+
104
+ // 检查环境
105
+ func CheckEnvironment() bool {
106
+ APP_ENV := os.Getenv("APP_ENV")
107
+ if APP_ENV == "" {
108
+ // fmt.Println("APP_ENV环境变量未设置")
109
+ return false
110
+ }
111
+ if strings.Contains(APP_ENV, "development") {
112
+ // fmt.Println("你现在是开发环境")
113
+ return true
114
+ }
115
+ // fmt.Println("你现在是生产环境")
116
+ return false
117
+ }
node/hy.go ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package node
2
+
3
+ import (
4
+ "fmt"
5
+ "net/url"
6
+ "strconv"
7
+ "strings"
8
+ )
9
+
10
+ type HY struct {
11
+ Host string
12
+ Port int
13
+ Insecure int
14
+ Peer string
15
+ Auth string
16
+ UpMbps int
17
+ DownMbps int
18
+ ALPN []string
19
+ Name string
20
+ }
21
+
22
+ // 开发者测试 CallHy 调用
23
+ func CallHy() {
24
+ hy := HY{
25
+ Host: "qq.com",
26
+ Port: 11926,
27
+ Insecure: 1,
28
+ Peer: "youku.com",
29
+ Auth: "",
30
+ UpMbps: 11,
31
+ DownMbps: 55,
32
+ // ALPN: "h3",
33
+ }
34
+ fmt.Println(EncodeHYURL(hy))
35
+ }
36
+
37
+ // hy 编码
38
+ func EncodeHYURL(hy HY) string {
39
+ // 如果没有设置 Name,则使用 Host:Port 作为 Fragment
40
+ if hy.Name == "" {
41
+ hy.Name = fmt.Sprintf("%s:%d", hy.Host, hy.Port)
42
+ }
43
+ u := url.URL{
44
+ Scheme: "hysteria",
45
+ Host: fmt.Sprintf("%s:%d", hy.Host, hy.Port),
46
+ Fragment: hy.Name,
47
+ }
48
+ q := u.Query()
49
+ q.Set("insecure", strconv.Itoa(hy.Insecure))
50
+ q.Set("peer", hy.Peer)
51
+ q.Set("auth", hy.Auth)
52
+ q.Set("upmbps", strconv.Itoa(hy.UpMbps))
53
+ q.Set("downmbps", strconv.Itoa(hy.DownMbps))
54
+ // q.Set("alpn", hy.ALPN)
55
+ // 检查query是否有空值,有的话删除
56
+ for k, v := range q {
57
+ if v[0] == "" {
58
+ delete(q, k)
59
+ // fmt.Printf("k: %v, v: %v\n", k, v)
60
+ }
61
+ }
62
+ u.RawQuery = q.Encode()
63
+ return u.String()
64
+ }
65
+
66
+ // hy 解码
67
+ func DecodeHYURL(s string) (HY, error) {
68
+ u, err := url.Parse(s)
69
+ if err != nil {
70
+ return HY{}, fmt.Errorf("失败的URL: %s", s)
71
+ }
72
+ if u.Scheme != "hy" && u.Scheme != "hysteria" {
73
+ return HY{}, fmt.Errorf("非hy协议: %s", s)
74
+ }
75
+ server := u.Hostname()
76
+ port, _ := strconv.Atoi(u.Port())
77
+ insecure, _ := strconv.Atoi(u.Query().Get("insecure"))
78
+ auth := u.Query().Get("auth")
79
+ upMbps, _ := strconv.Atoi(u.Query().Get("upmbps"))
80
+ downMbps, _ := strconv.Atoi(u.Query().Get("downmbps"))
81
+ alpns := u.Query().Get("alpn")
82
+ alpn := strings.Split(alpns, ",")
83
+ if alpns == "" {
84
+ alpn = nil
85
+ }
86
+ // 如果没有设置 Name,则使用 Fragment 作为 Name
87
+ name := u.Fragment
88
+ if name == "" {
89
+ name = server + ":" + u.Port()
90
+ }
91
+ if CheckEnvironment() {
92
+ fmt.Println("server:", server)
93
+ fmt.Println("port:", port)
94
+ fmt.Println("insecure:", insecure)
95
+ fmt.Println("auth:", auth)
96
+ fmt.Println("upMbps:", upMbps)
97
+ fmt.Println("downMbps:", downMbps)
98
+ fmt.Println("alpn:", alpn)
99
+ fmt.Println("name:", name)
100
+ }
101
+ return HY{
102
+ Host: server,
103
+ Port: port,
104
+ Insecure: insecure,
105
+ Auth: auth,
106
+ UpMbps: upMbps,
107
+ DownMbps: downMbps,
108
+ ALPN: alpn,
109
+ Name: name,
110
+ }, nil
111
+ }
node/hy2.go ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package node
2
+
3
+ import (
4
+ "fmt"
5
+ "net/url"
6
+ "strconv"
7
+ "strings"
8
+ )
9
+
10
+ type HY2 struct {
11
+ Password string
12
+ Host string
13
+ Port int
14
+ Insecure int
15
+ Peer string
16
+ Auth string
17
+ UpMbps int
18
+ DownMbps int
19
+ ALPN []string
20
+ Name string
21
+ Sni string
22
+ Obfs string
23
+ ObfsPassword string
24
+ }
25
+
26
+ // 开发者测试 CallHy 调用
27
+ func CallHy2() {
28
+ hy2 := HY2{
29
+ Password: "asdasd",
30
+ Host: "qq.com",
31
+ Port: 11926,
32
+ Insecure: 1,
33
+ Peer: "youku.com",
34
+ Auth: "",
35
+ UpMbps: 11,
36
+ DownMbps: 55,
37
+ // ALPN: "h3",
38
+ }
39
+ fmt.Println(EncodeHY2URL(hy2))
40
+ }
41
+
42
+ // hy2 编码
43
+ func EncodeHY2URL(hy2 HY2) string {
44
+ // 如果没有设置 Name,则使用 Host:Port 作为 Fragment
45
+ if hy2.Name == "" {
46
+ hy2.Name = fmt.Sprintf("%s:%d", hy2.Host, hy2.Port)
47
+ }
48
+ u := url.URL{
49
+ Scheme: "hy2",
50
+ User: url.User(hy2.Password),
51
+ Host: fmt.Sprintf("%s:%d", hy2.Host, hy2.Port),
52
+ Fragment: hy2.Name,
53
+ }
54
+ q := u.Query()
55
+ q.Set("insecure", strconv.Itoa(hy2.Insecure))
56
+ q.Set("peer", hy2.Peer)
57
+ q.Set("auth", hy2.Auth)
58
+ q.Set("upmbps", strconv.Itoa(hy2.UpMbps))
59
+ q.Set("downmbps", strconv.Itoa(hy2.DownMbps))
60
+ // q.Set("alpn", hy2.ALPN)
61
+ // 检查query是否有空值,有的话删除
62
+ for k, v := range q {
63
+ if v[0] == "" {
64
+ delete(q, k)
65
+ // fmt.Printf("k: %v, v: %v\n", k, v)
66
+ }
67
+ }
68
+ u.RawQuery = q.Encode()
69
+ return u.String()
70
+ }
71
+
72
+ // hy2 解码
73
+ func DecodeHY2URL(s string) (HY2, error) {
74
+ u, err := url.Parse(s)
75
+ if err != nil {
76
+ return HY2{}, fmt.Errorf("解析失败的URL: %s,错误:%s", s, err)
77
+ }
78
+ if u.Scheme != "hy2" && u.Scheme != "hysteria2" {
79
+ return HY2{}, fmt.Errorf("非hy2协议: %s", s)
80
+ }
81
+ password := u.User.Username()
82
+ server := u.Hostname()
83
+ port, _ := strconv.Atoi(u.Port())
84
+ insecure, _ := strconv.Atoi(u.Query().Get("insecure"))
85
+ auth := u.Query().Get("auth")
86
+ upMbps, _ := strconv.Atoi(u.Query().Get("upmbps"))
87
+ downMbps, _ := strconv.Atoi(u.Query().Get("downmbps"))
88
+ alpns := u.Query().Get("alpn")
89
+ alpn := strings.Split(alpns, ",")
90
+ if alpns == "" {
91
+ alpn = nil
92
+ }
93
+ sni := u.Query().Get("sni")
94
+ obfs := u.Query().Get("obfs")
95
+ obfsPassword := u.Query().Get("obfs-password")
96
+ name := u.Fragment
97
+ // 如果没有设置 Name,则使用 Host:Port 作为 Fragment
98
+ if name == "" {
99
+ name = server + ":" + u.Port()
100
+ }
101
+ if CheckEnvironment() {
102
+ fmt.Println("password:", password)
103
+ fmt.Println("server:", server)
104
+ fmt.Println("port:", port)
105
+ fmt.Println("insecure:", insecure)
106
+ fmt.Println("auth:", auth)
107
+ fmt.Println("upMbps:", upMbps)
108
+ fmt.Println("downMbps:", downMbps)
109
+ fmt.Println("alpn:", alpn)
110
+ fmt.Println("sni:", sni)
111
+ fmt.Println("obfs:", obfs)
112
+ fmt.Println("obfsPassword:", obfsPassword)
113
+ fmt.Println("name:", name)
114
+ }
115
+ return HY2{
116
+ Password: password,
117
+ Host: server,
118
+ Port: port,
119
+ Insecure: insecure,
120
+ Auth: auth,
121
+ UpMbps: upMbps,
122
+ DownMbps: downMbps,
123
+ ALPN: alpn,
124
+ Name: name,
125
+ Sni: sni,
126
+ Obfs: obfs,
127
+ ObfsPassword: obfsPassword,
128
+ }, nil
129
+ }
node/ss.go ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package node
2
+
3
+ import (
4
+ "fmt"
5
+ "log"
6
+ "net/url"
7
+ "strconv"
8
+ "strings"
9
+ )
10
+
11
+ // ss匹配规则
12
+ type Ss struct {
13
+ Param Param
14
+ Server string
15
+ Port int
16
+ Name string
17
+ Type string
18
+ }
19
+ type Param struct {
20
+ Cipher string
21
+ Password string
22
+ }
23
+
24
+ func parsingSS(s string) (string, string, string) {
25
+ /* ss url编码分为三部分:加密方式、服务器地址和端口、备注
26
+ ://和@之前为第一部分 @到#之间为第二部分 #之后为第三部分
27
+ 第一部分 为加密方式和密码,格式为:加密方式:密码 示例:aes-128-gcm:123456
28
+ 第二部分 为服务器地址和端口,格式为:服务器地址:端口 示例:xxx.xxx:12345
29
+ 第三部分 为备注,格式为:#备注 示例:#备注
30
+ */
31
+ u, err := url.Parse(s)
32
+ if err != nil {
33
+ log.Println("ss url parse fail.", err)
34
+ return "", "", ""
35
+ }
36
+ if u.Scheme != "ss" {
37
+ log.Println("ss url parse fail, not ss url.")
38
+ return "", "", ""
39
+ }
40
+ // 处理url全编码的情况
41
+ if u.User == nil {
42
+ // 截取ss://后的字符串
43
+ raw := s[5:]
44
+ s = "ss://" + Base64Decode(raw)
45
+ u, err = url.Parse(s)
46
+ }
47
+ var auth, addr, name string
48
+ auth = u.User.String()
49
+ if u.Host != "" {
50
+ addr = u.Host
51
+ }
52
+ if u.Fragment != "" {
53
+ name = u.Fragment
54
+ }
55
+ return auth, addr, name
56
+ }
57
+
58
+ // 开发者测试
59
+ func CallSSURL() {
60
+ ss := Ss{}
61
+ // ss.Name = "测试"
62
+ ss.Server = "baidu.com"
63
+ ss.Port = 443
64
+ ss.Param.Cipher = "2022-blake3-aes-256-gcm"
65
+ ss.Param.Password = "asdasd"
66
+ fmt.Println(EncodeSSURL(ss))
67
+ }
68
+
69
+ // ss 编码输出
70
+ func EncodeSSURL(s Ss) string {
71
+ //编码格式 ss://base64(base64(method:password)@hostname:port)
72
+ p := Base64Encode(s.Param.Cipher + ":" + s.Param.Password)
73
+ // 假设备注没有使用服务器加端口命名
74
+ if s.Name == "" {
75
+ s.Name = s.Server + ":" + strconv.Itoa(s.Port)
76
+ }
77
+ param := fmt.Sprintf("%s@%s:%s#%s",
78
+ p,
79
+ s.Server,
80
+ strconv.Itoa(s.Port),
81
+ s.Name,
82
+ )
83
+ return "ss://" + param
84
+ }
85
+
86
+ func DecodeSSURL(s string) (Ss, error) {
87
+ // 解析ss链接
88
+ param, addr, name := parsingSS(s)
89
+ // base64解码
90
+ param = Base64Decode(param)
91
+ // 判断是否为空
92
+ if param == "" || addr == "" {
93
+ return Ss{}, fmt.Errorf("invalid SS URL")
94
+ }
95
+ // 解析参数
96
+ parts := strings.Split(addr, ":")
97
+ port, _ := strconv.Atoi(parts[len(parts)-1])
98
+ server := strings.Replace(ValRetIPv6Addr(addr), ":"+parts[len(parts)-1], "", -1)
99
+ cipher := strings.Split(param, ":")[0]
100
+ password := strings.Replace(param, cipher+":", "", 1)
101
+ // 如果没有备注则使用服务器加端口命名
102
+ if name == "" {
103
+ name = addr
104
+ }
105
+ // 开发环境输出结果
106
+ if CheckEnvironment() {
107
+ fmt.Println("Param:", Base64Decode(param))
108
+ fmt.Println("Server", server)
109
+ fmt.Println("Port", port)
110
+ fmt.Println("Name:", name)
111
+ fmt.Println("Cipher:", cipher)
112
+ fmt.Println("Password:", password)
113
+ }
114
+ // 返回结果
115
+ return Ss{
116
+ Param: Param{
117
+ Cipher: cipher,
118
+ Password: password,
119
+ },
120
+ Server: server,
121
+ Port: port,
122
+ Name: name,
123
+ Type: "ss",
124
+ }, nil
125
+ }
node/ss_test.go ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package node
2
+
3
+ import (
4
+ "fmt"
5
+ "testing"
6
+ )
7
+
8
+ func TestDecodeSSURL(t *testing.T) {
9
+ type args struct {
10
+ s string
11
+ }
12
+ tests := []struct {
13
+ name string
14
+ args args
15
+ want Ss
16
+ wantErr bool
17
+ }{
18
+ {
19
+ name: "ss",
20
+ args: args{
21
+ s: "ss://YWVzLTI1Ni1nY206NEhrdSt0Vk53SnFyblVZR2JycE95YkVhck03QmhxYmdhRTFxRk1JPQ==@127.0.0.1:34020?type=tcp#ocent-ss-ndptvd0p",
22
+ },
23
+ }, {
24
+ name: "ss2",
25
+ args: args{
26
+ s: "ss://YWVzLTI1Ni1jZmI6S1NYTmhuWnBqd0M2UGM2Q0A1NC4xNjkuMzUuMjI4OjMxNDQ0",
27
+ },
28
+ }, {
29
+ name: "no ss schema",
30
+ args: args{
31
+ s: "noss://YWVzLTI1Ni1jZmI6S1NYTmhuWnBqd0M2UGM2Q0A1NC4xNjkuMzUuMjI4OjMxNDQ0",
32
+ },
33
+ wantErr: true,
34
+ },
35
+ }
36
+ for _, tt := range tests {
37
+ t.Run(tt.name, func(t *testing.T) {
38
+ got, err := DecodeSSURL(tt.args.s)
39
+ if (err != nil) != tt.wantErr {
40
+ t.Errorf("DecodeSSURL() error = %v, wantErr %v", err, tt.wantErr)
41
+ return
42
+ }
43
+ fmt.Println(got)
44
+ })
45
+ }
46
+ }
node/ssr.go ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package node
2
+
3
+ import (
4
+ "errors"
5
+ "fmt"
6
+ "strconv"
7
+ "strings"
8
+ )
9
+
10
+ func CallSSRURL() {
11
+ ssr := new(Ssr)
12
+ ssr.Server = "xx.com"
13
+ ssr.Port = 443
14
+ ssr.Protocol = "auth_aes128_md5"
15
+ ssr.Method = "aes-256-cfb"
16
+ ssr.Obfs = "tls1.2_ticket_auth"
17
+ ssr.Password = "123456"
18
+ ssr.Qurey = Ssrquery{
19
+ Obfsparam: "",
20
+ Remarks: "没有名字",
21
+ }
22
+ cc := EncodeSSRURL(*ssr)
23
+ fmt.Println(cc)
24
+ }
25
+
26
+ // ssr格式编码输出
27
+ func EncodeSSRURL(s Ssr) string {
28
+ /*编码格式
29
+ ssr://base64(host:port:protocol:method:obfs:base64(password)/?obfsparam=base64(obfsparam)&protoparam=base64(protoparam)&remarks=base64(remarks)&group=base64(group))
30
+ */
31
+ obfsparam := "obfsparam=" + Base64Encode(s.Qurey.Obfsparam)
32
+ remarks := "remarks=" + Base64Encode(s.Qurey.Remarks)
33
+ // 如果没有备注默认使用服务器+端口作为备注
34
+ if s.Qurey.Remarks == "" {
35
+ server_port := Base64Encode(s.Server + ":" + strconv.Itoa(s.Port))
36
+ remarks = fmt.Sprintf("remarks=%s", server_port)
37
+ }
38
+ param := fmt.Sprintf("%s:%d:%s:%s:%s:%s/?%s&%s",
39
+ s.Server,
40
+ s.Port,
41
+ s.Protocol,
42
+ s.Method,
43
+ s.Obfs,
44
+ Base64Encode(s.Password),
45
+ obfsparam,
46
+ remarks,
47
+ )
48
+ return "ssr://" + Base64Encode(param)
49
+
50
+ }
51
+
52
+ // ssr解码
53
+ func DecodeSSRURL(s string) (Ssr, error) {
54
+ /*解析格式
55
+ ssr://base64(host:port:protocol:method:obfs:base64(password)/?obfsparam=base64(obfsparam)&protoparam=base64(protoparam)&remarks=base64(remarks)&group=base64(group))
56
+ */
57
+ // 处理url链接中的base64编码
58
+ parts := strings.SplitN(s, "ssr://", 2)
59
+ if len(parts) != 2 {
60
+ return Ssr{}, errors.New("invalid SSR URL")
61
+ }
62
+ s = parts[0] + Base64Decode(parts[1])
63
+ // 检查是否包含"/?" 如果有就是有备注信息
64
+ var remarks, obfsparam string
65
+ if strings.Contains(s, "/?") {
66
+ // 解析备注信息
67
+ query := strings.Split(s, "/?")[1]
68
+ s = strings.Replace(s, "/?"+query, "", 1)
69
+ paramMap := make(map[string]string)
70
+ if strings.Contains(query, "&") {
71
+ params := strings.Split(query, "&")
72
+ for _, param := range params {
73
+ parts := strings.SplitN(param, "=", 2)
74
+ if len(parts) != 2 {
75
+ fmt.Println("Invalid parameter: ", param)
76
+ continue
77
+ }
78
+ paramMap[parts[0]] = parts[1]
79
+ }
80
+ } else {
81
+ q := strings.Split(query, "=")
82
+ paramMap[q[0]] = q[1]
83
+ }
84
+ remarks = Base64Decode(paramMap["remarks"])
85
+ obfsparam = Base64Decode(paramMap["obfsparam"])
86
+ defer func() {
87
+ if CheckEnvironment() {
88
+ fmt.Println("remarks", remarks)
89
+ fmt.Println("obfsparam", obfsparam)
90
+ }
91
+ }()
92
+ }
93
+ // 反着解析参数 怕有ipv6地址冒号混淆
94
+ param := strings.Split(s, ":")
95
+ if len(param) < 6 {
96
+ return Ssr{}, errors.New("长度没有6")
97
+ }
98
+ password := param[len(param)-1]
99
+ obfs := param[len(param)-2]
100
+ method := param[len(param)-3]
101
+ protocol := param[len(param)-4]
102
+ port, _ := strconv.Atoi(param[len(param)-5])
103
+ server := ValRetIPv6Addr(param[len(param)-6])
104
+ // 如果没有备注默认使用服务器+端口作为备注
105
+ if remarks == "" {
106
+ remarks = server + ":" + strconv.Itoa(port)
107
+ }
108
+ if CheckEnvironment() {
109
+ fmt.Println("password", password)
110
+ fmt.Println("obfs", obfs)
111
+ fmt.Println("method", method)
112
+ fmt.Println("protocol", protocol)
113
+ fmt.Println("port", port)
114
+ fmt.Println("server", server)
115
+ }
116
+ return Ssr{
117
+ Server: server,
118
+ Port: port,
119
+ Protocol: protocol,
120
+ Method: method,
121
+ Obfs: obfs,
122
+ Password: password,
123
+ Qurey: Ssrquery{
124
+ Obfsparam: obfsparam,
125
+ Remarks: remarks,
126
+ },
127
+ Type: "ssr",
128
+ }, nil
129
+ }
130
+
131
+ type Ssr struct {
132
+ Server string
133
+ Port int
134
+ Protocol string
135
+ Method string
136
+ Obfs string
137
+ Password string
138
+ Qurey Ssrquery
139
+ Type string
140
+ }
141
+ type Ssrquery struct {
142
+ Obfsparam string
143
+ Remarks string
144
+ }
node/surge.go ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package node
2
+
3
+ import (
4
+ "fmt"
5
+ "io"
6
+ "log"
7
+ "net/http"
8
+ "os"
9
+ "regexp"
10
+ "strings"
11
+ )
12
+
13
+ func EncodeSurge(urls []string, sqlconfig SqlConfig) (string, error) {
14
+ var proxys, groups []string
15
+ for _, link := range urls {
16
+ Scheme := strings.Split(link, "://")[0]
17
+ switch {
18
+ case Scheme == "ss":
19
+ ss, err := DecodeSSURL(link)
20
+ if err != nil {
21
+ log.Println(err)
22
+ continue
23
+ }
24
+ proxy := map[string]interface{}{
25
+ "name": ss.Name,
26
+ "server": ss.Server,
27
+ "port": ss.Port,
28
+ "cipher": ss.Param.Cipher,
29
+ "password": ss.Param.Password,
30
+ "udp": sqlconfig.Udp,
31
+ }
32
+ ssproxy := fmt.Sprintf("%s = ss, %s, %d, encrypt-method=%s, password=%s, udp-relay=%t",
33
+ proxy["name"], proxy["server"], proxy["port"], proxy["cipher"], proxy["password"], proxy["udp"])
34
+ groups = append(groups, ss.Name)
35
+ proxys = append(proxys, ssproxy)
36
+ case Scheme == "vmess":
37
+ vmess, err := DecodeVMESSURL(link)
38
+ if err != nil {
39
+ log.Println(err)
40
+ continue
41
+ }
42
+ tls := false
43
+ if vmess.Tls != "none" && vmess.Tls != "" {
44
+ tls = true
45
+ }
46
+ port, _ := convertToInt(vmess.Port)
47
+ proxy := map[string]interface{}{
48
+ "name": vmess.Ps,
49
+ "server": vmess.Add,
50
+ "port": port,
51
+ "uuid": vmess.Id,
52
+ "tls": tls,
53
+ "network": vmess.Net,
54
+ "ws-path": vmess.Path,
55
+ "ws-host": vmess.Host,
56
+ "udp": sqlconfig.Udp,
57
+ "skip-cert-verify": sqlconfig.Cert,
58
+ }
59
+ vmessproxy := fmt.Sprintf("%s = vmess, %s, %d, username=%s , tls=%t, vmess-aead=true, udp-relay=%t , skip-cert-verify=%t",
60
+ proxy["name"], proxy["server"], proxy["port"], proxy["uuid"], proxy["tls"], proxy["udp"], proxy["skip-cert-verify"])
61
+ if vmess.Net == "ws" {
62
+ vmessproxy = fmt.Sprintf("%s, ws=true,ws-path=%s", vmessproxy, proxy["ws-path"])
63
+ if vmess.Host != "" && vmess.Host != "none" {
64
+ vmessproxy = fmt.Sprintf("%s, ws-headers=Host:%s", vmessproxy, proxy["ws-host"])
65
+ }
66
+ }
67
+ if vmess.Sni != "" {
68
+ vmessproxy = fmt.Sprintf("%s, sni=%s", vmessproxy, vmess.Sni)
69
+ }
70
+ groups = append(groups, vmess.Ps)
71
+ proxys = append(proxys, vmessproxy)
72
+ case Scheme == "trojan":
73
+ trojan, err := DecodeTrojanURL(link)
74
+ if err != nil {
75
+ log.Println(err)
76
+ continue
77
+ }
78
+ proxy := map[string]interface{}{
79
+ "name": trojan.Name,
80
+ "server": trojan.Hostname,
81
+ "port": trojan.Port,
82
+ "password": trojan.Password,
83
+ "udp": sqlconfig.Udp,
84
+ "skip-cert-verify": sqlconfig.Cert,
85
+ }
86
+ trojanproxy := fmt.Sprintf("%s = trojan, %s, %d, password=%s, udp-relay=%t, skip-cert-verify=%t",
87
+ proxy["name"], proxy["server"], proxy["port"], proxy["password"], proxy["udp"], proxy["skip-cert-verify"])
88
+ if trojan.Query.Sni != "" {
89
+ trojanproxy = fmt.Sprintf("%s, sni=%s", trojanproxy, trojan.Query.Sni)
90
+
91
+ }
92
+ groups = append(groups, trojan.Name)
93
+ proxys = append(proxys, trojanproxy)
94
+ case Scheme == "hysteria2" || Scheme == "hy2":
95
+ hy2, err := DecodeHY2URL(link)
96
+ if err != nil {
97
+ log.Println(err)
98
+ continue
99
+ }
100
+ proxy := map[string]interface{}{
101
+ "name": hy2.Name,
102
+ "server": hy2.Host,
103
+ "port": hy2.Port,
104
+ "password": hy2.Password,
105
+ "udp": sqlconfig.Udp,
106
+ "skip-cert-verify": sqlconfig.Cert,
107
+ }
108
+ hy2proxy := fmt.Sprintf("%s = hysteria2, %s, %d, password=%s, udp-relay=%t, skip-cert-verify=%t",
109
+ proxy["name"], proxy["server"], proxy["port"], proxy["password"], proxy["udp"], proxy["skip-cert-verify"])
110
+ if hy2.Sni != "" {
111
+ hy2proxy = fmt.Sprintf("%s, sni=%s", hy2proxy, hy2.Sni)
112
+
113
+ }
114
+ groups = append(groups, hy2.Name)
115
+ proxys = append(proxys, hy2proxy)
116
+ case Scheme == "tuic":
117
+ tuic, err := DecodeTuicURL(link)
118
+ if err != nil {
119
+ log.Println(err)
120
+ continue
121
+ }
122
+ proxy := map[string]interface{}{
123
+ "name": tuic.Name,
124
+ "server": tuic.Host,
125
+ "port": tuic.Port,
126
+ "password": tuic.Password,
127
+ "udp": sqlconfig.Udp,
128
+ "skip-cert-verify": sqlconfig.Cert,
129
+ }
130
+ tuicproxy := fmt.Sprintf("%s = tuic, %s, %d, token=%s, udp-relay=%t, skip-cert-verify=%t",
131
+ proxy["name"], proxy["server"], proxy["port"], proxy["password"], proxy["udp"], proxy["skip-cert-verify"])
132
+ groups = append(groups, tuic.Name)
133
+ proxys = append(proxys, tuicproxy)
134
+ }
135
+ }
136
+ return DecodeSurge(proxys, groups, sqlconfig.Surge)
137
+ }
138
+ func DecodeSurge(proxys, groups []string, file string) (string, error) {
139
+ var surge []byte
140
+ var err error
141
+ if strings.Contains(file, "://") {
142
+ resp, err := http.Get(file)
143
+ if err != nil {
144
+ log.Println("http.Get error", err)
145
+ return "", err
146
+ }
147
+ defer resp.Body.Close()
148
+ surge, err = io.ReadAll(resp.Body)
149
+ if err != nil {
150
+ log.Printf("error: %v", err)
151
+ return "", err
152
+ }
153
+ } else {
154
+ surge, err = os.ReadFile(file)
155
+ if err != nil {
156
+ log.Println(err)
157
+ return "", err
158
+ }
159
+ }
160
+
161
+ proxyReg := regexp.MustCompile(`(?s)\[Proxy\](.*?)\[*]`)
162
+ groupReg := regexp.MustCompile(`(?s)\[Proxy Group\](.*?)\[*]`)
163
+
164
+ proxyPart := proxyReg.ReplaceAllStringFunc(string(surge), func(s string) string {
165
+
166
+ text := strings.Join(proxys, "\n")
167
+ return "[Proxy]\n" + text + s[len("[Proxy]"):]
168
+ })
169
+ groupPart := groupReg.ReplaceAllStringFunc(proxyPart, func(s string) string {
170
+ lines := strings.Split(s, "\n")
171
+ grouplist := strings.Join(groups, ",")
172
+ for i, line := range lines {
173
+
174
+ if strings.Contains(line, "=") {
175
+ lines[i] = strings.TrimSpace(line) + ", " + grouplist
176
+ // lines[i] = line + "," + grouplist
177
+ }
178
+ }
179
+ return strings.Join(lines, "\n") + s[len("[Proxy Group]"):]
180
+ })
181
+
182
+ return groupPart, nil
183
+ }
node/trojan.go ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package node
2
+
3
+ import (
4
+ "fmt"
5
+ "net/url"
6
+ "strconv"
7
+ "strings"
8
+ )
9
+
10
+ type Trojan struct {
11
+ Password string `json:"password"`
12
+ Hostname string `json:"hostname"`
13
+ Port int `json:"port"`
14
+ Query TrojanQuery `json:"query,omitempty"`
15
+ Name string `json:"name"`
16
+ Type string `json:"type"`
17
+ }
18
+ type TrojanQuery struct {
19
+ Peer string `json:"peer,omitempty"`
20
+ Type string `json:"type,omitempty"`
21
+ Path string `json:"path,omitempty"`
22
+ Security string `json:"security,omitempty"`
23
+ Fp string `json:"fp,omitempty"`
24
+ AllowInsecure int `json:"allowInsecure,omitempty"`
25
+ Alpn []string `json:"alpn,omitempty"`
26
+ Sni string `json:"sni,omitempty"`
27
+ Host string `json:"host,omitempty"`
28
+ Flow string `json:"flow,omitempty"`
29
+ }
30
+
31
+ // 开发者测试
32
+ func CallTrojan() {
33
+ trojan := Trojan{
34
+ Password: "4cf3ca26cf114871b3d186a361a3de3",
35
+ Hostname: "baidu.com",
36
+ Port: 443,
37
+ Query: TrojanQuery{
38
+ Peer: "",
39
+ Type: "tcp",
40
+ Path: "",
41
+ Security: "tls",
42
+ Fp: "",
43
+ AllowInsecure: 0,
44
+ Alpn: []string{"h2", "http/1.1"},
45
+ Sni: "baidu.com",
46
+ Host: "",
47
+ Flow: "",
48
+ },
49
+ }
50
+ fmt.Println(EncodeTrojanURL(trojan))
51
+ }
52
+
53
+ // trojan 编码
54
+ func EncodeTrojanURL(t Trojan) string {
55
+ /*
56
+ trojan://password@hostname:port?peer=example.com&allowInsecure=0&sni=example.com
57
+ */
58
+ u := url.URL{
59
+ Scheme: "trojan",
60
+ User: url.User(t.Password),
61
+ Host: fmt.Sprintf("%s:%d", t.Hostname, t.Port),
62
+ }
63
+ q := u.Query()
64
+ q.Set("peer", t.Query.Peer)
65
+ q.Set("allowInsecure", fmt.Sprintf("%d", t.Query.AllowInsecure))
66
+ q.Set("sni", t.Query.Sni)
67
+ q.Set("type", t.Query.Type)
68
+ q.Set("path", t.Query.Path)
69
+ q.Set("security", t.Query.Security)
70
+ q.Set("fp", t.Query.Fp)
71
+ // q.Set("alpn", t.Query.Alpn)
72
+ q.Set("host", t.Query.Host)
73
+ q.Set("flow", t.Query.Flow)
74
+ // 检查query是否有空值,有的话删除
75
+ for k, v := range q {
76
+ if v[0] == "" {
77
+ delete(q, k)
78
+ // fmt.Printf("k: %v, v: %v\n", k, v)
79
+ }
80
+ }
81
+ // 如果没有设置name,则使用hostname:port
82
+ if t.Name == "" {
83
+ t.Name = t.Hostname + ":" + strconv.Itoa(t.Port)
84
+ }
85
+ u.Fragment = t.Name
86
+ u.RawQuery = q.Encode()
87
+ return u.String()
88
+ }
89
+
90
+ // trojan 解码
91
+ func DecodeTrojanURL(s string) (Trojan, error) {
92
+ /*
93
+ trojan://password@hostname:port?peer=example.com&allowInsecure=0&sni=example.com
94
+ */
95
+ u, err := url.Parse(s)
96
+ if err != nil {
97
+ return Trojan{}, fmt.Errorf("url格式化失败:%s", s)
98
+ }
99
+ if u.Scheme != "trojan" {
100
+ return Trojan{}, fmt.Errorf("非trojan协议: %s", s)
101
+ }
102
+ password := u.User.Username()
103
+ hostname := u.Hostname()
104
+ port, _ := strconv.Atoi(u.Port())
105
+ peer := u.Query().Get("peer")
106
+ allowInsecure := u.Query().Get("allowInsecure")
107
+ sni := u.Query().Get("sni")
108
+ types := u.Query().Get("type")
109
+ path := u.Query().Get("path")
110
+ security := u.Query().Get("security")
111
+ fp := u.Query().Get("fp")
112
+ alpns := u.Query().Get("alpn")
113
+ alpn := strings.Split(alpns, ",")
114
+ if alpns == "" {
115
+ alpn = nil
116
+ }
117
+ host := u.Query().Get("host")
118
+ flow := u.Query().Get("flow")
119
+ name := u.Fragment
120
+ // 如果没有设置name,则使用hostname:port
121
+ if name == "" {
122
+ name = hostname + ":" + u.Port()
123
+ }
124
+ if CheckEnvironment() {
125
+ fmt.Println("password:", password)
126
+ fmt.Println("password:", u.User.Username())
127
+ fmt.Println("hostname:", hostname)
128
+ fmt.Println("port:", port)
129
+ fmt.Println("peer:", peer)
130
+ fmt.Println("allowInsecure:", allowInsecure)
131
+ fmt.Println("sni:", sni)
132
+ fmt.Println("type:", types)
133
+ fmt.Println("path:", path)
134
+ fmt.Println("security:", security)
135
+ fmt.Println("fp:", fp)
136
+ fmt.Println("alpn:", alpn)
137
+ fmt.Println("host:", host)
138
+ fmt.Println("flow:", flow)
139
+ fmt.Println("name:", name)
140
+ }
141
+ return Trojan{
142
+ Password: password,
143
+ Hostname: hostname,
144
+ Port: port,
145
+ Query: TrojanQuery{
146
+ Peer: peer,
147
+ Type: types,
148
+ Path: path,
149
+ Security: security,
150
+ Fp: fp,
151
+ AllowInsecure: 0,
152
+ Alpn: alpn,
153
+ Sni: sni,
154
+ Host: host,
155
+ Flow: flow,
156
+ },
157
+ Name: name,
158
+ Type: "trojan",
159
+ }, nil
160
+ }
node/tuic.go ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package node
2
+
3
+ import (
4
+ "fmt"
5
+ "net/url"
6
+ "strconv"
7
+ "strings"
8
+ )
9
+
10
+ type Tuic struct {
11
+ Name string
12
+ Password string
13
+ Host string
14
+ Port int
15
+ Uuid string
16
+ Congestion_control string
17
+ Alpn []string
18
+ Sni string
19
+ Udp_relay_mode string
20
+ Disable_sni int
21
+ }
22
+
23
+ // Tuic 解码
24
+ func DecodeTuicURL(s string) (Tuic, error) {
25
+ u, err := url.Parse(s)
26
+ if err != nil {
27
+ return Tuic{}, fmt.Errorf("解析失败的URL: %s", s)
28
+ }
29
+ if u.Scheme != "tuic" {
30
+ return Tuic{}, fmt.Errorf("非tuic协议: %s", s)
31
+ }
32
+
33
+ uuid := u.User.Username()
34
+ password, _ := u.User.Password()
35
+ // log.Println(password)
36
+ // password = Base64Decode2(password)
37
+ server := u.Hostname()
38
+ port, _ := strconv.Atoi(u.Port())
39
+ Congestioncontrol := u.Query().Get("Congestion_control")
40
+ alpns := u.Query().Get("alpn")
41
+ alpn := strings.Split(alpns, ",")
42
+ if alpns == "" {
43
+ alpn = nil
44
+ }
45
+ sni := u.Query().Get("sni")
46
+ Udprelay_mode := u.Query().Get("Udp_relay_mode")
47
+ Disablesni, _ := strconv.Atoi(u.Query().Get("Disable_sni"))
48
+ name := u.Fragment
49
+ // 如果没有设置 Name,则使用 Host:Port 作为 Fragment
50
+ if name == "" {
51
+ name = server + ":" + u.Port()
52
+ }
53
+ if CheckEnvironment() {
54
+ fmt.Println("password:", password)
55
+ fmt.Println("server:", server)
56
+ fmt.Println("port:", port)
57
+ fmt.Println("insecure:", Congestioncontrol)
58
+ fmt.Println("uuid:", uuid)
59
+ fmt.Println("Udprelay_mode:", Udprelay_mode)
60
+ fmt.Println("alpn:", alpn)
61
+ fmt.Println("sni:", sni)
62
+ fmt.Println("Disablesni:", Disablesni)
63
+ fmt.Println("name:", name)
64
+ }
65
+ return Tuic{
66
+ Name: name,
67
+ Password: password,
68
+ Host: server,
69
+ Port: port,
70
+ Uuid: uuid,
71
+ Congestion_control: Congestioncontrol,
72
+ Alpn: alpn,
73
+ Sni: sni,
74
+ Udp_relay_mode: Udprelay_mode,
75
+ Disable_sni: Disablesni,
76
+ }, nil
77
+ }
node/vless.go ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package node
2
+
3
+ import (
4
+ "fmt"
5
+ "net/url"
6
+ "strconv"
7
+ "strings"
8
+ )
9
+
10
+ type VLESS struct {
11
+ Name string `json:"name"`
12
+ Uuid string `json:"uuid"`
13
+ Server string `json:"server"`
14
+ Port int `json:"port"`
15
+ Query VLESSQuery `json:"query"`
16
+ }
17
+ type VLESSQuery struct {
18
+ Security string `json:"security"`
19
+ Alpn []string `json:"alpn"`
20
+ Sni string `json:"sni"`
21
+ Fp string `json:"fp"`
22
+ Sid string `json:"sid"`
23
+ Pbk string `json:"pbk"`
24
+ Flow string `json:"flow"`
25
+ Encryption string `json:"encryption"`
26
+ Type string `json:"type"`
27
+ HeaderType string `json:"headerType"`
28
+ Path string `json:"path"`
29
+ Host string `json:"host"`
30
+ }
31
+
32
+ func CallVLESS() {
33
+ vless := VLESS{
34
+ Name: "Sharon-香港",
35
+ Uuid: "6adb4f43-9813-45f4-abf8-772be7db08sd",
36
+ Server: "ss.com",
37
+ Port: 443,
38
+ Query: VLESSQuery{
39
+ Security: "reality",
40
+ // Alpn: "",
41
+ Sni: "ss.com",
42
+ Fp: "chrome",
43
+ Sid: "",
44
+ Pbk: "g-oxbqigzCaXqARxuyD2_vbTYeMD9zn8wnTo02S69QM",
45
+ Flow: "xtls-rprx-vision",
46
+ Encryption: "none",
47
+ Type: "tcp",
48
+ HeaderType: "none",
49
+ Path: "",
50
+ Host: "",
51
+ },
52
+ }
53
+ fmt.Println(EncodeVLESSURL(vless))
54
+ }
55
+
56
+ // vless编码
57
+ func EncodeVLESSURL(v VLESS) string {
58
+ /*
59
+ base64(username@host:port?encryption=none&security=auto&type=tcp)
60
+ */
61
+ u := url.URL{
62
+ Scheme: "vless",
63
+ User: url.User(v.Uuid),
64
+ Host: fmt.Sprintf("%s:%d", v.Server, v.Port),
65
+ }
66
+ q := u.Query()
67
+ q.Set("security", v.Query.Security)
68
+ // q.Set("alpn", v.Query.Alpn)
69
+ q.Set("sni", v.Query.Sni)
70
+ q.Set("fp", v.Query.Fp)
71
+ q.Set("sid", v.Query.Sid)
72
+ q.Set("pbk", v.Query.Pbk)
73
+ q.Set("flow", v.Query.Flow)
74
+ q.Set("encryption", v.Query.Encryption)
75
+ q.Set("type", v.Query.Type)
76
+ q.Set("headerType", v.Query.HeaderType)
77
+ q.Set("path", v.Query.Path)
78
+ q.Set("host", v.Query.Host)
79
+ u.Fragment = v.Name
80
+ // 检查query是否有空值,有的话删除
81
+ for k, v := range q {
82
+ if v[0] == "" {
83
+ delete(q, k)
84
+ // fmt.Printf("k: %v, v: %v\n", k, v)
85
+ }
86
+ }
87
+ u.RawQuery = q.Encode()
88
+ // 如果没有name则用服务器加端口
89
+ if v.Name != "" {
90
+ u.Fragment = v.Server + ":" + strconv.Itoa(v.Port)
91
+ }
92
+ return u.String()
93
+ }
94
+
95
+ // vless解码
96
+ func DecodeVLESSURL(s string) (VLESS, error) {
97
+ /*
98
+ base64(username@host:port?encryption=none&security=auto&type=tcp)
99
+ */
100
+ // 解析base64然后重新url编码
101
+ if !strings.Contains(s, "vless://") {
102
+ return VLESS{}, fmt.Errorf("非vless协议: %s", s)
103
+ }
104
+ s = "vless://" + Base64Decode(strings.Split(s, "://")[1])
105
+ // 解析url
106
+ u, err := url.Parse(s)
107
+ if err != nil {
108
+ return VLESS{}, fmt.Errorf("url parse error: %v", err)
109
+ }
110
+ uuid := u.User.Username()
111
+ hostname := u.Hostname()
112
+ port, _ := strconv.Atoi(u.Port())
113
+ encryption := u.Query().Get("encryption")
114
+ security := u.Query().Get("security")
115
+ types := u.Query().Get("type")
116
+ flow := u.Query().Get("flow")
117
+ headerType := u.Query().Get("headerType")
118
+ pbk := u.Query().Get("pbk")
119
+ sid := u.Query().Get("sid")
120
+ fp := u.Query().Get("fp")
121
+ alpns := u.Query().Get("alpn")
122
+ alpn := strings.Split(alpns, ",")
123
+ if alpns == "" {
124
+ alpn = nil
125
+ }
126
+ sni := u.Query().Get("sni")
127
+ path := u.Query().Get("path")
128
+ host := u.Query().Get("host")
129
+ // 如果没有设置name,则使用hostname:port
130
+ name := u.Fragment
131
+ if name == "" {
132
+ name = hostname + ":" + u.Port()
133
+ }
134
+ if CheckEnvironment() {
135
+ fmt.Println("uuid:", uuid)
136
+ fmt.Println("hostname:", hostname)
137
+ fmt.Println("port:", port)
138
+ fmt.Println("encryption:", encryption)
139
+ fmt.Println("security:", security)
140
+ fmt.Println("type:", types)
141
+ fmt.Println("flow:", flow)
142
+ fmt.Println("headerType:", headerType)
143
+ fmt.Println("pbk:", pbk)
144
+ fmt.Println("sid:", sid)
145
+ fmt.Println("fp:", fp)
146
+ fmt.Println("alpn:", alpn)
147
+ fmt.Println("sni:", sni)
148
+ fmt.Println("path:", path)
149
+ fmt.Println("host:", host)
150
+ fmt.Println("name:", name)
151
+ }
152
+ return VLESS{
153
+ Name: name,
154
+ Uuid: uuid,
155
+ Server: hostname,
156
+ Port: port,
157
+ Query: VLESSQuery{
158
+ Security: security,
159
+ Alpn: alpn,
160
+ Sni: sni,
161
+ Fp: fp,
162
+ Sid: sid,
163
+ Pbk: pbk,
164
+ Flow: flow,
165
+ Encryption: encryption,
166
+ Type: types,
167
+ HeaderType: headerType,
168
+ Path: path,
169
+ Host: host,
170
+ },
171
+ }, nil
172
+ }
node/vmess.go ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package node
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "log"
7
+ "strings"
8
+ )
9
+
10
+ type Vmess struct {
11
+ Add string `json:"add,omitempty"` // 服务器地址
12
+ Aid interface{} `json:"aid,omitempty"`
13
+ Alpn string `json:"alpn,omitempty"`
14
+ Fp string `json:"fp,omitempty"`
15
+ Host string `json:"host,omitempty"`
16
+ Id string `json:"id,omitempty"`
17
+ Net string `json:"net,omitempty"`
18
+ Path string `json:"path,omitempty"`
19
+ Port interface{} `json:"port,omitempty"`
20
+ Ps string `json:"ps,omitempty"`
21
+ Scy string `json:"scy,omitempty"`
22
+ Sni string `json:"sni,omitempty"`
23
+ Tls string `json:"tls,omitempty"`
24
+ Type string `json:"type,omitempty"`
25
+ V string `json:"v,omitempty"`
26
+ }
27
+
28
+ // 开发者测试
29
+ func CallVmessURL() {
30
+ vmess := Vmess{
31
+ Add: "xx.xxx.ru",
32
+ Port: "2095",
33
+ Aid: 0,
34
+ Scy: "auto",
35
+ Net: "ws",
36
+ Type: "none",
37
+ Id: "7a737f41-b792-4260-94ff-3d864da67380",
38
+ Host: "xx.xxx.ru",
39
+ Path: "/",
40
+ Tls: "",
41
+ }
42
+ fmt.Println(EncodeVmessURL(vmess))
43
+ }
44
+
45
+ // vmess 编码
46
+ func EncodeVmessURL(v Vmess) string {
47
+ // 如果备注为空,则使用服务器地址+端口
48
+ if v.Ps == "" {
49
+ v.Ps = v.Add + ":" + v.Port.(string)
50
+ }
51
+ // 如果版本为空,则默认为2
52
+ if v.V == "" {
53
+ v.V = "2"
54
+ }
55
+ param, _ := json.Marshal(v)
56
+ return "vmess://" + Base64Encode(string(param))
57
+ }
58
+
59
+ // vmess 解码
60
+ func DecodeVMESSURL(s string) (Vmess, error) {
61
+ if !strings.Contains(s, "vmess://") {
62
+ return Vmess{}, fmt.Errorf("非vmess协议:%s", s)
63
+ }
64
+ param := strings.Split(s, "://")[1]
65
+ param = Base64Decode(strings.TrimSpace(param))
66
+ // fmt.Println(param)
67
+ var vmess Vmess
68
+ err := json.Unmarshal([]byte(param), &vmess)
69
+ if err != nil {
70
+ log.Println(err)
71
+ return Vmess{}, fmt.Errorf("json格式化失败:%s", param)
72
+ }
73
+ if vmess.Scy == "" {
74
+ vmess.Scy = "auto"
75
+ }
76
+ // 如果备注为空,则使用服务器地址+端口
77
+ if vmess.Ps == "" {
78
+ vmess.Ps = vmess.Add + ":" + vmess.Port.(string)
79
+ }
80
+ if CheckEnvironment() {
81
+ fmt.Println("服务器地址", vmess.Add)
82
+ fmt.Println("端口", vmess.Port)
83
+ fmt.Println("path", vmess.Path)
84
+ fmt.Println("uuid", vmess.Id)
85
+ fmt.Println("alterId", vmess.Aid)
86
+ fmt.Println("cipher", vmess.Scy)
87
+ fmt.Println("client-fingerprint", vmess.Fp)
88
+ fmt.Println("network", vmess.Net)
89
+ fmt.Println("tls", vmess.Tls)
90
+ fmt.Println("备注", vmess.Ps)
91
+ }
92
+ return vmess, nil
93
+ }
routers/clients.go ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package routers
2
+
3
+ import (
4
+ "sublink/api"
5
+ "sublink/middlewares"
6
+
7
+ "github.com/gin-gonic/gin"
8
+ )
9
+
10
+ func Clients(r *gin.Engine) {
11
+ ClientsGroup := r.Group("/c")
12
+ ClientsGroup.Use(middlewares.GetIp)
13
+ {
14
+ // ClientsGroup.GET("/v2ray/:subname", api.GetV2ray)
15
+ // ClientsGroup.GET("/clash/:subname", api.GetClash)
16
+ // ClientsGroup.GET("/surge/:subname", api.GetSurge)
17
+ ClientsGroup.GET("/", api.GetClient)
18
+ }
19
+
20
+ }
routers/index.go ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package routers
2
+
3
+ // // 加载index.html
4
+ // func Index(r *gin.Engine) {
5
+ // TotalGroup := r.Group("/api/v1/total")
6
+ // {
7
+ // TotalGroup.GET("/sub", api.SubTotal)
8
+ // TotalGroup.GET("/node", api.NodesTotal)
9
+ // }
10
+
11
+ // }
routers/menus.go ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package routers
2
+
3
+ import (
4
+ "sublink/api"
5
+
6
+ "github.com/gin-gonic/gin"
7
+ )
8
+
9
+ func Mentus(r *gin.Engine) {
10
+ MentusGroup := r.Group("/api/v1/menus")
11
+ {
12
+ // MentusGroup.GET("/menus", api.GetMenus)
13
+ MentusGroup.GET("/routes", api.GetMenus)
14
+ }
15
+ }
routers/node.go ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package routers
2
+
3
+ import (
4
+ "sublink/api"
5
+
6
+ "github.com/gin-gonic/gin"
7
+ )
8
+
9
+ func Nodes(r *gin.Engine) {
10
+ NodesGroup := r.Group("/api/v1/nodes")
11
+ {
12
+ NodesGroup.POST("/add", api.NodeAdd)
13
+ NodesGroup.DELETE("/delete", api.NodeDel)
14
+ NodesGroup.GET("/get", api.NodeGet)
15
+ NodesGroup.POST("/update", api.NodeUpdadte)
16
+ }
17
+
18
+ }
routers/subcription.go ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package routers
2
+
3
+ import (
4
+ "sublink/api"
5
+
6
+ "github.com/gin-gonic/gin"
7
+ )
8
+
9
+ func Subcription(r *gin.Engine) {
10
+ SubcriptionGroup := r.Group("/api/v1/subcription")
11
+ {
12
+ SubcriptionGroup.POST("/add", api.SubAdd)
13
+ SubcriptionGroup.DELETE("/delete", api.SubDel)
14
+ SubcriptionGroup.GET("/get", api.SubGet)
15
+ SubcriptionGroup.POST("/update", api.SubUpdate)
16
+ }
17
+
18
+ }
routers/template.go ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package routers
2
+
3
+ import (
4
+ "sublink/api"
5
+
6
+ "github.com/gin-gonic/gin"
7
+ )
8
+
9
+ func Templates(r *gin.Engine) {
10
+ TempsGroup := r.Group("/api/v1/template")
11
+ {
12
+ TempsGroup.POST("/add", api.AddTemp)
13
+ TempsGroup.POST("/delete", api.DelTemp)
14
+ TempsGroup.GET("/get", api.GetTempS)
15
+ TempsGroup.POST("/update", api.UpdateTemp)
16
+ }
17
+
18
+ }
routers/total.go ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package routers
2
+
3
+ import (
4
+ "sublink/api"
5
+
6
+ "github.com/gin-gonic/gin"
7
+ )
8
+
9
+ func Total(r *gin.Engine) {
10
+ TotalGroup := r.Group("/api/v1/total")
11
+ {
12
+ TotalGroup.GET("/sub", api.SubTotal)
13
+ TotalGroup.GET("/node", api.NodesTotal)
14
+ }
15
+
16
+ }
routers/user.go ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package routers
2
+
3
+ import (
4
+ "sublink/api"
5
+
6
+ "github.com/gin-gonic/gin"
7
+ )
8
+
9
+ func User(r *gin.Engine) {
10
+ authGroup := r.Group("/api/v1/auth")
11
+ {
12
+ authGroup.POST("/login", api.UserLogin)
13
+ authGroup.DELETE("/logout", api.UserOut)
14
+ authGroup.GET("/captcha", api.GetCaptcha)
15
+ }
16
+ userGroup := r.Group("/api/v1/users")
17
+ {
18
+ userGroup.GET("/me", api.UserMe)
19
+ userGroup.GET("/page", api.UserPages)
20
+ userGroup.PATCH(":username/:password", api.UserSet)
21
+ }
22
+ }