49 | ${this.replaceArray.map((_, i) => this.RenameTemplate(i))}
50 |
`;
51 | }
52 |
53 | RenameTemplate(index: number) {
54 | const replaceItem = this.replaceArray[index];
55 | return html` = [];
11 |
12 | @state()
13 | set ruleProviders(value) {
14 | this.dispatchEvent(
15 | new CustomEvent("change", {
16 | detail: value,
17 | })
18 | );
19 | this._ruleProviders = value;
20 | }
21 |
22 | get ruleProviders() {
23 | return this._ruleProviders;
24 | }
25 |
26 | RuleProviderTemplate(index: number) {
27 | return html`
28 |
29 |
30 | {
35 | const target = e.target as HTMLInputElement;
36 | let updatedRuleProviders = this.ruleProviders;
37 | updatedRuleProviders![index].name = target.value;
38 | this.ruleProviders = updatedRuleProviders;
39 | }}" />
40 |
41 |
42 |
55 |
56 |
57 | {
62 | const target = e.target as HTMLInputElement;
63 | let updatedRuleProviders = this.ruleProviders;
64 | updatedRuleProviders![index].url = target.value;
65 | this.ruleProviders = updatedRuleProviders;
66 | }}" />
67 |
68 |
{
73 | const target = e.target as HTMLInputElement;
74 | let updatedRuleProviders = this.ruleProviders;
75 | updatedRuleProviders![index].group = target.value;
76 | this.ruleProviders = updatedRuleProviders;
77 | }}" />
78 |
79 |
91 |
92 |
103 |
104 | `;
105 | }
106 |
107 | render() {
108 | return html`
109 |
110 |
131 |
132 |
133 |
134 | ${this.ruleProviders?.map((_, i) => this.RuleProviderTemplate(i))}
135 |
`;
136 | }
137 | }
138 |
139 | declare global {
140 | interface HTMLElementTagNameMap {
141 | "rule-provider-input": RuleProviderInput;
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/test/parser/hysteria_test.go:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/bestnite/sub2clash/model/proxy"
7 | "github.com/bestnite/sub2clash/parser"
8 | )
9 |
10 | func TestHysteria_Basic_SimpleLink(t *testing.T) {
11 | p := &parser.HysteriaParser{}
12 | input := "hysteria://127.0.0.1:8080?protocol=udp&auth=password123&upmbps=100&downmbps=100#Hysteria%20Proxy"
13 |
14 | expected := proxy.Proxy{
15 | Type: "hysteria",
16 | Name: "Hysteria Proxy",
17 | Hysteria: proxy.Hysteria{
18 | Server: "127.0.0.1",
19 | Port: 8080,
20 | Protocol: "udp",
21 | Auth: "password123",
22 | Up: "100",
23 | Down: "100",
24 | SkipCertVerify: false,
25 | },
26 | }
27 |
28 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
29 | if err != nil {
30 | t.Errorf("Unexpected error: %v", err)
31 | return
32 | }
33 |
34 | validateResult(t, expected, result)
35 | }
36 |
37 | func TestHysteria_Basic_WithAuthString(t *testing.T) {
38 | p := &parser.HysteriaParser{}
39 | input := "hysteria://proxy.example.com:443?protocol=wechat-video&auth-str=myauth&upmbps=50&downmbps=200&insecure=true#Hysteria%20Auth"
40 |
41 | expected := proxy.Proxy{
42 | Type: "hysteria",
43 | Name: "Hysteria Auth",
44 | Hysteria: proxy.Hysteria{
45 | Server: "proxy.example.com",
46 | Port: 443,
47 | Protocol: "wechat-video",
48 | AuthString: "myauth",
49 | Up: "50",
50 | Down: "200",
51 | SkipCertVerify: true,
52 | },
53 | }
54 |
55 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
56 | if err != nil {
57 | t.Errorf("Unexpected error: %v", err)
58 | return
59 | }
60 |
61 | validateResult(t, expected, result)
62 | }
63 |
64 | func TestHysteria_Basic_WithObfs(t *testing.T) {
65 | p := &parser.HysteriaParser{}
66 | input := "hysteria://127.0.0.1:8080?auth=password123&upmbps=100&downmbps=100&obfs=xplus&alpn=h3#Hysteria%20Obfs"
67 |
68 | expected := proxy.Proxy{
69 | Type: "hysteria",
70 | Name: "Hysteria Obfs",
71 | Hysteria: proxy.Hysteria{
72 | Server: "127.0.0.1",
73 | Port: 8080,
74 | Auth: "password123",
75 | Up: "100",
76 | Down: "100",
77 | Obfs: "xplus",
78 | ALPN: []string{"h3"},
79 | SkipCertVerify: false,
80 | },
81 | }
82 |
83 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
84 | if err != nil {
85 | t.Errorf("Unexpected error: %v", err)
86 | return
87 | }
88 |
89 | validateResult(t, expected, result)
90 | }
91 |
92 | func TestHysteria_Basic_IPv6Address(t *testing.T) {
93 | p := &parser.HysteriaParser{}
94 | input := "hysteria://[2001:db8::1]:8080?auth=password123&upmbps=100&downmbps=100#Hysteria%20IPv6"
95 |
96 | expected := proxy.Proxy{
97 | Type: "hysteria",
98 | Name: "Hysteria IPv6",
99 | Hysteria: proxy.Hysteria{
100 | Server: "2001:db8::1",
101 | Port: 8080,
102 | Auth: "password123",
103 | Up: "100",
104 | Down: "100",
105 | SkipCertVerify: false,
106 | },
107 | }
108 |
109 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
110 | if err != nil {
111 | t.Errorf("Unexpected error: %v", err)
112 | return
113 | }
114 |
115 | validateResult(t, expected, result)
116 | }
117 |
118 | func TestHysteria_Basic_MultiALPN(t *testing.T) {
119 | p := &parser.HysteriaParser{}
120 | input := "hysteria://proxy.example.com:443?auth=password123&upmbps=100&downmbps=100&alpn=h3,h2,http/1.1#Hysteria%20Multi%20ALPN"
121 |
122 | expected := proxy.Proxy{
123 | Type: "hysteria",
124 | Name: "Hysteria Multi ALPN",
125 | Hysteria: proxy.Hysteria{
126 | Server: "proxy.example.com",
127 | Port: 443,
128 | Auth: "password123",
129 | Up: "100",
130 | Down: "100",
131 | ALPN: []string{"h3", "h2", "http/1.1"},
132 | SkipCertVerify: false,
133 | },
134 | }
135 |
136 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
137 | if err != nil {
138 | t.Errorf("Unexpected error: %v", err)
139 | return
140 | }
141 |
142 | validateResult(t, expected, result)
143 | }
144 |
145 | func TestHysteria_Error_MissingServer(t *testing.T) {
146 | p := &parser.HysteriaParser{}
147 | input := "hysteria://:8080?auth=password123"
148 |
149 | _, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
150 | if err == nil {
151 | t.Errorf("Expected error but got none")
152 | }
153 | }
154 |
155 | func TestHysteria_Error_MissingPort(t *testing.T) {
156 | p := &parser.HysteriaParser{}
157 | input := "hysteria://127.0.0.1?auth=password123"
158 |
159 | _, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
160 | if err == nil {
161 | t.Errorf("Expected error but got none")
162 | }
163 | }
164 |
165 | func TestHysteria_Error_InvalidPort(t *testing.T) {
166 | p := &parser.HysteriaParser{}
167 | input := "hysteria://127.0.0.1:99999?auth=password123"
168 |
169 | _, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
170 | if err == nil {
171 | t.Errorf("Expected error but got none")
172 | }
173 | }
174 |
175 | func TestHysteria_Error_InvalidProtocol(t *testing.T) {
176 | p := &parser.HysteriaParser{}
177 | input := "hysteria2://example.com:8080"
178 |
179 | _, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
180 | if err == nil {
181 | t.Errorf("Expected error but got none")
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/server/frontend/src/components/short-link-input-group.ts:
--------------------------------------------------------------------------------
1 | import { html, LitElement, unsafeCSS } from "lit";
2 | import { customElement, property } from "lit/decorators.js";
3 | import globalStyles from "../index.css?inline";
4 |
5 | @customElement("short-link-input-group")
6 | export class ShortLinkInputGroup extends LitElement {
7 | static styles = unsafeCSS(globalStyles);
8 |
9 | @property()
10 | id: string = "";
11 |
12 | @property({ type: Number })
13 | _screenSizeLevel: number = 0;
14 |
15 | @property()
16 | passwd: string = "";
17 |
18 | connectedCallback() {
19 | super.connectedCallback();
20 | window.addEventListener("resize", this._checkScreenSize);
21 | this._checkScreenSize(); // Initial check
22 | }
23 |
24 | disconnectedCallback() {
25 | window.removeEventListener("resize", this._checkScreenSize);
26 | super.disconnectedCallback();
27 | }
28 |
29 | _checkScreenSize = () => {
30 | const width = window.innerWidth;
31 | if (width < 365) {
32 | this._screenSizeLevel = 0; // sm
33 | } else if (width < 640) {
34 | this._screenSizeLevel = 1; // md
35 | } else {
36 | this._screenSizeLevel = 2; // other
37 | }
38 | };
39 |
40 | async copyToClipboard(content: string, e: HTMLButtonElement) {
41 | try {
42 | await navigator.clipboard.writeText(content);
43 | let text = e.textContent;
44 | e.addEventListener("mouseout", function () {
45 | e.textContent = text;
46 | });
47 | e.textContent = "复制成功";
48 | } catch (err) {
49 | console.error("复制到剪贴板失败:", err);
50 | }
51 | }
52 |
53 | idInputTemplate() {
54 | return html` {
60 | this.id = (e.target as HTMLInputElement).value;
61 | this.dispatchEvent(
62 | new CustomEvent("id-change", {
63 | detail: this.id,
64 | })
65 | );
66 | }}" />`;
67 | }
68 |
69 | passwdInputTemplate() {
70 | return html` {
76 | this.passwd = (e.target as HTMLInputElement).value;
77 | this.dispatchEvent(
78 | new CustomEvent("passwd-change", {
79 | detail: this.passwd,
80 | })
81 | );
82 | }}" />`;
83 | }
84 |
85 | generateBtnTemplate(extraClass: string = "") {
86 | return html``;
96 | }
97 |
98 | updateBtnTemplate(extraClass: string = "") {
99 | return html``;
107 | }
108 |
109 | deleteBtnTemplate(extraClass: string = "") {
110 | return html``;
118 | }
119 |
120 | copyBtnTemplate(extraClass: string = "") {
121 | return html``;
132 | }
133 |
134 | render() {
135 | const sm = html``;
146 |
147 | const md = html``;
156 |
157 | const other = html``;
164 |
165 | switch (this._screenSizeLevel) {
166 | case 0:
167 | return sm;
168 | case 1:
169 | return md;
170 | default:
171 | return other;
172 | }
173 | }
174 | }
175 |
176 | declare global {
177 | interface HTMLElementTagNameMap {
178 | "short-link-input-group": ShortLinkInputGroup;
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/test/parser/hysteria2_test.go:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/bestnite/sub2clash/model/proxy"
7 | "github.com/bestnite/sub2clash/parser"
8 | )
9 |
10 | func TestHysteria2_Basic_SimpleLink(t *testing.T) {
11 | p := &parser.Hysteria2Parser{}
12 | input := "hysteria2://password123@127.0.0.1:8080#Hysteria2%20Proxy"
13 |
14 | expected := proxy.Proxy{
15 | Type: "hysteria2",
16 | Name: "Hysteria2 Proxy",
17 | Hysteria2: proxy.Hysteria2{
18 | Server: "127.0.0.1",
19 | Port: 8080,
20 | Password: "password123",
21 | SkipCertVerify: false,
22 | },
23 | }
24 |
25 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
26 | if err != nil {
27 | t.Errorf("Unexpected error: %v", err)
28 | return
29 | }
30 |
31 | validateResult(t, expected, result)
32 | }
33 |
34 | func TestHysteria2_Basic_AltPrefix(t *testing.T) {
35 | p := &parser.Hysteria2Parser{}
36 | input := "hy2://password123@proxy.example.com:443?insecure=1&sni=proxy.example.com#Hysteria2%20Alt"
37 |
38 | expected := proxy.Proxy{
39 | Type: "hysteria2",
40 | Name: "Hysteria2 Alt",
41 | Hysteria2: proxy.Hysteria2{
42 | Server: "proxy.example.com",
43 | Port: 443,
44 | Password: "password123",
45 | SNI: "proxy.example.com",
46 | SkipCertVerify: true,
47 | },
48 | }
49 |
50 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
51 | if err != nil {
52 | t.Errorf("Unexpected error: %v", err)
53 | return
54 | }
55 |
56 | validateResult(t, expected, result)
57 | }
58 |
59 | func TestHysteria2_Basic_WithObfs(t *testing.T) {
60 | p := &parser.Hysteria2Parser{}
61 | input := "hysteria2://password123@127.0.0.1:8080?obfs=salamander&obfs-password=obfs123#Hysteria2%20Obfs"
62 |
63 | expected := proxy.Proxy{
64 | Type: "hysteria2",
65 | Name: "Hysteria2 Obfs",
66 | Hysteria2: proxy.Hysteria2{
67 | Server: "127.0.0.1",
68 | Port: 8080,
69 | Password: "password123",
70 | Obfs: "salamander",
71 | ObfsPassword: "obfs123",
72 | SkipCertVerify: false,
73 | },
74 | }
75 |
76 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
77 | if err != nil {
78 | t.Errorf("Unexpected error: %v", err)
79 | return
80 | }
81 |
82 | validateResult(t, expected, result)
83 | }
84 |
85 | func TestHysteria2_Basic_IPv6Address(t *testing.T) {
86 | p := &parser.Hysteria2Parser{}
87 | input := "hysteria2://password123@[2001:db8::1]:8080#Hysteria2%20IPv6"
88 |
89 | expected := proxy.Proxy{
90 | Type: "hysteria2",
91 | Name: "Hysteria2 IPv6",
92 | Hysteria2: proxy.Hysteria2{
93 | Server: "2001:db8::1",
94 | Port: 8080,
95 | Password: "password123",
96 | SkipCertVerify: false,
97 | },
98 | }
99 |
100 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
101 | if err != nil {
102 | t.Errorf("Unexpected error: %v", err)
103 | return
104 | }
105 |
106 | validateResult(t, expected, result)
107 | }
108 |
109 | func TestHysteria2_Basic_FullConfig(t *testing.T) {
110 | p := &parser.Hysteria2Parser{}
111 | input := "hysteria2://password123@proxy.example.com:443?insecure=1&sni=proxy.example.com&obfs=salamander&obfs-password=obfs123#Hysteria2%20Full"
112 |
113 | expected := proxy.Proxy{
114 | Type: "hysteria2",
115 | Name: "Hysteria2 Full",
116 | Hysteria2: proxy.Hysteria2{
117 | Server: "proxy.example.com",
118 | Port: 443,
119 | Password: "password123",
120 | SNI: "proxy.example.com",
121 | Obfs: "salamander",
122 | ObfsPassword: "obfs123",
123 | SkipCertVerify: true,
124 | },
125 | }
126 |
127 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
128 | if err != nil {
129 | t.Errorf("Unexpected error: %v", err)
130 | return
131 | }
132 |
133 | validateResult(t, expected, result)
134 | }
135 |
136 | func TestHysteria2_Basic_NoPassword(t *testing.T) {
137 | p := &parser.Hysteria2Parser{}
138 | input := "hysteria2://@127.0.0.1:8080#No%20Password"
139 |
140 | expected := proxy.Proxy{
141 | Type: "hysteria2",
142 | Name: "No Password",
143 | Hysteria2: proxy.Hysteria2{
144 | Server: "127.0.0.1",
145 | Port: 8080,
146 | Password: "",
147 | SkipCertVerify: false,
148 | },
149 | }
150 |
151 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
152 | if err != nil {
153 | t.Errorf("Unexpected error: %v", err)
154 | return
155 | }
156 |
157 | validateResult(t, expected, result)
158 | }
159 |
160 | func TestHysteria2_Error_MissingServer(t *testing.T) {
161 | p := &parser.Hysteria2Parser{}
162 | input := "hysteria2://password123@:8080"
163 |
164 | _, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
165 | if err == nil {
166 | t.Errorf("Expected error but got none")
167 | }
168 | }
169 |
170 | func TestHysteria2_Error_MissingPort(t *testing.T) {
171 | p := &parser.Hysteria2Parser{}
172 | input := "hysteria2://password123@127.0.0.1"
173 |
174 | _, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
175 | if err == nil {
176 | t.Errorf("Expected error but got none")
177 | }
178 | }
179 |
180 | func TestHysteria2_Error_InvalidPort(t *testing.T) {
181 | p := &parser.Hysteria2Parser{}
182 | input := "hysteria2://password123@127.0.0.1:99999"
183 |
184 | _, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
185 | if err == nil {
186 | t.Errorf("Expected error but got none")
187 | }
188 | }
189 |
190 | func TestHysteria2_Error_InvalidProtocol(t *testing.T) {
191 | p := &parser.Hysteria2Parser{}
192 | input := "hysteria://example.com:8080"
193 |
194 | _, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
195 | if err == nil {
196 | t.Errorf("Expected error but got none")
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/test/parser/anytls_test.go:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/bestnite/sub2clash/model/proxy"
7 | "github.com/bestnite/sub2clash/parser"
8 | )
9 |
10 | func TestAnytls_Basic_SimpleLink(t *testing.T) {
11 | p := &parser.AnytlsParser{}
12 | input := "anytls://password123@127.0.0.1:8080#Anytls%20Proxy"
13 |
14 | expected := proxy.Proxy{
15 | Type: "anytls",
16 | Name: "Anytls Proxy",
17 | Anytls: proxy.Anytls{
18 | Server: "127.0.0.1",
19 | Port: 8080,
20 | Password: "password123",
21 | SkipCertVerify: false,
22 | },
23 | }
24 |
25 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
26 | if err != nil {
27 | t.Errorf("Unexpected error: %v", err)
28 | return
29 | }
30 |
31 | validateResult(t, expected, result)
32 | }
33 |
34 | func TestAnytls_Basic_WithSNI(t *testing.T) {
35 | p := &parser.AnytlsParser{}
36 | input := "anytls://password123@proxy.example.com:443?sni=proxy.example.com#Anytls%20SNI"
37 |
38 | expected := proxy.Proxy{
39 | Type: "anytls",
40 | Name: "Anytls SNI",
41 | Anytls: proxy.Anytls{
42 | Server: "proxy.example.com",
43 | Port: 443,
44 | Password: "password123",
45 | SNI: "proxy.example.com",
46 | SkipCertVerify: false,
47 | },
48 | }
49 |
50 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
51 | if err != nil {
52 | t.Errorf("Unexpected error: %v", err)
53 | return
54 | }
55 |
56 | validateResult(t, expected, result)
57 | }
58 |
59 | func TestAnytls_Basic_WithInsecure(t *testing.T) {
60 | p := &parser.AnytlsParser{}
61 | input := "anytls://password123@proxy.example.com:443?insecure=1&sni=proxy.example.com#Anytls%20Insecure"
62 |
63 | expected := proxy.Proxy{
64 | Type: "anytls",
65 | Name: "Anytls Insecure",
66 | Anytls: proxy.Anytls{
67 | Server: "proxy.example.com",
68 | Port: 443,
69 | Password: "password123",
70 | SNI: "proxy.example.com",
71 | SkipCertVerify: true,
72 | },
73 | }
74 |
75 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
76 | if err != nil {
77 | t.Errorf("Unexpected error: %v", err)
78 | return
79 | }
80 |
81 | validateResult(t, expected, result)
82 | }
83 |
84 | func TestAnytls_Basic_IPv6Address(t *testing.T) {
85 | p := &parser.AnytlsParser{}
86 | input := "anytls://password123@[2001:db8::1]:8080#Anytls%20IPv6"
87 |
88 | expected := proxy.Proxy{
89 | Type: "anytls",
90 | Name: "Anytls IPv6",
91 | Anytls: proxy.Anytls{
92 | Server: "2001:db8::1",
93 | Port: 8080,
94 | Password: "password123",
95 | SkipCertVerify: false,
96 | },
97 | }
98 |
99 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
100 | if err != nil {
101 | t.Errorf("Unexpected error: %v", err)
102 | return
103 | }
104 |
105 | validateResult(t, expected, result)
106 | }
107 |
108 | func TestAnytls_Basic_ComplexPassword(t *testing.T) {
109 | p := &parser.AnytlsParser{}
110 | input := "anytls://ComplexPassword!%40%23%24@proxy.example.com:8443?sni=example.com&insecure=1#Anytls%20Full"
111 |
112 | expected := proxy.Proxy{
113 | Type: "anytls",
114 | Name: "Anytls Full",
115 | Anytls: proxy.Anytls{
116 | Server: "proxy.example.com",
117 | Port: 8443,
118 | Password: "ComplexPassword!@#$",
119 | SNI: "example.com",
120 | SkipCertVerify: true,
121 | },
122 | }
123 |
124 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
125 | if err != nil {
126 | t.Errorf("Unexpected error: %v", err)
127 | return
128 | }
129 |
130 | validateResult(t, expected, result)
131 | }
132 |
133 | func TestAnytls_Basic_NoPassword(t *testing.T) {
134 | p := &parser.AnytlsParser{}
135 | input := "anytls://@127.0.0.1:8080#No%20Password"
136 |
137 | expected := proxy.Proxy{
138 | Type: "anytls",
139 | Name: "No Password",
140 | Anytls: proxy.Anytls{
141 | Server: "127.0.0.1",
142 | Port: 8080,
143 | Password: "",
144 | SkipCertVerify: false,
145 | },
146 | }
147 |
148 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
149 | if err != nil {
150 | t.Errorf("Unexpected error: %v", err)
151 | return
152 | }
153 |
154 | validateResult(t, expected, result)
155 | }
156 |
157 | func TestAnytls_Basic_UsernameOnly(t *testing.T) {
158 | p := &parser.AnytlsParser{}
159 | input := "anytls://username@127.0.0.1:8080#Username%20Only"
160 |
161 | expected := proxy.Proxy{
162 | Type: "anytls",
163 | Name: "Username Only",
164 | Anytls: proxy.Anytls{
165 | Server: "127.0.0.1",
166 | Port: 8080,
167 | Password: "username",
168 | SkipCertVerify: false,
169 | },
170 | }
171 |
172 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
173 | if err != nil {
174 | t.Errorf("Unexpected error: %v", err)
175 | return
176 | }
177 |
178 | validateResult(t, expected, result)
179 | }
180 |
181 | func TestAnytls_Error_MissingServer(t *testing.T) {
182 | p := &parser.AnytlsParser{}
183 | input := "anytls://password123@:8080"
184 |
185 | _, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
186 | if err == nil {
187 | t.Errorf("Expected error but got none")
188 | }
189 | }
190 |
191 | func TestAnytls_Error_MissingPort(t *testing.T) {
192 | p := &parser.AnytlsParser{}
193 | input := "anytls://password123@127.0.0.1"
194 |
195 | _, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
196 | if err == nil {
197 | t.Errorf("Expected error but got none")
198 | }
199 | }
200 |
201 | func TestAnytls_Error_InvalidPort(t *testing.T) {
202 | p := &parser.AnytlsParser{}
203 | input := "anytls://password123@127.0.0.1:99999"
204 |
205 | _, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
206 | if err == nil {
207 | t.Errorf("Expected error but got none")
208 | }
209 | }
210 |
211 | func TestAnytls_Error_InvalidProtocol(t *testing.T) {
212 | p := &parser.AnytlsParser{}
213 | input := "anyssl://example.com:8080"
214 |
215 | _, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
216 | if err == nil {
217 | t.Errorf("Expected error but got none")
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/test/parser/vless_test.go:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/bestnite/sub2clash/model/proxy"
7 | "github.com/bestnite/sub2clash/parser"
8 | )
9 |
10 | func TestVless_Basic_SimpleLink(t *testing.T) {
11 | p := &parser.VlessParser{}
12 | input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1:8080#VLESS%20Proxy"
13 |
14 | expected := proxy.Proxy{
15 | Type: "vless",
16 | Name: "VLESS Proxy",
17 | Vless: proxy.Vless{
18 | Server: "127.0.0.1",
19 | Port: 8080,
20 | UUID: "b831b0c4-33b7-4873-9834-28d66d87d4ce",
21 | },
22 | }
23 |
24 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
25 | if err != nil {
26 | t.Errorf("Unexpected error: %v", err)
27 | return
28 | }
29 |
30 | validateResult(t, expected, result)
31 | }
32 |
33 | func TestVless_Basic_WithTLS(t *testing.T) {
34 | p := &parser.VlessParser{}
35 | input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1:443?security=tls&sni=example.com&alpn=h2,http/1.1#VLESS%20TLS"
36 |
37 | expected := proxy.Proxy{
38 | Type: "vless",
39 | Name: "VLESS TLS",
40 | Vless: proxy.Vless{
41 | Server: "127.0.0.1",
42 | Port: 443,
43 | UUID: "b831b0c4-33b7-4873-9834-28d66d87d4ce",
44 | TLS: true,
45 | ALPN: []string{"h2", "http/1.1"},
46 | ServerName: "example.com",
47 | },
48 | }
49 |
50 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
51 | if err != nil {
52 | t.Errorf("Unexpected error: %v", err)
53 | return
54 | }
55 |
56 | validateResult(t, expected, result)
57 | }
58 |
59 | func TestVless_Basic_WithReality(t *testing.T) {
60 | p := &parser.VlessParser{}
61 | input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1:443?security=reality&sni=example.com&pbk=publickey123&sid=shortid123&fp=chrome#VLESS%20Reality"
62 |
63 | expected := proxy.Proxy{
64 | Type: "vless",
65 | Name: "VLESS Reality",
66 | Vless: proxy.Vless{
67 | Server: "127.0.0.1",
68 | Port: 443,
69 | UUID: "b831b0c4-33b7-4873-9834-28d66d87d4ce",
70 | TLS: true,
71 | ServerName: "example.com",
72 | RealityOpts: proxy.RealityOptions{
73 | PublicKey: "publickey123",
74 | ShortID: "shortid123",
75 | },
76 | Fingerprint: "chrome",
77 | },
78 | }
79 |
80 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
81 | if err != nil {
82 | t.Errorf("Unexpected error: %v", err)
83 | return
84 | }
85 |
86 | validateResult(t, expected, result)
87 | }
88 |
89 | func TestVless_Basic_WithWebSocket(t *testing.T) {
90 | p := &parser.VlessParser{}
91 | input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1:443?type=ws&path=/ws&host=example.com#VLESS%20WS"
92 |
93 | expected := proxy.Proxy{
94 | Type: "vless",
95 | Name: "VLESS WS",
96 | Vless: proxy.Vless{
97 | Server: "127.0.0.1",
98 | Port: 443,
99 | UUID: "b831b0c4-33b7-4873-9834-28d66d87d4ce",
100 | Network: "ws",
101 | WSOpts: proxy.WSOptions{
102 | Path: "/ws",
103 | Headers: map[string]string{
104 | "Host": "example.com",
105 | },
106 | },
107 | },
108 | }
109 |
110 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
111 | if err != nil {
112 | t.Errorf("Unexpected error: %v", err)
113 | return
114 | }
115 |
116 | validateResult(t, expected, result)
117 | }
118 |
119 | func TestVless_Basic_WithGrpc(t *testing.T) {
120 | p := &parser.VlessParser{}
121 | input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1:443?type=grpc&serviceName=grpc_service#VLESS%20gRPC"
122 |
123 | expected := proxy.Proxy{
124 | Type: "vless",
125 | Name: "VLESS gRPC",
126 | Vless: proxy.Vless{
127 | Server: "127.0.0.1",
128 | Port: 443,
129 | UUID: "b831b0c4-33b7-4873-9834-28d66d87d4ce",
130 | Network: "grpc",
131 | GrpcOpts: proxy.GrpcOptions{
132 | GrpcServiceName: "grpc_service",
133 | },
134 | },
135 | }
136 |
137 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
138 | if err != nil {
139 | t.Errorf("Unexpected error: %v", err)
140 | return
141 | }
142 |
143 | validateResult(t, expected, result)
144 | }
145 |
146 | func TestVless_Basic_WithHTTP(t *testing.T) {
147 | p := &parser.VlessParser{}
148 | input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1:443?type=http&path=/path1,/path2&host=host1.com,host2.com#VLESS%20HTTP"
149 |
150 | expected := proxy.Proxy{
151 | Type: "vless",
152 | Name: "VLESS HTTP",
153 | Vless: proxy.Vless{
154 | Server: "127.0.0.1",
155 | Port: 443,
156 | UUID: "b831b0c4-33b7-4873-9834-28d66d87d4ce",
157 | Network: "http",
158 | HTTPOpts: proxy.HTTPOptions{
159 | Path: []string{"/path1", "/path2"},
160 | Headers: map[string][]string{
161 | "host": {"host1.com", "host2.com"},
162 | },
163 | },
164 | },
165 | }
166 |
167 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
168 | if err != nil {
169 | t.Errorf("Unexpected error: %v", err)
170 | return
171 | }
172 |
173 | validateResult(t, expected, result)
174 | }
175 |
176 | func TestVless_Error_MissingServer(t *testing.T) {
177 | p := &parser.VlessParser{}
178 | input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@:8080"
179 |
180 | _, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
181 | if err == nil {
182 | t.Errorf("Expected error but got none")
183 | }
184 | }
185 |
186 | func TestVless_Error_MissingPort(t *testing.T) {
187 | p := &parser.VlessParser{}
188 | input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1"
189 |
190 | _, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
191 | if err == nil {
192 | t.Errorf("Expected error but got none")
193 | }
194 | }
195 |
196 | func TestVless_Error_InvalidPort(t *testing.T) {
197 | p := &parser.VlessParser{}
198 | input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1:99999"
199 |
200 | _, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
201 | if err == nil {
202 | t.Errorf("Expected error but got none")
203 | }
204 | }
205 |
206 | func TestVless_Error_InvalidProtocol(t *testing.T) {
207 | p := &parser.VlessParser{}
208 | input := "ss://example.com:8080"
209 |
210 | _, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
211 | if err == nil {
212 | t.Errorf("Expected error but got none")
213 | }
214 | }
215 |
--------------------------------------------------------------------------------
/common/errors.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | )
7 |
8 | // CommonError represents a structured error type for the common package
9 | type CommonError struct {
10 | Code ErrorCode
11 | Message string
12 | Cause error
13 | }
14 |
15 | // ErrorCode represents different types of errors
16 | type ErrorCode string
17 |
18 | const (
19 | // Directory operation errors
20 | ErrDirCreation ErrorCode = "DIRECTORY_CREATION_FAILED"
21 | ErrDirAccess ErrorCode = "DIRECTORY_ACCESS_FAILED"
22 |
23 | // File operation errors
24 | ErrFileNotFound ErrorCode = "FILE_NOT_FOUND"
25 | ErrFileRead ErrorCode = "FILE_READ_FAILED"
26 | ErrFileWrite ErrorCode = "FILE_WRITE_FAILED"
27 | ErrFileCreate ErrorCode = "FILE_CREATE_FAILED"
28 |
29 | // Network operation errors
30 | ErrNetworkRequest ErrorCode = "NETWORK_REQUEST_FAILED"
31 | ErrNetworkResponse ErrorCode = "NETWORK_RESPONSE_FAILED"
32 |
33 | // Template and configuration errors
34 | ErrTemplateLoad ErrorCode = "TEMPLATE_LOAD_FAILED"
35 | ErrTemplateParse ErrorCode = "TEMPLATE_PARSE_FAILED"
36 | ErrConfigInvalid ErrorCode = "CONFIG_INVALID"
37 |
38 | // Subscription errors
39 | ErrSubscriptionLoad ErrorCode = "SUBSCRIPTION_LOAD_FAILED"
40 | ErrSubscriptionParse ErrorCode = "SUBSCRIPTION_PARSE_FAILED"
41 |
42 | // Regex errors
43 | ErrRegexCompile ErrorCode = "REGEX_COMPILE_FAILED"
44 | ErrRegexInvalid ErrorCode = "REGEX_INVALID"
45 |
46 | // Database errors
47 | ErrDatabaseConnect ErrorCode = "DATABASE_CONNECTION_FAILED"
48 | ErrDatabaseQuery ErrorCode = "DATABASE_QUERY_FAILED"
49 | ErrRecordNotFound ErrorCode = "RECORD_NOT_FOUND"
50 |
51 | // Validation errors
52 | ErrValidation ErrorCode = "VALIDATION_FAILED"
53 | ErrInvalidInput ErrorCode = "INVALID_INPUT"
54 | )
55 |
56 | // Error returns the string representation of the error
57 | func (e *CommonError) Error() string {
58 | if e.Cause != nil {
59 | return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Cause)
60 | }
61 | return fmt.Sprintf("[%s] %s", e.Code, e.Message)
62 | }
63 |
64 | // Unwrap returns the underlying error
65 | func (e *CommonError) Unwrap() error {
66 | return e.Cause
67 | }
68 |
69 | // Is allows error comparison
70 | func (e *CommonError) Is(target error) bool {
71 | if t, ok := target.(*CommonError); ok {
72 | return e.Code == t.Code
73 | }
74 | return false
75 | }
76 |
77 | // NewError creates a new CommonError
78 | func NewError(code ErrorCode, message string, cause error) *CommonError {
79 | return &CommonError{
80 | Code: code,
81 | Message: message,
82 | Cause: cause,
83 | }
84 | }
85 |
86 | // NewSimpleError creates a new CommonError without a cause
87 | func NewSimpleError(code ErrorCode, message string) *CommonError {
88 | return &CommonError{
89 | Code: code,
90 | Message: message,
91 | }
92 | }
93 |
94 | // Convenience constructors for common error types
95 |
96 | // Directory errors
97 | func NewDirCreationError(dirPath string, cause error) *CommonError {
98 | return NewError(ErrDirCreation, fmt.Sprintf("failed to create directory: %s", dirPath), cause)
99 | }
100 |
101 | func NewDirAccessError(dirPath string, cause error) *CommonError {
102 | return NewError(ErrDirAccess, fmt.Sprintf("failed to access directory: %s", dirPath), cause)
103 | }
104 |
105 | // File errors
106 | func NewFileNotFoundError(filePath string) *CommonError {
107 | return NewSimpleError(ErrFileNotFound, fmt.Sprintf("file not found: %s", filePath))
108 | }
109 |
110 | func NewFileReadError(filePath string, cause error) *CommonError {
111 | return NewError(ErrFileRead, fmt.Sprintf("failed to read file: %s", filePath), cause)
112 | }
113 |
114 | func NewFileWriteError(filePath string, cause error) *CommonError {
115 | return NewError(ErrFileWrite, fmt.Sprintf("failed to write file: %s", filePath), cause)
116 | }
117 |
118 | func NewFileCreateError(filePath string, cause error) *CommonError {
119 | return NewError(ErrFileCreate, fmt.Sprintf("failed to create file: %s", filePath), cause)
120 | }
121 |
122 | // Network errors
123 | func NewNetworkRequestError(url string, cause error) *CommonError {
124 | return NewError(ErrNetworkRequest, fmt.Sprintf("network request failed for URL: %s", url), cause)
125 | }
126 |
127 | func NewNetworkResponseError(message string, cause error) *CommonError {
128 | return NewError(ErrNetworkResponse, message, cause)
129 | }
130 |
131 | // Template errors
132 | func NewTemplateLoadError(template string, cause error) *CommonError {
133 | return NewError(ErrTemplateLoad, fmt.Sprintf("failed to load template: %s", template), cause)
134 | }
135 |
136 | func NewTemplateParseError(data []byte, cause error) *CommonError {
137 | return NewError(ErrTemplateParse, fmt.Sprintf("failed to parse template: %s", data), cause)
138 | }
139 |
140 | // Subscription errors
141 | func NewSubscriptionLoadError(url string, cause error) *CommonError {
142 | return NewError(ErrSubscriptionLoad, fmt.Sprintf("failed to load subscription: %s", url), cause)
143 | }
144 |
145 | func NewSubscriptionParseError(data []byte, cause error) *CommonError {
146 | return NewError(ErrSubscriptionParse, fmt.Sprintf("failed to parse subscription: %s", string(data)), cause)
147 | }
148 |
149 | // Regex errors
150 | func NewRegexCompileError(pattern string, cause error) *CommonError {
151 | return NewError(ErrRegexCompile, fmt.Sprintf("failed to compile regex pattern: %s", pattern), cause)
152 | }
153 |
154 | func NewRegexInvalidError(paramName string, cause error) *CommonError {
155 | return NewError(ErrRegexInvalid, fmt.Sprintf("invalid regex in parameter: %s", paramName), cause)
156 | }
157 |
158 | // Database errors
159 | func NewDatabaseConnectError(cause error) *CommonError {
160 | return NewError(ErrDatabaseConnect, "failed to connect to database", cause)
161 | }
162 |
163 | func NewRecordNotFoundError(recordType string, id string) *CommonError {
164 | return NewSimpleError(ErrRecordNotFound, fmt.Sprintf("%s not found: %s", recordType, id))
165 | }
166 |
167 | // Validation errors
168 | func NewValidationError(field string, message string) *CommonError {
169 | return NewSimpleError(ErrValidation, fmt.Sprintf("validation failed for %s: %s", field, message))
170 | }
171 |
172 | func NewInvalidInputError(paramName string, value string) *CommonError {
173 | return NewSimpleError(ErrInvalidInput, fmt.Sprintf("invalid input for parameter %s: %s", paramName, value))
174 | }
175 |
176 | // IsErrorCode checks if an error has a specific error code
177 | func IsErrorCode(err error, code ErrorCode) bool {
178 | var commonErr *CommonError
179 | if errors.As(err, &commonErr) {
180 | return commonErr.Code == code
181 | }
182 | return false
183 | }
184 |
185 | // GetErrorCode extracts the error code from an error
186 | func GetErrorCode(err error) (ErrorCode, bool) {
187 | var commonErr *CommonError
188 | if errors.As(err, &commonErr) {
189 | return commonErr.Code, true
190 | }
191 | return "", false
192 | }
193 |
--------------------------------------------------------------------------------
/test/parser/vmess_test.go:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/bestnite/sub2clash/model/proxy"
7 | "github.com/bestnite/sub2clash/parser"
8 | )
9 |
10 | func TestVmess_Basic_SimpleLink(t *testing.T) {
11 | p := &parser.VmessParser{}
12 | input := "vmess://eyJhZGQiOiIxMjcuMC4wLjEiLCJhaWQiOiIwIiwiaWQiOiIxMjM0NTY3OC05MDEyLTM0NTYtNzg5MC0xMjM0NTY3ODkwMTIiLCJuZXQiOiJ3cyIsInBvcnQiOiI0NDMiLCJwcyI6IkhBSEEiLCJ0bHMiOiJ0bHMiLCJ0eXBlIjoibm9uZSIsInYiOiIyIn0="
13 |
14 | expected := proxy.Proxy{
15 | Type: "vmess",
16 | Name: "HAHA",
17 | Vmess: proxy.Vmess{
18 | UUID: "12345678-9012-3456-7890-123456789012",
19 | AlterID: 0,
20 | Cipher: "auto",
21 | Server: "127.0.0.1",
22 | Port: 443,
23 | TLS: true,
24 | Network: "ws",
25 | WSOpts: proxy.WSOptions{
26 | Path: "/",
27 | Headers: map[string]string{
28 | "Host": "127.0.0.1",
29 | },
30 | },
31 | },
32 | }
33 |
34 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
35 | if err != nil {
36 | t.Errorf("Unexpected error: %v", err)
37 | return
38 | }
39 |
40 | validateResult(t, expected, result)
41 | }
42 |
43 | func TestVmess_Basic_WithPath(t *testing.T) {
44 | p := &parser.VmessParser{}
45 | input := "vmess://eyJhZGQiOiIxMjcuMC4wLjEiLCJhaWQiOiIwIiwiaWQiOiIxMjM0NTY3OC05MDEyLTM0NTYtNzg5MC0xMjM0NTY3ODkwMTIiLCJuZXQiOiJ3cyIsInBhdGgiOiIvd3MiLCJwb3J0IjoiNDQzIiwicHMiOiJIQUNLIiwidGxzIjoidGxzIiwidHlwZSI6Im5vbmUiLCJ2IjoiMiJ9"
46 |
47 | expected := proxy.Proxy{
48 | Type: "vmess",
49 | Name: "HACK",
50 | Vmess: proxy.Vmess{
51 | UUID: "12345678-9012-3456-7890-123456789012",
52 | AlterID: 0,
53 | Cipher: "auto",
54 | Server: "127.0.0.1",
55 | Port: 443,
56 | TLS: true,
57 | Network: "ws",
58 | WSOpts: proxy.WSOptions{
59 | Path: "/ws",
60 | Headers: map[string]string{
61 | "Host": "127.0.0.1",
62 | },
63 | },
64 | },
65 | }
66 |
67 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
68 | if err != nil {
69 | t.Errorf("Unexpected error: %v", err)
70 | return
71 | }
72 |
73 | validateResult(t, expected, result)
74 | }
75 |
76 | func TestVmess_Basic_WithHost(t *testing.T) {
77 | p := &parser.VmessParser{}
78 | input := "vmess://eyJhZGQiOiIxMjcuMC4wLjEiLCJhaWQiOiIwIiwiaG9zdCI6ImV4YW1wbGUuY29tIiwiaWQiOiIxMjM0NTY3OC05MDEyLTM0NTYtNzg5MC0xMjM0NTY3ODkwMTIiLCJuZXQiOiJ3cyIsInBvcnQiOiI0NDMiLCJwcyI6IkhBSEEiLCJ0bHMiOiJ0bHMiLCJ0eXBlIjoibm9uZSIsInYiOiIyIn0="
79 |
80 | expected := proxy.Proxy{
81 | Type: "vmess",
82 | Name: "HAHA",
83 | Vmess: proxy.Vmess{
84 | UUID: "12345678-9012-3456-7890-123456789012",
85 | AlterID: 0,
86 | Cipher: "auto",
87 | Server: "127.0.0.1",
88 | Port: 443,
89 | TLS: true,
90 | Network: "ws",
91 | WSOpts: proxy.WSOptions{
92 | Path: "/",
93 | Headers: map[string]string{
94 | "Host": "example.com",
95 | },
96 | },
97 | },
98 | }
99 |
100 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
101 | if err != nil {
102 | t.Errorf("Unexpected error: %v", err)
103 | return
104 | }
105 |
106 | validateResult(t, expected, result)
107 | }
108 |
109 | func TestVmess_Basic_WithSNI(t *testing.T) {
110 | p := &parser.VmessParser{}
111 | input := "vmess://eyJhZGQiOiIxMjcuMC4wLjEiLCJhaWQiOiIwIiwiaWQiOiIxMjM0NTY3OC05MDEyLTM0NTYtNzg5MC0xMjM0NTY3ODkwMTIiLCJuZXQiOiJ3cyIsInBvcnQiOiI0NDMiLCJwcyI6IkhBSEEiLCJzbmkiOiJleGFtcGxlLmNvbSIsInRscyI6InRscyIsInR5cGUiOiJub25lIiwidiI6IjIifQ=="
112 |
113 | expected := proxy.Proxy{
114 | Type: "vmess",
115 | Name: "HAHA",
116 | Vmess: proxy.Vmess{
117 | UUID: "12345678-9012-3456-7890-123456789012",
118 | AlterID: 0,
119 | Cipher: "auto",
120 | Server: "127.0.0.1",
121 | Port: 443,
122 | TLS: true,
123 | Network: "ws",
124 | ServerName: "example.com",
125 | WSOpts: proxy.WSOptions{
126 | Path: "/",
127 | Headers: map[string]string{
128 | "Host": "127.0.0.1",
129 | },
130 | },
131 | },
132 | }
133 |
134 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
135 | if err != nil {
136 | t.Errorf("Unexpected error: %v", err)
137 | return
138 | }
139 |
140 | validateResult(t, expected, result)
141 | }
142 |
143 | func TestVmess_Basic_WithAlterID(t *testing.T) {
144 | p := &parser.VmessParser{}
145 | input := "vmess://eyJhZGQiOiIxMjcuMC4wLjEiLCJhaWQiOiIxIiwiaWQiOiIxMjM0NTY3OC05MDEyLTM0NTYtNzg5MC0xMjM0NTY3ODkwMTIiLCJuZXQiOiJ3cyIsInBvcnQiOiI0NDMiLCJwcyI6IkhBSEEiLCJ0bHMiOiJ0bHMiLCJ0eXBlIjoibm9uZSIsInYiOiIyIn0="
146 |
147 | expected := proxy.Proxy{
148 | Type: "vmess",
149 | Name: "HAHA",
150 | Vmess: proxy.Vmess{
151 | UUID: "12345678-9012-3456-7890-123456789012",
152 | AlterID: 1,
153 | Cipher: "auto",
154 | Server: "127.0.0.1",
155 | Port: 443,
156 | TLS: true,
157 | Network: "ws",
158 | WSOpts: proxy.WSOptions{
159 | Path: "/",
160 | Headers: map[string]string{
161 | "Host": "127.0.0.1",
162 | },
163 | },
164 | },
165 | }
166 |
167 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
168 | if err != nil {
169 | t.Errorf("Unexpected error: %v", err)
170 | return
171 | }
172 |
173 | validateResult(t, expected, result)
174 | }
175 |
176 | func TestVmess_Basic_GRPC(t *testing.T) {
177 | p := &parser.VmessParser{}
178 | input := "vmess://eyJhZGQiOiIxMjcuMC4wLjEiLCJhaWQiOiIwIiwiaWQiOiIxMjM0NTY3OC05MDEyLTM0NTYtNzg5MC0xMjM0NTY3ODkwMTIiLCJuZXQiOiJncnBjIiwicG9ydCI6IjQ0MyIsInBzIjoiSEFIQSIsInRscyI6InRscyIsInR5cGUiOiJub25lIiwidiI6IjIifQ=="
179 |
180 | expected := proxy.Proxy{
181 | Type: "vmess",
182 | Name: "HAHA",
183 | Vmess: proxy.Vmess{
184 | UUID: "12345678-9012-3456-7890-123456789012",
185 | AlterID: 0,
186 | Cipher: "auto",
187 | Server: "127.0.0.1",
188 | Port: 443,
189 | TLS: true,
190 | Network: "grpc",
191 | GrpcOpts: proxy.GrpcOptions{},
192 | },
193 | }
194 |
195 | result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
196 | if err != nil {
197 | t.Errorf("Unexpected error: %v", err)
198 | return
199 | }
200 |
201 | validateResult(t, expected, result)
202 | }
203 |
204 | func TestVmess_Error_InvalidBase64(t *testing.T) {
205 | p := &parser.VmessParser{}
206 | input := "vmess://invalid_base64"
207 |
208 | _, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
209 | if err == nil {
210 | t.Errorf("Expected error but got none")
211 | }
212 | }
213 |
214 | func TestVmess_Error_InvalidJSON(t *testing.T) {
215 | p := &parser.VmessParser{}
216 | input := "vmess://eyJpbnZhbGlkIjoianNvbn0="
217 |
218 | _, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
219 | if err == nil {
220 | t.Errorf("Expected error but got none")
221 | }
222 | }
223 |
224 | func TestVmess_Error_InvalidProtocol(t *testing.T) {
225 | p := &parser.VmessParser{}
226 | input := "ss://example.com:8080"
227 |
228 | _, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
229 | if err == nil {
230 | t.Errorf("Expected error but got none")
231 | }
232 | }
233 |
--------------------------------------------------------------------------------
/server/handler/short_link.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "encoding/json"
5 | "log"
6 | "net/http"
7 | "os"
8 | "strings"
9 | "time"
10 |
11 | "github.com/bestnite/sub2clash/common"
12 | "github.com/bestnite/sub2clash/common/database"
13 | "github.com/bestnite/sub2clash/config"
14 | "github.com/bestnite/sub2clash/model"
15 | M "github.com/bestnite/sub2clash/model"
16 | "gopkg.in/yaml.v3"
17 |
18 | "github.com/gin-gonic/gin"
19 | )
20 |
21 | type shortLinkGenRequset struct {
22 | Config model.ConvertConfig `form:"config" binding:"required"`
23 | Password string `form:"password"`
24 | ID string `form:"id"`
25 | }
26 |
27 | type shortLinkUpdateRequest struct {
28 | Config model.ConvertConfig `form:"config" binding:"required"`
29 | Password string `form:"password" binding:"required"`
30 | ID string `form:"id" binding:"required"`
31 | }
32 |
33 | var DB *database.Database
34 |
35 | func init() {
36 | var err error
37 | DB, err = database.ConnectDB()
38 | if err != nil {
39 | log.Printf("failed to connect to database: %v", err)
40 | os.Exit(1)
41 | }
42 | }
43 |
44 | func GenerateLinkHandler(c *gin.Context) {
45 | var params shortLinkGenRequset
46 | if err := c.ShouldBind(¶ms); err != nil {
47 | c.String(http.StatusBadRequest, "参数错误: "+err.Error())
48 | return
49 | }
50 |
51 | var id string
52 | var password string
53 | var err error
54 |
55 | if params.ID != "" {
56 | // 检查自定义ID是否已存在
57 | exists, err := DB.CheckShortLinkIDExists(params.ID)
58 | if err != nil {
59 | c.String(http.StatusInternalServerError, "数据库错误")
60 | return
61 | }
62 | if exists {
63 | c.String(http.StatusBadRequest, "短链已存在")
64 | return
65 | }
66 | id = params.ID
67 | password = params.Password
68 | } else {
69 | // 自动生成短链ID和密码
70 | id, err = generateUniqueHash(config.GlobalConfig.ShortLinkLength)
71 | if err != nil {
72 | c.String(http.StatusInternalServerError, "生成短链失败")
73 | return
74 | }
75 | if params.Password == "" {
76 | password = common.RandomString(8) // 生成8位随机密码
77 | } else {
78 | password = params.Password
79 | }
80 | }
81 |
82 | shortLink := model.ShortLink{
83 | ID: id,
84 | Config: params.Config,
85 | Password: password,
86 | }
87 |
88 | if err := DB.CreateShortLink(&shortLink); err != nil {
89 | c.String(http.StatusInternalServerError, "数据库错误")
90 | return
91 | }
92 |
93 | // 返回生成的短链ID和密码
94 | response := map[string]string{
95 | "id": id,
96 | "password": password,
97 | }
98 | c.JSON(http.StatusOK, response)
99 | }
100 |
101 | func generateUniqueHash(length int) (string, error) {
102 | for {
103 | hash := common.RandomString(length)
104 | exists, err := DB.CheckShortLinkIDExists(hash)
105 | if err != nil {
106 | return "", err
107 | }
108 | if !exists {
109 | return hash, nil
110 | }
111 | }
112 | }
113 |
114 | func UpdateLinkHandler(c *gin.Context) {
115 | var params shortLinkUpdateRequest
116 | if err := c.ShouldBindJSON(¶ms); err != nil {
117 | c.String(http.StatusBadRequest, "参数错误: "+err.Error())
118 | return
119 | }
120 |
121 | // 先获取原有的短链
122 | existingLink, err := DB.FindShortLinkByID(params.ID)
123 | if err != nil {
124 | c.String(http.StatusUnauthorized, "短链不存在或密码错误")
125 | return
126 | }
127 |
128 | // 验证密码
129 | if existingLink.Password != params.Password {
130 | c.String(http.StatusUnauthorized, "短链不存在或密码错误")
131 | return
132 | }
133 |
134 | jsonData, err := json.Marshal(params.Config)
135 | if err != nil {
136 | c.String(http.StatusBadRequest, "配置格式错误")
137 | return
138 | }
139 | if err := DB.UpdataShortLink(params.ID, "config", jsonData); err != nil {
140 | c.String(http.StatusInternalServerError, "数据库错误")
141 | return
142 | }
143 |
144 | c.String(http.StatusOK, "短链更新成功")
145 | }
146 |
147 | func GetRawConfHandler(c *gin.Context) {
148 | id := c.Param("id")
149 | password := c.Query("password")
150 |
151 | if strings.TrimSpace(id) == "" {
152 | c.String(http.StatusBadRequest, "参数错误")
153 | return
154 | }
155 |
156 | shortLink, err := DB.FindShortLinkByID(id)
157 | if err != nil {
158 | c.String(http.StatusUnauthorized, "短链不存在或密码错误")
159 | return
160 | }
161 |
162 | if shortLink.Password != "" && shortLink.Password != password {
163 | c.String(http.StatusUnauthorized, "短链不存在或密码错误")
164 | return
165 | }
166 |
167 | err = DB.UpdataShortLink(shortLink.ID, "last_request_time", time.Now().Unix())
168 | if err != nil {
169 | c.String(http.StatusInternalServerError, "数据库错误")
170 | return
171 | }
172 |
173 | template := ""
174 | switch shortLink.Config.ClashType {
175 | case model.Clash:
176 | template = config.GlobalConfig.ClashTemplate
177 | case model.ClashMeta:
178 | template = config.GlobalConfig.MetaTemplate
179 | }
180 | sub, err := common.BuildSub(shortLink.Config.ClashType, shortLink.Config, template, config.GlobalConfig.CacheExpire, config.GlobalConfig.RequestRetryTimes)
181 | if err != nil {
182 | c.String(http.StatusInternalServerError, err.Error())
183 | return
184 | }
185 |
186 | if len(shortLink.Config.Subs) == 1 {
187 | userInfoHeader, err := common.FetchSubscriptionUserInfo(shortLink.Config.Subs[0], "clash", config.GlobalConfig.RequestRetryTimes)
188 | if err == nil {
189 | c.Header("subscription-userinfo", userInfoHeader)
190 | }
191 | }
192 |
193 | if shortLink.Config.NodeListMode {
194 | nodelist := M.NodeList{}
195 | nodelist.Proxy = sub.Proxy
196 | marshal, err := yaml.Marshal(nodelist)
197 | if err != nil {
198 | c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
199 | return
200 | }
201 | c.String(http.StatusOK, string(marshal))
202 | return
203 | }
204 | marshal, err := yaml.Marshal(sub)
205 | if err != nil {
206 | c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
207 | return
208 | }
209 |
210 | c.String(http.StatusOK, string(marshal))
211 | }
212 |
213 | func GetRawConfUriHandler(c *gin.Context) {
214 | id := c.Param("id")
215 | password := c.Query("password")
216 |
217 | if strings.TrimSpace(id) == "" {
218 | c.String(http.StatusBadRequest, "参数错误")
219 | return
220 | }
221 |
222 | shortLink, err := DB.FindShortLinkByID(id)
223 | if err != nil {
224 | c.String(http.StatusUnauthorized, "短链不存在或密码错误")
225 | return
226 | }
227 |
228 | if shortLink.Password != "" && shortLink.Password != password {
229 | c.String(http.StatusUnauthorized, "短链不存在或密码错误")
230 | return
231 | }
232 |
233 | c.JSON(http.StatusOK, shortLink.Config)
234 | }
235 |
236 | func DeleteShortLinkHandler(c *gin.Context) {
237 | id := c.Param("id")
238 | password := c.Query("password")
239 | shortLink, err := DB.FindShortLinkByID(id)
240 | if err != nil {
241 | c.String(http.StatusBadRequest, "短链不存在或密码错误")
242 | return
243 | }
244 | if shortLink.Password != password {
245 | c.String(http.StatusUnauthorized, "短链不存在或密码错误")
246 | return
247 | }
248 |
249 | err = DB.DeleteShortLink(id)
250 | if err != nil {
251 | c.String(http.StatusInternalServerError, "删除失败", err)
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # sub2clash
2 |
3 | 将订阅链接转换为 Clash、Clash.Meta 配置
4 | [预览](https://clash.nite07.com/)
5 |
6 | ## 特性
7 |
8 | - 开箱即用的规则、策略组配置
9 | - 自动根据节点名称按国家划分策略组
10 | - 多订阅合并
11 | - 自定义 Rule Provider、Rule
12 | - 支持多种协议
13 | - Shadowsocks
14 | - ShadowsocksR
15 | - Vmess
16 | - Vless (Clash.Meta)
17 | - Trojan
18 | - Hysteria (Clash.Meta)
19 | - Hysteria2 (Clash.Meta)
20 | - Socks5
21 | - Anytls (Clash.Meta)
22 |
23 | ## 使用
24 |
25 | ### 部署
26 |
27 | - [docker compose](./compose.yml)
28 | - 运行[二进制文件](https://github.com/bestnite/sub2clash/releases/latest)
29 |
30 | ### 配置
31 |
32 | 支持多种配置方式,按优先级排序:
33 |
34 | 1. **配置文件**:支持多种格式(YAML、JSON),按以下优先级搜索:
35 | - `config.yaml` / `config.yml`
36 | - `config.json`
37 | - `sub2clash.yaml` / `sub2clash.yml`
38 | - `sub2clash.json`
39 | 2. **环境变量**:使用 `SUB2CLASH_` 前缀,例如 `SUB2CLASH_ADDRESS=0.0.0.0:8011`
40 | 3. **默认值**:内置默认配置
41 |
42 | | 配置项 | 环境变量 | 说明 | 默认值 |
43 | | --------------------- | ------------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------- |
44 | | address | SUB2CLASH_ADDRESS | 服务监听地址 | `0.0.0.0:8011` |
45 | | meta_template | SUB2CLASH_META_TEMPLATE | 默认 meta 模板 URL | `https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_meta.yaml` |
46 | | clash_template | SUB2CLASH_CLASH_TEMPLATE | 默认 clash 模板 URL | `https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_clash.yaml` |
47 | | request_retry_times | SUB2CLASH_REQUEST_RETRY_TIMES | 请求重试次数 | `3` |
48 | | request_max_file_size | SUB2CLASH_REQUEST_MAX_FILE_SIZE | 请求文件最大大小(byte) | `1048576` |
49 | | cache_expire | SUB2CLASH_CACHE_EXPIRE | 订阅缓存时间(秒) | `300` |
50 | | log_level | SUB2CLASH_LOG_LEVEL | 日志等级:`debug`,`info`,`warn`,`error` | `info` |
51 | | short_link_length | SUB2CLASH_SHORT_LINK_LENGTH | 短链长度 | `6` |
52 |
53 | #### 配置文件示例
54 |
55 | 参考示例文件:
56 |
57 | - [config.example.yaml](./config.example.yaml) - YAML 格式
58 | - [config.example.json](./config.example.json) - JSON 格式
59 |
60 | ### API
61 |
62 | #### `GET /convert/:config`
63 |
64 | 获取 Clash/Clash.Meta 配置链接
65 |
66 | | Path 参数 | 类型 | 说明 |
67 | | --------- | ------ | ---------------------------------------------- |
68 | | config | string | Base64 URL Safe 编码后的 JSON 字符串,格式如下 |
69 |
70 | ##### `config` JSON 结构
71 |
72 | | Query 参数 | 类型 | 是否必须 | 默认值 | 说明 |
73 | | ------------------ | ----------------- | ------------------------ | --------- | -------------------------------------------------------------------------------------------------------- |
74 | | clashType | int | 是 | 1 | 配置文件类型 (1: Clash, 2: Clash.Meta) |
75 | | subscriptions | []string | sub/proxy 至少有一项存在 | - | 订阅链接(v2ray 或 clash 格式),可以在链接结尾加上`#名称`,来给订阅中的节点加上统一前缀(可以输入多个) |
76 | | proxies | []string | sub/proxy 至少有一项存在 | - | 节点分享链接(可以输入多个) |
77 | | refresh | bool | 否 | `false` | 强制刷新配置(默认缓存 5 分钟) |
78 | | template | string | 否 | - | 外部模板链接或内部模板名称 |
79 | | ruleProviders | []RuleProvider | 否 | - | 规则 |
80 | | rules | []Rule | 否 | - | 规则 |
81 | | autoTest | bool | 否 | `false` | 国家策略组是否自动测速 |
82 | | lazy | bool | 否 | `false` | 自动测速是否启用 lazy |
83 | | sort | string | 否 | `nameasc` | 国家策略组排序策略,可选值 `nameasc`、`namedesc`、`sizeasc`、`sizedesc` |
84 | | replace | map[string]string | 否 | - | 通过正则表达式重命名节点 |
85 | | remove | string | 否 | - | 通过正则表达式删除节点 |
86 | | nodeList | bool | 否 | `false` | 只输出节点 |
87 | | ignoreCountryGroup | bool | 否 | `false` | 是否忽略国家分组 |
88 | | userAgent | string | 否 | - | 订阅 user-agent |
89 | | useUDP | bool | 否 | `false` | 是否使用 UDP |
90 |
91 | ###### `RuleProvider` 结构
92 |
93 | | 字段 | 类型 | 说明 |
94 | | -------- | ------ | ---------------------------------------------------------------- |
95 | | behavior | string | rule-set 的 behavior |
96 | | url | string | rule-set 的 url |
97 | | group | string | 该规则集使用的策略组名 |
98 | | prepend | bool | 如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部 |
99 | | name | string | 该 rule-provider 的名称,不能重复 |
100 |
101 | ###### `Rule` 结构
102 |
103 | | 字段 | 类型 | 说明 |
104 | | ------- | ------ | ---------------------------------------------------------------- |
105 | | rule | string | 规则 |
106 | | prepend | bool | 如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部 |
107 |
108 | ### 模板
109 |
110 | 可以通过变量自定义模板中的策略组代理节点
111 | 具体参考下方默认模板
112 |
113 | - `` 为添加所有节点
114 | - `` 为添加所有国家策略组
115 | - `<地区二位字母代码>` 为添加指定地区所有节点,例如 `` 将添加所有香港节点
116 |
117 | #### 默认模板
118 |
119 | - [Clash](./templates/template_clash.yaml)
120 | - [Clash.Meta](./templates/template_meta.yaml)
121 |
122 | ## 开发
123 |
124 | ### 添加新协议支持
125 |
126 | 添加新协议支持需要实现以下组件:
127 |
128 | 1. 在 `parser` 目录下实现协议解析器,用于解析节点链接
129 | 2. 在 `model/proxy` 目录下定义协议结构体
130 |
131 | ## 贡献者
132 |
133 | [](https://github.com/bestnite/sub2clash/graphs/contributors)
134 |
--------------------------------------------------------------------------------
/model/proxy/proxy.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 |
7 | "gopkg.in/yaml.v3"
8 | )
9 |
10 | type IntOrString int
11 |
12 | func (i *IntOrString) UnmarshalYAML(value *yaml.Node) error {
13 | intVal := 0
14 | err := yaml.Unmarshal([]byte(value.Value), &intVal)
15 | if err == nil {
16 | *i = IntOrString(intVal)
17 | }
18 | strVal := ""
19 | err = yaml.Unmarshal([]byte(value.Value), &strVal)
20 | if err == nil {
21 | _int, err := strconv.ParseInt(strVal, 10, 64)
22 | if err != nil {
23 | *i = IntOrString(_int)
24 | }
25 | return err
26 | }
27 | return nil
28 | }
29 |
30 | type HTTPOptions struct {
31 | Method string `yaml:"method,omitempty"`
32 | Path []string `yaml:"path,omitempty"`
33 | Headers map[string][]string `yaml:"headers,omitempty"`
34 | }
35 |
36 | type HTTP2Options struct {
37 | Host []string `yaml:"host,omitempty"`
38 | Path string `yaml:"path,omitempty"`
39 | }
40 |
41 | type GrpcOptions struct {
42 | GrpcServiceName string `yaml:"grpc-service-name,omitempty"`
43 | }
44 |
45 | type RealityOptions struct {
46 | PublicKey string `yaml:"public-key"`
47 | ShortID string `yaml:"short-id,omitempty"`
48 | }
49 |
50 | type WSOptions struct {
51 | Path string `yaml:"path,omitempty"`
52 | Headers map[string]string `yaml:"headers,omitempty"`
53 | MaxEarlyData int `yaml:"max-early-data,omitempty"`
54 | EarlyDataHeaderName string `yaml:"early-data-header-name,omitempty"`
55 | }
56 |
57 | type SmuxStruct struct {
58 | Enabled bool `yaml:"enable"`
59 | }
60 |
61 | type WireGuardPeerOption struct {
62 | Server string `yaml:"server"`
63 | Port int `yaml:"port"`
64 | PublicKey string `yaml:"public-key,omitempty"`
65 | PreSharedKey string `yaml:"pre-shared-key,omitempty"`
66 | Reserved []uint8 `yaml:"reserved,omitempty"`
67 | AllowedIPs []string `yaml:"allowed-ips,omitempty"`
68 | }
69 |
70 | type ECHOptions struct {
71 | Enable bool `yaml:"enable,omitempty" obfs:"enable,omitempty"`
72 | Config string `yaml:"config,omitempty" obfs:"config,omitempty"`
73 | }
74 |
75 | type Proxy struct {
76 | Type string
77 | Name string
78 | SubName string `yaml:"-"`
79 | Anytls
80 | Hysteria
81 | Hysteria2
82 | ShadowSocks
83 | ShadowSocksR
84 | Trojan
85 | Vless
86 | Vmess
87 | Socks
88 | Tuic
89 | }
90 |
91 | func (p Proxy) MarshalYAML() (any, error) {
92 | switch p.Type {
93 | case "anytls":
94 | return struct {
95 | Type string `yaml:"type"`
96 | Name string `yaml:"name"`
97 | Anytls `yaml:",inline"`
98 | }{
99 | Type: p.Type,
100 | Name: p.Name,
101 | Anytls: p.Anytls,
102 | }, nil
103 | case "hysteria":
104 | return struct {
105 | Type string `yaml:"type"`
106 | Name string `yaml:"name"`
107 | Hysteria `yaml:",inline"`
108 | }{
109 | Type: p.Type,
110 | Name: p.Name,
111 | Hysteria: p.Hysteria,
112 | }, nil
113 | case "hysteria2":
114 | return struct {
115 | Type string `yaml:"type"`
116 | Name string `yaml:"name"`
117 | Hysteria2 `yaml:",inline"`
118 | }{
119 | Type: p.Type,
120 | Name: p.Name,
121 | Hysteria2: p.Hysteria2,
122 | }, nil
123 | case "ss":
124 | return struct {
125 | Type string `yaml:"type"`
126 | Name string `yaml:"name"`
127 | ShadowSocks `yaml:",inline"`
128 | }{
129 | Type: p.Type,
130 | Name: p.Name,
131 | ShadowSocks: p.ShadowSocks,
132 | }, nil
133 | case "ssr":
134 | return struct {
135 | Type string `yaml:"type"`
136 | Name string `yaml:"name"`
137 | ShadowSocksR `yaml:",inline"`
138 | }{
139 | Type: p.Type,
140 | Name: p.Name,
141 | ShadowSocksR: p.ShadowSocksR,
142 | }, nil
143 | case "trojan":
144 | return struct {
145 | Type string `yaml:"type"`
146 | Name string `yaml:"name"`
147 | Trojan `yaml:",inline"`
148 | }{
149 | Type: p.Type,
150 | Name: p.Name,
151 | Trojan: p.Trojan,
152 | }, nil
153 | case "vless":
154 | return struct {
155 | Type string `yaml:"type"`
156 | Name string `yaml:"name"`
157 | Vless `yaml:",inline"`
158 | }{
159 | Type: p.Type,
160 | Name: p.Name,
161 | Vless: p.Vless,
162 | }, nil
163 | case "vmess":
164 | return struct {
165 | Type string `yaml:"type"`
166 | Name string `yaml:"name"`
167 | Vmess `yaml:",inline"`
168 | }{
169 | Type: p.Type,
170 | Name: p.Name,
171 | Vmess: p.Vmess,
172 | }, nil
173 | case "socks5":
174 | return struct {
175 | Type string `yaml:"type"`
176 | Name string `yaml:"name"`
177 | Socks `yaml:",inline"`
178 | }{
179 | Type: p.Type,
180 | Name: p.Name,
181 | Socks: p.Socks,
182 | }, nil
183 | case "tuic":
184 | return struct {
185 | Type string `yaml:"type"`
186 | Name string `yaml:"name"`
187 | Tuic `yaml:",inline"`
188 | }{
189 | Type: p.Type,
190 | Name: p.Name,
191 | Tuic: p.Tuic,
192 | }, nil
193 | default:
194 | return nil, fmt.Errorf("unsupported proxy type: %s", p.Type)
195 | }
196 | }
197 |
198 | func (p *Proxy) UnmarshalYAML(node *yaml.Node) error {
199 | var temp struct {
200 | Type string `yaml:"type"`
201 | Name string `yaml:"name"`
202 | }
203 |
204 | if err := node.Decode(&temp); err != nil {
205 | return err
206 | }
207 |
208 | p.Type = temp.Type
209 | p.Name = temp.Name
210 |
211 | switch temp.Type {
212 | case "anytls":
213 | var data struct {
214 | Type string `yaml:"type"`
215 | Name string `yaml:"name"`
216 | Anytls `yaml:",inline"`
217 | }
218 | if err := node.Decode(&data); err != nil {
219 | return err
220 | }
221 | p.Anytls = data.Anytls
222 |
223 | case "hysteria":
224 | var data struct {
225 | Type string `yaml:"type"`
226 | Name string `yaml:"name"`
227 | Hysteria `yaml:",inline"`
228 | }
229 | if err := node.Decode(&data); err != nil {
230 | return err
231 | }
232 | p.Hysteria = data.Hysteria
233 |
234 | case "hysteria2":
235 | var data struct {
236 | Type string `yaml:"type"`
237 | Name string `yaml:"name"`
238 | Hysteria2 `yaml:",inline"`
239 | }
240 | if err := node.Decode(&data); err != nil {
241 | return err
242 | }
243 | p.Hysteria2 = data.Hysteria2
244 |
245 | case "ss":
246 | var data struct {
247 | Type string `yaml:"type"`
248 | Name string `yaml:"name"`
249 | ShadowSocks `yaml:",inline"`
250 | }
251 | if err := node.Decode(&data); err != nil {
252 | return err
253 | }
254 | p.ShadowSocks = data.ShadowSocks
255 |
256 | case "ssr":
257 | var data struct {
258 | Type string `yaml:"type"`
259 | Name string `yaml:"name"`
260 | ShadowSocksR `yaml:",inline"`
261 | }
262 | if err := node.Decode(&data); err != nil {
263 | return err
264 | }
265 | p.ShadowSocksR = data.ShadowSocksR
266 |
267 | case "trojan":
268 | var data struct {
269 | Type string `yaml:"type"`
270 | Name string `yaml:"name"`
271 | Trojan `yaml:",inline"`
272 | }
273 | if err := node.Decode(&data); err != nil {
274 | return err
275 | }
276 | p.Trojan = data.Trojan
277 |
278 | case "vless":
279 | var data struct {
280 | Type string `yaml:"type"`
281 | Name string `yaml:"name"`
282 | Vless `yaml:",inline"`
283 | }
284 | if err := node.Decode(&data); err != nil {
285 | return err
286 | }
287 | p.Vless = data.Vless
288 |
289 | case "vmess":
290 | var data struct {
291 | Type string `yaml:"type"`
292 | Name string `yaml:"name"`
293 | Vmess `yaml:",inline"`
294 | }
295 | if err := node.Decode(&data); err != nil {
296 | return err
297 | }
298 | p.Vmess = data.Vmess
299 |
300 | case "socks5":
301 | var data struct {
302 | Type string `yaml:"type"`
303 | Name string `yaml:"name"`
304 | Socks `yaml:",inline"`
305 | }
306 | if err := node.Decode(&data); err != nil {
307 | return err
308 | }
309 | p.Socks = data.Socks
310 | case "tuic":
311 | var data struct {
312 | Type string `yaml:"type"`
313 | Name string `yaml:"name"`
314 | Tuic `yaml:",inline"`
315 | }
316 | if err := node.Decode(&data); err != nil {
317 | return err
318 | }
319 | p.Tuic = data.Tuic
320 | default:
321 | return fmt.Errorf("unsupported proxy type: %s", temp.Type)
322 | }
323 |
324 | return nil
325 | }
326 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/bestnite/sub2clash
2 |
3 | go 1.25
4 |
5 | require (
6 | github.com/gin-gonic/gin v1.10.1
7 | github.com/glebarez/sqlite v1.11.0
8 | github.com/metacubex/mihomo v1.19.10
9 | github.com/spf13/viper v1.20.1
10 | go.uber.org/zap v1.27.0
11 | golang.org/x/text v0.30.0
12 | gopkg.in/natefinch/lumberjack.v2 v2.2.1
13 | gopkg.in/yaml.v3 v3.0.1
14 | gorm.io/gorm v1.31.0
15 | resty.dev/v3 v3.0.0-beta.3
16 | )
17 |
18 | require (
19 | github.com/3andne/restls-client-go v0.1.6 // indirect
20 | github.com/RyuaNerin/go-krypto v1.3.0 // indirect
21 | github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
22 | github.com/andybalholm/brotli v1.0.6 // indirect
23 | github.com/bahlo/generic-list-go v0.2.0 // indirect
24 | github.com/buger/jsonparser v1.1.1 // indirect
25 | github.com/bytedance/sonic v1.11.6 // indirect
26 | github.com/bytedance/sonic/loader v0.1.1 // indirect
27 | github.com/cloudflare/circl v1.3.7 // indirect
28 | github.com/cloudwego/base64x v0.1.4 // indirect
29 | github.com/cloudwego/iasm v0.2.0 // indirect
30 | github.com/coreos/go-iptables v0.8.0 // indirect
31 | github.com/dlclark/regexp2 v1.11.5 // indirect
32 | github.com/dustin/go-humanize v1.0.1 // indirect
33 | github.com/ebitengine/purego v0.8.3 // indirect
34 | github.com/enfein/mieru/v3 v3.13.0 // indirect
35 | github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect
36 | github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
37 | github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
38 | github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
39 | github.com/fsnotify/fsnotify v1.9.0 // indirect
40 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect
41 | github.com/gaukas/godicttls v0.0.4 // indirect
42 | github.com/gin-contrib/sse v0.1.0 // indirect
43 | github.com/glebarez/go-sqlite v1.21.2 // indirect
44 | github.com/go-ole/go-ole v1.3.0 // indirect
45 | github.com/go-playground/locales v0.14.1 // indirect
46 | github.com/go-playground/universal-translator v0.18.1 // indirect
47 | github.com/go-playground/validator/v10 v10.20.0 // indirect
48 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
49 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
50 | github.com/gobwas/httphead v0.1.0 // indirect
51 | github.com/gobwas/pool v0.2.1 // indirect
52 | github.com/gobwas/ws v1.4.0 // indirect
53 | github.com/goccy/go-json v0.10.2 // indirect
54 | github.com/gofrs/uuid/v5 v5.3.2 // indirect
55 | github.com/google/btree v1.1.3 // indirect
56 | github.com/google/go-cmp v0.6.0 // indirect
57 | github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 // indirect
58 | github.com/google/uuid v1.6.0 // indirect
59 | github.com/hashicorp/yamux v0.1.2 // indirect
60 | github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 // indirect
61 | github.com/jinzhu/inflection v1.0.0 // indirect
62 | github.com/jinzhu/now v1.1.5 // indirect
63 | github.com/josharian/native v1.1.0 // indirect
64 | github.com/json-iterator/go v1.1.12 // indirect
65 | github.com/klauspost/compress v1.17.9 // indirect
66 | github.com/klauspost/cpuid/v2 v2.2.9 // indirect
67 | github.com/leodido/go-urn v1.4.0 // indirect
68 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
69 | github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect
70 | github.com/mailru/easyjson v0.7.7 // indirect
71 | github.com/mattn/go-isatty v0.0.20 // indirect
72 | github.com/mdlayher/netlink v1.7.2 // indirect
73 | github.com/mdlayher/socket v0.4.1 // indirect
74 | github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
75 | github.com/metacubex/bart v0.20.5 // indirect
76 | github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
77 | github.com/metacubex/chacha v0.1.2 // indirect
78 | github.com/metacubex/fswatch v0.1.1 // indirect
79 | github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
80 | github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b // indirect
81 | github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 // indirect
82 | github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639 // indirect
83 | github.com/metacubex/randv2 v0.2.0 // indirect
84 | github.com/metacubex/sing v0.5.3 // indirect
85 | github.com/metacubex/sing-mux v0.3.2 // indirect
86 | github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f // indirect
87 | github.com/metacubex/sing-shadowsocks v0.2.10 // indirect
88 | github.com/metacubex/sing-shadowsocks2 v0.2.4 // indirect
89 | github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 // indirect
90 | github.com/metacubex/sing-tun v0.4.6-0.20250524142129-9d110c0af70c // indirect
91 | github.com/metacubex/sing-vmess v0.2.2 // indirect
92 | github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f // indirect
93 | github.com/metacubex/smux v0.0.0-20250503055512-501391591dee // indirect
94 | github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 // indirect
95 | github.com/metacubex/utls v1.7.3 // indirect
96 | github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
97 | github.com/miekg/dns v1.1.63 // indirect
98 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
99 | github.com/modern-go/reflect2 v1.0.2 // indirect
100 | github.com/mroth/weightedrand/v2 v2.1.0 // indirect
101 | github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
102 | github.com/onsi/ginkgo/v2 v2.9.5 // indirect
103 | github.com/openacid/low v0.1.21 // indirect
104 | github.com/oschwald/maxminddb-golang v1.12.0 // indirect
105 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect
106 | github.com/pierrec/lz4/v4 v4.1.14 // indirect
107 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
108 | github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
109 | github.com/quic-go/qpack v0.4.0 // indirect
110 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
111 | github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
112 | github.com/sagikazarmark/locafero v0.7.0 // indirect
113 | github.com/samber/lo v1.50.0 // indirect
114 | github.com/shirou/gopsutil/v4 v4.25.1 // indirect
115 | github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
116 | github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
117 | github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect
118 | github.com/sirupsen/logrus v1.9.3 // indirect
119 | github.com/sourcegraph/conc v0.3.0 // indirect
120 | github.com/spf13/afero v1.12.0 // indirect
121 | github.com/spf13/cast v1.7.1 // indirect
122 | github.com/spf13/pflag v1.0.6 // indirect
123 | github.com/subosito/gotenv v1.6.0 // indirect
124 | github.com/tklauser/go-sysconf v0.3.12 // indirect
125 | github.com/tklauser/numcpus v0.6.1 // indirect
126 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
127 | github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
128 | github.com/ugorji/go/codec v1.2.12 // indirect
129 | github.com/vishvananda/netns v0.0.4 // indirect
130 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
131 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
132 | github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
133 | github.com/yusufpapurcu/wmi v1.2.4 // indirect
134 | gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 // indirect
135 | gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
136 | go.uber.org/mock v0.4.0 // indirect
137 | go.uber.org/multierr v1.11.0 // indirect
138 | go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
139 | golang.org/x/arch v0.8.0 // indirect
140 | golang.org/x/crypto v0.42.0 // indirect
141 | golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
142 | golang.org/x/mod v0.28.0 // indirect
143 | golang.org/x/net v0.44.0 // indirect
144 | golang.org/x/sync v0.17.0 // indirect
145 | golang.org/x/sys v0.36.0 // indirect
146 | golang.org/x/time v0.8.0 // indirect
147 | golang.org/x/tools v0.37.0 // indirect
148 | google.golang.org/protobuf v1.36.1 // indirect
149 | lukechampine.com/blake3 v1.3.0 // indirect
150 | modernc.org/libc v1.22.5 // indirect
151 | modernc.org/mathutil v1.5.0 // indirect
152 | modernc.org/memory v1.5.0 // indirect
153 | modernc.org/sqlite v1.23.1 // indirect
154 | )
155 |
--------------------------------------------------------------------------------
/common/sub.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "crypto/sha256"
5 | "encoding/hex"
6 | "fmt"
7 | "io"
8 | "net/url"
9 | "os"
10 | "path/filepath"
11 | "regexp"
12 | "sort"
13 | "strconv"
14 | "strings"
15 | "sync"
16 | "time"
17 |
18 | "github.com/bestnite/sub2clash/logger"
19 | "github.com/bestnite/sub2clash/model"
20 | P "github.com/bestnite/sub2clash/model/proxy"
21 | "github.com/bestnite/sub2clash/parser"
22 | "github.com/bestnite/sub2clash/utils"
23 | "go.uber.org/zap"
24 | "gopkg.in/yaml.v3"
25 | )
26 |
27 | var subsDir = "subs"
28 | var fileLock sync.RWMutex
29 |
30 | func LoadSubscription(url string, refresh bool, userAgent string, cacheExpire int64, retryTimes int) ([]byte, error) {
31 | if refresh {
32 | return FetchSubscriptionFromAPI(url, userAgent, retryTimes)
33 | }
34 | hash := sha256.Sum224([]byte(url))
35 | fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:]))
36 | stat, err := os.Stat(fileName)
37 | if err != nil {
38 | if !os.IsNotExist(err) {
39 | return nil, err
40 | }
41 | return FetchSubscriptionFromAPI(url, userAgent, retryTimes)
42 | }
43 | lastGetTime := stat.ModTime().Unix()
44 | if lastGetTime+cacheExpire > time.Now().Unix() {
45 | file, err := os.Open(fileName)
46 | if err != nil {
47 | return nil, err
48 | }
49 | defer func(file *os.File) {
50 | if file != nil {
51 | _ = file.Close()
52 | }
53 | }(file)
54 | fileLock.RLock()
55 | defer fileLock.RUnlock()
56 | subContent, err := io.ReadAll(file)
57 | if err != nil {
58 | return nil, err
59 | }
60 | return subContent, nil
61 | }
62 | return FetchSubscriptionFromAPI(url, userAgent, retryTimes)
63 | }
64 |
65 | func FetchSubscriptionFromAPI(url string, userAgent string, retryTimes int) ([]byte, error) {
66 | hash := sha256.Sum224([]byte(url))
67 | fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:]))
68 | client := Request(retryTimes)
69 | defer client.Close()
70 | resp, err := client.R().SetHeader("User-Agent", userAgent).Get(url)
71 | if err != nil {
72 | return nil, err
73 | }
74 | data, err := io.ReadAll(resp.Body)
75 | if err != nil {
76 | return nil, fmt.Errorf("failed to read response body: %w", err)
77 | }
78 | file, err := os.Create(fileName)
79 | if err != nil {
80 | return nil, err
81 | }
82 | defer func(file *os.File) {
83 | if file != nil {
84 | _ = file.Close()
85 | }
86 | }(file)
87 | fileLock.Lock()
88 | defer fileLock.Unlock()
89 | _, err = file.Write(data)
90 | if err != nil {
91 | return nil, fmt.Errorf("failed to write to sub.yaml: %w", err)
92 | }
93 | return data, nil
94 | }
95 |
96 | func BuildSub(clashType model.ClashType, query model.ConvertConfig, template string, cacheExpire int64, retryTimes int) (
97 | *model.Subscription, error,
98 | ) {
99 | var temp = &model.Subscription{}
100 | var sub = &model.Subscription{}
101 | var err error
102 | var templateBytes []byte
103 |
104 | if query.Template != "" {
105 | template = query.Template
106 | }
107 | if strings.HasPrefix(template, "http") {
108 | templateBytes, err = LoadSubscription(template, query.Refresh, query.UserAgent, cacheExpire, retryTimes)
109 | if err != nil {
110 | logger.Logger.Debug(
111 | "load template failed", zap.String("template", template), zap.Error(err),
112 | )
113 | return nil, NewTemplateLoadError(template, err)
114 | }
115 | } else {
116 | unescape, err := url.QueryUnescape(template)
117 | if err != nil {
118 | return nil, NewTemplateLoadError(template, err)
119 | }
120 | templateBytes, err = LoadTemplate(unescape)
121 | if err != nil {
122 | logger.Logger.Debug(
123 | "load template failed", zap.String("template", template), zap.Error(err),
124 | )
125 | return nil, NewTemplateLoadError(unescape, err)
126 | }
127 | }
128 |
129 | err = yaml.Unmarshal(templateBytes, &temp)
130 | if err != nil {
131 | logger.Logger.Debug("parse template failed", zap.Error(err))
132 | return nil, NewTemplateParseError(templateBytes, err)
133 | }
134 | var proxyList []P.Proxy
135 |
136 | for i := range query.Subs {
137 | data, err := LoadSubscription(query.Subs[i], query.Refresh, query.UserAgent, cacheExpire, retryTimes)
138 | if err != nil {
139 | logger.Logger.Debug(
140 | "load subscription failed", zap.String("url", query.Subs[i]), zap.Error(err),
141 | )
142 | return nil, NewSubscriptionLoadError(query.Subs[i], err)
143 | }
144 | subName := ""
145 | if strings.Contains(query.Subs[i], "#") {
146 | subName = query.Subs[i][strings.LastIndex(query.Subs[i], "#")+1:]
147 | }
148 |
149 | err = yaml.Unmarshal(data, &sub)
150 | var newProxies []P.Proxy
151 | if err != nil {
152 | reg, err := regexp.Compile("(" + strings.Join(parser.GetAllPrefixes(), "|") + ")://")
153 | if err != nil {
154 | logger.Logger.Debug("compile regex failed", zap.Error(err))
155 | return nil, NewRegexInvalidError("prefix", err)
156 | }
157 | if reg.Match(data) {
158 | p, err := parser.ParseProxies(parser.ParseConfig{UseUDP: query.UseUDP}, strings.Split(string(data), "\n")...)
159 | if err != nil {
160 | return nil, err
161 | }
162 | newProxies = p
163 | } else {
164 | base64, err := utils.DecodeBase64(string(data), false)
165 | if err != nil {
166 | logger.Logger.Debug(
167 | "parse subscription failed", zap.String("url", query.Subs[i]),
168 | zap.String("data", string(data)),
169 | zap.Error(err),
170 | )
171 | return nil, NewSubscriptionParseError(data, err)
172 | }
173 | p, err := parser.ParseProxies(parser.ParseConfig{UseUDP: query.UseUDP}, strings.Split(base64, "\n")...)
174 | if err != nil {
175 | return nil, err
176 | }
177 | newProxies = p
178 | }
179 | } else {
180 | newProxies = sub.Proxy
181 | }
182 | if subName != "" {
183 | for i := range newProxies {
184 | newProxies[i].SubName = subName
185 | }
186 | }
187 | proxyList = append(proxyList, newProxies...)
188 | }
189 |
190 | if len(query.Proxies) != 0 {
191 | p, err := parser.ParseProxies(parser.ParseConfig{UseUDP: query.UseUDP}, query.Proxies...)
192 | if err != nil {
193 | return nil, err
194 | }
195 | proxyList = append(proxyList, p...)
196 | }
197 |
198 | for i := range proxyList {
199 | if proxyList[i].SubName != "" {
200 | proxyList[i].Name = strings.TrimSpace(proxyList[i].SubName) + " " + strings.TrimSpace(proxyList[i].Name)
201 | }
202 | }
203 |
204 | // 去重
205 | proxies := make(map[string]*P.Proxy)
206 | newProxies := make([]P.Proxy, 0, len(proxyList))
207 | for i := range proxyList {
208 | yamlBytes, err := yaml.Marshal(proxyList[i])
209 | if err != nil {
210 | logger.Logger.Debug("marshal proxy failed", zap.Error(err))
211 | return nil, fmt.Errorf("marshal proxy failed: %w", err)
212 | }
213 | key := string(yamlBytes)
214 | if _, exist := proxies[key]; !exist {
215 | proxies[key] = &proxyList[i]
216 | newProxies = append(newProxies, proxyList[i])
217 | }
218 | }
219 | proxyList = newProxies
220 |
221 | // 移除
222 | if strings.TrimSpace(query.Remove) != "" {
223 | newProxyList := make([]P.Proxy, 0, len(proxyList))
224 | for i := range proxyList {
225 | removeReg, err := regexp.Compile(query.Remove)
226 | if err != nil {
227 | logger.Logger.Debug("remove regexp compile failed", zap.Error(err))
228 | return nil, NewRegexInvalidError("remove", err)
229 | }
230 |
231 | if removeReg.MatchString(proxyList[i].Name) {
232 | continue
233 | }
234 | newProxyList = append(newProxyList, proxyList[i])
235 | }
236 | proxyList = newProxyList
237 | }
238 |
239 | // 替换
240 | if len(query.Replace) != 0 {
241 | for k, v := range query.Replace {
242 | replaceReg, err := regexp.Compile(k)
243 | if err != nil {
244 | logger.Logger.Debug("replace regexp compile failed", zap.Error(err))
245 | return nil, NewRegexInvalidError("replace", err)
246 | }
247 | for i := range proxyList {
248 | if replaceReg.MatchString(proxyList[i].Name) {
249 | proxyList[i].Name = replaceReg.ReplaceAllString(
250 | proxyList[i].Name, v,
251 | )
252 | }
253 | }
254 | }
255 | }
256 |
257 | // 重命名有相同名称的节点
258 | names := make(map[string]int)
259 | for i := range proxyList {
260 | if _, exist := names[proxyList[i].Name]; exist {
261 | names[proxyList[i].Name] = names[proxyList[i].Name] + 1
262 | proxyList[i].Name = proxyList[i].Name + " " + strconv.Itoa(names[proxyList[i].Name])
263 | } else {
264 | names[proxyList[i].Name] = 0
265 | }
266 | }
267 |
268 | for i := range proxyList {
269 | proxyList[i].Name = strings.TrimSpace(proxyList[i].Name)
270 | }
271 |
272 | var t = &model.Subscription{}
273 | AddProxy(t, query.AutoTest, query.Lazy, clashType, proxyList...)
274 |
275 | // 排序
276 | switch query.Sort {
277 | case "sizeasc":
278 | sort.Sort(model.ProxyGroupsSortBySize(t.ProxyGroup))
279 | case "sizedesc":
280 | sort.Sort(sort.Reverse(model.ProxyGroupsSortBySize(t.ProxyGroup)))
281 | case "nameasc":
282 | sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroup))
283 | case "namedesc":
284 | sort.Sort(sort.Reverse(model.ProxyGroupsSortByName(t.ProxyGroup)))
285 | default:
286 | sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroup))
287 | }
288 |
289 | MergeSubAndTemplate(temp, t, query.IgnoreCountryGrooup)
290 |
291 | for _, v := range query.Rules {
292 | if v.Prepend {
293 | PrependRules(temp, v.Rule)
294 | } else {
295 | AppendRules(temp, v.Rule)
296 | }
297 | }
298 |
299 | for _, v := range query.RuleProviders {
300 | hash := sha256.Sum224([]byte(v.Url))
301 | name := hex.EncodeToString(hash[:])
302 | provider := model.RuleProvider{
303 | Type: "http",
304 | Behavior: v.Behavior,
305 | Url: v.Url,
306 | Path: "./" + name + ".yaml",
307 | Interval: 3600,
308 | }
309 | if v.Prepend {
310 | PrependRuleProvider(
311 | temp, v.Name, v.Group, provider,
312 | )
313 | } else {
314 | AppenddRuleProvider(
315 | temp, v.Name, v.Group, provider,
316 | )
317 | }
318 | }
319 | return temp, nil
320 | }
321 |
322 | func FetchSubscriptionUserInfo(url string, userAgent string, retryTimes int) (string, error) {
323 | client := Request(retryTimes)
324 | defer client.Close()
325 | resp, err := client.R().SetHeader("User-Agent", userAgent).Head(url)
326 | if err != nil {
327 | logger.Logger.Debug("创建 HEAD 请求失败", zap.Error(err))
328 | return "", NewNetworkRequestError(url, err)
329 | }
330 | defer resp.Body.Close()
331 | if userInfo := resp.Header().Get("subscription-userinfo"); userInfo != "" {
332 | return userInfo, nil
333 | }
334 |
335 | logger.Logger.Debug("subscription-userinfo header not found in response")
336 | return "", NewNetworkResponseError("subscription-userinfo header not found", nil)
337 | }
338 |
339 | func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription, igcg bool) {
340 | var countryGroupNames []string
341 | for _, proxyGroup := range sub.ProxyGroup {
342 | if proxyGroup.IsCountryGrop {
343 | countryGroupNames = append(
344 | countryGroupNames, proxyGroup.Name,
345 | )
346 | }
347 | }
348 | var proxyNames []string
349 | for _, proxy := range sub.Proxy {
350 | proxyNames = append(proxyNames, proxy.Name)
351 | }
352 |
353 | temp.Proxy = append(temp.Proxy, sub.Proxy...)
354 |
355 | for i := range temp.ProxyGroup {
356 | if temp.ProxyGroup[i].IsCountryGrop {
357 | continue
358 | }
359 | newProxies := make([]string, 0)
360 | countryGroupMap := make(map[string]model.ProxyGroup)
361 | for _, v := range sub.ProxyGroup {
362 | if v.IsCountryGrop {
363 | countryGroupMap[v.Name] = v
364 | }
365 | }
366 | for j := range temp.ProxyGroup[i].Proxies {
367 | reg := regexp.MustCompile("<(.*?)>")
368 | if reg.Match([]byte(temp.ProxyGroup[i].Proxies[j])) {
369 | key := reg.FindStringSubmatch(temp.ProxyGroup[i].Proxies[j])[1]
370 | switch key {
371 | case "all":
372 | newProxies = append(newProxies, proxyNames...)
373 | case "countries":
374 | if !igcg {
375 | newProxies = append(newProxies, countryGroupNames...)
376 | }
377 | default:
378 | if !igcg {
379 | if len(key) == 2 {
380 | newProxies = append(
381 | newProxies, countryGroupMap[GetContryName(key)].Proxies...,
382 | )
383 | }
384 | }
385 | }
386 | } else {
387 | newProxies = append(newProxies, temp.ProxyGroup[i].Proxies[j])
388 | }
389 | }
390 | temp.ProxyGroup[i].Proxies = newProxies
391 | }
392 | if !igcg {
393 | temp.ProxyGroup = append(temp.ProxyGroup, sub.ProxyGroup...)
394 | }
395 | }
396 |
--------------------------------------------------------------------------------