mirror of
https://github.com/fatedier/frp.git
synced 2026-03-30 05:39:16 +08:00
Compare commits
1368 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d667be7a0a | ||
|
|
31c3deb4f7 | ||
|
|
31e271939b | ||
|
|
061c141756 | ||
|
|
98ee1adb13 | ||
|
|
76abeff881 | ||
|
|
c694b1f6a9 | ||
|
|
5ed02275da | ||
|
|
60c4f5d4bd | ||
|
|
d20e384bf1 | ||
|
|
c95dc9d88a | ||
|
|
38a71a6803 | ||
|
|
6cdef90113 | ||
|
|
85e8e2c830 | ||
|
|
ff4ad2f907 | ||
|
|
94a631fe9c | ||
|
|
6b1be922e1 | ||
|
|
4f584f81d0 | ||
|
|
9669e1ca0c | ||
|
|
48e8901466 | ||
|
|
bcd2424c24 | ||
|
|
c7ac12ea0f | ||
|
|
eeb0dacfc1 | ||
|
|
535eb3db35 | ||
|
|
605f3bdece | ||
|
|
764a626b6e | ||
|
|
c2454e7114 | ||
|
|
017d71717f | ||
|
|
bd200b1a3b | ||
|
|
c70ceff370 | ||
|
|
bb3d0e7140 | ||
|
|
cf396563f8 | ||
|
|
0b4f83cd04 | ||
|
|
e9f7a1a9f2 | ||
|
|
d644593342 | ||
|
|
427c4ca3ae | ||
|
|
f2d1f3739a | ||
|
|
c23894f156 | ||
|
|
cb459b02b6 | ||
|
|
8f633fe363 | ||
|
|
c62a1da161 | ||
|
|
f22f7d539c | ||
|
|
462c987f6d | ||
|
|
541878af4d | ||
|
|
b7435967b0 | ||
|
|
774478d071 | ||
|
|
fbeb6ca43a | ||
|
|
381245a439 | ||
|
|
01997deb98 | ||
|
|
d0347325fc | ||
|
|
519368b1fd | ||
|
|
9634fd99d1 | ||
|
|
7a1c248b67 | ||
|
|
886c9c2fdb | ||
|
|
266c492b5d | ||
|
|
5dd70ace6b | ||
|
|
fb2c98e87b | ||
|
|
ed13141c56 | ||
|
|
3370bd53f5 | ||
|
|
1245f8804e | ||
|
|
479e9f50c2 | ||
|
|
a4175a2595 | ||
|
|
36718d88e4 | ||
|
|
bc378bcbec | ||
|
|
33428ab538 | ||
|
|
ef96481f58 | ||
|
|
7526d7a69a | ||
|
|
2bdf25bae6 | ||
|
|
0fe8f7a0b6 | ||
|
|
2e2802ea13 | ||
|
|
c3821202b1 | ||
|
|
15fd19a16d | ||
|
|
66973a03db | ||
|
|
f736d171ac | ||
|
|
b27b846971 | ||
|
|
e025843d3c | ||
|
|
a75320ef2f | ||
|
|
1cf325bb0c | ||
|
|
469097a549 | ||
|
|
2def23bb0b | ||
|
|
ee3cc4b14e | ||
|
|
e382676659 | ||
|
|
b5e90c03a1 | ||
|
|
b642a6323c | ||
|
|
6561107945 | ||
|
|
abf4942e8a | ||
|
|
7cfa546b55 | ||
|
|
0a798a7a69 | ||
|
|
604700cea5 | ||
|
|
610e5ed479 | ||
|
|
80d3f332e1 | ||
|
|
14253afe2f | ||
|
|
024c334d9d | ||
|
|
f795950742 | ||
|
|
024e4f5f1d | ||
|
|
dc3bc9182c | ||
|
|
e6dacf3a67 | ||
|
|
7fe295f4f4 | ||
|
|
c3bf952d8f | ||
|
|
f9065a6a78 | ||
|
|
61330d4d79 | ||
|
|
c777891f75 | ||
|
|
43cf1688e4 | ||
|
|
720c09c06b | ||
|
|
3fa76b72f3 | ||
|
|
8eb525a648 | ||
|
|
077ba80ba3 | ||
|
|
c99986fa28 | ||
|
|
b41d8f8e40 | ||
|
|
3c8d648ddc | ||
|
|
27f66baf54 | ||
|
|
c5a8f6ef4a | ||
|
|
e208043323 | ||
|
|
a78814a2e9 | ||
|
|
31b44c1feb | ||
|
|
773169e0c4 | ||
|
|
9757a351c6 | ||
|
|
1e8db66743 | ||
|
|
e0dd947e6a | ||
|
|
8b86e1473c | ||
|
|
b8d3ace113 | ||
|
|
450b8393bc | ||
|
|
27db6217ec | ||
|
|
6542dcd4ed | ||
|
|
092e5d3f94 | ||
|
|
01fed8d1a9 | ||
|
|
2a7aa69890 | ||
|
|
f47d8ab97f | ||
|
|
bb912d6c37 | ||
|
|
c73096f2bf | ||
|
|
0358113948 | ||
|
|
8593eff752 | ||
|
|
dff56cb0ca | ||
|
|
4383756fd4 | ||
|
|
6ba849fc75 | ||
|
|
9d5638cae6 | ||
|
|
62352c7ba5 | ||
|
|
4bbec09d57 | ||
|
|
f7a06cbe61 | ||
|
|
3a08c2aeb0 | ||
|
|
b14192a8d3 | ||
|
|
2466e65f43 | ||
|
|
2855ac71e3 | ||
|
|
fe4ca1b54e | ||
|
|
edd7cf8967 | ||
|
|
ccfe8c97f4 | ||
|
|
03c8d7bf96 | ||
|
|
2dcdb24cc4 | ||
|
|
d47e138bc9 | ||
|
|
f1fb2d721a | ||
|
|
ae73ec2fed | ||
|
|
e8045194cd | ||
|
|
69cc422edf | ||
|
|
243ca994e0 | ||
|
|
b4d5d8c756 | ||
|
|
c6f9d8d403 | ||
|
|
939c490768 | ||
|
|
f390e4a401 | ||
|
|
e649692217 | ||
|
|
77990c31ef | ||
|
|
e680acf42d | ||
|
|
522e2c94c1 | ||
|
|
301515d2e8 | ||
|
|
f0442d0cd5 | ||
|
|
9ced717d69 | ||
|
|
4e8e9e1dec | ||
|
|
92cb0b30c2 | ||
|
|
e81b36c5ba | ||
|
|
d0d396becb | ||
|
|
ee3892798d | ||
|
|
405969085f | ||
|
|
c1893ee1b4 | ||
|
|
eaae212d2d | ||
|
|
885278c045 | ||
|
|
2626d6ed92 | ||
|
|
f3a71bc08f | ||
|
|
dd7e2e8473 | ||
|
|
07946e9752 | ||
|
|
e52727e01c | ||
|
|
ba937e9fbf | ||
|
|
8f23733f47 | ||
|
|
d2d03a8fd9 | ||
|
|
590ccda677 | ||
|
|
86f90f4d27 | ||
|
|
5a6d9f60c2 | ||
|
|
f16ef00975 | ||
|
|
b36f3834eb | ||
|
|
c08be0fd92 | ||
|
|
bc5fb91c05 | ||
|
|
002831ea82 | ||
|
|
acf33db4e4 | ||
|
|
3585f5c0c0 | ||
|
|
8383d528d9 | ||
|
|
fa977c839f | ||
|
|
86c2ad78c8 | ||
|
|
a5b7abfc8b | ||
|
|
d5589213c5 | ||
|
|
e0c979e98e | ||
|
|
1e650ea9a7 | ||
|
|
e6ec5a509b | ||
|
|
43ba7bd338 | ||
|
|
49443cb2c6 | ||
|
|
32f09c4b60 | ||
|
|
f63b4d5c29 | ||
|
|
b3946489dd | ||
|
|
7ae3719b82 | ||
|
|
80cfd0938e | ||
|
|
52f66b05e6 | ||
|
|
b2b580be22 | ||
|
|
3e0c78233a | ||
|
|
b6361fb143 | ||
|
|
adb04e81e7 | ||
|
|
518ca2ceb2 | ||
|
|
4957fd23ee | ||
|
|
2f958c2095 | ||
|
|
dc34a68542 | ||
|
|
3529158f31 | ||
|
|
9152c59570 | ||
|
|
2af2cf7dbd | ||
|
|
cf025d6320 | ||
|
|
d689f0fc53 | ||
|
|
e8ace492a5 | ||
|
|
1c8bc0bfa8 | ||
|
|
b31c67d7c0 | ||
|
|
8023d147b0 | ||
|
|
6a488cc081 | ||
|
|
7418ae098d | ||
|
|
7999791708 | ||
|
|
f7efbfeec5 | ||
|
|
1e8806d26b | ||
|
|
d01f4a3ec1 | ||
|
|
596262d5e0 | ||
|
|
cdfa8fa66f | ||
|
|
256b87321d | ||
|
|
5b7b81a117 | ||
|
|
d505ecb473 | ||
|
|
2a9a7a0e4a | ||
|
|
2b83436a97 | ||
|
|
5e77c8e2d3 | ||
|
|
3bf6605e1a | ||
|
|
3540910879 | ||
|
|
2d67e2e0c6 | ||
|
|
051299ec25 | ||
|
|
cc2076970f | ||
|
|
e7652f4ccc | ||
|
|
e66e77cb8f | ||
|
|
1bc9d1a28e | ||
|
|
7ad62818bd | ||
|
|
9ecafeab40 | ||
|
|
95cf418963 | ||
|
|
6d9e0c20f6 | ||
|
|
97d3cf1a3b | ||
|
|
38f297a395 | ||
|
|
7c799ee921 | ||
|
|
69ae2b0b69 | ||
|
|
d5b41f1e14 | ||
|
|
8b432e179d | ||
|
|
f5d5a00eef | ||
|
|
526e809bd5 | ||
|
|
e8deb65c4b | ||
|
|
184223cb2f | ||
|
|
5760c1cf92 | ||
|
|
5c4d820eb4 | ||
|
|
46266e4d30 | ||
|
|
44985f574d | ||
|
|
a6478aeac8 | ||
|
|
806b55c292 | ||
|
|
c9ca9353cf | ||
|
|
496b1f1078 | ||
|
|
9cb0726ebc | ||
|
|
31190c703d | ||
|
|
1452facf77 | ||
|
|
6d4d8e616d | ||
|
|
a7ad967231 | ||
|
|
31fa3f021a | ||
|
|
01a0d557ef | ||
|
|
b9c24e9b69 | ||
|
|
df12cc2b9d | ||
|
|
7cc67e852e | ||
|
|
307d1bfa3f | ||
|
|
5eb8f3db03 | ||
|
|
2d3af8a108 | ||
|
|
3ae1a4f45a | ||
|
|
21d8e674f0 | ||
|
|
5e70d5bee0 | ||
|
|
5c8ea51eb5 | ||
|
|
bae0b4d7c0 | ||
|
|
74255f711e | ||
|
|
7cd02f5bd8 | ||
|
|
c95311d1a0 | ||
|
|
885b029fcf | ||
|
|
466d69eae0 | ||
|
|
f1454e91f5 | ||
|
|
7c8cbeb250 | ||
|
|
e9e12cf888 | ||
|
|
6430afcfa5 | ||
|
|
3235addaaa | ||
|
|
46ff40543a | ||
|
|
4fd6301577 | ||
|
|
efcc028a3d | ||
|
|
90861b6821 | ||
|
|
8f105adbca | ||
|
|
53626b370c | ||
|
|
b1789afbab | ||
|
|
88c7e8bf7c | ||
|
|
fc4e787fe2 | ||
|
|
4c4d5f0d0d | ||
|
|
801e8c6742 | ||
|
|
4fd800bc48 | ||
|
|
b146989703 | ||
|
|
685d7618f3 | ||
|
|
15a245766e | ||
|
|
e1cef053be | ||
|
|
9ba6a06470 | ||
|
|
ea08de668e | ||
|
|
de85c9455a | ||
|
|
cceab7e1b1 | ||
|
|
9aef3b9944 | ||
|
|
341a5e3e3a | ||
|
|
c7a0cfc66d | ||
|
|
555db9d272 | ||
|
|
0d6d968fe8 | ||
|
|
98068402c8 | ||
|
|
4915852b9c | ||
|
|
756dd1ad5e | ||
|
|
c71efde303 | ||
|
|
9f029e3248 | ||
|
|
8095075719 | ||
|
|
2225a1781f | ||
|
|
0214b974dd | ||
|
|
738c53ce47 | ||
|
|
db52f07d34 | ||
|
|
f6b8645f56 | ||
|
|
2c2c4ecdbc | ||
|
|
3faae194d0 | ||
|
|
a22d6c9504 | ||
|
|
9800b4cfcf | ||
|
|
8f394dba27 | ||
|
|
fccd518512 | ||
|
|
8fb99ef7a9 | ||
|
|
968ba4d3a1 | ||
|
|
862b1642ba | ||
|
|
54eb704650 | ||
|
|
8c6303c1e5 | ||
|
|
871511ba52 | ||
|
|
cb6d7ba7f9 | ||
|
|
31f40aa913 | ||
|
|
2f59e967a0 | ||
|
|
fe8374e99b | ||
|
|
24f0b3afa5 | ||
|
|
39941117b6 | ||
|
|
6a1f9ad893 | ||
|
|
88e74ff24d | ||
|
|
18ab58eb25 | ||
|
|
534dc99d55 | ||
|
|
fa0593ae2c | ||
|
|
89fff7d11d | ||
|
|
38d42dbe4b | ||
|
|
aa31d7ad0b | ||
|
|
113e3b0b0d | ||
|
|
100148d925 | ||
|
|
6b3daffaf0 | ||
|
|
5e17bc7bf1 | ||
|
|
b1b8d9a82b | ||
|
|
24c7d1d9e2 | ||
|
|
d205c26480 | ||
|
|
0eecab06c1 | ||
|
|
ad3548d332 | ||
|
|
595aba5a9b | ||
|
|
679992db25 | ||
|
|
5cfbb976f4 | ||
|
|
b03f0ad1e6 | ||
|
|
804f2910fd | ||
|
|
a4189ba474 | ||
|
|
e2d28d9929 | ||
|
|
9ec84f8143 | ||
|
|
7678938c08 | ||
|
|
b2e3946800 | ||
|
|
af0b7939a7 | ||
|
|
2f66dc3e99 | ||
|
|
649df8827c | ||
|
|
da51adc276 | ||
|
|
e5af37bc8c | ||
|
|
8ab474cc97 | ||
|
|
e8c8d5903a | ||
|
|
a301046f3d | ||
|
|
34ab6b0e74 | ||
|
|
cf66ca10b4 | ||
|
|
3fbe6b659e | ||
|
|
6a71d71e58 | ||
|
|
6ecc97c857 | ||
|
|
ba492f07c3 | ||
|
|
9d077b02cf | ||
|
|
f4e4fbea62 | ||
|
|
3e721d122b | ||
|
|
1bc899ec12 | ||
|
|
6f2571980c | ||
|
|
8888610d83 | ||
|
|
fa7c05c617 | ||
|
|
218b354f82 | ||
|
|
c652b8ef07 | ||
|
|
5b8b145577 | ||
|
|
fe5fb0326b | ||
|
|
0711295b0a | ||
|
|
4af85da0c2 | ||
|
|
bd89eaba2f | ||
|
|
a72259c604 | ||
|
|
44eb513f05 | ||
|
|
eb1e19a821 | ||
|
|
6c658586f6 | ||
|
|
888ed25314 | ||
|
|
21240ed962 | ||
|
|
6481870d03 | ||
|
|
a7a4ba270d | ||
|
|
915d9f4c09 | ||
|
|
18a2af4703 | ||
|
|
305e40fa8a | ||
|
|
10f2620131 | ||
|
|
4acae540c8 | ||
|
|
11b13533a0 | ||
|
|
100d556336 | ||
|
|
452fe25cc6 | ||
|
|
63efa6b776 | ||
|
|
37c27169ac | ||
|
|
ce677820c6 | ||
|
|
1f88a7a0b8 | ||
|
|
eeea7602d9 | ||
|
|
bf635c0e90 | ||
|
|
cd31359a27 | ||
|
|
19739ed31a | ||
|
|
10100c28d9 | ||
|
|
88fcc079e8 | ||
|
|
ddc1e163c4 | ||
|
|
d20a6d3d75 | ||
|
|
6194273615 | ||
|
|
b2311e55e7 | ||
|
|
07873d471f | ||
|
|
2dab5d0bca | ||
|
|
9ca2b586f8 | ||
|
|
e59eacb8a2 | ||
|
|
0db4fc07fb | ||
|
|
70f4caac23 | ||
|
|
293003fcdb | ||
|
|
4bfc89d988 | ||
|
|
22412851b4 | ||
|
|
e9775bd70f | ||
|
|
ff7b8b0b62 | ||
|
|
491c1d7dc4 | ||
|
|
ea568e8a4f | ||
|
|
0fb6aeef58 | ||
|
|
032f33fe5a | ||
|
|
bbc8b438d5 | ||
|
|
05b1ace21f | ||
|
|
cbdd73b94f | ||
|
|
bf06e3b107 | ||
|
|
143750901e | ||
|
|
71489d194c | ||
|
|
85aa3df256 | ||
|
|
f1a51eba18 | ||
|
|
1d26ea440b | ||
|
|
998e678a7f | ||
|
|
0cee1877e3 | ||
|
|
72a7fd948e | ||
|
|
357c9b0dcb | ||
|
|
14bd0716d0 | ||
|
|
2f74f54f18 | ||
|
|
a62a9431b1 | ||
|
|
42745a3da2 | ||
|
|
82f80a22be | ||
|
|
f570dcb307 | ||
|
|
997d406ec2 | ||
|
|
87e60683ed | ||
|
|
86b2e686a5 | ||
|
|
09f39de74e | ||
|
|
2a68c1152f | ||
|
|
df5859b5f7 | ||
|
|
3dd888a9ea | ||
|
|
a51e221db3 | ||
|
|
fe4e9b55f3 | ||
|
|
3f11b6a082 | ||
|
|
8a333c2ae0 | ||
|
|
1fd6ba2738 | ||
|
|
a98a9616f6 | ||
|
|
95cd9ab900 | ||
|
|
900454e58b | ||
|
|
c7d4637382 | ||
|
|
56925961df | ||
|
|
cfd1a3128a | ||
|
|
2393923870 | ||
|
|
5f594e9a71 | ||
|
|
57577ea044 | ||
|
|
8637077d90 | ||
|
|
ccb85a9926 | ||
|
|
02b12df887 | ||
|
|
c32a2ed140 | ||
|
|
9ae322cccf | ||
|
|
9cebfccb39 | ||
|
|
630dad50ed | ||
|
|
0d84da91d4 | ||
|
|
2408f1df04 | ||
|
|
fbaa5f866e | ||
|
|
c5c79e4148 | ||
|
|
9a849a29e9 | ||
|
|
6b80861bd6 | ||
|
|
fa0e84382e | ||
|
|
1a11b28f8d | ||
|
|
bed13d7ef1 | ||
|
|
e7d76b180d | ||
|
|
dba8925eaa | ||
|
|
55da58eca4 | ||
|
|
fdef7448a7 | ||
|
|
0ff27fc9ac | ||
|
|
76a1efccd9 | ||
|
|
9f8db314d6 | ||
|
|
980f084ad1 | ||
|
|
0c35863d97 | ||
|
|
184a0ff9ab | ||
|
|
8e25f13201 | ||
|
|
b5aee82ca9 | ||
|
|
0a2384a283 | ||
|
|
78b8bb7bc6 | ||
|
|
8fcd4f4a95 | ||
|
|
976fd81d4d | ||
|
|
52d5c9e25b | ||
|
|
fa89671452 | ||
|
|
3621aad1c1 | ||
|
|
3bf1eb8565 | ||
|
|
a821db3f45 | ||
|
|
ecb6ed9258 | ||
|
|
b2ae433e18 | ||
|
|
b26080589b | ||
|
|
aff979c2b6 | ||
|
|
46f809d711 | ||
|
|
72595b2da8 | ||
|
|
c842558ace | ||
|
|
ed61049041 | ||
|
|
abe6f580c0 | ||
|
|
e940066012 | ||
|
|
1e846df870 | ||
|
|
0ab055e946 | ||
|
|
fca59c71e2 | ||
|
|
fae2f8768d | ||
|
|
3d9499f554 | ||
|
|
7adeeedd55 | ||
|
|
127a31ea6a | ||
|
|
a85bd9a4d9 | ||
|
|
01d551ec8d | ||
|
|
16cabf4127 | ||
|
|
968be4a2c2 | ||
|
|
aa0a41ee4e | ||
|
|
8a779eb88c | ||
|
|
0138dbd352 | ||
|
|
9b45c93c14 | ||
|
|
191da54980 | ||
|
|
c3b7575453 | ||
|
|
1ea1530b36 | ||
|
|
7f7305fa03 | ||
|
|
644a0cfdb6 | ||
|
|
3c2e2bcea5 | ||
|
|
e0c45a1aca | ||
|
|
e52dfc4a5c | ||
|
|
cc003a2570 | ||
|
|
0f8040b875 | ||
|
|
ef5ae3e598 | ||
|
|
3acf1bb6e9 | ||
|
|
1089eb9d22 | ||
|
|
edf9596ca8 | ||
|
|
26e54b901f | ||
|
|
008933f304 | ||
|
|
317f901c1c | ||
|
|
cd5314466c | ||
|
|
3fbdea0f6b | ||
|
|
710ecf44f5 | ||
|
|
04dafd7ff0 | ||
|
|
c6aa74a2bb | ||
|
|
813c45f5c2 | ||
|
|
c0e05bb41e | ||
|
|
aa74dc4646 | ||
|
|
1e420cc766 | ||
|
|
4fff3c7472 | ||
|
|
48fa618c34 | ||
|
|
c9fe23eb10 | ||
|
|
268afb3438 | ||
|
|
b1181fd17a | ||
|
|
b23548eeff | ||
|
|
262317192c | ||
|
|
8b75b8b837 | ||
|
|
2170c481ce | ||
|
|
dfbf9c4542 | ||
|
|
964a1bbf39 | ||
|
|
228e225f84 | ||
|
|
bd6435c982 | ||
|
|
591023a1f0 | ||
|
|
1ab23b5e0e | ||
|
|
d193519329 | ||
|
|
2406ecdfea | ||
|
|
7266154d54 | ||
|
|
4797136965 | ||
|
|
6d78af6144 | ||
|
|
7728e35c52 | ||
|
|
5a61fd84ad | ||
|
|
ad0c449a75 | ||
|
|
1c330185c4 | ||
|
|
8668fef136 | ||
|
|
7491b327f8 | ||
|
|
abb5b05d49 | ||
|
|
b6ec9dad28 | ||
|
|
caa6e8cf01 | ||
|
|
ffb932390f | ||
|
|
a8efaee1f3 | ||
|
|
4c2afb5c28 | ||
|
|
809f517db8 | ||
|
|
a4b105dedb | ||
|
|
10acf638f8 | ||
|
|
ea62bc5a34 | ||
|
|
f65ffe2812 | ||
|
|
23bb76397a | ||
|
|
859a330e6c | ||
|
|
86ac511763 | ||
|
|
f2e98ef8a4 | ||
|
|
495d999b6c | ||
|
|
6d1af85e80 | ||
|
|
1db091b381 | ||
|
|
0b9124d4fd | ||
|
|
6c6607ae68 | ||
|
|
83d80857fd | ||
|
|
98fa3855bd | ||
|
|
9440bc5d72 | ||
|
|
95753ebf1c | ||
|
|
f8c6795119 | ||
|
|
7033f3e72b | ||
|
|
e3101b7aa8 | ||
|
|
c747f160aa | ||
|
|
c8748a2948 | ||
|
|
487c8d7c29 | ||
|
|
69fa7ed16e | ||
|
|
5336155365 | ||
|
|
4feb74cb89 | ||
|
|
4a4cf552af | ||
|
|
0f59b8f329 | ||
|
|
f480160e2d | ||
|
|
4832a2a1e9 | ||
|
|
52ecd84d8a | ||
|
|
30c246c488 | ||
|
|
42014eea23 | ||
|
|
c2da396230 | ||
|
|
e91c9473be | ||
|
|
13e48c6ca0 | ||
|
|
31e2cb76bb | ||
|
|
91e46a2c53 | ||
|
|
a57679f837 | ||
|
|
75f3bce04d | ||
|
|
df18375308 | ||
|
|
c63737ab3e | ||
|
|
1cdceee347 | ||
|
|
694c434b9e | ||
|
|
62af5c8844 | ||
|
|
56c53909aa | ||
|
|
21a126e4e4 | ||
|
|
8affab1a2b | ||
|
|
12cc53d699 | ||
|
|
2ab832bb89 | ||
|
|
42425d8218 | ||
|
|
6da093a402 | ||
|
|
adc3adc13b | ||
|
|
22a79710d8 | ||
|
|
0927553fe4 | ||
|
|
858d8f0ba7 | ||
|
|
8eb945ee9b | ||
|
|
dc0fd60d30 | ||
|
|
cd44c9f55c | ||
|
|
649f47c345 | ||
|
|
6ca3160b33 | ||
|
|
5f8ed4fc60 | ||
|
|
bf0993d2a6 | ||
|
|
5dc8175fc8 | ||
|
|
dc6a5a29c1 | ||
|
|
e62d9a5242 | ||
|
|
94212ac8b8 | ||
|
|
e9e86fccf0 | ||
|
|
58745992ef | ||
|
|
234d634bfe | ||
|
|
fdc6902a90 | ||
|
|
d8d587fd93 | ||
|
|
92791260a7 | ||
|
|
4dfd851c46 | ||
|
|
bc4df74b5e | ||
|
|
666f122a72 | ||
|
|
f999c8a87e | ||
|
|
90a32ab75d | ||
|
|
0713fd28da | ||
|
|
f5b33e6de8 | ||
|
|
fc6043bb4d | ||
|
|
bc46e3330a | ||
|
|
5fc7b3ceb5 | ||
|
|
6277af4790 | ||
|
|
00bd0a8af4 | ||
|
|
a415573e45 | ||
|
|
e68012858e | ||
|
|
ca8a5b753c | ||
|
|
d1f4ac0f2d | ||
|
|
ff357882ac | ||
|
|
934ac2b836 | ||
|
|
1ad50d5982 | ||
|
|
388b016842 | ||
|
|
134a46c00b | ||
|
|
50796643fb | ||
|
|
b1838b1d5e | ||
|
|
757b3613fe | ||
|
|
ae08811636 | ||
|
|
b657c0fe09 | ||
|
|
84df71047c | ||
|
|
abc6d720d0 | ||
|
|
80154639e3 | ||
|
|
f2117d8331 | ||
|
|
261be6a7b7 | ||
|
|
b53a2c1ed9 | ||
|
|
ee0df07a3c | ||
|
|
4e363eca2b | ||
|
|
4277405c0e | ||
|
|
6a99f0caf7 | ||
|
|
394af08561 | ||
|
|
6451583e60 | ||
|
|
30cb0a3ab0 | ||
|
|
5680a88267 | ||
|
|
6b089858db | ||
|
|
b3ed863021 | ||
|
|
5796c27ed5 | ||
|
|
310e8dd768 | ||
|
|
0b40ac2dbc | ||
|
|
f22c8e0882 | ||
|
|
a388bb2c95 | ||
|
|
e611c44dea | ||
|
|
8e36e2bb67 | ||
|
|
541ad8d899 | ||
|
|
17cc0735d1 | ||
|
|
fd336a5503 | ||
|
|
802d1c1861 | ||
|
|
65fe0a1179 | ||
|
|
2d24879fa3 | ||
|
|
75383a95b3 | ||
|
|
95444ea46b | ||
|
|
9f9c01b520 | ||
|
|
285d1eba0d | ||
|
|
0dfd3a421c | ||
|
|
6a1f15b25e | ||
|
|
9f47c324b7 | ||
|
|
f0df6084af | ||
|
|
879ca47590 | ||
|
|
6a7efc81c9 | ||
|
|
12c5c553c3 | ||
|
|
988e9b1de3 | ||
|
|
db6bbc5187 | ||
|
|
c67b4e7b94 | ||
|
|
b7a73d3469 | ||
|
|
7f9d88c10a | ||
|
|
79237d2b94 | ||
|
|
9c4ec56491 | ||
|
|
74a8752570 | ||
|
|
a8ab4c5003 | ||
|
|
9cee263c91 | ||
|
|
c6bf6f59e6 | ||
|
|
4b7aef2196 | ||
|
|
f6d0046b5a | ||
|
|
84363266d2 | ||
|
|
9ac8f2a047 | ||
|
|
b2b55533b8 | ||
|
|
a4cfab689a | ||
|
|
c7df39074c | ||
|
|
fdcdccb0c2 | ||
|
|
e945c1667a | ||
|
|
87a4de4370 | ||
|
|
e1e2913b77 | ||
|
|
9be24db410 | ||
|
|
6b61cb3742 | ||
|
|
90b7f2080f | ||
|
|
d1f1c72a55 | ||
|
|
1925847ef8 | ||
|
|
8b216b0ca9 | ||
|
|
dbfeea99f3 | ||
|
|
5e64bbfa7c | ||
|
|
e691a40260 | ||
|
|
d812488767 | ||
|
|
3c03690ab7 | ||
|
|
3df27b9c04 | ||
|
|
ba45d29b7c | ||
|
|
3cf83f57a8 | ||
|
|
03e4318d79 | ||
|
|
178d134f46 | ||
|
|
cbf9c731a0 | ||
|
|
de4bfcc43c | ||
|
|
9737978f28 | ||
|
|
5bc7fe2cea | ||
|
|
65d8fe37c5 | ||
|
|
1723d7b651 | ||
|
|
2481dfab64 | ||
|
|
95a881a7d3 | ||
|
|
fe403ab328 | ||
|
|
66555dbb00 | ||
|
|
7f9ea48405 | ||
|
|
96d7e2da6f | ||
|
|
d879b8208b | ||
|
|
3585e456d4 | ||
|
|
1de8c3fc87 | ||
|
|
bbab3fe9ca | ||
|
|
48990da22e | ||
|
|
5543fc2a9a | ||
|
|
c41de6fd28 | ||
|
|
8c8fd9790e | ||
|
|
5a7ef3be74 | ||
|
|
d9b5e0bde0 | ||
|
|
05ca72dbf0 | ||
|
|
ef6f8bbf6c | ||
|
|
70ac7d3d11 | ||
|
|
385c4d3dd5 | ||
|
|
5e1983f7ed | ||
|
|
516cdbddb0 | ||
|
|
3954ceb93b | ||
|
|
2061ef11c8 | ||
|
|
71cbe5decc | ||
|
|
a2ccb6c190 | ||
|
|
5bdf530b7e | ||
|
|
5177570da4 | ||
|
|
0bd8f9cd9b | ||
|
|
649a2f2457 | ||
|
|
54916793f9 | ||
|
|
0b06c1c821 | ||
|
|
bbc6f1687d | ||
|
|
b250342e27 | ||
|
|
f76deb8898 | ||
|
|
611d063e1f | ||
|
|
0c7d778896 | ||
|
|
7c21906884 | ||
|
|
a4106ec4b7 | ||
|
|
655c52f9ce | ||
|
|
25cfda5768 | ||
|
|
b61cb14c8f | ||
|
|
d5ce4d4916 | ||
|
|
4f0ee5980d | ||
|
|
146956ac6e | ||
|
|
35278ad17f | ||
|
|
aea9f9fbcc | ||
|
|
08c17c3247 | ||
|
|
6934a18f95 | ||
|
|
5165b0821f | ||
|
|
0aec869513 | ||
|
|
826b9db5f2 | ||
|
|
89d1a1fb2b | ||
|
|
450e0b7148 | ||
|
|
a1ac002694 | ||
|
|
951d33d47c | ||
|
|
b33ea9274c | ||
|
|
3b3f3dc2b5 | ||
|
|
ec0b59732c | ||
|
|
bae1ecdc69 | ||
|
|
5c2ab5a749 | ||
|
|
1a8ac148ca | ||
|
|
698219b621 | ||
|
|
229740524e | ||
|
|
cbeeac06a5 | ||
|
|
66a69f873f | ||
|
|
fb13774457 | ||
|
|
f14ed87b29 | ||
|
|
07623027bc | ||
|
|
941ac25648 | ||
|
|
f645082d72 | ||
|
|
7793f55545 | ||
|
|
ca88b07ecf | ||
|
|
6e305db4be | ||
|
|
9bb08396c7 | ||
|
|
64136a3b3e | ||
|
|
b8037475ed | ||
|
|
082447f517 | ||
|
|
cc6486addb | ||
|
|
57417c83ae | ||
|
|
d74b45be5d | ||
|
|
0d02f291e3 | ||
|
|
42ee536dae | ||
|
|
c33b5152e7 | ||
|
|
b6c219aa97 | ||
|
|
bbc36be052 | ||
|
|
f5778349d5 | ||
|
|
71603c6d0b | ||
|
|
e64fcce417 | ||
|
|
629f2856b1 | ||
|
|
aeb9f2b64d | ||
|
|
85dd41c17b | ||
|
|
102408d37f | ||
|
|
495b577819 | ||
|
|
f56b49ad3b | ||
|
|
cb1bf71bef | ||
|
|
b9f062bef2 | ||
|
|
490019fb51 | ||
|
|
2e497274ba | ||
|
|
cf4136fe99 | ||
|
|
b1e9cff622 | ||
|
|
db2d1fce76 | ||
|
|
8579de9d3f | ||
|
|
0c35273759 | ||
|
|
6eb8146334 | ||
|
|
da78e3f52e | ||
|
|
e1918f6396 | ||
|
|
ad1e32fd2d | ||
|
|
3726f99b04 | ||
|
|
c8a7405992 | ||
|
|
040d198e36 | ||
|
|
1a6cbbb2d2 | ||
|
|
ea79e03bd0 | ||
|
|
3e349455a0 | ||
|
|
c7a457a045 | ||
|
|
0b0d5c982e | ||
|
|
c4f873c07a | ||
|
|
01b1df2b91 | ||
|
|
f1bea49314 | ||
|
|
2ffae3489b | ||
|
|
96b94d9164 | ||
|
|
76b04f52d1 | ||
|
|
97db0d187a | ||
|
|
d9aadab4cb | ||
|
|
a0fe2fc2c2 | ||
|
|
1464836f05 | ||
|
|
b2a2037032 | ||
|
|
071cbf4b15 | ||
|
|
20fcb58437 | ||
|
|
a27e3dda88 | ||
|
|
1dd7317c06 | ||
|
|
58a54bd0fb | ||
|
|
caec4982cc | ||
|
|
dd8f788ca4 | ||
|
|
8a6d6c534a | ||
|
|
39089cf262 | ||
|
|
55800dc29f | ||
|
|
04560c1896 | ||
|
|
178efd67f1 | ||
|
|
6ef5fb6391 | ||
|
|
1ae43e4d41 | ||
|
|
5db605ca02 | ||
|
|
e087301425 | ||
|
|
f45283dbdb | ||
|
|
b0959b3caa | ||
|
|
c5c89a2519 | ||
|
|
bebd1db22a | ||
|
|
cd37d22f3b | ||
|
|
30af32728a | ||
|
|
60ecd1d58c | ||
|
|
a60be8f562 | ||
|
|
c008b14d0f | ||
|
|
853892f3cd | ||
|
|
e43f9f5850 | ||
|
|
d5f30ccd6b | ||
|
|
b87df569e7 | ||
|
|
976cf3e9f8 | ||
|
|
371c401f5b | ||
|
|
69919e8ef9 | ||
|
|
9abbe33790 | ||
|
|
4a5c00286e | ||
|
|
dfb892c8f6 | ||
|
|
461c4c18fd | ||
|
|
00b9ba95ae | ||
|
|
c47aad348d | ||
|
|
4cb4da3afc | ||
|
|
c1f57da00d | ||
|
|
fe187eb8ec | ||
|
|
0f6f674a64 | ||
|
|
814afbe1f6 | ||
|
|
3fde9176c9 | ||
|
|
af7cca1a93 | ||
|
|
7dd28a14aa | ||
|
|
1325c59a4c | ||
|
|
82dc1e924f | ||
|
|
3166bdf3f0 | ||
|
|
8af70c8822 | ||
|
|
87763e8251 | ||
|
|
e9241aeb94 | ||
|
|
2eaf134042 | ||
|
|
1739e012d6 | ||
|
|
9e8980429f | ||
|
|
1d0865ca49 | ||
|
|
5c9909aeef | ||
|
|
456ce09061 | ||
|
|
ffc13b704a | ||
|
|
5d239127bb | ||
|
|
9b990adf96 | ||
|
|
44e8108910 | ||
|
|
1c35e9a0c6 | ||
|
|
8e719ff0ff | ||
|
|
637ddbce1f | ||
|
|
ce8fde793c | ||
|
|
eede31c064 | ||
|
|
41c41789b6 | ||
|
|
68dfc89bce | ||
|
|
8690075c0c | ||
|
|
33d8816ced | ||
|
|
90cd25ac21 | ||
|
|
ff28668cf2 | ||
|
|
a6f2736b80 | ||
|
|
902f6f84a5 | ||
|
|
cf9193a429 | ||
|
|
3f64d73ea9 | ||
|
|
a77c7e8625 | ||
|
|
14733dd109 | ||
|
|
74b75e8c57 | ||
|
|
63e6e0dc92 | ||
|
|
4d4a738aa9 | ||
|
|
1ed130e704 | ||
|
|
2e773d550b | ||
|
|
e155ff056e | ||
|
|
37210d9983 | ||
|
|
338d5bae37 | ||
|
|
3e62198612 | ||
|
|
4f7dfcdb31 | ||
|
|
5b08201e5d | ||
|
|
b2c846664d | ||
|
|
3f6799c06a | ||
|
|
9a5f0c23c4 | ||
|
|
afde0c515c | ||
|
|
584e098e8e | ||
|
|
37395b3ef5 | ||
|
|
43fb3f3ff7 | ||
|
|
82b127494c | ||
|
|
4d79648657 | ||
|
|
3bb404dfb5 | ||
|
|
ff4bdec3f7 | ||
|
|
69f8b08ac0 | ||
|
|
d873df5ca8 | ||
|
|
a384bf5580 | ||
|
|
92046a7ca2 | ||
|
|
4cc5ddc012 | ||
|
|
46358d466d | ||
|
|
7da61f004b | ||
|
|
63037f1c65 | ||
|
|
cc160995da | ||
|
|
de48d97cb2 | ||
|
|
1a6a179b68 | ||
|
|
3a2946a2ff | ||
|
|
ae9a4623d9 | ||
|
|
bd1e9a3010 | ||
|
|
92fff5c191 | ||
|
|
8c65b337ca | ||
|
|
0f1005ff61 | ||
|
|
ad858a0d32 | ||
|
|
1e905839f0 | ||
|
|
bf50f932d9 | ||
|
|
673047be2c | ||
|
|
fa2b9a836c | ||
|
|
9e0fd0c4ef | ||
|
|
0559865fe5 | ||
|
|
4fc85a36c2 | ||
|
|
3f1174a519 | ||
|
|
bcbdfcb99b | ||
|
|
df046bdeeb | ||
|
|
f83447c652 | ||
|
|
9ae69b4aac | ||
|
|
c48a89731a | ||
|
|
36b58ab60c | ||
|
|
6320f15a7c | ||
|
|
066172e9c1 | ||
|
|
d5931758b6 | ||
|
|
c75c3acd21 | ||
|
|
0208ecd1d9 | ||
|
|
23e9845e65 | ||
|
|
2b1ba3a946 | ||
|
|
ee9ddf52cd | ||
|
|
d246400a71 | ||
|
|
f63a4f0cdd | ||
|
|
b743b5aaed | ||
|
|
9d9416ab94 | ||
|
|
c081df40e1 | ||
|
|
fe32a7c4bb | ||
|
|
7bb8c10647 | ||
|
|
0752508469 | ||
|
|
4cc1663a5f | ||
|
|
b55a24a27e | ||
|
|
aede4e54f8 | ||
|
|
b811a620c3 | ||
|
|
07fe05a9d5 | ||
|
|
171bc8dd22 | ||
|
|
9c175d4eb5 | ||
|
|
9f736558e2 | ||
|
|
8f071dd2c2 | ||
|
|
bcaf51a6ad | ||
|
|
ad3cf9a64a | ||
|
|
e3fc73dbc5 | ||
|
|
f884e894f2 | ||
|
|
d57ed7d3d8 | ||
|
|
a2c318d24c | ||
|
|
32f8745d61 | ||
|
|
66120fe49d | ||
|
|
fca7f42b37 | ||
|
|
5b303f5148 | ||
|
|
2a044c9d6d | ||
|
|
70e2aee46d | ||
|
|
6742fa2ea8 | ||
|
|
511503d34c | ||
|
|
1eaf17fd05 | ||
|
|
04f4fd0a81 | ||
|
|
3a4d769bb3 | ||
|
|
84341b7fcc | ||
|
|
80ba931326 | ||
|
|
7ebcc7503a | ||
|
|
74cf57feb3 | ||
|
|
712afed0ab | ||
|
|
e29a1330ed | ||
|
|
44971c7918 | ||
|
|
7bc6c72844 | ||
|
|
93461e0094 | ||
|
|
03d55201b2 | ||
|
|
e6d82f3162 | ||
|
|
1af6276be9 | ||
|
|
d1f5ec083a | ||
|
|
716ec281f6 | ||
|
|
67bfae5d23 | ||
|
|
f0dc3ed47b | ||
|
|
08b0885564 | ||
|
|
49b503c17b | ||
|
|
150682ec63 | ||
|
|
4dc96f41c9 | ||
|
|
6c13b6d37a | ||
|
|
1c04de380d | ||
|
|
738e5dad22 | ||
|
|
6d81e4c8c6 | ||
|
|
faf584e1dd | ||
|
|
ba6afd5789 | ||
|
|
11260389a1 | ||
|
|
b8082e6e08 | ||
|
|
7957572ced | ||
|
|
c2ff37d0d8 | ||
|
|
c67f9d5e76 | ||
|
|
1cc61b60f9 | ||
|
|
9c38baeb9e | ||
|
|
84465a7463 | ||
|
|
3fe50df200 | ||
|
|
93d86ca635 | ||
|
|
b600a07ec0 | ||
|
|
a5f06489cb | ||
|
|
2883d70ea9 | ||
|
|
3f17837a2c | ||
|
|
fd268b5082 | ||
|
|
69b09eb8a2 | ||
|
|
a84dd05351 | ||
|
|
71f7caa1ee | ||
|
|
5360febd72 | ||
|
|
1b70f0c4fd | ||
|
|
5c75efa222 | ||
|
|
ab4a53965b | ||
|
|
a0c83bdb78 | ||
|
|
30aeaf968e | ||
|
|
58d0d41501 | ||
|
|
6a95a63fd4 | ||
|
|
aa185eb9f3 | ||
|
|
d8683a0079 | ||
|
|
8b2cde3a30 | ||
|
|
634e048d0c | ||
|
|
a4fece3f51 | ||
|
|
9e683fe446 | ||
|
|
54bbfe26b0 | ||
|
|
a1023fdfc2 | ||
|
|
b02e1007fb | ||
|
|
f90028cf96 | ||
|
|
f83a2a73ab | ||
|
|
307b74cc13 | ||
|
|
f00a28598f | ||
|
|
6ee0b25782 | ||
|
|
88083d21e8 | ||
|
|
a22440aade | ||
|
|
b006540141 | ||
|
|
e655f07674 | ||
|
|
aafa96db58 | ||
|
|
1325148cd3 | ||
|
|
3f9749488a | ||
|
|
f9a0d891a1 | ||
|
|
92daa45b68 | ||
|
|
5f20a22b0d | ||
|
|
63be94c611 | ||
|
|
694ee44af6 | ||
|
|
edb97abf50 | ||
|
|
0c10279deb | ||
|
|
1f49510e3e | ||
|
|
1868b3bafb | ||
|
|
a23521885c | ||
|
|
c80dcd050d | ||
|
|
043ab62587 | ||
|
|
a8969b1901 | ||
|
|
e26285eefc | ||
|
|
299bd7b5cb | ||
|
|
90d1384bf7 | ||
|
|
a5434e31b7 | ||
|
|
044bb692dc | ||
|
|
34b98dde52 | ||
|
|
020f786bf5 | ||
|
|
cdcc1240ec | ||
|
|
c2c9f68a00 | ||
|
|
37470c26f0 | ||
|
|
04a4591caa | ||
|
|
8bf61d5e39 | ||
|
|
659f84bab2 | ||
|
|
9faf4acd62 | ||
|
|
4c3fb22295 | ||
|
|
d243f70125 | ||
|
|
a56f068f8c | ||
|
|
6a6ccc5302 | ||
|
|
6f90c3400c | ||
|
|
eb4f779384 | ||
|
|
59a34b81e0 | ||
|
|
b1d1a7a20a | ||
|
|
6b34ed4644 | ||
|
|
dde734c953 | ||
|
|
5532881b09 | ||
|
|
94ddeebc21 | ||
|
|
ddbb56ee8f | ||
|
|
10fc6c67e0 | ||
|
|
0573ddcd84 | ||
|
|
5eb5fec761 | ||
|
|
52fe721202 | ||
|
|
d7d2b72431 | ||
|
|
d04d31b39a | ||
|
|
d9304d8166 | ||
|
|
a44be1e2ed | ||
|
|
2bf1d3e922 | ||
|
|
19f349a65e | ||
|
|
b0e56945cd | ||
|
|
f2999e3317 | ||
|
|
a4c05e6ff9 | ||
|
|
d93dd82ed9 | ||
|
|
edf4bc431d | ||
|
|
47db75e921 | ||
|
|
c702355669 | ||
|
|
7cc5d03f35 | ||
|
|
54beb19435 | ||
|
|
396e148f80 | ||
|
|
4c69a4810e | ||
|
|
40e023f5f4 | ||
|
|
adcb2c1ea5 | ||
|
|
8c497793c5 | ||
|
|
78c6845781 | ||
|
|
dc5e130d33 | ||
|
|
fbc504dfa3 | ||
|
|
77f207d69a | ||
|
|
b65e037b5e | ||
|
|
b8a28e945c | ||
|
|
0476a85a7d | ||
|
|
5661537f7c | ||
|
|
19f7950485 | ||
|
|
c21f8ad291 | ||
|
|
3d6578b15f | ||
|
|
899d6837df | ||
|
|
0e1752b5ce | ||
|
|
da182ecd81 | ||
|
|
94c7f57949 | ||
|
|
c8e5096f48 | ||
|
|
5079bf01fd | ||
|
|
603d7df49a | ||
|
|
2b1c39e03d | ||
|
|
46ee2f2bc8 | ||
|
|
3d5c3acee0 | ||
|
|
41fd4bb673 | ||
|
|
e1e18ba9d6 | ||
|
|
ab9eff97a8 | ||
|
|
6f40b1a70a | ||
|
|
87c9b8f548 | ||
|
|
3fcf7efc5a | ||
|
|
a655f5699b | ||
|
|
09624b56ca | ||
|
|
e262ac6abd | ||
|
|
47c1a3e52c | ||
|
|
4dadaac905 | ||
|
|
e1ed6660b0 | ||
|
|
b71b2cf46d | ||
|
|
a0903d4121 | ||
|
|
b403e4142b | ||
|
|
46716acd8e | ||
|
|
c7f85bcdd3 | ||
|
|
ddd2acfe9f | ||
|
|
e3bf7e2b2b | ||
|
|
4914472215 | ||
|
|
5d9300c1e9 | ||
|
|
b4a577b0d7 | ||
|
|
32d0ce9ea0 | ||
|
|
2d30a6e8a7 | ||
|
|
740691b080 | ||
|
|
11fe4b1d8b | ||
|
|
c64931fce9 | ||
|
|
d4ecc2218d | ||
|
|
9c0ca8675d | ||
|
|
5cdb84c666 | ||
|
|
060277308b | ||
|
|
31dfd5101f | ||
|
|
4300169041 | ||
|
|
3ab9850871 | ||
|
|
d813b953dd | ||
|
|
1da81ad7d3 | ||
|
|
3b06d771ac | ||
|
|
7f386fc042 | ||
|
|
df8edefa56 | ||
|
|
ecb6ad4885 | ||
|
|
785dcaad44 | ||
|
|
fd3c97a0e9 | ||
|
|
8f5f0b0a9a | ||
|
|
452e02adab | ||
|
|
d2e1cfa5bc | ||
|
|
6dd51e0951 | ||
|
|
e0f2993b70 | ||
|
|
4067591a4d | ||
|
|
926d0b74a9 | ||
|
|
4f49458af0 | ||
|
|
fd6b94908b | ||
|
|
dee4cbd48c | ||
|
|
9a3564f29c | ||
|
|
ac09ba3982 | ||
|
|
a9bf25f255 | ||
|
|
6bc05de58e | ||
|
|
5265b79957 | ||
|
|
fefc0a38c3 | ||
|
|
c387138006 | ||
|
|
36f8beee3d | ||
|
|
366a0c898d | ||
|
|
d747f9207e | ||
|
|
5400366036 | ||
|
|
9dae7ad6fe | ||
|
|
a4e051d494 | ||
|
|
28251a8104 | ||
|
|
e99357da4e | ||
|
|
e580c7b6e6 | ||
|
|
ba74934a1f | ||
|
|
1bad5c6561 | ||
|
|
f968f3eace | ||
|
|
b14441d5cd | ||
|
|
0a50c3bd82 | ||
|
|
ef5702213f | ||
|
|
c5e4b24f8f | ||
|
|
1987a399c1 | ||
|
|
ab6c5c813e | ||
|
|
51eaec14ab | ||
|
|
f3876d69bb | ||
|
|
817f4463f4 | ||
|
|
654981019d | ||
|
|
740fb05b21 | ||
|
|
e8c830e5c8 | ||
|
|
2640c0b570 | ||
|
|
04014bb78f | ||
|
|
150c4beef8 | ||
|
|
5febee6201 | ||
|
|
ee8786a6b3 | ||
|
|
d569a60eff | ||
|
|
14607b352d | ||
|
|
bc7ad2bb20 | ||
|
|
cd59bbdad6 | ||
|
|
f404a0a5ee | ||
|
|
da7c473288 | ||
|
|
ea323084ad | ||
|
|
c680d87edc | ||
|
|
d3c4401473 | ||
|
|
7a9a675d58 | ||
|
|
040841db48 | ||
|
|
f804330dbf | ||
|
|
d39d745e43 | ||
|
|
c10321ead6 | ||
|
|
d7797cbd18 | ||
|
|
0b9d823168 | ||
|
|
deb750652f | ||
|
|
14ba38a1b4 | ||
|
|
f650d3f330 | ||
|
|
2c39719cc0 | ||
|
|
6874688e07 | ||
|
|
fdd7436736 | ||
|
|
0f326449e8 | ||
|
|
7c3e00ed28 | ||
|
|
d5913fc77b |
24
.circleci/config.yml
Normal file
24
.circleci/config.yml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
version: 2
|
||||||
|
jobs:
|
||||||
|
go-version-latest:
|
||||||
|
docker:
|
||||||
|
- image: cimg/go:1.25-node
|
||||||
|
resource_class: large
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run:
|
||||||
|
name: Build web assets (frps)
|
||||||
|
command: make install build
|
||||||
|
working_directory: web/frps
|
||||||
|
- run:
|
||||||
|
name: Build web assets (frpc)
|
||||||
|
command: make install build
|
||||||
|
working_directory: web/frpc
|
||||||
|
- run: make
|
||||||
|
- run: make alltest
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
build_and_test:
|
||||||
|
jobs:
|
||||||
|
- go-version-latest
|
||||||
4
.github/FUNDING.yml
vendored
Normal file
4
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [fatedier]
|
||||||
|
custom: ["https://afdian.com/a/fatedier"]
|
||||||
77
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
77
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
name: Bug report
|
||||||
|
description: Report a bug to help us improve frp
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this bug report!
|
||||||
|
- type: textarea
|
||||||
|
id: bug-description
|
||||||
|
attributes:
|
||||||
|
label: Bug Description
|
||||||
|
description: Tell us what issues you ran into
|
||||||
|
placeholder: Include information about what you tried, what you expected to happen, and what actually happened. The more details, the better!
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: frpc-version
|
||||||
|
attributes:
|
||||||
|
label: frpc Version
|
||||||
|
description: Include the output of `frpc -v`
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: frps-version
|
||||||
|
attributes:
|
||||||
|
label: frps Version
|
||||||
|
description: Include the output of `frps -v`
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: system-architecture
|
||||||
|
attributes:
|
||||||
|
label: System Architecture
|
||||||
|
description: Include which architecture you used, such as `linux/amd64`, `windows/amd64`
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: config
|
||||||
|
attributes:
|
||||||
|
label: Configurations
|
||||||
|
description: Include what configurrations you used and ran into this problem
|
||||||
|
placeholder: Pay attention to hiding the token and password in your output
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: log
|
||||||
|
attributes:
|
||||||
|
label: Logs
|
||||||
|
description: Prefer you providing releated error logs here
|
||||||
|
placeholder: Pay attention to hiding your personal informations
|
||||||
|
- type: textarea
|
||||||
|
id: steps-to-reproduce
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: How to reproduce it? It's important for us to find the bug
|
||||||
|
value: |
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
...
|
||||||
|
- type: checkboxes
|
||||||
|
id: area
|
||||||
|
attributes:
|
||||||
|
label: Affected area
|
||||||
|
options:
|
||||||
|
- label: "Docs"
|
||||||
|
- label: "Installation"
|
||||||
|
- label: "Performance and Scalability"
|
||||||
|
- label: "Security"
|
||||||
|
- label: "User Experience"
|
||||||
|
- label: "Test and Release"
|
||||||
|
- label: "Developer Infrastructure"
|
||||||
|
- label: "Client Plugin"
|
||||||
|
- label: "Server Plugin"
|
||||||
|
- label: "Extensions"
|
||||||
|
- label: "Others"
|
||||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
36
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
36
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Suggest an idea to improve frp
|
||||||
|
title: "[Feature Request] "
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
This is only used to request new product features.
|
||||||
|
- type: textarea
|
||||||
|
id: feature-request
|
||||||
|
attributes:
|
||||||
|
label: Describe the feature request
|
||||||
|
description: Tell us what's you want and why it should be added in frp.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Describe alternatives you've considered
|
||||||
|
- type: checkboxes
|
||||||
|
id: area
|
||||||
|
attributes:
|
||||||
|
label: Affected area
|
||||||
|
options:
|
||||||
|
- label: "Docs"
|
||||||
|
- label: "Installation"
|
||||||
|
- label: "Performance and Scalability"
|
||||||
|
- label: "Security"
|
||||||
|
- label: "User Experience"
|
||||||
|
- label: "Test and Release"
|
||||||
|
- label: "Developer Infrastructure"
|
||||||
|
- label: "Client Plugin"
|
||||||
|
- label: "Server Plugin"
|
||||||
|
- label: "Extensions"
|
||||||
|
- label: "Others"
|
||||||
3
.github/pull_request_template.md
vendored
Normal file
3
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
### WHY
|
||||||
|
|
||||||
|
<!-- author to complete -->
|
||||||
83
.github/workflows/build-and-push-image.yml
vendored
Normal file
83
.github/workflows/build-and-push-image.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
name: Build Image and Publish to Dockerhub & GPR
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [ published ]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Image tag'
|
||||||
|
required: true
|
||||||
|
default: 'test'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
image:
|
||||||
|
name: Build Image from Dockerfile and binaries
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
# environment
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: '0'
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
|
# get image tag name
|
||||||
|
- name: Get Image Tag Name
|
||||||
|
run: |
|
||||||
|
if [ x${{ github.event.inputs.tag }} == x"" ]; then
|
||||||
|
echo "TAG_NAME=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "TAG_NAME=${{ github.event.inputs.tag }}" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Login to the GPR
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GPR_TOKEN }}
|
||||||
|
|
||||||
|
# prepare image tags
|
||||||
|
- name: Prepare Image Tags
|
||||||
|
run: |
|
||||||
|
echo "DOCKERFILE_FRPC_PATH=dockerfiles/Dockerfile-for-frpc" >> $GITHUB_ENV
|
||||||
|
echo "DOCKERFILE_FRPS_PATH=dockerfiles/Dockerfile-for-frps" >> $GITHUB_ENV
|
||||||
|
echo "TAG_FRPC=fatedier/frpc:${{ env.TAG_NAME }}" >> $GITHUB_ENV
|
||||||
|
echo "TAG_FRPS=fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV
|
||||||
|
echo "TAG_FRPC_GPR=ghcr.io/fatedier/frpc:${{ env.TAG_NAME }}" >> $GITHUB_ENV
|
||||||
|
echo "TAG_FRPS_GPR=ghcr.io/fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Build and push frpc
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./dockerfiles/Dockerfile-for-frpc
|
||||||
|
platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.TAG_FRPC }}
|
||||||
|
${{ env.TAG_FRPC_GPR }}
|
||||||
|
|
||||||
|
- name: Build and push frps
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./dockerfiles/Dockerfile-for-frps
|
||||||
|
platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.TAG_FRPS }}
|
||||||
|
${{ env.TAG_FRPS_GPR }}
|
||||||
35
.github/workflows/golangci-lint.yml
vendored
Normal file
35
.github/workflows/golangci-lint.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: golangci-lint
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- dev
|
||||||
|
pull_request:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
# Optional: allow read access to pull request. Use with `only-new-issues` option.
|
||||||
|
pull-requests: read
|
||||||
|
jobs:
|
||||||
|
golangci:
|
||||||
|
name: lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: '1.25'
|
||||||
|
cache: false
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
- name: Build web assets (frps)
|
||||||
|
run: make build
|
||||||
|
working-directory: web/frps
|
||||||
|
- name: Build web assets (frpc)
|
||||||
|
run: make build
|
||||||
|
working-directory: web/frpc
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v9
|
||||||
|
with:
|
||||||
|
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
||||||
|
version: v2.10
|
||||||
38
.github/workflows/goreleaser.yml
vendored
Normal file
38
.github/workflows/goreleaser.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: goreleaser
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
goreleaser:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: '1.25'
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
- name: Build web assets (frps)
|
||||||
|
run: make build
|
||||||
|
working-directory: web/frps
|
||||||
|
- name: Build web assets (frpc)
|
||||||
|
run: make build
|
||||||
|
working-directory: web/frpc
|
||||||
|
- name: Make All
|
||||||
|
run: |
|
||||||
|
./package.sh
|
||||||
|
|
||||||
|
- name: Run GoReleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v7
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
args: release --clean --release-notes=./Release.md
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GPR_TOKEN }}
|
||||||
35
.github/workflows/stale.yml
vendored
Normal file
35
.github/workflows/stale.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: "Close stale issues and PRs"
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "20 0 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
debug-only:
|
||||||
|
description: 'In debug mod'
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
permissions:
|
||||||
|
issues: write # for actions/stale to close stale issues
|
||||||
|
pull-requests: write # for actions/stale to close stale PRs
|
||||||
|
actions: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v10
|
||||||
|
with:
|
||||||
|
stale-issue-message: 'Issues go stale after 14d of inactivity. Stale issues rot after an additional 3d of inactivity and eventually close.'
|
||||||
|
stale-pr-message: "PRs go stale after 14d of inactivity. Stale PRs rot after an additional 3d of inactivity and eventually close."
|
||||||
|
stale-issue-label: 'lifecycle/stale'
|
||||||
|
exempt-issue-labels: 'bug,doc,enhancement,future,proposal,question,testing,todo,easy,help wanted,assigned'
|
||||||
|
stale-pr-label: 'lifecycle/stale'
|
||||||
|
exempt-pr-labels: 'bug,doc,enhancement,future,proposal,question,testing,todo,easy,help wanted,assigned'
|
||||||
|
days-before-stale: 14
|
||||||
|
days-before-close: 3
|
||||||
|
debug-only: ${{ github.event.inputs.debug-only }}
|
||||||
|
exempt-all-pr-milestones: true
|
||||||
|
exempt-all-pr-assignees: true
|
||||||
|
operations-per-run: 200
|
||||||
30
.gitignore
vendored
30
.gitignore
vendored
@@ -7,24 +7,30 @@
|
|||||||
_obj
|
_obj
|
||||||
_test
|
_test
|
||||||
|
|
||||||
# Architecture specific extensions/prefixes
|
|
||||||
*.[568vq]
|
|
||||||
[568vq].out
|
|
||||||
|
|
||||||
*.cgo1.go
|
|
||||||
*.cgo2.c
|
|
||||||
_cgo_defun.c
|
|
||||||
_cgo_gotypes.go
|
|
||||||
_cgo_export.*
|
|
||||||
|
|
||||||
_testmain.go
|
|
||||||
|
|
||||||
*.exe
|
*.exe
|
||||||
*.test
|
*.test
|
||||||
*.prof
|
*.prof
|
||||||
|
|
||||||
# Self
|
# Self
|
||||||
bin/
|
bin/
|
||||||
|
packages/
|
||||||
|
release/
|
||||||
|
test/bin/
|
||||||
|
vendor/
|
||||||
|
lastversion/
|
||||||
|
dist/
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
.autogen_ssh_key
|
||||||
|
client.crt
|
||||||
|
client.key
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
|
||||||
# Cache
|
# Cache
|
||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
|
# AI
|
||||||
|
.claude/
|
||||||
|
.sisyphus/
|
||||||
|
.superpowers/
|
||||||
|
|||||||
119
.golangci.yml
Normal file
119
.golangci.yml
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
version: "2"
|
||||||
|
run:
|
||||||
|
concurrency: 4
|
||||||
|
timeout: 20m
|
||||||
|
build-tags:
|
||||||
|
- integ
|
||||||
|
- integfuzz
|
||||||
|
linters:
|
||||||
|
default: none
|
||||||
|
enable:
|
||||||
|
- asciicheck
|
||||||
|
- copyloopvar
|
||||||
|
- errcheck
|
||||||
|
- gocritic
|
||||||
|
- gosec
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- lll
|
||||||
|
- makezero
|
||||||
|
- misspell
|
||||||
|
- modernize
|
||||||
|
- prealloc
|
||||||
|
- predeclared
|
||||||
|
- revive
|
||||||
|
- staticcheck
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- unused
|
||||||
|
settings:
|
||||||
|
errcheck:
|
||||||
|
check-type-assertions: false
|
||||||
|
check-blank: false
|
||||||
|
gocritic:
|
||||||
|
disabled-checks:
|
||||||
|
- exitAfterDefer
|
||||||
|
gosec:
|
||||||
|
excludes: ["G115", "G117", "G204", "G401", "G402", "G404", "G501", "G703", "G704", "G705"]
|
||||||
|
severity: low
|
||||||
|
confidence: low
|
||||||
|
govet:
|
||||||
|
disable:
|
||||||
|
- shadow
|
||||||
|
lll:
|
||||||
|
line-length: 160
|
||||||
|
tab-width: 1
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
ignore-rules:
|
||||||
|
- cancelled
|
||||||
|
- marshalled
|
||||||
|
modernize:
|
||||||
|
disable:
|
||||||
|
- omitzero
|
||||||
|
unparam:
|
||||||
|
check-exported: false
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
presets:
|
||||||
|
- comments
|
||||||
|
- common-false-positives
|
||||||
|
- legacy
|
||||||
|
- std-error-handling
|
||||||
|
rules:
|
||||||
|
- linters:
|
||||||
|
- errcheck
|
||||||
|
- maligned
|
||||||
|
path: _test\.go$|^tests/|^samples/
|
||||||
|
- linters:
|
||||||
|
- revive
|
||||||
|
- staticcheck
|
||||||
|
text: use underscores in Go names
|
||||||
|
- linters:
|
||||||
|
- revive
|
||||||
|
text: unused-parameter
|
||||||
|
- linters:
|
||||||
|
- revive
|
||||||
|
text: "avoid meaningless package names"
|
||||||
|
- linters:
|
||||||
|
- revive
|
||||||
|
text: "Go standard library package names"
|
||||||
|
- linters:
|
||||||
|
- unparam
|
||||||
|
text: is always false
|
||||||
|
paths:
|
||||||
|
- .*\.pb\.go
|
||||||
|
- .*\.gen\.go
|
||||||
|
- genfiles$
|
||||||
|
- vendor$
|
||||||
|
- bin$
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
- node_modules
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gci
|
||||||
|
- gofumpt
|
||||||
|
- goimports
|
||||||
|
settings:
|
||||||
|
gci:
|
||||||
|
sections:
|
||||||
|
- standard
|
||||||
|
- default
|
||||||
|
- prefix(github.com/fatedier/frp/)
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
paths:
|
||||||
|
- .*\.pb\.go
|
||||||
|
- .*\.gen\.go
|
||||||
|
- genfiles$
|
||||||
|
- vendor$
|
||||||
|
- bin$
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
- node_modules
|
||||||
|
issues:
|
||||||
|
max-issues-per-linter: 0
|
||||||
|
max-same-issues: 0
|
||||||
22
.goreleaser.yml
Normal file
22
.goreleaser.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
builds:
|
||||||
|
- skip: true
|
||||||
|
checksum:
|
||||||
|
name_template: '{{ .ProjectName }}_sha256_checksums.txt'
|
||||||
|
algorithm: sha256
|
||||||
|
extra_files:
|
||||||
|
- glob: ./release/packages/*
|
||||||
|
release:
|
||||||
|
# Same as for github
|
||||||
|
# Note: it can only be one: either github, gitlab or gitea
|
||||||
|
github:
|
||||||
|
owner: fatedier
|
||||||
|
name: frp
|
||||||
|
|
||||||
|
draft: false
|
||||||
|
|
||||||
|
# You can add extra pre-existing files to the release.
|
||||||
|
# The filename on the release will be the last part of the path (base). If
|
||||||
|
# another file with the same name exists, the latest one found will be used.
|
||||||
|
# Defaults to empty.
|
||||||
|
extra_files:
|
||||||
|
- glob: ./release/packages/*
|
||||||
12
.travis.yml
12
.travis.yml
@@ -1,12 +0,0 @@
|
|||||||
sudo: false
|
|
||||||
language: go
|
|
||||||
|
|
||||||
go:
|
|
||||||
- 1.4.2
|
|
||||||
- 1.5.3
|
|
||||||
|
|
||||||
install:
|
|
||||||
- make
|
|
||||||
|
|
||||||
script:
|
|
||||||
- make test
|
|
||||||
39
AGENTS.md
Normal file
39
AGENTS.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Build
|
||||||
|
- `make build` - Build both frps and frpc binaries
|
||||||
|
- `make frps` - Build server binary only
|
||||||
|
- `make frpc` - Build client binary only
|
||||||
|
- `make all` - Build everything with formatting
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- `make test` - Run unit tests
|
||||||
|
- `make e2e` - Run end-to-end tests
|
||||||
|
- `make e2e-trace` - Run e2e tests with trace logging
|
||||||
|
- `make alltest` - Run all tests including vet, unit tests, and e2e
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
- `make fmt` - Run go fmt
|
||||||
|
- `make fmt-more` - Run gofumpt for more strict formatting
|
||||||
|
- `make gci` - Run gci import organizer
|
||||||
|
- `make vet` - Run go vet
|
||||||
|
- `golangci-lint run` - Run comprehensive linting (configured in .golangci.yml)
|
||||||
|
|
||||||
|
### Assets
|
||||||
|
- `make web` - Build web dashboards (frps and frpc)
|
||||||
|
|
||||||
|
### Cleanup
|
||||||
|
- `make clean` - Remove built binaries and temporary files
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- E2E tests using Ginkgo/Gomega framework
|
||||||
|
- Mock servers in `/test/e2e/mock/`
|
||||||
|
- Run: `make e2e` or `make alltest`
|
||||||
|
|
||||||
|
## Agent Runbooks
|
||||||
|
|
||||||
|
Operational procedures for agents are in `doc/agents/`:
|
||||||
|
- `doc/agents/release.md` - Release process
|
||||||
14
Dockerfile
14
Dockerfile
@@ -1,14 +0,0 @@
|
|||||||
FROM golang:1.5
|
|
||||||
|
|
||||||
MAINTAINER fatedier
|
|
||||||
|
|
||||||
RUN echo "[common]\nbind_addr = 0.0.0.0\nbind_port = 7000\n[test]\npasswd = 123\nbind_addr = 0.0.0.0\nlisten_port = 80" > /usr/share/frps.ini
|
|
||||||
|
|
||||||
ADD ./ /usr/share/frp/
|
|
||||||
|
|
||||||
RUN cd /usr/share/frp && make
|
|
||||||
|
|
||||||
EXPOSE 80
|
|
||||||
EXPOSE 7000
|
|
||||||
|
|
||||||
CMD ["/usr/share/frp/bin/frps", "-c", "/usr/share/frps.ini"]
|
|
||||||
23
Godeps/Godeps.json
generated
23
Godeps/Godeps.json
generated
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"ImportPath": "frp",
|
|
||||||
"GoVersion": "go1.4",
|
|
||||||
"Packages": [
|
|
||||||
"./..."
|
|
||||||
],
|
|
||||||
"Deps": [
|
|
||||||
{
|
|
||||||
"ImportPath": "github.com/astaxie/beego/logs",
|
|
||||||
"Comment": "v1.5.0-9-gfb7314f",
|
|
||||||
"Rev": "fb7314f8ac86b83ccd34386518d97cf2363e2ae5"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ImportPath": "github.com/docopt/docopt-go",
|
|
||||||
"Comment": "0.6.2",
|
|
||||||
"Rev": "784ddc588536785e7299f7272f39101f7faccc3f"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ImportPath": "github.com/vaughan0/go-ini",
|
|
||||||
"Rev": "a98ad7ee00ec53921f08832bc06ecf7fd600e6a1"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
5
Godeps/Readme
generated
5
Godeps/Readme
generated
@@ -1,5 +0,0 @@
|
|||||||
This directory tree is generated automatically by godep.
|
|
||||||
|
|
||||||
Please do not edit.
|
|
||||||
|
|
||||||
See https://github.com/tools/godep for more information.
|
|
||||||
2
Godeps/_workspace/.gitignore
generated
vendored
2
Godeps/_workspace/.gitignore
generated
vendored
@@ -1,2 +0,0 @@
|
|||||||
/pkg
|
|
||||||
/bin
|
|
||||||
13
Godeps/_workspace/src/github.com/astaxie/beego/LICENSE
generated
vendored
13
Godeps/_workspace/src/github.com/astaxie/beego/LICENSE
generated
vendored
@@ -1,13 +0,0 @@
|
|||||||
Copyright 2014 astaxie
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
63
Godeps/_workspace/src/github.com/astaxie/beego/logs/README.md
generated
vendored
63
Godeps/_workspace/src/github.com/astaxie/beego/logs/README.md
generated
vendored
@@ -1,63 +0,0 @@
|
|||||||
## logs
|
|
||||||
logs is a Go logs manager. It can use many logs adapters. The repo is inspired by `database/sql` .
|
|
||||||
|
|
||||||
|
|
||||||
## How to install?
|
|
||||||
|
|
||||||
go get github.com/astaxie/beego/logs
|
|
||||||
|
|
||||||
|
|
||||||
## What adapters are supported?
|
|
||||||
|
|
||||||
As of now this logs support console, file,smtp and conn.
|
|
||||||
|
|
||||||
|
|
||||||
## How to use it?
|
|
||||||
|
|
||||||
First you must import it
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/astaxie/beego/logs"
|
|
||||||
)
|
|
||||||
|
|
||||||
Then init a Log (example with console adapter)
|
|
||||||
|
|
||||||
log := NewLogger(10000)
|
|
||||||
log.SetLogger("console", "")
|
|
||||||
|
|
||||||
> the first params stand for how many channel
|
|
||||||
|
|
||||||
Use it like this:
|
|
||||||
|
|
||||||
log.Trace("trace")
|
|
||||||
log.Info("info")
|
|
||||||
log.Warn("warning")
|
|
||||||
log.Debug("debug")
|
|
||||||
log.Critical("critical")
|
|
||||||
|
|
||||||
|
|
||||||
## File adapter
|
|
||||||
|
|
||||||
Configure file adapter like this:
|
|
||||||
|
|
||||||
log := NewLogger(10000)
|
|
||||||
log.SetLogger("file", `{"filename":"test.log"}`)
|
|
||||||
|
|
||||||
|
|
||||||
## Conn adapter
|
|
||||||
|
|
||||||
Configure like this:
|
|
||||||
|
|
||||||
log := NewLogger(1000)
|
|
||||||
log.SetLogger("conn", `{"net":"tcp","addr":":7020"}`)
|
|
||||||
log.Info("info")
|
|
||||||
|
|
||||||
|
|
||||||
## Smtp adapter
|
|
||||||
|
|
||||||
Configure like this:
|
|
||||||
|
|
||||||
log := NewLogger(10000)
|
|
||||||
log.SetLogger("smtp", `{"username":"beegotest@gmail.com","password":"xxxxxxxx","host":"smtp.gmail.com:587","sendTos":["xiemengjun@gmail.com"]}`)
|
|
||||||
log.Critical("sendmail critical")
|
|
||||||
time.Sleep(time.Second * 30)
|
|
||||||
116
Godeps/_workspace/src/github.com/astaxie/beego/logs/conn.go
generated
vendored
116
Godeps/_workspace/src/github.com/astaxie/beego/logs/conn.go
generated
vendored
@@ -1,116 +0,0 @@
|
|||||||
// Copyright 2014 beego Author. All Rights Reserved.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package logs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ConnWriter implements LoggerInterface.
|
|
||||||
// it writes messages in keep-live tcp connection.
|
|
||||||
type ConnWriter struct {
|
|
||||||
lg *log.Logger
|
|
||||||
innerWriter io.WriteCloser
|
|
||||||
ReconnectOnMsg bool `json:"reconnectOnMsg"`
|
|
||||||
Reconnect bool `json:"reconnect"`
|
|
||||||
Net string `json:"net"`
|
|
||||||
Addr string `json:"addr"`
|
|
||||||
Level int `json:"level"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// create new ConnWrite returning as LoggerInterface.
|
|
||||||
func NewConn() LoggerInterface {
|
|
||||||
conn := new(ConnWriter)
|
|
||||||
conn.Level = LevelTrace
|
|
||||||
return conn
|
|
||||||
}
|
|
||||||
|
|
||||||
// init connection writer with json config.
|
|
||||||
// json config only need key "level".
|
|
||||||
func (c *ConnWriter) Init(jsonconfig string) error {
|
|
||||||
return json.Unmarshal([]byte(jsonconfig), c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// write message in connection.
|
|
||||||
// if connection is down, try to re-connect.
|
|
||||||
func (c *ConnWriter) WriteMsg(msg string, level int) error {
|
|
||||||
if level > c.Level {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if c.neddedConnectOnMsg() {
|
|
||||||
err := c.connect()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.ReconnectOnMsg {
|
|
||||||
defer c.innerWriter.Close()
|
|
||||||
}
|
|
||||||
c.lg.Println(msg)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// implementing method. empty.
|
|
||||||
func (c *ConnWriter) Flush() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// destroy connection writer and close tcp listener.
|
|
||||||
func (c *ConnWriter) Destroy() {
|
|
||||||
if c.innerWriter != nil {
|
|
||||||
c.innerWriter.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ConnWriter) connect() error {
|
|
||||||
if c.innerWriter != nil {
|
|
||||||
c.innerWriter.Close()
|
|
||||||
c.innerWriter = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := net.Dial(c.Net, c.Addr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if tcpConn, ok := conn.(*net.TCPConn); ok {
|
|
||||||
tcpConn.SetKeepAlive(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.innerWriter = conn
|
|
||||||
c.lg = log.New(conn, "", log.Ldate|log.Ltime)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ConnWriter) neddedConnectOnMsg() bool {
|
|
||||||
if c.Reconnect {
|
|
||||||
c.Reconnect = false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.innerWriter == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.ReconnectOnMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
Register("conn", NewConn)
|
|
||||||
}
|
|
||||||
95
Godeps/_workspace/src/github.com/astaxie/beego/logs/console.go
generated
vendored
95
Godeps/_workspace/src/github.com/astaxie/beego/logs/console.go
generated
vendored
@@ -1,95 +0,0 @@
|
|||||||
// Copyright 2014 beego Author. All Rights Reserved.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package logs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Brush func(string) string
|
|
||||||
|
|
||||||
func NewBrush(color string) Brush {
|
|
||||||
pre := "\033["
|
|
||||||
reset := "\033[0m"
|
|
||||||
return func(text string) string {
|
|
||||||
return pre + color + "m" + text + reset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var colors = []Brush{
|
|
||||||
NewBrush("1;37"), // Emergency white
|
|
||||||
NewBrush("1;36"), // Alert cyan
|
|
||||||
NewBrush("1;35"), // Critical magenta
|
|
||||||
NewBrush("1;31"), // Error red
|
|
||||||
NewBrush("1;33"), // Warning yellow
|
|
||||||
NewBrush("1;32"), // Notice green
|
|
||||||
NewBrush("1;34"), // Informational blue
|
|
||||||
NewBrush("1;34"), // Debug blue
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConsoleWriter implements LoggerInterface and writes messages to terminal.
|
|
||||||
type ConsoleWriter struct {
|
|
||||||
lg *log.Logger
|
|
||||||
Level int `json:"level"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// create ConsoleWriter returning as LoggerInterface.
|
|
||||||
func NewConsole() LoggerInterface {
|
|
||||||
cw := &ConsoleWriter{
|
|
||||||
lg: log.New(os.Stdout, "", log.Ldate|log.Ltime),
|
|
||||||
Level: LevelDebug,
|
|
||||||
}
|
|
||||||
return cw
|
|
||||||
}
|
|
||||||
|
|
||||||
// init console logger.
|
|
||||||
// jsonconfig like '{"level":LevelTrace}'.
|
|
||||||
func (c *ConsoleWriter) Init(jsonconfig string) error {
|
|
||||||
if len(jsonconfig) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return json.Unmarshal([]byte(jsonconfig), c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// write message in console.
|
|
||||||
func (c *ConsoleWriter) WriteMsg(msg string, level int) error {
|
|
||||||
if level > c.Level {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if goos := runtime.GOOS; goos == "windows" {
|
|
||||||
c.lg.Println(msg)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
c.lg.Println(colors[level](msg))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// implementing method. empty.
|
|
||||||
func (c *ConsoleWriter) Destroy() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// implementing method. empty.
|
|
||||||
func (c *ConsoleWriter) Flush() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
Register("console", NewConsole)
|
|
||||||
}
|
|
||||||
76
Godeps/_workspace/src/github.com/astaxie/beego/logs/es/es.go
generated
vendored
76
Godeps/_workspace/src/github.com/astaxie/beego/logs/es/es.go
generated
vendored
@@ -1,76 +0,0 @@
|
|||||||
package es
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/url"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/astaxie/beego/logs"
|
|
||||||
"github.com/belogik/goes"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewES() logs.LoggerInterface {
|
|
||||||
cw := &esLogger{
|
|
||||||
Level: logs.LevelDebug,
|
|
||||||
}
|
|
||||||
return cw
|
|
||||||
}
|
|
||||||
|
|
||||||
type esLogger struct {
|
|
||||||
*goes.Connection
|
|
||||||
DSN string `json:"dsn"`
|
|
||||||
Level int `json:"level"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// {"dsn":"http://localhost:9200/","level":1}
|
|
||||||
func (el *esLogger) Init(jsonconfig string) error {
|
|
||||||
err := json.Unmarshal([]byte(jsonconfig), el)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if el.DSN == "" {
|
|
||||||
return errors.New("empty dsn")
|
|
||||||
} else if u, err := url.Parse(el.DSN); err != nil {
|
|
||||||
return err
|
|
||||||
} else if u.Path == "" {
|
|
||||||
return errors.New("missing prefix")
|
|
||||||
} else if host, port, err := net.SplitHostPort(u.Host); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
conn := goes.NewConnection(host, port)
|
|
||||||
el.Connection = conn
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (el *esLogger) WriteMsg(msg string, level int) error {
|
|
||||||
if level > el.Level {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
t := time.Now()
|
|
||||||
vals := make(map[string]interface{})
|
|
||||||
vals["@timestamp"] = t.Format(time.RFC3339)
|
|
||||||
vals["@msg"] = msg
|
|
||||||
d := goes.Document{
|
|
||||||
Index: fmt.Sprintf("%04d.%02d.%02d", t.Year(), t.Month(), t.Day()),
|
|
||||||
Type: "logs",
|
|
||||||
Fields: vals,
|
|
||||||
}
|
|
||||||
_, err := el.Index(d, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (el *esLogger) Destroy() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (el *esLogger) Flush() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
logs.Register("es", NewES)
|
|
||||||
}
|
|
||||||
283
Godeps/_workspace/src/github.com/astaxie/beego/logs/file.go
generated
vendored
283
Godeps/_workspace/src/github.com/astaxie/beego/logs/file.go
generated
vendored
@@ -1,283 +0,0 @@
|
|||||||
// Copyright 2014 beego Author. All Rights Reserved.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package logs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FileLogWriter implements LoggerInterface.
|
|
||||||
// It writes messages by lines limit, file size limit, or time frequency.
|
|
||||||
type FileLogWriter struct {
|
|
||||||
*log.Logger
|
|
||||||
mw *MuxWriter
|
|
||||||
// The opened file
|
|
||||||
Filename string `json:"filename"`
|
|
||||||
|
|
||||||
Maxlines int `json:"maxlines"`
|
|
||||||
maxlines_curlines int
|
|
||||||
|
|
||||||
// Rotate at size
|
|
||||||
Maxsize int `json:"maxsize"`
|
|
||||||
maxsize_cursize int
|
|
||||||
|
|
||||||
// Rotate daily
|
|
||||||
Daily bool `json:"daily"`
|
|
||||||
Maxdays int64 `json:"maxdays"`
|
|
||||||
daily_opendate int
|
|
||||||
|
|
||||||
Rotate bool `json:"rotate"`
|
|
||||||
|
|
||||||
startLock sync.Mutex // Only one log can write to the file
|
|
||||||
|
|
||||||
Level int `json:"level"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// an *os.File writer with locker.
|
|
||||||
type MuxWriter struct {
|
|
||||||
sync.Mutex
|
|
||||||
fd *os.File
|
|
||||||
}
|
|
||||||
|
|
||||||
// write to os.File.
|
|
||||||
func (l *MuxWriter) Write(b []byte) (int, error) {
|
|
||||||
l.Lock()
|
|
||||||
defer l.Unlock()
|
|
||||||
return l.fd.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// set os.File in writer.
|
|
||||||
func (l *MuxWriter) SetFd(fd *os.File) {
|
|
||||||
if l.fd != nil {
|
|
||||||
l.fd.Close()
|
|
||||||
}
|
|
||||||
l.fd = fd
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a FileLogWriter returning as LoggerInterface.
|
|
||||||
func NewFileWriter() LoggerInterface {
|
|
||||||
w := &FileLogWriter{
|
|
||||||
Filename: "",
|
|
||||||
Maxlines: 1000000,
|
|
||||||
Maxsize: 1 << 28, //256 MB
|
|
||||||
Daily: true,
|
|
||||||
Maxdays: 7,
|
|
||||||
Rotate: true,
|
|
||||||
Level: LevelTrace,
|
|
||||||
}
|
|
||||||
// use MuxWriter instead direct use os.File for lock write when rotate
|
|
||||||
w.mw = new(MuxWriter)
|
|
||||||
// set MuxWriter as Logger's io.Writer
|
|
||||||
w.Logger = log.New(w.mw, "", log.Ldate|log.Ltime)
|
|
||||||
return w
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init file logger with json config.
|
|
||||||
// jsonconfig like:
|
|
||||||
// {
|
|
||||||
// "filename":"logs/beego.log",
|
|
||||||
// "maxlines":10000,
|
|
||||||
// "maxsize":1<<30,
|
|
||||||
// "daily":true,
|
|
||||||
// "maxdays":15,
|
|
||||||
// "rotate":true
|
|
||||||
// }
|
|
||||||
func (w *FileLogWriter) Init(jsonconfig string) error {
|
|
||||||
err := json.Unmarshal([]byte(jsonconfig), w)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(w.Filename) == 0 {
|
|
||||||
return errors.New("jsonconfig must have filename")
|
|
||||||
}
|
|
||||||
err = w.startLogger()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// start file logger. create log file and set to locker-inside file writer.
|
|
||||||
func (w *FileLogWriter) startLogger() error {
|
|
||||||
fd, err := w.createLogFile()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
w.mw.SetFd(fd)
|
|
||||||
return w.initFd()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *FileLogWriter) docheck(size int) {
|
|
||||||
w.startLock.Lock()
|
|
||||||
defer w.startLock.Unlock()
|
|
||||||
if w.Rotate && ((w.Maxlines > 0 && w.maxlines_curlines >= w.Maxlines) ||
|
|
||||||
(w.Maxsize > 0 && w.maxsize_cursize >= w.Maxsize) ||
|
|
||||||
(w.Daily && time.Now().Day() != w.daily_opendate)) {
|
|
||||||
if err := w.DoRotate(); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w.maxlines_curlines++
|
|
||||||
w.maxsize_cursize += size
|
|
||||||
}
|
|
||||||
|
|
||||||
// write logger message into file.
|
|
||||||
func (w *FileLogWriter) WriteMsg(msg string, level int) error {
|
|
||||||
if level > w.Level {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
n := 24 + len(msg) // 24 stand for the length "2013/06/23 21:00:22 [T] "
|
|
||||||
w.docheck(n)
|
|
||||||
w.Logger.Println(msg)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *FileLogWriter) createLogFile() (*os.File, error) {
|
|
||||||
// Open the log file
|
|
||||||
fd, err := os.OpenFile(w.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0660)
|
|
||||||
return fd, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *FileLogWriter) initFd() error {
|
|
||||||
fd := w.mw.fd
|
|
||||||
finfo, err := fd.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get stat err: %s\n", err)
|
|
||||||
}
|
|
||||||
w.maxsize_cursize = int(finfo.Size())
|
|
||||||
w.daily_opendate = time.Now().Day()
|
|
||||||
w.maxlines_curlines = 0
|
|
||||||
if finfo.Size() > 0 {
|
|
||||||
count, err := w.lines()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
w.maxlines_curlines = count
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *FileLogWriter) lines() (int, error) {
|
|
||||||
fd, err := os.Open(w.Filename)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
defer fd.Close()
|
|
||||||
|
|
||||||
buf := make([]byte, 32768) // 32k
|
|
||||||
count := 0
|
|
||||||
lineSep := []byte{'\n'}
|
|
||||||
|
|
||||||
for {
|
|
||||||
c, err := fd.Read(buf)
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
return count, err
|
|
||||||
}
|
|
||||||
|
|
||||||
count += bytes.Count(buf[:c], lineSep)
|
|
||||||
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return count, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DoRotate means it need to write file in new file.
|
|
||||||
// new file name like xx.log.2013-01-01.2
|
|
||||||
func (w *FileLogWriter) DoRotate() error {
|
|
||||||
_, err := os.Lstat(w.Filename)
|
|
||||||
if err == nil { // file exists
|
|
||||||
// Find the next available number
|
|
||||||
num := 1
|
|
||||||
fname := ""
|
|
||||||
for ; err == nil && num <= 999; num++ {
|
|
||||||
fname = w.Filename + fmt.Sprintf(".%s.%03d", time.Now().Format("2006-01-02"), num)
|
|
||||||
_, err = os.Lstat(fname)
|
|
||||||
}
|
|
||||||
// return error if the last file checked still existed
|
|
||||||
if err == nil {
|
|
||||||
return fmt.Errorf("Rotate: Cannot find free log number to rename %s\n", w.Filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
// block Logger's io.Writer
|
|
||||||
w.mw.Lock()
|
|
||||||
defer w.mw.Unlock()
|
|
||||||
|
|
||||||
fd := w.mw.fd
|
|
||||||
fd.Close()
|
|
||||||
|
|
||||||
// close fd before rename
|
|
||||||
// Rename the file to its newfound home
|
|
||||||
err = os.Rename(w.Filename, fname)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Rotate: %s\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// re-start logger
|
|
||||||
err = w.startLogger()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Rotate StartLogger: %s\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
go w.deleteOldLog()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *FileLogWriter) deleteOldLog() {
|
|
||||||
dir := filepath.Dir(w.Filename)
|
|
||||||
filepath.Walk(dir, func(path string, info os.FileInfo, err error) (returnErr error) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
returnErr = fmt.Errorf("Unable to delete old log '%s', error: %+v", path, r)
|
|
||||||
fmt.Println(returnErr)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if !info.IsDir() && info.ModTime().Unix() < (time.Now().Unix()-60*60*24*w.Maxdays) {
|
|
||||||
if strings.HasPrefix(filepath.Base(path), filepath.Base(w.Filename)) {
|
|
||||||
os.Remove(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// destroy file logger, close file writer.
|
|
||||||
func (w *FileLogWriter) Destroy() {
|
|
||||||
w.mw.fd.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// flush file logger.
|
|
||||||
// there are no buffering messages in file logger in memory.
|
|
||||||
// flush file means sync file from disk.
|
|
||||||
func (w *FileLogWriter) Flush() {
|
|
||||||
w.mw.fd.Sync()
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
Register("file", NewFileWriter)
|
|
||||||
}
|
|
||||||
350
Godeps/_workspace/src/github.com/astaxie/beego/logs/log.go
generated
vendored
350
Godeps/_workspace/src/github.com/astaxie/beego/logs/log.go
generated
vendored
@@ -1,350 +0,0 @@
|
|||||||
// Copyright 2014 beego Author. All Rights Reserved.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
// Usage:
|
|
||||||
//
|
|
||||||
// import "github.com/astaxie/beego/logs"
|
|
||||||
//
|
|
||||||
// log := NewLogger(10000)
|
|
||||||
// log.SetLogger("console", "")
|
|
||||||
//
|
|
||||||
// > the first params stand for how many channel
|
|
||||||
//
|
|
||||||
// Use it like this:
|
|
||||||
//
|
|
||||||
// log.Trace("trace")
|
|
||||||
// log.Info("info")
|
|
||||||
// log.Warn("warning")
|
|
||||||
// log.Debug("debug")
|
|
||||||
// log.Critical("critical")
|
|
||||||
//
|
|
||||||
// more docs http://beego.me/docs/module/logs.md
|
|
||||||
package logs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
"runtime"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RFC5424 log message levels.
|
|
||||||
const (
|
|
||||||
LevelEmergency = iota
|
|
||||||
LevelAlert
|
|
||||||
LevelCritical
|
|
||||||
LevelError
|
|
||||||
LevelWarning
|
|
||||||
LevelNotice
|
|
||||||
LevelInformational
|
|
||||||
LevelDebug
|
|
||||||
)
|
|
||||||
|
|
||||||
// Legacy loglevel constants to ensure backwards compatibility.
|
|
||||||
//
|
|
||||||
// Deprecated: will be removed in 1.5.0.
|
|
||||||
const (
|
|
||||||
LevelInfo = LevelInformational
|
|
||||||
LevelTrace = LevelDebug
|
|
||||||
LevelWarn = LevelWarning
|
|
||||||
)
|
|
||||||
|
|
||||||
type loggerType func() LoggerInterface
|
|
||||||
|
|
||||||
// LoggerInterface defines the behavior of a log provider.
|
|
||||||
type LoggerInterface interface {
|
|
||||||
Init(config string) error
|
|
||||||
WriteMsg(msg string, level int) error
|
|
||||||
Destroy()
|
|
||||||
Flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
var adapters = make(map[string]loggerType)
|
|
||||||
|
|
||||||
// Register makes a log provide available by the provided name.
|
|
||||||
// If Register is called twice with the same name or if driver is nil,
|
|
||||||
// it panics.
|
|
||||||
func Register(name string, log loggerType) {
|
|
||||||
if log == nil {
|
|
||||||
panic("logs: Register provide is nil")
|
|
||||||
}
|
|
||||||
if _, dup := adapters[name]; dup {
|
|
||||||
panic("logs: Register called twice for provider " + name)
|
|
||||||
}
|
|
||||||
adapters[name] = log
|
|
||||||
}
|
|
||||||
|
|
||||||
// BeeLogger is default logger in beego application.
|
|
||||||
// it can contain several providers and log message into all providers.
|
|
||||||
type BeeLogger struct {
|
|
||||||
lock sync.Mutex
|
|
||||||
level int
|
|
||||||
enableFuncCallDepth bool
|
|
||||||
loggerFuncCallDepth int
|
|
||||||
asynchronous bool
|
|
||||||
msg chan *logMsg
|
|
||||||
outputs map[string]LoggerInterface
|
|
||||||
}
|
|
||||||
|
|
||||||
type logMsg struct {
|
|
||||||
level int
|
|
||||||
msg string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLogger returns a new BeeLogger.
|
|
||||||
// channellen means the number of messages in chan.
|
|
||||||
// if the buffering chan is full, logger adapters write to file or other way.
|
|
||||||
func NewLogger(channellen int64) *BeeLogger {
|
|
||||||
bl := new(BeeLogger)
|
|
||||||
bl.level = LevelDebug
|
|
||||||
bl.loggerFuncCallDepth = 2
|
|
||||||
bl.msg = make(chan *logMsg, channellen)
|
|
||||||
bl.outputs = make(map[string]LoggerInterface)
|
|
||||||
return bl
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bl *BeeLogger) Async() *BeeLogger {
|
|
||||||
bl.asynchronous = true
|
|
||||||
go bl.startLogger()
|
|
||||||
return bl
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLogger provides a given logger adapter into BeeLogger with config string.
|
|
||||||
// config need to be correct JSON as string: {"interval":360}.
|
|
||||||
func (bl *BeeLogger) SetLogger(adaptername string, config string) error {
|
|
||||||
bl.lock.Lock()
|
|
||||||
defer bl.lock.Unlock()
|
|
||||||
if log, ok := adapters[adaptername]; ok {
|
|
||||||
lg := log()
|
|
||||||
err := lg.Init(config)
|
|
||||||
bl.outputs[adaptername] = lg
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("logs.BeeLogger.SetLogger: " + err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("logs: unknown adaptername %q (forgotten Register?)", adaptername)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove a logger adapter in BeeLogger.
|
|
||||||
func (bl *BeeLogger) DelLogger(adaptername string) error {
|
|
||||||
bl.lock.Lock()
|
|
||||||
defer bl.lock.Unlock()
|
|
||||||
if lg, ok := bl.outputs[adaptername]; ok {
|
|
||||||
lg.Destroy()
|
|
||||||
delete(bl.outputs, adaptername)
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("logs: unknown adaptername %q (forgotten Register?)", adaptername)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bl *BeeLogger) writerMsg(loglevel int, msg string) error {
|
|
||||||
lm := new(logMsg)
|
|
||||||
lm.level = loglevel
|
|
||||||
if bl.enableFuncCallDepth {
|
|
||||||
_, file, line, ok := runtime.Caller(bl.loggerFuncCallDepth)
|
|
||||||
if !ok {
|
|
||||||
file = "???"
|
|
||||||
line = 0
|
|
||||||
}
|
|
||||||
_, filename := path.Split(file)
|
|
||||||
lm.msg = fmt.Sprintf("[%s:%d] %s", filename, line, msg)
|
|
||||||
} else {
|
|
||||||
lm.msg = msg
|
|
||||||
}
|
|
||||||
if bl.asynchronous {
|
|
||||||
bl.msg <- lm
|
|
||||||
} else {
|
|
||||||
for name, l := range bl.outputs {
|
|
||||||
err := l.WriteMsg(lm.msg, lm.level)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("unable to WriteMsg to adapter:", name, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set log message level.
|
|
||||||
//
|
|
||||||
// If message level (such as LevelDebug) is higher than logger level (such as LevelWarning),
|
|
||||||
// log providers will not even be sent the message.
|
|
||||||
func (bl *BeeLogger) SetLevel(l int) {
|
|
||||||
bl.level = l
|
|
||||||
}
|
|
||||||
|
|
||||||
// set log funcCallDepth
|
|
||||||
func (bl *BeeLogger) SetLogFuncCallDepth(d int) {
|
|
||||||
bl.loggerFuncCallDepth = d
|
|
||||||
}
|
|
||||||
|
|
||||||
// get log funcCallDepth for wrapper
|
|
||||||
func (bl *BeeLogger) GetLogFuncCallDepth() int {
|
|
||||||
return bl.loggerFuncCallDepth
|
|
||||||
}
|
|
||||||
|
|
||||||
// enable log funcCallDepth
|
|
||||||
func (bl *BeeLogger) EnableFuncCallDepth(b bool) {
|
|
||||||
bl.enableFuncCallDepth = b
|
|
||||||
}
|
|
||||||
|
|
||||||
// start logger chan reading.
|
|
||||||
// when chan is not empty, write logs.
|
|
||||||
func (bl *BeeLogger) startLogger() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case bm := <-bl.msg:
|
|
||||||
for _, l := range bl.outputs {
|
|
||||||
err := l.WriteMsg(bm.msg, bm.level)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("ERROR, unable to WriteMsg:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log EMERGENCY level message.
|
|
||||||
func (bl *BeeLogger) Emergency(format string, v ...interface{}) {
|
|
||||||
if LevelEmergency > bl.level {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
msg := fmt.Sprintf("[M] "+format, v...)
|
|
||||||
bl.writerMsg(LevelEmergency, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log ALERT level message.
|
|
||||||
func (bl *BeeLogger) Alert(format string, v ...interface{}) {
|
|
||||||
if LevelAlert > bl.level {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
msg := fmt.Sprintf("[A] "+format, v...)
|
|
||||||
bl.writerMsg(LevelAlert, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log CRITICAL level message.
|
|
||||||
func (bl *BeeLogger) Critical(format string, v ...interface{}) {
|
|
||||||
if LevelCritical > bl.level {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
msg := fmt.Sprintf("[C] "+format, v...)
|
|
||||||
bl.writerMsg(LevelCritical, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log ERROR level message.
|
|
||||||
func (bl *BeeLogger) Error(format string, v ...interface{}) {
|
|
||||||
if LevelError > bl.level {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
msg := fmt.Sprintf("[E] "+format, v...)
|
|
||||||
bl.writerMsg(LevelError, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log WARNING level message.
|
|
||||||
func (bl *BeeLogger) Warning(format string, v ...interface{}) {
|
|
||||||
if LevelWarning > bl.level {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
msg := fmt.Sprintf("[W] "+format, v...)
|
|
||||||
bl.writerMsg(LevelWarning, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log NOTICE level message.
|
|
||||||
func (bl *BeeLogger) Notice(format string, v ...interface{}) {
|
|
||||||
if LevelNotice > bl.level {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
msg := fmt.Sprintf("[N] "+format, v...)
|
|
||||||
bl.writerMsg(LevelNotice, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log INFORMATIONAL level message.
|
|
||||||
func (bl *BeeLogger) Informational(format string, v ...interface{}) {
|
|
||||||
if LevelInformational > bl.level {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
msg := fmt.Sprintf("[I] "+format, v...)
|
|
||||||
bl.writerMsg(LevelInformational, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log DEBUG level message.
|
|
||||||
func (bl *BeeLogger) Debug(format string, v ...interface{}) {
|
|
||||||
if LevelDebug > bl.level {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
msg := fmt.Sprintf("[D] "+format, v...)
|
|
||||||
bl.writerMsg(LevelDebug, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log WARN level message.
|
|
||||||
// compatibility alias for Warning()
|
|
||||||
func (bl *BeeLogger) Warn(format string, v ...interface{}) {
|
|
||||||
if LevelWarning > bl.level {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
msg := fmt.Sprintf("[W] "+format, v...)
|
|
||||||
bl.writerMsg(LevelWarning, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log INFO level message.
|
|
||||||
// compatibility alias for Informational()
|
|
||||||
func (bl *BeeLogger) Info(format string, v ...interface{}) {
|
|
||||||
if LevelInformational > bl.level {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
msg := fmt.Sprintf("[I] "+format, v...)
|
|
||||||
bl.writerMsg(LevelInformational, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log TRACE level message.
|
|
||||||
// compatibility alias for Debug()
|
|
||||||
func (bl *BeeLogger) Trace(format string, v ...interface{}) {
|
|
||||||
if LevelDebug > bl.level {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
msg := fmt.Sprintf("[D] "+format, v...)
|
|
||||||
bl.writerMsg(LevelDebug, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// flush all chan data.
|
|
||||||
func (bl *BeeLogger) Flush() {
|
|
||||||
for _, l := range bl.outputs {
|
|
||||||
l.Flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// close logger, flush all chan data and destroy all adapters in BeeLogger.
|
|
||||||
func (bl *BeeLogger) Close() {
|
|
||||||
for {
|
|
||||||
if len(bl.msg) > 0 {
|
|
||||||
bm := <-bl.msg
|
|
||||||
for _, l := range bl.outputs {
|
|
||||||
err := l.WriteMsg(bm.msg, bm.level)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("ERROR, unable to WriteMsg (while closing logger):", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
for _, l := range bl.outputs {
|
|
||||||
l.Flush()
|
|
||||||
l.Destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
165
Godeps/_workspace/src/github.com/astaxie/beego/logs/smtp.go
generated
vendored
165
Godeps/_workspace/src/github.com/astaxie/beego/logs/smtp.go
generated
vendored
@@ -1,165 +0,0 @@
|
|||||||
// Copyright 2014 beego Author. All Rights Reserved.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package logs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/smtp"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// no usage
|
|
||||||
// subjectPhrase = "Diagnostic message from server"
|
|
||||||
)
|
|
||||||
|
|
||||||
// smtpWriter implements LoggerInterface and is used to send emails via given SMTP-server.
|
|
||||||
type SmtpWriter struct {
|
|
||||||
Username string `json:"Username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
Host string `json:"Host"`
|
|
||||||
Subject string `json:"subject"`
|
|
||||||
FromAddress string `json:"fromAddress"`
|
|
||||||
RecipientAddresses []string `json:"sendTos"`
|
|
||||||
Level int `json:"level"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// create smtp writer.
|
|
||||||
func NewSmtpWriter() LoggerInterface {
|
|
||||||
return &SmtpWriter{Level: LevelTrace}
|
|
||||||
}
|
|
||||||
|
|
||||||
// init smtp writer with json config.
|
|
||||||
// config like:
|
|
||||||
// {
|
|
||||||
// "Username":"example@gmail.com",
|
|
||||||
// "password:"password",
|
|
||||||
// "host":"smtp.gmail.com:465",
|
|
||||||
// "subject":"email title",
|
|
||||||
// "fromAddress":"from@example.com",
|
|
||||||
// "sendTos":["email1","email2"],
|
|
||||||
// "level":LevelError
|
|
||||||
// }
|
|
||||||
func (s *SmtpWriter) Init(jsonconfig string) error {
|
|
||||||
err := json.Unmarshal([]byte(jsonconfig), s)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SmtpWriter) GetSmtpAuth(host string) smtp.Auth {
|
|
||||||
if len(strings.Trim(s.Username, " ")) == 0 && len(strings.Trim(s.Password, " ")) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return smtp.PlainAuth(
|
|
||||||
"",
|
|
||||||
s.Username,
|
|
||||||
s.Password,
|
|
||||||
host,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SmtpWriter) sendMail(hostAddressWithPort string, auth smtp.Auth, fromAddress string, recipients []string, msgContent []byte) error {
|
|
||||||
client, err := smtp.Dial(hostAddressWithPort)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
host, _, _ := net.SplitHostPort(hostAddressWithPort)
|
|
||||||
tlsConn := &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
ServerName: host,
|
|
||||||
}
|
|
||||||
if err = client.StartTLS(tlsConn); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if auth != nil {
|
|
||||||
if err = client.Auth(auth); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = client.Mail(fromAddress); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, rec := range recipients {
|
|
||||||
if err = client.Rcpt(rec); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w, err := client.Data()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = w.Write([]byte(msgContent))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = w.Close()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = client.Quit()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// write message in smtp writer.
|
|
||||||
// it will send an email with subject and only this message.
|
|
||||||
func (s *SmtpWriter) WriteMsg(msg string, level int) error {
|
|
||||||
if level > s.Level {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
hp := strings.Split(s.Host, ":")
|
|
||||||
|
|
||||||
// Set up authentication information.
|
|
||||||
auth := s.GetSmtpAuth(hp[0])
|
|
||||||
|
|
||||||
// Connect to the server, authenticate, set the sender and recipient,
|
|
||||||
// and send the email all in one step.
|
|
||||||
content_type := "Content-Type: text/plain" + "; charset=UTF-8"
|
|
||||||
mailmsg := []byte("To: " + strings.Join(s.RecipientAddresses, ";") + "\r\nFrom: " + s.FromAddress + "<" + s.FromAddress +
|
|
||||||
">\r\nSubject: " + s.Subject + "\r\n" + content_type + "\r\n\r\n" + fmt.Sprintf(".%s", time.Now().Format("2006-01-02 15:04:05")) + msg)
|
|
||||||
|
|
||||||
return s.sendMail(s.Host, auth, s.FromAddress, s.RecipientAddresses, mailmsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// implementing method. empty.
|
|
||||||
func (s *SmtpWriter) Flush() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// implementing method. empty.
|
|
||||||
func (s *SmtpWriter) Destroy() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
Register("smtp", NewSmtpWriter)
|
|
||||||
}
|
|
||||||
19
Godeps/_workspace/src/github.com/astaxie/beego/utils/captcha/LICENSE
generated
vendored
19
Godeps/_workspace/src/github.com/astaxie/beego/utils/captcha/LICENSE
generated
vendored
@@ -1,19 +0,0 @@
|
|||||||
Copyright (c) 2011-2014 Dmitry Chestnykh <dmitry@codingrobots.com>
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
25
Godeps/_workspace/src/github.com/docopt/docopt-go/.gitignore
generated
vendored
25
Godeps/_workspace/src/github.com/docopt/docopt-go/.gitignore
generated
vendored
@@ -1,25 +0,0 @@
|
|||||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
|
||||||
*.o
|
|
||||||
*.a
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Folders
|
|
||||||
_obj
|
|
||||||
_test
|
|
||||||
|
|
||||||
# Architecture specific extensions/prefixes
|
|
||||||
*.[568vq]
|
|
||||||
[568vq].out
|
|
||||||
|
|
||||||
*.cgo1.go
|
|
||||||
*.cgo2.c
|
|
||||||
_cgo_defun.c
|
|
||||||
_cgo_gotypes.go
|
|
||||||
_cgo_export.*
|
|
||||||
|
|
||||||
_testmain.go
|
|
||||||
|
|
||||||
*.exe
|
|
||||||
|
|
||||||
# coverage droppings
|
|
||||||
profile.cov
|
|
||||||
31
Godeps/_workspace/src/github.com/docopt/docopt-go/.travis.yml
generated
vendored
31
Godeps/_workspace/src/github.com/docopt/docopt-go/.travis.yml
generated
vendored
@@ -1,31 +0,0 @@
|
|||||||
# Travis CI (http://travis-ci.org/) is a continuous integration
|
|
||||||
# service for open source projects. This file configures it
|
|
||||||
# to run unit tests for docopt-go.
|
|
||||||
|
|
||||||
language: go
|
|
||||||
|
|
||||||
go:
|
|
||||||
- 1.4
|
|
||||||
- 1.5
|
|
||||||
- tip
|
|
||||||
|
|
||||||
matrix:
|
|
||||||
fast_finish: true
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- go get golang.org/x/tools/cmd/vet
|
|
||||||
- go get golang.org/x/tools/cmd/cover
|
|
||||||
- go get github.com/golang/lint/golint
|
|
||||||
- go get github.com/mattn/goveralls
|
|
||||||
|
|
||||||
install:
|
|
||||||
- go get -d -v ./... && go build -v ./...
|
|
||||||
|
|
||||||
script:
|
|
||||||
- go vet -x ./...
|
|
||||||
- $HOME/gopath/bin/golint ./...
|
|
||||||
- go test -v ./...
|
|
||||||
- go test -covermode=count -coverprofile=profile.cov .
|
|
||||||
|
|
||||||
after_script:
|
|
||||||
- $HOME/gopath/bin/goveralls -coverprofile=profile.cov -service=travis-ci
|
|
||||||
20
Godeps/_workspace/src/github.com/docopt/docopt-go/LICENSE
generated
vendored
20
Godeps/_workspace/src/github.com/docopt/docopt-go/LICENSE
generated
vendored
@@ -1,20 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2013 Keith Batten
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
|
||||||
the Software without restriction, including without limitation the rights to
|
|
||||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
88
Godeps/_workspace/src/github.com/docopt/docopt-go/README.md
generated
vendored
88
Godeps/_workspace/src/github.com/docopt/docopt-go/README.md
generated
vendored
@@ -1,88 +0,0 @@
|
|||||||
docopt-go
|
|
||||||
=========
|
|
||||||
|
|
||||||
[](https://travis-ci.org/docopt/docopt.go)
|
|
||||||
[](https://coveralls.io/r/docopt/docopt.go)
|
|
||||||
[](https://godoc.org/github.com/docopt/docopt.go)
|
|
||||||
|
|
||||||
An implementation of [docopt](http://docopt.org/) in the
|
|
||||||
[Go](http://golang.org/) programming language.
|
|
||||||
|
|
||||||
**docopt** helps you create *beautiful* command-line interfaces easily:
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/docopt/docopt-go"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
usage := `Naval Fate.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
naval_fate ship new <name>...
|
|
||||||
naval_fate ship <name> move <x> <y> [--speed=<kn>]
|
|
||||||
naval_fate ship shoot <x> <y>
|
|
||||||
naval_fate mine (set|remove) <x> <y> [--moored|--drifting]
|
|
||||||
naval_fate -h | --help
|
|
||||||
naval_fate --version
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-h --help Show this screen.
|
|
||||||
--version Show version.
|
|
||||||
--speed=<kn> Speed in knots [default: 10].
|
|
||||||
--moored Moored (anchored) mine.
|
|
||||||
--drifting Drifting mine.`
|
|
||||||
|
|
||||||
arguments, _ := docopt.Parse(usage, nil, true, "Naval Fate 2.0", false)
|
|
||||||
fmt.Println(arguments)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**docopt** parses command-line arguments based on a help message. Don't
|
|
||||||
write parser code: a good help message already has all the necessary
|
|
||||||
information in it.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
⚠ Use the alias “docopt-go”. To use docopt in your Go code:
|
|
||||||
|
|
||||||
```go
|
|
||||||
import "github.com/docopt/docopt-go"
|
|
||||||
```
|
|
||||||
|
|
||||||
To install docopt according to your `$GOPATH`:
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ go get github.com/docopt/docopt-go
|
|
||||||
```
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
```go
|
|
||||||
func Parse(doc string, argv []string, help bool, version string,
|
|
||||||
optionsFirst bool, exit ...bool) (map[string]interface{}, error)
|
|
||||||
```
|
|
||||||
Parse `argv` based on the command-line interface described in `doc`.
|
|
||||||
|
|
||||||
Given a conventional command-line help message, docopt creates a parser and
|
|
||||||
processes the arguments. See
|
|
||||||
https://github.com/docopt/docopt#help-message-format for a description of the
|
|
||||||
help message format. If `argv` is `nil`, `os.Args[1:]` is used.
|
|
||||||
|
|
||||||
docopt returns a map of option names to the values parsed from `argv`, and an
|
|
||||||
error or `nil`.
|
|
||||||
|
|
||||||
More documentation for docopt is available at
|
|
||||||
[GoDoc.org](https://godoc.org/github.com/docopt/docopt.go).
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
All tests from the Python version are implemented and passing
|
|
||||||
at [Travis CI](https://travis-ci.org/docopt/docopt.go). New
|
|
||||||
language-agnostic tests have been added
|
|
||||||
to [test_golang.docopt](test_golang.docopt).
|
|
||||||
|
|
||||||
To run tests for docopt-go, use `go test`.
|
|
||||||
1239
Godeps/_workspace/src/github.com/docopt/docopt-go/docopt.go
generated
vendored
1239
Godeps/_workspace/src/github.com/docopt/docopt-go/docopt.go
generated
vendored
File diff suppressed because it is too large
Load Diff
9
Godeps/_workspace/src/github.com/docopt/docopt-go/test_golang.docopt
generated
vendored
9
Godeps/_workspace/src/github.com/docopt/docopt-go/test_golang.docopt
generated
vendored
@@ -1,9 +0,0 @@
|
|||||||
r"""usage: prog [NAME_-2]..."""
|
|
||||||
$ prog 10 20
|
|
||||||
{"NAME_-2": ["10", "20"]}
|
|
||||||
|
|
||||||
$ prog 10
|
|
||||||
{"NAME_-2": ["10"]}
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
{"NAME_-2": []}
|
|
||||||
957
Godeps/_workspace/src/github.com/docopt/docopt-go/testcases.docopt
generated
vendored
957
Godeps/_workspace/src/github.com/docopt/docopt-go/testcases.docopt
generated
vendored
@@ -1,957 +0,0 @@
|
|||||||
r"""Usage: prog
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog
|
|
||||||
{}
|
|
||||||
|
|
||||||
$ prog --xxx
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
|
|
||||||
r"""Usage: prog [options]
|
|
||||||
|
|
||||||
Options: -a All.
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog
|
|
||||||
{"-a": false}
|
|
||||||
|
|
||||||
$ prog -a
|
|
||||||
{"-a": true}
|
|
||||||
|
|
||||||
$ prog -x
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
|
|
||||||
r"""Usage: prog [options]
|
|
||||||
|
|
||||||
Options: --all All.
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog
|
|
||||||
{"--all": false}
|
|
||||||
|
|
||||||
$ prog --all
|
|
||||||
{"--all": true}
|
|
||||||
|
|
||||||
$ prog --xxx
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
|
|
||||||
r"""Usage: prog [options]
|
|
||||||
|
|
||||||
Options: -v, --verbose Verbose.
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog --verbose
|
|
||||||
{"--verbose": true}
|
|
||||||
|
|
||||||
$ prog --ver
|
|
||||||
{"--verbose": true}
|
|
||||||
|
|
||||||
$ prog -v
|
|
||||||
{"--verbose": true}
|
|
||||||
|
|
||||||
|
|
||||||
r"""Usage: prog [options]
|
|
||||||
|
|
||||||
Options: -p PATH
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog -p home/
|
|
||||||
{"-p": "home/"}
|
|
||||||
|
|
||||||
$ prog -phome/
|
|
||||||
{"-p": "home/"}
|
|
||||||
|
|
||||||
$ prog -p
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
|
|
||||||
r"""Usage: prog [options]
|
|
||||||
|
|
||||||
Options: --path <path>
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog --path home/
|
|
||||||
{"--path": "home/"}
|
|
||||||
|
|
||||||
$ prog --path=home/
|
|
||||||
{"--path": "home/"}
|
|
||||||
|
|
||||||
$ prog --pa home/
|
|
||||||
{"--path": "home/"}
|
|
||||||
|
|
||||||
$ prog --pa=home/
|
|
||||||
{"--path": "home/"}
|
|
||||||
|
|
||||||
$ prog --path
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
|
|
||||||
r"""Usage: prog [options]
|
|
||||||
|
|
||||||
Options: -p PATH, --path=<path> Path to files.
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog -proot
|
|
||||||
{"--path": "root"}
|
|
||||||
|
|
||||||
|
|
||||||
r"""Usage: prog [options]
|
|
||||||
|
|
||||||
Options: -p --path PATH Path to files.
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog -p root
|
|
||||||
{"--path": "root"}
|
|
||||||
|
|
||||||
$ prog --path root
|
|
||||||
{"--path": "root"}
|
|
||||||
|
|
||||||
|
|
||||||
r"""Usage: prog [options]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-p PATH Path to files [default: ./]
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog
|
|
||||||
{"-p": "./"}
|
|
||||||
|
|
||||||
$ prog -phome
|
|
||||||
{"-p": "home"}
|
|
||||||
|
|
||||||
|
|
||||||
r"""UsAgE: prog [options]
|
|
||||||
|
|
||||||
OpTiOnS: --path=<files> Path to files
|
|
||||||
[dEfAuLt: /root]
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog
|
|
||||||
{"--path": "/root"}
|
|
||||||
|
|
||||||
$ prog --path=home
|
|
||||||
{"--path": "home"}
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog [options]
|
|
||||||
|
|
||||||
options:
|
|
||||||
-a Add
|
|
||||||
-r Remote
|
|
||||||
-m <msg> Message
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog -a -r -m Hello
|
|
||||||
{"-a": true,
|
|
||||||
"-r": true,
|
|
||||||
"-m": "Hello"}
|
|
||||||
|
|
||||||
$ prog -armyourass
|
|
||||||
{"-a": true,
|
|
||||||
"-r": true,
|
|
||||||
"-m": "yourass"}
|
|
||||||
|
|
||||||
$ prog -a -r
|
|
||||||
{"-a": true,
|
|
||||||
"-r": true,
|
|
||||||
"-m": null}
|
|
||||||
|
|
||||||
|
|
||||||
r"""Usage: prog [options]
|
|
||||||
|
|
||||||
Options: --version
|
|
||||||
--verbose
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog --version
|
|
||||||
{"--version": true,
|
|
||||||
"--verbose": false}
|
|
||||||
|
|
||||||
$ prog --verbose
|
|
||||||
{"--version": false,
|
|
||||||
"--verbose": true}
|
|
||||||
|
|
||||||
$ prog --ver
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
$ prog --verb
|
|
||||||
{"--version": false,
|
|
||||||
"--verbose": true}
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog [-a -r -m <msg>]
|
|
||||||
|
|
||||||
options:
|
|
||||||
-a Add
|
|
||||||
-r Remote
|
|
||||||
-m <msg> Message
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog -armyourass
|
|
||||||
{"-a": true,
|
|
||||||
"-r": true,
|
|
||||||
"-m": "yourass"}
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog [-armmsg]
|
|
||||||
|
|
||||||
options: -a Add
|
|
||||||
-r Remote
|
|
||||||
-m <msg> Message
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog -a -r -m Hello
|
|
||||||
{"-a": true,
|
|
||||||
"-r": true,
|
|
||||||
"-m": "Hello"}
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog -a -b
|
|
||||||
|
|
||||||
options:
|
|
||||||
-a
|
|
||||||
-b
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog -a -b
|
|
||||||
{"-a": true, "-b": true}
|
|
||||||
|
|
||||||
$ prog -b -a
|
|
||||||
{"-a": true, "-b": true}
|
|
||||||
|
|
||||||
$ prog -a
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog (-a -b)
|
|
||||||
|
|
||||||
options: -a
|
|
||||||
-b
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog -a -b
|
|
||||||
{"-a": true, "-b": true}
|
|
||||||
|
|
||||||
$ prog -b -a
|
|
||||||
{"-a": true, "-b": true}
|
|
||||||
|
|
||||||
$ prog -a
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog [-a] -b
|
|
||||||
|
|
||||||
options: -a
|
|
||||||
-b
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog -a -b
|
|
||||||
{"-a": true, "-b": true}
|
|
||||||
|
|
||||||
$ prog -b -a
|
|
||||||
{"-a": true, "-b": true}
|
|
||||||
|
|
||||||
$ prog -a
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
$ prog -b
|
|
||||||
{"-a": false, "-b": true}
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog [(-a -b)]
|
|
||||||
|
|
||||||
options: -a
|
|
||||||
-b
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog -a -b
|
|
||||||
{"-a": true, "-b": true}
|
|
||||||
|
|
||||||
$ prog -b -a
|
|
||||||
{"-a": true, "-b": true}
|
|
||||||
|
|
||||||
$ prog -a
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
$ prog -b
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
{"-a": false, "-b": false}
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog (-a|-b)
|
|
||||||
|
|
||||||
options: -a
|
|
||||||
-b
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog -a -b
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
$ prog -a
|
|
||||||
{"-a": true, "-b": false}
|
|
||||||
|
|
||||||
$ prog -b
|
|
||||||
{"-a": false, "-b": true}
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog [ -a | -b ]
|
|
||||||
|
|
||||||
options: -a
|
|
||||||
-b
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog -a -b
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
{"-a": false, "-b": false}
|
|
||||||
|
|
||||||
$ prog -a
|
|
||||||
{"-a": true, "-b": false}
|
|
||||||
|
|
||||||
$ prog -b
|
|
||||||
{"-a": false, "-b": true}
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog <arg>"""
|
|
||||||
$ prog 10
|
|
||||||
{"<arg>": "10"}
|
|
||||||
|
|
||||||
$ prog 10 20
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog [<arg>]"""
|
|
||||||
$ prog 10
|
|
||||||
{"<arg>": "10"}
|
|
||||||
|
|
||||||
$ prog 10 20
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
{"<arg>": null}
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog <kind> <name> <type>"""
|
|
||||||
$ prog 10 20 40
|
|
||||||
{"<kind>": "10", "<name>": "20", "<type>": "40"}
|
|
||||||
|
|
||||||
$ prog 10 20
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog <kind> [<name> <type>]"""
|
|
||||||
$ prog 10 20 40
|
|
||||||
{"<kind>": "10", "<name>": "20", "<type>": "40"}
|
|
||||||
|
|
||||||
$ prog 10 20
|
|
||||||
{"<kind>": "10", "<name>": "20", "<type>": null}
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog [<kind> | <name> <type>]"""
|
|
||||||
$ prog 10 20 40
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
$ prog 20 40
|
|
||||||
{"<kind>": null, "<name>": "20", "<type>": "40"}
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
{"<kind>": null, "<name>": null, "<type>": null}
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog (<kind> --all | <name>)
|
|
||||||
|
|
||||||
options:
|
|
||||||
--all
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog 10 --all
|
|
||||||
{"<kind>": "10", "--all": true, "<name>": null}
|
|
||||||
|
|
||||||
$ prog 10
|
|
||||||
{"<kind>": null, "--all": false, "<name>": "10"}
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog [<name> <name>]"""
|
|
||||||
$ prog 10 20
|
|
||||||
{"<name>": ["10", "20"]}
|
|
||||||
|
|
||||||
$ prog 10
|
|
||||||
{"<name>": ["10"]}
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
{"<name>": []}
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog [(<name> <name>)]"""
|
|
||||||
$ prog 10 20
|
|
||||||
{"<name>": ["10", "20"]}
|
|
||||||
|
|
||||||
$ prog 10
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
{"<name>": []}
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog NAME..."""
|
|
||||||
$ prog 10 20
|
|
||||||
{"NAME": ["10", "20"]}
|
|
||||||
|
|
||||||
$ prog 10
|
|
||||||
{"NAME": ["10"]}
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog [NAME]..."""
|
|
||||||
$ prog 10 20
|
|
||||||
{"NAME": ["10", "20"]}
|
|
||||||
|
|
||||||
$ prog 10
|
|
||||||
{"NAME": ["10"]}
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
{"NAME": []}
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog [NAME...]"""
|
|
||||||
$ prog 10 20
|
|
||||||
{"NAME": ["10", "20"]}
|
|
||||||
|
|
||||||
$ prog 10
|
|
||||||
{"NAME": ["10"]}
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
{"NAME": []}
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog [NAME [NAME ...]]"""
|
|
||||||
$ prog 10 20
|
|
||||||
{"NAME": ["10", "20"]}
|
|
||||||
|
|
||||||
$ prog 10
|
|
||||||
{"NAME": ["10"]}
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
{"NAME": []}
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog (NAME | --foo NAME)
|
|
||||||
|
|
||||||
options: --foo
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog 10
|
|
||||||
{"NAME": "10", "--foo": false}
|
|
||||||
|
|
||||||
$ prog --foo 10
|
|
||||||
{"NAME": "10", "--foo": true}
|
|
||||||
|
|
||||||
$ prog --foo=10
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog (NAME | --foo) [--bar | NAME]
|
|
||||||
|
|
||||||
options: --foo
|
|
||||||
options: --bar
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog 10
|
|
||||||
{"NAME": ["10"], "--foo": false, "--bar": false}
|
|
||||||
|
|
||||||
$ prog 10 20
|
|
||||||
{"NAME": ["10", "20"], "--foo": false, "--bar": false}
|
|
||||||
|
|
||||||
$ prog --foo --bar
|
|
||||||
{"NAME": [], "--foo": true, "--bar": true}
|
|
||||||
|
|
||||||
|
|
||||||
r"""Naval Fate.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
prog ship new <name>...
|
|
||||||
prog ship [<name>] move <x> <y> [--speed=<kn>]
|
|
||||||
prog ship shoot <x> <y>
|
|
||||||
prog mine (set|remove) <x> <y> [--moored|--drifting]
|
|
||||||
prog -h | --help
|
|
||||||
prog --version
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-h --help Show this screen.
|
|
||||||
--version Show version.
|
|
||||||
--speed=<kn> Speed in knots [default: 10].
|
|
||||||
--moored Mored (anchored) mine.
|
|
||||||
--drifting Drifting mine.
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog ship Guardian move 150 300 --speed=20
|
|
||||||
{"--drifting": false,
|
|
||||||
"--help": false,
|
|
||||||
"--moored": false,
|
|
||||||
"--speed": "20",
|
|
||||||
"--version": false,
|
|
||||||
"<name>": ["Guardian"],
|
|
||||||
"<x>": "150",
|
|
||||||
"<y>": "300",
|
|
||||||
"mine": false,
|
|
||||||
"move": true,
|
|
||||||
"new": false,
|
|
||||||
"remove": false,
|
|
||||||
"set": false,
|
|
||||||
"ship": true,
|
|
||||||
"shoot": false}
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog --hello"""
|
|
||||||
$ prog --hello
|
|
||||||
{"--hello": true}
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog [--hello=<world>]"""
|
|
||||||
$ prog
|
|
||||||
{"--hello": null}
|
|
||||||
|
|
||||||
$ prog --hello wrld
|
|
||||||
{"--hello": "wrld"}
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog [-o]"""
|
|
||||||
$ prog
|
|
||||||
{"-o": false}
|
|
||||||
|
|
||||||
$ prog -o
|
|
||||||
{"-o": true}
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog [-opr]"""
|
|
||||||
$ prog -op
|
|
||||||
{"-o": true, "-p": true, "-r": false}
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog --aabb | --aa"""
|
|
||||||
$ prog --aa
|
|
||||||
{"--aabb": false, "--aa": true}
|
|
||||||
|
|
||||||
$ prog --a
|
|
||||||
"user-error" # not a unique prefix
|
|
||||||
|
|
||||||
#
|
|
||||||
# Counting number of flags
|
|
||||||
#
|
|
||||||
|
|
||||||
r"""Usage: prog -v"""
|
|
||||||
$ prog -v
|
|
||||||
{"-v": true}
|
|
||||||
|
|
||||||
|
|
||||||
r"""Usage: prog [-v -v]"""
|
|
||||||
$ prog
|
|
||||||
{"-v": 0}
|
|
||||||
|
|
||||||
$ prog -v
|
|
||||||
{"-v": 1}
|
|
||||||
|
|
||||||
$ prog -vv
|
|
||||||
{"-v": 2}
|
|
||||||
|
|
||||||
|
|
||||||
r"""Usage: prog -v ..."""
|
|
||||||
$ prog
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
$ prog -v
|
|
||||||
{"-v": 1}
|
|
||||||
|
|
||||||
$ prog -vv
|
|
||||||
{"-v": 2}
|
|
||||||
|
|
||||||
$ prog -vvvvvv
|
|
||||||
{"-v": 6}
|
|
||||||
|
|
||||||
|
|
||||||
r"""Usage: prog [-v | -vv | -vvv]
|
|
||||||
|
|
||||||
This one is probably most readable user-friednly variant.
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog
|
|
||||||
{"-v": 0}
|
|
||||||
|
|
||||||
$ prog -v
|
|
||||||
{"-v": 1}
|
|
||||||
|
|
||||||
$ prog -vv
|
|
||||||
{"-v": 2}
|
|
||||||
|
|
||||||
$ prog -vvvv
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog [--ver --ver]"""
|
|
||||||
$ prog --ver --ver
|
|
||||||
{"--ver": 2}
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Counting commands
|
|
||||||
#
|
|
||||||
|
|
||||||
r"""usage: prog [go]"""
|
|
||||||
$ prog go
|
|
||||||
{"go": true}
|
|
||||||
|
|
||||||
|
|
||||||
r"""usage: prog [go go]"""
|
|
||||||
$ prog
|
|
||||||
{"go": 0}
|
|
||||||
|
|
||||||
$ prog go
|
|
||||||
{"go": 1}
|
|
||||||
|
|
||||||
$ prog go go
|
|
||||||
{"go": 2}
|
|
||||||
|
|
||||||
$ prog go go go
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
r"""usage: prog go..."""
|
|
||||||
$ prog go go go go go
|
|
||||||
{"go": 5}
|
|
||||||
|
|
||||||
#
|
|
||||||
# [options] does not include options from usage-pattern
|
|
||||||
#
|
|
||||||
r"""usage: prog [options] [-a]
|
|
||||||
|
|
||||||
options: -a
|
|
||||||
-b
|
|
||||||
"""
|
|
||||||
$ prog -a
|
|
||||||
{"-a": true, "-b": false}
|
|
||||||
|
|
||||||
$ prog -aa
|
|
||||||
"user-error"
|
|
||||||
|
|
||||||
#
|
|
||||||
# Test [options] shourtcut
|
|
||||||
#
|
|
||||||
|
|
||||||
r"""Usage: prog [options] A
|
|
||||||
Options:
|
|
||||||
-q Be quiet
|
|
||||||
-v Be verbose.
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog arg
|
|
||||||
{"A": "arg", "-v": false, "-q": false}
|
|
||||||
|
|
||||||
$ prog -v arg
|
|
||||||
{"A": "arg", "-v": true, "-q": false}
|
|
||||||
|
|
||||||
$ prog -q arg
|
|
||||||
{"A": "arg", "-v": false, "-q": true}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Test single dash
|
|
||||||
#
|
|
||||||
|
|
||||||
r"""usage: prog [-]"""
|
|
||||||
|
|
||||||
$ prog -
|
|
||||||
{"-": true}
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
{"-": false}
|
|
||||||
|
|
||||||
#
|
|
||||||
# If argument is repeated, its value should always be a list
|
|
||||||
#
|
|
||||||
|
|
||||||
r"""usage: prog [NAME [NAME ...]]"""
|
|
||||||
|
|
||||||
$ prog a b
|
|
||||||
{"NAME": ["a", "b"]}
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
{"NAME": []}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Option's argument defaults to null/None
|
|
||||||
#
|
|
||||||
|
|
||||||
r"""usage: prog [options]
|
|
||||||
options:
|
|
||||||
-a Add
|
|
||||||
-m <msg> Message
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog -a
|
|
||||||
{"-m": null, "-a": true}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Test options without description
|
|
||||||
#
|
|
||||||
|
|
||||||
r"""usage: prog --hello"""
|
|
||||||
$ prog --hello
|
|
||||||
{"--hello": true}
|
|
||||||
|
|
||||||
r"""usage: prog [--hello=<world>]"""
|
|
||||||
$ prog
|
|
||||||
{"--hello": null}
|
|
||||||
|
|
||||||
$ prog --hello wrld
|
|
||||||
{"--hello": "wrld"}
|
|
||||||
|
|
||||||
r"""usage: prog [-o]"""
|
|
||||||
$ prog
|
|
||||||
{"-o": false}
|
|
||||||
|
|
||||||
$ prog -o
|
|
||||||
{"-o": true}
|
|
||||||
|
|
||||||
r"""usage: prog [-opr]"""
|
|
||||||
$ prog -op
|
|
||||||
{"-o": true, "-p": true, "-r": false}
|
|
||||||
|
|
||||||
r"""usage: git [-v | --verbose]"""
|
|
||||||
$ prog -v
|
|
||||||
{"-v": true, "--verbose": false}
|
|
||||||
|
|
||||||
r"""usage: git remote [-v | --verbose]"""
|
|
||||||
$ prog remote -v
|
|
||||||
{"remote": true, "-v": true, "--verbose": false}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Test empty usage pattern
|
|
||||||
#
|
|
||||||
|
|
||||||
r"""usage: prog"""
|
|
||||||
$ prog
|
|
||||||
{}
|
|
||||||
|
|
||||||
r"""usage: prog
|
|
||||||
prog <a> <b>
|
|
||||||
"""
|
|
||||||
$ prog 1 2
|
|
||||||
{"<a>": "1", "<b>": "2"}
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
{"<a>": null, "<b>": null}
|
|
||||||
|
|
||||||
r"""usage: prog <a> <b>
|
|
||||||
prog
|
|
||||||
"""
|
|
||||||
$ prog
|
|
||||||
{"<a>": null, "<b>": null}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Option's argument should not capture default value from usage pattern
|
|
||||||
#
|
|
||||||
|
|
||||||
r"""usage: prog [--file=<f>]"""
|
|
||||||
$ prog
|
|
||||||
{"--file": null}
|
|
||||||
|
|
||||||
r"""usage: prog [--file=<f>]
|
|
||||||
|
|
||||||
options: --file <a>
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog
|
|
||||||
{"--file": null}
|
|
||||||
|
|
||||||
r"""Usage: prog [-a <host:port>]
|
|
||||||
|
|
||||||
Options: -a, --address <host:port> TCP address [default: localhost:6283].
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog
|
|
||||||
{"--address": "localhost:6283"}
|
|
||||||
|
|
||||||
#
|
|
||||||
# If option with argument could be repeated,
|
|
||||||
# its arguments should be accumulated into a list
|
|
||||||
#
|
|
||||||
|
|
||||||
r"""usage: prog --long=<arg> ..."""
|
|
||||||
|
|
||||||
$ prog --long one
|
|
||||||
{"--long": ["one"]}
|
|
||||||
|
|
||||||
$ prog --long one --long two
|
|
||||||
{"--long": ["one", "two"]}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Test multiple elements repeated at once
|
|
||||||
#
|
|
||||||
|
|
||||||
r"""usage: prog (go <direction> --speed=<km/h>)..."""
|
|
||||||
$ prog go left --speed=5 go right --speed=9
|
|
||||||
{"go": 2, "<direction>": ["left", "right"], "--speed": ["5", "9"]}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Required options should work with option shortcut
|
|
||||||
#
|
|
||||||
|
|
||||||
r"""usage: prog [options] -a
|
|
||||||
|
|
||||||
options: -a
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog -a
|
|
||||||
{"-a": true}
|
|
||||||
|
|
||||||
#
|
|
||||||
# If option could be repeated its defaults should be split into a list
|
|
||||||
#
|
|
||||||
|
|
||||||
r"""usage: prog [-o <o>]...
|
|
||||||
|
|
||||||
options: -o <o> [default: x]
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog -o this -o that
|
|
||||||
{"-o": ["this", "that"]}
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
{"-o": ["x"]}
|
|
||||||
|
|
||||||
r"""usage: prog [-o <o>]...
|
|
||||||
|
|
||||||
options: -o <o> [default: x y]
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog -o this
|
|
||||||
{"-o": ["this"]}
|
|
||||||
|
|
||||||
$ prog
|
|
||||||
{"-o": ["x", "y"]}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Test stacked option's argument
|
|
||||||
#
|
|
||||||
|
|
||||||
r"""usage: prog -pPATH
|
|
||||||
|
|
||||||
options: -p PATH
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog -pHOME
|
|
||||||
{"-p": "HOME"}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Issue 56: Repeated mutually exclusive args give nested lists sometimes
|
|
||||||
#
|
|
||||||
|
|
||||||
r"""Usage: foo (--xx=x|--yy=y)..."""
|
|
||||||
$ prog --xx=1 --yy=2
|
|
||||||
{"--xx": ["1"], "--yy": ["2"]}
|
|
||||||
|
|
||||||
#
|
|
||||||
# POSIXly correct tokenization
|
|
||||||
#
|
|
||||||
|
|
||||||
r"""usage: prog [<input file>]"""
|
|
||||||
$ prog f.txt
|
|
||||||
{"<input file>": "f.txt"}
|
|
||||||
|
|
||||||
r"""usage: prog [--input=<file name>]..."""
|
|
||||||
$ prog --input a.txt --input=b.txt
|
|
||||||
{"--input": ["a.txt", "b.txt"]}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Issue 85: `[options]` shourtcut with multiple subcommands
|
|
||||||
#
|
|
||||||
|
|
||||||
r"""usage: prog good [options]
|
|
||||||
prog fail [options]
|
|
||||||
|
|
||||||
options: --loglevel=N
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog fail --loglevel 5
|
|
||||||
{"--loglevel": "5", "fail": true, "good": false}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Usage-section syntax
|
|
||||||
#
|
|
||||||
|
|
||||||
r"""usage:prog --foo"""
|
|
||||||
$ prog --foo
|
|
||||||
{"--foo": true}
|
|
||||||
|
|
||||||
r"""PROGRAM USAGE: prog --foo"""
|
|
||||||
$ prog --foo
|
|
||||||
{"--foo": true}
|
|
||||||
|
|
||||||
r"""Usage: prog --foo
|
|
||||||
prog --bar
|
|
||||||
NOT PART OF SECTION"""
|
|
||||||
$ prog --foo
|
|
||||||
{"--foo": true, "--bar": false}
|
|
||||||
|
|
||||||
r"""Usage:
|
|
||||||
prog --foo
|
|
||||||
prog --bar
|
|
||||||
|
|
||||||
NOT PART OF SECTION"""
|
|
||||||
$ prog --foo
|
|
||||||
{"--foo": true, "--bar": false}
|
|
||||||
|
|
||||||
r"""Usage:
|
|
||||||
prog --foo
|
|
||||||
prog --bar
|
|
||||||
NOT PART OF SECTION"""
|
|
||||||
$ prog --foo
|
|
||||||
{"--foo": true, "--bar": false}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Options-section syntax
|
|
||||||
#
|
|
||||||
|
|
||||||
r"""Usage: prog [options]
|
|
||||||
|
|
||||||
global options: --foo
|
|
||||||
local options: --baz
|
|
||||||
--bar
|
|
||||||
other options:
|
|
||||||
--egg
|
|
||||||
--spam
|
|
||||||
-not-an-option-
|
|
||||||
|
|
||||||
"""
|
|
||||||
$ prog --baz --egg
|
|
||||||
{"--foo": false, "--baz": true, "--bar": false, "--egg": true, "--spam": false}
|
|
||||||
14
Godeps/_workspace/src/github.com/vaughan0/go-ini/LICENSE
generated
vendored
14
Godeps/_workspace/src/github.com/vaughan0/go-ini/LICENSE
generated
vendored
@@ -1,14 +0,0 @@
|
|||||||
Copyright (c) 2013 Vaughan Newton
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
||||||
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
|
||||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
|
||||||
persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
|
||||||
Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
|
||||||
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
||||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
70
Godeps/_workspace/src/github.com/vaughan0/go-ini/README.md
generated
vendored
70
Godeps/_workspace/src/github.com/vaughan0/go-ini/README.md
generated
vendored
@@ -1,70 +0,0 @@
|
|||||||
go-ini
|
|
||||||
======
|
|
||||||
|
|
||||||
INI parsing library for Go (golang).
|
|
||||||
|
|
||||||
View the API documentation [here](http://godoc.org/github.com/vaughan0/go-ini).
|
|
||||||
|
|
||||||
Usage
|
|
||||||
-----
|
|
||||||
|
|
||||||
Parse an INI file:
|
|
||||||
|
|
||||||
```go
|
|
||||||
import "github.com/vaughan0/go-ini"
|
|
||||||
|
|
||||||
file, err := ini.LoadFile("myfile.ini")
|
|
||||||
```
|
|
||||||
|
|
||||||
Get data from the parsed file:
|
|
||||||
|
|
||||||
```go
|
|
||||||
name, ok := file.Get("person", "name")
|
|
||||||
if !ok {
|
|
||||||
panic("'name' variable missing from 'person' section")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Iterate through values in a section:
|
|
||||||
|
|
||||||
```go
|
|
||||||
for key, value := range file["mysection"] {
|
|
||||||
fmt.Printf("%s => %s\n", key, value)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Iterate through sections in a file:
|
|
||||||
|
|
||||||
```go
|
|
||||||
for name, section := range file {
|
|
||||||
fmt.Printf("Section name: %s\n", name)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
File Format
|
|
||||||
-----------
|
|
||||||
|
|
||||||
INI files are parsed by go-ini line-by-line. Each line may be one of the following:
|
|
||||||
|
|
||||||
* A section definition: [section-name]
|
|
||||||
* A property: key = value
|
|
||||||
* A comment: #blahblah _or_ ;blahblah
|
|
||||||
* Blank. The line will be ignored.
|
|
||||||
|
|
||||||
Properties defined before any section headers are placed in the default section, which has
|
|
||||||
the empty string as it's key.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
# I am a comment
|
|
||||||
; So am I!
|
|
||||||
|
|
||||||
[apples]
|
|
||||||
colour = red or green
|
|
||||||
shape = applish
|
|
||||||
|
|
||||||
[oranges]
|
|
||||||
shape = square
|
|
||||||
colour = blue
|
|
||||||
```
|
|
||||||
123
Godeps/_workspace/src/github.com/vaughan0/go-ini/ini.go
generated
vendored
123
Godeps/_workspace/src/github.com/vaughan0/go-ini/ini.go
generated
vendored
@@ -1,123 +0,0 @@
|
|||||||
// Package ini provides functions for parsing INI configuration files.
|
|
||||||
package ini
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
sectionRegex = regexp.MustCompile(`^\[(.*)\]$`)
|
|
||||||
assignRegex = regexp.MustCompile(`^([^=]+)=(.*)$`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrSyntax is returned when there is a syntax error in an INI file.
|
|
||||||
type ErrSyntax struct {
|
|
||||||
Line int
|
|
||||||
Source string // The contents of the erroneous line, without leading or trailing whitespace
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ErrSyntax) Error() string {
|
|
||||||
return fmt.Sprintf("invalid INI syntax on line %d: %s", e.Line, e.Source)
|
|
||||||
}
|
|
||||||
|
|
||||||
// A File represents a parsed INI file.
|
|
||||||
type File map[string]Section
|
|
||||||
|
|
||||||
// A Section represents a single section of an INI file.
|
|
||||||
type Section map[string]string
|
|
||||||
|
|
||||||
// Returns a named Section. A Section will be created if one does not already exist for the given name.
|
|
||||||
func (f File) Section(name string) Section {
|
|
||||||
section := f[name]
|
|
||||||
if section == nil {
|
|
||||||
section = make(Section)
|
|
||||||
f[name] = section
|
|
||||||
}
|
|
||||||
return section
|
|
||||||
}
|
|
||||||
|
|
||||||
// Looks up a value for a key in a section and returns that value, along with a boolean result similar to a map lookup.
|
|
||||||
func (f File) Get(section, key string) (value string, ok bool) {
|
|
||||||
if s := f[section]; s != nil {
|
|
||||||
value, ok = s[key]
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads INI data from a reader and stores the data in the File.
|
|
||||||
func (f File) Load(in io.Reader) (err error) {
|
|
||||||
bufin, ok := in.(*bufio.Reader)
|
|
||||||
if !ok {
|
|
||||||
bufin = bufio.NewReader(in)
|
|
||||||
}
|
|
||||||
return parseFile(bufin, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads INI data from a named file and stores the data in the File.
|
|
||||||
func (f File) LoadFile(file string) (err error) {
|
|
||||||
in, err := os.Open(file)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer in.Close()
|
|
||||||
return f.Load(in)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseFile(in *bufio.Reader, file File) (err error) {
|
|
||||||
section := ""
|
|
||||||
lineNum := 0
|
|
||||||
for done := false; !done; {
|
|
||||||
var line string
|
|
||||||
if line, err = in.ReadString('\n'); err != nil {
|
|
||||||
if err == io.EOF {
|
|
||||||
done = true
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lineNum++
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if len(line) == 0 {
|
|
||||||
// Skip blank lines
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if line[0] == ';' || line[0] == '#' {
|
|
||||||
// Skip comments
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if groups := assignRegex.FindStringSubmatch(line); groups != nil {
|
|
||||||
key, val := groups[1], groups[2]
|
|
||||||
key, val = strings.TrimSpace(key), strings.TrimSpace(val)
|
|
||||||
file.Section(section)[key] = val
|
|
||||||
} else if groups := sectionRegex.FindStringSubmatch(line); groups != nil {
|
|
||||||
name := strings.TrimSpace(groups[1])
|
|
||||||
section = name
|
|
||||||
// Create the section if it does not exist
|
|
||||||
file.Section(section)
|
|
||||||
} else {
|
|
||||||
return ErrSyntax{lineNum, line}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads and returns a File from a reader.
|
|
||||||
func Load(in io.Reader) (File, error) {
|
|
||||||
file := make(File)
|
|
||||||
err := file.Load(in)
|
|
||||||
return file, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads and returns an INI File from a file on disk.
|
|
||||||
func LoadFile(filename string) (File, error) {
|
|
||||||
file := make(File)
|
|
||||||
err := file.LoadFile(filename)
|
|
||||||
return file, err
|
|
||||||
}
|
|
||||||
2
Godeps/_workspace/src/github.com/vaughan0/go-ini/test.ini
generated
vendored
2
Godeps/_workspace/src/github.com/vaughan0/go-ini/test.ini
generated
vendored
@@ -1,2 +0,0 @@
|
|||||||
[default]
|
|
||||||
stuff = things
|
|
||||||
76
Makefile
76
Makefile
@@ -1,21 +1,75 @@
|
|||||||
export PATH := $(GOPATH)/bin:$(PATH)
|
export PATH := $(PATH):`go env GOPATH`/bin
|
||||||
export NEW_GOPATH := $(shell pwd)
|
export GO111MODULE=on
|
||||||
|
LDFLAGS := -s -w
|
||||||
|
NOWEB_TAG = $(shell [ ! -d web/frps/dist ] || [ ! -d web/frpc/dist ] && echo ',noweb')
|
||||||
|
|
||||||
all: build
|
.PHONY: web frps-web frpc-web frps frpc
|
||||||
|
|
||||||
build: godep fmt frps frpc
|
all: env fmt web build
|
||||||
|
|
||||||
godep:
|
build: frps frpc
|
||||||
@go get github.com/tools/godep
|
|
||||||
|
env:
|
||||||
|
@go version
|
||||||
|
|
||||||
|
web: frps-web frpc-web
|
||||||
|
|
||||||
|
frps-web:
|
||||||
|
$(MAKE) -C web/frps build
|
||||||
|
|
||||||
|
frpc-web:
|
||||||
|
$(MAKE) -C web/frpc build
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
GOPATH=$(NEW_GOPATH) godep go fmt ./...
|
go fmt ./...
|
||||||
|
|
||||||
|
fmt-more:
|
||||||
|
gofumpt -l -w .
|
||||||
|
|
||||||
|
gci:
|
||||||
|
gci write -s standard -s default -s "prefix(github.com/fatedier/frp/)" ./
|
||||||
|
|
||||||
|
vet:
|
||||||
|
go vet -tags "$(NOWEB_TAG)" ./...
|
||||||
|
|
||||||
frps:
|
frps:
|
||||||
GOPATH=$(NEW_GOPATH) godep go build -o bin/frps ./src/frp/cmd/frps
|
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags "frps$(NOWEB_TAG)" -o bin/frps ./cmd/frps
|
||||||
|
|
||||||
frpc:
|
frpc:
|
||||||
GOPATH=$(NEW_GOPATH) godep go build -o bin/frpc ./src/frp/cmd/frpc
|
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags "frpc$(NOWEB_TAG)" -o bin/frpc ./cmd/frpc
|
||||||
|
|
||||||
test:
|
test: gotest
|
||||||
@GOPATH=$(NEW_GOPATH) godep go test -v ./...
|
|
||||||
|
gotest:
|
||||||
|
go test -tags "$(NOWEB_TAG)" -v --cover ./assets/...
|
||||||
|
go test -tags "$(NOWEB_TAG)" -v --cover ./cmd/...
|
||||||
|
go test -tags "$(NOWEB_TAG)" -v --cover ./client/...
|
||||||
|
go test -tags "$(NOWEB_TAG)" -v --cover ./server/...
|
||||||
|
go test -tags "$(NOWEB_TAG)" -v --cover ./pkg/...
|
||||||
|
|
||||||
|
e2e:
|
||||||
|
./hack/run-e2e.sh
|
||||||
|
|
||||||
|
e2e-trace:
|
||||||
|
DEBUG=true LOG_LEVEL=trace ./hack/run-e2e.sh
|
||||||
|
|
||||||
|
e2e-compatibility-last-frpc:
|
||||||
|
if [ ! -d "./lastversion" ]; then \
|
||||||
|
TARGET_DIRNAME=lastversion ./hack/download.sh; \
|
||||||
|
fi
|
||||||
|
FRPC_PATH="`pwd`/lastversion/frpc" ./hack/run-e2e.sh
|
||||||
|
rm -r ./lastversion
|
||||||
|
|
||||||
|
e2e-compatibility-last-frps:
|
||||||
|
if [ ! -d "./lastversion" ]; then \
|
||||||
|
TARGET_DIRNAME=lastversion ./hack/download.sh; \
|
||||||
|
fi
|
||||||
|
FRPS_PATH="`pwd`/lastversion/frps" ./hack/run-e2e.sh
|
||||||
|
rm -r ./lastversion
|
||||||
|
|
||||||
|
alltest: vet gotest e2e
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f ./bin/frpc
|
||||||
|
rm -f ./bin/frps
|
||||||
|
rm -rf ./lastversion
|
||||||
|
|||||||
37
Makefile.cross-compiles
Normal file
37
Makefile.cross-compiles
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
export PATH := $(PATH):`go env GOPATH`/bin
|
||||||
|
export GO111MODULE=on
|
||||||
|
LDFLAGS := -s -w
|
||||||
|
|
||||||
|
os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 openbsd:amd64 linux:amd64 linux:arm:7 linux:arm:5 linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 linux:loong64 android:arm64
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
build: app
|
||||||
|
|
||||||
|
app:
|
||||||
|
@$(foreach n, $(os-archs), \
|
||||||
|
os=$(shell echo "$(n)" | cut -d : -f 1); \
|
||||||
|
arch=$(shell echo "$(n)" | cut -d : -f 2); \
|
||||||
|
extra=$(shell echo "$(n)" | cut -d : -f 3); \
|
||||||
|
flags=''; \
|
||||||
|
target_suffix=$${os}_$${arch}; \
|
||||||
|
if [ "$${os}" = "linux" ] && [ "$${arch}" = "arm" ] && [ "$${extra}" != "" ] ; then \
|
||||||
|
if [ "$${extra}" = "7" ]; then \
|
||||||
|
flags=GOARM=7; \
|
||||||
|
target_suffix=$${os}_arm_hf; \
|
||||||
|
elif [ "$${extra}" = "5" ]; then \
|
||||||
|
flags=GOARM=5; \
|
||||||
|
target_suffix=$${os}_arm; \
|
||||||
|
fi; \
|
||||||
|
elif [ "$${os}" = "linux" ] && ([ "$${arch}" = "mips" ] || [ "$${arch}" = "mipsle" ]) && [ "$${extra}" != "" ] ; then \
|
||||||
|
flags=GOMIPS=$${extra}; \
|
||||||
|
fi; \
|
||||||
|
echo "Build $${os}-$${arch}$${extra:+ ($${extra})}..."; \
|
||||||
|
env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} $${flags} go build -trimpath -ldflags "$(LDFLAGS)" -tags frpc -o ./release/frpc_$${target_suffix} ./cmd/frpc; \
|
||||||
|
env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} $${flags} go build -trimpath -ldflags "$(LDFLAGS)" -tags frps -o ./release/frps_$${target_suffix} ./cmd/frps; \
|
||||||
|
echo "Build $${os}-$${arch}$${extra:+ ($${extra})} done"; \
|
||||||
|
)
|
||||||
|
@mv ./release/frpc_windows_amd64 ./release/frpc_windows_amd64.exe
|
||||||
|
@mv ./release/frps_windows_amd64 ./release/frps_windows_amd64.exe
|
||||||
|
@mv ./release/frpc_windows_arm64 ./release/frpc_windows_arm64.exe
|
||||||
|
@mv ./release/frps_windows_arm64 ./release/frps_windows_arm64.exe
|
||||||
126
README_zh.md
126
README_zh.md
@@ -1,40 +1,126 @@
|
|||||||
# frp
|
# frp
|
||||||
|
|
||||||
[](https://travis-ci.org/fatedier/frp)
|
[](https://circleci.com/gh/fatedier/frp)
|
||||||
|
[](https://github.com/fatedier/frp/releases)
|
||||||
|
[](https://goreportcard.com/report/github.com/fatedier/frp)
|
||||||
|
[](https://somsubhra.github.io/github-release-stats/?username=fatedier&repository=frp)
|
||||||
|
|
||||||
[README](README.md) | [中文文档](README_zh.md)
|
[README](README.md) | [中文文档](README_zh.md)
|
||||||
|
|
||||||
>frp 是一个高性能的反向代理应用,可以帮助你轻松的进行内网穿透,对外网提供服务。
|
frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP、UDP、HTTP、HTTPS 等多种协议,且支持 P2P 通信。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。
|
||||||
|
|
||||||
|
## Sponsors
|
||||||
|
|
||||||
|
frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者们的支持。如果你愿意加入他们的行列,请考虑 [赞助 frp 的开发](https://github.com/sponsors/fatedier)。
|
||||||
|
|
||||||
|
<h3 align="center">Gold Sponsors</h3>
|
||||||
|
<!--gold sponsors start-->
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/beclab/Olares" target="_blank">
|
||||||
|
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
|
||||||
|
<br>
|
||||||
|
<b>The sovereign cloud that puts you in control</b>
|
||||||
|
<br>
|
||||||
|
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
## Recall.ai - API for meeting recordings
|
||||||
|
|
||||||
|
If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp),
|
||||||
|
|
||||||
|
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
||||||
|
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
||||||
|
<br>
|
||||||
|
<b>Requestly - Free & Open-Source alternative to Postman</b>
|
||||||
|
<br>
|
||||||
|
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://jb.gg/frp" target="_blank">
|
||||||
|
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
|
||||||
|
<br>
|
||||||
|
<b>The complete IDE crafted for professional Go developers</b>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<!--gold sponsors end-->
|
||||||
|
|
||||||
|
## 为什么使用 frp ?
|
||||||
|
|
||||||
|
通过在具有公网 IP 的节点上部署 frp 服务端,可以轻松地将内网服务穿透到公网,同时提供诸多专业的功能特性,这包括:
|
||||||
|
|
||||||
|
* 客户端服务端通信支持 TCP、QUIC、KCP 以及 Websocket 等多种协议。
|
||||||
|
* 采用 TCP 连接流式复用,在单个连接间承载更多请求,节省连接建立时间,降低请求延迟。
|
||||||
|
* 代理组间的负载均衡。
|
||||||
|
* 端口复用,多个服务通过同一个服务端端口暴露。
|
||||||
|
* 支持 P2P 通信,流量不经过服务器中转,充分利用带宽资源。
|
||||||
|
* 多个原生支持的客户端插件(静态文件查看,HTTPS/HTTP 协议转换,HTTP、SOCK5 代理等),便于独立使用 frp 客户端完成某些工作。
|
||||||
|
* 高度扩展性的服务端插件系统,易于结合自身需求进行功能扩展。
|
||||||
|
* 服务端和客户端 UI 页面。
|
||||||
|
|
||||||
## 开发状态
|
## 开发状态
|
||||||
|
|
||||||
frp 目前正在前期开发阶段,master分支用于发布稳定版本,dev分支用于开发,您可以尝试下载最新的 release 版本进行测试。
|
frp 目前已被很多公司广泛用于测试、生产环境。
|
||||||
|
|
||||||
**在 1.x 版本以前,交互协议都可能会被改变,不能保证向后兼容。**
|
master 分支用于发布稳定版本,dev 分支用于开发,您可以尝试下载最新的 release 版本进行测试。
|
||||||
|
|
||||||
## 快速开始
|
我们正在进行 v2 大版本的开发,将会尝试在各个方面进行重构和升级,且不会与 v1 版本进行兼容,预计会持续较长的一段时间。
|
||||||
|
|
||||||
[QuickStart](doc/quick_start_en.md) | [使用文档](doc/quick_start_zh.md)
|
现在的 v0 版本将会在合适的时间切换为 v1 版本并且保证兼容性,后续只做 bug 修复和优化,不再进行大的功能性更新。
|
||||||
|
|
||||||
## 架构
|
### 关于 v2 的一些说明
|
||||||
|
|
||||||

|
v2 版本的复杂度和难度比我们预期的要高得多。我只能利用零散的时间进行开发,而且由于上下文经常被打断,效率极低。由于这种情况可能会持续一段时间,我们仍然会在当前版本上进行一些优化和迭代,直到我们有更多空闲时间来推进大版本的重构,或者也有可能放弃一次性的重构,而是采用渐进的方式在当前版本上逐步做一些可能会导致不兼容的修改。
|
||||||
|
|
||||||
## frp 的作用?
|
v2 的构想是基于我多年在云原生领域,特别是在 K8s 和 ServiceMesh 方面的工作经验和思考。它的核心是一个现代化的四层和七层代理,类似于 envoy。这个代理本身高度可扩展,不仅可以用于实现内网穿透的功能,还可以应用于更多领域。在这个高度可扩展的内核基础上,我们将实现 frp v1 中的所有功能,并且能够以一种更加优雅的方式实现原先架构中无法实现或不易实现的功能。同时,我们将保持高效的开发和迭代能力。
|
||||||
|
|
||||||
* 利用处于内网或防火墙后的机器,对外网环境提供http服务。(针对http的优化正在开发中)
|
除此之外,我希望 frp 本身也成为一个高度可扩展的系统和平台,就像我们可以基于 K8s 提供一系列扩展能力一样。在 K8s 上,我们可以根据企业需求进行定制化开发,例如使用 CRD、controller 模式、webhook、CSI 和 CNI 等。在 frp v1 中,我们引入了服务端插件的概念,实现了一些简单的扩展性。但是,它实际上依赖于简单的 HTTP 协议,并且需要用户自己启动独立的进程和管理。这种方式远远不够灵活和方便,而且现实世界的需求千差万别,我们不能期望一个由少数人维护的非营利性开源项目能够满足所有人的需求。
|
||||||
* 利用处于内网或防火墙后的机器,对外网环境提供tcp服务。
|
|
||||||
* 可查看通过代理的所有http请求和响应信息。(待开发)
|
|
||||||
|
|
||||||
## 贡献代码
|
最后,我们意识到像配置管理、权限验证、证书管理和管理 API 等模块的当前设计并不够现代化。尽管我们可能在 v1 版本中进行一些优化,但确保兼容性是一个令人头疼的问题,需要投入大量精力来解决。
|
||||||
|
|
||||||
如果您对这个项目感兴趣,并且想要参与其中,我们非常欢迎!
|
非常感谢您对 frp 的支持。
|
||||||
|
|
||||||
* 如果您需要提交问题,可以通过 [issues](https://github.com/fatedier/frp/issues) 来完成。
|
## 文档
|
||||||
* 如果您有新的功能需求,可以反馈至 fatedier@gmail.com 共同讨论。
|
|
||||||
|
|
||||||
## 贡献者
|
完整文档已经迁移至 [https://gofrp.org](https://gofrp.org)。
|
||||||
|
|
||||||
* [fatedier](https://github.com/fatedier)
|
## 为 frp 做贡献
|
||||||
* [Hurricanezwf](https://github.com/Hurricanezwf)
|
|
||||||
* [vashstorm](https://github.com/vashstorm)
|
frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进步贡献力量。
|
||||||
|
|
||||||
|
* 在使用过程中出现任何问题,可以通过 [issues](https://github.com/fatedier/frp/issues) 来反馈。
|
||||||
|
* Bug 的修复可以直接提交 Pull Request 到 dev 分支。
|
||||||
|
* 如果是增加新的功能特性,请先创建一个 issue 并做简单描述以及大致的实现方法,提议被采纳后,就可以创建一个实现新特性的 Pull Request。
|
||||||
|
* 欢迎对说明文档做出改善,帮助更多的人使用 frp,特别是英文文档。
|
||||||
|
* 贡献代码请提交 PR 至 dev 分支,master 分支仅用于发布稳定可用版本。
|
||||||
|
* 如果你有任何其他方面的问题或合作,欢迎发送邮件至 fatedier@gmail.com 。
|
||||||
|
|
||||||
|
**提醒:和项目相关的问题请在 [issues](https://github.com/fatedier/frp/issues) 中反馈,这样方便其他有类似问题的人可以快速查找解决方法,并且也避免了我们重复回答一些问题。**
|
||||||
|
|
||||||
|
## 关联项目
|
||||||
|
|
||||||
|
* [gofrp/plugin](https://github.com/gofrp/plugin) - frp 插件仓库,收录了基于 frp 扩展机制实现的各种插件,满足各种场景下的定制化需求。
|
||||||
|
* [gofrp/tiny-frpc](https://github.com/gofrp/tiny-frpc) - 基于 ssh 协议实现的 frp 客户端的精简版本(最低约 3.5MB 左右),支持常用的部分功能,适用于资源有限的设备。
|
||||||
|
|
||||||
|
## 赞助
|
||||||
|
|
||||||
|
如果您觉得 frp 对你有帮助,欢迎给予我们一定的捐助来维持项目的长期发展。
|
||||||
|
|
||||||
|
### Sponsors
|
||||||
|
|
||||||
|
长期赞助可以帮助我们保持项目的持续发展。
|
||||||
|
|
||||||
|
您可以通过 [GitHub Sponsors](https://github.com/sponsors/fatedier) 赞助我们。
|
||||||
|
|
||||||
|
国内用户可以通过 [爱发电](https://afdian.com/a/fatedier) 赞助我们。
|
||||||
|
|
||||||
|
企业赞助者可以将贵公司的 Logo 以及链接放置在项目 README 文件中。
|
||||||
|
|||||||
1
Release.md
Normal file
1
Release.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
## Features
|
||||||
57
assets/assets.go
Normal file
57
assets/assets.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// Copyright 2016 fatedier, fatedier@gmail.com
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package assets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// read-only filesystem created by "embed" for embedded files
|
||||||
|
content fs.FS
|
||||||
|
|
||||||
|
FileSystem http.FileSystem
|
||||||
|
|
||||||
|
// if prefix is not empty, we get file content from disk
|
||||||
|
prefixPath string
|
||||||
|
)
|
||||||
|
|
||||||
|
type emptyFS struct{}
|
||||||
|
|
||||||
|
func (emptyFS) Open(name string) (http.File, error) {
|
||||||
|
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if path is empty, load assets in memory
|
||||||
|
// or set FileSystem using disk files
|
||||||
|
func Load(path string) {
|
||||||
|
prefixPath = path
|
||||||
|
switch {
|
||||||
|
case prefixPath != "":
|
||||||
|
FileSystem = http.Dir(prefixPath)
|
||||||
|
case content != nil:
|
||||||
|
FileSystem = http.FS(content)
|
||||||
|
default:
|
||||||
|
FileSystem = emptyFS{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Register(fileSystem fs.FS) {
|
||||||
|
subFs, err := fs.Sub(fileSystem, "dist")
|
||||||
|
if err == nil {
|
||||||
|
content = subFs
|
||||||
|
}
|
||||||
|
}
|
||||||
87
client/api_router.go
Normal file
87
client/api_router.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// Copyright 2017 fatedier, fatedier@gmail.com
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
adminapi "github.com/fatedier/frp/client/http"
|
||||||
|
"github.com/fatedier/frp/client/proxy"
|
||||||
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
|
||||||
|
apiController := newAPIController(svr)
|
||||||
|
|
||||||
|
// Healthz endpoint without auth
|
||||||
|
helper.Router.HandleFunc("/healthz", healthz)
|
||||||
|
|
||||||
|
// API routes and static files with auth
|
||||||
|
subRouter := helper.Router.NewRoute().Subrouter()
|
||||||
|
subRouter.Use(helper.AuthMiddleware)
|
||||||
|
subRouter.Use(httppkg.NewRequestLogger)
|
||||||
|
subRouter.HandleFunc("/api/reload", httppkg.MakeHTTPHandlerFunc(apiController.Reload)).Methods(http.MethodGet)
|
||||||
|
subRouter.HandleFunc("/api/stop", httppkg.MakeHTTPHandlerFunc(apiController.Stop)).Methods(http.MethodPost)
|
||||||
|
subRouter.HandleFunc("/api/status", httppkg.MakeHTTPHandlerFunc(apiController.Status)).Methods(http.MethodGet)
|
||||||
|
subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.GetConfig)).Methods(http.MethodGet)
|
||||||
|
subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.PutConfig)).Methods(http.MethodPut)
|
||||||
|
subRouter.HandleFunc("/api/proxy/{name}/config", httppkg.MakeHTTPHandlerFunc(apiController.GetProxyConfig)).Methods(http.MethodGet)
|
||||||
|
subRouter.HandleFunc("/api/visitor/{name}/config", httppkg.MakeHTTPHandlerFunc(apiController.GetVisitorConfig)).Methods(http.MethodGet)
|
||||||
|
|
||||||
|
if svr.storeSource != nil {
|
||||||
|
subRouter.HandleFunc("/api/store/proxies", httppkg.MakeHTTPHandlerFunc(apiController.ListStoreProxies)).Methods(http.MethodGet)
|
||||||
|
subRouter.HandleFunc("/api/store/proxies", httppkg.MakeHTTPHandlerFunc(apiController.CreateStoreProxy)).Methods(http.MethodPost)
|
||||||
|
subRouter.HandleFunc("/api/store/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.GetStoreProxy)).Methods(http.MethodGet)
|
||||||
|
subRouter.HandleFunc("/api/store/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.UpdateStoreProxy)).Methods(http.MethodPut)
|
||||||
|
subRouter.HandleFunc("/api/store/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.DeleteStoreProxy)).Methods(http.MethodDelete)
|
||||||
|
subRouter.HandleFunc("/api/store/visitors", httppkg.MakeHTTPHandlerFunc(apiController.ListStoreVisitors)).Methods(http.MethodGet)
|
||||||
|
subRouter.HandleFunc("/api/store/visitors", httppkg.MakeHTTPHandlerFunc(apiController.CreateStoreVisitor)).Methods(http.MethodPost)
|
||||||
|
subRouter.HandleFunc("/api/store/visitors/{name}", httppkg.MakeHTTPHandlerFunc(apiController.GetStoreVisitor)).Methods(http.MethodGet)
|
||||||
|
subRouter.HandleFunc("/api/store/visitors/{name}", httppkg.MakeHTTPHandlerFunc(apiController.UpdateStoreVisitor)).Methods(http.MethodPut)
|
||||||
|
subRouter.HandleFunc("/api/store/visitors/{name}", httppkg.MakeHTTPHandlerFunc(apiController.DeleteStoreVisitor)).Methods(http.MethodDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
|
||||||
|
subRouter.PathPrefix("/static/").Handler(
|
||||||
|
netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
|
||||||
|
).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func healthz(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAPIController(svr *Service) *adminapi.Controller {
|
||||||
|
manager := newServiceConfigManager(svr)
|
||||||
|
return adminapi.NewController(adminapi.ControllerParams{
|
||||||
|
ServerAddr: svr.common.ServerAddr,
|
||||||
|
Manager: manager,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAllProxyStatus returns all proxy statuses.
|
||||||
|
func (svr *Service) getAllProxyStatus() []*proxy.WorkingStatus {
|
||||||
|
svr.ctlMu.RLock()
|
||||||
|
ctl := svr.ctl
|
||||||
|
svr.ctlMu.RUnlock()
|
||||||
|
if ctl == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ctl.pm.GetAllProxyStatus()
|
||||||
|
}
|
||||||
464
client/config_manager.go
Normal file
464
client/config_manager.go
Normal file
@@ -0,0 +1,464 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/client/configmgmt"
|
||||||
|
"github.com/fatedier/frp/client/proxy"
|
||||||
|
"github.com/fatedier/frp/pkg/config"
|
||||||
|
"github.com/fatedier/frp/pkg/config/source"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type serviceConfigManager struct {
|
||||||
|
svr *Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func newServiceConfigManager(svr *Service) configmgmt.ConfigManager {
|
||||||
|
return &serviceConfigManager{svr: svr}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) ReloadFromFile(strict bool) error {
|
||||||
|
if m.svr.configFilePath == "" {
|
||||||
|
return fmt.Errorf("%w: frpc has no config file path", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := config.LoadClientConfigResult(m.svr.configFilePath, strict)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", configmgmt.ErrInvalidArgument, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyCfgsForValidation, visitorCfgsForValidation := config.FilterClientConfigurers(
|
||||||
|
result.Common,
|
||||||
|
result.Proxies,
|
||||||
|
result.Visitors,
|
||||||
|
)
|
||||||
|
proxyCfgsForValidation = config.CompleteProxyConfigurers(proxyCfgsForValidation)
|
||||||
|
visitorCfgsForValidation = config.CompleteVisitorConfigurers(visitorCfgsForValidation)
|
||||||
|
|
||||||
|
if _, err := validation.ValidateAllClientConfig(result.Common, proxyCfgsForValidation, visitorCfgsForValidation, m.svr.unsafeFeatures); err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", configmgmt.ErrInvalidArgument, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.svr.UpdateConfigSource(result.Common, result.Proxies, result.Visitors); err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", configmgmt.ErrApplyConfig, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("success reload conf")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) ReadConfigFile() (string, error) {
|
||||||
|
if m.svr.configFilePath == "" {
|
||||||
|
return "", fmt.Errorf("%w: frpc has no config file path", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(m.svr.configFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("%w: %v", configmgmt.ErrInvalidArgument, err)
|
||||||
|
}
|
||||||
|
return string(content), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) WriteConfigFile(content []byte) error {
|
||||||
|
if len(content) == 0 {
|
||||||
|
return fmt.Errorf("%w: body can't be empty", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(m.svr.configFilePath, content, 0o600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) GetProxyStatus() []*proxy.WorkingStatus {
|
||||||
|
return m.svr.getAllProxyStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) GetProxyConfig(name string) (v1.ProxyConfigurer, bool) {
|
||||||
|
// Try running proxy manager first
|
||||||
|
ws, ok := m.svr.getProxyStatus(name)
|
||||||
|
if ok {
|
||||||
|
return ws.Cfg, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to store
|
||||||
|
m.svr.reloadMu.Lock()
|
||||||
|
storeSource := m.svr.storeSource
|
||||||
|
m.svr.reloadMu.Unlock()
|
||||||
|
|
||||||
|
if storeSource != nil {
|
||||||
|
cfg := storeSource.GetProxy(name)
|
||||||
|
if cfg != nil {
|
||||||
|
return cfg, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) GetVisitorConfig(name string) (v1.VisitorConfigurer, bool) {
|
||||||
|
// Try running visitor manager first
|
||||||
|
cfg, ok := m.svr.getVisitorCfg(name)
|
||||||
|
if ok {
|
||||||
|
return cfg, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to store
|
||||||
|
m.svr.reloadMu.Lock()
|
||||||
|
storeSource := m.svr.storeSource
|
||||||
|
m.svr.reloadMu.Unlock()
|
||||||
|
|
||||||
|
if storeSource != nil {
|
||||||
|
vcfg := storeSource.GetVisitor(name)
|
||||||
|
if vcfg != nil {
|
||||||
|
return vcfg, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) IsStoreProxyEnabled(name string) bool {
|
||||||
|
if name == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
m.svr.reloadMu.Lock()
|
||||||
|
storeSource := m.svr.storeSource
|
||||||
|
m.svr.reloadMu.Unlock()
|
||||||
|
|
||||||
|
if storeSource == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := storeSource.GetProxy(name)
|
||||||
|
if cfg == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
enabled := cfg.GetBaseConfig().Enabled
|
||||||
|
return enabled == nil || *enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) StoreEnabled() bool {
|
||||||
|
m.svr.reloadMu.Lock()
|
||||||
|
storeSource := m.svr.storeSource
|
||||||
|
m.svr.reloadMu.Unlock()
|
||||||
|
return storeSource != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) ListStoreProxies() ([]v1.ProxyConfigurer, error) {
|
||||||
|
storeSource, err := m.storeSourceOrError()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return storeSource.GetAllProxies()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) GetStoreProxy(name string) (v1.ProxyConfigurer, error) {
|
||||||
|
if name == "" {
|
||||||
|
return nil, fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
storeSource, err := m.storeSourceOrError()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := storeSource.GetProxy(name)
|
||||||
|
if cfg == nil {
|
||||||
|
return nil, fmt.Errorf("%w: proxy %q", configmgmt.ErrNotFound, name)
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
if err := m.validateStoreProxyConfigurer(cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
name := cfg.GetBaseConfig().Name
|
||||||
|
persisted, err := m.withStoreProxyMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
||||||
|
if err := storeSource.AddProxy(cfg); err != nil {
|
||||||
|
if errors.Is(err, source.ErrAlreadyExists) {
|
||||||
|
return fmt.Errorf("%w: %v", configmgmt.ErrConflict, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
log.Infof("store: created proxy %q", name)
|
||||||
|
return persisted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
if name == "" {
|
||||||
|
return nil, fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
if cfg == nil {
|
||||||
|
return nil, fmt.Errorf("%w: invalid proxy config: type is required", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
bodyName := cfg.GetBaseConfig().Name
|
||||||
|
if bodyName != name {
|
||||||
|
return nil, fmt.Errorf("%w: proxy name in URL must match name in body", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
if err := m.validateStoreProxyConfigurer(cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
persisted, err := m.withStoreProxyMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
||||||
|
if err := storeSource.UpdateProxy(cfg); err != nil {
|
||||||
|
if errors.Is(err, source.ErrNotFound) {
|
||||||
|
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("store: updated proxy %q", name)
|
||||||
|
return persisted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) DeleteStoreProxy(name string) error {
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error {
|
||||||
|
if err := storeSource.RemoveProxy(name); err != nil {
|
||||||
|
if errors.Is(err, source.ErrNotFound) {
|
||||||
|
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("store: deleted proxy %q", name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) ListStoreVisitors() ([]v1.VisitorConfigurer, error) {
|
||||||
|
storeSource, err := m.storeSourceOrError()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return storeSource.GetAllVisitors()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) GetStoreVisitor(name string) (v1.VisitorConfigurer, error) {
|
||||||
|
if name == "" {
|
||||||
|
return nil, fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
storeSource, err := m.storeSourceOrError()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := storeSource.GetVisitor(name)
|
||||||
|
if cfg == nil {
|
||||||
|
return nil, fmt.Errorf("%w: visitor %q", configmgmt.ErrNotFound, name)
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
|
if err := m.validateStoreVisitorConfigurer(cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
name := cfg.GetBaseConfig().Name
|
||||||
|
persisted, err := m.withStoreVisitorMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
||||||
|
if err := storeSource.AddVisitor(cfg); err != nil {
|
||||||
|
if errors.Is(err, source.ErrAlreadyExists) {
|
||||||
|
return fmt.Errorf("%w: %v", configmgmt.ErrConflict, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("store: created visitor %q", name)
|
||||||
|
return persisted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
|
if name == "" {
|
||||||
|
return nil, fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
if cfg == nil {
|
||||||
|
return nil, fmt.Errorf("%w: invalid visitor config: type is required", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
bodyName := cfg.GetBaseConfig().Name
|
||||||
|
if bodyName != name {
|
||||||
|
return nil, fmt.Errorf("%w: visitor name in URL must match name in body", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
if err := m.validateStoreVisitorConfigurer(cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
persisted, err := m.withStoreVisitorMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
||||||
|
if err := storeSource.UpdateVisitor(cfg); err != nil {
|
||||||
|
if errors.Is(err, source.ErrNotFound) {
|
||||||
|
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("store: updated visitor %q", name)
|
||||||
|
return persisted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) DeleteStoreVisitor(name string) error {
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error {
|
||||||
|
if err := storeSource.RemoveVisitor(name); err != nil {
|
||||||
|
if errors.Is(err, source.ErrNotFound) {
|
||||||
|
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("store: deleted visitor %q", name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) GracefulClose(d time.Duration) {
|
||||||
|
m.svr.GracefulClose(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) storeSourceOrError() (*source.StoreSource, error) {
|
||||||
|
m.svr.reloadMu.Lock()
|
||||||
|
storeSource := m.svr.storeSource
|
||||||
|
m.svr.reloadMu.Unlock()
|
||||||
|
|
||||||
|
if storeSource == nil {
|
||||||
|
return nil, fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled)
|
||||||
|
}
|
||||||
|
return storeSource, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) withStoreMutationAndReload(
|
||||||
|
fn func(storeSource *source.StoreSource) error,
|
||||||
|
) error {
|
||||||
|
m.svr.reloadMu.Lock()
|
||||||
|
defer m.svr.reloadMu.Unlock()
|
||||||
|
|
||||||
|
storeSource := m.svr.storeSource
|
||||||
|
if storeSource == nil {
|
||||||
|
return fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fn(storeSource); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.svr.reloadConfigFromSourcesLocked(); err != nil {
|
||||||
|
return fmt.Errorf("%w: failed to apply config: %v", configmgmt.ErrApplyConfig, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) withStoreProxyMutationAndReload(
|
||||||
|
name string,
|
||||||
|
fn func(storeSource *source.StoreSource) error,
|
||||||
|
) (v1.ProxyConfigurer, error) {
|
||||||
|
m.svr.reloadMu.Lock()
|
||||||
|
defer m.svr.reloadMu.Unlock()
|
||||||
|
|
||||||
|
storeSource := m.svr.storeSource
|
||||||
|
if storeSource == nil {
|
||||||
|
return nil, fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fn(storeSource); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := m.svr.reloadConfigFromSourcesLocked(); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: failed to apply config: %v", configmgmt.ErrApplyConfig, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
persisted := storeSource.GetProxy(name)
|
||||||
|
if persisted == nil {
|
||||||
|
return nil, fmt.Errorf("%w: proxy %q not found in store after mutation", configmgmt.ErrApplyConfig, name)
|
||||||
|
}
|
||||||
|
return persisted.Clone(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) withStoreVisitorMutationAndReload(
|
||||||
|
name string,
|
||||||
|
fn func(storeSource *source.StoreSource) error,
|
||||||
|
) (v1.VisitorConfigurer, error) {
|
||||||
|
m.svr.reloadMu.Lock()
|
||||||
|
defer m.svr.reloadMu.Unlock()
|
||||||
|
|
||||||
|
storeSource := m.svr.storeSource
|
||||||
|
if storeSource == nil {
|
||||||
|
return nil, fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fn(storeSource); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := m.svr.reloadConfigFromSourcesLocked(); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: failed to apply config: %v", configmgmt.ErrApplyConfig, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
persisted := storeSource.GetVisitor(name)
|
||||||
|
if persisted == nil {
|
||||||
|
return nil, fmt.Errorf("%w: visitor %q not found in store after mutation", configmgmt.ErrApplyConfig, name)
|
||||||
|
}
|
||||||
|
return persisted.Clone(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) validateStoreProxyConfigurer(cfg v1.ProxyConfigurer) error {
|
||||||
|
if cfg == nil {
|
||||||
|
return fmt.Errorf("invalid proxy config")
|
||||||
|
}
|
||||||
|
runtimeCfg := cfg.Clone()
|
||||||
|
if runtimeCfg == nil {
|
||||||
|
return fmt.Errorf("invalid proxy config")
|
||||||
|
}
|
||||||
|
runtimeCfg.Complete()
|
||||||
|
return validation.ValidateProxyConfigurerForClient(runtimeCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *serviceConfigManager) validateStoreVisitorConfigurer(cfg v1.VisitorConfigurer) error {
|
||||||
|
if cfg == nil {
|
||||||
|
return fmt.Errorf("invalid visitor config")
|
||||||
|
}
|
||||||
|
runtimeCfg := cfg.Clone()
|
||||||
|
if runtimeCfg == nil {
|
||||||
|
return fmt.Errorf("invalid visitor config")
|
||||||
|
}
|
||||||
|
runtimeCfg.Complete()
|
||||||
|
return validation.ValidateVisitorConfigurer(runtimeCfg)
|
||||||
|
}
|
||||||
137
client/config_manager_test.go
Normal file
137
client/config_manager_test.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/client/configmgmt"
|
||||||
|
"github.com/fatedier/frp/pkg/config/source"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestRawTCPProxyConfig(name string) *v1.TCPProxyConfig {
|
||||||
|
return &v1.TCPProxyConfig{
|
||||||
|
ProxyBaseConfig: v1.ProxyBaseConfig{
|
||||||
|
Name: name,
|
||||||
|
Type: "tcp",
|
||||||
|
ProxyBackend: v1.ProxyBackend{
|
||||||
|
LocalPort: 10080,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServiceConfigManagerCreateStoreProxyConflict(t *testing.T) {
|
||||||
|
storeSource, err := source.NewStoreSource(source.StoreSourceConfig{
|
||||||
|
Path: filepath.Join(t.TempDir(), "store.json"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("new store source: %v", err)
|
||||||
|
}
|
||||||
|
if err := storeSource.AddProxy(newTestRawTCPProxyConfig("p1")); err != nil {
|
||||||
|
t.Fatalf("seed proxy: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
agg := source.NewAggregator(source.NewConfigSource())
|
||||||
|
agg.SetStoreSource(storeSource)
|
||||||
|
|
||||||
|
mgr := &serviceConfigManager{
|
||||||
|
svr: &Service{
|
||||||
|
aggregator: agg,
|
||||||
|
configSource: agg.ConfigSource(),
|
||||||
|
storeSource: storeSource,
|
||||||
|
reloadCommon: &v1.ClientCommonConfig{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected conflict error")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, configmgmt.ErrConflict) {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServiceConfigManagerCreateStoreProxyKeepsStoreOnReloadFailure(t *testing.T) {
|
||||||
|
storeSource, err := source.NewStoreSource(source.StoreSourceConfig{
|
||||||
|
Path: filepath.Join(t.TempDir(), "store.json"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("new store source: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr := &serviceConfigManager{
|
||||||
|
svr: &Service{
|
||||||
|
storeSource: storeSource,
|
||||||
|
reloadCommon: &v1.ClientCommonConfig{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected apply config error")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, configmgmt.ErrApplyConfig) {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if storeSource.GetProxy("p1") == nil {
|
||||||
|
t.Fatal("proxy should remain in store after reload failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServiceConfigManagerCreateStoreProxyStoreDisabled(t *testing.T) {
|
||||||
|
mgr := &serviceConfigManager{
|
||||||
|
svr: &Service{
|
||||||
|
reloadCommon: &v1.ClientCommonConfig{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected store disabled error")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, configmgmt.ErrStoreDisabled) {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServiceConfigManagerCreateStoreProxyDoesNotPersistRuntimeDefaults(t *testing.T) {
|
||||||
|
storeSource, err := source.NewStoreSource(source.StoreSourceConfig{
|
||||||
|
Path: filepath.Join(t.TempDir(), "store.json"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("new store source: %v", err)
|
||||||
|
}
|
||||||
|
agg := source.NewAggregator(source.NewConfigSource())
|
||||||
|
agg.SetStoreSource(storeSource)
|
||||||
|
|
||||||
|
mgr := &serviceConfigManager{
|
||||||
|
svr: &Service{
|
||||||
|
aggregator: agg,
|
||||||
|
configSource: agg.ConfigSource(),
|
||||||
|
storeSource: storeSource,
|
||||||
|
reloadCommon: &v1.ClientCommonConfig{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
persisted, err := mgr.CreateStoreProxy(newTestRawTCPProxyConfig("raw-proxy"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create store proxy: %v", err)
|
||||||
|
}
|
||||||
|
if persisted == nil {
|
||||||
|
t.Fatal("expected persisted proxy to be returned")
|
||||||
|
}
|
||||||
|
|
||||||
|
got := storeSource.GetProxy("raw-proxy")
|
||||||
|
if got == nil {
|
||||||
|
t.Fatal("proxy not found in store")
|
||||||
|
}
|
||||||
|
if got.GetBaseConfig().LocalIP != "" {
|
||||||
|
t.Fatalf("localIP was persisted with runtime default: %q", got.GetBaseConfig().LocalIP)
|
||||||
|
}
|
||||||
|
if got.GetBaseConfig().Transport.BandwidthLimitMode != "" {
|
||||||
|
t.Fatalf("bandwidthLimitMode was persisted with runtime default: %q", got.GetBaseConfig().Transport.BandwidthLimitMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
45
client/configmgmt/types.go
Normal file
45
client/configmgmt/types.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package configmgmt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/client/proxy"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidArgument = errors.New("invalid argument")
|
||||||
|
ErrNotFound = errors.New("not found")
|
||||||
|
ErrConflict = errors.New("conflict")
|
||||||
|
ErrStoreDisabled = errors.New("store disabled")
|
||||||
|
ErrApplyConfig = errors.New("apply config failed")
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigManager interface {
|
||||||
|
ReloadFromFile(strict bool) error
|
||||||
|
|
||||||
|
ReadConfigFile() (string, error)
|
||||||
|
WriteConfigFile(content []byte) error
|
||||||
|
|
||||||
|
GetProxyStatus() []*proxy.WorkingStatus
|
||||||
|
IsStoreProxyEnabled(name string) bool
|
||||||
|
StoreEnabled() bool
|
||||||
|
|
||||||
|
GetProxyConfig(name string) (v1.ProxyConfigurer, bool)
|
||||||
|
GetVisitorConfig(name string) (v1.VisitorConfigurer, bool)
|
||||||
|
|
||||||
|
ListStoreProxies() ([]v1.ProxyConfigurer, error)
|
||||||
|
GetStoreProxy(name string) (v1.ProxyConfigurer, error)
|
||||||
|
CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
||||||
|
UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
||||||
|
DeleteStoreProxy(name string) error
|
||||||
|
|
||||||
|
ListStoreVisitors() ([]v1.VisitorConfigurer, error)
|
||||||
|
GetStoreVisitor(name string) (v1.VisitorConfigurer, error)
|
||||||
|
CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
||||||
|
UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
||||||
|
DeleteStoreVisitor(name string) error
|
||||||
|
|
||||||
|
GracefulClose(d time.Duration)
|
||||||
|
}
|
||||||
228
client/connector.go
Normal file
228
client/connector.go
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
// Copyright 2023 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
libnet "github.com/fatedier/golib/net"
|
||||||
|
fmux "github.com/hashicorp/yamux"
|
||||||
|
quic "github.com/quic-go/quic-go"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/transport"
|
||||||
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Connector is an interface for establishing connections to the server.
|
||||||
|
type Connector interface {
|
||||||
|
Open() error
|
||||||
|
Connect() (net.Conn, error)
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultConnectorImpl is the default implementation of Connector for normal frpc.
|
||||||
|
type defaultConnectorImpl struct {
|
||||||
|
ctx context.Context
|
||||||
|
cfg *v1.ClientCommonConfig
|
||||||
|
|
||||||
|
muxSession *fmux.Session
|
||||||
|
quicConn *quic.Conn
|
||||||
|
closeOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConnector(ctx context.Context, cfg *v1.ClientCommonConfig) Connector {
|
||||||
|
return &defaultConnectorImpl{
|
||||||
|
ctx: ctx,
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open opens an underlying connection to the server.
|
||||||
|
// The underlying connection is either a TCP connection or a QUIC connection.
|
||||||
|
// After the underlying connection is established, you can call Connect() to get a stream.
|
||||||
|
// If TCPMux isn't enabled, the underlying connection is nil, you will get a new real TCP connection every time you call Connect().
|
||||||
|
func (c *defaultConnectorImpl) Open() error {
|
||||||
|
xl := xlog.FromContextSafe(c.ctx)
|
||||||
|
|
||||||
|
// special for quic
|
||||||
|
if strings.EqualFold(c.cfg.Transport.Protocol, "quic") {
|
||||||
|
var tlsConfig *tls.Config
|
||||||
|
var err error
|
||||||
|
sn := c.cfg.Transport.TLS.ServerName
|
||||||
|
if sn == "" {
|
||||||
|
sn = c.cfg.ServerAddr
|
||||||
|
}
|
||||||
|
if lo.FromPtr(c.cfg.Transport.TLS.Enable) {
|
||||||
|
tlsConfig, err = transport.NewClientTLSConfig(
|
||||||
|
c.cfg.Transport.TLS.CertFile,
|
||||||
|
c.cfg.Transport.TLS.KeyFile,
|
||||||
|
c.cfg.Transport.TLS.TrustedCaFile,
|
||||||
|
sn)
|
||||||
|
} else {
|
||||||
|
tlsConfig, err = transport.NewClientTLSConfig("", "", "", sn)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("fail to build tls configuration, err: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tlsConfig.NextProtos = []string{"frp"}
|
||||||
|
|
||||||
|
conn, err := quic.DialAddr(
|
||||||
|
c.ctx,
|
||||||
|
net.JoinHostPort(c.cfg.ServerAddr, strconv.Itoa(c.cfg.ServerPort)),
|
||||||
|
tlsConfig, &quic.Config{
|
||||||
|
MaxIdleTimeout: time.Duration(c.cfg.Transport.QUIC.MaxIdleTimeout) * time.Second,
|
||||||
|
MaxIncomingStreams: int64(c.cfg.Transport.QUIC.MaxIncomingStreams),
|
||||||
|
KeepAlivePeriod: time.Duration(c.cfg.Transport.QUIC.KeepalivePeriod) * time.Second,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.quicConn = conn
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !lo.FromPtr(c.cfg.Transport.TCPMux) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := c.realConnect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmuxCfg := fmux.DefaultConfig()
|
||||||
|
fmuxCfg.KeepAliveInterval = time.Duration(c.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second
|
||||||
|
// Use trace level for yamux logs
|
||||||
|
fmuxCfg.LogOutput = xlog.NewTraceWriter(xl)
|
||||||
|
fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
|
||||||
|
session, err := fmux.Client(conn, fmuxCfg)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.muxSession = session
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect returns a stream from the underlying connection, or a new TCP connection if TCPMux isn't enabled.
|
||||||
|
func (c *defaultConnectorImpl) Connect() (net.Conn, error) {
|
||||||
|
if c.quicConn != nil {
|
||||||
|
stream, err := c.quicConn.OpenStreamSync(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return netpkg.QuicStreamToNetConn(stream, c.quicConn), nil
|
||||||
|
} else if c.muxSession != nil {
|
||||||
|
stream, err := c.muxSession.OpenStream()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return stream, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.realConnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *defaultConnectorImpl) realConnect() (net.Conn, error) {
|
||||||
|
xl := xlog.FromContextSafe(c.ctx)
|
||||||
|
var tlsConfig *tls.Config
|
||||||
|
var err error
|
||||||
|
tlsEnable := lo.FromPtr(c.cfg.Transport.TLS.Enable)
|
||||||
|
if c.cfg.Transport.Protocol == "wss" {
|
||||||
|
tlsEnable = true
|
||||||
|
}
|
||||||
|
if tlsEnable {
|
||||||
|
sn := c.cfg.Transport.TLS.ServerName
|
||||||
|
if sn == "" {
|
||||||
|
sn = c.cfg.ServerAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConfig, err = transport.NewClientTLSConfig(
|
||||||
|
c.cfg.Transport.TLS.CertFile,
|
||||||
|
c.cfg.Transport.TLS.KeyFile,
|
||||||
|
c.cfg.Transport.TLS.TrustedCaFile,
|
||||||
|
sn)
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("fail to build tls configuration, err: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyType, addr, auth, err := libnet.ParseProxyURL(c.cfg.Transport.ProxyURL)
|
||||||
|
if err != nil {
|
||||||
|
xl.Errorf("fail to parse proxy url")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dialOptions := []libnet.DialOption{}
|
||||||
|
protocol := c.cfg.Transport.Protocol
|
||||||
|
switch protocol {
|
||||||
|
case "websocket":
|
||||||
|
protocol = "tcp"
|
||||||
|
dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, "")}))
|
||||||
|
dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{
|
||||||
|
Hook: netpkg.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(c.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
|
||||||
|
}))
|
||||||
|
dialOptions = append(dialOptions, libnet.WithTLSConfig(tlsConfig))
|
||||||
|
case "wss":
|
||||||
|
protocol = "tcp"
|
||||||
|
dialOptions = append(dialOptions, libnet.WithTLSConfigAndPriority(100, tlsConfig))
|
||||||
|
// Make sure that if it is wss, the websocket hook is executed after the tls hook.
|
||||||
|
dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, tlsConfig.ServerName), Priority: 110}))
|
||||||
|
default:
|
||||||
|
dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{
|
||||||
|
Hook: netpkg.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(c.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
|
||||||
|
}))
|
||||||
|
dialOptions = append(dialOptions, libnet.WithTLSConfig(tlsConfig))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.cfg.Transport.ConnectServerLocalIP != "" {
|
||||||
|
dialOptions = append(dialOptions, libnet.WithLocalAddr(c.cfg.Transport.ConnectServerLocalIP))
|
||||||
|
}
|
||||||
|
dialOptions = append(dialOptions,
|
||||||
|
libnet.WithProtocol(protocol),
|
||||||
|
libnet.WithTimeout(time.Duration(c.cfg.Transport.DialServerTimeout)*time.Second),
|
||||||
|
libnet.WithKeepAlive(time.Duration(c.cfg.Transport.DialServerKeepAlive)*time.Second),
|
||||||
|
libnet.WithProxy(proxyType, addr),
|
||||||
|
libnet.WithProxyAuth(auth),
|
||||||
|
)
|
||||||
|
conn, err := libnet.DialContext(
|
||||||
|
c.ctx,
|
||||||
|
net.JoinHostPort(c.cfg.ServerAddr, strconv.Itoa(c.cfg.ServerPort)),
|
||||||
|
dialOptions...,
|
||||||
|
)
|
||||||
|
return conn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *defaultConnectorImpl) Close() error {
|
||||||
|
c.closeOnce.Do(func() {
|
||||||
|
if c.quicConn != nil {
|
||||||
|
_ = c.quicConn.CloseWithError(0, "")
|
||||||
|
}
|
||||||
|
if c.muxSession != nil {
|
||||||
|
_ = c.muxSession.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
300
client/control.go
Normal file
300
client/control.go
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
// Copyright 2017 fatedier, fatedier@gmail.com
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/client/proxy"
|
||||||
|
"github.com/fatedier/frp/client/visitor"
|
||||||
|
"github.com/fatedier/frp/pkg/auth"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
"github.com/fatedier/frp/pkg/naming"
|
||||||
|
"github.com/fatedier/frp/pkg/transport"
|
||||||
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
|
"github.com/fatedier/frp/pkg/util/wait"
|
||||||
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionContext struct {
|
||||||
|
// The client common configuration.
|
||||||
|
Common *v1.ClientCommonConfig
|
||||||
|
|
||||||
|
// Unique ID obtained from frps.
|
||||||
|
// It should be attached to the login message when reconnecting.
|
||||||
|
RunID string
|
||||||
|
// Underlying control connection. Once conn is closed, the msgDispatcher and the entire Control will exit.
|
||||||
|
Conn net.Conn
|
||||||
|
// Indicates whether the connection is encrypted.
|
||||||
|
ConnEncrypted bool
|
||||||
|
// Auth runtime used for login, heartbeats, and encryption.
|
||||||
|
Auth *auth.ClientAuth
|
||||||
|
// Connector is used to create new connections, which could be real TCP connections or virtual streams.
|
||||||
|
Connector Connector
|
||||||
|
// Virtual net controller
|
||||||
|
VnetController *vnet.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
type Control struct {
|
||||||
|
// service context
|
||||||
|
ctx context.Context
|
||||||
|
xl *xlog.Logger
|
||||||
|
|
||||||
|
// session context
|
||||||
|
sessionCtx *SessionContext
|
||||||
|
|
||||||
|
// manage all proxies
|
||||||
|
pm *proxy.Manager
|
||||||
|
|
||||||
|
// manage all visitors
|
||||||
|
vm *visitor.Manager
|
||||||
|
|
||||||
|
doneCh chan struct{}
|
||||||
|
|
||||||
|
// of time.Time, last time got the Pong message
|
||||||
|
lastPong atomic.Value
|
||||||
|
|
||||||
|
// The role of msgTransporter is similar to HTTP2.
|
||||||
|
// It allows multiple messages to be sent simultaneously on the same control connection.
|
||||||
|
// The server's response messages will be dispatched to the corresponding waiting goroutines based on the laneKey and message type.
|
||||||
|
msgTransporter transport.MessageTransporter
|
||||||
|
|
||||||
|
// msgDispatcher is a wrapper for control connection.
|
||||||
|
// It provides a channel for sending messages, and you can register handlers to process messages based on their respective types.
|
||||||
|
msgDispatcher *msg.Dispatcher
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, error) {
|
||||||
|
// new xlog instance
|
||||||
|
ctl := &Control{
|
||||||
|
ctx: ctx,
|
||||||
|
xl: xlog.FromContextSafe(ctx),
|
||||||
|
sessionCtx: sessionCtx,
|
||||||
|
doneCh: make(chan struct{}),
|
||||||
|
}
|
||||||
|
ctl.lastPong.Store(time.Now())
|
||||||
|
|
||||||
|
if sessionCtx.ConnEncrypted {
|
||||||
|
cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, sessionCtx.Auth.EncryptionKey())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ctl.msgDispatcher = msg.NewDispatcher(cryptoRW)
|
||||||
|
} else {
|
||||||
|
ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
|
||||||
|
}
|
||||||
|
ctl.registerMsgHandlers()
|
||||||
|
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher)
|
||||||
|
|
||||||
|
ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, sessionCtx.Auth.EncryptionKey(), ctl.msgTransporter, sessionCtx.VnetController)
|
||||||
|
ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common,
|
||||||
|
ctl.connectServer, ctl.msgTransporter, sessionCtx.VnetController)
|
||||||
|
return ctl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctl *Control) Run(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) {
|
||||||
|
go ctl.worker()
|
||||||
|
|
||||||
|
// start all proxies
|
||||||
|
ctl.pm.UpdateAll(proxyCfgs)
|
||||||
|
|
||||||
|
// start all visitors
|
||||||
|
ctl.vm.UpdateAll(visitorCfgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctl *Control) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
|
||||||
|
ctl.pm.SetInWorkConnCallback(cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctl *Control) handleReqWorkConn(_ msg.Message) {
|
||||||
|
xl := ctl.xl
|
||||||
|
workConn, err := ctl.connectServer()
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("start new connection to server error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m := &msg.NewWorkConn{
|
||||||
|
RunID: ctl.sessionCtx.RunID,
|
||||||
|
}
|
||||||
|
if err = ctl.sessionCtx.Auth.Setter.SetNewWorkConn(m); err != nil {
|
||||||
|
xl.Warnf("error during NewWorkConn authentication: %v", err)
|
||||||
|
workConn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = msg.WriteMsg(workConn, m); err != nil {
|
||||||
|
xl.Warnf("work connection write to server error: %v", err)
|
||||||
|
workConn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var startMsg msg.StartWorkConn
|
||||||
|
if err = msg.ReadMsgInto(workConn, &startMsg); err != nil {
|
||||||
|
xl.Tracef("work connection closed before response StartWorkConn message: %v", err)
|
||||||
|
workConn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if startMsg.Error != "" {
|
||||||
|
xl.Errorf("StartWorkConn contains error: %s", startMsg.Error)
|
||||||
|
workConn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
startMsg.ProxyName = naming.StripUserPrefix(ctl.sessionCtx.Common.User, startMsg.ProxyName)
|
||||||
|
|
||||||
|
// dispatch this work connection to related proxy
|
||||||
|
ctl.pm.HandleWorkConn(startMsg.ProxyName, workConn, &startMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctl *Control) handleNewProxyResp(m msg.Message) {
|
||||||
|
xl := ctl.xl
|
||||||
|
inMsg := m.(*msg.NewProxyResp)
|
||||||
|
// Server will return NewProxyResp message to each NewProxy message.
|
||||||
|
// Start a new proxy handler if no error got
|
||||||
|
proxyName := naming.StripUserPrefix(ctl.sessionCtx.Common.User, inMsg.ProxyName)
|
||||||
|
err := ctl.pm.StartProxy(proxyName, inMsg.RemoteAddr, inMsg.Error)
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("[%s] start error: %v", proxyName, err)
|
||||||
|
} else {
|
||||||
|
xl.Infof("[%s] start proxy success", proxyName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctl *Control) handleNatHoleResp(m msg.Message) {
|
||||||
|
xl := ctl.xl
|
||||||
|
inMsg := m.(*msg.NatHoleResp)
|
||||||
|
|
||||||
|
// Dispatch the NatHoleResp message to the related proxy.
|
||||||
|
ok := ctl.msgTransporter.DispatchWithType(inMsg, msg.TypeNameNatHoleResp, inMsg.TransactionID)
|
||||||
|
if !ok {
|
||||||
|
xl.Tracef("dispatch NatHoleResp message to related proxy error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctl *Control) handlePong(m msg.Message) {
|
||||||
|
xl := ctl.xl
|
||||||
|
inMsg := m.(*msg.Pong)
|
||||||
|
|
||||||
|
if inMsg.Error != "" {
|
||||||
|
xl.Errorf("pong message contains error: %s", inMsg.Error)
|
||||||
|
ctl.closeSession()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctl.lastPong.Store(time.Now())
|
||||||
|
xl.Debugf("receive heartbeat from server")
|
||||||
|
}
|
||||||
|
|
||||||
|
// closeSession closes the control connection.
|
||||||
|
func (ctl *Control) closeSession() {
|
||||||
|
ctl.sessionCtx.Conn.Close()
|
||||||
|
ctl.sessionCtx.Connector.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctl *Control) Close() error {
|
||||||
|
return ctl.GracefulClose(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctl *Control) GracefulClose(d time.Duration) error {
|
||||||
|
ctl.pm.Close()
|
||||||
|
ctl.vm.Close()
|
||||||
|
|
||||||
|
time.Sleep(d)
|
||||||
|
|
||||||
|
ctl.closeSession()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done returns a channel that will be closed after all resources are released
|
||||||
|
func (ctl *Control) Done() <-chan struct{} {
|
||||||
|
return ctl.doneCh
|
||||||
|
}
|
||||||
|
|
||||||
|
// connectServer return a new connection to frps
|
||||||
|
func (ctl *Control) connectServer() (net.Conn, error) {
|
||||||
|
return ctl.sessionCtx.Connector.Connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctl *Control) registerMsgHandlers() {
|
||||||
|
ctl.msgDispatcher.RegisterHandler(&msg.ReqWorkConn{}, msg.AsyncHandler(ctl.handleReqWorkConn))
|
||||||
|
ctl.msgDispatcher.RegisterHandler(&msg.NewProxyResp{}, ctl.handleNewProxyResp)
|
||||||
|
ctl.msgDispatcher.RegisterHandler(&msg.NatHoleResp{}, ctl.handleNatHoleResp)
|
||||||
|
ctl.msgDispatcher.RegisterHandler(&msg.Pong{}, ctl.handlePong)
|
||||||
|
}
|
||||||
|
|
||||||
|
// heartbeatWorker sends heartbeat to server and check heartbeat timeout.
|
||||||
|
func (ctl *Control) heartbeatWorker() {
|
||||||
|
xl := ctl.xl
|
||||||
|
|
||||||
|
if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 {
|
||||||
|
// Send heartbeat to server.
|
||||||
|
sendHeartBeat := func() (bool, error) {
|
||||||
|
xl.Debugf("send heartbeat to server")
|
||||||
|
pingMsg := &msg.Ping{}
|
||||||
|
if err := ctl.sessionCtx.Auth.Setter.SetPing(pingMsg); err != nil {
|
||||||
|
xl.Warnf("error during ping authentication: %v, skip sending ping message", err)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
_ = ctl.msgDispatcher.Send(pingMsg)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
go wait.BackoffUntil(sendHeartBeat,
|
||||||
|
wait.NewFastBackoffManager(wait.FastBackoffOptions{
|
||||||
|
Duration: time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatInterval) * time.Second,
|
||||||
|
InitDurationIfFail: time.Second,
|
||||||
|
Factor: 2.0,
|
||||||
|
Jitter: 0.1,
|
||||||
|
MaxDuration: time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatInterval) * time.Second,
|
||||||
|
}),
|
||||||
|
true, ctl.doneCh,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check heartbeat timeout.
|
||||||
|
if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 && ctl.sessionCtx.Common.Transport.HeartbeatTimeout > 0 {
|
||||||
|
go wait.Until(func() {
|
||||||
|
if time.Since(ctl.lastPong.Load().(time.Time)) > time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatTimeout)*time.Second {
|
||||||
|
xl.Warnf("heartbeat timeout")
|
||||||
|
ctl.closeSession()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}, time.Second, ctl.doneCh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctl *Control) worker() {
|
||||||
|
xl := ctl.xl
|
||||||
|
go ctl.heartbeatWorker()
|
||||||
|
go ctl.msgDispatcher.Run()
|
||||||
|
|
||||||
|
<-ctl.msgDispatcher.Done()
|
||||||
|
xl.Debugf("control message dispatcher exited")
|
||||||
|
ctl.closeSession()
|
||||||
|
|
||||||
|
ctl.pm.Close()
|
||||||
|
ctl.vm.Close()
|
||||||
|
close(ctl.doneCh)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctl *Control) UpdateAllConfigurer(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
|
||||||
|
ctl.vm.UpdateAll(visitorCfgs)
|
||||||
|
ctl.pm.UpdateAll(proxyCfgs)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
19
client/event/event.go
Normal file
19
client/event/event.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrPayloadType = errors.New("error payload type")
|
||||||
|
|
||||||
|
type Handler func(payload any) error
|
||||||
|
|
||||||
|
type StartProxyPayload struct {
|
||||||
|
NewProxyMsg *msg.NewProxy
|
||||||
|
}
|
||||||
|
|
||||||
|
type CloseProxyPayload struct {
|
||||||
|
CloseProxyMsg *msg.CloseProxy
|
||||||
|
}
|
||||||
185
client/health/health.go
Normal file
185
client/health/health.go
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
// Copyright 2018 fatedier, fatedier@gmail.com
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package health
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrHealthCheckType = errors.New("error health check type")
|
||||||
|
|
||||||
|
type Monitor struct {
|
||||||
|
checkType string
|
||||||
|
interval time.Duration
|
||||||
|
timeout time.Duration
|
||||||
|
maxFailedTimes int
|
||||||
|
|
||||||
|
// For tcp
|
||||||
|
addr string
|
||||||
|
|
||||||
|
// For http
|
||||||
|
url string
|
||||||
|
header http.Header
|
||||||
|
failedTimes uint64
|
||||||
|
statusOK bool
|
||||||
|
statusNormalFn func()
|
||||||
|
statusFailedFn func()
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMonitor(ctx context.Context, cfg v1.HealthCheckConfig, addr string,
|
||||||
|
statusNormalFn func(), statusFailedFn func(),
|
||||||
|
) *Monitor {
|
||||||
|
if cfg.IntervalSeconds <= 0 {
|
||||||
|
cfg.IntervalSeconds = 10
|
||||||
|
}
|
||||||
|
if cfg.TimeoutSeconds <= 0 {
|
||||||
|
cfg.TimeoutSeconds = 3
|
||||||
|
}
|
||||||
|
if cfg.MaxFailed <= 0 {
|
||||||
|
cfg.MaxFailed = 1
|
||||||
|
}
|
||||||
|
newctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
|
var url string
|
||||||
|
if cfg.Type == "http" && cfg.Path != "" {
|
||||||
|
s := "http://" + addr
|
||||||
|
if !strings.HasPrefix(cfg.Path, "/") {
|
||||||
|
s += "/"
|
||||||
|
}
|
||||||
|
url = s + cfg.Path
|
||||||
|
}
|
||||||
|
header := make(http.Header)
|
||||||
|
for _, h := range cfg.HTTPHeaders {
|
||||||
|
header.Set(h.Name, h.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Monitor{
|
||||||
|
checkType: cfg.Type,
|
||||||
|
interval: time.Duration(cfg.IntervalSeconds) * time.Second,
|
||||||
|
timeout: time.Duration(cfg.TimeoutSeconds) * time.Second,
|
||||||
|
maxFailedTimes: cfg.MaxFailed,
|
||||||
|
addr: addr,
|
||||||
|
url: url,
|
||||||
|
header: header,
|
||||||
|
statusOK: false,
|
||||||
|
statusNormalFn: statusNormalFn,
|
||||||
|
statusFailedFn: statusFailedFn,
|
||||||
|
ctx: newctx,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (monitor *Monitor) Start() {
|
||||||
|
go monitor.checkWorker()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (monitor *Monitor) Stop() {
|
||||||
|
monitor.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (monitor *Monitor) checkWorker() {
|
||||||
|
xl := xlog.FromContextSafe(monitor.ctx)
|
||||||
|
for {
|
||||||
|
doCtx, cancel := context.WithDeadline(monitor.ctx, time.Now().Add(monitor.timeout))
|
||||||
|
err := monitor.doCheck(doCtx)
|
||||||
|
|
||||||
|
// check if this monitor has been closed
|
||||||
|
select {
|
||||||
|
case <-monitor.ctx.Done():
|
||||||
|
cancel()
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
xl.Tracef("do one health check success")
|
||||||
|
if !monitor.statusOK && monitor.statusNormalFn != nil {
|
||||||
|
xl.Infof("health check status change to success")
|
||||||
|
monitor.statusOK = true
|
||||||
|
monitor.statusNormalFn()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
xl.Warnf("do one health check failed: %v", err)
|
||||||
|
monitor.failedTimes++
|
||||||
|
if monitor.statusOK && int(monitor.failedTimes) >= monitor.maxFailedTimes && monitor.statusFailedFn != nil {
|
||||||
|
xl.Warnf("health check status change to failed")
|
||||||
|
monitor.statusOK = false
|
||||||
|
monitor.statusFailedFn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(monitor.interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (monitor *Monitor) doCheck(ctx context.Context) error {
|
||||||
|
switch monitor.checkType {
|
||||||
|
case "tcp":
|
||||||
|
return monitor.doTCPCheck(ctx)
|
||||||
|
case "http":
|
||||||
|
return monitor.doHTTPCheck(ctx)
|
||||||
|
default:
|
||||||
|
return ErrHealthCheckType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (monitor *Monitor) doTCPCheck(ctx context.Context) error {
|
||||||
|
// if tcp address is not specified, always return nil
|
||||||
|
if monitor.addr == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var d net.Dialer
|
||||||
|
conn, err := d.DialContext(ctx, "tcp", monitor.addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (monitor *Monitor) doHTTPCheck(ctx context.Context) error {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", monitor.url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header = monitor.header
|
||||||
|
req.Host = monitor.header.Get("Host")
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
_, _ = io.Copy(io.Discard, resp.Body)
|
||||||
|
|
||||||
|
if resp.StatusCode/100 != 2 {
|
||||||
|
return fmt.Errorf("do http health check, StatusCode is [%d] not 2xx", resp.StatusCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
433
client/http/controller.go
Normal file
433
client/http/controller.go
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
// Copyright 2025 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/client/configmgmt"
|
||||||
|
"github.com/fatedier/frp/client/http/model"
|
||||||
|
"github.com/fatedier/frp/client/proxy"
|
||||||
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
|
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Controller handles HTTP API requests for frpc.
|
||||||
|
type Controller struct {
|
||||||
|
serverAddr string
|
||||||
|
manager configmgmt.ConfigManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// ControllerParams contains parameters for creating an APIController.
|
||||||
|
type ControllerParams struct {
|
||||||
|
ServerAddr string
|
||||||
|
Manager configmgmt.ConfigManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewController(params ControllerParams) *Controller {
|
||||||
|
return &Controller{
|
||||||
|
serverAddr: params.ServerAddr,
|
||||||
|
manager: params.Manager,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) toHTTPError(err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
code := http.StatusInternalServerError
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, configmgmt.ErrInvalidArgument):
|
||||||
|
code = http.StatusBadRequest
|
||||||
|
case errors.Is(err, configmgmt.ErrNotFound), errors.Is(err, configmgmt.ErrStoreDisabled):
|
||||||
|
code = http.StatusNotFound
|
||||||
|
case errors.Is(err, configmgmt.ErrConflict):
|
||||||
|
code = http.StatusConflict
|
||||||
|
}
|
||||||
|
return httppkg.NewError(code, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload handles GET /api/reload
|
||||||
|
func (c *Controller) Reload(ctx *httppkg.Context) (any, error) {
|
||||||
|
strictConfigMode := false
|
||||||
|
strictStr := ctx.Query("strictConfig")
|
||||||
|
if strictStr != "" {
|
||||||
|
strictConfigMode, _ = strconv.ParseBool(strictStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.manager.ReloadFromFile(strictConfigMode); err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop handles POST /api/stop
|
||||||
|
func (c *Controller) Stop(ctx *httppkg.Context) (any, error) {
|
||||||
|
go c.manager.GracefulClose(100 * time.Millisecond)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status handles GET /api/status
|
||||||
|
func (c *Controller) Status(ctx *httppkg.Context) (any, error) {
|
||||||
|
res := make(model.StatusResp)
|
||||||
|
ps := c.manager.GetProxyStatus()
|
||||||
|
if ps == nil {
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, status := range ps {
|
||||||
|
res[status.Type] = append(res[status.Type], c.buildProxyStatusResp(status))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, arrs := range res {
|
||||||
|
if len(arrs) <= 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
slices.SortFunc(arrs, func(a, b model.ProxyStatusResp) int {
|
||||||
|
return cmp.Compare(a.Name, b.Name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfig handles GET /api/config
|
||||||
|
func (c *Controller) GetConfig(ctx *httppkg.Context) (any, error) {
|
||||||
|
content, err := c.manager.ReadConfigFile()
|
||||||
|
if err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutConfig handles PUT /api/config
|
||||||
|
func (c *Controller) PutConfig(ctx *httppkg.Context) (any, error) {
|
||||||
|
body, err := ctx.Body()
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read request body error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(body) == 0 {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, "body can't be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.manager.WriteConfigFile(body); err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) model.ProxyStatusResp {
|
||||||
|
psr := model.ProxyStatusResp{
|
||||||
|
Name: status.Name,
|
||||||
|
Type: status.Type,
|
||||||
|
Status: status.Phase,
|
||||||
|
Err: status.Err,
|
||||||
|
}
|
||||||
|
baseCfg := status.Cfg.GetBaseConfig()
|
||||||
|
if baseCfg.LocalPort != 0 {
|
||||||
|
psr.LocalAddr = net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort))
|
||||||
|
}
|
||||||
|
psr.Plugin = baseCfg.Plugin.Type
|
||||||
|
|
||||||
|
if status.Err == "" {
|
||||||
|
psr.RemoteAddr = status.RemoteAddr
|
||||||
|
if slices.Contains([]string{"tcp", "udp"}, status.Type) {
|
||||||
|
psr.RemoteAddr = c.serverAddr + psr.RemoteAddr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.manager.IsStoreProxyEnabled(status.Name) {
|
||||||
|
psr.Source = model.SourceStore
|
||||||
|
}
|
||||||
|
return psr
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProxyConfig handles GET /api/proxy/{name}/config
|
||||||
|
func (c *Controller) GetProxyConfig(ctx *httppkg.Context) (any, error) {
|
||||||
|
name := ctx.Param("name")
|
||||||
|
if name == "" {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, ok := c.manager.GetProxyConfig(name)
|
||||||
|
if !ok {
|
||||||
|
return nil, httppkg.NewError(http.StatusNotFound, fmt.Sprintf("proxy %q not found", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := model.ProxyDefinitionFromConfigurer(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVisitorConfig handles GET /api/visitor/{name}/config
|
||||||
|
func (c *Controller) GetVisitorConfig(ctx *httppkg.Context) (any, error) {
|
||||||
|
name := ctx.Param("name")
|
||||||
|
if name == "" {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, ok := c.manager.GetVisitorConfig(name)
|
||||||
|
if !ok {
|
||||||
|
return nil, httppkg.NewError(http.StatusNotFound, fmt.Sprintf("visitor %q not found", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := model.VisitorDefinitionFromConfigurer(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) ListStoreProxies(ctx *httppkg.Context) (any, error) {
|
||||||
|
proxies, err := c.manager.ListStoreProxies()
|
||||||
|
if err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := model.ProxyListResp{Proxies: make([]model.ProxyDefinition, 0, len(proxies))}
|
||||||
|
for _, p := range proxies {
|
||||||
|
payload, err := model.ProxyDefinitionFromConfigurer(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
resp.Proxies = append(resp.Proxies, payload)
|
||||||
|
}
|
||||||
|
slices.SortFunc(resp.Proxies, func(a, b model.ProxyDefinition) int {
|
||||||
|
return cmp.Compare(a.Name, b.Name)
|
||||||
|
})
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) GetStoreProxy(ctx *httppkg.Context) (any, error) {
|
||||||
|
name := ctx.Param("name")
|
||||||
|
if name == "" {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := c.manager.GetStoreProxy(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := model.ProxyDefinitionFromConfigurer(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) CreateStoreProxy(ctx *httppkg.Context) (any, error) {
|
||||||
|
body, err := ctx.Body()
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload model.ProxyDefinition
|
||||||
|
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := payload.Validate("", false); err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
cfg, err := payload.ToConfigurer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
created, err := c.manager.CreateStoreProxy(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := model.ProxyDefinitionFromConfigurer(created)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) UpdateStoreProxy(ctx *httppkg.Context) (any, error) {
|
||||||
|
name := ctx.Param("name")
|
||||||
|
if name == "" {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ctx.Body()
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload model.ProxyDefinition
|
||||||
|
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := payload.Validate(name, true); err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
cfg, err := payload.ToConfigurer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
updated, err := c.manager.UpdateStoreProxy(name, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := model.ProxyDefinitionFromConfigurer(updated)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) DeleteStoreProxy(ctx *httppkg.Context) (any, error) {
|
||||||
|
name := ctx.Param("name")
|
||||||
|
if name == "" {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.manager.DeleteStoreProxy(name); err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) ListStoreVisitors(ctx *httppkg.Context) (any, error) {
|
||||||
|
visitors, err := c.manager.ListStoreVisitors()
|
||||||
|
if err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := model.VisitorListResp{Visitors: make([]model.VisitorDefinition, 0, len(visitors))}
|
||||||
|
for _, v := range visitors {
|
||||||
|
payload, err := model.VisitorDefinitionFromConfigurer(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
resp.Visitors = append(resp.Visitors, payload)
|
||||||
|
}
|
||||||
|
slices.SortFunc(resp.Visitors, func(a, b model.VisitorDefinition) int {
|
||||||
|
return cmp.Compare(a.Name, b.Name)
|
||||||
|
})
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) GetStoreVisitor(ctx *httppkg.Context) (any, error) {
|
||||||
|
name := ctx.Param("name")
|
||||||
|
if name == "" {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := c.manager.GetStoreVisitor(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := model.VisitorDefinitionFromConfigurer(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) CreateStoreVisitor(ctx *httppkg.Context) (any, error) {
|
||||||
|
body, err := ctx.Body()
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload model.VisitorDefinition
|
||||||
|
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := payload.Validate("", false); err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
cfg, err := payload.ToConfigurer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
created, err := c.manager.CreateStoreVisitor(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := model.VisitorDefinitionFromConfigurer(created)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) UpdateStoreVisitor(ctx *httppkg.Context) (any, error) {
|
||||||
|
name := ctx.Param("name")
|
||||||
|
if name == "" {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ctx.Body()
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload model.VisitorDefinition
|
||||||
|
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := payload.Validate(name, true); err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
cfg, err := payload.ToConfigurer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
updated, err := c.manager.UpdateStoreVisitor(name, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := model.VisitorDefinitionFromConfigurer(updated)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) DeleteStoreVisitor(ctx *httppkg.Context) (any, error) {
|
||||||
|
name := ctx.Param("name")
|
||||||
|
if name == "" {
|
||||||
|
return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.manager.DeleteStoreVisitor(name); err != nil {
|
||||||
|
return nil, c.toHTTPError(err)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
662
client/http/controller_test.go
Normal file
662
client/http/controller_test.go
Normal file
@@ -0,0 +1,662 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/client/configmgmt"
|
||||||
|
"github.com/fatedier/frp/client/http/model"
|
||||||
|
"github.com/fatedier/frp/client/proxy"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeConfigManager struct {
|
||||||
|
reloadFromFileFn func(strict bool) error
|
||||||
|
readConfigFileFn func() (string, error)
|
||||||
|
writeConfigFileFn func(content []byte) error
|
||||||
|
getProxyStatusFn func() []*proxy.WorkingStatus
|
||||||
|
isStoreProxyEnabledFn func(name string) bool
|
||||||
|
storeEnabledFn func() bool
|
||||||
|
getProxyConfigFn func(name string) (v1.ProxyConfigurer, bool)
|
||||||
|
getVisitorConfigFn func(name string) (v1.VisitorConfigurer, bool)
|
||||||
|
|
||||||
|
listStoreProxiesFn func() ([]v1.ProxyConfigurer, error)
|
||||||
|
getStoreProxyFn func(name string) (v1.ProxyConfigurer, error)
|
||||||
|
createStoreProxyFn func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
||||||
|
updateStoreProxyFn func(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
||||||
|
deleteStoreProxyFn func(name string) error
|
||||||
|
listStoreVisitorsFn func() ([]v1.VisitorConfigurer, error)
|
||||||
|
getStoreVisitorFn func(name string) (v1.VisitorConfigurer, error)
|
||||||
|
createStoreVisitFn func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
||||||
|
updateStoreVisitFn func(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
||||||
|
deleteStoreVisitFn func(name string) error
|
||||||
|
gracefulCloseFn func(d time.Duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) ReloadFromFile(strict bool) error {
|
||||||
|
if m.reloadFromFileFn != nil {
|
||||||
|
return m.reloadFromFileFn(strict)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) ReadConfigFile() (string, error) {
|
||||||
|
if m.readConfigFileFn != nil {
|
||||||
|
return m.readConfigFileFn()
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) WriteConfigFile(content []byte) error {
|
||||||
|
if m.writeConfigFileFn != nil {
|
||||||
|
return m.writeConfigFileFn(content)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) GetProxyStatus() []*proxy.WorkingStatus {
|
||||||
|
if m.getProxyStatusFn != nil {
|
||||||
|
return m.getProxyStatusFn()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) IsStoreProxyEnabled(name string) bool {
|
||||||
|
if m.isStoreProxyEnabledFn != nil {
|
||||||
|
return m.isStoreProxyEnabledFn(name)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) StoreEnabled() bool {
|
||||||
|
if m.storeEnabledFn != nil {
|
||||||
|
return m.storeEnabledFn()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) GetProxyConfig(name string) (v1.ProxyConfigurer, bool) {
|
||||||
|
if m.getProxyConfigFn != nil {
|
||||||
|
return m.getProxyConfigFn(name)
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) GetVisitorConfig(name string) (v1.VisitorConfigurer, bool) {
|
||||||
|
if m.getVisitorConfigFn != nil {
|
||||||
|
return m.getVisitorConfigFn(name)
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) ListStoreProxies() ([]v1.ProxyConfigurer, error) {
|
||||||
|
if m.listStoreProxiesFn != nil {
|
||||||
|
return m.listStoreProxiesFn()
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) GetStoreProxy(name string) (v1.ProxyConfigurer, error) {
|
||||||
|
if m.getStoreProxyFn != nil {
|
||||||
|
return m.getStoreProxyFn(name)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
if m.createStoreProxyFn != nil {
|
||||||
|
return m.createStoreProxyFn(cfg)
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
if m.updateStoreProxyFn != nil {
|
||||||
|
return m.updateStoreProxyFn(name, cfg)
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) DeleteStoreProxy(name string) error {
|
||||||
|
if m.deleteStoreProxyFn != nil {
|
||||||
|
return m.deleteStoreProxyFn(name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) ListStoreVisitors() ([]v1.VisitorConfigurer, error) {
|
||||||
|
if m.listStoreVisitorsFn != nil {
|
||||||
|
return m.listStoreVisitorsFn()
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) GetStoreVisitor(name string) (v1.VisitorConfigurer, error) {
|
||||||
|
if m.getStoreVisitorFn != nil {
|
||||||
|
return m.getStoreVisitorFn(name)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
|
if m.createStoreVisitFn != nil {
|
||||||
|
return m.createStoreVisitFn(cfg)
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
|
if m.updateStoreVisitFn != nil {
|
||||||
|
return m.updateStoreVisitFn(name, cfg)
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) DeleteStoreVisitor(name string) error {
|
||||||
|
if m.deleteStoreVisitFn != nil {
|
||||||
|
return m.deleteStoreVisitFn(name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeConfigManager) GracefulClose(d time.Duration) {
|
||||||
|
if m.gracefulCloseFn != nil {
|
||||||
|
m.gracefulCloseFn(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRawTCPProxyConfig(name string) *v1.TCPProxyConfig {
|
||||||
|
return &v1.TCPProxyConfig{
|
||||||
|
ProxyBaseConfig: v1.ProxyBaseConfig{
|
||||||
|
Name: name,
|
||||||
|
Type: "tcp",
|
||||||
|
ProxyBackend: v1.ProxyBackend{
|
||||||
|
LocalPort: 10080,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildProxyStatusRespStoreSourceEnabled(t *testing.T) {
|
||||||
|
status := &proxy.WorkingStatus{
|
||||||
|
Name: "shared-proxy",
|
||||||
|
Type: "tcp",
|
||||||
|
Phase: proxy.ProxyPhaseRunning,
|
||||||
|
RemoteAddr: ":8080",
|
||||||
|
Cfg: newRawTCPProxyConfig("shared-proxy"),
|
||||||
|
}
|
||||||
|
|
||||||
|
controller := &Controller{
|
||||||
|
serverAddr: "127.0.0.1",
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
isStoreProxyEnabledFn: func(name string) bool {
|
||||||
|
return name == "shared-proxy"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := controller.buildProxyStatusResp(status)
|
||||||
|
if resp.Source != "store" {
|
||||||
|
t.Fatalf("unexpected source: %q", resp.Source)
|
||||||
|
}
|
||||||
|
if resp.RemoteAddr != "127.0.0.1:8080" {
|
||||||
|
t.Fatalf("unexpected remote addr: %q", resp.RemoteAddr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReloadErrorMapping(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
expectedCode int
|
||||||
|
}{
|
||||||
|
{name: "invalid arg", err: fmtError(configmgmt.ErrInvalidArgument, "bad cfg"), expectedCode: http.StatusBadRequest},
|
||||||
|
{name: "apply fail", err: fmtError(configmgmt.ErrApplyConfig, "reload failed"), expectedCode: http.StatusInternalServerError},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{reloadFromFileFn: func(bool) error { return tc.err }},
|
||||||
|
}
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/api/reload", nil))
|
||||||
|
_, err := controller.Reload(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
assertHTTPCode(t, err, tc.expectedCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreProxyErrorMapping(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
expectedCode int
|
||||||
|
}{
|
||||||
|
{name: "not found", err: fmtError(configmgmt.ErrNotFound, "not found"), expectedCode: http.StatusNotFound},
|
||||||
|
{name: "conflict", err: fmtError(configmgmt.ErrConflict, "exists"), expectedCode: http.StatusConflict},
|
||||||
|
{name: "internal", err: errors.New("persist failed"), expectedCode: http.StatusInternalServerError},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
body := []byte(`{"name":"shared-proxy","type":"tcp","tcp":{"localPort":10080}}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/shared-proxy", bytes.NewReader(body))
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "shared-proxy"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
updateStoreProxyFn: func(_ string, _ v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
return nil, tc.err
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := controller.UpdateStoreProxy(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
assertHTTPCode(t, err, tc.expectedCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreVisitorErrorMapping(t *testing.T) {
|
||||||
|
body := []byte(`{"name":"shared-visitor","type":"xtcp","xtcp":{"serverName":"server","bindPort":10081,"secretKey":"secret"}}`)
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/api/store/visitors/shared-visitor", bytes.NewReader(body))
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "shared-visitor"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
deleteStoreVisitFn: func(string) error {
|
||||||
|
return fmtError(configmgmt.ErrStoreDisabled, "disabled")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := controller.DeleteStoreVisitor(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
assertHTTPCode(t, err, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateStoreProxyIgnoresUnknownFields(t *testing.T) {
|
||||||
|
var gotName string
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
createStoreProxyFn: func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
gotName = cfg.GetBaseConfig().Name
|
||||||
|
return cfg, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
body := []byte(`{"name":"raw-proxy","type":"tcp","unexpected":"value","tcp":{"localPort":10080,"unknownInBlock":"value"}}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/store/proxies", bytes.NewReader(body))
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.CreateStoreProxy(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create store proxy: %v", err)
|
||||||
|
}
|
||||||
|
if gotName != "raw-proxy" {
|
||||||
|
t.Fatalf("unexpected proxy name: %q", gotName)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, ok := resp.(model.ProxyDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.Type != "tcp" || payload.TCP == nil {
|
||||||
|
t.Fatalf("unexpected payload: %#v", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateStoreVisitorIgnoresUnknownFields(t *testing.T) {
|
||||||
|
var gotName string
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
createStoreVisitFn: func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
|
gotName = cfg.GetBaseConfig().Name
|
||||||
|
return cfg, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
body := []byte(`{
|
||||||
|
"name":"raw-visitor","type":"xtcp","unexpected":"value",
|
||||||
|
"xtcp":{"serverName":"server","bindPort":10081,"secretKey":"secret","unknownInBlock":"value"}
|
||||||
|
}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/store/visitors", bytes.NewReader(body))
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.CreateStoreVisitor(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create store visitor: %v", err)
|
||||||
|
}
|
||||||
|
if gotName != "raw-visitor" {
|
||||||
|
t.Fatalf("unexpected visitor name: %q", gotName)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, ok := resp.(model.VisitorDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.Type != "xtcp" || payload.XTCP == nil {
|
||||||
|
t.Fatalf("unexpected payload: %#v", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateStoreProxyPluginUnknownFieldsAreIgnored(t *testing.T) {
|
||||||
|
var gotPluginType string
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
createStoreProxyFn: func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
gotPluginType = cfg.GetBaseConfig().Plugin.Type
|
||||||
|
return cfg, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
body := []byte(`{"name":"plugin-proxy","type":"tcp","tcp":{"plugin":{"type":"http2https","localAddr":"127.0.0.1:8080","unknownInPlugin":"value"}}}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/store/proxies", bytes.NewReader(body))
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.CreateStoreProxy(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create store proxy: %v", err)
|
||||||
|
}
|
||||||
|
if gotPluginType != "http2https" {
|
||||||
|
t.Fatalf("unexpected plugin type: %q", gotPluginType)
|
||||||
|
}
|
||||||
|
payload, ok := resp.(model.ProxyDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.TCP == nil {
|
||||||
|
t.Fatalf("unexpected response payload: %#v", payload)
|
||||||
|
}
|
||||||
|
pluginType := payload.TCP.Plugin.Type
|
||||||
|
|
||||||
|
if pluginType != "http2https" {
|
||||||
|
t.Fatalf("unexpected plugin type in response payload: %q", pluginType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateStoreVisitorPluginUnknownFieldsAreIgnored(t *testing.T) {
|
||||||
|
var gotPluginType string
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
createStoreVisitFn: func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
||||||
|
gotPluginType = cfg.GetBaseConfig().Plugin.Type
|
||||||
|
return cfg, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
body := []byte(`{
|
||||||
|
"name":"plugin-visitor","type":"stcp",
|
||||||
|
"stcp":{"serverName":"server","bindPort":10081,"plugin":{"type":"virtual_net","destinationIP":"10.0.0.1","unknownInPlugin":"value"}}
|
||||||
|
}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/store/visitors", bytes.NewReader(body))
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.CreateStoreVisitor(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create store visitor: %v", err)
|
||||||
|
}
|
||||||
|
if gotPluginType != "virtual_net" {
|
||||||
|
t.Fatalf("unexpected plugin type: %q", gotPluginType)
|
||||||
|
}
|
||||||
|
payload, ok := resp.(model.VisitorDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.STCP == nil {
|
||||||
|
t.Fatalf("unexpected response payload: %#v", payload)
|
||||||
|
}
|
||||||
|
pluginType := payload.STCP.Plugin.Type
|
||||||
|
|
||||||
|
if pluginType != "virtual_net" {
|
||||||
|
t.Fatalf("unexpected plugin type in response payload: %q", pluginType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateStoreProxyRejectsMismatchedTypeBlock(t *testing.T) {
|
||||||
|
controller := &Controller{manager: &fakeConfigManager{}}
|
||||||
|
body := []byte(`{"name":"p1","type":"tcp","udp":{"localPort":10080}}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/p1", bytes.NewReader(body))
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "p1"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
_, err := controller.UpdateStoreProxy(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
assertHTTPCode(t, err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateStoreProxyRejectsNameMismatch(t *testing.T) {
|
||||||
|
controller := &Controller{manager: &fakeConfigManager{}}
|
||||||
|
body := []byte(`{"name":"p2","type":"tcp","tcp":{"localPort":10080}}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/p1", bytes.NewReader(body))
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "p1"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
_, err := controller.UpdateStoreProxy(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
assertHTTPCode(t, err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListStoreProxiesReturnsSortedPayload(t *testing.T) {
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
listStoreProxiesFn: func() ([]v1.ProxyConfigurer, error) {
|
||||||
|
b := newRawTCPProxyConfig("b")
|
||||||
|
a := newRawTCPProxyConfig("a")
|
||||||
|
return []v1.ProxyConfigurer{b, a}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/api/store/proxies", nil))
|
||||||
|
|
||||||
|
resp, err := controller.ListStoreProxies(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list store proxies: %v", err)
|
||||||
|
}
|
||||||
|
out, ok := resp.(model.ProxyListResp)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if len(out.Proxies) != 2 {
|
||||||
|
t.Fatalf("unexpected proxy count: %d", len(out.Proxies))
|
||||||
|
}
|
||||||
|
if out.Proxies[0].Name != "a" || out.Proxies[1].Name != "b" {
|
||||||
|
t.Fatalf("proxies are not sorted by name: %#v", out.Proxies)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fmtError(sentinel error, msg string) error {
|
||||||
|
return fmt.Errorf("%w: %s", sentinel, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertHTTPCode(t *testing.T, err error, expected int) {
|
||||||
|
t.Helper()
|
||||||
|
var httpErr *httppkg.Error
|
||||||
|
if !errors.As(err, &httpErr) {
|
||||||
|
t.Fatalf("unexpected error type: %T", err)
|
||||||
|
}
|
||||||
|
if httpErr.Code != expected {
|
||||||
|
t.Fatalf("unexpected status code: got %d, want %d", httpErr.Code, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateStoreProxyReturnsTypedPayload(t *testing.T) {
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
updateStoreProxyFn: func(_ string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
||||||
|
return cfg, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
body := map[string]any{
|
||||||
|
"name": "shared-proxy",
|
||||||
|
"type": "tcp",
|
||||||
|
"tcp": map[string]any{
|
||||||
|
"localPort": 10080,
|
||||||
|
"remotePort": 7000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/shared-proxy", bytes.NewReader(data))
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "shared-proxy"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.UpdateStoreProxy(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("update store proxy: %v", err)
|
||||||
|
}
|
||||||
|
payload, ok := resp.(model.ProxyDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.TCP == nil || payload.TCP.RemotePort != 7000 {
|
||||||
|
t.Fatalf("unexpected response payload: %#v", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetProxyConfigFromManager(t *testing.T) {
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
getProxyConfigFn: func(name string) (v1.ProxyConfigurer, bool) {
|
||||||
|
if name == "ssh" {
|
||||||
|
cfg := &v1.TCPProxyConfig{
|
||||||
|
ProxyBaseConfig: v1.ProxyBaseConfig{
|
||||||
|
Name: "ssh",
|
||||||
|
Type: "tcp",
|
||||||
|
ProxyBackend: v1.ProxyBackend{
|
||||||
|
LocalPort: 22,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cfg, true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/proxy/ssh/config", nil)
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "ssh"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.GetProxyConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get proxy config: %v", err)
|
||||||
|
}
|
||||||
|
payload, ok := resp.(model.ProxyDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.Name != "ssh" || payload.Type != "tcp" || payload.TCP == nil {
|
||||||
|
t.Fatalf("unexpected payload: %#v", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetProxyConfigNotFound(t *testing.T) {
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
getProxyConfigFn: func(name string) (v1.ProxyConfigurer, bool) {
|
||||||
|
return nil, false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/proxy/missing/config", nil)
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "missing"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
_, err := controller.GetProxyConfig(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
assertHTTPCode(t, err, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetVisitorConfigFromManager(t *testing.T) {
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
getVisitorConfigFn: func(name string) (v1.VisitorConfigurer, bool) {
|
||||||
|
if name == "my-stcp" {
|
||||||
|
cfg := &v1.STCPVisitorConfig{
|
||||||
|
VisitorBaseConfig: v1.VisitorBaseConfig{
|
||||||
|
Name: "my-stcp",
|
||||||
|
Type: "stcp",
|
||||||
|
ServerName: "server1",
|
||||||
|
BindPort: 9000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cfg, true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/visitor/my-stcp/config", nil)
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "my-stcp"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
resp, err := controller.GetVisitorConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get visitor config: %v", err)
|
||||||
|
}
|
||||||
|
payload, ok := resp.(model.VisitorDefinition)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
if payload.Name != "my-stcp" || payload.Type != "stcp" || payload.STCP == nil {
|
||||||
|
t.Fatalf("unexpected payload: %#v", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetVisitorConfigNotFound(t *testing.T) {
|
||||||
|
controller := &Controller{
|
||||||
|
manager: &fakeConfigManager{
|
||||||
|
getVisitorConfigFn: func(name string) (v1.VisitorConfigurer, bool) {
|
||||||
|
return nil, false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/visitor/missing/config", nil)
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"name": "missing"})
|
||||||
|
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
_, err := controller.GetVisitorConfig(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
assertHTTPCode(t, err, http.StatusNotFound)
|
||||||
|
}
|
||||||
148
client/http/model/proxy_definition.go
Normal file
148
client/http/model/proxy_definition.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProxyDefinition struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
|
||||||
|
TCP *v1.TCPProxyConfig `json:"tcp,omitempty"`
|
||||||
|
UDP *v1.UDPProxyConfig `json:"udp,omitempty"`
|
||||||
|
HTTP *v1.HTTPProxyConfig `json:"http,omitempty"`
|
||||||
|
HTTPS *v1.HTTPSProxyConfig `json:"https,omitempty"`
|
||||||
|
TCPMux *v1.TCPMuxProxyConfig `json:"tcpmux,omitempty"`
|
||||||
|
STCP *v1.STCPProxyConfig `json:"stcp,omitempty"`
|
||||||
|
SUDP *v1.SUDPProxyConfig `json:"sudp,omitempty"`
|
||||||
|
XTCP *v1.XTCPProxyConfig `json:"xtcp,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProxyDefinition) Validate(pathName string, isUpdate bool) error {
|
||||||
|
if strings.TrimSpace(p.Name) == "" {
|
||||||
|
return fmt.Errorf("proxy name is required")
|
||||||
|
}
|
||||||
|
if !IsProxyType(p.Type) {
|
||||||
|
return fmt.Errorf("invalid proxy type: %s", p.Type)
|
||||||
|
}
|
||||||
|
if isUpdate && pathName != "" && pathName != p.Name {
|
||||||
|
return fmt.Errorf("proxy name in URL must match name in body")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, blockType, blockCount := p.activeBlock()
|
||||||
|
if blockCount != 1 {
|
||||||
|
return fmt.Errorf("exactly one proxy type block is required")
|
||||||
|
}
|
||||||
|
if blockType != p.Type {
|
||||||
|
return fmt.Errorf("proxy type block %q does not match type %q", blockType, p.Type)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProxyDefinition) ToConfigurer() (v1.ProxyConfigurer, error) {
|
||||||
|
block, _, _ := p.activeBlock()
|
||||||
|
if block == nil {
|
||||||
|
return nil, fmt.Errorf("exactly one proxy type block is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := block
|
||||||
|
cfg.GetBaseConfig().Name = p.Name
|
||||||
|
cfg.GetBaseConfig().Type = p.Type
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProxyDefinitionFromConfigurer(cfg v1.ProxyConfigurer) (ProxyDefinition, error) {
|
||||||
|
if cfg == nil {
|
||||||
|
return ProxyDefinition{}, fmt.Errorf("proxy config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
base := cfg.GetBaseConfig()
|
||||||
|
payload := ProxyDefinition{
|
||||||
|
Name: base.Name,
|
||||||
|
Type: base.Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c := cfg.(type) {
|
||||||
|
case *v1.TCPProxyConfig:
|
||||||
|
payload.TCP = c
|
||||||
|
case *v1.UDPProxyConfig:
|
||||||
|
payload.UDP = c
|
||||||
|
case *v1.HTTPProxyConfig:
|
||||||
|
payload.HTTP = c
|
||||||
|
case *v1.HTTPSProxyConfig:
|
||||||
|
payload.HTTPS = c
|
||||||
|
case *v1.TCPMuxProxyConfig:
|
||||||
|
payload.TCPMux = c
|
||||||
|
case *v1.STCPProxyConfig:
|
||||||
|
payload.STCP = c
|
||||||
|
case *v1.SUDPProxyConfig:
|
||||||
|
payload.SUDP = c
|
||||||
|
case *v1.XTCPProxyConfig:
|
||||||
|
payload.XTCP = c
|
||||||
|
default:
|
||||||
|
return ProxyDefinition{}, fmt.Errorf("unsupported proxy configurer type %T", cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProxyDefinition) activeBlock() (v1.ProxyConfigurer, string, int) {
|
||||||
|
count := 0
|
||||||
|
var block v1.ProxyConfigurer
|
||||||
|
var blockType string
|
||||||
|
|
||||||
|
if p.TCP != nil {
|
||||||
|
count++
|
||||||
|
block = p.TCP
|
||||||
|
blockType = "tcp"
|
||||||
|
}
|
||||||
|
if p.UDP != nil {
|
||||||
|
count++
|
||||||
|
block = p.UDP
|
||||||
|
blockType = "udp"
|
||||||
|
}
|
||||||
|
if p.HTTP != nil {
|
||||||
|
count++
|
||||||
|
block = p.HTTP
|
||||||
|
blockType = "http"
|
||||||
|
}
|
||||||
|
if p.HTTPS != nil {
|
||||||
|
count++
|
||||||
|
block = p.HTTPS
|
||||||
|
blockType = "https"
|
||||||
|
}
|
||||||
|
if p.TCPMux != nil {
|
||||||
|
count++
|
||||||
|
block = p.TCPMux
|
||||||
|
blockType = "tcpmux"
|
||||||
|
}
|
||||||
|
if p.STCP != nil {
|
||||||
|
count++
|
||||||
|
block = p.STCP
|
||||||
|
blockType = "stcp"
|
||||||
|
}
|
||||||
|
if p.SUDP != nil {
|
||||||
|
count++
|
||||||
|
block = p.SUDP
|
||||||
|
blockType = "sudp"
|
||||||
|
}
|
||||||
|
if p.XTCP != nil {
|
||||||
|
count++
|
||||||
|
block = p.XTCP
|
||||||
|
blockType = "xtcp"
|
||||||
|
}
|
||||||
|
|
||||||
|
return block, blockType, count
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsProxyType(typ string) bool {
|
||||||
|
switch typ {
|
||||||
|
case "tcp", "udp", "http", "https", "tcpmux", "stcp", "sudp", "xtcp":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
42
client/http/model/types.go
Normal file
42
client/http/model/types.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// Copyright 2025 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
const SourceStore = "store"
|
||||||
|
|
||||||
|
// StatusResp is the response for GET /api/status
|
||||||
|
type StatusResp map[string][]ProxyStatusResp
|
||||||
|
|
||||||
|
// ProxyStatusResp contains proxy status information
|
||||||
|
type ProxyStatusResp struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Err string `json:"err"`
|
||||||
|
LocalAddr string `json:"local_addr"`
|
||||||
|
Plugin string `json:"plugin"`
|
||||||
|
RemoteAddr string `json:"remote_addr"`
|
||||||
|
Source string `json:"source,omitempty"` // "store" or "config"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProxyListResp is the response for GET /api/store/proxies
|
||||||
|
type ProxyListResp struct {
|
||||||
|
Proxies []ProxyDefinition `json:"proxies"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VisitorListResp is the response for GET /api/store/visitors
|
||||||
|
type VisitorListResp struct {
|
||||||
|
Visitors []VisitorDefinition `json:"visitors"`
|
||||||
|
}
|
||||||
107
client/http/model/visitor_definition.go
Normal file
107
client/http/model/visitor_definition.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VisitorDefinition struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
|
||||||
|
STCP *v1.STCPVisitorConfig `json:"stcp,omitempty"`
|
||||||
|
SUDP *v1.SUDPVisitorConfig `json:"sudp,omitempty"`
|
||||||
|
XTCP *v1.XTCPVisitorConfig `json:"xtcp,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *VisitorDefinition) Validate(pathName string, isUpdate bool) error {
|
||||||
|
if strings.TrimSpace(p.Name) == "" {
|
||||||
|
return fmt.Errorf("visitor name is required")
|
||||||
|
}
|
||||||
|
if !IsVisitorType(p.Type) {
|
||||||
|
return fmt.Errorf("invalid visitor type: %s", p.Type)
|
||||||
|
}
|
||||||
|
if isUpdate && pathName != "" && pathName != p.Name {
|
||||||
|
return fmt.Errorf("visitor name in URL must match name in body")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, blockType, blockCount := p.activeBlock()
|
||||||
|
if blockCount != 1 {
|
||||||
|
return fmt.Errorf("exactly one visitor type block is required")
|
||||||
|
}
|
||||||
|
if blockType != p.Type {
|
||||||
|
return fmt.Errorf("visitor type block %q does not match type %q", blockType, p.Type)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *VisitorDefinition) ToConfigurer() (v1.VisitorConfigurer, error) {
|
||||||
|
block, _, _ := p.activeBlock()
|
||||||
|
if block == nil {
|
||||||
|
return nil, fmt.Errorf("exactly one visitor type block is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := block
|
||||||
|
cfg.GetBaseConfig().Name = p.Name
|
||||||
|
cfg.GetBaseConfig().Type = p.Type
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func VisitorDefinitionFromConfigurer(cfg v1.VisitorConfigurer) (VisitorDefinition, error) {
|
||||||
|
if cfg == nil {
|
||||||
|
return VisitorDefinition{}, fmt.Errorf("visitor config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
base := cfg.GetBaseConfig()
|
||||||
|
payload := VisitorDefinition{
|
||||||
|
Name: base.Name,
|
||||||
|
Type: base.Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c := cfg.(type) {
|
||||||
|
case *v1.STCPVisitorConfig:
|
||||||
|
payload.STCP = c
|
||||||
|
case *v1.SUDPVisitorConfig:
|
||||||
|
payload.SUDP = c
|
||||||
|
case *v1.XTCPVisitorConfig:
|
||||||
|
payload.XTCP = c
|
||||||
|
default:
|
||||||
|
return VisitorDefinition{}, fmt.Errorf("unsupported visitor configurer type %T", cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *VisitorDefinition) activeBlock() (v1.VisitorConfigurer, string, int) {
|
||||||
|
count := 0
|
||||||
|
var block v1.VisitorConfigurer
|
||||||
|
var blockType string
|
||||||
|
|
||||||
|
if p.STCP != nil {
|
||||||
|
count++
|
||||||
|
block = p.STCP
|
||||||
|
blockType = "stcp"
|
||||||
|
}
|
||||||
|
if p.SUDP != nil {
|
||||||
|
count++
|
||||||
|
block = p.SUDP
|
||||||
|
blockType = "sudp"
|
||||||
|
}
|
||||||
|
if p.XTCP != nil {
|
||||||
|
count++
|
||||||
|
block = p.XTCP
|
||||||
|
blockType = "xtcp"
|
||||||
|
}
|
||||||
|
return block, blockType, count
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsVisitorType(typ string) bool {
|
||||||
|
switch typ {
|
||||||
|
case "stcp", "sudp", "xtcp":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
47
client/proxy/general_tcp.go
Normal file
47
client/proxy/general_tcp.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright 2023 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
pxyConfs := []v1.ProxyConfigurer{
|
||||||
|
&v1.TCPProxyConfig{},
|
||||||
|
&v1.HTTPProxyConfig{},
|
||||||
|
&v1.HTTPSProxyConfig{},
|
||||||
|
&v1.STCPProxyConfig{},
|
||||||
|
&v1.TCPMuxProxyConfig{},
|
||||||
|
}
|
||||||
|
for _, cfg := range pxyConfs {
|
||||||
|
RegisterProxyFactory(reflect.TypeOf(cfg), NewGeneralTCPProxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneralTCPProxy is a general implementation of Proxy interface for TCP protocol.
|
||||||
|
// If the default GeneralTCPProxy cannot meet the requirements, you can customize
|
||||||
|
// the implementation of the Proxy interface.
|
||||||
|
type GeneralTCPProxy struct {
|
||||||
|
*BaseProxy
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGeneralTCPProxy(baseProxy *BaseProxy, _ v1.ProxyConfigurer) Proxy {
|
||||||
|
return &GeneralTCPProxy{
|
||||||
|
BaseProxy: baseProxy,
|
||||||
|
}
|
||||||
|
}
|
||||||
240
client/proxy/proxy.go
Normal file
240
client/proxy/proxy.go
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
// Copyright 2017 fatedier, fatedier@gmail.com
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
libio "github.com/fatedier/golib/io"
|
||||||
|
libnet "github.com/fatedier/golib/net"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/config/types"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
plugin "github.com/fatedier/frp/pkg/plugin/client"
|
||||||
|
"github.com/fatedier/frp/pkg/transport"
|
||||||
|
"github.com/fatedier/frp/pkg/util/limit"
|
||||||
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
|
)
|
||||||
|
|
||||||
|
var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, v1.ProxyConfigurer) Proxy{}
|
||||||
|
|
||||||
|
func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy, v1.ProxyConfigurer) Proxy) {
|
||||||
|
proxyFactoryRegistry[proxyConfType] = factory
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proxy defines how to handle work connections for different proxy type.
|
||||||
|
type Proxy interface {
|
||||||
|
Run() error
|
||||||
|
// InWorkConn accept work connections registered to server.
|
||||||
|
InWorkConn(net.Conn, *msg.StartWorkConn)
|
||||||
|
SetInWorkConnCallback(func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) /* continue */ bool)
|
||||||
|
Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProxy(
|
||||||
|
ctx context.Context,
|
||||||
|
pxyConf v1.ProxyConfigurer,
|
||||||
|
clientCfg *v1.ClientCommonConfig,
|
||||||
|
encryptionKey []byte,
|
||||||
|
msgTransporter transport.MessageTransporter,
|
||||||
|
vnetController *vnet.Controller,
|
||||||
|
) (pxy Proxy) {
|
||||||
|
var limiter *rate.Limiter
|
||||||
|
limitBytes := pxyConf.GetBaseConfig().Transport.BandwidthLimit.Bytes()
|
||||||
|
if limitBytes > 0 && pxyConf.GetBaseConfig().Transport.BandwidthLimitMode == types.BandwidthLimitModeClient {
|
||||||
|
limiter = rate.NewLimiter(rate.Limit(float64(limitBytes)), int(limitBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
baseProxy := BaseProxy{
|
||||||
|
baseCfg: pxyConf.GetBaseConfig(),
|
||||||
|
clientCfg: clientCfg,
|
||||||
|
encryptionKey: encryptionKey,
|
||||||
|
limiter: limiter,
|
||||||
|
msgTransporter: msgTransporter,
|
||||||
|
vnetController: vnetController,
|
||||||
|
xl: xlog.FromContextSafe(ctx),
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
|
||||||
|
factory := proxyFactoryRegistry[reflect.TypeOf(pxyConf)]
|
||||||
|
if factory == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return factory(&baseProxy, pxyConf)
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseProxy struct {
|
||||||
|
baseCfg *v1.ProxyBaseConfig
|
||||||
|
clientCfg *v1.ClientCommonConfig
|
||||||
|
encryptionKey []byte
|
||||||
|
msgTransporter transport.MessageTransporter
|
||||||
|
vnetController *vnet.Controller
|
||||||
|
limiter *rate.Limiter
|
||||||
|
// proxyPlugin is used to handle connections instead of dialing to local service.
|
||||||
|
// It's only validate for TCP protocol now.
|
||||||
|
proxyPlugin plugin.Plugin
|
||||||
|
inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) /* continue */ bool
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
xl *xlog.Logger
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pxy *BaseProxy) Run() error {
|
||||||
|
if pxy.baseCfg.Plugin.Type != "" {
|
||||||
|
p, err := plugin.Create(pxy.baseCfg.Plugin.Type, plugin.PluginContext{
|
||||||
|
Name: pxy.baseCfg.Name,
|
||||||
|
VnetController: pxy.vnetController,
|
||||||
|
}, pxy.baseCfg.Plugin.ClientPluginOptions)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pxy.proxyPlugin = p
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pxy *BaseProxy) Close() {
|
||||||
|
if pxy.proxyPlugin != nil {
|
||||||
|
pxy.proxyPlugin.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrapWorkConn applies rate limiting, encryption, and compression
|
||||||
|
// to a work connection based on the proxy's transport configuration.
|
||||||
|
// The returned recycle function should be called when the stream is no longer in use
|
||||||
|
// to return compression resources to the pool. It is safe to not call recycle,
|
||||||
|
// in which case resources will be garbage collected normally.
|
||||||
|
func (pxy *BaseProxy) wrapWorkConn(conn net.Conn, encKey []byte) (io.ReadWriteCloser, func(), error) {
|
||||||
|
var rwc io.ReadWriteCloser = conn
|
||||||
|
if pxy.limiter != nil {
|
||||||
|
rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
|
||||||
|
return conn.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if pxy.baseCfg.Transport.UseEncryption {
|
||||||
|
var err error
|
||||||
|
rwc, err = libio.WithEncryption(rwc, encKey)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, nil, fmt.Errorf("create encryption stream error: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var recycleFn func()
|
||||||
|
if pxy.baseCfg.Transport.UseCompression {
|
||||||
|
rwc, recycleFn = libio.WithCompressionFromPool(rwc)
|
||||||
|
}
|
||||||
|
return rwc, recycleFn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pxy *BaseProxy) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
|
||||||
|
pxy.inWorkConnCallback = cb
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
|
||||||
|
if pxy.inWorkConnCallback != nil {
|
||||||
|
if !pxy.inWorkConnCallback(pxy.baseCfg, conn, m) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pxy.HandleTCPWorkConnection(conn, m, pxy.encryptionKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common handler for tcp work connections.
|
||||||
|
func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWorkConn, encKey []byte) {
|
||||||
|
xl := pxy.xl
|
||||||
|
baseCfg := pxy.baseCfg
|
||||||
|
|
||||||
|
xl.Tracef("handle tcp work connection, useEncryption: %t, useCompression: %t",
|
||||||
|
baseCfg.Transport.UseEncryption, baseCfg.Transport.UseCompression)
|
||||||
|
|
||||||
|
remote, recycleFn, err := pxy.wrapWorkConn(workConn, encKey)
|
||||||
|
if err != nil {
|
||||||
|
xl.Errorf("wrap work connection: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we need to send proxy protocol info
|
||||||
|
var connInfo plugin.ConnectionInfo
|
||||||
|
if m.SrcAddr != "" && m.SrcPort != 0 {
|
||||||
|
if m.DstAddr == "" {
|
||||||
|
m.DstAddr = "127.0.0.1"
|
||||||
|
}
|
||||||
|
srcAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.SrcAddr, strconv.Itoa(int(m.SrcPort))))
|
||||||
|
dstAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.DstAddr, strconv.Itoa(int(m.DstPort))))
|
||||||
|
connInfo.SrcAddr = srcAddr
|
||||||
|
connInfo.DstAddr = dstAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 {
|
||||||
|
header := netpkg.BuildProxyProtocolHeaderStruct(connInfo.SrcAddr, connInfo.DstAddr, baseCfg.Transport.ProxyProtocolVersion)
|
||||||
|
connInfo.ProxyProtocolHeader = header
|
||||||
|
}
|
||||||
|
connInfo.Conn = remote
|
||||||
|
connInfo.UnderlyingConn = workConn
|
||||||
|
|
||||||
|
if pxy.proxyPlugin != nil {
|
||||||
|
// if plugin is set, let plugin handle connection first
|
||||||
|
// Don't recycle compression resources here because plugins may
|
||||||
|
// retain the connection after Handle returns.
|
||||||
|
xl.Debugf("handle by plugin: %s", pxy.proxyPlugin.Name())
|
||||||
|
pxy.proxyPlugin.Handle(pxy.ctx, &connInfo)
|
||||||
|
xl.Debugf("handle by plugin finished")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if recycleFn != nil {
|
||||||
|
defer recycleFn()
|
||||||
|
}
|
||||||
|
|
||||||
|
localConn, err := libnet.Dial(
|
||||||
|
net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)),
|
||||||
|
libnet.WithTimeout(10*time.Second),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
workConn.Close()
|
||||||
|
xl.Errorf("connect to local service [%s:%d] error: %v", baseCfg.LocalIP, baseCfg.LocalPort, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
xl.Debugf("join connections, localConn(l[%s] r[%s]) workConn(l[%s] r[%s])", localConn.LocalAddr().String(),
|
||||||
|
localConn.RemoteAddr().String(), workConn.LocalAddr().String(), workConn.RemoteAddr().String())
|
||||||
|
|
||||||
|
if connInfo.ProxyProtocolHeader != nil {
|
||||||
|
if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
|
||||||
|
workConn.Close()
|
||||||
|
localConn.Close()
|
||||||
|
xl.Errorf("write proxy protocol header to local conn error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, errs := libio.Join(localConn, remote)
|
||||||
|
xl.Debugf("join connections closed")
|
||||||
|
if len(errs) > 0 {
|
||||||
|
xl.Tracef("join connections errors: %v", errs)
|
||||||
|
}
|
||||||
|
}
|
||||||
182
client/proxy/proxy_manager.go
Normal file
182
client/proxy/proxy_manager.go
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
// Copyright 2023 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/samber/lo"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/client/event"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
"github.com/fatedier/frp/pkg/transport"
|
||||||
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
proxies map[string]*Wrapper
|
||||||
|
msgTransporter transport.MessageTransporter
|
||||||
|
inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
|
||||||
|
vnetController *vnet.Controller
|
||||||
|
|
||||||
|
closed bool
|
||||||
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
encryptionKey []byte
|
||||||
|
clientCfg *v1.ClientCommonConfig
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager(
|
||||||
|
ctx context.Context,
|
||||||
|
clientCfg *v1.ClientCommonConfig,
|
||||||
|
encryptionKey []byte,
|
||||||
|
msgTransporter transport.MessageTransporter,
|
||||||
|
vnetController *vnet.Controller,
|
||||||
|
) *Manager {
|
||||||
|
return &Manager{
|
||||||
|
proxies: make(map[string]*Wrapper),
|
||||||
|
msgTransporter: msgTransporter,
|
||||||
|
vnetController: vnetController,
|
||||||
|
closed: false,
|
||||||
|
encryptionKey: encryptionKey,
|
||||||
|
clientCfg: clientCfg,
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *Manager) StartProxy(name string, remoteAddr string, serverRespErr string) error {
|
||||||
|
pm.mu.RLock()
|
||||||
|
pxy, ok := pm.proxies[name]
|
||||||
|
pm.mu.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("proxy [%s] not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := pxy.SetRunningStatus(remoteAddr, serverRespErr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *Manager) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
|
||||||
|
pm.inWorkConnCallback = cb
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *Manager) Close() {
|
||||||
|
pm.mu.Lock()
|
||||||
|
defer pm.mu.Unlock()
|
||||||
|
for _, pxy := range pm.proxies {
|
||||||
|
pxy.Stop()
|
||||||
|
}
|
||||||
|
pm.proxies = make(map[string]*Wrapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *Manager) HandleWorkConn(name string, workConn net.Conn, m *msg.StartWorkConn) {
|
||||||
|
pm.mu.RLock()
|
||||||
|
pw, ok := pm.proxies[name]
|
||||||
|
pm.mu.RUnlock()
|
||||||
|
if ok {
|
||||||
|
pw.InWorkConn(workConn, m)
|
||||||
|
} else {
|
||||||
|
workConn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *Manager) HandleEvent(payload any) error {
|
||||||
|
var m msg.Message
|
||||||
|
switch e := payload.(type) {
|
||||||
|
case *event.StartProxyPayload:
|
||||||
|
m = e.NewProxyMsg
|
||||||
|
case *event.CloseProxyPayload:
|
||||||
|
m = e.CloseProxyMsg
|
||||||
|
default:
|
||||||
|
return event.ErrPayloadType
|
||||||
|
}
|
||||||
|
|
||||||
|
return pm.msgTransporter.Send(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *Manager) GetAllProxyStatus() []*WorkingStatus {
|
||||||
|
pm.mu.RLock()
|
||||||
|
defer pm.mu.RUnlock()
|
||||||
|
ps := make([]*WorkingStatus, 0, len(pm.proxies))
|
||||||
|
for _, pxy := range pm.proxies {
|
||||||
|
ps = append(ps, pxy.GetStatus())
|
||||||
|
}
|
||||||
|
return ps
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *Manager) GetProxyStatus(name string) (*WorkingStatus, bool) {
|
||||||
|
pm.mu.RLock()
|
||||||
|
defer pm.mu.RUnlock()
|
||||||
|
if pxy, ok := pm.proxies[name]; ok {
|
||||||
|
return pxy.GetStatus(), true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) {
|
||||||
|
xl := xlog.FromContextSafe(pm.ctx)
|
||||||
|
proxyCfgsMap := lo.KeyBy(proxyCfgs, func(c v1.ProxyConfigurer) string {
|
||||||
|
return c.GetBaseConfig().Name
|
||||||
|
})
|
||||||
|
pm.mu.Lock()
|
||||||
|
defer pm.mu.Unlock()
|
||||||
|
|
||||||
|
delPxyNames := make([]string, 0)
|
||||||
|
for name, pxy := range pm.proxies {
|
||||||
|
del := false
|
||||||
|
cfg, ok := proxyCfgsMap[name]
|
||||||
|
if !ok || !reflect.DeepEqual(pxy.Cfg, cfg) {
|
||||||
|
del = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if del {
|
||||||
|
delPxyNames = append(delPxyNames, name)
|
||||||
|
delete(pm.proxies, name)
|
||||||
|
pxy.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(delPxyNames) > 0 {
|
||||||
|
xl.Infof("proxy removed: %s", delPxyNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
addPxyNames := make([]string, 0)
|
||||||
|
for _, cfg := range proxyCfgs {
|
||||||
|
name := cfg.GetBaseConfig().Name
|
||||||
|
if _, ok := pm.proxies[name]; !ok {
|
||||||
|
pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.encryptionKey, pm.HandleEvent, pm.msgTransporter, pm.vnetController)
|
||||||
|
if pm.inWorkConnCallback != nil {
|
||||||
|
pxy.SetInWorkConnCallback(pm.inWorkConnCallback)
|
||||||
|
}
|
||||||
|
pm.proxies[name] = pxy
|
||||||
|
addPxyNames = append(addPxyNames, name)
|
||||||
|
|
||||||
|
pxy.Start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(addPxyNames) > 0 {
|
||||||
|
xl.Infof("proxy added: %s", addPxyNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
290
client/proxy/proxy_wrapper.go
Normal file
290
client/proxy/proxy_wrapper.go
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
// Copyright 2023 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatedier/golib/errors"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/client/event"
|
||||||
|
"github.com/fatedier/frp/client/health"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
"github.com/fatedier/frp/pkg/naming"
|
||||||
|
"github.com/fatedier/frp/pkg/transport"
|
||||||
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProxyPhaseNew = "new"
|
||||||
|
ProxyPhaseWaitStart = "wait start"
|
||||||
|
ProxyPhaseStartErr = "start error"
|
||||||
|
ProxyPhaseRunning = "running"
|
||||||
|
ProxyPhaseCheckFailed = "check failed"
|
||||||
|
ProxyPhaseClosed = "closed"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
statusCheckInterval = 3 * time.Second
|
||||||
|
waitResponseTimeout = 20 * time.Second
|
||||||
|
startErrTimeout = 30 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
type WorkingStatus struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Phase string `json:"status"`
|
||||||
|
Err string `json:"err"`
|
||||||
|
Cfg v1.ProxyConfigurer `json:"cfg"`
|
||||||
|
|
||||||
|
// Got from server.
|
||||||
|
RemoteAddr string `json:"remote_addr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Wrapper struct {
|
||||||
|
WorkingStatus
|
||||||
|
|
||||||
|
// underlying proxy
|
||||||
|
pxy Proxy
|
||||||
|
|
||||||
|
// if ProxyConf has healcheck config
|
||||||
|
// monitor will watch if it is alive
|
||||||
|
monitor *health.Monitor
|
||||||
|
|
||||||
|
// event handler
|
||||||
|
handler event.Handler
|
||||||
|
|
||||||
|
msgTransporter transport.MessageTransporter
|
||||||
|
// vnet controller
|
||||||
|
vnetController *vnet.Controller
|
||||||
|
|
||||||
|
health uint32
|
||||||
|
lastSendStartMsg time.Time
|
||||||
|
lastStartErr time.Time
|
||||||
|
closeCh chan struct{}
|
||||||
|
healthNotifyCh chan struct{}
|
||||||
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
xl *xlog.Logger
|
||||||
|
ctx context.Context
|
||||||
|
|
||||||
|
wireName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWrapper(
|
||||||
|
ctx context.Context,
|
||||||
|
cfg v1.ProxyConfigurer,
|
||||||
|
clientCfg *v1.ClientCommonConfig,
|
||||||
|
encryptionKey []byte,
|
||||||
|
eventHandler event.Handler,
|
||||||
|
msgTransporter transport.MessageTransporter,
|
||||||
|
vnetController *vnet.Controller,
|
||||||
|
) *Wrapper {
|
||||||
|
baseInfo := cfg.GetBaseConfig()
|
||||||
|
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.Name)
|
||||||
|
pw := &Wrapper{
|
||||||
|
WorkingStatus: WorkingStatus{
|
||||||
|
Name: baseInfo.Name,
|
||||||
|
Type: baseInfo.Type,
|
||||||
|
Phase: ProxyPhaseNew,
|
||||||
|
Cfg: cfg,
|
||||||
|
},
|
||||||
|
closeCh: make(chan struct{}),
|
||||||
|
healthNotifyCh: make(chan struct{}),
|
||||||
|
handler: eventHandler,
|
||||||
|
msgTransporter: msgTransporter,
|
||||||
|
vnetController: vnetController,
|
||||||
|
xl: xl,
|
||||||
|
ctx: xlog.NewContext(ctx, xl),
|
||||||
|
wireName: naming.AddUserPrefix(clientCfg.User, baseInfo.Name),
|
||||||
|
}
|
||||||
|
|
||||||
|
if baseInfo.HealthCheck.Type != "" && baseInfo.LocalPort > 0 {
|
||||||
|
pw.health = 1 // means failed
|
||||||
|
addr := net.JoinHostPort(baseInfo.LocalIP, strconv.Itoa(baseInfo.LocalPort))
|
||||||
|
pw.monitor = health.NewMonitor(pw.ctx, baseInfo.HealthCheck, addr,
|
||||||
|
pw.statusNormalCallback, pw.statusFailedCallback)
|
||||||
|
xl.Tracef("enable health check monitor")
|
||||||
|
}
|
||||||
|
|
||||||
|
pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, encryptionKey, pw.msgTransporter, pw.vnetController)
|
||||||
|
return pw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pw *Wrapper) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
|
||||||
|
pw.pxy.SetInWorkConnCallback(cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pw *Wrapper) SetRunningStatus(remoteAddr string, respErr string) error {
|
||||||
|
pw.mu.Lock()
|
||||||
|
defer pw.mu.Unlock()
|
||||||
|
if pw.Phase != ProxyPhaseWaitStart {
|
||||||
|
return fmt.Errorf("status not wait start, ignore start message")
|
||||||
|
}
|
||||||
|
|
||||||
|
pw.RemoteAddr = remoteAddr
|
||||||
|
if respErr != "" {
|
||||||
|
pw.Phase = ProxyPhaseStartErr
|
||||||
|
pw.Err = respErr
|
||||||
|
pw.lastStartErr = time.Now()
|
||||||
|
return fmt.Errorf("%s", pw.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pw.pxy.Run(); err != nil {
|
||||||
|
pw.close()
|
||||||
|
pw.Phase = ProxyPhaseStartErr
|
||||||
|
pw.Err = err.Error()
|
||||||
|
pw.lastStartErr = time.Now()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pw.Phase = ProxyPhaseRunning
|
||||||
|
pw.Err = ""
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pw *Wrapper) Start() {
|
||||||
|
go pw.checkWorker()
|
||||||
|
if pw.monitor != nil {
|
||||||
|
go pw.monitor.Start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pw *Wrapper) Stop() {
|
||||||
|
pw.mu.Lock()
|
||||||
|
defer pw.mu.Unlock()
|
||||||
|
close(pw.closeCh)
|
||||||
|
close(pw.healthNotifyCh)
|
||||||
|
pw.pxy.Close()
|
||||||
|
if pw.monitor != nil {
|
||||||
|
pw.monitor.Stop()
|
||||||
|
}
|
||||||
|
pw.Phase = ProxyPhaseClosed
|
||||||
|
pw.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pw *Wrapper) close() {
|
||||||
|
_ = pw.handler(&event.CloseProxyPayload{
|
||||||
|
CloseProxyMsg: &msg.CloseProxy{
|
||||||
|
ProxyName: pw.wireName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pw *Wrapper) checkWorker() {
|
||||||
|
xl := pw.xl
|
||||||
|
if pw.monitor != nil {
|
||||||
|
// let monitor do check request first
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
// check proxy status
|
||||||
|
now := time.Now()
|
||||||
|
if atomic.LoadUint32(&pw.health) == 0 {
|
||||||
|
pw.mu.Lock()
|
||||||
|
if pw.Phase == ProxyPhaseNew ||
|
||||||
|
pw.Phase == ProxyPhaseCheckFailed ||
|
||||||
|
(pw.Phase == ProxyPhaseWaitStart && now.After(pw.lastSendStartMsg.Add(waitResponseTimeout))) ||
|
||||||
|
(pw.Phase == ProxyPhaseStartErr && now.After(pw.lastStartErr.Add(startErrTimeout))) {
|
||||||
|
|
||||||
|
xl.Tracef("change status from [%s] to [%s]", pw.Phase, ProxyPhaseWaitStart)
|
||||||
|
pw.Phase = ProxyPhaseWaitStart
|
||||||
|
|
||||||
|
var newProxyMsg msg.NewProxy
|
||||||
|
pw.Cfg.MarshalToMsg(&newProxyMsg)
|
||||||
|
newProxyMsg.ProxyName = pw.wireName
|
||||||
|
pw.lastSendStartMsg = now
|
||||||
|
_ = pw.handler(&event.StartProxyPayload{
|
||||||
|
NewProxyMsg: &newProxyMsg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pw.mu.Unlock()
|
||||||
|
} else {
|
||||||
|
pw.mu.Lock()
|
||||||
|
if pw.Phase == ProxyPhaseRunning || pw.Phase == ProxyPhaseWaitStart {
|
||||||
|
pw.close()
|
||||||
|
xl.Tracef("change status from [%s] to [%s]", pw.Phase, ProxyPhaseCheckFailed)
|
||||||
|
pw.Phase = ProxyPhaseCheckFailed
|
||||||
|
}
|
||||||
|
pw.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-pw.closeCh:
|
||||||
|
return
|
||||||
|
case <-time.After(statusCheckInterval):
|
||||||
|
case <-pw.healthNotifyCh:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pw *Wrapper) statusNormalCallback() {
|
||||||
|
xl := pw.xl
|
||||||
|
atomic.StoreUint32(&pw.health, 0)
|
||||||
|
_ = errors.PanicToError(func() {
|
||||||
|
select {
|
||||||
|
case pw.healthNotifyCh <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
})
|
||||||
|
xl.Infof("health check success")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pw *Wrapper) statusFailedCallback() {
|
||||||
|
xl := pw.xl
|
||||||
|
atomic.StoreUint32(&pw.health, 1)
|
||||||
|
_ = errors.PanicToError(func() {
|
||||||
|
select {
|
||||||
|
case pw.healthNotifyCh <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
})
|
||||||
|
xl.Infof("health check failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pw *Wrapper) InWorkConn(workConn net.Conn, m *msg.StartWorkConn) {
|
||||||
|
xl := pw.xl
|
||||||
|
pw.mu.RLock()
|
||||||
|
pxy := pw.pxy
|
||||||
|
pw.mu.RUnlock()
|
||||||
|
if pxy != nil && pw.Phase == ProxyPhaseRunning {
|
||||||
|
xl.Debugf("start a new work connection, localAddr: %s remoteAddr: %s", workConn.LocalAddr().String(), workConn.RemoteAddr().String())
|
||||||
|
go pxy.InWorkConn(workConn, m)
|
||||||
|
} else {
|
||||||
|
workConn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pw *Wrapper) GetStatus() *WorkingStatus {
|
||||||
|
pw.mu.RLock()
|
||||||
|
defer pw.mu.RUnlock()
|
||||||
|
ps := &WorkingStatus{
|
||||||
|
Name: pw.Name,
|
||||||
|
Type: pw.Type,
|
||||||
|
Phase: pw.Phase,
|
||||||
|
Err: pw.Err,
|
||||||
|
Cfg: pw.Cfg,
|
||||||
|
RemoteAddr: pw.RemoteAddr,
|
||||||
|
}
|
||||||
|
return ps
|
||||||
|
}
|
||||||
192
client/proxy/sudp.go
Normal file
192
client/proxy/sudp.go
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
// Copyright 2023 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//go:build !frps
|
||||||
|
|
||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatedier/golib/errors"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
"github.com/fatedier/frp/pkg/proto/udp"
|
||||||
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterProxyFactory(reflect.TypeFor[*v1.SUDPProxyConfig](), NewSUDPProxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SUDPProxy struct {
|
||||||
|
*BaseProxy
|
||||||
|
|
||||||
|
cfg *v1.SUDPProxyConfig
|
||||||
|
|
||||||
|
localAddr *net.UDPAddr
|
||||||
|
|
||||||
|
closeCh chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSUDPProxy(baseProxy *BaseProxy, cfg v1.ProxyConfigurer) Proxy {
|
||||||
|
unwrapped, ok := cfg.(*v1.SUDPProxyConfig)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &SUDPProxy{
|
||||||
|
BaseProxy: baseProxy,
|
||||||
|
cfg: unwrapped,
|
||||||
|
closeCh: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pxy *SUDPProxy) Run() (err error) {
|
||||||
|
pxy.localAddr, err = net.ResolveUDPAddr("udp", net.JoinHostPort(pxy.cfg.LocalIP, strconv.Itoa(pxy.cfg.LocalPort)))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pxy *SUDPProxy) Close() {
|
||||||
|
pxy.mu.Lock()
|
||||||
|
defer pxy.mu.Unlock()
|
||||||
|
select {
|
||||||
|
case <-pxy.closeCh:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
close(pxy.closeCh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
||||||
|
xl := pxy.xl
|
||||||
|
xl.Infof("incoming a new work connection for sudp proxy, %s", conn.RemoteAddr().String())
|
||||||
|
|
||||||
|
remote, _, err := pxy.wrapWorkConn(conn, pxy.encryptionKey)
|
||||||
|
if err != nil {
|
||||||
|
xl.Errorf("wrap work connection: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
workConn := netpkg.WrapReadWriteCloserToConn(remote, conn)
|
||||||
|
readCh := make(chan *msg.UDPPacket, 1024)
|
||||||
|
sendCh := make(chan msg.Message, 1024)
|
||||||
|
isClose := false
|
||||||
|
|
||||||
|
mu := &sync.Mutex{}
|
||||||
|
|
||||||
|
closeFn := func() {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if isClose {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isClose = true
|
||||||
|
if workConn != nil {
|
||||||
|
workConn.Close()
|
||||||
|
}
|
||||||
|
close(readCh)
|
||||||
|
close(sendCh)
|
||||||
|
}
|
||||||
|
|
||||||
|
// udp service <- frpc <- frps <- frpc visitor <- user
|
||||||
|
workConnReaderFn := func(conn net.Conn, readCh chan *msg.UDPPacket) {
|
||||||
|
defer closeFn()
|
||||||
|
|
||||||
|
for {
|
||||||
|
// first to check sudp proxy is closed or not
|
||||||
|
select {
|
||||||
|
case <-pxy.closeCh:
|
||||||
|
xl.Tracef("frpc sudp proxy is closed")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
var udpMsg msg.UDPPacket
|
||||||
|
if errRet := msg.ReadMsgInto(conn, &udpMsg); errRet != nil {
|
||||||
|
xl.Warnf("read from workConn for sudp error: %v", errRet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if errRet := errors.PanicToError(func() {
|
||||||
|
readCh <- &udpMsg
|
||||||
|
}); errRet != nil {
|
||||||
|
xl.Warnf("reader goroutine for sudp work connection closed: %v", errRet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// udp service -> frpc -> frps -> frpc visitor -> user
|
||||||
|
workConnSenderFn := func(conn net.Conn, sendCh chan msg.Message) {
|
||||||
|
defer func() {
|
||||||
|
closeFn()
|
||||||
|
xl.Infof("writer goroutine for sudp work connection closed")
|
||||||
|
}()
|
||||||
|
|
||||||
|
var errRet error
|
||||||
|
for rawMsg := range sendCh {
|
||||||
|
switch m := rawMsg.(type) {
|
||||||
|
case *msg.UDPPacket:
|
||||||
|
xl.Tracef("frpc send udp package to frpc visitor, [udp local: %v, remote: %v], [tcp work conn local: %v, remote: %v]",
|
||||||
|
m.LocalAddr.String(), m.RemoteAddr.String(), conn.LocalAddr().String(), conn.RemoteAddr().String())
|
||||||
|
case *msg.Ping:
|
||||||
|
xl.Tracef("frpc send ping message to frpc visitor")
|
||||||
|
}
|
||||||
|
|
||||||
|
if errRet = msg.WriteMsg(conn, rawMsg); errRet != nil {
|
||||||
|
xl.Errorf("sudp work write error: %v", errRet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
heartbeatFn := func(sendCh chan msg.Message) {
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer func() {
|
||||||
|
ticker.Stop()
|
||||||
|
closeFn()
|
||||||
|
}()
|
||||||
|
|
||||||
|
var errRet error
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if errRet = errors.PanicToError(func() {
|
||||||
|
sendCh <- &msg.Ping{}
|
||||||
|
}); errRet != nil {
|
||||||
|
xl.Warnf("heartbeat goroutine for sudp work connection closed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-pxy.closeCh:
|
||||||
|
xl.Tracef("frpc sudp proxy is closed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go workConnSenderFn(workConn, sendCh)
|
||||||
|
go workConnReaderFn(workConn, readCh)
|
||||||
|
go heartbeatFn(sendCh)
|
||||||
|
|
||||||
|
udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize), pxy.cfg.Transport.ProxyProtocolVersion)
|
||||||
|
}
|
||||||
160
client/proxy/udp.go
Normal file
160
client/proxy/udp.go
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
// Copyright 2023 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//go:build !frps
|
||||||
|
|
||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatedier/golib/errors"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
"github.com/fatedier/frp/pkg/proto/udp"
|
||||||
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterProxyFactory(reflect.TypeFor[*v1.UDPProxyConfig](), NewUDPProxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
type UDPProxy struct {
|
||||||
|
*BaseProxy
|
||||||
|
|
||||||
|
cfg *v1.UDPProxyConfig
|
||||||
|
|
||||||
|
localAddr *net.UDPAddr
|
||||||
|
readCh chan *msg.UDPPacket
|
||||||
|
|
||||||
|
// include msg.UDPPacket and msg.Ping
|
||||||
|
sendCh chan msg.Message
|
||||||
|
workConn net.Conn
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUDPProxy(baseProxy *BaseProxy, cfg v1.ProxyConfigurer) Proxy {
|
||||||
|
unwrapped, ok := cfg.(*v1.UDPProxyConfig)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &UDPProxy{
|
||||||
|
BaseProxy: baseProxy,
|
||||||
|
cfg: unwrapped,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pxy *UDPProxy) Run() (err error) {
|
||||||
|
pxy.localAddr, err = net.ResolveUDPAddr("udp", net.JoinHostPort(pxy.cfg.LocalIP, strconv.Itoa(pxy.cfg.LocalPort)))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pxy *UDPProxy) Close() {
|
||||||
|
pxy.mu.Lock()
|
||||||
|
defer pxy.mu.Unlock()
|
||||||
|
|
||||||
|
if !pxy.closed {
|
||||||
|
pxy.closed = true
|
||||||
|
if pxy.workConn != nil {
|
||||||
|
pxy.workConn.Close()
|
||||||
|
}
|
||||||
|
if pxy.readCh != nil {
|
||||||
|
close(pxy.readCh)
|
||||||
|
}
|
||||||
|
if pxy.sendCh != nil {
|
||||||
|
close(pxy.sendCh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
||||||
|
xl := pxy.xl
|
||||||
|
xl.Infof("incoming a new work connection for udp proxy, %s", conn.RemoteAddr().String())
|
||||||
|
// close resources related with old workConn
|
||||||
|
pxy.Close()
|
||||||
|
|
||||||
|
remote, _, err := pxy.wrapWorkConn(conn, pxy.encryptionKey)
|
||||||
|
if err != nil {
|
||||||
|
xl.Errorf("wrap work connection: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pxy.mu.Lock()
|
||||||
|
pxy.workConn = netpkg.WrapReadWriteCloserToConn(remote, conn)
|
||||||
|
pxy.readCh = make(chan *msg.UDPPacket, 1024)
|
||||||
|
pxy.sendCh = make(chan msg.Message, 1024)
|
||||||
|
pxy.closed = false
|
||||||
|
pxy.mu.Unlock()
|
||||||
|
|
||||||
|
workConnReaderFn := func(conn net.Conn, readCh chan *msg.UDPPacket) {
|
||||||
|
for {
|
||||||
|
var udpMsg msg.UDPPacket
|
||||||
|
if errRet := msg.ReadMsgInto(conn, &udpMsg); errRet != nil {
|
||||||
|
xl.Warnf("read from workConn for udp error: %v", errRet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if errRet := errors.PanicToError(func() {
|
||||||
|
xl.Tracef("get udp package from workConn, len: %d", len(udpMsg.Content))
|
||||||
|
readCh <- &udpMsg
|
||||||
|
}); errRet != nil {
|
||||||
|
xl.Infof("reader goroutine for udp work connection closed: %v", errRet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
workConnSenderFn := func(conn net.Conn, sendCh chan msg.Message) {
|
||||||
|
defer func() {
|
||||||
|
xl.Infof("writer goroutine for udp work connection closed")
|
||||||
|
}()
|
||||||
|
var errRet error
|
||||||
|
for rawMsg := range sendCh {
|
||||||
|
switch m := rawMsg.(type) {
|
||||||
|
case *msg.UDPPacket:
|
||||||
|
xl.Tracef("send udp package to workConn, len: %d", len(m.Content))
|
||||||
|
case *msg.Ping:
|
||||||
|
xl.Tracef("send ping message to udp workConn")
|
||||||
|
}
|
||||||
|
if errRet = msg.WriteMsg(conn, rawMsg); errRet != nil {
|
||||||
|
xl.Errorf("udp work write error: %v", errRet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
heartbeatFn := func(sendCh chan msg.Message) {
|
||||||
|
var errRet error
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Duration(30) * time.Second)
|
||||||
|
if errRet = errors.PanicToError(func() {
|
||||||
|
sendCh <- &msg.Ping{}
|
||||||
|
}); errRet != nil {
|
||||||
|
xl.Tracef("heartbeat goroutine for udp work connection closed")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go workConnSenderFn(pxy.workConn, pxy.sendCh)
|
||||||
|
go workConnReaderFn(pxy.workConn, pxy.readCh)
|
||||||
|
go heartbeatFn(pxy.sendCh)
|
||||||
|
|
||||||
|
// Call Forwarder with proxy protocol version (empty string means no proxy protocol)
|
||||||
|
udp.Forwarder(pxy.localAddr, pxy.readCh, pxy.sendCh, int(pxy.clientCfg.UDPPacketSize), pxy.cfg.Transport.ProxyProtocolVersion)
|
||||||
|
}
|
||||||
208
client/proxy/xtcp.go
Normal file
208
client/proxy/xtcp.go
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
// Copyright 2023 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//go:build !frps
|
||||||
|
|
||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
fmux "github.com/hashicorp/yamux"
|
||||||
|
"github.com/quic-go/quic-go"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
"github.com/fatedier/frp/pkg/naming"
|
||||||
|
"github.com/fatedier/frp/pkg/nathole"
|
||||||
|
"github.com/fatedier/frp/pkg/transport"
|
||||||
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterProxyFactory(reflect.TypeFor[*v1.XTCPProxyConfig](), NewXTCPProxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
type XTCPProxy struct {
|
||||||
|
*BaseProxy
|
||||||
|
|
||||||
|
cfg *v1.XTCPProxyConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewXTCPProxy(baseProxy *BaseProxy, cfg v1.ProxyConfigurer) Proxy {
|
||||||
|
unwrapped, ok := cfg.(*v1.XTCPProxyConfig)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &XTCPProxy{
|
||||||
|
BaseProxy: baseProxy,
|
||||||
|
cfg: unwrapped,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkConn) {
|
||||||
|
xl := pxy.xl
|
||||||
|
defer conn.Close()
|
||||||
|
var natHoleSidMsg msg.NatHoleSid
|
||||||
|
err := msg.ReadMsgInto(conn, &natHoleSidMsg)
|
||||||
|
if err != nil {
|
||||||
|
xl.Errorf("xtcp read from workConn error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
xl.Tracef("nathole prepare start")
|
||||||
|
|
||||||
|
// Prepare NAT traversal options
|
||||||
|
var opts nathole.PrepareOptions
|
||||||
|
if pxy.cfg.NatTraversal != nil && pxy.cfg.NatTraversal.DisableAssistedAddrs {
|
||||||
|
opts.DisableAssistedAddrs = true
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareResult, err := nathole.Prepare([]string{pxy.clientCfg.NatHoleSTUNServer}, opts)
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("nathole prepare error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v",
|
||||||
|
prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs)
|
||||||
|
defer prepareResult.ListenConn.Close()
|
||||||
|
|
||||||
|
// send NatHoleClient msg to server
|
||||||
|
transactionID := nathole.NewTransactionID()
|
||||||
|
natHoleClientMsg := &msg.NatHoleClient{
|
||||||
|
TransactionID: transactionID,
|
||||||
|
ProxyName: naming.AddUserPrefix(pxy.clientCfg.User, pxy.cfg.Name),
|
||||||
|
Sid: natHoleSidMsg.Sid,
|
||||||
|
MappedAddrs: prepareResult.Addrs,
|
||||||
|
AssistedAddrs: prepareResult.AssistedAddrs,
|
||||||
|
}
|
||||||
|
|
||||||
|
xl.Tracef("nathole exchange info start")
|
||||||
|
natHoleRespMsg, err := nathole.ExchangeInfo(pxy.ctx, pxy.msgTransporter, transactionID, natHoleClientMsg, 5*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("nathole exchange info error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
xl.Infof("get natHoleRespMsg, sid [%s], protocol [%s], candidate address %v, assisted address %v, detectBehavior: %+v",
|
||||||
|
natHoleRespMsg.Sid, natHoleRespMsg.Protocol, natHoleRespMsg.CandidateAddrs,
|
||||||
|
natHoleRespMsg.AssistedAddrs, natHoleRespMsg.DetectBehavior)
|
||||||
|
|
||||||
|
listenConn := prepareResult.ListenConn
|
||||||
|
newListenConn, raddr, err := nathole.MakeHole(pxy.ctx, listenConn, natHoleRespMsg, []byte(pxy.cfg.Secretkey))
|
||||||
|
if err != nil {
|
||||||
|
listenConn.Close()
|
||||||
|
xl.Warnf("make hole error: %v", err)
|
||||||
|
_ = pxy.msgTransporter.Send(&msg.NatHoleReport{
|
||||||
|
Sid: natHoleRespMsg.Sid,
|
||||||
|
Success: false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
listenConn = newListenConn
|
||||||
|
xl.Infof("establishing nat hole connection successful, sid [%s], remoteAddr [%s]", natHoleRespMsg.Sid, raddr)
|
||||||
|
|
||||||
|
_ = pxy.msgTransporter.Send(&msg.NatHoleReport{
|
||||||
|
Sid: natHoleRespMsg.Sid,
|
||||||
|
Success: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if natHoleRespMsg.Protocol == "kcp" {
|
||||||
|
pxy.listenByKCP(listenConn, raddr, startWorkConnMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// default is quic
|
||||||
|
pxy.listenByQUIC(listenConn, raddr, startWorkConnMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pxy *XTCPProxy) listenByKCP(listenConn *net.UDPConn, raddr *net.UDPAddr, startWorkConnMsg *msg.StartWorkConn) {
|
||||||
|
xl := pxy.xl
|
||||||
|
listenConn.Close()
|
||||||
|
laddr, _ := net.ResolveUDPAddr("udp", listenConn.LocalAddr().String())
|
||||||
|
lConn, err := net.DialUDP("udp", laddr, raddr)
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("dial udp error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer lConn.Close()
|
||||||
|
|
||||||
|
remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String())
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("create kcp connection from udp connection error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmuxCfg := fmux.DefaultConfig()
|
||||||
|
fmuxCfg.KeepAliveInterval = 10 * time.Second
|
||||||
|
fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
|
||||||
|
fmuxCfg.LogOutput = io.Discard
|
||||||
|
session, err := fmux.Server(remote, fmuxCfg)
|
||||||
|
if err != nil {
|
||||||
|
xl.Errorf("create mux session error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer session.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
muxConn, err := session.Accept()
|
||||||
|
if err != nil {
|
||||||
|
xl.Errorf("accept connection error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go pxy.HandleTCPWorkConnection(muxConn, startWorkConnMsg, []byte(pxy.cfg.Secretkey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pxy *XTCPProxy) listenByQUIC(listenConn *net.UDPConn, _ *net.UDPAddr, startWorkConnMsg *msg.StartWorkConn) {
|
||||||
|
xl := pxy.xl
|
||||||
|
defer listenConn.Close()
|
||||||
|
|
||||||
|
tlsConfig, err := transport.NewServerTLSConfig("", "", "")
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("create tls config error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tlsConfig.NextProtos = []string{"frp"}
|
||||||
|
quicListener, err := quic.Listen(listenConn, tlsConfig,
|
||||||
|
&quic.Config{
|
||||||
|
MaxIdleTimeout: time.Duration(pxy.clientCfg.Transport.QUIC.MaxIdleTimeout) * time.Second,
|
||||||
|
MaxIncomingStreams: int64(pxy.clientCfg.Transport.QUIC.MaxIncomingStreams),
|
||||||
|
KeepAlivePeriod: time.Duration(pxy.clientCfg.Transport.QUIC.KeepalivePeriod) * time.Second,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("dial quic error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// only accept one connection from raddr
|
||||||
|
c, err := quicListener.Accept(pxy.ctx)
|
||||||
|
if err != nil {
|
||||||
|
xl.Errorf("quic accept connection error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
stream, err := c.AcceptStream(pxy.ctx)
|
||||||
|
if err != nil {
|
||||||
|
xl.Debugf("quic accept stream error: %v", err)
|
||||||
|
_ = c.CloseWithError(0, "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go pxy.HandleTCPWorkConnection(netpkg.QuicStreamToNetConn(stream, c), startWorkConnMsg, []byte(pxy.cfg.Secretkey))
|
||||||
|
}
|
||||||
|
}
|
||||||
583
client/service.go
Normal file
583
client/service.go
Normal file
@@ -0,0 +1,583 @@
|
|||||||
|
// Copyright 2017 fatedier, fatedier@gmail.com
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatedier/golib/crypto"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/client/proxy"
|
||||||
|
"github.com/fatedier/frp/pkg/auth"
|
||||||
|
"github.com/fatedier/frp/pkg/config"
|
||||||
|
"github.com/fatedier/frp/pkg/config/source"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
|
"github.com/fatedier/frp/pkg/util/version"
|
||||||
|
"github.com/fatedier/frp/pkg/util/wait"
|
||||||
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
crypto.DefaultSalt = "frp"
|
||||||
|
// Disable quic-go's receive buffer warning.
|
||||||
|
os.Setenv("QUIC_GO_DISABLE_RECEIVE_BUFFER_WARNING", "true")
|
||||||
|
// Disable quic-go's ECN support by default. It may cause issues on certain operating systems.
|
||||||
|
if os.Getenv("QUIC_GO_DISABLE_ECN") == "" {
|
||||||
|
os.Setenv("QUIC_GO_DISABLE_ECN", "true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type cancelErr struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e cancelErr) Error() string {
|
||||||
|
return e.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceOptions contains options for creating a new client service.
|
||||||
|
type ServiceOptions struct {
|
||||||
|
Common *v1.ClientCommonConfig
|
||||||
|
|
||||||
|
// ConfigSourceAggregator manages internal config and optional store sources.
|
||||||
|
// It is required for creating a Service.
|
||||||
|
ConfigSourceAggregator *source.Aggregator
|
||||||
|
|
||||||
|
UnsafeFeatures *security.UnsafeFeatures
|
||||||
|
|
||||||
|
// ConfigFilePath is the path to the configuration file used to initialize.
|
||||||
|
// If it is empty, it means that the configuration file is not used for initialization.
|
||||||
|
// It may be initialized using command line parameters or called directly.
|
||||||
|
ConfigFilePath string
|
||||||
|
|
||||||
|
// ClientSpec is the client specification that control the client behavior.
|
||||||
|
ClientSpec *msg.ClientSpec
|
||||||
|
|
||||||
|
// ConnectorCreator is a function that creates a new connector to make connections to the server.
|
||||||
|
// The Connector shields the underlying connection details, whether it is through TCP or QUIC connection,
|
||||||
|
// and regardless of whether multiplexing is used.
|
||||||
|
//
|
||||||
|
// If it is not set, the default frpc connector will be used.
|
||||||
|
// By using a custom Connector, it can be used to implement a VirtualClient, which connects to frps
|
||||||
|
// through a pipe instead of a real physical connection.
|
||||||
|
ConnectorCreator func(context.Context, *v1.ClientCommonConfig) Connector
|
||||||
|
|
||||||
|
// HandleWorkConnCb is a callback function that is called when a new work connection is created.
|
||||||
|
//
|
||||||
|
// If it is not set, the default frpc implementation will be used.
|
||||||
|
HandleWorkConnCb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// setServiceOptionsDefault sets the default values for ServiceOptions.
|
||||||
|
func setServiceOptionsDefault(options *ServiceOptions) error {
|
||||||
|
if options.Common != nil {
|
||||||
|
if err := options.Common.Complete(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if options.ConnectorCreator == nil {
|
||||||
|
options.ConnectorCreator = NewConnector
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service is the client service that connects to frps and provides proxy services.
|
||||||
|
type Service struct {
|
||||||
|
ctlMu sync.RWMutex
|
||||||
|
// manager control connection with server
|
||||||
|
ctl *Control
|
||||||
|
// Uniq id got from frps, it will be attached to loginMsg.
|
||||||
|
runID string
|
||||||
|
|
||||||
|
// Auth runtime and encryption materials
|
||||||
|
auth *auth.ClientAuth
|
||||||
|
|
||||||
|
// web server for admin UI and apis
|
||||||
|
webServer *httppkg.Server
|
||||||
|
|
||||||
|
vnetController *vnet.Controller
|
||||||
|
|
||||||
|
cfgMu sync.RWMutex
|
||||||
|
// reloadMu serializes reload transactions to keep reloadCommon and applied
|
||||||
|
// config in sync across concurrent API operations.
|
||||||
|
reloadMu sync.Mutex
|
||||||
|
common *v1.ClientCommonConfig
|
||||||
|
// reloadCommon is used for filtering/defaulting during config-source reloads.
|
||||||
|
// It can be updated by /api/reload without mutating startup-only common behavior.
|
||||||
|
reloadCommon *v1.ClientCommonConfig
|
||||||
|
proxyCfgs []v1.ProxyConfigurer
|
||||||
|
visitorCfgs []v1.VisitorConfigurer
|
||||||
|
clientSpec *msg.ClientSpec
|
||||||
|
|
||||||
|
// aggregator manages multiple configuration sources.
|
||||||
|
// When set, the service watches for config changes and reloads automatically.
|
||||||
|
aggregator *source.Aggregator
|
||||||
|
configSource *source.ConfigSource
|
||||||
|
storeSource *source.StoreSource
|
||||||
|
|
||||||
|
unsafeFeatures *security.UnsafeFeatures
|
||||||
|
|
||||||
|
// The configuration file used to initialize this client, or an empty
|
||||||
|
// string if no configuration file was used.
|
||||||
|
configFilePath string
|
||||||
|
|
||||||
|
// service context
|
||||||
|
ctx context.Context
|
||||||
|
// call cancel to stop service
|
||||||
|
cancel context.CancelCauseFunc
|
||||||
|
gracefulShutdownDuration time.Duration
|
||||||
|
|
||||||
|
connectorCreator func(context.Context, *v1.ClientCommonConfig) Connector
|
||||||
|
handleWorkConnCb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(options ServiceOptions) (*Service, error) {
|
||||||
|
if err := setServiceOptionsDefault(&options); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
authRuntime, err := auth.BuildClientAuth(&options.Common.Auth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.ConfigSourceAggregator == nil {
|
||||||
|
return nil, fmt.Errorf("config source aggregator is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
configSource := options.ConfigSourceAggregator.ConfigSource()
|
||||||
|
storeSource := options.ConfigSourceAggregator.StoreSource()
|
||||||
|
|
||||||
|
proxyCfgs, visitorCfgs, loadErr := options.ConfigSourceAggregator.Load()
|
||||||
|
if loadErr != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load config from aggregator: %w", loadErr)
|
||||||
|
}
|
||||||
|
proxyCfgs, visitorCfgs = config.FilterClientConfigurers(options.Common, proxyCfgs, visitorCfgs)
|
||||||
|
proxyCfgs = config.CompleteProxyConfigurers(proxyCfgs)
|
||||||
|
visitorCfgs = config.CompleteVisitorConfigurers(visitorCfgs)
|
||||||
|
|
||||||
|
// Create the web server after all fallible steps so its listener is not
|
||||||
|
// leaked when an earlier error causes NewService to return.
|
||||||
|
var webServer *httppkg.Server
|
||||||
|
if options.Common.WebServer.Port > 0 {
|
||||||
|
ws, err := httppkg.NewServer(options.Common.WebServer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
webServer = ws
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &Service{
|
||||||
|
ctx: context.Background(),
|
||||||
|
auth: authRuntime,
|
||||||
|
webServer: webServer,
|
||||||
|
common: options.Common,
|
||||||
|
reloadCommon: options.Common,
|
||||||
|
configFilePath: options.ConfigFilePath,
|
||||||
|
unsafeFeatures: options.UnsafeFeatures,
|
||||||
|
proxyCfgs: proxyCfgs,
|
||||||
|
visitorCfgs: visitorCfgs,
|
||||||
|
clientSpec: options.ClientSpec,
|
||||||
|
aggregator: options.ConfigSourceAggregator,
|
||||||
|
configSource: configSource,
|
||||||
|
storeSource: storeSource,
|
||||||
|
connectorCreator: options.ConnectorCreator,
|
||||||
|
handleWorkConnCb: options.HandleWorkConnCb,
|
||||||
|
}
|
||||||
|
|
||||||
|
if webServer != nil {
|
||||||
|
webServer.RouteRegister(s.registerRouteHandlers)
|
||||||
|
}
|
||||||
|
if options.Common.VirtualNet.Address != "" {
|
||||||
|
s.vnetController = vnet.NewController(options.Common.VirtualNet)
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Service) Run(ctx context.Context) error {
|
||||||
|
ctx, cancel := context.WithCancelCause(ctx)
|
||||||
|
svr.ctx = xlog.NewContext(ctx, xlog.FromContextSafe(ctx))
|
||||||
|
svr.cancel = cancel
|
||||||
|
|
||||||
|
// set custom DNSServer
|
||||||
|
if svr.common.DNSServer != "" {
|
||||||
|
netpkg.SetDefaultDNSAddress(svr.common.DNSServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if svr.vnetController != nil {
|
||||||
|
vnetController := svr.vnetController
|
||||||
|
if err := svr.vnetController.Init(); err != nil {
|
||||||
|
log.Errorf("init virtual network controller error: %v", err)
|
||||||
|
svr.stop()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
log.Infof("virtual network controller start...")
|
||||||
|
if err := vnetController.Run(); err != nil && !errors.Is(err, net.ErrClosed) {
|
||||||
|
log.Warnf("virtual network controller exit with error: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if svr.webServer != nil {
|
||||||
|
webServer := svr.webServer
|
||||||
|
go func() {
|
||||||
|
log.Infof("admin server listen on %s", webServer.Address())
|
||||||
|
if err := webServer.Run(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
log.Warnf("admin server exit with error: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// first login to frps
|
||||||
|
svr.loopLoginUntilSuccess(10*time.Second, lo.FromPtr(svr.common.LoginFailExit))
|
||||||
|
if svr.ctl == nil {
|
||||||
|
cancelCause := cancelErr{}
|
||||||
|
_ = errors.As(context.Cause(svr.ctx), &cancelCause)
|
||||||
|
svr.stop()
|
||||||
|
return fmt.Errorf("login to the server failed: %v. With loginFailExit enabled, no additional retries will be attempted", cancelCause.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go svr.keepControllerWorking()
|
||||||
|
|
||||||
|
<-svr.ctx.Done()
|
||||||
|
svr.stop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Service) keepControllerWorking() {
|
||||||
|
<-svr.ctl.Done()
|
||||||
|
|
||||||
|
// There is a situation where the login is successful but due to certain reasons,
|
||||||
|
// the control immediately exits. It is necessary to limit the frequency of reconnection in this case.
|
||||||
|
// The interval for the first three retries in 1 minute will be very short, and then it will increase exponentially.
|
||||||
|
// The maximum interval is 20 seconds.
|
||||||
|
wait.BackoffUntil(func() (bool, error) {
|
||||||
|
// loopLoginUntilSuccess is another layer of loop that will continuously attempt to
|
||||||
|
// login to the server until successful.
|
||||||
|
svr.loopLoginUntilSuccess(20*time.Second, false)
|
||||||
|
if svr.ctl != nil {
|
||||||
|
<-svr.ctl.Done()
|
||||||
|
return false, errors.New("control is closed and try another loop")
|
||||||
|
}
|
||||||
|
// If the control is nil, it means that the login failed and the service is also closed.
|
||||||
|
return false, nil
|
||||||
|
}, wait.NewFastBackoffManager(
|
||||||
|
wait.FastBackoffOptions{
|
||||||
|
Duration: time.Second,
|
||||||
|
Factor: 2,
|
||||||
|
Jitter: 0.1,
|
||||||
|
MaxDuration: 20 * time.Second,
|
||||||
|
FastRetryCount: 3,
|
||||||
|
FastRetryDelay: 200 * time.Millisecond,
|
||||||
|
FastRetryWindow: time.Minute,
|
||||||
|
FastRetryJitter: 0.5,
|
||||||
|
},
|
||||||
|
), true, svr.ctx.Done())
|
||||||
|
}
|
||||||
|
|
||||||
|
// login creates a connection to frps and registers it self as a client
|
||||||
|
// conn: control connection
|
||||||
|
// session: if it's not nil, using tcp mux
|
||||||
|
func (svr *Service) login() (conn net.Conn, connector Connector, err error) {
|
||||||
|
xl := xlog.FromContextSafe(svr.ctx)
|
||||||
|
connector = svr.connectorCreator(svr.ctx, svr.common)
|
||||||
|
if err = connector.Open(); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
connector.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
conn, err = connector.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hostname, _ := os.Hostname()
|
||||||
|
|
||||||
|
loginMsg := &msg.Login{
|
||||||
|
Arch: runtime.GOARCH,
|
||||||
|
Os: runtime.GOOS,
|
||||||
|
Hostname: hostname,
|
||||||
|
PoolCount: svr.common.Transport.PoolCount,
|
||||||
|
User: svr.common.User,
|
||||||
|
ClientID: svr.common.ClientID,
|
||||||
|
Version: version.Full(),
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
RunID: svr.runID,
|
||||||
|
Metas: svr.common.Metadatas,
|
||||||
|
}
|
||||||
|
if svr.clientSpec != nil {
|
||||||
|
loginMsg.ClientSpec = *svr.clientSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add auth
|
||||||
|
if err = svr.auth.Setter.SetLogin(loginMsg); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = msg.WriteMsg(conn, loginMsg); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var loginRespMsg msg.LoginResp
|
||||||
|
_ = conn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||||
|
if err = msg.ReadMsgInto(conn, &loginRespMsg); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = conn.SetReadDeadline(time.Time{})
|
||||||
|
|
||||||
|
if loginRespMsg.Error != "" {
|
||||||
|
err = fmt.Errorf("%s", loginRespMsg.Error)
|
||||||
|
xl.Errorf("%s", loginRespMsg.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
svr.runID = loginRespMsg.RunID
|
||||||
|
xl.AddPrefix(xlog.LogPrefix{Name: "runID", Value: svr.runID})
|
||||||
|
|
||||||
|
xl.Infof("login to server success, get run id [%s]", loginRespMsg.RunID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginExit bool) {
|
||||||
|
xl := xlog.FromContextSafe(svr.ctx)
|
||||||
|
|
||||||
|
loginFunc := func() (bool, error) {
|
||||||
|
xl.Infof("try to connect to server...")
|
||||||
|
conn, connector, err := svr.login()
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("connect to server error: %v", err)
|
||||||
|
if firstLoginExit {
|
||||||
|
svr.cancel(cancelErr{Err: err})
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
svr.cfgMu.RLock()
|
||||||
|
proxyCfgs := svr.proxyCfgs
|
||||||
|
visitorCfgs := svr.visitorCfgs
|
||||||
|
svr.cfgMu.RUnlock()
|
||||||
|
|
||||||
|
connEncrypted := svr.clientSpec == nil || svr.clientSpec.Type != "ssh-tunnel"
|
||||||
|
|
||||||
|
sessionCtx := &SessionContext{
|
||||||
|
Common: svr.common,
|
||||||
|
RunID: svr.runID,
|
||||||
|
Conn: conn,
|
||||||
|
ConnEncrypted: connEncrypted,
|
||||||
|
Auth: svr.auth,
|
||||||
|
Connector: connector,
|
||||||
|
VnetController: svr.vnetController,
|
||||||
|
}
|
||||||
|
ctl, err := NewControl(svr.ctx, sessionCtx)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
xl.Errorf("new control error: %v", err)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
ctl.SetInWorkConnCallback(svr.handleWorkConnCb)
|
||||||
|
|
||||||
|
ctl.Run(proxyCfgs, visitorCfgs)
|
||||||
|
// close and replace previous control
|
||||||
|
svr.ctlMu.Lock()
|
||||||
|
if svr.ctl != nil {
|
||||||
|
svr.ctl.Close()
|
||||||
|
}
|
||||||
|
svr.ctl = ctl
|
||||||
|
svr.ctlMu.Unlock()
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to reconnect to server until success
|
||||||
|
wait.BackoffUntil(loginFunc, wait.NewFastBackoffManager(
|
||||||
|
wait.FastBackoffOptions{
|
||||||
|
Duration: time.Second,
|
||||||
|
Factor: 2,
|
||||||
|
Jitter: 0.1,
|
||||||
|
MaxDuration: maxInterval,
|
||||||
|
}), true, svr.ctx.Done())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Service) UpdateAllConfigurer(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
|
||||||
|
svr.cfgMu.Lock()
|
||||||
|
svr.proxyCfgs = proxyCfgs
|
||||||
|
svr.visitorCfgs = visitorCfgs
|
||||||
|
svr.cfgMu.Unlock()
|
||||||
|
|
||||||
|
svr.ctlMu.RLock()
|
||||||
|
ctl := svr.ctl
|
||||||
|
svr.ctlMu.RUnlock()
|
||||||
|
|
||||||
|
if ctl != nil {
|
||||||
|
return svr.ctl.UpdateAllConfigurer(proxyCfgs, visitorCfgs)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Service) UpdateConfigSource(
|
||||||
|
common *v1.ClientCommonConfig,
|
||||||
|
proxyCfgs []v1.ProxyConfigurer,
|
||||||
|
visitorCfgs []v1.VisitorConfigurer,
|
||||||
|
) error {
|
||||||
|
svr.reloadMu.Lock()
|
||||||
|
defer svr.reloadMu.Unlock()
|
||||||
|
|
||||||
|
cfgSource := svr.configSource
|
||||||
|
if cfgSource == nil {
|
||||||
|
return fmt.Errorf("config source is not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cfgSource.ReplaceAll(proxyCfgs, visitorCfgs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-atomic update semantics: source has been updated at this point.
|
||||||
|
// Even if reload fails below, keep this common config for subsequent reloads.
|
||||||
|
svr.cfgMu.Lock()
|
||||||
|
svr.reloadCommon = common
|
||||||
|
svr.cfgMu.Unlock()
|
||||||
|
|
||||||
|
if err := svr.reloadConfigFromSourcesLocked(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Service) Close() {
|
||||||
|
svr.GracefulClose(time.Duration(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Service) GracefulClose(d time.Duration) {
|
||||||
|
svr.gracefulShutdownDuration = d
|
||||||
|
svr.cancel(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Service) stop() {
|
||||||
|
// Coordinate shutdown with reload/update paths that read source pointers.
|
||||||
|
svr.reloadMu.Lock()
|
||||||
|
if svr.aggregator != nil {
|
||||||
|
svr.aggregator = nil
|
||||||
|
}
|
||||||
|
svr.configSource = nil
|
||||||
|
svr.storeSource = nil
|
||||||
|
svr.reloadMu.Unlock()
|
||||||
|
|
||||||
|
svr.ctlMu.Lock()
|
||||||
|
defer svr.ctlMu.Unlock()
|
||||||
|
if svr.ctl != nil {
|
||||||
|
svr.ctl.GracefulClose(svr.gracefulShutdownDuration)
|
||||||
|
svr.ctl = nil
|
||||||
|
}
|
||||||
|
if svr.webServer != nil {
|
||||||
|
svr.webServer.Close()
|
||||||
|
svr.webServer = nil
|
||||||
|
}
|
||||||
|
if svr.vnetController != nil {
|
||||||
|
_ = svr.vnetController.Stop()
|
||||||
|
svr.vnetController = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Service) getProxyStatus(name string) (*proxy.WorkingStatus, bool) {
|
||||||
|
svr.ctlMu.RLock()
|
||||||
|
ctl := svr.ctl
|
||||||
|
svr.ctlMu.RUnlock()
|
||||||
|
|
||||||
|
if ctl == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return ctl.pm.GetProxyStatus(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Service) getVisitorCfg(name string) (v1.VisitorConfigurer, bool) {
|
||||||
|
svr.ctlMu.RLock()
|
||||||
|
ctl := svr.ctl
|
||||||
|
svr.ctlMu.RUnlock()
|
||||||
|
|
||||||
|
if ctl == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return ctl.vm.GetVisitorCfg(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Service) StatusExporter() StatusExporter {
|
||||||
|
return &statusExporterImpl{
|
||||||
|
getProxyStatusFunc: svr.getProxyStatus,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatusExporter interface {
|
||||||
|
GetProxyStatus(name string) (*proxy.WorkingStatus, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
type statusExporterImpl struct {
|
||||||
|
getProxyStatusFunc func(name string) (*proxy.WorkingStatus, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *statusExporterImpl) GetProxyStatus(name string) (*proxy.WorkingStatus, bool) {
|
||||||
|
return s.getProxyStatusFunc(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Service) reloadConfigFromSources() error {
|
||||||
|
svr.reloadMu.Lock()
|
||||||
|
defer svr.reloadMu.Unlock()
|
||||||
|
return svr.reloadConfigFromSourcesLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Service) reloadConfigFromSourcesLocked() error {
|
||||||
|
aggregator := svr.aggregator
|
||||||
|
if aggregator == nil {
|
||||||
|
return errors.New("config aggregator is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
svr.cfgMu.RLock()
|
||||||
|
reloadCommon := svr.reloadCommon
|
||||||
|
svr.cfgMu.RUnlock()
|
||||||
|
|
||||||
|
proxies, visitors, err := aggregator.Load()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reload config from sources failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
proxies, visitors = config.FilterClientConfigurers(reloadCommon, proxies, visitors)
|
||||||
|
proxies = config.CompleteProxyConfigurers(proxies)
|
||||||
|
visitors = config.CompleteVisitorConfigurers(visitors)
|
||||||
|
|
||||||
|
// Atomically replace the entire configuration
|
||||||
|
if err := svr.UpdateAllConfigurer(proxies, visitors); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
246
client/service_test.go
Normal file
246
client/service_test.go
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/samber/lo"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/config/source"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type failingConnector struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *failingConnector) Open() error {
|
||||||
|
return c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *failingConnector) Connect() (net.Conn, error) {
|
||||||
|
return nil, c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *failingConnector) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFreeTCPPort(t *testing.T) int {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("listen on ephemeral port: %v", err)
|
||||||
|
}
|
||||||
|
defer ln.Close()
|
||||||
|
|
||||||
|
return ln.Addr().(*net.TCPAddr).Port
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunStopsStartedComponentsOnInitialLoginFailure(t *testing.T) {
|
||||||
|
port := getFreeTCPPort(t)
|
||||||
|
agg := source.NewAggregator(source.NewConfigSource())
|
||||||
|
|
||||||
|
svr, err := NewService(ServiceOptions{
|
||||||
|
Common: &v1.ClientCommonConfig{
|
||||||
|
LoginFailExit: lo.ToPtr(true),
|
||||||
|
WebServer: v1.WebServerConfig{
|
||||||
|
Addr: "127.0.0.1",
|
||||||
|
Port: port,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ConfigSourceAggregator: agg,
|
||||||
|
ConnectorCreator: func(context.Context, *v1.ClientCommonConfig) Connector {
|
||||||
|
return &failingConnector{err: errors.New("login boom")}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("new service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = svr.Run(context.Background())
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected run error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "login boom") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if svr.webServer != nil {
|
||||||
|
t.Fatal("expected web server to be cleaned up after initial login failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected admin port to be released: %v", err)
|
||||||
|
}
|
||||||
|
_ = ln.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewServiceDoesNotLeakAdminListenerOnAuthBuildFailure(t *testing.T) {
|
||||||
|
port := getFreeTCPPort(t)
|
||||||
|
agg := source.NewAggregator(source.NewConfigSource())
|
||||||
|
|
||||||
|
_, err := NewService(ServiceOptions{
|
||||||
|
Common: &v1.ClientCommonConfig{
|
||||||
|
Auth: v1.AuthClientConfig{
|
||||||
|
Method: v1.AuthMethodOIDC,
|
||||||
|
OIDC: v1.AuthOIDCClientConfig{
|
||||||
|
TokenEndpointURL: "://bad",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
WebServer: v1.WebServerConfig{
|
||||||
|
Addr: "127.0.0.1",
|
||||||
|
Port: port,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ConfigSourceAggregator: agg,
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected new service error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "auth.oidc.tokenEndpointURL") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected admin port to remain free: %v", err)
|
||||||
|
}
|
||||||
|
_ = ln.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateConfigSourceRollsBackReloadCommonOnReplaceAllFailure(t *testing.T) {
|
||||||
|
prevCommon := &v1.ClientCommonConfig{User: "old-user"}
|
||||||
|
newCommon := &v1.ClientCommonConfig{User: "new-user"}
|
||||||
|
|
||||||
|
svr := &Service{
|
||||||
|
configSource: source.NewConfigSource(),
|
||||||
|
reloadCommon: prevCommon,
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidProxy := &v1.TCPProxyConfig{}
|
||||||
|
err := svr.UpdateConfigSource(newCommon, []v1.ProxyConfigurer{invalidProxy}, nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(err.Error(), "proxy name cannot be empty") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if svr.reloadCommon != prevCommon {
|
||||||
|
t.Fatalf("reloadCommon should roll back on ReplaceAll failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateConfigSourceKeepsReloadCommonOnReloadFailure(t *testing.T) {
|
||||||
|
prevCommon := &v1.ClientCommonConfig{User: "old-user"}
|
||||||
|
newCommon := &v1.ClientCommonConfig{User: "new-user"}
|
||||||
|
|
||||||
|
svr := &Service{
|
||||||
|
// Keep configSource valid so ReplaceAll succeeds first.
|
||||||
|
configSource: source.NewConfigSource(),
|
||||||
|
reloadCommon: prevCommon,
|
||||||
|
// Keep aggregator nil to force reload failure.
|
||||||
|
aggregator: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
validProxy := &v1.TCPProxyConfig{
|
||||||
|
ProxyBaseConfig: v1.ProxyBaseConfig{
|
||||||
|
Name: "p1",
|
||||||
|
Type: "tcp",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := svr.UpdateConfigSource(newCommon, []v1.ProxyConfigurer{validProxy}, nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(err.Error(), "config aggregator is not initialized") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if svr.reloadCommon != newCommon {
|
||||||
|
t.Fatalf("reloadCommon should keep new value on reload failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReloadConfigFromSourcesDoesNotMutateStoreConfigs(t *testing.T) {
|
||||||
|
storeSource, err := source.NewStoreSource(source.StoreSourceConfig{
|
||||||
|
Path: filepath.Join(t.TempDir(), "store.json"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("new store source: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyCfg := &v1.TCPProxyConfig{
|
||||||
|
ProxyBaseConfig: v1.ProxyBaseConfig{
|
||||||
|
Name: "store-proxy",
|
||||||
|
Type: "tcp",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
visitorCfg := &v1.STCPVisitorConfig{
|
||||||
|
VisitorBaseConfig: v1.VisitorBaseConfig{
|
||||||
|
Name: "store-visitor",
|
||||||
|
Type: "stcp",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := storeSource.AddProxy(proxyCfg); err != nil {
|
||||||
|
t.Fatalf("add proxy to store: %v", err)
|
||||||
|
}
|
||||||
|
if err := storeSource.AddVisitor(visitorCfg); err != nil {
|
||||||
|
t.Fatalf("add visitor to store: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
agg := source.NewAggregator(source.NewConfigSource())
|
||||||
|
agg.SetStoreSource(storeSource)
|
||||||
|
svr := &Service{
|
||||||
|
aggregator: agg,
|
||||||
|
configSource: agg.ConfigSource(),
|
||||||
|
storeSource: storeSource,
|
||||||
|
reloadCommon: &v1.ClientCommonConfig{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := svr.reloadConfigFromSources(); err != nil {
|
||||||
|
t.Fatalf("reload config from sources: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotProxy := storeSource.GetProxy("store-proxy")
|
||||||
|
if gotProxy == nil {
|
||||||
|
t.Fatalf("proxy not found in store")
|
||||||
|
}
|
||||||
|
if gotProxy.GetBaseConfig().LocalIP != "" {
|
||||||
|
t.Fatalf("store proxy localIP should stay empty, got %q", gotProxy.GetBaseConfig().LocalIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotVisitor := storeSource.GetVisitor("store-visitor")
|
||||||
|
if gotVisitor == nil {
|
||||||
|
t.Fatalf("visitor not found in store")
|
||||||
|
}
|
||||||
|
if gotVisitor.GetBaseConfig().BindAddr != "" {
|
||||||
|
t.Fatalf("store visitor bindAddr should stay empty, got %q", gotVisitor.GetBaseConfig().BindAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
svr.cfgMu.RLock()
|
||||||
|
defer svr.cfgMu.RUnlock()
|
||||||
|
|
||||||
|
if len(svr.proxyCfgs) != 1 {
|
||||||
|
t.Fatalf("expected 1 runtime proxy, got %d", len(svr.proxyCfgs))
|
||||||
|
}
|
||||||
|
if svr.proxyCfgs[0].GetBaseConfig().LocalIP != "127.0.0.1" {
|
||||||
|
t.Fatalf("runtime proxy localIP should be defaulted, got %q", svr.proxyCfgs[0].GetBaseConfig().LocalIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(svr.visitorCfgs) != 1 {
|
||||||
|
t.Fatalf("expected 1 runtime visitor, got %d", len(svr.visitorCfgs))
|
||||||
|
}
|
||||||
|
if svr.visitorCfgs[0].GetBaseConfig().BindAddr != "127.0.0.1" {
|
||||||
|
t.Fatalf("runtime visitor bindAddr should be defaulted, got %q", svr.visitorCfgs[0].GetBaseConfig().BindAddr)
|
||||||
|
}
|
||||||
|
}
|
||||||
85
client/visitor/stcp.go
Normal file
85
client/visitor/stcp.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
// Copyright 2017 fatedier, fatedier@gmail.com
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package visitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
libio "github.com/fatedier/golib/io"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type STCPVisitor struct {
|
||||||
|
*BaseVisitor
|
||||||
|
|
||||||
|
cfg *v1.STCPVisitorConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv *STCPVisitor) Run() (err error) {
|
||||||
|
if sv.cfg.BindPort > 0 {
|
||||||
|
sv.l, err = net.Listen("tcp", net.JoinHostPort(sv.cfg.BindAddr, strconv.Itoa(sv.cfg.BindPort)))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go sv.acceptLoop(sv.l, "stcp local", sv.handleConn)
|
||||||
|
}
|
||||||
|
|
||||||
|
go sv.acceptLoop(sv.internalLn, "stcp internal", sv.handleConn)
|
||||||
|
|
||||||
|
if sv.plugin != nil {
|
||||||
|
sv.plugin.Start()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv *STCPVisitor) Close() {
|
||||||
|
sv.BaseVisitor.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
||||||
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
|
var tunnelErr error
|
||||||
|
defer func() {
|
||||||
|
if tunnelErr != nil {
|
||||||
|
if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok {
|
||||||
|
_ = eConn.CloseWithError(tunnelErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userConn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
xl.Debugf("get a new stcp user connection")
|
||||||
|
visitorConn, err := sv.dialRawVisitorConn(sv.cfg.GetBaseConfig())
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("dialRawVisitorConn error: %v", err)
|
||||||
|
tunnelErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer visitorConn.Close()
|
||||||
|
|
||||||
|
remote, recycleFn, err := wrapVisitorConn(visitorConn, sv.cfg.GetBaseConfig())
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("wrapVisitorConn error: %v", err)
|
||||||
|
tunnelErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer recycleFn()
|
||||||
|
|
||||||
|
libio.Join(userConn, remote)
|
||||||
|
}
|
||||||
230
client/visitor/sudp.go
Normal file
230
client/visitor/sudp.go
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
// Copyright 2017 fatedier, fatedier@gmail.com
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package visitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatedier/golib/errors"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
"github.com/fatedier/frp/pkg/proto/udp"
|
||||||
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SUDPVisitor struct {
|
||||||
|
*BaseVisitor
|
||||||
|
|
||||||
|
checkCloseCh chan struct{}
|
||||||
|
// udpConn is the listener of udp packet
|
||||||
|
udpConn *net.UDPConn
|
||||||
|
readCh chan *msg.UDPPacket
|
||||||
|
sendCh chan *msg.UDPPacket
|
||||||
|
|
||||||
|
cfg *v1.SUDPVisitorConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// SUDP Run start listen a udp port
|
||||||
|
func (sv *SUDPVisitor) Run() (err error) {
|
||||||
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
|
|
||||||
|
addr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(sv.cfg.BindAddr, strconv.Itoa(sv.cfg.BindPort)))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("sudp ResolveUDPAddr error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sv.udpConn, err = net.ListenUDP("udp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("listen udp port %s error: %v", addr.String(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sv.sendCh = make(chan *msg.UDPPacket, 1024)
|
||||||
|
sv.readCh = make(chan *msg.UDPPacket, 1024)
|
||||||
|
|
||||||
|
xl.Infof("sudp start to work, listen on %s", addr)
|
||||||
|
|
||||||
|
go sv.dispatcher()
|
||||||
|
go udp.ForwardUserConn(sv.udpConn, sv.readCh, sv.sendCh, int(sv.clientCfg.UDPPacketSize))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv *SUDPVisitor) dispatcher() {
|
||||||
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
|
|
||||||
|
var (
|
||||||
|
visitorConn net.Conn
|
||||||
|
recycleFn func()
|
||||||
|
err error
|
||||||
|
|
||||||
|
firstPacket *msg.UDPPacket
|
||||||
|
)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case firstPacket = <-sv.sendCh:
|
||||||
|
if firstPacket == nil {
|
||||||
|
xl.Infof("frpc sudp visitor proxy is closed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-sv.checkCloseCh:
|
||||||
|
xl.Infof("frpc sudp visitor proxy is closed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
visitorConn, recycleFn, err = sv.getNewVisitorConn()
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("newVisitorConn to frps error: %v, try to reconnect", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// visitorConn always be closed when worker done.
|
||||||
|
func() {
|
||||||
|
defer recycleFn()
|
||||||
|
sv.worker(visitorConn, firstPacket)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-sv.checkCloseCh:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
|
||||||
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
|
xl.Debugf("starting sudp proxy worker")
|
||||||
|
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
wg.Add(2)
|
||||||
|
closeCh := make(chan struct{})
|
||||||
|
|
||||||
|
// udp service -> frpc -> frps -> frpc visitor -> user
|
||||||
|
workConnReaderFn := func(conn net.Conn) {
|
||||||
|
defer func() {
|
||||||
|
conn.Close()
|
||||||
|
close(closeCh)
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
var (
|
||||||
|
rawMsg msg.Message
|
||||||
|
errRet error
|
||||||
|
)
|
||||||
|
|
||||||
|
// frpc will send heartbeat in workConn to frpc visitor for keeping alive
|
||||||
|
_ = conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||||
|
if rawMsg, errRet = msg.ReadMsg(conn); errRet != nil {
|
||||||
|
xl.Warnf("read from workconn for user udp conn error: %v", errRet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = conn.SetReadDeadline(time.Time{})
|
||||||
|
switch m := rawMsg.(type) {
|
||||||
|
case *msg.Ping:
|
||||||
|
xl.Debugf("frpc visitor get ping message from frpc")
|
||||||
|
continue
|
||||||
|
case *msg.UDPPacket:
|
||||||
|
if errRet := errors.PanicToError(func() {
|
||||||
|
sv.readCh <- m
|
||||||
|
xl.Tracef("frpc visitor get udp packet from workConn, len: %d", len(m.Content))
|
||||||
|
}); errRet != nil {
|
||||||
|
xl.Infof("reader goroutine for udp work connection closed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// udp service <- frpc <- frps <- frpc visitor <- user
|
||||||
|
workConnSenderFn := func(conn net.Conn) {
|
||||||
|
defer func() {
|
||||||
|
conn.Close()
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
var errRet error
|
||||||
|
if firstPacket != nil {
|
||||||
|
if errRet = msg.WriteMsg(conn, firstPacket); errRet != nil {
|
||||||
|
xl.Warnf("sender goroutine for udp work connection closed: %v", errRet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
xl.Tracef("send udp package to workConn, len: %d", len(firstPacket.Content))
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case udpMsg, ok := <-sv.sendCh:
|
||||||
|
if !ok {
|
||||||
|
xl.Infof("sender goroutine for udp work connection closed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if errRet = msg.WriteMsg(conn, udpMsg); errRet != nil {
|
||||||
|
xl.Warnf("sender goroutine for udp work connection closed: %v", errRet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
xl.Tracef("send udp package to workConn, len: %d", len(udpMsg.Content))
|
||||||
|
case <-closeCh:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go workConnReaderFn(workConn)
|
||||||
|
go workConnSenderFn(workConn)
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
xl.Infof("sudp worker is closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, func(), error) {
|
||||||
|
rawConn, err := sv.dialRawVisitorConn(sv.cfg.GetBaseConfig())
|
||||||
|
if err != nil {
|
||||||
|
return nil, func() {}, err
|
||||||
|
}
|
||||||
|
rwc, recycleFn, err := wrapVisitorConn(rawConn, sv.cfg.GetBaseConfig())
|
||||||
|
if err != nil {
|
||||||
|
rawConn.Close()
|
||||||
|
return nil, func() {}, err
|
||||||
|
}
|
||||||
|
return netpkg.WrapReadWriteCloserToConn(rwc, rawConn), recycleFn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv *SUDPVisitor) Close() {
|
||||||
|
sv.mu.Lock()
|
||||||
|
defer sv.mu.Unlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-sv.checkCloseCh:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
close(sv.checkCloseCh)
|
||||||
|
}
|
||||||
|
sv.BaseVisitor.Close()
|
||||||
|
if sv.udpConn != nil {
|
||||||
|
sv.udpConn.Close()
|
||||||
|
}
|
||||||
|
close(sv.readCh)
|
||||||
|
close(sv.sendCh)
|
||||||
|
}
|
||||||
206
client/visitor/visitor.go
Normal file
206
client/visitor/visitor.go
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
// Copyright 2017 fatedier, fatedier@gmail.com
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package visitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
libio "github.com/fatedier/golib/io"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
"github.com/fatedier/frp/pkg/naming"
|
||||||
|
plugin "github.com/fatedier/frp/pkg/plugin/visitor"
|
||||||
|
"github.com/fatedier/frp/pkg/transport"
|
||||||
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper wraps some functions for visitor to use.
|
||||||
|
type Helper interface {
|
||||||
|
// ConnectServer directly connects to the frp server.
|
||||||
|
ConnectServer() (net.Conn, error)
|
||||||
|
// TransferConn transfers the connection to another visitor.
|
||||||
|
TransferConn(string, net.Conn) error
|
||||||
|
// MsgTransporter returns the message transporter that is used to send and receive messages
|
||||||
|
// to the frp server through the controller.
|
||||||
|
MsgTransporter() transport.MessageTransporter
|
||||||
|
// VNetController returns the vnet controller that is used to manage the virtual network.
|
||||||
|
VNetController() *vnet.Controller
|
||||||
|
// RunID returns the run id of current controller.
|
||||||
|
RunID() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visitor is used for forward traffics from local port tot remote service.
|
||||||
|
type Visitor interface {
|
||||||
|
Run() error
|
||||||
|
AcceptConn(conn net.Conn) error
|
||||||
|
Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVisitor(
|
||||||
|
ctx context.Context,
|
||||||
|
cfg v1.VisitorConfigurer,
|
||||||
|
clientCfg *v1.ClientCommonConfig,
|
||||||
|
helper Helper,
|
||||||
|
) (Visitor, error) {
|
||||||
|
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(cfg.GetBaseConfig().Name)
|
||||||
|
ctx = xlog.NewContext(ctx, xl)
|
||||||
|
var visitor Visitor
|
||||||
|
baseVisitor := BaseVisitor{
|
||||||
|
clientCfg: clientCfg,
|
||||||
|
helper: helper,
|
||||||
|
ctx: ctx,
|
||||||
|
internalLn: netpkg.NewInternalListener(),
|
||||||
|
}
|
||||||
|
if cfg.GetBaseConfig().Plugin.Type != "" {
|
||||||
|
p, err := plugin.Create(
|
||||||
|
cfg.GetBaseConfig().Plugin.Type,
|
||||||
|
plugin.PluginContext{
|
||||||
|
Name: cfg.GetBaseConfig().Name,
|
||||||
|
Ctx: ctx,
|
||||||
|
VnetController: helper.VNetController(),
|
||||||
|
SendConnToVisitor: func(conn net.Conn) {
|
||||||
|
_ = baseVisitor.AcceptConn(conn)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cfg.GetBaseConfig().Plugin.VisitorPluginOptions,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
baseVisitor.plugin = p
|
||||||
|
}
|
||||||
|
switch cfg := cfg.(type) {
|
||||||
|
case *v1.STCPVisitorConfig:
|
||||||
|
visitor = &STCPVisitor{
|
||||||
|
BaseVisitor: &baseVisitor,
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
case *v1.XTCPVisitorConfig:
|
||||||
|
visitor = &XTCPVisitor{
|
||||||
|
BaseVisitor: &baseVisitor,
|
||||||
|
cfg: cfg,
|
||||||
|
startTunnelCh: make(chan struct{}),
|
||||||
|
}
|
||||||
|
case *v1.SUDPVisitorConfig:
|
||||||
|
visitor = &SUDPVisitor{
|
||||||
|
BaseVisitor: &baseVisitor,
|
||||||
|
cfg: cfg,
|
||||||
|
checkCloseCh: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return visitor, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseVisitor struct {
|
||||||
|
clientCfg *v1.ClientCommonConfig
|
||||||
|
helper Helper
|
||||||
|
l net.Listener
|
||||||
|
internalLn *netpkg.InternalListener
|
||||||
|
plugin plugin.Plugin
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *BaseVisitor) AcceptConn(conn net.Conn) error {
|
||||||
|
return v.internalLn.PutConn(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *BaseVisitor) acceptLoop(l net.Listener, name string, handleConn func(net.Conn)) {
|
||||||
|
xl := xlog.FromContextSafe(v.ctx)
|
||||||
|
for {
|
||||||
|
conn, err := l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("%s listener closed", name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go handleConn(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *BaseVisitor) Close() {
|
||||||
|
if v.l != nil {
|
||||||
|
v.l.Close()
|
||||||
|
}
|
||||||
|
if v.internalLn != nil {
|
||||||
|
v.internalLn.Close()
|
||||||
|
}
|
||||||
|
if v.plugin != nil {
|
||||||
|
v.plugin.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *BaseVisitor) dialRawVisitorConn(cfg *v1.VisitorBaseConfig) (net.Conn, error) {
|
||||||
|
visitorConn, err := v.helper.ConnectServer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("connect to server error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().Unix()
|
||||||
|
targetProxyName := naming.BuildTargetServerProxyName(v.clientCfg.User, cfg.ServerUser, cfg.ServerName)
|
||||||
|
newVisitorConnMsg := &msg.NewVisitorConn{
|
||||||
|
RunID: v.helper.RunID(),
|
||||||
|
ProxyName: targetProxyName,
|
||||||
|
SignKey: util.GetAuthKey(cfg.SecretKey, now),
|
||||||
|
Timestamp: now,
|
||||||
|
UseEncryption: cfg.Transport.UseEncryption,
|
||||||
|
UseCompression: cfg.Transport.UseCompression,
|
||||||
|
}
|
||||||
|
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
||||||
|
if err != nil {
|
||||||
|
visitorConn.Close()
|
||||||
|
return nil, fmt.Errorf("send newVisitorConnMsg to server error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var newVisitorConnRespMsg msg.NewVisitorConnResp
|
||||||
|
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||||
|
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
||||||
|
if err != nil {
|
||||||
|
visitorConn.Close()
|
||||||
|
return nil, fmt.Errorf("read newVisitorConnRespMsg error: %v", err)
|
||||||
|
}
|
||||||
|
_ = visitorConn.SetReadDeadline(time.Time{})
|
||||||
|
|
||||||
|
if newVisitorConnRespMsg.Error != "" {
|
||||||
|
visitorConn.Close()
|
||||||
|
return nil, fmt.Errorf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
||||||
|
}
|
||||||
|
return visitorConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapVisitorConn(conn io.ReadWriteCloser, cfg *v1.VisitorBaseConfig) (io.ReadWriteCloser, func(), error) {
|
||||||
|
rwc := conn
|
||||||
|
if cfg.Transport.UseEncryption {
|
||||||
|
var err error
|
||||||
|
rwc, err = libio.WithEncryption(rwc, []byte(cfg.SecretKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, func() {}, fmt.Errorf("create encryption stream error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recycleFn := func() {}
|
||||||
|
if cfg.Transport.UseCompression {
|
||||||
|
rwc, recycleFn = libio.WithCompressionFromPool(rwc)
|
||||||
|
}
|
||||||
|
return rwc, recycleFn, nil
|
||||||
|
}
|
||||||
227
client/visitor/visitor_manager.go
Normal file
227
client/visitor/visitor_manager.go
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
// Copyright 2018 fatedier, fatedier@gmail.com
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package visitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/samber/lo"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/transport"
|
||||||
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
clientCfg *v1.ClientCommonConfig
|
||||||
|
cfgs map[string]v1.VisitorConfigurer
|
||||||
|
visitors map[string]Visitor
|
||||||
|
helper Helper
|
||||||
|
|
||||||
|
checkInterval time.Duration
|
||||||
|
keepVisitorsRunningOnce sync.Once
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
ctx context.Context
|
||||||
|
|
||||||
|
stopCh chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager(
|
||||||
|
ctx context.Context,
|
||||||
|
runID string,
|
||||||
|
clientCfg *v1.ClientCommonConfig,
|
||||||
|
connectServer func() (net.Conn, error),
|
||||||
|
msgTransporter transport.MessageTransporter,
|
||||||
|
vnetController *vnet.Controller,
|
||||||
|
) *Manager {
|
||||||
|
m := &Manager{
|
||||||
|
clientCfg: clientCfg,
|
||||||
|
cfgs: make(map[string]v1.VisitorConfigurer),
|
||||||
|
visitors: make(map[string]Visitor),
|
||||||
|
checkInterval: 10 * time.Second,
|
||||||
|
ctx: ctx,
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
|
}
|
||||||
|
m.helper = &visitorHelperImpl{
|
||||||
|
connectServerFn: connectServer,
|
||||||
|
msgTransporter: msgTransporter,
|
||||||
|
vnetController: vnetController,
|
||||||
|
transferConnFn: m.TransferConn,
|
||||||
|
runID: runID,
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// keepVisitorsRunning checks all visitors' status periodically, if some visitor is not running, start it.
|
||||||
|
// It will only start after Reload is called and a new visitor is added.
|
||||||
|
func (vm *Manager) keepVisitorsRunning() {
|
||||||
|
xl := xlog.FromContextSafe(vm.ctx)
|
||||||
|
|
||||||
|
ticker := time.NewTicker(vm.checkInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-vm.stopCh:
|
||||||
|
xl.Tracef("gracefully shutdown visitor manager")
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
vm.mu.Lock()
|
||||||
|
for _, cfg := range vm.cfgs {
|
||||||
|
name := cfg.GetBaseConfig().Name
|
||||||
|
if _, exist := vm.visitors[name]; !exist {
|
||||||
|
xl.Infof("try to start visitor [%s]", name)
|
||||||
|
_ = vm.startVisitor(cfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vm.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *Manager) Close() {
|
||||||
|
vm.mu.Lock()
|
||||||
|
defer vm.mu.Unlock()
|
||||||
|
for _, v := range vm.visitors {
|
||||||
|
v.Close()
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-vm.stopCh:
|
||||||
|
default:
|
||||||
|
close(vm.stopCh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hold lock before calling this function.
|
||||||
|
func (vm *Manager) startVisitor(cfg v1.VisitorConfigurer) (err error) {
|
||||||
|
xl := xlog.FromContextSafe(vm.ctx)
|
||||||
|
name := cfg.GetBaseConfig().Name
|
||||||
|
visitor, err := NewVisitor(vm.ctx, cfg, vm.clientCfg, vm.helper)
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("new visitor error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = visitor.Run()
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("start error: %v", err)
|
||||||
|
} else {
|
||||||
|
vm.visitors[name] = visitor
|
||||||
|
xl.Infof("start visitor success")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *Manager) UpdateAll(cfgs []v1.VisitorConfigurer) {
|
||||||
|
if len(cfgs) > 0 {
|
||||||
|
// Only start keepVisitorsRunning goroutine once and only when there is at least one visitor.
|
||||||
|
vm.keepVisitorsRunningOnce.Do(func() {
|
||||||
|
go vm.keepVisitorsRunning()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
xl := xlog.FromContextSafe(vm.ctx)
|
||||||
|
cfgsMap := lo.KeyBy(cfgs, func(c v1.VisitorConfigurer) string {
|
||||||
|
return c.GetBaseConfig().Name
|
||||||
|
})
|
||||||
|
vm.mu.Lock()
|
||||||
|
defer vm.mu.Unlock()
|
||||||
|
|
||||||
|
delNames := make([]string, 0)
|
||||||
|
for name, oldCfg := range vm.cfgs {
|
||||||
|
del := false
|
||||||
|
cfg, ok := cfgsMap[name]
|
||||||
|
if !ok || !reflect.DeepEqual(oldCfg, cfg) {
|
||||||
|
del = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if del {
|
||||||
|
delNames = append(delNames, name)
|
||||||
|
delete(vm.cfgs, name)
|
||||||
|
if visitor, ok := vm.visitors[name]; ok {
|
||||||
|
visitor.Close()
|
||||||
|
}
|
||||||
|
delete(vm.visitors, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(delNames) > 0 {
|
||||||
|
xl.Infof("visitor removed: %v", delNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
addNames := make([]string, 0)
|
||||||
|
for _, cfg := range cfgs {
|
||||||
|
name := cfg.GetBaseConfig().Name
|
||||||
|
if _, ok := vm.cfgs[name]; !ok {
|
||||||
|
vm.cfgs[name] = cfg
|
||||||
|
addNames = append(addNames, name)
|
||||||
|
_ = vm.startVisitor(cfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(addNames) > 0 {
|
||||||
|
xl.Infof("visitor added: %v", addNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransferConn transfers a connection to a visitor.
|
||||||
|
func (vm *Manager) TransferConn(name string, conn net.Conn) error {
|
||||||
|
vm.mu.RLock()
|
||||||
|
defer vm.mu.RUnlock()
|
||||||
|
v, ok := vm.visitors[name]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("visitor [%s] not found", name)
|
||||||
|
}
|
||||||
|
return v.AcceptConn(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *Manager) GetVisitorCfg(name string) (v1.VisitorConfigurer, bool) {
|
||||||
|
vm.mu.RLock()
|
||||||
|
defer vm.mu.RUnlock()
|
||||||
|
cfg, ok := vm.cfgs[name]
|
||||||
|
return cfg, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
type visitorHelperImpl struct {
|
||||||
|
connectServerFn func() (net.Conn, error)
|
||||||
|
msgTransporter transport.MessageTransporter
|
||||||
|
vnetController *vnet.Controller
|
||||||
|
transferConnFn func(name string, conn net.Conn) error
|
||||||
|
runID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *visitorHelperImpl) ConnectServer() (net.Conn, error) {
|
||||||
|
return v.connectServerFn()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *visitorHelperImpl) TransferConn(name string, conn net.Conn) error {
|
||||||
|
return v.transferConnFn(name, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *visitorHelperImpl) MsgTransporter() transport.MessageTransporter {
|
||||||
|
return v.msgTransporter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *visitorHelperImpl) VNetController() *vnet.Controller {
|
||||||
|
return v.vnetController
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *visitorHelperImpl) RunID() string {
|
||||||
|
return v.runID
|
||||||
|
}
|
||||||
450
client/visitor/xtcp.go
Normal file
450
client/visitor/xtcp.go
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
// Copyright 2017 fatedier, fatedier@gmail.com
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package visitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
libio "github.com/fatedier/golib/io"
|
||||||
|
fmux "github.com/hashicorp/yamux"
|
||||||
|
quic "github.com/quic-go/quic-go"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
"github.com/fatedier/frp/pkg/naming"
|
||||||
|
"github.com/fatedier/frp/pkg/nathole"
|
||||||
|
"github.com/fatedier/frp/pkg/transport"
|
||||||
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNoTunnelSession = errors.New("no tunnel session")
|
||||||
|
|
||||||
|
type XTCPVisitor struct {
|
||||||
|
*BaseVisitor
|
||||||
|
session TunnelSession
|
||||||
|
startTunnelCh chan struct{}
|
||||||
|
retryLimiter *rate.Limiter
|
||||||
|
cancel context.CancelFunc
|
||||||
|
|
||||||
|
cfg *v1.XTCPVisitorConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv *XTCPVisitor) Run() (err error) {
|
||||||
|
sv.ctx, sv.cancel = context.WithCancel(sv.ctx)
|
||||||
|
|
||||||
|
if sv.cfg.Protocol == "kcp" {
|
||||||
|
sv.session = NewKCPTunnelSession()
|
||||||
|
} else {
|
||||||
|
sv.session = NewQUICTunnelSession(sv.clientCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sv.cfg.BindPort > 0 {
|
||||||
|
sv.l, err = net.Listen("tcp", net.JoinHostPort(sv.cfg.BindAddr, strconv.Itoa(sv.cfg.BindPort)))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go sv.acceptLoop(sv.l, "xtcp local", sv.handleConn)
|
||||||
|
}
|
||||||
|
|
||||||
|
go sv.acceptLoop(sv.internalLn, "xtcp internal", sv.handleConn)
|
||||||
|
go sv.processTunnelStartEvents()
|
||||||
|
if sv.cfg.KeepTunnelOpen {
|
||||||
|
sv.retryLimiter = rate.NewLimiter(rate.Every(time.Hour/time.Duration(sv.cfg.MaxRetriesAnHour)), sv.cfg.MaxRetriesAnHour)
|
||||||
|
go sv.keepTunnelOpenWorker()
|
||||||
|
}
|
||||||
|
|
||||||
|
if sv.plugin != nil {
|
||||||
|
sv.plugin.Start()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv *XTCPVisitor) Close() {
|
||||||
|
sv.mu.Lock()
|
||||||
|
defer sv.mu.Unlock()
|
||||||
|
sv.BaseVisitor.Close()
|
||||||
|
if sv.cancel != nil {
|
||||||
|
sv.cancel()
|
||||||
|
}
|
||||||
|
if sv.session != nil {
|
||||||
|
sv.session.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv *XTCPVisitor) processTunnelStartEvents() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-sv.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-sv.startTunnelCh:
|
||||||
|
start := time.Now()
|
||||||
|
sv.makeNatHole()
|
||||||
|
duration := time.Since(start)
|
||||||
|
// avoid too frequently
|
||||||
|
if duration < 10*time.Second {
|
||||||
|
time.Sleep(10*time.Second - duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv *XTCPVisitor) keepTunnelOpenWorker() {
|
||||||
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
|
ticker := time.NewTicker(time.Duration(sv.cfg.MinRetryInterval) * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
sv.startTunnelCh <- struct{}{}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-sv.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
xl.Debugf("keepTunnelOpenWorker try to check tunnel...")
|
||||||
|
conn, err := sv.getTunnelConn(sv.ctx)
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("keepTunnelOpenWorker get tunnel connection error: %v", err)
|
||||||
|
_ = sv.retryLimiter.Wait(sv.ctx)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
xl.Debugf("keepTunnelOpenWorker check success")
|
||||||
|
if conn != nil {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
||||||
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
|
isConnTransferred := false
|
||||||
|
var tunnelErr error
|
||||||
|
defer func() {
|
||||||
|
if !isConnTransferred {
|
||||||
|
// If there was an error and connection supports CloseWithError, use it
|
||||||
|
if tunnelErr != nil {
|
||||||
|
if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok {
|
||||||
|
_ = eConn.CloseWithError(tunnelErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userConn.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
xl.Debugf("get a new xtcp user connection")
|
||||||
|
|
||||||
|
// Open a tunnel connection to the server. If there is already a successful hole-punching connection,
|
||||||
|
// it will be reused. Otherwise, it will block and wait for a successful hole-punching connection until timeout.
|
||||||
|
ctx := sv.ctx
|
||||||
|
if sv.cfg.FallbackTo != "" {
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(sv.cfg.FallbackTimeoutMs)*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
ctx = timeoutCtx
|
||||||
|
}
|
||||||
|
tunnelConn, err := sv.openTunnel(ctx)
|
||||||
|
if err != nil {
|
||||||
|
xl.Errorf("open tunnel error: %v", err)
|
||||||
|
tunnelErr = err
|
||||||
|
|
||||||
|
// no fallback, just return
|
||||||
|
if sv.cfg.FallbackTo == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
xl.Debugf("try to transfer connection to visitor: %s", sv.cfg.FallbackTo)
|
||||||
|
if err := sv.helper.TransferConn(sv.cfg.FallbackTo, userConn); err != nil {
|
||||||
|
xl.Errorf("transfer connection to visitor %s error: %v", sv.cfg.FallbackTo, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isConnTransferred = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
muxConnRWCloser, recycleFn, err := wrapVisitorConn(tunnelConn, sv.cfg.GetBaseConfig())
|
||||||
|
if err != nil {
|
||||||
|
xl.Errorf("%v", err)
|
||||||
|
tunnelConn.Close()
|
||||||
|
tunnelErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer recycleFn()
|
||||||
|
|
||||||
|
_, _, errs := libio.Join(userConn, muxConnRWCloser)
|
||||||
|
xl.Debugf("join connections closed")
|
||||||
|
if len(errs) > 0 {
|
||||||
|
xl.Tracef("join connections errors: %v", errs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// openTunnel will open a tunnel connection to the target server.
|
||||||
|
func (sv *XTCPVisitor) openTunnel(ctx context.Context) (conn net.Conn, err error) {
|
||||||
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
timer := time.NewTimer(0)
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-sv.ctx.Done():
|
||||||
|
return nil, sv.ctx.Err()
|
||||||
|
case <-ctx.Done():
|
||||||
|
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||||
|
return nil, fmt.Errorf("open tunnel timeout")
|
||||||
|
}
|
||||||
|
return nil, ctx.Err()
|
||||||
|
case <-timer.C:
|
||||||
|
conn, err = sv.getTunnelConn(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, ErrNoTunnelSession) {
|
||||||
|
xl.Warnf("get tunnel connection error: %v", err)
|
||||||
|
}
|
||||||
|
timer.Reset(500 * time.Millisecond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv *XTCPVisitor) getTunnelConn(ctx context.Context) (net.Conn, error) {
|
||||||
|
conn, err := sv.session.OpenConn(ctx)
|
||||||
|
if err == nil {
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
sv.session.Close()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case sv.startTunnelCh <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0. PreCheck
|
||||||
|
// 1. Prepare
|
||||||
|
// 2. ExchangeInfo
|
||||||
|
// 3. MakeNATHole
|
||||||
|
// 4. Create a tunnel session using an underlying UDP connection.
|
||||||
|
func (sv *XTCPVisitor) makeNatHole() {
|
||||||
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
|
targetProxyName := naming.BuildTargetServerProxyName(sv.clientCfg.User, sv.cfg.ServerUser, sv.cfg.ServerName)
|
||||||
|
xl.Tracef("makeNatHole start")
|
||||||
|
if err := nathole.PreCheck(sv.ctx, sv.helper.MsgTransporter(), targetProxyName, 5*time.Second); err != nil {
|
||||||
|
xl.Warnf("nathole precheck error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
xl.Tracef("nathole prepare start")
|
||||||
|
|
||||||
|
// Prepare NAT traversal options
|
||||||
|
var opts nathole.PrepareOptions
|
||||||
|
if sv.cfg.NatTraversal != nil && sv.cfg.NatTraversal.DisableAssistedAddrs {
|
||||||
|
opts.DisableAssistedAddrs = true
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareResult, err := nathole.Prepare([]string{sv.clientCfg.NatHoleSTUNServer}, opts)
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("nathole prepare error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v",
|
||||||
|
prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs)
|
||||||
|
|
||||||
|
listenConn := prepareResult.ListenConn
|
||||||
|
|
||||||
|
// send NatHoleVisitor to server
|
||||||
|
now := time.Now().Unix()
|
||||||
|
transactionID := nathole.NewTransactionID()
|
||||||
|
natHoleVisitorMsg := &msg.NatHoleVisitor{
|
||||||
|
TransactionID: transactionID,
|
||||||
|
ProxyName: targetProxyName,
|
||||||
|
Protocol: sv.cfg.Protocol,
|
||||||
|
SignKey: util.GetAuthKey(sv.cfg.SecretKey, now),
|
||||||
|
Timestamp: now,
|
||||||
|
MappedAddrs: prepareResult.Addrs,
|
||||||
|
AssistedAddrs: prepareResult.AssistedAddrs,
|
||||||
|
}
|
||||||
|
|
||||||
|
xl.Tracef("nathole exchange info start")
|
||||||
|
natHoleRespMsg, err := nathole.ExchangeInfo(sv.ctx, sv.helper.MsgTransporter(), transactionID, natHoleVisitorMsg, 5*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
listenConn.Close()
|
||||||
|
xl.Warnf("nathole exchange info error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
xl.Infof("get natHoleRespMsg, sid [%s], protocol [%s], candidate address %v, assisted address %v, detectBehavior: %+v",
|
||||||
|
natHoleRespMsg.Sid, natHoleRespMsg.Protocol, natHoleRespMsg.CandidateAddrs,
|
||||||
|
natHoleRespMsg.AssistedAddrs, natHoleRespMsg.DetectBehavior)
|
||||||
|
|
||||||
|
newListenConn, raddr, err := nathole.MakeHole(sv.ctx, listenConn, natHoleRespMsg, []byte(sv.cfg.SecretKey))
|
||||||
|
if err != nil {
|
||||||
|
listenConn.Close()
|
||||||
|
xl.Warnf("make hole error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
listenConn = newListenConn
|
||||||
|
xl.Infof("establishing nat hole connection successful, sid [%s], remoteAddr [%s]", natHoleRespMsg.Sid, raddr)
|
||||||
|
|
||||||
|
if err := sv.session.Init(listenConn, raddr); err != nil {
|
||||||
|
listenConn.Close()
|
||||||
|
xl.Warnf("init tunnel session error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TunnelSession interface {
|
||||||
|
Init(listenConn *net.UDPConn, raddr *net.UDPAddr) error
|
||||||
|
OpenConn(context.Context) (net.Conn, error)
|
||||||
|
Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
type KCPTunnelSession struct {
|
||||||
|
session *fmux.Session
|
||||||
|
lConn *net.UDPConn
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKCPTunnelSession() TunnelSession {
|
||||||
|
return &KCPTunnelSession{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ks *KCPTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) error {
|
||||||
|
listenConn.Close()
|
||||||
|
laddr, _ := net.ResolveUDPAddr("udp", listenConn.LocalAddr().String())
|
||||||
|
lConn, err := net.DialUDP("udp", laddr, raddr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dial udp error: %v", err)
|
||||||
|
}
|
||||||
|
remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String())
|
||||||
|
if err != nil {
|
||||||
|
lConn.Close()
|
||||||
|
return fmt.Errorf("create kcp connection from udp connection error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmuxCfg := fmux.DefaultConfig()
|
||||||
|
fmuxCfg.KeepAliveInterval = 10 * time.Second
|
||||||
|
fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
|
||||||
|
fmuxCfg.LogOutput = io.Discard
|
||||||
|
session, err := fmux.Client(remote, fmuxCfg)
|
||||||
|
if err != nil {
|
||||||
|
remote.Close()
|
||||||
|
return fmt.Errorf("initial client session error: %v", err)
|
||||||
|
}
|
||||||
|
ks.mu.Lock()
|
||||||
|
ks.session = session
|
||||||
|
ks.lConn = lConn
|
||||||
|
ks.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ks *KCPTunnelSession) OpenConn(_ context.Context) (net.Conn, error) {
|
||||||
|
ks.mu.RLock()
|
||||||
|
defer ks.mu.RUnlock()
|
||||||
|
session := ks.session
|
||||||
|
if session == nil {
|
||||||
|
return nil, ErrNoTunnelSession
|
||||||
|
}
|
||||||
|
return session.Open()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ks *KCPTunnelSession) Close() {
|
||||||
|
ks.mu.Lock()
|
||||||
|
defer ks.mu.Unlock()
|
||||||
|
if ks.session != nil {
|
||||||
|
_ = ks.session.Close()
|
||||||
|
ks.session = nil
|
||||||
|
}
|
||||||
|
if ks.lConn != nil {
|
||||||
|
_ = ks.lConn.Close()
|
||||||
|
ks.lConn = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type QUICTunnelSession struct {
|
||||||
|
session *quic.Conn
|
||||||
|
listenConn *net.UDPConn
|
||||||
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
clientCfg *v1.ClientCommonConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewQUICTunnelSession(clientCfg *v1.ClientCommonConfig) TunnelSession {
|
||||||
|
return &QUICTunnelSession{
|
||||||
|
clientCfg: clientCfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qs *QUICTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) error {
|
||||||
|
tlsConfig, err := transport.NewClientTLSConfig("", "", "", raddr.String())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create tls config error: %v", err)
|
||||||
|
}
|
||||||
|
tlsConfig.NextProtos = []string{"frp"}
|
||||||
|
quicConn, err := quic.Dial(context.Background(), listenConn, raddr, tlsConfig,
|
||||||
|
&quic.Config{
|
||||||
|
MaxIdleTimeout: time.Duration(qs.clientCfg.Transport.QUIC.MaxIdleTimeout) * time.Second,
|
||||||
|
MaxIncomingStreams: int64(qs.clientCfg.Transport.QUIC.MaxIncomingStreams),
|
||||||
|
KeepAlivePeriod: time.Duration(qs.clientCfg.Transport.QUIC.KeepalivePeriod) * time.Second,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dial quic error: %v", err)
|
||||||
|
}
|
||||||
|
qs.mu.Lock()
|
||||||
|
qs.session = quicConn
|
||||||
|
qs.listenConn = listenConn
|
||||||
|
qs.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qs *QUICTunnelSession) OpenConn(ctx context.Context) (net.Conn, error) {
|
||||||
|
qs.mu.RLock()
|
||||||
|
defer qs.mu.RUnlock()
|
||||||
|
session := qs.session
|
||||||
|
if session == nil {
|
||||||
|
return nil, ErrNoTunnelSession
|
||||||
|
}
|
||||||
|
stream, err := session.OpenStreamSync(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return netpkg.QuicStreamToNetConn(stream, session), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qs *QUICTunnelSession) Close() {
|
||||||
|
qs.mu.Lock()
|
||||||
|
defer qs.mu.Unlock()
|
||||||
|
if qs.session != nil {
|
||||||
|
_ = qs.session.CloseWithError(0, "")
|
||||||
|
qs.session = nil
|
||||||
|
}
|
||||||
|
if qs.listenConn != nil {
|
||||||
|
_ = qs.listenConn.Close()
|
||||||
|
qs.listenConn = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,24 +12,15 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package msg
|
package main
|
||||||
|
|
||||||
type GeneralRes struct {
|
import (
|
||||||
Code int64 `json:"code"`
|
"github.com/fatedier/frp/cmd/frpc/sub"
|
||||||
Msg string `json:"msg"`
|
"github.com/fatedier/frp/pkg/util/system"
|
||||||
}
|
_ "github.com/fatedier/frp/web/frpc"
|
||||||
|
)
|
||||||
|
|
||||||
// messages between control connection of frpc and frps
|
func main() {
|
||||||
type ControlReq struct {
|
system.EnableCompatibilityMode()
|
||||||
Type int64 `json:"type"`
|
sub.Execute()
|
||||||
ProxyName string `json:"proxy_name,omitempty"`
|
|
||||||
AuthKey string `json:"auth_key, omitempty"`
|
|
||||||
UseEncryption bool `json:"use_encryption, omitempty"`
|
|
||||||
Timestamp int64 `json:"timestamp, omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ControlRes struct {
|
|
||||||
Type int64 `json:"type"`
|
|
||||||
Code int64 `json:"code"`
|
|
||||||
Msg string `json:"msg"`
|
|
||||||
}
|
}
|
||||||
125
cmd/frpc/sub/admin.go
Normal file
125
cmd/frpc/sub/admin.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
// Copyright 2023 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package sub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rodaine/table"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/config"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
clientsdk "github.com/fatedier/frp/pkg/sdk/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
var adminAPITimeout = 30 * time.Second
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commands := []struct {
|
||||||
|
name string
|
||||||
|
description string
|
||||||
|
handler func(*v1.ClientCommonConfig) error
|
||||||
|
}{
|
||||||
|
{"reload", "Hot-Reload frpc configuration", ReloadHandler},
|
||||||
|
{"status", "Overview of all proxies status", StatusHandler},
|
||||||
|
{"stop", "Stop the running frpc", StopHandler},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cmdConfig := range commands {
|
||||||
|
cmd := NewAdminCommand(cmdConfig.name, cmdConfig.description, cmdConfig.handler)
|
||||||
|
cmd.Flags().DurationVar(&adminAPITimeout, "api-timeout", adminAPITimeout, "Timeout for admin API calls")
|
||||||
|
rootCmd.AddCommand(cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) error) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: name,
|
||||||
|
Short: short,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if cfg.WebServer.Port <= 0 {
|
||||||
|
fmt.Println("web server port should be set if you want to use this feature")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := handler(cfg); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReloadHandler(clientCfg *v1.ClientCommonConfig) error {
|
||||||
|
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
|
||||||
|
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout)
|
||||||
|
defer cancel()
|
||||||
|
if err := client.Reload(ctx, strictConfigMode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println("reload success")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func StatusHandler(clientCfg *v1.ClientCommonConfig) error {
|
||||||
|
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
|
||||||
|
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout)
|
||||||
|
defer cancel()
|
||||||
|
res, err := client.GetAllProxyStatus(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Proxy Status...\n\n")
|
||||||
|
for _, typ := range proxyTypes {
|
||||||
|
arrs := res[string(typ)]
|
||||||
|
if len(arrs) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(strings.ToUpper(string(typ)))
|
||||||
|
tbl := table.New("Name", "Status", "LocalAddr", "Plugin", "RemoteAddr", "Error")
|
||||||
|
for _, ps := range arrs {
|
||||||
|
tbl.AddRow(ps.Name, ps.Status, ps.LocalAddr, ps.Plugin, ps.RemoteAddr, ps.Err)
|
||||||
|
}
|
||||||
|
tbl.Print()
|
||||||
|
fmt.Println("")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func StopHandler(clientCfg *v1.ClientCommonConfig) error {
|
||||||
|
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
|
||||||
|
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout)
|
||||||
|
defer cancel()
|
||||||
|
if err := client.Stop(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println("stop success")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
100
cmd/frpc/sub/nathole.go
Normal file
100
cmd/frpc/sub/nathole.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
// Copyright 2023 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package sub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/config"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/nathole"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
natHoleSTUNServer string
|
||||||
|
natHoleLocalAddr string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(natholeCmd)
|
||||||
|
natholeCmd.AddCommand(natholeDiscoveryCmd)
|
||||||
|
|
||||||
|
natholeCmd.PersistentFlags().StringVarP(&natHoleSTUNServer, "nat_hole_stun_server", "", "", "STUN server address for nathole")
|
||||||
|
natholeCmd.PersistentFlags().StringVarP(&natHoleLocalAddr, "nat_hole_local_addr", "l", "", "local address to connect STUN server")
|
||||||
|
}
|
||||||
|
|
||||||
|
var natholeCmd = &cobra.Command{
|
||||||
|
Use: "nathole",
|
||||||
|
Short: "Actions about nathole",
|
||||||
|
}
|
||||||
|
|
||||||
|
var natholeDiscoveryCmd = &cobra.Command{
|
||||||
|
Use: "discover",
|
||||||
|
Short: "Discover nathole information from stun server",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
// ignore error here, because we can use command line parameters
|
||||||
|
cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
|
||||||
|
if err != nil {
|
||||||
|
cfg = &v1.ClientCommonConfig{}
|
||||||
|
if err := cfg.Complete(); err != nil {
|
||||||
|
fmt.Printf("failed to complete config: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if natHoleSTUNServer != "" {
|
||||||
|
cfg.NatHoleSTUNServer = natHoleSTUNServer
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateForNatHoleDiscovery(cfg); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs, localAddr, err := nathole.Discover([]string{cfg.NatHoleSTUNServer}, natHoleLocalAddr)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("discover error:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if len(addrs) < 2 {
|
||||||
|
fmt.Printf("discover error: can not get enough addresses, need 2, got: %v\n", addrs)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
localIPs, _ := nathole.ListLocalIPsForNatHole(10)
|
||||||
|
|
||||||
|
natFeature, err := nathole.ClassifyNATFeature(addrs, localIPs)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("classify nat feature error:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println("STUN server:", cfg.NatHoleSTUNServer)
|
||||||
|
fmt.Println("Your NAT type is:", natFeature.NatType)
|
||||||
|
fmt.Println("Behavior is:", natFeature.Behavior)
|
||||||
|
fmt.Println("External address is:", addrs)
|
||||||
|
fmt.Println("Local address is:", localAddr.String())
|
||||||
|
fmt.Println("Public Network:", natFeature.PublicNetwork)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateForNatHoleDiscovery(cfg *v1.ClientCommonConfig) error {
|
||||||
|
if cfg.NatHoleSTUNServer == "" {
|
||||||
|
return fmt.Errorf("nat_hole_stun_server can not be empty")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
151
cmd/frpc/sub/proxy.go
Normal file
151
cmd/frpc/sub/proxy.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
// Copyright 2023 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package sub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/config"
|
||||||
|
"github.com/fatedier/frp/pkg/config/source"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
|
)
|
||||||
|
|
||||||
|
var proxyTypes = []v1.ProxyType{
|
||||||
|
v1.ProxyTypeTCP,
|
||||||
|
v1.ProxyTypeUDP,
|
||||||
|
v1.ProxyTypeTCPMUX,
|
||||||
|
v1.ProxyTypeHTTP,
|
||||||
|
v1.ProxyTypeHTTPS,
|
||||||
|
v1.ProxyTypeSTCP,
|
||||||
|
v1.ProxyTypeSUDP,
|
||||||
|
v1.ProxyTypeXTCP,
|
||||||
|
}
|
||||||
|
|
||||||
|
var visitorTypes = []v1.VisitorType{
|
||||||
|
v1.VisitorTypeSTCP,
|
||||||
|
v1.VisitorTypeSUDP,
|
||||||
|
v1.VisitorTypeXTCP,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for _, typ := range proxyTypes {
|
||||||
|
c := v1.NewProxyConfigurerByType(typ)
|
||||||
|
if c == nil {
|
||||||
|
panic("proxy type: " + typ + " not support")
|
||||||
|
}
|
||||||
|
clientCfg := v1.ClientCommonConfig{}
|
||||||
|
cmd := NewProxyCommand(string(typ), c, &clientCfg)
|
||||||
|
config.RegisterClientCommonConfigFlags(cmd, &clientCfg)
|
||||||
|
config.RegisterProxyFlags(cmd, c)
|
||||||
|
|
||||||
|
// add sub command for visitor
|
||||||
|
if slices.Contains(visitorTypes, v1.VisitorType(typ)) {
|
||||||
|
vc := v1.NewVisitorConfigurerByType(v1.VisitorType(typ))
|
||||||
|
if vc == nil {
|
||||||
|
panic("visitor type: " + typ + " not support")
|
||||||
|
}
|
||||||
|
visitorCmd := NewVisitorCommand(string(typ), vc, &clientCfg)
|
||||||
|
config.RegisterVisitorFlags(visitorCmd, vc)
|
||||||
|
cmd.AddCommand(visitorCmd)
|
||||||
|
}
|
||||||
|
rootCmd.AddCommand(cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientCommonConfig) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: name,
|
||||||
|
Short: fmt.Sprintf("Run frpc with a single %s proxy", name),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := clientCfg.Complete(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||||
|
validator := validation.NewConfigValidator(unsafeFeatures)
|
||||||
|
if _, err := validator.ValidateClientCommonConfig(clientCfg); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.GetBaseConfig().Type = name
|
||||||
|
c.Complete()
|
||||||
|
proxyCfg := c
|
||||||
|
if err := validation.ValidateProxyConfigurerForClient(proxyCfg); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
err := startService(clientCfg, []v1.ProxyConfigurer{proxyCfg}, nil, unsafeFeatures, "")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.ClientCommonConfig) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "visitor",
|
||||||
|
Short: fmt.Sprintf("Run frpc with a single %s visitor", name),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := clientCfg.Complete(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||||
|
validator := validation.NewConfigValidator(unsafeFeatures)
|
||||||
|
if _, err := validator.ValidateClientCommonConfig(clientCfg); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.GetBaseConfig().Type = name
|
||||||
|
c.Complete()
|
||||||
|
visitorCfg := c
|
||||||
|
if err := validation.ValidateVisitorConfigurer(visitorCfg); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
err := startService(clientCfg, nil, []v1.VisitorConfigurer{visitorCfg}, unsafeFeatures, "")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startService(
|
||||||
|
cfg *v1.ClientCommonConfig,
|
||||||
|
proxyCfgs []v1.ProxyConfigurer,
|
||||||
|
visitorCfgs []v1.VisitorConfigurer,
|
||||||
|
unsafeFeatures *security.UnsafeFeatures,
|
||||||
|
cfgFile string,
|
||||||
|
) error {
|
||||||
|
configSource := source.NewConfigSource()
|
||||||
|
if err := configSource.ReplaceAll(proxyCfgs, visitorCfgs); err != nil {
|
||||||
|
return fmt.Errorf("failed to set config source: %w", err)
|
||||||
|
}
|
||||||
|
aggregator := source.NewAggregator(configSource)
|
||||||
|
return startServiceWithAggregator(cfg, aggregator, unsafeFeatures, cfgFile)
|
||||||
|
}
|
||||||
219
cmd/frpc/sub/root.go
Normal file
219
cmd/frpc/sub/root.go
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
// Copyright 2018 fatedier, fatedier@gmail.com
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package sub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/client"
|
||||||
|
"github.com/fatedier/frp/pkg/config"
|
||||||
|
"github.com/fatedier/frp/pkg/config/source"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/featuregate"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
|
"github.com/fatedier/frp/pkg/util/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
cfgFile string
|
||||||
|
cfgDir string
|
||||||
|
showVersion bool
|
||||||
|
strictConfigMode bool
|
||||||
|
allowUnsafe []string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "./frpc.ini", "config file of frpc")
|
||||||
|
rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory")
|
||||||
|
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")
|
||||||
|
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause an errors")
|
||||||
|
|
||||||
|
rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{},
|
||||||
|
fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ClientUnsafeFeatures, ", ")))
|
||||||
|
}
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "frpc",
|
||||||
|
Short: "frpc is the client of frp (https://github.com/fatedier/frp)",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if showVersion {
|
||||||
|
fmt.Println(version.Full())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||||
|
|
||||||
|
// If cfgDir is not empty, run multiple frpc service for each config file in cfgDir.
|
||||||
|
// Note that it's only designed for testing. It's not guaranteed to be stable.
|
||||||
|
if cfgDir != "" {
|
||||||
|
_ = runMultipleClients(cfgDir, unsafeFeatures)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not show command usage here.
|
||||||
|
err := runClient(cfgFile, unsafeFeatures)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMultipleClients(cfgDir string, unsafeFeatures *security.UnsafeFeatures) error {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
err := filepath.WalkDir(cfgDir, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil || d.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
wg.Add(1)
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
err := runClient(path, unsafeFeatures)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("frpc service error for config file [%s]\n", path)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
wg.Wait()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func Execute() {
|
||||||
|
rootCmd.SetGlobalNormalizationFunc(config.WordSepNormalizeFunc)
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTermSignal(svr *client.Service) {
|
||||||
|
ch := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-ch
|
||||||
|
svr.GracefulClose(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) error {
|
||||||
|
// Load configuration
|
||||||
|
result, err := config.LoadClientConfigResult(cfgFilePath, strictConfigMode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if result.IsLegacyFormat {
|
||||||
|
fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " +
|
||||||
|
"please use yaml/json/toml format instead!\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Common.FeatureGates) > 0 {
|
||||||
|
if err := featuregate.SetFromMap(result.Common.FeatureGates); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return runClientWithAggregator(result, unsafeFeatures, cfgFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// runClientWithAggregator runs the client using the internal source aggregator.
|
||||||
|
func runClientWithAggregator(result *config.ClientConfigLoadResult, unsafeFeatures *security.UnsafeFeatures, cfgFilePath string) error {
|
||||||
|
configSource := source.NewConfigSource()
|
||||||
|
if err := configSource.ReplaceAll(result.Proxies, result.Visitors); err != nil {
|
||||||
|
return fmt.Errorf("failed to set config source: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var storeSource *source.StoreSource
|
||||||
|
|
||||||
|
if result.Common.Store.IsEnabled() {
|
||||||
|
storePath := result.Common.Store.Path
|
||||||
|
if storePath != "" && cfgFilePath != "" && !filepath.IsAbs(storePath) {
|
||||||
|
storePath = filepath.Join(filepath.Dir(cfgFilePath), storePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := source.NewStoreSource(source.StoreSourceConfig{
|
||||||
|
Path: storePath,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create store source: %w", err)
|
||||||
|
}
|
||||||
|
storeSource = s
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregator := source.NewAggregator(configSource)
|
||||||
|
if storeSource != nil {
|
||||||
|
aggregator.SetStoreSource(storeSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyCfgs, visitorCfgs, err := aggregator.Load()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load config from sources: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyCfgs, visitorCfgs = config.FilterClientConfigurers(result.Common, proxyCfgs, visitorCfgs)
|
||||||
|
proxyCfgs = config.CompleteProxyConfigurers(proxyCfgs)
|
||||||
|
visitorCfgs = config.CompleteVisitorConfigurers(visitorCfgs)
|
||||||
|
|
||||||
|
warning, err := validation.ValidateAllClientConfig(result.Common, proxyCfgs, visitorCfgs, unsafeFeatures)
|
||||||
|
if warning != nil {
|
||||||
|
fmt.Printf("WARNING: %v\n", warning)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return startServiceWithAggregator(result.Common, aggregator, unsafeFeatures, cfgFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startServiceWithAggregator(
|
||||||
|
cfg *v1.ClientCommonConfig,
|
||||||
|
aggregator *source.Aggregator,
|
||||||
|
unsafeFeatures *security.UnsafeFeatures,
|
||||||
|
cfgFile string,
|
||||||
|
) error {
|
||||||
|
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
|
||||||
|
|
||||||
|
if cfgFile != "" {
|
||||||
|
log.Infof("start frpc service for config file [%s] with aggregated configuration", cfgFile)
|
||||||
|
defer log.Infof("frpc service for config file [%s] stopped", cfgFile)
|
||||||
|
}
|
||||||
|
svr, err := client.NewService(client.ServiceOptions{
|
||||||
|
Common: cfg,
|
||||||
|
ConfigSourceAggregator: aggregator,
|
||||||
|
UnsafeFeatures: unsafeFeatures,
|
||||||
|
ConfigFilePath: cfgFile,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldGracefulClose := cfg.Transport.Protocol == "kcp" || cfg.Transport.Protocol == "quic"
|
||||||
|
if shouldGracefulClose {
|
||||||
|
go handleTermSignal(svr)
|
||||||
|
}
|
||||||
|
return svr.Run(context.Background())
|
||||||
|
}
|
||||||
59
cmd/frpc/sub/verify.go
Normal file
59
cmd/frpc/sub/verify.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// Copyright 2021 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package sub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/config"
|
||||||
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(verifyCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
var verifyCmd = &cobra.Command{
|
||||||
|
Use: "verify",
|
||||||
|
Short: "Verify that the configures is valid",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if cfgFile == "" {
|
||||||
|
fmt.Println("frpc: the configuration file is not specified")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||||
|
warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, unsafeFeatures)
|
||||||
|
if warning != nil {
|
||||||
|
fmt.Printf("WARNING: %v\n", warning)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("frpc: the configuration file %s syntax is ok\n", cfgFile)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
26
cmd/frps/main.go
Normal file
26
cmd/frps/main.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Copyright 2018 fatedier, fatedier@gmail.com
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "github.com/fatedier/frp/pkg/metrics"
|
||||||
|
"github.com/fatedier/frp/pkg/util/system"
|
||||||
|
_ "github.com/fatedier/frp/web/frps"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
system.EnableCompatibilityMode()
|
||||||
|
Execute()
|
||||||
|
}
|
||||||
127
cmd/frps/root.go
Normal file
127
cmd/frps/root.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
// Copyright 2018 fatedier, fatedier@gmail.com
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/config"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
|
"github.com/fatedier/frp/pkg/util/version"
|
||||||
|
"github.com/fatedier/frp/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
cfgFile string
|
||||||
|
showVersion bool
|
||||||
|
strictConfigMode bool
|
||||||
|
allowUnsafe []string
|
||||||
|
|
||||||
|
serverCfg v1.ServerConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps")
|
||||||
|
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps")
|
||||||
|
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause errors")
|
||||||
|
rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{},
|
||||||
|
fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ServerUnsafeFeatures, ", ")))
|
||||||
|
|
||||||
|
config.RegisterServerConfigFlags(rootCmd, &serverCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "frps",
|
||||||
|
Short: "frps is the server of frp (https://github.com/fatedier/frp)",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if showVersion {
|
||||||
|
fmt.Println(version.Full())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
svrCfg *v1.ServerConfig
|
||||||
|
isLegacyFormat bool
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if cfgFile != "" {
|
||||||
|
svrCfg, isLegacyFormat, err = config.LoadServerConfig(cfgFile, strictConfigMode)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if isLegacyFormat {
|
||||||
|
fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " +
|
||||||
|
"please use yaml/json/toml format instead!\n")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := serverCfg.Complete(); err != nil {
|
||||||
|
fmt.Printf("failed to complete server config: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
svrCfg = &serverCfg
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||||
|
validator := validation.NewConfigValidator(unsafeFeatures)
|
||||||
|
warning, err := validator.ValidateServerConfig(svrCfg)
|
||||||
|
if warning != nil {
|
||||||
|
fmt.Printf("WARNING: %v\n", warning)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runServer(svrCfg); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func Execute() {
|
||||||
|
rootCmd.SetGlobalNormalizationFunc(config.WordSepNormalizeFunc)
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runServer(cfg *v1.ServerConfig) (err error) {
|
||||||
|
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
|
||||||
|
|
||||||
|
if cfgFile != "" {
|
||||||
|
log.Infof("frps uses config file: %s", cfgFile)
|
||||||
|
} else {
|
||||||
|
log.Infof("frps uses command line arguments for config")
|
||||||
|
}
|
||||||
|
|
||||||
|
svr, err := server.NewService(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Infof("frps started successfully")
|
||||||
|
svr.Run(context.Background())
|
||||||
|
return
|
||||||
|
}
|
||||||
59
cmd/frps/verify.go
Normal file
59
cmd/frps/verify.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// Copyright 2021 The frp Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/config"
|
||||||
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(verifyCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
var verifyCmd = &cobra.Command{
|
||||||
|
Use: "verify",
|
||||||
|
Short: "Verify that the configures is valid",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if cfgFile == "" {
|
||||||
|
fmt.Println("frps: the configuration file is not specified")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
svrCfg, _, err := config.LoadServerConfig(cfgFile, strictConfigMode)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||||
|
validator := validation.NewConfigValidator(unsafeFeatures)
|
||||||
|
warning, err := validator.ValidateServerConfig(svrCfg)
|
||||||
|
if warning != nil {
|
||||||
|
fmt.Printf("WARNING: %v\n", warning)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("frps: the configuration file %s syntax is ok\n", cfgFile)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# [common] is integral section
|
|
||||||
[common]
|
|
||||||
server_addr = 0.0.0.0
|
|
||||||
server_port = 7000
|
|
||||||
# console or real logFile path like ./frpc.log
|
|
||||||
log_file = console
|
|
||||||
# debug, info, warn, error
|
|
||||||
log_level = debug
|
|
||||||
# for authentication
|
|
||||||
auth_token = 123
|
|
||||||
|
|
||||||
# test1 is the proxy name same as server's configuration
|
|
||||||
[test1]
|
|
||||||
local_ip = 127.0.0.1
|
|
||||||
local_port = 22
|
|
||||||
# true or false, if true, messages between frps and frpc will be encrypted, default is false
|
|
||||||
use_encryption = true
|
|
||||||
9
conf/frpc.toml
Normal file
9
conf/frpc.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
serverAddr = "127.0.0.1"
|
||||||
|
serverPort = 7000
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "test-tcp"
|
||||||
|
type = "tcp"
|
||||||
|
localIP = "127.0.0.1"
|
||||||
|
localPort = 22
|
||||||
|
remotePort = 6000
|
||||||
465
conf/frpc_full_example.toml
Normal file
465
conf/frpc_full_example.toml
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
# This configuration file is for reference only. Please do not use this configuration directly to run the program as it may have various issues.
|
||||||
|
|
||||||
|
# Optional unique identifier for this frpc instance.
|
||||||
|
clientID = "your_client_id"
|
||||||
|
# your proxy name will be changed to {user}.{proxy}
|
||||||
|
user = "your_name"
|
||||||
|
|
||||||
|
# A literal address or host name for IPv6 must be enclosed
|
||||||
|
# in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80"
|
||||||
|
# For single serverAddr field, no need square brackets, like serverAddr = "::".
|
||||||
|
serverAddr = "0.0.0.0"
|
||||||
|
serverPort = 7000
|
||||||
|
|
||||||
|
# STUN server to help penetrate NAT hole.
|
||||||
|
# natHoleStunServer = "stun.easyvoip.com:3478"
|
||||||
|
|
||||||
|
# Decide if exit program when first login failed, otherwise continuous relogin to frps
|
||||||
|
# default is true
|
||||||
|
loginFailExit = true
|
||||||
|
|
||||||
|
# console or real logFile path like ./frpc.log
|
||||||
|
log.to = "./frpc.log"
|
||||||
|
# trace, debug, info, warn, error
|
||||||
|
log.level = "info"
|
||||||
|
log.maxDays = 3
|
||||||
|
# disable log colors when log.to is console, default is false
|
||||||
|
log.disablePrintColor = false
|
||||||
|
|
||||||
|
auth.method = "token"
|
||||||
|
# auth.additionalScopes specifies additional scopes to include authentication information.
|
||||||
|
# Optional values are HeartBeats, NewWorkConns.
|
||||||
|
# auth.additionalScopes = ["HeartBeats", "NewWorkConns"]
|
||||||
|
|
||||||
|
# auth token
|
||||||
|
auth.token = "12345678"
|
||||||
|
|
||||||
|
# alternatively, you can use tokenSource to load the token from a file
|
||||||
|
# this is mutually exclusive with auth.token
|
||||||
|
# auth.tokenSource.type = "file"
|
||||||
|
# auth.tokenSource.file.path = "/etc/frp/token"
|
||||||
|
|
||||||
|
# oidc.clientID specifies the client ID to use to get a token in OIDC authentication.
|
||||||
|
# auth.oidc.clientID = ""
|
||||||
|
# oidc.clientSecret specifies the client secret to use to get a token in OIDC authentication.
|
||||||
|
# auth.oidc.clientSecret = ""
|
||||||
|
# oidc.audience specifies the audience of the token in OIDC authentication.
|
||||||
|
# auth.oidc.audience = ""
|
||||||
|
# oidc.scope specifies the permissions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
|
||||||
|
# auth.oidc.scope = ""
|
||||||
|
# oidc.tokenEndpointURL specifies the URL which implements OIDC Token Endpoint.
|
||||||
|
# It will be used to get an OIDC token.
|
||||||
|
# auth.oidc.tokenEndpointURL = ""
|
||||||
|
|
||||||
|
# oidc.additionalEndpointParams specifies additional parameters to be sent to the OIDC Token Endpoint.
|
||||||
|
# For example, if you want to specify the "audience" parameter, you can set as follow.
|
||||||
|
# frp will add "audience=<value>" "var1=<value>" to the additional parameters.
|
||||||
|
# auth.oidc.additionalEndpointParams.audience = "https://dev.auth.com/api/v2/"
|
||||||
|
# auth.oidc.additionalEndpointParams.var1 = "foobar"
|
||||||
|
|
||||||
|
# OIDC TLS and proxy configuration
|
||||||
|
# Specify a custom CA certificate file for verifying the OIDC token endpoint's TLS certificate.
|
||||||
|
# This is useful when the OIDC provider uses a self-signed certificate or a custom CA.
|
||||||
|
# auth.oidc.trustedCaFile = "/path/to/ca.crt"
|
||||||
|
|
||||||
|
# Skip TLS certificate verification for the OIDC token endpoint.
|
||||||
|
# INSECURE: Only use this for debugging purposes, not recommended for production.
|
||||||
|
# auth.oidc.insecureSkipVerify = false
|
||||||
|
|
||||||
|
# Specify a proxy server for OIDC token endpoint connections.
|
||||||
|
# Supports http, https, socks5, and socks5h proxy protocols.
|
||||||
|
# If not specified, no proxy is used for OIDC connections.
|
||||||
|
# auth.oidc.proxyURL = "http://proxy.example.com:8080"
|
||||||
|
|
||||||
|
# Set admin address for control frpc's action by http api such as reload
|
||||||
|
webServer.addr = "127.0.0.1"
|
||||||
|
webServer.port = 7400
|
||||||
|
webServer.user = "admin"
|
||||||
|
webServer.password = "admin"
|
||||||
|
# Admin assets directory. By default, these assets are bundled with frpc.
|
||||||
|
# webServer.assetsDir = "./static"
|
||||||
|
|
||||||
|
# Enable golang pprof handlers in admin listener.
|
||||||
|
webServer.pprofEnable = false
|
||||||
|
|
||||||
|
# The maximum amount of time a dial to server will wait for a connect to complete. Default value is 10 seconds.
|
||||||
|
# transport.dialServerTimeout = 10
|
||||||
|
|
||||||
|
# dialServerKeepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
|
||||||
|
# If negative, keep-alive probes are disabled.
|
||||||
|
# transport.dialServerKeepalive = 7200
|
||||||
|
|
||||||
|
# connections will be established in advance, default value is zero
|
||||||
|
transport.poolCount = 5
|
||||||
|
|
||||||
|
# If tcp stream multiplexing is used, default is true, it must be same with frps
|
||||||
|
# transport.tcpMux = true
|
||||||
|
|
||||||
|
# Specify keep alive interval for tcp mux.
|
||||||
|
# only valid if tcpMux is enabled.
|
||||||
|
# transport.tcpMuxKeepaliveInterval = 30
|
||||||
|
|
||||||
|
# Communication protocol used to connect to server
|
||||||
|
# supports tcp, kcp, quic, websocket and wss now, default is tcp
|
||||||
|
transport.protocol = "tcp"
|
||||||
|
|
||||||
|
# set client binding ip when connect server, default is empty.
|
||||||
|
# only when protocol = tcp or websocket, the value will be used.
|
||||||
|
transport.connectServerLocalIP = "0.0.0.0"
|
||||||
|
|
||||||
|
# if you want to connect frps by http proxy or socks5 proxy or ntlm proxy, you can set proxyURL here or in global environment variables
|
||||||
|
# it only works when protocol is tcp
|
||||||
|
# transport.proxyURL = "http://user:passwd@192.168.1.128:8080"
|
||||||
|
# transport.proxyURL = "socks5://user:passwd@192.168.1.128:1080"
|
||||||
|
# transport.proxyURL = "ntlm://user:passwd@192.168.1.128:2080"
|
||||||
|
|
||||||
|
# quic protocol options
|
||||||
|
# transport.quic.keepalivePeriod = 10
|
||||||
|
# transport.quic.maxIdleTimeout = 30
|
||||||
|
# transport.quic.maxIncomingStreams = 100000
|
||||||
|
|
||||||
|
# If tls.enable is true, frpc will connect frps by tls.
|
||||||
|
# Since v0.50.0, the default value has been changed to true, and tls is enabled by default.
|
||||||
|
transport.tls.enable = true
|
||||||
|
|
||||||
|
# transport.tls.certFile = "client.crt"
|
||||||
|
# transport.tls.keyFile = "client.key"
|
||||||
|
# transport.tls.trustedCaFile = "ca.crt"
|
||||||
|
# transport.tls.serverName = "example.com"
|
||||||
|
|
||||||
|
# If the disableCustomTLSFirstByte is set to false, frpc will establish a connection with frps using the
|
||||||
|
# first custom byte when tls is enabled.
|
||||||
|
# Since v0.50.0, the default value has been changed to true, and the first custom byte is disabled by default.
|
||||||
|
# transport.tls.disableCustomTLSFirstByte = true
|
||||||
|
|
||||||
|
# Heartbeat configure, it's not recommended to modify the default value.
|
||||||
|
# The default value of heartbeatInterval is 10 and heartbeatTimeout is 90. Set negative value
|
||||||
|
# to disable it.
|
||||||
|
# transport.heartbeatInterval = 30
|
||||||
|
# transport.heartbeatTimeout = 90
|
||||||
|
|
||||||
|
# Specify a dns server, so frpc will use this instead of default one
|
||||||
|
# dnsServer = "8.8.8.8"
|
||||||
|
|
||||||
|
# Proxy names you want to start.
|
||||||
|
# Default is empty, means all proxies.
|
||||||
|
# This list is a global allowlist after config + store are merged, so entries
|
||||||
|
# created via Store API are also filtered by this list.
|
||||||
|
# If start is non-empty, any proxy/visitor not listed here will not be started.
|
||||||
|
# start = ["ssh", "dns"]
|
||||||
|
|
||||||
|
# Alternative to 'start': You can control each proxy individually using the 'enabled' field.
|
||||||
|
# Set 'enabled = false' in a proxy configuration to disable it.
|
||||||
|
# If 'enabled' is not set or set to true, the proxy is enabled by default.
|
||||||
|
# The 'enabled' field provides more granular control and is recommended over 'start'.
|
||||||
|
|
||||||
|
# Specify udp packet size, unit is byte. If not set, the default value is 1500.
|
||||||
|
# This parameter should be same between client and server.
|
||||||
|
# It affects the udp and sudp proxy.
|
||||||
|
udpPacketSize = 1500
|
||||||
|
|
||||||
|
# Feature gates allows you to enable or disable experimental features
|
||||||
|
# Format is a map of feature names to boolean values
|
||||||
|
# You can enable specific features:
|
||||||
|
#featureGates = { VirtualNet = true }
|
||||||
|
|
||||||
|
# VirtualNet settings for experimental virtual network capabilities
|
||||||
|
# The virtual network feature requires enabling the VirtualNet feature gate above
|
||||||
|
# virtualNet.address = "100.86.1.1/24"
|
||||||
|
|
||||||
|
# Additional metadatas for client.
|
||||||
|
metadatas.var1 = "abc"
|
||||||
|
metadatas.var2 = "123"
|
||||||
|
|
||||||
|
# Include other config files for proxies.
|
||||||
|
# includes = ["./confd/*.ini"]
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
# 'ssh' is the unique proxy name
|
||||||
|
# If global user is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh'
|
||||||
|
name = "ssh"
|
||||||
|
type = "tcp"
|
||||||
|
# Enable or disable this proxy. true or omit this field to enable, false to disable.
|
||||||
|
# enabled = true
|
||||||
|
localIP = "127.0.0.1"
|
||||||
|
localPort = 22
|
||||||
|
# Limit bandwidth for this proxy, unit is KB and MB
|
||||||
|
transport.bandwidthLimit = "1MB"
|
||||||
|
# Where to limit bandwidth, can be 'client' or 'server', default is 'client'
|
||||||
|
transport.bandwidthLimitMode = "client"
|
||||||
|
# If true, traffic of this proxy will be encrypted, default is false
|
||||||
|
transport.useEncryption = false
|
||||||
|
# If true, traffic will be compressed
|
||||||
|
transport.useCompression = false
|
||||||
|
# Remote port listen by frps
|
||||||
|
remotePort = 6001
|
||||||
|
# frps will load balancing connections for proxies in same group
|
||||||
|
loadBalancer.group = "test_group"
|
||||||
|
# group should have same group key
|
||||||
|
loadBalancer.groupKey = "123456"
|
||||||
|
# Enable health check for the backend service, it supports 'tcp' and 'http' now.
|
||||||
|
# frpc will connect local service's port to detect it's healthy status
|
||||||
|
healthCheck.type = "tcp"
|
||||||
|
# Health check connection timeout
|
||||||
|
healthCheck.timeoutSeconds = 3
|
||||||
|
# If continuous failed in 3 times, the proxy will be removed from frps
|
||||||
|
healthCheck.maxFailed = 3
|
||||||
|
# Every 10 seconds will do a health check
|
||||||
|
healthCheck.intervalSeconds = 10
|
||||||
|
# Additional meta info for each proxy. It will be passed to the server-side plugin for use.
|
||||||
|
metadatas.var1 = "abc"
|
||||||
|
metadatas.var2 = "123"
|
||||||
|
# You can add some extra information to the proxy through annotations.
|
||||||
|
# These annotations will be displayed on the frps dashboard.
|
||||||
|
[proxies.annotations]
|
||||||
|
key1 = "value1"
|
||||||
|
"prefix/key2" = "value2"
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "ssh_random"
|
||||||
|
type = "tcp"
|
||||||
|
localIP = "192.168.31.100"
|
||||||
|
localPort = 22
|
||||||
|
# If remotePort is 0, frps will assign a random port for you
|
||||||
|
remotePort = 0
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "dns"
|
||||||
|
type = "udp"
|
||||||
|
localIP = "114.114.114.114"
|
||||||
|
localPort = 53
|
||||||
|
remotePort = 6002
|
||||||
|
|
||||||
|
# Resolve your domain names to [serverAddr] so you can use http://web01.yourdomain.com to browse web01 and http://web02.yourdomain.com to browse web02
|
||||||
|
[[proxies]]
|
||||||
|
name = "web01"
|
||||||
|
type = "http"
|
||||||
|
localIP = "127.0.0.1"
|
||||||
|
localPort = 80
|
||||||
|
# http username and password are safety certification for http protocol
|
||||||
|
# if not set, you can access this customDomains without certification
|
||||||
|
httpUser = "admin"
|
||||||
|
httpPassword = "admin"
|
||||||
|
# if domain for frps is frps.com, then you can access [web01] proxy by URL http://web01.frps.com
|
||||||
|
subdomain = "web01"
|
||||||
|
customDomains = ["web01.yourdomain.com"]
|
||||||
|
# locations is only available for http type
|
||||||
|
locations = ["/", "/pic"]
|
||||||
|
# route requests to this service if http basic auto user is abc
|
||||||
|
# routeByHTTPUser = abc
|
||||||
|
hostHeaderRewrite = "example.com"
|
||||||
|
requestHeaders.set.x-from-where = "frp"
|
||||||
|
responseHeaders.set.foo = "bar"
|
||||||
|
healthCheck.type = "http"
|
||||||
|
# frpc will send a GET http request '/status' to local http service
|
||||||
|
# http service is alive when it return 2xx http response code
|
||||||
|
healthCheck.path = "/status"
|
||||||
|
healthCheck.intervalSeconds = 10
|
||||||
|
healthCheck.maxFailed = 3
|
||||||
|
healthCheck.timeoutSeconds = 3
|
||||||
|
# set health check headers
|
||||||
|
healthCheck.httpHeaders=[
|
||||||
|
{ name = "x-from-where", value = "frp" }
|
||||||
|
]
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "web02"
|
||||||
|
type = "https"
|
||||||
|
# Disable this proxy by setting enabled to false
|
||||||
|
# enabled = false
|
||||||
|
localIP = "127.0.0.1"
|
||||||
|
localPort = 8000
|
||||||
|
subdomain = "web02"
|
||||||
|
customDomains = ["web02.yourdomain.com"]
|
||||||
|
# if not empty, frpc will use proxy protocol to transfer connection info to your local service
|
||||||
|
# v1 or v2 or empty
|
||||||
|
transport.proxyProtocolVersion = "v2"
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "tcpmuxhttpconnect"
|
||||||
|
type = "tcpmux"
|
||||||
|
multiplexer = "httpconnect"
|
||||||
|
localIP = "127.0.0.1"
|
||||||
|
localPort = 10701
|
||||||
|
customDomains = ["tunnel1"]
|
||||||
|
# routeByHTTPUser = "user1"
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "plugin_unix_domain_socket"
|
||||||
|
type = "tcp"
|
||||||
|
remotePort = 6003
|
||||||
|
# if plugin is defined, localIP and localPort is useless
|
||||||
|
# plugin will handle connections got from frps
|
||||||
|
[proxies.plugin]
|
||||||
|
type = "unix_domain_socket"
|
||||||
|
unixPath = "/var/run/docker.sock"
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "plugin_http_proxy"
|
||||||
|
type = "tcp"
|
||||||
|
remotePort = 6004
|
||||||
|
[proxies.plugin]
|
||||||
|
type = "http_proxy"
|
||||||
|
httpUser = "abc"
|
||||||
|
httpPassword = "abc"
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "plugin_socks5"
|
||||||
|
type = "tcp"
|
||||||
|
remotePort = 6005
|
||||||
|
[proxies.plugin]
|
||||||
|
type = "socks5"
|
||||||
|
username = "abc"
|
||||||
|
password = "abc"
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "plugin_static_file"
|
||||||
|
type = "tcp"
|
||||||
|
remotePort = 6006
|
||||||
|
[proxies.plugin]
|
||||||
|
type = "static_file"
|
||||||
|
localPath = "/var/www/blog"
|
||||||
|
stripPrefix = "static"
|
||||||
|
httpUser = "abc"
|
||||||
|
httpPassword = "abc"
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "plugin_https2http"
|
||||||
|
type = "https"
|
||||||
|
customDomains = ["test.yourdomain.com"]
|
||||||
|
[proxies.plugin]
|
||||||
|
type = "https2http"
|
||||||
|
localAddr = "127.0.0.1:80"
|
||||||
|
crtPath = "./server.crt"
|
||||||
|
keyPath = "./server.key"
|
||||||
|
hostHeaderRewrite = "127.0.0.1"
|
||||||
|
requestHeaders.set.x-from-where = "frp"
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "plugin_https2https"
|
||||||
|
type = "https"
|
||||||
|
customDomains = ["test.yourdomain.com"]
|
||||||
|
[proxies.plugin]
|
||||||
|
type = "https2https"
|
||||||
|
localAddr = "127.0.0.1:443"
|
||||||
|
crtPath = "./server.crt"
|
||||||
|
keyPath = "./server.key"
|
||||||
|
hostHeaderRewrite = "127.0.0.1"
|
||||||
|
requestHeaders.set.x-from-where = "frp"
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "plugin_http2https"
|
||||||
|
type = "http"
|
||||||
|
customDomains = ["test.yourdomain.com"]
|
||||||
|
[proxies.plugin]
|
||||||
|
type = "http2https"
|
||||||
|
localAddr = "127.0.0.1:443"
|
||||||
|
hostHeaderRewrite = "127.0.0.1"
|
||||||
|
requestHeaders.set.x-from-where = "frp"
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "plugin_http2http"
|
||||||
|
type = "tcp"
|
||||||
|
remotePort = 6007
|
||||||
|
[proxies.plugin]
|
||||||
|
type = "http2http"
|
||||||
|
localAddr = "127.0.0.1:80"
|
||||||
|
hostHeaderRewrite = "127.0.0.1"
|
||||||
|
requestHeaders.set.x-from-where = "frp"
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "plugin_tls2raw"
|
||||||
|
type = "tcp"
|
||||||
|
remotePort = 6008
|
||||||
|
[proxies.plugin]
|
||||||
|
type = "tls2raw"
|
||||||
|
localAddr = "127.0.0.1:80"
|
||||||
|
crtPath = "./server.crt"
|
||||||
|
keyPath = "./server.key"
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "secret_tcp"
|
||||||
|
# If the type is secret tcp, remotePort is useless
|
||||||
|
# Who want to connect local port should deploy another frpc with stcp proxy and role is visitor
|
||||||
|
type = "stcp"
|
||||||
|
# secretKey is used for authentication for visitors
|
||||||
|
secretKey = "abcdefg"
|
||||||
|
localIP = "127.0.0.1"
|
||||||
|
localPort = 22
|
||||||
|
# If not empty, only visitors from specified users can connect.
|
||||||
|
# Otherwise, visitors from same user can connect. '*' means allow all users.
|
||||||
|
allowUsers = ["*"]
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "p2p_tcp"
|
||||||
|
type = "xtcp"
|
||||||
|
secretKey = "abcdefg"
|
||||||
|
localIP = "127.0.0.1"
|
||||||
|
localPort = 22
|
||||||
|
# If not empty, only visitors from specified users can connect.
|
||||||
|
# Otherwise, visitors from same user can connect. '*' means allow all users.
|
||||||
|
allowUsers = ["user1", "user2"]
|
||||||
|
|
||||||
|
# NAT traversal configuration (optional)
|
||||||
|
[proxies.natTraversal]
|
||||||
|
# Disable the use of local network interfaces (assisted addresses) for NAT traversal.
|
||||||
|
# When enabled, only STUN-discovered public addresses will be used.
|
||||||
|
# This can improve performance when you have slow VPN connections.
|
||||||
|
# Default: false
|
||||||
|
disableAssistedAddrs = false
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "vnet-server"
|
||||||
|
type = "stcp"
|
||||||
|
secretKey = "your-secret-key"
|
||||||
|
[proxies.plugin]
|
||||||
|
type = "virtual_net"
|
||||||
|
|
||||||
|
# frpc role visitor -> frps -> frpc role server
|
||||||
|
[[visitors]]
|
||||||
|
name = "secret_tcp_visitor"
|
||||||
|
type = "stcp"
|
||||||
|
# the server name you want to visitor
|
||||||
|
serverName = "secret_tcp"
|
||||||
|
secretKey = "abcdefg"
|
||||||
|
# connect this address to visitor stcp server
|
||||||
|
bindAddr = "127.0.0.1"
|
||||||
|
# bindPort can be less than 0, it means don't bind to the port and only receive connections redirected from
|
||||||
|
# other visitors. (This is not supported for SUDP now)
|
||||||
|
bindPort = 9000
|
||||||
|
|
||||||
|
[[visitors]]
|
||||||
|
name = "p2p_tcp_visitor"
|
||||||
|
type = "xtcp"
|
||||||
|
# if the server user is not set, it defaults to the current user
|
||||||
|
serverUser = "user1"
|
||||||
|
serverName = "p2p_tcp"
|
||||||
|
secretKey = "abcdefg"
|
||||||
|
bindAddr = "127.0.0.1"
|
||||||
|
# bindPort can be less than 0, it means don't bind to the port and only receive connections redirected from
|
||||||
|
# other visitors. (This is not supported for SUDP now)
|
||||||
|
bindPort = 9001
|
||||||
|
# when automatic tunnel persistence is required, set it to true
|
||||||
|
keepTunnelOpen = false
|
||||||
|
# effective when keepTunnelOpen is set to true, the number of attempts to punch through per hour
|
||||||
|
maxRetriesAnHour = 8
|
||||||
|
minRetryInterval = 90
|
||||||
|
# fallbackTo = "stcp_visitor"
|
||||||
|
# fallbackTimeoutMs = 500
|
||||||
|
|
||||||
|
# NAT traversal configuration (optional)
|
||||||
|
[visitors.natTraversal]
|
||||||
|
# Disable the use of local network interfaces (assisted addresses) for NAT traversal.
|
||||||
|
# When enabled, only STUN-discovered public addresses will be used.
|
||||||
|
# Default: false
|
||||||
|
disableAssistedAddrs = false
|
||||||
|
|
||||||
|
[[visitors]]
|
||||||
|
name = "vnet-visitor"
|
||||||
|
type = "stcp"
|
||||||
|
serverName = "vnet-server"
|
||||||
|
secretKey = "your-secret-key"
|
||||||
|
bindPort = -1
|
||||||
|
[visitors.plugin]
|
||||||
|
type = "virtual_net"
|
||||||
|
destinationIP = "100.86.0.1"
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# [common] is integral section
|
|
||||||
[common]
|
|
||||||
bind_addr = 0.0.0.0
|
|
||||||
bind_port = 7000
|
|
||||||
# console or real logFile path like ./frps.log
|
|
||||||
log_file = console
|
|
||||||
# debug, info, warn, error
|
|
||||||
log_level = debug
|
|
||||||
|
|
||||||
# test1 is the proxy name, client will use this name and auth_token to connect to server
|
|
||||||
[test1]
|
|
||||||
auth_token = 123
|
|
||||||
bind_addr = 0.0.0.0
|
|
||||||
listen_port = 6000
|
|
||||||
1
conf/frps.toml
Normal file
1
conf/frps.toml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
bindPort = 7000
|
||||||
169
conf/frps_full_example.toml
Normal file
169
conf/frps_full_example.toml
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
# This configuration file is for reference only. Please do not use this configuration directly to run the program as it may have various issues.
|
||||||
|
|
||||||
|
# A literal address or host name for IPv6 must be enclosed
|
||||||
|
# in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80"
|
||||||
|
# For single "bindAddr" field, no need square brackets, like `bindAddr = "::"`.
|
||||||
|
bindAddr = "0.0.0.0"
|
||||||
|
bindPort = 7000
|
||||||
|
|
||||||
|
# udp port used for kcp protocol, it can be same with 'bindPort'.
|
||||||
|
# if not set, kcp is disabled in frps.
|
||||||
|
kcpBindPort = 7000
|
||||||
|
|
||||||
|
# udp port used for quic protocol.
|
||||||
|
# if not set, quic is disabled in frps.
|
||||||
|
# quicBindPort = 7002
|
||||||
|
|
||||||
|
# Specify which address proxy will listen for, default value is same with bindAddr
|
||||||
|
# proxyBindAddr = "127.0.0.1"
|
||||||
|
|
||||||
|
# quic protocol options
|
||||||
|
# transport.quic.keepalivePeriod = 10
|
||||||
|
# transport.quic.maxIdleTimeout = 30
|
||||||
|
# transport.quic.maxIncomingStreams = 100000
|
||||||
|
|
||||||
|
# Heartbeat configure, it's not recommended to modify the default value
|
||||||
|
# The default value of heartbeatTimeout is 90. Set negative value to disable it.
|
||||||
|
# transport.heartbeatTimeout = 90
|
||||||
|
|
||||||
|
# Pool count in each proxy will keep no more than maxPoolCount.
|
||||||
|
transport.maxPoolCount = 5
|
||||||
|
|
||||||
|
# If tcp stream multiplexing is used, default is true
|
||||||
|
# transport.tcpMux = true
|
||||||
|
|
||||||
|
# Specify keep alive interval for tcp mux.
|
||||||
|
# only valid if tcpMux is true.
|
||||||
|
# transport.tcpMuxKeepaliveInterval = 30
|
||||||
|
|
||||||
|
# tcpKeepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
|
||||||
|
# If negative, keep-alive probes are disabled.
|
||||||
|
# transport.tcpKeepalive = 7200
|
||||||
|
|
||||||
|
# transport.tls.force specifies whether to only accept TLS-encrypted connections. By default, the value is false.
|
||||||
|
transport.tls.force = false
|
||||||
|
|
||||||
|
# transport.tls.certFile = "server.crt"
|
||||||
|
# transport.tls.keyFile = "server.key"
|
||||||
|
# transport.tls.trustedCaFile = "ca.crt"
|
||||||
|
|
||||||
|
# If you want to support virtual host, you must set the http port for listening (optional)
|
||||||
|
# Note: http port and https port can be same with bindPort
|
||||||
|
vhostHTTPPort = 80
|
||||||
|
vhostHTTPSPort = 443
|
||||||
|
|
||||||
|
# Response header timeout(seconds) for vhost http server, default is 60s
|
||||||
|
# vhostHTTPTimeout = 60
|
||||||
|
|
||||||
|
# tcpmuxHTTPConnectPort specifies the port that the server listens for TCP
|
||||||
|
# HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP
|
||||||
|
# requests on one single port. If it's not - it will listen on this value for
|
||||||
|
# HTTP CONNECT requests. By default, this value is 0.
|
||||||
|
# tcpmuxHTTPConnectPort = 1337
|
||||||
|
|
||||||
|
# If tcpmuxPassthrough is true, frps won't do any update on traffic.
|
||||||
|
# tcpmuxPassthrough = false
|
||||||
|
|
||||||
|
# Configure the web server to enable the dashboard for frps.
|
||||||
|
# dashboard is available only if webServer.port is set.
|
||||||
|
webServer.addr = "127.0.0.1"
|
||||||
|
webServer.port = 7500
|
||||||
|
webServer.user = "admin"
|
||||||
|
webServer.password = "admin"
|
||||||
|
# webServer.tls.certFile = "server.crt"
|
||||||
|
# webServer.tls.keyFile = "server.key"
|
||||||
|
# dashboard assets directory(only for debug mode)
|
||||||
|
# webServer.assetsDir = "./static"
|
||||||
|
|
||||||
|
# Enable golang pprof handlers in dashboard listener.
|
||||||
|
# Dashboard port must be set first
|
||||||
|
webServer.pprofEnable = false
|
||||||
|
|
||||||
|
# enablePrometheus will export prometheus metrics on webServer in /metrics api.
|
||||||
|
enablePrometheus = true
|
||||||
|
|
||||||
|
# console or real logFile path like ./frps.log
|
||||||
|
log.to = "./frps.log"
|
||||||
|
# trace, debug, info, warn, error
|
||||||
|
log.level = "info"
|
||||||
|
log.maxDays = 3
|
||||||
|
# disable log colors when log.to is console, default is false
|
||||||
|
log.disablePrintColor = false
|
||||||
|
|
||||||
|
# DetailedErrorsToClient defines whether to send the specific error (with debug info) to frpc. By default, this value is true.
|
||||||
|
detailedErrorsToClient = true
|
||||||
|
|
||||||
|
# auth.method specifies what authentication method to use authenticate frpc with frps.
|
||||||
|
# If "token" is specified - token will be read into login message.
|
||||||
|
# If "oidc" is specified - OIDC (Open ID Connect) token will be issued using OIDC settings. By default, this value is "token".
|
||||||
|
auth.method = "token"
|
||||||
|
|
||||||
|
# auth.additionalScopes specifies additional scopes to include authentication information.
|
||||||
|
# Optional values are HeartBeats, NewWorkConns.
|
||||||
|
# auth.additionalScopes = ["HeartBeats", "NewWorkConns"]
|
||||||
|
|
||||||
|
# auth token
|
||||||
|
auth.token = "12345678"
|
||||||
|
|
||||||
|
# alternatively, you can use tokenSource to load the token from a file
|
||||||
|
# this is mutually exclusive with auth.token
|
||||||
|
# auth.tokenSource.type = "file"
|
||||||
|
# auth.tokenSource.file.path = "/etc/frp/token"
|
||||||
|
|
||||||
|
# oidc issuer specifies the issuer to verify OIDC tokens with.
|
||||||
|
auth.oidc.issuer = ""
|
||||||
|
# oidc audience specifies the audience OIDC tokens should contain when validated.
|
||||||
|
auth.oidc.audience = ""
|
||||||
|
# oidc skipExpiryCheck specifies whether to skip checking if the OIDC token is expired.
|
||||||
|
auth.oidc.skipExpiryCheck = false
|
||||||
|
# oidc skipIssuerCheck specifies whether to skip checking if the OIDC token's issuer claim matches the issuer specified in OidcIssuer.
|
||||||
|
auth.oidc.skipIssuerCheck = false
|
||||||
|
|
||||||
|
# userConnTimeout specifies the maximum time to wait for a work connection.
|
||||||
|
# userConnTimeout = 10
|
||||||
|
|
||||||
|
# Only allow frpc to bind ports you list. By default, there won't be any limit.
|
||||||
|
allowPorts = [
|
||||||
|
{ start = 2000, end = 3000 },
|
||||||
|
{ single = 3001 },
|
||||||
|
{ single = 3003 },
|
||||||
|
{ start = 4000, end = 50000 }
|
||||||
|
]
|
||||||
|
|
||||||
|
# Max ports can be used for each client, default value is 0 means no limit
|
||||||
|
maxPortsPerClient = 0
|
||||||
|
|
||||||
|
# If subDomainHost is not empty, you can set subdomain when type is http or https in frpc's configure file
|
||||||
|
# When subdomain is test, the host used by routing is test.frps.com
|
||||||
|
subDomainHost = "frps.com"
|
||||||
|
|
||||||
|
# custom 404 page for HTTP requests
|
||||||
|
# custom404Page = "/path/to/404.html"
|
||||||
|
|
||||||
|
# specify udp packet size, unit is byte. If not set, the default value is 1500.
|
||||||
|
# This parameter should be same between client and server.
|
||||||
|
# It affects the udp and sudp proxy.
|
||||||
|
udpPacketSize = 1500
|
||||||
|
|
||||||
|
# Retention time for NAT hole punching strategy data.
|
||||||
|
natholeAnalysisDataReserveHours = 168
|
||||||
|
|
||||||
|
# ssh tunnel gateway
|
||||||
|
# If you want to enable this feature, the bindPort parameter is required, while others are optional.
|
||||||
|
# By default, this feature is disabled. It will be enabled if bindPort is greater than 0.
|
||||||
|
# sshTunnelGateway.bindPort = 2200
|
||||||
|
# sshTunnelGateway.privateKeyFile = "/home/frp-user/.ssh/id_rsa"
|
||||||
|
# sshTunnelGateway.autoGenPrivateKeyPath = ""
|
||||||
|
# sshTunnelGateway.authorizedKeysFile = "/home/frp-user/.ssh/authorized_keys"
|
||||||
|
|
||||||
|
[[httpPlugins]]
|
||||||
|
name = "user-manager"
|
||||||
|
addr = "127.0.0.1:9000"
|
||||||
|
path = "/handler"
|
||||||
|
ops = ["Login"]
|
||||||
|
|
||||||
|
[[httpPlugins]]
|
||||||
|
name = "port-manager"
|
||||||
|
addr = "127.0.0.1:9001"
|
||||||
|
path = "/handler"
|
||||||
|
ops = ["NewProxy"]
|
||||||
389
conf/legacy/frpc_legacy_full.ini
Normal file
389
conf/legacy/frpc_legacy_full.ini
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
# [common] is integral section
|
||||||
|
[common]
|
||||||
|
# A literal address or host name for IPv6 must be enclosed
|
||||||
|
# in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80"
|
||||||
|
# For single "server_addr" field, no need square brackets, like "server_addr = ::".
|
||||||
|
server_addr = 0.0.0.0
|
||||||
|
server_port = 7000
|
||||||
|
|
||||||
|
# STUN server to help penetrate NAT hole.
|
||||||
|
# nat_hole_stun_server = stun.easyvoip.com:3478
|
||||||
|
|
||||||
|
# The maximum amount of time a dial to server will wait for a connect to complete. Default value is 10 seconds.
|
||||||
|
# dial_server_timeout = 10
|
||||||
|
|
||||||
|
# dial_server_keepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
|
||||||
|
# If negative, keep-alive probes are disabled.
|
||||||
|
# dial_server_keepalive = 7200
|
||||||
|
|
||||||
|
# if you want to connect frps by http proxy or socks5 proxy or ntlm proxy, you can set http_proxy here or in global environment variables
|
||||||
|
# it only works when protocol is tcp
|
||||||
|
# http_proxy = http://user:passwd@192.168.1.128:8080
|
||||||
|
# http_proxy = socks5://user:passwd@192.168.1.128:1080
|
||||||
|
# http_proxy = ntlm://user:passwd@192.168.1.128:2080
|
||||||
|
|
||||||
|
# console or real logFile path like ./frpc.log
|
||||||
|
log_file = ./frpc.log
|
||||||
|
|
||||||
|
# trace, debug, info, warn, error
|
||||||
|
log_level = info
|
||||||
|
|
||||||
|
log_max_days = 3
|
||||||
|
|
||||||
|
# disable log colors when log_file is console, default is false
|
||||||
|
disable_log_color = false
|
||||||
|
|
||||||
|
# for authentication, should be same as your frps.ini
|
||||||
|
# authenticate_heartbeats specifies whether to include authentication token in heartbeats sent to frps. By default, this value is false.
|
||||||
|
authenticate_heartbeats = false
|
||||||
|
|
||||||
|
# authenticate_new_work_conns specifies whether to include authentication token in new work connections sent to frps. By default, this value is false.
|
||||||
|
authenticate_new_work_conns = false
|
||||||
|
|
||||||
|
# auth token
|
||||||
|
token = 12345678
|
||||||
|
|
||||||
|
authentication_method =
|
||||||
|
|
||||||
|
# oidc_client_id specifies the client ID to use to get a token in OIDC authentication if AuthenticationMethod == "oidc".
|
||||||
|
# By default, this value is "".
|
||||||
|
oidc_client_id =
|
||||||
|
|
||||||
|
# oidc_client_secret specifies the client secret to use to get a token in OIDC authentication if AuthenticationMethod == "oidc".
|
||||||
|
# By default, this value is "".
|
||||||
|
oidc_client_secret =
|
||||||
|
|
||||||
|
# oidc_audience specifies the audience of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
|
||||||
|
oidc_audience =
|
||||||
|
|
||||||
|
# oidc_scope specifies the permissions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
|
||||||
|
oidc_scope =
|
||||||
|
|
||||||
|
# oidc_token_endpoint_url specifies the URL which implements OIDC Token Endpoint.
|
||||||
|
# It will be used to get an OIDC token if AuthenticationMethod == "oidc". By default, this value is "".
|
||||||
|
oidc_token_endpoint_url =
|
||||||
|
|
||||||
|
# oidc_additional_xxx specifies additional parameters to be sent to the OIDC Token Endpoint.
|
||||||
|
# For example, if you want to specify the "audience" parameter, you can set as follow.
|
||||||
|
# frp will add "audience=<value>" "var1=<value>" to the additional parameters.
|
||||||
|
# oidc_additional_audience = https://dev.auth.com/api/v2/
|
||||||
|
# oidc_additional_var1 = foobar
|
||||||
|
|
||||||
|
# set admin address for control frpc's action by http api such as reload
|
||||||
|
admin_addr = 127.0.0.1
|
||||||
|
admin_port = 7400
|
||||||
|
admin_user = admin
|
||||||
|
admin_pwd = admin
|
||||||
|
# Admin assets directory. By default, these assets are bundled with frpc.
|
||||||
|
# assets_dir = ./static
|
||||||
|
|
||||||
|
# connections will be established in advance, default value is zero
|
||||||
|
pool_count = 5
|
||||||
|
|
||||||
|
# if tcp stream multiplexing is used, default is true, it must be same with frps
|
||||||
|
# tcp_mux = true
|
||||||
|
|
||||||
|
# specify keep alive interval for tcp mux.
|
||||||
|
# only valid if tcp_mux is true.
|
||||||
|
# tcp_mux_keepalive_interval = 60
|
||||||
|
|
||||||
|
# your proxy name will be changed to {user}.{proxy}
|
||||||
|
user = your_name
|
||||||
|
|
||||||
|
# decide if exit program when first login failed, otherwise continuous relogin to frps
|
||||||
|
# default is true
|
||||||
|
login_fail_exit = true
|
||||||
|
|
||||||
|
# communication protocol used to connect to server
|
||||||
|
# supports tcp, kcp, quic, websocket and wss now, default is tcp
|
||||||
|
protocol = tcp
|
||||||
|
|
||||||
|
# set client binding ip when connect server, default is empty.
|
||||||
|
# only when protocol = tcp or websocket, the value will be used.
|
||||||
|
connect_server_local_ip = 0.0.0.0
|
||||||
|
|
||||||
|
# quic protocol options
|
||||||
|
# quic_keepalive_period = 10
|
||||||
|
# quic_max_idle_timeout = 30
|
||||||
|
# quic_max_incoming_streams = 100000
|
||||||
|
|
||||||
|
# If tls_enable is true, frpc will connect frps by tls.
|
||||||
|
# Since v0.50.0, the default value has been changed to true, and tls is enabled by default.
|
||||||
|
tls_enable = true
|
||||||
|
|
||||||
|
# tls_cert_file = client.crt
|
||||||
|
# tls_key_file = client.key
|
||||||
|
# tls_trusted_ca_file = ca.crt
|
||||||
|
# tls_server_name = example.com
|
||||||
|
|
||||||
|
# specify a dns server, so frpc will use this instead of default one
|
||||||
|
# dns_server = 8.8.8.8
|
||||||
|
|
||||||
|
# proxy names you want to start separated by ','
|
||||||
|
# default is empty, means all proxies
|
||||||
|
# start = ssh,dns
|
||||||
|
|
||||||
|
# heartbeat configure, it's not recommended to modify the default value
|
||||||
|
# The default value of heartbeat_interval is 10 and heartbeat_timeout is 90. Set negative value
|
||||||
|
# to disable it.
|
||||||
|
# heartbeat_interval = 30
|
||||||
|
# heartbeat_timeout = 90
|
||||||
|
|
||||||
|
# additional meta info for client
|
||||||
|
meta_var1 = 123
|
||||||
|
meta_var2 = 234
|
||||||
|
|
||||||
|
# specify udp packet size, unit is byte. If not set, the default value is 1500.
|
||||||
|
# This parameter should be same between client and server.
|
||||||
|
# It affects the udp and sudp proxy.
|
||||||
|
udp_packet_size = 1500
|
||||||
|
|
||||||
|
# include other config files for proxies.
|
||||||
|
# includes = ./confd/*.ini
|
||||||
|
|
||||||
|
# If the disable_custom_tls_first_byte is set to false, frpc will establish a connection with frps using the
|
||||||
|
# first custom byte when tls is enabled.
|
||||||
|
# Since v0.50.0, the default value has been changed to true, and the first custom byte is disabled by default.
|
||||||
|
disable_custom_tls_first_byte = true
|
||||||
|
|
||||||
|
# Enable golang pprof handlers in admin listener.
|
||||||
|
# Admin port must be set first.
|
||||||
|
pprof_enable = false
|
||||||
|
|
||||||
|
# 'ssh' is the unique proxy name
|
||||||
|
# if user in [common] section is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh'
|
||||||
|
[ssh]
|
||||||
|
# tcp | udp | http | https | stcp | xtcp, default is tcp
|
||||||
|
type = tcp
|
||||||
|
local_ip = 127.0.0.1
|
||||||
|
local_port = 22
|
||||||
|
# limit bandwidth for this proxy, unit is KB and MB
|
||||||
|
bandwidth_limit = 1MB
|
||||||
|
# where to limit bandwidth, can be 'client' or 'server', default is 'client'
|
||||||
|
bandwidth_limit_mode = client
|
||||||
|
# true or false, if true, messages between frps and frpc will be encrypted, default is false
|
||||||
|
use_encryption = false
|
||||||
|
# if true, message will be compressed
|
||||||
|
use_compression = false
|
||||||
|
# remote port listen by frps
|
||||||
|
remote_port = 6001
|
||||||
|
# frps will load balancing connections for proxies in same group
|
||||||
|
group = test_group
|
||||||
|
# group should have same group key
|
||||||
|
group_key = 123456
|
||||||
|
# enable health check for the backend service, it support 'tcp' and 'http' now
|
||||||
|
# frpc will connect local service's port to detect it's healthy status
|
||||||
|
health_check_type = tcp
|
||||||
|
# health check connection timeout
|
||||||
|
health_check_timeout_s = 3
|
||||||
|
# if continuous failed in 3 times, the proxy will be removed from frps
|
||||||
|
health_check_max_failed = 3
|
||||||
|
# every 10 seconds will do a health check
|
||||||
|
health_check_interval_s = 10
|
||||||
|
# additional meta info for each proxy
|
||||||
|
meta_var1 = 123
|
||||||
|
meta_var2 = 234
|
||||||
|
|
||||||
|
[ssh_random]
|
||||||
|
type = tcp
|
||||||
|
local_ip = 127.0.0.1
|
||||||
|
local_port = 22
|
||||||
|
# if remote_port is 0, frps will assign a random port for you
|
||||||
|
remote_port = 0
|
||||||
|
|
||||||
|
# if you want to expose multiple ports, add 'range:' prefix to the section name
|
||||||
|
# frpc will generate multiple proxies such as 'tcp_port_6010', 'tcp_port_6011' and so on.
|
||||||
|
[range:tcp_port]
|
||||||
|
type = tcp
|
||||||
|
local_ip = 127.0.0.1
|
||||||
|
local_port = 6010-6020,6022,6024-6028
|
||||||
|
remote_port = 6010-6020,6022,6024-6028
|
||||||
|
use_encryption = false
|
||||||
|
use_compression = false
|
||||||
|
|
||||||
|
[dns]
|
||||||
|
type = udp
|
||||||
|
local_ip = 114.114.114.114
|
||||||
|
local_port = 53
|
||||||
|
remote_port = 6002
|
||||||
|
use_encryption = false
|
||||||
|
use_compression = false
|
||||||
|
|
||||||
|
[range:udp_port]
|
||||||
|
type = udp
|
||||||
|
local_ip = 127.0.0.1
|
||||||
|
local_port = 6010-6020
|
||||||
|
remote_port = 6010-6020
|
||||||
|
use_encryption = false
|
||||||
|
use_compression = false
|
||||||
|
|
||||||
|
# Resolve your domain names to [server_addr] so you can use http://web01.yourdomain.com to browse web01 and http://web02.yourdomain.com to browse web02
|
||||||
|
[web01]
|
||||||
|
type = http
|
||||||
|
local_ip = 127.0.0.1
|
||||||
|
local_port = 80
|
||||||
|
use_encryption = false
|
||||||
|
use_compression = true
|
||||||
|
# http username and password are safety certification for http protocol
|
||||||
|
# if not set, you can access this custom_domains without certification
|
||||||
|
http_user = admin
|
||||||
|
http_pwd = admin
|
||||||
|
# if domain for frps is frps.com, then you can access [web01] proxy by URL http://web01.frps.com
|
||||||
|
subdomain = web01
|
||||||
|
custom_domains = web01.yourdomain.com
|
||||||
|
# locations is only available for http type
|
||||||
|
locations = /,/pic
|
||||||
|
# route requests to this service if http basic auto user is abc
|
||||||
|
# route_by_http_user = abc
|
||||||
|
host_header_rewrite = example.com
|
||||||
|
# params with prefix "header_" will be used to update http request headers
|
||||||
|
header_X-From-Where = frp
|
||||||
|
health_check_type = http
|
||||||
|
# frpc will send a GET http request '/status' to local http service
|
||||||
|
# http service is alive when it return 2xx http response code
|
||||||
|
health_check_url = /status
|
||||||
|
health_check_interval_s = 10
|
||||||
|
health_check_max_failed = 3
|
||||||
|
health_check_timeout_s = 3
|
||||||
|
|
||||||
|
[web02]
|
||||||
|
type = https
|
||||||
|
local_ip = 127.0.0.1
|
||||||
|
local_port = 8000
|
||||||
|
use_encryption = false
|
||||||
|
use_compression = false
|
||||||
|
subdomain = web02
|
||||||
|
custom_domains = web02.yourdomain.com
|
||||||
|
# if not empty, frpc will use proxy protocol to transfer connection info to your local service
|
||||||
|
# v1 or v2 or empty
|
||||||
|
proxy_protocol_version = v2
|
||||||
|
|
||||||
|
[plugin_unix_domain_socket]
|
||||||
|
type = tcp
|
||||||
|
remote_port = 6003
|
||||||
|
# if plugin is defined, local_ip and local_port is useless
|
||||||
|
# plugin will handle connections got from frps
|
||||||
|
plugin = unix_domain_socket
|
||||||
|
# params with prefix "plugin_" that plugin needed
|
||||||
|
plugin_unix_path = /var/run/docker.sock
|
||||||
|
|
||||||
|
[plugin_http_proxy]
|
||||||
|
type = tcp
|
||||||
|
remote_port = 6004
|
||||||
|
plugin = http_proxy
|
||||||
|
plugin_http_user = abc
|
||||||
|
plugin_http_passwd = abc
|
||||||
|
|
||||||
|
[plugin_socks5]
|
||||||
|
type = tcp
|
||||||
|
remote_port = 6005
|
||||||
|
plugin = socks5
|
||||||
|
plugin_user = abc
|
||||||
|
plugin_passwd = abc
|
||||||
|
|
||||||
|
[plugin_static_file]
|
||||||
|
type = tcp
|
||||||
|
remote_port = 6006
|
||||||
|
plugin = static_file
|
||||||
|
plugin_local_path = /var/www/blog
|
||||||
|
plugin_strip_prefix = static
|
||||||
|
plugin_http_user = abc
|
||||||
|
plugin_http_passwd = abc
|
||||||
|
|
||||||
|
[plugin_https2http]
|
||||||
|
type = https
|
||||||
|
custom_domains = test.yourdomain.com
|
||||||
|
plugin = https2http
|
||||||
|
plugin_local_addr = 127.0.0.1:80
|
||||||
|
plugin_crt_path = ./server.crt
|
||||||
|
plugin_key_path = ./server.key
|
||||||
|
plugin_host_header_rewrite = 127.0.0.1
|
||||||
|
plugin_header_X-From-Where = frp
|
||||||
|
|
||||||
|
[plugin_https2https]
|
||||||
|
type = https
|
||||||
|
custom_domains = test.yourdomain.com
|
||||||
|
plugin = https2https
|
||||||
|
plugin_local_addr = 127.0.0.1:443
|
||||||
|
plugin_crt_path = ./server.crt
|
||||||
|
plugin_key_path = ./server.key
|
||||||
|
plugin_host_header_rewrite = 127.0.0.1
|
||||||
|
plugin_header_X-From-Where = frp
|
||||||
|
|
||||||
|
[plugin_http2https]
|
||||||
|
type = http
|
||||||
|
custom_domains = test.yourdomain.com
|
||||||
|
plugin = http2https
|
||||||
|
plugin_local_addr = 127.0.0.1:443
|
||||||
|
plugin_host_header_rewrite = 127.0.0.1
|
||||||
|
plugin_header_X-From-Where = frp
|
||||||
|
|
||||||
|
[secret_tcp]
|
||||||
|
# If the type is secret tcp, remote_port is useless
|
||||||
|
# Who want to connect local port should deploy another frpc with stcp proxy and role is visitor
|
||||||
|
type = stcp
|
||||||
|
# sk used for authentication for visitors
|
||||||
|
sk = abcdefg
|
||||||
|
local_ip = 127.0.0.1
|
||||||
|
local_port = 22
|
||||||
|
use_encryption = false
|
||||||
|
use_compression = false
|
||||||
|
# If not empty, only visitors from specified users can connect.
|
||||||
|
# Otherwise, visitors from same user can connect. '*' means allow all users.
|
||||||
|
allow_users = *
|
||||||
|
|
||||||
|
# user of frpc should be same in both stcp server and stcp visitor
|
||||||
|
[secret_tcp_visitor]
|
||||||
|
# frpc role visitor -> frps -> frpc role server
|
||||||
|
role = visitor
|
||||||
|
type = stcp
|
||||||
|
# the server name you want to visitor
|
||||||
|
server_name = secret_tcp
|
||||||
|
sk = abcdefg
|
||||||
|
# connect this address to visitor stcp server
|
||||||
|
bind_addr = 127.0.0.1
|
||||||
|
# bind_port can be less than 0, it means don't bind to the port and only receive connections redirected from
|
||||||
|
# other visitors. (This is not supported for SUDP now)
|
||||||
|
bind_port = 9000
|
||||||
|
use_encryption = false
|
||||||
|
use_compression = false
|
||||||
|
|
||||||
|
[p2p_tcp]
|
||||||
|
type = xtcp
|
||||||
|
sk = abcdefg
|
||||||
|
local_ip = 127.0.0.1
|
||||||
|
local_port = 22
|
||||||
|
use_encryption = false
|
||||||
|
use_compression = false
|
||||||
|
# If not empty, only visitors from specified users can connect.
|
||||||
|
# Otherwise, visitors from same user can connect. '*' means allow all users.
|
||||||
|
allow_users = user1, user2
|
||||||
|
|
||||||
|
[p2p_tcp_visitor]
|
||||||
|
role = visitor
|
||||||
|
type = xtcp
|
||||||
|
# if the server user is not set, it defaults to the current user
|
||||||
|
server_user = user1
|
||||||
|
server_name = p2p_tcp
|
||||||
|
sk = abcdefg
|
||||||
|
bind_addr = 127.0.0.1
|
||||||
|
# bind_port can be less than 0, it means don't bind to the port and only receive connections redirected from
|
||||||
|
# other visitors. (This is not supported for SUDP now)
|
||||||
|
bind_port = 9001
|
||||||
|
use_encryption = false
|
||||||
|
use_compression = false
|
||||||
|
# when automatic tunnel persistence is required, set it to true
|
||||||
|
keep_tunnel_open = false
|
||||||
|
# effective when keep_tunnel_open is set to true, the number of attempts to punch through per hour
|
||||||
|
max_retries_an_hour = 8
|
||||||
|
min_retry_interval = 90
|
||||||
|
# fallback_to = stcp_visitor
|
||||||
|
# fallback_timeout_ms = 500
|
||||||
|
|
||||||
|
[tcpmuxhttpconnect]
|
||||||
|
type = tcpmux
|
||||||
|
multiplexer = httpconnect
|
||||||
|
local_ip = 127.0.0.1
|
||||||
|
local_port = 10701
|
||||||
|
custom_domains = tunnel1
|
||||||
|
# route_by_http_user = user1
|
||||||
168
conf/legacy/frps_legacy_full.ini
Normal file
168
conf/legacy/frps_legacy_full.ini
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# [common] is integral section
|
||||||
|
[common]
|
||||||
|
# A literal address or host name for IPv6 must be enclosed
|
||||||
|
# in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80"
|
||||||
|
# For single "bind_addr" field, no need square brackets, like "bind_addr = ::".
|
||||||
|
bind_addr = 0.0.0.0
|
||||||
|
bind_port = 7000
|
||||||
|
|
||||||
|
# udp port used for kcp protocol, it can be same with 'bind_port'.
|
||||||
|
# if not set, kcp is disabled in frps.
|
||||||
|
kcp_bind_port = 7000
|
||||||
|
|
||||||
|
# udp port used for quic protocol.
|
||||||
|
# if not set, quic is disabled in frps.
|
||||||
|
# quic_bind_port = 7002
|
||||||
|
# quic protocol options
|
||||||
|
# quic_keepalive_period = 10
|
||||||
|
# quic_max_idle_timeout = 30
|
||||||
|
# quic_max_incoming_streams = 100000
|
||||||
|
|
||||||
|
# specify which address proxy will listen for, default value is same with bind_addr
|
||||||
|
# proxy_bind_addr = 127.0.0.1
|
||||||
|
|
||||||
|
# if you want to support virtual host, you must set the http port for listening (optional)
|
||||||
|
# Note: http port and https port can be same with bind_port
|
||||||
|
vhost_http_port = 80
|
||||||
|
vhost_https_port = 443
|
||||||
|
|
||||||
|
# response header timeout(seconds) for vhost http server, default is 60s
|
||||||
|
# vhost_http_timeout = 60
|
||||||
|
|
||||||
|
# tcpmux_httpconnect_port specifies the port that the server listens for TCP
|
||||||
|
# HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP
|
||||||
|
# requests on one single port. If it's not - it will listen on this value for
|
||||||
|
# HTTP CONNECT requests. By default, this value is 0.
|
||||||
|
# tcpmux_httpconnect_port = 1337
|
||||||
|
|
||||||
|
# If tcpmux_passthrough is true, frps won't do any update on traffic.
|
||||||
|
# tcpmux_passthrough = false
|
||||||
|
|
||||||
|
# set dashboard_addr and dashboard_port to view dashboard of frps
|
||||||
|
# dashboard_addr's default value is same with bind_addr
|
||||||
|
# dashboard is available only if dashboard_port is set
|
||||||
|
dashboard_addr = 0.0.0.0
|
||||||
|
dashboard_port = 7500
|
||||||
|
|
||||||
|
# dashboard user and passwd for basic auth protect
|
||||||
|
dashboard_user = admin
|
||||||
|
dashboard_pwd = admin
|
||||||
|
|
||||||
|
# dashboard TLS mode
|
||||||
|
dashboard_tls_mode = false
|
||||||
|
# dashboard_tls_cert_file = server.crt
|
||||||
|
# dashboard_tls_key_file = server.key
|
||||||
|
|
||||||
|
# enable_prometheus will export prometheus metrics on {dashboard_addr}:{dashboard_port} in /metrics api.
|
||||||
|
enable_prometheus = true
|
||||||
|
|
||||||
|
# dashboard assets directory(only for debug mode)
|
||||||
|
# assets_dir = ./static
|
||||||
|
|
||||||
|
# console or real logFile path like ./frps.log
|
||||||
|
log_file = ./frps.log
|
||||||
|
|
||||||
|
# trace, debug, info, warn, error
|
||||||
|
log_level = info
|
||||||
|
|
||||||
|
log_max_days = 3
|
||||||
|
|
||||||
|
# disable log colors when log_file is console, default is false
|
||||||
|
disable_log_color = false
|
||||||
|
|
||||||
|
# DetailedErrorsToClient defines whether to send the specific error (with debug info) to frpc. By default, this value is true.
|
||||||
|
detailed_errors_to_client = true
|
||||||
|
|
||||||
|
# authentication_method specifies what authentication method to use authenticate frpc with frps.
|
||||||
|
# If "token" is specified - token will be read into login message.
|
||||||
|
# If "oidc" is specified - OIDC (Open ID Connect) token will be issued using OIDC settings. By default, this value is "token".
|
||||||
|
authentication_method = token
|
||||||
|
|
||||||
|
# authenticate_heartbeats specifies whether to include authentication token in heartbeats sent to frps. By default, this value is false.
|
||||||
|
authenticate_heartbeats = false
|
||||||
|
|
||||||
|
# AuthenticateNewWorkConns specifies whether to include authentication token in new work connections sent to frps. By default, this value is false.
|
||||||
|
authenticate_new_work_conns = false
|
||||||
|
|
||||||
|
# auth token
|
||||||
|
token = 12345678
|
||||||
|
|
||||||
|
# oidc_issuer specifies the issuer to verify OIDC tokens with.
|
||||||
|
# By default, this value is "".
|
||||||
|
oidc_issuer =
|
||||||
|
|
||||||
|
# oidc_audience specifies the audience OIDC tokens should contain when validated.
|
||||||
|
# By default, this value is "".
|
||||||
|
oidc_audience =
|
||||||
|
|
||||||
|
# oidc_skip_expiry_check specifies whether to skip checking if the OIDC token is expired.
|
||||||
|
# By default, this value is false.
|
||||||
|
oidc_skip_expiry_check = false
|
||||||
|
|
||||||
|
# oidc_skip_issuer_check specifies whether to skip checking if the OIDC token's issuer claim matches the issuer specified in OidcIssuer.
|
||||||
|
# By default, this value is false.
|
||||||
|
oidc_skip_issuer_check = false
|
||||||
|
|
||||||
|
# heartbeat configure, it's not recommended to modify the default value
|
||||||
|
# the default value of heartbeat_timeout is 90. Set negative value to disable it.
|
||||||
|
# heartbeat_timeout = 90
|
||||||
|
|
||||||
|
# user_conn_timeout configure, it's not recommended to modify the default value
|
||||||
|
# the default value of user_conn_timeout is 10
|
||||||
|
# user_conn_timeout = 10
|
||||||
|
|
||||||
|
# only allow frpc to bind ports you list, if you set nothing, there won't be any limit
|
||||||
|
allow_ports = 2000-3000,3001,3003,4000-50000
|
||||||
|
|
||||||
|
# pool_count in each proxy will change to max_pool_count if they exceed the maximum value
|
||||||
|
max_pool_count = 5
|
||||||
|
|
||||||
|
# max ports can be used for each client, default value is 0 means no limit
|
||||||
|
max_ports_per_client = 0
|
||||||
|
|
||||||
|
# tls_only specifies whether to only accept TLS-encrypted connections. By default, the value is false.
|
||||||
|
tls_only = false
|
||||||
|
|
||||||
|
# tls_cert_file = server.crt
|
||||||
|
# tls_key_file = server.key
|
||||||
|
# tls_trusted_ca_file = ca.crt
|
||||||
|
|
||||||
|
# if subdomain_host is not empty, you can set subdomain when type is http or https in frpc's configure file
|
||||||
|
# when subdomain is test, the host used by routing is test.frps.com
|
||||||
|
subdomain_host = frps.com
|
||||||
|
|
||||||
|
# if tcp stream multiplexing is used, default is true
|
||||||
|
# tcp_mux = true
|
||||||
|
|
||||||
|
# specify keep alive interval for tcp mux.
|
||||||
|
# only valid if tcp_mux is true.
|
||||||
|
# tcp_mux_keepalive_interval = 60
|
||||||
|
|
||||||
|
# tcp_keepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
|
||||||
|
# If negative, keep-alive probes are disabled.
|
||||||
|
# tcp_keepalive = 7200
|
||||||
|
|
||||||
|
# custom 404 page for HTTP requests
|
||||||
|
# custom_404_page = /path/to/404.html
|
||||||
|
|
||||||
|
# specify udp packet size, unit is byte. If not set, the default value is 1500.
|
||||||
|
# This parameter should be same between client and server.
|
||||||
|
# It affects the udp and sudp proxy.
|
||||||
|
udp_packet_size = 1500
|
||||||
|
|
||||||
|
# Enable golang pprof handlers in dashboard listener.
|
||||||
|
# Dashboard port must be set first
|
||||||
|
pprof_enable = false
|
||||||
|
|
||||||
|
# Retention time for NAT hole punching strategy data.
|
||||||
|
nat_hole_analysis_data_reserve_hours = 168
|
||||||
|
|
||||||
|
[plugin.user-manager]
|
||||||
|
addr = 127.0.0.1:9000
|
||||||
|
path = /handler
|
||||||
|
ops = Login
|
||||||
|
|
||||||
|
[plugin.port-manager]
|
||||||
|
addr = 127.0.0.1:9001
|
||||||
|
path = /handler
|
||||||
|
ops = NewProxy
|
||||||
80
doc/agents/release.md
Normal file
80
doc/agents/release.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# Release Process
|
||||||
|
|
||||||
|
## 1. Update Release Notes
|
||||||
|
|
||||||
|
Edit `Release.md` in the project root with the changes for this version:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Features
|
||||||
|
* ...
|
||||||
|
|
||||||
|
## Improvements
|
||||||
|
* ...
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
* ...
|
||||||
|
```
|
||||||
|
|
||||||
|
This file is used by GoReleaser as the GitHub Release body.
|
||||||
|
|
||||||
|
## 2. Bump Version
|
||||||
|
|
||||||
|
Update the version string in `pkg/util/version/version.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var version = "0.X.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
Commit and push to `dev`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add pkg/util/version/version.go Release.md
|
||||||
|
git commit -m "bump version to vX.Y.Z"
|
||||||
|
git push origin dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Merge dev → master
|
||||||
|
|
||||||
|
Create a PR from `dev` to `master`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh pr create --base master --head dev --title "bump version"
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait for CI to pass, then merge using **merge commit** (not squash).
|
||||||
|
|
||||||
|
## 4. Tag the Release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout master
|
||||||
|
git pull origin master
|
||||||
|
git tag -a vX.Y.Z -m "bump version"
|
||||||
|
git push origin vX.Y.Z
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Trigger GoReleaser
|
||||||
|
|
||||||
|
Manually trigger the `goreleaser` workflow in GitHub Actions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh workflow run goreleaser --ref master
|
||||||
|
```
|
||||||
|
|
||||||
|
GoReleaser will:
|
||||||
|
1. Run `package.sh` to cross-compile all platforms and create archives
|
||||||
|
2. Create a GitHub Release with all packages, using `Release.md` as release notes
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `pkg/util/version/version.go` | Version string |
|
||||||
|
| `Release.md` | Release notes (read by GoReleaser) |
|
||||||
|
| `.goreleaser.yml` | GoReleaser config |
|
||||||
|
| `package.sh` | Cross-compile and packaging script |
|
||||||
|
| `.github/workflows/goreleaser.yml` | GitHub Actions workflow (manual trigger) |
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
- Minor release: `v0.X.0`
|
||||||
|
- Patch release: `v0.X.Y` (e.g., `v0.62.1`)
|
||||||
BIN
doc/pic/architecture.jpg
Normal file
BIN
doc/pic/architecture.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
BIN
doc/pic/dashboard.png
Normal file
BIN
doc/pic/dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
doc/pic/sponsor_daytona.png
Normal file
BIN
doc/pic/sponsor_daytona.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
doc/pic/sponsor_jetbrains.jpg
Normal file
BIN
doc/pic/sponsor_jetbrains.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
BIN
doc/pic/sponsor_olares.jpeg
Normal file
BIN
doc/pic/sponsor_olares.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
@@ -1,71 +0,0 @@
|
|||||||
# Quick Start
|
|
||||||
|
|
||||||
frp is easier to use compared with other similar projects.
|
|
||||||
|
|
||||||
We will use a simple demo to demonstrate how to create a connection to server A's ssh port by server B with public IP address x.x.x.x(replace to the real IP address of your server).
|
|
||||||
|
|
||||||
### Download SourceCode
|
|
||||||
|
|
||||||
`go get github.com/fatedier/frp` is recommended, then the code will be copied to the directory `$GOPATH/src/github.com/fatedier/frp`.
|
|
||||||
|
|
||||||
Or you can use `git clone https://github.com/fatedier/frp.git $GOPATH/src/github.com/fatedier/frp`.
|
|
||||||
|
|
||||||
### Compile
|
|
||||||
|
|
||||||
Enter the root directory and execute `make`, then wait until finished.
|
|
||||||
|
|
||||||
**bin** include all executable programs when **conf** include corresponding configuration files.
|
|
||||||
|
|
||||||
### Pre-requirement
|
|
||||||
|
|
||||||
* Go environment. Version of go >= 1.4.
|
|
||||||
* Godep (if not exist, go get will be executed to download godep when compiling)
|
|
||||||
|
|
||||||
### Deploy
|
|
||||||
|
|
||||||
1. Move `./bin/frps` and `./conf/frps.ini` to any directory of server B.
|
|
||||||
2. Move `./bin/frpc` and `./conf/frpc.ini` to any directory of server A.
|
|
||||||
3. Modify all configuration files, details in next paragraph.
|
|
||||||
4. Execute `nohup ./frps &` or `nohup ./frps -c ./frps.ini &` in server B.
|
|
||||||
5. Execute `nohup ./frpc &` or `nohup ./frpc -c ./frpc.ini &` in server A.
|
|
||||||
6. Use `ssh -oPort=6000 {user}@x.x.x.x` to test if frp is work(replace {user} to real username in server A).
|
|
||||||
|
|
||||||
### Configuration files
|
|
||||||
|
|
||||||
#### frps.ini
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[common]
|
|
||||||
bind_addr = 0.0.0.0
|
|
||||||
# for accept connections from frpc
|
|
||||||
bind_port = 7000
|
|
||||||
log_file = ./frps.log
|
|
||||||
log_level = info
|
|
||||||
|
|
||||||
# test is the custom name of proxy and there can be many proxies with unique name in one configure file
|
|
||||||
[test]
|
|
||||||
auth_token = 123
|
|
||||||
bind_addr = 0.0.0.0
|
|
||||||
# finally we connect to server A by this port
|
|
||||||
listen_port = 6000
|
|
||||||
```
|
|
||||||
|
|
||||||
#### frpc.ini
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[common]
|
|
||||||
# server address of frps
|
|
||||||
server_addr = x.x.x.x
|
|
||||||
server_port = 7000
|
|
||||||
log_file = ./frpc.log
|
|
||||||
log_level = info
|
|
||||||
# for authentication
|
|
||||||
auth_token = 123
|
|
||||||
|
|
||||||
# test is proxy name same with configure in frps.ini
|
|
||||||
[test]
|
|
||||||
# local port which need to be transferred
|
|
||||||
local_port = 22
|
|
||||||
# if use_encryption equals true, messages between frpc and frps will be encrypted, default is false
|
|
||||||
use_encryption = true
|
|
||||||
```
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
# frp 使用文档
|
|
||||||
|
|
||||||
frp 相比于其他项目而言非常易于部署和使用,这里我们用一个简单的示例演示如何通过一台拥有公网IP地址的服务器B,访问处于内网环境中的服务器A的ssh端口,服务器B的IP地址为 x.x.x.x(测试时替换为真实的IP地址)。
|
|
||||||
|
|
||||||
### 下载源码
|
|
||||||
|
|
||||||
推荐直接使用 `go get github.com/fatedier/frp` 下载源代码安装,执行命令后代码将会拷贝到 `$GOPATH/src/github.com/fatedier/frp` 目录下。
|
|
||||||
|
|
||||||
或者可以使用 `git clone https://github.com/fatedier/frp.git $GOPATH/src/github.com/fatedier/frp` 拷贝到相应目录下。
|
|
||||||
|
|
||||||
### 编译
|
|
||||||
|
|
||||||
进入下载后的源码根目录,执行 `make` 命令,等待编译完成。
|
|
||||||
|
|
||||||
编译完成后, **bin** 目录下是编译好的可执行文件,**conf** 目录下是示例配置文件。
|
|
||||||
|
|
||||||
### 依赖
|
|
||||||
|
|
||||||
* go 1.4 以上版本
|
|
||||||
* godep (如果检查不存在,编译时会通过 go get 命令安装)
|
|
||||||
|
|
||||||
### 部署
|
|
||||||
|
|
||||||
1. 将 ./bin/frps 和 ./conf/frps.ini 拷贝至服务器B任意目录。
|
|
||||||
2. 将 ./bin/frpc 和 ./conf/frpc.ini 拷贝至服务器A任意目录。
|
|
||||||
3. 修改两边的配置文件,见下一节说明。
|
|
||||||
4. 在服务器B执行 `nohup ./frps &` 或者 `nohup ./frps -c ./frps.ini &`。
|
|
||||||
5. 在服务器A执行 `nohup ./frpc &` 或者 `nohup ./frpc -c ./frpc.ini &`。
|
|
||||||
6. 通过 `ssh -oPort=6000 {user}@x.x.x.x` 测试是否能够成功连接服务器A({user}替换为服务器A上存在的真实用户)。
|
|
||||||
|
|
||||||
### 配置文件
|
|
||||||
|
|
||||||
#### frps.ini
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[common]
|
|
||||||
bind_addr = 0.0.0.0
|
|
||||||
# 用于接收 frpc 连接的端口
|
|
||||||
bind_port = 7000
|
|
||||||
log_file = ./frps.log
|
|
||||||
log_level = info
|
|
||||||
|
|
||||||
# test 为代理的自定义名称,可以有多个,不能重复,和frpc中名称对应
|
|
||||||
[test]
|
|
||||||
auth_token = 123
|
|
||||||
bind_addr = 0.0.0.0
|
|
||||||
# 最后将通过此端口访问后端服务
|
|
||||||
listen_port = 6000
|
|
||||||
```
|
|
||||||
|
|
||||||
#### frpc.ini
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[common]
|
|
||||||
# frps 所在服务器绑定的IP地址
|
|
||||||
server_addr = x.x.x.x
|
|
||||||
server_port = 7000
|
|
||||||
log_file = ./frpc.log
|
|
||||||
log_level = info
|
|
||||||
# 用于身份验证
|
|
||||||
auth_token = 123
|
|
||||||
|
|
||||||
# test需要和 frps.ini 中配置一致
|
|
||||||
[test]
|
|
||||||
# 需要转发的本地端口
|
|
||||||
local_port = 22
|
|
||||||
# 启用加密,frpc与frps之间通信加密,默认为 false
|
|
||||||
use_encryption = true
|
|
||||||
```
|
|
||||||
265
doc/server_plugin.md
Normal file
265
doc/server_plugin.md
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
### Server Plugin
|
||||||
|
|
||||||
|
frp server plugin is aimed to extend frp's ability without modifying the Golang code.
|
||||||
|
|
||||||
|
An external server should run in a different process receiving RPC calls from frps.
|
||||||
|
Before frps is doing some operations, it will send RPC requests to notify the external RPC server and act according to its response.
|
||||||
|
|
||||||
|
### RPC request
|
||||||
|
|
||||||
|
RPC requests are based on JSON over HTTP.
|
||||||
|
|
||||||
|
When a server plugin accepts an operation request, it can respond with three different responses:
|
||||||
|
|
||||||
|
* Reject operation and return a reason.
|
||||||
|
* Allow operation and keep original content.
|
||||||
|
* Allow operation and return modified content.
|
||||||
|
|
||||||
|
### Interface
|
||||||
|
|
||||||
|
HTTP path can be configured for each manage plugin in frps. We'll assume for this example that it's `/handler`.
|
||||||
|
|
||||||
|
A request to the RPC server will look like:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /handler?version=0.1.0&op=Login
|
||||||
|
{
|
||||||
|
"version": "0.1.0",
|
||||||
|
"op": "Login",
|
||||||
|
"content": {
|
||||||
|
... // Operation info
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Request Header:
|
||||||
|
X-Frp-Reqid: for tracing
|
||||||
|
```
|
||||||
|
|
||||||
|
The response can look like any of the following:
|
||||||
|
|
||||||
|
* Non-200 HTTP response status code (this will automatically tell frps that the request should fail)
|
||||||
|
|
||||||
|
* Reject operation:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"reject": true,
|
||||||
|
"reject_reason": "invalid user"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* Allow operation and keep original content:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"reject": false,
|
||||||
|
"unchange": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* Allow operation and modify content
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"unchange": "false",
|
||||||
|
"content": {
|
||||||
|
... // Replaced content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Operation
|
||||||
|
|
||||||
|
Currently `Login`, `NewProxy`, `CloseProxy`, `Ping`, `NewWorkConn` and `NewUserConn` operations are supported.
|
||||||
|
|
||||||
|
#### Login
|
||||||
|
|
||||||
|
Client login operation
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"version": <string>,
|
||||||
|
"hostname": <string>,
|
||||||
|
"os": <string>,
|
||||||
|
"arch": <string>,
|
||||||
|
"user": <string>,
|
||||||
|
"timestamp": <int64>,
|
||||||
|
"privilege_key": <string>,
|
||||||
|
"run_id": <string>,
|
||||||
|
"pool_count": <int>,
|
||||||
|
"metas": map<string>string,
|
||||||
|
"client_address": <string>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### NewProxy
|
||||||
|
|
||||||
|
Create new proxy
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"user": {
|
||||||
|
"user": <string>,
|
||||||
|
"metas": map<string>string
|
||||||
|
"run_id": <string>
|
||||||
|
},
|
||||||
|
"proxy_name": <string>,
|
||||||
|
"proxy_type": <string>,
|
||||||
|
"use_encryption": <bool>,
|
||||||
|
"use_compression": <bool>,
|
||||||
|
"bandwidth_limit": <string>,
|
||||||
|
"bandwidth_limit_mode": <string>,
|
||||||
|
"group": <string>,
|
||||||
|
"group_key": <string>,
|
||||||
|
|
||||||
|
// tcp and udp only
|
||||||
|
"remote_port": <int>,
|
||||||
|
|
||||||
|
// http and https only
|
||||||
|
"custom_domains": []<string>,
|
||||||
|
"subdomain": <string>,
|
||||||
|
"locations": []<string>,
|
||||||
|
"http_user": <string>,
|
||||||
|
"http_pwd": <string>,
|
||||||
|
"host_header_rewrite": <string>,
|
||||||
|
"headers": map<string>string,
|
||||||
|
|
||||||
|
// stcp only
|
||||||
|
"sk": <string>,
|
||||||
|
|
||||||
|
// tcpmux only
|
||||||
|
"multiplexer": <string>
|
||||||
|
|
||||||
|
"metas": map<string>string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### CloseProxy
|
||||||
|
|
||||||
|
A previously created proxy is closed.
|
||||||
|
|
||||||
|
Please note that one request will be sent for every proxy that is closed, do **NOT** use this
|
||||||
|
if you have too many proxies bound to a single client, as this may exhaust the server's resources.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"user": {
|
||||||
|
"user": <string>,
|
||||||
|
"metas": map<string>string
|
||||||
|
"run_id": <string>
|
||||||
|
},
|
||||||
|
"proxy_name": <string>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Ping
|
||||||
|
|
||||||
|
Heartbeat from frpc
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"user": {
|
||||||
|
"user": <string>,
|
||||||
|
"metas": map<string>string
|
||||||
|
"run_id": <string>
|
||||||
|
},
|
||||||
|
"timestamp": <int64>,
|
||||||
|
"privilege_key": <string>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### NewWorkConn
|
||||||
|
|
||||||
|
New work connection received from frpc (RPC sent after `run_id` is matched with an existing frp connection)
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"user": {
|
||||||
|
"user": <string>,
|
||||||
|
"metas": map<string>string
|
||||||
|
"run_id": <string>
|
||||||
|
},
|
||||||
|
"run_id": <string>
|
||||||
|
"timestamp": <int64>,
|
||||||
|
"privilege_key": <string>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### NewUserConn
|
||||||
|
|
||||||
|
New user connection received from proxy (support `tcp`, `stcp`, `https` and `tcpmux`) .
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"user": {
|
||||||
|
"user": <string>,
|
||||||
|
"metas": map<string>string
|
||||||
|
"run_id": <string>
|
||||||
|
},
|
||||||
|
"proxy_name": <string>,
|
||||||
|
"proxy_type": <string>,
|
||||||
|
"remote_addr": <string>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Plugin Configuration
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# frps.toml
|
||||||
|
bindPort = 7000
|
||||||
|
|
||||||
|
[[httpPlugins]]
|
||||||
|
name = "user-manager"
|
||||||
|
addr = "127.0.0.1:9000"
|
||||||
|
path = "/handler"
|
||||||
|
ops = ["Login"]
|
||||||
|
|
||||||
|
[[httpPlugins]]
|
||||||
|
name = "port-manager"
|
||||||
|
addr = "127.0.0.1:9001"
|
||||||
|
path = "/handler"
|
||||||
|
ops = ["NewProxy"]
|
||||||
|
```
|
||||||
|
|
||||||
|
- addr: the address where the external RPC service listens. Defaults to http. For https, specify the schema: `addr = "https://127.0.0.1:9001"`.
|
||||||
|
- path: http request url path for the POST request.
|
||||||
|
- ops: operations plugin needs to handle (e.g. "Login", "NewProxy", ...).
|
||||||
|
- tlsVerify: When the schema is https, we verify by default. Set this value to false if you want to skip verification.
|
||||||
|
|
||||||
|
### Metadata
|
||||||
|
|
||||||
|
Metadata will be sent to the server plugin in each RPC request.
|
||||||
|
|
||||||
|
There are 2 types of metadata entries - global one and the other under each proxy configuration.
|
||||||
|
Global metadata entries will be sent in `Login` under the key `metas`, and in any other RPC request under `user.metas`.
|
||||||
|
Metadata entries under each proxy configuration will be sent in `NewProxy` op only, under `metas`.
|
||||||
|
|
||||||
|
This is an example of metadata entries:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# frpc.toml
|
||||||
|
serverAddr = "127.0.0.1"
|
||||||
|
serverPort = 7000
|
||||||
|
user = "fake"
|
||||||
|
metadatas.token = "fake"
|
||||||
|
metadatas.version = "1.0.0"
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "ssh"
|
||||||
|
type = "tcp"
|
||||||
|
localPort = 22
|
||||||
|
remotePort = 6000
|
||||||
|
metadatas.id = "123"
|
||||||
|
```
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user