Compare commits
1443 Commits
v1.2.2
...
Group_Mana
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
0630642849 | ||
|
|
8bd3827b41 | ||
|
|
24b367b82f | ||
|
|
011443bfc1 | ||
|
|
7417c52c8f | ||
|
|
4d4eef8d8a | ||
|
|
9de8b4acaf | ||
|
|
f21c293693 | ||
|
|
56159d9c52 | ||
|
|
6cbdf64013 | ||
|
|
bb9b9100a8 | ||
|
|
816adfc3ea | ||
|
|
0a95b0c7b2 | ||
|
|
d298f4ffbd | ||
|
|
315e8af025 | ||
|
|
de985263f5 | ||
|
|
dfe0bbd371 | ||
|
|
60cb328698 | ||
|
|
3d7f13225a | ||
|
|
76fdfb2ef2 | ||
|
|
8018023187 | ||
|
|
ea9d5dc2d5 | ||
|
|
96e43fa195 | ||
|
|
f1500a5d31 | ||
|
|
c9a218d060 | ||
|
|
6d18a15c4e | ||
|
|
c0f86d2f38 | ||
|
|
5227fefaeb | ||
|
|
7a51d2f2cc | ||
|
|
02ae61fe6b | ||
|
|
24b9e5bfa3 | ||
|
|
9ff7f14b6e | ||
|
|
c3b42b8ea4 | ||
|
|
5afb8d85fc | ||
|
|
767ee4ec2b | ||
|
|
21b64beb96 | ||
|
|
b84e3ef338 | ||
|
|
86586b7e8f | ||
|
|
f9792632d4 | ||
|
|
a9ec24f811 | ||
|
|
2da7dda794 | ||
|
|
39aae6fd16 | ||
|
|
f355ab5758 | ||
|
|
3847bc0a78 | ||
|
|
546d676472 | ||
|
|
6fb6241c3c | ||
|
|
f481ab993e | ||
|
|
3ef4ab423f | ||
|
|
b5a32ef57e | ||
|
|
4033001798 | ||
|
|
2486b5ff43 | ||
|
|
58647c6496 | ||
|
|
0b8a28d56d | ||
|
|
5d007435ab | ||
|
|
9c05aa514b | ||
|
|
7f2c11220f | ||
|
|
52b02fdef9 | ||
|
|
a4b76929f4 | ||
|
|
33082a271f | ||
|
|
28ede36a10 | ||
|
|
5036e9e28a | ||
|
|
4ca481d071 | ||
|
|
8259024fbe | ||
|
|
e275adbccd | ||
|
|
f96df5bc3d | ||
|
|
a09701c17b | ||
|
|
713285457f | ||
|
|
e1ef746cab | ||
|
|
fc714962ad | ||
|
|
2f46d42214 | ||
|
|
1b9360dfaf | ||
|
|
25b910c6d9 | ||
|
|
7ac79446c7 | ||
|
|
fdf805f264 | ||
|
|
baf8c94b2e | ||
|
|
adcfccbe45 | ||
|
|
c422214ae8 | ||
|
|
bd2d7bce62 | ||
|
|
ca4f83c7b1 | ||
|
|
b6bb0b1787 | ||
|
|
c0b5d5506f | ||
|
|
a2f6d3b8dc | ||
|
|
80cd793154 | ||
|
|
d070a82b3d | ||
|
|
5ec16301a6 | ||
|
|
07245d614a | ||
|
|
6e734553e2 | ||
|
|
275370e32c | ||
|
|
e7c59adc59 | ||
|
|
68c9b55447 | ||
|
|
70f9e32f28 | ||
|
|
5aae32c888 | ||
|
|
36cade2a02 | ||
|
|
907c30f743 | ||
|
|
5202fb2df4 | ||
|
|
73a19a45d7 | ||
|
|
0fb4094fe6 | ||
|
|
d9254fec03 | ||
|
|
2f02b71ed4 | ||
|
|
559aad9967 | ||
|
|
a1c15e9578 | ||
|
|
be9747dcbc | ||
|
|
94eb27d2c4 | ||
|
|
d1f67f7f2f | ||
|
|
bb6757df0f | ||
|
|
e094c351a3 | ||
|
|
fb31ecfa55 | ||
|
|
93a865f010 | ||
|
|
01a3a7b862 | ||
|
|
e233fdb092 | ||
|
|
55bf0c3e55 | ||
|
|
3129fdc103 | ||
|
|
2d20983690 | ||
|
|
490048a975 | ||
|
|
3950f596d8 | ||
|
|
835cf2801c | ||
|
|
e794d3d87f | ||
|
|
2cf762642b | ||
|
|
db24d21621 | ||
|
|
3d5c06bf08 | ||
|
|
a3a2d7a6a3 | ||
|
|
2cf2c451c2 | ||
|
|
2708c816a8 | ||
|
|
18cc3d5158 | ||
|
|
8e8a7d92e9 | ||
|
|
a99d031fe8 | ||
|
|
d0c9cebac1 | ||
|
|
d37dbb7dc8 | ||
|
|
a9f4c5fd77 | ||
|
|
8a9d9ae24c | ||
|
|
e02a199425 | ||
|
|
d712b44ea1 | ||
|
|
d85a23b6f2 | ||
|
|
a660ad6421 | ||
|
|
0e17521326 | ||
|
|
7118d92980 | ||
|
|
59708d6410 | ||
|
|
be8e78797a | ||
|
|
6d364638ca | ||
|
|
c147636133 | ||
|
|
fa7a6a3f99 | ||
|
|
620da1784b | ||
|
|
b13e1d666d | ||
|
|
2935eeb36a | ||
|
|
eb382cd5b4 | ||
|
|
f02c82677d | ||
|
|
5a5f7afb68 | ||
|
|
3a6bca794f | ||
|
|
4e20bb5f02 | ||
|
|
44b7eb881c | ||
|
|
9b1146780c | ||
|
|
866f7bd042 | ||
|
|
9454af46b7 | ||
|
|
9d18e925cc | ||
|
|
21d988133e | ||
|
|
0633a25d29 | ||
|
|
47243f0374 | ||
|
|
ede67f2d6c | ||
|
|
d759e09569 | ||
|
|
684e777628 | ||
|
|
1e902c2511 | ||
|
|
1a0cdfbfbc | ||
|
|
53e7608ddf | ||
|
|
43ec88bb24 | ||
|
|
c5b30d6c6c | ||
|
|
7fad81edab | ||
|
|
b9a5fd2c09 | ||
|
|
0bec29f2ba | ||
|
|
a7418d9708 | ||
|
|
abd69a253a | ||
|
|
2b420bd517 | ||
|
|
14d04e8ec9 | ||
|
|
1e2be10429 | ||
|
|
569c9428fb | ||
|
|
97489e743a | ||
|
|
c74efa1d43 | ||
|
|
18af7047f8 | ||
|
|
e7ae846823 | ||
|
|
b042f01e58 | ||
|
|
e43601ac08 | ||
|
|
bc29d7a252 | ||
|
|
dd21bb2db7 | ||
|
|
4d07b99fe7 | ||
|
|
8b5fe0b018 | ||
|
|
fc23af5db6 | ||
|
|
10f54cd937 | ||
|
|
b21758e6a6 | ||
|
|
903db95783 | ||
|
|
6fdc07a2d0 | ||
|
|
c9f245cb25 | ||
|
|
5dc95f8556 | ||
|
|
d9d8545cca | ||
|
|
4593147b65 | ||
|
|
020d1adc55 | ||
|
|
389b5408b1 | ||
|
|
037dbb5670 | ||
|
|
6af0d55ca9 | ||
|
|
0917eb2742 | ||
|
|
f8be7a7649 | ||
|
|
5b87b12535 | ||
|
|
8908e8b16a | ||
|
|
4f7af1e57a | ||
|
|
3af55cc5b4 | ||
|
|
ac5d8af4f9 | ||
|
|
01cd7539f9 | ||
|
|
102864525c | ||
|
|
e6254e23f2 | ||
|
|
5f3c2f781e | ||
|
|
6c73791cab | ||
|
|
f76e3ff94b | ||
|
|
7f0fc1b8ef | ||
|
|
d18eb7e4e4 | ||
|
|
d3377cd45e | ||
|
|
64a5a9f1bc | ||
|
|
32afd7200a | ||
|
|
ffa531f9ca | ||
|
|
05deb89451 | ||
|
|
6f807823b9 | ||
|
|
2f594ca7c9 | ||
|
|
ecae6e5ead | ||
|
|
34ab6ed7ee | ||
|
|
5ba9d6e118 | ||
|
|
c47a67975f | ||
|
|
6563d23f38 | ||
|
|
3a46c3302b | ||
|
|
1c97593360 | ||
|
|
95416cd553 | ||
|
|
d6cd0611d4 | ||
|
|
aad2cd8739 | ||
|
|
4cc0149317 | ||
|
|
836ee29b78 | ||
|
|
806b57f959 | ||
|
|
e827c1477c | ||
|
|
b827a4680d | ||
|
|
3fd124f76d | ||
|
|
3e8863f6ce | ||
|
|
9b026572cf | ||
|
|
6b34a3ae13 | ||
|
|
8b64180136 | ||
|
|
688ae4da10 | ||
|
|
0785da7223 | ||
|
|
2f1aad3e63 | ||
|
|
754b591e4f | ||
|
|
2b9d2d044c | ||
|
|
511eef54bb | ||
|
|
e705ae8e48 | ||
|
|
c7926d0bc0 | ||
|
|
034bc5e228 | ||
|
|
a39d07a68a | ||
|
|
81c9b4450b | ||
|
|
fc3ea2dd4b | ||
|
|
fe7a5f1813 | ||
|
|
3cd1b59a6c | ||
|
|
7ec6989c99 | ||
|
|
ee4d7a02a9 | ||
|
|
d6fd1c7ff0 | ||
|
|
5fbd5e8518 | ||
|
|
c31882cb92 | ||
|
|
81d47f7512 | ||
|
|
0baa204ce9 | ||
|
|
660e5ad101 | ||
|
|
aebf52efb2 | ||
|
|
c83a1db0c8 | ||
|
|
865d3e08e7 | ||
|
|
91ee6dc7cb | ||
|
|
7708bb9af2 | ||
|
|
03b7a34793 | ||
|
|
f3eb4f055d | ||
|
|
c61575ac9a | ||
|
|
f8796386dc | ||
|
|
ad38481312 | ||
|
|
937285ea3b | ||
|
|
328eeb8233 | ||
|
|
02239c8f2d | ||
|
|
70b3db074a | ||
|
|
d560cd9cc8 | ||
|
|
7526c4d969 | ||
|
|
766ef54b31 | ||
|
|
6b5535e60a | ||
|
|
fe00cfb09b | ||
|
|
2b4d6160c4 | ||
|
|
57029b1a40 | ||
|
|
61489077d7 | ||
|
|
4621933e5b | ||
|
|
fb76b2d500 | ||
|
|
9f6957ef3f | ||
|
|
d6e05d4a1a | ||
|
|
ea67b9760d | ||
|
|
3a503f12c8 | ||
|
|
2b7ad7cb9b | ||
|
|
9f38e19b81 | ||
|
|
73718a5dc5 | ||
|
|
bb9d00a0b3 | ||
|
|
3a1be63a40 | ||
|
|
4daaf0a647 | ||
|
|
f5dacd28e1 | ||
|
|
f65d3a5a98 | ||
|
|
13de2c6ca0 | ||
|
|
6cf29d5145 | ||
|
|
182710b86c | ||
|
|
4a1387ea83 | ||
|
|
c53cee31f5 | ||
|
|
222b9734ca | ||
|
|
c9ba393ce7 | ||
|
|
aa40016ec8 | ||
|
|
dc49304aa5 | ||
|
|
bb7b667467 | ||
|
|
d171850255 | ||
|
|
1207501405 | ||
|
|
2a2bf531ee | ||
|
|
9d724d34e1 | ||
|
|
618a566283 | ||
|
|
6804facabc | ||
|
|
98dd6bb949 | ||
|
|
f0e9aa0b8f | ||
|
|
68a16ef0e2 | ||
|
|
012775833a | ||
|
|
e4567a2b24 | ||
|
|
6c0775b120 | ||
|
|
9fbaede59f | ||
|
|
fd75cca266 | ||
|
|
9f904f8f47 | ||
|
|
78e1194ebb | ||
|
|
e04283c1fb | ||
|
|
a6742f395a | ||
|
|
9fba92d879 | ||
|
|
daa4354047 | ||
|
|
ec88053df0 | ||
|
|
08e259327b | ||
|
|
98384ac236 | ||
|
|
5f9058c84f | ||
|
|
979fdedbbe | ||
|
|
2463b99479 | ||
|
|
ff547a258d | ||
|
|
251ceeedba | ||
|
|
c1422be269 | ||
|
|
538fc9b365 | ||
|
|
b172d450e3 | ||
|
|
5c695ca652 | ||
|
|
e7ce8c8ddb | ||
|
|
4b9bbbc34b | ||
|
|
820e302aac | ||
|
|
ebaabf6150 | ||
|
|
a7bea936c5 | ||
|
|
38378fe36f | ||
|
|
7f13adbd05 | ||
|
|
74ba6d7825 | ||
|
|
7fd4015f17 | ||
|
|
589a8702aa | ||
|
|
91af54abf1 | ||
|
|
82244ced73 | ||
|
|
6856807726 | ||
|
|
2488adc042 | ||
|
|
62f08e877d | ||
|
|
3ed6fc4036 | ||
|
|
6550aa6bad | ||
|
|
2a20243751 | ||
|
|
fc56a1acac | ||
|
|
679dc69f6e | ||
|
|
ca2b3dc4fc | ||
|
|
c3d90c3f94 | ||
|
|
98cf1f2db6 | ||
|
|
4b5dd99555 | ||
|
|
491f7aecef | ||
|
|
590a8f07b9 | ||
|
|
594f004d2a | ||
|
|
bee690429f | ||
|
|
2111632702 | ||
|
|
a9229ecafe | ||
|
|
dadfa107af | ||
|
|
6feff78968 | ||
|
|
f035837aed | ||
|
|
b056c2dda0 | ||
|
|
97e6420018 | ||
|
|
abfcbf4226 | ||
|
|
1f9b3730d4 | ||
|
|
0824512a46 | ||
|
|
ee703ad857 | ||
|
|
722f5e716f | ||
|
|
fdf31d80e7 | ||
|
|
f8fccc057b | ||
|
|
4bb31b0af4 | ||
|
|
8b25e21f4e | ||
|
|
984b469c6f | ||
|
|
96c4cfeb23 | ||
|
|
e50b8db6c5 | ||
|
|
0734f4cd54 | ||
|
|
fbab0df504 | ||
|
|
5e3478f1c1 | ||
|
|
c76199514a | ||
|
|
31e9734414 | ||
|
|
ceee1e4277 | ||
|
|
b725ea7de5 | ||
|
|
0a4c8ffcf5 | ||
|
|
d305dd4e9f | ||
|
|
bbcab768ca | ||
|
|
954bf6fb5d | ||
|
|
77b83d81e2 | ||
|
|
10cd5159d1 | ||
|
|
b8b2acf853 | ||
|
|
4f3b93171a | ||
|
|
9261f9c665 | ||
|
|
6a41e19f7a | ||
|
|
0d2bdde149 | ||
|
|
bcfa47e2ad | ||
|
|
77776e1a62 | ||
|
|
c0d8f931da | ||
|
|
eec4d71097 | ||
|
|
a2bdf23940 | ||
|
|
d08aaa0068 | ||
|
|
1e1f947060 | ||
|
|
62ae7aaffa | ||
|
|
c234bbe9e1 | ||
|
|
ed38102f17 | ||
|
|
69651ecfb5 | ||
|
|
78d3680ced | ||
|
|
495bfb9683 | ||
|
|
9b60b0fd45 | ||
|
|
c6881e5149 | ||
|
|
5971e3f856 | ||
|
|
647b72e4fa | ||
|
|
b9b74139bf | ||
|
|
653e26dad6 | ||
|
|
0135be8757 | ||
|
|
76598dfa1a | ||
|
|
6a2019629b | ||
|
|
8d18c8e98f | ||
|
|
82e2241bdd | ||
|
|
24a0b143ae | ||
|
|
4b894760a1 | ||
|
|
e300fbc7cb | ||
|
|
038bbfaee1 | ||
|
|
f13bfabde7 | ||
|
|
775042459d | ||
|
|
af54b34f3a | ||
|
|
24ef749842 | ||
|
|
eaca3f2de0 | ||
|
|
c529b3aa08 | ||
|
|
25505a1915 | ||
|
|
b01f429e24 | ||
|
|
e3f1d3c892 | ||
|
|
9fbe80f87f | ||
|
|
0f05537a5d | ||
|
|
e1da2a2eed | ||
|
|
848e549c0c | ||
|
|
578ebf3cb5 | ||
|
|
1309504ff6 | ||
|
|
bd3ad7b0c2 | ||
|
|
4ca90c9071 | ||
|
|
523e49833e | ||
|
|
ad7d04feda | ||
|
|
070fd52d86 | ||
|
|
4cb67fd1c3 | ||
|
|
655e17e162 | ||
|
|
83769f963e | ||
|
|
4db42ba409 | ||
|
|
56e456ff47 | ||
|
|
0f1f3d8439 | ||
|
|
2088a4f815 | ||
|
|
bf267c2c48 | ||
|
|
07019d6594 | ||
|
|
e69c224ad3 | ||
|
|
6df393882e | ||
|
|
95200facde | ||
|
|
6411facf6a | ||
|
|
c980a06969 | ||
|
|
35feef650f | ||
|
|
38013e5ea9 | ||
|
|
e5fc20b8ae | ||
|
|
68ee4e003c | ||
|
|
f46ed6267c | ||
|
|
f2f066ac3b | ||
|
|
1efbb29ba5 | ||
|
|
784b93cfd0 | ||
|
|
5fbf8f0d53 | ||
|
|
bcc897640e | ||
|
|
5e47b4e949 | ||
|
|
549f230221 | ||
|
|
36cf7c0a8f | ||
|
|
0b55a4a001 | ||
|
|
e1cd6d9601 | ||
|
|
a7ffa42013 | ||
|
|
4ee986aa71 | ||
|
|
aa19637288 | ||
|
|
1bbef6d612 | ||
|
|
f7e439b930 | ||
|
|
e6c6c92ca2 | ||
|
|
c419eadf15 | ||
|
|
4d3bea48e1 | ||
|
|
d8c509b410 | ||
|
|
75fb66ade0 | ||
|
|
e677069d81 | ||
|
|
c19235b4dd | ||
|
|
fcacada1bf | ||
|
|
375814da27 | ||
|
|
ba2d02ae1d | ||
|
|
6a404ed6e8 | ||
|
|
4fca34f7b5 | ||
|
|
6367b1c69f | ||
|
|
7cb88d665f | ||
|
|
3b98550816 | ||
|
|
12dfe8a35f | ||
|
|
5bcaf7ba4b | ||
|
|
3eb232910a | ||
|
|
2a8da2ba3c | ||
|
|
a8b7063647 | ||
|
|
729d8549e2 | ||
|
|
f734c821d6 | ||
|
|
8dc23f97b6 | ||
|
|
45a6ced462 | ||
|
|
52614dc25c | ||
|
|
e77424a846 | ||
|
|
f305b980d9 | ||
|
|
02d0493c32 | ||
|
|
e70cde1c46 | ||
|
|
9bab8baad4 | ||
|
|
710ba4e53a | ||
|
|
abc590ae71 | ||
|
|
ad6d07326a | ||
|
|
d9fde8b64f | ||
|
|
673e97e4e2 | ||
|
|
60c0d8b93c | ||
|
|
0aa146381b | ||
|
|
6f09e548c7 | ||
|
|
2650f10314 | ||
|
|
1e874350ac | ||
|
|
cad6eeb9f5 | ||
|
|
ff55ddd036 | ||
|
|
e1a824e4a5 | ||
|
|
ee8bc6d6e3 | ||
|
|
d9b860db2a | ||
|
|
f537701b4a | ||
|
|
8e21d36bdc | ||
|
|
6e6424a085 | ||
|
|
1d4b5975b3 | ||
|
|
9f3efff88c | ||
|
|
7c1ecef589 | ||
|
|
44970e79b4 | ||
|
|
b0aaece6eb | ||
|
|
c0ef791f01 | ||
|
|
b63612ab98 | ||
|
|
7e7d80f8e1 | ||
|
|
e08ddc82ba | ||
|
|
e87fbbfde4 | ||
|
|
547b341d13 | ||
|
|
1bacf26c0a | ||
|
|
c9a6e2be86 | ||
|
|
2a354819c7 | ||
|
|
e926500870 | ||
|
|
0b96622b4c | ||
|
|
02c3bb90e9 | ||
|
|
77470ad8f2 | ||
|
|
60e9f01be8 | ||
|
|
d1f8796bb6 | ||
|
|
634111e815 | ||
|
|
084e26e353 | ||
|
|
86cfa14408 | ||
|
|
0927f44350 | ||
|
|
6bd0a2c772 | ||
|
|
73e97e1f42 | ||
|
|
6816b8085e | ||
|
|
80878517f6 | ||
|
|
388556a965 | ||
|
|
57cc9954af | ||
|
|
463eac1433 | ||
|
|
73be36869e | ||
|
|
45976e8114 | ||
|
|
be4a68bd8b | ||
|
|
4e6484caec | ||
|
|
30559007dd | ||
|
|
4ab5bf04eb | ||
|
|
a808170efb | ||
|
|
a7470a71e4 | ||
|
|
6765babe7b | ||
|
|
4b4be5f837 | ||
|
|
25430b7818 | ||
|
|
6aeedac051 | ||
|
|
9c231a41d1 | ||
|
|
ba673778ea | ||
|
|
8698024b61 | ||
|
|
e8b576fa65 | ||
|
|
c49147c5db | ||
|
|
149bd0ec51 | ||
|
|
f3280b46fe | ||
|
|
0f91e4d5f6 | ||
|
|
18617afd43 | ||
|
|
731e83a7da | ||
|
|
1c1f53267a | ||
|
|
8489f5f528 | ||
|
|
872974910c | ||
|
|
147999dd88 | ||
|
|
fe22cbd0e5 | ||
|
|
1277285d08 | ||
|
|
75df8a05f1 | ||
|
|
38e1d0f94e | ||
|
|
71c1a05386 | ||
|
|
4a6bd23873 | ||
|
|
8a1d647547 | ||
|
|
fb5180a890 | ||
|
|
d2c75b94cf | ||
|
|
edfc2d8d93 | ||
|
|
dd177b19f7 | ||
|
|
ae7283fc73 | ||
|
|
51a7c56cae | ||
|
|
c812168d6e | ||
|
|
fa10ae14f7 | ||
|
|
ed53df5ef3 | ||
|
|
b541290ded | ||
|
|
792fe6d9ef | ||
|
|
eb0c1dabf1 | ||
|
|
e00c3f1823 | ||
|
|
05bc655e16 | ||
|
|
7cddada8b4 | ||
|
|
24eb36715a | ||
|
|
22cf278ce2 | ||
|
|
5ab5986bd0 | ||
|
|
f0775abc67 | ||
|
|
d424b4bc32 | ||
|
|
329889ec00 | ||
|
|
95268dbc61 | ||
|
|
9a3200c9b5 | ||
|
|
c213fb6216 | ||
|
|
dd0217b46b | ||
|
|
b805bf6222 | ||
|
|
07b9474212 | ||
|
|
bf971911d5 | ||
|
|
c46ced0c4c | ||
|
|
dd3bbbc4af | ||
|
|
a97f90d225 | ||
|
|
6066edd510 | ||
|
|
eaec9e54ad | ||
|
|
fafcb2e8e7 | ||
|
|
145ea1e6f1 | ||
|
|
4cfed17650 | ||
|
|
c2b1fb4855 | ||
|
|
09807b39aa | ||
|
|
a0ec2f3972 | ||
|
|
e4b1dc20c3 | ||
|
|
2f05d4960e | ||
|
|
e63d2644bd | ||
|
|
56e4d13179 | ||
|
|
0dd0ba717f | ||
|
|
8050330e2e | ||
|
|
65e35c1711 | ||
|
|
0311ae4d05 | ||
|
|
6f09fae28b | ||
|
|
c2e9ee3665 | ||
|
|
1f78842b70 | ||
|
|
81a057d638 | ||
|
|
64d17bee70 | ||
|
|
6beb19b945 | ||
|
|
6b84d39700 | ||
|
|
5f3b91f3f1 | ||
|
|
9e433ea4c4 | ||
|
|
39537f6f44 | ||
|
|
44b34c437e | ||
|
|
467c5f807e | ||
|
|
1028319386 | ||
|
|
f726474a5d | ||
|
|
cd7a790637 | ||
|
|
4f74f5154b | ||
|
|
6e22aa59e7 | ||
|
|
85df1301dc | ||
|
|
e92aba0179 | ||
|
|
85fb2fda5e | ||
|
|
f57e693023 | ||
|
|
83f6f13b50 | ||
|
|
b833ed7992 | ||
|
|
5188d516e3 | ||
|
|
97925eeebe | ||
|
|
1328bb5aba | ||
|
|
4cc755c883 | ||
|
|
4e89c71095 | ||
|
|
d40e61fc45 | ||
|
|
c0f1a926e5 | ||
|
|
970dd0915e | ||
|
|
526e2578b0 | ||
|
|
ad9134bc1a | ||
|
|
b5657ab87d | ||
|
|
2c7ec3dc8a | ||
|
|
0fe35fde49 | ||
|
|
c7658973c3 | ||
|
|
dea61b839c | ||
|
|
9063336778 | ||
|
|
7fc3d37851 | ||
|
|
81aa3ed10e | ||
|
|
ca10623e40 | ||
|
|
547e38079f | ||
|
|
29e40a0bce | ||
|
|
4c1fa59453 | ||
|
|
84b0471ca0 | ||
|
|
1c785b930f | ||
|
|
c8ea9e43ec | ||
|
|
594d682e20 | ||
|
|
70f250dfe1 | ||
|
|
1030bcf321 | ||
|
|
fdc1124ea4 | ||
|
|
33a598366b | ||
|
|
459c19eee2 | ||
|
|
d73a995257 | ||
|
|
e4e0deeed4 | ||
|
|
48dce38e14 | ||
|
|
3eebd7c3e5 | ||
|
|
2fa151d52e | ||
|
|
c182f48079 | ||
|
|
94fad02737 | ||
|
|
d694e6eafc | ||
|
|
2a9cb6d29e | ||
|
|
c565a429af | ||
|
|
572d912858 | ||
|
|
6ae80fc992 | ||
|
|
ef7b979d53 | ||
|
|
b203067dfd | ||
|
|
1c9fc9422e | ||
|
|
6c26e40aea | ||
|
|
fe963d91ef | ||
|
|
c9461f1647 | ||
|
|
ea7fe09c27 | ||
|
|
8170b65db4 | ||
|
|
a2d8c98b0d | ||
|
|
8442836512 | ||
|
|
331f2adb1c | ||
|
|
45a8d6c95e | ||
|
|
1f2eb2ca1a | ||
|
|
de571a2da4 | ||
|
|
2c233dffa5 | ||
|
|
31339d6bf8 | ||
|
|
8865443438 | ||
|
|
835deb77f4 | ||
|
|
6a7c3716ac | ||
|
|
15211f81b1 | ||
|
|
b3f7a6572e | ||
|
|
6f28a3a2fe | ||
|
|
896cc5386c | ||
|
|
76f70ce1e9 | ||
|
|
6aa3c8d4a2 | ||
|
|
aff9d0ea15 | ||
|
|
526426e2dd | ||
|
|
2223a21cfc | ||
|
|
47ccc7b501 | ||
|
|
c38e1e0cfe | ||
|
|
f36034541e | ||
|
|
783fa856c3 | ||
|
|
6139effb8a | ||
|
|
8ae7f4a564 | ||
|
|
6f46f3e636 | ||
|
|
ba278a4269 | ||
|
|
3825e36ef7 | ||
|
|
f6e0e1b3cf | ||
|
|
769590d779 | ||
|
|
1fa9101b40 | ||
|
|
3f2e1aede9 | ||
|
|
235e6880c1 | ||
|
|
ffa23a43c6 | ||
|
|
66e3c505e3 | ||
|
|
0938519f49 | ||
|
|
5f489c3d08 | ||
|
|
f82d0051b2 | ||
|
|
76267b23a0 | ||
|
|
40a926a54a | ||
|
|
3a835fbeb8 | ||
|
|
f6461b8386 | ||
|
|
586663c840 | ||
|
|
82ead05093 | ||
|
|
d9b1b200ce | ||
|
|
786a3ac992 | ||
|
|
8c5648eb09 | ||
|
|
4dfe527f20 | ||
|
|
980ebd99ca | ||
|
|
9b7cddbec7 | ||
|
|
2bd706aa92 | ||
|
|
95e0d9a468 | ||
|
|
4865754b3d | ||
|
|
92eaff9608 | ||
|
|
0b7aa8a9e0 | ||
|
|
678962d4ca | ||
|
|
795835c54f | ||
|
|
91360a3f49 | ||
|
|
4831c2f1b2 | ||
|
|
3166d497f9 | ||
|
|
f50ccce9ec | ||
|
|
c7e300f14d | ||
|
|
419a1938ee | ||
|
|
a48745cb3e | ||
|
|
ac9408c37f | ||
|
|
3d71289075 | ||
|
|
fbd3772788 | ||
|
|
334b28cddc | ||
|
|
2fbfc88bc1 | ||
|
|
f781979d38 | ||
|
|
aba37be6eb | ||
|
|
30042bc047 | ||
|
|
49f60f7775 | ||
|
|
5f44c80cd5 | ||
|
|
85d42ce94f | ||
|
|
2f3c3d0ed2 | ||
|
|
7debf96610 | ||
|
|
d8b60c3cd5 | ||
|
|
f8eb548376 | ||
|
|
88fc4f81d4 | ||
|
|
911f2b0bb5 | ||
|
|
2d16eabc6e | ||
|
|
be50be75fe | ||
|
|
70e7987df5 | ||
|
|
8ceeb454ee | ||
|
|
f311bf1dbf | ||
|
|
837d7f77a1 | ||
|
|
d0213ce50b | ||
|
|
622e440366 | ||
|
|
1dc5452f1d | ||
|
|
a0daf2fae2 | ||
|
|
a21bdc9396 | ||
|
|
4cf7f75749 | ||
|
|
bcdac5aad6 | ||
|
|
3d5f851fce | ||
|
|
5487dc41cc | ||
|
|
5a908b9f58 | ||
|
|
61288db11e | ||
|
|
317f7fe9da | ||
|
|
7b5dd2d0ee | ||
|
|
b1302c70fb | ||
|
|
addedb1adf | ||
|
|
62bb42cfab | ||
|
|
f4be9f234a | ||
|
|
947129a62a | ||
|
|
66f0a13145 | ||
|
|
9626379731 | ||
|
|
c2c61cdd5b | ||
|
|
b5ae580d12 | ||
|
|
63939244a4 | ||
|
|
213b693bd3 | ||
|
|
a289ef5d10 | ||
|
|
955eb8f142 | ||
|
|
d396fb5d06 | ||
|
|
b5dd258074 | ||
|
|
c855a292cb | ||
|
|
f2132c62e9 | ||
|
|
94a3807353 | ||
|
|
7cacfc074e | ||
|
|
9e8ac8a087 | ||
|
|
e64a9eeee6 | ||
|
|
a55a1a7102 | ||
|
|
46bc39c160 | ||
|
|
2a182d8b9a | ||
|
|
77241c7fcf | ||
|
|
fd6a85afd9 | ||
|
|
9a89d7bfab | ||
|
|
edd6b22109 | ||
|
|
5468069bef | ||
|
|
0cce35784e | ||
|
|
80c1e58ed5 | ||
|
|
b0871a6ef6 | ||
|
|
288374d5fa | ||
|
|
1f7c79c735 | ||
|
|
251fd608df | ||
|
|
456941323b | ||
|
|
a6a77688dc | ||
|
|
09cd2248dc | ||
|
|
8143379645 | ||
|
|
5bd6baa055 | ||
|
|
41e9290574 | ||
|
|
cf7d50617b | ||
|
|
95e006963c | ||
|
|
65588a4492 | ||
|
|
d39c7e4ae3 | ||
|
|
3bec9ee273 | ||
|
|
7b3628d33b | ||
|
|
ad1aa5b2f9 | ||
|
|
46ef8c503e | ||
|
|
721fec3b5a | ||
|
|
30a5f66f26 | ||
|
|
bb6e6861ca | ||
|
|
4c0e391597 | ||
|
|
43c1fc9aad | ||
|
|
7a48cbb191 | ||
|
|
004d69392b | ||
|
|
fc0882805d | ||
|
|
f553922d53 | ||
|
|
7b2764566c | ||
|
|
55d38dfa48 | ||
|
|
0e266b88f0 | ||
|
|
7bb3e517b2 | ||
|
|
7d0c3b6517 | ||
|
|
67201fc678 | ||
|
|
d137deccfa | ||
|
|
00777e3a25 | ||
|
|
bcb2f125ff | ||
|
|
37ab8f42e9 | ||
|
|
cf1cfbee96 | ||
|
|
d89e03023f | ||
|
|
5ec1559c7b | ||
|
|
cf2b1fd9ec | ||
|
|
ddbc1602ce | ||
|
|
77cee098ad | ||
|
|
89b94c2c90 | ||
|
|
350743fea3 | ||
|
|
8011d0b6c6 | ||
|
|
9e5d7ac1d0 | ||
|
|
e01fb9b605 | ||
|
|
52a468d586 | ||
|
|
c73c71cc83 | ||
|
|
2141d62069 | ||
|
|
f286c9a86a | ||
|
|
b4fd254c71 | ||
|
|
0a8cf7e41b | ||
|
|
abd48551fd | ||
|
|
bb9a10051f | ||
|
|
5c6406ab58 | ||
|
|
a25137f215 | ||
|
|
50c296eb28 | ||
|
|
b1a302de95 | ||
|
|
7b567458ff | ||
|
|
13e3e23f20 | ||
|
|
efecdf5fd0 | ||
|
|
f21904caf2 | ||
|
|
15a97af215 | ||
|
|
20a55c086e | ||
|
|
c727b81772 | ||
|
|
77692e0298 | ||
|
|
5e3e965647 | ||
|
|
88cde18bb2 | ||
|
|
7fd93e25fd | ||
|
|
1c9643e6a3 | ||
|
|
eb83516fa5 | ||
|
|
1b8df3c0a1 | ||
|
|
54d45cc029 | ||
|
|
e35767aff2 | ||
|
|
9bbcb74db6 | ||
|
|
515e7f7fef | ||
|
|
87a5190b7d | ||
|
|
9d47d74a7a | ||
|
|
92f5dfed1c | ||
|
|
867e5ea022 | ||
|
|
a24814104c | ||
|
|
024b65524d | ||
|
|
f22dd6b53d | ||
|
|
735df6bd4e | ||
|
|
0e77547e98 | ||
|
|
747a1e1f60 | ||
|
|
ac31d6d9fb | ||
|
|
83c853ffb6 | ||
|
|
058ab5f901 | ||
|
|
78638a9737 | ||
|
|
7f8f0b0f2d | ||
|
|
7b9e0b946e | ||
|
|
6c087ceb1a | ||
|
|
6602c55f3c | ||
|
|
d405141ad0 | ||
|
|
3ee3432d8f | ||
|
|
b2d70a2a9b | ||
|
|
26f160fb89 | ||
|
|
0a5811adf8 | ||
|
|
733a011b28 | ||
|
|
c8023b7c8d | ||
|
|
bed3cd445d | ||
|
|
c8baf5ceee | ||
|
|
85c715a2f6 | ||
|
|
55c1fe26fb | ||
|
|
8d11f83ac7 | ||
|
|
d349bffcd6 | ||
|
|
5856160c30 | ||
|
|
3cd3693b2c | ||
|
|
bc3003be54 | ||
|
|
e53615d119 | ||
|
|
55232f9033 | ||
|
|
fd40e97008 | ||
|
|
1598ad804d | ||
|
|
c8e666d8ae | ||
|
|
e1533b9418 | ||
|
|
a60a8d8a2f | ||
|
|
73704e38d5 | ||
|
|
1680bb36c3 | ||
|
|
cd483c191a | ||
|
|
7d09b4e840 | ||
|
|
83ffa25d6f | ||
|
|
a53d2b927f | ||
|
|
146dc6ce4a | ||
|
|
e597ea5ab2 | ||
|
|
3e833fca9b | ||
|
|
c96cf85619 | ||
|
|
0a63c75138 | ||
|
|
c6295085fe | ||
|
|
4b3bdebfa5 | ||
|
|
1a603b2501 | ||
|
|
3fa5f834b8 | ||
|
|
ff33539fba | ||
|
|
961636b510 | ||
|
|
88b7d0dc44 | ||
|
|
e164c7e780 | ||
|
|
481d4beabb | ||
|
|
19c991014e | ||
|
|
12ec487241 | ||
|
|
a18cbdcf11 | ||
|
|
4f8de18d1f | ||
|
|
dbac48f05d | ||
|
|
3a02359325 | ||
|
|
9e63b0e2b3 | ||
|
|
07dc8c4803 | ||
|
|
20bfd71cf1 | ||
|
|
6a33a48a9a | ||
|
|
1885a8c0bf | ||
|
|
4ce53920fe | ||
|
|
5100bbba52 | ||
|
|
f93d912644 | ||
|
|
ce8551b8c4 | ||
|
|
d26c21d900 | ||
|
|
f5f9347661 | ||
|
|
a0f5875cb3 | ||
|
|
3055c68615 | ||
|
|
c3ed8051f3 | ||
|
|
d2cdc51c54 | ||
|
|
ee896662f5 | ||
|
|
177bd036a3 | ||
|
|
d03e049320 | ||
|
|
957d9e24fb | ||
|
|
865e47e9a6 | ||
|
|
607c5d3598 | ||
|
|
8879541999 | ||
|
|
0b896d9c31 | ||
|
|
6f4a2809e2 | ||
|
|
103a26edb6 | ||
|
|
7419592626 | ||
|
|
edabfad559 | ||
|
|
2849cc0b2e | ||
|
|
46eb174af1 | ||
|
|
f8878208ca | ||
|
|
e126095949 | ||
|
|
df2f292b68 | ||
|
|
9f5ba0cf93 | ||
|
|
b5c5539501 | ||
|
|
252afe47c0 | ||
|
|
379451135d | ||
|
|
bc06dbab21 | ||
|
|
6a71ea7f5e | ||
|
|
942b9862d8 | ||
|
|
ae55fdc38a | ||
|
|
cc3ff61ae2 | ||
|
|
045717010a | ||
|
|
aae32d9211 | ||
|
|
cd5ad78f56 | ||
|
|
b450aacebd | ||
|
|
640068b279 | ||
|
|
04d85af40e | ||
|
|
fca882ee31 | ||
|
|
2832106bc6 | ||
|
|
bf24838939 | ||
|
|
16e3107d23 | ||
|
|
262e3c0985 | ||
|
|
2b460bac1a | ||
|
|
d1c4eb9b4c | ||
|
|
dfa3d39ab3 | ||
|
|
2b54d0344e | ||
|
|
f817f922fe | ||
|
|
55f7fcd1b3 | ||
|
|
b0f974a94d | ||
|
|
6bebde4105 | ||
|
|
fa4a63c958 | ||
|
|
11def0a753 | ||
|
|
513f87550a | ||
|
|
81838b504c | ||
|
|
4980744793 | ||
|
|
5ff6f4094e | ||
|
|
bbce1eb3f7 | ||
|
|
204f73a692 | ||
|
|
641a7d3e57 | ||
|
|
3f7e819a9b | ||
|
|
834d003ab6 | ||
|
|
c627227893 | ||
|
|
2a8725a7a5 | ||
|
|
ca2d1bb901 | ||
|
|
fa19649286 | ||
|
|
56d75f5293 | ||
|
|
e1132a3f41 | ||
|
|
4d479102ad | ||
|
|
e90c575bfd | ||
|
|
9d02f455cc | ||
|
|
13f67f595c | ||
|
|
25741dcb08 | ||
|
|
a7af62162c | ||
|
|
3e0faecaae | ||
|
|
dc7dbae14a | ||
|
|
dd8c763b21 | ||
|
|
e0e7c102b8 | ||
|
|
f26a7df11b | ||
|
|
72a1b1e3f3 | ||
|
|
dfdb77c491 | ||
|
|
846efe8eb4 | ||
|
|
8b79b5a315 | ||
|
|
c71041a60d | ||
|
|
6865cb108c | ||
|
|
ee2089257a | ||
|
|
9f85ec72a8 | ||
|
|
c4c266205b | ||
|
|
e4afbcea3b | ||
|
|
44cede41fd | ||
|
|
672fd1da19 | ||
|
|
f3fe866af2 | ||
|
|
25e50aa6f1 | ||
|
|
ff3657e15a | ||
|
|
4af626bb4b | ||
|
|
936f2e6ec2 | ||
|
|
3e5984930e | ||
|
|
40a0297499 | ||
|
|
e13015a920 | ||
|
|
ec4755952a | ||
|
|
d8fb83b1f2 | ||
|
|
4fb060d25e | ||
|
|
3eff8022f2 | ||
|
|
19bb28cb26 | ||
|
|
b70ecc12b3 | ||
|
|
1e72a22c96 | ||
|
|
d695ea8192 | ||
|
|
b639a1bbd5 | ||
|
|
4f952963ae | ||
|
|
7f2ef94c7f | ||
|
|
81372de369 | ||
|
|
a3b170d6c4 | ||
|
|
4548755375 | ||
|
|
0c047cf124 | ||
|
|
c6b59f1ee3 | ||
|
|
b40759fe18 | ||
|
|
e6f08517a3 | ||
|
|
293787f867 | ||
|
|
54bf24f662 | ||
|
|
b4727cc5f8 | ||
|
|
b4fc5a7ab8 | ||
|
|
d0e53231fe | ||
|
|
cce4b0a449 | ||
|
|
ff90e2a02e | ||
|
|
c575425292 | ||
|
|
63f71e527c | ||
|
|
5be9131078 |
14
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
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: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
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.
|
||||
56
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,56 +0,0 @@
|
||||
name: Issue Report
|
||||
description: "Create a report to help us improve."
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Welcome
|
||||
options:
|
||||
- label: Yes, I'm using the latest major release. Only such installations are supported.
|
||||
required: true
|
||||
- label: Yes, I'm using the supported system. Only such systems are supported.
|
||||
required: true
|
||||
- label: Yes, I have read all WIKI document,nothing can help me in my problem.
|
||||
required: true
|
||||
- label: Yes, I've searched similar issues on GitHub and didn't find any.
|
||||
required: true
|
||||
- label: Yes, I've included all information below (version, config, log, etc).
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Description of the problem,screencshot would be good
|
||||
placeholder: Your problem description
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: Version of 3x-ui
|
||||
value: |-
|
||||
<details>
|
||||
|
||||
```console
|
||||
# Paste here
|
||||
```
|
||||
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: x-ui log reports or xray log
|
||||
value: |-
|
||||
<details>
|
||||
|
||||
```console
|
||||
# paste log here
|
||||
```
|
||||
|
||||
</details>
|
||||
validations:
|
||||
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"
|
||||
57
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
name: Release 3X-UI for Docker
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
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=pep440,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
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
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 }}
|
||||
162
.github/workflows/release.yml
vendored
@@ -1,44 +1,142 @@
|
||||
name: Release 3X-ui
|
||||
name: Release 3X-UI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
linuxamd64build:
|
||||
name: build x-ui amd64 version
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
- amd64
|
||||
- arm64
|
||||
- armv7
|
||||
- armv6
|
||||
- 386
|
||||
- armv5
|
||||
- s390x
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3.5.0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4.0.0
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- name: build linux amd64 version
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o xui-release -v main.go
|
||||
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
|
||||
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 -ldflags "-w -s" -o xui-release -v main.go
|
||||
|
||||
mkdir x-ui
|
||||
cp xui-release x-ui/xui-release
|
||||
cp x-ui.service x-ui/x-ui.service
|
||||
cp x-ui.sh x-ui/x-ui.sh
|
||||
cd x-ui
|
||||
mv xui-release x-ui
|
||||
mkdir bin
|
||||
cd bin
|
||||
wget https://github.com/mhsanaei/Xray-core/releases/latest/download/Xray-linux-64.zip
|
||||
unzip Xray-linux-64.zip
|
||||
rm -f Xray-linux-64.zip 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 https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat
|
||||
mv xray xray-linux-amd64
|
||||
cd ..
|
||||
cd ..
|
||||
- name: package
|
||||
run: tar -zcvf x-ui-linux-amd64.tar.gz x-ui
|
||||
- name: upload
|
||||
uses: svenstaro/upload-release-action@2.5.0
|
||||
cp xui-release x-ui/
|
||||
cp x-ui.service 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/v25.1.30/"
|
||||
if [ "${{ matrix.platform }}" == "amd64" ]; then
|
||||
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 -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 -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 -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 -q ${Xray_URL}Xray-linux-32.zip
|
||||
unzip Xray-linux-32.zip
|
||||
rm -f Xray-linux-32.zip
|
||||
elif [ "${{ matrix.platform }}" == "armv5" ]; then
|
||||
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 -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 -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 ../..
|
||||
|
||||
- name: Package
|
||||
run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui
|
||||
|
||||
- name: Upload files to Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: x-ui-linux-${{ matrix.platform }}
|
||||
path: ./x-ui-linux-${{ matrix.platform }}.tar.gz
|
||||
|
||||
- name: Upload files to GH release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref }}
|
||||
file: x-ui-linux-amd64.tar.gz
|
||||
asset_name: x-ui-linux-amd64.tar.gz
|
||||
file: x-ui-linux-${{ matrix.platform }}.tar.gz
|
||||
asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
|
||||
prerelease: true
|
||||
overwrite: true
|
||||
|
||||
18
.gitignore
vendored
@@ -1,12 +1,16 @@
|
||||
.idea
|
||||
.vscode
|
||||
.cache
|
||||
.sync*
|
||||
*.tar.gz
|
||||
*.log
|
||||
access.log
|
||||
error.log
|
||||
tmp
|
||||
main
|
||||
backup/
|
||||
bin/
|
||||
dist/
|
||||
x-ui-*.tar.gz
|
||||
/x-ui
|
||||
/release.sh
|
||||
.sync*
|
||||
main
|
||||
release/
|
||||
access.log
|
||||
.cache
|
||||
/release.sh
|
||||
/x-ui
|
||||
|
||||
7
DockerEntrypoint.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Start fail2ban
|
||||
[ $X_UI_ENABLE_FAIL2BAN == "true" ] && fail2ban-client -x start
|
||||
|
||||
# Run x-ui
|
||||
exec /app/x-ui
|
||||
40
DockerInit.sh
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/bin/sh
|
||||
case $1 in
|
||||
amd64)
|
||||
ARCH="64"
|
||||
FNAME="amd64"
|
||||
;;
|
||||
i386)
|
||||
ARCH="32"
|
||||
FNAME="i386"
|
||||
;;
|
||||
armv8 | arm64 | aarch64)
|
||||
ARCH="arm64-v8a"
|
||||
FNAME="arm64"
|
||||
;;
|
||||
armv7 | arm | arm32)
|
||||
ARCH="arm32-v7a"
|
||||
FNAME="arm32"
|
||||
;;
|
||||
armv6)
|
||||
ARCH="arm32-v6"
|
||||
FNAME="armv6"
|
||||
;;
|
||||
*)
|
||||
ARCH="64"
|
||||
FNAME="amd64"
|
||||
;;
|
||||
esac
|
||||
mkdir -p build/bin
|
||||
cd build/bin
|
||||
wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.1.30/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 -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
|
||||
cd ../../
|
||||
54
Dockerfile
Normal file
@@ -0,0 +1,54 @@
|
||||
# ========================================================
|
||||
# Stage: Builder
|
||||
# ========================================================
|
||||
FROM golang:1.23-alpine AS builder
|
||||
WORKDIR /app
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN apk --no-cache --update add \
|
||||
build-base \
|
||||
gcc \
|
||||
wget \
|
||||
unzip
|
||||
|
||||
COPY . .
|
||||
|
||||
ENV CGO_ENABLED=1
|
||||
ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
|
||||
RUN go build -ldflags "-w -s" -o build/x-ui main.go
|
||||
RUN ./DockerInit.sh "$TARGETARCH"
|
||||
|
||||
# ========================================================
|
||||
# Stage: Final Image of 3x-ui
|
||||
# ========================================================
|
||||
FROM alpine
|
||||
ENV TZ=Asia/Tehran
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache --update \
|
||||
ca-certificates \
|
||||
tzdata \
|
||||
fail2ban \
|
||||
bash
|
||||
|
||||
COPY --from=builder /app/build/ /app/
|
||||
COPY --from=builder /app/DockerEntrypoint.sh /app/
|
||||
COPY --from=builder /app/x-ui.sh /usr/bin/x-ui
|
||||
|
||||
|
||||
# Configure fail2ban
|
||||
RUN rm -f /etc/fail2ban/jail.d/alpine-ssh.conf \
|
||||
&& cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local \
|
||||
&& sed -i "s/^\[ssh\]$/&\nenabled = false/" /etc/fail2ban/jail.local \
|
||||
&& sed -i "s/^\[sshd\]$/&\nenabled = false/" /etc/fail2ban/jail.local \
|
||||
&& sed -i "s/#allowipv6 = auto/allowipv6 = auto/g" /etc/fail2ban/fail2ban.conf
|
||||
|
||||
RUN chmod +x \
|
||||
/app/DockerEntrypoint.sh \
|
||||
/app/x-ui \
|
||||
/usr/bin/x-ui
|
||||
|
||||
ENV X_UI_ENABLE_FAIL2BAN="true"
|
||||
VOLUME [ "/etc/x-ui" ]
|
||||
CMD [ "./x-ui" ]
|
||||
ENTRYPOINT [ "/app/DockerEntrypoint.sh" ]
|
||||
587
README.es_ES.md
Normal file
@@ -0,0 +1,587 @@
|
||||
[English](/README.md) | [فارسی](/README.fa_IR.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>
|
||||
|
||||
**Un Panel Web Avanzado • Construido sobre Xray Core**
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](#)
|
||||
[](#)
|
||||
[](#)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
> **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
|
||||
|
||||
**Si este proyecto te es útil, podrías considerar darle una**:star2:
|
||||
|
||||
<p align="left">
|
||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
||||
<img src="./media/buymeacoffe.png" alt="Image">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
|
||||
## Instalar y Actualizar
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
|
||||
```
|
||||
|
||||
## Instalar versión antigua (no recomendamos)
|
||||
|
||||
Para instalar la versión deseada, utiliza el siguiente comando de instalación. Por ejemplo, ver `v1.7.9`:
|
||||
|
||||
```
|
||||
VERSION=v1.7.9 && <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/$VERSION/install.sh") $VERSION
|
||||
```
|
||||
|
||||
## Certificado SSL
|
||||
|
||||
<details>
|
||||
<summary>Haga clic para ver los detalles del certificado SSL</summary>
|
||||
|
||||
### ACME
|
||||
|
||||
Para gestionar certificados SSL utilizando ACME:
|
||||
|
||||
1. Asegúrate de que tu dominio esté correctamente resuelto al servidor.
|
||||
2. Ejecuta el comando `x-ui` en la terminal y elige `Gestión de Certificados SSL`.
|
||||
3. Se te presentarán las siguientes opciones:
|
||||
|
||||
- **Get SSL:** Obtener certificados SSL.
|
||||
- **Revoke:** Revocar certificados SSL existentes.
|
||||
- **Force Renew:** Forzar la renovación de certificados SSL.
|
||||
- **Show Existing Domains:** Mostrar todos los certificados de dominio disponibles en el servidor.
|
||||
- **Set Certificate Paths for the Panel:** Especificar el certificado para tu dominio que será utilizado por el panel.
|
||||
|
||||
### Certbot
|
||||
|
||||
Para instalar y usar Certbot:
|
||||
|
||||
```sh
|
||||
apt-get install certbot -y
|
||||
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
|
||||
certbot renew --dry-run
|
||||
```
|
||||
|
||||
### Cloudflare
|
||||
|
||||
El script de gestión incluye una aplicación de certificado SSL integrada para Cloudflare. Para usar este script para solicitar un certificado, necesitas lo siguiente:
|
||||
|
||||
- Correo electrónico registrado en Cloudflare
|
||||
- Clave API Global de Cloudflare
|
||||
- El nombre de dominio debe estar resuelto al servidor actual a través de Cloudflare
|
||||
|
||||
**Cómo obtener la Clave API Global de Cloudflare:**
|
||||
|
||||
1. Ejecuta el comando `x-ui` en la terminal y elige `Certificado SSL de Cloudflare`.
|
||||
2. Visita el enlace: [Tokens de API de Cloudflare](https://dash.cloudflare.com/profile/api-tokens).
|
||||
3. Haz clic en "Ver Clave API Global" (consulta la captura de pantalla a continuación):
|
||||

|
||||
4. Es posible que necesites volver a autenticar tu cuenta. Después de eso, se mostrará la Clave API (consulta la captura de pantalla a continuación):
|
||||

|
||||
|
||||
Al utilizarlo, simplemente ingresa tu `nombre de dominio`, `correo electrónico` y `CLAVE API`. El diagrama es el siguiente:
|
||||

|
||||
|
||||
</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>
|
||||
|
||||
## Configuración de Nginx
|
||||
<details>
|
||||
<summary>Haga clic aquí para configurar el proxy inverso</summary>
|
||||
|
||||
#### Proxy inverso Nginx
|
||||
```nginx
|
||||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Range $http_range;
|
||||
proxy_set_header If-Range $http_if_range;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://127.0.0.1:2053;
|
||||
}
|
||||
```
|
||||
|
||||
#### Nginx sub-path
|
||||
- EAsegúrese de que la "Ruta Raíz de la URL del Panel" en la configuración del panel `/sub` es la misma.
|
||||
- El `url` en la configuración del panel debe terminar con `/`.
|
||||
|
||||
```nginx
|
||||
location /sub {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Range $http_range;
|
||||
proxy_set_header If-Range $http_if_range;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://127.0.0.1:2053;
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
## SO Recomendados
|
||||
|
||||
- Ubuntu 20.04+
|
||||
- Debian 11+
|
||||
- CentOS 8+
|
||||
- OpenEuler 22.03+
|
||||
- Fedora 36+
|
||||
- Arch Linux
|
||||
- Parch Linux
|
||||
- Manjaro
|
||||
- Armbian
|
||||
- AlmaLinux 8.0+
|
||||
- Rocky Linux 8+
|
||||
- Oracle Linux 8+
|
||||
- OpenSUSE Tubleweed
|
||||
- Amazon Linux 2023
|
||||
- Windows x64
|
||||
|
||||
## 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
|
||||
|
||||
- English (inglés)
|
||||
- Persian (persa)
|
||||
- Traditional Chinese (chino tradicional)
|
||||
- Simplified Chinese (chino simplificado)
|
||||
- Japanese (japonés)
|
||||
- Russian (ruso)
|
||||
- Vietnamese (vietnamita)
|
||||
- Spanish (español)
|
||||
- Indonesian (indonesio)
|
||||
- Ukrainian (ucraniano)
|
||||
- Turkish (turco)
|
||||
- Português (Brazil) (portugués (Brasil))
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
## Configuración Predeterminada del Panel
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para ver los detalles de la configuración predeterminada</summary>
|
||||
|
||||
### Nombre de usuario, Contraseña, Puerto y Ruta Base Web
|
||||
|
||||
Si elige no modificar estas configuraciones, se generarán aleatoriamente (esto no se aplica a Docker).
|
||||
|
||||
**Configuraciones predeterminadas para Docker:**
|
||||
- **Nombre de usuario:** admin
|
||||
- **Contraseña:** admin
|
||||
- **Puerto:** 2053
|
||||
|
||||
### Gestión de la Base de Datos:
|
||||
|
||||
Puedes realizar copias de seguridad y restauraciones de la base de datos directamente desde el panel.
|
||||
|
||||
- **Ruta de la Base de Datos:**
|
||||
- `/etc/x-ui/x-ui.db`
|
||||
|
||||
### Ruta Base Web
|
||||
|
||||
1. **Restablecer la Ruta Base Web:**
|
||||
- Abre tu terminal.
|
||||
- Ejecuta el comando `x-ui`.
|
||||
- Selecciona la opción `Restablecer la Ruta Base Web`.
|
||||
|
||||
2. **Generar o Personalizar la Ruta:**
|
||||
- La ruta se generará aleatoriamente, o puedes ingresar una ruta personalizada.
|
||||
|
||||
3. **Ver Configuración Actual:**
|
||||
- Para ver tu configuración actual, utiliza el comando `x-ui settings` en el terminal o selecciona `Ver Configuración Actual` en `x-ui`.
|
||||
|
||||
### Recomendación de Seguridad:
|
||||
- Para mayor seguridad, utiliza una palabra larga y aleatoria en la estructura de tu URL.
|
||||
|
||||
**Ejemplos:**
|
||||
- `http://ip:port/*webbasepath*/panel`
|
||||
- `http://domain:port/*webbasepath*/panel`
|
||||
|
||||
</details>
|
||||
|
||||
## Configuración de WARP
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para ver los detalles de la configuración de WARP</summary>
|
||||
|
||||
#### Uso
|
||||
|
||||
**Para versiones `v2.1.0` y posteriores:**
|
||||
|
||||
WARP está integrado, no se requiere instalación adicional. Simplemente habilita la configuración necesaria en el panel.
|
||||
|
||||
</details>
|
||||
|
||||
## Límite de IP
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para ver los detalles del límite de IP</summary>
|
||||
|
||||
#### Uso
|
||||
|
||||
**Nota:** El Límite de IP no funcionará correctamente cuando uses Túnel IP.
|
||||
|
||||
- **Para versiones hasta `v1.6.1`:**
|
||||
- El límite de IP está integrado en el panel.
|
||||
|
||||
**Para versiones `v1.7.0` y posteriores:**
|
||||
|
||||
Para habilitar la funcionalidad de límite de IP, necesitas instalar `fail2ban` y los archivos requeridos siguiendo estos pasos:
|
||||
|
||||
1. Ejecuta el comando `x-ui` en el terminal, luego elige `Gestión de Límite de IP`.
|
||||
2. Verás las siguientes opciones:
|
||||
|
||||
- **Cambiar la Duración del Bloqueo:** Ajustar la duración de los bloqueos.
|
||||
- **Desbloquear a Todos:** Levantar todos los bloqueos actuales.
|
||||
- **Revisar los Registros:** Revisar los registros.
|
||||
- **Estado de Fail2ban:** Verificar el estado de `fail2ban`.
|
||||
- **Reiniciar Fail2ban:** Reiniciar el servicio `fail2ban`.
|
||||
- **Desinstalar Fail2ban:** Desinstalar Fail2ban con la configuración.
|
||||
|
||||
3. Agrega una ruta para el registro de acceso en el panel configurando `Xray Configs/log/Access log` a `./access.log`, luego guarda y reinicia Xray.
|
||||
|
||||
- **Para versiones anteriores a `v2.1.3`:**
|
||||
- Necesitas configurar manualmente la ruta del registro de acceso en tu configuración de Xray:
|
||||
|
||||
```sh
|
||||
"log": {
|
||||
"access": "./access.log",
|
||||
"dnsLog": false,
|
||||
"loglevel": "warning"
|
||||
},
|
||||
```
|
||||
|
||||
- **Para versiones `v2.1.3` y posteriores:**
|
||||
- Hay una opción para configurar `access.log` directamente desde el panel.
|
||||
|
||||
</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
|
||||
|
||||
- [Documentación de API](https://www.postman.com/hsanaei/3x-ui/collection/q1l5l0u/3x-ui)
|
||||
- `/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
|
||||
|
||||
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5%26entityType%3Dcollection%26workspaceId%3Dd64f609f-485a-4951-9b8f-876b3f917124)
|
||||
</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
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/01-overview-dark.png">
|
||||
<img alt="3x-ui" src="./media/01-overview-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/02-inbounds-dark.png">
|
||||
<img alt="3x-ui" src="./media/02-inbounds-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/03-add-inbound-dark.png">
|
||||
<img alt="3x-ui" src="./media/03-add-inbound-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/04-add-client-dark.png">
|
||||
<img alt="3x-ui" src="./media/04-add-client-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/05-settings-dark.png">
|
||||
<img alt="3x-ui" src="./media/05-settings-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/06-configs-dark.png">
|
||||
<img alt="3x-ui" src="./media/06-configs-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/07-bot-dark.png">
|
||||
<img alt="3x-ui" src="./media/07-bot-light.png">
|
||||
</picture>
|
||||
|
||||
## 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._
|
||||
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (License: **GPL-3.0**): _Este repositorio contiene reglas de enrutamiento de V2Ray actualizadas automáticamente basadas en datos de dominios y direcciones bloqueados en Rusia._
|
||||
|
||||
## Estrellas a lo largo del tiempo
|
||||
|
||||
[](https://starchart.cc/MHSanaei/3x-ui)
|
||||
527
README.fa_IR.md
Normal file
@@ -0,0 +1,527 @@
|
||||
[English](/README.md) | [فارسی](/README.fa_IR.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>
|
||||
|
||||
**یک پنل وب پیشرفته • ساخته شده بر پایه Xray Core**
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](#)
|
||||
[](#)
|
||||
[](#)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
> **سلب مسئولیت:** این پروژه صرفاً برای اهداف آموزشی و تحقیقاتی است. استفاده از آن برای مقاصد غیرقانونی یا در محیطهای عملیاتی ممنوع است.
|
||||
|
||||
**اگر این پروژه برای شما مفید بوده، میتوانید با دادن یک**:star2: از آن حمایت کنید.
|
||||
|
||||
<p align="left">
|
||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
||||
<img src="./media/buymeacoffe.png" 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)
|
||||
```
|
||||
|
||||
## نصب نسخههای قدیمی (توصیه نمیشود)
|
||||
|
||||
برای نصب نسخه خاصی از دستور زیر استفاده کنید. مثال برای نسخه `v1.7.9`:
|
||||
|
||||
```
|
||||
VERSION=v1.7.9 && bash <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/$VERSION/install.sh") $VERSION
|
||||
```
|
||||
|
||||
## گواهی SSL
|
||||
|
||||
<details>
|
||||
<summary>جزئیات گواهی SSL</summary>
|
||||
|
||||
### ACME
|
||||
|
||||
برای مدیریت گواهیهای SSL با استفاده از ACME:
|
||||
|
||||
1. اطمینان حاصل کنید دامنه شما به درستی به سرور متصل است.
|
||||
2. دستور `x-ui` را در ترمینال اجرا کرده و گزینه `مدیریت گواهی SSL` را انتخاب کنید.
|
||||
3. گزینههای زیر نمایش داده میشوند:
|
||||
|
||||
- **دریافت SSL:** دریافت گواهی SSL
|
||||
- **لغو:** لغو گواهیهای موجود
|
||||
- **تمدید اجباری:** تمدید اجباری گواهیها
|
||||
- **نمایش دامنههای موجود:** نمایش تمام دامنههای دارای گواهی
|
||||
- **تنظیم مسیر گواهی برای پنل:** تنظیم مسیر گواهی برای دامنه شما
|
||||
|
||||
### Certbot
|
||||
|
||||
نصب و استفاده از Certbot:
|
||||
|
||||
```sh
|
||||
apt-get install certbot -y
|
||||
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
|
||||
certbot renew --dry-run
|
||||
```
|
||||
|
||||
### Cloudflare
|
||||
|
||||
اسکریپت داخلی برای دریافت گواهی SSL از Cloudflare. نیازمند:
|
||||
|
||||
- ایمیل ثبتشده در Cloudflare
|
||||
- کلید API جهانی Cloudflare
|
||||
- دامنه باید از طریق Cloudflare به سرور متصل باشد
|
||||
|
||||
**دریافت کلید API جهانی Cloudflare:**
|
||||
|
||||
1. دستور `x-ui` را اجرا و گزینه `گواهی SSL کلادفلر` را انتخاب کنید.
|
||||
2. به لینک [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens) مراجعه کنید.
|
||||
3. روی "View Global API Key" کلیک کنید:
|
||||

|
||||
4. پس از احراز هویت، کلید API نمایش داده میشود:
|
||||

|
||||
|
||||
در هنگام استفاده، نام دامنه، ایمیل و کلید API را وارد کنید:
|
||||

|
||||
|
||||
</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" ;;
|
||||
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. نصب یا ارتقا:
|
||||
|
||||
```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>
|
||||
|
||||
## نصب با 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
|
||||
```
|
||||
|
||||
4. **بهروزرسانی:**
|
||||
|
||||
```sh
|
||||
cd 3x-ui
|
||||
docker compose down
|
||||
docker compose pull 3x-ui
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
5. **حذف:**
|
||||
|
||||
```sh
|
||||
docker stop 3x-ui
|
||||
docker rm 3x-ui
|
||||
cd --
|
||||
rm -r 3x-ui
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## تنظیمات Nginx
|
||||
<details>
|
||||
<summary>پیکربندی Reverse Proxy</summary>
|
||||
|
||||
#### Nginx Reverse Proxy
|
||||
```nginx
|
||||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Range $http_range;
|
||||
proxy_set_header If-Range $http_if_range;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://127.0.0.1:2053;
|
||||
}
|
||||
```
|
||||
|
||||
#### مسیر فرعی در Nginx
|
||||
- اطمینان حاصل کنید "URI Path" در تنظیمات پنل یکسان باشد.
|
||||
- `url` در تنظیمات پنل باید با `/` پایان یابد.
|
||||
|
||||
```nginx
|
||||
location /sub {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Range $http_range;
|
||||
proxy_set_header If-Range $http_if_range;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://127.0.0.1:2053;
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
## سیستمعاملهای توصیه شده
|
||||
|
||||
- Ubuntu 20.04+
|
||||
- Debian 11+
|
||||
- CentOS 8+
|
||||
- OpenEuler 22.03+
|
||||
- Fedora 36+
|
||||
- Arch Linux
|
||||
- Parch Linux
|
||||
- Manjaro
|
||||
- Armbian
|
||||
- AlmaLinux 8.0+
|
||||
- Rocky Linux 8+
|
||||
- Oracle Linux 8+
|
||||
- OpenSUSE Tubleweed
|
||||
- Amazon Linux 2023
|
||||
- Windows x64
|
||||
|
||||
## معماریها و دستگاههای پشتیبانی شده
|
||||
|
||||
<details>
|
||||
<summary>جزئیات معماریها و دستگاهها</summary>
|
||||
|
||||
- **amd64**: معماری استاندارد برای کامپیوترهای شخصی و سرورها
|
||||
- **x86 / i386**: سیستمهای دسکتاپ و لپتاپ
|
||||
- **armv8 / arm64 / aarch64**: دستگاههای موبایل و embedded مانند Raspberry Pi 4
|
||||
- **armv7 / arm / arm32**: دستگاههای قدیمی مانند Orange Pi Zero
|
||||
- **armv6 / arm / arm32**: دستگاههای بسیار قدیمی مانند Raspberry Pi 1
|
||||
- **armv5 / arm / arm32**: سیستمهای embedded قدیمی
|
||||
- **s390x**: کامپیوترهای IBM mainframe
|
||||
</details>
|
||||
|
||||
## زبانهای پشتیبانی شده
|
||||
|
||||
- انگلیسی
|
||||
- فارسی
|
||||
- چینی سنتی
|
||||
- چینی سادهشده
|
||||
- ژاپنی
|
||||
- روسی
|
||||
- ویتنامی
|
||||
- اسپانیایی
|
||||
- اندونزیایی
|
||||
- اوکراینی
|
||||
- ترکی
|
||||
- پرتغالی (برزیل)
|
||||
|
||||
## ویژگیها
|
||||
|
||||
- مانیتورینگ وضعیت سیستم
|
||||
- جستجو در بین inboundها و کلاینتها
|
||||
- تم تاریک/روشن
|
||||
- پشتیبانی از چند کاربر و پروتکل
|
||||
- پروتکلهای VMESS، VLESS، Trojan، Shadowsocks، Dokodemo-door، Socks، HTTP، WireGuard
|
||||
- پشتیبانی از XTLS شامل RPRX-Direct، Vision، REALITY
|
||||
- آمار ترافیک، محدودیت ترافیک، محدودیت زمانی
|
||||
- تنظیمات سفارشی Xray
|
||||
- پشتیبانی از HTTPS برای پنل
|
||||
- دریافت خودکار گواهی SSL
|
||||
- مسیرهای API اصلاح شده
|
||||
- پشتیبانی از تغییر تنظیمات از طریق پنل
|
||||
- امکان export/import دیتابیس
|
||||
|
||||
## تنظیمات پیشفرض پنل
|
||||
|
||||
<details>
|
||||
<summary>جزئیات تنظیمات پیشفرض</summary>
|
||||
|
||||
### نام کاربری، رمز عبور، پورت و مسیر وب
|
||||
|
||||
در صورت عدم تغییر، این موارد به صورت تصادفی ایجاد میشوند (به جز Docker).
|
||||
|
||||
**تنظیمات پیشفرض Docker:**
|
||||
- **نام کاربری:** admin
|
||||
- **رمز عبور:** admin
|
||||
- **پورت:** 2053
|
||||
|
||||
### مدیریت دیتابیس:
|
||||
|
||||
امکان Backup و Restore دیتابیس از طریق پنل.
|
||||
|
||||
- **مسیر دیتابیس:**
|
||||
- `/etc/x-ui/x-ui.db`
|
||||
|
||||
### مسیر پایه وب
|
||||
|
||||
1. **بازنشانی مسیر:**
|
||||
- اجرای دستور `x-ui`
|
||||
- انتخاب گزینه `Reset Web Base Path`
|
||||
|
||||
2. **ساخت یا تنظیم مسیر:**
|
||||
- مسیر به صورت تصادفی ساخته شده یا قابل تنظیم است
|
||||
|
||||
3. **مشاهده تنظیمات فعلی:**
|
||||
- استفاده از دستور `x-ui settings` یا `View Current Settings` در `x-ui`
|
||||
|
||||
**توصیه امنیتی:**
|
||||
- استفاده از مسیرهای طولانی و تصادفی برای افزایش امنیت
|
||||
|
||||
**مثال:**
|
||||
- `http://ip:port/*webbasepath*/panel`
|
||||
- `http://domain:port/*webbasepath*/panel`
|
||||
|
||||
</details>
|
||||
|
||||
## پیکربندی WARP
|
||||
|
||||
<details>
|
||||
<summary>جزئیات WARP</summary>
|
||||
|
||||
#### استفاده
|
||||
|
||||
**برای نسخههای `v2.1.0` و جدیدتر:**
|
||||
|
||||
WARP به صورت داخلی پشتیبانی میشود. تنها نیاز به فعالسازی در پنل است.
|
||||
|
||||
</details>
|
||||
|
||||
## محدودیت IP
|
||||
|
||||
<details>
|
||||
<summary>جزئیات محدودیت IP</summary>
|
||||
|
||||
#### استفاده
|
||||
|
||||
**توجه:** محدودیت IP در صورت استفاده از IP Tunnel کار نمیکند.
|
||||
|
||||
- **تا نسخه `v1.6.1`:**
|
||||
- محدودیت IP به صورت داخلی در پنل وجود دارد
|
||||
|
||||
**برای نسخههای `v1.7.0` و جدیدتر:**
|
||||
|
||||
برای فعالسازی نیاز به نصب `fail2ban` است:
|
||||
|
||||
1. اجرای دستور `x-ui` و انتخاب `مدیریت محدودیت IP`
|
||||
2. گزینههای موجود:
|
||||
|
||||
- **تغییر مدت زمان Ban**
|
||||
- **حذف تمام Banها**
|
||||
- **مشاهده لاگها**
|
||||
- **وضعیت Fail2ban**
|
||||
- **راهاندازی مجدد Fail2ban**
|
||||
- **حذف Fail2ban**
|
||||
|
||||
3. تنظیم مسیر `Access log` در پنل به `./access.log` و ذخیره و راهاندازی مجدد Xray
|
||||
|
||||
- **قبل از نسخه `v2.1.3`:**
|
||||
- تنظیم دستی `access.log` در تنظیمات Xray:
|
||||
|
||||
```sh
|
||||
"log": {
|
||||
"access": "./access.log",
|
||||
"dnsLog": false,
|
||||
"loglevel": "warning"
|
||||
},
|
||||
```
|
||||
|
||||
- **از نسخه `v2.1.3`:**
|
||||
- امکان تنظیم `access.log` از طریق پنل
|
||||
|
||||
</details>
|
||||
|
||||
## ربات تلگرام
|
||||
|
||||
<details>
|
||||
<summary>جزئیات ربات تلگرام</summary>
|
||||
|
||||
#### استفاده
|
||||
|
||||
ربات تلگرام برای اطلاعرسانی ترافیک، ورود به پنل، Backup دیتابیس و ... استفاده میشود. نیازمند تنظیم:
|
||||
|
||||
- توکن تلگرام
|
||||
- Chat ID ادمینها
|
||||
- زمان اطلاعرسانی (Cron syntax)
|
||||
- اطلاعرسانی انقضا
|
||||
- اطلاعرسانی ترافیک
|
||||
- Backup دیتابیس
|
||||
- اطلاعرسانی مصرف CPU
|
||||
|
||||
**سینتکس نمونه:**
|
||||
|
||||
- `30 \* \* \* \* \*` - اطلاع در ثانیه 30 هر دقیقه
|
||||
- `@hourly` - هر ساعت
|
||||
- `@daily` - هر روز
|
||||
|
||||
### ویژگیهای ربات
|
||||
|
||||
- گزارش دورهای
|
||||
- اطلاع ورود به پنل
|
||||
- اطلاع مصرف CPU
|
||||
- اطلاع پیشاز موعد انقضا و ترافیک
|
||||
- گزارش ترافیک کلاینتها
|
||||
- منوی مبتنی بر دستور
|
||||
- جستجوی کلاینت بر اساس ایمیل
|
||||
- بررسی inboundها
|
||||
- بررسی وضعیت سرور
|
||||
- دریافت Backup
|
||||
- چندزبانه
|
||||
|
||||
### راهاندازی ربات
|
||||
|
||||
- شروع [Botfather](https://t.me/BotFather) در تلگرام:
|
||||

|
||||
|
||||
- ساخت ربات جدید با دستور /newbot:
|
||||

|
||||
|
||||
- شروع ربات ساخته شده:
|
||||

|
||||
|
||||
- تنظیمات پنل:
|
||||

|
||||
|
||||
وارد کردن توکن و Chat ID (دریافت از [این ربات](https://t.me/useridinfobot)):
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## مسیرهای API
|
||||
|
||||
<details>
|
||||
<summary>جزئیات API</summary>
|
||||
|
||||
#### استفاده
|
||||
|
||||
- [مستندات API](https://www.postman.com/hsanaei/3x-ui/collection/q1l5l0u/3x-ui)
|
||||
- `/login` با `POST` داده کاربر: `{username: '', password: ''}`
|
||||
|
||||
| Method | مسیر | عملکرد |
|
||||
| :----: | ---------------------------------- | ------------------------------------------- |
|
||||
| `GET` | `"/list"` | دریافت تمام inboundها |
|
||||
| `GET` | `"/get/:id"` | دریافت inbound بر اساس id |
|
||||
| `POST` | `"/add"` | افزودن inbound |
|
||||
| `POST` | `"/del/:id"` | حذف inbound |
|
||||
|
||||
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5%26entityType%3Dcollection%26workspaceId%3Dd64f609f-485a-4951-9b8f-876b3f917124)
|
||||
</details>
|
||||
|
||||
## متغیرهای محیطی
|
||||
|
||||
<details>
|
||||
<summary>جزئیات متغیرها</summary>
|
||||
|
||||
#### استفاده
|
||||
|
||||
| متغیر | نوع | پیشفرض |
|
||||
| ------------- | :--------------------------------------------: | :------------ |
|
||||
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
|
||||
| XUI_DEBUG | `boolean` | `false` |
|
||||
| XUI_BIN_FOLDER| `string` | `"bin"` |
|
||||
|
||||
مثال:
|
||||
|
||||
```sh
|
||||
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## پیشنمایش
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/01-overview-dark.png">
|
||||
<img alt="3x-ui" src="./media/01-overview-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/02-inbounds-dark.png">
|
||||
<img alt="3x-ui" src="./media/02-inbounds-light.png">
|
||||
</picture>
|
||||
|
||||
## قدردانی ویژه از
|
||||
|
||||
- [alireza0](https://github.com/alireza0/)
|
||||
|
||||
## تشکر و قدردانی
|
||||
|
||||
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (مجوز: **GPL-3.0**)
|
||||
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (مجوز: **GPL-3.0**)
|
||||
|
||||
## Stargazers over Time
|
||||
|
||||
[](https://starchart.cc/MHSanaei/3x-ui)
|
||||
626
README.md
@@ -1,144 +1,596 @@
|
||||
# 3x-ui
|
||||
[English](/README.md) | [فارسی](/README.fa_IR.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>
|
||||
|
||||
**An Advanced Web Panel • Built on Xray Core**
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](#)
|
||||
[](#)
|
||||
[](#)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
> **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
|
||||
|
||||
> **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**
|
||||
**If this project is helpful to you, you may wish to give it a**:star2:
|
||||
|
||||
xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)**
|
||||
<p align="left">
|
||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
||||
<img src="./media/buymeacoffe.png" alt="Image">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# Install & Upgrade
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
|
||||
## Install & Upgrade
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
|
||||
```
|
||||
## Install custom version
|
||||
To install your desired version you can add the version to the end of install command. Example for ver `v1.0.9`:
|
||||
|
||||
## Install legacy Version (we don't recommend)
|
||||
|
||||
To install your desired version, use following installation command. e.g., ver `v1.7.9`:
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.0.9
|
||||
```
|
||||
# SSL
|
||||
VERSION=v1.7.9 && bash <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/$VERSION/install.sh") $VERSION
|
||||
```
|
||||
|
||||
## 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.
|
||||
- **Show Existing Domains:** Display all domain certificates available on the server.
|
||||
- **Set Certificate Paths for the Panel:** Specify the certificate for your domain to be used by the panel.
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
**If you think this project is helpful to you, you may wish to give a** :star2:
|
||||
### Cloudflare
|
||||
|
||||
# Default settings
|
||||
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:
|
||||
|
||||
- Port: 2053
|
||||
- username and password will be generated randomly if you skip to modify your own security(x-ui "7")
|
||||
- database path: /etc/x-ui/x-ui.db
|
||||
- xray config path: /usr/local/x-ui/bin/config.json
|
||||
- Cloudflare registered email
|
||||
- Cloudflare Global API Key
|
||||
- The domain name must be resolved to the current server through Cloudflare
|
||||
|
||||
before you set ssl on settings
|
||||
- http:// ip or domain:2053/xui
|
||||
**How to get the Cloudflare Global API Key:**
|
||||
|
||||
After you set ssl on settings
|
||||
- https://yourdomain:2053/xui
|
||||
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):
|
||||

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

|
||||
|
||||
**copy and paste to xray Configuration :** (you don't need to do this if you have a fresh install)
|
||||
- [enable traffic](./media/enable-traffic.txt)
|
||||
- [enable traffic+block all IR IP address](./media/enable-traffic+block-IR-IP.txt)
|
||||
- [enable traffic+block all IR domain](./media/enable-traffic+block-IR-domain.txt)
|
||||
|
||||
# Features
|
||||
</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
|
||||
```
|
||||
|
||||
Add ```--pull always``` flag to make docker automatically recreate container if a newer image is pulled. See https://docs.docker.com/reference/cli/docker/container/run/#pull for more info.
|
||||
|
||||
**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>
|
||||
|
||||
## Nginx Settings
|
||||
<details>
|
||||
<summary>Click for Reverse Proxy Configuration</summary>
|
||||
|
||||
#### Nginx Reverse Proxy
|
||||
```nginx
|
||||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Range $http_range;
|
||||
proxy_set_header If-Range $http_if_range;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://127.0.0.1:2053;
|
||||
}
|
||||
```
|
||||
|
||||
#### Nginx sub-path
|
||||
- Ensure that the "URI Path" in the `/sub` panel settings is the same.
|
||||
- The `url` in the panel settings needs to end with `/`.
|
||||
|
||||
```nginx
|
||||
location /sub {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Range $http_range;
|
||||
proxy_set_header If-Range $http_if_range;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://127.0.0.1:2053;
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
## Recommended OS
|
||||
|
||||
- Ubuntu 20.04+
|
||||
- Debian 11+
|
||||
- CentOS 8+
|
||||
- OpenEuler 22.03+
|
||||
- Fedora 36+
|
||||
- Arch Linux
|
||||
- Parch Linux
|
||||
- Manjaro
|
||||
- Armbian
|
||||
- AlmaLinux 8.0+
|
||||
- Rocky Linux 8+
|
||||
- Oracle Linux 8+
|
||||
- OpenSUSE Tubleweed
|
||||
- Amazon Linux 2023
|
||||
- Windows x64
|
||||
|
||||
## 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
|
||||
- Persian
|
||||
- Traditional Chinese
|
||||
- Simplified Chinese
|
||||
- Japanese
|
||||
- Russian
|
||||
- Vietnamese
|
||||
- Spanish
|
||||
- Indonesian
|
||||
- Ukrainian
|
||||
- Turkish
|
||||
- Português (Brazil)
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- System Status Monitoring
|
||||
- Search within all inbounds and clients
|
||||
- Support Dark/Light theme UI
|
||||
- Support multi-user multi-protocol, web page visualization operation
|
||||
- Supported protocols: vmess, vless, trojan, shadowsocks, dokodemo-door, socks, http
|
||||
- Support for configuring more transport configurations
|
||||
- Traffic statistics, limit traffic, limit expiration time
|
||||
- Customizable xray configuration templates
|
||||
- Support https access panel (self-provided domain name + ssl certificate)
|
||||
- Support one-click SSL certificate application and automatic renewal
|
||||
- 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
|
||||
- fix api routes (user setting will create with api)
|
||||
- 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
|
||||
|
||||
# Tg robot use
|
||||
|
||||
X-UI supports daily traffic notification, panel login reminder and other functions through the Tg robot. To use the Tg robot, you need to apply for the specific application tutorial. You can refer to the [blog](https://coderfan.net/how-to-use-telegram-bot-to-alarm-you-when-someone-login-into-your-vps.html)
|
||||
Set the robot-related parameters in the panel background, including:
|
||||
## Default Panel Settings
|
||||
|
||||
- Tg robot Token
|
||||
- Tg robot ChatId
|
||||
- Tg robot cycle runtime, in crontab syntax
|
||||
- Tg robot Expiration threshold
|
||||
- Tg robot Traffic threshold
|
||||
- Tg robot Enable send backup in cycle runtime
|
||||
- Tg robot Enable CPU usage alarm threshold
|
||||
<details>
|
||||
<summary>Click for default settings details</summary>
|
||||
|
||||
Reference syntax:
|
||||
### Username, Password, Port, and Web Base Path
|
||||
|
||||
- 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)
|
||||
- @every 8h // notify every 8 hours
|
||||
If you choose not to modify these settings, they will be generated randomly (this does not apply to Docker).
|
||||
|
||||
# Telegram Bot Features
|
||||
**Default Settings for Docker:**
|
||||
- **Username:** admin
|
||||
- **Password:** admin
|
||||
- **Port:** 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.
|
||||
|
||||
</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 UID (VMESS/VLESS) or Password (TROJAN) - anonymously
|
||||
- Menu based bot
|
||||
- Search client by email ( only admin )
|
||||
- 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
|
||||
|
||||
## API routes
|
||||
- Start [Botfather](https://t.me/BotFather) in your Telegram account:
|
||||

|
||||
|
||||
- `/login` with `PUSH` user data: `{username: '', password: ''}` for login
|
||||
- `/xui/API/inbounds` base for following actions:
|
||||
- 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".
|
||||

|
||||
|
||||
| Method | Path | Action |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| GET | "/list" | Get all inbounds |
|
||||
| GET | "/get/:id" | Get inbound with inbound.id |
|
||||
| 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 | "/delClient/:email" | Delete Client |
|
||||
| POST | "/updateClient/:index" | Update Client |
|
||||
| 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 |
|
||||
- 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
|
||||
|
||||
- [API Documentation](https://www.postman.com/hsanaei/3x-ui/collection/q1l5l0u/3x-ui)
|
||||
- `/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` | `"/getClientTrafficsById/:id"` | Get client's traffic By ID |
|
||||
| `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
|
||||
|
||||
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5%26entityType%3Dcollection%26workspaceId%3Dd64f609f-485a-4951-9b8f-876b3f917124)
|
||||
</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
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/01-overview-dark.png">
|
||||
<img alt="3x-ui" src="./media/01-overview-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/02-inbounds-dark.png">
|
||||
<img alt="3x-ui" src="./media/02-inbounds-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/03-add-inbound-dark.png">
|
||||
<img alt="3x-ui" src="./media/03-add-inbound-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/04-add-client-dark.png">
|
||||
<img alt="3x-ui" src="./media/04-add-client-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/05-settings-dark.png">
|
||||
<img alt="3x-ui" src="./media/05-settings-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/06-configs-dark.png">
|
||||
<img alt="3x-ui" src="./media/06-configs-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/07-bot-dark.png">
|
||||
<img alt="3x-ui" src="./media/07-bot-light.png">
|
||||
</picture>
|
||||
|
||||
## A Special Thanks to
|
||||
|
||||
# A Special Thanks To
|
||||
- [alireza0](https://github.com/alireza0/)
|
||||
- [FranzKafkaYu](https://github.com/FranzKafkaYu)
|
||||
|
||||
# Suggestion System
|
||||
- Ubuntu 20.04+
|
||||
- Debian 10+
|
||||
- CentOS 8+
|
||||
- Fedora 36+
|
||||
## Acknowledgment
|
||||
|
||||
# Pictures
|
||||
- [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._
|
||||
- [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._
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
## Stargazers over Time
|
||||
|
||||
## Stargazers over time
|
||||
|
||||
[](https://starchart.cc/MHSanaei/3x-ui)
|
||||
[](https://starchart.cc/MHSanaei/3x-ui)
|
||||
594
README.ru_RU.md
Normal file
@@ -0,0 +1,594 @@
|
||||
[English](/README.md) | [فارسی](/README.fa_IR.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>
|
||||
|
||||
**Продвинутая веб-панель • Построена на основе Xray Core**
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](#)
|
||||
[](#)
|
||||
[](#)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
> **Отказ от ответственности:** Этот проект предназначен только для личного обучения и общения. Пожалуйста, не используйте его в незаконных целях и не применяйте в производственной среде.
|
||||
|
||||
**Если этот проект оказался полезным для вас, вы можете оценить его, поставив звёздочку** :star2:
|
||||
|
||||
<p align="left">
|
||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
||||
<img src="./media/buymeacoffe.png" 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)
|
||||
```
|
||||
|
||||
## Установить старую версию (мы не рекомендуем)
|
||||
|
||||
Чтобы установить желаемую версию, используйте следующую команду установки. Например, ver `v1.7.9`:
|
||||
|
||||
```
|
||||
VERSION=v1.7.9 && <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/$VERSION/install.sh") $VERSION
|
||||
```
|
||||
|
||||
## SSL Сертификат
|
||||
|
||||
<details>
|
||||
<summary>Нажмите для получения информации об SSL сертификате</summary>
|
||||
|
||||
### ACME
|
||||
|
||||
Для управления SSL сертификатами с помощью ACME:
|
||||
|
||||
1. Убедитесь, что ваш домен правильно настроен и указывает на сервер.
|
||||
2. Выполните команду `x-ui` в терминале, затем выберите `SSL Certificate Management`.
|
||||
3. Вам будут предложены следующие опции:
|
||||
|
||||
- **Get SSL:** Получить SSL сертификаты.
|
||||
- **Revoke:** Отозвать существующие SSL сертификаты.
|
||||
- **Force Renew:** Принудительно перевыпустить SSL сертификаты.
|
||||
- **Show Existing Domains:** Отобразить все сертификаты доменов, доступные на сервере.
|
||||
- **Set Certificate Paths for the Panel:** Укажите сертификат для вашего домена, который будет использоваться панелью.
|
||||
|
||||
### Certbot
|
||||
|
||||
Для установки и использования Certbot:
|
||||
|
||||
```sh
|
||||
apt-get install certbot -y
|
||||
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d вашдомен.com
|
||||
certbot renew --dry-run
|
||||
```
|
||||
|
||||
### Cloudflare
|
||||
|
||||
Скрипт управления включает встроенное приложение для получения SSL сертификата через Cloudflare. Чтобы использовать этот скрипт для запроса сертификата, вам потребуется следующее:
|
||||
|
||||
- Email, зарегистрированный в Cloudflare
|
||||
- Глобальный API-ключ Cloudflare
|
||||
- Доменное имя должно указывать на текущий сервер через Cloudflare
|
||||
|
||||
**Как получить глобальный API-ключ Cloudflare:**
|
||||
|
||||
1. Выполните команду `x-ui` в терминале, затем выберите `Cloudflare SSL Certificate`.
|
||||
2. Перейдите по ссылке: [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens).
|
||||
3. Нажмите на "View Global API Key" (см. скриншот ниже):
|
||||

|
||||
4. Возможно, вам потребуется повторно пройти аутентификацию. После этого ключ API будет отображён (см. скриншот ниже):
|
||||

|
||||
|
||||
При использовании просто введите ваше `доменное имя`, `email` и `API-ключ`. Схема приведена ниже:
|
||||

|
||||
|
||||
</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" ;;
|
||||
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. После загрузки архива выполните следующие команды для установки или обновления 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>
|
||||
|
||||
## Установка с помощью 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
|
||||
```
|
||||
|
||||
Добавьте параметр ```--pull always``` для автоматического обновления контейнера, когда публикуется новый образ. Подробности: https://docs.docker.com/reference/cli/docker/container/run/#pull
|
||||
|
||||
**ИЛИ**
|
||||
|
||||
```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. **Обновление до последней версии:**
|
||||
|
||||
```sh
|
||||
cd 3x-ui
|
||||
docker compose down
|
||||
docker compose pull 3x-ui
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
5. **Удаление 3x-ui из Docker:**
|
||||
|
||||
```sh
|
||||
docker stop 3x-ui
|
||||
docker rm 3x-ui
|
||||
cd --
|
||||
rm -r 3x-ui
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Настройки Nginx
|
||||
<details>
|
||||
<summary>Нажмите чтобы просмотреть конфигурацию обратного прокси-сервера</summary>
|
||||
|
||||
#### Обратный прокси-сервер Nginx
|
||||
```nginx
|
||||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Range $http_range;
|
||||
proxy_set_header If-Range $http_if_range;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://127.0.0.1:2053;
|
||||
}
|
||||
```
|
||||
|
||||
#### Nginx sub-path
|
||||
- Убедитесь, что "корневой путь URL адреса панели" в настройках панели и `/sub` совпадают.
|
||||
- В настройках панели `url` должен заканчиваться на `/`.
|
||||
|
||||
```nginx
|
||||
location /sub {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Range $http_range;
|
||||
proxy_set_header If-Range $http_if_range;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://127.0.0.1:2053;
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
## Рекомендуемые ОС
|
||||
|
||||
- Ubuntu 20.04+
|
||||
- Debian 11+
|
||||
- CentOS 8+
|
||||
- OpenEuler 22.03+
|
||||
- Fedora 36+
|
||||
- Arch Linux
|
||||
- Parch Linux
|
||||
- Manjaro
|
||||
- Armbian
|
||||
- AlmaLinux 8.0+
|
||||
- Rocky Linux 8+
|
||||
- Oracle Linux 8+
|
||||
- OpenSUSE Tubleweed
|
||||
- Amazon Linux 2023
|
||||
- Windows x64
|
||||
|
||||
## Поддерживаемые архитектуры и устройства
|
||||
|
||||
<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 и некоторые старые смартфоны.
|
||||
|
||||
- **s390x**: Эта архитектура обычно используется в мейнфреймах IBM и обеспечивает высокую производительность и надежность для корпоративных рабочих нагрузок.
|
||||
</details>
|
||||
|
||||
## Языки
|
||||
|
||||
- English (английский)
|
||||
- Persian (персидский)
|
||||
- Traditional Chinese (традиционный китайский)
|
||||
- Simplified Chinese (упрощенный китайский)
|
||||
- Japanese (японский)
|
||||
- Russian (русский)
|
||||
- Vietnamese (вьетнамский)
|
||||
- Spanish (испанский)
|
||||
- Indonesian (индонезийский)
|
||||
- Ukrainian (украинский)
|
||||
- Turkish (турецкий)
|
||||
- Português (Brazil) (португальский (Бразилия))
|
||||
|
||||
## Возможности
|
||||
|
||||
- Мониторинг состояния системы
|
||||
- Поиск по всем входящим подключениям и клиентам
|
||||
- Тёмная/светлая тема
|
||||
- Поддержка нескольких пользователей и протоколов
|
||||
- Поддержка протоколов, включая VMESS, VLESS, Trojan, Shadowsocks, Dokodemo-door, Socks, HTTP, WireGuard
|
||||
- Поддержка протоколов XTLS, включая RPRX-Direct, Vision, REALITY
|
||||
- Статистика трафика, ограничение трафика, ограничение по времени истечения
|
||||
- Настраиваемые шаблоны конфигурации Xray
|
||||
- Поддержка HTTPS доступа к панели (ваше доменное имя + SSL сертификат)
|
||||
- Поддержка установки SSL-сертификата в один клик и автоматического перевыпуска
|
||||
- Для получения более продвинутых настроек обращайтесь к панели
|
||||
- Исправляет маршруты API (настройка пользователя будет создана через API)
|
||||
- Поддержка изменения конфигураций по различным элементам, предоставленным в панели
|
||||
- Поддержка экспорта/импорта базы данных из панели
|
||||
|
||||
## Настройки панели по умолчанию
|
||||
|
||||
<details>
|
||||
<summary>Нажмите для получения информации о настройках по умолчанию</summary>
|
||||
|
||||
### Имя пользователя, Пароль, Порт и Web Base Path
|
||||
|
||||
Если вы не измените эти настройки, они будут сгенерированы случайным образом (это не относится к Docker).
|
||||
|
||||
**Настройки по умолчанию для Docker:**
|
||||
- **Имя пользователя:** admin
|
||||
- **Пароль:** admin
|
||||
- **Порт:** 2053
|
||||
|
||||
### Управление базой данных:
|
||||
|
||||
Вы можете удобно выполнять резервное копирование и восстановление базы данных прямо из панели.
|
||||
|
||||
- **Путь к базе данных:**
|
||||
- `/etc/x-ui/x-ui.db`
|
||||
|
||||
### Webbasepath
|
||||
|
||||
1. **Сбросить webbasepath:**
|
||||
- Откройте терминал.
|
||||
- Выполните команду `x-ui`.
|
||||
- Выберите опцию `Reset Web Base Path`.
|
||||
|
||||
2. **Генерация или настройка пути:**
|
||||
- Путь будет сгенерирован случайным образом, или вы можете ввести собственный путь.
|
||||
|
||||
3. **Просмотр текущих настроек:**
|
||||
- Чтобы просмотреть текущие настройки, используйте команду `x-ui settings` в терминале или опцию `View Current Settings` в `x-ui`.
|
||||
|
||||
### Рекомендации по безопасности:
|
||||
- Для повышения безопасности используйте длинное случайное слово в структуре вашего URL.
|
||||
|
||||
**Примеры:**
|
||||
- `http://ip_адрес:порт/*webbasepath*/panel`
|
||||
- `http://домен:порт/*webbasepath*/panel`
|
||||
|
||||
</details>
|
||||
|
||||
## Настройка WARP
|
||||
|
||||
<details>
|
||||
<summary>Нажмите для получения информации о настройке WARP</summary>
|
||||
|
||||
#### Использование
|
||||
|
||||
**Для версий `v2.1.0` и новее:**
|
||||
|
||||
WARP встроен, и дополнительная установка не требуется. Просто включите необходимую конфигурацию в панели.
|
||||
|
||||
</details>
|
||||
|
||||
## Ограничение IP
|
||||
|
||||
<details>
|
||||
<summary>Нажмите для получения информации об ограничении IP</summary>
|
||||
|
||||
#### Использование
|
||||
|
||||
**Примечание:** Ограничение IP не будет работать корректно при использовании IP Tunnel.
|
||||
|
||||
- **Для версий до `v1.6.1`:**
|
||||
- Ограничение IP встроено в панель.
|
||||
|
||||
**Для версий `v1.7.0` и новее:**
|
||||
|
||||
Чтобы включить функциональность ограничения IP, вам нужно установить `fail2ban` и его необходимые файлы, выполнив следующие шаги:
|
||||
|
||||
1. Выполните команду `x-ui` в терминале, затем выберите `IP Limit Management`.
|
||||
2. Вам будут предложены следующие опции:
|
||||
|
||||
- **Change Ban Duration:** Отрегулировать длительность блокировок.
|
||||
- **Unban Everyone:** Снять все текущие блокировки.
|
||||
- **Check Logs:** Просмотреть логи.
|
||||
- **Fail2ban Status:** Проверить статус `fail2ban`.
|
||||
- **Restart Fail2ban:** Перезапустить службу `fail2ban`.
|
||||
- **Uninstall Fail2ban:** Удалить Fail2ban с его конфигурацией.
|
||||
|
||||
3. Добавьте путь к логам доступа в панели, установив `Xray Configs/log/Access log` в `./access.log`, затем сохраните и перезапустите xray.
|
||||
|
||||
- **Для версий до `v2.1.3`:**
|
||||
- Вам нужно вручную установить путь к логам доступа в вашей конфигурации Xray:
|
||||
|
||||
```sh
|
||||
"log": {
|
||||
"access": "./access.log",
|
||||
"dnsLog": false,
|
||||
"loglevel": "warning"
|
||||
},
|
||||
```
|
||||
|
||||
- **Для версий `v2.1.3` и новее:**
|
||||
- Есть возможность настройки `access.log` непосредственно из панели.
|
||||
|
||||
</details>
|
||||
|
||||
## Телеграм-бот
|
||||
|
||||
<details>
|
||||
<summary>Нажмите для получения информации о телеграм-боте</summary>
|
||||
|
||||
#### Использование
|
||||
|
||||
Веб-панель поддерживает уведомления и функции, такие как ежедневный трафик, вход в панель, резервное копирование базы данных, состояние системы, информация о клиентах и другие, через телеграм-бота. Чтобы использовать бота, вам нужно настроить параметры, связанные с ботом, в панели, включая:
|
||||
|
||||
- Токен Telegram
|
||||
- ID чата админа(-ов)
|
||||
- Время уведомлений (в синтаксисе cron)
|
||||
- Уведомления о дате истечения
|
||||
- Уведомления о лимите трафика
|
||||
- Резервное копирование базы данных
|
||||
- Уведомления о загрузке CPU
|
||||
|
||||
**Примеры синтаксиса:**
|
||||
|
||||
- `30 * * * * *` - Уведомлять на 30-й секунде каждого часа
|
||||
- `0 */10 * * * *` - Уведомлять на первой секунде каждых 10 минут
|
||||
- `@hourly` - Ежечасное уведомление
|
||||
- `@daily` - Ежедневное уведомление (в 00:00)
|
||||
- `@weekly` - Еженедельное уведомление
|
||||
- `@every 8h` - Уведомлять каждые 8 часов
|
||||
|
||||
### Возможности телеграм-бота
|
||||
|
||||
- Периодические отчеты
|
||||
- Уведомления о входе
|
||||
- Уведомления о пороге загруженности процессора
|
||||
- Уведомления о времени истечения и трафике заранее
|
||||
- Поддерживает меню отчетов клиента, если имя пользователя телеграм клиента добавлено в конфигурации пользователя
|
||||
- Поддержка отчета о трафике через Telegram, поиск по UUID (VMESS/VLESS) или паролю (TROJAN) - анонимно
|
||||
- Бот, основанный на меню
|
||||
- Поиск клиента по email (только администратор)
|
||||
- Проверка всех входящих соединений
|
||||
- Проверка состояния сервера
|
||||
- Проверка истекших пользователей
|
||||
- Получение резервных копий по запросу и в периодических отчётах
|
||||
- Многоязычный бот
|
||||
|
||||
### Настройка телеграм-бота
|
||||
|
||||
- Запустите [Botfather](https://t.me/BotFather) в вашем аккаунте Telegram:
|
||||

|
||||
|
||||
- Создайте нового бота с помощью команды /newbot: у вас спросят 2 вопроса: отображаемое имя и имя пользователя для вашего бота. Обратите внимание, что имя пользователя должно заканчиваться на слово "bot".
|
||||

|
||||
|
||||
- Запустите созданного бота. Ссылку на вашего бота можно найти здесь.
|
||||

|
||||
|
||||
- Перейдите в панель и настройте параметры телеграм-бота следующим образом:
|
||||

|
||||
|
||||
Введите токен вашего бота в поле ввода номер 3.
|
||||
Введите ID пользователя в поле ввода номер 4. Telegram-аккаунты с этим ID будут администраторами бота. (Вы можете ввести несколько ID, разделяя их запятой)
|
||||
|
||||
- Как получить ID пользователя Telegram? Используйте этот [бот](https://t.me/useridinfobot). Запустите бота, и он отобразит ваш ID пользователя Telegram.
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## Маршруты API
|
||||
|
||||
<details>
|
||||
<summary>Нажмите для получения информации о маршрутах API</summary>
|
||||
|
||||
#### Использование
|
||||
|
||||
- [API документация](https://www.postman.com/hsanaei/3x-ui/collection/q1l5l0u/3x-ui)
|
||||
- `/login` с `POST`-данными: `{username: '', password: ''}` для входа
|
||||
- `/panel/api/inbounds` это базовый путь для следующих действий:
|
||||
|
||||
| Метод | Путь | Действие
|
||||
| :----: | -----------------------------------| -------------------------------------------
|
||||
| `GET` | `"/list"` | Получить все входящие соединения
|
||||
| `GET` | `"/get/:id"` | Получить входящее соединение с inbound.id
|
||||
| `GET` | `"/getClientTraffics/:email"` | Получить трафик клиента по email
|
||||
| `GET` | `"/getClientTrafficsById/:id"` | Получить трафик клиента по ID
|
||||
| `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: всех)
|
||||
| `POST` | `"/onlines"` | Получить пользователей, которые находятся онлайн (список email'ов)
|
||||
|
||||
\*- Поле `clientId` должно быть заполнено следующим образом:
|
||||
|
||||
- `client.id` для VMESS и VLESS
|
||||
- `client.password` для TROJAN
|
||||
- `client.email` для Shadowsocks
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5%26entityType%3Dcollection%26workspaceId%3Dd64f609f-485a-4951-9b8f-876b3f917124)
|
||||
</details>
|
||||
|
||||
## Переменные среды
|
||||
|
||||
<details>
|
||||
<summary>Нажмите для получения информации о переменных среды</summary>
|
||||
|
||||
#### Использование
|
||||
|
||||
| Переменная | Тип | Значение по умолчанию |
|
||||
| ---------------- | :------------------------------------------: | :-------------------- |
|
||||
| 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>
|
||||
|
||||
## Предварительный Просмотр
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/01-overview-dark.png">
|
||||
<img alt="3x-ui" src="./media/01-overview-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/02-inbounds-dark.png">
|
||||
<img alt="3x-ui" src="./media/02-inbounds-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/03-add-inbound-dark.png">
|
||||
<img alt="3x-ui" src="./media/03-add-inbound-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/04-add-client-dark.png">
|
||||
<img alt="3x-ui" src="./media/04-add-client-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/05-settings-dark.png">
|
||||
<img alt="3x-ui" src="./media/05-settings-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/06-configs-dark.png">
|
||||
<img alt="3x-ui" src="./media/06-configs-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/07-bot-dark.png">
|
||||
<img alt="3x-ui" src="./media/07-bot-light.png">
|
||||
</picture>
|
||||
|
||||
## Особая благодарность
|
||||
|
||||
- [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._
|
||||
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (License: **GPL-3.0**): _Этот репозиторий содержит автоматически обновляемые правила маршрутизации V2Ray на основе данных о заблокированных доменах и адресах в России._
|
||||
|
||||
## Число звёзд со временем
|
||||
|
||||
[](https://starchart.cc/MHSanaei/3x-ui)
|
||||
587
README.zh_CN.md
Normal file
@@ -0,0 +1,587 @@
|
||||
[English](/README.md) | [فارسی](/README.fa_IR.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>
|
||||
|
||||
**一个更好的面板 • 基于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="https://buymeacoffee.com/mhsanaei" target="_blank">
|
||||
<img src="./media/buymeacoffe.png" 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)
|
||||
```
|
||||
|
||||
## 安装旧版本 (我们不建议)
|
||||
|
||||
要安装您想要的版本,请使用以下安装命令。例如,ver `v1.7.9`:
|
||||
|
||||
```
|
||||
VERSION=v1.7.9 && <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/$VERSION/install.sh") $VERSION
|
||||
```
|
||||
|
||||
### SSL证书
|
||||
|
||||
<details>
|
||||
<summary>点击查看SSL证书详情</summary>
|
||||
|
||||
### ACME
|
||||
|
||||
使用ACME管理SSL证书:
|
||||
|
||||
1. 确保您的域名正确解析到服务器。
|
||||
2. 在终端中运行 `x-ui` 命令,然后选择 `SSL证书管理`。
|
||||
3. 您将看到以下选项:
|
||||
|
||||
- **Get SSL:** 获取SSL证书。
|
||||
- **Revoke:** 吊销现有的SSL证书。
|
||||
- **Force Renew:** 强制更新SSL证书。
|
||||
- **Show Existing Domains:** 显示服务器上所有可用的域证书。
|
||||
- **Set Certificate Paths for the Panel:** 指定用于面板的域证书。
|
||||
|
||||
### Certbot
|
||||
|
||||
安装并使用Certbot:
|
||||
|
||||
```sh
|
||||
apt-get install certbot -y
|
||||
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
|
||||
certbot renew --dry-run
|
||||
```
|
||||
|
||||
### Cloudflare
|
||||
|
||||
管理脚本内置了Cloudflare的SSL证书申请。要使用此脚本申请证书,您需要以下信息:
|
||||
|
||||
- Cloudflare注册的电子邮件
|
||||
- Cloudflare全局API密钥
|
||||
- 域名必须通过Cloudflare解析到当前服务器
|
||||
|
||||
**如何获取Cloudflare全局API密钥:**
|
||||
|
||||
1. 在终端中运行 `x-ui` 命令,然后选择 `Cloudflare SSL证书`。
|
||||
2. 访问链接:[Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens)。
|
||||
3. 点击“查看全局API密钥”(参见下图):
|
||||

|
||||
4. 您可能需要重新验证您的账户。之后将显示API密钥(参见下图):
|
||||

|
||||
|
||||
使用时,只需输入您的 `域名`、`电子邮件` 和 `API密钥`。如下图所示:
|
||||

|
||||
|
||||
</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>
|
||||
|
||||
|
||||
## Nginx 设置
|
||||
<details>
|
||||
<summary>点击查看 反向代理配置</summary>
|
||||
|
||||
#### Nginx反向代理
|
||||
```nginx
|
||||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Range $http_range;
|
||||
proxy_set_header If-Range $http_if_range;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://127.0.0.1:2053;
|
||||
}
|
||||
```
|
||||
|
||||
#### Nginx子路径
|
||||
- 确保 `/sub` 面板设置中的"面板url根路径"一致
|
||||
- 面板设置中的 `url` 需要以 `/` 结尾
|
||||
|
||||
```nginx
|
||||
location /sub {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Range $http_range;
|
||||
proxy_set_header If-Range $http_if_range;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://127.0.0.1:2053;
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
## 建议使用的操作系统
|
||||
|
||||
- Ubuntu 20.04+
|
||||
- Debian 11+
|
||||
- CentOS 8+
|
||||
- OpenEuler 22.03+
|
||||
- Fedora 36+
|
||||
- Arch Linux
|
||||
- Parch Linux
|
||||
- Manjaro
|
||||
- Armbian
|
||||
- AlmaLinux 8.0+
|
||||
- Rocky Linux 8+
|
||||
- Oracle Linux 8+
|
||||
- OpenSUSE Tubleweed
|
||||
- Amazon Linux 2023
|
||||
- Windows x64
|
||||
|
||||
## 支持的架构和设备
|
||||
<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(英语)
|
||||
- Persian(波斯语)
|
||||
- Traditional Chinese(繁体中文)
|
||||
- Simplified Chinese(简体中文)
|
||||
- Japanese(日语)
|
||||
- Russian(俄语)
|
||||
- Vietnamese(越南语)
|
||||
- Spanish(西班牙语)
|
||||
- Indonesian(印尼语)
|
||||
- Ukrainian(乌克兰语)
|
||||
- Turkish(土耳其语)
|
||||
- Português (Brazil)(葡萄牙语(巴西))
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- 系统状态监控
|
||||
- 在所有入站和客户端中搜索
|
||||
- 深色/浅色主题
|
||||
- 支持多用户和多协议
|
||||
- 支持多种协议,包括 VMess、VLESS、Trojan、Shadowsocks、Dokodemo-door、Socks、HTTP、wireguard
|
||||
- 支持 XTLS 原生协议,包括 RPRX-Direct、Vision、REALITY
|
||||
- 流量统计、流量限制、过期时间限制
|
||||
- 可自定义的 Xray配置模板
|
||||
- 支持HTTPS访问面板(自建域名+SSL证书)
|
||||
- 支持一键式SSL证书申请和自动续费
|
||||
- 更多高级配置项目请参考面板
|
||||
- 修复了 API 路由(用户设置将使用 API 创建)
|
||||
- 支持通过面板中提供的不同项目更改配置。
|
||||
- 支持从面板导出/导入数据库
|
||||
|
||||
|
||||
## 默认面板设置
|
||||
|
||||
<details>
|
||||
<summary>点击查看默认设置详情</summary>
|
||||
|
||||
### 用户名、密码、端口和 Web Base Path
|
||||
|
||||
如果您选择不修改这些设置,它们将随机生成(不适用于 Docker)。
|
||||
|
||||
**Docker 的默认设置:**
|
||||
- **用户名:** admin
|
||||
- **密码:** admin
|
||||
- **端口:** 2053
|
||||
|
||||
### 数据库管理:
|
||||
|
||||
您可以直接在面板中方便地进行数据库备份和还原。
|
||||
|
||||
- **数据库路径:**
|
||||
- `/etc/x-ui/x-ui.db`
|
||||
|
||||
### Web 基础路径
|
||||
|
||||
1. **重置 Web 基础路径:**
|
||||
- 打开终端。
|
||||
- 运行 `x-ui` 命令。
|
||||
- 选择 `重置 Web 基础路径` 选项。
|
||||
|
||||
2. **生成或自定义路径:**
|
||||
- 路径将会随机生成,或者您可以输入自定义路径。
|
||||
|
||||
3. **查看当前设置:**
|
||||
- 要查看当前设置,请在终端中使用 `x-ui settings` 命令,或在 `x-ui` 面板中点击 `查看当前设置`。
|
||||
|
||||
### 安全建议:
|
||||
- 为了提高安全性,建议在URL结构中使用一个长的随机词。
|
||||
|
||||
**示例:**
|
||||
- `http://ip:port/*webbasepath*/panel`
|
||||
- `http://domain:port/*webbasepath*/panel`
|
||||
|
||||
</details>
|
||||
|
||||
## WARP 配置
|
||||
|
||||
<details>
|
||||
<summary>点击查看 WARP 配置详情</summary>
|
||||
|
||||
#### 使用方法
|
||||
|
||||
**对于 `v2.1.0` 及之后的版本:**
|
||||
|
||||
WARP 已内置,无需额外安装。只需在面板中开启相关配置即可。
|
||||
|
||||
</details>
|
||||
|
||||
## IP 限制
|
||||
|
||||
<details>
|
||||
<summary>点击查看 IP 限制详情</summary>
|
||||
|
||||
#### 使用方法
|
||||
|
||||
**注意:** 当使用 IP 隧道时,IP 限制将无法正常工作。
|
||||
|
||||
- **对于 `v1.6.1` 及之前的版本:**
|
||||
- IP 限制功能已内置于面板中。
|
||||
|
||||
**对于 `v1.7.0` 及更新的版本:**
|
||||
|
||||
要启用 IP 限制功能,您需要安装 `fail2ban` 及其所需的文件,步骤如下:
|
||||
|
||||
1. 在终端中运行 `x-ui` 命令,然后选择 `IP 限制管理`。
|
||||
2. 您将看到以下选项:
|
||||
|
||||
- **更改封禁时长:** 调整封禁时长。
|
||||
- **解除所有封禁:** 解除当前的所有封禁。
|
||||
- **查看日志:** 查看日志。
|
||||
- **Fail2ban 状态:** 检查 `fail2ban` 的状态。
|
||||
- **重启 Fail2ban:** 重启 `fail2ban` 服务。
|
||||
- **卸载 Fail2ban:** 卸载带有配置的 Fail2ban。
|
||||
|
||||
3. 在面板中通过设置 `Xray 配置/log/访问日志` 为 `./access.log` 添加访问日志路径,然后保存并重启 Xray。
|
||||
|
||||
- **对于 `v2.1.3` 之前的版本:**
|
||||
- 您需要在 Xray 配置中手动设置访问日志路径:
|
||||
|
||||
```sh
|
||||
"log": {
|
||||
"access": "./access.log",
|
||||
"dnsLog": false,
|
||||
"loglevel": "warning"
|
||||
},
|
||||
```
|
||||
|
||||
- **对于 `v2.1.3` 及之后的版本:**
|
||||
- 面板中直接提供了配置 `access.log` 的选项。
|
||||
|
||||
</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>
|
||||
|
||||
#### 使用
|
||||
|
||||
- [API 文档](https://www.postman.com/hsanaei/3x-ui/collection/q1l5l0u/3x-ui)
|
||||
- `/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
|
||||
|
||||
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5%26entityType%3Dcollection%26workspaceId%3Dd64f609f-485a-4951-9b8f-876b3f917124)
|
||||
</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>
|
||||
|
||||
## 预览
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/01-overview-dark.png">
|
||||
<img alt="3x-ui" src="./media/01-overview-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/02-inbounds-dark.png">
|
||||
<img alt="3x-ui" src="./media/02-inbounds-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/03-add-inbound-dark.png">
|
||||
<img alt="3x-ui" src="./media/03-add-inbound-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/04-add-client-dark.png">
|
||||
<img alt="3x-ui" src="./media/04-add-client-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/05-settings-dark.png">
|
||||
<img alt="3x-ui" src="./media/05-settings-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/06-configs-dark.png">
|
||||
<img alt="3x-ui" src="./media/06-configs-light.png">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./media/07-bot-dark.png">
|
||||
<img alt="3x-ui" src="./media/07-bot-light.png">
|
||||
</picture>
|
||||
|
||||
## 特别感谢
|
||||
|
||||
- [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._
|
||||
- [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._
|
||||
|
||||
## Star趋势
|
||||
|
||||
[](https://starchart.cc/MHSanaei/3x-ui)
|
||||
@@ -16,10 +16,11 @@ var name string
|
||||
type LogLevel string
|
||||
|
||||
const (
|
||||
Debug LogLevel = "debug"
|
||||
Info LogLevel = "info"
|
||||
Warn LogLevel = "warn"
|
||||
Error LogLevel = "error"
|
||||
Debug LogLevel = "debug"
|
||||
Info LogLevel = "info"
|
||||
Notice LogLevel = "notice"
|
||||
Warn LogLevel = "warn"
|
||||
Error LogLevel = "error"
|
||||
)
|
||||
|
||||
func GetVersion() string {
|
||||
@@ -45,6 +46,30 @@ func IsDebug() bool {
|
||||
return os.Getenv("XUI_DEBUG") == "true"
|
||||
}
|
||||
|
||||
func GetDBPath() string {
|
||||
return fmt.Sprintf("/etc/%s/%s.db", GetName(), GetName())
|
||||
func GetBinFolderPath() string {
|
||||
binFolderPath := os.Getenv("XUI_BIN_FOLDER")
|
||||
if binFolderPath == "" {
|
||||
binFolderPath = "bin"
|
||||
}
|
||||
return binFolderPath
|
||||
}
|
||||
|
||||
func GetDBFolderPath() string {
|
||||
dbFolderPath := os.Getenv("XUI_DB_FOLDER")
|
||||
if dbFolderPath == "" {
|
||||
dbFolderPath = "/etc/x-ui"
|
||||
}
|
||||
return dbFolderPath
|
||||
}
|
||||
|
||||
func GetDBPath() string {
|
||||
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName())
|
||||
}
|
||||
|
||||
func GetLogFolder() string {
|
||||
logFolderPath := os.Getenv("XUI_LOG_FOLDER")
|
||||
if logFolderPath == "" {
|
||||
logFolderPath = "/var/log"
|
||||
}
|
||||
return logFolderPath
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.2.2
|
||||
2.5.1
|
||||
157
database/db.go
@@ -1,9 +1,13 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"x-ui/config"
|
||||
"x-ui/database/model"
|
||||
"x-ui/xray"
|
||||
@@ -15,43 +19,71 @@ import (
|
||||
|
||||
var db *gorm.DB
|
||||
|
||||
const (
|
||||
defaultUsername = "admin"
|
||||
defaultPassword = "admin"
|
||||
defaultSecret = ""
|
||||
)
|
||||
|
||||
func initModels() error {
|
||||
// Order matters: first create tables without dependencies
|
||||
baseModels := []interface{}{
|
||||
&model.User{},
|
||||
&model.Setting{},
|
||||
}
|
||||
|
||||
// Migrate base models
|
||||
for _, model := range baseModels {
|
||||
if err := db.AutoMigrate(model); err != nil {
|
||||
log.Printf("Error auto migrating base model: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Then migrate models with dependencies
|
||||
dependentModels := []interface{}{
|
||||
&model.Inbound{},
|
||||
&model.OutboundTraffics{},
|
||||
&model.InboundClientIps{},
|
||||
&xray.ClientTraffic{},
|
||||
}
|
||||
|
||||
for _, model := range dependentModels {
|
||||
if err := db.AutoMigrate(model); err != nil {
|
||||
log.Printf("Error auto migrating dependent model: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initUser() error {
|
||||
err := db.AutoMigrate(&model.User{})
|
||||
empty, err := isTableEmpty("users")
|
||||
if err != nil {
|
||||
log.Printf("Error checking if users table is empty: %v", err)
|
||||
return err
|
||||
}
|
||||
var count int64
|
||||
err = db.Model(&model.User{}).Count(&count).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
if empty {
|
||||
user := &model.User{
|
||||
Username: "admin",
|
||||
Password: "admin",
|
||||
Username: defaultUsername,
|
||||
Password: defaultPassword,
|
||||
LoginSecret: defaultSecret,
|
||||
}
|
||||
return db.Create(user).Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func initInbound() error {
|
||||
return db.AutoMigrate(&model.Inbound{})
|
||||
}
|
||||
|
||||
func initSetting() error {
|
||||
return db.AutoMigrate(&model.Setting{})
|
||||
}
|
||||
func initInboundClientIps() error {
|
||||
return db.AutoMigrate(&model.InboundClientIps{})
|
||||
}
|
||||
func initClientTraffic() error {
|
||||
return db.AutoMigrate(&xray.ClientTraffic{})
|
||||
func isTableEmpty(tableName string) (bool, error) {
|
||||
var count int64
|
||||
err := db.Table(tableName).Count(&count).Error
|
||||
return count == 0, err
|
||||
}
|
||||
|
||||
func InitDB(dbPath string) error {
|
||||
dir := path.Dir(dbPath)
|
||||
err := os.MkdirAll(dir, fs.ModeDir)
|
||||
err := os.MkdirAll(dir, fs.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -65,34 +97,58 @@ func InitDB(dbPath string) error {
|
||||
}
|
||||
|
||||
c := &gorm.Config{
|
||||
Logger: gormLogger,
|
||||
Logger: gormLogger,
|
||||
SkipDefaultTransaction: true,
|
||||
PrepareStmt: true,
|
||||
}
|
||||
db, err = gorm.Open(sqlite.Open(dbPath), c)
|
||||
|
||||
dsn := dbPath + "?cache=shared&_journal_mode=WAL&_synchronous=NORMAL"
|
||||
db, err = gorm.Open(sqlite.Open(dsn), c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = initUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = initInbound()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = initSetting()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = initInboundClientIps()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = initClientTraffic()
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = sqlDB.Exec("PRAGMA cache_size = -64000;")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = sqlDB.Exec("PRAGMA temp_store = MEMORY;")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = sqlDB.Exec("PRAGMA foreign_keys = ON;")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := initModels(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := initUser(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CloseDB() error {
|
||||
if db != nil {
|
||||
|
||||
if err := Checkpoint(); err != nil {
|
||||
log.Printf("error executing checkpoint: %v", err)
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sqlDB.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -103,3 +159,22 @@ func GetDB() *gorm.DB {
|
||||
func IsNotFound(err error) bool {
|
||||
return err == gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
func IsSQLiteDB(file io.ReaderAt) (bool, error) {
|
||||
signature := []byte("SQLite format 3\x00")
|
||||
buf := make([]byte, len(signature))
|
||||
_, err := file.ReadAt(buf, 0)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return bytes.Equal(buf, signature), nil
|
||||
}
|
||||
|
||||
func Checkpoint() error {
|
||||
// Update WAL
|
||||
err := db.Exec("PRAGMA wal_checkpoint;").Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"x-ui/util/json_util"
|
||||
"x-ui/xray"
|
||||
)
|
||||
@@ -9,40 +10,53 @@ import (
|
||||
type Protocol string
|
||||
|
||||
const (
|
||||
VMess Protocol = "vmess"
|
||||
VMESS Protocol = "vmess"
|
||||
VLESS Protocol = "vless"
|
||||
Dokodemo Protocol = "Dokodemo-door"
|
||||
Http Protocol = "http"
|
||||
DOKODEMO Protocol = "dokodemo-door"
|
||||
HTTP Protocol = "http"
|
||||
Trojan Protocol = "trojan"
|
||||
Shadowsocks Protocol = "shadowsocks"
|
||||
Socks Protocol = "socks"
|
||||
WireGuard Protocol = "wireguard"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
LoginSecret string `json:"loginSecret"`
|
||||
}
|
||||
|
||||
type Inbound struct {
|
||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||
UserId int `json:"-"`
|
||||
UserId int `json:"-" gorm:"index"`
|
||||
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"`
|
||||
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id;constraint:OnDelete:CASCADE" json:"clientStats"`
|
||||
|
||||
// config part
|
||||
Listen string `json:"listen" form:"listen"`
|
||||
Port int `json:"port" form:"port" gorm:"unique"`
|
||||
Port int `json:"port" form:"port"`
|
||||
Protocol Protocol `json:"protocol" form:"protocol"`
|
||||
Settings string `json:"settings" form:"settings"`
|
||||
StreamSettings string `json:"streamSettings" form:"streamSettings"`
|
||||
Tag string `json:"tag" form:"tag" gorm:"unique"`
|
||||
Sniffing string `json:"sniffing" form:"sniffing"`
|
||||
Allocate string `json:"allocate" form:"allocate"`
|
||||
}
|
||||
|
||||
type OutboundTraffics struct {
|
||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Tag string `json:"tag" form:"tag" gorm:"unique"`
|
||||
Up int64 `json:"up" form:"up" gorm:"default:0"`
|
||||
Down int64 `json:"down" form:"down" gorm:"default:0"`
|
||||
Total int64 `json:"total" form:"total" gorm:"default:0"`
|
||||
}
|
||||
|
||||
type InboundClientIps struct {
|
||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
|
||||
@@ -62,6 +76,7 @@ func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
|
||||
StreamSettings: json_util.RawMessage(i.StreamSettings),
|
||||
Tag: i.Tag,
|
||||
Sniffing: json_util.RawMessage(i.Sniffing),
|
||||
Allocate: json_util.RawMessage(i.Allocate),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,14 +88,16 @@ type Setting struct {
|
||||
|
||||
type Client struct {
|
||||
ID string `json:"id"`
|
||||
Security string `json:"security"`
|
||||
Password string `json:"password"`
|
||||
Flow string `json:"flow"`
|
||||
AlterIds uint16 `json:"alterId"`
|
||||
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 string `json:"tgId" form:"tgId"`
|
||||
TgID int64 `json:"tgId" form:"tgId"`
|
||||
SubID string `json:"subId" form:"subId"`
|
||||
Comment string `json:"comment" form:"comment"`
|
||||
Reset int `json:"reset" form:"reset"`
|
||||
}
|
||||
|
||||
17
docker-compose.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
3x-ui:
|
||||
image: ghcr.io/mhsanaei/3x-ui:latest
|
||||
container_name: 3x-ui
|
||||
hostname: yourhostname
|
||||
volumes:
|
||||
- $PWD/db/:/etc/x-ui/
|
||||
- $PWD/cert/:/root/cert/
|
||||
environment:
|
||||
XRAY_VMESS_AEAD_FORCED: "false"
|
||||
X_UI_ENABLE_FAIL2BAN: "true"
|
||||
tty: true
|
||||
network_mode: host
|
||||
restart: unless-stopped
|
||||
126
go.mod
@@ -1,63 +1,101 @@
|
||||
module x-ui
|
||||
|
||||
go 1.20
|
||||
go 1.23.5
|
||||
|
||||
require (
|
||||
github.com/Workiva/go-datastructures v1.0.53
|
||||
github.com/gin-contrib/sessions v0.0.4
|
||||
github.com/gin-gonic/gin v1.9.0
|
||||
github.com/go-cmd/cmd v1.4.1
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
||||
github.com/goccy/go-json v0.10.2
|
||||
github.com/nicksnyder/go-i18n/v2 v2.2.1
|
||||
github.com/gin-contrib/gzip v1.2.2
|
||||
github.com/gin-contrib/sessions v1.0.2
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/goccy/go-json v0.10.5
|
||||
github.com/mymmrac/telego v0.32.0
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pelletier/go-toml/v2 v2.0.7
|
||||
github.com/pelletier/go-toml/v2 v2.2.3
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil/v3 v3.23.3
|
||||
github.com/xtls/xray-core v1.8.0
|
||||
go.uber.org/atomic v1.10.0
|
||||
golang.org/x/text v0.9.0
|
||||
google.golang.org/grpc v1.54.0
|
||||
gorm.io/driver/sqlite v1.5.0
|
||||
gorm.io/gorm v1.25.0
|
||||
github.com/shirou/gopsutil/v4 v4.25.1
|
||||
github.com/valyala/fasthttp v1.58.0
|
||||
github.com/xtls/xray-core v1.8.25-0.20250130105737-0a8470cb14eb
|
||||
go.uber.org/atomic v1.11.0
|
||||
golang.org/x/text v0.21.0
|
||||
google.golang.org/grpc v1.70.0
|
||||
gorm.io/driver/sqlite v1.5.7
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||
github.com/bytedance/sonic v1.8.7 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/bytedance/sonic v1.12.8 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.3 // indirect
|
||||
github.com/cloudflare/circl v1.5.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/fasthttp/router v1.5.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gin-contrib/sse v1.0.0 // 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.12.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/gorilla/sessions v1.2.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.24.0 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/pprof v0.0.0-20250202011525-fc3143867406 // indirect
|
||||
github.com/gorilla/context v1.1.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.4.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // 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/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.2.3 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
|
||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.16 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pires/go-proxyproto v0.7.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.5 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.11 // indirect
|
||||
github.com/tklauser/numcpus v0.6.0 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.49.0 // indirect
|
||||
github.com/refraction-networking/utls v1.6.7 // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
github.com/sagernet/sing v0.5.1 // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||
github.com/tklauser/numcpus v0.9.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.8.0 // indirect
|
||||
golang.org/x/net v0.9.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // 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.3.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/arch v0.13.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
golang.org/x/tools v0.29.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-20250127172529-29210b9bc287 // indirect
|
||||
google.golang.org/protobuf v1.36.4 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20240320123526-dc6abceb7ff0 // indirect
|
||||
lukechampine.com/blake3 v1.3.0 // indirect
|
||||
)
|
||||
|
||||
425
go.sum
@@ -1,259 +1,268 @@
|
||||
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig=
|
||||
github.com/Workiva/go-datastructures v1.0.53/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/antonlindstrom/pgstore v0.0.0-20200229204646-b08ebf1105e0/go.mod h1:2Ti6VUHVxpC0VSmTZzEvpzysnaGAfGBOoMIz5ykPyyw=
|
||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.8.7 h1:d3sry5vGgVq/OpgozRUNP6xBsSo0mtNdwliApw+SAMQ=
|
||||
github.com/bytedance/sonic v1.8.7/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
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.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/bytedance/sonic v1.12.8 h1:4xYRVRlXIgvSZ4e8iVTlMF5szgpXd4AfvuWgA8I8lgs=
|
||||
github.com/bytedance/sonic v1.12.8/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
|
||||
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
|
||||
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0=
|
||||
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
||||
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
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.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/fasthttp/router v1.5.4 h1:oxdThbBwQgsDIYZ3wR1IavsNl6ZS9WdjKukeMikOnC8=
|
||||
github.com/fasthttp/router v1.5.4/go.mod h1:3/hysWq6cky7dTfzaaEPZGdptwjwx0qzTgFCKEWRjgc=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
|
||||
github.com/gin-contrib/sessions v0.0.4 h1:gq4fNa1Zmp564iHP5G6EBuktilEos8VKhe2sza1KMgo=
|
||||
github.com/gin-contrib/sessions v0.0.4/go.mod h1:pQ3sIyviBBGcxgyR8mkeJuXbeV3h3NYmhJADQTq5+Vo=
|
||||
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.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
||||
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
|
||||
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
|
||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-cmd/cmd v1.4.1 h1:JUcEIE84v8DSy02XTZpUDeGKExk2oW3DA10hTjbQwmc=
|
||||
github.com/go-cmd/cmd v1.4.1/go.mod h1:tbBenttXtZU4c5djS1o7PWL5pd2xAr5sIqH1kGdNiRc=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||
github.com/gin-contrib/gzip v1.2.2 h1:iUU/EYCM8ENfkjmZaVrxbjF/ZC267Iqv5S0MMCMEliI=
|
||||
github.com/gin-contrib/gzip v1.2.2/go.mod h1:C1a5cacjlDsS20cKnHlZRCPUu57D3qH6B2pV0rl+Y/s=
|
||||
github.com/gin-contrib/sessions v1.0.2 h1:UaIjUvTH1cMeOdj3in6dl+Xb6It8RiKRF9Z1anbUyCA=
|
||||
github.com/gin-contrib/sessions v1.0.2/go.mod h1:KxKxWqWP5LJVDCInulOl4WbLzK2KSPlLesfZ66wRvMs=
|
||||
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
||||
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
||||
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/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/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-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
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=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
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.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI=
|
||||
github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
|
||||
github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
|
||||
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
||||
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.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
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.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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10 h1:CqYfpuYIjnlNxM3msdyPRKabhXZWbKjf3Q8BWROFBso=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
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/pprof v0.0.0-20250202011525-fc3143867406 h1:wlQI2cYY0BsWmmPPAnxfQ8SDW0S3Jasn+4B8kXFxprg=
|
||||
github.com/google/pprof v0.0.0-20250202011525-fc3143867406/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
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.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/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.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
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/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
|
||||
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/leodido/go-urn v1.2.3 h1:6BE2vPT0lqoz3fmOesHZiaiFh7889ssCo2GMvLCfiuA=
|
||||
github.com/leodido/go-urn v1.2.3/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik=
|
||||
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
|
||||
github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
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-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||
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.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
|
||||
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
|
||||
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 v0.0.0-20180701023420-4b7aa43c6742/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/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0=
|
||||
github.com/onsi/ginkgo/v2 v2.9.0 h1:Tugw2BKlNHTMfG+CheOITkYvk4LAh6MFOvikhGVnhE8=
|
||||
github.com/mymmrac/telego v0.32.0 h1:4X8C1l3k+opkk86r95+eQE8DxiS2LYlR61L/G7yreDY=
|
||||
github.com/mymmrac/telego v0.32.0/go.mod h1:qS6NaRhJgcuEEBEMVCV79S2xCAuHq9O+ixwfLuRW31M=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
|
||||
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
|
||||
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
|
||||
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
|
||||
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
|
||||
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/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
|
||||
github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
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.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pires/go-proxyproto v0.8.0 h1:5unRmEAPbHXHuLjDg01CxJWf91cw3lKHc/0xzKpXEe0=
|
||||
github.com/pires/go-proxyproto v0.8.0/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY=
|
||||
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-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig=
|
||||
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
|
||||
github.com/quic-go/qtls-go1-19 v0.2.1 h1:aJcKNMkH5ASEJB9FXNeZCyTEIHU1J7MmHyz1Q1TSG1A=
|
||||
github.com/quic-go/qtls-go1-20 v0.1.1 h1:KbChDlg82d3IHqaj2bn6GfKRj84Per2VGf5XV3wSwQk=
|
||||
github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0=
|
||||
github.com/refraction-networking/utls v1.2.3-0.20230308205431-4f1df6c200db h1:ULRv/GPW5KYDafE0FACN2no+HTCyQLUtfyOIeyp3GNc=
|
||||
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/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.49.0 h1:w5iJHXwHxs1QxyBv1EHKuC50GX5to8mJAxvtnttJp94=
|
||||
github.com/quic-go/quic-go v0.49.0/go.mod h1:s2wDnmCdooUQBmQfpUSTCYBl1/D4FcqbULMMkASvR6s=
|
||||
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/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/sagernet/sing v0.1.7 h1:g4vjr3q8SUlBZSx97Emz5OBfSMBxxW5Q8C2PfdoSo08=
|
||||
github.com/sagernet/sing-shadowsocks v0.1.1 h1:uFK2rlVeD/b1xhDwSMbUI2goWc6fOKxp+ZeKHZq6C9Q=
|
||||
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
|
||||
github.com/shirou/gopsutil/v3 v3.23.3 h1:Syt5vVZXUDXPEXpIBt5ziWsJ4LdSAAxF4l/xZeQgSEE=
|
||||
github.com/shirou/gopsutil/v3 v3.23.3/go.mod h1:lSBNN6t3+D6W5e5nXTxc8KIMMVxAcS+6IJlffjRRlMU=
|
||||
github.com/shoenig/go-m1cpu v0.1.4/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
|
||||
github.com/shoenig/go-m1cpu v0.1.5 h1:LF57Z/Fpb/WdGLjt2HZilNnmZOxg/q2bSKTQhgbrLrQ=
|
||||
github.com/shoenig/go-m1cpu v0.1.5/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
|
||||
github.com/shoenig/test v0.6.3 h1:GVXWJFk9PiOjN0KoJ7VrJGH6uLPnqxR7/fe3HUPfE0c=
|
||||
github.com/shoenig/test v0.6.3/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
|
||||
github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
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/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/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
|
||||
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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
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.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg=
|
||||
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
|
||||
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
|
||||
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
|
||||
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
|
||||
github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
|
||||
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
|
||||
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 v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
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/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
|
||||
github.com/xtls/reality v0.0.0-20230309125256-0d0713b108c8 h1:LLtLxEe3S0Ko+ckqt4t29RLskpNdOZfgjZCC2/Byr50=
|
||||
github.com/xtls/xray-core v1.8.0 h1:/OD0sDv6YIBqvE+cVfnqlKrtbMs0Fm9IP5BR5d8Eu4k=
|
||||
github.com/xtls/xray-core v1.8.0/go.mod h1:i9KWgbLyxg/NT+3+g4nE74Zp3DgTCP3X04YkSfsJeDI=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
|
||||
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg=
|
||||
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/exp v0.0.0-20230307190834-24139beb5833 h1:SChBja7BCQewoTAU7IgvucQKMIXrEpFxNMs0spT3/5s=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE=
|
||||
github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw=
|
||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
|
||||
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463 h1:g1Cj7d+my6k/HHxLAyxPwyX8i7FGRr6ulBDMkBzg2BM=
|
||||
github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463/go.mod h1:BjIOLmkEEtAgloAiVUcYj0Mt+YU00JARZw8AEU0IwAg=
|
||||
github.com/xtls/xray-core v1.8.25-0.20250130105737-0a8470cb14eb h1:VBzDZ4XHT9FM0S3qvXp6KuTOk7mXQQm0pjOW0Neeog4=
|
||||
github.com/xtls/xray-core v1.8.25-0.20250130105737-0a8470cb14eb/go.mod h1:EKhNEDY/WIB+ylEbVv2pzUaP08W/lt0sYmJQ9FJruko=
|
||||
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.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
|
||||
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
|
||||
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
|
||||
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
||||
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
|
||||
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
|
||||
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
||||
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
||||
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.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
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.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
|
||||
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
|
||||
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.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.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
||||
google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
|
||||
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
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/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 h1:J1H9f+LEdWAfHcez/4cvaVBox7cOYT+IU6rgqj5x++8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
|
||||
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
|
||||
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
|
||||
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
|
||||
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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/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.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c=
|
||||
gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I=
|
||||
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU=
|
||||
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4=
|
||||
lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
|
||||
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
gvisor.dev/gvisor v0.0.0-20240320123526-dc6abceb7ff0 h1:P+U/06iIKPQ3DLcg+zBfSCia1luZ2msPZrJ8jYDFPs0=
|
||||
gvisor.dev/gvisor v0.0.0-20240320123526-dc6abceb7ff0/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0=
|
||||
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=
|
||||
|
||||
297
install.sh
@@ -2,13 +2,14 @@
|
||||
|
||||
red='\033[0;31m'
|
||||
green='\033[0;32m'
|
||||
blue='\033[0;34m'
|
||||
yellow='\033[0;33m'
|
||||
plain='\033[0m'
|
||||
|
||||
cur_dir=$(pwd)
|
||||
|
||||
# check root
|
||||
[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error:${plain} Please run this script with root privilege \n " && exit 1
|
||||
[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error: ${plain} Please run this script with root privilege \n " && exit 1
|
||||
|
||||
# Check OS and set release variable
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
@@ -23,161 +24,263 @@ else
|
||||
fi
|
||||
echo "The OS release is: $release"
|
||||
|
||||
arch=$(arch)
|
||||
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 "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [[ $arch == "x86_64" || $arch == "x64" || $arch == "amd64" ]]; then
|
||||
arch="amd64"
|
||||
elif [[ $arch == "aarch64" || $arch == "arm64" ]]; then
|
||||
arch="arm64"
|
||||
else
|
||||
arch="amd64"
|
||||
echo -e "${red} Failed to check system arch, will use default arch: ${arch}${plain}"
|
||||
fi
|
||||
|
||||
echo "arch: ${arch}"
|
||||
|
||||
if [ $(getconf WORD_BIT) != '32' ] && [ $(getconf LONG_BIT) != '64' ]; then
|
||||
echo "x-ui dosen't support 32-bit(x86) system, please use 64 bit operating system(x86_64) instead, if there is something wrong, please get in touch with me!"
|
||||
exit -1
|
||||
fi
|
||||
echo "arch: $(arch)"
|
||||
|
||||
os_version=""
|
||||
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
|
||||
os_version=$(grep "^VERSION_ID" /etc/os-release | cut -d '=' -f2 | tr -d '"' | tr -d '.')
|
||||
|
||||
if [[ "${release}" == "centos" ]]; then
|
||||
if [[ "${release}" == "arch" ]]; then
|
||||
echo "Your OS is Arch Linux"
|
||||
elif [[ "${release}" == "parch" ]]; then
|
||||
echo "Your OS is Parch Linux"
|
||||
elif [[ "${release}" == "manjaro" ]]; then
|
||||
echo "Your OS is Manjaro"
|
||||
elif [[ "${release}" == "armbian" ]]; then
|
||||
echo "Your OS is Armbian"
|
||||
elif [[ "${release}" == "alpine" ]]; then
|
||||
echo "Your OS is Alpine Linux"
|
||||
elif [[ "${release}" == "opensuse-tumbleweed" ]]; then
|
||||
echo "Your OS is OpenSUSE Tumbleweed"
|
||||
elif [[ "${release}" == "openEuler" ]]; then
|
||||
if [[ ${os_version} -lt 2203 ]]; then
|
||||
echo -e "${red} Please use OpenEuler 22.03 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "centos" ]]; then
|
||||
if [[ ${os_version} -lt 8 ]]; then
|
||||
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "ubuntu" ]]; then
|
||||
if [[ ${os_version} -lt 20 ]]; then
|
||||
echo -e "${red}please use Ubuntu 20 or higher version!${plain}\n" && exit 1
|
||||
elif [[ "${release}" == "ubuntu" ]]; then
|
||||
if [[ ${os_version} -lt 2004 ]]; then
|
||||
echo -e "${red} Please use Ubuntu 20 or higher version!${plain}\n" && exit 1
|
||||
fi
|
||||
|
||||
elif [[ "${release}" == "fedora" ]]; then
|
||||
if [[ ${os_version} -lt 36 ]]; then
|
||||
echo -e "${red}please use Fedora 36 or higher version!${plain}\n" && exit 1
|
||||
echo -e "${red} Please use Fedora 36 or higher version!${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "amzn" ]]; then
|
||||
if [[ ${os_version} != "2023" ]]; then
|
||||
echo -e "${red} Please use Amazon Linux 2023!${plain}\n" && exit 1
|
||||
fi
|
||||
|
||||
elif [[ "${release}" == "debian" ]]; then
|
||||
if [[ ${os_version} -lt 10 ]]; then
|
||||
echo -e "${red} Please use Debian 10 or higher ${plain}\n" && exit 1
|
||||
if [[ ${os_version} -lt 11 ]]; then
|
||||
echo -e "${red} Please use Debian 11 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "almalinux" ]]; then
|
||||
if [[ ${os_version} -lt 80 ]]; then
|
||||
echo -e "${red} Please use AlmaLinux 8.0 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "rocky" ]]; then
|
||||
if [[ ${os_version} -lt 8 ]]; then
|
||||
echo -e "${red} Please use Rocky Linux 8 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "ol" ]]; then
|
||||
if [[ ${os_version} -lt 8 ]]; then
|
||||
echo -e "${red} Please use Oracle Linux 8 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${red}Failed to check the OS version, please contact the author!${plain}" && exit 1
|
||||
echo -e "${red}Your operating system is not supported by this script.${plain}\n"
|
||||
echo "Please ensure you are using one of the following supported operating systems:"
|
||||
echo "- Ubuntu 20.04+"
|
||||
echo "- Debian 11+"
|
||||
echo "- CentOS 8+"
|
||||
echo "- OpenEuler 22.03+"
|
||||
echo "- Fedora 36+"
|
||||
echo "- Arch Linux"
|
||||
echo "- Parch Linux"
|
||||
echo "- Manjaro"
|
||||
echo "- Armbian"
|
||||
echo "- AlmaLinux 8.0+"
|
||||
echo "- Rocky Linux 8+"
|
||||
echo "- Oracle Linux 8+"
|
||||
echo "- OpenSUSE Tumbleweed"
|
||||
echo "- Amazon Linux 2023"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install_base() {
|
||||
case "${release}" in
|
||||
centos|fedora)
|
||||
yum install -y -q wget curl tar
|
||||
;;
|
||||
*)
|
||||
apt install -y -q wget curl tar
|
||||
;;
|
||||
ubuntu | debian | armbian)
|
||||
apt-get update && apt-get install -y -q wget curl tar tzdata
|
||||
;;
|
||||
centos | almalinux | rocky | ol)
|
||||
yum -y update && yum install -y -q wget curl tar tzdata
|
||||
;;
|
||||
fedora | amzn)
|
||||
dnf -y update && dnf install -y -q wget curl tar tzdata
|
||||
;;
|
||||
arch | manjaro | parch)
|
||||
pacman -Syu && pacman -Syu --noconfirm wget curl tar tzdata
|
||||
;;
|
||||
opensuse-tumbleweed)
|
||||
zypper refresh && zypper -q install -y wget curl tar timezone
|
||||
;;
|
||||
*)
|
||||
apt-get update && apt install -y -q wget curl tar tzdata
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
#This function will be called when user installed x-ui out of sercurity
|
||||
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"
|
||||
}
|
||||
|
||||
config_after_install() {
|
||||
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
|
||||
read -p "Do you want to continue with the modification [y/n]? ": config_confirm
|
||||
if [[ x"${config_confirm}" == x"y" || x"${config_confirm}" == x"Y" ]]; then
|
||||
read -p "Please set up your username:" config_account
|
||||
echo -e "${yellow}Your username will be:${config_account}${plain}"
|
||||
read -p "Please set up your password:" config_password
|
||||
echo -e "${yellow}Your password will be:${config_password}${plain}"
|
||||
read -p "Please set up the panel port:" config_port
|
||||
echo -e "${yellow}Your panel port is:${config_port}${plain}"
|
||||
echo -e "${yellow}Initializing, please wait...${plain}"
|
||||
/usr/local/x-ui/x-ui setting -username ${config_account} -password ${config_password}
|
||||
echo -e "${yellow}Account name and password set successfully!${plain}"
|
||||
/usr/local/x-ui/x-ui setting -port ${config_port}
|
||||
echo -e "${yellow}Panel port set successfully!${plain}"
|
||||
else
|
||||
echo -e "${red}cancel...${plain}"
|
||||
if [[ ! -f "/etc/x-ui/x-ui.db" ]]; then
|
||||
local usernameTemp=$(head -c 6 /dev/urandom | base64)
|
||||
local passwordTemp=$(head -c 6 /dev/urandom | base64)
|
||||
/usr/local/x-ui/x-ui setting -username ${usernameTemp} -password ${passwordTemp}
|
||||
echo -e "this is a fresh installation,will generate random login info for security concerns:"
|
||||
local existing_username=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'username: .+' | awk '{print $2}')
|
||||
local existing_password=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'password: .+' | awk '{print $2}')
|
||||
local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')
|
||||
local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
|
||||
local server_ip=$(curl -s https://api.ipify.org)
|
||||
|
||||
if [[ ${#existing_webBasePath} -lt 4 ]]; then
|
||||
if [[ "$existing_username" == "admin" && "$existing_password" == "admin" ]]; then
|
||||
local config_webBasePath=$(gen_random_string 15)
|
||||
local config_username=$(gen_random_string 10)
|
||||
local config_password=$(gen_random_string 10)
|
||||
|
||||
read -p "Would you like to customize the Panel Port settings? (If not, a random port will be applied) [y/n]: " config_confirm
|
||||
if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then
|
||||
read -p "Please set up the panel port: " config_port
|
||||
echo -e "${yellow}Your Panel Port is: ${config_port}${plain}"
|
||||
else
|
||||
local config_port=$(shuf -i 1024-62000 -n 1)
|
||||
echo -e "${yellow}Generated random port: ${config_port}${plain}"
|
||||
fi
|
||||
|
||||
/usr/local/x-ui/x-ui setting -username "${config_username}" -password "${config_password}" -port "${config_port}" -webBasePath "${config_webBasePath}"
|
||||
echo -e "This is a fresh installation, generating random login info for security concerns:"
|
||||
echo -e "###############################################"
|
||||
echo -e "${green}username:${usernameTemp}${plain}"
|
||||
echo -e "${green}password:${passwordTemp}${plain}"
|
||||
echo -e "${green}Username: ${config_username}${plain}"
|
||||
echo -e "${green}Password: ${config_password}${plain}"
|
||||
echo -e "${green}Port: ${config_port}${plain}"
|
||||
echo -e "${green}WebBasePath: ${config_webBasePath}${plain}"
|
||||
echo -e "${green}Access URL: http://${server_ip}:${config_port}/${config_webBasePath}${plain}"
|
||||
echo -e "###############################################"
|
||||
echo -e "${red}if you forgot your login info,you can type x-ui and then type 7 to check after installation${plain}"
|
||||
echo -e "${yellow}If you forgot your login info, you can type 'x-ui settings' to check${plain}"
|
||||
else
|
||||
echo -e "${red} this is your upgrade,will keep old settings,if you forgot your login info,you can type x-ui and then type 7 to check${plain}"
|
||||
local config_webBasePath=$(gen_random_string 15)
|
||||
echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}"
|
||||
/usr/local/x-ui/x-ui setting -webBasePath "${config_webBasePath}"
|
||||
echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}"
|
||||
echo -e "${green}Access URL: http://${server_ip}:${existing_port}/${config_webBasePath}${plain}"
|
||||
fi
|
||||
else
|
||||
if [[ "$existing_username" == "admin" && "$existing_password" == "admin" ]]; then
|
||||
local config_username=$(gen_random_string 10)
|
||||
local config_password=$(gen_random_string 10)
|
||||
|
||||
echo -e "${yellow}Default credentials detected. Security update required...${plain}"
|
||||
/usr/local/x-ui/x-ui setting -username "${config_username}" -password "${config_password}"
|
||||
echo -e "Generated new random login credentials:"
|
||||
echo -e "###############################################"
|
||||
echo -e "${green}Username: ${config_username}${plain}"
|
||||
echo -e "${green}Password: ${config_password}${plain}"
|
||||
echo -e "###############################################"
|
||||
echo -e "${yellow}If you forgot your login info, you can type 'x-ui settings' to check${plain}"
|
||||
else
|
||||
echo -e "${green}Username, Password, and WebBasePath are properly set. Exiting...${plain}"
|
||||
fi
|
||||
fi
|
||||
|
||||
/usr/local/x-ui/x-ui migrate
|
||||
}
|
||||
|
||||
install_x-ui() {
|
||||
systemctl stop x-ui
|
||||
cd /usr/local/
|
||||
|
||||
if [ $# == 0 ]; then
|
||||
last_version=$(curl -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
if [[ ! -n "$last_version" ]]; then
|
||||
echo -e "${red}Failed to fetch x-ui version, it maybe due to Github API restrictions, please try it later${plain}"
|
||||
tag_version=$(curl -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
if [[ ! -n "$tag_version" ]]; then
|
||||
echo -e "${red}Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later${plain}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "Got x-ui latest version: ${last_version}, beginning the installation..."
|
||||
wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz
|
||||
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
|
||||
wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${red}Downloading x-ui failed, please be sure that your server can access Github ${plain}"
|
||||
echo -e "${red}Downloading x-ui failed, please be sure that your server can access GitHub ${plain}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
last_version=$1
|
||||
url="https://github.com/MHSanaei/3x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz"
|
||||
echo -e "Begining to install x-ui $1"
|
||||
wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz ${url}
|
||||
tag_version=$1
|
||||
tag_version_numeric=${tag_version#v}
|
||||
min_version="2.3.5"
|
||||
|
||||
if [[ "$(printf '%s\n' "$min_version" "$tag_version_numeric" | sort -V | head -n1)" != "$min_version" ]]; then
|
||||
echo -e "${red}Please use a newer version (at least v2.3.5). Exiting installation.${plain}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
url="https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz"
|
||||
echo -e "Beginning to install x-ui $1"
|
||||
wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch).tar.gz ${url}
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${red}Download x-ui $1 failed,please check the version exists${plain}"
|
||||
echo -e "${red}Download x-ui $1 failed, please check if the version exists ${plain}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -e /usr/local/x-ui/ ]]; then
|
||||
systemctl stop x-ui
|
||||
rm /usr/local/x-ui/ -rf
|
||||
fi
|
||||
|
||||
tar zxvf x-ui-linux-${arch}.tar.gz
|
||||
rm x-ui-linux-${arch}.tar.gz -f
|
||||
tar zxvf x-ui-linux-$(arch).tar.gz
|
||||
rm x-ui-linux-$(arch).tar.gz -f
|
||||
cd x-ui
|
||||
chmod +x x-ui bin/xray-linux-${arch}
|
||||
chmod +x x-ui
|
||||
|
||||
# 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
|
||||
chmod +x bin/xray-linux-arm
|
||||
fi
|
||||
|
||||
chmod +x x-ui bin/xray-linux-$(arch)
|
||||
cp -f x-ui.service /etc/systemd/system/
|
||||
wget --no-check-certificate -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh
|
||||
chmod +x /usr/local/x-ui/x-ui.sh
|
||||
chmod +x /usr/bin/x-ui
|
||||
config_after_install
|
||||
#echo -e "If it is a new installation, the default web port is ${green}2053${plain}, The username and password are ${green}admin${plain} by default"
|
||||
#echo -e "Please make sure that this port is not occupied by other procedures,${yellow} And make sure that port 2053 has been released${plain}"
|
||||
# echo -e "If you want to modify the 2053 to other ports and enter the x-ui command to modify it, you must also ensure that the port you modify is also released"
|
||||
#echo -e ""
|
||||
#echo -e "If it is updated panel, access the panel in your previous way"
|
||||
#echo -e ""
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable x-ui
|
||||
systemctl start x-ui
|
||||
echo -e "${green}x-ui ${last_version}${plain} installation finished, it is running now..."
|
||||
echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..."
|
||||
echo -e ""
|
||||
echo -e "x-ui control menu usages: "
|
||||
echo -e "----------------------------------------------"
|
||||
echo -e "x-ui - Enter Admin menu"
|
||||
echo -e "x-ui start - Start x-ui"
|
||||
echo -e "x-ui stop - Stop x-ui"
|
||||
echo -e "x-ui restart - Restart x-ui"
|
||||
echo -e "x-ui status - Show x-ui status"
|
||||
echo -e "x-ui enable - Enable x-ui on system startup"
|
||||
echo -e "x-ui disable - Disable x-ui on system startup"
|
||||
echo -e "x-ui log - Check x-ui logs"
|
||||
echo -e "x-ui update - Update x-ui"
|
||||
echo -e "x-ui install - Install x-ui"
|
||||
echo -e "x-ui uninstall - Uninstall x-ui"
|
||||
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
|
||||
install_x-ui $1
|
||||
install_x-ui $1
|
||||
@@ -1,25 +1,47 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"github.com/op/go-logging"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/op/go-logging"
|
||||
)
|
||||
|
||||
var logger *logging.Logger
|
||||
var (
|
||||
logger *logging.Logger
|
||||
logBuffer []struct {
|
||||
time string
|
||||
level logging.Level
|
||||
log string
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
InitLogger(logging.INFO)
|
||||
}
|
||||
|
||||
func InitLogger(level logging.Level) {
|
||||
format := logging.MustStringFormatter(
|
||||
`%{time:2006/01/02 15:04:05} %{level} - %{message}`,
|
||||
)
|
||||
newLogger := logging.MustGetLogger("x-ui")
|
||||
backend := logging.NewLogBackend(os.Stderr, "", 0)
|
||||
var err error
|
||||
var backend logging.Backend
|
||||
var format logging.Formatter
|
||||
ppid := os.Getppid()
|
||||
|
||||
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}`)
|
||||
}
|
||||
|
||||
backendFormatter := logging.NewBackendFormatter(backend, format)
|
||||
backendLeveled := logging.AddModuleLevel(backendFormatter)
|
||||
backendLeveled.SetLevel(level, "")
|
||||
backendLeveled.SetLevel(level, "x-ui")
|
||||
newLogger.SetBackend(backendLeveled)
|
||||
|
||||
logger = newLogger
|
||||
@@ -27,32 +49,80 @@ func InitLogger(level logging.Level) {
|
||||
|
||||
func Debug(args ...interface{}) {
|
||||
logger.Debug(args...)
|
||||
addToBuffer("DEBUG", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func Debugf(format string, args ...interface{}) {
|
||||
logger.Debugf(format, args...)
|
||||
addToBuffer("DEBUG", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func Info(args ...interface{}) {
|
||||
logger.Info(args...)
|
||||
addToBuffer("INFO", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func Infof(format string, args ...interface{}) {
|
||||
logger.Infof(format, args...)
|
||||
addToBuffer("INFO", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func Notice(args ...interface{}) {
|
||||
logger.Notice(args...)
|
||||
addToBuffer("NOTICE", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func Noticef(format string, args ...interface{}) {
|
||||
logger.Noticef(format, args...)
|
||||
addToBuffer("NOTICE", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func Warning(args ...interface{}) {
|
||||
logger.Warning(args...)
|
||||
addToBuffer("WARNING", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func Warningf(format string, args ...interface{}) {
|
||||
logger.Warningf(format, args...)
|
||||
addToBuffer("WARNING", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func Error(args ...interface{}) {
|
||||
logger.Error(args...)
|
||||
addToBuffer("ERROR", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func Errorf(format string, args ...interface{}) {
|
||||
logger.Errorf(format, args...)
|
||||
addToBuffer("ERROR", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func addToBuffer(level string, newLog string) {
|
||||
t := time.Now()
|
||||
if len(logBuffer) >= 10240 {
|
||||
logBuffer = logBuffer[1:]
|
||||
}
|
||||
|
||||
logLevel, _ := logging.LogLevel(level)
|
||||
logBuffer = append(logBuffer, struct {
|
||||
time string
|
||||
level logging.Level
|
||||
log string
|
||||
}{
|
||||
time: t.Format("2006/01/02 15:04:05"),
|
||||
level: logLevel,
|
||||
log: newLog,
|
||||
})
|
||||
}
|
||||
|
||||
func GetLogs(c int, level string) []string {
|
||||
var output []string
|
||||
logLevel, _ := logging.LogLevel(level)
|
||||
|
||||
for i := len(logBuffer) - 1; i >= 0 && len(output) <= c; i-- {
|
||||
if logBuffer[i].level <= logLevel {
|
||||
output = append(output, fmt.Sprintf("%s %s - %s", logBuffer[i].time, logBuffer[i].level, logBuffer[i].log))
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
334
main.go
@@ -8,10 +8,11 @@ import (
|
||||
"os/signal"
|
||||
"syscall"
|
||||
_ "unsafe"
|
||||
|
||||
"x-ui/config"
|
||||
"x-ui/database"
|
||||
"x-ui/logger"
|
||||
"x-ui/v2ui"
|
||||
"x-ui/sub"
|
||||
"x-ui/web"
|
||||
"x-ui/web/global"
|
||||
"x-ui/web/service"
|
||||
@@ -20,57 +21,87 @@ import (
|
||||
)
|
||||
|
||||
func runWebServer() {
|
||||
log.Printf("%v %v", config.GetName(), config.GetVersion())
|
||||
log.Printf("Starting %v %v", config.GetName(), config.GetVersion())
|
||||
|
||||
switch config.GetLogLevel() {
|
||||
case config.Debug:
|
||||
logger.InitLogger(logging.DEBUG)
|
||||
case config.Info:
|
||||
logger.InitLogger(logging.INFO)
|
||||
case config.Notice:
|
||||
logger.InitLogger(logging.NOTICE)
|
||||
case config.Warn:
|
||||
logger.InitLogger(logging.WARNING)
|
||||
case config.Error:
|
||||
logger.InitLogger(logging.ERROR)
|
||||
default:
|
||||
log.Fatal("unknown log level:", config.GetLogLevel())
|
||||
log.Fatalf("Unknown log level: %v", config.GetLogLevel())
|
||||
}
|
||||
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("Error initializing database: %v", err)
|
||||
}
|
||||
|
||||
var server *web.Server
|
||||
|
||||
server = web.NewServer()
|
||||
global.SetWebServer(server)
|
||||
err = server.Start()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
log.Fatalf("Error starting web server: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var subServer *sub.Server
|
||||
subServer = sub.NewServer()
|
||||
global.SetSubServer(subServer)
|
||||
err = subServer.Start()
|
||||
if err != nil {
|
||||
log.Fatalf("Error starting sub server: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
//信号量捕获处理
|
||||
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGKILL)
|
||||
// Trap shutdown signals
|
||||
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM)
|
||||
for {
|
||||
sig := <-sigCh
|
||||
|
||||
switch sig {
|
||||
case syscall.SIGHUP:
|
||||
logger.Info("Received SIGHUP signal. Restarting servers...")
|
||||
|
||||
err := server.Stop()
|
||||
if err != nil {
|
||||
logger.Warning("stop server err:", err)
|
||||
logger.Debug("Error stopping web server:", err)
|
||||
}
|
||||
err = subServer.Stop()
|
||||
if err != nil {
|
||||
logger.Debug("Error stopping sub server:", err)
|
||||
}
|
||||
|
||||
server = web.NewServer()
|
||||
global.SetWebServer(server)
|
||||
err = server.Start()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
log.Fatalf("Error restarting web server: %v", err)
|
||||
return
|
||||
}
|
||||
log.Println("Web server restarted successfully.")
|
||||
|
||||
subServer = sub.NewServer()
|
||||
global.SetSubServer(subServer)
|
||||
err = subServer.Start()
|
||||
if err != nil {
|
||||
log.Fatalf("Error restarting sub server: %v", err)
|
||||
return
|
||||
}
|
||||
log.Println("Sub server restarted successfully.")
|
||||
|
||||
default:
|
||||
server.Stop()
|
||||
subServer.Stop()
|
||||
log.Println("Shutting down servers.")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -79,16 +110,16 @@ func runWebServer() {
|
||||
func resetSetting() {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println("Failed to initialize database:", err)
|
||||
return
|
||||
}
|
||||
|
||||
settingService := service.SettingService{}
|
||||
err = settingService.ResetSettings()
|
||||
if err != nil {
|
||||
fmt.Println("reset setting failed:", err)
|
||||
fmt.Println("Failed to reset settings:", err)
|
||||
} else {
|
||||
fmt.Println("reset setting success")
|
||||
fmt.Println("Settings successfully reset.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,49 +128,71 @@ func showSetting(show bool) {
|
||||
settingService := service.SettingService{}
|
||||
port, err := settingService.GetPort()
|
||||
if err != nil {
|
||||
fmt.Println("get current port fialed,error info:", err)
|
||||
fmt.Println("get current port failed, error info:", err)
|
||||
}
|
||||
|
||||
webBasePath, err := settingService.GetBasePath()
|
||||
if err != nil {
|
||||
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)
|
||||
fmt.Println("get current user info failed, error info:", err)
|
||||
}
|
||||
|
||||
username := userModel.Username
|
||||
userpasswd := userModel.Password
|
||||
if (username == "") || (userpasswd == "") {
|
||||
if username == "" || userpasswd == "" {
|
||||
fmt.Println("current username or password is empty")
|
||||
}
|
||||
fmt.Println("current pannel settings as follows:")
|
||||
|
||||
fmt.Println("current panel settings as follows:")
|
||||
if certFile == "" || keyFile == "" {
|
||||
fmt.Println("Warning: Panel is not secure with SSL")
|
||||
} else {
|
||||
fmt.Println("Panel is secure with SSL")
|
||||
}
|
||||
fmt.Println("username:", username)
|
||||
fmt.Println("userpasswd:", userpasswd)
|
||||
fmt.Println("password:", userpasswd)
|
||||
fmt.Println("port:", port)
|
||||
fmt.Println("webBasePath:", webBasePath)
|
||||
}
|
||||
}
|
||||
|
||||
func updateTgbotEnableSts(status bool) {
|
||||
settingService := service.SettingService{}
|
||||
currentTgSts, err := settingService.GetTgbotenabled()
|
||||
currentTgSts, err := settingService.GetTgbotEnabled()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
logger.Infof("current enabletgbot status[%v],need update to status[%v]", currentTgSts, status)
|
||||
if currentTgSts != status {
|
||||
err := settingService.SetTgbotenabled(status)
|
||||
err := settingService.SetTgbotEnabled(status)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
} else {
|
||||
logger.Infof("SetTgbotenabled[%v] success", status)
|
||||
logger.Infof("SetTgbotEnabled[%v] success", status)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println("Error initializing database:", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -148,62 +201,178 @@ func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime stri
|
||||
if tgBotToken != "" {
|
||||
err := settingService.SetTgBotToken(tgBotToken)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Printf("Error setting Telegram bot token: %v\n", err)
|
||||
return
|
||||
} else {
|
||||
logger.Info("updateTgbotSetting tgBotToken success")
|
||||
}
|
||||
logger.Info("Successfully updated Telegram bot token.")
|
||||
}
|
||||
|
||||
if tgBotRuntime != "" {
|
||||
err := settingService.SetTgbotRuntime(tgBotRuntime)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Printf("Error setting Telegram bot runtime: %v\n", err)
|
||||
return
|
||||
} else {
|
||||
logger.Infof("updateTgbotSetting tgBotRuntime[%s] success", tgBotRuntime)
|
||||
}
|
||||
logger.Infof("Successfully updated Telegram bot runtime to [%s].", tgBotRuntime)
|
||||
}
|
||||
|
||||
if tgBotChatid != "" {
|
||||
err := settingService.SetTgBotChatId(tgBotChatid)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Printf("Error setting Telegram bot chat ID: %v\n", err)
|
||||
return
|
||||
}
|
||||
logger.Info("Successfully updated Telegram bot chat ID.")
|
||||
}
|
||||
}
|
||||
|
||||
func updateSetting(port int, username string, password string, webBasePath string, listenIP string) {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
fmt.Println("Database initialization failed:", err)
|
||||
return
|
||||
}
|
||||
|
||||
settingService := service.SettingService{}
|
||||
userService := service.UserService{}
|
||||
|
||||
if port > 0 {
|
||||
err := settingService.SetPort(port)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to set port:", err)
|
||||
} else {
|
||||
logger.Info("updateTgbotSetting tgBotChatid success")
|
||||
fmt.Printf("Port set successfully: %v\n", port)
|
||||
}
|
||||
}
|
||||
|
||||
if username != "" || password != "" {
|
||||
err := userService.UpdateFirstUser(username, password)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to update username and password:", err)
|
||||
} else {
|
||||
fmt.Println("Username and password updated successfully")
|
||||
}
|
||||
}
|
||||
|
||||
if webBasePath != "" {
|
||||
err := settingService.SetBasePath(webBasePath)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to set base URI path:", err)
|
||||
} else {
|
||||
fmt.Println("Base URI path set 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateSetting(port int, username string, password string) {
|
||||
func updateCert(publicKey string, privateKey string) {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
settingService := service.SettingService{}
|
||||
if (privateKey != "" && publicKey != "") || (privateKey == "" && publicKey == "") {
|
||||
settingService := service.SettingService{}
|
||||
err = settingService.SetCertFile(publicKey)
|
||||
if err != nil {
|
||||
fmt.Println("set certificate public key failed:", err)
|
||||
} else {
|
||||
fmt.Println("set certificate public key success")
|
||||
}
|
||||
|
||||
if port > 0 {
|
||||
err := settingService.SetPort(port)
|
||||
err = settingService.SetKeyFile(privateKey)
|
||||
if err != nil {
|
||||
fmt.Println("set port failed:", err)
|
||||
fmt.Println("set certificate private key failed:", err)
|
||||
} else {
|
||||
fmt.Printf("set port %v success", port)
|
||||
fmt.Println("set certificate private key success")
|
||||
}
|
||||
} else {
|
||||
fmt.Println("both public and private key should be entered.")
|
||||
}
|
||||
if username != "" || password != "" {
|
||||
userService := service.UserService{}
|
||||
err := userService.UpdateFirstUser(username, password)
|
||||
}
|
||||
|
||||
func GetCertificate(getCert bool) {
|
||||
if getCert {
|
||||
settingService := service.SettingService{}
|
||||
certFile, err := settingService.GetCertFile()
|
||||
if err != nil {
|
||||
fmt.Println("set username and password failed:", err)
|
||||
} else {
|
||||
fmt.Println("set username and password success")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func migrateDb() {
|
||||
inboundService := service.InboundService{}
|
||||
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("Start migrating database...")
|
||||
inboundService.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.")
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
runWebServer()
|
||||
@@ -215,29 +384,39 @@ func main() {
|
||||
|
||||
runCmd := flag.NewFlagSet("run", flag.ExitOnError)
|
||||
|
||||
v2uiCmd := flag.NewFlagSet("v2-ui", flag.ExitOnError)
|
||||
var dbPath string
|
||||
v2uiCmd.StringVar(&dbPath, "db", "/etc/v2-ui/v2-ui.db", "set v2-ui db file path")
|
||||
|
||||
settingCmd := flag.NewFlagSet("setting", flag.ExitOnError)
|
||||
var port int
|
||||
var username string
|
||||
var password string
|
||||
var webBasePath string
|
||||
var listenIP string
|
||||
var getListen bool
|
||||
var webCertFile string
|
||||
var webKeyFile string
|
||||
var tgbottoken string
|
||||
var tgbotchatid string
|
||||
var enabletgbot bool
|
||||
var tgbotRuntime string
|
||||
var reset bool
|
||||
var show bool
|
||||
settingCmd.BoolVar(&reset, "reset", false, "reset all settings")
|
||||
settingCmd.BoolVar(&show, "show", false, "show current settings")
|
||||
settingCmd.IntVar(&port, "port", 0, "set panel port")
|
||||
settingCmd.StringVar(&username, "username", "", "set login username")
|
||||
settingCmd.StringVar(&password, "password", "", "set login password")
|
||||
settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegrame bot token")
|
||||
settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegrame bot cron time")
|
||||
settingCmd.StringVar(&tgbotchatid, "tgbotchatid", "", "set telegrame bot chat id")
|
||||
settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "enable telegram bot notify")
|
||||
var getCert bool
|
||||
var remove_secret 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(&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")
|
||||
settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "Set cron time for Telegram bot notifications")
|
||||
settingCmd.StringVar(&tgbotchatid, "tgbotchatid", "", "Set chat ID for Telegram bot notifications")
|
||||
settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "Enable notifications via Telegram bot")
|
||||
|
||||
oldUsage := flag.Usage
|
||||
flag.Usage = func() {
|
||||
@@ -245,7 +424,7 @@ func main() {
|
||||
fmt.Println()
|
||||
fmt.Println("Commands:")
|
||||
fmt.Println(" run run web panel")
|
||||
fmt.Println(" v2-ui migrate form v2-ui")
|
||||
fmt.Println(" migrate migrate form other/old x-ui")
|
||||
fmt.Println(" setting set settings")
|
||||
}
|
||||
|
||||
@@ -263,16 +442,8 @@ func main() {
|
||||
return
|
||||
}
|
||||
runWebServer()
|
||||
case "v2-ui":
|
||||
err := v2uiCmd.Parse(os.Args[2:])
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
err = v2ui.MigrateFromV2UI(dbPath)
|
||||
if err != nil {
|
||||
fmt.Println("migrate from v2-ui failed:", err)
|
||||
}
|
||||
case "migrate":
|
||||
migrateDb()
|
||||
case "setting":
|
||||
err := settingCmd.Parse(os.Args[2:])
|
||||
if err != nil {
|
||||
@@ -282,21 +453,42 @@ func main() {
|
||||
if reset {
|
||||
resetSetting()
|
||||
} else {
|
||||
updateSetting(port, username, password)
|
||||
updateSetting(port, username, password, webBasePath, listenIP)
|
||||
}
|
||||
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)
|
||||
}
|
||||
case "cert":
|
||||
err := settingCmd.Parse(os.Args[2:])
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
if reset {
|
||||
updateCert("", "")
|
||||
} else {
|
||||
updateCert(webCertFile, webKeyFile)
|
||||
}
|
||||
default:
|
||||
fmt.Println("except 'run' or 'v2-ui' or 'setting' subcommands")
|
||||
fmt.Println("Invalid subcommands")
|
||||
fmt.Println()
|
||||
runCmd.Usage()
|
||||
fmt.Println()
|
||||
v2uiCmd.Usage()
|
||||
fmt.Println()
|
||||
settingCmd.Usage()
|
||||
}
|
||||
}
|
||||
|
||||
BIN
media/01-overview-dark.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
media/01-overview-light.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
media/02-inbounds-dark.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
media/02-inbounds-light.png
Normal file
|
After Width: | Height: | Size: 106 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: 84 KiB |
BIN
media/05-settings-light.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
media/06-configs-dark.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
media/06-configs-light.png
Normal file
|
After Width: | Height: | Size: 66 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: 50 KiB |
BIN
media/2.png
|
Before Width: | Height: | Size: 63 KiB |
BIN
media/3.png
|
Before Width: | Height: | Size: 59 KiB |
BIN
media/3x-ui-dark.png
Normal file
|
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: 157 KiB |
BIN
media/APIKey1.PNG
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
media/APIKey2.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
media/DetailEnter.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
media/botfather.png
Normal file
|
After Width: | Height: | Size: 544 KiB |
BIN
media/buymeacoffe.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
@@ -1,82 +0,0 @@
|
||||
{
|
||||
"log": {
|
||||
"loglevel": "warning",
|
||||
"access": "./access.log"
|
||||
},
|
||||
"api": {
|
||||
"services": [
|
||||
"HandlerService",
|
||||
"LoggerService",
|
||||
"StatsService"
|
||||
],
|
||||
"tag": "api"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"listen": "127.0.0.1",
|
||||
"port": 62789,
|
||||
"protocol": "dokodemo-door",
|
||||
"settings": {
|
||||
"address": "127.0.0.1"
|
||||
},
|
||||
"tag": "api"
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"protocol": "freedom",
|
||||
"settings": {}
|
||||
},
|
||||
{
|
||||
"protocol": "blackhole",
|
||||
"settings": {},
|
||||
"tag": "blocked"
|
||||
}
|
||||
],
|
||||
"policy": {
|
||||
"levels": {
|
||||
"0": {
|
||||
"statsUserUplink": true,
|
||||
"statsUserDownlink": true
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"statsInboundDownlink": true,
|
||||
"statsInboundUplink": true
|
||||
}
|
||||
},
|
||||
"routing": {
|
||||
"domainStrategy": "IPIfNonMatch",
|
||||
"rules": [
|
||||
{
|
||||
"inboundTag": [
|
||||
"api"
|
||||
],
|
||||
"outboundTag": "api",
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"outboundTag": "blocked",
|
||||
"protocol": [
|
||||
"bittorrent"
|
||||
],
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"outboundTag": "blocked",
|
||||
"ip": [
|
||||
"geoip:private"
|
||||
],
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"outboundTag": "blocked",
|
||||
"ip": [
|
||||
"geoip:ir"
|
||||
],
|
||||
"type": "field"
|
||||
}
|
||||
]
|
||||
},
|
||||
"stats": {}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
{
|
||||
"log": {
|
||||
"loglevel": "warning",
|
||||
"access": "./access.log"
|
||||
},
|
||||
"api": {
|
||||
"services": [
|
||||
"HandlerService",
|
||||
"LoggerService",
|
||||
"StatsService"
|
||||
],
|
||||
"tag": "api"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"listen": "127.0.0.1",
|
||||
"port": 62789,
|
||||
"protocol": "dokodemo-door",
|
||||
"settings": {
|
||||
"address": "127.0.0.1"
|
||||
},
|
||||
"tag": "api"
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"protocol": "freedom",
|
||||
"settings": {}
|
||||
},
|
||||
{
|
||||
"protocol": "blackhole",
|
||||
"settings": {},
|
||||
"tag": "blocked"
|
||||
}
|
||||
],
|
||||
"policy": {
|
||||
"levels": {
|
||||
"0": {
|
||||
"statsUserUplink": true,
|
||||
"statsUserDownlink": true
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"statsInboundDownlink": true,
|
||||
"statsInboundUplink": true
|
||||
}
|
||||
},
|
||||
"routing": {
|
||||
"domainStrategy": "IPIfNonMatch",
|
||||
"rules": [
|
||||
{
|
||||
"inboundTag": [
|
||||
"api"
|
||||
],
|
||||
"outboundTag": "api",
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"ip": [
|
||||
"geoip:private"
|
||||
],
|
||||
"outboundTag": "blocked",
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"outboundTag": "blocked",
|
||||
"protocol": [
|
||||
"bittorrent"
|
||||
],
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"outboundTag": "blocked",
|
||||
"domain": [
|
||||
"regexp:.+.ir$",
|
||||
"ext:iran.dat:ir",
|
||||
"ext:iran.dat:other"
|
||||
],
|
||||
"type": "field"
|
||||
}
|
||||
]
|
||||
},
|
||||
"stats": {}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
{
|
||||
"log": {
|
||||
"loglevel": "warning",
|
||||
"access": "./access.log"
|
||||
},
|
||||
"api": {
|
||||
"services": [
|
||||
"HandlerService",
|
||||
"LoggerService",
|
||||
"StatsService"
|
||||
],
|
||||
"tag": "api"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"listen": "127.0.0.1",
|
||||
"port": 62789,
|
||||
"protocol": "dokodemo-door",
|
||||
"settings": {
|
||||
"address": "127.0.0.1"
|
||||
},
|
||||
"tag": "api"
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"protocol": "freedom",
|
||||
"settings": {}
|
||||
},
|
||||
{
|
||||
"protocol": "blackhole",
|
||||
"settings": {},
|
||||
"tag": "blocked"
|
||||
}
|
||||
],
|
||||
"policy": {
|
||||
"levels": {
|
||||
"0": {
|
||||
"statsUserUplink": true,
|
||||
"statsUserDownlink": true
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"statsInboundDownlink": true,
|
||||
"statsInboundUplink": true
|
||||
}
|
||||
},
|
||||
"routing": {
|
||||
"domainStrategy": "IPIfNonMatch",
|
||||
"rules": [
|
||||
{
|
||||
"inboundTag": [
|
||||
"api"
|
||||
],
|
||||
"outboundTag": "api",
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"outboundTag": "blocked",
|
||||
"ip": [
|
||||
"geoip:private"
|
||||
],
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"outboundTag": "blocked",
|
||||
"protocol": [
|
||||
"bittorrent"
|
||||
],
|
||||
"type": "field"
|
||||
}
|
||||
]
|
||||
},
|
||||
"stats": {}
|
||||
}
|
||||
BIN
media/newbot.png
Normal file
|
After Width: | Height: | Size: 455 KiB |
BIN
media/panel-bot-config.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
media/token.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
media/user-id.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
90
sub/default.json
Normal file
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"remarks": "",
|
||||
"dns": {
|
||||
"tag": "dns_out",
|
||||
"queryStrategy": "UseIP",
|
||||
"servers": [
|
||||
{
|
||||
"address": "8.8.8.8",
|
||||
"skipFallback": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"port": 10808,
|
||||
"protocol": "socks",
|
||||
"settings": {
|
||||
"auth": "noauth",
|
||||
"udp": true,
|
||||
"userLevel": 8
|
||||
},
|
||||
"sniffing": {
|
||||
"destOverride": [
|
||||
"http",
|
||||
"tls",
|
||||
"quic",
|
||||
"fakedns"
|
||||
],
|
||||
"enabled": true
|
||||
},
|
||||
"tag": "socks"
|
||||
},
|
||||
{
|
||||
"port": 10809,
|
||||
"protocol": "http",
|
||||
"settings": {
|
||||
"userLevel": 8
|
||||
},
|
||||
"tag": "http"
|
||||
}
|
||||
],
|
||||
"log": {
|
||||
"loglevel": "warning"
|
||||
},
|
||||
"outbounds": [
|
||||
{
|
||||
"tag": "direct",
|
||||
"protocol": "freedom",
|
||||
"settings": {
|
||||
"domainStrategy": "AsIs",
|
||||
"redirect": "",
|
||||
"noises": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "block",
|
||||
"protocol": "blackhole",
|
||||
"settings": {
|
||||
"response": {
|
||||
"type": "http"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"policy": {
|
||||
"levels": {
|
||||
"8": {
|
||||
"connIdle": 300,
|
||||
"downlinkOnly": 1,
|
||||
"handshake": 4,
|
||||
"uplinkOnly": 1
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"statsOutboundUplink": true,
|
||||
"statsOutboundDownlink": true
|
||||
}
|
||||
},
|
||||
"routing": {
|
||||
"domainStrategy": "AsIs",
|
||||
"rules": [
|
||||
{
|
||||
"type": "field",
|
||||
"network": "tcp,udp",
|
||||
"outboundTag": "proxy"
|
||||
}
|
||||
]
|
||||
},
|
||||
"stats": {}
|
||||
}
|
||||
208
sub/sub.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"x-ui/config"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
"x-ui/web/middleware"
|
||||
"x-ui/web/network"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
httpServer *http.Server
|
||||
listener net.Listener
|
||||
|
||||
sub *SUBController
|
||||
settingService service.SettingService
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Server{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
engine := gin.Default()
|
||||
|
||||
subDomain, err := s.settingService.GetSubDomain()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if subDomain != "" {
|
||||
engine.Use(middleware.DomainValidatorMiddleware(subDomain))
|
||||
}
|
||||
|
||||
LinksPath, err := s.settingService.GetSubPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
JsonPath, err := s.settingService.GetSubJsonPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Encrypt, err := s.settingService.GetSubEncrypt()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ShowInfo, err := s.settingService.GetSubShowInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
RemarkModel, err := s.settingService.GetRemarkModel()
|
||||
if err != nil {
|
||||
RemarkModel = "-ieo"
|
||||
}
|
||||
|
||||
SubUpdates, err := s.settingService.GetSubUpdates()
|
||||
if err != nil {
|
||||
SubUpdates = "10"
|
||||
}
|
||||
|
||||
SubJsonFragment, err := s.settingService.GetSubJsonFragment()
|
||||
if err != nil {
|
||||
SubJsonFragment = ""
|
||||
}
|
||||
|
||||
SubJsonNoises, err := s.settingService.GetSubJsonNoises()
|
||||
if err != nil {
|
||||
SubJsonNoises = ""
|
||||
}
|
||||
|
||||
SubJsonMux, err := s.settingService.GetSubJsonMux()
|
||||
if err != nil {
|
||||
SubJsonMux = ""
|
||||
}
|
||||
|
||||
SubJsonRules, err := s.settingService.GetSubJsonRules()
|
||||
if err != nil {
|
||||
SubJsonRules = ""
|
||||
}
|
||||
|
||||
g := engine.Group("/")
|
||||
|
||||
s.sub = NewSUBController(
|
||||
g, LinksPath, JsonPath, Encrypt, ShowInfo, RemarkModel, SubUpdates,
|
||||
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules)
|
||||
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
func (s *Server) Start() (err error) {
|
||||
// This is an anonymous function, no function name
|
||||
defer func() {
|
||||
if err != nil {
|
||||
s.Stop()
|
||||
}
|
||||
}()
|
||||
|
||||
subEnable, err := s.settingService.GetSubEnable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !subEnable {
|
||||
return nil
|
||||
}
|
||||
|
||||
engine, err := s.initRouter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
certFile, err := s.settingService.GetSubCertFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keyFile, err := s.settingService.GetSubKeyFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listen, err := s.settingService.GetSubListen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
port, err := s.settingService.GetSubPort()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
|
||||
listener, err := net.Listen("tcp", listenAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if certFile != "" || keyFile != "" {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err == nil {
|
||||
c := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
listener = network.NewAutoHttpsListener(listener)
|
||||
listener = tls.NewListener(listener, c)
|
||||
logger.Info("Sub server running HTTPS on", listener.Addr())
|
||||
} else {
|
||||
logger.Error("Error loading certificates:", err)
|
||||
logger.Info("Sub server running HTTP on", listener.Addr())
|
||||
}
|
||||
} else {
|
||||
logger.Info("Sub server running HTTP on", listener.Addr())
|
||||
}
|
||||
s.listener = listener
|
||||
|
||||
s.httpServer = &http.Server{
|
||||
Handler: engine,
|
||||
}
|
||||
|
||||
go func() {
|
||||
s.httpServer.Serve(listener)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Stop() error {
|
||||
s.cancel()
|
||||
|
||||
var err1 error
|
||||
var err2 error
|
||||
if s.httpServer != nil {
|
||||
err1 = s.httpServer.Shutdown(s.ctx)
|
||||
}
|
||||
if s.listener != nil {
|
||||
err2 = s.listener.Close()
|
||||
}
|
||||
return common.Combine(err1, err2)
|
||||
}
|
||||
|
||||
func (s *Server) GetCtx() context.Context {
|
||||
return s.ctx
|
||||
}
|
||||
134
sub/subController.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SUBController struct {
|
||||
subPath string
|
||||
subJsonPath string
|
||||
subEncrypt bool
|
||||
updateInterval string
|
||||
|
||||
subService *SubService
|
||||
subJsonService *SubJsonService
|
||||
}
|
||||
|
||||
func NewSUBController(
|
||||
g *gin.RouterGroup,
|
||||
subPath string,
|
||||
jsonPath string,
|
||||
encrypt bool,
|
||||
showInfo bool,
|
||||
rModel string,
|
||||
update string,
|
||||
jsonFragment string,
|
||||
jsonNoise string,
|
||||
jsonMux string,
|
||||
jsonRules string,
|
||||
) *SUBController {
|
||||
sub := NewSubService(showInfo, rModel)
|
||||
a := &SUBController{
|
||||
subPath: subPath,
|
||||
subJsonPath: jsonPath,
|
||||
subEncrypt: encrypt,
|
||||
updateInterval: update,
|
||||
|
||||
subService: sub,
|
||||
subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub),
|
||||
}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (a *SUBController) subs(c *gin.Context) {
|
||||
subId := c.Param("subid")
|
||||
var host string
|
||||
if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil {
|
||||
host = h
|
||||
}
|
||||
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)
|
||||
if err != nil || len(subs) == 0 {
|
||||
c.String(400, "Error!")
|
||||
} else {
|
||||
result := ""
|
||||
for _, sub := range subs {
|
||||
result += sub + "\n"
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
if a.subEncrypt {
|
||||
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
|
||||
} else {
|
||||
c.String(200, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *SUBController) subJsons(c *gin.Context) {
|
||||
subId := c.Param("subid")
|
||||
var host string
|
||||
if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil {
|
||||
host = h
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
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)
|
||||
|
||||
c.String(200, jsonSub)
|
||||
}
|
||||
}
|
||||
|
||||
func getHostFromXFH(s string) (string, error) {
|
||||
if strings.Contains(s, ":") {
|
||||
realHost, _, err := net.SplitHostPort(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return realHost, nil
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
394
sub/subJsonService.go
Normal file
@@ -0,0 +1,394 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/json_util"
|
||||
"x-ui/util/random"
|
||||
"x-ui/web/service"
|
||||
"x-ui/xray"
|
||||
)
|
||||
|
||||
//go:embed default.json
|
||||
var defaultJson string
|
||||
|
||||
type SubJsonService struct {
|
||||
configJson map[string]interface{}
|
||||
defaultOutbounds []json_util.RawMessage
|
||||
fragment string
|
||||
noises string
|
||||
mux string
|
||||
|
||||
inboundService service.InboundService
|
||||
SubService *SubService
|
||||
}
|
||||
|
||||
func NewSubJsonService(fragment string, noises string, mux string, rules string, subService *SubService) *SubJsonService {
|
||||
var configJson map[string]interface{}
|
||||
var defaultOutbounds []json_util.RawMessage
|
||||
json.Unmarshal([]byte(defaultJson), &configJson)
|
||||
if outboundSlices, ok := configJson["outbounds"].([]interface{}); ok {
|
||||
for _, defaultOutbound := range outboundSlices {
|
||||
jsonBytes, _ := json.Marshal(defaultOutbound)
|
||||
defaultOutbounds = append(defaultOutbounds, jsonBytes)
|
||||
}
|
||||
}
|
||||
|
||||
if rules != "" {
|
||||
var newRules []interface{}
|
||||
routing, _ := configJson["routing"].(map[string]interface{})
|
||||
defaultRules, _ := routing["rules"].([]interface{})
|
||||
json.Unmarshal([]byte(rules), &newRules)
|
||||
defaultRules = append(newRules, defaultRules...)
|
||||
routing["rules"] = defaultRules
|
||||
configJson["routing"] = routing
|
||||
}
|
||||
|
||||
if fragment != "" {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SubJsonService) GetJson(subId string, host string) (string, string, error) {
|
||||
inbounds, err := s.SubService.getInboundsBySubId(subId)
|
||||
if err != nil || len(inbounds) == 0 {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
var header string
|
||||
var traffic xray.ClientTraffic
|
||||
var clientTraffics []xray.ClientTraffic
|
||||
var configArray []json_util.RawMessage
|
||||
|
||||
// Prepare Inbounds
|
||||
for _, inbound := range inbounds {
|
||||
clients, err := s.inboundService.GetClients(inbound)
|
||||
if err != nil {
|
||||
logger.Error("SubJsonService - GetClients: Unable to get clients from inbound")
|
||||
}
|
||||
if clients == nil {
|
||||
continue
|
||||
}
|
||||
if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
|
||||
listen, port, streamSettings, err := s.SubService.getFallbackMaster(inbound.Listen, inbound.StreamSettings)
|
||||
if err == nil {
|
||||
inbound.Listen = listen
|
||||
inbound.Port = port
|
||||
inbound.StreamSettings = streamSettings
|
||||
}
|
||||
}
|
||||
|
||||
for _, client := range clients {
|
||||
if client.Enable && client.SubID == subId {
|
||||
clientTraffics = append(clientTraffics, s.SubService.getClientTraffics(inbound.ClientStats, client.Email))
|
||||
newConfigs := s.getConfig(inbound, client, host)
|
||||
configArray = append(configArray, newConfigs...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(configArray) == 0 {
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
// Prepare statistics
|
||||
for index, clientTraffic := range clientTraffics {
|
||||
if index == 0 {
|
||||
traffic.Up = clientTraffic.Up
|
||||
traffic.Down = clientTraffic.Down
|
||||
traffic.Total = clientTraffic.Total
|
||||
if clientTraffic.ExpiryTime > 0 {
|
||||
traffic.ExpiryTime = clientTraffic.ExpiryTime
|
||||
}
|
||||
} else {
|
||||
traffic.Up += clientTraffic.Up
|
||||
traffic.Down += clientTraffic.Down
|
||||
if traffic.Total == 0 || clientTraffic.Total == 0 {
|
||||
traffic.Total = 0
|
||||
} else {
|
||||
traffic.Total += clientTraffic.Total
|
||||
}
|
||||
if clientTraffic.ExpiryTime != traffic.ExpiryTime {
|
||||
traffic.ExpiryTime = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Combile outbounds
|
||||
var finalJson []byte
|
||||
if len(configArray) == 1 {
|
||||
finalJson, _ = json.MarshalIndent(configArray[0], "", " ")
|
||||
} else {
|
||||
finalJson, _ = json.MarshalIndent(configArray, "", " ")
|
||||
}
|
||||
|
||||
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
|
||||
return string(finalJson), header, nil
|
||||
}
|
||||
|
||||
func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client, host string) []json_util.RawMessage {
|
||||
var newJsonArray []json_util.RawMessage
|
||||
stream := s.streamData(inbound.StreamSettings)
|
||||
|
||||
externalProxies, ok := stream["externalProxy"].([]interface{})
|
||||
if !ok || len(externalProxies) == 0 {
|
||||
externalProxies = []interface{}{
|
||||
map[string]interface{}{
|
||||
"forceTls": "same",
|
||||
"dest": host,
|
||||
"port": float64(inbound.Port),
|
||||
"remark": "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
delete(stream, "externalProxy")
|
||||
|
||||
for _, ep := range externalProxies {
|
||||
extPrxy := ep.(map[string]interface{})
|
||||
inbound.Listen = extPrxy["dest"].(string)
|
||||
inbound.Port = int(extPrxy["port"].(float64))
|
||||
newStream := stream
|
||||
switch extPrxy["forceTls"].(string) {
|
||||
case "tls":
|
||||
if newStream["security"] != "tls" {
|
||||
newStream["security"] = "tls"
|
||||
newStream["tslSettings"] = map[string]interface{}{}
|
||||
}
|
||||
case "none":
|
||||
if newStream["security"] != "none" {
|
||||
newStream["security"] = "none"
|
||||
delete(newStream, "tslSettings")
|
||||
}
|
||||
}
|
||||
streamSettings, _ := json.MarshalIndent(newStream, "", " ")
|
||||
|
||||
var newOutbounds []json_util.RawMessage
|
||||
|
||||
switch inbound.Protocol {
|
||||
case "vmess", "vless":
|
||||
newOutbounds = append(newOutbounds, s.genVnext(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["outbounds"] = newOutbounds
|
||||
newConfigJson["remarks"] = s.SubService.genRemark(inbound, client.Email, extPrxy["remark"].(string))
|
||||
|
||||
newConfig, _ := json.MarshalIndent(newConfigJson, "", " ")
|
||||
newJsonArray = append(newJsonArray, newConfig)
|
||||
}
|
||||
|
||||
return newJsonArray
|
||||
}
|
||||
|
||||
func (s *SubJsonService) streamData(stream string) map[string]interface{} {
|
||||
var streamSettings map[string]interface{}
|
||||
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{}))
|
||||
}
|
||||
delete(streamSettings, "sockopt")
|
||||
|
||||
if s.fragment != "" {
|
||||
streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "fragment", "tcpKeepAliveIdle": 100, "tcpMptcp": true, "penetrate": true}`)
|
||||
}
|
||||
|
||||
// remove proxy protocol
|
||||
network, _ := streamSettings["network"].(string)
|
||||
switch network {
|
||||
case "tcp":
|
||||
streamSettings["tcpSettings"] = s.removeAcceptProxy(streamSettings["tcpSettings"])
|
||||
case "ws":
|
||||
streamSettings["wsSettings"] = s.removeAcceptProxy(streamSettings["wsSettings"])
|
||||
case "httpupgrade":
|
||||
streamSettings["httpupgradeSettings"] = s.removeAcceptProxy(streamSettings["httpupgradeSettings"])
|
||||
}
|
||||
return streamSettings
|
||||
}
|
||||
|
||||
func (s *SubJsonService) removeAcceptProxy(setting interface{}) map[string]interface{} {
|
||||
netSettings, ok := setting.(map[string]interface{})
|
||||
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{})
|
||||
|
||||
tlsData["serverName"] = tData["serverName"]
|
||||
tlsData["alpn"] = tData["alpn"]
|
||||
if allowInsecure, ok := tlsClientSettings["allowInsecure"].(bool); ok {
|
||||
tlsData["allowInsecure"] = allowInsecure
|
||||
}
|
||||
if fingerprint, ok := tlsClientSettings["fingerprint"].(string); ok {
|
||||
tlsData["fingerprint"] = fingerprint
|
||||
}
|
||||
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{})
|
||||
|
||||
rltyData["show"] = false
|
||||
rltyData["publicKey"] = rltyClientSettings["publicKey"]
|
||||
rltyData["fingerprint"] = rltyClientSettings["fingerprint"]
|
||||
|
||||
// Set random data
|
||||
rltyData["spiderX"] = "/" + random.Seq(15)
|
||||
shortIds, ok := rData["shortIds"].([]interface{})
|
||||
if ok && len(shortIds) > 0 {
|
||||
rltyData["shortId"] = shortIds[random.Num(len(shortIds))].(string)
|
||||
} else {
|
||||
rltyData["shortId"] = ""
|
||||
}
|
||||
serverNames, ok := rData["serverNames"].([]interface{})
|
||||
if ok && len(serverNames) > 0 {
|
||||
rltyData["serverName"] = serverNames[random.Num(len(serverNames))].(string)
|
||||
} else {
|
||||
rltyData["serverName"] = ""
|
||||
}
|
||||
|
||||
return rltyData
|
||||
}
|
||||
|
||||
func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {
|
||||
outbound := Outbound{}
|
||||
usersData := make([]UserVnext, 1)
|
||||
|
||||
usersData[0].ID = client.ID
|
||||
usersData[0].Level = 8
|
||||
if inbound.Protocol == model.VMESS {
|
||||
usersData[0].Security = client.Security
|
||||
}
|
||||
if inbound.Protocol == model.VLESS {
|
||||
usersData[0].Flow = client.Flow
|
||||
usersData[0].Encryption = "none"
|
||||
}
|
||||
|
||||
vnextData := make([]VnextSetting, 1)
|
||||
vnextData[0] = VnextSetting{
|
||||
Address: inbound.Listen,
|
||||
Port: inbound.Port,
|
||||
Users: usersData,
|
||||
}
|
||||
|
||||
outbound.Protocol = string(inbound.Protocol)
|
||||
outbound.Tag = "proxy"
|
||||
if s.mux != "" {
|
||||
outbound.Mux = json_util.RawMessage(s.mux)
|
||||
}
|
||||
outbound.StreamSettings = streamSettings
|
||||
outbound.Settings = OutboundSettings{
|
||||
Vnext: vnextData,
|
||||
}
|
||||
|
||||
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{}
|
||||
|
||||
serverData := make([]ServerSetting, 1)
|
||||
serverData[0] = ServerSetting{
|
||||
Address: inbound.Listen,
|
||||
Port: inbound.Port,
|
||||
Level: 8,
|
||||
Password: client.Password,
|
||||
}
|
||||
|
||||
if inbound.Protocol == model.Shadowsocks {
|
||||
var inboundSettings map[string]interface{}
|
||||
json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
|
||||
method, _ := inboundSettings["method"].(string)
|
||||
serverData[0].Method = method
|
||||
|
||||
// server password in multi-user 2022 protocols
|
||||
if strings.HasPrefix(method, "2022") {
|
||||
if serverPassword, ok := inboundSettings["password"].(string); ok {
|
||||
serverData[0].Password = fmt.Sprintf("%s:%s", serverPassword, client.Password)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
outbound.Protocol = string(inbound.Protocol)
|
||||
outbound.Tag = "proxy"
|
||||
if s.mux != "" {
|
||||
outbound.Mux = json_util.RawMessage(s.mux)
|
||||
}
|
||||
outbound.StreamSettings = streamSettings
|
||||
outbound.Settings = OutboundSettings{
|
||||
Servers: serverData,
|
||||
}
|
||||
|
||||
result, _ := json.MarshalIndent(outbound, "", " ")
|
||||
return result
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type VnextSetting struct {
|
||||
Address string `json:"address"`
|
||||
Port int `json:"port"`
|
||||
Users []UserVnext `json:"users"`
|
||||
}
|
||||
|
||||
type UserVnext struct {
|
||||
Encryption string `json:"encryption,omitempty"`
|
||||
Flow string `json:"flow,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Security string `json:"security,omitempty"`
|
||||
Level int `json:"level"`
|
||||
}
|
||||
|
||||
type ServerSetting struct {
|
||||
Password string `json:"password"`
|
||||
Level int `json:"level"`
|
||||
Address string `json:"address"`
|
||||
Port int `json:"port"`
|
||||
Flow string `json:"flow,omitempty"`
|
||||
Method string `json:"method,omitempty"`
|
||||
}
|
||||
987
sub/subService.go
Normal file
@@ -0,0 +1,987 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
"x-ui/util/random"
|
||||
"x-ui/web/service"
|
||||
"x-ui/xray"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
type SubService struct {
|
||||
address string
|
||||
showInfo bool
|
||||
remarkModel string
|
||||
datepicker string
|
||||
inboundService service.InboundService
|
||||
settingService service.SettingService
|
||||
}
|
||||
|
||||
func NewSubService(showInfo bool, remarkModel string) *SubService {
|
||||
return &SubService{
|
||||
showInfo: showInfo,
|
||||
remarkModel: remarkModel,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SubService) GetSubs(subId string, host string) ([]string, string, error) {
|
||||
s.address = host
|
||||
var result []string
|
||||
var header string
|
||||
var traffic xray.ClientTraffic
|
||||
var clientTraffics []xray.ClientTraffic
|
||||
inbounds, err := s.getInboundsBySubId(subId)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if len(inbounds) == 0 {
|
||||
return nil, "", common.NewError("No inbounds found with ", subId)
|
||||
}
|
||||
|
||||
s.datepicker, err = s.settingService.GetDatepicker()
|
||||
if err != nil {
|
||||
s.datepicker = "gregorian"
|
||||
}
|
||||
for _, inbound := range inbounds {
|
||||
clients, err := s.inboundService.GetClients(inbound)
|
||||
if err != nil {
|
||||
logger.Error("SubService - GetClients: Unable to get clients from inbound")
|
||||
}
|
||||
if clients == nil {
|
||||
continue
|
||||
}
|
||||
if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
|
||||
listen, port, streamSettings, err := s.getFallbackMaster(inbound.Listen, inbound.StreamSettings)
|
||||
if err == nil {
|
||||
inbound.Listen = listen
|
||||
inbound.Port = port
|
||||
inbound.StreamSettings = streamSettings
|
||||
}
|
||||
}
|
||||
for _, client := range clients {
|
||||
if client.Enable && client.SubID == subId {
|
||||
link := s.getLink(inbound, client.Email)
|
||||
result = append(result, link)
|
||||
clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare statistics
|
||||
for index, clientTraffic := range clientTraffics {
|
||||
if index == 0 {
|
||||
traffic.Up = clientTraffic.Up
|
||||
traffic.Down = clientTraffic.Down
|
||||
traffic.Total = clientTraffic.Total
|
||||
if clientTraffic.ExpiryTime > 0 {
|
||||
traffic.ExpiryTime = clientTraffic.ExpiryTime
|
||||
}
|
||||
} else {
|
||||
traffic.Up += clientTraffic.Up
|
||||
traffic.Down += clientTraffic.Down
|
||||
if traffic.Total == 0 || clientTraffic.Total == 0 {
|
||||
traffic.Total = 0
|
||||
} else {
|
||||
traffic.Total += clientTraffic.Total
|
||||
}
|
||||
if clientTraffic.ExpiryTime != traffic.ExpiryTime {
|
||||
traffic.ExpiryTime = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
|
||||
return result, header, nil
|
||||
}
|
||||
|
||||
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
|
||||
db := database.GetDB()
|
||||
var inbounds []*model.Inbound
|
||||
err := db.Model(model.Inbound{}).Preload("ClientStats").Where(`id in (
|
||||
SELECT DISTINCT inbounds.id
|
||||
FROM inbounds,
|
||||
JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
|
||||
WHERE
|
||||
protocol in ('vmess','vless','trojan','shadowsocks')
|
||||
AND JSON_EXTRACT(client.value, '$.subId') = ? AND enable = ?
|
||||
)`, subId, true).Find(&inbounds).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return inbounds, nil
|
||||
}
|
||||
|
||||
func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email string) xray.ClientTraffic {
|
||||
for _, traffic := range traffics {
|
||||
if traffic.Email == email {
|
||||
return traffic
|
||||
}
|
||||
}
|
||||
return xray.ClientTraffic{}
|
||||
}
|
||||
|
||||
func (s *SubService) getFallbackMaster(dest string, streamSettings string) (string, int, string, error) {
|
||||
db := database.GetDB()
|
||||
var inbound *model.Inbound
|
||||
err := db.Model(model.Inbound{}).
|
||||
Where("JSON_TYPE(settings, '$.fallbacks') = 'array'").
|
||||
Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", dest).
|
||||
Find(&inbound).Error
|
||||
if err != nil {
|
||||
return "", 0, "", err
|
||||
}
|
||||
|
||||
var stream map[string]interface{}
|
||||
json.Unmarshal([]byte(streamSettings), &stream)
|
||||
var masterStream map[string]interface{}
|
||||
json.Unmarshal([]byte(inbound.StreamSettings), &masterStream)
|
||||
stream["security"] = masterStream["security"]
|
||||
stream["tlsSettings"] = masterStream["tlsSettings"]
|
||||
stream["externalProxy"] = masterStream["externalProxy"]
|
||||
modifiedStream, _ := json.MarshalIndent(stream, "", " ")
|
||||
|
||||
return inbound.Listen, inbound.Port, string(modifiedStream), nil
|
||||
}
|
||||
|
||||
func (s *SubService) getLink(inbound *model.Inbound, email string) string {
|
||||
switch inbound.Protocol {
|
||||
case "vmess":
|
||||
return s.genVmessLink(inbound, email)
|
||||
case "vless":
|
||||
return s.genVlessLink(inbound, email)
|
||||
case "trojan":
|
||||
return s.genTrojanLink(inbound, email)
|
||||
case "shadowsocks":
|
||||
return s.genShadowsocksLink(inbound, email)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
if inbound.Protocol != model.VMESS {
|
||||
return ""
|
||||
}
|
||||
obj := map[string]interface{}{
|
||||
"v": "2",
|
||||
"add": s.address,
|
||||
"port": inbound.Port,
|
||||
"type": "none",
|
||||
}
|
||||
var stream map[string]interface{}
|
||||
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||
network, _ := stream["network"].(string)
|
||||
obj["net"] = network
|
||||
switch network {
|
||||
case "tcp":
|
||||
tcp, _ := stream["tcpSettings"].(map[string]interface{})
|
||||
header, _ := tcp["header"].(map[string]interface{})
|
||||
typeStr, _ := header["type"].(string)
|
||||
obj["type"] = typeStr
|
||||
if typeStr == "http" {
|
||||
request := header["request"].(map[string]interface{})
|
||||
requestPath, _ := request["path"].([]interface{})
|
||||
obj["path"] = requestPath[0].(string)
|
||||
headers, _ := request["headers"].(map[string]interface{})
|
||||
obj["host"] = searchHost(headers)
|
||||
}
|
||||
case "kcp":
|
||||
kcp, _ := stream["kcpSettings"].(map[string]interface{})
|
||||
header, _ := kcp["header"].(map[string]interface{})
|
||||
obj["type"], _ = header["type"].(string)
|
||||
obj["path"], _ = kcp["seed"].(string)
|
||||
case "ws":
|
||||
ws, _ := stream["wsSettings"].(map[string]interface{})
|
||||
obj["path"] = ws["path"].(string)
|
||||
if host, ok := ws["host"].(string); ok && len(host) > 0 {
|
||||
obj["host"] = host
|
||||
} else {
|
||||
headers, _ := ws["headers"].(map[string]interface{})
|
||||
obj["host"] = searchHost(headers)
|
||||
}
|
||||
case "grpc":
|
||||
grpc, _ := stream["grpcSettings"].(map[string]interface{})
|
||||
obj["path"] = grpc["serviceName"].(string)
|
||||
obj["authority"] = grpc["authority"].(string)
|
||||
if grpc["multiMode"].(bool) {
|
||||
obj["type"] = "multi"
|
||||
}
|
||||
case "httpupgrade":
|
||||
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]interface{})
|
||||
obj["path"] = httpupgrade["path"].(string)
|
||||
if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 {
|
||||
obj["host"] = host
|
||||
} else {
|
||||
headers, _ := httpupgrade["headers"].(map[string]interface{})
|
||||
obj["host"] = searchHost(headers)
|
||||
}
|
||||
case "xhttp":
|
||||
xhttp, _ := stream["xhttpSettings"].(map[string]interface{})
|
||||
obj["path"] = xhttp["path"].(string)
|
||||
if host, ok := xhttp["host"].(string); ok && len(host) > 0 {
|
||||
obj["host"] = host
|
||||
} else {
|
||||
headers, _ := xhttp["headers"].(map[string]interface{})
|
||||
obj["host"] = searchHost(headers)
|
||||
}
|
||||
obj["mode"] = xhttp["mode"].(string)
|
||||
}
|
||||
security, _ := stream["security"].(string)
|
||||
obj["tls"] = security
|
||||
if security == "tls" {
|
||||
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||
alpns, _ := tlsSetting["alpn"].([]interface{})
|
||||
if len(alpns) > 0 {
|
||||
var alpn []string
|
||||
for _, a := range alpns {
|
||||
alpn = append(alpn, a.(string))
|
||||
}
|
||||
obj["alpn"] = strings.Join(alpn, ",")
|
||||
}
|
||||
if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
|
||||
obj["sni"], _ = sniValue.(string)
|
||||
}
|
||||
|
||||
tlsSettings, _ := searchKey(tlsSetting, "settings")
|
||||
if tlsSetting != nil {
|
||||
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||
obj["fp"], _ = fpValue.(string)
|
||||
}
|
||||
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
||||
obj["allowInsecure"], _ = insecure.(bool)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clients, _ := s.inboundService.GetClients(inbound)
|
||||
clientIndex := -1
|
||||
for i, client := range clients {
|
||||
if client.Email == email {
|
||||
clientIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
obj["id"] = clients[clientIndex].ID
|
||||
obj["scy"] = clients[clientIndex].Security
|
||||
|
||||
externalProxies, _ := stream["externalProxy"].([]interface{})
|
||||
|
||||
if len(externalProxies) > 0 {
|
||||
links := ""
|
||||
for index, externalProxy := range externalProxies {
|
||||
ep, _ := externalProxy.(map[string]interface{})
|
||||
newSecurity, _ := ep["forceTls"].(string)
|
||||
newObj := map[string]interface{}{}
|
||||
for key, value := range obj {
|
||||
if !(newSecurity == "none" && (key == "alpn" || key == "sni" || key == "fp" || key == "allowInsecure")) {
|
||||
newObj[key] = value
|
||||
}
|
||||
}
|
||||
newObj["ps"] = s.genRemark(inbound, email, ep["remark"].(string))
|
||||
newObj["add"] = ep["dest"].(string)
|
||||
newObj["port"] = int(ep["port"].(float64))
|
||||
|
||||
if newSecurity != "same" {
|
||||
newObj["tls"] = newSecurity
|
||||
}
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
}
|
||||
jsonStr, _ := json.MarshalIndent(newObj, "", " ")
|
||||
links += "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
obj["ps"] = s.genRemark(inbound, email, "")
|
||||
|
||||
jsonStr, _ := json.MarshalIndent(obj, "", " ")
|
||||
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
|
||||
}
|
||||
|
||||
func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
address := s.address
|
||||
if inbound.Protocol != model.VLESS {
|
||||
return ""
|
||||
}
|
||||
var stream map[string]interface{}
|
||||
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||
clients, _ := s.inboundService.GetClients(inbound)
|
||||
clientIndex := -1
|
||||
for i, client := range clients {
|
||||
if client.Email == email {
|
||||
clientIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
uuid := clients[clientIndex].ID
|
||||
port := inbound.Port
|
||||
streamNetwork := stream["network"].(string)
|
||||
params := make(map[string]string)
|
||||
params["type"] = streamNetwork
|
||||
|
||||
switch streamNetwork {
|
||||
case "tcp":
|
||||
tcp, _ := stream["tcpSettings"].(map[string]interface{})
|
||||
header, _ := tcp["header"].(map[string]interface{})
|
||||
typeStr, _ := header["type"].(string)
|
||||
if typeStr == "http" {
|
||||
request := header["request"].(map[string]interface{})
|
||||
requestPath, _ := request["path"].([]interface{})
|
||||
params["path"] = requestPath[0].(string)
|
||||
headers, _ := request["headers"].(map[string]interface{})
|
||||
params["host"] = searchHost(headers)
|
||||
params["headerType"] = "http"
|
||||
}
|
||||
case "kcp":
|
||||
kcp, _ := stream["kcpSettings"].(map[string]interface{})
|
||||
header, _ := kcp["header"].(map[string]interface{})
|
||||
params["headerType"] = header["type"].(string)
|
||||
params["seed"] = kcp["seed"].(string)
|
||||
case "ws":
|
||||
ws, _ := stream["wsSettings"].(map[string]interface{})
|
||||
params["path"] = ws["path"].(string)
|
||||
if host, ok := ws["host"].(string); ok && len(host) > 0 {
|
||||
params["host"] = host
|
||||
} else {
|
||||
headers, _ := ws["headers"].(map[string]interface{})
|
||||
params["host"] = searchHost(headers)
|
||||
}
|
||||
case "grpc":
|
||||
grpc, _ := stream["grpcSettings"].(map[string]interface{})
|
||||
params["serviceName"] = grpc["serviceName"].(string)
|
||||
params["authority"], _ = grpc["authority"].(string)
|
||||
if grpc["multiMode"].(bool) {
|
||||
params["mode"] = "multi"
|
||||
}
|
||||
case "httpupgrade":
|
||||
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]interface{})
|
||||
params["path"] = httpupgrade["path"].(string)
|
||||
if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 {
|
||||
params["host"] = host
|
||||
} else {
|
||||
headers, _ := httpupgrade["headers"].(map[string]interface{})
|
||||
params["host"] = searchHost(headers)
|
||||
}
|
||||
case "xhttp":
|
||||
xhttp, _ := stream["xhttpSettings"].(map[string]interface{})
|
||||
params["path"] = xhttp["path"].(string)
|
||||
if host, ok := xhttp["host"].(string); ok && len(host) > 0 {
|
||||
params["host"] = host
|
||||
} else {
|
||||
headers, _ := xhttp["headers"].(map[string]interface{})
|
||||
params["host"] = searchHost(headers)
|
||||
}
|
||||
params["mode"] = xhttp["mode"].(string)
|
||||
}
|
||||
security, _ := stream["security"].(string)
|
||||
if security == "tls" {
|
||||
params["security"] = "tls"
|
||||
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||
alpns, _ := tlsSetting["alpn"].([]interface{})
|
||||
var alpn []string
|
||||
for _, a := range alpns {
|
||||
alpn = append(alpn, a.(string))
|
||||
}
|
||||
if len(alpn) > 0 {
|
||||
params["alpn"] = strings.Join(alpn, ",")
|
||||
}
|
||||
if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
|
||||
params["sni"], _ = sniValue.(string)
|
||||
}
|
||||
|
||||
tlsSettings, _ := searchKey(tlsSetting, "settings")
|
||||
if tlsSetting != nil {
|
||||
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||
params["fp"], _ = fpValue.(string)
|
||||
}
|
||||
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
||||
if insecure.(bool) {
|
||||
params["allowInsecure"] = "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
||||
params["flow"] = clients[clientIndex].Flow
|
||||
}
|
||||
}
|
||||
|
||||
if security == "reality" {
|
||||
params["security"] = "reality"
|
||||
realitySetting, _ := stream["realitySettings"].(map[string]interface{})
|
||||
realitySettings, _ := searchKey(realitySetting, "settings")
|
||||
if realitySetting != nil {
|
||||
if sniValue, ok := searchKey(realitySetting, "serverNames"); ok {
|
||||
sNames, _ := sniValue.([]interface{})
|
||||
params["sni"] = sNames[random.Num(len(sNames))].(string)
|
||||
}
|
||||
if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
|
||||
params["pbk"], _ = pbkValue.(string)
|
||||
}
|
||||
if sidValue, ok := searchKey(realitySetting, "shortIds"); ok {
|
||||
shortIds, _ := sidValue.([]interface{})
|
||||
params["sid"] = shortIds[random.Num(len(shortIds))].(string)
|
||||
}
|
||||
if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok {
|
||||
if fp, ok := fpValue.(string); ok && len(fp) > 0 {
|
||||
params["fp"] = fp
|
||||
}
|
||||
}
|
||||
params["spx"] = "/" + random.Seq(15)
|
||||
}
|
||||
|
||||
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
||||
params["flow"] = clients[clientIndex].Flow
|
||||
}
|
||||
}
|
||||
|
||||
if security != "tls" && security != "reality" {
|
||||
params["security"] = "none"
|
||||
}
|
||||
|
||||
externalProxies, _ := stream["externalProxy"].([]interface{})
|
||||
|
||||
if len(externalProxies) > 0 {
|
||||
links := ""
|
||||
for index, externalProxy := range externalProxies {
|
||||
ep, _ := externalProxy.(map[string]interface{})
|
||||
newSecurity, _ := ep["forceTls"].(string)
|
||||
dest, _ := ep["dest"].(string)
|
||||
port := int(ep["port"].(float64))
|
||||
link := fmt.Sprintf("vless://%s@%s:%d", uuid, dest, port)
|
||||
|
||||
if newSecurity != "same" {
|
||||
params["security"] = newSecurity
|
||||
} else {
|
||||
params["security"] = security
|
||||
}
|
||||
url, _ := url.Parse(link)
|
||||
q := url.Query()
|
||||
|
||||
for k, v := range params {
|
||||
if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) {
|
||||
q.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
||||
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
}
|
||||
links += url.String()
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port)
|
||||
url, _ := url.Parse(link)
|
||||
q := url.Query()
|
||||
|
||||
for k, v := range params {
|
||||
q.Add(k, v)
|
||||
}
|
||||
|
||||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, "")
|
||||
return url.String()
|
||||
}
|
||||
|
||||
func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string {
|
||||
address := s.address
|
||||
if inbound.Protocol != model.Trojan {
|
||||
return ""
|
||||
}
|
||||
var stream map[string]interface{}
|
||||
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||
clients, _ := s.inboundService.GetClients(inbound)
|
||||
clientIndex := -1
|
||||
for i, client := range clients {
|
||||
if client.Email == email {
|
||||
clientIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
password := clients[clientIndex].Password
|
||||
port := inbound.Port
|
||||
streamNetwork := stream["network"].(string)
|
||||
params := make(map[string]string)
|
||||
params["type"] = streamNetwork
|
||||
|
||||
switch streamNetwork {
|
||||
case "tcp":
|
||||
tcp, _ := stream["tcpSettings"].(map[string]interface{})
|
||||
header, _ := tcp["header"].(map[string]interface{})
|
||||
typeStr, _ := header["type"].(string)
|
||||
if typeStr == "http" {
|
||||
request := header["request"].(map[string]interface{})
|
||||
requestPath, _ := request["path"].([]interface{})
|
||||
params["path"] = requestPath[0].(string)
|
||||
headers, _ := request["headers"].(map[string]interface{})
|
||||
params["host"] = searchHost(headers)
|
||||
params["headerType"] = "http"
|
||||
}
|
||||
case "kcp":
|
||||
kcp, _ := stream["kcpSettings"].(map[string]interface{})
|
||||
header, _ := kcp["header"].(map[string]interface{})
|
||||
params["headerType"] = header["type"].(string)
|
||||
params["seed"] = kcp["seed"].(string)
|
||||
case "ws":
|
||||
ws, _ := stream["wsSettings"].(map[string]interface{})
|
||||
params["path"] = ws["path"].(string)
|
||||
if host, ok := ws["host"].(string); ok && len(host) > 0 {
|
||||
params["host"] = host
|
||||
} else {
|
||||
headers, _ := ws["headers"].(map[string]interface{})
|
||||
params["host"] = searchHost(headers)
|
||||
}
|
||||
case "grpc":
|
||||
grpc, _ := stream["grpcSettings"].(map[string]interface{})
|
||||
params["serviceName"] = grpc["serviceName"].(string)
|
||||
params["authority"], _ = grpc["authority"].(string)
|
||||
if grpc["multiMode"].(bool) {
|
||||
params["mode"] = "multi"
|
||||
}
|
||||
case "httpupgrade":
|
||||
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]interface{})
|
||||
params["path"] = httpupgrade["path"].(string)
|
||||
if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 {
|
||||
params["host"] = host
|
||||
} else {
|
||||
headers, _ := httpupgrade["headers"].(map[string]interface{})
|
||||
params["host"] = searchHost(headers)
|
||||
}
|
||||
case "xhttp":
|
||||
xhttp, _ := stream["xhttpSettings"].(map[string]interface{})
|
||||
params["path"] = xhttp["path"].(string)
|
||||
if host, ok := xhttp["host"].(string); ok && len(host) > 0 {
|
||||
params["host"] = host
|
||||
} else {
|
||||
headers, _ := xhttp["headers"].(map[string]interface{})
|
||||
params["host"] = searchHost(headers)
|
||||
}
|
||||
params["mode"] = xhttp["mode"].(string)
|
||||
}
|
||||
security, _ := stream["security"].(string)
|
||||
if security == "tls" {
|
||||
params["security"] = "tls"
|
||||
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||
alpns, _ := tlsSetting["alpn"].([]interface{})
|
||||
var alpn []string
|
||||
for _, a := range alpns {
|
||||
alpn = append(alpn, a.(string))
|
||||
}
|
||||
if len(alpn) > 0 {
|
||||
params["alpn"] = strings.Join(alpn, ",")
|
||||
}
|
||||
if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
|
||||
params["sni"], _ = sniValue.(string)
|
||||
}
|
||||
|
||||
tlsSettings, _ := searchKey(tlsSetting, "settings")
|
||||
if tlsSetting != nil {
|
||||
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||
params["fp"], _ = fpValue.(string)
|
||||
}
|
||||
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
||||
if insecure.(bool) {
|
||||
params["allowInsecure"] = "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if security == "reality" {
|
||||
params["security"] = "reality"
|
||||
realitySetting, _ := stream["realitySettings"].(map[string]interface{})
|
||||
realitySettings, _ := searchKey(realitySetting, "settings")
|
||||
if realitySetting != nil {
|
||||
if sniValue, ok := searchKey(realitySetting, "serverNames"); ok {
|
||||
sNames, _ := sniValue.([]interface{})
|
||||
params["sni"] = sNames[random.Num(len(sNames))].(string)
|
||||
}
|
||||
if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
|
||||
params["pbk"], _ = pbkValue.(string)
|
||||
}
|
||||
if sidValue, ok := searchKey(realitySetting, "shortIds"); ok {
|
||||
shortIds, _ := sidValue.([]interface{})
|
||||
params["sid"] = shortIds[random.Num(len(shortIds))].(string)
|
||||
}
|
||||
if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok {
|
||||
if fp, ok := fpValue.(string); ok && len(fp) > 0 {
|
||||
params["fp"] = fp
|
||||
}
|
||||
}
|
||||
params["spx"] = "/" + random.Seq(15)
|
||||
}
|
||||
|
||||
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
||||
params["flow"] = clients[clientIndex].Flow
|
||||
}
|
||||
}
|
||||
|
||||
if security != "tls" && security != "reality" {
|
||||
params["security"] = "none"
|
||||
}
|
||||
|
||||
externalProxies, _ := stream["externalProxy"].([]interface{})
|
||||
|
||||
if len(externalProxies) > 0 {
|
||||
links := ""
|
||||
for index, externalProxy := range externalProxies {
|
||||
ep, _ := externalProxy.(map[string]interface{})
|
||||
newSecurity, _ := ep["forceTls"].(string)
|
||||
dest, _ := ep["dest"].(string)
|
||||
port := int(ep["port"].(float64))
|
||||
link := fmt.Sprintf("trojan://%s@%s:%d", password, dest, port)
|
||||
|
||||
if newSecurity != "same" {
|
||||
params["security"] = newSecurity
|
||||
} else {
|
||||
params["security"] = security
|
||||
}
|
||||
url, _ := url.Parse(link)
|
||||
q := url.Query()
|
||||
|
||||
for k, v := range params {
|
||||
if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) {
|
||||
q.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
||||
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
}
|
||||
links += url.String()
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
link := fmt.Sprintf("trojan://%s@%s:%d", password, address, port)
|
||||
|
||||
url, _ := url.Parse(link)
|
||||
q := url.Query()
|
||||
|
||||
for k, v := range params {
|
||||
q.Add(k, v)
|
||||
}
|
||||
|
||||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, "")
|
||||
return url.String()
|
||||
}
|
||||
|
||||
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string {
|
||||
address := s.address
|
||||
if inbound.Protocol != model.Shadowsocks {
|
||||
return ""
|
||||
}
|
||||
var stream map[string]interface{}
|
||||
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||
clients, _ := s.inboundService.GetClients(inbound)
|
||||
|
||||
var settings map[string]interface{}
|
||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||
inboundPassword := settings["password"].(string)
|
||||
method := settings["method"].(string)
|
||||
clientIndex := -1
|
||||
for i, client := range clients {
|
||||
if client.Email == email {
|
||||
clientIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
streamNetwork := stream["network"].(string)
|
||||
params := make(map[string]string)
|
||||
params["type"] = streamNetwork
|
||||
|
||||
switch streamNetwork {
|
||||
case "tcp":
|
||||
tcp, _ := stream["tcpSettings"].(map[string]interface{})
|
||||
header, _ := tcp["header"].(map[string]interface{})
|
||||
typeStr, _ := header["type"].(string)
|
||||
if typeStr == "http" {
|
||||
request := header["request"].(map[string]interface{})
|
||||
requestPath, _ := request["path"].([]interface{})
|
||||
params["path"] = requestPath[0].(string)
|
||||
headers, _ := request["headers"].(map[string]interface{})
|
||||
params["host"] = searchHost(headers)
|
||||
params["headerType"] = "http"
|
||||
}
|
||||
case "kcp":
|
||||
kcp, _ := stream["kcpSettings"].(map[string]interface{})
|
||||
header, _ := kcp["header"].(map[string]interface{})
|
||||
params["headerType"] = header["type"].(string)
|
||||
params["seed"] = kcp["seed"].(string)
|
||||
case "ws":
|
||||
ws, _ := stream["wsSettings"].(map[string]interface{})
|
||||
params["path"] = ws["path"].(string)
|
||||
if host, ok := ws["host"].(string); ok && len(host) > 0 {
|
||||
params["host"] = host
|
||||
} else {
|
||||
headers, _ := ws["headers"].(map[string]interface{})
|
||||
params["host"] = searchHost(headers)
|
||||
}
|
||||
case "grpc":
|
||||
grpc, _ := stream["grpcSettings"].(map[string]interface{})
|
||||
params["serviceName"] = grpc["serviceName"].(string)
|
||||
params["authority"], _ = grpc["authority"].(string)
|
||||
if grpc["multiMode"].(bool) {
|
||||
params["mode"] = "multi"
|
||||
}
|
||||
case "httpupgrade":
|
||||
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]interface{})
|
||||
params["path"] = httpupgrade["path"].(string)
|
||||
if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 {
|
||||
params["host"] = host
|
||||
} else {
|
||||
headers, _ := httpupgrade["headers"].(map[string]interface{})
|
||||
params["host"] = searchHost(headers)
|
||||
}
|
||||
case "xhttp":
|
||||
xhttp, _ := stream["xhttpSettings"].(map[string]interface{})
|
||||
params["path"] = xhttp["path"].(string)
|
||||
if host, ok := xhttp["host"].(string); ok && len(host) > 0 {
|
||||
params["host"] = host
|
||||
} else {
|
||||
headers, _ := xhttp["headers"].(map[string]interface{})
|
||||
params["host"] = searchHost(headers)
|
||||
}
|
||||
params["mode"] = xhttp["mode"].(string)
|
||||
}
|
||||
|
||||
security, _ := stream["security"].(string)
|
||||
if security == "tls" {
|
||||
params["security"] = "tls"
|
||||
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||
alpns, _ := tlsSetting["alpn"].([]interface{})
|
||||
var alpn []string
|
||||
for _, a := range alpns {
|
||||
alpn = append(alpn, a.(string))
|
||||
}
|
||||
if len(alpn) > 0 {
|
||||
params["alpn"] = strings.Join(alpn, ",")
|
||||
}
|
||||
if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
|
||||
params["sni"], _ = sniValue.(string)
|
||||
}
|
||||
|
||||
tlsSettings, _ := searchKey(tlsSetting, "settings")
|
||||
if tlsSetting != nil {
|
||||
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||
params["fp"], _ = fpValue.(string)
|
||||
}
|
||||
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
||||
if insecure.(bool) {
|
||||
params["allowInsecure"] = "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
encPart := fmt.Sprintf("%s:%s", method, clients[clientIndex].Password)
|
||||
if method[0] == '2' {
|
||||
encPart = fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)
|
||||
}
|
||||
|
||||
externalProxies, _ := stream["externalProxy"].([]interface{})
|
||||
|
||||
if len(externalProxies) > 0 {
|
||||
links := ""
|
||||
for index, externalProxy := range externalProxies {
|
||||
ep, _ := externalProxy.(map[string]interface{})
|
||||
newSecurity, _ := ep["forceTls"].(string)
|
||||
dest, _ := ep["dest"].(string)
|
||||
port := int(ep["port"].(float64))
|
||||
link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), dest, port)
|
||||
|
||||
if newSecurity != "same" {
|
||||
params["security"] = newSecurity
|
||||
} else {
|
||||
params["security"] = security
|
||||
}
|
||||
url, _ := url.Parse(link)
|
||||
q := url.Query()
|
||||
|
||||
for k, v := range params {
|
||||
if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) {
|
||||
q.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
||||
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
}
|
||||
links += url.String()
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port)
|
||||
url, _ := url.Parse(link)
|
||||
q := url.Query()
|
||||
|
||||
for k, v := range params {
|
||||
q.Add(k, v)
|
||||
}
|
||||
|
||||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, "")
|
||||
return url.String()
|
||||
}
|
||||
|
||||
func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string) string {
|
||||
separationChar := string(s.remarkModel[0])
|
||||
orderChars := s.remarkModel[1:]
|
||||
orders := map[byte]string{
|
||||
'i': "",
|
||||
'e': "",
|
||||
'o': "",
|
||||
}
|
||||
if len(email) > 0 {
|
||||
orders['e'] = email
|
||||
}
|
||||
if len(inbound.Remark) > 0 {
|
||||
orders['i'] = inbound.Remark
|
||||
}
|
||||
if len(extra) > 0 {
|
||||
orders['o'] = extra
|
||||
}
|
||||
|
||||
var remark []string
|
||||
for i := 0; i < len(orderChars); i++ {
|
||||
char := orderChars[i]
|
||||
order, exists := orders[char]
|
||||
if exists && order != "" {
|
||||
remark = append(remark, order)
|
||||
}
|
||||
}
|
||||
|
||||
if s.showInfo {
|
||||
statsExist := false
|
||||
var stats xray.ClientTraffic
|
||||
for _, clientStat := range inbound.ClientStats {
|
||||
if clientStat.Email == email {
|
||||
stats = clientStat
|
||||
statsExist = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Get remained days
|
||||
if statsExist {
|
||||
if !stats.Enable {
|
||||
return fmt.Sprintf("⛔️N/A%s%s", separationChar, strings.Join(remark, separationChar))
|
||||
}
|
||||
if vol := stats.Total - (stats.Up + stats.Down); vol > 0 {
|
||||
remark = append(remark, fmt.Sprintf("%s%s", common.FormatTraffic(vol), "📊"))
|
||||
}
|
||||
now := time.Now().Unix()
|
||||
switch exp := stats.ExpiryTime / 1000; {
|
||||
case exp > 0:
|
||||
remainingSeconds := exp - now
|
||||
days := remainingSeconds / 86400
|
||||
hours := (remainingSeconds % 86400) / 3600
|
||||
minutes := (remainingSeconds % 3600) / 60
|
||||
if days > 0 {
|
||||
if hours > 0 {
|
||||
remark = append(remark, fmt.Sprintf("%dD,%dH⏳", days, hours))
|
||||
} else {
|
||||
remark = append(remark, fmt.Sprintf("%dD⏳", days))
|
||||
}
|
||||
} else if hours > 0 {
|
||||
remark = append(remark, fmt.Sprintf("%dH⏳", hours))
|
||||
} else {
|
||||
remark = append(remark, fmt.Sprintf("%dM⏳", minutes))
|
||||
}
|
||||
case exp < 0:
|
||||
days := exp / -86400
|
||||
hours := (exp % -86400) / 3600
|
||||
minutes := (exp % -3600) / 60
|
||||
if days > 0 {
|
||||
if hours > 0 {
|
||||
remark = append(remark, fmt.Sprintf("%dD,%dH⏳", days, hours))
|
||||
} else {
|
||||
remark = append(remark, fmt.Sprintf("%dD⏳", days))
|
||||
}
|
||||
} else if hours > 0 {
|
||||
remark = append(remark, fmt.Sprintf("%dH⏳", hours))
|
||||
} else {
|
||||
remark = append(remark, fmt.Sprintf("%dM⏳", minutes))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Join(remark, separationChar)
|
||||
}
|
||||
|
||||
func searchKey(data interface{}, key string) (interface{}, bool) {
|
||||
switch val := data.(type) {
|
||||
case map[string]interface{}:
|
||||
for k, v := range val {
|
||||
if k == key {
|
||||
return v, true
|
||||
}
|
||||
if result, ok := searchKey(v, key); ok {
|
||||
return result, true
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
for _, v := range val {
|
||||
if result, ok := searchKey(v, key); ok {
|
||||
return result, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func searchHost(headers interface{}) string {
|
||||
data, _ := headers.(map[string]interface{})
|
||||
for k, v := range data {
|
||||
if strings.EqualFold(k, "host") {
|
||||
switch v.(type) {
|
||||
case []interface{}:
|
||||
hosts, _ := v.([]interface{})
|
||||
if len(hosts) > 0 {
|
||||
return hosts[0].(string)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
case interface{}:
|
||||
return v.(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
@@ -3,11 +3,10 @@ package common
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"x-ui/logger"
|
||||
)
|
||||
|
||||
var CtxDone = errors.New("context done")
|
||||
|
||||
func NewErrorf(format string, a ...interface{}) error {
|
||||
msg := fmt.Sprintf(format, a...)
|
||||
return errors.New(msg)
|
||||
|
||||
@@ -4,18 +4,14 @@ 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))
|
||||
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])
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package common
|
||||
|
||||
import "sort"
|
||||
|
||||
func IsSubString(target string, str_array []string) bool {
|
||||
sort.Strings(str_array)
|
||||
index := sort.SearchStrings(str_array, target)
|
||||
return index < len(str_array) && str_array[index] == target
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package util
|
||||
|
||||
import "context"
|
||||
|
||||
func IsDone(ctx context.Context) bool {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
type RawMessage []byte
|
||||
|
||||
// MarshalJSON 自定义 json.RawMessage 默认行为
|
||||
// MarshalJSON: Customize json.RawMessage default behavior
|
||||
func (m RawMessage) MarshalJSON() ([]byte, error) {
|
||||
if len(m) == 0 {
|
||||
return []byte("null"), nil
|
||||
@@ -14,7 +14,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 data.
|
||||
func (m *RawMessage) UnmarshalJSON(data []byte) error {
|
||||
if m == nil {
|
||||
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
|
||||
|
||||
@@ -2,19 +2,18 @@ package random
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
var numSeq [10]rune
|
||||
var lowerSeq [26]rune
|
||||
var upperSeq [26]rune
|
||||
var numLowerSeq [36]rune
|
||||
var numUpperSeq [36]rune
|
||||
var allSeq [62]rune
|
||||
var (
|
||||
numSeq [10]rune
|
||||
lowerSeq [26]rune
|
||||
upperSeq [26]rune
|
||||
numLowerSeq [36]rune
|
||||
numUpperSeq [36]rune
|
||||
allSeq [62]rune
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
numSeq[i] = rune('0' + i)
|
||||
}
|
||||
@@ -41,3 +40,7 @@ func Seq(n int) string {
|
||||
}
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
func Num(n int) int {
|
||||
return rand.Intn(n)
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ import (
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
//go:linkname HostProc github.com/shirou/gopsutil/v3/internal/common.HostProc
|
||||
//go:linkname HostProc github.com/shirou/gopsutil/v4/internal/common.HostProc
|
||||
func HostProc(combineWith ...string) string
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"github.com/shirou/gopsutil/v3/net"
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
)
|
||||
|
||||
func GetTCPCount() (int, error) {
|
||||
|
||||
@@ -24,8 +24,8 @@ func getLinesNum(filename string) (int, error) {
|
||||
|
||||
var buffPosition int
|
||||
for {
|
||||
i := bytes.IndexByte(buf[buffPosition:], '\n')
|
||||
if i < 0 || n == buffPosition {
|
||||
i := bytes.IndexByte(buf[buffPosition:n], '\n')
|
||||
if i < 0 {
|
||||
break
|
||||
}
|
||||
buffPosition += i + 1
|
||||
@@ -33,11 +33,12 @@ func getLinesNum(filename string) (int, error) {
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
return sum, nil
|
||||
break
|
||||
} else if err != nil {
|
||||
return sum, err
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return sum, nil
|
||||
}
|
||||
|
||||
func GetTCPCount() (int, error) {
|
||||
@@ -45,11 +46,11 @@ func GetTCPCount() (int, error) {
|
||||
|
||||
tcp4, err := getLinesNum(fmt.Sprintf("%v/net/tcp", root))
|
||||
if err != nil {
|
||||
return tcp4, err
|
||||
return 0, err
|
||||
}
|
||||
tcp6, err := getLinesNum(fmt.Sprintf("%v/net/tcp6", root))
|
||||
if err != nil {
|
||||
return tcp4 + tcp6, nil
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return tcp4 + tcp6, nil
|
||||
@@ -60,11 +61,11 @@ func GetUDPCount() (int, error) {
|
||||
|
||||
udp4, err := getLinesNum(fmt.Sprintf("%v/net/udp", root))
|
||||
if err != nil {
|
||||
return udp4, err
|
||||
return 0, err
|
||||
}
|
||||
udp6, err := getLinesNum(fmt.Sprintf("%v/net/udp6", root))
|
||||
if err != nil {
|
||||
return udp4 + udp6, nil
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return udp4 + udp6, nil
|
||||
|
||||
@@ -4,21 +4,27 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"github.com/shirou/gopsutil/v3/net"
|
||||
"errors"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
)
|
||||
|
||||
func GetTCPCount() (int, error) {
|
||||
stats, err := net.Connections("tcp")
|
||||
func GetConnectionCount(proto string) (int, error) {
|
||||
if proto != "tcp" && proto != "udp" {
|
||||
return 0, errors.New("invalid protocol")
|
||||
}
|
||||
|
||||
stats, err := net.Connections(proto)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(stats), nil
|
||||
}
|
||||
|
||||
func GetTCPCount() (int, error) {
|
||||
return GetConnectionCount("tcp")
|
||||
}
|
||||
|
||||
func GetUDPCount() (int, error) {
|
||||
stats, err := net.Connections("udp")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(stats), nil
|
||||
return GetConnectionCount("udp")
|
||||
}
|
||||
|
||||
28
v2ui/db.go
@@ -1,28 +0,0 @@
|
||||
package v2ui
|
||||
|
||||
import (
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
var v2db *gorm.DB
|
||||
|
||||
func initDB(dbPath string) error {
|
||||
c := &gorm.Config{
|
||||
Logger: logger.Discard,
|
||||
}
|
||||
var err error
|
||||
v2db, err = gorm.Open(sqlite.Open(dbPath), c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getV2Inbounds() ([]*V2Inbound, error) {
|
||||
inbounds := make([]*V2Inbound, 0)
|
||||
err := v2db.Model(V2Inbound{}).Find(&inbounds).Error
|
||||
return inbounds, err
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package v2ui
|
||||
|
||||
import "x-ui/database/model"
|
||||
|
||||
type V2Inbound struct {
|
||||
Id int `gorm:"primaryKey;autoIncrement"`
|
||||
Port int `gorm:"unique"`
|
||||
Listen string
|
||||
Protocol string
|
||||
Settings string
|
||||
StreamSettings string
|
||||
Tag string `gorm:"unique"`
|
||||
Sniffing string
|
||||
Remark string
|
||||
Up int64
|
||||
Down int64
|
||||
Enable bool
|
||||
}
|
||||
|
||||
func (i *V2Inbound) TableName() string {
|
||||
return "inbound"
|
||||
}
|
||||
|
||||
func (i *V2Inbound) ToInbound(userId int) *model.Inbound {
|
||||
return &model.Inbound{
|
||||
UserId: userId,
|
||||
Up: i.Up,
|
||||
Down: i.Down,
|
||||
Total: 0,
|
||||
Remark: i.Remark,
|
||||
Enable: i.Enable,
|
||||
ExpiryTime: 0,
|
||||
Listen: i.Listen,
|
||||
Port: i.Port,
|
||||
Protocol: model.Protocol(i.Protocol),
|
||||
Settings: i.Settings,
|
||||
StreamSettings: i.StreamSettings,
|
||||
Tag: i.Tag,
|
||||
Sniffing: i.Sniffing,
|
||||
}
|
||||
}
|
||||
51
v2ui/v2ui.go
@@ -1,51 +0,0 @@
|
||||
package v2ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"x-ui/config"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/util/common"
|
||||
"x-ui/web/service"
|
||||
)
|
||||
|
||||
func MigrateFromV2UI(dbPath string) error {
|
||||
err := initDB(dbPath)
|
||||
if err != nil {
|
||||
return common.NewError("init v2-ui database failed:", err)
|
||||
}
|
||||
err = database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
return common.NewError("init x-ui database failed:", err)
|
||||
}
|
||||
|
||||
v2Inbounds, err := getV2Inbounds()
|
||||
if err != nil {
|
||||
return common.NewError("get v2-ui inbounds failed:", err)
|
||||
}
|
||||
if len(v2Inbounds) == 0 {
|
||||
fmt.Println("migrate v2-ui inbounds success: 0")
|
||||
return nil
|
||||
}
|
||||
|
||||
userService := service.UserService{}
|
||||
user, err := userService.GetFirstUser()
|
||||
if err != nil {
|
||||
return common.NewError("get x-ui user failed:", err)
|
||||
}
|
||||
|
||||
inbounds := make([]*model.Inbound, 0)
|
||||
for _, v2inbound := range v2Inbounds {
|
||||
inbounds = append(inbounds, v2inbound.ToInbound(user.Id))
|
||||
}
|
||||
|
||||
inboundService := service.InboundService{}
|
||||
err = inboundService.AddInbounds(inbounds)
|
||||
if err != nil {
|
||||
return common.NewError("add x-ui inbounds failed:", err)
|
||||
}
|
||||
|
||||
fmt.Println("migrate v2-ui inbounds success:", len(inbounds))
|
||||
|
||||
return nil
|
||||
}
|
||||
BIN
web/assets/Vazirmatn-UI-NL-Regular.woff2
Normal file
3
web/assets/ant-design-vue/antd-with-locales.min.js
vendored
Normal file
7
web/assets/ant-design-vue/antd.less
Normal file
@@ -0,0 +1,7 @@
|
||||
@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;
|
||||
8
web/assets/ant-design-vue/antd.min.css
vendored
Normal file
3
web/assets/ant-design-vue/antd.min.js
vendored
Normal file
1
web/assets/ant-design-vue/antd.min.js.map
Normal file
@@ -1,2 +0,0 @@
|
||||
@import "../lib/style/index.less";
|
||||
@import "../lib/style/components.less";
|
||||
4494
web/assets/ant-design-vue@1.7.2/antd.min.css
vendored
2
web/assets/ant-design-vue@1.7.2/antd.min.js
vendored
10
web/assets/axios/axios.min.js
vendored
8
web/assets/clipboard/clipboard.min.js
vendored
1
web/assets/codemirror/codemirror.min.css
vendored
Normal file
1
web/assets/codemirror/codemirror.min.js
vendored
Normal file
119
web/assets/codemirror/fold/brace-fold.js
Normal file
@@ -0,0 +1,119 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
function bracketFolding(pairs) {
|
||||
return function(cm, start) {
|
||||
var line = start.line, lineText = cm.getLine(line);
|
||||
|
||||
function findOpening(pair) {
|
||||
var tokenType;
|
||||
for (var at = start.ch, pass = 0;;) {
|
||||
var found = at <= 0 ? -1 : lineText.lastIndexOf(pair[0], at - 1);
|
||||
if (found == -1) {
|
||||
if (pass == 1) break;
|
||||
pass = 1;
|
||||
at = lineText.length;
|
||||
continue;
|
||||
}
|
||||
if (pass == 1 && found < start.ch) break;
|
||||
tokenType = cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1));
|
||||
if (!/^(comment|string)/.test(tokenType)) return {ch: found + 1, tokenType: tokenType, pair: pair};
|
||||
at = found - 1;
|
||||
}
|
||||
}
|
||||
|
||||
function findRange(found) {
|
||||
var count = 1, lastLine = cm.lastLine(), end, startCh = found.ch, endCh
|
||||
outer: for (var i = line; i <= lastLine; ++i) {
|
||||
var text = cm.getLine(i), pos = i == line ? startCh : 0;
|
||||
for (;;) {
|
||||
var nextOpen = text.indexOf(found.pair[0], pos), nextClose = text.indexOf(found.pair[1], pos);
|
||||
if (nextOpen < 0) nextOpen = text.length;
|
||||
if (nextClose < 0) nextClose = text.length;
|
||||
pos = Math.min(nextOpen, nextClose);
|
||||
if (pos == text.length) break;
|
||||
if (cm.getTokenTypeAt(CodeMirror.Pos(i, pos + 1)) == found.tokenType) {
|
||||
if (pos == nextOpen) ++count;
|
||||
else if (!--count) { end = i; endCh = pos; break outer; }
|
||||
}
|
||||
++pos;
|
||||
}
|
||||
}
|
||||
|
||||
if (end == null || line == end) return null
|
||||
return {from: CodeMirror.Pos(line, startCh),
|
||||
to: CodeMirror.Pos(end, endCh)};
|
||||
}
|
||||
|
||||
var found = []
|
||||
for (var i = 0; i < pairs.length; i++) {
|
||||
var open = findOpening(pairs[i])
|
||||
if (open) found.push(open)
|
||||
}
|
||||
found.sort(function(a, b) { return a.ch - b.ch })
|
||||
for (var i = 0; i < found.length; i++) {
|
||||
var range = findRange(found[i])
|
||||
if (range) return range
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
CodeMirror.registerHelper("fold", "brace", bracketFolding([["{", "}"], ["[", "]"]]));
|
||||
|
||||
CodeMirror.registerHelper("fold", "brace-paren", bracketFolding([["{", "}"], ["[", "]"], ["(", ")"]]));
|
||||
|
||||
CodeMirror.registerHelper("fold", "import", function(cm, start) {
|
||||
function hasImport(line) {
|
||||
if (line < cm.firstLine() || line > cm.lastLine()) return null;
|
||||
var start = cm.getTokenAt(CodeMirror.Pos(line, 1));
|
||||
if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1));
|
||||
if (start.type != "keyword" || start.string != "import") return null;
|
||||
// Now find closing semicolon, return its position
|
||||
for (var i = line, e = Math.min(cm.lastLine(), line + 10); i <= e; ++i) {
|
||||
var text = cm.getLine(i), semi = text.indexOf(";");
|
||||
if (semi != -1) return {startCh: start.end, end: CodeMirror.Pos(i, semi)};
|
||||
}
|
||||
}
|
||||
|
||||
var startLine = start.line, has = hasImport(startLine), prev;
|
||||
if (!has || hasImport(startLine - 1) || ((prev = hasImport(startLine - 2)) && prev.end.line == startLine - 1))
|
||||
return null;
|
||||
for (var end = has.end;;) {
|
||||
var next = hasImport(end.line + 1);
|
||||
if (next == null) break;
|
||||
end = next.end;
|
||||
}
|
||||
return {from: cm.clipPos(CodeMirror.Pos(startLine, has.startCh + 1)), to: end};
|
||||
});
|
||||
|
||||
CodeMirror.registerHelper("fold", "include", function(cm, start) {
|
||||
function hasInclude(line) {
|
||||
if (line < cm.firstLine() || line > cm.lastLine()) return null;
|
||||
var start = cm.getTokenAt(CodeMirror.Pos(line, 1));
|
||||
if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1));
|
||||
if (start.type == "meta" && start.string.slice(0, 8) == "#include") return start.start + 8;
|
||||
}
|
||||
|
||||
var startLine = start.line, has = hasInclude(startLine);
|
||||
if (has == null || hasInclude(startLine - 1) != null) return null;
|
||||
for (var end = startLine;;) {
|
||||
var next = hasInclude(end + 1);
|
||||
if (next == null) break;
|
||||
++end;
|
||||
}
|
||||
return {from: CodeMirror.Pos(startLine, has + 1),
|
||||
to: cm.clipPos(CodeMirror.Pos(end))};
|
||||
});
|
||||
|
||||
});
|
||||
159
web/assets/codemirror/fold/foldcode.js
Normal file
@@ -0,0 +1,159 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
function doFold(cm, pos, options, force) {
|
||||
if (options && options.call) {
|
||||
var finder = options;
|
||||
options = null;
|
||||
} else {
|
||||
var finder = getOption(cm, options, "rangeFinder");
|
||||
}
|
||||
if (typeof pos == "number") pos = CodeMirror.Pos(pos, 0);
|
||||
var minSize = getOption(cm, options, "minFoldSize");
|
||||
|
||||
function getRange(allowFolded) {
|
||||
var range = finder(cm, pos);
|
||||
if (!range || range.to.line - range.from.line < minSize) return null;
|
||||
if (force === "fold") return range;
|
||||
|
||||
var marks = cm.findMarksAt(range.from);
|
||||
for (var i = 0; i < marks.length; ++i) {
|
||||
if (marks[i].__isFold) {
|
||||
if (!allowFolded) return null;
|
||||
range.cleared = true;
|
||||
marks[i].clear();
|
||||
}
|
||||
}
|
||||
return range;
|
||||
}
|
||||
|
||||
var range = getRange(true);
|
||||
if (getOption(cm, options, "scanUp")) while (!range && pos.line > cm.firstLine()) {
|
||||
pos = CodeMirror.Pos(pos.line - 1, 0);
|
||||
range = getRange(false);
|
||||
}
|
||||
if (!range || range.cleared || force === "unfold") return;
|
||||
|
||||
var myWidget = makeWidget(cm, options, range);
|
||||
CodeMirror.on(myWidget, "mousedown", function(e) {
|
||||
myRange.clear();
|
||||
CodeMirror.e_preventDefault(e);
|
||||
});
|
||||
var myRange = cm.markText(range.from, range.to, {
|
||||
replacedWith: myWidget,
|
||||
clearOnEnter: getOption(cm, options, "clearOnEnter"),
|
||||
__isFold: true
|
||||
});
|
||||
myRange.on("clear", function(from, to) {
|
||||
CodeMirror.signal(cm, "unfold", cm, from, to);
|
||||
});
|
||||
CodeMirror.signal(cm, "fold", cm, range.from, range.to);
|
||||
}
|
||||
|
||||
function makeWidget(cm, options, range) {
|
||||
var widget = getOption(cm, options, "widget");
|
||||
|
||||
if (typeof widget == "function") {
|
||||
widget = widget(range.from, range.to);
|
||||
}
|
||||
|
||||
if (typeof widget == "string") {
|
||||
var text = document.createTextNode(widget);
|
||||
widget = document.createElement("span");
|
||||
widget.appendChild(text);
|
||||
widget.className = "CodeMirror-foldmarker";
|
||||
} else if (widget) {
|
||||
widget = widget.cloneNode(true)
|
||||
}
|
||||
return widget;
|
||||
}
|
||||
|
||||
// Clumsy backwards-compatible interface
|
||||
CodeMirror.newFoldFunction = function(rangeFinder, widget) {
|
||||
return function(cm, pos) { doFold(cm, pos, {rangeFinder: rangeFinder, widget: widget}); };
|
||||
};
|
||||
|
||||
// New-style interface
|
||||
CodeMirror.defineExtension("foldCode", function(pos, options, force) {
|
||||
doFold(this, pos, options, force);
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension("isFolded", function(pos) {
|
||||
var marks = this.findMarksAt(pos);
|
||||
for (var i = 0; i < marks.length; ++i)
|
||||
if (marks[i].__isFold) return true;
|
||||
});
|
||||
|
||||
CodeMirror.commands.toggleFold = function(cm) {
|
||||
cm.foldCode(cm.getCursor());
|
||||
};
|
||||
CodeMirror.commands.fold = function(cm) {
|
||||
cm.foldCode(cm.getCursor(), null, "fold");
|
||||
};
|
||||
CodeMirror.commands.unfold = function(cm) {
|
||||
cm.foldCode(cm.getCursor(), { scanUp: false }, "unfold");
|
||||
};
|
||||
CodeMirror.commands.foldAll = function(cm) {
|
||||
cm.operation(function() {
|
||||
for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)
|
||||
cm.foldCode(CodeMirror.Pos(i, 0), { scanUp: false }, "fold");
|
||||
});
|
||||
};
|
||||
CodeMirror.commands.unfoldAll = function(cm) {
|
||||
cm.operation(function() {
|
||||
for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)
|
||||
cm.foldCode(CodeMirror.Pos(i, 0), { scanUp: false }, "unfold");
|
||||
});
|
||||
};
|
||||
|
||||
CodeMirror.registerHelper("fold", "combine", function() {
|
||||
var funcs = Array.prototype.slice.call(arguments, 0);
|
||||
return function(cm, start) {
|
||||
for (var i = 0; i < funcs.length; ++i) {
|
||||
var found = funcs[i](cm, start);
|
||||
if (found) return found;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
CodeMirror.registerHelper("fold", "auto", function(cm, start) {
|
||||
var helpers = cm.getHelpers(start, "fold");
|
||||
for (var i = 0; i < helpers.length; i++) {
|
||||
var cur = helpers[i](cm, start);
|
||||
if (cur) return cur;
|
||||
}
|
||||
});
|
||||
|
||||
var defaultOptions = {
|
||||
rangeFinder: CodeMirror.fold.auto,
|
||||
widget: "\u2194",
|
||||
minFoldSize: 0,
|
||||
scanUp: false,
|
||||
clearOnEnter: true
|
||||
};
|
||||
|
||||
CodeMirror.defineOption("foldOptions", null);
|
||||
|
||||
function getOption(cm, options, name) {
|
||||
if (options && options[name] !== undefined)
|
||||
return options[name];
|
||||
var editorOptions = cm.options.foldOptions;
|
||||
if (editorOptions && editorOptions[name] !== undefined)
|
||||
return editorOptions[name];
|
||||
return defaultOptions[name];
|
||||
}
|
||||
|
||||
CodeMirror.defineExtension("foldOption", function(options, name) {
|
||||
return getOption(this, options, name);
|
||||
});
|
||||
});
|
||||
20
web/assets/codemirror/fold/foldgutter.css
Normal file
@@ -0,0 +1,20 @@
|
||||
.CodeMirror-foldmarker {
|
||||
color: blue;
|
||||
text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px;
|
||||
font-family: arial;
|
||||
line-height: .3;
|
||||
cursor: pointer;
|
||||
}
|
||||
.CodeMirror-foldgutter {
|
||||
width: .7em;
|
||||
}
|
||||
.CodeMirror-foldgutter-open,
|
||||
.CodeMirror-foldgutter-folded {
|
||||
cursor: pointer;
|
||||
}
|
||||
.CodeMirror-foldgutter-open:after {
|
||||
content: "\25BE";
|
||||
}
|
||||
.CodeMirror-foldgutter-folded:after {
|
||||
content: "\25B8";
|
||||
}
|
||||
169
web/assets/codemirror/fold/foldgutter.js
Normal file
@@ -0,0 +1,169 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"), require("./foldcode"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror", "./foldcode"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
CodeMirror.defineOption("foldGutter", false, function(cm, val, old) {
|
||||
if (old && old != CodeMirror.Init) {
|
||||
cm.clearGutter(cm.state.foldGutter.options.gutter);
|
||||
cm.state.foldGutter = null;
|
||||
cm.off("gutterClick", onGutterClick);
|
||||
cm.off("changes", onChange);
|
||||
cm.off("viewportChange", onViewportChange);
|
||||
cm.off("fold", onFold);
|
||||
cm.off("unfold", onFold);
|
||||
cm.off("swapDoc", onChange);
|
||||
cm.off("optionChange", optionChange);
|
||||
}
|
||||
if (val) {
|
||||
cm.state.foldGutter = new State(parseOptions(val));
|
||||
updateInViewport(cm);
|
||||
cm.on("gutterClick", onGutterClick);
|
||||
cm.on("changes", onChange);
|
||||
cm.on("viewportChange", onViewportChange);
|
||||
cm.on("fold", onFold);
|
||||
cm.on("unfold", onFold);
|
||||
cm.on("swapDoc", onChange);
|
||||
cm.on("optionChange", optionChange);
|
||||
}
|
||||
});
|
||||
|
||||
var Pos = CodeMirror.Pos;
|
||||
|
||||
function State(options) {
|
||||
this.options = options;
|
||||
this.from = this.to = 0;
|
||||
}
|
||||
|
||||
function parseOptions(opts) {
|
||||
if (opts === true) opts = {};
|
||||
if (opts.gutter == null) opts.gutter = "CodeMirror-foldgutter";
|
||||
if (opts.indicatorOpen == null) opts.indicatorOpen = "CodeMirror-foldgutter-open";
|
||||
if (opts.indicatorFolded == null) opts.indicatorFolded = "CodeMirror-foldgutter-folded";
|
||||
return opts;
|
||||
}
|
||||
|
||||
function isFolded(cm, line) {
|
||||
var marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0));
|
||||
for (var i = 0; i < marks.length; ++i) {
|
||||
if (marks[i].__isFold) {
|
||||
var fromPos = marks[i].find(-1);
|
||||
if (fromPos && fromPos.line === line)
|
||||
return marks[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function marker(spec) {
|
||||
if (typeof spec == "string") {
|
||||
var elt = document.createElement("div");
|
||||
elt.className = spec + " CodeMirror-guttermarker-subtle";
|
||||
return elt;
|
||||
} else {
|
||||
return spec.cloneNode(true);
|
||||
}
|
||||
}
|
||||
|
||||
function updateFoldInfo(cm, from, to) {
|
||||
var opts = cm.state.foldGutter.options, cur = from - 1;
|
||||
var minSize = cm.foldOption(opts, "minFoldSize");
|
||||
var func = cm.foldOption(opts, "rangeFinder");
|
||||
// we can reuse the built-in indicator element if its className matches the new state
|
||||
var clsFolded = typeof opts.indicatorFolded == "string" && classTest(opts.indicatorFolded);
|
||||
var clsOpen = typeof opts.indicatorOpen == "string" && classTest(opts.indicatorOpen);
|
||||
cm.eachLine(from, to, function(line) {
|
||||
++cur;
|
||||
var mark = null;
|
||||
var old = line.gutterMarkers;
|
||||
if (old) old = old[opts.gutter];
|
||||
if (isFolded(cm, cur)) {
|
||||
if (clsFolded && old && clsFolded.test(old.className)) return;
|
||||
mark = marker(opts.indicatorFolded);
|
||||
} else {
|
||||
var pos = Pos(cur, 0);
|
||||
var range = func && func(cm, pos);
|
||||
if (range && range.to.line - range.from.line >= minSize) {
|
||||
if (clsOpen && old && clsOpen.test(old.className)) return;
|
||||
mark = marker(opts.indicatorOpen);
|
||||
}
|
||||
}
|
||||
if (!mark && !old) return;
|
||||
cm.setGutterMarker(line, opts.gutter, mark);
|
||||
});
|
||||
}
|
||||
|
||||
// copied from CodeMirror/src/util/dom.js
|
||||
function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*") }
|
||||
|
||||
function updateInViewport(cm) {
|
||||
var vp = cm.getViewport(), state = cm.state.foldGutter;
|
||||
if (!state) return;
|
||||
cm.operation(function() {
|
||||
updateFoldInfo(cm, vp.from, vp.to);
|
||||
});
|
||||
state.from = vp.from; state.to = vp.to;
|
||||
}
|
||||
|
||||
function onGutterClick(cm, line, gutter) {
|
||||
var state = cm.state.foldGutter;
|
||||
if (!state) return;
|
||||
var opts = state.options;
|
||||
if (gutter != opts.gutter) return;
|
||||
var folded = isFolded(cm, line);
|
||||
if (folded) folded.clear();
|
||||
else cm.foldCode(Pos(line, 0), opts);
|
||||
}
|
||||
|
||||
function optionChange(cm, option) {
|
||||
if (option == "mode") onChange(cm)
|
||||
}
|
||||
|
||||
function onChange(cm) {
|
||||
var state = cm.state.foldGutter;
|
||||
if (!state) return;
|
||||
var opts = state.options;
|
||||
state.from = state.to = 0;
|
||||
clearTimeout(state.changeUpdate);
|
||||
state.changeUpdate = setTimeout(function() { updateInViewport(cm); }, opts.foldOnChangeTimeSpan || 600);
|
||||
}
|
||||
|
||||
function onViewportChange(cm) {
|
||||
var state = cm.state.foldGutter;
|
||||
if (!state) return;
|
||||
var opts = state.options;
|
||||
clearTimeout(state.changeUpdate);
|
||||
state.changeUpdate = setTimeout(function() {
|
||||
var vp = cm.getViewport();
|
||||
if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) {
|
||||
updateInViewport(cm);
|
||||
} else {
|
||||
cm.operation(function() {
|
||||
if (vp.from < state.from) {
|
||||
updateFoldInfo(cm, vp.from, state.from);
|
||||
state.from = vp.from;
|
||||
}
|
||||
if (vp.to > state.to) {
|
||||
updateFoldInfo(cm, state.to, vp.to);
|
||||
state.to = vp.to;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, opts.updateViewportTimeSpan || 400);
|
||||
}
|
||||
|
||||
function onFold(cm, from) {
|
||||
var state = cm.state.foldGutter;
|
||||
if (!state) return;
|
||||
var line = from.line;
|
||||
if (line >= state.from && line < state.to)
|
||||
updateFoldInfo(cm, line, line + 1);
|
||||
}
|
||||
});
|
||||
162
web/assets/codemirror/hint/javascript-hint.js
Normal file
@@ -0,0 +1,162 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
var Pos = CodeMirror.Pos;
|
||||
|
||||
function forEach(arr, f) {
|
||||
for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]);
|
||||
}
|
||||
|
||||
function arrayContains(arr, item) {
|
||||
if (!Array.prototype.indexOf) {
|
||||
var i = arr.length;
|
||||
while (i--) {
|
||||
if (arr[i] === item) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return arr.indexOf(item) != -1;
|
||||
}
|
||||
|
||||
function scriptHint(editor, keywords, getToken, options) {
|
||||
// Find the token at the cursor
|
||||
var cur = editor.getCursor(), token = getToken(editor, cur);
|
||||
if (/\b(?:string|comment)\b/.test(token.type)) return;
|
||||
var innerMode = CodeMirror.innerMode(editor.getMode(), token.state);
|
||||
if (innerMode.mode.helperType === "json") return;
|
||||
token.state = innerMode.state;
|
||||
|
||||
// If it's not a 'word-style' token, ignore the token.
|
||||
if (!/^[\w$_]*$/.test(token.string)) {
|
||||
token = {start: cur.ch, end: cur.ch, string: "", state: token.state,
|
||||
type: token.string == "." ? "property" : null};
|
||||
} else if (token.end > cur.ch) {
|
||||
token.end = cur.ch;
|
||||
token.string = token.string.slice(0, cur.ch - token.start);
|
||||
}
|
||||
|
||||
var tprop = token;
|
||||
// If it is a property, find out what it is a property of.
|
||||
while (tprop.type == "property") {
|
||||
tprop = getToken(editor, Pos(cur.line, tprop.start));
|
||||
if (tprop.string != ".") return;
|
||||
tprop = getToken(editor, Pos(cur.line, tprop.start));
|
||||
if (!context) var context = [];
|
||||
context.push(tprop);
|
||||
}
|
||||
return {list: getCompletions(token, context, keywords, options),
|
||||
from: Pos(cur.line, token.start),
|
||||
to: Pos(cur.line, token.end)};
|
||||
}
|
||||
|
||||
function javascriptHint(editor, options) {
|
||||
return scriptHint(editor, javascriptKeywords,
|
||||
function (e, cur) {return e.getTokenAt(cur);},
|
||||
options);
|
||||
}
|
||||
CodeMirror.registerHelper("hint", "javascript", javascriptHint);
|
||||
|
||||
function getCoffeeScriptToken(editor, cur) {
|
||||
// This getToken, it is for coffeescript, imitates the behavior of
|
||||
// getTokenAt method in javascript.js, that is, returning "property"
|
||||
// type and treat "." as independent token.
|
||||
var token = editor.getTokenAt(cur);
|
||||
if (cur.ch == token.start + 1 && token.string.charAt(0) == '.') {
|
||||
token.end = token.start;
|
||||
token.string = '.';
|
||||
token.type = "property";
|
||||
}
|
||||
else if (/^\.[\w$_]*$/.test(token.string)) {
|
||||
token.type = "property";
|
||||
token.start++;
|
||||
token.string = token.string.replace(/\./, '');
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
function coffeescriptHint(editor, options) {
|
||||
return scriptHint(editor, coffeescriptKeywords, getCoffeeScriptToken, options);
|
||||
}
|
||||
CodeMirror.registerHelper("hint", "coffeescript", coffeescriptHint);
|
||||
|
||||
var stringProps = ("charAt charCodeAt indexOf lastIndexOf substring substr slice trim trimLeft trimRight " +
|
||||
"toUpperCase toLowerCase split concat match replace search").split(" ");
|
||||
var arrayProps = ("length concat join splice push pop shift unshift slice reverse sort indexOf " +
|
||||
"lastIndexOf every some filter forEach map reduce reduceRight ").split(" ");
|
||||
var funcProps = "prototype apply call bind".split(" ");
|
||||
var javascriptKeywords = ("break case catch class const continue debugger default delete do else export extends false finally for function " +
|
||||
"if in import instanceof new null return super switch this throw true try typeof var void while with yield").split(" ");
|
||||
var coffeescriptKeywords = ("and break catch class continue delete do else extends false finally for " +
|
||||
"if in instanceof isnt new no not null of off on or return switch then throw true try typeof until void while with yes").split(" ");
|
||||
|
||||
function forAllProps(obj, callback) {
|
||||
if (!Object.getOwnPropertyNames || !Object.getPrototypeOf) {
|
||||
for (var name in obj) callback(name)
|
||||
} else {
|
||||
for (var o = obj; o; o = Object.getPrototypeOf(o))
|
||||
Object.getOwnPropertyNames(o).forEach(callback)
|
||||
}
|
||||
}
|
||||
|
||||
function getCompletions(token, context, keywords, options) {
|
||||
var found = [], start = token.string, global = options && options.globalScope || window;
|
||||
function maybeAdd(str) {
|
||||
if (str.lastIndexOf(start, 0) == 0 && !arrayContains(found, str)) found.push(str);
|
||||
}
|
||||
function gatherCompletions(obj) {
|
||||
if (typeof obj == "string") forEach(stringProps, maybeAdd);
|
||||
else if (obj instanceof Array) forEach(arrayProps, maybeAdd);
|
||||
else if (obj instanceof Function) forEach(funcProps, maybeAdd);
|
||||
forAllProps(obj, maybeAdd)
|
||||
}
|
||||
|
||||
if (context && context.length) {
|
||||
// If this is a property, see if it belongs to some object we can
|
||||
// find in the current environment.
|
||||
var obj = context.pop(), base;
|
||||
if (obj.type && obj.type.indexOf("variable") === 0) {
|
||||
if (options && options.additionalContext)
|
||||
base = options.additionalContext[obj.string];
|
||||
if (!options || options.useGlobalScope !== false)
|
||||
base = base || global[obj.string];
|
||||
} else if (obj.type == "string") {
|
||||
base = "";
|
||||
} else if (obj.type == "atom") {
|
||||
base = 1;
|
||||
} else if (obj.type == "function") {
|
||||
if (global.jQuery != null && (obj.string == '$' || obj.string == 'jQuery') &&
|
||||
(typeof global.jQuery == 'function'))
|
||||
base = global.jQuery();
|
||||
else if (global._ != null && (obj.string == '_') && (typeof global._ == 'function'))
|
||||
base = global._();
|
||||
}
|
||||
while (base != null && context.length)
|
||||
base = base[context.pop().string];
|
||||
if (base != null) gatherCompletions(base);
|
||||
} else {
|
||||
// If not, just look in the global object, any local scope, and optional additional-context
|
||||
// (reading into JS mode internals to get at the local and global variables)
|
||||
for (var v = token.state.localVars; v; v = v.next) maybeAdd(v.name);
|
||||
for (var c = token.state.context; c; c = c.prev)
|
||||
for (var v = c.vars; v; v = v.next) maybeAdd(v.name)
|
||||
for (var v = token.state.globalVars; v; v = v.next) maybeAdd(v.name);
|
||||
if (options && options.additionalContext != null)
|
||||
for (var key in options.additionalContext)
|
||||
maybeAdd(key);
|
||||
if (!options || options.useGlobalScope !== false)
|
||||
gatherCompletions(global);
|
||||
forEach(keywords, maybeAdd);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
});
|
||||
960
web/assets/codemirror/javascript.js
Normal file
@@ -0,0 +1,960 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||
var indentUnit = config.indentUnit;
|
||||
var statementIndent = parserConfig.statementIndent;
|
||||
var jsonldMode = parserConfig.jsonld;
|
||||
var jsonMode = parserConfig.json || jsonldMode;
|
||||
var trackScope = parserConfig.trackScope !== false
|
||||
var isTS = parserConfig.typescript;
|
||||
var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/;
|
||||
|
||||
// Tokenizer
|
||||
|
||||
var keywords = function(){
|
||||
function kw(type) {return {type: type, style: "keyword"};}
|
||||
var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"), D = kw("keyword d");
|
||||
var operator = kw("operator"), atom = {type: "atom", style: "atom"};
|
||||
|
||||
return {
|
||||
"if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B,
|
||||
"return": D, "break": D, "continue": D, "new": kw("new"), "delete": C, "void": C, "throw": C,
|
||||
"debugger": kw("debugger"), "var": kw("var"), "const": kw("var"), "let": kw("var"),
|
||||
"function": kw("function"), "catch": kw("catch"),
|
||||
"for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"),
|
||||
"in": operator, "typeof": operator, "instanceof": operator,
|
||||
"true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom,
|
||||
"this": kw("this"), "class": kw("class"), "super": kw("atom"),
|
||||
"yield": C, "export": kw("export"), "import": kw("import"), "extends": C,
|
||||
"await": C
|
||||
};
|
||||
}();
|
||||
|
||||
var isOperatorChar = /[+\-*&%=<>!?|~^@]/;
|
||||
var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/;
|
||||
|
||||
function readRegexp(stream) {
|
||||
var escaped = false, next, inSet = false;
|
||||
while ((next = stream.next()) != null) {
|
||||
if (!escaped) {
|
||||
if (next == "/" && !inSet) return;
|
||||
if (next == "[") inSet = true;
|
||||
else if (inSet && next == "]") inSet = false;
|
||||
}
|
||||
escaped = !escaped && next == "\\";
|
||||
}
|
||||
}
|
||||
|
||||
// Used as scratch variables to communicate multiple values without
|
||||
// consing up tons of objects.
|
||||
var type, content;
|
||||
function ret(tp, style, cont) {
|
||||
type = tp; content = cont;
|
||||
return style;
|
||||
}
|
||||
function tokenBase(stream, state) {
|
||||
var ch = stream.next();
|
||||
if (ch == '"' || ch == "'") {
|
||||
state.tokenize = tokenString(ch);
|
||||
return state.tokenize(stream, state);
|
||||
} else if (ch == "." && stream.match(/^\d[\d_]*(?:[eE][+\-]?[\d_]+)?/)) {
|
||||
return ret("number", "number");
|
||||
} else if (ch == "." && stream.match("..")) {
|
||||
return ret("spread", "meta");
|
||||
} else if (/[\[\]{}\(\),;\:\.]/.test(ch)) {
|
||||
return ret(ch);
|
||||
} else if (ch == "=" && stream.eat(">")) {
|
||||
return ret("=>", "operator");
|
||||
} else if (ch == "0" && stream.match(/^(?:x[\dA-Fa-f_]+|o[0-7_]+|b[01_]+)n?/)) {
|
||||
return ret("number", "number");
|
||||
} else if (/\d/.test(ch)) {
|
||||
stream.match(/^[\d_]*(?:n|(?:\.[\d_]*)?(?:[eE][+\-]?[\d_]+)?)?/);
|
||||
return ret("number", "number");
|
||||
} else if (ch == "/") {
|
||||
if (stream.eat("*")) {
|
||||
state.tokenize = tokenComment;
|
||||
return tokenComment(stream, state);
|
||||
} else if (stream.eat("/")) {
|
||||
stream.skipToEnd();
|
||||
return ret("comment", "comment");
|
||||
} else if (expressionAllowed(stream, state, 1)) {
|
||||
readRegexp(stream);
|
||||
stream.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/);
|
||||
return ret("regexp", "string-2");
|
||||
} else {
|
||||
stream.eat("=");
|
||||
return ret("operator", "operator", stream.current());
|
||||
}
|
||||
} else if (ch == "`") {
|
||||
state.tokenize = tokenQuasi;
|
||||
return tokenQuasi(stream, state);
|
||||
} else if (ch == "#" && stream.peek() == "!") {
|
||||
stream.skipToEnd();
|
||||
return ret("meta", "meta");
|
||||
} else if (ch == "#" && stream.eatWhile(wordRE)) {
|
||||
return ret("variable", "property")
|
||||
} else if (ch == "<" && stream.match("!--") ||
|
||||
(ch == "-" && stream.match("->") && !/\S/.test(stream.string.slice(0, stream.start)))) {
|
||||
stream.skipToEnd()
|
||||
return ret("comment", "comment")
|
||||
} else if (isOperatorChar.test(ch)) {
|
||||
if (ch != ">" || !state.lexical || state.lexical.type != ">") {
|
||||
if (stream.eat("=")) {
|
||||
if (ch == "!" || ch == "=") stream.eat("=")
|
||||
} else if (/[<>*+\-|&?]/.test(ch)) {
|
||||
stream.eat(ch)
|
||||
if (ch == ">") stream.eat(ch)
|
||||
}
|
||||
}
|
||||
if (ch == "?" && stream.eat(".")) return ret(".")
|
||||
return ret("operator", "operator", stream.current());
|
||||
} else if (wordRE.test(ch)) {
|
||||
stream.eatWhile(wordRE);
|
||||
var word = stream.current()
|
||||
if (state.lastType != ".") {
|
||||
if (keywords.propertyIsEnumerable(word)) {
|
||||
var kw = keywords[word]
|
||||
return ret(kw.type, kw.style, word)
|
||||
}
|
||||
if (word == "async" && stream.match(/^(\s|\/\*([^*]|\*(?!\/))*?\*\/)*[\[\(\w]/, false))
|
||||
return ret("async", "keyword", word)
|
||||
}
|
||||
return ret("variable", "variable", word)
|
||||
}
|
||||
}
|
||||
|
||||
function tokenString(quote) {
|
||||
return function(stream, state) {
|
||||
var escaped = false, next;
|
||||
if (jsonldMode && stream.peek() == "@" && stream.match(isJsonldKeyword)){
|
||||
state.tokenize = tokenBase;
|
||||
return ret("jsonld-keyword", "meta");
|
||||
}
|
||||
while ((next = stream.next()) != null) {
|
||||
if (next == quote && !escaped) break;
|
||||
escaped = !escaped && next == "\\";
|
||||
}
|
||||
if (!escaped) state.tokenize = tokenBase;
|
||||
return ret("string", "string");
|
||||
};
|
||||
}
|
||||
|
||||
function tokenComment(stream, state) {
|
||||
var maybeEnd = false, ch;
|
||||
while (ch = stream.next()) {
|
||||
if (ch == "/" && maybeEnd) {
|
||||
state.tokenize = tokenBase;
|
||||
break;
|
||||
}
|
||||
maybeEnd = (ch == "*");
|
||||
}
|
||||
return ret("comment", "comment");
|
||||
}
|
||||
|
||||
function tokenQuasi(stream, state) {
|
||||
var escaped = false, next;
|
||||
while ((next = stream.next()) != null) {
|
||||
if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) {
|
||||
state.tokenize = tokenBase;
|
||||
break;
|
||||
}
|
||||
escaped = !escaped && next == "\\";
|
||||
}
|
||||
return ret("quasi", "string-2", stream.current());
|
||||
}
|
||||
|
||||
var brackets = "([{}])";
|
||||
// This is a crude lookahead trick to try and notice that we're
|
||||
// parsing the argument patterns for a fat-arrow function before we
|
||||
// actually hit the arrow token. It only works if the arrow is on
|
||||
// the same line as the arguments and there's no strange noise
|
||||
// (comments) in between. Fallback is to only notice when we hit the
|
||||
// arrow, and not declare the arguments as locals for the arrow
|
||||
// body.
|
||||
function findFatArrow(stream, state) {
|
||||
if (state.fatArrowAt) state.fatArrowAt = null;
|
||||
var arrow = stream.string.indexOf("=>", stream.start);
|
||||
if (arrow < 0) return;
|
||||
|
||||
if (isTS) { // Try to skip TypeScript return type declarations after the arguments
|
||||
var m = /:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(stream.string.slice(stream.start, arrow))
|
||||
if (m) arrow = m.index
|
||||
}
|
||||
|
||||
var depth = 0, sawSomething = false;
|
||||
for (var pos = arrow - 1; pos >= 0; --pos) {
|
||||
var ch = stream.string.charAt(pos);
|
||||
var bracket = brackets.indexOf(ch);
|
||||
if (bracket >= 0 && bracket < 3) {
|
||||
if (!depth) { ++pos; break; }
|
||||
if (--depth == 0) { if (ch == "(") sawSomething = true; break; }
|
||||
} else if (bracket >= 3 && bracket < 6) {
|
||||
++depth;
|
||||
} else if (wordRE.test(ch)) {
|
||||
sawSomething = true;
|
||||
} else if (/["'\/`]/.test(ch)) {
|
||||
for (;; --pos) {
|
||||
if (pos == 0) return
|
||||
var next = stream.string.charAt(pos - 1)
|
||||
if (next == ch && stream.string.charAt(pos - 2) != "\\") { pos--; break }
|
||||
}
|
||||
} else if (sawSomething && !depth) {
|
||||
++pos;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (sawSomething && !depth) state.fatArrowAt = pos;
|
||||
}
|
||||
|
||||
// Parser
|
||||
|
||||
var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true,
|
||||
"regexp": true, "this": true, "import": true, "jsonld-keyword": true};
|
||||
|
||||
function JSLexical(indented, column, type, align, prev, info) {
|
||||
this.indented = indented;
|
||||
this.column = column;
|
||||
this.type = type;
|
||||
this.prev = prev;
|
||||
this.info = info;
|
||||
if (align != null) this.align = align;
|
||||
}
|
||||
|
||||
function inScope(state, varname) {
|
||||
if (!trackScope) return false
|
||||
for (var v = state.localVars; v; v = v.next)
|
||||
if (v.name == varname) return true;
|
||||
for (var cx = state.context; cx; cx = cx.prev) {
|
||||
for (var v = cx.vars; v; v = v.next)
|
||||
if (v.name == varname) return true;
|
||||
}
|
||||
}
|
||||
|
||||
function parseJS(state, style, type, content, stream) {
|
||||
var cc = state.cc;
|
||||
// Communicate our context to the combinators.
|
||||
// (Less wasteful than consing up a hundred closures on every call.)
|
||||
cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style;
|
||||
|
||||
if (!state.lexical.hasOwnProperty("align"))
|
||||
state.lexical.align = true;
|
||||
|
||||
while(true) {
|
||||
var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement;
|
||||
if (combinator(type, content)) {
|
||||
while(cc.length && cc[cc.length - 1].lex)
|
||||
cc.pop()();
|
||||
if (cx.marked) return cx.marked;
|
||||
if (type == "variable" && inScope(state, content)) return "variable-2";
|
||||
return style;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Combinator utils
|
||||
|
||||
var cx = {state: null, column: null, marked: null, cc: null};
|
||||
function pass() {
|
||||
for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]);
|
||||
}
|
||||
function cont() {
|
||||
pass.apply(null, arguments);
|
||||
return true;
|
||||
}
|
||||
function inList(name, list) {
|
||||
for (var v = list; v; v = v.next) if (v.name == name) return true
|
||||
return false;
|
||||
}
|
||||
function register(varname) {
|
||||
var state = cx.state;
|
||||
cx.marked = "def";
|
||||
if (!trackScope) return
|
||||
if (state.context) {
|
||||
if (state.lexical.info == "var" && state.context && state.context.block) {
|
||||
// FIXME function decls are also not block scoped
|
||||
var newContext = registerVarScoped(varname, state.context)
|
||||
if (newContext != null) {
|
||||
state.context = newContext
|
||||
return
|
||||
}
|
||||
} else if (!inList(varname, state.localVars)) {
|
||||
state.localVars = new Var(varname, state.localVars)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Fall through means this is global
|
||||
if (parserConfig.globalVars && !inList(varname, state.globalVars))
|
||||
state.globalVars = new Var(varname, state.globalVars)
|
||||
}
|
||||
function registerVarScoped(varname, context) {
|
||||
if (!context) {
|
||||
return null
|
||||
} else if (context.block) {
|
||||
var inner = registerVarScoped(varname, context.prev)
|
||||
if (!inner) return null
|
||||
if (inner == context.prev) return context
|
||||
return new Context(inner, context.vars, true)
|
||||
} else if (inList(varname, context.vars)) {
|
||||
return context
|
||||
} else {
|
||||
return new Context(context.prev, new Var(varname, context.vars), false)
|
||||
}
|
||||
}
|
||||
|
||||
function isModifier(name) {
|
||||
return name == "public" || name == "private" || name == "protected" || name == "abstract" || name == "readonly"
|
||||
}
|
||||
|
||||
// Combinators
|
||||
|
||||
function Context(prev, vars, block) { this.prev = prev; this.vars = vars; this.block = block }
|
||||
function Var(name, next) { this.name = name; this.next = next }
|
||||
|
||||
var defaultVars = new Var("this", new Var("arguments", null))
|
||||
function pushcontext() {
|
||||
cx.state.context = new Context(cx.state.context, cx.state.localVars, false)
|
||||
cx.state.localVars = defaultVars
|
||||
}
|
||||
function pushblockcontext() {
|
||||
cx.state.context = new Context(cx.state.context, cx.state.localVars, true)
|
||||
cx.state.localVars = null
|
||||
}
|
||||
pushcontext.lex = pushblockcontext.lex = true
|
||||
function popcontext() {
|
||||
cx.state.localVars = cx.state.context.vars
|
||||
cx.state.context = cx.state.context.prev
|
||||
}
|
||||
popcontext.lex = true
|
||||
function pushlex(type, info) {
|
||||
var result = function() {
|
||||
var state = cx.state, indent = state.indented;
|
||||
if (state.lexical.type == "stat") indent = state.lexical.indented;
|
||||
else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev)
|
||||
indent = outer.indented;
|
||||
state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info);
|
||||
};
|
||||
result.lex = true;
|
||||
return result;
|
||||
}
|
||||
function poplex() {
|
||||
var state = cx.state;
|
||||
if (state.lexical.prev) {
|
||||
if (state.lexical.type == ")")
|
||||
state.indented = state.lexical.indented;
|
||||
state.lexical = state.lexical.prev;
|
||||
}
|
||||
}
|
||||
poplex.lex = true;
|
||||
|
||||
function expect(wanted) {
|
||||
function exp(type) {
|
||||
if (type == wanted) return cont();
|
||||
else if (wanted == ";" || type == "}" || type == ")" || type == "]") return pass();
|
||||
else return cont(exp);
|
||||
}
|
||||
return exp;
|
||||
}
|
||||
|
||||
function statement(type, value) {
|
||||
if (type == "var") return cont(pushlex("vardef", value), vardef, expect(";"), poplex);
|
||||
if (type == "keyword a") return cont(pushlex("form"), parenExpr, statement, poplex);
|
||||
if (type == "keyword b") return cont(pushlex("form"), statement, poplex);
|
||||
if (type == "keyword d") return cx.stream.match(/^\s*$/, false) ? cont() : cont(pushlex("stat"), maybeexpression, expect(";"), poplex);
|
||||
if (type == "debugger") return cont(expect(";"));
|
||||
if (type == "{") return cont(pushlex("}"), pushblockcontext, block, poplex, popcontext);
|
||||
if (type == ";") return cont();
|
||||
if (type == "if") {
|
||||
if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex)
|
||||
cx.state.cc.pop()();
|
||||
return cont(pushlex("form"), parenExpr, statement, poplex, maybeelse);
|
||||
}
|
||||
if (type == "function") return cont(functiondef);
|
||||
if (type == "for") return cont(pushlex("form"), pushblockcontext, forspec, statement, popcontext, poplex);
|
||||
if (type == "class" || (isTS && value == "interface")) {
|
||||
cx.marked = "keyword"
|
||||
return cont(pushlex("form", type == "class" ? type : value), className, poplex)
|
||||
}
|
||||
if (type == "variable") {
|
||||
if (isTS && value == "declare") {
|
||||
cx.marked = "keyword"
|
||||
return cont(statement)
|
||||
} else if (isTS && (value == "module" || value == "enum" || value == "type") && cx.stream.match(/^\s*\w/, false)) {
|
||||
cx.marked = "keyword"
|
||||
if (value == "enum") return cont(enumdef);
|
||||
else if (value == "type") return cont(typename, expect("operator"), typeexpr, expect(";"));
|
||||
else return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex)
|
||||
} else if (isTS && value == "namespace") {
|
||||
cx.marked = "keyword"
|
||||
return cont(pushlex("form"), expression, statement, poplex)
|
||||
} else if (isTS && value == "abstract") {
|
||||
cx.marked = "keyword"
|
||||
return cont(statement)
|
||||
} else {
|
||||
return cont(pushlex("stat"), maybelabel);
|
||||
}
|
||||
}
|
||||
if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"), pushblockcontext,
|
||||
block, poplex, poplex, popcontext);
|
||||
if (type == "case") return cont(expression, expect(":"));
|
||||
if (type == "default") return cont(expect(":"));
|
||||
if (type == "catch") return cont(pushlex("form"), pushcontext, maybeCatchBinding, statement, poplex, popcontext);
|
||||
if (type == "export") return cont(pushlex("stat"), afterExport, poplex);
|
||||
if (type == "import") return cont(pushlex("stat"), afterImport, poplex);
|
||||
if (type == "async") return cont(statement)
|
||||
if (value == "@") return cont(expression, statement)
|
||||
return pass(pushlex("stat"), expression, expect(";"), poplex);
|
||||
}
|
||||
function maybeCatchBinding(type) {
|
||||
if (type == "(") return cont(funarg, expect(")"))
|
||||
}
|
||||
function expression(type, value) {
|
||||
return expressionInner(type, value, false);
|
||||
}
|
||||
function expressionNoComma(type, value) {
|
||||
return expressionInner(type, value, true);
|
||||
}
|
||||
function parenExpr(type) {
|
||||
if (type != "(") return pass()
|
||||
return cont(pushlex(")"), maybeexpression, expect(")"), poplex)
|
||||
}
|
||||
function expressionInner(type, value, noComma) {
|
||||
if (cx.state.fatArrowAt == cx.stream.start) {
|
||||
var body = noComma ? arrowBodyNoComma : arrowBody;
|
||||
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, expect("=>"), body, popcontext);
|
||||
else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext);
|
||||
}
|
||||
|
||||
var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma;
|
||||
if (atomicTypes.hasOwnProperty(type)) return cont(maybeop);
|
||||
if (type == "function") return cont(functiondef, maybeop);
|
||||
if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword"; return cont(pushlex("form"), classExpression, poplex); }
|
||||
if (type == "keyword c" || type == "async") return cont(noComma ? expressionNoComma : expression);
|
||||
if (type == "(") return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeop);
|
||||
if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression);
|
||||
if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop);
|
||||
if (type == "{") return contCommasep(objprop, "}", null, maybeop);
|
||||
if (type == "quasi") return pass(quasi, maybeop);
|
||||
if (type == "new") return cont(maybeTarget(noComma));
|
||||
return cont();
|
||||
}
|
||||
function maybeexpression(type) {
|
||||
if (type.match(/[;\}\)\],]/)) return pass();
|
||||
return pass(expression);
|
||||
}
|
||||
|
||||
function maybeoperatorComma(type, value) {
|
||||
if (type == ",") return cont(maybeexpression);
|
||||
return maybeoperatorNoComma(type, value, false);
|
||||
}
|
||||
function maybeoperatorNoComma(type, value, noComma) {
|
||||
var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma;
|
||||
var expr = noComma == false ? expression : expressionNoComma;
|
||||
if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext);
|
||||
if (type == "operator") {
|
||||
if (/\+\+|--/.test(value) || isTS && value == "!") return cont(me);
|
||||
if (isTS && value == "<" && cx.stream.match(/^([^<>]|<[^<>]*>)*>\s*\(/, false))
|
||||
return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, me);
|
||||
if (value == "?") return cont(expression, expect(":"), expr);
|
||||
return cont(expr);
|
||||
}
|
||||
if (type == "quasi") { return pass(quasi, me); }
|
||||
if (type == ";") return;
|
||||
if (type == "(") return contCommasep(expressionNoComma, ")", "call", me);
|
||||
if (type == ".") return cont(property, me);
|
||||
if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me);
|
||||
if (isTS && value == "as") { cx.marked = "keyword"; return cont(typeexpr, me) }
|
||||
if (type == "regexp") {
|
||||
cx.state.lastType = cx.marked = "operator"
|
||||
cx.stream.backUp(cx.stream.pos - cx.stream.start - 1)
|
||||
return cont(expr)
|
||||
}
|
||||
}
|
||||
function quasi(type, value) {
|
||||
if (type != "quasi") return pass();
|
||||
if (value.slice(value.length - 2) != "${") return cont(quasi);
|
||||
return cont(maybeexpression, continueQuasi);
|
||||
}
|
||||
function continueQuasi(type) {
|
||||
if (type == "}") {
|
||||
cx.marked = "string-2";
|
||||
cx.state.tokenize = tokenQuasi;
|
||||
return cont(quasi);
|
||||
}
|
||||
}
|
||||
function arrowBody(type) {
|
||||
findFatArrow(cx.stream, cx.state);
|
||||
return pass(type == "{" ? statement : expression);
|
||||
}
|
||||
function arrowBodyNoComma(type) {
|
||||
findFatArrow(cx.stream, cx.state);
|
||||
return pass(type == "{" ? statement : expressionNoComma);
|
||||
}
|
||||
function maybeTarget(noComma) {
|
||||
return function(type) {
|
||||
if (type == ".") return cont(noComma ? targetNoComma : target);
|
||||
else if (type == "variable" && isTS) return cont(maybeTypeArgs, noComma ? maybeoperatorNoComma : maybeoperatorComma)
|
||||
else return pass(noComma ? expressionNoComma : expression);
|
||||
};
|
||||
}
|
||||
function target(_, value) {
|
||||
if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorComma); }
|
||||
}
|
||||
function targetNoComma(_, value) {
|
||||
if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorNoComma); }
|
||||
}
|
||||
function maybelabel(type) {
|
||||
if (type == ":") return cont(poplex, statement);
|
||||
return pass(maybeoperatorComma, expect(";"), poplex);
|
||||
}
|
||||
function property(type) {
|
||||
if (type == "variable") {cx.marked = "property"; return cont();}
|
||||
}
|
||||
function objprop(type, value) {
|
||||
if (type == "async") {
|
||||
cx.marked = "property";
|
||||
return cont(objprop);
|
||||
} else if (type == "variable" || cx.style == "keyword") {
|
||||
cx.marked = "property";
|
||||
if (value == "get" || value == "set") return cont(getterSetter);
|
||||
var m // Work around fat-arrow-detection complication for detecting typescript typed arrow params
|
||||
if (isTS && cx.state.fatArrowAt == cx.stream.start && (m = cx.stream.match(/^\s*:\s*/, false)))
|
||||
cx.state.fatArrowAt = cx.stream.pos + m[0].length
|
||||
return cont(afterprop);
|
||||
} else if (type == "number" || type == "string") {
|
||||
cx.marked = jsonldMode ? "property" : (cx.style + " property");
|
||||
return cont(afterprop);
|
||||
} else if (type == "jsonld-keyword") {
|
||||
return cont(afterprop);
|
||||
} else if (isTS && isModifier(value)) {
|
||||
cx.marked = "keyword"
|
||||
return cont(objprop)
|
||||
} else if (type == "[") {
|
||||
return cont(expression, maybetype, expect("]"), afterprop);
|
||||
} else if (type == "spread") {
|
||||
return cont(expressionNoComma, afterprop);
|
||||
} else if (value == "*") {
|
||||
cx.marked = "keyword";
|
||||
return cont(objprop);
|
||||
} else if (type == ":") {
|
||||
return pass(afterprop)
|
||||
}
|
||||
}
|
||||
function getterSetter(type) {
|
||||
if (type != "variable") return pass(afterprop);
|
||||
cx.marked = "property";
|
||||
return cont(functiondef);
|
||||
}
|
||||
function afterprop(type) {
|
||||
if (type == ":") return cont(expressionNoComma);
|
||||
if (type == "(") return pass(functiondef);
|
||||
}
|
||||
function commasep(what, end, sep) {
|
||||
function proceed(type, value) {
|
||||
if (sep ? sep.indexOf(type) > -1 : type == ",") {
|
||||
var lex = cx.state.lexical;
|
||||
if (lex.info == "call") lex.pos = (lex.pos || 0) + 1;
|
||||
return cont(function(type, value) {
|
||||
if (type == end || value == end) return pass()
|
||||
return pass(what)
|
||||
}, proceed);
|
||||
}
|
||||
if (type == end || value == end) return cont();
|
||||
if (sep && sep.indexOf(";") > -1) return pass(what)
|
||||
return cont(expect(end));
|
||||
}
|
||||
return function(type, value) {
|
||||
if (type == end || value == end) return cont();
|
||||
return pass(what, proceed);
|
||||
};
|
||||
}
|
||||
function contCommasep(what, end, info) {
|
||||
for (var i = 3; i < arguments.length; i++)
|
||||
cx.cc.push(arguments[i]);
|
||||
return cont(pushlex(end, info), commasep(what, end), poplex);
|
||||
}
|
||||
function block(type) {
|
||||
if (type == "}") return cont();
|
||||
return pass(statement, block);
|
||||
}
|
||||
function maybetype(type, value) {
|
||||
if (isTS) {
|
||||
if (type == ":") return cont(typeexpr);
|
||||
if (value == "?") return cont(maybetype);
|
||||
}
|
||||
}
|
||||
function maybetypeOrIn(type, value) {
|
||||
if (isTS && (type == ":" || value == "in")) return cont(typeexpr)
|
||||
}
|
||||
function mayberettype(type) {
|
||||
if (isTS && type == ":") {
|
||||
if (cx.stream.match(/^\s*\w+\s+is\b/, false)) return cont(expression, isKW, typeexpr)
|
||||
else return cont(typeexpr)
|
||||
}
|
||||
}
|
||||
function isKW(_, value) {
|
||||
if (value == "is") {
|
||||
cx.marked = "keyword"
|
||||
return cont()
|
||||
}
|
||||
}
|
||||
function typeexpr(type, value) {
|
||||
if (value == "keyof" || value == "typeof" || value == "infer" || value == "readonly") {
|
||||
cx.marked = "keyword"
|
||||
return cont(value == "typeof" ? expressionNoComma : typeexpr)
|
||||
}
|
||||
if (type == "variable" || value == "void") {
|
||||
cx.marked = "type"
|
||||
return cont(afterType)
|
||||
}
|
||||
if (value == "|" || value == "&") return cont(typeexpr)
|
||||
if (type == "string" || type == "number" || type == "atom") return cont(afterType);
|
||||
if (type == "[") return cont(pushlex("]"), commasep(typeexpr, "]", ","), poplex, afterType)
|
||||
if (type == "{") return cont(pushlex("}"), typeprops, poplex, afterType)
|
||||
if (type == "(") return cont(commasep(typearg, ")"), maybeReturnType, afterType)
|
||||
if (type == "<") return cont(commasep(typeexpr, ">"), typeexpr)
|
||||
if (type == "quasi") { return pass(quasiType, afterType); }
|
||||
}
|
||||
function maybeReturnType(type) {
|
||||
if (type == "=>") return cont(typeexpr)
|
||||
}
|
||||
function typeprops(type) {
|
||||
if (type.match(/[\}\)\]]/)) return cont()
|
||||
if (type == "," || type == ";") return cont(typeprops)
|
||||
return pass(typeprop, typeprops)
|
||||
}
|
||||
function typeprop(type, value) {
|
||||
if (type == "variable" || cx.style == "keyword") {
|
||||
cx.marked = "property"
|
||||
return cont(typeprop)
|
||||
} else if (value == "?" || type == "number" || type == "string") {
|
||||
return cont(typeprop)
|
||||
} else if (type == ":") {
|
||||
return cont(typeexpr)
|
||||
} else if (type == "[") {
|
||||
return cont(expect("variable"), maybetypeOrIn, expect("]"), typeprop)
|
||||
} else if (type == "(") {
|
||||
return pass(functiondecl, typeprop)
|
||||
} else if (!type.match(/[;\}\)\],]/)) {
|
||||
return cont()
|
||||
}
|
||||
}
|
||||
function quasiType(type, value) {
|
||||
if (type != "quasi") return pass();
|
||||
if (value.slice(value.length - 2) != "${") return cont(quasiType);
|
||||
return cont(typeexpr, continueQuasiType);
|
||||
}
|
||||
function continueQuasiType(type) {
|
||||
if (type == "}") {
|
||||
cx.marked = "string-2";
|
||||
cx.state.tokenize = tokenQuasi;
|
||||
return cont(quasiType);
|
||||
}
|
||||
}
|
||||
function typearg(type, value) {
|
||||
if (type == "variable" && cx.stream.match(/^\s*[?:]/, false) || value == "?") return cont(typearg)
|
||||
if (type == ":") return cont(typeexpr)
|
||||
if (type == "spread") return cont(typearg)
|
||||
return pass(typeexpr)
|
||||
}
|
||||
function afterType(type, value) {
|
||||
if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType)
|
||||
if (value == "|" || type == "." || value == "&") return cont(typeexpr)
|
||||
if (type == "[") return cont(typeexpr, expect("]"), afterType)
|
||||
if (value == "extends" || value == "implements") { cx.marked = "keyword"; return cont(typeexpr) }
|
||||
if (value == "?") return cont(typeexpr, expect(":"), typeexpr)
|
||||
}
|
||||
function maybeTypeArgs(_, value) {
|
||||
if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType)
|
||||
}
|
||||
function typeparam() {
|
||||
return pass(typeexpr, maybeTypeDefault)
|
||||
}
|
||||
function maybeTypeDefault(_, value) {
|
||||
if (value == "=") return cont(typeexpr)
|
||||
}
|
||||
function vardef(_, value) {
|
||||
if (value == "enum") {cx.marked = "keyword"; return cont(enumdef)}
|
||||
return pass(pattern, maybetype, maybeAssign, vardefCont);
|
||||
}
|
||||
function pattern(type, value) {
|
||||
if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(pattern) }
|
||||
if (type == "variable") { register(value); return cont(); }
|
||||
if (type == "spread") return cont(pattern);
|
||||
if (type == "[") return contCommasep(eltpattern, "]");
|
||||
if (type == "{") return contCommasep(proppattern, "}");
|
||||
}
|
||||
function proppattern(type, value) {
|
||||
if (type == "variable" && !cx.stream.match(/^\s*:/, false)) {
|
||||
register(value);
|
||||
return cont(maybeAssign);
|
||||
}
|
||||
if (type == "variable") cx.marked = "property";
|
||||
if (type == "spread") return cont(pattern);
|
||||
if (type == "}") return pass();
|
||||
if (type == "[") return cont(expression, expect(']'), expect(':'), proppattern);
|
||||
return cont(expect(":"), pattern, maybeAssign);
|
||||
}
|
||||
function eltpattern() {
|
||||
return pass(pattern, maybeAssign)
|
||||
}
|
||||
function maybeAssign(_type, value) {
|
||||
if (value == "=") return cont(expressionNoComma);
|
||||
}
|
||||
function vardefCont(type) {
|
||||
if (type == ",") return cont(vardef);
|
||||
}
|
||||
function maybeelse(type, value) {
|
||||
if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex);
|
||||
}
|
||||
function forspec(type, value) {
|
||||
if (value == "await") return cont(forspec);
|
||||
if (type == "(") return cont(pushlex(")"), forspec1, poplex);
|
||||
}
|
||||
function forspec1(type) {
|
||||
if (type == "var") return cont(vardef, forspec2);
|
||||
if (type == "variable") return cont(forspec2);
|
||||
return pass(forspec2)
|
||||
}
|
||||
function forspec2(type, value) {
|
||||
if (type == ")") return cont()
|
||||
if (type == ";") return cont(forspec2)
|
||||
if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression, forspec2) }
|
||||
return pass(expression, forspec2)
|
||||
}
|
||||
function functiondef(type, value) {
|
||||
if (value == "*") {cx.marked = "keyword"; return cont(functiondef);}
|
||||
if (type == "variable") {register(value); return cont(functiondef);}
|
||||
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, statement, popcontext);
|
||||
if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondef)
|
||||
}
|
||||
function functiondecl(type, value) {
|
||||
if (value == "*") {cx.marked = "keyword"; return cont(functiondecl);}
|
||||
if (type == "variable") {register(value); return cont(functiondecl);}
|
||||
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, popcontext);
|
||||
if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondecl)
|
||||
}
|
||||
function typename(type, value) {
|
||||
if (type == "keyword" || type == "variable") {
|
||||
cx.marked = "type"
|
||||
return cont(typename)
|
||||
} else if (value == "<") {
|
||||
return cont(pushlex(">"), commasep(typeparam, ">"), poplex)
|
||||
}
|
||||
}
|
||||
function funarg(type, value) {
|
||||
if (value == "@") cont(expression, funarg)
|
||||
if (type == "spread") return cont(funarg);
|
||||
if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(funarg); }
|
||||
if (isTS && type == "this") return cont(maybetype, maybeAssign)
|
||||
return pass(pattern, maybetype, maybeAssign);
|
||||
}
|
||||
function classExpression(type, value) {
|
||||
// Class expressions may have an optional name.
|
||||
if (type == "variable") return className(type, value);
|
||||
return classNameAfter(type, value);
|
||||
}
|
||||
function className(type, value) {
|
||||
if (type == "variable") {register(value); return cont(classNameAfter);}
|
||||
}
|
||||
function classNameAfter(type, value) {
|
||||
if (value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, classNameAfter)
|
||||
if (value == "extends" || value == "implements" || (isTS && type == ",")) {
|
||||
if (value == "implements") cx.marked = "keyword";
|
||||
return cont(isTS ? typeexpr : expression, classNameAfter);
|
||||
}
|
||||
if (type == "{") return cont(pushlex("}"), classBody, poplex);
|
||||
}
|
||||
function classBody(type, value) {
|
||||
if (type == "async" ||
|
||||
(type == "variable" &&
|
||||
(value == "static" || value == "get" || value == "set" || (isTS && isModifier(value))) &&
|
||||
cx.stream.match(/^\s+#?[\w$\xa1-\uffff]/, false))) {
|
||||
cx.marked = "keyword";
|
||||
return cont(classBody);
|
||||
}
|
||||
if (type == "variable" || cx.style == "keyword") {
|
||||
cx.marked = "property";
|
||||
return cont(classfield, classBody);
|
||||
}
|
||||
if (type == "number" || type == "string") return cont(classfield, classBody);
|
||||
if (type == "[")
|
||||
return cont(expression, maybetype, expect("]"), classfield, classBody)
|
||||
if (value == "*") {
|
||||
cx.marked = "keyword";
|
||||
return cont(classBody);
|
||||
}
|
||||
if (isTS && type == "(") return pass(functiondecl, classBody)
|
||||
if (type == ";" || type == ",") return cont(classBody);
|
||||
if (type == "}") return cont();
|
||||
if (value == "@") return cont(expression, classBody)
|
||||
}
|
||||
function classfield(type, value) {
|
||||
if (value == "!") return cont(classfield)
|
||||
if (value == "?") return cont(classfield)
|
||||
if (type == ":") return cont(typeexpr, maybeAssign)
|
||||
if (value == "=") return cont(expressionNoComma)
|
||||
var context = cx.state.lexical.prev, isInterface = context && context.info == "interface"
|
||||
return pass(isInterface ? functiondecl : functiondef)
|
||||
}
|
||||
function afterExport(type, value) {
|
||||
if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); }
|
||||
if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); }
|
||||
if (type == "{") return cont(commasep(exportField, "}"), maybeFrom, expect(";"));
|
||||
return pass(statement);
|
||||
}
|
||||
function exportField(type, value) {
|
||||
if (value == "as") { cx.marked = "keyword"; return cont(expect("variable")); }
|
||||
if (type == "variable") return pass(expressionNoComma, exportField);
|
||||
}
|
||||
function afterImport(type) {
|
||||
if (type == "string") return cont();
|
||||
if (type == "(") return pass(expression);
|
||||
if (type == ".") return pass(maybeoperatorComma);
|
||||
return pass(importSpec, maybeMoreImports, maybeFrom);
|
||||
}
|
||||
function importSpec(type, value) {
|
||||
if (type == "{") return contCommasep(importSpec, "}");
|
||||
if (type == "variable") register(value);
|
||||
if (value == "*") cx.marked = "keyword";
|
||||
return cont(maybeAs);
|
||||
}
|
||||
function maybeMoreImports(type) {
|
||||
if (type == ",") return cont(importSpec, maybeMoreImports)
|
||||
}
|
||||
function maybeAs(_type, value) {
|
||||
if (value == "as") { cx.marked = "keyword"; return cont(importSpec); }
|
||||
}
|
||||
function maybeFrom(_type, value) {
|
||||
if (value == "from") { cx.marked = "keyword"; return cont(expression); }
|
||||
}
|
||||
function arrayLiteral(type) {
|
||||
if (type == "]") return cont();
|
||||
return pass(commasep(expressionNoComma, "]"));
|
||||
}
|
||||
function enumdef() {
|
||||
return pass(pushlex("form"), pattern, expect("{"), pushlex("}"), commasep(enummember, "}"), poplex, poplex)
|
||||
}
|
||||
function enummember() {
|
||||
return pass(pattern, maybeAssign);
|
||||
}
|
||||
|
||||
function isContinuedStatement(state, textAfter) {
|
||||
return state.lastType == "operator" || state.lastType == "," ||
|
||||
isOperatorChar.test(textAfter.charAt(0)) ||
|
||||
/[,.]/.test(textAfter.charAt(0));
|
||||
}
|
||||
|
||||
function expressionAllowed(stream, state, backUp) {
|
||||
return state.tokenize == tokenBase &&
|
||||
/^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(state.lastType) ||
|
||||
(state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0))))
|
||||
}
|
||||
|
||||
// Interface
|
||||
|
||||
return {
|
||||
startState: function(basecolumn) {
|
||||
var state = {
|
||||
tokenize: tokenBase,
|
||||
lastType: "sof",
|
||||
cc: [],
|
||||
lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false),
|
||||
localVars: parserConfig.localVars,
|
||||
context: parserConfig.localVars && new Context(null, null, false),
|
||||
indented: basecolumn || 0
|
||||
};
|
||||
if (parserConfig.globalVars && typeof parserConfig.globalVars == "object")
|
||||
state.globalVars = parserConfig.globalVars;
|
||||
return state;
|
||||
},
|
||||
|
||||
token: function(stream, state) {
|
||||
if (stream.sol()) {
|
||||
if (!state.lexical.hasOwnProperty("align"))
|
||||
state.lexical.align = false;
|
||||
state.indented = stream.indentation();
|
||||
findFatArrow(stream, state);
|
||||
}
|
||||
if (state.tokenize != tokenComment && stream.eatSpace()) return null;
|
||||
var style = state.tokenize(stream, state);
|
||||
if (type == "comment") return style;
|
||||
state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type;
|
||||
return parseJS(state, style, type, content, stream);
|
||||
},
|
||||
|
||||
indent: function(state, textAfter) {
|
||||
if (state.tokenize == tokenComment || state.tokenize == tokenQuasi) return CodeMirror.Pass;
|
||||
if (state.tokenize != tokenBase) return 0;
|
||||
var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, top
|
||||
// Kludge to prevent 'maybelse' from blocking lexical scope pops
|
||||
if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) {
|
||||
var c = state.cc[i];
|
||||
if (c == poplex) lexical = lexical.prev;
|
||||
else if (c != maybeelse && c != popcontext) break;
|
||||
}
|
||||
while ((lexical.type == "stat" || lexical.type == "form") &&
|
||||
(firstChar == "}" || ((top = state.cc[state.cc.length - 1]) &&
|
||||
(top == maybeoperatorComma || top == maybeoperatorNoComma) &&
|
||||
!/^[,\.=+\-*:?[\(]/.test(textAfter))))
|
||||
lexical = lexical.prev;
|
||||
if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat")
|
||||
lexical = lexical.prev;
|
||||
var type = lexical.type, closing = firstChar == type;
|
||||
|
||||
if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info.length + 1 : 0);
|
||||
else if (type == "form" && firstChar == "{") return lexical.indented;
|
||||
else if (type == "form") return lexical.indented + indentUnit;
|
||||
else if (type == "stat")
|
||||
return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0);
|
||||
else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false)
|
||||
return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit);
|
||||
else if (lexical.align) return lexical.column + (closing ? 0 : 1);
|
||||
else return lexical.indented + (closing ? 0 : indentUnit);
|
||||
},
|
||||
|
||||
electricInput: /^\s*(?:case .*?:|default:|\{|\})$/,
|
||||
blockCommentStart: jsonMode ? null : "/*",
|
||||
blockCommentEnd: jsonMode ? null : "*/",
|
||||
blockCommentContinue: jsonMode ? null : " * ",
|
||||
lineComment: jsonMode ? null : "//",
|
||||
fold: "brace",
|
||||
closeBrackets: "()[]{}''\"\"``",
|
||||
|
||||
helperType: jsonMode ? "json" : "javascript",
|
||||
jsonldMode: jsonldMode,
|
||||
jsonMode: jsonMode,
|
||||
|
||||
expressionAllowed: expressionAllowed,
|
||||
|
||||
skipExpression: function(state) {
|
||||
parseJS(state, "atom", "atom", "true", new CodeMirror.StringStream("", 2, null))
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/);
|
||||
|
||||
CodeMirror.defineMIME("text/javascript", "javascript");
|
||||
CodeMirror.defineMIME("text/ecmascript", "javascript");
|
||||
CodeMirror.defineMIME("application/javascript", "javascript");
|
||||
CodeMirror.defineMIME("application/x-javascript", "javascript");
|
||||
CodeMirror.defineMIME("application/ecmascript", "javascript");
|
||||
CodeMirror.defineMIME("application/json", { name: "javascript", json: true });
|
||||
CodeMirror.defineMIME("application/x-json", { name: "javascript", json: true });
|
||||
CodeMirror.defineMIME("application/manifest+json", { name: "javascript", json: true })
|
||||
CodeMirror.defineMIME("application/ld+json", { name: "javascript", jsonld: true });
|
||||
CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true });
|
||||
CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true });
|
||||
|
||||
});
|
||||
32076
web/assets/codemirror/jshint.js
Normal file
1
web/assets/codemirror/jsonlint.js
Normal file
65
web/assets/codemirror/lint/javascript-lint.js
Normal file
@@ -0,0 +1,65 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
|
||||
// Depends on jshint.js from https://github.com/jshint/jshint
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
// declare global: JSHINT
|
||||
|
||||
function validator(text, options) {
|
||||
if (!window.JSHINT) {
|
||||
if (window.console) {
|
||||
window.console.error("Error: window.JSHINT not defined, CodeMirror JavaScript linting cannot run.");
|
||||
}
|
||||
return [];
|
||||
}
|
||||
if (!options.indent) // JSHint error.character actually is a column index, this fixes underlining on lines using tabs for indentation
|
||||
options.indent = 1; // JSHint default value is 4
|
||||
JSHINT(text, options, options.globals);
|
||||
var errors = JSHINT.data().errors, result = [];
|
||||
if (errors) parseErrors(errors, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
CodeMirror.registerHelper("lint", "javascript", validator);
|
||||
|
||||
function parseErrors(errors, output) {
|
||||
for ( var i = 0; i < errors.length; i++) {
|
||||
var error = errors[i];
|
||||
if (error) {
|
||||
if (error.line <= 0) {
|
||||
if (window.console) {
|
||||
window.console.warn("Cannot display JSHint error (invalid line " + error.line + ")", error);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var start = error.character - 1, end = start + 1;
|
||||
if (error.evidence) {
|
||||
var index = error.evidence.substring(start).search(/.\b/);
|
||||
if (index > -1) {
|
||||
end += index;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to format expected by validation service
|
||||
var hint = {
|
||||
message: error.reason,
|
||||
severity: error.code ? (error.code.startsWith('W') ? "warning" : "error") : "error",
|
||||
from: CodeMirror.Pos(error.line - 1, start),
|
||||
to: CodeMirror.Pos(error.line - 1, end)
|
||||
};
|
||||
|
||||
output.push(hint);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
79
web/assets/codemirror/lint/lint.css
Normal file
@@ -0,0 +1,79 @@
|
||||
/* The lint marker gutter */
|
||||
.CodeMirror-lint-markers {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-tooltip {
|
||||
background-color: #ffd;
|
||||
border: 1px solid black;
|
||||
border-radius: 4px;
|
||||
color: black;
|
||||
font-family: monospace;
|
||||
font-size: 10pt;
|
||||
overflow: hidden;
|
||||
padding: 2px 5px;
|
||||
position: fixed;
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
z-index: 100;
|
||||
max-width: 600px;
|
||||
opacity: 0;
|
||||
transition: opacity .4s;
|
||||
-moz-transition: opacity .4s;
|
||||
-webkit-transition: opacity .4s;
|
||||
-o-transition: opacity .4s;
|
||||
-ms-transition: opacity .4s;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-mark {
|
||||
background-position: left bottom;
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-mark-warning {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-mark-error {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker {
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-message {
|
||||
padding-left: 18px;
|
||||
background-position: top left;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-multiple {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right bottom;
|
||||
width: 100%; height: 100%;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-line-error {
|
||||
background-color: rgba(183, 76, 81, 0.08);
|
||||
}
|
||||
|
||||
.CodeMirror-lint-line-warning {
|
||||
background-color: rgba(255, 211, 0, 0.1);
|
||||
}
|
||||