Compare commits
755 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70b365171f | ||
|
|
328ba3b45e | ||
|
|
5370b6943a | ||
|
|
d8c783a296 | ||
|
|
809f69729a | ||
|
|
93b7ce199f | ||
|
|
2a76cec804 | ||
|
|
88eab032be | ||
|
|
20ec863f51 | ||
|
|
2f4018bbe5 | ||
|
|
f273708f6d | ||
|
|
e6318d57e4 | ||
|
|
77fa976ee9 | ||
|
|
8098d2b1b1 | ||
|
|
a691eaea8d | ||
|
|
da447e5669 | ||
|
|
f8c9aac97c | ||
|
|
e42c17f2b2 | ||
|
|
427b7b67d8 | ||
|
|
ccf08086ac | ||
|
|
7b0a3929ff | ||
|
|
570ab8e5e0 | ||
|
|
1240e4c962 | ||
|
|
c117b8b272 | ||
|
|
6041d10e3d | ||
|
|
4800f8fb70 | ||
|
|
a9770e1da2 | ||
|
|
3f15d21f13 | ||
|
|
a6b3623634 | ||
|
|
947fd4fae1 | ||
|
|
e69a31dd59 | ||
|
|
719ae0e014 | ||
|
|
5bcf6a8aeb | ||
|
|
945fefde12 | ||
|
|
313a2acbf6 | ||
|
|
b747730211 | ||
|
|
692a73788a | ||
|
|
3287fa4d80 | ||
|
|
1393f981bc | ||
|
|
9a2c1c6b43 | ||
|
|
278aa1c85c | ||
|
|
8fe297ef9d | ||
|
|
c881d1015a | ||
|
|
c061337ce7 | ||
|
|
260eedf8c4 | ||
|
|
69ccdba734 | ||
|
|
4c797dc154 | ||
|
|
f000322a06 | ||
|
|
0ea8b5352a | ||
|
|
68240061aa | ||
|
|
0695f677ba | ||
|
|
70f6d6b21a | ||
|
|
e8c509c720 | ||
|
|
83a1c721c7 | ||
|
|
7ccc0877a1 | ||
|
|
ad659e48cf | ||
|
|
784ed39930 | ||
|
|
538f7fd5d7 | ||
|
|
cf38226b5d | ||
|
|
575ee854c8 | ||
|
|
9936af80dd | ||
|
|
4a75bd0a48 | ||
|
|
b0c223c631 | ||
|
|
313b51f96f | ||
|
|
020cd63e22 | ||
|
|
6e46e9b16e | ||
|
|
713a7328f6 | ||
|
|
01d4a7488d | ||
|
|
2b2ed3349a | ||
|
|
d8523bbdac | ||
|
|
8afa39144e | ||
|
|
00baeffe74 | ||
|
|
b578a33518 | ||
|
|
8153e0ac05 | ||
|
|
2eb9d2e2e8 | ||
|
|
a824875c4f | ||
|
|
cafcb250ec | ||
|
|
e7cfee570b | ||
|
|
90c3529301 | ||
|
|
b65ec83c39 | ||
|
|
28a17a80ec | ||
|
|
3056583388 | ||
|
|
172f2ddaa7 | ||
|
|
d69af328dc | ||
|
|
ee0e3093ba | ||
|
|
89def9aee6 | ||
|
|
b2b0024648 | ||
|
|
5822758b7c | ||
|
|
49430b3991 | ||
|
|
104526aab2 | ||
|
|
a0c07241c0 | ||
|
|
adf3242602 | ||
|
|
3f62592e4b | ||
|
|
02bff4db6c | ||
|
|
8ff4e1ff31 | ||
|
|
26c6438ec2 | ||
|
|
b3e96230c4 | ||
|
|
1016f3b4f9 | ||
|
|
020bc9d77c | ||
|
|
5620d739c6 | ||
|
|
d518979e4f | ||
|
|
83f8a03b50 | ||
|
|
b45e63a14a | ||
|
|
3007bcff97 | ||
|
|
55f1d72af5 | ||
|
|
806ecbd7c5 | ||
|
|
ae79b43cdb | ||
|
|
e64e6327ef | ||
|
|
9f024b9e6a | ||
|
|
eacfbc86b5 | ||
|
|
37c17357fc | ||
|
|
b35d339665 | ||
|
|
5e7a3db873 | ||
|
|
6ced549dea | ||
|
|
f60682a6b7 | ||
|
|
50bd7a8040 | ||
|
|
7465768ff7 | ||
|
|
5b00a52c65 | ||
|
|
151f1173a1 | ||
|
|
e262132b9d | ||
|
|
ca0a7aeb5a | ||
|
|
7447cec17e | ||
|
|
0ffd27c0aa | ||
|
|
054cb1dea0 | ||
|
|
3757ae0b11 | ||
|
|
e3883fca87 | ||
|
|
b46a0b404b | ||
|
|
0ce58a095a | ||
|
|
59ea2645db | ||
|
|
8c8d280f14 | ||
|
|
c720008187 | ||
|
|
170d24499e | ||
|
|
99c79d4056 | ||
|
|
fcdeb1fc79 | ||
|
|
0a58b5e745 | ||
|
|
db7e7dcd29 | ||
|
|
01b8a27996 | ||
|
|
3764ece26c | ||
|
|
d7efc2aef9 | ||
|
|
2eb8abf61e | ||
|
|
299572a4c2 | ||
|
|
22afa50901 | ||
|
|
bc274d1e1f | ||
|
|
dc21f41932 | ||
|
|
f137b1af76 | ||
|
|
c4871ef8fe | ||
|
|
ecfffa882a | ||
|
|
3af5026abe | ||
|
|
1de7accd7c | ||
|
|
76afff2a6f | ||
|
|
9623e87511 | ||
|
|
bc0518391e | ||
|
|
5408a2f82c | ||
|
|
c8d71ea748 | ||
|
|
46de886b53 | ||
|
|
6d41320ed7 | ||
|
|
bf9d2e6aeb | ||
|
|
ed96fa090b | ||
|
|
3ac1d7f546 | ||
|
|
10025ffa66 | ||
|
|
5ee62b25ca | ||
|
|
311d11a3c1 | ||
|
|
40b6d7707a | ||
|
|
cbf316db31 | ||
|
|
33a36ada4b | ||
|
|
82ddd10627 | ||
|
|
2401c99817 | ||
|
|
2f36a4047c | ||
|
|
dc3b0d218a | ||
|
|
610d29765a | ||
|
|
b1ea8005e4 | ||
|
|
3f0bfa2472 | ||
|
|
1e2ff650ad | ||
|
|
c2d6dd923f | ||
|
|
723ec25fb2 | ||
|
|
7dc52e9a53 | ||
|
|
fe9f0d1d0e | ||
|
|
18d74d54ca | ||
|
|
c7ba6ae909 | ||
|
|
3edf79e589 | ||
|
|
5420e643cf | ||
|
|
9fcd0387ca | ||
|
|
7b039d219e | ||
|
|
dbec28b915 | ||
|
|
e5126806d7 | ||
|
|
b008ff4ad2 | ||
|
|
da6b89fdcd | ||
|
|
d7882c25d1 | ||
|
|
ed2a0a0bcf | ||
|
|
4a0914cb1e | ||
|
|
664269d513 | ||
|
|
d0796b26c9 | ||
|
|
2750f46c01 | ||
|
|
023eb513e4 | ||
|
|
0c7b59ed47 | ||
|
|
3087c1b123 | ||
|
|
2198397197 | ||
|
|
d10c312e62 | ||
|
|
24a3411465 | ||
|
|
2198e7a28f | ||
|
|
6b23b416a7 | ||
|
|
16f53ce4c2 | ||
|
|
27445b30e9 | ||
|
|
3d0212c21d | ||
|
|
978755960f | ||
|
|
9b51e9a5c5 | ||
|
|
6879a8fbcb | ||
|
|
7258841491 | ||
|
|
23dd80fbb0 | ||
|
|
6556884c7f | ||
|
|
d5c532c64f | ||
|
|
ad5f774a1e | ||
|
|
aa285914fa | ||
|
|
4d02756e1e | ||
|
|
825d93d95f | ||
|
|
5ea6386815 | ||
|
|
d064e85ecd | ||
|
|
9fc03bd10a | ||
|
|
ae08a29cde | ||
|
|
4f25eb230e | ||
|
|
ce72d53d1a | ||
|
|
5e641ff9e8 | ||
|
|
58898e5758 | ||
|
|
569550d5f6 | ||
|
|
419ea63dd0 | ||
|
|
6a17285935 | ||
|
|
4b03e9d919 | ||
|
|
7e9c3bdbaf | ||
|
|
957f3dbb54 | ||
|
|
05e60af283 | ||
|
|
5e40458116 | ||
|
|
baf6fdd29d | ||
|
|
45f78d3521 | ||
|
|
01f984e054 | ||
|
|
e4ba5ba53a | ||
|
|
6ff555c8bb | ||
|
|
3c1634ca7c | ||
|
|
561c4810be | ||
|
|
eb1b96643d | ||
|
|
de5314c01f | ||
|
|
1088d1faf3 | ||
|
|
267024c43f | ||
|
|
0d595f56e4 | ||
|
|
a4c4f9efb3 | ||
|
|
73a5722cca | ||
|
|
30264043f8 | ||
|
|
c6062eb15c | ||
|
|
f1b7944828 | ||
|
|
7a57b31ff3 | ||
|
|
6e1b949081 | ||
|
|
0ad708b1b6 | ||
|
|
71f13ebcbd | ||
|
|
f5f4a530cc | ||
|
|
702f03e4b7 | ||
|
|
487ec74e0b | ||
|
|
b4dae36345 | ||
|
|
761728255c | ||
|
|
b1ab156e42 | ||
|
|
fa45bf87de | ||
|
|
75416eebd7 | ||
|
|
87042d77ba | ||
|
|
011e0f309a | ||
|
|
b7164805f8 | ||
|
|
bbdeb65291 | ||
|
|
038cf34219 | ||
|
|
98a1517470 | ||
|
|
ce76cedb0d | ||
|
|
24a313d605 | ||
|
|
c81c27073c | ||
|
|
5d11e6e13f | ||
|
|
f3d0b92e4a | ||
|
|
c8c0e77714 | ||
|
|
49b8f46864 | ||
|
|
cad07be847 | ||
|
|
4b20f16024 | ||
|
|
d642774a44 | ||
|
|
1644904755 | ||
|
|
5c10035bd9 | ||
|
|
2e6faf69e6 | ||
|
|
f88b7b07f0 | ||
|
|
e5752239f4 | ||
|
|
cb22b4ad47 | ||
|
|
6a2e0071cf | ||
|
|
f86219f4de | ||
|
|
e272c160b1 | ||
|
|
ba50c99c10 | ||
|
|
00b61de646 | ||
|
|
dff4ad31ff | ||
|
|
13baf77893 | ||
|
|
4531574de3 | ||
|
|
d1e07954c5 | ||
|
|
d9922d93af | ||
|
|
c7d315f848 | ||
|
|
1781790dce | ||
|
|
29f950046a | ||
|
|
5dae785786 | ||
|
|
1b1cbfff42 | ||
|
|
c93467b852 | ||
|
|
c988d55256 | ||
|
|
ef625c75d8 | ||
|
|
182e591c48 | ||
|
|
3666d1193f | ||
|
|
7a5a833af3 | ||
|
|
58f978bb0a | ||
|
|
6d47496069 | ||
|
|
e5c19759db | ||
|
|
295a8b6e37 | ||
|
|
384e23aeb2 | ||
|
|
23293813bb | ||
|
|
c15ec5315a | ||
|
|
1ddfe4aba3 | ||
|
|
fe3b1c9b52 | ||
|
|
d39ccf4b8f | ||
|
|
1aed2d8cdc | ||
|
|
c3084aaece | ||
|
|
13cf7271d6 | ||
|
|
63edc63ab0 | ||
|
|
85cbad3ef4 | ||
|
|
3d54e33051 | ||
|
|
01be9fec95 | ||
|
|
0306e75c2a | ||
|
|
255ff9cc20 | ||
|
|
2fbb1ca6c9 | ||
|
|
3b47028060 | ||
|
|
d9ab8b4ce4 | ||
|
|
e6389f3fb3 | ||
|
|
96fd7d0e7c | ||
|
|
cf02f02210 | ||
|
|
4dc8974af0 | ||
|
|
b527a528ea | ||
|
|
1a53af0434 | ||
|
|
be8d55dadb | ||
|
|
d54e7a9b14 | ||
|
|
45c3d730d4 | ||
|
|
aab01ff11a | ||
|
|
236dddf482 | ||
|
|
8e472838d8 | ||
|
|
b75a1ef5e1 | ||
|
|
d956f78347 | ||
|
|
8ef447a997 | ||
|
|
520f7a2d15 | ||
|
|
3ded4ee658 | ||
|
|
bea19a263d | ||
|
|
878e0d02cd | ||
|
|
b15ea1f74d | ||
|
|
431d7350a5 | ||
|
|
127bea7f73 | ||
|
|
7c58bcbb46 | ||
|
|
fec9b25248 | ||
|
|
728166bd1a | ||
|
|
d376ce057c | ||
|
|
5e6e900e64 | ||
|
|
19f7938617 | ||
|
|
fe791b6e99 | ||
|
|
a02bf3195d | ||
|
|
3ea05d30c1 | ||
|
|
40ebf2902e | ||
|
|
14253c3586 | ||
|
|
7b15274c84 | ||
|
|
6545d8b61d | ||
|
|
6f4eefe601 | ||
|
|
510c35f450 | ||
|
|
00addb0dd9 | ||
|
|
db140a1e9b | ||
|
|
db945e2fbd | ||
|
|
667fac15f4 | ||
|
|
29033a7828 | ||
|
|
2ffde55f8f | ||
|
|
6e5ed881f2 | ||
|
|
d52c50fd9e | ||
|
|
fa5fb927c1 | ||
|
|
f7198c4c2f | ||
|
|
21e7d45b54 | ||
|
|
db62a07fb8 | ||
|
|
e3120c4028 | ||
|
|
7ae855e7c9 | ||
|
|
b9307c6c9c | ||
|
|
d30cdbf49a | ||
|
|
cac00224db | ||
|
|
b68f0a206c | ||
|
|
0bde51b91e | ||
|
|
280a22b57d | ||
|
|
315d852087 | ||
|
|
6a0d2e0a29 | ||
|
|
a811225610 | ||
|
|
1893c3814d | ||
|
|
422c391f96 | ||
|
|
f7f95ffbae | ||
|
|
f408bd7c77 | ||
|
|
ad13ce6cde | ||
|
|
c35179d924 | ||
|
|
cedc7f0fb8 | ||
|
|
64fa0e97a3 | ||
|
|
a45e9de472 | ||
|
|
101e9ebf35 | ||
|
|
17a76d2843 | ||
|
|
a23a5de540 | ||
|
|
a16e83468b | ||
|
|
1c59afe031 | ||
|
|
c49ec9a74c | ||
|
|
a0dd101d97 | ||
|
|
700cf9c10b | ||
|
|
697cd5e6d9 | ||
|
|
c6d27a4463 | ||
|
|
751f564c4a | ||
|
|
6658f648e6 | ||
|
|
d6f9f3f6d3 | ||
|
|
fad6c497eb | ||
|
|
42fa64770b | ||
|
|
0a207b8a2c | ||
|
|
26bf693dbd | ||
|
|
7483fb2ec5 | ||
|
|
2d8cca3a2e | ||
|
|
cf7fec1351 | ||
|
|
361849b9db | ||
|
|
c13db7922e | ||
|
|
d6d05a9b4d | ||
|
|
3caace2cb6 | ||
|
|
99f26be30d | ||
|
|
b0edd24c52 | ||
|
|
f0cfd48f66 | ||
|
|
f5aea03765 | ||
|
|
14cdde371f | ||
|
|
99a23f25d5 | ||
|
|
653ec90451 | ||
|
|
91a84db479 | ||
|
|
0f97eca314 | ||
|
|
fb79081aa1 | ||
|
|
ea19fb8ff6 | ||
|
|
f4cfe9eb63 | ||
|
|
4def70a006 | ||
|
|
e0e9e2681a | ||
|
|
31e1581d6b | ||
|
|
018e98a510 | ||
|
|
21ea673c30 | ||
|
|
08b55da408 | ||
|
|
7a3ee69a7f | ||
|
|
664bd9b596 | ||
|
|
ceb1217121 | ||
|
|
e754523689 | ||
|
|
e84503feec | ||
|
|
1bbf31df9f | ||
|
|
49bfff9fa5 | ||
|
|
d18a1a37ce | ||
|
|
04c6b2722b | ||
|
|
94d651fc93 | ||
|
|
aae0cb37b7 | ||
|
|
b922d986d6 | ||
|
|
8a7cffd63f | ||
|
|
c8e8c97afc | ||
|
|
46ba4c4518 | ||
|
|
a787ab497c | ||
|
|
3be204f272 | ||
|
|
de13729a97 | ||
|
|
468eb8b908 | ||
|
|
e95a748e77 | ||
|
|
34e2d961f5 | ||
|
|
b4a1d81444 | ||
|
|
46ef506aa6 | ||
|
|
51220917c4 | ||
|
|
b34956647b | ||
|
|
5c4e2dfd39 | ||
|
|
dd4c2adb37 | ||
|
|
2dec7f48f5 | ||
|
|
04cf250a54 | ||
|
|
ac9ab828b5 | ||
|
|
4dd40f6f19 | ||
|
|
7911eeb69f | ||
|
|
6e9180a665 | ||
|
|
66fe84181b | ||
|
|
7b7eb98acb | ||
|
|
7eb5afdd8d | ||
|
|
522ccda71c | ||
|
|
f780efb430 | ||
|
|
783f1a073e | ||
|
|
a4c38ec8ae | ||
|
|
0c47771671 | ||
|
|
67920a1962 | ||
|
|
49d3957c07 | ||
|
|
ee946ceab2 | ||
|
|
b650064177 | ||
|
|
9fb9d7201e | ||
|
|
4a3b9b913d | ||
|
|
da674d44cf | ||
|
|
cc3252531b | ||
|
|
284731deeb | ||
|
|
9bc5c1d070 | ||
|
|
26a7700557 | ||
|
|
b6a919218a | ||
|
|
6cc07254e0 | ||
|
|
4ad5a5aba4 | ||
|
|
7ab8164de4 | ||
|
|
195effd177 | ||
|
|
747ad3b9c8 | ||
|
|
cf879f9527 | ||
|
|
04c658f1a0 | ||
|
|
2ab1a174db | ||
|
|
dfca2af997 | ||
|
|
0a7d15b48c | ||
|
|
518bc72f90 | ||
|
|
3c65209ce9 | ||
|
|
174535b05d | ||
|
|
fff54fe7f3 | ||
|
|
0859d230b0 | ||
|
|
02998c5467 | ||
|
|
3f38c42852 | ||
|
|
49295661fd | ||
|
|
e4301f8d93 | ||
|
|
c6586f8df2 | ||
|
|
8e81008cdc | ||
|
|
e33ad809d6 | ||
|
|
d804043a18 | ||
|
|
0fb0df7056 | ||
|
|
73e90e0eaa | ||
|
|
44ef1ac9a6 | ||
|
|
aeac7f2c8b | ||
|
|
39ef172b87 | ||
|
|
0df85cc3d9 | ||
|
|
f0f4f082ae | ||
|
|
b29bd993d4 | ||
|
|
127eaf69b6 | ||
|
|
36b0289bc6 | ||
|
|
0abd0be725 | ||
|
|
918a2b1533 | ||
|
|
88a17cd227 | ||
|
|
b60387accb | ||
|
|
049177024b | ||
|
|
9c63638af1 | ||
|
|
67dfe664a6 | ||
|
|
4efcdb3e01 | ||
|
|
ddc2cfacb9 | ||
|
|
337729529a | ||
|
|
4c4cc362b3 | ||
|
|
4e0aca16c2 | ||
|
|
749a426a71 | ||
|
|
b859327b8a | ||
|
|
3e8fc59213 | ||
|
|
462e02140d | ||
|
|
6b41df2d89 | ||
|
|
eb5ed5c0dd | ||
|
|
e0bbacf013 | ||
|
|
34af7f8bfa | ||
|
|
b569c21fec | ||
|
|
c4a5c059e3 | ||
|
|
c21ed90da0 | ||
|
|
9b58277945 | ||
|
|
5a4a42aeb8 | ||
|
|
9476472bf6 | ||
|
|
2ce9c3cc81 | ||
|
|
0a8bfc2725 | ||
|
|
c5bbb6b632 | ||
|
|
f497a8dbcc | ||
|
|
4290081486 | ||
|
|
ccda652e69 | ||
|
|
2982d809ab | ||
|
|
7ad4a3dffc | ||
|
|
c0ef53e542 | ||
|
|
b7d1c84cd0 | ||
|
|
111bfe5d2e | ||
|
|
6e59aa14b0 | ||
|
|
a4cf77422f | ||
|
|
35df2a0505 | ||
|
|
9f445686a4 | ||
|
|
0fc935e996 | ||
|
|
adb08a60cf | ||
|
|
e3576e8a85 | ||
|
|
9c065aed4e | ||
|
|
7abb092211 | ||
|
|
937bfb4c78 | ||
|
|
1bcdc54b68 | ||
|
|
eb58314c53 | ||
|
|
c158e6ec73 | ||
|
|
5ae587ee81 | ||
|
|
d40fa46851 | ||
|
|
19a31686da | ||
|
|
13f7e07128 | ||
|
|
0e3691fdbd | ||
|
|
e359b5c75e | ||
|
|
3b3bd3dea4 | ||
|
|
8f36b7ea84 | ||
|
|
569d99512c | ||
|
|
ac84553a68 | ||
|
|
610db7827d | ||
|
|
088b55c9ed | ||
|
|
c800e29900 | ||
|
|
14435db0d8 | ||
|
|
bd6402562e | ||
|
|
d16ad11136 | ||
|
|
6c27e4177d | ||
|
|
bebf83f06c | ||
|
|
07bf741b15 | ||
|
|
5e5851029d | ||
|
|
e6020850fc | ||
|
|
80183f61e8 | ||
|
|
1b9432ff37 | ||
|
|
f1f813269c | ||
|
|
a23f390402 | ||
|
|
2950ce0c17 | ||
|
|
514c4909a4 | ||
|
|
99cadf7652 | ||
|
|
4ca36d64a8 | ||
|
|
863009dcaa | ||
|
|
2ef5ccc2fd | ||
|
|
744583b4e7 | ||
|
|
b36032e22c | ||
|
|
ac7901abba | ||
|
|
dff2496d73 | ||
|
|
d97d36bb9e | ||
|
|
b0d2cb93e1 | ||
|
|
f98d78c356 | ||
|
|
d85226dc79 | ||
|
|
1c2b6095c9 | ||
|
|
5f8c8f4525 | ||
|
|
6a49e99a2c | ||
|
|
6062031de4 | ||
|
|
fb2b58110d | ||
|
|
c385662783 | ||
|
|
4a05188a7f | ||
|
|
1454c4ebc5 | ||
|
|
4b1c76e972 | ||
|
|
f1f5d323e8 | ||
|
|
e37f2d3222 | ||
|
|
4f2f855c04 | ||
|
|
dcab4e6f9c | ||
|
|
e703055793 | ||
|
|
7efe1d60d5 | ||
|
|
1d84284b6d | ||
|
|
8404b33232 | ||
|
|
41d39dfaa8 | ||
|
|
761eb5f384 | ||
|
|
e72f67ca54 | ||
|
|
b8df15171e | ||
|
|
5f531f2de1 | ||
|
|
8335238eb3 | ||
|
|
aaf68ecb21 | ||
|
|
db6781f311 | ||
|
|
a85b02c6ab | ||
|
|
19a832cad8 | ||
|
|
f0dd6152fd | ||
|
|
decac2ef74 | ||
|
|
c3ce1da0d6 | ||
|
|
02bc488c1c | ||
|
|
b22f859c38 | ||
|
|
98869c7169 | ||
|
|
150c89db72 | ||
|
|
fe0a8375a3 | ||
|
|
80cfbefd75 | ||
|
|
f2ee18235f | ||
|
|
f48df1e5c0 | ||
|
|
b24855082e | ||
|
|
27434f3235 | ||
|
|
cdb6eac0e6 | ||
|
|
9e13513205 | ||
|
|
b09f52357c | ||
|
|
ac08e86747 | ||
|
|
c9c8abe97b | ||
|
|
0b8beafc89 | ||
|
|
8b6e3491c4 | ||
|
|
6cf2b56f9b | ||
|
|
19b95829e0 | ||
|
|
4bea427c79 | ||
|
|
da7e4d51d6 | ||
|
|
43713fbdf8 | ||
|
|
dc29e858c9 | ||
|
|
c30c6f08f3 | ||
|
|
7c892ac051 | ||
|
|
75dd7b93f5 | ||
|
|
d5de8e1bf3 | ||
|
|
16b4795956 | ||
|
|
e5835c299c | ||
|
|
4fdef3cfde | ||
|
|
cf6a8bd463 | ||
|
|
59a84e844c | ||
|
|
6b0c9a5fad | ||
|
|
77edea5419 | ||
|
|
521870df0a | ||
|
|
dc1c1eb998 | ||
|
|
e78427245a | ||
|
|
fbcab5bc52 | ||
|
|
89c79c3ec3 | ||
|
|
566cd9e9c4 | ||
|
|
ad78cec7c7 | ||
|
|
176ab5f48e | ||
|
|
9c4fa23931 | ||
|
|
2938694c45 | ||
|
|
88d0fb9753 | ||
|
|
d23a7f81ef | ||
|
|
2e363445fc | ||
|
|
33d983bc20 | ||
|
|
3e7c7831bc | ||
|
|
374d49eb92 | ||
|
|
663cf5649f | ||
|
|
095ebccbb0 | ||
|
|
f4bb6b0517 | ||
|
|
575234ac37 | ||
|
|
16cbd86371 | ||
|
|
2d3666e339 | ||
|
|
47987b7785 | ||
|
|
2e1461e6dc | ||
|
|
272457740f | ||
|
|
58c721e7d2 | ||
|
|
b4baf35ed8 | ||
|
|
2001d96148 | ||
|
|
5c390341fb | ||
|
|
c8b1b162ff | ||
|
|
a55645584b | ||
|
|
f536307914 | ||
|
|
4494ffabb6 | ||
|
|
2dc59a601c | ||
|
|
4ad04e2032 | ||
|
|
f2ebe128cc | ||
|
|
f0165c1ad8 | ||
|
|
898f80cb30 | ||
|
|
5d7de0858c | ||
|
|
c0b88fe736 | ||
|
|
fa43248e30 | ||
|
|
cb3da25bc8 | ||
|
|
a40058bb0b | ||
|
|
6ab3bbe7bd | ||
|
|
9e73c82eb3 | ||
|
|
6b3b1b6cbc | ||
|
|
b3b433f84b | ||
|
|
7f16a53309 | ||
|
|
2471bda211 | ||
|
|
cd49971535 | ||
|
|
0013f8989b | ||
|
|
de8c80597f | ||
|
|
9dcc55ea1b | ||
|
|
8255390131 | ||
|
|
438a9684ee | ||
|
|
a6000f22a2 | ||
|
|
2ec5eeb442 | ||
|
|
d319476eb6 | ||
|
|
93d52bc86c | ||
|
|
bda5c2c915 | ||
|
|
b838fe2e74 | ||
|
|
604b9be4a0 | ||
|
|
7b8ef98846 | ||
|
|
2c7c6c260a | ||
|
|
b8c3555b09 | ||
|
|
2d2b30daf1 | ||
|
|
3e3ed4ed52 | ||
|
|
d8d9c64847 | ||
|
|
c489673130 | ||
|
|
2544305fb9 | ||
|
|
519d228db2 | ||
|
|
4cd89f4379 | ||
|
|
fdfc29f6cd | ||
|
|
4ec104c5ee | ||
|
|
bae89272b0 | ||
|
|
8408a45eff | ||
|
|
a37b1bde4c | ||
|
|
953b5d3dea | ||
|
|
c7906e8598 | ||
|
|
e1bc43da5f |
4
.env.example
Normal file
@@ -0,0 +1,4 @@
|
||||
XUI_DEBUG=true
|
||||
XUI_DB_FOLDER=x-ui
|
||||
XUI_LOG_FOLDER=x-ui
|
||||
XUI_BIN_FOLDER=x-ui
|
||||
14
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: MHSanaei
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: mhsanaei
|
||||
custom: https://nowpayments.io/donation/hsanaei
|
||||
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,24 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Version (please complete the following information):**
|
||||
- 3X-UI Version : [e.g. 2.3.5]
|
||||
- Xray Version : [e.g. 1.8.13]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
77
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
name: Bug report
|
||||
description: Create a report to help us improve
|
||||
title: "Bug report"
|
||||
labels: ["bug"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for reporting a bug! Please fill out the following information.
|
||||
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is.
|
||||
placeholder: My problem is...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: how-repeat-problem
|
||||
attributes:
|
||||
label: How to repeat the problem?
|
||||
description: Sequence of actions that allow you to reproduce the bug
|
||||
placeholder: |
|
||||
1. Open `Inbounds` page
|
||||
2. ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-action
|
||||
attributes:
|
||||
label: Expected action
|
||||
description: What's going to happen
|
||||
placeholder: Must be...
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: received-action
|
||||
attributes:
|
||||
label: Received action
|
||||
description: What's really happening
|
||||
placeholder: It's actually happening...
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: xui-version
|
||||
attributes:
|
||||
label: 3x-ui Version
|
||||
description: Which version of 3x-ui are you using?
|
||||
placeholder: 2.X.X
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: xray-version
|
||||
attributes:
|
||||
label: Xray-core Version
|
||||
description: Which version of Xray-core are you using?
|
||||
placeholder: 2.X.X
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: Please check all the checkboxes
|
||||
options:
|
||||
- label: This bug report is written entirely in English.
|
||||
required: true
|
||||
- label: This bug report is new and no one has reported it before me.
|
||||
required: true
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
39
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Feature request
|
||||
description: Suggest an idea for this project
|
||||
title: "Feature request"
|
||||
labels: ["enhancement"]
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: is-related-problem
|
||||
attributes:
|
||||
label: Is your feature request related to a problem?
|
||||
description: A clear and concise description of what the problem is.
|
||||
placeholder: I'm always frustrated when...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: Please check all the checkboxes
|
||||
options:
|
||||
- label: This feature report is written entirely in English.
|
||||
required: true
|
||||
10
.github/ISSUE_TEMPLATE/question-.md
vendored
@@ -1,10 +0,0 @@
|
||||
---
|
||||
name: 'Question '
|
||||
about: Describe this issue template's purpose here.
|
||||
title: ''
|
||||
labels: question
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
22
.github/ISSUE_TEMPLATE/question.yaml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: Question
|
||||
description: Describe this issue template's purpose here.
|
||||
title: "Question"
|
||||
labels: ["question"]
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: Question
|
||||
placeholder: I have a question, ..., how can I solve it?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: Please check all the checkboxes
|
||||
options:
|
||||
- label: This question is written entirely in English.
|
||||
required: true
|
||||
10
.github/dependabot.yml
vendored
@@ -1,10 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
20
.github/pull_request_template.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
## What is the pull request?
|
||||
|
||||
<!-- Briefly describe the changes introduced by this pull request -->
|
||||
|
||||
## Which part of the application is affected by the change?
|
||||
|
||||
- [ ] Frontend
|
||||
- [ ] Backend
|
||||
|
||||
## Type of Changes
|
||||
|
||||
- [ ] Bug fix
|
||||
- [ ] New feature
|
||||
- [ ] Refactoring
|
||||
- [ ] Other
|
||||
|
||||
## Screenshots
|
||||
|
||||
<!-- Add screenshots to illustrate the changes -->
|
||||
<!-- Remove this section if it is not applicable. -->
|
||||
49
.github/workflows/docker.yml
vendored
@@ -1,22 +1,49 @@
|
||||
name: Release 3X-UI for Docker
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
workflow_dispatch:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
build_and_push:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
hsanaeii/3x-ui
|
||||
ghcr.io/mhsanaei/3x-ui
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=semver,pattern={{version}}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
install: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
@@ -25,17 +52,11 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
platforms: linux/amd64, linux/arm64/v8, linux/arm/v7, linux/arm/v6, linux/386
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6,linux/386
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
210
.github/workflows/release.yml
vendored
@@ -1,13 +1,30 @@
|
||||
name: Release 3X-UI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
paths:
|
||||
- '**.js'
|
||||
- '**.css'
|
||||
- '**.html'
|
||||
- '**.sh'
|
||||
- '**.go'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'x-ui.service.debian'
|
||||
- 'x-ui.service.arch'
|
||||
- 'x-ui.service.rhel'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
@@ -18,108 +35,97 @@ jobs:
|
||||
- 386
|
||||
- armv5
|
||||
- s390x
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
if [ "${{ matrix.platform }}" == "arm64" ]; then
|
||||
sudo apt install gcc-aarch64-linux-gnu
|
||||
elif [ "${{ matrix.platform }}" == "armv7" ]; then
|
||||
sudo apt install gcc-arm-linux-gnueabihf
|
||||
elif [ "${{ matrix.platform }}" == "armv6" ]; then
|
||||
sudo apt install gcc-arm-linux-gnueabihf
|
||||
elif [ "${{ matrix.platform }}" == "386" ]; then
|
||||
sudo apt install gcc-i686-linux-gnu
|
||||
elif [ "${{ matrix.platform }}" == "armv5" ]; then
|
||||
sudo apt install gcc-arm-linux-gnueabi
|
||||
elif [ "${{ matrix.platform }}" == "s390x" ]; then
|
||||
sudo apt install gcc-s390x-linux-gnu
|
||||
fi
|
||||
|
||||
- name: Build x-ui
|
||||
- name: Build 3X-UI
|
||||
run: |
|
||||
export CGO_ENABLED=1
|
||||
export GOOS=linux
|
||||
export GOARCH=${{ matrix.platform }}
|
||||
if [ "${{ matrix.platform }}" == "arm64" ]; then
|
||||
export GOARCH=arm64
|
||||
export CC=aarch64-linux-gnu-gcc
|
||||
elif [ "${{ matrix.platform }}" == "armv7" ]; then
|
||||
export GOARCH=arm
|
||||
export GOARM=7
|
||||
export CC=arm-linux-gnueabihf-gcc
|
||||
elif [ "${{ matrix.platform }}" == "armv6" ]; then
|
||||
export GOARCH=arm
|
||||
export GOARM=6
|
||||
export CC=arm-linux-gnueabihf-gcc
|
||||
elif [ "${{ matrix.platform }}" == "386" ]; then
|
||||
export GOARCH=386
|
||||
export CC=i686-linux-gnu-gcc
|
||||
elif [ "${{ matrix.platform }}" == "armv5" ]; then
|
||||
export GOARCH=arm
|
||||
export GOARM=5
|
||||
export CC=arm-linux-gnueabi-gcc
|
||||
elif [ "${{ matrix.platform }}" == "s390x" ]; then
|
||||
export GOARCH=s390x
|
||||
export CC=s390x-linux-gnu-gcc
|
||||
fi
|
||||
go build -o xui-release -v main.go
|
||||
# Use Bootlin prebuilt cross-toolchains (musl 1.2.5 in stable series)
|
||||
case "${{ matrix.platform }}" in
|
||||
amd64) BOOTLIN_ARCH="x86-64" ;;
|
||||
arm64) BOOTLIN_ARCH="aarch64" ;;
|
||||
armv7) BOOTLIN_ARCH="armv7-eabihf"; export GOARCH=arm GOARM=7 ;;
|
||||
armv6) BOOTLIN_ARCH="armv6-eabihf"; export GOARCH=arm GOARM=6 ;;
|
||||
armv5) BOOTLIN_ARCH="armv5-eabi"; export GOARCH=arm GOARM=5 ;;
|
||||
386) BOOTLIN_ARCH="x86-i686" ;;
|
||||
s390x) BOOTLIN_ARCH="s390x-z13" ;;
|
||||
esac
|
||||
echo "Resolving Bootlin musl toolchain for arch=$BOOTLIN_ARCH (platform=${{ matrix.platform }})"
|
||||
TARBALL_BASE="https://toolchains.bootlin.com/downloads/releases/toolchains/$BOOTLIN_ARCH/tarballs/"
|
||||
TARBALL_URL=$(curl -fsSL "$TARBALL_BASE" | grep -oE "${BOOTLIN_ARCH}--musl--stable-[^\"]+\\.tar\\.xz" | sort -r | head -n1)
|
||||
[ -z "$TARBALL_URL" ] && { echo "Failed to locate Bootlin musl toolchain for arch=$BOOTLIN_ARCH" >&2; exit 1; }
|
||||
echo "Downloading: $TARBALL_URL"
|
||||
cd /tmp
|
||||
curl -fL -sS -o "$(basename "$TARBALL_URL")" "$TARBALL_BASE/$TARBALL_URL"
|
||||
tar -xf "$(basename "$TARBALL_URL")"
|
||||
TOOLCHAIN_DIR=$(find . -maxdepth 1 -type d -name "${BOOTLIN_ARCH}--musl--stable-*" | head -n1)
|
||||
export PATH="$(realpath "$TOOLCHAIN_DIR")/bin:$PATH"
|
||||
export CC=$(realpath "$(find "$TOOLCHAIN_DIR/bin" -name '*-gcc.br_real' -type f -executable | head -n1)")
|
||||
[ -z "$CC" ] && { echo "No gcc.br_real found in $TOOLCHAIN_DIR/bin" >&2; exit 1; }
|
||||
cd -
|
||||
go build -ldflags "-w -s -linkmode external -extldflags '-static'" -o xui-release -v main.go
|
||||
file xui-release
|
||||
ldd xui-release || echo "Static binary confirmed"
|
||||
|
||||
mkdir x-ui
|
||||
cp xui-release x-ui/
|
||||
cp x-ui.service x-ui/
|
||||
cp x-ui.service.debian x-ui/
|
||||
cp x-ui.service.arch x-ui/
|
||||
cp x-ui.service.rhel x-ui/
|
||||
cp x-ui.sh x-ui/
|
||||
mv x-ui/xui-release x-ui/x-ui
|
||||
mkdir x-ui/bin
|
||||
cd x-ui/bin
|
||||
|
||||
# Download dependencies
|
||||
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v1.8.21/"
|
||||
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v26.1.18/"
|
||||
if [ "${{ matrix.platform }}" == "amd64" ]; then
|
||||
wget ${Xray_URL}Xray-linux-64.zip
|
||||
wget -q ${Xray_URL}Xray-linux-64.zip
|
||||
unzip Xray-linux-64.zip
|
||||
rm -f Xray-linux-64.zip
|
||||
elif [ "${{ matrix.platform }}" == "arm64" ]; then
|
||||
wget ${Xray_URL}Xray-linux-arm64-v8a.zip
|
||||
wget -q ${Xray_URL}Xray-linux-arm64-v8a.zip
|
||||
unzip Xray-linux-arm64-v8a.zip
|
||||
rm -f Xray-linux-arm64-v8a.zip
|
||||
elif [ "${{ matrix.platform }}" == "armv7" ]; then
|
||||
wget ${Xray_URL}Xray-linux-arm32-v7a.zip
|
||||
wget -q ${Xray_URL}Xray-linux-arm32-v7a.zip
|
||||
unzip Xray-linux-arm32-v7a.zip
|
||||
rm -f Xray-linux-arm32-v7a.zip
|
||||
elif [ "${{ matrix.platform }}" == "armv6" ]; then
|
||||
wget ${Xray_URL}Xray-linux-arm32-v6.zip
|
||||
wget -q ${Xray_URL}Xray-linux-arm32-v6.zip
|
||||
unzip Xray-linux-arm32-v6.zip
|
||||
rm -f Xray-linux-arm32-v6.zip
|
||||
elif [ "${{ matrix.platform }}" == "386" ]; then
|
||||
wget ${Xray_URL}Xray-linux-32.zip
|
||||
wget -q ${Xray_URL}Xray-linux-32.zip
|
||||
unzip Xray-linux-32.zip
|
||||
rm -f Xray-linux-32.zip
|
||||
elif [ "${{ matrix.platform }}" == "armv5" ]; then
|
||||
wget ${Xray_URL}Xray-linux-arm32-v5.zip
|
||||
wget -q ${Xray_URL}Xray-linux-arm32-v5.zip
|
||||
unzip Xray-linux-arm32-v5.zip
|
||||
rm -f Xray-linux-arm32-v5.zip
|
||||
elif [ "${{ matrix.platform }}" == "s390x" ]; then
|
||||
wget ${Xray_URL}Xray-linux-s390x.zip
|
||||
wget -q ${Xray_URL}Xray-linux-s390x.zip
|
||||
unzip Xray-linux-s390x.zip
|
||||
rm -f Xray-linux-s390x.zip
|
||||
fi
|
||||
rm -f geoip.dat geosite.dat
|
||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
wget -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
|
||||
wget -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
|
||||
wget -O geoip_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geoip.dat
|
||||
wget -O geosite_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geosite.dat
|
||||
wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
wget -q -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
|
||||
wget -q -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
|
||||
wget -q -O geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
wget -q -O geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
mv xray xray-linux-${{ matrix.platform }}
|
||||
cd ../..
|
||||
|
||||
@@ -134,9 +140,89 @@ jobs:
|
||||
|
||||
- name: Upload files to GH release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
if: |
|
||||
(github.event_name == 'release' && github.event.action == 'published') ||
|
||||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref }}
|
||||
file: x-ui-linux-${{ matrix.platform }}.tar.gz
|
||||
asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
|
||||
overwrite: true
|
||||
prerelease: true
|
||||
|
||||
# =================================
|
||||
# Windows Build
|
||||
# =================================
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
- amd64
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
- name: Build 3X-UI for Windows
|
||||
shell: pwsh
|
||||
run: |
|
||||
$env:CGO_ENABLED="1"
|
||||
$env:GOOS="windows"
|
||||
$env:GOARCH="amd64"
|
||||
go build -ldflags "-w -s" -o xui-release.exe -v main.go
|
||||
|
||||
mkdir x-ui
|
||||
Copy-Item xui-release.exe x-ui\
|
||||
mkdir x-ui\bin
|
||||
cd x-ui\bin
|
||||
|
||||
# Download Xray for Windows
|
||||
$Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v26.1.18/"
|
||||
Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip"
|
||||
Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath .
|
||||
Remove-Item "Xray-windows-64.zip"
|
||||
Remove-Item geoip.dat, geosite.dat -ErrorAction SilentlyContinue
|
||||
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip.dat"
|
||||
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite.dat"
|
||||
Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat" -OutFile "geoip_IR.dat"
|
||||
Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat" -OutFile "geosite_IR.dat"
|
||||
Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip_RU.dat"
|
||||
Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite_RU.dat"
|
||||
Rename-Item xray.exe xray-windows-amd64.exe
|
||||
cd ..
|
||||
Copy-Item -Path ..\windows_files\* -Destination . -Recurse
|
||||
cd ..
|
||||
|
||||
- name: Package to Zip
|
||||
shell: pwsh
|
||||
run: |
|
||||
Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip"
|
||||
|
||||
- name: Upload files to Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: x-ui-windows-amd64
|
||||
path: ./x-ui-windows-amd64.zip
|
||||
|
||||
- name: Upload files to GH release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
if: |
|
||||
(github.event_name == 'release' && github.event.action == 'published') ||
|
||||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref }}
|
||||
file: x-ui-windows-amd64.zip
|
||||
asset_name: x-ui-windows-amd64.zip
|
||||
overwrite: true
|
||||
prerelease: true
|
||||
|
||||
40
.gitignore
vendored
@@ -1,16 +1,40 @@
|
||||
.idea
|
||||
.vscode
|
||||
.cache
|
||||
# Ignore editor and IDE settings
|
||||
.idea/
|
||||
.vscode/
|
||||
.cache/
|
||||
.sync*
|
||||
*.tar.gz
|
||||
|
||||
# Ignore log files
|
||||
*.log
|
||||
access.log
|
||||
error.log
|
||||
tmp
|
||||
main
|
||||
|
||||
# Ignore temporary files
|
||||
tmp/
|
||||
*.tar.gz
|
||||
|
||||
# Ignore build and distribution directories
|
||||
backup/
|
||||
bin/
|
||||
dist/
|
||||
release/
|
||||
node_modules/
|
||||
|
||||
# Ignore compiled binaries
|
||||
main
|
||||
|
||||
# Ignore script and executable files
|
||||
/release.sh
|
||||
/x-ui
|
||||
|
||||
# Ignore OS specific files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Ignore Go build files
|
||||
*.exe
|
||||
x-ui.db
|
||||
|
||||
# Ignore Docker specific files
|
||||
docker-compose.override.yml
|
||||
|
||||
# Ignore .env (Environment Variables) file
|
||||
.env
|
||||
35
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"$schema": "vscode://schemas/launch",
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run 3x-ui (Debug)",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
"XUI_DEBUG": "true"
|
||||
},
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
"name": "Run 3x-ui (Debug, custom env)",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
// Set to true to serve assets/templates directly from disk for development
|
||||
"XUI_DEBUG": "true",
|
||||
// Uncomment to override DB folder location (by default uses working dir on Windows when debug)
|
||||
// "XUI_DB_FOLDER": "${workspaceFolder}",
|
||||
// Example: override log level (debug|info|notice|warn|error)
|
||||
// "XUI_LOG_LEVEL": "debug"
|
||||
},
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
75
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "go: build",
|
||||
"type": "shell",
|
||||
"command": "go",
|
||||
"args": [
|
||||
"build",
|
||||
"-o",
|
||||
"bin/3x-ui.exe",
|
||||
"./main.go"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$go"
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "go: run",
|
||||
"type": "shell",
|
||||
"command": "go",
|
||||
"args": [
|
||||
"run",
|
||||
"./main.go"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
"XUI_DEBUG": "true"
|
||||
}
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$go"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "go: test",
|
||||
"type": "shell",
|
||||
"command": "go",
|
||||
"args": [
|
||||
"test",
|
||||
"./..."
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$go"
|
||||
],
|
||||
"group": "test"
|
||||
},
|
||||
{
|
||||
"label": "go: vet",
|
||||
"type": "shell",
|
||||
"command": "go",
|
||||
"args": [
|
||||
"vet",
|
||||
"./..."
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$go"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
5
CONTRIBUTING.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## Local Development Setup
|
||||
|
||||
- Create a directory named `x-ui` in the project root
|
||||
- Rename `.env.example` to `.env `
|
||||
- Run `main.go`
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Start fail2ban
|
||||
fail2ban-client -x start
|
||||
[ $XUI_ENABLE_FAIL2BAN == "true" ] && fail2ban-client -x start
|
||||
|
||||
# Run x-ui
|
||||
exec /app/x-ui
|
||||
|
||||
@@ -27,14 +27,14 @@ case $1 in
|
||||
esac
|
||||
mkdir -p build/bin
|
||||
cd build/bin
|
||||
wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.21/Xray-linux-${ARCH}.zip"
|
||||
curl -sfLRO "https://github.com/XTLS/Xray-core/releases/download/v26.1.18/Xray-linux-${ARCH}.zip"
|
||||
unzip "Xray-linux-${ARCH}.zip"
|
||||
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
|
||||
mv xray "xray-linux-${FNAME}"
|
||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
wget -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
|
||||
wget -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
|
||||
wget -O geoip_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geoip.dat
|
||||
wget -O geosite_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geosite.dat
|
||||
curl -sfLRO https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
curl -sfLRO https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
curl -sfLRo geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
|
||||
curl -sfLRo geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
|
||||
curl -sfLRo geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
curl -sfLRo geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
cd ../../
|
||||
11
Dockerfile
@@ -1,21 +1,21 @@
|
||||
# ========================================================
|
||||
# Stage: Builder
|
||||
# ========================================================
|
||||
FROM golang:1.22-alpine AS builder
|
||||
FROM golang:1.25-alpine AS builder
|
||||
WORKDIR /app
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN apk --no-cache --update add \
|
||||
build-base \
|
||||
gcc \
|
||||
wget \
|
||||
curl \
|
||||
unzip
|
||||
|
||||
COPY . .
|
||||
|
||||
ENV CGO_ENABLED=1
|
||||
ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
|
||||
RUN go build -o build/x-ui main.go
|
||||
RUN go build -ldflags "-w -s" -o build/x-ui main.go
|
||||
RUN ./DockerInit.sh "$TARGETARCH"
|
||||
|
||||
# ========================================================
|
||||
@@ -29,7 +29,8 @@ RUN apk add --no-cache --update \
|
||||
ca-certificates \
|
||||
tzdata \
|
||||
fail2ban \
|
||||
bash
|
||||
bash \
|
||||
curl
|
||||
|
||||
COPY --from=builder /app/build/ /app/
|
||||
COPY --from=builder /app/DockerEntrypoint.sh /app/
|
||||
@@ -48,6 +49,8 @@ RUN chmod +x \
|
||||
/app/x-ui \
|
||||
/usr/bin/x-ui
|
||||
|
||||
ENV XUI_ENABLE_FAIL2BAN="true"
|
||||
EXPOSE 2053
|
||||
VOLUME [ "/etc/x-ui" ]
|
||||
CMD [ "./x-ui" ]
|
||||
ENTRYPOINT [ "/app/DockerEntrypoint.sh" ]
|
||||
|
||||
56
README.ar_EG.md
Normal file
@@ -0,0 +1,56 @@
|
||||
[English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/3x-ui-dark.png">
|
||||
<img alt="3x-ui" src="./media/3x-ui-light.png">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||
|
||||
**3X-UI** — لوحة تحكم متقدمة مفتوحة المصدر تعتمد على الويب مصممة لإدارة خادم Xray-core. توفر واجهة سهلة الاستخدام لتكوين ومراقبة بروتوكولات VPN والوكيل المختلفة.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> هذا المشروع مخصص للاستخدام الشخصي والاتصال فقط، يرجى عدم استخدامه لأغراض غير قانونية، يرجى عدم استخدامه في بيئة الإنتاج.
|
||||
|
||||
كمشروع محسن من مشروع X-UI الأصلي، يوفر 3X-UI استقرارًا محسنًا ودعمًا أوسع للبروتوكولات وميزات إضافية.
|
||||
|
||||
## البدء السريع
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
|
||||
```
|
||||
|
||||
للحصول على الوثائق الكاملة، يرجى زيارة [ويكي المشروع](https://github.com/MHSanaei/3x-ui/wiki).
|
||||
|
||||
## شكر خاص إلى
|
||||
|
||||
- [alireza0](https://github.com/alireza0/)
|
||||
|
||||
## الاعتراف
|
||||
|
||||
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (الترخيص: **GPL-3.0**): _قواعد توجيه v2ray/xray و v2ray/xray-clients المحسنة مع النطاقات الإيرانية المدمجة وتركيز على الأمان وحظر الإعلانات._
|
||||
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (الترخيص: **GPL-3.0**): _يحتوي هذا المستودع على قواعد توجيه V2Ray محدثة تلقائيًا بناءً على بيانات النطاقات والعناوين المحظورة في روسيا._
|
||||
|
||||
## دعم المشروع
|
||||
|
||||
**إذا كان هذا المشروع مفيدًا لك، فقد ترغب في إعطائه**:star2:
|
||||
|
||||
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||
</a>
|
||||
</br>
|
||||
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||
</a>
|
||||
|
||||
## النجوم عبر الزمن
|
||||
|
||||
[](https://starchart.cc/MHSanaei/3x-ui)
|
||||
490
README.es_ES.md
@@ -1,475 +1,57 @@
|
||||
[English](/README.md) | [Chinese](/README.zh.md) | [Español](/README.es_ES.md)
|
||||
[English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
|
||||
|
||||
<p align="center"><a href="#"><img src="./media/3X-UI.png" alt="Image"></a></p>
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/3x-ui-dark.png">
|
||||
<img alt="3x-ui" src="./media/3x-ui-light.png">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
**Un Panel Web Avanzado • Construido sobre Xray Core**
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||
|
||||
> **Descargo de responsabilidad:** Este proyecto es solo para aprendizaje personal y comunicación, por favor no lo uses con fines ilegales, por favor no lo uses en un entorno de producción
|
||||
**3X-UI** — panel de control avanzado basado en web de código abierto diseñado para gestionar el servidor Xray-core. Ofrece una interfaz fácil de usar para configurar y monitorear varios protocolos VPN y proxy.
|
||||
|
||||
**Si este proyecto te es útil, podrías considerar darle una**:star2:
|
||||
> [!IMPORTANT]
|
||||
> Este proyecto es solo para uso personal y comunicación, por favor no lo use para fines ilegales, por favor no lo use en un entorno de producción.
|
||||
|
||||
<p align="left"><a href="#"><img width="125" src="https://github.com/MHSanaei/3x-ui/assets/115543613/7aa895dd-048a-42e7-989b-afd41a74e2e1" alt="Image"></a></p>
|
||||
Como una versión mejorada del proyecto X-UI original, 3X-UI proporciona mayor estabilidad, soporte más amplio de protocolos y características adicionales.
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
|
||||
## Instalar y Actualizar
|
||||
## Inicio Rápido
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
|
||||
```
|
||||
|
||||
## Instalar una Versión Personalizada
|
||||
Para documentación completa, visita la [Wiki del proyecto](https://github.com/MHSanaei/3x-ui/wiki).
|
||||
|
||||
Para instalar la versión deseada, agrega la versión al final del comando de instalación. Por ejemplo, ver `v2.3.6`:
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.3.6
|
||||
```
|
||||
|
||||
## Certificado SSL
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para el Certificado SSL</summary>
|
||||
|
||||
### Cloudflare
|
||||
|
||||
El script de gestión tiene una aplicación de certificado SSL incorporada para Cloudflare. Para usar este script para colocar un certificado, necesitas lo siguiente:
|
||||
|
||||
- Correo electrónico registrado en Cloudflare
|
||||
- Clave Global de API de Cloudflare
|
||||
- El nombre de dominio se ha resuelto en el servidor actual a través de Cloudflare
|
||||
|
||||
**1:** Ejecuta el comando`x-ui`en la terminal, luego elige `Certificado SSL de Cloudflare`.
|
||||
|
||||
|
||||
### Certbot
|
||||
```
|
||||
apt-get install certbot -y
|
||||
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
|
||||
certbot renew --dry-run
|
||||
```
|
||||
|
||||
***Consejo:*** *Certbot también está integrado en el script de gestión. Puedes ejecutar el comando `x-ui` , luego elegir `Gestión de Certificados SSL`.*
|
||||
|
||||
</details>
|
||||
|
||||
## Instalación y Actualización Manual
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para más detalles de la instalación manual</summary>
|
||||
|
||||
#### Uso
|
||||
|
||||
1. Para descargar la última versión del paquete comprimido directamente en tu servidor, ejecuta el siguiente comando:
|
||||
|
||||
```sh
|
||||
ARCH=$(uname -m)
|
||||
case "${ARCH}" in
|
||||
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
|
||||
i*86 | x86) XUI_ARCH="386" ;;
|
||||
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
|
||||
armv7* | armv7) XUI_ARCH="armv7" ;;
|
||||
armv6* | armv6) XUI_ARCH="armv6" ;;
|
||||
armv5* | armv5) XUI_ARCH="armv5" ;;
|
||||
*) XUI_ARCH="amd64" ;;
|
||||
esac
|
||||
|
||||
|
||||
wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz
|
||||
```
|
||||
|
||||
2. Una vez que se haya descargado el paquete comprimido, ejecuta los siguientes comandos para instalar o actualizar x-ui:
|
||||
|
||||
```sh
|
||||
ARCH=$(uname -m)
|
||||
case "${ARCH}" in
|
||||
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
|
||||
i*86 | x86) XUI_ARCH="386" ;;
|
||||
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
|
||||
armv7* | armv7) XUI_ARCH="armv7" ;;
|
||||
armv6* | armv6) XUI_ARCH="armv6" ;;
|
||||
armv5* | armv5) XUI_ARCH="armv5" ;;
|
||||
*) XUI_ARCH="amd64" ;;
|
||||
esac
|
||||
|
||||
cd /root/
|
||||
rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui
|
||||
tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz
|
||||
chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh
|
||||
cp x-ui/x-ui.sh /usr/bin/x-ui
|
||||
cp -f x-ui/x-ui.service /etc/systemd/system/
|
||||
mv x-ui/ /usr/local/
|
||||
systemctl daemon-reload
|
||||
systemctl enable x-ui
|
||||
systemctl restart x-ui
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Instalar con Docker
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para más detalles del Docker</summary>
|
||||
|
||||
#### Uso
|
||||
|
||||
1. Instala Docker:
|
||||
|
||||
```sh
|
||||
bash <(curl -sSL https://get.docker.com)
|
||||
```
|
||||
|
||||
2. Clona el Repositorio del Proyecto:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/MHSanaei/3x-ui.git
|
||||
cd 3x-ui
|
||||
```
|
||||
|
||||
3. Inicia el Servicio
|
||||
|
||||
```sh
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
O tambien
|
||||
|
||||
```sh
|
||||
docker run -itd \
|
||||
-e XRAY_VMESS_AEAD_FORCED=false \
|
||||
-v $PWD/db/:/etc/x-ui/ \
|
||||
-v $PWD/cert/:/root/cert/ \
|
||||
--network=host \
|
||||
--restart=unless-stopped \
|
||||
--name 3x-ui \
|
||||
ghcr.io/mhsanaei/3x-ui:latest
|
||||
```
|
||||
|
||||
actualizar a la última versión
|
||||
|
||||
```sh
|
||||
cd 3x-ui
|
||||
docker compose down
|
||||
docker compose pull 3x-ui
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
eliminar 3x-ui de docker
|
||||
|
||||
```sh
|
||||
docker stop 3x-ui
|
||||
docker rm 3x-ui
|
||||
cd --
|
||||
rm -r 3x-ui
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## SO Recomendados
|
||||
|
||||
- Ubuntu 20.04+
|
||||
- Debian 11+
|
||||
- CentOS 8+
|
||||
- Fedora 36+
|
||||
- Arch Linux
|
||||
- Manjaro
|
||||
- Armbian
|
||||
- AlmaLinux 9+
|
||||
- Rockylinux 9+
|
||||
- OpenSUSE Tubleweed
|
||||
|
||||
## Arquitecturas y Dispositivos Compatibles
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para detalles de arquitecturas y dispositivos compatibles</summary>
|
||||
|
||||
Nuestra plataforma ofrece compatibilidad con una amplia gama de arquitecturas y dispositivos, garantizando flexibilidad en diversos entornos informáticos. A continuación se presentan las principales arquitecturas que admitimos:
|
||||
|
||||
- **amd64**: Esta arquitectura predominante es la estándar para computadoras personales y servidores, y admite la mayoría de los sistemas operativos modernos sin problemas.
|
||||
|
||||
- **x86 / i386**: Ampliamente adoptada en computadoras de escritorio y portátiles, esta arquitectura cuenta con un amplio soporte de numerosos sistemas operativos y aplicaciones, incluidos, entre otros, Windows, macOS y sistemas Linux.
|
||||
|
||||
- **armv8 / arm64 / aarch64**: Diseñada para dispositivos móviles y embebidos contemporáneos, como teléfonos inteligentes y tabletas, esta arquitectura está ejemplificada por dispositivos como Raspberry Pi 4, Raspberry Pi 3, Raspberry Pi Zero 2/Zero 2 W, Orange Pi 3 LTS, entre otros.
|
||||
|
||||
- **armv7 / arm / arm32**: Sirve como arquitectura para dispositivos móviles y embebidos más antiguos, y sigue siendo ampliamente utilizada en dispositivos como Orange Pi Zero LTS, Orange Pi PC Plus, Raspberry Pi 2, entre otros.
|
||||
|
||||
- **armv6 / arm / arm32**: Orientada a dispositivos embebidos muy antiguos, esta arquitectura, aunque menos común, todavía se utiliza. Dispositivos como Raspberry Pi 1, Raspberry Pi Zero/Zero W, dependen de esta arquitectura.
|
||||
|
||||
- **armv5 / arm / arm32**: Una arquitectura más antigua asociada principalmente con sistemas embebidos tempranos, es menos común hoy en día pero aún puede encontrarse en dispositivos heredados como versiones antiguas de Raspberry Pi y algunos teléfonos inteligentes más antiguos.
|
||||
</details>
|
||||
|
||||
## Idiomas
|
||||
|
||||
- Inglés
|
||||
- Farsi
|
||||
- Chino
|
||||
- Ruso
|
||||
- Vietnamita
|
||||
- Español
|
||||
- Indonesio
|
||||
- Ucraniano
|
||||
|
||||
|
||||
## Características
|
||||
|
||||
- Monitoreo del Estado del Sistema
|
||||
- Búsqueda dentro de todas las reglas de entrada y clientes
|
||||
- Tema Oscuro/Claro
|
||||
- Soporta multiusuario y multiprotocolo
|
||||
- Soporta protocolos, incluyendo VMess, VLESS, Trojan, Shadowsocks, Dokodemo-door, Socks, HTTP, wireguard
|
||||
- Soporta Protocolos nativos XTLS, incluyendo RPRX-Direct, Visión, REALITY
|
||||
- Estadísticas de tráfico, límite de tráfico, límite de tiempo de vencimiento
|
||||
- Plantillas de configuración de Xray personalizables
|
||||
- Soporta acceso HTTPS al panel (dominio proporcionado por uno mismo + certificado SSL)
|
||||
- Soporta la solicitud y renovación automática de certificados SSL con un clic
|
||||
- Para elementos de configuración más avanzados, consulta el panel
|
||||
- Corrige rutas de API (la configuración del usuario se creará con la API)
|
||||
- Soporta cambiar las configuraciones por diferentes elementos proporcionados en el panel.
|
||||
- Soporta exportar/importar base de datos desde el panel
|
||||
|
||||
|
||||
## Configuraciones por Defecto
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para detalles de las configuraciones por defecto</summary>
|
||||
|
||||
### Información
|
||||
|
||||
- **Puerto:** 2053
|
||||
- **Usuario y Contraseña:** Se generarán aleatoriamente si omites la modificación.
|
||||
- **Ruta de la Base de Datos:**
|
||||
- /etc/x-ui/x-ui.db
|
||||
- **Ruta de Configuración de Xray:**
|
||||
- /usr/local/x-ui/bin/config.json
|
||||
- **Ruta del Panel Web sin Implementar SSL:**
|
||||
- http://ip:2053/panel
|
||||
- http://domain:2053/panel
|
||||
- **Ruta del Panel Web con Implementación de SSL:**
|
||||
- https://domain:2053/panel
|
||||
|
||||
</details>
|
||||
|
||||
## Configuración WARP
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para detalles de la configuración WARP</summary>
|
||||
|
||||
#### Uso
|
||||
|
||||
Si deseas usar enrutamiento a WARP antes de la versión v2.1.0, sigue los pasos a continuación:
|
||||
|
||||
**1.** Instala WARP en **Modo de Proxy SOCKS**:
|
||||
|
||||
```sh
|
||||
bash <(curl -sSL https://raw.githubusercontent.com/hamid-gh98/x-ui-scripts/main/install_warp_proxy.sh)
|
||||
```
|
||||
|
||||
**2.** Si ya instalaste warp, puedes desinstalarlo usando el siguiente comando:
|
||||
|
||||
```sh
|
||||
warp u
|
||||
```
|
||||
|
||||
**3.** Activa la configuración que necesites en el panel
|
||||
|
||||
Características de Configuración:
|
||||
|
||||
- Bloquear Anuncios
|
||||
- Enrutar Google + Netflix + Spotify + OpenAI (ChatGPT) a WARP
|
||||
- Corregir error 403 de Google
|
||||
|
||||
</details>
|
||||
|
||||
## Límite de IP
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para más detalles del límite de IP</summary>
|
||||
|
||||
#### Uso
|
||||
|
||||
**Nota:** El Límite de IP no funcionará correctamente cuando se use IP Tunnel
|
||||
|
||||
- Para versiones hasta `v1.6.1`:
|
||||
|
||||
- El límite de IP está integrado en el panel.
|
||||
|
||||
- Para versiones `v1.7.0` y posteriores:
|
||||
|
||||
- Para que el Límite de IP funcione correctamente, necesitas instalar fail2ban y sus archivos requeridos siguiendo estos pasos:
|
||||
|
||||
1. Usa el comando `x-ui` dentro de la terminal.
|
||||
2. Selecciona `Gestión de Límite de IP`.
|
||||
3. Elige las opciones apropiadas según tus necesidades.
|
||||
|
||||
- asegúrate de tener ./access.log en tu Configuración de Xray después de la v2.1.3 tenemos una opción para ello
|
||||
|
||||
```sh
|
||||
"log": {
|
||||
"access": "./access.log",
|
||||
"dnsLog": false,
|
||||
"loglevel": "warning"
|
||||
},
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Bot de Telegram
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para más detalles del bot de Telegram</summary>
|
||||
|
||||
#### Uso
|
||||
|
||||
El panel web admite tráfico diario, inicio de sesión en el panel, copia de seguridad de la base de datos, estado del sistema, información del cliente y otras notificaciones y funciones a través del Bot de Telegram. Para usar el bot, debes establecer los parámetros relacionados con el bot en el panel, que incluyen:
|
||||
|
||||
- Token de Telegram
|
||||
- ID de chat de administrador(es)
|
||||
- Hora de Notificación (en sintaxis cron)
|
||||
- Notificación de Fecha de Caducidad
|
||||
- Notificación de Capacidad de Tráfico
|
||||
- Copia de seguridad de la base de datos
|
||||
- Notificación de Carga de CPU
|
||||
|
||||
|
||||
**Sintaxis de referencia:**
|
||||
|
||||
- `30 \* \* \* \* \*` - Notifica a los 30s de cada punto
|
||||
- `0 \*/10 \* \* \* \*` - Notifica en el primer segundo de cada 10 minutos
|
||||
- `@hourly` - Notificación por hora
|
||||
- `@daily` - Notificación diaria (00:00 de la mañana)
|
||||
- `@weekly` - Notificación semanal
|
||||
- `@every 8h` - Notifica cada 8 horas
|
||||
|
||||
### Funcionalidades del Bot de Telegram
|
||||
|
||||
- Reporte periódico
|
||||
- Notificación de inicio de sesión
|
||||
- Notificación de umbral de CPU
|
||||
- Umbral de Notificación para Fecha de Caducidad y Tráfico para informar con anticipación
|
||||
- Soporte para menú de reporte de cliente si el nombre de usuario de Telegram del cliente se agrega a las configuraciones de usuario
|
||||
- Soporte para reporte de tráfico de Telegram buscado con UUID (VMESS/VLESS) o Contraseña (TROJAN) - anónimamente
|
||||
- Bot basado en menú
|
||||
- Buscar cliente por correo electrónico (solo administrador)
|
||||
- Ver todas las Entradas
|
||||
- Ver estado del servidor
|
||||
- Ver clientes agotados
|
||||
- Recibir copia de seguridad bajo demanda y en informes periódicos
|
||||
- Bot multilingüe
|
||||
|
||||
### Configuración del Bot de Telegram
|
||||
|
||||
- Inicia [Botfather](https://t.me/BotFather) en tu cuenta de Telegram:
|
||||

|
||||
|
||||
- Crea un nuevo bot usando el comando /newbot: Te hará 2 preguntas, Un nombre y un nombre de usuario para tu bot. Ten en cuenta que el nombre de usuario debe terminar con la palabra "bot".
|
||||

|
||||
|
||||
- Inicia el bot que acabas de crear. Puedes encontrar el enlace a tu bot aquí.
|
||||

|
||||
|
||||
- Ingresa a tu panel y configura los ajustes del bot de Telegram como se muestra a continuación:
|
||||

|
||||
|
||||
Ingresa el token de tu bot en el campo de entrada número 3.
|
||||
Ingresa el ID de chat de usuario en el campo de entrada número 4. Las cuentas de Telegram con esta ID serán los administradores del bot. (Puedes ingresar más de uno, solo sepáralos con ,)
|
||||
|
||||
- ¿Cómo obtener el ID de chat de Telegram? Usa este [bot](https://t.me/useridinfobot), Inicia el bot y te dará el ID de chat del usuario de Telegram.
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## Rutas de API
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para más detalles de las rutas de API</summary>
|
||||
|
||||
#### Uso
|
||||
|
||||
- `/login` con `POST` datos de usuario: `{username: '', password: ''}` para iniciar sesión
|
||||
- `/panel/api/inbounds` base para las siguientes acciones:
|
||||
|
||||
| Método | Ruta | Acción |
|
||||
| :----: | ---------------------------------- | --------------------------------------------------------- |
|
||||
| `GET` | `"/list"` | Obtener todas los Entradas |
|
||||
| `GET` | `"/get/:id"` | Obtener Entrada con inbound.id |
|
||||
| `GET` | `"/getClientTraffics/:email"` | Obtener Tráficos del Cliente con email |
|
||||
| `GET` | `"/createbackup"` | El bot de Telegram envía copia de seguridad a los admins |
|
||||
| `POST` | `"/add"` | Agregar Entrada |
|
||||
| `POST` | `"/del/:id"` | Eliminar Entrada |
|
||||
| `POST` | `"/update/:id"` | Actualizar Entrada |
|
||||
| `POST` | `"/clientIps/:email"` | Dirección IP del Cliente |
|
||||
| `POST` | `"/clearClientIps/:email"` | Borrar Dirección IP del Cliente |
|
||||
| `POST` | `"/addClient"` | Agregar Cliente a la Entrada |
|
||||
| `POST` | `"/:id/delClient/:clientId"` | Eliminar Cliente por clientId\* |
|
||||
| `POST` | `"/updateClient/:clientId"` | Actualizar Cliente por clientId\* |
|
||||
| `POST` | `"/:id/resetClientTraffic/:email"` | Restablecer Tráfico del Cliente |
|
||||
| `POST` | `"/resetAllTraffics"` | Restablecer tráfico de todos las Entradas |
|
||||
| `POST` | `"/resetAllClientTraffics/:id"` | Restablecer tráfico de todos los clientes en una Entrada |
|
||||
| `POST` | `"/delDepletedClients/:id"` | Eliminar clientes agotados de la entrada (-1: todos) |
|
||||
| `POST` | `"/onlines"` | Obtener usuarios en línea (lista de correos electrónicos) |
|
||||
|
||||
\*- El campo `clientId` debe llenarse por:
|
||||
|
||||
- `client.id` para VMESS y VLESS
|
||||
- `client.password` para TROJAN
|
||||
- `client.email` para Shadowsocks
|
||||
|
||||
|
||||
- [Documentación de API](https://documenter.getpostman.com/view/16802678/2s9YkgD5jm)
|
||||
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415%26entityType%3Dcollection%26workspaceId%3D2cd38c01-c851-4a15-a972-f181c23359d9)
|
||||
</details>
|
||||
|
||||
## Variables de Entorno
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para más detalles de las variables de entorno</summary>
|
||||
|
||||
#### Uso
|
||||
|
||||
| Variable | Tipo | Predeterminado |
|
||||
| -------------- | :--------------------------------------------: | :------------- |
|
||||
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
|
||||
| XUI_DEBUG | `boolean` | `false` |
|
||||
| XUI_BIN_FOLDER | `string` | `"bin"` |
|
||||
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
|
||||
| XUI_LOG_FOLDER | `string` | `"/var/log"` |
|
||||
|
||||
Ejemplo:
|
||||
|
||||
```sh
|
||||
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Vista previa
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Un agradecimiento especial a
|
||||
## Un Agradecimiento Especial a
|
||||
|
||||
- [alireza0](https://github.com/alireza0/)
|
||||
|
||||
## Reconocimientos
|
||||
|
||||
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (Licencia: **GPL-3.0**): _Reglas de enrutamiento mejoradas de v2ray/xray y v2ray/xray-clients con dominios iraníes integrados y un enfoque en seguridad y bloqueo de anuncios._
|
||||
- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _Un dominio alojado en Vietnam y una lista de bloqueo con la máxima eficiencia para vietnamitas._
|
||||
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (Licencia: **GPL-3.0**): _Reglas de enrutamiento mejoradas para v2ray/xray y v2ray/xray-clients con dominios iraníes incorporados y un enfoque en seguridad y bloqueo de anuncios._
|
||||
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (Licencia: **GPL-3.0**): _Este repositorio contiene reglas de enrutamiento V2Ray actualizadas automáticamente basadas en datos de dominios y direcciones bloqueadas en Rusia._
|
||||
|
||||
## Estrellas a lo largo del tiempo
|
||||
## Apoyar el Proyecto
|
||||
|
||||
[](https://starchart.cc/MHSanaei/3x-ui)
|
||||
**Si este proyecto te es útil, puedes darle una**:star2:
|
||||
|
||||
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||
</a>
|
||||
|
||||
</br>
|
||||
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||
</a>
|
||||
|
||||
## Estrellas a lo Largo del Tiempo
|
||||
|
||||
[](https://starchart.cc/MHSanaei/3x-ui)
|
||||
|
||||
57
README.fa_IR.md
Normal file
@@ -0,0 +1,57 @@
|
||||
[English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/3x-ui-dark.png">
|
||||
<img alt="3x-ui" src="./media/3x-ui-light.png">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||
|
||||
**3X-UI** — یک پنل کنترل پیشرفته مبتنی بر وب با کد باز که برای مدیریت سرور Xray-core طراحی شده است. این پنل یک رابط کاربری آسان برای پیکربندی و نظارت بر پروتکلهای مختلف VPN و پراکسی ارائه میدهد.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> این پروژه فقط برای استفاده شخصی و ارتباطات است، لطفاً از آن برای اهداف غیرقانونی استفاده نکنید، لطفاً از آن در محیط تولید استفاده نکنید.
|
||||
|
||||
به عنوان یک نسخه بهبود یافته از پروژه اصلی X-UI، 3X-UI پایداری بهتر، پشتیبانی گستردهتر از پروتکلها و ویژگیهای اضافی را ارائه میدهد.
|
||||
|
||||
## شروع سریع
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
|
||||
```
|
||||
|
||||
برای مستندات کامل، لطفاً به [ویکی پروژه](https://github.com/MHSanaei/3x-ui/wiki) مراجعه کنید.
|
||||
|
||||
## تشکر ویژه از
|
||||
|
||||
- [alireza0](https://github.com/alireza0/)
|
||||
|
||||
## قدردانی
|
||||
|
||||
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (مجوز: **GPL-3.0**): _قوانین مسیریابی بهبود یافته v2ray/xray و v2ray/xray-clients با دامنههای ایرانی داخلی و تمرکز بر امنیت و مسدود کردن تبلیغات._
|
||||
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (مجوز: **GPL-3.0**): _این مخزن شامل قوانین مسیریابی V2Ray بهروزرسانی شده خودکار بر اساس دادههای دامنهها و آدرسهای مسدود شده در روسیه است._
|
||||
|
||||
## پشتیبانی از پروژه
|
||||
|
||||
**اگر این پروژه برای شما مفید است، میتوانید به آن یک**:star2: بدهید
|
||||
|
||||
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||
</a>
|
||||
|
||||
</br>
|
||||
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||
</a>
|
||||
|
||||
## ستارهها در طول زمان
|
||||
|
||||
[](https://starchart.cc/MHSanaei/3x-ui)
|
||||
539
README.md
@@ -1,518 +1,34 @@
|
||||
[English](/README.md) | [Chinese](/README.zh.md) | [Español](/README.es_ES.md)
|
||||
[English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
|
||||
|
||||
<p align="center"><a href="#"><img src="./media/3X-UI.png" alt="Image"></a></p>
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/3x-ui-dark.png">
|
||||
<img alt="3x-ui" src="./media/3x-ui-light.png">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
**An Advanced Web Panel • Built on Xray Core**
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||
|
||||
> **Disclaimer:** This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment
|
||||
**3X-UI** — advanced, open-source web-based control panel designed for managing Xray-core server. It offers a user-friendly interface for configuring and monitoring various VPN and proxy protocols.
|
||||
|
||||
**If this project is helpful to you, you may wish to give it a**:star2:
|
||||
> [!IMPORTANT]
|
||||
> This project is only for personal usage, please do not use it for illegal purposes, and please do not use it in a production environment.
|
||||
|
||||
<p align="left"><a href="#"><img width="125" src="https://github.com/MHSanaei/3x-ui/assets/115543613/7aa895dd-048a-42e7-989b-afd41a74e2e1" alt="Image"></a></p>
|
||||
As an enhanced fork of the original X-UI project, 3X-UI provides improved stability, broader protocol support, and additional features.
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
## Quick Start
|
||||
|
||||
## Install & Upgrade
|
||||
|
||||
```
|
||||
```bash
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
|
||||
```
|
||||
|
||||
## Install Custom Version
|
||||
|
||||
To install your desired version, add the version to the end of the installation command. e.g., ver `v2.3.10`:
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.3.10
|
||||
```
|
||||
|
||||
## SSL Certificate
|
||||
|
||||
<details>
|
||||
<summary>Click for SSL Certificate details</summary>
|
||||
|
||||
### ACME
|
||||
|
||||
To manage SSL certificates using ACME:
|
||||
|
||||
1. Ensure your domain is correctly resolved to the server.
|
||||
2. Run the `x-ui` command in the terminal, then choose `SSL Certificate Management`.
|
||||
3. You will be presented with the following options:
|
||||
|
||||
- **Get SSL:** Obtain SSL certificates.
|
||||
- **Revoke:** Revoke existing SSL certificates.
|
||||
- **Force Renew:** Force renewal of SSL certificates.
|
||||
|
||||
### Certbot
|
||||
|
||||
To install and use Certbot:
|
||||
|
||||
```sh
|
||||
apt-get install certbot -y
|
||||
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
|
||||
certbot renew --dry-run
|
||||
```
|
||||
|
||||
### Cloudflare
|
||||
|
||||
The management script includes a built-in SSL certificate application for Cloudflare. To use this script to apply for a certificate, you need the following:
|
||||
|
||||
- Cloudflare registered email
|
||||
- Cloudflare Global API Key
|
||||
- The domain name must be resolved to the current server through Cloudflare
|
||||
|
||||
**How to get the Cloudflare Global API Key:**
|
||||
|
||||
1. Run the `x-ui` command in the terminal, then choose `Cloudflare SSL Certificate`.
|
||||
2. Visit the link: [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens).
|
||||
3. Click on "View Global API Key" (see the screenshot below):
|
||||

|
||||
4. You may need to re-authenticate your account. After that, the API Key will be shown (see the screenshot below):
|
||||

|
||||
|
||||
When using, just enter your `domain name`, `email`, and `API KEY`. The diagram is as follows:
|
||||

|
||||
|
||||
|
||||
</details>
|
||||
|
||||
## Manual Install & Upgrade
|
||||
|
||||
<details>
|
||||
<summary>Click for manual install details</summary>
|
||||
|
||||
#### Usage
|
||||
|
||||
1. To download the latest version of the compressed package directly to your server, run the following command:
|
||||
|
||||
```sh
|
||||
ARCH=$(uname -m)
|
||||
case "${ARCH}" in
|
||||
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
|
||||
i*86 | x86) XUI_ARCH="386" ;;
|
||||
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
|
||||
armv7* | armv7) XUI_ARCH="armv7" ;;
|
||||
armv6* | armv6) XUI_ARCH="armv6" ;;
|
||||
armv5* | armv5) XUI_ARCH="armv5" ;;
|
||||
s390x) echo 's390x' ;;
|
||||
*) XUI_ARCH="amd64" ;;
|
||||
esac
|
||||
|
||||
|
||||
wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz
|
||||
```
|
||||
|
||||
2. Once the compressed package is downloaded, execute the following commands to install or upgrade x-ui:
|
||||
|
||||
```sh
|
||||
ARCH=$(uname -m)
|
||||
case "${ARCH}" in
|
||||
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
|
||||
i*86 | x86) XUI_ARCH="386" ;;
|
||||
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
|
||||
armv7* | armv7) XUI_ARCH="armv7" ;;
|
||||
armv6* | armv6) XUI_ARCH="armv6" ;;
|
||||
armv5* | armv5) XUI_ARCH="armv5" ;;
|
||||
s390x) echo 's390x' ;;
|
||||
*) XUI_ARCH="amd64" ;;
|
||||
esac
|
||||
|
||||
cd /root/
|
||||
rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui
|
||||
tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz
|
||||
chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh
|
||||
cp x-ui/x-ui.sh /usr/bin/x-ui
|
||||
cp -f x-ui/x-ui.service /etc/systemd/system/
|
||||
mv x-ui/ /usr/local/
|
||||
systemctl daemon-reload
|
||||
systemctl enable x-ui
|
||||
systemctl restart x-ui
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Install with Docker
|
||||
|
||||
<details>
|
||||
<summary>Click for Docker details</summary>
|
||||
|
||||
#### Usage
|
||||
|
||||
1. **Install Docker:**
|
||||
|
||||
```sh
|
||||
bash <(curl -sSL https://get.docker.com)
|
||||
```
|
||||
|
||||
2. **Clone the Project Repository:**
|
||||
|
||||
```sh
|
||||
git clone https://github.com/MHSanaei/3x-ui.git
|
||||
cd 3x-ui
|
||||
```
|
||||
|
||||
3. **Start the Service:**
|
||||
|
||||
```sh
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**OR**
|
||||
|
||||
```sh
|
||||
docker run -itd \
|
||||
-e XRAY_VMESS_AEAD_FORCED=false \
|
||||
-v $PWD/db/:/etc/x-ui/ \
|
||||
-v $PWD/cert/:/root/cert/ \
|
||||
--network=host \
|
||||
--restart=unless-stopped \
|
||||
--name 3x-ui \
|
||||
ghcr.io/mhsanaei/3x-ui:latest
|
||||
```
|
||||
|
||||
4. **Update to the Latest Version:**
|
||||
|
||||
```sh
|
||||
cd 3x-ui
|
||||
docker compose down
|
||||
docker compose pull 3x-ui
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
5. **Remove 3x-ui from Docker:**
|
||||
|
||||
```sh
|
||||
docker stop 3x-ui
|
||||
docker rm 3x-ui
|
||||
cd --
|
||||
rm -r 3x-ui
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## Recommended OS
|
||||
|
||||
- Ubuntu 20.04+
|
||||
- Debian 11+
|
||||
- CentOS 8+
|
||||
- Fedora 36+
|
||||
- Arch Linux
|
||||
- Parch Linux
|
||||
- Manjaro
|
||||
- Armbian
|
||||
- AlmaLinux 9+
|
||||
- Rocky Linux 9+
|
||||
- Oracle Linux 8+
|
||||
- OpenSUSE Tubleweed
|
||||
|
||||
## Supported Architectures and Devices
|
||||
|
||||
<details>
|
||||
<summary>Click for Supported Architectures and devices details</summary>
|
||||
|
||||
Our platform offers compatibility with a diverse range of architectures and devices, ensuring flexibility across various computing environments. The following are key architectures that we support:
|
||||
|
||||
- **amd64**: This prevalent architecture is the standard for personal computers and servers, accommodating most modern operating systems seamlessly.
|
||||
|
||||
- **x86 / i386**: Widely adopted in desktop and laptop computers, this architecture enjoys broad support from numerous operating systems and applications, including but not limited to Windows, macOS, and Linux systems.
|
||||
|
||||
- **armv8 / arm64 / aarch64**: Tailored for contemporary mobile and embedded devices, such as smartphones and tablets, this architecture is exemplified by devices like Raspberry Pi 4, Raspberry Pi 3, Raspberry Pi Zero 2/Zero 2 W, Orange Pi 3 LTS, and more.
|
||||
|
||||
- **armv7 / arm / arm32**: Serving as the architecture for older mobile and embedded devices, it remains widely utilized in devices like Orange Pi Zero LTS, Orange Pi PC Plus, Raspberry Pi 2, among others.
|
||||
|
||||
- **armv6 / arm / arm32**: Geared towards very old embedded devices, this architecture, while less prevalent, is still in use. Devices such as Raspberry Pi 1, Raspberry Pi Zero/Zero W, rely on this architecture.
|
||||
|
||||
- **armv5 / arm / arm32**: An older architecture primarily associated with early embedded systems, it is less common today but may still be found in legacy devices like early Raspberry Pi versions and some older smartphones.
|
||||
|
||||
- **s390x**: This architecture is commonly used in IBM mainframe computers and offers high performance and reliability for enterprise workloads.
|
||||
</details>
|
||||
|
||||
## Languages
|
||||
|
||||
- English
|
||||
- Farsi
|
||||
- Chinese
|
||||
- Russian
|
||||
- Vietnamese
|
||||
- Spanish
|
||||
- Indonesian
|
||||
- Ukrainian
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- System Status Monitoring
|
||||
- Search within all inbounds and clients
|
||||
- Dark/Light theme
|
||||
- Supports multi-user and multi-protocol
|
||||
- Supports protocols, including VMess, VLESS, Trojan, Shadowsocks, Dokodemo-door, Socks, HTTP, wireguard
|
||||
- Supports XTLS native Protocols, including RPRX-Direct, Vision, REALITY
|
||||
- Traffic statistics, traffic limit, expiration time limit
|
||||
- Customizable Xray configuration templates
|
||||
- Supports HTTPS access panel (self-provided domain name + SSL certificate)
|
||||
- Supports One-Click SSL certificate application and automatic renewal
|
||||
- For more advanced configuration items, please refer to the panel
|
||||
- Fixes API routes (user setting will be created with API)
|
||||
- Supports changing configs by different items provided in the panel.
|
||||
- Supports export/import database from the panel
|
||||
|
||||
|
||||
## Default Panel Settings
|
||||
|
||||
<details>
|
||||
<summary>Click for default settings details</summary>
|
||||
|
||||
### Username & Password & webbasepath:
|
||||
|
||||
These will be generated randomly if you skip modifying them.
|
||||
|
||||
- **Port:** the default port for panel is `2053`
|
||||
|
||||
### Database Management:
|
||||
|
||||
You can conveniently perform database Backups and Restores directly from the panel.
|
||||
|
||||
- **Database Path:**
|
||||
- `/etc/x-ui/x-ui.db`
|
||||
|
||||
|
||||
### Web Base Path
|
||||
|
||||
1. **Reset Web Base Path:**
|
||||
- Open your terminal.
|
||||
- Run the `x-ui` command.
|
||||
- Select the option to `Reset Web Base Path`.
|
||||
|
||||
2. **Generate or Customize Path:**
|
||||
- The path will be randomly generated, or you can enter a custom path.
|
||||
|
||||
3. **View Current Settings:**
|
||||
- To view your current settings, use the `x-ui settings` command in the terminal or `View Current Settings` in `x-ui`
|
||||
|
||||
### Security Recommendation:
|
||||
- For enhanced security, use a long, random word in your URL structure.
|
||||
|
||||
**Examples:**
|
||||
- `http://ip:port/*webbasepath*/panel`
|
||||
- `http://domain:port/*webbasepath*/panel`
|
||||
|
||||
</details>
|
||||
|
||||
## WARP Configuration
|
||||
|
||||
<details>
|
||||
<summary>Click for WARP configuration details</summary>
|
||||
|
||||
#### Usage
|
||||
|
||||
**For versions `v2.1.0` and later:**
|
||||
|
||||
WARP is built-in, and no additional installation is required. Simply turn on the necessary configuration in the panel.
|
||||
|
||||
**For versions before `v2.1.0`:**
|
||||
|
||||
1. Run the `x-ui` command in the terminal, then choose `WARP Management`.
|
||||
2. You will see the following options:
|
||||
|
||||
- **Account Type (free, plus, team):** Choose the appropriate account type.
|
||||
- **Enable/Disable WireProxy:** Toggle WireProxy on or off.
|
||||
- **Uninstall WARP:** Remove the WARP application.
|
||||
|
||||
3. Configure the settings as needed in the panel.
|
||||
|
||||
</details>
|
||||
|
||||
## IP Limit
|
||||
|
||||
<details>
|
||||
<summary>Click for IP limit details</summary>
|
||||
|
||||
#### Usage
|
||||
|
||||
**Note:** IP Limit won't work correctly when using IP Tunnel.
|
||||
|
||||
- **For versions up to `v1.6.1`:**
|
||||
- The IP limit is built-in to the panel
|
||||
|
||||
**For versions `v1.7.0` and newer:**
|
||||
|
||||
To enable the IP Limit functionality, you need to install `fail2ban` and its required files by following these steps:
|
||||
|
||||
1. Run the `x-ui` command in the terminal, then choose `IP Limit Management`.
|
||||
2. You will see the following options:
|
||||
|
||||
- **Change Ban Duration:** Adjust the duration of bans.
|
||||
- **Unban Everyone:** Lift all current bans.
|
||||
- **Check Logs:** Review the logs.
|
||||
- **Fail2ban Status:** Check the status of `fail2ban`.
|
||||
- **Restart Fail2ban:** Restart the `fail2ban` service.
|
||||
- **Uninstall Fail2ban:** Uninstall Fail2ban with configuration.
|
||||
|
||||
3. Add a path for the access log on the panel by setting `Xray Configs/log/Access log` to `./access.log` then save and restart xray.
|
||||
|
||||
- **For versions before `v2.1.3`:**
|
||||
- You need to set the access log path manually in your Xray configuration:
|
||||
|
||||
```sh
|
||||
"log": {
|
||||
"access": "./access.log",
|
||||
"dnsLog": false,
|
||||
"loglevel": "warning"
|
||||
},
|
||||
```
|
||||
|
||||
- **For versions `v2.1.3` and newer:**
|
||||
- There is an option for configuring `access.log` directly from the panel.
|
||||
|
||||
</details>
|
||||
|
||||
## Telegram Bot
|
||||
|
||||
<details>
|
||||
<summary>Click for Telegram bot details</summary>
|
||||
|
||||
#### Usage
|
||||
|
||||
The web panel supports daily traffic, panel login, database backup, system status, client info, and other notification and functions through the Telegram Bot. To use the bot, you need to set the bot-related parameters in the panel, including:
|
||||
|
||||
- Telegram Token
|
||||
- Admin Chat ID(s)
|
||||
- Notification Time (in cron syntax)
|
||||
- Expiration Date Notification
|
||||
- Traffic Cap Notification
|
||||
- Database Backup
|
||||
- CPU Load Notification
|
||||
|
||||
|
||||
**Reference syntax:**
|
||||
|
||||
- `30 \* \* \* \* \*` - Notify at the 30s of each point
|
||||
- `0 \*/10 \* \* \* \*` - Notify at the first second of each 10 minutes
|
||||
- `@hourly` - Hourly notification
|
||||
- `@daily` - Daily notification (00:00 in the morning)
|
||||
- `@weekly` - weekly notification
|
||||
- `@every 8h` - Notify every 8 hours
|
||||
|
||||
### Telegram Bot Features
|
||||
|
||||
- Report periodic
|
||||
- Login notification
|
||||
- CPU threshold notification
|
||||
- Threshold for Expiration time and Traffic to report in advance
|
||||
- Support client report menu if client's telegram username added to the user's configurations
|
||||
- Support telegram traffic report searched with UUID (VMESS/VLESS) or Password (TROJAN) - anonymously
|
||||
- Menu based bot
|
||||
- Search client by email ( only admin )
|
||||
- Check all inbounds
|
||||
- Check server status
|
||||
- Check depleted users
|
||||
- Receive backup by request and in periodic reports
|
||||
- Multi language bot
|
||||
|
||||
### Setting up Telegram bot
|
||||
|
||||
- Start [Botfather](https://t.me/BotFather) in your Telegram account:
|
||||

|
||||
|
||||
- Create a new Bot using /newbot command: It will ask you 2 questions, A name and a username for your bot. Note that the username has to end with the word "bot".
|
||||

|
||||
|
||||
- Start the bot you've just created. You can find the link to your bot here.
|
||||

|
||||
|
||||
- Enter your panel and config Telegram bot settings like below:
|
||||

|
||||
|
||||
Enter your bot token in input field number 3.
|
||||
Enter the user ID in input field number 4. The Telegram accounts with this id will be the bot admin. (You can enter more than one, Just separate them with ,)
|
||||
|
||||
- How to get Telegram user ID? Use this [bot](https://t.me/useridinfobot), Start the bot and it will give you the Telegram user ID.
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## API Routes
|
||||
|
||||
<details>
|
||||
<summary>Click for API routes details</summary>
|
||||
|
||||
#### Usage
|
||||
|
||||
- `/login` with `POST` user data: `{username: '', password: ''}` for login
|
||||
- `/panel/api/inbounds` base for following actions:
|
||||
|
||||
| Method | Path | Action |
|
||||
| :----: | ---------------------------------- | ------------------------------------------- |
|
||||
| `GET` | `"/list"` | Get all inbounds |
|
||||
| `GET` | `"/get/:id"` | Get inbound with inbound.id |
|
||||
| `GET` | `"/getClientTraffics/:email"` | Get Client Traffics with email |
|
||||
| `GET` | `"/createbackup"` | Telegram bot sends backup to admins |
|
||||
| `POST` | `"/add"` | Add inbound |
|
||||
| `POST` | `"/del/:id"` | Delete Inbound |
|
||||
| `POST` | `"/update/:id"` | Update Inbound |
|
||||
| `POST` | `"/clientIps/:email"` | Client Ip address |
|
||||
| `POST` | `"/clearClientIps/:email"` | Clear Client Ip address |
|
||||
| `POST` | `"/addClient"` | Add Client to inbound |
|
||||
| `POST` | `"/:id/delClient/:clientId"` | Delete Client by clientId\* |
|
||||
| `POST` | `"/updateClient/:clientId"` | Update Client by clientId\* |
|
||||
| `POST` | `"/:id/resetClientTraffic/:email"` | Reset Client's Traffic |
|
||||
| `POST` | `"/resetAllTraffics"` | Reset traffics of all inbounds |
|
||||
| `POST` | `"/resetAllClientTraffics/:id"` | Reset traffics of all clients in an inbound |
|
||||
| `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) |
|
||||
| `POST` | `"/onlines"` | Get Online users ( list of emails ) |
|
||||
|
||||
\*- The field `clientId` should be filled by:
|
||||
|
||||
- `client.id` for VMESS and VLESS
|
||||
- `client.password` for TROJAN
|
||||
- `client.email` for Shadowsocks
|
||||
|
||||
|
||||
- [API Documentation](https://documenter.getpostman.com/view/16802678/2s9YkgD5jm)
|
||||
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415%26entityType%3Dcollection%26workspaceId%3D2cd38c01-c851-4a15-a972-f181c23359d9)
|
||||
</details>
|
||||
|
||||
## Environment Variables
|
||||
|
||||
<details>
|
||||
<summary>Click for environment variables details</summary>
|
||||
|
||||
#### Usage
|
||||
|
||||
| Variable | Type | Default |
|
||||
| -------------- | :--------------------------------------------: | :------------ |
|
||||
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
|
||||
| XUI_DEBUG | `boolean` | `false` |
|
||||
| XUI_BIN_FOLDER | `string` | `"bin"` |
|
||||
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
|
||||
| XUI_LOG_FOLDER | `string` | `"/var/log"` |
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Preview
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
For full documentation, please visit the [project Wiki](https://github.com/MHSanaei/3x-ui/wiki).
|
||||
|
||||
## A Special Thanks to
|
||||
|
||||
@@ -521,8 +37,21 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
|
||||
## Acknowledgment
|
||||
|
||||
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (License: **GPL-3.0**): _Enhanced v2ray/xray and v2ray/xray-clients routing rules with built-in Iranian domains and a focus on security and adblocking._
|
||||
- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _A hosted domain hosted in Vietnam and blocklist with the most efficiency for Vietnamese._
|
||||
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (License: **GPL-3.0**): _This repository contains automatically updated V2Ray routing rules based on data on blocked domains and addresses in Russia._
|
||||
|
||||
## Support project
|
||||
|
||||
**If this project is helpful to you, you may wish to give it a**:star2:
|
||||
|
||||
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||
</a>
|
||||
|
||||
</br>
|
||||
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||
</a>
|
||||
|
||||
## Stargazers over Time
|
||||
|
||||
[](https://starchart.cc/MHSanaei/3x-ui)
|
||||
[](https://starchart.cc/MHSanaei/3x-ui)
|
||||
|
||||
57
README.ru_RU.md
Normal file
@@ -0,0 +1,57 @@
|
||||
[English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/3x-ui-dark.png">
|
||||
<img alt="3x-ui" src="./media/3x-ui-light.png">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||
|
||||
**3X-UI** — продвинутая панель управления с открытым исходным кодом на основе веб-интерфейса, разработанная для управления сервером Xray-core. Предоставляет удобный интерфейс для настройки и мониторинга различных VPN и прокси-протоколов.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Этот проект предназначен только для личного использования, пожалуйста, не используйте его в незаконных целях и в производственной среде.
|
||||
|
||||
Как улучшенная версия оригинального проекта X-UI, 3X-UI обеспечивает повышенную стабильность, более широкую поддержку протоколов и дополнительные функции.
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
|
||||
```
|
||||
|
||||
Полную документацию смотрите в [вики проекта](https://github.com/MHSanaei/3x-ui/wiki).
|
||||
|
||||
## Особая благодарность
|
||||
|
||||
- [alireza0](https://github.com/alireza0/)
|
||||
|
||||
## Благодарности
|
||||
|
||||
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (Лицензия: **GPL-3.0**): _Улучшенные правила маршрутизации для v2ray/xray и v2ray/xray-clients со встроенными иранскими доменами и фокусом на безопасность и блокировку рекламы._
|
||||
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (Лицензия: **GPL-3.0**): _Этот репозиторий содержит автоматически обновляемые правила маршрутизации V2Ray на основе данных о заблокированных доменах и адресах в России._
|
||||
|
||||
## Поддержка проекта
|
||||
|
||||
**Если этот проект полезен для вас, вы можете поставить ему**:star2:
|
||||
|
||||
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||
</a>
|
||||
|
||||
</br>
|
||||
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||
</a>
|
||||
|
||||
## Звезды с течением времени
|
||||
|
||||
[](https://starchart.cc/MHSanaei/3x-ui)
|
||||
474
README.zh.md
@@ -1,474 +0,0 @@
|
||||
[English](/README.md) | [Chinese](/README.zh.md) | [Español](/README.es_ES.md)
|
||||
|
||||
<p align="center"><a href="#"><img src="./media/3X-UI.png" alt="Image"></a></p>
|
||||
|
||||
**一个更好的面板 • 基于Xray Core构建**
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](#)
|
||||
[](#)
|
||||
[](#)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
> **Disclaimer:** 此项目仅供个人学习交流,请不要用于非法目的,请不要在生产环境中使用。
|
||||
|
||||
**如果此项目对你有用,请给一个**:star2:
|
||||
|
||||
<p align="left"><a href="#"><img width="125" src="https://github.com/MHSanaei/3x-ui/assets/115543613/7aa895dd-048a-42e7-989b-afd41a74e2e1" alt="Image"></a></p>
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
|
||||
## 安装 & 升级
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
|
||||
```
|
||||
|
||||
## 安装指定版本
|
||||
|
||||
要安装所需的版本,请将该版本添加到安装命令的末尾。 e.g., ver `v2.3.6`:
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.3.6
|
||||
```
|
||||
|
||||
## SSL 认证
|
||||
|
||||
<details>
|
||||
<summary>点击查看 SSL 认证</summary>
|
||||
|
||||
### Cloudflare
|
||||
|
||||
管理脚本具有用于 Cloudflare 的内置 SSL 证书应用程序。若要使用此脚本申请证书,需要满足以下条件:
|
||||
|
||||
- Cloudflare 邮箱地址
|
||||
- Cloudflare Global API Key
|
||||
- 域名已通过 cloudflare 解析到当前服务器
|
||||
|
||||
**1:** 在终端中运行`x-ui`, 选择 `Cloudflare SSL Certificate`.
|
||||
|
||||
|
||||
### Certbot
|
||||
```
|
||||
apt-get install certbot -y
|
||||
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
|
||||
certbot renew --dry-run
|
||||
```
|
||||
|
||||
***Tip:*** *管理脚本具有 Certbot 。使用 `x-ui` 命令, 选择 `SSL Certificate Management`.*
|
||||
|
||||
</details>
|
||||
|
||||
## 手动安装 & 升级
|
||||
|
||||
<details>
|
||||
<summary>点击查看 手动安装 & 升级</summary>
|
||||
|
||||
#### 使用
|
||||
|
||||
1. 若要将最新版本的压缩包直接下载到服务器,请运行以下命令:
|
||||
|
||||
```sh
|
||||
ARCH=$(uname -m)
|
||||
case "${ARCH}" in
|
||||
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
|
||||
i*86 | x86) XUI_ARCH="386" ;;
|
||||
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
|
||||
armv7* | armv7) XUI_ARCH="armv7" ;;
|
||||
armv6* | armv6) XUI_ARCH="armv6" ;;
|
||||
armv5* | armv5) XUI_ARCH="armv5" ;;
|
||||
*) XUI_ARCH="amd64" ;;
|
||||
esac
|
||||
|
||||
|
||||
wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz
|
||||
```
|
||||
|
||||
2. 下载压缩包后,执行以下命令安装或升级 x-ui:
|
||||
|
||||
```sh
|
||||
ARCH=$(uname -m)
|
||||
case "${ARCH}" in
|
||||
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
|
||||
i*86 | x86) XUI_ARCH="386" ;;
|
||||
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
|
||||
armv7* | armv7) XUI_ARCH="armv7" ;;
|
||||
armv6* | armv6) XUI_ARCH="armv6" ;;
|
||||
armv5* | armv5) XUI_ARCH="armv5" ;;
|
||||
*) XUI_ARCH="amd64" ;;
|
||||
esac
|
||||
|
||||
cd /root/
|
||||
rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui
|
||||
tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz
|
||||
chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh
|
||||
cp x-ui/x-ui.sh /usr/bin/x-ui
|
||||
cp -f x-ui/x-ui.service /etc/systemd/system/
|
||||
mv x-ui/ /usr/local/
|
||||
systemctl daemon-reload
|
||||
systemctl enable x-ui
|
||||
systemctl restart x-ui
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 通过Docker安装
|
||||
|
||||
<details>
|
||||
<summary>点击查看 通过Docker安装</summary>
|
||||
|
||||
#### 使用
|
||||
|
||||
1. 安装Docker:
|
||||
|
||||
```sh
|
||||
bash <(curl -sSL https://get.docker.com)
|
||||
```
|
||||
|
||||
2. 克隆仓库:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/MHSanaei/3x-ui.git
|
||||
cd 3x-ui
|
||||
```
|
||||
|
||||
3. 运行服务:
|
||||
|
||||
```sh
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
或
|
||||
|
||||
```sh
|
||||
docker run -itd \
|
||||
-e XRAY_VMESS_AEAD_FORCED=false \
|
||||
-v $PWD/db/:/etc/x-ui/ \
|
||||
-v $PWD/cert/:/root/cert/ \
|
||||
--network=host \
|
||||
--restart=unless-stopped \
|
||||
--name 3x-ui \
|
||||
ghcr.io/mhsanaei/3x-ui:latest
|
||||
```
|
||||
|
||||
更新至最新版本
|
||||
|
||||
```sh
|
||||
cd 3x-ui
|
||||
docker compose down
|
||||
docker compose pull 3x-ui
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
从Docker中删除3x-ui
|
||||
|
||||
```sh
|
||||
docker stop 3x-ui
|
||||
docker rm 3x-ui
|
||||
cd --
|
||||
rm -r 3x-ui
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## 建议使用的操作系统
|
||||
|
||||
- Ubuntu 20.04+
|
||||
- Debian 11+
|
||||
- CentOS 8+
|
||||
- Fedora 36+
|
||||
- Arch Linux
|
||||
- Manjaro
|
||||
- Armbian
|
||||
- AlmaLinux 9+
|
||||
- Rockylinux 9+
|
||||
- OpenSUSE Tubleweed
|
||||
|
||||
## 支持的架构和设备
|
||||
<details>
|
||||
<summary>点击查看 支持的架构和设备</summary>
|
||||
|
||||
我们的平台提供与各种架构和设备的兼容性,确保在各种计算环境中的灵活性。以下是我们支持的关键架构:
|
||||
|
||||
- **amd64**: 这种流行的架构是个人计算机和服务器的标准,可以无缝地适应大多数现代操作系统。
|
||||
|
||||
- **x86 / i386**: 这种架构在台式机和笔记本电脑中被广泛采用,得到了众多操作系统和应用程序的广泛支持,包括但不限于 Windows、macOS 和 Linux 系统。
|
||||
|
||||
- **armv8 / arm64 / aarch64**: 这种架构专为智能手机和平板电脑等当代移动和嵌入式设备量身定制,以 Raspberry Pi 4、Raspberry Pi 3、Raspberry Pi Zero 2/Zero 2 W、Orange Pi 3 LTS 等设备为例。
|
||||
|
||||
- **armv7 / arm / arm32**: 作为较旧的移动和嵌入式设备的架构,它仍然广泛用于Orange Pi Zero LTS、Orange Pi PC Plus、Raspberry Pi 2等设备。
|
||||
|
||||
- **armv6 / arm / arm32**: 这种架构面向非常老旧的嵌入式设备,虽然不太普遍,但仍在使用中。Raspberry Pi 1、Raspberry Pi Zero/Zero W 等设备都依赖于这种架构。
|
||||
|
||||
- **armv5 / arm / arm32**: 它是一种主要与早期嵌入式系统相关的旧架构,目前不太常见,但仍可能出现在早期 Raspberry Pi 版本和一些旧智能手机等传统设备中。
|
||||
</details>
|
||||
|
||||
## Languages
|
||||
|
||||
- English(英语)
|
||||
- Farsi(伊朗语)
|
||||
- Chinese(中文)
|
||||
- Russian(俄语)
|
||||
- Vietnamese(越南语)
|
||||
- Spanish(西班牙语)
|
||||
- Indonesian (印度尼西亚语)
|
||||
- Ukrainian(乌克兰语)
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- 系统状态监控
|
||||
- 在所有入站和客户端中搜索
|
||||
- 深色/浅色主题
|
||||
- 支持多用户和多协议
|
||||
- 支持多种协议,包括 VMess、VLESS、Trojan、Shadowsocks、Dokodemo-door、Socks、HTTP、wireguard
|
||||
- 支持 XTLS 原生协议,包括 RPRX-Direct、Vision、REALITY
|
||||
- 流量统计、流量限制、过期时间限制
|
||||
- 可自定义的 Xray配置模板
|
||||
- 支持HTTPS访问面板(自建域名+SSL证书)
|
||||
- 支持一键式SSL证书申请和自动续费
|
||||
- 更多高级配置项目请参考面板
|
||||
- 修复了 API 路由(用户设置将使用 API 创建)
|
||||
- 支持通过面板中提供的不同项目更改配置。
|
||||
- 支持从面板导出/导入数据库
|
||||
|
||||
|
||||
## 默认设置
|
||||
|
||||
<details>
|
||||
<summary>点击查看 默认设置</summary>
|
||||
|
||||
### 信息
|
||||
|
||||
- **端口:** 2053
|
||||
- **用户名 & 密码:** 当您跳过设置时,此项会随机生成。
|
||||
- **数据库路径:**
|
||||
- /etc/x-ui/x-ui.db
|
||||
- **Xray 配置路径:**
|
||||
- /usr/local/x-ui/bin/config.json
|
||||
- **面板链接(无SSL):**
|
||||
- http://ip:2053/panel
|
||||
- http://domain:2053/panel
|
||||
- **面板链接(有SSL):**
|
||||
- https://domain:2053/panel
|
||||
|
||||
</details>
|
||||
|
||||
## WARP 配置
|
||||
|
||||
<details>
|
||||
<summary>点击查看 WARP 配置</summary>
|
||||
|
||||
#### 使用
|
||||
|
||||
如果要在 v2.1.0 之前使用 WARP 路由,请按照以下步骤操作:
|
||||
|
||||
**1.** 在 **SOCKS Proxy Mode** 模式中安装Wrap
|
||||
|
||||
```sh
|
||||
bash <(curl -sSL https://raw.githubusercontent.com/hamid-gh98/x-ui-scripts/main/install_warp_proxy.sh)
|
||||
```
|
||||
|
||||
**2.** 如果您已经安装了 warp,您可以使用以下命令卸载:
|
||||
|
||||
```sh
|
||||
warp u
|
||||
```
|
||||
|
||||
**3.** 在面板中打开您需要的配置
|
||||
|
||||
配置:
|
||||
|
||||
- Block Ads
|
||||
- Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP
|
||||
- Fix Google 403 error
|
||||
|
||||
</details>
|
||||
|
||||
## IP 限制
|
||||
|
||||
<details>
|
||||
<summary>点击查看 IP 限制</summary>
|
||||
|
||||
#### 使用
|
||||
|
||||
**注意:** 使用 IP 隧道时,IP 限制无法正常工作。
|
||||
|
||||
- 适用于最高 `v1.6.1` :
|
||||
|
||||
- IP 限制 已被集成在面板中。
|
||||
|
||||
- 适用于 `v1.7.0` 以及更新的版本:
|
||||
|
||||
- 要使 IP 限制正常工作,您需要按照以下步骤安装 fail2ban 及其所需的文件:
|
||||
|
||||
1. 使用面板内置的 `x-ui` 指令
|
||||
2. 选择 `IP Limit Management`.
|
||||
3. 根据您的需要选择合适的选项。
|
||||
|
||||
- 确保您的 Xray 配置上有 ./access.log 。在 v2.1.3 之后,我们有一个选项。
|
||||
|
||||
```sh
|
||||
"log": {
|
||||
"access": "./access.log",
|
||||
"dnsLog": false,
|
||||
"loglevel": "warning"
|
||||
},
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Telegram 机器人
|
||||
|
||||
<details>
|
||||
<summary>点击查看 Telegram 机器人</summary>
|
||||
|
||||
#### 使用
|
||||
|
||||
Web 面板通过 Telegram Bot 支持每日流量、面板登录、数据库备份、系统状态、客户端信息等通知和功能。要使用机器人,您需要在面板中设置机器人相关参数,包括:
|
||||
|
||||
- 电报令牌
|
||||
- 管理员聊天 ID
|
||||
- 通知时间(cron 语法)
|
||||
- 到期日期通知
|
||||
- 流量上限通知
|
||||
- 数据库备份
|
||||
- CPU 负载通知
|
||||
|
||||
|
||||
**参考:**
|
||||
|
||||
- `30 \* \* \* \* \*` - 在每个点的 30 秒处通知
|
||||
- `0 \*/10 \* \* \* \*` - 每 10 分钟的第一秒通知
|
||||
- `@hourly` - 每小时通知
|
||||
- `@daily` - 每天通知 (00:00)
|
||||
- `@weekly` - 每周通知
|
||||
- `@every 8h` - 每8小时通知
|
||||
|
||||
### Telegram Bot 功能
|
||||
|
||||
- 定期报告
|
||||
- 登录通知
|
||||
- CPU 阈值通知
|
||||
- 提前报告的过期时间和流量阈值
|
||||
- 如果将客户的电报用户名添加到用户的配置中,则支持客户端报告菜单
|
||||
- 支持使用UUID(VMESS/VLESS)或密码(TROJAN)搜索报文流量报告 - 匿名
|
||||
- 基于菜单的机器人
|
||||
- 通过电子邮件搜索客户端(仅限管理员)
|
||||
- 检查所有入库
|
||||
- 检查服务器状态
|
||||
- 检查耗尽的用户
|
||||
- 根据请求和定期报告接收备份
|
||||
- 多语言机器人
|
||||
|
||||
### 注册 Telegram bot
|
||||
|
||||
- 与 [Botfather](https://t.me/BotFather) 对话:
|
||||

|
||||
|
||||
- 使用 /newbot 创建新机器人:你需要提供机器人名称以及用户名,注意名称中末尾要包含“bot”
|
||||

|
||||
|
||||
- 启动您刚刚创建的机器人。可以在此处找到机器人的链接。
|
||||

|
||||
|
||||
- 输入您的面板并配置 Telegram 机器人设置,如下所示:
|
||||

|
||||
|
||||
在输入字段编号 3 中输入机器人令牌。
|
||||
在输入字段编号 4 中输入用户 ID。具有此 id 的 Telegram 帐户将是机器人管理员。 (您可以输入多个,只需将它们用“ ,”分开即可)
|
||||
|
||||
- 如何获取TG ID? 使用 [bot](https://t.me/useridinfobot), 启动机器人,它会给你 Telegram 用户 ID。
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## API 路由
|
||||
|
||||
<details>
|
||||
<summary>点击查看 API 路由</summary>
|
||||
|
||||
#### 使用
|
||||
|
||||
- `/login` 使用 `POST` 用户名称 & 密码: `{username: '', password: ''}` 登录
|
||||
- `/panel/api/inbounds` 以下操作的基础:
|
||||
|
||||
| 方法 | 路径 | 操作 |
|
||||
| :----: | ---------------------------------- | --------------------------------- |
|
||||
| `GET` | `"/list"` | 获取所有入站 |
|
||||
| `GET` | `"/get/:id"` | 获取所有入站以及inbound.id |
|
||||
| `GET` | `"/getClientTraffics/:email"` | 通过电子邮件获取客户端流量 |
|
||||
| `GET` | `"/createbackup"` | Telegram 机器人向管理员发送备份 |
|
||||
| `POST` | `"/add"` | 添加入站 |
|
||||
| `POST` | `"/del/:id"` | 删除入站 |
|
||||
| `POST` | `"/update/:id"` | 更新入站 |
|
||||
| `POST` | `"/clientIps/:email"` | 客户端 IP 地址 |
|
||||
| `POST` | `"/clearClientIps/:email"` | 清除客户端 IP 地址 |
|
||||
| `POST` | `"/addClient"` | 将客户端添加到入站 |
|
||||
| `POST` | `"/:id/delClient/:clientId"` | 通过 clientId\* 删除客户端 |
|
||||
| `POST` | `"/updateClient/:clientId"` | 通过 clientId\* 更新客户端 |
|
||||
| `POST` | `"/:id/resetClientTraffic/:email"` | 重置客户端的流量 |
|
||||
| `POST` | `"/resetAllTraffics"` | 重置所有入站的流量 |
|
||||
| `POST` | `"/resetAllClientTraffics/:id"` | 重置入站中所有客户端的流量 |
|
||||
| `POST` | `"/delDepletedClients/:id"` | 删除入站耗尽的客户端 (-1: all) |
|
||||
| `POST` | `"/onlines"` | 获取在线用户 ( 电子邮件列表 ) |
|
||||
|
||||
\*- `clientId` 项应该使用下列数据
|
||||
|
||||
- `client.id` VMESS and VLESS
|
||||
- `client.password` TROJAN
|
||||
- `client.email` Shadowsocks
|
||||
|
||||
|
||||
- [API 文档](https://documenter.getpostman.com/view/16802678/2s9YkgD5jm)
|
||||
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415%26entityType%3Dcollection%26workspaceId%3D2cd38c01-c851-4a15-a972-f181c23359d9)
|
||||
</details>
|
||||
|
||||
## 环境变量
|
||||
|
||||
<details>
|
||||
<summary>点击查看 环境变量</summary>
|
||||
|
||||
#### Usage
|
||||
|
||||
| 变量 | Type | 默认 |
|
||||
| -------------- | :--------------------------------------------: | :------------ |
|
||||
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
|
||||
| XUI_DEBUG | `boolean` | `false` |
|
||||
| XUI_BIN_FOLDER | `string` | `"bin"` |
|
||||
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
|
||||
| XUI_LOG_FOLDER | `string` | `"/var/log"` |
|
||||
|
||||
例子:
|
||||
|
||||
```sh
|
||||
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 预览
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 特别感谢
|
||||
|
||||
- [alireza0](https://github.com/alireza0/)
|
||||
|
||||
## 致谢
|
||||
|
||||
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (License: **GPL-3.0**): _Enhanced v2ray/xray and v2ray/xray-clients routing rules with built-in Iranian domains and a focus on security and adblocking._
|
||||
- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _A hosted domain hosted in Vietnam and blocklist with the most efficiency for Vietnamese._
|
||||
|
||||
## Star趋势
|
||||
|
||||
[](https://starchart.cc/MHSanaei/3x-ui)
|
||||
57
README.zh_CN.md
Normal file
@@ -0,0 +1,57 @@
|
||||
[English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/3x-ui-dark.png">
|
||||
<img alt="3x-ui" src="./media/3x-ui-light.png">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||
|
||||
**3X-UI** — 一个基于网页的高级开源控制面板,专为管理 Xray-core 服务器而设计。它提供了用户友好的界面,用于配置和监控各种 VPN 和代理协议。
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 本项目仅用于个人使用和通信,请勿将其用于非法目的,请勿在生产环境中使用。
|
||||
|
||||
作为原始 X-UI 项目的增强版本,3X-UI 提供了更好的稳定性、更广泛的协议支持和额外的功能。
|
||||
|
||||
## 快速开始
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
|
||||
```
|
||||
|
||||
完整文档请参阅 [项目Wiki](https://github.com/MHSanaei/3x-ui/wiki)。
|
||||
|
||||
## 特别感谢
|
||||
|
||||
- [alireza0](https://github.com/alireza0/)
|
||||
|
||||
## 致谢
|
||||
|
||||
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (许可证: **GPL-3.0**): _增强的 v2ray/xray 和 v2ray/xray-clients 路由规则,内置伊朗域名,专注于安全性和广告拦截。_
|
||||
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (许可证: **GPL-3.0**): _此仓库包含基于俄罗斯被阻止域名和地址数据自动更新的 V2Ray 路由规则。_
|
||||
|
||||
## 支持项目
|
||||
|
||||
**如果这个项目对您有帮助,您可以给它一个**:star2:
|
||||
|
||||
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||
</a>
|
||||
|
||||
</br>
|
||||
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||
</a>
|
||||
|
||||
## 随时间变化的星标数
|
||||
|
||||
[](https://starchart.cc/MHSanaei/3x-ui)
|
||||
107
config/config.go
@@ -1,9 +1,14 @@
|
||||
// Package config provides configuration management utilities for the 3x-ui panel,
|
||||
// including version information, logging levels, database paths, and environment variable handling.
|
||||
package config
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -13,24 +18,29 @@ var version string
|
||||
//go:embed name
|
||||
var name string
|
||||
|
||||
// LogLevel represents the logging level for the application.
|
||||
type LogLevel string
|
||||
|
||||
// Logging level constants
|
||||
const (
|
||||
Debug LogLevel = "debug"
|
||||
Info LogLevel = "info"
|
||||
Notice LogLevel = "notice"
|
||||
Warn LogLevel = "warn"
|
||||
Error LogLevel = "error"
|
||||
Debug LogLevel = "debug"
|
||||
Info LogLevel = "info"
|
||||
Notice LogLevel = "notice"
|
||||
Warning LogLevel = "warning"
|
||||
Error LogLevel = "error"
|
||||
)
|
||||
|
||||
// GetVersion returns the version string of the 3x-ui application.
|
||||
func GetVersion() string {
|
||||
return strings.TrimSpace(version)
|
||||
}
|
||||
|
||||
// GetName returns the name of the 3x-ui application.
|
||||
func GetName() string {
|
||||
return strings.TrimSpace(name)
|
||||
}
|
||||
|
||||
// GetLogLevel returns the current logging level based on environment variables or defaults to Info.
|
||||
func GetLogLevel() LogLevel {
|
||||
if IsDebug() {
|
||||
return Debug
|
||||
@@ -42,10 +52,12 @@ func GetLogLevel() LogLevel {
|
||||
return LogLevel(logLevel)
|
||||
}
|
||||
|
||||
// IsDebug returns true if debug mode is enabled via the XUI_DEBUG environment variable.
|
||||
func IsDebug() bool {
|
||||
return os.Getenv("XUI_DEBUG") == "true"
|
||||
}
|
||||
|
||||
// GetBinFolderPath returns the path to the binary folder, defaulting to "bin" if not set via XUI_BIN_FOLDER.
|
||||
func GetBinFolderPath() string {
|
||||
binFolderPath := os.Getenv("XUI_BIN_FOLDER")
|
||||
if binFolderPath == "" {
|
||||
@@ -54,22 +66,91 @@ func GetBinFolderPath() string {
|
||||
return binFolderPath
|
||||
}
|
||||
|
||||
func GetDBFolderPath() string {
|
||||
dbFolderPath := os.Getenv("XUI_DB_FOLDER")
|
||||
if dbFolderPath == "" {
|
||||
dbFolderPath = "/etc/x-ui"
|
||||
func getBaseDir() string {
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return "."
|
||||
}
|
||||
return dbFolderPath
|
||||
exeDir := filepath.Dir(exePath)
|
||||
exeDirLower := strings.ToLower(filepath.ToSlash(exeDir))
|
||||
if strings.Contains(exeDirLower, "/appdata/local/temp/") || strings.Contains(exeDirLower, "/go-build") {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "."
|
||||
}
|
||||
return wd
|
||||
}
|
||||
return exeDir
|
||||
}
|
||||
|
||||
// GetDBFolderPath returns the path to the database folder based on environment variables or platform defaults.
|
||||
func GetDBFolderPath() string {
|
||||
dbFolderPath := os.Getenv("XUI_DB_FOLDER")
|
||||
if dbFolderPath != "" {
|
||||
return dbFolderPath
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
return getBaseDir()
|
||||
}
|
||||
return "/etc/x-ui"
|
||||
}
|
||||
|
||||
// GetDBPath returns the full path to the database file.
|
||||
func GetDBPath() string {
|
||||
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName())
|
||||
}
|
||||
|
||||
// GetLogFolder returns the path to the log folder based on environment variables or platform defaults.
|
||||
func GetLogFolder() string {
|
||||
logFolderPath := os.Getenv("XUI_LOG_FOLDER")
|
||||
if logFolderPath == "" {
|
||||
logFolderPath = "/var/log"
|
||||
if logFolderPath != "" {
|
||||
return logFolderPath
|
||||
}
|
||||
return logFolderPath
|
||||
if runtime.GOOS == "windows" {
|
||||
return filepath.Join(".", "log")
|
||||
}
|
||||
return "/var/log/x-ui"
|
||||
}
|
||||
|
||||
func copyFile(src, dst string) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return out.Sync()
|
||||
}
|
||||
|
||||
func init() {
|
||||
if runtime.GOOS != "windows" {
|
||||
return
|
||||
}
|
||||
if os.Getenv("XUI_DB_FOLDER") != "" {
|
||||
return
|
||||
}
|
||||
oldDBFolder := "/etc/x-ui"
|
||||
oldDBPath := fmt.Sprintf("%s/%s.db", oldDBFolder, GetName())
|
||||
newDBFolder := GetDBFolderPath()
|
||||
newDBPath := fmt.Sprintf("%s/%s.db", newDBFolder, GetName())
|
||||
_, err := os.Stat(newDBPath)
|
||||
if err == nil {
|
||||
return // new exists
|
||||
}
|
||||
_, err = os.Stat(oldDBPath)
|
||||
if os.IsNotExist(err) {
|
||||
return // old does not exist
|
||||
}
|
||||
_ = copyFile(oldDBPath, newDBPath) // ignore error
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.3.10
|
||||
2.8.8
|
||||
110
database/db.go
@@ -1,16 +1,21 @@
|
||||
// Package database provides database initialization, migration, and management utilities
|
||||
// for the 3x-ui panel using GORM with SQLite.
|
||||
package database
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
|
||||
"x-ui/config"
|
||||
"x-ui/database/model"
|
||||
"x-ui/xray"
|
||||
"github.com/mhsanaei/3x-ui/v2/config"
|
||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/crypto"
|
||||
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
@@ -22,17 +27,17 @@ var db *gorm.DB
|
||||
const (
|
||||
defaultUsername = "admin"
|
||||
defaultPassword = "admin"
|
||||
defaultSecret = ""
|
||||
)
|
||||
|
||||
func initModels() error {
|
||||
models := []interface{}{
|
||||
models := []any{
|
||||
&model.User{},
|
||||
&model.Inbound{},
|
||||
&model.OutboundTraffics{},
|
||||
&model.Setting{},
|
||||
&model.InboundClientIps{},
|
||||
&xray.ClientTraffic{},
|
||||
&model.HistoryOfSeeders{},
|
||||
}
|
||||
for _, model := range models {
|
||||
if err := db.AutoMigrate(model); err != nil {
|
||||
@@ -43,6 +48,7 @@ func initModels() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// initUser creates a default admin user if the users table is empty.
|
||||
func initUser() error {
|
||||
empty, err := isTableEmpty("users")
|
||||
if err != nil {
|
||||
@@ -50,22 +56,70 @@ func initUser() error {
|
||||
return err
|
||||
}
|
||||
if empty {
|
||||
hashedPassword, err := crypto.HashPasswordAsBcrypt(defaultPassword)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error hashing default password: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
user := &model.User{
|
||||
Username: defaultUsername,
|
||||
Password: defaultPassword,
|
||||
LoginSecret: defaultSecret,
|
||||
Username: defaultUsername,
|
||||
Password: hashedPassword,
|
||||
}
|
||||
return db.Create(user).Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runSeeders migrates user passwords to bcrypt and records seeder execution to prevent re-running.
|
||||
func runSeeders(isUsersEmpty bool) error {
|
||||
empty, err := isTableEmpty("history_of_seeders")
|
||||
if err != nil {
|
||||
log.Printf("Error checking if users table is empty: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if empty && isUsersEmpty {
|
||||
hashSeeder := &model.HistoryOfSeeders{
|
||||
SeederName: "UserPasswordHash",
|
||||
}
|
||||
return db.Create(hashSeeder).Error
|
||||
} else {
|
||||
var seedersHistory []string
|
||||
db.Model(&model.HistoryOfSeeders{}).Pluck("seeder_name", &seedersHistory)
|
||||
|
||||
if !slices.Contains(seedersHistory, "UserPasswordHash") && !isUsersEmpty {
|
||||
var users []model.User
|
||||
db.Find(&users)
|
||||
|
||||
for _, user := range users {
|
||||
hashedPassword, err := crypto.HashPasswordAsBcrypt(user.Password)
|
||||
if err != nil {
|
||||
log.Printf("Error hashing password for user '%s': %v", user.Username, err)
|
||||
return err
|
||||
}
|
||||
db.Model(&user).Update("password", hashedPassword)
|
||||
}
|
||||
|
||||
hashSeeder := &model.HistoryOfSeeders{
|
||||
SeederName: "UserPasswordHash",
|
||||
}
|
||||
return db.Create(hashSeeder).Error
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isTableEmpty returns true if the named table contains zero rows.
|
||||
func isTableEmpty(tableName string) (bool, error) {
|
||||
var count int64
|
||||
err := db.Table(tableName).Count(&count).Error
|
||||
return count == 0, err
|
||||
}
|
||||
|
||||
// InitDB sets up the database connection, migrates models, and runs seeders.
|
||||
func InitDB(dbPath string) error {
|
||||
dir := path.Dir(dbPath)
|
||||
err := os.MkdirAll(dir, fs.ModePerm)
|
||||
@@ -92,13 +146,19 @@ func InitDB(dbPath string) error {
|
||||
if err := initModels(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := initUser(); err != nil {
|
||||
|
||||
isUsersEmpty, err := isTableEmpty("users")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
if err := initUser(); err != nil {
|
||||
return err
|
||||
}
|
||||
return runSeeders(isUsersEmpty)
|
||||
}
|
||||
|
||||
// CloseDB closes the database connection if it exists.
|
||||
func CloseDB() error {
|
||||
if db != nil {
|
||||
sqlDB, err := db.DB()
|
||||
@@ -110,14 +170,17 @@ func CloseDB() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDB returns the global GORM database instance.
|
||||
func GetDB() *gorm.DB {
|
||||
return db
|
||||
}
|
||||
|
||||
// IsNotFound checks if the given error is a GORM record not found error.
|
||||
func IsNotFound(err error) bool {
|
||||
return err == gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
// IsSQLiteDB checks if the given file is a valid SQLite database by reading its signature.
|
||||
func IsSQLiteDB(file io.ReaderAt) (bool, error) {
|
||||
signature := []byte("SQLite format 3\x00")
|
||||
buf := make([]byte, len(signature))
|
||||
@@ -128,6 +191,7 @@ func IsSQLiteDB(file io.ReaderAt) (bool, error) {
|
||||
return bytes.Equal(buf, signature), nil
|
||||
}
|
||||
|
||||
// Checkpoint performs a WAL checkpoint on the SQLite database to ensure data consistency.
|
||||
func Checkpoint() error {
|
||||
// Update WAL
|
||||
err := db.Exec("PRAGMA wal_checkpoint;").Error
|
||||
@@ -136,3 +200,29 @@ func Checkpoint() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSQLiteDB opens the provided sqlite DB path with a throw-away connection
|
||||
// and runs a PRAGMA integrity_check to ensure the file is structurally sound.
|
||||
// It does not mutate global state or run migrations.
|
||||
func ValidateSQLiteDB(dbPath string) error {
|
||||
if _, err := os.Stat(dbPath); err != nil { // file must exist
|
||||
return err
|
||||
}
|
||||
gdb, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{Logger: logger.Discard})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sqlDB, err := gdb.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sqlDB.Close()
|
||||
var res string
|
||||
if err := gdb.Raw("PRAGMA integrity_check;").Scan(&res).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if res != "ok" {
|
||||
return errors.New("sqlite integrity check failed: " + res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,44 +1,51 @@
|
||||
// Package model defines the database models and data structures used by the 3x-ui panel.
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"x-ui/util/json_util"
|
||||
"x-ui/xray"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/json_util"
|
||||
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||
)
|
||||
|
||||
// Protocol represents the protocol type for Xray inbounds.
|
||||
type Protocol string
|
||||
|
||||
// Protocol constants for different Xray inbound protocols
|
||||
const (
|
||||
VMess Protocol = "vmess"
|
||||
VMESS Protocol = "vmess"
|
||||
VLESS Protocol = "vless"
|
||||
DOKODEMO Protocol = "dokodemo-door"
|
||||
Tunnel Protocol = "tunnel"
|
||||
HTTP Protocol = "http"
|
||||
Trojan Protocol = "trojan"
|
||||
Shadowsocks Protocol = "shadowsocks"
|
||||
Socks Protocol = "socks"
|
||||
Mixed Protocol = "mixed"
|
||||
WireGuard Protocol = "wireguard"
|
||||
)
|
||||
|
||||
// User represents a user account in the 3x-ui panel.
|
||||
type User struct {
|
||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
LoginSecret string `json:"loginSecret"`
|
||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// Inbound represents an Xray inbound configuration with traffic statistics and settings.
|
||||
type Inbound struct {
|
||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||
UserId int `json:"-"`
|
||||
Up int64 `json:"up" form:"up"`
|
||||
Down int64 `json:"down" form:"down"`
|
||||
Total int64 `json:"total" form:"total"`
|
||||
Remark string `json:"remark" form:"remark"`
|
||||
Enable bool `json:"enable" form:"enable"`
|
||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
||||
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"`
|
||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` // Unique identifier
|
||||
UserId int `json:"-"` // Associated user ID
|
||||
Up int64 `json:"up" form:"up"` // Upload traffic in bytes
|
||||
Down int64 `json:"down" form:"down"` // Download traffic in bytes
|
||||
Total int64 `json:"total" form:"total"` // Total traffic limit in bytes
|
||||
AllTime int64 `json:"allTime" form:"allTime" gorm:"default:0"` // All-time traffic usage
|
||||
Remark string `json:"remark" form:"remark"` // Human-readable remark
|
||||
Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1"` // Whether the inbound is enabled
|
||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
|
||||
TrafficReset string `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2"` // Traffic reset schedule
|
||||
LastTrafficResetTime int64 `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"` // Last traffic reset timestamp
|
||||
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` // Client traffic statistics
|
||||
|
||||
// config part
|
||||
// Xray configuration fields
|
||||
Listen string `json:"listen" form:"listen"`
|
||||
Port int `json:"port" form:"port"`
|
||||
Protocol Protocol `json:"protocol" form:"protocol"`
|
||||
@@ -48,6 +55,7 @@ type Inbound struct {
|
||||
Sniffing string `json:"sniffing" form:"sniffing"`
|
||||
}
|
||||
|
||||
// OutboundTraffics tracks traffic statistics for Xray outbound connections.
|
||||
type OutboundTraffics struct {
|
||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Tag string `json:"tag" form:"tag" gorm:"unique"`
|
||||
@@ -56,17 +64,28 @@ type OutboundTraffics struct {
|
||||
Total int64 `json:"total" form:"total" gorm:"default:0"`
|
||||
}
|
||||
|
||||
// InboundClientIps stores IP addresses associated with inbound clients for access control.
|
||||
type InboundClientIps struct {
|
||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
|
||||
Ips string `json:"ips" form:"ips"`
|
||||
}
|
||||
|
||||
// HistoryOfSeeders tracks which database seeders have been executed to prevent re-running.
|
||||
type HistoryOfSeeders struct {
|
||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
SeederName string `json:"seederName"`
|
||||
}
|
||||
|
||||
// GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model.
|
||||
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
|
||||
listen := i.Listen
|
||||
if listen != "" {
|
||||
listen = fmt.Sprintf("\"%v\"", listen)
|
||||
// Default to 0.0.0.0 (all interfaces) when listen is empty
|
||||
// This ensures proper dual-stack IPv4/IPv6 binding in systems where bindv6only=0
|
||||
if listen == "" {
|
||||
listen = "0.0.0.0"
|
||||
}
|
||||
listen = fmt.Sprintf("\"%v\"", listen)
|
||||
return &xray.InboundConfig{
|
||||
Listen: json_util.RawMessage(listen),
|
||||
Port: i.Port,
|
||||
@@ -78,22 +97,28 @@ func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// Setting stores key-value configuration settings for the 3x-ui panel.
|
||||
type Setting struct {
|
||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Key string `json:"key" form:"key"`
|
||||
Value string `json:"value" form:"value"`
|
||||
}
|
||||
|
||||
// Client represents a client configuration for Xray inbounds with traffic limits and settings.
|
||||
type Client struct {
|
||||
ID string `json:"id"`
|
||||
Password string `json:"password"`
|
||||
Flow string `json:"flow"`
|
||||
Email string `json:"email"`
|
||||
LimitIP int `json:"limitIp"`
|
||||
TotalGB int64 `json:"totalGB" form:"totalGB"`
|
||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
||||
Enable bool `json:"enable" form:"enable"`
|
||||
TgID int64 `json:"tgId" form:"tgId"`
|
||||
SubID string `json:"subId" form:"subId"`
|
||||
Reset int `json:"reset" form:"reset"`
|
||||
ID string `json:"id"` // Unique client identifier
|
||||
Security string `json:"security"` // Security method (e.g., "auto", "aes-128-gcm")
|
||||
Password string `json:"password"` // Client password
|
||||
Flow string `json:"flow"` // Flow control (XTLS)
|
||||
Email string `json:"email"` // Client email identifier
|
||||
LimitIP int `json:"limitIp"` // IP limit for this client
|
||||
TotalGB int64 `json:"totalGB" form:"totalGB"` // Total traffic limit in GB
|
||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
|
||||
Enable bool `json:"enable" form:"enable"` // Whether the client is enabled
|
||||
TgID int64 `json:"tgId" form:"tgId"` // Telegram user ID for notifications
|
||||
SubID string `json:"subId" form:"subId"` // Subscription identifier
|
||||
Comment string `json:"comment" form:"comment"` // Client comment
|
||||
Reset int `json:"reset" form:"reset"` // Reset period in days
|
||||
CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp
|
||||
UpdatedAt int64 `json:"updated_at,omitempty"` // Last update timestamp
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
---
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
3x-ui:
|
||||
image: ghcr.io/mhsanaei/3x-ui:latest
|
||||
container_name: 3x-ui
|
||||
hostname: yourhostname
|
||||
3xui:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile
|
||||
container_name: 3xui_app
|
||||
# hostname: yourhostname <- optional
|
||||
volumes:
|
||||
- $PWD/db/:/etc/x-ui/
|
||||
- $PWD/cert/:/root/cert/
|
||||
environment:
|
||||
XRAY_VMESS_AEAD_FORCED: "false"
|
||||
XUI_ENABLE_FAIL2BAN: "true"
|
||||
tty: true
|
||||
network_mode: host
|
||||
restart: unless-stopped
|
||||
|
||||
143
go.mod
@@ -1,102 +1,105 @@
|
||||
module x-ui
|
||||
module github.com/mhsanaei/3x-ui/v2
|
||||
|
||||
go 1.22.5
|
||||
go 1.25.6
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/gzip v1.0.1
|
||||
github.com/gin-contrib/sessions v1.0.1
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/goccy/go-json v0.10.3
|
||||
github.com/mymmrac/telego v0.31.0
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0
|
||||
github.com/gin-contrib/gzip v1.2.5
|
||||
github.com/gin-contrib/sessions v1.0.4
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.12
|
||||
github.com/goccy/go-json v0.10.5
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mymmrac/telego v1.5.0
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pelletier/go-toml/v2 v2.2.2
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil/v4 v4.24.6
|
||||
github.com/valyala/fasthttp v1.55.0
|
||||
github.com/xtls/xray-core v1.8.21
|
||||
github.com/shirou/gopsutil/v4 v4.25.12
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/valyala/fasthttp v1.69.0
|
||||
github.com/xlzd/gotp v0.1.0
|
||||
github.com/xtls/xray-core v1.260118.0
|
||||
go.uber.org/atomic v1.11.0
|
||||
golang.org/x/text v0.16.0
|
||||
google.golang.org/grpc v1.65.0
|
||||
gorm.io/driver/sqlite v1.5.6
|
||||
gorm.io/gorm v1.25.11
|
||||
golang.org/x/crypto v0.47.0
|
||||
golang.org/x/sys v0.40.0
|
||||
golang.org/x/text v0.33.0
|
||||
google.golang.org/grpc v1.78.0
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/bytedance/sonic v1.11.9 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudflare/circl v1.3.9 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
|
||||
github.com/fasthttp/router v1.5.2 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.4 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.14.2 // indirect
|
||||
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.2 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.22.0 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/pprof v0.0.0-20240711041743-f6c9dda6c6da // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/gorilla/context v1.1.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.3.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/gorilla/sessions v1.4.0 // indirect
|
||||
github.com/grbit/go-json v0.11.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
github.com/juju/ratelimit v1.0.2 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.33 // indirect
|
||||
github.com/miekg/dns v1.1.70 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.19.0 // indirect
|
||||
github.com/pires/go-proxyproto v0.7.0 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/quic-go v0.45.1 // indirect
|
||||
github.com/refraction-networking/utls v1.6.7 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/refraction-networking/utls v1.8.2 // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
github.com/sagernet/sing v0.4.2 // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/sagernet/sing v0.7.14 // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||
github.com/tklauser/numcpus v0.8.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fastjson v1.6.4 // indirect
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2.0.20230316163032-ced5aaba43e3 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
github.com/xtls/reality v0.0.0-20240712055506-48f0b2d5ed6d // indirect
|
||||
github.com/valyala/fastjson v1.6.7 // indirect
|
||||
github.com/vishvananda/netlink v1.3.1 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
||||
golang.org/x/mod v0.19.0 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.23.0 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 // indirect
|
||||
lukechampine.com/blake3 v1.3.0 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20260109181451-4be7c433dae2 // indirect
|
||||
lukechampine.com/blake3 v1.4.1 // indirect
|
||||
)
|
||||
|
||||
509
go.sum
@@ -1,65 +1,51 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
|
||||
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
|
||||
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
|
||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0 h1:Wo41lDOevRJSGpevP+8Pk5bANX7fJacO2w04aqLiC5I=
|
||||
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0/go.mod h1:FVGavL/QEBQDcBpr3fAojoK17xX5k9bicBphrOpP7uM=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||
github.com/bytedance/sonic v1.11.9 h1:LFHENlIY/SLzDWverzdOvgMztTxcfcF+cqNsz9pK5zg=
|
||||
github.com/bytedance/sonic v1.11.9/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=
|
||||
github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
|
||||
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178 h1:bSq8n+gX4oO/qnM3MKf4kroW75n+phO9Qp6nigJKZ1E=
|
||||
github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178/go.mod h1:N1WIjPphkqs4efXWuyDNQ6OjjIK04vM3h+bEgwV+eVU=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
||||
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
|
||||
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
|
||||
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ=
|
||||
github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0=
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/fasthttp/router v1.5.2 h1:ckJCCdV7hWkkrMeId3WfEhz+4Gyyf6QPwxi/RHIMZ6I=
|
||||
github.com/fasthttp/router v1.5.2/go.mod h1:C8EY53ozOwpONyevc/V7Gr8pqnEjwnkFFqPo1alAGs0=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
|
||||
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mTEIGbvhcYU3S8+uSNkuMjx/qZFfhtM=
|
||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||
github.com/gin-contrib/gzip v1.0.1 h1:HQ8ENHODeLY7a4g1Au/46Z92bdGFl74OhxcZble9WJE=
|
||||
github.com/gin-contrib/gzip v1.0.1/go.mod h1:njt428fdUNRvjuJf16tZMYZ2Yl+WQB53X5wmhDwXvC4=
|
||||
github.com/gin-contrib/sessions v1.0.1 h1:3hsJyNs7v7N8OtelFmYXFrulAf6zSR7nW/putcPEHxI=
|
||||
github.com/gin-contrib/sessions v1.0.1/go.mod h1:ouxSFM24/OgIud5MJYQJLpy6AwxQ5EYO9yLhbtObGkM=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI=
|
||||
github.com/gin-contrib/gzip v1.2.5/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw=
|
||||
github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U=
|
||||
github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
@@ -69,343 +55,234 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
|
||||
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
|
||||
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20240711041743-f6c9dda6c6da h1:xRmpO92tb8y+Z85iUOMOicpCfaYcv7o3Cg3wKrIpg8g=
|
||||
github.com/google/pprof v0.0.0-20240711041743-f6c9dda6c6da/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
||||
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFzg=
|
||||
github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
|
||||
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
||||
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc=
|
||||
github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
||||
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
|
||||
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI=
|
||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
|
||||
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
|
||||
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
|
||||
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
|
||||
github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mymmrac/telego v0.31.0 h1:vsN+JCNkh7Z9vfL/2/AHZ2xBsRk2GCMj3zydjCxkgIc=
|
||||
github.com/mymmrac/telego v0.31.0/go.mod h1:MuqgVf2xXnIOWZs0prvsp3f4Yss80kCSjVEj4CRl7Ig=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
|
||||
github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
|
||||
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
|
||||
github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
|
||||
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
|
||||
github.com/mymmrac/telego v1.5.0 h1:VjBDZcSpEQim1Y3JX2WCsF/PJqOA2DKfZknXUvtKCnw=
|
||||
github.com/mymmrac/telego v1.5.0/go.mod h1:MDYHIeT68tURdcwH4SNCQQ+0xBC3u6wOcH2hBpa4Ip0=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
|
||||
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/quic-go v0.45.1 h1:tPfeYCk+uZHjmDRwHHQmvHRYL2t44ROTujLeFVBmjCA=
|
||||
github.com/quic-go/quic-go v0.45.1/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI=
|
||||
github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
|
||||
github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
|
||||
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/sagernet/sing v0.4.2 h1:jzGNJdZVRI0xlAfFugsIQUPvyB9SuWvbJK7zQCXc4QM=
|
||||
github.com/sagernet/sing v0.4.2/go.mod h1:ieZHA/+Y9YZfXs2I3WtuwgyCZ6GPsIR7HdKb1SdEnls=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
|
||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
|
||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sagernet/sing v0.7.14 h1:5QQRDCUvYNOMyVp3LuK/hYEBAIv0VsbD3x/l9zH467s=
|
||||
github.com/sagernet/sing v0.7.14/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shirou/gopsutil/v4 v4.24.6 h1:9qqCSYF2pgOU+t+NgJtp7Co5+5mHF/HyKBUckySQL64=
|
||||
github.com/shirou/gopsutil/v4 v4.24.6/go.mod h1:aoebb2vxetJ/yIDZISmduFvVNPHqXQ9SEJwRXxkf0RA=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
|
||||
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
||||
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
|
||||
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
|
||||
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
|
||||
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
|
||||
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
|
||||
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
|
||||
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
||||
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||
github.com/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY=
|
||||
github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
||||
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
||||
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
|
||||
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
|
||||
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
|
||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
|
||||
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2.0.20230316163032-ced5aaba43e3 h1:tkMT5pTye+1NlKIXETU78NXw0fyjnaNHmJyyLyzw8+U=
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2.0.20230316163032-ced5aaba43e3/go.mod h1:cAAsePK2e15YDAMJNyOpGYEWNe4sIghTY7gpz4cX/Ik=
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/xtls/reality v0.0.0-20240712055506-48f0b2d5ed6d h1:+B97uD9uHLgAAulhigmys4BVwZZypzK7gPN3WtpgRJg=
|
||||
github.com/xtls/reality v0.0.0-20240712055506-48f0b2d5ed6d/go.mod h1:dm4y/1QwzjGaK17ofi0Vs6NpKAHegZky8qk6J2JJZAE=
|
||||
github.com/xtls/xray-core v1.8.21 h1:cNdepud+R9PENKzXlSZsq0je4BWI6liXAuep6CD6xvk=
|
||||
github.com/xtls/xray-core v1.8.21/go.mod h1:0CwyMPNA5Cs+ukPXHbYQGgne/ug0PuXOSVqBu7zyXOc=
|
||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||
github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM=
|
||||
github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
|
||||
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
|
||||
github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237 h1:UXjrmniKlY+ZbIqpN91lejB3pszQQQRVu1vqH/p/aGM=
|
||||
github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237/go.mod h1:vbHCV/3VWUvy1oKvTxxWJRPEWSeR1sYgQHIh6u/JiZQ=
|
||||
github.com/xtls/xray-core v1.260118.0 h1:RJtgIbQ3ykFRcH1CKeoCgQ5WvhsMFu+lnvLF/fFHagE=
|
||||
github.com/xtls/xray-core v1.260118.0/go.mod h1:A5k7TXE2KfAjT8dAq6Ir4mMP1q0OTh+8VMmUdqWMQpg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
||||
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
||||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 h1:C4WAdL+FbjnGlpp2S+HMVhBeCq2Lcib4xZqfPNF6OoQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
|
||||
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 h1:ze1vwAdliUAr68RQ5NtufWaXaOg8WUO2OACzEV+TNdE=
|
||||
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
||||
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
gvisor.dev/gvisor v0.0.0-20260109181451-4be7c433dae2 h1:fr6L00yGG2RP5NMea6njWpdC+bm+cMdFClrSpaicp1c=
|
||||
gvisor.dev/gvisor v0.0.0-20260109181451-4be7c433dae2/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
|
||||
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
|
||||
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
|
||||
|
||||
1032
install.sh
158
logger/logger.go
@@ -1,15 +1,29 @@
|
||||
// Package logger provides logging functionality for the 3x-ui panel with
|
||||
// dual-backend logging (console/syslog and file) and buffered log storage for web UI.
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/config"
|
||||
"github.com/op/go-logging"
|
||||
)
|
||||
|
||||
const (
|
||||
maxLogBufferSize = 10240 // Maximum log entries kept in memory
|
||||
logFileName = "3xui.log" // Log file name
|
||||
timeFormat = "2006/01/02 15:04:05" // Log timestamp format
|
||||
)
|
||||
|
||||
var (
|
||||
logger *logging.Logger
|
||||
logger *logging.Logger
|
||||
logFile *os.File
|
||||
|
||||
// logBuffer maintains recent log entries in memory for web UI retrieval
|
||||
logBuffer []struct {
|
||||
time string
|
||||
level logging.Level
|
||||
@@ -17,89 +31,164 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
InitLogger(logging.INFO)
|
||||
}
|
||||
|
||||
// InitLogger initializes dual logging backends: console/syslog and file.
|
||||
// Console logging uses the specified level, file logging always uses DEBUG level.
|
||||
func InitLogger(level logging.Level) {
|
||||
newLogger := logging.MustGetLogger("x-ui")
|
||||
var err error
|
||||
var backend logging.Backend
|
||||
var format logging.Formatter
|
||||
ppid := os.Getppid()
|
||||
backends := make([]logging.Backend, 0, 2)
|
||||
|
||||
backend, err = logging.NewSyslogBackend("")
|
||||
if err != nil {
|
||||
println(err)
|
||||
backend = logging.NewLogBackend(os.Stderr, "", 0)
|
||||
}
|
||||
if ppid > 0 && err != nil {
|
||||
format = logging.MustStringFormatter(`%{time:2006/01/02 15:04:05} %{level} - %{message}`)
|
||||
} else {
|
||||
format = logging.MustStringFormatter(`%{level} - %{message}`)
|
||||
// Console/syslog backend with configurable level
|
||||
if consoleBackend := initDefaultBackend(); consoleBackend != nil {
|
||||
leveledBackend := logging.AddModuleLevel(consoleBackend)
|
||||
leveledBackend.SetLevel(level, "x-ui")
|
||||
backends = append(backends, leveledBackend)
|
||||
}
|
||||
|
||||
backendFormatter := logging.NewBackendFormatter(backend, format)
|
||||
backendLeveled := logging.AddModuleLevel(backendFormatter)
|
||||
backendLeveled.SetLevel(level, "x-ui")
|
||||
newLogger.SetBackend(backendLeveled)
|
||||
// File backend with DEBUG level for comprehensive logging
|
||||
if fileBackend := initFileBackend(); fileBackend != nil {
|
||||
leveledBackend := logging.AddModuleLevel(fileBackend)
|
||||
leveledBackend.SetLevel(logging.DEBUG, "x-ui")
|
||||
backends = append(backends, leveledBackend)
|
||||
}
|
||||
|
||||
multiBackend := logging.MultiLogger(backends...)
|
||||
newLogger.SetBackend(multiBackend)
|
||||
logger = newLogger
|
||||
}
|
||||
|
||||
func Debug(args ...interface{}) {
|
||||
// initDefaultBackend creates the console/syslog logging backend.
|
||||
// Windows: Uses stderr directly (no syslog support)
|
||||
// Unix-like: Attempts syslog, falls back to stderr
|
||||
func initDefaultBackend() logging.Backend {
|
||||
var backend logging.Backend
|
||||
includeTime := false
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows: Use stderr directly (no syslog support)
|
||||
backend = logging.NewLogBackend(os.Stderr, "", 0)
|
||||
includeTime = true
|
||||
} else {
|
||||
// Unix-like: Try syslog, fallback to stderr
|
||||
if syslogBackend, err := logging.NewSyslogBackend(""); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "syslog backend disabled: %v\n", err)
|
||||
backend = logging.NewLogBackend(os.Stderr, "", 0)
|
||||
includeTime = os.Getppid() > 0
|
||||
} else {
|
||||
backend = syslogBackend
|
||||
}
|
||||
}
|
||||
|
||||
return logging.NewBackendFormatter(backend, newFormatter(includeTime))
|
||||
}
|
||||
|
||||
// initFileBackend creates the file logging backend.
|
||||
// Creates log directory and truncates log file on startup for fresh logs.
|
||||
func initFileBackend() logging.Backend {
|
||||
logDir := config.GetLogFolder()
|
||||
if err := os.MkdirAll(logDir, 0o750); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to create log folder %s: %v\n", logDir, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
logPath := filepath.Join(logDir, logFileName)
|
||||
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o660)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to open log file %s: %v\n", logPath, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close previous log file if exists
|
||||
if logFile != nil {
|
||||
_ = logFile.Close()
|
||||
}
|
||||
logFile = file
|
||||
|
||||
backend := logging.NewLogBackend(file, "", 0)
|
||||
return logging.NewBackendFormatter(backend, newFormatter(true))
|
||||
}
|
||||
|
||||
// newFormatter creates a log formatter with optional timestamp.
|
||||
func newFormatter(withTime bool) logging.Formatter {
|
||||
format := `%{level} - %{message}`
|
||||
if withTime {
|
||||
format = `%{time:` + timeFormat + `} %{level} - %{message}`
|
||||
}
|
||||
return logging.MustStringFormatter(format)
|
||||
}
|
||||
|
||||
// CloseLogger closes the log file and cleans up resources.
|
||||
// Should be called during application shutdown.
|
||||
func CloseLogger() {
|
||||
if logFile != nil {
|
||||
_ = logFile.Close()
|
||||
logFile = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Debug logs a debug message and adds it to the log buffer.
|
||||
func Debug(args ...any) {
|
||||
logger.Debug(args...)
|
||||
addToBuffer("DEBUG", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func Debugf(format string, args ...interface{}) {
|
||||
// Debugf logs a formatted debug message and adds it to the log buffer.
|
||||
func Debugf(format string, args ...any) {
|
||||
logger.Debugf(format, args...)
|
||||
addToBuffer("DEBUG", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func Info(args ...interface{}) {
|
||||
// Info logs an info message and adds it to the log buffer.
|
||||
func Info(args ...any) {
|
||||
logger.Info(args...)
|
||||
addToBuffer("INFO", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func Infof(format string, args ...interface{}) {
|
||||
// Infof logs a formatted info message and adds it to the log buffer.
|
||||
func Infof(format string, args ...any) {
|
||||
logger.Infof(format, args...)
|
||||
addToBuffer("INFO", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func Notice(args ...interface{}) {
|
||||
// Notice logs a notice message and adds it to the log buffer.
|
||||
func Notice(args ...any) {
|
||||
logger.Notice(args...)
|
||||
addToBuffer("NOTICE", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func Noticef(format string, args ...interface{}) {
|
||||
// Noticef logs a formatted notice message and adds it to the log buffer.
|
||||
func Noticef(format string, args ...any) {
|
||||
logger.Noticef(format, args...)
|
||||
addToBuffer("NOTICE", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func Warning(args ...interface{}) {
|
||||
// Warning logs a warning message and adds it to the log buffer.
|
||||
func Warning(args ...any) {
|
||||
logger.Warning(args...)
|
||||
addToBuffer("WARNING", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func Warningf(format string, args ...interface{}) {
|
||||
// Warningf logs a formatted warning message and adds it to the log buffer.
|
||||
func Warningf(format string, args ...any) {
|
||||
logger.Warningf(format, args...)
|
||||
addToBuffer("WARNING", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func Error(args ...interface{}) {
|
||||
// Error logs an error message and adds it to the log buffer.
|
||||
func Error(args ...any) {
|
||||
logger.Error(args...)
|
||||
addToBuffer("ERROR", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func Errorf(format string, args ...interface{}) {
|
||||
// Errorf logs a formatted error message and adds it to the log buffer.
|
||||
func Errorf(format string, args ...any) {
|
||||
logger.Errorf(format, args...)
|
||||
addToBuffer("ERROR", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// addToBuffer adds a log entry to the in-memory ring buffer for web UI retrieval.
|
||||
func addToBuffer(level string, newLog string) {
|
||||
t := time.Now()
|
||||
if len(logBuffer) >= 10240 {
|
||||
if len(logBuffer) >= maxLogBufferSize {
|
||||
logBuffer = logBuffer[1:]
|
||||
}
|
||||
|
||||
@@ -109,12 +198,13 @@ func addToBuffer(level string, newLog string) {
|
||||
level logging.Level
|
||||
log string
|
||||
}{
|
||||
time: t.Format("2006/01/02 15:04:05"),
|
||||
time: t.Format(timeFormat),
|
||||
level: logLevel,
|
||||
log: newLog,
|
||||
})
|
||||
}
|
||||
|
||||
// GetLogs retrieves up to c log entries from the buffer that are at or below the specified level.
|
||||
func GetLogs(c int, level string) []string {
|
||||
var output []string
|
||||
logLevel, _ := logging.LogLevel(level)
|
||||
|
||||
195
main.go
@@ -1,3 +1,5 @@
|
||||
// Package main is the entry point for the 3x-ui web panel application.
|
||||
// It initializes the database, web server, and handles command-line operations for managing the panel.
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -9,17 +11,20 @@ import (
|
||||
"syscall"
|
||||
_ "unsafe"
|
||||
|
||||
"x-ui/config"
|
||||
"x-ui/database"
|
||||
"x-ui/logger"
|
||||
"x-ui/sub"
|
||||
"x-ui/web"
|
||||
"x-ui/web/global"
|
||||
"x-ui/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v2/config"
|
||||
"github.com/mhsanaei/3x-ui/v2/database"
|
||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||
"github.com/mhsanaei/3x-ui/v2/sub"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/crypto"
|
||||
"github.com/mhsanaei/3x-ui/v2/web"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/global"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/op/go-logging"
|
||||
)
|
||||
|
||||
// runWebServer initializes and starts the web server for the 3x-ui panel.
|
||||
func runWebServer() {
|
||||
log.Printf("Starting %v %v", config.GetName(), config.GetVersion())
|
||||
|
||||
@@ -30,7 +35,7 @@ func runWebServer() {
|
||||
logger.InitLogger(logging.INFO)
|
||||
case config.Notice:
|
||||
logger.InitLogger(logging.NOTICE)
|
||||
case config.Warn:
|
||||
case config.Warning:
|
||||
logger.InitLogger(logging.WARNING)
|
||||
case config.Error:
|
||||
logger.InitLogger(logging.ERROR)
|
||||
@@ -38,6 +43,8 @@ func runWebServer() {
|
||||
log.Fatalf("Unknown log level: %v", config.GetLogLevel())
|
||||
}
|
||||
|
||||
godotenv.Load()
|
||||
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
log.Fatalf("Error initializing database: %v", err)
|
||||
@@ -71,13 +78,17 @@ func runWebServer() {
|
||||
case syscall.SIGHUP:
|
||||
logger.Info("Received SIGHUP signal. Restarting servers...")
|
||||
|
||||
// --- FIX FOR TELEGRAM BOT CONFLICT (409): Stop bot before restart ---
|
||||
service.StopBot()
|
||||
// --
|
||||
|
||||
err := server.Stop()
|
||||
if err != nil {
|
||||
logger.Warning("Error stopping web server:", err)
|
||||
logger.Debug("Error stopping web server:", err)
|
||||
}
|
||||
err = subServer.Stop()
|
||||
if err != nil {
|
||||
logger.Warning("Error stopping sub server:", err)
|
||||
logger.Debug("Error stopping sub server:", err)
|
||||
}
|
||||
|
||||
server = web.NewServer()
|
||||
@@ -99,6 +110,10 @@ func runWebServer() {
|
||||
log.Println("Sub server restarted successfully.")
|
||||
|
||||
default:
|
||||
// --- FIX FOR TELEGRAM BOT CONFLICT (409) on full shutdown ---
|
||||
service.StopBot()
|
||||
// ------------------------------------------------------------
|
||||
|
||||
server.Stop()
|
||||
subServer.Stop()
|
||||
log.Println("Shutting down servers.")
|
||||
@@ -107,6 +122,7 @@ func runWebServer() {
|
||||
}
|
||||
}
|
||||
|
||||
// resetSetting resets all panel settings to their default values.
|
||||
func resetSetting() {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
@@ -123,6 +139,7 @@ func resetSetting() {
|
||||
}
|
||||
}
|
||||
|
||||
// showSetting displays the current panel settings if show is true.
|
||||
func showSetting(show bool) {
|
||||
if show {
|
||||
settingService := service.SettingService{}
|
||||
@@ -136,30 +153,43 @@ func showSetting(show bool) {
|
||||
fmt.Println("get webBasePath failed, error info:", err)
|
||||
}
|
||||
|
||||
certFile, err := settingService.GetCertFile()
|
||||
if err != nil {
|
||||
fmt.Println("get cert file failed, error info:", err)
|
||||
}
|
||||
keyFile, err := settingService.GetKeyFile()
|
||||
if err != nil {
|
||||
fmt.Println("get key file failed, error info:", err)
|
||||
}
|
||||
|
||||
userService := service.UserService{}
|
||||
userModel, err := userService.GetFirstUser()
|
||||
if err != nil {
|
||||
fmt.Println("get current user info failed, error info:", err)
|
||||
}
|
||||
|
||||
username := userModel.Username
|
||||
userpasswd := userModel.Password
|
||||
if username == "" || userpasswd == "" {
|
||||
if userModel.Username == "" || userModel.Password == "" {
|
||||
fmt.Println("current username or password is empty")
|
||||
}
|
||||
|
||||
fmt.Println("current panel settings as follows:")
|
||||
fmt.Println("username:", username)
|
||||
fmt.Println("password:", userpasswd)
|
||||
fmt.Println("port:", port)
|
||||
if webBasePath != "" {
|
||||
fmt.Println("webBasePath:", webBasePath)
|
||||
if certFile == "" || keyFile == "" {
|
||||
fmt.Println("Warning: Panel is not secure with SSL")
|
||||
} else {
|
||||
fmt.Println("webBasePath is not set")
|
||||
fmt.Println("Panel is secure with SSL")
|
||||
}
|
||||
|
||||
hasDefaultCredential := func() bool {
|
||||
return userModel.Username == "admin" && crypto.CheckPasswordHash(userModel.Password, "admin")
|
||||
}()
|
||||
|
||||
fmt.Println("hasDefaultCredential:", hasDefaultCredential)
|
||||
fmt.Println("port:", port)
|
||||
fmt.Println("webBasePath:", webBasePath)
|
||||
}
|
||||
}
|
||||
|
||||
// updateTgbotEnableSts enables or disables the Telegram bot notifications based on the status parameter.
|
||||
func updateTgbotEnableSts(status bool) {
|
||||
settingService := service.SettingService{}
|
||||
currentTgSts, err := settingService.GetTgbotEnabled()
|
||||
@@ -179,6 +209,7 @@ func updateTgbotEnableSts(status bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// updateTgbotSetting updates Telegram bot settings including token, chat ID, and runtime schedule.
|
||||
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
@@ -216,7 +247,8 @@ func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime stri
|
||||
}
|
||||
}
|
||||
|
||||
func updateSetting(port int, username string, password string, webBasePath string) {
|
||||
// updateSetting updates various panel settings including port, credentials, base path, listen IP, and two-factor authentication.
|
||||
func updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool) {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
fmt.Println("Database initialization failed:", err)
|
||||
@@ -252,8 +284,29 @@ func updateSetting(port int, username string, password string, webBasePath strin
|
||||
fmt.Println("Base URI path set successfully")
|
||||
}
|
||||
}
|
||||
|
||||
if resetTwoFactor {
|
||||
err := settingService.SetTwoFactorEnable(false)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Failed to reset two-factor authentication:", err)
|
||||
} else {
|
||||
settingService.SetTwoFactorToken("")
|
||||
fmt.Println("Two-factor authentication reset successfully")
|
||||
}
|
||||
}
|
||||
|
||||
if listenIP != "" {
|
||||
err := settingService.SetListen(listenIP)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to set listen IP:", err)
|
||||
} else {
|
||||
fmt.Printf("listen %v set successfully", listenIP)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateCert updates the SSL certificate files for the panel.
|
||||
func updateCert(publicKey string, privateKey string) {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
@@ -276,11 +329,59 @@ func updateCert(publicKey string, privateKey string) {
|
||||
} else {
|
||||
fmt.Println("set certificate private key success")
|
||||
}
|
||||
|
||||
err = settingService.SetSubCertFile(publicKey)
|
||||
if err != nil {
|
||||
fmt.Println("set certificate for subscription public key failed:", err)
|
||||
} else {
|
||||
fmt.Println("set certificate for subscription public key success")
|
||||
}
|
||||
|
||||
err = settingService.SetSubKeyFile(privateKey)
|
||||
if err != nil {
|
||||
fmt.Println("set certificate for subscription private key failed:", err)
|
||||
} else {
|
||||
fmt.Println("set certificate for subscription private key success")
|
||||
}
|
||||
} else {
|
||||
fmt.Println("both public and private key should be entered.")
|
||||
}
|
||||
}
|
||||
|
||||
// GetCertificate displays the current SSL certificate settings if getCert is true.
|
||||
func GetCertificate(getCert bool) {
|
||||
if getCert {
|
||||
settingService := service.SettingService{}
|
||||
certFile, err := settingService.GetCertFile()
|
||||
if err != nil {
|
||||
fmt.Println("get cert file failed, error info:", err)
|
||||
}
|
||||
keyFile, err := settingService.GetKeyFile()
|
||||
if err != nil {
|
||||
fmt.Println("get key file failed, error info:", err)
|
||||
}
|
||||
|
||||
fmt.Println("cert:", certFile)
|
||||
fmt.Println("key:", keyFile)
|
||||
}
|
||||
}
|
||||
|
||||
// GetListenIP displays the current panel listen IP address if getListen is true.
|
||||
func GetListenIP(getListen bool) {
|
||||
if getListen {
|
||||
|
||||
settingService := service.SettingService{}
|
||||
ListenIP, err := settingService.GetListen()
|
||||
if err != nil {
|
||||
log.Printf("Failed to retrieve listen IP: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("listenIP:", ListenIP)
|
||||
}
|
||||
}
|
||||
|
||||
// migrateDb performs database migration operations for the 3x-ui panel.
|
||||
func migrateDb() {
|
||||
inboundService := service.InboundService{}
|
||||
|
||||
@@ -293,36 +394,8 @@ func migrateDb() {
|
||||
fmt.Println("Migration done!")
|
||||
}
|
||||
|
||||
func removeSecret() {
|
||||
userService := service.UserService{}
|
||||
|
||||
secretExists, err := userService.CheckSecretExistence()
|
||||
if err != nil {
|
||||
fmt.Println("Error checking secret existence:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !secretExists {
|
||||
fmt.Println("No secret exists to remove.")
|
||||
return
|
||||
}
|
||||
|
||||
err = userService.RemoveUserSecret()
|
||||
if err != nil {
|
||||
fmt.Println("Error removing secret:", err)
|
||||
return
|
||||
}
|
||||
|
||||
settingService := service.SettingService{}
|
||||
err = settingService.SetSecretStatus(false)
|
||||
if err != nil {
|
||||
fmt.Println("Error updating secret status:", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Secret removed successfully.")
|
||||
}
|
||||
|
||||
// main is the entry point of the 3x-ui application.
|
||||
// It parses command-line arguments to run the web server, migrate database, or update settings.
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
runWebServer()
|
||||
@@ -339,6 +412,8 @@ func main() {
|
||||
var username string
|
||||
var password string
|
||||
var webBasePath string
|
||||
var listenIP string
|
||||
var getListen bool
|
||||
var webCertFile string
|
||||
var webKeyFile string
|
||||
var tgbottoken string
|
||||
@@ -347,14 +422,18 @@ func main() {
|
||||
var tgbotRuntime string
|
||||
var reset bool
|
||||
var show bool
|
||||
var remove_secret bool
|
||||
var getCert bool
|
||||
var resetTwoFactor bool
|
||||
settingCmd.BoolVar(&reset, "reset", false, "Reset all settings")
|
||||
settingCmd.BoolVar(&show, "show", false, "Display current settings")
|
||||
settingCmd.BoolVar(&remove_secret, "remove_secret", false, "Remove secret key")
|
||||
settingCmd.IntVar(&port, "port", 0, "Set panel port number")
|
||||
settingCmd.StringVar(&username, "username", "", "Set login username")
|
||||
settingCmd.StringVar(&password, "password", "", "Set login password")
|
||||
settingCmd.StringVar(&webBasePath, "webBasePath", "", "Set base path for Panel")
|
||||
settingCmd.StringVar(&listenIP, "listenIP", "", "set panel listenIP IP")
|
||||
settingCmd.BoolVar(&resetTwoFactor, "resetTwoFactor", false, "Reset two-factor authentication settings")
|
||||
settingCmd.BoolVar(&getListen, "getListen", false, "Display current panel listenIP IP")
|
||||
settingCmd.BoolVar(&getCert, "getCert", false, "Display current certificate settings")
|
||||
settingCmd.StringVar(&webCertFile, "webCert", "", "Set path to public key file for panel")
|
||||
settingCmd.StringVar(&webKeyFile, "webCertKey", "", "Set path to private key file for panel")
|
||||
settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "Set token for Telegram bot")
|
||||
@@ -397,17 +476,20 @@ func main() {
|
||||
if reset {
|
||||
resetSetting()
|
||||
} else {
|
||||
updateSetting(port, username, password, webBasePath)
|
||||
updateSetting(port, username, password, webBasePath, listenIP, resetTwoFactor)
|
||||
}
|
||||
if show {
|
||||
showSetting(show)
|
||||
}
|
||||
if getListen {
|
||||
GetListenIP(getListen)
|
||||
}
|
||||
if getCert {
|
||||
GetCertificate(getCert)
|
||||
}
|
||||
if (tgbottoken != "") || (tgbotchatid != "") || (tgbotRuntime != "") {
|
||||
updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
|
||||
}
|
||||
if remove_secret {
|
||||
removeSecret()
|
||||
}
|
||||
if enabletgbot {
|
||||
updateTgbotEnableSts(enabletgbot)
|
||||
}
|
||||
@@ -422,7 +504,6 @@ func main() {
|
||||
} else {
|
||||
updateCert(webCertFile, webKeyFile)
|
||||
}
|
||||
|
||||
default:
|
||||
fmt.Println("Invalid subcommands")
|
||||
fmt.Println()
|
||||
|
||||
BIN
media/01-overview-dark.png
Normal file
|
After Width: | Height: | Size: 253 KiB |
BIN
media/01-overview-light.png
Normal file
|
After Width: | Height: | Size: 248 KiB |
BIN
media/02-inbounds-dark.png
Normal file
|
After Width: | Height: | Size: 240 KiB |
BIN
media/02-inbounds-light.png
Normal file
|
After Width: | Height: | Size: 240 KiB |
BIN
media/03-add-inbound-dark.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
media/03-add-inbound-light.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
media/04-add-client-dark.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
media/04-add-client-light.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
media/05-settings-dark.png
Normal file
|
After Width: | Height: | Size: 277 KiB |
BIN
media/05-settings-light.png
Normal file
|
After Width: | Height: | Size: 266 KiB |
BIN
media/06-configs-dark.png
Normal file
|
After Width: | Height: | Size: 202 KiB |
BIN
media/06-configs-light.png
Normal file
|
After Width: | Height: | Size: 206 KiB |
BIN
media/07-bot-dark.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
media/07-bot-light.png
Normal file
|
After Width: | Height: | Size: 255 KiB |
BIN
media/1.png
|
Before Width: | Height: | Size: 150 KiB |
BIN
media/2.png
|
Before Width: | Height: | Size: 214 KiB |
BIN
media/3.png
|
Before Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 226 KiB After Width: | Height: | Size: 226 KiB |
BIN
media/3x-ui-light.png
Normal file
|
After Width: | Height: | Size: 224 KiB |
BIN
media/4.png
|
Before Width: | Height: | Size: 52 KiB |
BIN
media/5.png
|
Before Width: | Height: | Size: 165 KiB |
BIN
media/6.png
|
Before Width: | Height: | Size: 88 KiB |
BIN
media/7.png
|
Before Width: | Height: | Size: 261 KiB |
BIN
media/default-yellow.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
1
media/donation-button-black.svg
Normal file
|
After Width: | Height: | Size: 10 KiB |
@@ -13,7 +13,7 @@
|
||||
"inbounds": [
|
||||
{
|
||||
"port": 10808,
|
||||
"protocol": "socks",
|
||||
"protocol": "mixed",
|
||||
"settings": {
|
||||
"auth": "noauth",
|
||||
"udp": true,
|
||||
@@ -23,11 +23,12 @@
|
||||
"destOverride": [
|
||||
"http",
|
||||
"tls",
|
||||
"quic",
|
||||
"fakedns"
|
||||
],
|
||||
"enabled": true
|
||||
},
|
||||
"tag": "socks"
|
||||
"tag": "mixed"
|
||||
},
|
||||
{
|
||||
"port": 10809,
|
||||
@@ -46,7 +47,9 @@
|
||||
"tag": "direct",
|
||||
"protocol": "freedom",
|
||||
"settings": {
|
||||
"domainStrategy": "UseIP"
|
||||
"domainStrategy": "AsIs",
|
||||
"redirect": "",
|
||||
"noises": []
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
182
sub/sub.go
@@ -1,23 +1,47 @@
|
||||
// Package sub provides subscription server functionality for the 3x-ui panel,
|
||||
// including HTTP/HTTPS servers for serving subscription links and JSON configurations.
|
||||
package sub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"x-ui/config"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
"x-ui/web/middleware"
|
||||
"x-ui/web/network"
|
||||
"x-ui/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||
webpkg "github.com/mhsanaei/3x-ui/v2/web"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/locale"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/middleware"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/network"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// setEmbeddedTemplates parses and sets embedded templates on the engine
|
||||
func setEmbeddedTemplates(engine *gin.Engine) error {
|
||||
t, err := template.New("").Funcs(engine.FuncMap).ParseFS(
|
||||
webpkg.EmbeddedHTML(),
|
||||
"html/common/page.html",
|
||||
"html/component/aThemeSwitch.html",
|
||||
"html/settings/panel/subscription/subpage.html",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
engine.SetHTMLTemplate(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Server represents the subscription server that serves subscription links and JSON configurations.
|
||||
type Server struct {
|
||||
httpServer *http.Server
|
||||
listener net.Listener
|
||||
@@ -29,6 +53,7 @@ type Server struct {
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// NewServer creates a new subscription server instance with a cancellable context.
|
||||
func NewServer() *Server {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Server{
|
||||
@@ -37,14 +62,13 @@ func NewServer() *Server {
|
||||
}
|
||||
}
|
||||
|
||||
// initRouter configures the subscription server's Gin engine, middleware,
|
||||
// templates and static assets and returns the ready-to-use engine.
|
||||
func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
if config.IsDebug() {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
} else {
|
||||
gin.DefaultWriter = io.Discard
|
||||
gin.DefaultErrorWriter = io.Discard
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
// Always run in release mode for the subscription server
|
||||
gin.DefaultWriter = io.Discard
|
||||
gin.DefaultErrorWriter = io.Discard
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
engine := gin.Default()
|
||||
|
||||
@@ -67,6 +91,23 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Determine if JSON subscription endpoint is enabled
|
||||
subJsonEnable, err := s.settingService.GetSubJsonEnable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set base_path based on LinksPath for template rendering
|
||||
// Ensure LinksPath ends with "/" for proper asset URL generation
|
||||
basePath := LinksPath
|
||||
if basePath != "/" && !strings.HasSuffix(basePath, "/") {
|
||||
basePath += "/"
|
||||
}
|
||||
// logger.Debug("sub: Setting base_path to:", basePath)
|
||||
engine.Use(func(c *gin.Context) {
|
||||
c.Set("base_path", basePath)
|
||||
})
|
||||
|
||||
Encrypt, err := s.settingService.GetSubEncrypt()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -92,6 +133,11 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
SubJsonFragment = ""
|
||||
}
|
||||
|
||||
SubJsonNoises, err := s.settingService.GetSubJsonNoises()
|
||||
if err != nil {
|
||||
SubJsonNoises = ""
|
||||
}
|
||||
|
||||
SubJsonMux, err := s.settingService.GetSubJsonMux()
|
||||
if err != nil {
|
||||
SubJsonMux = ""
|
||||
@@ -102,15 +148,119 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
SubJsonRules = ""
|
||||
}
|
||||
|
||||
SubTitle, err := s.settingService.GetSubTitle()
|
||||
if err != nil {
|
||||
SubTitle = ""
|
||||
}
|
||||
|
||||
// set per-request localizer from headers/cookies
|
||||
engine.Use(locale.LocalizerMiddleware())
|
||||
|
||||
// register i18n function similar to web server
|
||||
i18nWebFunc := func(key string, params ...string) string {
|
||||
return locale.I18n(locale.Web, key, params...)
|
||||
}
|
||||
engine.SetFuncMap(map[string]any{"i18n": i18nWebFunc})
|
||||
|
||||
// Templates: prefer embedded; fallback to disk if necessary
|
||||
if err := setEmbeddedTemplates(engine); err != nil {
|
||||
logger.Warning("sub: failed to parse embedded templates:", err)
|
||||
if files, derr := s.getHtmlFiles(); derr == nil {
|
||||
engine.LoadHTMLFiles(files...)
|
||||
} else {
|
||||
logger.Error("sub: no templates available (embedded parse and disk load failed)", err, derr)
|
||||
}
|
||||
}
|
||||
|
||||
// Assets: use disk if present, fallback to embedded
|
||||
// Serve under both root (/assets) and under the subscription path prefix (LinksPath + "assets")
|
||||
// so reverse proxies with a URI prefix can load assets correctly.
|
||||
// Determine LinksPath earlier to compute prefixed assets mount.
|
||||
// Note: LinksPath always starts and ends with "/" (validated in settings).
|
||||
var linksPathForAssets string
|
||||
if LinksPath == "/" {
|
||||
linksPathForAssets = "/assets"
|
||||
} else {
|
||||
// ensure single slash join
|
||||
linksPathForAssets = strings.TrimRight(LinksPath, "/") + "/assets"
|
||||
}
|
||||
|
||||
// Mount assets in multiple paths to handle different URL patterns
|
||||
var assetsFS http.FileSystem
|
||||
if _, err := os.Stat("web/assets"); err == nil {
|
||||
assetsFS = http.FS(os.DirFS("web/assets"))
|
||||
} else {
|
||||
if subFS, err := fs.Sub(webpkg.EmbeddedAssets(), "assets"); err == nil {
|
||||
assetsFS = http.FS(subFS)
|
||||
} else {
|
||||
logger.Error("sub: failed to mount embedded assets:", err)
|
||||
}
|
||||
}
|
||||
|
||||
if assetsFS != nil {
|
||||
engine.StaticFS("/assets", assetsFS)
|
||||
if linksPathForAssets != "/assets" {
|
||||
engine.StaticFS(linksPathForAssets, assetsFS)
|
||||
}
|
||||
|
||||
// Add middleware to handle dynamic asset paths with subid
|
||||
if LinksPath != "/" {
|
||||
engine.Use(func(c *gin.Context) {
|
||||
path := c.Request.URL.Path
|
||||
// Check if this is an asset request with subid pattern: /sub/path/{subid}/assets/...
|
||||
pathPrefix := strings.TrimRight(LinksPath, "/") + "/"
|
||||
if strings.HasPrefix(path, pathPrefix) && strings.Contains(path, "/assets/") {
|
||||
// Extract the asset path after /assets/
|
||||
assetsIndex := strings.Index(path, "/assets/")
|
||||
if assetsIndex != -1 {
|
||||
assetPath := path[assetsIndex+8:] // +8 to skip "/assets/"
|
||||
if assetPath != "" {
|
||||
// Serve the asset file
|
||||
c.FileFromFS(assetPath, assetsFS)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
c.Next()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
g := engine.Group("/")
|
||||
|
||||
s.sub = NewSUBController(
|
||||
g, LinksPath, JsonPath, Encrypt, ShowInfo, RemarkModel, SubUpdates,
|
||||
SubJsonFragment, SubJsonMux, SubJsonRules)
|
||||
g, LinksPath, JsonPath, subJsonEnable, Encrypt, ShowInfo, RemarkModel, SubUpdates,
|
||||
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle)
|
||||
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
// getHtmlFiles loads templates from local folder (used in debug mode)
|
||||
func (s *Server) getHtmlFiles() ([]string, error) {
|
||||
dir, _ := os.Getwd()
|
||||
files := []string{}
|
||||
// common layout
|
||||
common := filepath.Join(dir, "web", "html", "common", "page.html")
|
||||
if _, err := os.Stat(common); err == nil {
|
||||
files = append(files, common)
|
||||
}
|
||||
// components used
|
||||
theme := filepath.Join(dir, "web", "html", "component", "aThemeSwitch.html")
|
||||
if _, err := os.Stat(theme); err == nil {
|
||||
files = append(files, theme)
|
||||
}
|
||||
// page itself
|
||||
page := filepath.Join(dir, "web", "html", "subpage.html")
|
||||
if _, err := os.Stat(page); err == nil {
|
||||
files = append(files, page)
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// Start initializes and starts the subscription server with configured settings.
|
||||
func (s *Server) Start() (err error) {
|
||||
// This is an anonymous function, no function name
|
||||
defer func() {
|
||||
@@ -184,6 +334,7 @@ func (s *Server) Start() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop gracefully shuts down the subscription server and closes the listener.
|
||||
func (s *Server) Stop() error {
|
||||
s.cancel()
|
||||
|
||||
@@ -198,6 +349,7 @@ func (s *Server) Stop() error {
|
||||
return common.Combine(err1, err2)
|
||||
}
|
||||
|
||||
// GetCtx returns the server's context for cancellation and deadline management.
|
||||
func (s *Server) GetCtx() context.Context {
|
||||
return s.ctx
|
||||
}
|
||||
|
||||
@@ -2,14 +2,20 @@ package sub
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SUBController handles HTTP requests for subscription links and JSON configurations.
|
||||
type SUBController struct {
|
||||
subTitle string
|
||||
subPath string
|
||||
subJsonPath string
|
||||
jsonEnabled bool
|
||||
subEncrypt bool
|
||||
updateInterval string
|
||||
|
||||
@@ -17,55 +23,54 @@ type SUBController struct {
|
||||
subJsonService *SubJsonService
|
||||
}
|
||||
|
||||
// NewSUBController creates a new subscription controller with the given configuration.
|
||||
func NewSUBController(
|
||||
g *gin.RouterGroup,
|
||||
subPath string,
|
||||
jsonPath string,
|
||||
jsonEnabled bool,
|
||||
encrypt bool,
|
||||
showInfo bool,
|
||||
rModel string,
|
||||
update string,
|
||||
jsonFragment string,
|
||||
jsonNoise string,
|
||||
jsonMux string,
|
||||
jsonRules string,
|
||||
subTitle string,
|
||||
) *SUBController {
|
||||
sub := NewSubService(showInfo, rModel)
|
||||
a := &SUBController{
|
||||
subTitle: subTitle,
|
||||
subPath: subPath,
|
||||
subJsonPath: jsonPath,
|
||||
jsonEnabled: jsonEnabled,
|
||||
subEncrypt: encrypt,
|
||||
updateInterval: update,
|
||||
|
||||
subService: sub,
|
||||
subJsonService: NewSubJsonService(jsonFragment, jsonMux, jsonRules, sub),
|
||||
subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub),
|
||||
}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
// initRouter registers HTTP routes for subscription links and JSON endpoints
|
||||
// on the provided router group.
|
||||
func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
||||
gLink := g.Group(a.subPath)
|
||||
gJson := g.Group(a.subJsonPath)
|
||||
|
||||
gLink.GET(":subid", a.subs)
|
||||
|
||||
gJson.GET(":subid", a.subJsons)
|
||||
if a.jsonEnabled {
|
||||
gJson := g.Group(a.subJsonPath)
|
||||
gJson.GET(":subid", a.subJsons)
|
||||
}
|
||||
}
|
||||
|
||||
// subs handles HTTP requests for subscription links, returning either HTML page or base64-encoded subscription data.
|
||||
func (a *SUBController) subs(c *gin.Context) {
|
||||
subId := c.Param("subid")
|
||||
host := c.GetHeader("X-Forwarded-Host")
|
||||
if host == "" {
|
||||
host = c.GetHeader("X-Real-IP")
|
||||
}
|
||||
if host == "" {
|
||||
var err error
|
||||
host, _, err = net.SplitHostPort(c.Request.Host)
|
||||
if err != nil {
|
||||
host = c.Request.Host
|
||||
}
|
||||
}
|
||||
subs, header, err := a.subService.GetSubs(subId, host)
|
||||
scheme, host, hostWithPort, hostHeader := a.subService.ResolveRequest(c)
|
||||
subs, lastOnline, traffic, err := a.subService.GetSubs(subId, host)
|
||||
if err != nil || len(subs) == 0 {
|
||||
c.String(400, "Error!")
|
||||
} else {
|
||||
@@ -74,10 +79,55 @@ func (a *SUBController) subs(c *gin.Context) {
|
||||
result += sub + "\n"
|
||||
}
|
||||
|
||||
// If the request expects HTML (e.g., browser) or explicitly asked (?html=1 or ?view=html), render the info page here
|
||||
accept := c.GetHeader("Accept")
|
||||
if strings.Contains(strings.ToLower(accept), "text/html") || c.Query("html") == "1" || strings.EqualFold(c.Query("view"), "html") {
|
||||
// Build page data in service
|
||||
subURL, subJsonURL := a.subService.BuildURLs(scheme, hostWithPort, a.subPath, a.subJsonPath, subId)
|
||||
if !a.jsonEnabled {
|
||||
subJsonURL = ""
|
||||
}
|
||||
// Get base_path from context (set by middleware)
|
||||
basePath, exists := c.Get("base_path")
|
||||
if !exists {
|
||||
basePath = "/"
|
||||
}
|
||||
// Add subId to base_path for asset URLs
|
||||
basePathStr := basePath.(string)
|
||||
if basePathStr == "/" {
|
||||
basePathStr = "/" + subId + "/"
|
||||
} else {
|
||||
// Remove trailing slash if exists, add subId, then add trailing slash
|
||||
basePathStr = strings.TrimRight(basePathStr, "/") + "/" + subId + "/"
|
||||
}
|
||||
page := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, subJsonURL, basePathStr)
|
||||
c.HTML(200, "subpage.html", gin.H{
|
||||
"title": "subscription.title",
|
||||
"cur_ver": config.GetVersion(),
|
||||
"host": page.Host,
|
||||
"base_path": page.BasePath,
|
||||
"sId": page.SId,
|
||||
"download": page.Download,
|
||||
"upload": page.Upload,
|
||||
"total": page.Total,
|
||||
"used": page.Used,
|
||||
"remained": page.Remained,
|
||||
"expire": page.Expire,
|
||||
"lastOnline": page.LastOnline,
|
||||
"datepicker": page.Datepicker,
|
||||
"downloadByte": page.DownloadByte,
|
||||
"uploadByte": page.UploadByte,
|
||||
"totalByte": page.TotalByte,
|
||||
"subUrl": page.SubUrl,
|
||||
"subJsonUrl": page.SubJsonUrl,
|
||||
"result": page.Result,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Add headers
|
||||
c.Writer.Header().Set("Subscription-Userinfo", header)
|
||||
c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
|
||||
c.Writer.Header().Set("Profile-Title", subId)
|
||||
header := fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
|
||||
a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle)
|
||||
|
||||
if a.subEncrypt {
|
||||
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
|
||||
@@ -87,29 +137,25 @@ func (a *SUBController) subs(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// subJsons handles HTTP requests for JSON subscription configurations.
|
||||
func (a *SUBController) subJsons(c *gin.Context) {
|
||||
subId := c.Param("subid")
|
||||
host := c.GetHeader("X-Forwarded-Host")
|
||||
if host == "" {
|
||||
host = c.GetHeader("X-Real-IP")
|
||||
}
|
||||
if host == "" {
|
||||
var err error
|
||||
host, _, err = net.SplitHostPort(c.Request.Host)
|
||||
if err != nil {
|
||||
host = c.Request.Host
|
||||
}
|
||||
}
|
||||
_, host, _, _ := a.subService.ResolveRequest(c)
|
||||
jsonSub, header, err := a.subJsonService.GetJson(subId, host)
|
||||
if err != nil || len(jsonSub) == 0 {
|
||||
c.String(400, "Error!")
|
||||
} else {
|
||||
|
||||
// Add headers
|
||||
c.Writer.Header().Set("Subscription-Userinfo", header)
|
||||
c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
|
||||
c.Writer.Header().Set("Profile-Title", subId)
|
||||
a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle)
|
||||
|
||||
c.String(200, jsonSub)
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyCommonHeaders sets common HTTP headers for subscription responses including user info, update interval, and profile title.
|
||||
func (a *SUBController) ApplyCommonHeaders(c *gin.Context, header, updateInterval, profileTitle string) {
|
||||
c.Writer.Header().Set("Subscription-Userinfo", header)
|
||||
c.Writer.Header().Set("Profile-Update-Interval", updateInterval)
|
||||
c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileTitle)))
|
||||
}
|
||||
|
||||
@@ -4,34 +4,38 @@ import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"strings"
|
||||
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/json_util"
|
||||
"x-ui/util/random"
|
||||
"x-ui/web/service"
|
||||
"x-ui/xray"
|
||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/json_util"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/random"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||
)
|
||||
|
||||
//go:embed default.json
|
||||
var defaultJson string
|
||||
|
||||
// SubJsonService handles JSON subscription configuration generation and management.
|
||||
type SubJsonService struct {
|
||||
configJson map[string]interface{}
|
||||
configJson map[string]any
|
||||
defaultOutbounds []json_util.RawMessage
|
||||
fragment string
|
||||
noises string
|
||||
mux string
|
||||
|
||||
inboundService service.InboundService
|
||||
SubService *SubService
|
||||
}
|
||||
|
||||
func NewSubJsonService(fragment string, mux string, rules string, subService *SubService) *SubJsonService {
|
||||
var configJson map[string]interface{}
|
||||
// NewSubJsonService creates a new JSON subscription service with the given configuration.
|
||||
func NewSubJsonService(fragment string, noises string, mux string, rules string, subService *SubService) *SubJsonService {
|
||||
var configJson map[string]any
|
||||
var defaultOutbounds []json_util.RawMessage
|
||||
json.Unmarshal([]byte(defaultJson), &configJson)
|
||||
if outboundSlices, ok := configJson["outbounds"].([]interface{}); ok {
|
||||
if outboundSlices, ok := configJson["outbounds"].([]any); ok {
|
||||
for _, defaultOutbound := range outboundSlices {
|
||||
jsonBytes, _ := json.Marshal(defaultOutbound)
|
||||
defaultOutbounds = append(defaultOutbounds, jsonBytes)
|
||||
@@ -39,9 +43,9 @@ func NewSubJsonService(fragment string, mux string, rules string, subService *Su
|
||||
}
|
||||
|
||||
if rules != "" {
|
||||
var newRules []interface{}
|
||||
routing, _ := configJson["routing"].(map[string]interface{})
|
||||
defaultRules, _ := routing["rules"].([]interface{})
|
||||
var newRules []any
|
||||
routing, _ := configJson["routing"].(map[string]any)
|
||||
defaultRules, _ := routing["rules"].([]any)
|
||||
json.Unmarshal([]byte(rules), &newRules)
|
||||
defaultRules = append(newRules, defaultRules...)
|
||||
routing["rules"] = defaultRules
|
||||
@@ -52,15 +56,21 @@ func NewSubJsonService(fragment string, mux string, rules string, subService *Su
|
||||
defaultOutbounds = append(defaultOutbounds, json_util.RawMessage(fragment))
|
||||
}
|
||||
|
||||
if noises != "" {
|
||||
defaultOutbounds = append(defaultOutbounds, json_util.RawMessage(noises))
|
||||
}
|
||||
|
||||
return &SubJsonService{
|
||||
configJson: configJson,
|
||||
defaultOutbounds: defaultOutbounds,
|
||||
fragment: fragment,
|
||||
noises: noises,
|
||||
mux: mux,
|
||||
SubService: subService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetJson generates a JSON subscription configuration for the given subscription ID and host.
|
||||
func (s *SubJsonService) GetJson(subId string, host string) (string, string, error) {
|
||||
inbounds, err := s.SubService.getInboundsBySubId(subId)
|
||||
if err != nil || len(inbounds) == 0 {
|
||||
@@ -142,10 +152,10 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
|
||||
var newJsonArray []json_util.RawMessage
|
||||
stream := s.streamData(inbound.StreamSettings)
|
||||
|
||||
externalProxies, ok := stream["externalProxy"].([]interface{})
|
||||
externalProxies, ok := stream["externalProxy"].([]any)
|
||||
if !ok || len(externalProxies) == 0 {
|
||||
externalProxies = []interface{}{
|
||||
map[string]interface{}{
|
||||
externalProxies = []any{
|
||||
map[string]any{
|
||||
"forceTls": "same",
|
||||
"dest": host,
|
||||
"port": float64(inbound.Port),
|
||||
@@ -157,7 +167,7 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
|
||||
delete(stream, "externalProxy")
|
||||
|
||||
for _, ep := range externalProxies {
|
||||
extPrxy := ep.(map[string]interface{})
|
||||
extPrxy := ep.(map[string]any)
|
||||
inbound.Listen = extPrxy["dest"].(string)
|
||||
inbound.Port = int(extPrxy["port"].(float64))
|
||||
newStream := stream
|
||||
@@ -165,12 +175,12 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
|
||||
case "tls":
|
||||
if newStream["security"] != "tls" {
|
||||
newStream["security"] = "tls"
|
||||
newStream["tslSettings"] = map[string]interface{}{}
|
||||
newStream["tlsSettings"] = map[string]any{}
|
||||
}
|
||||
case "none":
|
||||
if newStream["security"] != "none" {
|
||||
newStream["security"] = "none"
|
||||
delete(newStream, "tslSettings")
|
||||
delete(newStream, "tlsSettings")
|
||||
}
|
||||
}
|
||||
streamSettings, _ := json.MarshalIndent(newStream, "", " ")
|
||||
@@ -178,17 +188,18 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
|
||||
var newOutbounds []json_util.RawMessage
|
||||
|
||||
switch inbound.Protocol {
|
||||
case "vmess", "vless":
|
||||
case "vmess":
|
||||
newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client))
|
||||
case "vless":
|
||||
newOutbounds = append(newOutbounds, s.genVless(inbound, streamSettings, client))
|
||||
case "trojan", "shadowsocks":
|
||||
newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client))
|
||||
}
|
||||
|
||||
newOutbounds = append(newOutbounds, s.defaultOutbounds...)
|
||||
newConfigJson := make(map[string]interface{})
|
||||
for key, value := range s.configJson {
|
||||
newConfigJson[key] = value
|
||||
}
|
||||
newConfigJson := make(map[string]any)
|
||||
maps.Copy(newConfigJson, s.configJson)
|
||||
|
||||
newConfigJson["outbounds"] = newOutbounds
|
||||
newConfigJson["remarks"] = s.SubService.genRemark(inbound, client.Email, extPrxy["remark"].(string))
|
||||
|
||||
@@ -199,19 +210,20 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
|
||||
return newJsonArray
|
||||
}
|
||||
|
||||
func (s *SubJsonService) streamData(stream string) map[string]interface{} {
|
||||
var streamSettings map[string]interface{}
|
||||
func (s *SubJsonService) streamData(stream string) map[string]any {
|
||||
var streamSettings map[string]any
|
||||
json.Unmarshal([]byte(stream), &streamSettings)
|
||||
security, _ := streamSettings["security"].(string)
|
||||
if security == "tls" {
|
||||
streamSettings["tlsSettings"] = s.tlsData(streamSettings["tlsSettings"].(map[string]interface{}))
|
||||
} else if security == "reality" {
|
||||
streamSettings["realitySettings"] = s.realityData(streamSettings["realitySettings"].(map[string]interface{}))
|
||||
switch security {
|
||||
case "tls":
|
||||
streamSettings["tlsSettings"] = s.tlsData(streamSettings["tlsSettings"].(map[string]any))
|
||||
case "reality":
|
||||
streamSettings["realitySettings"] = s.realityData(streamSettings["realitySettings"].(map[string]any))
|
||||
}
|
||||
delete(streamSettings, "sockopt")
|
||||
|
||||
if s.fragment != "" {
|
||||
streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "fragment", "tcpKeepAliveIdle": 100, "tcpMptcp": true, "tcpNoDelay": true}`)
|
||||
streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "fragment", "tcpKeepAliveIdle": 100, "tcpMptcp": true, "penetrate": true}`)
|
||||
}
|
||||
|
||||
// remove proxy protocol
|
||||
@@ -227,17 +239,17 @@ func (s *SubJsonService) streamData(stream string) map[string]interface{} {
|
||||
return streamSettings
|
||||
}
|
||||
|
||||
func (s *SubJsonService) removeAcceptProxy(setting interface{}) map[string]interface{} {
|
||||
netSettings, ok := setting.(map[string]interface{})
|
||||
func (s *SubJsonService) removeAcceptProxy(setting any) map[string]any {
|
||||
netSettings, ok := setting.(map[string]any)
|
||||
if ok {
|
||||
delete(netSettings, "acceptProxyProtocol")
|
||||
}
|
||||
return netSettings
|
||||
}
|
||||
|
||||
func (s *SubJsonService) tlsData(tData map[string]interface{}) map[string]interface{} {
|
||||
tlsData := make(map[string]interface{}, 1)
|
||||
tlsClientSettings, _ := tData["settings"].(map[string]interface{})
|
||||
func (s *SubJsonService) tlsData(tData map[string]any) map[string]any {
|
||||
tlsData := make(map[string]any, 1)
|
||||
tlsClientSettings, _ := tData["settings"].(map[string]any)
|
||||
|
||||
tlsData["serverName"] = tData["serverName"]
|
||||
tlsData["alpn"] = tData["alpn"]
|
||||
@@ -250,23 +262,24 @@ func (s *SubJsonService) tlsData(tData map[string]interface{}) map[string]interf
|
||||
return tlsData
|
||||
}
|
||||
|
||||
func (s *SubJsonService) realityData(rData map[string]interface{}) map[string]interface{} {
|
||||
rltyData := make(map[string]interface{}, 1)
|
||||
rltyClientSettings, _ := rData["settings"].(map[string]interface{})
|
||||
func (s *SubJsonService) realityData(rData map[string]any) map[string]any {
|
||||
rltyData := make(map[string]any, 1)
|
||||
rltyClientSettings, _ := rData["settings"].(map[string]any)
|
||||
|
||||
rltyData["show"] = false
|
||||
rltyData["publicKey"] = rltyClientSettings["publicKey"]
|
||||
rltyData["fingerprint"] = rltyClientSettings["fingerprint"]
|
||||
rltyData["mldsa65Verify"] = rltyClientSettings["mldsa65Verify"]
|
||||
|
||||
// Set random data
|
||||
rltyData["spiderX"] = "/" + random.Seq(15)
|
||||
shortIds, ok := rData["shortIds"].([]interface{})
|
||||
shortIds, ok := rData["shortIds"].([]any)
|
||||
if ok && len(shortIds) > 0 {
|
||||
rltyData["shortId"] = shortIds[random.Num(len(shortIds))].(string)
|
||||
} else {
|
||||
rltyData["shortId"] = ""
|
||||
}
|
||||
serverNames, ok := rData["serverNames"].([]interface{})
|
||||
serverNames, ok := rData["serverNames"].([]any)
|
||||
if ok && len(serverNames) > 0 {
|
||||
rltyData["serverName"] = serverNames[random.Num(len(serverNames))].(string)
|
||||
} else {
|
||||
@@ -281,12 +294,8 @@ func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_ut
|
||||
usersData := make([]UserVnext, 1)
|
||||
|
||||
usersData[0].ID = client.ID
|
||||
usersData[0].Level = 8
|
||||
if inbound.Protocol == model.VLESS {
|
||||
usersData[0].Flow = client.Flow
|
||||
usersData[0].Encryption = "none"
|
||||
}
|
||||
|
||||
usersData[0].Email = client.Email
|
||||
usersData[0].Security = client.Security
|
||||
vnextData := make([]VnextSetting, 1)
|
||||
vnextData[0] = VnextSetting{
|
||||
Address: inbound.Listen,
|
||||
@@ -300,14 +309,42 @@ func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_ut
|
||||
outbound.Mux = json_util.RawMessage(s.mux)
|
||||
}
|
||||
outbound.StreamSettings = streamSettings
|
||||
outbound.Settings = OutboundSettings{
|
||||
Vnext: vnextData,
|
||||
outbound.Settings = map[string]any{
|
||||
"vnext": vnextData,
|
||||
}
|
||||
|
||||
result, _ := json.MarshalIndent(outbound, "", " ")
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *SubJsonService) genVless(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {
|
||||
outbound := Outbound{}
|
||||
outbound.Protocol = string(inbound.Protocol)
|
||||
outbound.Tag = "proxy"
|
||||
if s.mux != "" {
|
||||
outbound.Mux = json_util.RawMessage(s.mux)
|
||||
}
|
||||
outbound.StreamSettings = streamSettings
|
||||
settings := make(map[string]any)
|
||||
settings["address"] = inbound.Listen
|
||||
settings["port"] = inbound.Port
|
||||
settings["id"] = client.ID
|
||||
if client.Flow != "" {
|
||||
settings["flow"] = client.Flow
|
||||
}
|
||||
|
||||
// Add encryption for VLESS outbound from inbound settings
|
||||
var inboundSettings map[string]any
|
||||
json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
|
||||
if encryption, ok := inboundSettings["encryption"].(string); ok {
|
||||
settings["encryption"] = encryption
|
||||
}
|
||||
|
||||
outbound.Settings = settings
|
||||
result, _ := json.MarshalIndent(outbound, "", " ")
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {
|
||||
outbound := Outbound{}
|
||||
|
||||
@@ -320,7 +357,7 @@ func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_u
|
||||
}
|
||||
|
||||
if inbound.Protocol == model.Shadowsocks {
|
||||
var inboundSettings map[string]interface{}
|
||||
var inboundSettings map[string]any
|
||||
json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
|
||||
method, _ := inboundSettings["method"].(string)
|
||||
serverData[0].Method = method
|
||||
@@ -339,8 +376,8 @@ func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_u
|
||||
outbound.Mux = json_util.RawMessage(s.mux)
|
||||
}
|
||||
outbound.StreamSettings = streamSettings
|
||||
outbound.Settings = OutboundSettings{
|
||||
Servers: serverData,
|
||||
outbound.Settings = map[string]any{
|
||||
"servers": serverData,
|
||||
}
|
||||
|
||||
result, _ := json.MarshalIndent(outbound, "", " ")
|
||||
@@ -348,17 +385,11 @@ func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_u
|
||||
}
|
||||
|
||||
type Outbound struct {
|
||||
Protocol string `json:"protocol"`
|
||||
Tag string `json:"tag"`
|
||||
StreamSettings json_util.RawMessage `json:"streamSettings"`
|
||||
Mux json_util.RawMessage `json:"mux,omitempty"`
|
||||
ProxySettings map[string]interface{} `json:"proxySettings,omitempty"`
|
||||
Settings OutboundSettings `json:"settings,omitempty"`
|
||||
}
|
||||
|
||||
type OutboundSettings struct {
|
||||
Vnext []VnextSetting `json:"vnext,omitempty"`
|
||||
Servers []ServerSetting `json:"servers,omitempty"`
|
||||
Protocol string `json:"protocol"`
|
||||
Tag string `json:"tag"`
|
||||
StreamSettings json_util.RawMessage `json:"streamSettings"`
|
||||
Mux json_util.RawMessage `json:"mux,omitempty"`
|
||||
Settings map[string]any `json:"settings,omitempty"`
|
||||
}
|
||||
|
||||
type VnextSetting struct {
|
||||
@@ -368,10 +399,9 @@ type VnextSetting struct {
|
||||
}
|
||||
|
||||
type UserVnext struct {
|
||||
Encryption string `json:"encryption,omitempty"`
|
||||
Flow string `json:"flow,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Level int `json:"level"`
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Security string `json:"security,omitempty"`
|
||||
}
|
||||
|
||||
type ServerSetting struct {
|
||||
|
||||
959
update.sh
Executable file
@@ -0,0 +1,959 @@
|
||||
#!/bin/bash
|
||||
|
||||
red='\033[0;31m'
|
||||
green='\033[0;32m'
|
||||
blue='\033[0;34m'
|
||||
yellow='\033[0;33m'
|
||||
plain='\033[0m'
|
||||
|
||||
xui_folder="${XUI_MAIN_FOLDER:=/usr/local/x-ui}"
|
||||
xui_service="${XUI_SERVICE:=/etc/systemd/system}"
|
||||
|
||||
# Don't edit this config
|
||||
b_source="${BASH_SOURCE[0]}"
|
||||
while [ -h "$b_source" ]; do
|
||||
b_dir="$(cd -P "$(dirname "$b_source")" >/dev/null 2>&1 && pwd || pwd -P)"
|
||||
b_source="$(readlink "$b_source")"
|
||||
[[ $b_source != /* ]] && b_source="$b_dir/$b_source"
|
||||
done
|
||||
cur_dir="$(cd -P "$(dirname "$b_source")" >/dev/null 2>&1 && pwd || pwd -P)"
|
||||
script_name=$(basename "$0")
|
||||
|
||||
# Check command exist function
|
||||
_command_exists() {
|
||||
type "$1" &>/dev/null
|
||||
}
|
||||
|
||||
# Fail, log and exit script function
|
||||
_fail() {
|
||||
local msg=${1}
|
||||
echo -e "${red}${msg}${plain}"
|
||||
exit 2
|
||||
}
|
||||
|
||||
# check root
|
||||
[[ $EUID -ne 0 ]] && _fail "FATAL ERROR: Please run this script with root privilege."
|
||||
|
||||
if _command_exists curl; then
|
||||
curl_bin=$(which curl)
|
||||
else
|
||||
_fail "ERROR: Command 'curl' not found."
|
||||
fi
|
||||
|
||||
# Check OS and set release variable
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
source /etc/os-release
|
||||
release=$ID
|
||||
elif [[ -f /usr/lib/os-release ]]; then
|
||||
source /usr/lib/os-release
|
||||
release=$ID
|
||||
else
|
||||
_fail "Failed to check the system OS, please contact the author!"
|
||||
fi
|
||||
echo "The OS release is: $release"
|
||||
|
||||
arch() {
|
||||
case "$(uname -m)" in
|
||||
x86_64 | x64 | amd64) echo 'amd64' ;;
|
||||
i*86 | x86) echo '386' ;;
|
||||
armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;;
|
||||
armv7* | armv7 | arm) echo 'armv7' ;;
|
||||
armv6* | armv6) echo 'armv6' ;;
|
||||
armv5* | armv5) echo 'armv5' ;;
|
||||
s390x) echo 's390x' ;;
|
||||
*) echo -e "${red}Unsupported CPU architecture!${plain}" && rm -f "${cur_dir}/${script_name}" >/dev/null 2>&1 && exit 2;;
|
||||
esac
|
||||
}
|
||||
|
||||
echo "Arch: $(arch)"
|
||||
|
||||
# Simple helpers
|
||||
is_ipv4() {
|
||||
[[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1
|
||||
}
|
||||
is_ipv6() {
|
||||
[[ "$1" =~ : ]] && return 0 || return 1
|
||||
}
|
||||
is_ip() {
|
||||
is_ipv4 "$1" || is_ipv6 "$1"
|
||||
}
|
||||
is_domain() {
|
||||
[[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1
|
||||
}
|
||||
|
||||
# Port helpers
|
||||
is_port_in_use() {
|
||||
local port="$1"
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
ss -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}'
|
||||
return
|
||||
fi
|
||||
if command -v netstat >/dev/null 2>&1; then
|
||||
netstat -lnt 2>/dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}'
|
||||
return
|
||||
fi
|
||||
if command -v lsof >/dev/null 2>&1; then
|
||||
lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
gen_random_string() {
|
||||
local length="$1"
|
||||
local random_string=$(LC_ALL=C tr -dc 'a-zA-Z0-9' </dev/urandom | fold -w "$length" | head -n 1)
|
||||
echo "$random_string"
|
||||
}
|
||||
|
||||
install_base() {
|
||||
echo -e "${green}Updating and install dependency packages...${plain}"
|
||||
case "${release}" in
|
||||
ubuntu | debian | armbian)
|
||||
apt-get update >/dev/null 2>&1 && apt-get install -y -q curl tar tzdata socat >/dev/null 2>&1
|
||||
;;
|
||||
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
|
||||
dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata socat >/dev/null 2>&1
|
||||
;;
|
||||
centos)
|
||||
if [[ "${VERSION_ID}" =~ ^7 ]]; then
|
||||
yum -y update >/dev/null 2>&1 && yum install -y -q curl tar tzdata socat >/dev/null 2>&1
|
||||
else
|
||||
dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata socat >/dev/null 2>&1
|
||||
fi
|
||||
;;
|
||||
arch | manjaro | parch)
|
||||
pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm curl tar tzdata socat >/dev/null 2>&1
|
||||
;;
|
||||
opensuse-tumbleweed | opensuse-leap)
|
||||
zypper refresh >/dev/null 2>&1 && zypper -q install -y curl tar timezone socat >/dev/null 2>&1
|
||||
;;
|
||||
alpine)
|
||||
apk update >/dev/null 2>&1 && apk add curl tar tzdata socat >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
apt-get update >/dev/null 2>&1 && apt install -y -q curl tar tzdata socat >/dev/null 2>&1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
install_acme() {
|
||||
echo -e "${green}Installing acme.sh for SSL certificate management...${plain}"
|
||||
cd ~ || return 1
|
||||
curl -s https://get.acme.sh | sh >/dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${red}Failed to install acme.sh${plain}"
|
||||
return 1
|
||||
else
|
||||
echo -e "${green}acme.sh installed successfully${plain}"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
setup_ssl_certificate() {
|
||||
local domain="$1"
|
||||
local server_ip="$2"
|
||||
local existing_port="$3"
|
||||
local existing_webBasePath="$4"
|
||||
|
||||
echo -e "${green}Setting up SSL certificate...${plain}"
|
||||
|
||||
# Check if acme.sh is installed
|
||||
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
||||
install_acme
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${yellow}Failed to install acme.sh, skipping SSL setup${plain}"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create certificate directory
|
||||
local certPath="/root/cert/${domain}"
|
||||
mkdir -p "$certPath"
|
||||
|
||||
# Issue certificate
|
||||
echo -e "${green}Issuing SSL certificate for ${domain}...${plain}"
|
||||
echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}"
|
||||
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt >/dev/null 2>&1
|
||||
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${yellow}Failed to issue certificate for ${domain}${plain}"
|
||||
echo -e "${yellow}Please ensure port 80 is open and try again later with: x-ui${plain}"
|
||||
rm -rf ~/.acme.sh/${domain} 2>/dev/null
|
||||
rm -rf "$certPath" 2>/dev/null
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Install certificate
|
||||
~/.acme.sh/acme.sh --installcert -d ${domain} \
|
||||
--key-file /root/cert/${domain}/privkey.pem \
|
||||
--fullchain-file /root/cert/${domain}/fullchain.pem \
|
||||
--reloadcmd "systemctl restart x-ui" >/dev/null 2>&1
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${yellow}Failed to install certificate${plain}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Enable auto-renew
|
||||
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
|
||||
chmod 600 $certPath/privkey.pem 2>/dev/null
|
||||
chmod 644 $certPath/fullchain.pem 2>/dev/null
|
||||
|
||||
# Set certificate for panel
|
||||
local webCertFile="/root/cert/${domain}/fullchain.pem"
|
||||
local webKeyFile="/root/cert/${domain}/privkey.pem"
|
||||
|
||||
if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
|
||||
${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" >/dev/null 2>&1
|
||||
echo -e "${green}SSL certificate installed and configured successfully!${plain}"
|
||||
return 0
|
||||
else
|
||||
echo -e "${yellow}Certificate files not found${plain}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Issue Let's Encrypt IP certificate with shortlived profile (~6 days validity)
|
||||
# Requires acme.sh and port 80 open for HTTP-01 challenge
|
||||
setup_ip_certificate() {
|
||||
local ipv4="$1"
|
||||
local ipv6="$2" # optional
|
||||
|
||||
echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}"
|
||||
echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}"
|
||||
echo -e "${yellow}Default listener is port 80. If you choose another port, ensure external port 80 forwards to it.${plain}"
|
||||
|
||||
# Check for acme.sh
|
||||
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
||||
install_acme
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${red}Failed to install acme.sh${plain}"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate IP address
|
||||
if [[ -z "$ipv4" ]]; then
|
||||
echo -e "${red}IPv4 address is required${plain}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! is_ipv4 "$ipv4"; then
|
||||
echo -e "${red}Invalid IPv4 address: $ipv4${plain}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Create certificate directory
|
||||
local certDir="/root/cert/ip"
|
||||
mkdir -p "$certDir"
|
||||
|
||||
# Build domain arguments
|
||||
local domain_args="-d ${ipv4}"
|
||||
if [[ -n "$ipv6" ]] && is_ipv6 "$ipv6"; then
|
||||
domain_args="${domain_args} -d ${ipv6}"
|
||||
echo -e "${green}Including IPv6 address: ${ipv6}${plain}"
|
||||
fi
|
||||
|
||||
# Set reload command for auto-renewal (add || true so it doesn't fail if service stopped)
|
||||
local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null || true"
|
||||
|
||||
# Choose port for HTTP-01 listener (default 80, prompt override)
|
||||
local WebPort=""
|
||||
read -rp "Port to use for ACME HTTP-01 listener (default 80): " WebPort
|
||||
WebPort="${WebPort:-80}"
|
||||
if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then
|
||||
echo -e "${red}Invalid port provided. Falling back to 80.${plain}"
|
||||
WebPort=80
|
||||
fi
|
||||
echo -e "${green}Using port ${WebPort} for standalone validation.${plain}"
|
||||
if [[ "${WebPort}" -ne 80 ]]; then
|
||||
echo -e "${yellow}Reminder: Let's Encrypt still connects on port 80; forward external port 80 to ${WebPort}.${plain}"
|
||||
fi
|
||||
|
||||
# Ensure chosen port is available
|
||||
while true; do
|
||||
if is_port_in_use "${WebPort}"; then
|
||||
echo -e "${yellow}Port ${WebPort} is currently in use.${plain}"
|
||||
|
||||
local alt_port=""
|
||||
read -rp "Enter another port for acme.sh standalone listener (leave empty to abort): " alt_port
|
||||
alt_port="${alt_port// /}"
|
||||
if [[ -z "${alt_port}" ]]; then
|
||||
echo -e "${red}Port ${WebPort} is busy; cannot proceed.${plain}"
|
||||
return 1
|
||||
fi
|
||||
if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then
|
||||
echo -e "${red}Invalid port provided.${plain}"
|
||||
return 1
|
||||
fi
|
||||
WebPort="${alt_port}"
|
||||
continue
|
||||
else
|
||||
echo -e "${green}Port ${WebPort} is free and ready for standalone validation.${plain}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Issue certificate with shortlived profile
|
||||
echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}"
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt >/dev/null 2>&1
|
||||
|
||||
~/.acme.sh/acme.sh --issue \
|
||||
${domain_args} \
|
||||
--standalone \
|
||||
--server letsencrypt \
|
||||
--certificate-profile shortlived \
|
||||
--days 6 \
|
||||
--httpport ${WebPort} \
|
||||
--force
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${red}Failed to issue IP certificate${plain}"
|
||||
echo -e "${yellow}Please ensure port ${WebPort} is reachable (or forwarded from external port 80)${plain}"
|
||||
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
|
||||
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
|
||||
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
|
||||
rm -rf ${certDir} 2>/dev/null
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "${green}Certificate issued successfully, installing...${plain}"
|
||||
|
||||
# Install certificate
|
||||
# Note: acme.sh may report "Reload error" and exit non-zero if reloadcmd fails,
|
||||
# but the cert files are still installed. We check for files instead of exit code.
|
||||
~/.acme.sh/acme.sh --installcert -d ${ipv4} \
|
||||
--key-file "${certDir}/privkey.pem" \
|
||||
--fullchain-file "${certDir}/fullchain.pem" \
|
||||
--reloadcmd "${reloadCmd}" 2>&1 || true
|
||||
|
||||
# Verify certificate files exist (don't rely on exit code - reloadcmd failure causes non-zero)
|
||||
if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then
|
||||
echo -e "${red}Certificate files not found after installation${plain}"
|
||||
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
|
||||
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
|
||||
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
|
||||
rm -rf ${certDir} 2>/dev/null
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "${green}Certificate files installed successfully${plain}"
|
||||
|
||||
# Enable auto-upgrade for acme.sh (ensures cron job runs)
|
||||
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
|
||||
|
||||
chmod 600 ${certDir}/privkey.pem 2>/dev/null
|
||||
chmod 644 ${certDir}/fullchain.pem 2>/dev/null
|
||||
|
||||
# Configure panel to use the certificate
|
||||
echo -e "${green}Setting certificate paths for the panel...${plain}"
|
||||
${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${yellow}Warning: Could not set certificate paths automatically.${plain}"
|
||||
echo -e "${yellow}You may need to set them manually in the panel settings.${plain}"
|
||||
echo -e "${yellow}Cert path: ${certDir}/fullchain.pem${plain}"
|
||||
echo -e "${yellow}Key path: ${certDir}/privkey.pem${plain}"
|
||||
else
|
||||
echo -e "${green}Certificate paths set successfully!${plain}"
|
||||
fi
|
||||
|
||||
echo -e "${green}IP certificate installed and configured successfully!${plain}"
|
||||
echo -e "${green}Certificate valid for ~6 days, auto-renews via acme.sh cron job.${plain}"
|
||||
echo -e "${yellow}Panel will automatically restart after each renewal.${plain}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Comprehensive manual SSL certificate issuance via acme.sh
|
||||
ssl_cert_issue() {
|
||||
local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##')
|
||||
local existing_port=$(${xui_folder}/x-ui setting -show true | grep 'port:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
|
||||
|
||||
# check for acme.sh first
|
||||
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
||||
echo "acme.sh could not be found. Installing now..."
|
||||
cd ~ || return 1
|
||||
curl -s https://get.acme.sh | sh
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${red}Failed to install acme.sh${plain}"
|
||||
return 1
|
||||
else
|
||||
echo -e "${green}acme.sh installed successfully${plain}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# get the domain here, and we need to verify it
|
||||
local domain=""
|
||||
while true; do
|
||||
read -rp "Please enter your domain name: " domain
|
||||
domain="${domain// /}" # Trim whitespace
|
||||
|
||||
if [[ -z "$domain" ]]; then
|
||||
echo -e "${red}Domain name cannot be empty. Please try again.${plain}"
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! is_domain "$domain"; then
|
||||
echo -e "${red}Invalid domain format: ${domain}. Please enter a valid domain name.${plain}"
|
||||
continue
|
||||
fi
|
||||
|
||||
break
|
||||
done
|
||||
echo -e "${green}Your domain is: ${domain}, checking it...${plain}"
|
||||
|
||||
# check if there already exists a certificate
|
||||
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
|
||||
if [ "${currentCert}" == "${domain}" ]; then
|
||||
local certInfo=$(~/.acme.sh/acme.sh --list)
|
||||
echo -e "${red}System already has certificates for this domain. Cannot issue again.${plain}"
|
||||
echo -e "${yellow}Current certificate details:${plain}"
|
||||
echo "$certInfo"
|
||||
return 1
|
||||
else
|
||||
echo -e "${green}Your domain is ready for issuing certificates now...${plain}"
|
||||
fi
|
||||
|
||||
# create a directory for the certificate
|
||||
certPath="/root/cert/${domain}"
|
||||
if [ ! -d "$certPath" ]; then
|
||||
mkdir -p "$certPath"
|
||||
else
|
||||
rm -rf "$certPath"
|
||||
mkdir -p "$certPath"
|
||||
fi
|
||||
|
||||
# get the port number for the standalone server
|
||||
local WebPort=80
|
||||
read -rp "Please choose which port to use (default is 80): " WebPort
|
||||
if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then
|
||||
echo -e "${yellow}Your input ${WebPort} is invalid, will use default port 80.${plain}"
|
||||
WebPort=80
|
||||
fi
|
||||
echo -e "${green}Will use port: ${WebPort} to issue certificates. Please make sure this port is open.${plain}"
|
||||
|
||||
# Stop panel temporarily
|
||||
echo -e "${yellow}Stopping panel temporarily...${plain}"
|
||||
systemctl stop x-ui 2>/dev/null || rc-service x-ui stop 2>/dev/null
|
||||
|
||||
# issue the certificate
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
|
||||
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${red}Issuing certificate failed, please check logs.${plain}"
|
||||
rm -rf ~/.acme.sh/${domain}
|
||||
systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null
|
||||
return 1
|
||||
else
|
||||
echo -e "${green}Issuing certificate succeeded, installing certificates...${plain}"
|
||||
fi
|
||||
|
||||
# Setup reload command
|
||||
reloadCmd="systemctl restart x-ui || rc-service x-ui restart"
|
||||
echo -e "${green}Default --reloadcmd for ACME is: ${yellow}systemctl restart x-ui || rc-service x-ui restart${plain}"
|
||||
echo -e "${green}This command will run on every certificate issue and renew.${plain}"
|
||||
read -rp "Would you like to modify --reloadcmd for ACME? (y/n): " setReloadcmd
|
||||
if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then
|
||||
echo -e "\n${green}\t1.${plain} Preset: systemctl reload nginx ; systemctl restart x-ui"
|
||||
echo -e "${green}\t2.${plain} Input your own command"
|
||||
echo -e "${green}\t0.${plain} Keep default reloadcmd"
|
||||
read -rp "Choose an option: " choice
|
||||
case "$choice" in
|
||||
1)
|
||||
echo -e "${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}"
|
||||
reloadCmd="systemctl reload nginx ; systemctl restart x-ui"
|
||||
;;
|
||||
2)
|
||||
echo -e "${yellow}It's recommended to put x-ui restart at the end${plain}"
|
||||
read -rp "Please enter your custom reloadcmd: " reloadCmd
|
||||
echo -e "${green}Reloadcmd is: ${reloadCmd}${plain}"
|
||||
;;
|
||||
*)
|
||||
echo -e "${green}Keeping default reloadcmd${plain}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# install the certificate
|
||||
~/.acme.sh/acme.sh --installcert -d ${domain} \
|
||||
--key-file /root/cert/${domain}/privkey.pem \
|
||||
--fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd "${reloadCmd}"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${red}Installing certificate failed, exiting.${plain}"
|
||||
rm -rf ~/.acme.sh/${domain}
|
||||
systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null
|
||||
return 1
|
||||
else
|
||||
echo -e "${green}Installing certificate succeeded, enabling auto renew...${plain}"
|
||||
fi
|
||||
|
||||
# enable auto-renew
|
||||
~/.acme.sh/acme.sh --upgrade --auto-upgrade
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}"
|
||||
ls -lah /root/cert/${domain}/
|
||||
chmod 600 $certPath/privkey.pem
|
||||
chmod 644 $certPath/fullchain.pem
|
||||
else
|
||||
echo -e "${green}Auto renew succeeded, certificate details:${plain}"
|
||||
ls -lah /root/cert/${domain}/
|
||||
chmod 600 $certPath/privkey.pem
|
||||
chmod 644 $certPath/fullchain.pem
|
||||
fi
|
||||
|
||||
# Restart panel
|
||||
systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null
|
||||
|
||||
# Prompt user to set panel paths after successful certificate installation
|
||||
read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel
|
||||
if [[ "$setPanel" == "y" || "$setPanel" == "Y" ]]; then
|
||||
local webCertFile="/root/cert/${domain}/fullchain.pem"
|
||||
local webKeyFile="/root/cert/${domain}/privkey.pem"
|
||||
|
||||
if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
|
||||
${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile"
|
||||
echo -e "${green}Certificate paths set for the panel${plain}"
|
||||
echo -e "${green}Certificate File: $webCertFile${plain}"
|
||||
echo -e "${green}Private Key File: $webKeyFile${plain}"
|
||||
echo ""
|
||||
echo -e "${green}Access URL: https://${domain}:${existing_port}/${existing_webBasePath}${plain}"
|
||||
echo -e "${yellow}Panel will restart to apply SSL certificate...${plain}"
|
||||
systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null
|
||||
else
|
||||
echo -e "${red}Error: Certificate or private key file not found for domain: $domain.${plain}"
|
||||
fi
|
||||
else
|
||||
echo -e "${yellow}Skipping panel path setting.${plain}"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
# Unified interactive SSL setup (domain or IP)
|
||||
# Sets global `SSL_HOST` to the chosen domain/IP
|
||||
prompt_and_setup_ssl() {
|
||||
local panel_port="$1"
|
||||
local web_base_path="$2" # expected without leading slash
|
||||
local server_ip="$3"
|
||||
|
||||
local ssl_choice=""
|
||||
|
||||
echo -e "${yellow}Choose SSL certificate setup method:${plain}"
|
||||
echo -e "${green}1.${plain} Let's Encrypt for Domain (90-day validity, auto-renews)"
|
||||
echo -e "${green}2.${plain} Let's Encrypt for IP Address (6-day validity, auto-renews)"
|
||||
echo -e "${green}3.${plain} Custom SSL Certificate (Path to existing files)"
|
||||
echo -e "${blue}Note:${plain} Options 1 & 2 require port 80 open. Option 3 requires manual paths."
|
||||
read -rp "Choose an option (default 2 for IP): " ssl_choice
|
||||
ssl_choice="${ssl_choice// /}" # Trim whitespace
|
||||
|
||||
# Default to 2 (IP cert) if input is empty or invalid (not 1 or 3)
|
||||
if [[ "$ssl_choice" != "1" && "$ssl_choice" != "3" ]]; then
|
||||
ssl_choice="2"
|
||||
fi
|
||||
|
||||
case "$ssl_choice" in
|
||||
1)
|
||||
# User chose Let's Encrypt domain option
|
||||
echo -e "${green}Using Let's Encrypt for domain certificate...${plain}"
|
||||
ssl_cert_issue
|
||||
# Extract the domain that was used from the certificate
|
||||
local cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}')
|
||||
if [[ -n "${cert_domain}" ]]; then
|
||||
SSL_HOST="${cert_domain}"
|
||||
echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}"
|
||||
else
|
||||
echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}"
|
||||
SSL_HOST="${server_ip}"
|
||||
fi
|
||||
;;
|
||||
2)
|
||||
# User chose Let's Encrypt IP certificate option
|
||||
echo -e "${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}"
|
||||
|
||||
# Ask for optional IPv6
|
||||
local ipv6_addr=""
|
||||
read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr
|
||||
ipv6_addr="${ipv6_addr// /}" # Trim whitespace
|
||||
|
||||
# Stop panel if running (port 80 needed)
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-service x-ui stop >/dev/null 2>&1
|
||||
else
|
||||
systemctl stop x-ui >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
setup_ip_certificate "${server_ip}" "${ipv6_addr}"
|
||||
if [ $? -eq 0 ]; then
|
||||
SSL_HOST="${server_ip}"
|
||||
echo -e "${green}✓ Let's Encrypt IP certificate configured successfully${plain}"
|
||||
else
|
||||
echo -e "${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}"
|
||||
SSL_HOST="${server_ip}"
|
||||
fi
|
||||
|
||||
# Restart panel after SSL is configured (restart applies new cert settings)
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-service x-ui restart >/dev/null 2>&1
|
||||
else
|
||||
systemctl restart x-ui >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
;;
|
||||
3)
|
||||
# User chose Custom Paths (User Provided) option
|
||||
echo -e "${green}Using custom existing certificate...${plain}"
|
||||
local custom_cert=""
|
||||
local custom_key=""
|
||||
local custom_domain=""
|
||||
|
||||
# 3.1 Request Domain to compose Panel URL later
|
||||
read -rp "Please enter domain name certificate issued for: " custom_domain
|
||||
custom_domain="${custom_domain// /}" # Убираем пробелы
|
||||
|
||||
# 3.2 Loop for Certificate Path
|
||||
while true; do
|
||||
read -rp "Input certificate path (keywords: .crt / fullchain): " custom_cert
|
||||
# Strip quotes if present
|
||||
custom_cert=$(echo "$custom_cert" | tr -d '"' | tr -d "'")
|
||||
|
||||
if [[ -f "$custom_cert" && -r "$custom_cert" && -s "$custom_cert" ]]; then
|
||||
break
|
||||
elif [[ ! -f "$custom_cert" ]]; then
|
||||
echo -e "${red}Error: File does not exist! Try again.${plain}"
|
||||
elif [[ ! -r "$custom_cert" ]]; then
|
||||
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
|
||||
else
|
||||
echo -e "${red}Error: File is empty!${plain}"
|
||||
fi
|
||||
done
|
||||
|
||||
# 3.3 Loop for Private Key Path
|
||||
while true; do
|
||||
read -rp "Input private key path (keywords: .key / privatekey): " custom_key
|
||||
# Strip quotes if present
|
||||
custom_key=$(echo "$custom_key" | tr -d '"' | tr -d "'")
|
||||
|
||||
if [[ -f "$custom_key" && -r "$custom_key" && -s "$custom_key" ]]; then
|
||||
break
|
||||
elif [[ ! -f "$custom_key" ]]; then
|
||||
echo -e "${red}Error: File does not exist! Try again.${plain}"
|
||||
elif [[ ! -r "$custom_key" ]]; then
|
||||
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
|
||||
else
|
||||
echo -e "${red}Error: File is empty!${plain}"
|
||||
fi
|
||||
done
|
||||
|
||||
# 3.4 Apply Settings via x-ui binary
|
||||
${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" >/dev/null 2>&1
|
||||
|
||||
# Set SSL_HOST for composing Panel URL
|
||||
if [[ -n "$custom_domain" ]]; then
|
||||
SSL_HOST="$custom_domain"
|
||||
else
|
||||
SSL_HOST="${server_ip}"
|
||||
fi
|
||||
|
||||
echo -e "${green}✓ Custom certificate paths applied.${plain}"
|
||||
echo -e "${yellow}Note: You are responsible for renewing these files externally.${plain}"
|
||||
|
||||
systemctl restart x-ui >/dev/null 2>&1 || rc-service x-ui restart >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
|
||||
SSL_HOST="${server_ip}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
config_after_update() {
|
||||
echo -e "${yellow}x-ui settings:${plain}"
|
||||
${xui_folder}/x-ui setting -show true
|
||||
${xui_folder}/x-ui migrate
|
||||
|
||||
# Properly detect empty cert by checking if cert: line exists and has content after it
|
||||
local existing_cert=$(${xui_folder}/x-ui setting -getCert true 2>/dev/null | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
|
||||
local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
|
||||
local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}' | sed 's#^/##')
|
||||
|
||||
# Get server IP
|
||||
local URL_lists=(
|
||||
"https://api4.ipify.org"
|
||||
"https://ipv4.icanhazip.com"
|
||||
"https://v4.api.ipinfo.io/ip"
|
||||
"https://ipv4.myexternalip.com/raw"
|
||||
"https://4.ident.me"
|
||||
"https://check-host.net/ip"
|
||||
)
|
||||
local server_ip=""
|
||||
for ip_address in "${URL_lists[@]}"; do
|
||||
server_ip=$(${curl_bin} -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]')
|
||||
if [[ -n "${server_ip}" ]]; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Handle missing/short webBasePath
|
||||
if [[ ${#existing_webBasePath} -lt 4 ]]; then
|
||||
echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}"
|
||||
local config_webBasePath=$(gen_random_string 18)
|
||||
${xui_folder}/x-ui setting -webBasePath "${config_webBasePath}"
|
||||
existing_webBasePath="${config_webBasePath}"
|
||||
echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}"
|
||||
fi
|
||||
|
||||
# Check and prompt for SSL if missing
|
||||
if [[ -z "$existing_cert" ]]; then
|
||||
echo ""
|
||||
echo -e "${red}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${red} ⚠ NO SSL CERTIFICATE DETECTED ⚠ ${plain}"
|
||||
echo -e "${red}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${yellow}For security, SSL certificate is MANDATORY for all panels.${plain}"
|
||||
echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}"
|
||||
echo ""
|
||||
|
||||
if [[ -z "${server_ip}" ]]; then
|
||||
echo -e "${red}Failed to detect server IP${plain}"
|
||||
echo -e "${yellow}Please configure SSL manually using: x-ui${plain}"
|
||||
return
|
||||
fi
|
||||
|
||||
# Prompt and setup SSL (domain or IP)
|
||||
prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}"
|
||||
|
||||
echo ""
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${green} Panel Access Information ${plain}"
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${existing_webBasePath}${plain}"
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${yellow}⚠ SSL Certificate: Enabled and configured${plain}"
|
||||
else
|
||||
echo -e "${green}SSL certificate is already configured${plain}"
|
||||
# Show access URL with existing certificate
|
||||
local cert_domain=$(basename "$(dirname "$existing_cert")")
|
||||
echo ""
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${green} Panel Access Information ${plain}"
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${green}Access URL: https://${cert_domain}:${existing_port}/${existing_webBasePath}${plain}"
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
fi
|
||||
}
|
||||
|
||||
update_x-ui() {
|
||||
cd ${xui_folder%/x-ui}/
|
||||
|
||||
if [ -f "${xui_folder}/x-ui" ]; then
|
||||
current_xui_version=$(${xui_folder}/x-ui -v)
|
||||
echo -e "${green}Current x-ui version: ${current_xui_version}${plain}"
|
||||
else
|
||||
_fail "ERROR: Current x-ui version: unknown"
|
||||
fi
|
||||
|
||||
echo -e "${green}Downloading new x-ui version...${plain}"
|
||||
|
||||
tag_version=$(${curl_bin} -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" 2>/dev/null | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
if [[ ! -n "$tag_version" ]]; then
|
||||
echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
|
||||
tag_version=$(${curl_bin} -4 -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
if [[ ! -n "$tag_version" ]]; then
|
||||
_fail "ERROR: Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later"
|
||||
fi
|
||||
fi
|
||||
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
|
||||
${curl_bin} -fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
|
||||
${curl_bin} -4fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null
|
||||
if [[ $? -ne 0 ]]; then
|
||||
_fail "ERROR: Failed to download x-ui, please be sure that your server can access GitHub"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -e ${xui_folder}/ ]]; then
|
||||
echo -e "${green}Stopping x-ui...${plain}"
|
||||
if [[ $release == "alpine" ]]; then
|
||||
if [ -f "/etc/init.d/x-ui" ]; then
|
||||
rc-service x-ui stop >/dev/null 2>&1
|
||||
rc-update del x-ui >/dev/null 2>&1
|
||||
echo -e "${green}Removing old service unit version...${plain}"
|
||||
rm -f /etc/init.d/x-ui >/dev/null 2>&1
|
||||
else
|
||||
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
|
||||
_fail "ERROR: x-ui service unit not installed."
|
||||
fi
|
||||
else
|
||||
if [ -f "${xui_service}/x-ui.service" ]; then
|
||||
systemctl stop x-ui >/dev/null 2>&1
|
||||
systemctl disable x-ui >/dev/null 2>&1
|
||||
echo -e "${green}Removing old systemd unit version...${plain}"
|
||||
rm ${xui_service}/x-ui.service -f >/dev/null 2>&1
|
||||
systemctl daemon-reload >/dev/null 2>&1
|
||||
else
|
||||
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
|
||||
_fail "ERROR: x-ui systemd unit not installed."
|
||||
fi
|
||||
fi
|
||||
echo -e "${green}Removing old x-ui version...${plain}"
|
||||
rm ${xui_folder} -f >/dev/null 2>&1
|
||||
rm ${xui_folder}/x-ui.service -f >/dev/null 2>&1
|
||||
rm ${xui_folder}/x-ui.service.debian -f >/dev/null 2>&1
|
||||
rm ${xui_folder}/x-ui.service.arch -f >/dev/null 2>&1
|
||||
rm ${xui_folder}/x-ui.service.rhel -f >/dev/null 2>&1
|
||||
rm ${xui_folder}/x-ui -f >/dev/null 2>&1
|
||||
rm ${xui_folder}/x-ui.sh -f >/dev/null 2>&1
|
||||
echo -e "${green}Removing old xray version...${plain}"
|
||||
rm ${xui_folder}/bin/xray-linux-amd64 -f >/dev/null 2>&1
|
||||
echo -e "${green}Removing old README and LICENSE file...${plain}"
|
||||
rm ${xui_folder}/bin/README.md -f >/dev/null 2>&1
|
||||
rm ${xui_folder}/bin/LICENSE -f >/dev/null 2>&1
|
||||
else
|
||||
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
|
||||
_fail "ERROR: x-ui not installed."
|
||||
fi
|
||||
|
||||
echo -e "${green}Installing new x-ui version...${plain}"
|
||||
tar zxvf x-ui-linux-$(arch).tar.gz >/dev/null 2>&1
|
||||
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
|
||||
cd x-ui >/dev/null 2>&1
|
||||
chmod +x x-ui >/dev/null 2>&1
|
||||
|
||||
# Check the system's architecture and rename the file accordingly
|
||||
if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then
|
||||
mv bin/xray-linux-$(arch) bin/xray-linux-arm >/dev/null 2>&1
|
||||
chmod +x bin/xray-linux-arm >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
chmod +x x-ui bin/xray-linux-$(arch) >/dev/null 2>&1
|
||||
|
||||
echo -e "${green}Downloading and installing x-ui.sh script...${plain}"
|
||||
${curl_bin} -fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${yellow}Trying to fetch x-ui with IPv4...${plain}"
|
||||
${curl_bin} -4fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
_fail "ERROR: Failed to download x-ui.sh script, please be sure that your server can access GitHub"
|
||||
fi
|
||||
fi
|
||||
|
||||
chmod +x ${xui_folder}/x-ui.sh >/dev/null 2>&1
|
||||
chmod +x /usr/bin/x-ui >/dev/null 2>&1
|
||||
mkdir -p /var/log/x-ui >/dev/null 2>&1
|
||||
|
||||
echo -e "${green}Changing owner...${plain}"
|
||||
chown -R root:root ${xui_folder} >/dev/null 2>&1
|
||||
|
||||
if [ -f "${xui_folder}/bin/config.json" ]; then
|
||||
echo -e "${green}Changing on config file permissions...${plain}"
|
||||
chmod 640 ${xui_folder}/bin/config.json >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
if [[ $release == "alpine" ]]; then
|
||||
echo -e "${green}Downloading and installing startup unit x-ui.rc...${plain}"
|
||||
${curl_bin} -fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc >/dev/null 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
${curl_bin} -4fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc >/dev/null 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
_fail "ERROR: Failed to download startup unit x-ui.rc, please be sure that your server can access GitHub"
|
||||
fi
|
||||
fi
|
||||
chmod +x /etc/init.d/x-ui >/dev/null 2>&1
|
||||
chown root:root /etc/init.d/x-ui >/dev/null 2>&1
|
||||
rc-update add x-ui >/dev/null 2>&1
|
||||
rc-service x-ui start >/dev/null 2>&1
|
||||
else
|
||||
if [ -f "x-ui.service" ]; then
|
||||
echo -e "${green}Installing systemd unit...${plain}"
|
||||
cp -f x-ui.service ${xui_service}/ >/dev/null 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${red}Failed to copy x-ui.service${plain}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
service_installed=false
|
||||
case "${release}" in
|
||||
ubuntu | debian | armbian)
|
||||
if [ -f "x-ui.service.debian" ]; then
|
||||
echo -e "${green}Installing debian-like systemd unit...${plain}"
|
||||
cp -f x-ui.service.debian ${xui_service}/x-ui.service >/dev/null 2>&1
|
||||
if [[ $? -eq 0 ]]; then
|
||||
service_installed=true
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
arch | manjaro | parch)
|
||||
if [ -f "x-ui.service.arch" ]; then
|
||||
echo -e "${green}Installing arch-like systemd unit...${plain}"
|
||||
cp -f x-ui.service.arch ${xui_service}/x-ui.service >/dev/null 2>&1
|
||||
if [[ $? -eq 0 ]]; then
|
||||
service_installed=true
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
if [ -f "x-ui.service.rhel" ]; then
|
||||
echo -e "${green}Installing rhel-like systemd unit...${plain}"
|
||||
cp -f x-ui.service.rhel ${xui_service}/x-ui.service >/dev/null 2>&1
|
||||
if [[ $? -eq 0 ]]; then
|
||||
service_installed=true
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# If service file not found in tar.gz, download from GitHub
|
||||
if [ "$service_installed" = false ]; then
|
||||
echo -e "${yellow}Service files not found in tar.gz, downloading from GitHub...${plain}"
|
||||
case "${release}" in
|
||||
ubuntu | debian | armbian)
|
||||
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian >/dev/null 2>&1
|
||||
;;
|
||||
arch | manjaro | parch)
|
||||
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel >/dev/null 2>&1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${red}Failed to install x-ui.service from GitHub${plain}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
chown root:root ${xui_service}/x-ui.service >/dev/null 2>&1
|
||||
chmod 644 ${xui_service}/x-ui.service >/dev/null 2>&1
|
||||
systemctl daemon-reload >/dev/null 2>&1
|
||||
systemctl enable x-ui >/dev/null 2>&1
|
||||
systemctl start x-ui >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
config_after_update
|
||||
|
||||
echo -e "${green}x-ui ${tag_version}${plain} updating finished, it is running now..."
|
||||
echo -e ""
|
||||
echo -e "┌───────────────────────────────────────────────────────┐
|
||||
│ ${blue}x-ui control menu usages (subcommands):${plain} │
|
||||
│ │
|
||||
│ ${blue}x-ui${plain} - Admin Management Script │
|
||||
│ ${blue}x-ui start${plain} - Start │
|
||||
│ ${blue}x-ui stop${plain} - Stop │
|
||||
│ ${blue}x-ui restart${plain} - Restart │
|
||||
│ ${blue}x-ui status${plain} - Current Status │
|
||||
│ ${blue}x-ui settings${plain} - Current Settings │
|
||||
│ ${blue}x-ui enable${plain} - Enable Autostart on OS Startup │
|
||||
│ ${blue}x-ui disable${plain} - Disable Autostart on OS Startup │
|
||||
│ ${blue}x-ui log${plain} - Check logs │
|
||||
│ ${blue}x-ui banlog${plain} - Check Fail2ban ban logs │
|
||||
│ ${blue}x-ui update${plain} - Update │
|
||||
│ ${blue}x-ui legacy${plain} - Legacy version │
|
||||
│ ${blue}x-ui install${plain} - Install │
|
||||
│ ${blue}x-ui uninstall${plain} - Uninstall │
|
||||
└───────────────────────────────────────────────────────┘"
|
||||
}
|
||||
|
||||
echo -e "${green}Running...${plain}"
|
||||
install_base
|
||||
update_x-ui $1
|
||||
@@ -1,23 +1,27 @@
|
||||
// Package common provides common utility functions for error handling, formatting, and multi-error management.
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"x-ui/logger"
|
||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||
)
|
||||
|
||||
func NewErrorf(format string, a ...interface{}) error {
|
||||
// NewErrorf creates a new error with formatted message.
|
||||
func NewErrorf(format string, a ...any) error {
|
||||
msg := fmt.Sprintf(format, a...)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
func NewError(a ...interface{}) error {
|
||||
// NewError creates a new error from the given arguments.
|
||||
func NewError(a ...any) error {
|
||||
msg := fmt.Sprintln(a...)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
func Recover(msg string) interface{} {
|
||||
// Recover handles panic recovery and logs the panic error if a message is provided.
|
||||
func Recover(msg string) any {
|
||||
panicErr := recover()
|
||||
if panicErr != nil {
|
||||
if msg != "" {
|
||||
|
||||
@@ -4,18 +4,15 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func FormatTraffic(trafficBytes int64) (size string) {
|
||||
if trafficBytes < 1024 {
|
||||
return fmt.Sprintf("%.2fB", float64(trafficBytes)/float64(1))
|
||||
} else if trafficBytes < (1024 * 1024) {
|
||||
return fmt.Sprintf("%.2fKB", float64(trafficBytes)/float64(1024))
|
||||
} else if trafficBytes < (1024 * 1024 * 1024) {
|
||||
return fmt.Sprintf("%.2fMB", float64(trafficBytes)/float64(1024*1024))
|
||||
} else if trafficBytes < (1024 * 1024 * 1024 * 1024) {
|
||||
return fmt.Sprintf("%.2fGB", float64(trafficBytes)/float64(1024*1024*1024))
|
||||
} else if trafficBytes < (1024 * 1024 * 1024 * 1024 * 1024) {
|
||||
return fmt.Sprintf("%.2fTB", float64(trafficBytes)/float64(1024*1024*1024*1024))
|
||||
} else {
|
||||
return fmt.Sprintf("%.2fEB", float64(trafficBytes)/float64(1024*1024*1024*1024*1024))
|
||||
// FormatTraffic formats traffic bytes into human-readable units (B, KB, MB, GB, TB, PB).
|
||||
func FormatTraffic(trafficBytes int64) string {
|
||||
units := []string{"B", "KB", "MB", "GB", "TB", "PB"}
|
||||
unitIndex := 0
|
||||
size := float64(trafficBytes)
|
||||
|
||||
for size >= 1024 && unitIndex < len(units)-1 {
|
||||
size /= 1024
|
||||
unitIndex++
|
||||
}
|
||||
return fmt.Sprintf("%.2f%s", size, units[unitIndex])
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// multiError represents a collection of errors.
|
||||
type multiError []error
|
||||
|
||||
// Error returns a string representation of all errors joined with " | ".
|
||||
func (e multiError) Error() string {
|
||||
var r strings.Builder
|
||||
r.WriteString("multierr: ")
|
||||
@@ -16,6 +18,7 @@ func (e multiError) Error() string {
|
||||
return r.String()
|
||||
}
|
||||
|
||||
// Combine combines multiple errors into a single error, filtering out nil errors.
|
||||
func Combine(maybeError ...error) error {
|
||||
var errs multiError
|
||||
for _, err := range maybeError {
|
||||
|
||||
17
util/crypto/crypto.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Package crypto provides cryptographic utilities for password hashing and verification.
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// HashPasswordAsBcrypt generates a bcrypt hash of the given password.
|
||||
func HashPasswordAsBcrypt(password string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(hash), err
|
||||
}
|
||||
|
||||
// CheckPasswordHash verifies if the given password matches the bcrypt hash.
|
||||
func CheckPasswordHash(hash, password string) bool {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
// Package json_util provides JSON utilities including a custom RawMessage type.
|
||||
package json_util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// RawMessage is a custom JSON raw message type that marshals empty slices as "null".
|
||||
type RawMessage []byte
|
||||
|
||||
// MarshalJSON: Customize json.RawMessage default behavior
|
||||
// MarshalJSON customizes the JSON marshaling behavior for RawMessage.
|
||||
// Empty RawMessage values are marshaled as "null" instead of "[]".
|
||||
func (m RawMessage) MarshalJSON() ([]byte, error) {
|
||||
if len(m) == 0 {
|
||||
return []byte("null"), nil
|
||||
@@ -14,7 +17,7 @@ func (m RawMessage) MarshalJSON() ([]byte, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON: sets *m to a copy of data.
|
||||
// UnmarshalJSON sets *m to a copy of the JSON data.
|
||||
func (m *RawMessage) UnmarshalJSON(data []byte) error {
|
||||
if m == nil {
|
||||
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
|
||||
|
||||
160
util/ldap/ldap.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package ldaputil
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
UseTLS bool
|
||||
BindDN string
|
||||
Password string
|
||||
BaseDN string
|
||||
UserFilter string
|
||||
UserAttr string
|
||||
FlagField string
|
||||
TruthyVals []string
|
||||
Invert bool
|
||||
}
|
||||
|
||||
// FetchVlessFlags returns map[email]enabled
|
||||
func FetchVlessFlags(cfg Config) (map[string]bool, error) {
|
||||
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
|
||||
|
||||
scheme := "ldap"
|
||||
if cfg.UseTLS {
|
||||
scheme = "ldaps"
|
||||
}
|
||||
|
||||
ldapURL := fmt.Sprintf("%s://%s", scheme, addr)
|
||||
|
||||
var opts []ldap.DialOpt
|
||||
if cfg.UseTLS {
|
||||
opts = append(opts, ldap.DialWithTLSConfig(&tls.Config{
|
||||
InsecureSkipVerify: false,
|
||||
}))
|
||||
}
|
||||
|
||||
conn, err := ldap.DialURL(ldapURL, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if cfg.BindDN != "" {
|
||||
if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.UserFilter == "" {
|
||||
cfg.UserFilter = "(objectClass=person)"
|
||||
}
|
||||
if cfg.UserAttr == "" {
|
||||
cfg.UserAttr = "mail"
|
||||
}
|
||||
// if field not set we fallback to legacy vless_enabled
|
||||
if cfg.FlagField == "" {
|
||||
cfg.FlagField = "vless_enabled"
|
||||
}
|
||||
|
||||
req := ldap.NewSearchRequest(
|
||||
cfg.BaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
cfg.UserFilter,
|
||||
[]string{cfg.UserAttr, cfg.FlagField},
|
||||
nil,
|
||||
)
|
||||
|
||||
res, err := conn.Search(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string]bool, len(res.Entries))
|
||||
for _, e := range res.Entries {
|
||||
user := e.GetAttributeValue(cfg.UserAttr)
|
||||
if user == "" {
|
||||
continue
|
||||
}
|
||||
val := e.GetAttributeValue(cfg.FlagField)
|
||||
enabled := false
|
||||
for _, t := range cfg.TruthyVals {
|
||||
if val == t {
|
||||
enabled = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if cfg.Invert {
|
||||
enabled = !enabled
|
||||
}
|
||||
result[user] = enabled
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// AuthenticateUser searches user by cfg.UserAttr and attempts to bind with provided password.
|
||||
func AuthenticateUser(cfg Config, username, password string) (bool, error) {
|
||||
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
|
||||
|
||||
scheme := "ldap"
|
||||
if cfg.UseTLS {
|
||||
scheme = "ldaps"
|
||||
}
|
||||
|
||||
ldapURL := fmt.Sprintf("%s://%s", scheme, addr)
|
||||
|
||||
var opts []ldap.DialOpt
|
||||
if cfg.UseTLS {
|
||||
opts = append(opts, ldap.DialWithTLSConfig(&tls.Config{
|
||||
InsecureSkipVerify: false,
|
||||
}))
|
||||
}
|
||||
|
||||
conn, err := ldap.DialURL(ldapURL, opts...)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Optional initial bind for search
|
||||
if cfg.BindDN != "" {
|
||||
if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.UserFilter == "" {
|
||||
cfg.UserFilter = "(objectClass=person)"
|
||||
}
|
||||
if cfg.UserAttr == "" {
|
||||
cfg.UserAttr = "uid"
|
||||
}
|
||||
|
||||
// Build filter to find specific user
|
||||
filter := fmt.Sprintf("(&%s(%s=%s))", cfg.UserFilter, cfg.UserAttr, ldap.EscapeFilter(username))
|
||||
req := ldap.NewSearchRequest(
|
||||
cfg.BaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false,
|
||||
filter,
|
||||
[]string{"dn"},
|
||||
nil,
|
||||
)
|
||||
res, err := conn.Search(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(res.Entries) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
userDN := res.Entries[0].DN
|
||||
// Try to bind as the user
|
||||
if err := conn.Bind(userDN, password); err != nil {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
// Package random provides utilities for generating random strings and numbers.
|
||||
package random
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -13,11 +15,13 @@ var (
|
||||
allSeq [62]rune
|
||||
)
|
||||
|
||||
// init initializes the character sequences used for random string generation.
|
||||
// It sets up arrays for numbers, lowercase letters, uppercase letters, and combinations.
|
||||
func init() {
|
||||
for i := 0; i < 10; i++ {
|
||||
for i := range 10 {
|
||||
numSeq[i] = rune('0' + i)
|
||||
}
|
||||
for i := 0; i < 26; i++ {
|
||||
for i := range 26 {
|
||||
lowerSeq[i] = rune('a' + i)
|
||||
upperSeq[i] = rune('A' + i)
|
||||
}
|
||||
@@ -33,14 +37,25 @@ func init() {
|
||||
copy(allSeq[len(numSeq)+len(lowerSeq):], upperSeq[:])
|
||||
}
|
||||
|
||||
// Seq generates a random string of length n containing alphanumeric characters (numbers, lowercase and uppercase letters).
|
||||
func Seq(n int) string {
|
||||
runes := make([]rune, n)
|
||||
for i := 0; i < n; i++ {
|
||||
runes[i] = allSeq[rand.Intn(len(allSeq))]
|
||||
for i := range n {
|
||||
idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(allSeq))))
|
||||
if err != nil {
|
||||
panic("crypto/rand failed: " + err.Error())
|
||||
}
|
||||
runes[i] = allSeq[idx.Int64()]
|
||||
}
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
// Num generates a random integer between 0 and n-1.
|
||||
func Num(n int) int {
|
||||
return rand.Intn(n)
|
||||
bn := big.NewInt(int64(n))
|
||||
r, err := rand.Int(rand.Reader, bn)
|
||||
if err != nil {
|
||||
panic("crypto/rand failed: " + err.Error())
|
||||
}
|
||||
return int(r.Int64())
|
||||
}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
// Package reflect_util provides reflection utilities for working with struct fields and values.
|
||||
package reflect_util
|
||||
|
||||
import "reflect"
|
||||
|
||||
// GetFields returns all struct fields of the given reflect.Type.
|
||||
func GetFields(t reflect.Type) []reflect.StructField {
|
||||
num := t.NumField()
|
||||
fields := make([]reflect.StructField, 0, num)
|
||||
for i := 0; i < num; i++ {
|
||||
for i := range num {
|
||||
fields = append(fields, t.Field(i))
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
// GetFieldValues returns all field values of the given reflect.Value.
|
||||
func GetFieldValues(v reflect.Value) []reflect.Value {
|
||||
num := v.NumField()
|
||||
fields := make([]reflect.Value, 0, num)
|
||||
for i := 0; i < num; i++ {
|
||||
for i := range num {
|
||||
fields = append(fields, v.Field(i))
|
||||
}
|
||||
return fields
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Package sys provides system utilities for monitoring network connections and CPU usage.
|
||||
// Platform-specific implementations are provided for Windows, Linux, and macOS.
|
||||
package sys
|
||||
|
||||
import (
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func GetTCPCount() (int, error) {
|
||||
@@ -22,3 +27,69 @@ func GetUDPCount() (int, error) {
|
||||
}
|
||||
return len(stats), nil
|
||||
}
|
||||
|
||||
// --- CPU Utilization (macOS native) ---
|
||||
|
||||
// sysctl kern.cp_time returns an array of 5 longs: user, nice, sys, idle, intr.
|
||||
// We compute utilization deltas without cgo.
|
||||
var (
|
||||
cpuMu sync.Mutex
|
||||
lastTotals [5]uint64
|
||||
hasLastCPUT bool
|
||||
)
|
||||
|
||||
func CPUPercentRaw() (float64, error) {
|
||||
raw, err := unix.SysctlRaw("kern.cp_time")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// Expect either 5*8 bytes (uint64) or 5*4 bytes (uint32)
|
||||
var out [5]uint64
|
||||
switch len(raw) {
|
||||
case 5 * 8:
|
||||
for i := range 5 {
|
||||
out[i] = binary.LittleEndian.Uint64(raw[i*8 : (i+1)*8])
|
||||
}
|
||||
case 5 * 4:
|
||||
for i := range 5 {
|
||||
out[i] = uint64(binary.LittleEndian.Uint32(raw[i*4 : (i+1)*4]))
|
||||
}
|
||||
default:
|
||||
return 0, fmt.Errorf("unexpected kern.cp_time size: %d", len(raw))
|
||||
}
|
||||
|
||||
// user, nice, sys, idle, intr
|
||||
user := out[0]
|
||||
nice := out[1]
|
||||
sysv := out[2]
|
||||
idle := out[3]
|
||||
intr := out[4]
|
||||
|
||||
cpuMu.Lock()
|
||||
defer cpuMu.Unlock()
|
||||
|
||||
if !hasLastCPUT {
|
||||
lastTotals = out
|
||||
hasLastCPUT = true
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
dUser := user - lastTotals[0]
|
||||
dNice := nice - lastTotals[1]
|
||||
dSys := sysv - lastTotals[2]
|
||||
dIdle := idle - lastTotals[3]
|
||||
dIntr := intr - lastTotals[4]
|
||||
|
||||
lastTotals = out
|
||||
|
||||
totald := dUser + dNice + dSys + dIdle + dIntr
|
||||
if totald == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
busy := totald - dIdle
|
||||
pct := float64(busy) / float64(totald) * 100.0
|
||||
if pct > 100 {
|
||||
pct = 100
|
||||
}
|
||||
return pct, nil
|
||||
}
|
||||
|
||||
@@ -4,10 +4,14 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func getLinesNum(filename string) (int, error) {
|
||||
@@ -41,14 +45,16 @@ func getLinesNum(filename string) (int, error) {
|
||||
return sum, nil
|
||||
}
|
||||
|
||||
// GetTCPCount returns the number of active TCP connections by reading
|
||||
// /proc/net/tcp and /proc/net/tcp6 when available.
|
||||
func GetTCPCount() (int, error) {
|
||||
root := HostProc()
|
||||
|
||||
tcp4, err := getLinesNum(fmt.Sprintf("%v/net/tcp", root))
|
||||
tcp4, err := safeGetLinesNum(fmt.Sprintf("%v/net/tcp", root))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
tcp6, err := getLinesNum(fmt.Sprintf("%v/net/tcp6", root))
|
||||
tcp6, err := safeGetLinesNum(fmt.Sprintf("%v/net/tcp6", root))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -59,14 +65,121 @@ func GetTCPCount() (int, error) {
|
||||
func GetUDPCount() (int, error) {
|
||||
root := HostProc()
|
||||
|
||||
udp4, err := getLinesNum(fmt.Sprintf("%v/net/udp", root))
|
||||
udp4, err := safeGetLinesNum(fmt.Sprintf("%v/net/udp", root))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
udp6, err := getLinesNum(fmt.Sprintf("%v/net/udp6", root))
|
||||
udp6, err := safeGetLinesNum(fmt.Sprintf("%v/net/udp6", root))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return udp4 + udp6, nil
|
||||
}
|
||||
|
||||
// safeGetLinesNum returns 0 if the file does not exist, otherwise forwards
|
||||
// to getLinesNum to count the number of lines.
|
||||
func safeGetLinesNum(path string) (int, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
} else if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return getLinesNum(path)
|
||||
}
|
||||
|
||||
// --- CPU Utilization (Linux native) ---
|
||||
|
||||
var (
|
||||
cpuMu sync.Mutex
|
||||
lastTotal uint64
|
||||
lastIdleAll uint64
|
||||
hasLast bool
|
||||
)
|
||||
|
||||
// CPUPercentRaw returns instantaneous total CPU utilization by reading /proc/stat.
|
||||
// First call initializes and returns 0; subsequent calls return busy/total * 100.
|
||||
func CPUPercentRaw() (float64, error) {
|
||||
f, err := os.Open("/proc/stat")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
rd := bufio.NewReader(f)
|
||||
line, err := rd.ReadString('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
return 0, err
|
||||
}
|
||||
// Expect line like: cpu user nice system idle iowait irq softirq steal guest guest_nice
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 5 || fields[0] != "cpu" {
|
||||
return 0, fmt.Errorf("unexpected /proc/stat format")
|
||||
}
|
||||
|
||||
var nums []uint64
|
||||
for i := 1; i < len(fields); i++ {
|
||||
v, err := strconv.ParseUint(fields[i], 10, 64)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
nums = append(nums, v)
|
||||
}
|
||||
if len(nums) < 4 { // need at least user,nice,system,idle
|
||||
return 0, fmt.Errorf("insufficient cpu fields")
|
||||
}
|
||||
|
||||
// Conform with standard Linux CPU accounting
|
||||
var user, nice, system, idle, iowait, irq, softirq, steal uint64
|
||||
user = nums[0]
|
||||
if len(nums) > 1 {
|
||||
nice = nums[1]
|
||||
}
|
||||
if len(nums) > 2 {
|
||||
system = nums[2]
|
||||
}
|
||||
if len(nums) > 3 {
|
||||
idle = nums[3]
|
||||
}
|
||||
if len(nums) > 4 {
|
||||
iowait = nums[4]
|
||||
}
|
||||
if len(nums) > 5 {
|
||||
irq = nums[5]
|
||||
}
|
||||
if len(nums) > 6 {
|
||||
softirq = nums[6]
|
||||
}
|
||||
if len(nums) > 7 {
|
||||
steal = nums[7]
|
||||
}
|
||||
|
||||
idleAll := idle + iowait
|
||||
nonIdle := user + nice + system + irq + softirq + steal
|
||||
total := idleAll + nonIdle
|
||||
|
||||
cpuMu.Lock()
|
||||
defer cpuMu.Unlock()
|
||||
|
||||
if !hasLast {
|
||||
lastTotal = total
|
||||
lastIdleAll = idleAll
|
||||
hasLast = true
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
totald := total - lastTotal
|
||||
idled := idleAll - lastIdleAll
|
||||
lastTotal = total
|
||||
lastIdleAll = idleAll
|
||||
|
||||
if totald == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
busy := totald - idled
|
||||
pct := float64(busy) / float64(totald) * 100.0
|
||||
if pct > 100 {
|
||||
pct = 100
|
||||
}
|
||||
return pct, nil
|
||||
}
|
||||
|
||||
@@ -5,10 +5,14 @@ package sys
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
)
|
||||
|
||||
// GetConnectionCount returns the number of active connections for the specified protocol ("tcp" or "udp").
|
||||
func GetConnectionCount(proto string) (int, error) {
|
||||
if proto != "tcp" && proto != "udp" {
|
||||
return 0, errors.New("invalid protocol")
|
||||
@@ -21,10 +25,92 @@ func GetConnectionCount(proto string) (int, error) {
|
||||
return len(stats), nil
|
||||
}
|
||||
|
||||
// GetTCPCount returns the number of active TCP connections.
|
||||
func GetTCPCount() (int, error) {
|
||||
return GetConnectionCount("tcp")
|
||||
}
|
||||
|
||||
// GetUDPCount returns the number of active UDP connections.
|
||||
func GetUDPCount() (int, error) {
|
||||
return GetConnectionCount("udp")
|
||||
}
|
||||
|
||||
// --- CPU Utilization (Windows native) ---
|
||||
|
||||
var (
|
||||
modKernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
procGetSystemTimes = modKernel32.NewProc("GetSystemTimes")
|
||||
|
||||
cpuMu sync.Mutex
|
||||
lastIdle uint64
|
||||
lastKernel uint64
|
||||
lastUser uint64
|
||||
hasLast bool
|
||||
)
|
||||
|
||||
type filetime struct {
|
||||
LowDateTime uint32
|
||||
HighDateTime uint32
|
||||
}
|
||||
|
||||
// ftToUint64 converts a Windows FILETIME-like struct to a uint64 for
|
||||
// arithmetic and delta calculations used by CPUPercentRaw.
|
||||
func ftToUint64(ft filetime) uint64 {
|
||||
return (uint64(ft.HighDateTime) << 32) | uint64(ft.LowDateTime)
|
||||
}
|
||||
|
||||
// CPUPercentRaw returns the instantaneous total CPU utilization percentage using
|
||||
// Windows GetSystemTimes across all logical processors. The first call returns 0
|
||||
// as it initializes the baseline. Subsequent calls compute deltas.
|
||||
func CPUPercentRaw() (float64, error) {
|
||||
var idleFT, kernelFT, userFT filetime
|
||||
r1, _, e1 := procGetSystemTimes.Call(
|
||||
uintptr(unsafe.Pointer(&idleFT)),
|
||||
uintptr(unsafe.Pointer(&kernelFT)),
|
||||
uintptr(unsafe.Pointer(&userFT)),
|
||||
)
|
||||
if r1 == 0 { // failure
|
||||
if e1 != nil {
|
||||
return 0, e1
|
||||
}
|
||||
return 0, syscall.GetLastError()
|
||||
}
|
||||
|
||||
idle := ftToUint64(idleFT)
|
||||
kernel := ftToUint64(kernelFT)
|
||||
user := ftToUint64(userFT)
|
||||
|
||||
cpuMu.Lock()
|
||||
defer cpuMu.Unlock()
|
||||
|
||||
if !hasLast {
|
||||
lastIdle = idle
|
||||
lastKernel = kernel
|
||||
lastUser = user
|
||||
hasLast = true
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
idleDelta := idle - lastIdle
|
||||
kernelDelta := kernel - lastKernel
|
||||
userDelta := user - lastUser
|
||||
|
||||
// Update for next call
|
||||
lastIdle = idle
|
||||
lastKernel = kernel
|
||||
lastUser = user
|
||||
|
||||
total := kernelDelta + userDelta
|
||||
if total == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
// On Windows, kernel time includes idle time; busy = total - idle
|
||||
busy := total - idleDelta
|
||||
|
||||
pct := float64(busy) / float64(total) * 100.0
|
||||
// lower bound not needed; ratios of uint64 are non-negative
|
||||
if pct > 100 {
|
||||
pct = 100
|
||||
}
|
||||
return pct, nil
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
@import "../lib/style/index.less";
|
||||
@import "../lib/style/components.less";
|
||||
|
||||
@green-6: #008771;
|
||||
@primary-color: @green-6;
|
||||
@border-radius-base: 1rem;
|
||||
@progress-remaining-color: #EDEDED;
|
||||
3
web/assets/axios/axios.min.js
vendored
1
web/assets/axios/axios.min.js.map
Normal file
1
web/assets/base64/base64.min.js
vendored
@@ -1 +0,0 @@
|
||||
(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory(global):typeof define==="function"&&define.amd?define(factory):factory(global)})(typeof self!=="undefined"?self:typeof window!=="undefined"?window:typeof global!=="undefined"?global:this,function(global){"use strict";var _Base64=global.Base64;var version="2.5.0";var buffer;if(typeof module!=="undefined"&&module.exports){try{buffer=eval("require('buffer').Buffer")}catch(err){buffer=undefined}}var b64chars="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";var b64tab=function(bin){var t={};for(var i=0,l=bin.length;i<l;i++)t[bin.charAt(i)]=i;return t}(b64chars);var fromCharCode=String.fromCharCode;var cb_utob=function(c){if(c.length<2){var cc=c.charCodeAt(0);return cc<128?c:cc<2048?fromCharCode(192|cc>>>6)+fromCharCode(128|cc&63):fromCharCode(224|cc>>>12&15)+fromCharCode(128|cc>>>6&63)+fromCharCode(128|cc&63)}else{var cc=65536+(c.charCodeAt(0)-55296)*1024+(c.charCodeAt(1)-56320);return fromCharCode(240|cc>>>18&7)+fromCharCode(128|cc>>>12&63)+fromCharCode(128|cc>>>6&63)+fromCharCode(128|cc&63)}};var re_utob=/[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g;var utob=function(u){return u.replace(re_utob,cb_utob)};var cb_encode=function(ccc){var padlen=[0,2,1][ccc.length%3],ord=ccc.charCodeAt(0)<<16|(ccc.length>1?ccc.charCodeAt(1):0)<<8|(ccc.length>2?ccc.charCodeAt(2):0),chars=[b64chars.charAt(ord>>>18),b64chars.charAt(ord>>>12&63),padlen>=2?"=":b64chars.charAt(ord>>>6&63),padlen>=1?"=":b64chars.charAt(ord&63)];return chars.join("")};var btoa=global.btoa?function(b){return global.btoa(b)}:function(b){return b.replace(/[\s\S]{1,3}/g,cb_encode)};var _encode=buffer?buffer.from&&Uint8Array&&buffer.from!==Uint8Array.from?function(u){return(u.constructor===buffer.constructor?u:buffer.from(u)).toString("base64")}:function(u){return(u.constructor===buffer.constructor?u:new buffer(u)).toString("base64")}:function(u){return btoa(utob(u))};var encode=function(u,urisafe){return!urisafe?_encode(String(u)):_encode(String(u)).replace(/[+\/]/g,function(m0){return m0=="+"?"-":"_"}).replace(/=/g,"")};var encodeURI=function(u){return encode(u,true)};var re_btou=new RegExp(["[À-ß][-¿]","[à-ï][-¿]{2}","[ð-÷][-¿]{3}"].join("|"),"g");var cb_btou=function(cccc){switch(cccc.length){case 4:var cp=(7&cccc.charCodeAt(0))<<18|(63&cccc.charCodeAt(1))<<12|(63&cccc.charCodeAt(2))<<6|63&cccc.charCodeAt(3),offset=cp-65536;return fromCharCode((offset>>>10)+55296)+fromCharCode((offset&1023)+56320);case 3:return fromCharCode((15&cccc.charCodeAt(0))<<12|(63&cccc.charCodeAt(1))<<6|63&cccc.charCodeAt(2));default:return fromCharCode((31&cccc.charCodeAt(0))<<6|63&cccc.charCodeAt(1))}};var btou=function(b){return b.replace(re_btou,cb_btou)};var cb_decode=function(cccc){var len=cccc.length,padlen=len%4,n=(len>0?b64tab[cccc.charAt(0)]<<18:0)|(len>1?b64tab[cccc.charAt(1)]<<12:0)|(len>2?b64tab[cccc.charAt(2)]<<6:0)|(len>3?b64tab[cccc.charAt(3)]:0),chars=[fromCharCode(n>>>16),fromCharCode(n>>>8&255),fromCharCode(n&255)];chars.length-=[0,0,2,1][padlen];return chars.join("")};var _atob=global.atob?function(a){return global.atob(a)}:function(a){return a.replace(/\S{1,4}/g,cb_decode)};var atob=function(a){return _atob(String(a).replace(/[^A-Za-z0-9\+\/]/g,""))};var _decode=buffer?buffer.from&&Uint8Array&&buffer.from!==Uint8Array.from?function(a){return(a.constructor===buffer.constructor?a:buffer.from(a,"base64")).toString()}:function(a){return(a.constructor===buffer.constructor?a:new buffer(a,"base64")).toString()}:function(a){return btou(_atob(a))};var decode=function(a){return _decode(String(a).replace(/[-_]/g,function(m0){return m0=="-"?"+":"/"}).replace(/[^A-Za-z0-9\+\/]/g,""))};var noConflict=function(){var Base64=global.Base64;global.Base64=_Base64;return Base64};global.Base64={VERSION:version,atob:atob,btoa:btoa,fromBase64:decode,toBase64:encode,utob:utob,encode:encode,encodeURI:encodeURI,btou:btou,decode:decode,noConflict:noConflict,__buffer__:buffer};if(typeof Object.defineProperty==="function"){var noEnum=function(v){return{value:v,enumerable:false,writable:true,configurable:true}};global.Base64.extendString=function(){Object.defineProperty(String.prototype,"fromBase64",noEnum(function(){return decode(this)}));Object.defineProperty(String.prototype,"toBase64",noEnum(function(urisafe){return encode(this,urisafe)}));Object.defineProperty(String.prototype,"toBase64URI",noEnum(function(){return encode(this,true)}))}}if(global["Meteor"]){Base64=global.Base64}if(typeof module!=="undefined"&&module.exports){module.exports.Base64=global.Base64}else if(typeof define==="function"&&define.amd){define([],function(){return global.Base64})}return{Base64:global.Base64}});
|
||||
7
web/assets/clipboard/clipboard.min.js
vendored
@@ -63,7 +63,7 @@
|
||||
return scriptHint(editor, javascriptKeywords,
|
||||
function (e, cur) {return e.getTokenAt(cur);},
|
||||
options);
|
||||
};
|
||||
}
|
||||
CodeMirror.registerHelper("hint", "javascript", javascriptHint);
|
||||
|
||||
function getCoffeeScriptToken(editor, cur) {
|
||||
|
||||
@@ -362,7 +362,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||
if (type == wanted) return cont();
|
||||
else if (wanted == ";" || type == "}" || type == ")" || type == "]") return pass();
|
||||
else return cont(exp);
|
||||
};
|
||||
}
|
||||
return exp;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
.CodeMirror-lint-tooltip {
|
||||
background-color: #ffd;
|
||||
border: 1px solid black;
|
||||
border-radius: 4px 4px 4px 4px;
|
||||
border-radius: 4px;
|
||||
color: black;
|
||||
font-family: monospace;
|
||||
font-size: 10pt;
|
||||
|
||||
2
web/assets/css/custom.min.css
vendored
@@ -1 +0,0 @@
|
||||
@media only screen and (max-width:767px){.hidden-xs-only{display:none!important}}@media only screen and (min-width:768px){.hidden-sm-and-up{display:none!important}}@media only screen and (min-width:768px) and (max-width:991px){.hidden-sm-only{display:none!important}}@media only screen and (max-width:991px){.hidden-sm-and-down{display:none!important}}@media only screen and (min-width:992px){.hidden-md-and-up{display:none!important}}@media only screen and (min-width:992px) and (max-width:1199px){.hidden-md-only{display:none!important}}@media only screen and (max-width:1199px){.hidden-md-and-down{display:none!important}}@media only screen and (min-width:1200px){.hidden-lg-and-up{display:none!important}}@media only screen and (min-width:1200px) and (max-width:1919px){.hidden-lg-only{display:none!important}}@media only screen and (max-width:1919px){.hidden-lg-and-down{display:none!important}}@media only screen and (min-width:1920px){.hidden-xl-only{display:none!important}}
|
||||
@@ -1,83 +0,0 @@
|
||||
const supportLangs = [
|
||||
{
|
||||
name: 'English',
|
||||
value: 'en-US',
|
||||
icon: '🇺🇸',
|
||||
},
|
||||
{
|
||||
name: 'فارسی',
|
||||
value: 'fa-IR',
|
||||
icon: '🇮🇷',
|
||||
},
|
||||
{
|
||||
name: '汉语',
|
||||
value: 'zh-Hans',
|
||||
icon: '🇨🇳',
|
||||
},
|
||||
{
|
||||
name: 'Русский',
|
||||
value: 'ru-RU',
|
||||
icon: '🇷🇺',
|
||||
},
|
||||
{
|
||||
name: 'Tiếng Việt',
|
||||
value: 'vi-VN',
|
||||
icon: '🇻🇳',
|
||||
},
|
||||
{
|
||||
name: 'Español',
|
||||
value: 'es-ES',
|
||||
icon: '🇪🇸',
|
||||
},
|
||||
{
|
||||
name: 'Indonesian',
|
||||
value: 'id-ID',
|
||||
icon: '🇮🇩',
|
||||
},
|
||||
{
|
||||
name: 'Український',
|
||||
value: 'uk-UA',
|
||||
icon: '🇺🇦',
|
||||
},
|
||||
];
|
||||
|
||||
function getLang() {
|
||||
let lang = getCookie('lang');
|
||||
|
||||
if (!lang) {
|
||||
if (window.navigator) {
|
||||
lang = window.navigator.language || window.navigator.userLanguage;
|
||||
|
||||
if (isSupportLang(lang)) {
|
||||
setCookie('lang', lang, 150);
|
||||
} else {
|
||||
setCookie('lang', 'en-US', 150);
|
||||
window.location.reload();
|
||||
}
|
||||
} else {
|
||||
setCookie('lang', 'en-US', 150);
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
return lang;
|
||||
}
|
||||
|
||||
function setLang(lang) {
|
||||
if (!isSupportLang(lang)) {
|
||||
lang = 'en-US';
|
||||
}
|
||||
|
||||
setCookie('lang', lang, 150);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function isSupportLang(lang) {
|
||||
for (l of supportLangs) {
|
||||
if (l.value === lang) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -6,9 +6,12 @@ class DBInbound {
|
||||
this.up = 0;
|
||||
this.down = 0;
|
||||
this.total = 0;
|
||||
this.allTime = 0;
|
||||
this.remark = "";
|
||||
this.enable = true;
|
||||
this.expiryTime = 0;
|
||||
this.trafficReset = "never";
|
||||
this.lastTrafficResetTime = 0;
|
||||
|
||||
this.listen = "";
|
||||
this.port = 0;
|
||||
@@ -25,11 +28,11 @@ class DBInbound {
|
||||
}
|
||||
|
||||
get totalGB() {
|
||||
return toFixed(this.total / ONE_GB, 2);
|
||||
return NumberFormatter.toFixed(this.total / SizeFormatter.ONE_GB, 2);
|
||||
}
|
||||
|
||||
set totalGB(gb) {
|
||||
this.total = toFixed(gb * ONE_GB, 0);
|
||||
this.total = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);
|
||||
}
|
||||
|
||||
get isVMess() {
|
||||
@@ -48,8 +51,8 @@ class DBInbound {
|
||||
return this.protocol === Protocols.SHADOWSOCKS;
|
||||
}
|
||||
|
||||
get isSocks() {
|
||||
return this.protocol === Protocols.SOCKS;
|
||||
get isMixed() {
|
||||
return this.protocol === Protocols.MIXED;
|
||||
}
|
||||
|
||||
get isHTTP() {
|
||||
@@ -140,9 +143,9 @@ class DBInbound {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
genInboundLinks(remarkModel) {
|
||||
|
||||
genInboundLinks(remarkModel) {
|
||||
const inbound = this.toInbound();
|
||||
return inbound.genInboundLinks(this.remark,remarkModel);
|
||||
return inbound.genInboundLinks(this.remark, remarkModel);
|
||||
}
|
||||
}
|
||||
31
web/assets/js/model/reality_targets.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// List of popular services for VLESS Reality Target/SNI randomization
|
||||
const REALITY_TARGETS = [
|
||||
{ target: 'www.icloud.com:443', sni: 'www.icloud.com,icloud.com' },
|
||||
{ target: 'www.apple.com:443', sni: 'www.apple.com,apple.com' },
|
||||
{ target: 'www.tesla.com:443', sni: 'www.tesla.com,tesla.com' },
|
||||
{ target: 'www.sony.com:443', sni: 'www.sony.com,sony.com' },
|
||||
{ target: 'www.nvidia.com:443', sni: 'www.nvidia.com,nvidia.com' },
|
||||
{ target: 'www.amd.com:443', sni: 'www.amd.com,amd.com' },
|
||||
{ target: 'azure.microsoft.com:443', sni: 'azure.microsoft.com,www.azure.com' },
|
||||
{ target: 'aws.amazon.com:443', sni: 'aws.amazon.com,amazon.com' },
|
||||
{ target: 'www.bing.com:443', sni: 'www.bing.com,bing.com' },
|
||||
{ target: 'www.oracle.com:443', sni: 'www.oracle.com,oracle.com' },
|
||||
{ target: 'www.intel.com:443', sni: 'www.intel.com,intel.com' },
|
||||
{ target: 'www.microsoft.com:443', sni: 'www.microsoft.com,microsoft.com' },
|
||||
{ target: 'www.amazon.com:443', sni: 'www.amazon.com,amazon.com' }
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns a random Reality target configuration from the predefined list
|
||||
* @returns {Object} Object with target and sni properties
|
||||
*/
|
||||
function getRandomRealityTarget() {
|
||||
const randomIndex = Math.floor(Math.random() * REALITY_TARGETS.length);
|
||||
const selected = REALITY_TARGETS[randomIndex];
|
||||
// Return a copy to avoid reference issues
|
||||
return {
|
||||
target: selected.target,
|
||||
sni: selected.sni
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,41 +7,70 @@ class AllSetting {
|
||||
this.webCertFile = "";
|
||||
this.webKeyFile = "";
|
||||
this.webBasePath = "/";
|
||||
this.sessionMaxAge = "";
|
||||
this.pageSize = 50;
|
||||
this.expireDiff = "";
|
||||
this.trafficDiff = "";
|
||||
this.sessionMaxAge = 360;
|
||||
this.pageSize = 25;
|
||||
this.expireDiff = 0;
|
||||
this.trafficDiff = 0;
|
||||
this.remarkModel = "-ieo";
|
||||
this.datepicker = "gregorian";
|
||||
this.tgBotEnable = false;
|
||||
this.tgBotToken = "";
|
||||
this.tgBotProxy = "";
|
||||
this.tgBotAPIServer = "";
|
||||
this.tgBotChatId = "";
|
||||
this.tgRunTime = "@daily";
|
||||
this.tgBotBackup = false;
|
||||
this.tgBotLoginNotify = false;
|
||||
this.tgCpu = "";
|
||||
this.tgBotLoginNotify = true;
|
||||
this.tgCpu = 80;
|
||||
this.tgLang = "en-US";
|
||||
this.twoFactorEnable = false;
|
||||
this.twoFactorToken = "";
|
||||
this.xrayTemplateConfig = "";
|
||||
this.secretEnable = false;
|
||||
this.subEnable = false;
|
||||
this.subEnable = true;
|
||||
this.subJsonEnable = false;
|
||||
this.subTitle = "";
|
||||
this.subListen = "";
|
||||
this.subPort = "2096";
|
||||
this.subPort = 2096;
|
||||
this.subPath = "/sub/";
|
||||
this.subJsonPath = "/json/";
|
||||
this.subDomain = "";
|
||||
this.externalTrafficInformEnable = false;
|
||||
this.externalTrafficInformURI = "";
|
||||
this.subCertFile = "";
|
||||
this.subKeyFile = "";
|
||||
this.subUpdates = 0;
|
||||
this.subUpdates = 12;
|
||||
this.subEncrypt = true;
|
||||
this.subShowInfo = false;
|
||||
this.subShowInfo = true;
|
||||
this.subURI = "";
|
||||
this.subJsonURI = "";
|
||||
this.subJsonFragment = "";
|
||||
this.subJsonNoises = "";
|
||||
this.subJsonMux = "";
|
||||
this.subJsonRules = "";
|
||||
|
||||
this.timeLocation = "Asia/Tehran";
|
||||
this.timeLocation = "Local";
|
||||
|
||||
// LDAP settings
|
||||
this.ldapEnable = false;
|
||||
this.ldapHost = "";
|
||||
this.ldapPort = 389;
|
||||
this.ldapUseTLS = false;
|
||||
this.ldapBindDN = "";
|
||||
this.ldapPassword = "";
|
||||
this.ldapBaseDN = "";
|
||||
this.ldapUserFilter = "(objectClass=person)";
|
||||
this.ldapUserAttr = "mail";
|
||||
this.ldapVlessField = "vless_enabled";
|
||||
this.ldapSyncCron = "@every 1m";
|
||||
this.ldapFlagField = "";
|
||||
this.ldapTruthyValues = "true,1,yes,on";
|
||||
this.ldapInvertFlag = false;
|
||||
this.ldapInboundTags = "";
|
||||
this.ldapAutoCreate = false;
|
||||
this.ldapAutoDelete = false;
|
||||
this.ldapDefaultTotalGB = 0;
|
||||
this.ldapDefaultExpiryDays = 0;
|
||||
this.ldapDefaultLimitIP = 0;
|
||||
|
||||
if (data == null) {
|
||||
return
|
||||
|
||||
160
web/assets/js/subscription.js
Normal file
@@ -0,0 +1,160 @@
|
||||
(function () {
|
||||
// Vue app for Subscription page
|
||||
const el = document.getElementById('subscription-data');
|
||||
if (!el) return;
|
||||
const textarea = document.getElementById('subscription-links');
|
||||
const rawLinks = (textarea?.value || '').split('\n').filter(Boolean);
|
||||
|
||||
const data = {
|
||||
sId: el.getAttribute('data-sid') || '',
|
||||
subUrl: el.getAttribute('data-sub-url') || '',
|
||||
subJsonUrl: el.getAttribute('data-subjson-url') || '',
|
||||
download: el.getAttribute('data-download') || '',
|
||||
upload: el.getAttribute('data-upload') || '',
|
||||
used: el.getAttribute('data-used') || '',
|
||||
total: el.getAttribute('data-total') || '',
|
||||
remained: el.getAttribute('data-remained') || '',
|
||||
expireMs: (parseInt(el.getAttribute('data-expire') || '0', 10) || 0) * 1000,
|
||||
lastOnlineMs: (parseInt(el.getAttribute('data-lastonline') || '0', 10) || 0),
|
||||
downloadByte: parseInt(el.getAttribute('data-downloadbyte') || '0', 10) || 0,
|
||||
uploadByte: parseInt(el.getAttribute('data-uploadbyte') || '0', 10) || 0,
|
||||
totalByte: parseInt(el.getAttribute('data-totalbyte') || '0', 10) || 0,
|
||||
datepicker: el.getAttribute('data-datepicker') || 'gregorian',
|
||||
};
|
||||
|
||||
// Normalize lastOnline to milliseconds if it looks like seconds
|
||||
if (data.lastOnlineMs && data.lastOnlineMs < 10_000_000_000) {
|
||||
data.lastOnlineMs *= 1000;
|
||||
}
|
||||
|
||||
function renderLink(item) {
|
||||
return (
|
||||
Vue.h('a-list-item', {}, [
|
||||
Vue.h('a-space', { props: { size: 'small' } }, [
|
||||
Vue.h('a-button', { props: { size: 'small' }, on: { click: () => copy(item) } }, [Vue.h('a-icon', { props: { type: 'copy' } })]),
|
||||
Vue.h('span', { class: 'break-all' }, item)
|
||||
])
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function copy(text) {
|
||||
ClipboardManager.copyText(text).then(ok => {
|
||||
const messageType = ok ? 'success' : 'error';
|
||||
Vue.prototype.$message[messageType](ok ? 'Copied' : 'Copy failed');
|
||||
});
|
||||
}
|
||||
|
||||
function open(url) {
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
function drawQR(value) {
|
||||
try {
|
||||
new QRious({ element: document.getElementById('qrcode'), value, size: 220 });
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to extract a human label (email/ps) from different link types
|
||||
function linkName(link, idx) {
|
||||
try {
|
||||
if (link.startsWith('vmess://')) {
|
||||
const json = JSON.parse(atob(link.replace('vmess://', '')));
|
||||
if (json.ps) return json.ps;
|
||||
if (json.add && json.id) return json.add; // fallback host
|
||||
} else if (link.startsWith('vless://') || link.startsWith('trojan://')) {
|
||||
const hashIdx = link.indexOf('#');
|
||||
if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1));
|
||||
const qIdx = link.indexOf('?');
|
||||
if (qIdx !== -1) {
|
||||
const qs = new URL('http://x/?' + link.substring(qIdx + 1, hashIdx !== -1 ? hashIdx : undefined)).searchParams;
|
||||
if (qs.get('remark')) return qs.get('remark');
|
||||
if (qs.get('email')) return qs.get('email');
|
||||
}
|
||||
const at = link.indexOf('@');
|
||||
const protSep = link.indexOf('://');
|
||||
if (at !== -1 && protSep !== -1) return link.substring(protSep + 3, at);
|
||||
} else if (link.startsWith('ss://')) {
|
||||
const hashIdx = link.indexOf('#');
|
||||
if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1));
|
||||
}
|
||||
} catch (e) { /* ignore and fallback */ }
|
||||
return 'Link ' + (idx + 1);
|
||||
}
|
||||
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
themeSwitcher,
|
||||
app: data,
|
||||
links: rawLinks,
|
||||
lang: '',
|
||||
viewportWidth: (typeof window !== 'undefined' ? window.innerWidth : 1024),
|
||||
},
|
||||
async mounted() {
|
||||
this.lang = LanguageManager.getLanguage();
|
||||
const tpl = document.getElementById('subscription-data');
|
||||
const sj = tpl ? tpl.getAttribute('data-subjson-url') : '';
|
||||
if (sj) this.app.subJsonUrl = sj;
|
||||
drawQR(this.app.subUrl);
|
||||
try {
|
||||
const elJson = document.getElementById('qrcode-subjson');
|
||||
if (elJson && this.app.subJsonUrl) {
|
||||
new QRious({ element: elJson, value: this.app.subJsonUrl, size: 220 });
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
this._onResize = () => { this.viewportWidth = window.innerWidth; };
|
||||
window.addEventListener('resize', this._onResize);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this._onResize) window.removeEventListener('resize', this._onResize);
|
||||
},
|
||||
computed: {
|
||||
isMobile() {
|
||||
return this.viewportWidth < 576;
|
||||
},
|
||||
isUnlimited() {
|
||||
return !this.app.totalByte;
|
||||
},
|
||||
isActive() {
|
||||
const now = Date.now();
|
||||
const expiryOk = !this.app.expireMs || this.app.expireMs >= now;
|
||||
const trafficOk = !this.app.totalByte || (this.app.uploadByte + this.app.downloadByte) <= this.app.totalByte;
|
||||
return expiryOk && trafficOk;
|
||||
},
|
||||
shadowrocketUrl() {
|
||||
const rawUrl = this.app.subUrl + '?flag=shadowrocket';
|
||||
const base64Url = btoa(rawUrl);
|
||||
const remark = encodeURIComponent(this.app.sId || 'Subscription');
|
||||
return `shadowrocket://add/sub/${base64Url}?remark=${remark}`;
|
||||
},
|
||||
v2boxUrl() {
|
||||
return `v2box://install-sub?url=${encodeURIComponent(this.app.subUrl)}&name=${encodeURIComponent(this.app.sId)}`;
|
||||
},
|
||||
streisandUrl() {
|
||||
return `streisand://import/${encodeURIComponent(this.app.subUrl)}`;
|
||||
},
|
||||
v2raytunUrl() {
|
||||
return this.app.subUrl;
|
||||
},
|
||||
npvtunUrl() {
|
||||
return this.app.subUrl;
|
||||
},
|
||||
happUrl() {
|
||||
return `happ://add/${encodeURIComponent(this.app.subUrl)}`;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
renderLink,
|
||||
copy,
|
||||
open,
|
||||
linkName,
|
||||
i18nLabel(key) {
|
||||
return '{{ i18n "' + key + '" }}';
|
||||
},
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -1,194 +0,0 @@
|
||||
const ONE_KB = 1024;
|
||||
const ONE_MB = ONE_KB * 1024;
|
||||
const ONE_GB = ONE_MB * 1024;
|
||||
const ONE_TB = ONE_GB * 1024;
|
||||
const ONE_PB = ONE_TB * 1024;
|
||||
|
||||
function sizeFormat(size) {
|
||||
if (size < 0) {
|
||||
return "0 B";
|
||||
} else if (size < ONE_KB) {
|
||||
return size.toFixed(0) + " B";
|
||||
} else if (size < ONE_MB) {
|
||||
return (size / ONE_KB).toFixed(2) + " KB";
|
||||
} else if (size < ONE_GB) {
|
||||
return (size / ONE_MB).toFixed(2) + " MB";
|
||||
} else if (size < ONE_TB) {
|
||||
return (size / ONE_GB).toFixed(2) + " GB";
|
||||
} else if (size < ONE_PB) {
|
||||
return (size / ONE_TB).toFixed(2) + " TB";
|
||||
} else {
|
||||
return (size / ONE_PB).toFixed(2) + " PB";
|
||||
}
|
||||
}
|
||||
|
||||
function cpuSpeedFormat(speed) {
|
||||
if (speed > 1000) {
|
||||
const GHz = speed / 1000;
|
||||
return GHz.toFixed(2) + " GHz";
|
||||
} else {
|
||||
return speed.toFixed(2) + " MHz";
|
||||
}
|
||||
}
|
||||
|
||||
function cpuCoreFormat(cores) {
|
||||
if (cores === 1) {
|
||||
return "1 Core";
|
||||
} else {
|
||||
return cores + " Cores";
|
||||
}
|
||||
}
|
||||
|
||||
function base64(str) {
|
||||
return Base64.encode(str);
|
||||
}
|
||||
|
||||
function safeBase64(str) {
|
||||
return base64(str)
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/=/g, '')
|
||||
.replace(/\//g, '_');
|
||||
}
|
||||
|
||||
function formatSecond(second) {
|
||||
if (second < 60) {
|
||||
return second.toFixed(0) + 's';
|
||||
} else if (second < 3600) {
|
||||
return (second / 60).toFixed(0) + 'm';
|
||||
} else if (second < 3600 * 24) {
|
||||
return (second / 3600).toFixed(0) + 'h';
|
||||
} else {
|
||||
day = Math.floor(second / 3600 / 24);
|
||||
remain = ((second/3600) - (day*24)).toFixed(0);
|
||||
return day + 'd' + (remain > 0 ? ' ' + remain + 'h' : '');
|
||||
}
|
||||
}
|
||||
|
||||
function addZero(num) {
|
||||
if (num < 10) {
|
||||
return "0" + num;
|
||||
} else {
|
||||
return num;
|
||||
}
|
||||
}
|
||||
|
||||
function toFixed(num, n) {
|
||||
n = Math.pow(10, n);
|
||||
return Math.floor(num * n) / n;
|
||||
}
|
||||
|
||||
function debounce(fn, delay) {
|
||||
var timeoutID = null;
|
||||
return function () {
|
||||
clearTimeout(timeoutID);
|
||||
var args = arguments;
|
||||
var that = this;
|
||||
timeoutID = setTimeout(function () {
|
||||
fn.apply(that, args);
|
||||
}, delay);
|
||||
};
|
||||
}
|
||||
|
||||
function getCookie(cname) {
|
||||
let name = cname + '=';
|
||||
let ca = document.cookie.split(';');
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) == ' ') {
|
||||
c = c.substring(1);
|
||||
}
|
||||
if (c.indexOf(name) == 0) {
|
||||
// decode cookie value only
|
||||
return decodeURIComponent(c.substring(name.length, c.length));
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
function setCookie(cname, cvalue, exdays) {
|
||||
const d = new Date();
|
||||
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);
|
||||
let expires = 'expires=' + d.toUTCString();
|
||||
// encode cookie value
|
||||
document.cookie = cname + '=' + encodeURIComponent(cvalue) + ';' + expires + ';path=/';
|
||||
}
|
||||
|
||||
function usageColor(data, threshold, total) {
|
||||
switch (true) {
|
||||
case data === null:
|
||||
return "purple";
|
||||
case total < 0:
|
||||
return "green";
|
||||
case total == 0:
|
||||
return "purple";
|
||||
case data < total - threshold:
|
||||
return "green";
|
||||
case data < total:
|
||||
return "orange";
|
||||
default:
|
||||
return "red";
|
||||
}
|
||||
}
|
||||
|
||||
function clientUsageColor(clientStats, trafficDiff) {
|
||||
switch (true) {
|
||||
case !clientStats || clientStats.total == 0:
|
||||
return "#7a316f"; // purple
|
||||
case clientStats.up + clientStats.down < clientStats.total - trafficDiff:
|
||||
return "#008771"; // Green
|
||||
case clientStats.up + clientStats.down < clientStats.total:
|
||||
return "#f37b24"; // Orange
|
||||
default:
|
||||
return "#cf3c3c"; // Red
|
||||
}
|
||||
}
|
||||
|
||||
function userExpiryColor(threshold, client, isDark = false) {
|
||||
if (!client.enable) {
|
||||
return isDark ? '#2c3950' : '#bcbcbc';
|
||||
}
|
||||
now = new Date().getTime(),
|
||||
expiry = client.expiryTime;
|
||||
switch (true) {
|
||||
case expiry === null:
|
||||
return "#7a316f"; // purple
|
||||
case expiry < 0:
|
||||
return "#008771"; // Green
|
||||
case expiry == 0:
|
||||
return "#7a316f"; // purple
|
||||
case now < expiry - threshold:
|
||||
return "#008771"; // Green
|
||||
case now < expiry:
|
||||
return "#f37b24"; // Orange
|
||||
default:
|
||||
return "#cf3c3c"; // Red
|
||||
}
|
||||
}
|
||||
|
||||
function doAllItemsExist(array1, array2) {
|
||||
for (let i = 0; i < array1.length; i++) {
|
||||
if (!array2.includes(array1[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildURL({ host, port, isTLS, base, path }) {
|
||||
if (!host || host.length === 0) host = window.location.hostname;
|
||||
if (!port || port.length === 0) port = window.location.port;
|
||||
|
||||
if (isTLS === undefined) isTLS = window.location.protocol === "https:";
|
||||
|
||||
const protocol = isTLS ? "https:" : "http:";
|
||||
|
||||
port = String(port);
|
||||
if (port === "" || (isTLS && port === "443") || (!isTLS && port === "80")) {
|
||||
port = "";
|
||||
} else {
|
||||
port = `:${port}`;
|
||||
}
|
||||
|
||||
return `${protocol}//${host}${port}${base}${path}`;
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
const oneMinute = 1000 * 60; // MilliseConds in a Minute
|
||||
const oneHour = oneMinute * 60; // The milliseconds of one hour
|
||||
const oneDay = oneHour * 24; // The Number of MilliseConds A Day
|
||||
const oneWeek = oneDay * 7; // The milliseconds per week
|
||||
const oneMonth = oneDay * 30; // The milliseconds of a month
|
||||
|
||||
/**
|
||||
* Decrease according to the number of days
|
||||
*
|
||||
* @param days to reduce the number of days to be reduced
|
||||
*/
|
||||
Date.prototype.minusDays = function (days) {
|
||||
return this.minusMillis(oneDay * days);
|
||||
};
|
||||
|
||||
/**
|
||||
* Increase according to the number of days
|
||||
*
|
||||
* @param days The number of days to be increased
|
||||
*/
|
||||
Date.prototype.plusDays = function (days) {
|
||||
return this.plusMillis(oneDay * days);
|
||||
};
|
||||
|
||||
/**
|
||||
* A few
|
||||
*
|
||||
* @param hours to be reduced
|
||||
*/
|
||||
Date.prototype.minusHours = function (hours) {
|
||||
return this.minusMillis(oneHour * hours);
|
||||
};
|
||||
|
||||
/**
|
||||
* Increase hourly
|
||||
*
|
||||
* @param hours to increase the number of hours
|
||||
*/
|
||||
Date.prototype.plusHours = function (hours) {
|
||||
return this.plusMillis(oneHour * hours);
|
||||
};
|
||||
|
||||
/**
|
||||
* Make reduction in minutes
|
||||
*
|
||||
* @param minutes to reduce the number of minutes
|
||||
*/
|
||||
Date.prototype.minusMinutes = function (minutes) {
|
||||
return this.minusMillis(oneMinute * minutes);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add in minutes
|
||||
*
|
||||
* @param minutes to increase the number of minutes
|
||||
*/
|
||||
Date.prototype.plusMinutes = function (minutes) {
|
||||
return this.plusMillis(oneMinute * minutes);
|
||||
};
|
||||
|
||||
/**
|
||||
* Decrease in milliseconds
|
||||
*
|
||||
* @param millis to reduce the milliseconds
|
||||
*/
|
||||
Date.prototype.minusMillis = function(millis) {
|
||||
let time = this.getTime() - millis;
|
||||
let newDate = new Date();
|
||||
newDate.setTime(time);
|
||||
return newDate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add in milliseconds to increase
|
||||
*
|
||||
* @param millis to increase the milliseconds to increase
|
||||
*/
|
||||
Date.prototype.plusMillis = function(millis) {
|
||||
let time = this.getTime() + millis;
|
||||
let newDate = new Date();
|
||||
newDate.setTime(time);
|
||||
return newDate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Setting time is 00: 00: 00.000 on the day
|
||||
*/
|
||||
Date.prototype.setMinTime = function () {
|
||||
this.setHours(0);
|
||||
this.setMinutes(0);
|
||||
this.setSeconds(0);
|
||||
this.setMilliseconds(0);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Setting time is 23: 59: 59.999 on the same day
|
||||
*/
|
||||
Date.prototype.setMaxTime = function () {
|
||||
this.setHours(23);
|
||||
this.setMinutes(59);
|
||||
this.setSeconds(59);
|
||||
this.setMilliseconds(999);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatting date
|
||||
*/
|
||||
Date.prototype.formatDate = function () {
|
||||
return this.getFullYear() + "-" + addZero(this.getMonth() + 1) + "-" + addZero(this.getDate());
|
||||
};
|
||||
|
||||
/**
|
||||
* Format time
|
||||
*/
|
||||
Date.prototype.formatTime = function () {
|
||||
return addZero(this.getHours()) + ":" + addZero(this.getMinutes()) + ":" + addZero(this.getSeconds());
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatting date plus time
|
||||
*
|
||||
* @param split Date and time separation symbols, default is a space
|
||||
*/
|
||||
Date.prototype.formatDateTime = function (split = ' ') {
|
||||
return this.formatDate() + split + this.formatTime();
|
||||
};
|
||||
|
||||
class DateUtil {
|
||||
// String to date object
|
||||
static parseDate(str) {
|
||||
return new Date(str.replace(/-/g, '/'));
|
||||
}
|
||||
|
||||
static formatMillis(millis) {
|
||||
return moment(millis).format('YYYY-M-D H:m:s');
|
||||
}
|
||||
|
||||
static firstDayOfMonth() {
|
||||
const date = new Date();
|
||||
date.setDate(1);
|
||||
date.setMinTime();
|
||||
return date;
|
||||
}
|
||||
|
||||
static convertToJalalian(date) {
|
||||
return date && moment.isMoment(date) ? date.format('jYYYY/jMM/jDD HH:mm:ss') : null;
|
||||
}
|
||||
|
||||
}
|
||||
919
web/assets/js/util/index.js
Normal file
@@ -0,0 +1,919 @@
|
||||
class Msg {
|
||||
constructor(success = false, msg = "", obj = null) {
|
||||
this.success = success;
|
||||
this.msg = msg;
|
||||
this.obj = obj;
|
||||
}
|
||||
}
|
||||
|
||||
class HttpUtil {
|
||||
static _handleMsg(msg) {
|
||||
if (!(msg instanceof Msg) || msg.msg === "") {
|
||||
return;
|
||||
}
|
||||
const messageType = msg.success ? 'success' : 'error';
|
||||
Vue.prototype.$message[messageType](msg.msg);
|
||||
}
|
||||
|
||||
static _respToMsg(resp) {
|
||||
if (!resp || !resp.data) {
|
||||
return new Msg(false, 'No response data');
|
||||
}
|
||||
const { data } = resp;
|
||||
if (data == null) {
|
||||
return new Msg(true);
|
||||
}
|
||||
if (typeof data === 'object' && 'success' in data) {
|
||||
return new Msg(data.success, data.msg, data.obj);
|
||||
}
|
||||
return typeof data === 'object' ? data : new Msg(false, 'unknown data:', data);
|
||||
}
|
||||
|
||||
static async get(url, params, options = {}) {
|
||||
try {
|
||||
const resp = await axios.get(url, { params, ...options });
|
||||
const msg = this._respToMsg(resp);
|
||||
this._handleMsg(msg);
|
||||
return msg;
|
||||
} catch (error) {
|
||||
console.error('GET request failed:', error);
|
||||
const errorMsg = new Msg(false, error.response?.data?.message || error.message || 'Request failed');
|
||||
this._handleMsg(errorMsg);
|
||||
return errorMsg;
|
||||
}
|
||||
}
|
||||
|
||||
static async post(url, data, options = {}) {
|
||||
try {
|
||||
const resp = await axios.post(url, data, options);
|
||||
const msg = this._respToMsg(resp);
|
||||
this._handleMsg(msg);
|
||||
return msg;
|
||||
} catch (error) {
|
||||
console.error('POST request failed:', error);
|
||||
const errorMsg = new Msg(false, error.response?.data?.message || error.message || 'Request failed');
|
||||
this._handleMsg(errorMsg);
|
||||
return errorMsg;
|
||||
}
|
||||
}
|
||||
|
||||
static async postWithModal(url, data, modal) {
|
||||
if (modal) {
|
||||
modal.loading(true);
|
||||
}
|
||||
const msg = await this.post(url, data);
|
||||
if (modal) {
|
||||
modal.loading(false);
|
||||
if (msg instanceof Msg && msg.success) {
|
||||
modal.close();
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
||||
class PromiseUtil {
|
||||
static async sleep(timeout) {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, timeout)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class RandomUtil {
|
||||
static getSeq({ type = "default", hasNumbers = true, hasLowercase = true, hasUppercase = true } = {}) {
|
||||
let seq = '';
|
||||
|
||||
switch (type) {
|
||||
case "hex":
|
||||
seq += "0123456789abcdef";
|
||||
break;
|
||||
default:
|
||||
if (hasNumbers) seq += "0123456789";
|
||||
if (hasLowercase) seq += "abcdefghijklmnopqrstuvwxyz";
|
||||
if (hasUppercase) seq += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
break;
|
||||
}
|
||||
|
||||
return seq;
|
||||
}
|
||||
|
||||
static randomInteger(min, max) {
|
||||
const range = max - min + 1;
|
||||
const randomBuffer = new Uint32Array(1);
|
||||
window.crypto.getRandomValues(randomBuffer);
|
||||
return Math.floor((randomBuffer[0] / (0xFFFFFFFF + 1)) * range) + min;
|
||||
}
|
||||
|
||||
static randomSeq(count, options = {}) {
|
||||
const seq = this.getSeq(options);
|
||||
const seqLength = seq.length;
|
||||
const randomValues = new Uint32Array(count);
|
||||
window.crypto.getRandomValues(randomValues);
|
||||
return Array.from(randomValues, v => seq[v % seqLength]).join('');
|
||||
}
|
||||
|
||||
static randomShortIds() {
|
||||
const lengths = [2, 4, 6, 8, 10, 12, 14, 16].sort(() => Math.random() - 0.5);
|
||||
|
||||
return lengths.map(len => this.randomSeq(len, { type: "hex" })).join(',');
|
||||
}
|
||||
|
||||
static randomLowerAndNum(len) {
|
||||
return this.randomSeq(len, { hasUppercase: false });
|
||||
}
|
||||
|
||||
static randomUUID() {
|
||||
if (window.location.protocol === "https:") {
|
||||
return window.crypto.randomUUID();
|
||||
} else {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
|
||||
.replace(/[xy]/g, function (c) {
|
||||
const randomValues = new Uint8Array(1);
|
||||
window.crypto.getRandomValues(randomValues);
|
||||
let randomValue = randomValues[0] % 16;
|
||||
let calculatedValue = (c === 'x') ? randomValue : (randomValue & 0x3 | 0x8);
|
||||
return calculatedValue.toString(16);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static randomShadowsocksPassword(method = SSMethods.BLAKE3_AES_256_GCM) {
|
||||
let length = 32;
|
||||
|
||||
if ([SSMethods.BLAKE3_AES_128_GCM].includes(method)) {
|
||||
length = 16;
|
||||
}
|
||||
|
||||
const array = new Uint8Array(length);
|
||||
|
||||
window.crypto.getRandomValues(array);
|
||||
|
||||
return Base64.alternativeEncode(String.fromCharCode(...array));
|
||||
}
|
||||
|
||||
static randomBase32String(length = 16) {
|
||||
const array = new Uint8Array(length);
|
||||
|
||||
window.crypto.getRandomValues(array);
|
||||
|
||||
const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||
let result = '';
|
||||
let bits = 0;
|
||||
let buffer = 0;
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
buffer = (buffer << 8) | array[i];
|
||||
bits += 8;
|
||||
|
||||
while (bits >= 5) {
|
||||
bits -= 5;
|
||||
result += base32Chars[(buffer >>> bits) & 0x1F];
|
||||
}
|
||||
}
|
||||
|
||||
if (bits > 0) {
|
||||
result += base32Chars[(buffer << (5 - bits)) & 0x1F];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class ObjectUtil {
|
||||
static getPropIgnoreCase(obj, prop) {
|
||||
for (const name in obj) {
|
||||
if (!obj.hasOwnProperty(name)) {
|
||||
continue;
|
||||
}
|
||||
if (name.toLowerCase() === prop.toLowerCase()) {
|
||||
return obj[name];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static deepSearch(obj, key) {
|
||||
if (obj instanceof Array) {
|
||||
for (let i = 0; i < obj.length; ++i) {
|
||||
if (this.deepSearch(obj[i], key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (obj instanceof Object) {
|
||||
for (let name in obj) {
|
||||
if (!obj.hasOwnProperty(name)) {
|
||||
continue;
|
||||
}
|
||||
if (this.deepSearch(obj[name], key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return this.isEmpty(obj) ? false : obj.toString().toLowerCase().indexOf(key.toLowerCase()) >= 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static isEmpty(obj) {
|
||||
return obj === null || obj === undefined || obj === '';
|
||||
}
|
||||
|
||||
static isArrEmpty(arr) {
|
||||
return !this.isEmpty(arr) && arr.length === 0;
|
||||
}
|
||||
|
||||
static copyArr(dest, src) {
|
||||
dest.splice(0);
|
||||
for (const item of src) {
|
||||
dest.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
static clone(obj) {
|
||||
let newObj;
|
||||
if (obj instanceof Array) {
|
||||
newObj = [];
|
||||
this.copyArr(newObj, obj);
|
||||
} else if (obj instanceof Object) {
|
||||
newObj = {};
|
||||
for (const key of Object.keys(obj)) {
|
||||
newObj[key] = obj[key];
|
||||
}
|
||||
} else {
|
||||
newObj = obj;
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
|
||||
static deepClone(obj) {
|
||||
let newObj;
|
||||
if (obj instanceof Array) {
|
||||
newObj = [];
|
||||
for (const item of obj) {
|
||||
newObj.push(this.deepClone(item));
|
||||
}
|
||||
} else if (obj instanceof Object) {
|
||||
newObj = {};
|
||||
for (const key of Object.keys(obj)) {
|
||||
newObj[key] = this.deepClone(obj[key]);
|
||||
}
|
||||
} else {
|
||||
newObj = obj;
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
|
||||
static cloneProps(dest, src, ...ignoreProps) {
|
||||
if (dest == null || src == null) {
|
||||
return;
|
||||
}
|
||||
const ignoreEmpty = this.isArrEmpty(ignoreProps);
|
||||
for (const key of Object.keys(src)) {
|
||||
if (!src.hasOwnProperty(key)) {
|
||||
continue;
|
||||
} else if (!dest.hasOwnProperty(key)) {
|
||||
continue;
|
||||
} else if (src[key] === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (ignoreEmpty) {
|
||||
dest[key] = src[key];
|
||||
} else {
|
||||
let ignore = false;
|
||||
for (let i = 0; i < ignoreProps.length; ++i) {
|
||||
if (key === ignoreProps[i]) {
|
||||
ignore = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!ignore) {
|
||||
dest[key] = src[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static delProps(obj, ...props) {
|
||||
for (const prop of props) {
|
||||
if (prop in obj) {
|
||||
delete obj[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static execute(func, ...args) {
|
||||
if (!this.isEmpty(func) && typeof func === 'function') {
|
||||
func(...args);
|
||||
}
|
||||
}
|
||||
|
||||
static orDefault(obj, defaultValue) {
|
||||
if (obj == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
static equals(a, b) {
|
||||
// shallow, symmetric comparison so newly added fields also affect equality
|
||||
const aKeys = Object.keys(a);
|
||||
const bKeys = Object.keys(b);
|
||||
if (aKeys.length !== bKeys.length) return false;
|
||||
for (const key of aKeys) {
|
||||
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
|
||||
if (a[key] !== b[key]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class Wireguard {
|
||||
static gf(init) {
|
||||
var r = new Float64Array(16);
|
||||
if (init) {
|
||||
for (var i = 0; i < init.length; ++i)
|
||||
r[i] = init[i];
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
static pack(o, n) {
|
||||
var b, m = this.gf(), t = this.gf();
|
||||
for (var i = 0; i < 16; ++i)
|
||||
t[i] = n[i];
|
||||
this.carry(t);
|
||||
this.carry(t);
|
||||
this.carry(t);
|
||||
for (var j = 0; j < 2; ++j) {
|
||||
m[0] = t[0] - 0xffed;
|
||||
for (var i = 1; i < 15; ++i) {
|
||||
m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1);
|
||||
m[i - 1] &= 0xffff;
|
||||
}
|
||||
m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);
|
||||
b = (m[15] >> 16) & 1;
|
||||
m[14] &= 0xffff;
|
||||
this.cswap(t, m, 1 - b);
|
||||
}
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
o[2 * i] = t[i] & 0xff;
|
||||
o[2 * i + 1] = t[i] >> 8;
|
||||
}
|
||||
}
|
||||
|
||||
static carry(o) {
|
||||
var c;
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
o[(i + 1) % 16] += (i < 15 ? 1 : 38) * Math.floor(o[i] / 65536);
|
||||
o[i] &= 0xffff;
|
||||
}
|
||||
}
|
||||
|
||||
static cswap(p, q, b) {
|
||||
var t, c = ~(b - 1);
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
t = c & (p[i] ^ q[i]);
|
||||
p[i] ^= t;
|
||||
q[i] ^= t;
|
||||
}
|
||||
}
|
||||
|
||||
static add(o, a, b) {
|
||||
for (var i = 0; i < 16; ++i)
|
||||
o[i] = (a[i] + b[i]) | 0;
|
||||
}
|
||||
|
||||
static subtract(o, a, b) {
|
||||
for (var i = 0; i < 16; ++i)
|
||||
o[i] = (a[i] - b[i]) | 0;
|
||||
}
|
||||
|
||||
static multmod(o, a, b) {
|
||||
var t = new Float64Array(31);
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
for (var j = 0; j < 16; ++j)
|
||||
t[i + j] += a[i] * b[j];
|
||||
}
|
||||
for (var i = 0; i < 15; ++i)
|
||||
t[i] += 38 * t[i + 16];
|
||||
for (var i = 0; i < 16; ++i)
|
||||
o[i] = t[i];
|
||||
this.carry(o);
|
||||
this.carry(o);
|
||||
}
|
||||
|
||||
static invert(o, i) {
|
||||
var c = this.gf();
|
||||
for (var a = 0; a < 16; ++a)
|
||||
c[a] = i[a];
|
||||
for (var a = 253; a >= 0; --a) {
|
||||
this.multmod(c, c, c);
|
||||
if (a !== 2 && a !== 4)
|
||||
this.multmod(c, c, i);
|
||||
}
|
||||
for (var a = 0; a < 16; ++a)
|
||||
o[a] = c[a];
|
||||
}
|
||||
|
||||
static clamp(z) {
|
||||
z[31] = (z[31] & 127) | 64;
|
||||
z[0] &= 248;
|
||||
}
|
||||
|
||||
static generatePublicKey(privateKey) {
|
||||
var r, z = new Uint8Array(32);
|
||||
var a = this.gf([1]),
|
||||
b = this.gf([9]),
|
||||
c = this.gf(),
|
||||
d = this.gf([1]),
|
||||
e = this.gf(),
|
||||
f = this.gf(),
|
||||
_121665 = this.gf([0xdb41, 1]),
|
||||
_9 = this.gf([9]);
|
||||
for (var i = 0; i < 32; ++i)
|
||||
z[i] = privateKey[i];
|
||||
this.clamp(z);
|
||||
for (var i = 254; i >= 0; --i) {
|
||||
r = (z[i >>> 3] >>> (i & 7)) & 1;
|
||||
this.cswap(a, b, r);
|
||||
this.cswap(c, d, r);
|
||||
this.add(e, a, c);
|
||||
this.subtract(a, a, c);
|
||||
this.add(c, b, d);
|
||||
this.subtract(b, b, d);
|
||||
this.multmod(d, e, e);
|
||||
this.multmod(f, a, a);
|
||||
this.multmod(a, c, a);
|
||||
this.multmod(c, b, e);
|
||||
this.add(e, a, c);
|
||||
this.subtract(a, a, c);
|
||||
this.multmod(b, a, a);
|
||||
this.subtract(c, d, f);
|
||||
this.multmod(a, c, _121665);
|
||||
this.add(a, a, d);
|
||||
this.multmod(c, c, a);
|
||||
this.multmod(a, d, f);
|
||||
this.multmod(d, b, _9);
|
||||
this.multmod(b, e, e);
|
||||
this.cswap(a, b, r);
|
||||
this.cswap(c, d, r);
|
||||
}
|
||||
this.invert(c, c);
|
||||
this.multmod(a, a, c);
|
||||
this.pack(z, a);
|
||||
return z;
|
||||
}
|
||||
|
||||
static generatePresharedKey() {
|
||||
var privateKey = new Uint8Array(32);
|
||||
window.crypto.getRandomValues(privateKey);
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
static generatePrivateKey() {
|
||||
var privateKey = this.generatePresharedKey();
|
||||
this.clamp(privateKey);
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
static encodeBase64(dest, src) {
|
||||
var input = Uint8Array.from([(src[0] >> 2) & 63, ((src[0] << 4) | (src[1] >> 4)) & 63, ((src[1] << 2) | (src[2] >> 6)) & 63, src[2] & 63]);
|
||||
for (var i = 0; i < 4; ++i)
|
||||
dest[i] = input[i] + 65 +
|
||||
(((25 - input[i]) >> 8) & 6) -
|
||||
(((51 - input[i]) >> 8) & 75) -
|
||||
(((61 - input[i]) >> 8) & 15) +
|
||||
(((62 - input[i]) >> 8) & 3);
|
||||
}
|
||||
|
||||
static keyToBase64(key) {
|
||||
var i, base64 = new Uint8Array(44);
|
||||
for (i = 0; i < 32 / 3; ++i)
|
||||
this.encodeBase64(base64.subarray(i * 4), key.subarray(i * 3));
|
||||
this.encodeBase64(base64.subarray(i * 4), Uint8Array.from([key[i * 3 + 0], key[i * 3 + 1], 0]));
|
||||
base64[43] = 61;
|
||||
return String.fromCharCode.apply(null, base64);
|
||||
}
|
||||
|
||||
static keyFromBase64(encoded) {
|
||||
const binaryStr = atob(encoded);
|
||||
const bytes = new Uint8Array(binaryStr.length);
|
||||
for (let i = 0; i < binaryStr.length; i++) {
|
||||
bytes[i] = binaryStr.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
static generateKeypair(secretKey = '') {
|
||||
var privateKey = secretKey.length > 0 ? this.keyFromBase64(secretKey) : this.generatePrivateKey();
|
||||
var publicKey = this.generatePublicKey(privateKey);
|
||||
return {
|
||||
publicKey: this.keyToBase64(publicKey),
|
||||
privateKey: secretKey.length > 0 ? secretKey : this.keyToBase64(privateKey)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ClipboardManager {
|
||||
static copyText(content = "") {
|
||||
// !! here old way of copying is used because not everyone can afford https connection
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const textarea = window.document.createElement('textarea');
|
||||
|
||||
textarea.style.fontSize = '12pt';
|
||||
textarea.style.border = '0';
|
||||
textarea.style.padding = '0';
|
||||
textarea.style.margin = '0';
|
||||
textarea.style.position = 'absolute';
|
||||
textarea.style.left = '-9999px';
|
||||
textarea.style.top = `${window.pageYOffset || document.documentElement.scrollTop}px`;
|
||||
textarea.setAttribute('readonly', '');
|
||||
textarea.value = content;
|
||||
|
||||
window.document.body.appendChild(textarea);
|
||||
|
||||
textarea.select();
|
||||
window.document.execCommand("copy");
|
||||
|
||||
window.document.body.removeChild(textarea);
|
||||
|
||||
resolve(true)
|
||||
} catch {
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class Base64 {
|
||||
static encode(content = "", safe = false) {
|
||||
if (safe) {
|
||||
return Base64.encode(content)
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/=/g, '')
|
||||
.replace(/\//g, '_')
|
||||
}
|
||||
|
||||
return window.btoa(
|
||||
String.fromCharCode(...new TextEncoder().encode(content))
|
||||
)
|
||||
}
|
||||
|
||||
static alternativeEncode(content) {
|
||||
return window.btoa(
|
||||
content
|
||||
)
|
||||
}
|
||||
|
||||
static decode(content = "") {
|
||||
return new TextDecoder()
|
||||
.decode(
|
||||
Uint8Array.from(window.atob(content), c => c.charCodeAt(0))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class SizeFormatter {
|
||||
static ONE_KB = 1024;
|
||||
static ONE_MB = this.ONE_KB * 1024;
|
||||
static ONE_GB = this.ONE_MB * 1024;
|
||||
static ONE_TB = this.ONE_GB * 1024;
|
||||
static ONE_PB = this.ONE_TB * 1024;
|
||||
|
||||
static sizeFormat(size) {
|
||||
if (size <= 0) return "0 B";
|
||||
if (size < this.ONE_KB) return size.toFixed(0) + " B";
|
||||
if (size < this.ONE_MB) return (size / this.ONE_KB).toFixed(2) + " KB";
|
||||
if (size < this.ONE_GB) return (size / this.ONE_MB).toFixed(2) + " MB";
|
||||
if (size < this.ONE_TB) return (size / this.ONE_GB).toFixed(2) + " GB";
|
||||
if (size < this.ONE_PB) return (size / this.ONE_TB).toFixed(2) + " TB";
|
||||
return (size / this.ONE_PB).toFixed(2) + " PB";
|
||||
}
|
||||
}
|
||||
|
||||
class CPUFormatter {
|
||||
static cpuSpeedFormat(speed) {
|
||||
return speed > 1000 ? (speed / 1000).toFixed(2) + " GHz" : speed.toFixed(2) + " MHz";
|
||||
}
|
||||
|
||||
static cpuCoreFormat(cores) {
|
||||
return cores === 1 ? "1 Core" : cores + " Cores";
|
||||
}
|
||||
}
|
||||
|
||||
class TimeFormatter {
|
||||
static formatSecond(second) {
|
||||
if (second < 60) return second.toFixed(0) + 's';
|
||||
if (second < 3600) return (second / 60).toFixed(0) + 'm';
|
||||
if (second < 3600 * 24) return (second / 3600).toFixed(0) + 'h';
|
||||
let day = Math.floor(second / 3600 / 24);
|
||||
let remain = ((second / 3600) - (day * 24)).toFixed(0);
|
||||
return day + 'd' + (remain > 0 ? ' ' + remain + 'h' : '');
|
||||
}
|
||||
}
|
||||
|
||||
class NumberFormatter {
|
||||
static addZero(num) {
|
||||
return num < 10 ? "0" + num : num;
|
||||
}
|
||||
|
||||
static toFixed(num, n) {
|
||||
n = Math.pow(10, n);
|
||||
return Math.floor(num * n) / n;
|
||||
}
|
||||
}
|
||||
|
||||
class Utils {
|
||||
static debounce(fn, delay) {
|
||||
let timeoutID = null;
|
||||
return function () {
|
||||
clearTimeout(timeoutID);
|
||||
let args = arguments;
|
||||
let that = this;
|
||||
timeoutID = setTimeout(() => fn.apply(that, args), delay);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class CookieManager {
|
||||
static getCookie(cname) {
|
||||
let name = cname + '=';
|
||||
let ca = document.cookie.split(';');
|
||||
for (let c of ca) {
|
||||
c = c.trim();
|
||||
if (c.indexOf(name) === 0) {
|
||||
return decodeURIComponent(c.substring(name.length, c.length));
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
static setCookie(cname, cvalue, exdays) {
|
||||
const d = new Date();
|
||||
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);
|
||||
let expires = 'expires=' + d.toUTCString();
|
||||
document.cookie = cname + '=' + encodeURIComponent(cvalue) + ';' + expires + ';path=/';
|
||||
}
|
||||
}
|
||||
|
||||
class ColorUtils {
|
||||
static usageColor(data, threshold, total) {
|
||||
switch (true) {
|
||||
case data === null: return "purple";
|
||||
case total < 0: return "green";
|
||||
case total == 0: return "purple";
|
||||
case data < total - threshold: return "green";
|
||||
case data < total: return "orange";
|
||||
default: return "red";
|
||||
}
|
||||
}
|
||||
|
||||
static clientUsageColor(clientStats, trafficDiff) {
|
||||
switch (true) {
|
||||
case !clientStats || clientStats.total == 0: return "#7a316f";
|
||||
case clientStats.up + clientStats.down < clientStats.total - trafficDiff: return "#008771";
|
||||
case clientStats.up + clientStats.down < clientStats.total: return "#f37b24";
|
||||
default: return "#cf3c3c";
|
||||
}
|
||||
}
|
||||
|
||||
static userExpiryColor(threshold, client, isDark = false) {
|
||||
if (!client.enable) return isDark ? '#2c3950' : '#bcbcbc';
|
||||
let now = new Date().getTime(), expiry = client.expiryTime;
|
||||
switch (true) {
|
||||
case expiry === null: return "#7a316f";
|
||||
case expiry < 0: return "#008771";
|
||||
case expiry == 0: return "#7a316f";
|
||||
case now < expiry - threshold: return "#008771";
|
||||
case now < expiry: return "#f37b24";
|
||||
default: return "#cf3c3c";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ArrayUtils {
|
||||
static doAllItemsExist(array1, array2) {
|
||||
return array1.every(item => array2.includes(item));
|
||||
}
|
||||
}
|
||||
|
||||
class URLBuilder {
|
||||
static buildURL({ host, port, isTLS, base, path }) {
|
||||
if (!host || host.length === 0) host = window.location.hostname;
|
||||
if (!port || port.length === 0) port = window.location.port;
|
||||
if (isTLS === undefined) isTLS = window.location.protocol === "https:";
|
||||
|
||||
const protocol = isTLS ? "https:" : "http:";
|
||||
port = String(port);
|
||||
if (port === "" || (isTLS && port === "443") || (!isTLS && port === "80")) {
|
||||
port = "";
|
||||
} else {
|
||||
port = `:${port}`;
|
||||
}
|
||||
|
||||
return `${protocol}//${host}${port}${base}${path}`;
|
||||
}
|
||||
}
|
||||
|
||||
class LanguageManager {
|
||||
static supportedLanguages = [
|
||||
{
|
||||
name: "العربية",
|
||||
value: "ar-EG",
|
||||
icon: "🇪🇬",
|
||||
},
|
||||
{
|
||||
name: "English",
|
||||
value: "en-US",
|
||||
icon: "🇺🇸",
|
||||
},
|
||||
{
|
||||
name: "فارسی",
|
||||
value: "fa-IR",
|
||||
icon: "🇮🇷",
|
||||
},
|
||||
{
|
||||
name: "简体中文",
|
||||
value: "zh-CN",
|
||||
icon: "🇨🇳",
|
||||
},
|
||||
{
|
||||
name: "繁體中文",
|
||||
value: "zh-TW",
|
||||
icon: "🇹🇼",
|
||||
},
|
||||
{
|
||||
name: "日本語",
|
||||
value: "ja-JP",
|
||||
icon: "🇯🇵",
|
||||
},
|
||||
{
|
||||
name: "Русский",
|
||||
value: "ru-RU",
|
||||
icon: "🇷🇺",
|
||||
},
|
||||
{
|
||||
name: "Tiếng Việt",
|
||||
value: "vi-VN",
|
||||
icon: "🇻🇳",
|
||||
},
|
||||
{
|
||||
name: "Español",
|
||||
value: "es-ES",
|
||||
icon: "🇪🇸",
|
||||
},
|
||||
{
|
||||
name: "Indonesian",
|
||||
value: "id-ID",
|
||||
icon: "🇮🇩",
|
||||
},
|
||||
{
|
||||
name: "Український",
|
||||
value: "uk-UA",
|
||||
icon: "🇺🇦",
|
||||
},
|
||||
{
|
||||
name: "Türkçe",
|
||||
value: "tr-TR",
|
||||
icon: "🇹🇷",
|
||||
},
|
||||
{
|
||||
name: "Português",
|
||||
value: "pt-BR",
|
||||
icon: "🇧🇷",
|
||||
}
|
||||
]
|
||||
|
||||
static getLanguage() {
|
||||
let lang = CookieManager.getCookie("lang");
|
||||
|
||||
if (!lang) {
|
||||
if (window.navigator) {
|
||||
lang = window.navigator.language || window.navigator.userLanguage;
|
||||
|
||||
const simularLangs = [
|
||||
["ar", this.supportedLanguages[0].value],
|
||||
["fa", this.supportedLanguages[2].value],
|
||||
["ja", this.supportedLanguages[5].value],
|
||||
["ru", this.supportedLanguages[6].value],
|
||||
["vi", this.supportedLanguages[7].value],
|
||||
["es", this.supportedLanguages[8].value],
|
||||
["id", this.supportedLanguages[9].value],
|
||||
["uk", this.supportedLanguages[10].value],
|
||||
["tr", this.supportedLanguages[11].value],
|
||||
["pt", this.supportedLanguages[12].value],
|
||||
]
|
||||
|
||||
simularLangs.forEach((pair) => {
|
||||
if (lang === pair[0]) {
|
||||
lang = pair[1];
|
||||
}
|
||||
});
|
||||
|
||||
if (LanguageManager.isSupportLanguage(lang)) {
|
||||
CookieManager.setCookie("lang", lang, 150);
|
||||
} else {
|
||||
CookieManager.setCookie("lang", "en-US", 150);
|
||||
window.location.reload();
|
||||
}
|
||||
} else {
|
||||
CookieManager.setCookie("lang", "en-US", 150);
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
return lang;
|
||||
}
|
||||
|
||||
static setLanguage(language) {
|
||||
if (!LanguageManager.isSupportLanguage(language)) {
|
||||
language = "en-US";
|
||||
}
|
||||
|
||||
CookieManager.setCookie("lang", language, 150);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
static isSupportLanguage(language) {
|
||||
const languageFilter = LanguageManager.supportedLanguages.filter((lang) => {
|
||||
return lang.value === language
|
||||
})
|
||||
|
||||
return languageFilter.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
const MediaQueryMixin = {
|
||||
data() {
|
||||
return {
|
||||
isMobile: window.innerWidth <= 768,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
updateDeviceType() {
|
||||
this.isMobile = window.innerWidth <= 768;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.updateDeviceType);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.updateDeviceType);
|
||||
},
|
||||
}
|
||||
|
||||
class FileManager {
|
||||
static downloadTextFile(content, filename = 'file.txt', options = { type: "text/plain" }) {
|
||||
let link = window.document.createElement('a');
|
||||
|
||||
link.download = filename;
|
||||
link.style.border = '0';
|
||||
link.style.padding = '0';
|
||||
link.style.margin = '0';
|
||||
link.style.position = 'absolute';
|
||||
link.style.left = '-9999px';
|
||||
link.style.top = `${window.pageYOffset || window.document.documentElement.scrollTop}px`;
|
||||
link.href = URL.createObjectURL(new Blob([content], options));
|
||||
link.click();
|
||||
|
||||
URL.revokeObjectURL(link.href);
|
||||
|
||||
link.remove();
|
||||
}
|
||||
}
|
||||
|
||||
class IntlUtil {
|
||||
static formatDate(date) {
|
||||
const language = LanguageManager.getLanguage()
|
||||
|
||||
let intlOptions = {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric"
|
||||
}
|
||||
|
||||
const intl = new Intl.DateTimeFormat(
|
||||
language,
|
||||
intlOptions
|
||||
)
|
||||
|
||||
return intl.format(new Date(date))
|
||||
}
|
||||
static formatRelativeTime(date) {
|
||||
const language = LanguageManager.getLanguage()
|
||||
const now = new Date()
|
||||
|
||||
// Handle delayed start (negative expiryTime values)
|
||||
const diff = date < 0
|
||||
? Math.round(date / (1000 * 60 * 60 * 24))
|
||||
: Math.round((date - now) / (1000 * 60 * 60 * 24))
|
||||
const formatter = new Intl.RelativeTimeFormat(language, { numeric: 'auto' })
|
||||
|
||||
return formatter.format(diff, 'day');
|
||||
}
|
||||
}
|
||||