Compare commits
1811 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
636a1c2c38 | ||
|
|
ffab21cec9 | ||
|
|
a09e914635 | ||
|
|
71b974d4f8 | ||
|
|
c8e8fd5b25 | ||
|
|
b78f8a3909 | ||
|
|
ec9a9fff2f | ||
|
|
87aecfc515 | ||
|
|
063e33ad76 | ||
|
|
fc1fecd021 | ||
|
|
3462bba3f4 | ||
|
|
7756cf9b1e | ||
|
|
3f9a03e60d | ||
|
|
1f8ac8bfe1 | ||
|
|
aceecd9800 | ||
|
|
952faaf76f | ||
|
|
b83aaa863f | ||
|
|
a215fad41a | ||
|
|
3efff2ea8a | ||
|
|
fb4b1e1bb5 | ||
|
|
50de6cccc1 | ||
|
|
db4558083e | ||
|
|
deb46db45a | ||
|
|
82cfe06140 | ||
|
|
67dbada65e | ||
|
|
98bdfad04d | ||
|
|
92f3b2b61c | ||
|
|
156983b59b | ||
|
|
df441be8e3 | ||
|
|
2cc481c3d0 | ||
|
|
3cb884fad2 | ||
|
|
0b49f1d88c | ||
|
|
8ecb47e65f | ||
|
|
c45be00963 | ||
|
|
699b28acaa | ||
|
|
4d9450a980 | ||
|
|
2469c949aa | ||
|
|
e7f315659f | ||
|
|
13513b563b | ||
|
|
bfe59cd8f8 | ||
|
|
79ab3e74a5 | ||
|
|
ed3b1fa3c2 | ||
|
|
19e9857fea | ||
|
|
665a980d62 | ||
|
|
eb0c6549c4 | ||
|
|
e7627bfcd3 | ||
|
|
62f5d4cb89 | ||
|
|
4502509c2d | ||
|
|
2f577c9884 | ||
|
|
499c7a432d | ||
|
|
5d24d665bd | ||
|
|
65753fe23e | ||
|
|
96825be11b | ||
|
|
ce2e65d776 | ||
|
|
ab320c9ecc | ||
|
|
76c912083e | ||
|
|
ea898ed104 | ||
|
|
0da12ef47b | ||
|
|
92aa89263b | ||
|
|
a1af33c6aa | ||
|
|
58a8b2b860 | ||
|
|
d9b91d074f | ||
|
|
de62be6f21 | ||
|
|
acfd4c3e55 | ||
|
|
dd446c805d | ||
|
|
d3f42e39db | ||
|
|
7b5ad6c38d | ||
|
|
8edce2055d | ||
|
|
d19976cc3f | ||
|
|
193d11587d | ||
|
|
4d4d2ad801 | ||
|
|
9b8407aeb0 | ||
|
|
aa4a7aa6f6 | ||
|
|
4bac74a149 | ||
|
|
dd9b0b151f | ||
|
|
0a8a0ee771 | ||
|
|
2bcf05ca45 | ||
|
|
aa426016f2 | ||
|
|
1fc0f21506 | ||
|
|
e1fdc10ef8 | ||
|
|
26d19abf61 | ||
|
|
590a1f1429 | ||
|
|
a020a4e0ed | ||
|
|
ad7dcdb628 | ||
|
|
a38fd26cf6 | ||
|
|
950cf67e4c | ||
|
|
d8341509e7 | ||
|
|
2bbd8b3a5f | ||
|
|
e315e48c39 | ||
|
|
150a338166 | ||
|
|
a957474740 | ||
|
|
019edf38f3 | ||
|
|
8ca069f6de | ||
|
|
2f16b06ffe | ||
|
|
456517af87 | ||
|
|
e8140d7310 | ||
|
|
ff48386cc8 | ||
|
|
70cb71acfa | ||
|
|
13418e9324 | ||
|
|
1196727448 | ||
|
|
aaae191710 | ||
|
|
1620e16b89 | ||
|
|
db577b154e | ||
|
|
c6164b8ae7 | ||
|
|
fc023748c1 | ||
|
|
cb3bc3f604 | ||
|
|
1dd63c29ec | ||
|
|
cc9a0d4dc2 | ||
|
|
74dd2a3b9a | ||
|
|
55c8677443 | ||
|
|
26d3105f54 | ||
|
|
ca2757d41e | ||
|
|
f38966c6ac | ||
|
|
baaef63d1d | ||
|
|
4d357a6a57 | ||
|
|
8b2188fcb6 | ||
|
|
799fdd7098 | ||
|
|
12f599fd65 | ||
|
|
be2ed1089c | ||
|
|
92911bda2b | ||
|
|
f7d9e56cac | ||
|
|
a577d8b3cd | ||
|
|
76ffa107dd | ||
|
|
9a6a65931e | ||
|
|
de089e51fd | ||
|
|
51ae2d7301 | ||
|
|
e5fc1bd574 | ||
|
|
aaf310ffff | ||
|
|
3ad86274d8 | ||
|
|
b4afdac8a0 | ||
|
|
19d405fa3a | ||
|
|
d92f85d1dd | ||
|
|
5a319dc64f | ||
|
|
a45aeb3bd6 | ||
|
|
162376fd74 | ||
|
|
0d4e4175a8 | ||
|
|
7ca390c85a | ||
|
|
d413775060 | ||
|
|
db0a467d33 | ||
|
|
e2ff12c589 | ||
|
|
849f0bd0a8 | ||
|
|
e61fb42cbc | ||
|
|
d8339ab967 | ||
|
|
410b7cd512 | ||
|
|
ad75543172 | ||
|
|
757185256c | ||
|
|
04dcb65eb0 | ||
|
|
1ff55bbfa7 | ||
|
|
c60eb050ef | ||
|
|
d7975d8d76 | ||
|
|
6b07908084 | ||
|
|
c49553abd0 | ||
|
|
ae309d64c4 | ||
|
|
8385acd0e3 | ||
|
|
e5836c8118 | ||
|
|
c23d779280 | ||
|
|
364c9c8162 | ||
|
|
3158190945 | ||
|
|
c8da72a7f7 | ||
|
|
6e041895c7 | ||
|
|
0aa6013342 | ||
|
|
6074ed21f7 | ||
|
|
dcecb79f63 | ||
|
|
71e01ab26d | ||
|
|
7ad6d99bd7 | ||
|
|
ad80d4e475 | ||
|
|
c85601146d | ||
|
|
b18b37042d | ||
|
|
0900a63b83 | ||
|
|
143d4611ba | ||
|
|
caa1d70aab | ||
|
|
a275ef17a8 | ||
|
|
856aed2d60 | ||
|
|
b52a517b16 | ||
|
|
69da5c10c6 | ||
|
|
d01fccf28c | ||
|
|
9fcff83f8f | ||
|
|
eec9c449d4 | ||
|
|
8180b75ef1 | ||
|
|
d381304136 | ||
|
|
d67f00546a | ||
|
|
810bf4542f | ||
|
|
e38350e8b3 | ||
|
|
3f479c5537 | ||
|
|
0d387d9799 | ||
|
|
8648351fc7 | ||
|
|
73b2573b14 | ||
|
|
91802fad3e | ||
|
|
87451560e3 | ||
|
|
5ac99ee556 | ||
|
|
d939a82225 | ||
|
|
0722c4369b | ||
|
|
1a0f734a9c | ||
|
|
bf94f8b87c | ||
|
|
5c8214e121 | ||
|
|
e6c8b0c86b | ||
|
|
03ebd5b841 | ||
|
|
c21b434c4e | ||
|
|
113724f340 | ||
|
|
9cde0909b0 | ||
|
|
86eab21be8 | ||
|
|
73d7779d89 | ||
|
|
9c31111249 | ||
|
|
e1b5d2fe39 | ||
|
|
ca880f6cbb | ||
|
|
784b7585c1 | ||
|
|
ce0693feda | ||
|
|
3e47a4f664 | ||
|
|
7318d1f32a | ||
|
|
259566fcce | ||
|
|
3121c35437 | ||
|
|
e35e07acdb | ||
|
|
a65e7782de | ||
|
|
a9341d7c0f | ||
|
|
723c15fb3e | ||
|
|
c7ba326540 | ||
|
|
61b5f97bf2 | ||
|
|
d396c24ad4 | ||
|
|
5f30ea3658 | ||
|
|
ba472c3c67 | ||
|
|
00ce4e4685 | ||
|
|
26a3c3085b | ||
|
|
f6fac68e1f | ||
|
|
4cc95f7269 | ||
|
|
fe41109c76 | ||
|
|
cec6420909 | ||
|
|
55847e7f0e | ||
|
|
c76a18168b | ||
|
|
f721cf5c40 | ||
|
|
ff2eed8ee9 | ||
|
|
61fe7c39a7 | ||
|
|
691133d7c8 | ||
|
|
8ce9af4adf | ||
|
|
d8b040e57c | ||
|
|
c71f0426ae | ||
|
|
7572daf9cc | ||
|
|
56d305fde4 | ||
|
|
74836af66e | ||
|
|
ed828458ab | ||
|
|
6175acb572 | ||
|
|
a91cf22e0f | ||
|
|
62854e4802 | ||
|
|
bde5713ed6 | ||
|
|
c14484856e | ||
|
|
84e387cc9c | ||
|
|
ac309cf9a3 | ||
|
|
59bdd4bc4e | ||
|
|
271d958acf | ||
|
|
bfa17314c6 | ||
|
|
6439569f36 | ||
|
|
50a9ac0163 | ||
|
|
1a765c7ff7 | ||
|
|
61e6cc6985 | ||
|
|
37b0c229fc | ||
|
|
d32d0d7587 | ||
|
|
3c522961af | ||
|
|
2d9e7dfba2 | ||
|
|
4a737be421 | ||
|
|
450ae868ff | ||
|
|
c8531a5492 | ||
|
|
c5c5860012 | ||
|
|
f83600225b | ||
|
|
a1346aa071 | ||
|
|
894e12e285 | ||
|
|
96c614550f | ||
|
|
6295be786f | ||
|
|
789d61f170 | ||
|
|
d5a9bec3da | ||
|
|
9e9d6a5585 | ||
|
|
654ce2e349 | ||
|
|
9456884584 | ||
|
|
010c36cab5 | ||
|
|
b872c423ee | ||
|
|
2ee2098a48 | ||
|
|
1acc2151cf | ||
|
|
0671178e29 | ||
|
|
7991b07165 | ||
|
|
37facd21d4 | ||
|
|
b4d9bf9c16 | ||
|
|
5452c3c121 | ||
|
|
9322701615 | ||
|
|
2fdcb44c14 | ||
|
|
87b12af932 | ||
|
|
75c2bcff8f | ||
|
|
822a05aa20 | ||
|
|
4139c79a77 | ||
|
|
379f87f571 | ||
|
|
51febb19fa | ||
|
|
5c938e46b7 | ||
|
|
9a7a3b00dc | ||
|
|
daf643596d | ||
|
|
bc8d71dfc7 | ||
|
|
8c31cc47b0 | ||
|
|
59378104b7 | ||
|
|
116be362ba | ||
|
|
e1c3097546 | ||
|
|
9bcdc90ca8 | ||
|
|
7da5d8fcea | ||
|
|
4a15775f65 | ||
|
|
691e44c1dc | ||
|
|
90bce505c4 | ||
|
|
320e404e4d | ||
|
|
e3c4ee0833 | ||
|
|
f1e52d99ba | ||
|
|
fc460922ad | ||
|
|
ba9df51b2e | ||
|
|
6282f95bd3 | ||
|
|
254824b781 | ||
|
|
40d0945450 | ||
|
|
63972edb96 | ||
|
|
da0eb5037e | ||
|
|
4b685b21a2 | ||
|
|
f05fe78737 | ||
|
|
19a95d8c55 | ||
|
|
64c7588a44 | ||
|
|
c55196a525 | ||
|
|
1da24ea0af | ||
|
|
75278d64de | ||
|
|
e54fd46a9e | ||
|
|
fac022090d | ||
|
|
dc7c829b73 | ||
|
|
aefcea034a | ||
|
|
1cbaa7c77b | ||
|
|
5ef0a2ed4b | ||
|
|
a592e388cd | ||
|
|
5d4145900f | ||
|
|
b94ec7597c | ||
|
|
c437f0ad76 | ||
|
|
7f7d2e57c2 | ||
|
|
397cad93df | ||
|
|
ce8dbda44b | ||
|
|
62b87083bb | ||
|
|
de35eb77cb | ||
|
|
163662a65a | ||
|
|
6395fa0b67 | ||
|
|
f03fdd1155 | ||
|
|
8ab4a9aa70 | ||
|
|
6c482a248d | ||
|
|
25450d9efc | ||
|
|
60cc07bc81 | ||
|
|
d8dd4b2131 | ||
|
|
b7b54b54c3 | ||
|
|
5011002d84 | ||
|
|
f5f56129df | ||
|
|
63212bb033 | ||
|
|
830116bcf2 | ||
|
|
ea96fe9a26 | ||
|
|
ebdda1b62e | ||
|
|
54a76e8c45 | ||
|
|
132d18d5d1 | ||
|
|
75e6ef6132 | ||
|
|
af0d7b48ad | ||
|
|
c03bcb3a8a | ||
|
|
39259ad6a9 | ||
|
|
2c070b7eda | ||
|
|
0413c0471c | ||
|
|
e4be4048e3 | ||
|
|
00366fce07 | ||
|
|
e88172dd7e | ||
|
|
a5cb26daf2 | ||
|
|
4f8794a255 | ||
|
|
5e5a09f164 | ||
|
|
f78e4b0443 | ||
|
|
51d8f3b436 | ||
|
|
ecc01f4f37 | ||
|
|
2af42da371 | ||
|
|
d1e4ee7bc8 | ||
|
|
4440c49174 | ||
|
|
76964a6b85 | ||
|
|
66f360e66c | ||
|
|
a38ce460bb | ||
|
|
80f21d1c91 | ||
|
|
1c1b76011f | ||
|
|
957d3a7b4d | ||
|
|
d7d7b0bbf0 | ||
|
|
a3156de4a8 | ||
|
|
99424bfa58 | ||
|
|
d120957736 | ||
|
|
324d695d93 | ||
|
|
9d60972743 | ||
|
|
f938af5a61 | ||
|
|
9ccdc3a597 | ||
|
|
3499edd5c2 | ||
|
|
9470cd6e69 | ||
|
|
1f7433e798 | ||
|
|
4ba3d026b4 | ||
|
|
98c639579f | ||
|
|
74e5999c63 | ||
|
|
48939b2b4f | ||
|
|
8339fee69d | ||
|
|
a2fc7d3cc5 | ||
|
|
ae7954eee2 | ||
|
|
8f934f7c82 | ||
|
|
b2781e0bfc | ||
|
|
e11473cf52 | ||
|
|
f8f8962ccb | ||
|
|
2238043efd | ||
|
|
d9426cef20 | ||
|
|
052d586364 | ||
|
|
11ba41e903 | ||
|
|
255985b7b0 | ||
|
|
2b77709a04 | ||
|
|
5b4a1bda2e | ||
|
|
3f94f6d0e7 | ||
|
|
d28a53a6cf | ||
|
|
963cec124e | ||
|
|
bbaca578cd | ||
|
|
da30389989 | ||
|
|
52ec36dbd6 | ||
|
|
b524d178dd | ||
|
|
e0d9b8bddf | ||
|
|
19da923369 | ||
|
|
824a70b22d | ||
|
|
cea70d5d6b | ||
|
|
adad8e658b | ||
|
|
e10487ad57 | ||
|
|
4eded56d5f | ||
|
|
43d011f125 | ||
|
|
a292044501 | ||
|
|
05c54614b2 | ||
|
|
32020e236f | ||
|
|
b9cf6e5083 | ||
|
|
ee5b7290a0 | ||
|
|
fd6a44c562 | ||
|
|
8d12872608 | ||
|
|
712f2053a4 | ||
|
|
54462c26f2 | ||
|
|
d0a171558d | ||
|
|
1ade850557 | ||
|
|
466f2e88b3 | ||
|
|
3cb53b2c33 | ||
|
|
6279216c2e | ||
|
|
5219c1fdd1 | ||
|
|
4294659785 | ||
|
|
f03f1b0156 | ||
|
|
184b99d500 | ||
|
|
74f05e5305 | ||
|
|
aefa7f77c2 | ||
|
|
084d4109b8 | ||
|
|
b60d3f680e | ||
|
|
ee90bfb506 | ||
|
|
e17068a76f | ||
|
|
354fc9b3d6 | ||
|
|
e29f6857db | ||
|
|
40344ec0ff | ||
|
|
783dff369b | ||
|
|
72e0325d05 | ||
|
|
2710207779 | ||
|
|
b719d03ebe | ||
|
|
84396343da | ||
|
|
14242b59a2 | ||
|
|
dad346cee8 | ||
|
|
04282f94a4 | ||
|
|
0423e8f157 | ||
|
|
bdcee06665 | ||
|
|
ae90ed2ba0 | ||
|
|
4ba3ae876d | ||
|
|
662164c7ff | ||
|
|
fad6af11e5 | ||
|
|
dba088daed | ||
|
|
a23fdea9e3 | ||
|
|
561976bcd0 | ||
|
|
874776bd12 | ||
|
|
ec67b67e9e | ||
|
|
71f691b208 | ||
|
|
e0cbb966f0 | ||
|
|
df9d47900a | ||
|
|
b8496c4d6e | ||
|
|
b0cfaf189c | ||
|
|
195cb9f081 | ||
|
|
9a10740218 | ||
|
|
7bcd79a70a | ||
|
|
beb8822df4 | ||
|
|
8805d85377 | ||
|
|
fcf9a8c673 | ||
|
|
2c1319985d | ||
|
|
a3fff56da5 | ||
|
|
14961a573f | ||
|
|
78cd5d8eba | ||
|
|
2df2803a37 | ||
|
|
7738faa040 | ||
|
|
157d1db0b1 | ||
|
|
7e85356325 | ||
|
|
a3d0cf5ddf | ||
|
|
04ab8e72f6 | ||
|
|
e0c3a13ac5 | ||
|
|
1b1745b7f7 | ||
|
|
2412a0a369 | ||
|
|
1e14d006b1 | ||
|
|
27c4ffd663 | ||
|
|
c0fe08b597 | ||
|
|
5550a5d2c0 | ||
|
|
2066ad7c83 | ||
|
|
61199172d0 | ||
|
|
3ce4d04b27 | ||
|
|
707729ee61 | ||
|
|
7b5bebc588 | ||
|
|
53f17b5715 | ||
|
|
496c8bc785 | ||
|
|
396d67bb2c | ||
|
|
bbebd9b163 | ||
|
|
c8d94f0a27 | ||
|
|
8be8343fee | ||
|
|
f3995901e3 | ||
|
|
f2618e7de6 | ||
|
|
6afbd77fd5 | ||
|
|
93e5cb36df | ||
|
|
09dea57850 | ||
|
|
8cad436421 | ||
|
|
f0dedbfabf | ||
|
|
51f0ded222 | ||
|
|
6b555cf0d8 | ||
|
|
0190d0b849 | ||
|
|
9977c64459 | ||
|
|
20706e45b0 | ||
|
|
53864fd8c1 | ||
|
|
7fa0959af4 | ||
|
|
2611dd2c98 | ||
|
|
6cebc037a0 | ||
|
|
15ad31da54 | ||
|
|
fe9904a54d | ||
|
|
831851c0c3 | ||
|
|
ea4c4dd57f | ||
|
|
e5a8220b8a | ||
|
|
ed949604d3 | ||
|
|
0841c7d7bd | ||
|
|
e17975ed7d | ||
|
|
f4eb9e7cd6 | ||
|
|
1085f9e5ec | ||
|
|
37eceffed9 | ||
|
|
6270b2c2d3 | ||
|
|
ad5bd18dd0 | ||
|
|
0296e0cafa | ||
|
|
147ad3b230 | ||
|
|
2da3eabc12 | ||
|
|
ac91170d65 | ||
|
|
f13b901f2d | ||
|
|
c23c73ed34 | ||
|
|
ad5d657a1a | ||
|
|
e2bebc99d1 | ||
|
|
926dcbbc63 | ||
|
|
a7f9581d99 | ||
|
|
75d911f29e | ||
|
|
91e4a54385 | ||
|
|
221a4878aa | ||
|
|
2ea43647ed | ||
|
|
04bdd3a5e4 | ||
|
|
1f9cf194fe | ||
|
|
e87118d2a8 | ||
|
|
fe888729f9 | ||
|
|
d7cd2ac803 | ||
|
|
ba9fe38b8b | ||
|
|
7b00fe3d5a | ||
|
|
fc1ba36ae5 | ||
|
|
2290137868 | ||
|
|
6ebe7691db | ||
|
|
29d1993a3b | ||
|
|
81c693de4e | ||
|
|
2017cb60e9 | ||
|
|
ec4cc33364 | ||
|
|
a22282f275 | ||
|
|
67de4c9c07 | ||
|
|
6591769a07 | ||
|
|
5a222807b7 | ||
|
|
a9207857cf | ||
|
|
37ffa3b55a | ||
|
|
048591553a | ||
|
|
33bfd61a0c | ||
|
|
965d059400 | ||
|
|
676286182a | ||
|
|
3b2002d9ef | ||
|
|
9d7e30807d | ||
|
|
91fae5c4d4 | ||
|
|
e3e85867b1 | ||
|
|
5618b95372 | ||
|
|
bf45d04600 | ||
|
|
80244bd83b | ||
|
|
9a9e7d1a7f | ||
|
|
6f422c3d8b | ||
|
|
222f0c735b | ||
|
|
63bf8eb1a1 | ||
|
|
db0e58ae7e | ||
|
|
87045284cc | ||
|
|
f3ee20980a | ||
|
|
54f1946aba | ||
|
|
47842ae614 | ||
|
|
7e0b62b703 | ||
|
|
15b4194e8f | ||
|
|
5a199acbb2 | ||
|
|
07b3f2f4d6 | ||
|
|
13ee236884 | ||
|
|
3822b7d3f7 | ||
|
|
2b2b69fb23 | ||
|
|
4b4edef0ad | ||
|
|
aa1e73326f | ||
|
|
07012aa812 | ||
|
|
0e54fa5655 | ||
|
|
3e44a1dd2d | ||
|
|
a417df60b3 | ||
|
|
2067c5c527 | ||
|
|
8a43486730 | ||
|
|
2636fedce8 | ||
|
|
a42e9ffa6b | ||
|
|
0e8c41bbd1 | ||
|
|
1e21aa9453 | ||
|
|
f9eadd7f04 | ||
|
|
04dc97072b | ||
|
|
ddda0b5ece | ||
|
|
76e89d07d4 | ||
|
|
a538255034 | ||
|
|
4ad2a9c1fa | ||
|
|
7ae9303c99 | ||
|
|
6c7b3ac5bb | ||
|
|
bd294bb3cf | ||
|
|
7349598b19 | ||
|
|
7f19f9f39c | ||
|
|
f19691250d | ||
|
|
554a1cb1f4 | ||
|
|
e54237ff70 | ||
|
|
e58709c822 | ||
|
|
5eca73a399 | ||
|
|
f8a19f747d | ||
|
|
ea3c1d7a3b | ||
|
|
bd585d8e52 | ||
|
|
a40fa93d7b | ||
|
|
4498bbf2e4 | ||
|
|
63e3891808 | ||
|
|
3ebdfa9b2d | ||
|
|
8debde842c | ||
|
|
3e5cf56460 | ||
|
|
f264b005ff | ||
|
|
bf76b0b158 | ||
|
|
c2a65a9a74 | ||
|
|
3267a50ae3 | ||
|
|
f0839519a8 | ||
|
|
95e9106902 | ||
|
|
da03f6c4e3 | ||
|
|
9e77cd1a26 | ||
|
|
56bf51277c | ||
|
|
37d98ca290 | ||
|
|
9473dc3937 | ||
|
|
6777008aec | ||
|
|
3e8254e398 | ||
|
|
9ddd2d3588 | ||
|
|
57935f585c | ||
|
|
2b463d61e3 | ||
|
|
ced4206c5f | ||
|
|
c86db09cd8 | ||
|
|
194c3c13ac | ||
|
|
d65c00728a | ||
|
|
526f6e0f6b | ||
|
|
a61211d32c | ||
|
|
78f75cdcb9 | ||
|
|
4cd340e07f | ||
|
|
890dde0e00 | ||
|
|
b1efe8d0b5 | ||
|
|
71fff28d29 | ||
|
|
6bfdf941bc | ||
|
|
fdc10aa6c7 | ||
|
|
455bb550ee | ||
|
|
2a827544ef | ||
|
|
9d2b5dc07d | ||
|
|
3ca62d76d7 | ||
|
|
00b9280834 | ||
|
|
ef0a3bc571 | ||
|
|
e3c5cf981f | ||
|
|
ec5da8b4a5 | ||
|
|
81de7d271e | ||
|
|
c8158e14e0 | ||
|
|
e96ae5ca51 | ||
|
|
e059197398 | ||
|
|
a2e73228d2 | ||
|
|
1470018054 | ||
|
|
e6bfbcd489 | ||
|
|
a0bbcf6ebb | ||
|
|
7f5a13d185 | ||
|
|
d5946da1e2 | ||
|
|
21682d1c1d | ||
|
|
fd52475ae2 | ||
|
|
55b47cf741 | ||
|
|
e0ce2e2e8a | ||
|
|
8fc4971df1 | ||
|
|
20e8cb898a | ||
|
|
b5894b257f | ||
|
|
cb517a3595 | ||
|
|
1b8f94c08f | ||
|
|
e46051299f | ||
|
|
bf2dcfe307 | ||
|
|
719f6077ab | ||
|
|
101783ee86 | ||
|
|
6843402d2e | ||
|
|
88feda6bf9 | ||
|
|
5c446ff645 | ||
|
|
9a6b1a1315 | ||
|
|
90009a649d | ||
|
|
8762628481 | ||
|
|
a5e41c9336 | ||
|
|
729f30aebf | ||
|
|
1da213a6e3 | ||
|
|
2b0b19da9e | ||
|
|
686166f2ce | ||
|
|
93ce593ed0 | ||
|
|
6f4475ff72 | ||
|
|
0b9a96ec6b | ||
|
|
f0f5ee392b | ||
|
|
dadaca141a | ||
|
|
7ab30099dd | ||
|
|
3170991aa8 | ||
|
|
118744a860 | ||
|
|
fe6a3f2ce8 | ||
|
|
75efaa9741 | ||
|
|
560e7f316a | ||
|
|
eee5d74e87 | ||
|
|
9ae473fcdc | ||
|
|
b774289c6d | ||
|
|
ecf715880f | ||
|
|
b2e28fe3a2 | ||
|
|
cc2f23bd89 | ||
|
|
7329cd804b | ||
|
|
84e3132ed1 | ||
|
|
f6b11c2d01 | ||
|
|
32da923dfe | ||
|
|
91dfa501f8 | ||
|
|
7c724e18fe | ||
|
|
302f83c7a4 | ||
|
|
984ca1fb7e | ||
|
|
87f6a18476 | ||
|
|
90c21458b8 | ||
|
|
f536c64043 | ||
|
|
1a33b5bb53 | ||
|
|
0ecaa862bd | ||
|
|
751946f47a | ||
|
|
796ea1dde9 | ||
|
|
a87aa9b98e | ||
|
|
9abd186166 | ||
|
|
18d0bf9dc3 | ||
|
|
c9bd08cf9c | ||
|
|
d2f4edcdb6 | ||
|
|
67abf03fe3 | ||
|
|
5d7f6960f3 | ||
|
|
4bea9ed760 | ||
|
|
4995cf1b02 | ||
|
|
a5d0cbbe44 | ||
|
|
7b1a0d3cd3 | ||
|
|
1e0b3a2a8c | ||
|
|
e72bb1e124 | ||
|
|
164621289c | ||
|
|
737109b2b8 | ||
|
|
8b8e27b702 | ||
|
|
4b099640de | ||
|
|
80da2dc722 | ||
|
|
61947e67ae | ||
|
|
9a37e3d159 | ||
|
|
14fb6c4038 | ||
|
|
dd9c5b2149 | ||
|
|
ecd488a840 | ||
|
|
4a44a7dfe1 | ||
|
|
16a44a144b | ||
|
|
97f8142b1e | ||
|
|
504cd3efda | ||
|
|
857b6cc10a | ||
|
|
002a06629e | ||
|
|
5bc0f4f8af | ||
|
|
cacfffc5bf | ||
|
|
87d7854453 | ||
|
|
aa34388de0 | ||
|
|
a3f50029ba | ||
|
|
f9d8b83c2a | ||
|
|
7c8bb5b18a | ||
|
|
254b2ae87f | ||
|
|
5a40f998ae | ||
|
|
77f3400161 | ||
|
|
3521bacc4a | ||
|
|
55f8171dd1 | ||
|
|
a7b159aebb | ||
|
|
5c114b28e3 | ||
|
|
e079444e8a | ||
|
|
3cb23ac956 | ||
|
|
8fb256ac91 | ||
|
|
ca32cd5e0e | ||
|
|
e0defafa26 | ||
|
|
5cccb872bb | ||
|
|
aaf940edab | ||
|
|
853086b942 | ||
|
|
81bdba6782 | ||
|
|
d955ddcef9 | ||
|
|
4bbb195711 | ||
|
|
a193089646 | ||
|
|
9bfdc10172 | ||
|
|
b062b38ef4 | ||
|
|
a31a9dc32c | ||
|
|
fa43791ea9 | ||
|
|
93b9c1617e | ||
|
|
4c710d731f | ||
|
|
d9f30e7ac5 | ||
|
|
03da7f696c | ||
|
|
883a3dceaf | ||
|
|
7b86e2ac59 | ||
|
|
8502d7b051 | ||
|
|
6f8b71b89f | ||
|
|
7e7f662a23 | ||
|
|
0bec1c6012 | ||
|
|
5582f5c811 | ||
|
|
48ed3dab1f | ||
|
|
d8de0faef5 | ||
|
|
df828b6021 | ||
|
|
056daaddfc | ||
|
|
5c2fd8d52a | ||
|
|
4519bffa39 | ||
|
|
1ea7429921 | ||
|
|
816c174036 | ||
|
|
79857a8733 | ||
|
|
dcc3292dbc | ||
|
|
7f674a7fb3 | ||
|
|
b64d3c2fbf | ||
|
|
7fc5cb80d6 | ||
|
|
92460f811f | ||
|
|
e18ad55067 | ||
|
|
4e9dae6fa4 | ||
|
|
f5a0559be6 | ||
|
|
670018f05e | ||
|
|
8bbf54d2b6 | ||
|
|
d31cccf85f | ||
|
|
c19b03a3f7 | ||
|
|
c6b8644828 | ||
|
|
f1a255aa6c | ||
|
|
876bf8aa4f | ||
|
|
900e519ff1 | ||
|
|
f1832d4478 | ||
|
|
ebbbf81e65 | ||
|
|
1fccd05e9e | ||
|
|
66945c0a02 | ||
|
|
fa0ca8fe89 | ||
|
|
c478c7dae9 | ||
|
|
9382db751c | ||
|
|
7e2a8e70c9 | ||
|
|
cd35636939 | ||
|
|
d51adb041e | ||
|
|
02db00d008 | ||
|
|
fb2d59ec92 | ||
|
|
1df1225eed | ||
|
|
aca71bff7a | ||
|
|
9709aed5e6 | ||
|
|
d2a4178846 | ||
|
|
d73be7aee5 | ||
|
|
ffe7f7ff16 | ||
|
|
a6ed6fc721 | ||
|
|
c3831de94e | ||
|
|
9b6b9cca3d | ||
|
|
64d1ea2d89 | ||
|
|
1c51239da8 | ||
|
|
51c15de892 | ||
|
|
b8efb1b8ec | ||
|
|
ec1d20f46f | ||
|
|
1f619d5ea6 | ||
|
|
6d3d94a01f | ||
|
|
0a3d94f73d | ||
|
|
7c68b03d07 | ||
|
|
2912b2e92e | ||
|
|
a6fe802370 | ||
|
|
ad483b7581 | ||
|
|
df86955f28 | ||
|
|
00ec426a80 | ||
|
|
222db53410 | ||
|
|
4d85dc108f | ||
|
|
6d582a821b | ||
|
|
794afbf85e | ||
|
|
e3f3997c5e | ||
|
|
f78090e47f | ||
|
|
4d7a4aa99a | ||
|
|
c36217c654 | ||
|
|
59bb578b89 | ||
|
|
7d8823307f | ||
|
|
8174349032 | ||
|
|
00a02dc14d | ||
|
|
ced73ed04e | ||
|
|
cc73bb811b | ||
|
|
a587228cf0 | ||
|
|
1472a0f415 | ||
|
|
0bb141960f | ||
|
|
c153330ab8 | ||
|
|
5b4ef0ee3b | ||
|
|
9632b6ee94 | ||
|
|
78eb1c1166 | ||
|
|
a7c0b07a2a | ||
|
|
dc1cc88a46 | ||
|
|
3f5451eab6 | ||
|
|
30d98326ca | ||
|
|
bedc8e288b | ||
|
|
6092b6628e | ||
|
|
6ee51c5cc1 | ||
|
|
4df0ae82ac | ||
|
|
5db31f0fb3 | ||
|
|
0f8170c10f | ||
|
|
3c24cb773f | ||
|
|
bec54ac8ae | ||
|
|
c330ac8418 | ||
|
|
3e478f42ea | ||
|
|
18ab757216 | ||
|
|
b6bcf0cd94 | ||
|
|
015aa36c56 | ||
|
|
f2480ce5c9 | ||
|
|
f828c58dca | ||
|
|
dc19921b0c | ||
|
|
3f3591bae0 | ||
|
|
fc048728d9 | ||
|
|
aeb4675196 | ||
|
|
4652f9ede8 | ||
|
|
531cb5b5a1 | ||
|
|
9fb43b2c46 | ||
|
|
8a8298ad46 | ||
|
|
3d6b09e949 | ||
|
|
fb8f013ea7 | ||
|
|
c41319bb7a | ||
|
|
46157ebbb6 | ||
|
|
200b1d08c7 | ||
|
|
24b0352eb6 | ||
|
|
52f3a98cc8 | ||
|
|
e29a3efd39 | ||
|
|
ca730e77a5 | ||
|
|
0833b4698e | ||
|
|
ee5c5e033d | ||
|
|
78233ff9a3 | ||
|
|
b331dc5686 | ||
|
|
dfcfcee208 | ||
|
|
094ee1522e | ||
|
|
3bc58f5988 | ||
|
|
f6938e76dc | ||
|
|
570964deb3 | ||
|
|
31984ffec1 | ||
|
|
74fc3aaf37 | ||
|
|
97d0a48557 | ||
|
|
3bbe67571f | ||
|
|
f131ef130b | ||
|
|
4a6a4ce28d | ||
|
|
a80ac80fcd | ||
|
|
4aa9686e3b | ||
|
|
64e87d64bd | ||
|
|
9ca0b46f30 | ||
|
|
6eb154bb74 | ||
|
|
ea01c3a125 | ||
|
|
1b4a1fbbe5 | ||
|
|
ec81a7ac29 | ||
|
|
22d28a37b6 | ||
|
|
cc134cad9a | ||
|
|
1459150024 | ||
|
|
87751e562e | ||
|
|
e6f969cb04 | ||
|
|
ba1febba73 | ||
|
|
af8fa7ff81 | ||
|
|
4ab2e4088a | ||
|
|
da0ccc6426 | ||
|
|
0661876e99 | ||
|
|
cd72ac4fc9 | ||
|
|
da5a061b65 | ||
|
|
65948a47f1 | ||
|
|
bf4b3e6840 | ||
|
|
6ea38188e8 | ||
|
|
b5639a51fd | ||
|
|
5c34d814d6 | ||
|
|
0eca4f1866 | ||
|
|
b52f829f05 | ||
|
|
90f64c9f63 | ||
|
|
c106498dd8 | ||
|
|
7bad65a43e | ||
|
|
101c2962ab | ||
|
|
59140a6d51 | ||
|
|
b1d54f69d9 | ||
|
|
374de07c7b | ||
|
|
8a4c21b64a | ||
|
|
16ba7ddb34 | ||
|
|
bd9506da42 | ||
|
|
b903a6e46f | ||
|
|
bcf088f586 | ||
|
|
be3857d572 | ||
|
|
b99d4ce82e | ||
|
|
0a558203da | ||
|
|
5a549a88fe | ||
|
|
fe953d6b38 | ||
|
|
05c62b9f40 | ||
|
|
555dc3b0c0 | ||
|
|
0de0d3308c | ||
|
|
a20373b613 | ||
|
|
ced2e16f41 | ||
|
|
3ac832c8dd | ||
|
|
a3c087456b | ||
|
|
419774158a | ||
|
|
0503215e7a | ||
|
|
9541843ff7 | ||
|
|
98f22ba110 | ||
|
|
1e9a19e326 | ||
|
|
0046c9960a | ||
|
|
7640612a95 | ||
|
|
a26962f367 | ||
|
|
f778e47d22 | ||
|
|
4781921336 | ||
|
|
3ae8abda9e | ||
|
|
90b324d707 | ||
|
|
3a22aae34f | ||
|
|
45a0473fec | ||
|
|
a7313e4492 | ||
|
|
c41ae116eb | ||
|
|
83c7453957 | ||
|
|
85a47810ff | ||
|
|
c997ef876c | ||
|
|
ae8ccadad2 | ||
|
|
5967aa1aa5 | ||
|
|
c900cde8e4 | ||
|
|
13183a9f76 | ||
|
|
5a568b4077 | ||
|
|
030507a2ce | ||
|
|
338301955f | ||
|
|
6d313f6d8f | ||
|
|
776dffcf12 | ||
|
|
e1a2451c22 | ||
|
|
7344366ce8 | ||
|
|
bd5191dfc5 | ||
|
|
bfa4085932 | ||
|
|
302ec2558c | ||
|
|
ff19879ffd | ||
|
|
04001f7ad3 | ||
|
|
076b2f0ee0 | ||
|
|
93dfb03eaf | ||
|
|
e09bdd43d4 | ||
|
|
ac8d8a3da1 | ||
|
|
a4157e83e9 | ||
|
|
13f23838a1 | ||
|
|
fd4c388b23 | ||
|
|
88b10da596 | ||
|
|
c07dc74d48 | ||
|
|
b48e01155c | ||
|
|
0ff010cc94 | ||
|
|
81aac15a6c | ||
|
|
c1b862394d | ||
|
|
f19937b715 | ||
|
|
f2f612b450 | ||
|
|
0c2640bbab | ||
|
|
3bb0ca1d2b | ||
|
|
d5b42f72e2 | ||
|
|
62744e081b | ||
|
|
9dcaf1555f | ||
|
|
a09cf5c8b9 | ||
|
|
47ebe42375 | ||
|
|
4d97ab9eb9 | ||
|
|
8ed13dc4a9 | ||
|
|
3b66dd0873 | ||
|
|
d992f0ffcc | ||
|
|
6c5a7e8f13 | ||
|
|
9d3d7db29c | ||
|
|
8607788975 | ||
|
|
4be6307d87 | ||
|
|
feec2118bb | ||
|
|
43182fc25e | ||
|
|
976f588863 | ||
|
|
575bcf1f03 | ||
|
|
969c992bfd | ||
|
|
c1239fbf59 | ||
|
|
c63b923ec3 | ||
|
|
574c4029fc | ||
|
|
423d8306be | ||
|
|
fc7066a25c | ||
|
|
e1bf46c6a5 | ||
|
|
3b46e6a6fb | ||
|
|
7a85c66ee7 | ||
|
|
25a44030f9 | ||
|
|
600268ebb8 | ||
|
|
1223957f91 | ||
|
|
15cde2dd1a | ||
|
|
50e441849a | ||
|
|
02bb09ec01 | ||
|
|
402947a43c | ||
|
|
b9bc8d722d | ||
|
|
0cb5c49cf3 | ||
|
|
9fc4be6d40 | ||
|
|
ecfed4dc04 | ||
|
|
b415e4d98f | ||
|
|
7d059efe06 | ||
|
|
60cfbd2989 | ||
|
|
8ecf64f481 | ||
|
|
019b0f2fd5 | ||
|
|
15d6cd144a | ||
|
|
f59f62317e | ||
|
|
f2b93c0402 | ||
|
|
0540b8780e | ||
|
|
fa45c9c138 | ||
|
|
b67cd0d3df | ||
|
|
c8f7fc9bc9 | ||
|
|
f1b998ce16 | ||
|
|
aaa758e978 | ||
|
|
716946a148 | ||
|
|
15934d72cc | ||
|
|
8f6cdacd00 | ||
|
|
8f736da4b8 | ||
|
|
4ea4202b99 | ||
|
|
d4bfc3f6b5 | ||
|
|
23d9ebfc91 | ||
|
|
5c99f4fb60 | ||
|
|
2263c7e20f | ||
|
|
515b2d917e | ||
|
|
af4723356d | ||
|
|
068dd34a38 | ||
|
|
b16a5c2caf | ||
|
|
a383957cfa | ||
|
|
00f97aabb4 | ||
|
|
32db0787bb | ||
|
|
1275328fdf | ||
|
|
7778716fa7 | ||
|
|
77476d0f56 | ||
|
|
c7a1fc2996 | ||
|
|
e7d8e73be8 | ||
|
|
3ee27f4370 | ||
|
|
92424cd1c2 | ||
|
|
0190dad984 | ||
|
|
198258f4e7 | ||
|
|
5be4b6bd44 | ||
|
|
3941255733 | ||
|
|
46998252e5 | ||
|
|
74b51f0ad3 | ||
|
|
b11865f971 | ||
|
|
f4369cdbef | ||
|
|
92638ce93d | ||
|
|
6ef85d6026 | ||
|
|
bc88503f25 | ||
|
|
47317bed9b | ||
|
|
f45c89fc46 | ||
|
|
112e3b2fc2 | ||
|
|
124c471a2b | ||
|
|
683ba6cd5b | ||
|
|
21fbcf4556 | ||
|
|
2ffefbeb33 | ||
|
|
c844fc7477 | ||
|
|
4b98f37df1 | ||
|
|
0bc4db9950 | ||
|
|
5acf29dae6 | ||
|
|
e9a42cd508 | ||
|
|
ed26d68948 | ||
|
|
b389f93d97 | ||
|
|
150aebf8d2 | ||
|
|
74e0223eb9 | ||
|
|
0823928f98 | ||
|
|
f895059660 | ||
|
|
acb4310c11 | ||
|
|
fdf3f23df5 | ||
|
|
d92861a8e8 | ||
|
|
1ee843757d | ||
|
|
ea26d7786c | ||
|
|
6eb43baf3d | ||
|
|
2f56375121 | ||
|
|
3bfd7e4d17 | ||
|
|
e1c66d96a1 | ||
|
|
a43854ae9b | ||
|
|
183bedd6ed | ||
|
|
2a89a8f664 | ||
|
|
5cd27ce529 | ||
|
|
cee2e18caf | ||
|
|
9ad750da54 | ||
|
|
5f49af1780 | ||
|
|
d5f092284a | ||
|
|
0e50310a66 | ||
|
|
5939ac4801 | ||
|
|
db274f1093 | ||
|
|
6bc5c64a3a | ||
|
|
70e035315e | ||
|
|
8a1249878a | ||
|
|
5e375f56dd | ||
|
|
28f1d66ae5 | ||
|
|
79060d37a7 | ||
|
|
800e64404b | ||
|
|
54c0c1b80d | ||
|
|
f7c7e2951d | ||
|
|
f249286cb1 | ||
|
|
d6dc3a507e | ||
|
|
0286da2356 | ||
|
|
76c08baaa0 | ||
|
|
67ea75cf03 | ||
|
|
4c658bb6f0 | ||
|
|
1ab02d5891 | ||
|
|
055506e518 | ||
|
|
88122ba2f8 | ||
|
|
bfe0c18976 | ||
|
|
df41f0c556 | ||
|
|
561c5021dd | ||
|
|
ad07fc78eb | ||
|
|
3243181c5f | ||
|
|
895117718e | ||
|
|
534b253c20 | ||
|
|
901cafc6da | ||
|
|
a6e36e7cad | ||
|
|
b566457e12 | ||
|
|
ca3e15578e | ||
|
|
4b2edff6dd | ||
|
|
2146b83343 | ||
|
|
3e1b07324d | ||
|
|
8cc2dfe5c2 | ||
|
|
78a837e8f1 | ||
|
|
49830516be | ||
|
|
41e1d9e68a | ||
|
|
5da4f931c5 | ||
|
|
552a96533e | ||
|
|
cebd069c77 | ||
|
|
be9230e85b | ||
|
|
b1ce6eb85b | ||
|
|
46176a54b4 | ||
|
|
a21ccad174 | ||
|
|
1129a868a5 | ||
|
|
1ac66d27b6 | ||
|
|
6a6e8fffbc | ||
|
|
51f110bc7b | ||
|
|
4ddfe41f23 | ||
|
|
ddd06fc2ac | ||
|
|
1bccb93fcb | ||
|
|
db80781716 | ||
|
|
a2a99f9b57 | ||
|
|
cd4a68cc96 | ||
|
|
b37eb68993 | ||
|
|
b13958a8d6 | ||
|
|
17e2b234a0 | ||
|
|
4ef1775e9a | ||
|
|
363977b474 | ||
|
|
05ae0ea5f2 | ||
|
|
8de7a81674 | ||
|
|
d32b195a57 | ||
|
|
267d9f1831 | ||
|
|
17a42a0c11 | ||
|
|
a219d25cac | ||
|
|
ce731020a7 | ||
|
|
fc9082c422 | ||
|
|
4872ba2ea0 | ||
|
|
70bb3c34ce | ||
|
|
1cde50f050 | ||
|
|
e9dd4ecdf0 | ||
|
|
f863530653 | ||
|
|
4f609cfa30 | ||
|
|
78bf808322 | ||
|
|
afe1da92c5 | ||
|
|
9985224966 | ||
|
|
02679d6df3 | ||
|
|
c2bbd468c4 | ||
|
|
46ab8f8d78 | ||
|
|
54321c5240 | ||
|
|
5fcbf2528f | ||
|
|
ea096db8e4 | ||
|
|
0caeb68680 | ||
|
|
2b9ba1d520 | ||
|
|
80f5ccd357 | ||
|
|
820169c5c6 | ||
|
|
aff75953e3 | ||
|
|
c0e09374a8 | ||
|
|
57976b4085 | ||
|
|
899f1a1844 | ||
|
|
41a1af863e | ||
|
|
778ec9b88f | ||
|
|
d42fcc3786 | ||
|
|
5d4f758c47 | ||
|
|
a8a17a223a | ||
|
|
aa40b04576 | ||
|
|
daac90c4e1 | ||
|
|
72b2c83392 | ||
|
|
c3410a3d91 | ||
|
|
173c1820e1 | ||
|
|
684f4ba1a6 | ||
|
|
6d84c5b9e3 | ||
|
|
4b522a2455 | ||
|
|
1e1c46ae1b | ||
|
|
d6b3acdb62 | ||
|
|
037d89a320 | ||
|
|
30eb3c4a99 | ||
|
|
0966d44c0f | ||
|
|
40e759c983 | ||
|
|
141ca6777c | ||
|
|
3c16a19269 | ||
|
|
b3c6d79f51 | ||
|
|
0c56b6d504 | ||
|
|
3d2da88da9 | ||
|
|
80c06d6b59 | ||
|
|
e536a638c9 | ||
|
|
bc397002d4 | ||
|
|
2a95d031ea | ||
|
|
1dce1eff48 | ||
|
|
5b1d8666b3 | ||
|
|
187a5b1908 | ||
|
|
7ab7941ddd | ||
|
|
c69d63c1f8 | ||
|
|
743b350fdd | ||
|
|
1ac610da1a | ||
|
|
bcf0fa073e | ||
|
|
140380716d | ||
|
|
143df87fee | ||
|
|
6d895843dc | ||
|
|
65e6d5475f | ||
|
|
15609cdbc7 | ||
|
|
f876c728ad | ||
|
|
f34462e3c3 | ||
|
|
ea0bf5e4c8 | ||
|
|
14d1b82f6b | ||
|
|
ed43ddd79d | ||
|
|
23192a3be7 | ||
|
|
72e3d464b8 | ||
|
|
a6985075b9 | ||
|
|
4d5494912d | ||
|
|
50982229e1 | ||
|
|
6977a4a18b | ||
|
|
ab1bf2ad44 | ||
|
|
c451f742aa | ||
|
|
034d89876d | ||
|
|
4a88ea5c03 | ||
|
|
95c6d41c35 | ||
|
|
2a9ed0abca | ||
|
|
3ff6b1bf64 | ||
|
|
a67276ccc2 | ||
|
|
87b51a6fd5 | ||
|
|
940836b25b | ||
|
|
634b723b5d | ||
|
|
af0c9b76c4 | ||
|
|
2142ef20c5 | ||
|
|
224ce5fe81 | ||
|
|
4bb9d07dde | ||
|
|
2054dfd83d | ||
|
|
6699f5c2cc | ||
|
|
70bde8b2bc | ||
|
|
ff73e5f53c | ||
|
|
0609188d3f | ||
|
|
99cd1ccfe5 | ||
|
|
dccc583b5d | ||
|
|
ac435b7890 | ||
|
|
37fc589896 | ||
|
|
5d789a01b7 | ||
|
|
ca0ff0d630 | ||
|
|
969b38586e | ||
|
|
e3eca424f1 | ||
|
|
a6355e298e | ||
|
|
c0f47a58f2 | ||
|
|
dc845fa2f4 | ||
|
|
7e855c83b3 | ||
|
|
3b8a9e0963 | ||
|
|
4445834fd3 | ||
|
|
19a619ff65 | ||
|
|
66a538dc9c | ||
|
|
1a6863f4b1 | ||
|
|
fbd9919afa | ||
|
|
eec8bc73f4 | ||
|
|
5720d40fee | ||
|
|
38e0cba675 | ||
|
|
4c5a0d663e | ||
|
|
093df15fac | ||
|
|
957430e675 | ||
|
|
14035f407e | ||
|
|
bf2b2525a9 | ||
|
|
4edb9cd6b9 | ||
|
|
c38d242bea | ||
|
|
c6ab6f94e7 | ||
|
|
36151d1ba9 | ||
|
|
1d5d184720 | ||
|
|
0119fd03a6 | ||
|
|
0a14297b48 | ||
|
|
442efa0607 | ||
|
|
6ad4cc317c | ||
|
|
57bec976ae | ||
|
|
641493e31a | ||
|
|
5b4e9ad982 | ||
|
|
950a5ad9ea | ||
|
|
fcfdd633f6 | ||
|
|
ebb18fa57d | ||
|
|
58b0ca585c | ||
|
|
5bc1c2de2d | ||
|
|
ec00613202 | ||
|
|
02ec3a5f48 | ||
|
|
ac3bae00fc | ||
|
|
e54828a7b8 | ||
|
|
f2acde789d | ||
|
|
9b49f63a97 | ||
|
|
14bcc6f2fc | ||
|
|
975a2f3632 | ||
|
|
5ff8f75917 | ||
|
|
db7e81e9d0 | ||
|
|
6a8039e76a | ||
|
|
56bf8364cd | ||
|
|
75750e3a79 | ||
|
|
bb5207ad77 | ||
|
|
b51d795e04 | ||
|
|
d12819932a | ||
|
|
d812c86812 | ||
|
|
1625cd5a9f | ||
|
|
756c3d0503 | ||
|
|
f884447b26 | ||
|
|
555394b95e | ||
|
|
00510a6af8 | ||
|
|
6c0839e197 | ||
|
|
5b79379c90 | ||
|
|
47fed45700 | ||
|
|
80d695f3a2 | ||
|
|
8d4f40ccd2 | ||
|
|
765bad5edd | ||
|
|
0c0382c9b5 | ||
|
|
bbab6149e8 | ||
|
|
ce9387f1ab | ||
|
|
d126c5736a | ||
|
|
5048d54d32 | ||
|
|
f22fe6af76 | ||
|
|
8034f289d1 | ||
|
|
eed61ac510 | ||
|
|
412d6096c0 | ||
|
|
c289ae07d2 | ||
|
|
87f78b07b3 | ||
|
|
5e2db77ef9 | ||
|
|
c992072286 | ||
|
|
0ef826c090 | ||
|
|
5da75c3915 | ||
|
|
8222baa7ed | ||
|
|
7b76b51314 | ||
|
|
c96dbbd3b5 | ||
|
|
da6ccedf24 | ||
|
|
13b37a835f | ||
|
|
863fa33309 | ||
|
|
9f4c54a212 | ||
|
|
2a7bff4c0e | ||
|
|
17406d1aab | ||
|
|
6537c53d43 | ||
|
|
b4bd10521a | ||
|
|
65cbef1962 | ||
|
|
a8d355900a | ||
|
|
ffd9c381ce | ||
|
|
2a0bce0beb | ||
|
|
f1f7b81088 | ||
|
|
f9827f958b | ||
|
|
3e2afc35ba | ||
|
|
c65dd86d5e | ||
|
|
2d6c0388af | ||
|
|
4d19d87720 | ||
|
|
5eabaf98e0 | ||
|
|
d1f0e9ae9f | ||
|
|
cd56039ab7 | ||
|
|
55515fee95 | ||
|
|
13d43a2d31 | ||
|
|
001261433b | ||
|
|
03bf595525 | ||
|
|
4ebedace1e | ||
|
|
b23276c002 | ||
|
|
bf708cb8bc | ||
|
|
a550d082a3 | ||
|
|
6c1a7449fe | ||
|
|
f0c9b55036 | ||
|
|
209badf10c | ||
|
|
242dde4480 | ||
|
|
2df0dd1f70 | ||
|
|
98a6d138d4 | ||
|
|
38f06ab373 | ||
|
|
3c1300721c | ||
|
|
61003c8079 | ||
|
|
01850c7399 | ||
|
|
b9c381e26f | ||
|
|
542554fb2c | ||
|
|
bdf18fa862 | ||
|
|
afc411c51b | ||
|
|
a59163e56c | ||
|
|
8391b19abb | ||
|
|
3925c7ff95 | ||
|
|
dbed110d02 | ||
|
|
f978355520 | ||
|
|
4748e6f54d | ||
|
|
91a4c64390 | ||
|
|
600a107699 | ||
|
|
2746c0b0f1 | ||
|
|
701a6115f8 | ||
|
|
56b00addc4 | ||
|
|
02e35ee002 | ||
|
|
5208e4a4ca | ||
|
|
7381a867ba | ||
|
|
f41ce6619f | ||
|
|
933427310d | ||
|
|
8b0a1817b3 | ||
|
|
04c9a5c008 | ||
|
|
bbc8c091e6 | ||
|
|
f3228713bc | ||
|
|
fa5333784b | ||
|
|
0dbf0cc81f | ||
|
|
196a56726e | ||
|
|
fe857dcb1b | ||
|
|
aa0ed5dbd0 | ||
|
|
a9e21c282a | ||
|
|
9a15a54885 | ||
|
|
91dcc349de | ||
|
|
fa41bfd06a | ||
|
|
8839c34d53 | ||
|
|
11ceaa8850 | ||
|
|
2a9f7db1e2 | ||
|
|
22338ed478 | ||
|
|
59a21158a6 | ||
|
|
93ce96d011 | ||
|
|
cc2f04b0e4 | ||
|
|
aa5191fa1b | ||
|
|
4e41a5583d | ||
|
|
ded8fad5e4 | ||
|
|
3702bc8413 | ||
|
|
7896d2eef7 | ||
|
|
da0f470f1c | ||
|
|
8fddb742df | ||
|
|
95fe26f3e3 | ||
|
|
1e10381143 | ||
|
|
96cbce52f9 | ||
|
|
0ea2ca3141 | ||
|
|
42877dd915 | ||
|
|
790c11c453 | ||
|
|
1ac4baa00a | ||
|
|
fc32286045 | ||
|
|
ee1131f254 | ||
|
|
c5dc3ee3b6 | ||
|
|
dd593b1035 | ||
|
|
4814786556 | ||
|
|
4f0a936ca0 | ||
|
|
aec372ca31 | ||
|
|
d2a739f8f6 | ||
|
|
165110872b | ||
|
|
6ab4e9f533 | ||
|
|
cf541d62ea | ||
|
|
19fc58dd1f | ||
|
|
ac9c475849 | ||
|
|
ddf99ab706 | ||
|
|
0056984d4b | ||
|
|
44fb276464 | ||
|
|
558a1b4050 | ||
|
|
8f934f2648 | ||
|
|
403b9a8310 | ||
|
|
33436488e2 | ||
|
|
3c28366fed | ||
|
|
b80abe6c05 | ||
|
|
8cb47817f6 | ||
|
|
23a80b01b6 | ||
|
|
b30614e9d8 | ||
|
|
e86089a9f3 | ||
|
|
3ceba7a147 | ||
|
|
c491133aff | ||
|
|
37418a7630 | ||
|
|
73a9c002e0 | ||
|
|
3d48fa7382 | ||
|
|
8e22dd1b13 | ||
|
|
7807fa7cc2 | ||
|
|
cd380973df | ||
|
|
01d681faa3 | ||
|
|
c231b663a3 | ||
|
|
8306b6bde6 | ||
|
|
dc011af90d | ||
|
|
c27e3ef436 | ||
|
|
760cc9ba5a | ||
|
|
5665e9c0e7 | ||
|
|
ad53429cf1 | ||
|
|
15298b0409 | ||
|
|
cfa710037c | ||
|
|
a08dd85efd | ||
|
|
469d36d979 | ||
|
|
7ae8b2cdeb | ||
|
|
cf148db75d | ||
|
|
738c7ab43e | ||
|
|
82fb7f8cf0 | ||
|
|
e0f2ab9c01 | ||
|
|
e0183217b6 | ||
|
|
f066b7fb9c | ||
|
|
0c6e2b566b | ||
|
|
f02e24437a | ||
|
|
e9534be1e6 | ||
|
|
7056997e49 | ||
|
|
155af19aaa | ||
|
|
f369fdf6f2 | ||
|
|
510a95bd6d | ||
|
|
da90dbe645 | ||
|
|
b006c5f914 | ||
|
|
3f75d46a16 | ||
|
|
14c2a244b7 | ||
|
|
94ff9d7346 | ||
|
|
14196167b0 | ||
|
|
d70959c34c | ||
|
|
67c6f27064 | ||
|
|
6bfbb27856 | ||
|
|
baac3749b3 | ||
|
|
d377181b25 | ||
|
|
ebd6a11f3a | ||
|
|
0a47412e8c | ||
|
|
4f668bf558 | ||
|
|
9248c5a987 | ||
|
|
b0ed190591 | ||
|
|
37357b2d63 | ||
|
|
9b06e0a3b7 | ||
|
|
5a5912ea66 | ||
|
|
b1c7317cf6 | ||
|
|
a0fe4cf5e4 | ||
|
|
7fe3c965e3 | ||
|
|
fd9b3c2767 | ||
|
|
fb9e188e36 | ||
|
|
c93d8cecfc | ||
|
|
94b46e57f1 | ||
|
|
9046acbe68 | ||
|
|
075bbe2aef | ||
|
|
b52d078986 | ||
|
|
0a9c4914aa | ||
|
|
f284008fb5 | ||
|
|
4759254e10 | ||
|
|
e22d377203 | ||
|
|
0787e3e595 | ||
|
|
c1194d558c | ||
|
|
952b10a9f6 | ||
|
|
f55851bdc8 | ||
|
|
76bb361393 | ||
|
|
81c8e8d898 | ||
|
|
f4e872c782 | ||
|
|
ddcb500c51 | ||
|
|
e8664c0ce4 | ||
|
|
3b002ddc86 | ||
|
|
1770da545d | ||
|
|
de3e69f846 | ||
|
|
cdf1233065 | ||
|
|
6b70f0b25f | ||
|
|
4fe51f7cce | ||
|
|
7221bf9b25 | ||
|
|
61f20f5449 | ||
|
|
5dafbb54de | ||
|
|
ec8ab28a22 | ||
|
|
aaa6d0c71f | ||
|
|
ea74aca165 | ||
|
|
9b119765fc | ||
|
|
df02496145 | ||
|
|
31d285813e | ||
|
|
f9fc5792fd | ||
|
|
6ad9c5ae64 | ||
|
|
016abda6d7 | ||
|
|
2eea6c95b9 | ||
|
|
7f1946de34 | ||
|
|
d0a81cabab | ||
|
|
df67f4ef34 | ||
|
|
ed11e1128a | ||
|
|
ed1c7cac17 | ||
|
|
7c115aa9c8 | ||
|
|
3ffddcba92 | ||
|
|
833b702b90 | ||
|
|
b885d453a2 | ||
|
|
7163fde724 | ||
|
|
830e3d1f64 | ||
|
|
637463a068 | ||
|
|
e69536f540 | ||
|
|
c516780289 | ||
|
|
eb1b869b73 | ||
|
|
703ccc8d91 | ||
|
|
45b9366dd0 | ||
|
|
382c6fda89 | ||
|
|
0f80de86b2 | ||
|
|
bc11cdd8d5 | ||
|
|
62b20cd884 | ||
|
|
ae8ed75ae5 | ||
|
|
c8cc81cf4a | ||
|
|
79c8b6cbc2 | ||
|
|
58253968fc | ||
|
|
dbd75209df | ||
|
|
da01848855 | ||
|
|
0b7be1175d | ||
|
|
3479a7e438 | ||
|
|
4f5c67e7df | ||
|
|
b99495ebbb | ||
|
|
0061978db8 | ||
|
|
e011f793ec | ||
|
|
5b47292366 | ||
|
|
8eff2df39c | ||
|
|
7bfe0ddf80 | ||
|
|
d6fa853a37 | ||
|
|
553cceab42 | ||
|
|
5bfaae9202 | ||
|
|
9359669cd4 | ||
|
|
8b039e0447 | ||
|
|
c64c080159 | ||
|
|
bcaf283c35 | ||
|
|
31a433cda2 | ||
|
|
e647f3626e | ||
|
|
3491717c26 | ||
|
|
45a13f5f4e | ||
|
|
6884ce3f3e | ||
|
|
5f4efc9148 | ||
|
|
d481294519 | ||
|
|
7ebbbe5c29 | ||
|
|
9ff303b8c0 | ||
|
|
4463421028 | ||
|
|
d75f56b914 | ||
|
|
a4834f4a83 | ||
|
|
0b42dbc3c3 | ||
|
|
2013ba497c | ||
|
|
2be37217cf | ||
|
|
55a03c2e2b | ||
|
|
27dbcf0066 | ||
|
|
ec194d73d2 | ||
|
|
1d9bb54073 | ||
|
|
5cf4a47b48 | ||
|
|
eec60d6309 | ||
|
|
37c602a477 | ||
|
|
8e604f888a | ||
|
|
531091906d | ||
|
|
e046b35b97 | ||
|
|
eb2ddc4798 | ||
|
|
aee9312cea | ||
|
|
6a99a5cb9f | ||
|
|
8e0ca88421 | ||
|
|
c7e55db4e0 | ||
|
|
1b1c740b29 | ||
|
|
ad5436e3f6 | ||
|
|
20606a0043 | ||
|
|
80e9902324 | ||
|
|
741e65a3a1 | ||
|
|
6aff8c2f5e | ||
|
|
e5770af2fa | ||
|
|
ae094d3479 | ||
|
|
f49c280a7f | ||
|
|
ae812e55af | ||
|
|
489101668c | ||
|
|
f8fd5c067c | ||
|
|
39fc9b73e9 | ||
|
|
80a5138115 | ||
|
|
bc844105b2 | ||
|
|
7de0fe467a | ||
|
|
0a025aabfd | ||
|
|
7a8b1645ef | ||
|
|
b3729e4666 | ||
|
|
9c4dbbc3f8 | ||
|
|
ca6cb34d98 | ||
|
|
fc442d7862 | ||
|
|
3ac5af47f2 | ||
|
|
bb37a1c1ce | ||
|
|
206799ff1c | ||
|
|
f3de83707f | ||
|
|
5be1d1be69 | ||
|
|
08e85f6be9 | ||
|
|
acdf351047 | ||
|
|
c2ff50c917 | ||
|
|
363b9ccc7f | ||
|
|
74367a65cc | ||
|
|
2221d3307a | ||
|
|
4ff34b3e53 | ||
|
|
191da1ecaf | ||
|
|
77db2bd3d1 | ||
|
|
758f2ee834 | ||
|
|
c5a6ca5650 | ||
|
|
b409523d5c | ||
|
|
8cd0aec417 | ||
|
|
a4cddf4f7f | ||
|
|
d970e757eb | ||
|
|
083d9f76c6 | ||
|
|
2003d08c59 | ||
|
|
9cf4653425 | ||
|
|
4f6bb00996 | ||
|
|
25f97bbe62 | ||
|
|
44d403cf9c | ||
|
|
8682ae4a54 | ||
|
|
f98a29a1e0 | ||
|
|
2932dba5cc | ||
|
|
24914e90d1 | ||
|
|
587c8a0347 | ||
|
|
62224debd2 | ||
|
|
871e2ccbbf | ||
|
|
4b5ce3913e | ||
|
|
1d917561fe | ||
|
|
4f36c1de06 | ||
|
|
5ffa34dacb | ||
|
|
60d4a3e1b5 | ||
|
|
3e0558c0e9 | ||
|
|
c74d90407b | ||
|
|
557831fa0d | ||
|
|
afd312f26a | ||
|
|
bb0338870a | ||
|
|
fb8ccfe824 | ||
|
|
0b4ff97a1a | ||
|
|
00dd5db226 | ||
|
|
71093bbe1b | ||
|
|
088e187e6a | ||
|
|
0d8506c93d | ||
|
|
df96ea7e9f | ||
|
|
580fae7a8f | ||
|
|
4a1baaee69 | ||
|
|
f3f38f5f09 | ||
|
|
bba78763e1 | ||
|
|
b5211fd31b | ||
|
|
360f32a91b | ||
|
|
018b10808d | ||
|
|
29aadbf3e3 | ||
|
|
7eb5b01169 | ||
|
|
fd59f35108 | ||
|
|
e7eb3476b7 | ||
|
|
bf00ca334d | ||
|
|
6b33d3d9f1 | ||
|
|
3d13fe15c3 | ||
|
|
9794ca7ee0 | ||
|
|
784f75f45b | ||
|
|
c1effdf701 | ||
|
|
7010f513e3 | ||
|
|
e2e122169d | ||
|
|
e79f7010b4 | ||
|
|
f4507aeec2 | ||
|
|
bd0eb2f0a8 | ||
|
|
e09ea00d8b | ||
|
|
bc5779e26f | ||
|
|
2a7e56ed29 | ||
|
|
29f69876fe | ||
|
|
4f1c2c094f | ||
|
|
abbb7f272b | ||
|
|
a7363a16be | ||
|
|
a26343a694 | ||
|
|
25260297aa | ||
|
|
bb589c6fc8 | ||
|
|
0737c672f5 | ||
|
|
53d70b68d8 | ||
|
|
795ffc4c5f | ||
|
|
e8db7d8539 | ||
|
|
c6343ee095 | ||
|
|
3441b75a58 | ||
|
|
cc2ccb3dd7 | ||
|
|
49a40f7a0b | ||
|
|
dc5eeb54fd |
31
.cirrus.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
freebsd_task:
|
||||
name: FreeBSD
|
||||
|
||||
matrix:
|
||||
- name: FreeBSD 14.0
|
||||
freebsd_instance:
|
||||
image_family: freebsd-14-0
|
||||
|
||||
pkginstall_script:
|
||||
- pkg update -f
|
||||
- pkg install -y go122
|
||||
- pkg install -y git
|
||||
|
||||
setup_script:
|
||||
- ln -s /usr/local/bin/go122 /usr/local/bin/go
|
||||
- pw groupadd sftpgo
|
||||
- pw useradd sftpgo -g sftpgo -w none -m
|
||||
- mkdir /home/sftpgo/sftpgo
|
||||
- cp -R . /home/sftpgo/sftpgo
|
||||
- chown -R sftpgo:sftpgo /home/sftpgo/sftpgo
|
||||
|
||||
compile_script:
|
||||
- su sftpgo -c 'cd ~/sftpgo && go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo'
|
||||
- su sftpgo -c 'cd ~/sftpgo/tests/eventsearcher && go build -trimpath -ldflags "-s -w" -o eventsearcher'
|
||||
- su sftpgo -c 'cd ~/sftpgo/tests/ipfilter && go build -trimpath -ldflags "-s -w" -o ipfilter'
|
||||
|
||||
check_script:
|
||||
- su sftpgo -c 'cd ~/sftpgo && ./sftpgo initprovider && ./sftpgo resetprovider --force'
|
||||
|
||||
test_script:
|
||||
- su sftpgo -c 'cd ~/sftpgo && go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 20m ./... -coverprofile=coverage.txt -covermode=atomic'
|
||||
12
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [drakkan] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
108
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
name: Open Source Bug Report
|
||||
description: "Submit a report and help us improve SFTPGo"
|
||||
title: "[Bug]: "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### 👍 Thank you for contributing to our project!
|
||||
Before asking for help please check the [support policy](https://github.com/drakkan/sftpgo#support-policy).
|
||||
If you are a commercial user or a project sponsor please contact us using the dedicated [email address](mailto:support@sftpgo.com).
|
||||
- type: checkboxes
|
||||
id: before-posting
|
||||
attributes:
|
||||
label: "⚠️ This issue respects the following points: ⚠️"
|
||||
description: All conditions are **required**.
|
||||
options:
|
||||
- label: This is a **bug**, not a question or a configuration issue.
|
||||
required: true
|
||||
- label: This issue is **not** already reported on Github _(I've searched it)_.
|
||||
required: true
|
||||
- type: textarea
|
||||
id: bug-description
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: |
|
||||
Provide a description of the bug you're experiencing.
|
||||
Don't just expect someone will guess what your specific problem is and provide full details.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: |
|
||||
Describe the steps to reproduce the bug.
|
||||
The better your description is the fastest you'll get an _(accurate)_ answer.
|
||||
value: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: Describe what you expected to happen instead.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: SFTPGo version
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: data-provider
|
||||
attributes:
|
||||
label: Data provider
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: install-method
|
||||
attributes:
|
||||
label: Installation method
|
||||
description: |
|
||||
Select installation method you've used.
|
||||
_Describe the method in the "Additional info" section if you chose "Other"._
|
||||
options:
|
||||
- "Community Docker image"
|
||||
- "Community Deb package"
|
||||
- "Community RPM package"
|
||||
- "Other"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Configuration
|
||||
description: "Describe your customizations to the configuration: both config file changes and overrides via environment variables"
|
||||
value: config
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
- type: dropdown
|
||||
id: usecase
|
||||
attributes:
|
||||
label: What are you using SFTPGo for?
|
||||
description: We'd like to understand your SFTPGo usecase more
|
||||
multiple: true
|
||||
options:
|
||||
- "Private user, home usecase (home backup/VPS)"
|
||||
- "Professional user, 1 person business"
|
||||
- "Small business (3-person firm with file exchange?)"
|
||||
- "Medium business"
|
||||
- "Enterprise"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-info
|
||||
attributes:
|
||||
label: Additional info
|
||||
description: Any additional information related to the issue.
|
||||
9
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Commercial Support
|
||||
url: https://sftpgo.com/
|
||||
about: >
|
||||
If you need Professional support, so your reports are prioritized and resolved more quickly.
|
||||
- name: GitHub Community Discussions
|
||||
url: https://github.com/drakkan/sftpgo/discussions
|
||||
about: Please ask and answer questions here.
|
||||
42
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: 🚀 Feature request
|
||||
description: Suggest an idea for SFTPGo
|
||||
labels: ["suggestion"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
description: A clear and concise description of what the problem is.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: usecase
|
||||
attributes:
|
||||
label: What are you using SFTPGo for?
|
||||
description: We'd like to understand your SFTPGo usecase more
|
||||
multiple: true
|
||||
options:
|
||||
- "Private user, home usecase (home backup/VPS)"
|
||||
- "Professional user, 1 person business"
|
||||
- "Small business (3-person firm with file exchange?)"
|
||||
- "Medium business"
|
||||
- "Enterprise"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
validations:
|
||||
required: false
|
||||
5
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Checklist for Pull Requests
|
||||
|
||||
- [ ] Have you signed the [Contributor License Agreement](https://sftpgo.com/cla.html)?
|
||||
|
||||
---
|
||||
20
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
#- package-ecosystem: "gomod"
|
||||
# directory: "/"
|
||||
# schedule:
|
||||
# interval: "weekly"
|
||||
# open-pull-requests-limit: 2
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 2
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 2
|
||||
2
.github/workflows/.editorconfig
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
[*.yml]
|
||||
indent_size = 2
|
||||
36
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: "Code scanning - action"
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '30 1 * * 6'
|
||||
|
||||
jobs:
|
||||
CodeQL-Build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: go
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
526
.github/workflows/development.yml
vendored
Normal file
@@ -0,0 +1,526 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [2.6.x]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test-deploy:
|
||||
name: Test and deploy
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go: ['1.22']
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
upload-coverage: [true]
|
||||
include:
|
||||
- go: '1.22'
|
||||
os: windows-latest
|
||||
upload-coverage: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Build for Linux/macOS x86_64
|
||||
if: startsWith(matrix.os, 'windows-') != true
|
||||
run: |
|
||||
go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
|
||||
cd tests/eventsearcher
|
||||
go build -trimpath -ldflags "-s -w" -o eventsearcher
|
||||
cd -
|
||||
cd tests/ipfilter
|
||||
go build -trimpath -ldflags "-s -w" -o ipfilter
|
||||
cd -
|
||||
./sftpgo initprovider
|
||||
./sftpgo resetprovider --force
|
||||
|
||||
- name: Build for macOS arm64
|
||||
if: startsWith(matrix.os, 'macos-') == true
|
||||
run: CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 SDKROOT=$(xcrun --sdk macosx --show-sdk-path) go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo_arm64
|
||||
|
||||
- name: Build for Windows
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
run: |
|
||||
$GIT_COMMIT = (git describe --always --abbrev=8 --dirty) | Out-String
|
||||
$DATE_TIME = ([datetime]::Now.ToUniversalTime().toString("yyyy-MM-ddTHH:mm:ssZ")) | Out-String
|
||||
$LATEST_TAG = ((git describe --tags $(git rev-list --tags --max-count=1)) | Out-String).Trim()
|
||||
$REV_LIST=$LATEST_TAG+"..HEAD"
|
||||
$COMMITS_FROM_TAG= ((git rev-list $REV_LIST --count) | Out-String).Trim()
|
||||
$FILE_VERSION = $LATEST_TAG.substring(1) + "." + $COMMITS_FROM_TAG
|
||||
go install github.com/tc-hib/go-winres@latest
|
||||
go-winres simply --arch amd64 --product-version $LATEST_TAG-dev-$GIT_COMMIT --file-version $FILE_VERSION --file-description "SFTPGo server" --product-name SFTPGo --copyright "AGPL-3.0" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
|
||||
go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o sftpgo.exe
|
||||
cd tests/eventsearcher
|
||||
go build -trimpath -ldflags "-s -w" -o eventsearcher.exe
|
||||
cd ../..
|
||||
cd tests/ipfilter
|
||||
go build -trimpath -ldflags "-s -w" -o ipfilter.exe
|
||||
cd ../..
|
||||
mkdir arm64
|
||||
$Env:CGO_ENABLED='0'
|
||||
$Env:GOOS='windows'
|
||||
$Env:GOARCH='arm64'
|
||||
go-winres simply --arch arm64 --product-version $LATEST_TAG-dev-$GIT_COMMIT --file-version $FILE_VERSION --file-description "SFTPGo server" --product-name SFTPGo --copyright "AGPL-3.0" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
|
||||
go build -trimpath -tags nopgxregisterdefaulttypes,nosqlite -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o .\arm64\sftpgo.exe
|
||||
mkdir x86
|
||||
$Env:GOARCH='386'
|
||||
go-winres simply --arch 386 --product-version $LATEST_TAG-dev-$GIT_COMMIT --file-version $FILE_VERSION --file-description "SFTPGo server" --product-name SFTPGo --copyright "AGPL-3.0" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
|
||||
go build -trimpath -tags nopgxregisterdefaulttypes,nosqlite -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o .\x86\sftpgo.exe
|
||||
Remove-Item Env:\CGO_ENABLED
|
||||
Remove-Item Env:\GOOS
|
||||
Remove-Item Env:\GOARCH
|
||||
|
||||
- name: Run test cases using SQLite provider
|
||||
run: go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -coverprofile=coverage.txt -covermode=atomic
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
if: ${{ matrix.upload-coverage }}
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: ./coverage.txt
|
||||
fail_ci_if_error: false
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: Run test cases using bolt provider
|
||||
run: |
|
||||
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 2m ./internal/config -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 5m ./internal/common -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 5m ./internal/httpd -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 8m ./internal/sftpd -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 5m ./internal/ftpd -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 5m ./internal/webdavd -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 2m ./internal/telemetry -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 2m ./internal/mfa -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 2m ./internal/command -covermode=atomic
|
||||
env:
|
||||
SFTPGO_DATA_PROVIDER__DRIVER: bolt
|
||||
SFTPGO_DATA_PROVIDER__NAME: 'sftpgo_bolt.db'
|
||||
|
||||
- name: Run test cases using memory provider
|
||||
run: go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -covermode=atomic
|
||||
env:
|
||||
SFTPGO_DATA_PROVIDER__DRIVER: memory
|
||||
SFTPGO_DATA_PROVIDER__NAME: ''
|
||||
|
||||
- name: Prepare build artifact for macOS
|
||||
if: startsWith(matrix.os, 'macos-') == true
|
||||
run: |
|
||||
mkdir -p output/{init,bash_completion,zsh_completion}
|
||||
cp sftpgo output/sftpgo_x86_64
|
||||
cp sftpgo_arm64 output/
|
||||
cp sftpgo.json output/
|
||||
cp -r templates output/
|
||||
cp -r static output/
|
||||
cp -r openapi output/
|
||||
cp init/com.github.drakkan.sftpgo.plist output/init/
|
||||
./sftpgo gen completion bash > output/bash_completion/sftpgo
|
||||
./sftpgo gen completion zsh > output/zsh_completion/_sftpgo
|
||||
./sftpgo gen man -d output/man/man1
|
||||
gzip output/man/man1/*
|
||||
|
||||
- name: Prepare Windows installer
|
||||
if: ${{ startsWith(matrix.os, 'windows-') && github.event_name != 'pull_request' }}
|
||||
run: |
|
||||
Remove-Item -LiteralPath "output" -Force -Recurse -ErrorAction Ignore
|
||||
mkdir output
|
||||
copy .\sftpgo.exe .\output
|
||||
copy .\sftpgo.json .\output
|
||||
copy .\sftpgo.db .\output
|
||||
copy .\LICENSE .\output\LICENSE.txt
|
||||
mkdir output\templates
|
||||
xcopy .\templates .\output\templates\ /E
|
||||
mkdir output\static
|
||||
xcopy .\static .\output\static\ /E
|
||||
mkdir output\openapi
|
||||
xcopy .\openapi .\output\openapi\ /E
|
||||
$LATEST_TAG = ((git describe --tags $(git rev-list --tags --max-count=1)) | Out-String).Trim()
|
||||
$REV_LIST=$LATEST_TAG+"..HEAD"
|
||||
$COMMITS_FROM_TAG= ((git rev-list $REV_LIST --count) | Out-String).Trim()
|
||||
$Env:SFTPGO_ISS_DEV_VERSION = $LATEST_TAG + "." + $COMMITS_FROM_TAG
|
||||
$CERT_PATH=(Get-Location -PSProvider FileSystem).ProviderPath + "\cert.pfx"
|
||||
[IO.File]::WriteAllBytes($CERT_PATH,[System.Convert]::FromBase64String($Env:CERT_DATA))
|
||||
certutil -f -p "$Env:CERT_PASS" -importpfx MY "$CERT_PATH"
|
||||
rm "$CERT_PATH"
|
||||
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.20348.0/x86/signtool.exe' sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n "Nicola Murino" /d "SFTPGo" .\sftpgo.exe
|
||||
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.20348.0/x86/signtool.exe' sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n "Nicola Murino" /d "SFTPGo" .\arm64\sftpgo.exe
|
||||
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.20348.0/x86/signtool.exe' sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n "Nicola Murino" /d "SFTPGo" .\x86\sftpgo.exe
|
||||
$INNO_S='/Ssigntool=$qC:/Program Files (x86)/Windows Kits/10/bin/10.0.20348.0/x86/signtool.exe$q sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n $qNicola Murino$q /d $qSFTPGo$q $f'
|
||||
iscc "$INNO_S" .\windows-installer\sftpgo.iss
|
||||
|
||||
rm .\output\sftpgo.exe
|
||||
rm .\output\sftpgo.db
|
||||
copy .\arm64\sftpgo.exe .\output
|
||||
(Get-Content .\output\sftpgo.json).replace('"sqlite"', '"bolt"') | Set-Content .\output\sftpgo.json
|
||||
$Env:SFTPGO_DATA_PROVIDER__DRIVER='bolt'
|
||||
$Env:SFTPGO_DATA_PROVIDER__NAME='.\output\sftpgo.db'
|
||||
.\sftpgo.exe initprovider
|
||||
Remove-Item Env:\SFTPGO_DATA_PROVIDER__DRIVER
|
||||
Remove-Item Env:\SFTPGO_DATA_PROVIDER__NAME
|
||||
$Env:SFTPGO_ISS_ARCH='arm64'
|
||||
iscc "$INNO_S" .\windows-installer\sftpgo.iss
|
||||
|
||||
rm .\output\sftpgo.exe
|
||||
copy .\x86\sftpgo.exe .\output
|
||||
$Env:SFTPGO_ISS_ARCH='x86'
|
||||
iscc "$INNO_S" .\windows-installer\sftpgo.iss
|
||||
certutil -delstore MY "Nicola Murino"
|
||||
env:
|
||||
CERT_DATA: ${{ secrets.CERT_DATA }}
|
||||
CERT_PASS: ${{ secrets.CERT_PASS }}
|
||||
|
||||
- name: Upload Windows installer x86_64 artifact
|
||||
if: ${{ startsWith(matrix.os, 'windows-') && github.event_name != 'pull_request' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sftpgo_windows_installer_x86_64
|
||||
path: ./sftpgo_windows_x86_64.exe
|
||||
|
||||
- name: Upload Windows installer arm64 artifact
|
||||
if: ${{ startsWith(matrix.os, 'windows-') && github.event_name != 'pull_request' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sftpgo_windows_installer_arm64
|
||||
path: ./sftpgo_windows_arm64.exe
|
||||
|
||||
- name: Upload Windows installer x86 artifact
|
||||
if: ${{ startsWith(matrix.os, 'windows-') && github.event_name != 'pull_request' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sftpgo_windows_installer_x86
|
||||
path: ./sftpgo_windows_x86.exe
|
||||
|
||||
- name: Prepare build artifact for Windows
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
run: |
|
||||
Remove-Item -LiteralPath "output" -Force -Recurse -ErrorAction Ignore
|
||||
mkdir output
|
||||
copy .\sftpgo.exe .\output
|
||||
mkdir output\arm64
|
||||
copy .\arm64\sftpgo.exe .\output\arm64
|
||||
mkdir output\x86
|
||||
copy .\x86\sftpgo.exe .\output\x86
|
||||
copy .\sftpgo.json .\output
|
||||
(Get-Content .\output\sftpgo.json).replace('"sqlite"', '"bolt"') | Set-Content .\output\sftpgo.json
|
||||
mkdir output\templates
|
||||
xcopy .\templates .\output\templates\ /E
|
||||
mkdir output\static
|
||||
xcopy .\static .\output\static\ /E
|
||||
mkdir output\openapi
|
||||
xcopy .\openapi .\output\openapi\ /E
|
||||
|
||||
- name: Upload build artifact
|
||||
if: startsWith(matrix.os, 'ubuntu-') != true
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sftpgo-${{ matrix.os }}-go-${{ matrix.go }}
|
||||
path: output
|
||||
|
||||
test-build-flags:
|
||||
name: Test build flags
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
go build -trimpath -tags nopgxregisterdefaulttypes,nogcs,nos3,noportable,nobolt,nomysql,nopgsql,nosqlite,nometrics,noazblob,unixcrypt -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
|
||||
./sftpgo -v
|
||||
cp -r openapi static templates internal/bundle/
|
||||
go build -trimpath -tags nopgxregisterdefaulttypes,bundle -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
|
||||
./sftpgo -v
|
||||
|
||||
test-postgresql-mysql-crdb:
|
||||
name: Test with PgSQL/MySQL/Cockroach
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: sftpgo
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
mariadb:
|
||||
image: mariadb:latest
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: mysql
|
||||
MYSQL_DATABASE: sftpgo
|
||||
MYSQL_USER: sftpgo
|
||||
MYSQL_PASSWORD: sftpgo
|
||||
options: >-
|
||||
--health-cmd "mariadb-admin status -h 127.0.0.1 -P 3306 -u root -p$MYSQL_ROOT_PASSWORD"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 6
|
||||
ports:
|
||||
- 3307:3306
|
||||
|
||||
mysql:
|
||||
image: mysql:latest
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: mysql
|
||||
MYSQL_DATABASE: sftpgo
|
||||
MYSQL_USER: sftpgo
|
||||
MYSQL_PASSWORD: sftpgo
|
||||
options: >-
|
||||
--health-cmd "mysqladmin status -h 127.0.0.1 -P 3306 -u root -p$MYSQL_ROOT_PASSWORD"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 6
|
||||
ports:
|
||||
- 3308:3306
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
|
||||
cd tests/eventsearcher
|
||||
go build -trimpath -ldflags "-s -w" -o eventsearcher
|
||||
cd -
|
||||
cd tests/ipfilter
|
||||
go build -trimpath -ldflags "-s -w" -o ipfilter
|
||||
cd -
|
||||
|
||||
- name: Run tests using MySQL provider
|
||||
run: |
|
||||
./sftpgo initprovider
|
||||
./sftpgo resetprovider --force
|
||||
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -covermode=atomic
|
||||
env:
|
||||
SFTPGO_DATA_PROVIDER__DRIVER: mysql
|
||||
SFTPGO_DATA_PROVIDER__NAME: sftpgo
|
||||
SFTPGO_DATA_PROVIDER__HOST: localhost
|
||||
SFTPGO_DATA_PROVIDER__PORT: 3308
|
||||
SFTPGO_DATA_PROVIDER__USERNAME: sftpgo
|
||||
SFTPGO_DATA_PROVIDER__PASSWORD: sftpgo
|
||||
|
||||
- name: Run tests using PostgreSQL provider
|
||||
run: |
|
||||
./sftpgo initprovider
|
||||
./sftpgo resetprovider --force
|
||||
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -covermode=atomic
|
||||
env:
|
||||
SFTPGO_DATA_PROVIDER__DRIVER: postgresql
|
||||
SFTPGO_DATA_PROVIDER__NAME: sftpgo
|
||||
SFTPGO_DATA_PROVIDER__HOST: localhost
|
||||
SFTPGO_DATA_PROVIDER__PORT: 5432
|
||||
SFTPGO_DATA_PROVIDER__USERNAME: postgres
|
||||
SFTPGO_DATA_PROVIDER__PASSWORD: postgres
|
||||
|
||||
- name: Run tests using MariaDB provider
|
||||
run: |
|
||||
./sftpgo initprovider
|
||||
./sftpgo resetprovider --force
|
||||
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -covermode=atomic
|
||||
env:
|
||||
SFTPGO_DATA_PROVIDER__DRIVER: mysql
|
||||
SFTPGO_DATA_PROVIDER__NAME: sftpgo
|
||||
SFTPGO_DATA_PROVIDER__HOST: localhost
|
||||
SFTPGO_DATA_PROVIDER__PORT: 3307
|
||||
SFTPGO_DATA_PROVIDER__USERNAME: sftpgo
|
||||
SFTPGO_DATA_PROVIDER__PASSWORD: sftpgo
|
||||
SFTPGO_DATA_PROVIDER__SQL_TABLES_PREFIX: prefix_
|
||||
|
||||
- name: Run tests using CockroachDB provider
|
||||
run: |
|
||||
docker run --rm --name crdb --health-cmd "curl -I http://127.0.0.1:8080" --health-interval 10s --health-timeout 5s --health-retries 6 -p 26257:26257 -d cockroachdb/cockroach:latest start-single-node --insecure --listen-addr :26257
|
||||
sleep 10
|
||||
docker exec crdb cockroach sql --insecure -e 'create database "sftpgo"'
|
||||
./sftpgo initprovider
|
||||
./sftpgo resetprovider --force
|
||||
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -covermode=atomic
|
||||
docker stop crdb
|
||||
env:
|
||||
SFTPGO_DATA_PROVIDER__DRIVER: cockroachdb
|
||||
SFTPGO_DATA_PROVIDER__NAME: sftpgo
|
||||
SFTPGO_DATA_PROVIDER__HOST: localhost
|
||||
SFTPGO_DATA_PROVIDER__PORT: 26257
|
||||
SFTPGO_DATA_PROVIDER__USERNAME: root
|
||||
SFTPGO_DATA_PROVIDER__PASSWORD:
|
||||
SFTPGO_DATA_PROVIDER__TARGET_SESSION_ATTRS: any
|
||||
SFTPGO_DATA_PROVIDER__SQL_TABLES_PREFIX: prefix_
|
||||
|
||||
build-linux-packages:
|
||||
name: Build Linux packages
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
distro: ubuntu:18.04
|
||||
go: latest
|
||||
go-arch: amd64
|
||||
- arch: aarch64
|
||||
distro: ubuntu18.04
|
||||
go: latest
|
||||
go-arch: arm64
|
||||
- arch: ppc64le
|
||||
distro: ubuntu18.04
|
||||
go: latest
|
||||
go-arch: ppc64le
|
||||
- arch: armv7
|
||||
distro: ubuntu18.04
|
||||
go: latest
|
||||
go-arch: arm7
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get commit SHA
|
||||
id: get_commit
|
||||
run: echo "COMMIT=${GITHUB_SHA::8}" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Build on amd64
|
||||
if: ${{ matrix.arch == 'amd64' }}
|
||||
run: |
|
||||
echo '#!/bin/bash' > build.sh
|
||||
echo '' >> build.sh
|
||||
echo 'set -e' >> build.sh
|
||||
echo 'apt-get update -q -y' >> build.sh
|
||||
echo 'apt-get install -q -y curl gcc' >> build.sh
|
||||
if [ ${{ matrix.go }} == 'latest' ]
|
||||
then
|
||||
echo 'GO_VERSION=$(curl -L https://go.dev/VERSION?m=text | head -n 1)' >> build.sh
|
||||
else
|
||||
echo 'GO_VERSION=${{ matrix.go }}' >> build.sh
|
||||
fi
|
||||
echo 'GO_DOWNLOAD_ARCH=${{ matrix.go-arch }}' >> build.sh
|
||||
echo 'curl --retry 5 --retry-delay 2 --connect-timeout 10 -o go.tar.gz -L https://go.dev/dl/${GO_VERSION}.linux-${GO_DOWNLOAD_ARCH}.tar.gz' >> build.sh
|
||||
echo 'tar -C /usr/local -xzf go.tar.gz' >> build.sh
|
||||
echo 'export PATH=$PATH:/usr/local/go/bin' >> build.sh
|
||||
echo 'go version' >> build.sh
|
||||
echo 'cd /usr/local/src' >> build.sh
|
||||
echo 'go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${{ steps.get_commit.outputs.COMMIT }} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo' >> build.sh
|
||||
|
||||
chmod 755 build.sh
|
||||
docker run --rm --name ubuntu-build --mount type=bind,source=`pwd`,target=/usr/local/src ${{ matrix.distro }} /usr/local/src/build.sh
|
||||
mkdir -p output/{init,bash_completion,zsh_completion}
|
||||
cp sftpgo.json output/
|
||||
cp -r templates output/
|
||||
cp -r static output/
|
||||
cp -r openapi output/
|
||||
cp init/sftpgo.service output/init/
|
||||
./sftpgo gen completion bash > output/bash_completion/sftpgo
|
||||
./sftpgo gen completion zsh > output/zsh_completion/_sftpgo
|
||||
./sftpgo gen man -d output/man/man1
|
||||
gzip output/man/man1/*
|
||||
cp sftpgo output/
|
||||
|
||||
- uses: uraimo/run-on-arch-action@v2
|
||||
if: ${{ matrix.arch != 'amd64' }}
|
||||
name: Build for ${{ matrix.arch }}
|
||||
id: build
|
||||
with:
|
||||
arch: ${{ matrix.arch }}
|
||||
distro: ${{ matrix.distro }}
|
||||
setup: |
|
||||
mkdir -p "${PWD}/output"
|
||||
dockerRunArgs: |
|
||||
--volume "${PWD}/output:/output"
|
||||
shell: /bin/bash
|
||||
install: |
|
||||
apt-get update -q -y
|
||||
apt-get install -q -y curl gcc
|
||||
if [ ${{ matrix.go }} == 'latest' ]
|
||||
then
|
||||
GO_VERSION=$(curl -L https://go.dev/VERSION?m=text | head -n 1)
|
||||
else
|
||||
GO_VERSION=${{ matrix.go }}
|
||||
fi
|
||||
GO_DOWNLOAD_ARCH=${{ matrix.go-arch }}
|
||||
if [ ${{ matrix.arch}} == 'armv7' ]
|
||||
then
|
||||
GO_DOWNLOAD_ARCH=armv6l
|
||||
fi
|
||||
curl --retry 5 --retry-delay 2 --connect-timeout 10 -o go.tar.gz -L https://go.dev/dl/${GO_VERSION}.linux-${GO_DOWNLOAD_ARCH}.tar.gz
|
||||
tar -C /usr/local -xzf go.tar.gz
|
||||
run: |
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
go version
|
||||
if [ ${{ matrix.arch}} == 'armv7' ]
|
||||
then
|
||||
export GOARM=7
|
||||
fi
|
||||
go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${{ steps.get_commit.outputs.COMMIT }} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
|
||||
mkdir -p output/{init,bash_completion,zsh_completion}
|
||||
cp sftpgo.json output/
|
||||
cp -r templates output/
|
||||
cp -r static output/
|
||||
cp -r openapi output/
|
||||
cp init/sftpgo.service output/init/
|
||||
./sftpgo gen completion bash > output/bash_completion/sftpgo
|
||||
./sftpgo gen completion zsh > output/zsh_completion/_sftpgo
|
||||
./sftpgo gen man -d output/man/man1
|
||||
gzip output/man/man1/*
|
||||
cp sftpgo output/
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sftpgo-linux-${{ matrix.arch }}-go-${{ matrix.go }}
|
||||
path: output
|
||||
|
||||
- name: Build Packages
|
||||
id: build_linux_pkgs
|
||||
run: |
|
||||
export NFPM_ARCH=${{ matrix.go-arch }}
|
||||
cd pkgs
|
||||
./build.sh
|
||||
PKG_VERSION=$(cat dist/version)
|
||||
echo "pkg-version=${PKG_VERSION}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload Debian Package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-${{ matrix.go-arch }}-deb
|
||||
path: pkgs/dist/deb/*
|
||||
|
||||
- name: Upload RPM Package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-${{ matrix.go-arch }}-rpm
|
||||
path: pkgs/dist/rpm/*
|
||||
|
||||
golangci-lint:
|
||||
name: golangci-lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: latest
|
||||
188
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,188 @@
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
#schedule:
|
||||
# - cron: '0 4 * * *' # everyday at 4:00 AM UTC
|
||||
push:
|
||||
branches:
|
||||
- 2.6.x
|
||||
tags:
|
||||
- v*
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
docker_pkg:
|
||||
- debian
|
||||
- alpine
|
||||
optional_deps:
|
||||
- true
|
||||
- false
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
docker_pkg: distroless
|
||||
optional_deps: false
|
||||
- os: ubuntu-latest
|
||||
docker_pkg: debian-plugins
|
||||
optional_deps: true
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Gather image information
|
||||
id: info
|
||||
run: |
|
||||
VERSION=noop
|
||||
DOCKERFILE=Dockerfile
|
||||
MINOR=""
|
||||
MAJOR=""
|
||||
FEATURES="nopgxregisterdefaulttypes"
|
||||
if [ "${{ github.event_name }}" = "schedule" ]; then
|
||||
VERSION=nightly
|
||||
elif [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
elif [[ $GITHUB_REF == refs/heads/* ]]; then
|
||||
VERSION=$(echo ${GITHUB_REF#refs/heads/} | sed -r 's#/+#-#g')
|
||||
if [ "${{ github.event.repository.default_branch }}" = "$VERSION" ]; then
|
||||
VERSION=edge
|
||||
fi
|
||||
elif [[ $GITHUB_REF == refs/pull/* ]]; then
|
||||
VERSION=pr-${{ github.event.number }}
|
||||
fi
|
||||
if [[ $VERSION =~ ^v[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
|
||||
MINOR=${VERSION%.*}
|
||||
MAJOR=${MINOR%.*}
|
||||
fi
|
||||
VERSION_SLIM="${VERSION}-slim"
|
||||
if [[ $DOCKER_PKG == alpine ]]; then
|
||||
VERSION="${VERSION}-alpine"
|
||||
VERSION_SLIM="${VERSION}-slim"
|
||||
DOCKERFILE=Dockerfile.alpine
|
||||
elif [[ $DOCKER_PKG == distroless ]]; then
|
||||
VERSION="${VERSION}-distroless"
|
||||
VERSION_SLIM="${VERSION}-slim"
|
||||
DOCKERFILE=Dockerfile.distroless
|
||||
FEATURES="${FEATURES},nosqlite"
|
||||
elif [[ $DOCKER_PKG == debian-plugins ]]; then
|
||||
VERSION="${VERSION}-plugins"
|
||||
VERSION_SLIM="${VERSION}-slim"
|
||||
FEATURES="${FEATURES},unixcrypt"
|
||||
elif [[ $DOCKER_PKG == debian ]]; then
|
||||
FEATURES="${FEATURES},unixcrypt"
|
||||
fi
|
||||
DOCKER_IMAGES=("drakkan/sftpgo" "ghcr.io/drakkan/sftpgo")
|
||||
TAGS="${DOCKER_IMAGES[0]}:${VERSION}"
|
||||
TAGS_SLIM="${DOCKER_IMAGES[0]}:${VERSION_SLIM}"
|
||||
|
||||
for DOCKER_IMAGE in ${DOCKER_IMAGES[@]}; do
|
||||
if [[ ${DOCKER_IMAGE} != ${DOCKER_IMAGES[0]} ]]; then
|
||||
TAGS="${TAGS},${DOCKER_IMAGE}:${VERSION}"
|
||||
TAGS_SLIM="${TAGS_SLIM},${DOCKER_IMAGE}:${VERSION_SLIM}"
|
||||
fi
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
if [[ $DOCKER_PKG == debian ]]; then
|
||||
if [[ -n $MAJOR && -n $MINOR ]]; then
|
||||
TAGS="${TAGS},${DOCKER_IMAGE}:${MINOR},${DOCKER_IMAGE}:${MAJOR}"
|
||||
TAGS_SLIM="${TAGS_SLIM},${DOCKER_IMAGE}:${MINOR}-slim,${DOCKER_IMAGE}:${MAJOR}-slim"
|
||||
fi
|
||||
TAGS="${TAGS},${DOCKER_IMAGE}:latest"
|
||||
TAGS_SLIM="${TAGS_SLIM},${DOCKER_IMAGE}:slim"
|
||||
elif [[ $DOCKER_PKG == distroless ]]; then
|
||||
if [[ -n $MAJOR && -n $MINOR ]]; then
|
||||
TAGS="${TAGS},${DOCKER_IMAGE}:${MINOR}-distroless,${DOCKER_IMAGE}:${MAJOR}-distroless"
|
||||
TAGS_SLIM="${TAGS_SLIM},${DOCKER_IMAGE}:${MINOR}-distroless-slim,${DOCKER_IMAGE}:${MAJOR}-distroless-slim"
|
||||
fi
|
||||
TAGS="${TAGS},${DOCKER_IMAGE}:distroless"
|
||||
TAGS_SLIM="${TAGS_SLIM},${DOCKER_IMAGE}:distroless-slim"
|
||||
elif [[ $DOCKER_PKG == debian-plugins ]]; then
|
||||
if [[ -n $MAJOR && -n $MINOR ]]; then
|
||||
TAGS="${TAGS},${DOCKER_IMAGE}:${MINOR}-plugins,${DOCKER_IMAGE}:${MAJOR}-plugins"
|
||||
TAGS_SLIM="${TAGS_SLIM},${DOCKER_IMAGE}:${MINOR}-plugins-slim,${DOCKER_IMAGE}:${MAJOR}-plugins-slim"
|
||||
fi
|
||||
TAGS="${TAGS},${DOCKER_IMAGE}:plugins"
|
||||
TAGS_SLIM="${TAGS_SLIM},${DOCKER_IMAGE}:plugins-slim"
|
||||
else
|
||||
if [[ -n $MAJOR && -n $MINOR ]]; then
|
||||
TAGS="${TAGS},${DOCKER_IMAGE}:${MINOR}-alpine,${DOCKER_IMAGE}:${MAJOR}-alpine"
|
||||
TAGS_SLIM="${TAGS_SLIM},${DOCKER_IMAGE}:${MINOR}-alpine-slim,${DOCKER_IMAGE}:${MAJOR}-alpine-slim"
|
||||
fi
|
||||
TAGS="${TAGS},${DOCKER_IMAGE}:alpine"
|
||||
TAGS_SLIM="${TAGS_SLIM},${DOCKER_IMAGE}:alpine-slim"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $OPTIONAL_DEPS == true ]]; then
|
||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
|
||||
echo "full=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "version=${VERSION_SLIM}" >> $GITHUB_OUTPUT
|
||||
echo "tags=${TAGS_SLIM}" >> $GITHUB_OUTPUT
|
||||
echo "full=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
if [[ $DOCKER_PKG == debian-plugins ]]; then
|
||||
echo "plugins=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "plugins=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
echo "dockerfile=${DOCKERFILE}" >> $GITHUB_OUTPUT
|
||||
echo "features=${FEATURES}" >> $GITHUB_OUTPUT
|
||||
echo "created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
|
||||
echo "sha=${GITHUB_SHA::8}" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
DOCKER_PKG: ${{ matrix.docker_pkg }}
|
||||
OPTIONAL_DEPS: ${{ matrix.optional_deps }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up builder
|
||||
uses: docker/setup-buildx-action@v3
|
||||
id: builder
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
builder: ${{ steps.builder.outputs.name }}
|
||||
file: ./${{ steps.info.outputs.dockerfile }}
|
||||
platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/arm/v7
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.info.outputs.tags }}
|
||||
build-args: |
|
||||
COMMIT_SHA=${{ steps.info.outputs.sha }}
|
||||
INSTALL_OPTIONAL_PACKAGES=${{ steps.info.outputs.full }}
|
||||
DOWNLOAD_PLUGINS=${{ steps.info.outputs.plugins }}
|
||||
FEATURES=${{ steps.info.outputs.features }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=SFTPGo
|
||||
org.opencontainers.image.description=Full-featured and highly configurable file transfer server: SFTP, HTTP/S,FTP/S, WebDAV
|
||||
org.opencontainers.image.url=https://github.com/drakkan/sftpgo
|
||||
org.opencontainers.image.documentation=https://github.com/drakkan/sftpgo/blob/${{ github.sha }}/docker/README.md
|
||||
org.opencontainers.image.source=https://github.com/drakkan/sftpgo
|
||||
org.opencontainers.image.version=${{ steps.info.outputs.version }}
|
||||
org.opencontainers.image.created=${{ steps.info.outputs.created }}
|
||||
org.opencontainers.image.revision=${{ github.sha }}
|
||||
org.opencontainers.image.licenses=AGPL-3.0-only
|
||||
607
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,607 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: 'v*'
|
||||
|
||||
env:
|
||||
GO_VERSION: 1.22.4
|
||||
|
||||
jobs:
|
||||
prepare-sources-with-deps:
|
||||
name: Prepare sources with deps
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Get SFTPGo version
|
||||
id: get_version
|
||||
run: echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Prepare release
|
||||
run: |
|
||||
go mod vendor
|
||||
echo "${SFTPGO_VERSION}" > VERSION.txt
|
||||
echo "${GITHUB_SHA::8}" >> VERSION.txt
|
||||
tar cJvf sftpgo_${SFTPGO_VERSION}_src_with_deps.tar.xz *
|
||||
env:
|
||||
SFTPGO_VERSION: ${{ steps.get_version.outputs.VERSION }}
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_src_with_deps.tar.xz
|
||||
path: ./sftpgo_${{ steps.get_version.outputs.VERSION }}_src_with_deps.tar.xz
|
||||
retention-days: 1
|
||||
|
||||
prepare-window-mac:
|
||||
name: Prepare binaries
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-12, windows-2022]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Get SFTPGo version
|
||||
id: get_version
|
||||
run: echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Get OS name
|
||||
id: get_os_name
|
||||
run: |
|
||||
if [[ $MATRIX_OS =~ ^macos.* ]]
|
||||
then
|
||||
echo "OS=macOS" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "OS=windows" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
shell: bash
|
||||
env:
|
||||
MATRIX_OS: ${{ matrix.os }}
|
||||
|
||||
- name: Build for macOS x86_64
|
||||
if: startsWith(matrix.os, 'windows-') != true
|
||||
run: go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
|
||||
|
||||
- name: Build for macOS arm64
|
||||
if: startsWith(matrix.os, 'macos-') == true
|
||||
run: CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 SDKROOT=$(xcrun --sdk macosx --show-sdk-path) go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo_arm64
|
||||
|
||||
- name: Build for Windows
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
run: |
|
||||
$GIT_COMMIT = (git describe --always --abbrev=8 --dirty) | Out-String
|
||||
$DATE_TIME = ([datetime]::Now.ToUniversalTime().toString("yyyy-MM-ddTHH:mm:ssZ")) | Out-String
|
||||
$FILE_VERSION = $Env:SFTPGO_VERSION.substring(1) + ".0"
|
||||
go install github.com/tc-hib/go-winres@latest
|
||||
go-winres simply --arch amd64 --product-version $Env:SFTPGO_VERSION-$GIT_COMMIT --file-version $FILE_VERSION --file-description "SFTPGo server" --product-name SFTPGo --copyright "AGPL-3.0" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
|
||||
go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o sftpgo.exe
|
||||
mkdir arm64
|
||||
$Env:CGO_ENABLED='0'
|
||||
$Env:GOOS='windows'
|
||||
$Env:GOARCH='arm64'
|
||||
go-winres simply --arch arm64 --product-version $Env:SFTPGO_VERSION-$GIT_COMMIT --file-version $FILE_VERSION --file-description "SFTPGo server" --product-name SFTPGo --copyright "AGPL-3.0" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
|
||||
go build -trimpath -tags nopgxregisterdefaulttypes,nosqlite -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o .\arm64\sftpgo.exe
|
||||
mkdir x86
|
||||
$Env:GOARCH='386'
|
||||
go-winres simply --arch 386 --product-version $Env:SFTPGO_VERSION-$GIT_COMMIT --file-version $FILE_VERSION --file-description "SFTPGo server" --product-name SFTPGo --copyright "AGPL-3.0" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
|
||||
go build -trimpath -tags nopgxregisterdefaulttypes,nosqlite -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o .\x86\sftpgo.exe
|
||||
Remove-Item Env:\CGO_ENABLED
|
||||
Remove-Item Env:\GOOS
|
||||
Remove-Item Env:\GOARCH
|
||||
env:
|
||||
SFTPGO_VERSION: ${{ steps.get_version.outputs.VERSION }}
|
||||
|
||||
- name: Initialize data provider
|
||||
run: ./sftpgo initprovider
|
||||
shell: bash
|
||||
|
||||
- name: Prepare Release for macOS
|
||||
if: startsWith(matrix.os, 'macos-')
|
||||
run: |
|
||||
mkdir -p output/{init,sqlite,bash_completion,zsh_completion}
|
||||
echo "For documentation please take a look here:" > output/README.txt
|
||||
echo "" >> output/README.txt
|
||||
echo "https://github.com/drakkan/sftpgo/blob/${SFTPGO_VERSION}/README.md" >> output/README.txt
|
||||
cp LICENSE output/
|
||||
cp sftpgo output/
|
||||
cp sftpgo.json output/
|
||||
cp sftpgo.db output/sqlite/
|
||||
cp -r static output/
|
||||
cp -r openapi output/
|
||||
cp -r templates output/
|
||||
cp init/com.github.drakkan.sftpgo.plist output/init/
|
||||
./sftpgo gen completion bash > output/bash_completion/sftpgo
|
||||
./sftpgo gen completion zsh > output/zsh_completion/_sftpgo
|
||||
./sftpgo gen man -d output/man/man1
|
||||
gzip output/man/man1/*
|
||||
cd output
|
||||
tar cJvf ../sftpgo_${SFTPGO_VERSION}_${OS}_x86_64.tar.xz *
|
||||
cd ..
|
||||
cp sftpgo_arm64 output/sftpgo
|
||||
cd output
|
||||
tar cJvf ../sftpgo_${SFTPGO_VERSION}_${OS}_arm64.tar.xz *
|
||||
cd ..
|
||||
env:
|
||||
SFTPGO_VERSION: ${{ steps.get_version.outputs.VERSION }}
|
||||
OS: ${{ steps.get_os_name.outputs.OS }}
|
||||
|
||||
- name: Prepare Release for Windows
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
run: |
|
||||
mkdir output
|
||||
copy .\sftpgo.exe .\output
|
||||
copy .\sftpgo.json .\output
|
||||
copy .\sftpgo.db .\output
|
||||
copy .\LICENSE .\output\LICENSE.txt
|
||||
mkdir output\templates
|
||||
xcopy .\templates .\output\templates\ /E
|
||||
mkdir output\static
|
||||
xcopy .\static .\output\static\ /E
|
||||
mkdir output\openapi
|
||||
xcopy .\openapi .\output\openapi\ /E
|
||||
$CERT_PATH=(Get-Location -PSProvider FileSystem).ProviderPath + "\cert.pfx"
|
||||
[IO.File]::WriteAllBytes($CERT_PATH,[System.Convert]::FromBase64String($Env:CERT_DATA))
|
||||
certutil -f -p "$Env:CERT_PASS" -importpfx MY "$CERT_PATH"
|
||||
rm "$CERT_PATH"
|
||||
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.20348.0/x86/signtool.exe' sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n "Nicola Murino" /d "SFTPGo" .\sftpgo.exe
|
||||
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.20348.0/x86/signtool.exe' sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n "Nicola Murino" /d "SFTPGo" .\arm64\sftpgo.exe
|
||||
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.20348.0/x86/signtool.exe' sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n "Nicola Murino" /d "SFTPGo" .\x86\sftpgo.exe
|
||||
$INNO_S='/Ssigntool=$qC:/Program Files (x86)/Windows Kits/10/bin/10.0.20348.0/x86/signtool.exe$q sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n $qNicola Murino$q /d $qSFTPGo$q $f'
|
||||
iscc "$INNO_S" .\windows-installer\sftpgo.iss
|
||||
|
||||
rm .\output\sftpgo.exe
|
||||
rm .\output\sftpgo.db
|
||||
copy .\arm64\sftpgo.exe .\output
|
||||
(Get-Content .\output\sftpgo.json).replace('"sqlite"', '"bolt"') | Set-Content .\output\sftpgo.json
|
||||
$Env:SFTPGO_DATA_PROVIDER__DRIVER='bolt'
|
||||
$Env:SFTPGO_DATA_PROVIDER__NAME='.\output\sftpgo.db'
|
||||
.\sftpgo.exe initprovider
|
||||
Remove-Item Env:\SFTPGO_DATA_PROVIDER__DRIVER
|
||||
Remove-Item Env:\SFTPGO_DATA_PROVIDER__NAME
|
||||
$Env:SFTPGO_ISS_ARCH='arm64'
|
||||
iscc "$INNO_S" .\windows-installer\sftpgo.iss
|
||||
|
||||
rm .\output\sftpgo.exe
|
||||
copy .\x86\sftpgo.exe .\output
|
||||
$Env:SFTPGO_ISS_ARCH='x86'
|
||||
iscc "$INNO_S" .\windows-installer\sftpgo.iss
|
||||
certutil -delstore MY "Nicola Murino"
|
||||
env:
|
||||
SFTPGO_ISS_VERSION: ${{ steps.get_version.outputs.VERSION }}
|
||||
SFTPGO_ISS_DOC_URL: https://github.com/drakkan/sftpgo/blob/${{ steps.get_version.outputs.VERSION }}/README.md
|
||||
CERT_DATA: ${{ secrets.CERT_DATA }}
|
||||
CERT_PASS: ${{ secrets.CERT_PASS }}
|
||||
|
||||
- name: Prepare Portable Release for Windows
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
run: |
|
||||
mkdir win-portable
|
||||
copy .\sftpgo.exe .\win-portable
|
||||
mkdir win-portable\arm64
|
||||
copy .\arm64\sftpgo.exe .\win-portable\arm64
|
||||
mkdir win-portable\x86
|
||||
copy .\x86\sftpgo.exe .\win-portable\x86
|
||||
copy .\sftpgo.json .\win-portable
|
||||
(Get-Content .\win-portable\sftpgo.json).replace('"sqlite"', '"bolt"') | Set-Content .\win-portable\sftpgo.json
|
||||
copy .\output\sftpgo.db .\win-portable
|
||||
copy .\LICENSE .\win-portable\LICENSE.txt
|
||||
mkdir win-portable\templates
|
||||
xcopy .\templates .\win-portable\templates\ /E
|
||||
mkdir win-portable\static
|
||||
xcopy .\static .\win-portable\static\ /E
|
||||
mkdir win-portable\openapi
|
||||
xcopy .\openapi .\win-portable\openapi\ /E
|
||||
Compress-Archive .\win-portable\* sftpgo_portable.zip
|
||||
|
||||
- name: Upload macOS x86_64 artifact
|
||||
if: startsWith(matrix.os, 'macos-')
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_x86_64.tar.xz
|
||||
path: ./sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_x86_64.tar.xz
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload macOS arm64 artifact
|
||||
if: startsWith(matrix.os, 'macos-')
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_arm64.tar.xz
|
||||
path: ./sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_arm64.tar.xz
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload Windows installer x86_64 artifact
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_x86_64.exe
|
||||
path: ./sftpgo_windows_x86_64.exe
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload Windows installer arm64 artifact
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_arm64.exe
|
||||
path: ./sftpgo_windows_arm64.exe
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload Windows installer x86 artifact
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_x86.exe
|
||||
path: ./sftpgo_windows_x86.exe
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload Windows portable artifact
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_portable.zip
|
||||
path: ./sftpgo_portable.zip
|
||||
retention-days: 1
|
||||
|
||||
prepare-linux:
|
||||
name: Prepare Linux binaries
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
distro: ubuntu:18.04
|
||||
go-arch: amd64
|
||||
deb-arch: amd64
|
||||
rpm-arch: x86_64
|
||||
tar-arch: x86_64
|
||||
- arch: aarch64
|
||||
distro: ubuntu18.04
|
||||
go-arch: arm64
|
||||
deb-arch: arm64
|
||||
rpm-arch: aarch64
|
||||
tar-arch: arm64
|
||||
- arch: ppc64le
|
||||
distro: ubuntu18.04
|
||||
go-arch: ppc64le
|
||||
deb-arch: ppc64el
|
||||
rpm-arch: ppc64le
|
||||
tar-arch: ppc64le
|
||||
- arch: armv7
|
||||
distro: ubuntu18.04
|
||||
go-arch: arm7
|
||||
deb-arch: armhf
|
||||
rpm-arch: armv7hl
|
||||
tar-arch: armv7
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get versions
|
||||
id: get_version
|
||||
run: |
|
||||
echo "SFTPGO_VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
|
||||
echo "GO_VERSION=${GO_VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "COMMIT=${GITHUB_SHA::8}" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
env:
|
||||
GO_VERSION: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Build on amd64
|
||||
if: ${{ matrix.arch == 'amd64' }}
|
||||
run: |
|
||||
echo '#!/bin/bash' > build.sh
|
||||
echo '' >> build.sh
|
||||
echo 'set -e' >> build.sh
|
||||
echo 'apt-get update -q -y' >> build.sh
|
||||
echo 'apt-get install -q -y curl gcc' >> build.sh
|
||||
echo 'curl --retry 5 --retry-delay 2 --connect-timeout 10 -o go.tar.gz -L https://go.dev/dl/go${{ steps.get_version.outputs.GO_VERSION }}.linux-${{ matrix.go-arch }}.tar.gz' >> build.sh
|
||||
echo 'tar -C /usr/local -xzf go.tar.gz' >> build.sh
|
||||
echo 'export PATH=$PATH:/usr/local/go/bin' >> build.sh
|
||||
echo 'go version' >> build.sh
|
||||
echo 'cd /usr/local/src' >> build.sh
|
||||
echo 'go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${{ steps.get_version.outputs.COMMIT }} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo' >> build.sh
|
||||
|
||||
chmod 755 build.sh
|
||||
docker run --rm --name ubuntu-build --mount type=bind,source=`pwd`,target=/usr/local/src ${{ matrix.distro }} /usr/local/src/build.sh
|
||||
mkdir -p output/{init,sqlite,bash_completion,zsh_completion}
|
||||
echo "For documentation please take a look here:" > output/README.txt
|
||||
echo "" >> output/README.txt
|
||||
echo "https://github.com/drakkan/sftpgo/blob/${SFTPGO_VERSION}/README.md" >> output/README.txt
|
||||
cp LICENSE output/
|
||||
cp sftpgo.json output/
|
||||
cp -r templates output/
|
||||
cp -r static output/
|
||||
cp -r openapi output/
|
||||
cp init/sftpgo.service output/init/
|
||||
./sftpgo initprovider
|
||||
./sftpgo gen completion bash > output/bash_completion/sftpgo
|
||||
./sftpgo gen completion zsh > output/zsh_completion/_sftpgo
|
||||
./sftpgo gen man -d output/man/man1
|
||||
gzip output/man/man1/*
|
||||
cp sftpgo output/
|
||||
cp sftpgo.db output/sqlite/
|
||||
cd output
|
||||
tar cJvf sftpgo_${SFTPGO_VERSION}_linux_${{ matrix.tar-arch }}.tar.xz *
|
||||
cd ..
|
||||
env:
|
||||
SFTPGO_VERSION: ${{ steps.get_version.outputs.SFTPGO_VERSION }}
|
||||
|
||||
- uses: uraimo/run-on-arch-action@v2
|
||||
if: ${{ matrix.arch != 'amd64' }}
|
||||
name: Build for ${{ matrix.arch }}
|
||||
id: build
|
||||
with:
|
||||
arch: ${{ matrix.arch }}
|
||||
distro: ${{ matrix.distro }}
|
||||
setup: |
|
||||
mkdir -p "${PWD}/output"
|
||||
dockerRunArgs: |
|
||||
--volume "${PWD}/output:/output"
|
||||
shell: /bin/bash
|
||||
install: |
|
||||
apt-get update -q -y
|
||||
apt-get install -q -y curl gcc xz-utils
|
||||
GO_DOWNLOAD_ARCH=${{ matrix.go-arch }}
|
||||
if [ ${{ matrix.arch}} == 'armv7' ]
|
||||
then
|
||||
GO_DOWNLOAD_ARCH=armv6l
|
||||
fi
|
||||
curl --retry 5 --retry-delay 2 --connect-timeout 10 -o go.tar.gz -L https://go.dev/dl/go${{ steps.get_version.outputs.GO_VERSION }}.linux-${GO_DOWNLOAD_ARCH}.tar.gz
|
||||
tar -C /usr/local -xzf go.tar.gz
|
||||
run: |
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
go version
|
||||
go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${{ steps.get_version.outputs.COMMIT }} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
|
||||
mkdir -p output/{init,sqlite,bash_completion,zsh_completion}
|
||||
echo "For documentation please take a look here:" > output/README.txt
|
||||
echo "" >> output/README.txt
|
||||
echo "https://github.com/drakkan/sftpgo/blob/${{ steps.get_version.outputs.SFTPGO_VERSION }}/README.md" >> output/README.txt
|
||||
cp LICENSE output/
|
||||
cp sftpgo.json output/
|
||||
cp -r templates output/
|
||||
cp -r static output/
|
||||
cp -r openapi output/
|
||||
cp init/sftpgo.service output/init/
|
||||
./sftpgo initprovider
|
||||
./sftpgo gen completion bash > output/bash_completion/sftpgo
|
||||
./sftpgo gen completion zsh > output/zsh_completion/_sftpgo
|
||||
./sftpgo gen man -d output/man/man1
|
||||
gzip output/man/man1/*
|
||||
cp sftpgo output/
|
||||
cp sftpgo.db output/sqlite/
|
||||
cd output
|
||||
tar cJvf sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_${{ matrix.tar-arch }}.tar.xz *
|
||||
cd ..
|
||||
|
||||
- name: Upload build artifact for ${{ matrix.arch }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_${{ matrix.tar-arch }}.tar.xz
|
||||
path: ./output/sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_${{ matrix.tar-arch }}.tar.xz
|
||||
retention-days: 1
|
||||
|
||||
- name: Build Packages
|
||||
id: build_linux_pkgs
|
||||
run: |
|
||||
export NFPM_ARCH=${{ matrix.go-arch }}
|
||||
cd pkgs
|
||||
./build.sh
|
||||
PKG_VERSION=${SFTPGO_VERSION:1}
|
||||
echo "pkg-version=${PKG_VERSION}" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
SFTPGO_VERSION: ${{ steps.get_version.outputs.SFTPGO_VERSION }}
|
||||
|
||||
- name: Upload Deb Package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.build_linux_pkgs.outputs.pkg-version }}-1_${{ matrix.deb-arch}}.deb
|
||||
path: ./pkgs/dist/deb/sftpgo_${{ steps.build_linux_pkgs.outputs.pkg-version }}-1_${{ matrix.deb-arch}}.deb
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload RPM Package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-1.${{ matrix.rpm-arch}}.rpm
|
||||
path: ./pkgs/dist/rpm/sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-1.${{ matrix.rpm-arch}}.rpm
|
||||
retention-days: 1
|
||||
|
||||
prepare-linux-bundle:
|
||||
name: Prepare Linux bundle
|
||||
needs: prepare-linux
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Get versions
|
||||
id: get_version
|
||||
run: |
|
||||
echo "SFTPGO_VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Download amd64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_x86_64.tar.xz
|
||||
|
||||
- name: Download arm64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_arm64.tar.xz
|
||||
|
||||
- name: Download ppc64le artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_ppc64le.tar.xz
|
||||
|
||||
- name: Download armv7 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_armv7.tar.xz
|
||||
|
||||
- name: Build bundle
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p bundle/{arm64,ppc64le,armv7}
|
||||
cd bundle
|
||||
tar xvf ../sftpgo_${SFTPGO_VERSION}_linux_x86_64.tar.xz
|
||||
cd arm64
|
||||
tar xvf ../../sftpgo_${SFTPGO_VERSION}_linux_arm64.tar.xz sftpgo
|
||||
cd ../ppc64le
|
||||
tar xvf ../../sftpgo_${SFTPGO_VERSION}_linux_ppc64le.tar.xz sftpgo
|
||||
cd ../armv7
|
||||
tar xvf ../../sftpgo_${SFTPGO_VERSION}_linux_armv7.tar.xz sftpgo
|
||||
cd ..
|
||||
tar cJvf sftpgo_${SFTPGO_VERSION}_linux_bundle.tar.xz *
|
||||
cd ..
|
||||
env:
|
||||
SFTPGO_VERSION: ${{ steps.get_version.outputs.SFTPGO_VERSION }}
|
||||
|
||||
- name: Upload Linux bundle
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_bundle.tar.xz
|
||||
path: ./bundle/sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_bundle.tar.xz
|
||||
retention-days: 1
|
||||
|
||||
create-release:
|
||||
name: Release
|
||||
needs: [prepare-linux-bundle, prepare-sources-with-deps, prepare-window-mac]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get versions
|
||||
id: get_version
|
||||
run: |
|
||||
SFTPGO_VERSION=${GITHUB_REF/refs\/tags\//}
|
||||
PKG_VERSION=${SFTPGO_VERSION:1}
|
||||
echo "SFTPGO_VERSION=${SFTPGO_VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "PKG_VERSION=${PKG_VERSION}" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Download amd64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_x86_64.tar.xz
|
||||
|
||||
- name: Download arm64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_arm64.tar.xz
|
||||
|
||||
- name: Download ppc64le artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_ppc64le.tar.xz
|
||||
|
||||
- name: Download armv7 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_armv7.tar.xz
|
||||
|
||||
- name: Download Linux bundle artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_bundle.tar.xz
|
||||
|
||||
- name: Download Deb amd64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.PKG_VERSION }}-1_amd64.deb
|
||||
|
||||
- name: Download Deb arm64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.PKG_VERSION }}-1_arm64.deb
|
||||
|
||||
- name: Download Deb ppc64le artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.PKG_VERSION }}-1_ppc64el.deb
|
||||
|
||||
- name: Download Deb armv7 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.PKG_VERSION }}-1_armhf.deb
|
||||
|
||||
- name: Download RPM x86_64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo-${{ steps.get_version.outputs.PKG_VERSION }}-1.x86_64.rpm
|
||||
|
||||
- name: Download RPM aarch64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo-${{ steps.get_version.outputs.PKG_VERSION }}-1.aarch64.rpm
|
||||
|
||||
- name: Download RPM ppc64le artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo-${{ steps.get_version.outputs.PKG_VERSION }}-1.ppc64le.rpm
|
||||
|
||||
- name: Download RPM armv7 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo-${{ steps.get_version.outputs.PKG_VERSION }}-1.armv7hl.rpm
|
||||
|
||||
- name: Download macOS x86_64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_macOS_x86_64.tar.xz
|
||||
|
||||
- name: Download macOS arm64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_macOS_arm64.tar.xz
|
||||
|
||||
- name: Download Windows installer x86_64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_windows_x86_64.exe
|
||||
|
||||
- name: Download Windows installer arm64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_windows_arm64.exe
|
||||
|
||||
- name: Download Windows installer x86 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_windows_x86.exe
|
||||
|
||||
- name: Download Windows portable artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_windows_portable.zip
|
||||
|
||||
- name: Download source with deps artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_src_with_deps.tar.xz
|
||||
|
||||
- name: Create release
|
||||
run: |
|
||||
mv sftpgo_windows_x86_64.exe sftpgo_${SFTPGO_VERSION}_windows_x86_64.exe
|
||||
mv sftpgo_windows_arm64.exe sftpgo_${SFTPGO_VERSION}_windows_arm64.exe
|
||||
mv sftpgo_windows_x86.exe sftpgo_${SFTPGO_VERSION}_windows_x86.exe
|
||||
mv sftpgo_portable.zip sftpgo_${SFTPGO_VERSION}_windows_portable.zip
|
||||
gh release create "${SFTPGO_VERSION}" -t "${SFTPGO_VERSION}"
|
||||
gh release upload "${SFTPGO_VERSION}" sftpgo_*.xz --clobber
|
||||
gh release upload "${SFTPGO_VERSION}" sftpgo-*.rpm --clobber
|
||||
gh release upload "${SFTPGO_VERSION}" sftpgo_*.deb --clobber
|
||||
gh release upload "${SFTPGO_VERSION}" sftpgo_*.exe --clobber
|
||||
gh release upload "${SFTPGO_VERSION}" sftpgo_*.zip --clobber
|
||||
gh release view "${SFTPGO_VERSION}"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
SFTPGO_VERSION: ${{ steps.get_version.outputs.SFTPGO_VERSION }}
|
||||
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# compilation output
|
||||
sftpgo
|
||||
sftpgo.exe
|
||||
52
.golangci.yml
Normal file
@@ -0,0 +1,52 @@
|
||||
run:
|
||||
timeout: 10m
|
||||
issues-exit-code: 1
|
||||
tests: true
|
||||
|
||||
|
||||
linters-settings:
|
||||
dupl:
|
||||
threshold: 150
|
||||
errcheck:
|
||||
check-type-assertions: false
|
||||
check-blank: false
|
||||
goconst:
|
||||
min-len: 3
|
||||
min-occurrences: 3
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
gofmt:
|
||||
simplify: true
|
||||
goimports:
|
||||
local-prefixes: github.com/drakkan/sftpgo
|
||||
#govet:
|
||||
# report about shadowed variables
|
||||
#check-shadowing: true
|
||||
#enable:
|
||||
# - fieldalignment
|
||||
|
||||
issues:
|
||||
include:
|
||||
- EXC0002
|
||||
- EXC0012
|
||||
- EXC0013
|
||||
- EXC0014
|
||||
- EXC0015
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- goconst
|
||||
- errcheck
|
||||
- gofmt
|
||||
- goimports
|
||||
- revive
|
||||
- unconvert
|
||||
- unparam
|
||||
- bodyclose
|
||||
- gocyclo
|
||||
- misspell
|
||||
- whitespace
|
||||
- dupl
|
||||
- rowserrcheck
|
||||
- dogsled
|
||||
- govet
|
||||
23
.travis.yml
@@ -1,23 +0,0 @@
|
||||
language: go
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
go:
|
||||
- "1.12.x"
|
||||
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
|
||||
before_script:
|
||||
- sqlite3 sftpgo.db 'CREATE TABLE "users" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "username" varchar(255) NOT NULL UNIQUE, "password" varchar(255) NULL, "public_keys" text NULL, "home_dir" varchar(255) NOT NULL, "uid" integer NOT NULL, "gid" integer NOT NULL, "max_sessions" integer NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL, "permissions" text NOT NULL, "used_quota_size" bigint NOT NULL, "used_quota_files" integer NOT NULL, "last_quota_update" bigint NOT NULL, "upload_bandwidth" integer NOT NULL, "download_bandwidth" integer NOT NULL);'
|
||||
|
||||
install:
|
||||
- go get -v -t ./...
|
||||
|
||||
script:
|
||||
- go test -v ./... -coverprofile=coverage.txt -covermode=atomic
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
1
CODEOWNERS
Normal file
@@ -0,0 +1 @@
|
||||
* @drakkan
|
||||
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
support@sftpgo.com.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
67
Dockerfile
Normal file
@@ -0,0 +1,67 @@
|
||||
FROM golang:1.22-bookworm as builder
|
||||
|
||||
ENV GOFLAGS="-mod=readonly"
|
||||
|
||||
RUN apt-get update && apt-get -y upgrade && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /workspace
|
||||
WORKDIR /workspace
|
||||
|
||||
ARG GOPROXY
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
ARG COMMIT_SHA
|
||||
|
||||
# This ARG allows to disable some optional features and it might be useful if you build the image yourself.
|
||||
# For example you can disable S3 and GCS support like this:
|
||||
# --build-arg FEATURES=nos3,nogcs
|
||||
ARG FEATURES
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN set -xe && \
|
||||
export COMMIT_SHA=${COMMIT_SHA:-$(git describe --always --abbrev=8 --dirty)} && \
|
||||
go build $(if [ -n "${FEATURES}" ]; then echo "-tags ${FEATURES}"; fi) -trimpath -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${COMMIT_SHA} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -v -o sftpgo
|
||||
|
||||
# Set to "true" to download the "official" plugins in /usr/local/bin
|
||||
ARG DOWNLOAD_PLUGINS=false
|
||||
|
||||
RUN if [ "${DOWNLOAD_PLUGINS}" = "true" ]; then apt-get update && apt-get install --no-install-recommends -y curl && ./docker/scripts/download-plugins.sh; fi
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
# Set to "true" to install jq and the optional git and rsync dependencies
|
||||
ARG INSTALL_OPTIONAL_PACKAGES=false
|
||||
|
||||
RUN apt-get update && apt-get -y upgrade && apt-get install --no-install-recommends -y ca-certificates media-types && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN if [ "${INSTALL_OPTIONAL_PACKAGES}" = "true" ]; then apt-get update && apt-get install --no-install-recommends -y jq git rsync && rm -rf /var/lib/apt/lists/*; fi
|
||||
|
||||
RUN mkdir -p /etc/sftpgo /var/lib/sftpgo /usr/share/sftpgo /srv/sftpgo/data /srv/sftpgo/backups
|
||||
|
||||
RUN groupadd --system -g 1000 sftpgo && \
|
||||
useradd --system --gid sftpgo --no-create-home \
|
||||
--home-dir /var/lib/sftpgo --shell /usr/sbin/nologin \
|
||||
--comment "SFTPGo user" --uid 1000 sftpgo
|
||||
|
||||
COPY --from=builder /workspace/sftpgo.json /etc/sftpgo/sftpgo.json
|
||||
COPY --from=builder /workspace/templates /usr/share/sftpgo/templates
|
||||
COPY --from=builder /workspace/static /usr/share/sftpgo/static
|
||||
COPY --from=builder /workspace/openapi /usr/share/sftpgo/openapi
|
||||
COPY --from=builder /workspace/sftpgo /usr/local/bin/sftpgo-plugin-* /usr/local/bin/
|
||||
|
||||
# Log to the stdout so the logs will be available using docker logs
|
||||
ENV SFTPGO_LOG_FILE_PATH=""
|
||||
|
||||
# Modify the default configuration file
|
||||
RUN sed -i 's|"users_base_dir": "",|"users_base_dir": "/srv/sftpgo/data",|' /etc/sftpgo/sftpgo.json && \
|
||||
sed -i 's|"backups"|"/srv/sftpgo/backups"|' /etc/sftpgo/sftpgo.json
|
||||
|
||||
RUN chown -R sftpgo:sftpgo /etc/sftpgo /srv/sftpgo && chown sftpgo:sftpgo /var/lib/sftpgo && chmod 700 /srv/sftpgo/backups
|
||||
|
||||
WORKDIR /var/lib/sftpgo
|
||||
USER 1000:1000
|
||||
|
||||
CMD ["sftpgo", "serve"]
|
||||
60
Dockerfile.alpine
Normal file
@@ -0,0 +1,60 @@
|
||||
FROM golang:1.22-alpine3.19 AS builder
|
||||
|
||||
ENV GOFLAGS="-mod=readonly"
|
||||
|
||||
RUN apk -U upgrade --no-cache && apk add --update --no-cache bash ca-certificates curl git gcc g++
|
||||
|
||||
RUN mkdir -p /workspace
|
||||
WORKDIR /workspace
|
||||
|
||||
ARG GOPROXY
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
ARG COMMIT_SHA
|
||||
|
||||
# This ARG allows to disable some optional features and it might be useful if you build the image yourself.
|
||||
# For example you can disable S3 and GCS support like this:
|
||||
# --build-arg FEATURES=nos3,nogcs
|
||||
ARG FEATURES
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN set -xe && \
|
||||
export COMMIT_SHA=${COMMIT_SHA:-$(git describe --always --abbrev=8 --dirty)} && \
|
||||
go build $(if [ -n "${FEATURES}" ]; then echo "-tags ${FEATURES}"; fi) -trimpath -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${COMMIT_SHA} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -v -o sftpgo
|
||||
|
||||
FROM alpine:3.19
|
||||
|
||||
# Set to "true" to install jq and the optional git and rsync dependencies
|
||||
ARG INSTALL_OPTIONAL_PACKAGES=false
|
||||
|
||||
RUN apk -U upgrade --no-cache && apk add --update --no-cache ca-certificates tzdata mailcap
|
||||
|
||||
RUN if [ "${INSTALL_OPTIONAL_PACKAGES}" = "true" ]; then apk add --update --no-cache jq git rsync; fi
|
||||
|
||||
RUN mkdir -p /etc/sftpgo /var/lib/sftpgo /usr/share/sftpgo /srv/sftpgo/data /srv/sftpgo/backups
|
||||
|
||||
RUN addgroup -g 1000 -S sftpgo && \
|
||||
adduser -u 1000 -h /var/lib/sftpgo -s /sbin/nologin -G sftpgo -S -D -H -g "SFTPGo user" sftpgo
|
||||
|
||||
COPY --from=builder /workspace/sftpgo.json /etc/sftpgo/sftpgo.json
|
||||
COPY --from=builder /workspace/templates /usr/share/sftpgo/templates
|
||||
COPY --from=builder /workspace/static /usr/share/sftpgo/static
|
||||
COPY --from=builder /workspace/openapi /usr/share/sftpgo/openapi
|
||||
COPY --from=builder /workspace/sftpgo /usr/local/bin/
|
||||
|
||||
# Log to the stdout so the logs will be available using docker logs
|
||||
ENV SFTPGO_LOG_FILE_PATH=""
|
||||
|
||||
# Modify the default configuration file
|
||||
RUN sed -i 's|"users_base_dir": "",|"users_base_dir": "/srv/sftpgo/data",|' /etc/sftpgo/sftpgo.json && \
|
||||
sed -i 's|"backups"|"/srv/sftpgo/backups"|' /etc/sftpgo/sftpgo.json
|
||||
|
||||
RUN chown -R sftpgo:sftpgo /etc/sftpgo /srv/sftpgo && chown sftpgo:sftpgo /var/lib/sftpgo && chmod 700 /srv/sftpgo/backups
|
||||
|
||||
WORKDIR /var/lib/sftpgo
|
||||
USER 1000:1000
|
||||
|
||||
CMD ["sftpgo", "serve"]
|
||||
57
Dockerfile.distroless
Normal file
@@ -0,0 +1,57 @@
|
||||
FROM golang:1.22-bookworm as builder
|
||||
|
||||
ENV CGO_ENABLED=0 GOFLAGS="-mod=readonly"
|
||||
|
||||
RUN apt-get update && apt-get -y upgrade && apt-get install --no-install-recommends -y media-types && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /workspace
|
||||
WORKDIR /workspace
|
||||
|
||||
ARG GOPROXY
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
ARG COMMIT_SHA
|
||||
|
||||
# This ARG allows to disable some optional features and it might be useful if you build the image yourself.
|
||||
# For this variant we disable SQLite support since it requires CGO and so a C runtime which is not installed
|
||||
# in distroless/static-* images
|
||||
ARG FEATURES
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN set -xe && \
|
||||
export COMMIT_SHA=${COMMIT_SHA:-$(git describe --always --abbrev=8 --dirty)} && \
|
||||
go build $(if [ -n "${FEATURES}" ]; then echo "-tags ${FEATURES}"; fi) -trimpath -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${COMMIT_SHA} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -v -o sftpgo
|
||||
|
||||
# Modify the default configuration file
|
||||
RUN sed -i 's|"users_base_dir": "",|"users_base_dir": "/srv/sftpgo/data",|' sftpgo.json && \
|
||||
sed -i 's|"backups"|"/srv/sftpgo/backups"|' sftpgo.json && \
|
||||
sed -i 's|"sqlite"|"bolt"|' sftpgo.json
|
||||
|
||||
RUN mkdir /etc/sftpgo /var/lib/sftpgo /srv/sftpgo
|
||||
|
||||
FROM gcr.io/distroless/static-debian12
|
||||
|
||||
COPY --from=builder --chown=1000:1000 /etc/sftpgo /etc/sftpgo
|
||||
COPY --from=builder --chown=1000:1000 /srv/sftpgo /srv/sftpgo
|
||||
COPY --from=builder --chown=1000:1000 /var/lib/sftpgo /var/lib/sftpgo
|
||||
COPY --from=builder --chown=1000:1000 /workspace/sftpgo.json /etc/sftpgo/sftpgo.json
|
||||
COPY --from=builder /workspace/templates /usr/share/sftpgo/templates
|
||||
COPY --from=builder /workspace/static /usr/share/sftpgo/static
|
||||
COPY --from=builder /workspace/openapi /usr/share/sftpgo/openapi
|
||||
COPY --from=builder /workspace/sftpgo /usr/local/bin/
|
||||
COPY --from=builder /etc/mime.types /etc/mime.types
|
||||
|
||||
# Log to the stdout so the logs will be available using docker logs
|
||||
ENV SFTPGO_LOG_FILE_PATH=""
|
||||
# These env vars are required to avoid the following error when calling user.Current():
|
||||
# unable to get the current user: user: Current requires cgo or $USER set in environment
|
||||
ENV USER=sftpgo
|
||||
ENV HOME=/var/lib/sftpgo
|
||||
|
||||
WORKDIR /var/lib/sftpgo
|
||||
USER 1000:1000
|
||||
|
||||
CMD ["sftpgo", "serve"]
|
||||
145
LICENSE
@@ -1,5 +1,5 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
@@ -7,17 +7,15 @@
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
@@ -72,7 +60,7 @@ modification follow.
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
@@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
369
README.md
@@ -1,344 +1,93 @@
|
||||
# SFTPGo
|
||||
[](https://travis-ci.org/drakkan/sftpgo) [](https://codecov.io/gh/drakkan/sftpgo/branch/master) [](https://goreportcard.com/report/github.com/drakkan/sftpgo) [](https://www.gnu.org/licenses/gpl-3.0) [](https://github.com/avelino/awesome-go)
|
||||
|
||||
Full featured and highly configurable SFTP server
|
||||
[](https://github.com/drakkan/sftpgo/workflows/CI/badge.svg?branch=main&event=push)
|
||||
[](https://codecov.io/gh/drakkan/sftpgo/branch/main)
|
||||
[](https://www.gnu.org/licenses/agpl-3.0)
|
||||
[](https://github.com/avelino/awesome-go)
|
||||
|
||||
## Features
|
||||
Full-featured and highly configurable event-driven file transfer solution.
|
||||
Server protocols: SFTP, HTTP/S, FTP/S, WebDAV.
|
||||
Storage backends: local filesystem, encrypted local filesystem, S3 (compatible) Object Storage, Google Cloud Storage, Azure Blob Storage, other SFTP servers.
|
||||
|
||||
- Each account is chrooted to his Home Dir.
|
||||
- SFTP accounts are virtual accounts stored in a "data provider".
|
||||
- SQLite, MySQL, PostgreSQL and bbolt (key/value store in pure Go) data providers are supported.
|
||||
- Public key and password authentication. Multiple public keys per user are supported.
|
||||
- Quota support: accounts can have individual quota expressed as max total size and/or max number of files.
|
||||
- Bandwidth throttling is supported, with distinct settings for upload and download.
|
||||
- Per user maximum concurrent sessions.
|
||||
- Per user permissions: list directories content, upload, download, delete, rename, create directories, create symlinks can be enabled or disabled.
|
||||
- Per user files/folders ownership: you can map all the users to the system account that runs SFTPGo (all platforms are supported) or you can run SFTPGo as root user and map each user or group of users to a different system account (*NIX only).
|
||||
- Configurable custom commands and/or HTTP notifications on upload, download, delete or rename.
|
||||
- Automatically terminating idle connections.
|
||||
- Atomic uploads are configurable.
|
||||
- Optional SCP support.
|
||||
- REST API for users and quota management and real time reports for the active connections with possibility of forcibly closing a connection.
|
||||
- Configuration is a your choice: JSON, TOML, YAML, HCL, envfile are supported.
|
||||
- Log files are accurate and they are saved in the easily parsable JSON format.
|
||||
With SFTPGo you can leverage local and cloud storage backends for exchanging and storing files internally or with business partners using the same tools and processes you are already familiar with.
|
||||
|
||||
## Platforms
|
||||
The WebAdmin UI allows to easily create and manage your users, folders, groups and other resources.
|
||||
|
||||
SFTPGo is developed and tested on Linux. After each commit the code is automatically built and tested on Linux and macOS using Travis CI.
|
||||
Regularly the test cases are manually executed and pass on Windows. Other UNIX variants such as *BSD should work too.
|
||||
The WebClient UI allows end users to change their credentials, browse and manage their files in the browser and setup two-factor authentication which works with Microsoft Authenticator, Google Authenticator, Authy and other compatible apps.
|
||||
|
||||
## Requirements
|
||||
## Sponsors
|
||||
|
||||
- Go 1.12 or higher.
|
||||
- A suitable SQL server or key/value store to use as data provider: PostreSQL 9.4+ or MySQL 5.6+ or SQLite 3.x or bbolt 1.3.x
|
||||
We strongly believe in Open Source software model, so we decided to make SFTPGo available to everyone, but maintaining and evolving SFTPGo takes a lot of time and work. To make development and maintenance sustainable you should consider to support the project with a [sponsorship](https://github.com/sponsors/drakkan).
|
||||
|
||||
## Installation
|
||||
We also provide [professional services](https://sftpgo.com/#pricing) to support you in using SFTPGo to the fullest.
|
||||
|
||||
Simple install the package to your [$GOPATH](https://github.com/golang/go/wiki/GOPATH "GOPATH") with the [go tool](https://golang.org/cmd/go/ "go command") from shell:
|
||||
The open source license grant you freedom but not assurance of help. So why would you rely on free software without support or any guarantee it will stay healthy and maintained for the upcoming years?
|
||||
|
||||
```bash
|
||||
$ go get -u github.com/drakkan/sftpgo
|
||||
```
|
||||
Supporting the project benefit businesses and the community because if the project is financially sustainable, using this business model, we don't have to restrict features and/or switch to an [Open-core](https://en.wikipedia.org/wiki/Open-core_model) model. The technology stays truly open source. Everyone wins.
|
||||
|
||||
Make sure [Git is installed](https://git-scm.com/downloads) on your machine and in your system's `PATH`.
|
||||
It is important to understand that you should support SFTPGo and any other Open Source project you rely on for ongoing maintenance, even if you don't have any questions or need new features, to mitigate the business risk of a project you depend on going unmaintained, with its security and development velocity implications.
|
||||
|
||||
SFTPGo depends on [go-sqlite3](https://github.com/mattn/go-sqlite3) that is a CGO package and so it requires a `C` compiler at build time.
|
||||
On Linux and macOS a compiler is easy to install or already installed, on Windows you need to download [MinGW-w64](https://sourceforge.net/projects/mingw-w64/files/) and build SFTPGo from its command prompt.
|
||||
### Thank you to our sponsors
|
||||
|
||||
The compiler is a build time only dependency, it is not not required at runtime.
|
||||
#### Platinum sponsors
|
||||
|
||||
If you don't need SQLite, you can also get/build SFTPGo setting the environment variable `GCO_ENABLED` to 0, this way SQLite support will be disabled but PostgreSQL, MySQL and bbolt will work and you don't need a `C` compiler for building.
|
||||
[<img src="./img/Aledade_logo.png" alt="Aledade logo" width="202" height="70">](https://www.aledade.com/)
|
||||
</br></br>
|
||||
[<img src="./img/jumptrading.png" alt="Jump Trading logo" width="362" height="63">](https://www.jumptrading.com/)
|
||||
</br></br>
|
||||
[<img src="./img/wpengine.png" alt="WP Engine logo" width="331" height="63">](https://wpengine.com/)
|
||||
|
||||
Version info, such as git commit and build date, can be embedded setting the following string variables at build time:
|
||||
#### Silver sponsors
|
||||
|
||||
- `github.com/drakkan/sftpgo/utils.commit`
|
||||
- `github.com/drakkan/sftpgo/utils.date`
|
||||
[<img src="./img/IDCS.png" alt="IDCS logo" width="212" height="51">](https://idcs.ip-paris.fr/)
|
||||
|
||||
For example you can build using the following command:
|
||||
#### Bronze sponsors
|
||||
|
||||
```bash
|
||||
go build -i -ldflags "-s -w -X github.com/drakkan/sftpgo/utils.commit=`git describe --tags --always --dirty` -X github.com/drakkan/sftpgo/utils.date=`date -u +%FT%TZ`" -o sftpgo
|
||||
```
|
||||
[<img src="./img/7digital.png" alt="7digital logo" width="178" height="56">](https://www.7digital.com/)
|
||||
</br></br>
|
||||
[<img src="./img/vps2day.png" alt="VPS2day logo" width="234" height="56">](https://www.vps2day.com/)
|
||||
|
||||
and you will get a version that includes git commit and build date like this one:
|
||||
## Support policy
|
||||
|
||||
```bash
|
||||
sftpgo -v
|
||||
SFTPGo version: 0.9.0-dev-90607d4-dirty-2019-08-08T19:28:36Z
|
||||
```
|
||||
You can use SFTPGo for free, respecting the obligations of the Open Source license, but please do not ask or expect free support as well.
|
||||
|
||||
For Linux, a systemd sample [service](https://github.com/drakkan/sftpgo/tree/master/init/sftpgo.service "systemd service") can be found inside the source tree.
|
||||
Use [discussions](https://github.com/drakkan/sftpgo/discussions) to ask questions and get support from the community.
|
||||
|
||||
Alternately you can use distro packages:
|
||||
If you report an invalid issue and/or ask for step-by-step support, your issue will be closed as invalid without further explanation and/or the "support request" label will be added. Invalid bug reports may confuse other users. Thanks for understanding.
|
||||
|
||||
- Arch Linux PKGBUILD is available on [AUR](https://aur.archlinux.org/packages/sftpgo-git/ "SFTPGo")
|
||||
## Documentation
|
||||
|
||||
## Configuration
|
||||
You can read more about supported features and documentation at [sftpgo.github.io](https://sftpgo.github.io/).
|
||||
|
||||
The `sftpgo` executable can be used this way:
|
||||
## Release Cadence
|
||||
|
||||
```bash
|
||||
Usage:
|
||||
sftpgo [command]
|
||||
|
||||
Available Commands:
|
||||
help Help about any command
|
||||
serve Start the SFTP Server
|
||||
|
||||
Flags:
|
||||
-h, --help help for sftpgo
|
||||
-v, --version
|
||||
```
|
||||
|
||||
The `serve` subcommand supports the following flags:
|
||||
|
||||
- `--config-dir` string. Location of the config dir. This directory should contain the `sftpgo` configuration file and is used as the base for files with a relative path (eg. the private keys for the SFTP server, the SQLite or bblot database if you use SQLite or bbolt as data provider). The default value is "." or the value of `SFTPGO_CONFIG_DIR` environment variable.
|
||||
- `--config-file` string. Name of the configuration file. It must be the name of a file stored in config-dir not the absolute path to the configuration file. The specified file name must have no extension we automatically load JSON, YAML, TOML, HCL and Java properties. The default value is "sftpgo" (and therefore `sftpgo.json`, `sftpgo.yaml` and so on are searched) or the value of `SFTPGO_CONFIG_FILE` environment variable.
|
||||
- `--log-compress` boolean. Determine if the rotated log files should be compressed using gzip. Default `false` or the value of `SFTPGO_LOG_COMPRESS` environment variable (1 or `true`, 0 or `false`).
|
||||
- `--log-file-path` string. Location for the log file, default "sftpgo.log" or the value of `SFTPGO_LOG_FILE_PATH` environment variable.
|
||||
- `--log-max-age` int. Maximum number of days to retain old log files. Default 28 or the value of `SFTPGO_LOG_MAX_AGE` environment variable.
|
||||
- `--log-max-backups` int. Maximum number of old log files to retain. Default 5 or the value of `SFTPGO_LOG_MAX_BACKUPS` environment variable.
|
||||
- `--log-max-size` int. Maximum size in megabytes of the log file before it gets rotated. Default 10 or the value of `SFTPGO_LOG_MAX_SIZE` environment variable.
|
||||
- `--log-verbose` boolean. Enable verbose logs. Default `true` or the value of `SFTPGO_LOG_VERBOSE` environment variable (1 or `true`, 0 or `false`).
|
||||
|
||||
If you don't configure any private host keys, the daemon will use `id_rsa` in the configuration directory. If that file doesn't exist, the daemon will attempt to autogenerate it (if the user that executes SFTPGo has write access to the config-dir). The server supports any private key format supported by [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/keys.go#L32).
|
||||
|
||||
Before starting `sftpgo` a dataprovider must be configured.
|
||||
|
||||
Sample SQL scripts to create the required database structure can be found inside the source tree [sql](https://github.com/drakkan/sftpgo/tree/master/sql "sql") directory. The SQL scripts filename's is, by convention, the date as `YYYYMMDD` and the suffix `.sql`. You need to apply all the SQL scripts for your database ordered by name, for example `20190706.sql` must be applied before `20190728.sql` and so on.
|
||||
|
||||
The `sftpgo` configuration file contains the following sections:
|
||||
|
||||
- **"sftpd"**, the configuration for the SFTP server
|
||||
- `bind_port`, integer. The port used for serving SFTP requests. Default: 2022
|
||||
- `bind_address`, string. Leave blank to listen on all available network interfaces. Default: ""
|
||||
- `idle_timeout`, integer. Time in minutes after which an idle client will be disconnected. Default: 15
|
||||
- `max_auth_tries` integer. Maximum number of authentication attempts permitted per connection. If set to a negative number, the number of attempts are unlimited. If set to zero, the number of attempts are limited to 6.
|
||||
- `umask`, string. Umask for the new files and directories. This setting has no effect on Windows. Default: "0022"
|
||||
- `banner`, string. Identification string used by the server. Default "SFTPGo"
|
||||
- `upload_mode` integer. 0 means standard, the files are uploaded directly to the requested path. 1 means atomic: files are uploaded to a temporary path and renamed to the requested path when the client ends the upload. Atomic mode avoids problems such as a web server that serves partial files when the files are being uploaded
|
||||
- `actions`, struct. It contains the command to execute and/or the HTTP URL to notify and the trigger conditions
|
||||
- `execute_on`, list of strings. Valid values are `download`, `upload`, `delete`, `rename`. On folder deletion a `delete` notification will be sent for each deleted file. Leave empty to disable actions.
|
||||
- `command`, string. Absolute path to the command to execute. Leave empty to disable. The command is invoked with the following arguments:
|
||||
- `action`, any valid `execute_on` string
|
||||
- `username`, user who did the action
|
||||
- `path` to the affected file. For `rename` action this is the old file name
|
||||
- `target_path`, non empty for `rename` action, this is the new file name
|
||||
- `http_notification_url`, a valid URL. An HTTP GET request will be executed to this URL. Leave empty to disable. The query string will contain the following parameters that have the same meaning of the command's arguments:
|
||||
- `action`
|
||||
- `username`
|
||||
- `path`
|
||||
- `target_path`, added for `rename` action only
|
||||
- `keys`, struct array. It contains the daemon's private keys. If empty or missing the daemon will search or try to generate `id_rsa` in the configuration directory.
|
||||
- `private_key`, path to the private key file. It can be a path relative to the config dir or an absolute one.
|
||||
- `enable_scp`, boolean. Default disabled. Set to `true` to enable SCP support. SCP is an experimental feature, we have our own SCP implementation since we can't rely on `scp` system command to proper handle permissions, quota and user's home dir restrictions. The SCP protocol is quite simple but there is no official docs about it, so we need more testing and feedbacks before enabling it by default. We may not handle some borderline cases or have sneaky bugs. Please do accurate tests yourself before enabling SCP and let us known if something does not work as expected for your use cases. SCP between two remote hosts is supported using the `-3` scp option.
|
||||
- **"data_provider"**, the configuration for the data provider
|
||||
- `driver`, string. Supported drivers are `sqlite`, `mysql`, `postgresql`, `bolt`
|
||||
- `name`, string. Database name. For driver `sqlite` this can be the database name relative to the config dir or the absolute path to the SQLite database.
|
||||
- `host`, string. Database host. Leave empty for driver `sqlite` and `bolt`
|
||||
- `port`, integer. Database port. Leave empty for driver `sqlite` and `bolt`
|
||||
- `username`, string. Database user. Leave empty for driver `sqlite` and `bolt`
|
||||
- `password`, string. Database password. Leave empty for driver `sqlite` and `bolt`
|
||||
- `sslmode`, integer. Used for drivers `mysql` and `postgresql`. 0 disable SSL/TLS connections, 1 require ssl, 2 set ssl mode to `verify-ca` for driver `postgresql` and `skip-verify` for driver `mysql`, 3 set ssl mode to `verify-full` for driver `postgresql` and `preferred` for driver `mysql`
|
||||
- `connectionstring`, string. Provide a custom database connection string. If not empty this connection string will be used instead of build one using the previous parameters. Leave empty for driver `bolt`
|
||||
- `users_table`, string. Database table for SFTP users
|
||||
- `manage_users`, integer. Set to 0 to disable users management, 1 to enable
|
||||
- `track_quota`, integer. Set the preferred way to track users quota between the following choices:
|
||||
- 0, disable quota tracking. REST API to scan user dir and update quota will do nothing
|
||||
- 1, quota is updated each time a user upload or delete a file even if the user has no quota restrictions
|
||||
- 2, quota is updated each time a user upload or delete a file but only for users with quota restrictions. With this configuration the "quota scan" REST API can still be used to periodically update space usage for users without quota restrictions
|
||||
- **"httpd"**, the configuration for the HTTP server used to serve REST API
|
||||
- `bind_port`, integer. The port used for serving HTTP requests. Set to 0 to disable HTTP server. Default: 8080
|
||||
- `bind_address`, string. Leave blank to listen on all available network interfaces. Default: "127.0.0.1"
|
||||
|
||||
Here is a full example showing the default config in JSON format:
|
||||
|
||||
```json
|
||||
{
|
||||
"sftpd": {
|
||||
"bind_port": 2022,
|
||||
"bind_address": "",
|
||||
"idle_timeout": 15,
|
||||
"max_auth_tries": 0,
|
||||
"umask": "0022",
|
||||
"banner": "SFTPGo",
|
||||
"actions": {
|
||||
"execute_on": [],
|
||||
"command": "",
|
||||
"http_notification_url": ""
|
||||
},
|
||||
"keys": [],
|
||||
"enable_scp": false
|
||||
},
|
||||
"data_provider": {
|
||||
"driver": "sqlite",
|
||||
"name": "sftpgo.db",
|
||||
"host": "",
|
||||
"port": 5432,
|
||||
"username": "",
|
||||
"password": "",
|
||||
"sslmode": 0,
|
||||
"connection_string": "",
|
||||
"users_table": "users",
|
||||
"manage_users": 1,
|
||||
"track_quota": 2
|
||||
},
|
||||
"httpd": {
|
||||
"bind_port": 8080,
|
||||
"bind_address": "127.0.0.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you want to use a private key that use an algorithm different from RSA or more than one private key then replace the empty `keys` array with something like this:
|
||||
|
||||
```json
|
||||
"keys": [
|
||||
{
|
||||
"private_key": "id_rsa"
|
||||
},
|
||||
{
|
||||
"private_key": "id_ecdsa"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
The configuration can be read from JSON, TOML, YAML, HCL, envfile and Java properties config files, if your `config-file` flag is set to `sftpgo` (default value) you need to create a configuration file called `sftpgo.json` or `sftpgo.yaml` and so on inside `config-dir`.
|
||||
|
||||
You can also configure all the available options using environment variables, sftpgo will check for environment variables with a name matching the key uppercased and prefixed with the `SFTPGO_`. You need to use `__` to traverse a struct.
|
||||
|
||||
Let's see some examples:
|
||||
|
||||
- To set sftpd `bind_port` you need to define the env var `SFTPGO_SFTPD__BIND_PORT`
|
||||
- To set the `execute_on` actions you need to define the env var `SFTPGO_SFTPD__ACTIONS__EXECUTE_ON` for example `SFTPGO_SFTPD__ACTIONS__EXECUTE_ON=upload,download`
|
||||
|
||||
To start the SFTP Server with the default values for the command line flags simply use:
|
||||
|
||||
```bash
|
||||
sftpgo serve
|
||||
```
|
||||
|
||||
## Account's configuration properties
|
||||
|
||||
For each account the following properties can be configured:
|
||||
|
||||
- `username`
|
||||
- `password` used for password authentication. For users created using SFTPGo REST API if the password has no known hashing algo prefix it will be stored using argon2id. SFTPGo supports checking passwords stored with bcrypt and pbkdf2 too. For pbkdf2 the supported format is `$<algo>$<iterations>$<salt>$<hashed pwd base64 encoded>`, where algo is `pbkdf2-sha1` or `pbkdf2-sha256` or `pbkdf2-sha512`. For example the `pbkdf2-sha256` of the word `password` using 150000 iterations and `E86a9YMX3zC7` as salt must be stored as `$pbkdf2-sha256$150000$E86a9YMX3zC7$R5J62hsSq+pYw00hLLPKBbcGXmq7fj5+/M0IFoYtZbo=`. For bcrypt the format must be the one supported by golang's [crypto/bcrypt](https://godoc.org/golang.org/x/crypto/bcrypt) package, for example the password `secret` with cost `14` must be stored as `$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK`. Using the REST API you can send a password hashed as bcrypt or pbkdf2 and it will be stored as is.
|
||||
- `public_keys` array of public keys. At least one public key or the password is mandatory.
|
||||
- `home_dir` The user cannot upload or download files outside this directory. Must be an absolute path
|
||||
- `uid`, `gid`. If sftpgo runs as root system user then the created files and directories will be assigned to this system uid/gid. Ignored on windows and if sftpgo runs as non root user: in this case files and directories for all SFTP users will be owned by the system user that runs sftpgo.
|
||||
- `max_sessions` maximum concurrent sessions. 0 means unlimited
|
||||
- `quota_size` maximum size allowed as bytes. 0 means unlimited
|
||||
- `quota_files` maximum number of files allowed. 0 means unlimited
|
||||
- `permissions` the following permissions are supported:
|
||||
- `*` all permission are granted
|
||||
- `list` list items is allowed
|
||||
- `download` download files is allowed
|
||||
- `upload` upload files is allowed
|
||||
- `delete` delete files or directories is allowed
|
||||
- `rename` rename files or directories is allowed
|
||||
- `create_dirs` create directories is allowed
|
||||
- `create_symlinks` create symbolic links is allowed
|
||||
- `upload_bandwidth` maximum upload bandwidth as KB/s, 0 means unlimited
|
||||
- `download_bandwidth` maximum download bandwidth as KB/s, 0 means unlimited
|
||||
|
||||
These properties are stored inside the data provider. If you want to use your existing accounts, you can create a database view. Since a view is read only, you have to disable user management and quota tracking so SFTPGo will never try to write to the view.
|
||||
|
||||
## REST API
|
||||
|
||||
SFTPGo exposes REST API to manage users and quota and to get real time reports for the active connections with possibility of forcibly closing a connection.
|
||||
|
||||
If quota tracking is enabled in `sftpgo` configuration file, then the used size and number of files are updated each time a file is added/removed. If files are added/removed not using SFTP or if you change `track_quota` from `2` to `1`, you can rescan the user home dir and update the used quota using the REST API.
|
||||
|
||||
REST API is designed to run on localhost or on a trusted network, if you need HTTPS or authentication you can setup a reverse proxy using an HTTP Server such as Apache or NGNIX.
|
||||
|
||||
For example you can keep SFTPGo listening on localhost and expose it externally configuring a reverse proxy using Apache HTTP Server this way:
|
||||
|
||||
```
|
||||
ProxyPass /api/v1 http://127.0.0.1:8080/api/v1
|
||||
ProxyPassReverse /api/v1 http://127.0.0.1:8080/api/v1
|
||||
```
|
||||
|
||||
and you can add authentication with something like this:
|
||||
|
||||
```
|
||||
<Location /api/v1>
|
||||
AuthType Digest
|
||||
AuthName "Private"
|
||||
AuthDigestDomain "/api/v1"
|
||||
AuthDigestProvider file
|
||||
AuthUserFile "/etc/httpd/conf/auth_digest"
|
||||
Require valid-user
|
||||
</Location>
|
||||
```
|
||||
|
||||
and, of course, you can configure the web server to use HTTPS.
|
||||
|
||||
The OpenAPI 3 schema for the exposed API can be found inside the source tree: [openapi.yaml](https://github.com/drakkan/sftpgo/tree/master/api/schema/openapi.yaml "OpenAPI 3 specs").
|
||||
|
||||
A sample CLI client for the REST API can be found inside the source tree [scripts](https://github.com/drakkan/sftpgo/tree/master/scripts "scripts") directory.
|
||||
|
||||
You can also generate your own REST client, in your preferred programming language or even bash scripts, using an OpenAPI generator such as [swagger-codegen](https://github.com/swagger-api/swagger-codegen) or [OpenAPI Generator](https://openapi-generator.tech/)
|
||||
|
||||
## Logs
|
||||
|
||||
Inside the log file each line is a JSON struct, each struct has a `sender` fields that identify the log type.
|
||||
|
||||
The logs can be divided into the following categories:
|
||||
|
||||
- **"app logs"**, internal logs used to debug `sftpgo`:
|
||||
- `sender` string. This is generally the package name that emits the log
|
||||
- `time` string. Date/time with millisecond precision
|
||||
- `level` string
|
||||
- `message` string
|
||||
- **"transfer logs"**, SFTP/SCP transfer logs:
|
||||
- `sender` string. `Upload` or `Download`
|
||||
- `time` string. Date/time with millisecond precision
|
||||
- `level` string
|
||||
- `elapsed_ms`, int64. Elapsed time, as milliseconds, for the upload/download
|
||||
- `size_bytes`, int64. Size, as bytes, of the download/upload
|
||||
- `username`, string
|
||||
- `file_path` string
|
||||
- `connection_id` string. Unique connection identifier
|
||||
- `protocol` string. `SFTP` or `SCP`
|
||||
- **"command logs"**, SFTP/SCP command logs:
|
||||
- `sender` string. `Rename`, `Rmdir`, `Mkdir`, `Symlink`, `Remove`
|
||||
- `level` string
|
||||
- `username`, string
|
||||
- `file_path` string
|
||||
- `target_path` string
|
||||
- `connection_id` string. Unique connection identifier
|
||||
- `protocol` string. `SFTP` or `SCP`
|
||||
- **"http logs"**, REST API logs:
|
||||
- `sender` string. `httpd`
|
||||
- `level` string
|
||||
- `remote_addr` string. IP and port of the remote client
|
||||
- `proto` string, for example `HTTP/1.1`
|
||||
- `method` string. HTTP method (`GET`, `POST`, `PUT`, `DELETE` etc.)
|
||||
- `user_agent` string
|
||||
- `uri` string. Full uri
|
||||
- `resp_status` integer. HTTP response status code
|
||||
- `resp_size` integer. Size in bytes of the HTTP response
|
||||
- `elapsed_ms` int64. Elapsed time, as milliseconds, to complete the request
|
||||
- `request_id` string. Unique request identifier
|
||||
SFTPGo releases are feature-driven, we don't have a fixed time based schedule. As a rough estimate, you can expect 1 or 2 new major releases per year and several bug fix releases.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- [pkg/sftp](https://github.com/pkg/sftp)
|
||||
- [go-chi](https://github.com/go-chi/chi)
|
||||
- [zerolog](https://github.com/rs/zerolog)
|
||||
- [lumberjack](https://gopkg.in/natefinch/lumberjack.v2)
|
||||
- [argon2id](https://github.com/alexedwards/argon2id)
|
||||
- [go-sqlite3](https://github.com/mattn/go-sqlite3)
|
||||
- [go-sql-driver/mysql](https://github.com/go-sql-driver/mysql)
|
||||
- [bbolt](https://github.com/etcd-io/bbolt)
|
||||
- [lib/pq](https://github.com/lib/pq)
|
||||
- [viper](https://github.com/spf13/viper)
|
||||
- [cobra](https://github.com/spf13/cobra)
|
||||
- [xid](https://github.com/rs/xid)
|
||||
SFTPGo makes use of the third party libraries listed inside [go.mod](./go.mod).
|
||||
|
||||
Some code was initially taken from [Pterodactyl sftp server](https://github.com/pterodactyl/sftp-server)
|
||||
We are very grateful to all the people who contributed with ideas and/or pull requests.
|
||||
|
||||
Thank you to [ysura](https://www.ysura.com/) for granting us stable access to a test AWS S3 account.
|
||||
|
||||
Thank you to [KeenThemes](https://keenthemes.com/) for granting us a custom license to use their amazing [Mega Bundle](https://keenthemes.com/products/templates-mega-bundle) for SFTPGo UI.
|
||||
|
||||
Thank you to [Crowdin](https://crowdin.com/) for granting us an Open Source License.
|
||||
|
||||
Thank you to [Incode](https://www.incode.it/) for helping us to improve the UI/UX.
|
||||
|
||||
## License
|
||||
|
||||
GNU GPLv3
|
||||
SFTPGo source code is licensed under the GNU AGPL-3.0-only.
|
||||
|
||||
The [theme](https://keenthemes.com/products/templates-mega-bundle) used in WebAdmin and WebClient user interfaces is proprietary, this means:
|
||||
|
||||
- KeenThemes HTML/CSS/JS components are allowed for use only within the SFTPGo product and restricted to be used in a resealable HTML template that can compete with KeenThemes products anyhow.
|
||||
- The SFTPGo WebAdmin and WebClient user interfaces (HTML, CSS and JS components) based on this theme are allowed for use only within the SFTPGo product and therefore cannot be used in derivative works/products without an explicit grant from the [SFTPGo Team](mailto:support@sftpgo.com).
|
||||
|
||||
More information about [compliance](https://sftpgo.com/compliance.html).
|
||||
|
||||
## Copyright
|
||||
|
||||
Copyright (C) 2019 Nicola Murino
|
||||
|
||||
10
SECURITY.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Only the current release of the software is actively supported.
|
||||
[Contact us](mailto:support@sftpgo.com) if you need early security patches and enterprise-grade security.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report (possible) security issues in SFTPGo, please either send a mail to the [SFTPGo Team](mailto:support@sftpgo.com) or use Github's [private reporting feature](https://github.com/drakkan/sftpgo/security/advisories/new).
|
||||
77
api/api.go
@@ -1,77 +0,0 @@
|
||||
// Package api implements REST API for sftpgo.
|
||||
// REST API allows to manage users and quota and to get real time reports for the active connections
|
||||
// with possibility of forcibly closing a connection.
|
||||
// The OpenAPI 3 schema for the exposed API can be found inside the source tree:
|
||||
// https://github.com/drakkan/sftpgo/tree/master/api/schema/openapi.yaml
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
const (
|
||||
logSender = "api"
|
||||
activeConnectionsPath = "/api/v1/connection"
|
||||
quotaScanPath = "/api/v1/quota_scan"
|
||||
userPath = "/api/v1/user"
|
||||
versionPath = "/api/v1/version"
|
||||
)
|
||||
|
||||
var (
|
||||
router *chi.Mux
|
||||
dataProvider dataprovider.Provider
|
||||
)
|
||||
|
||||
// HTTPDConf httpd daemon configuration
|
||||
type HTTPDConf struct {
|
||||
// The port used for serving HTTP requests. 0 disable the HTTP server. Default: 8080
|
||||
BindPort int `json:"bind_port" mapstructure:"bind_port"`
|
||||
// The address to listen on. A blank value means listen on all available network interfaces. Default: "127.0.0.1"
|
||||
BindAddress string `json:"bind_address" mapstructure:"bind_address"`
|
||||
}
|
||||
|
||||
type apiResponse struct {
|
||||
Error string `json:"error"`
|
||||
Message string `json:"message"`
|
||||
HTTPStatus int `json:"status"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
initializeRouter()
|
||||
}
|
||||
|
||||
// SetDataProvider sets the data provider to use to fetch the data about users
|
||||
func SetDataProvider(provider dataprovider.Provider) {
|
||||
dataProvider = provider
|
||||
}
|
||||
|
||||
func sendAPIResponse(w http.ResponseWriter, r *http.Request, err error, message string, code int) {
|
||||
var errorString string
|
||||
if err != nil {
|
||||
errorString = err.Error()
|
||||
}
|
||||
resp := apiResponse{
|
||||
Error: errorString,
|
||||
Message: message,
|
||||
HTTPStatus: code,
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(code)
|
||||
}
|
||||
render.JSON(w, r, resp)
|
||||
}
|
||||
|
||||
func getRespStatus(err error) int {
|
||||
if _, ok := err.(*dataprovider.ValidationError); ok {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
if _, ok := err.(*dataprovider.MethodDisabledError); ok {
|
||||
return http.StatusForbidden
|
||||
}
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
755
api/api_test.go
@@ -1,755 +0,0 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/lib/pq"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/drakkan/sftpgo/api"
|
||||
"github.com/drakkan/sftpgo/config"
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/sftpd"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultUsername = "test_user"
|
||||
defaultPassword = "test_password"
|
||||
testPubKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0= nicola@p1"
|
||||
logSender = "APITesting"
|
||||
userPath = "/api/v1/user"
|
||||
activeConnectionsPath = "/api/v1/connection"
|
||||
quotaScanPath = "/api/v1/quota_scan"
|
||||
versionPath = "/api/v1/version"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultPerms = []string{dataprovider.PermAny}
|
||||
homeBasePath string
|
||||
testServer *httptest.Server
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if runtime.GOOS == "windows" {
|
||||
homeBasePath = "C:\\"
|
||||
} else {
|
||||
homeBasePath = "/tmp"
|
||||
}
|
||||
configDir := ".."
|
||||
logfilePath := filepath.Join(configDir, "sftpgo_api_test.log")
|
||||
logger.InitLogger(logfilePath, 5, 1, 28, false, zerolog.DebugLevel)
|
||||
config.LoadConfig(configDir, "")
|
||||
providerConf := config.GetProviderConf()
|
||||
|
||||
err := dataprovider.Initialize(providerConf, configDir)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error initializing data provider: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
dataProvider := dataprovider.GetProvider()
|
||||
httpdConf := config.GetHTTPDConfig()
|
||||
router := api.GetHTTPRouter()
|
||||
|
||||
httpdConf.BindPort = 8081
|
||||
api.SetBaseURL("http://127.0.0.1:8081")
|
||||
|
||||
sftpd.SetDataProvider(dataProvider)
|
||||
api.SetDataProvider(dataProvider)
|
||||
|
||||
go func() {
|
||||
logger.Debug(logSender, "initializing HTTP server with config %+v", httpdConf)
|
||||
s := &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort),
|
||||
Handler: router,
|
||||
ReadTimeout: 300 * time.Second,
|
||||
WriteTimeout: 300 * time.Second,
|
||||
MaxHeaderBytes: 1 << 20, // 1MB
|
||||
}
|
||||
if err := s.ListenAndServe(); err != nil {
|
||||
logger.Error(logSender, "could not start HTTP server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
testServer = httptest.NewServer(api.GetHTTPRouter())
|
||||
defer testServer.Close()
|
||||
|
||||
waitTCPListening(fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort))
|
||||
|
||||
exitCode := m.Run()
|
||||
os.Remove(logfilePath)
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
func TestBasicUserHandling(t *testing.T) {
|
||||
user, _, err := api.AddUser(getTestUser(), http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to add user: %v", err)
|
||||
}
|
||||
user.MaxSessions = 10
|
||||
user.QuotaSize = 4096
|
||||
user.QuotaFiles = 2
|
||||
user.UploadBandwidth = 128
|
||||
user.DownloadBandwidth = 64
|
||||
user, _, err = api.UpdateUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to update user: %v", err)
|
||||
}
|
||||
users, _, err := api.GetUsers(0, 0, defaultUsername, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to get users: %v", err)
|
||||
}
|
||||
if len(users) != 1 {
|
||||
t.Errorf("number of users mismatch, expected: 1, actual: %v", len(users))
|
||||
}
|
||||
_, err = api.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to remove: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddUserNoCredentials(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.Password = ""
|
||||
u.PublicKeys = []string{}
|
||||
_, _, err := api.AddUser(u, http.StatusBadRequest)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error adding user with no credentials: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddUserNoUsername(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.Username = ""
|
||||
_, _, err := api.AddUser(u, http.StatusBadRequest)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error adding user with no home dir: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddUserNoHomeDir(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.HomeDir = ""
|
||||
_, _, err := api.AddUser(u, http.StatusBadRequest)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error adding user with no home dir: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddUserInvalidHomeDir(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.HomeDir = "relative_path"
|
||||
_, _, err := api.AddUser(u, http.StatusBadRequest)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error adding user with invalid home dir: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddUserNoPerms(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.Permissions = []string{}
|
||||
_, _, err := api.AddUser(u, http.StatusBadRequest)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error adding user with no perms: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddUserInvalidPerms(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.Permissions = []string{"invalidPerm"}
|
||||
_, _, err := api.AddUser(u, http.StatusBadRequest)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error adding user with no perms: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserPublicKey(t *testing.T) {
|
||||
u := getTestUser()
|
||||
invalidPubKey := "invalid"
|
||||
validPubKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0= nicola@p1"
|
||||
u.PublicKeys = []string{invalidPubKey}
|
||||
_, _, err := api.AddUser(u, http.StatusBadRequest)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error adding user with invalid pub key: %v", err)
|
||||
}
|
||||
u.PublicKeys = []string{validPubKey}
|
||||
user, _, err := api.AddUser(u, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to add user: %v", err)
|
||||
}
|
||||
user.PublicKeys = []string{validPubKey, invalidPubKey}
|
||||
_, _, err = api.UpdateUser(user, http.StatusBadRequest)
|
||||
if err != nil {
|
||||
t.Errorf("update user with invalid public key must fail: %v", err)
|
||||
}
|
||||
user.PublicKeys = []string{validPubKey, validPubKey, validPubKey}
|
||||
_, _, err = api.UpdateUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to update user: %v", err)
|
||||
}
|
||||
_, err = api.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to remove: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateUser(t *testing.T) {
|
||||
user, _, err := api.AddUser(getTestUser(), http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to add user: %v", err)
|
||||
}
|
||||
user.HomeDir = filepath.Join(homeBasePath, "testmod")
|
||||
user.UID = 33
|
||||
user.GID = 101
|
||||
user.MaxSessions = 10
|
||||
user.QuotaSize = 4096
|
||||
user.QuotaFiles = 2
|
||||
user.Permissions = []string{dataprovider.PermCreateDirs, dataprovider.PermDelete, dataprovider.PermDownload}
|
||||
user.UploadBandwidth = 1024
|
||||
user.DownloadBandwidth = 512
|
||||
user, _, err = api.UpdateUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to update user: %v", err)
|
||||
}
|
||||
_, err = api.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to remove: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateUserNoCredentials(t *testing.T) {
|
||||
user, _, err := api.AddUser(getTestUser(), http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to add user: %v", err)
|
||||
}
|
||||
user.Password = ""
|
||||
user.PublicKeys = []string{}
|
||||
// password and public key will be omitted from json serialization if empty and so they will remain unchanged
|
||||
// and no validation error will be raised
|
||||
_, _, err = api.UpdateUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error updating user with no credentials: %v", err)
|
||||
}
|
||||
_, err = api.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to remove: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateUserEmptyHomeDir(t *testing.T) {
|
||||
user, _, err := api.AddUser(getTestUser(), http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to add user: %v", err)
|
||||
}
|
||||
user.HomeDir = ""
|
||||
_, _, err = api.UpdateUser(user, http.StatusBadRequest)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error updating user with empty home dir: %v", err)
|
||||
}
|
||||
_, err = api.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to remove: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateUserInvalidHomeDir(t *testing.T) {
|
||||
user, _, err := api.AddUser(getTestUser(), http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to add user: %v", err)
|
||||
}
|
||||
user.HomeDir = "relative_path"
|
||||
_, _, err = api.UpdateUser(user, http.StatusBadRequest)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error updating user with empty home dir: %v", err)
|
||||
}
|
||||
_, err = api.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to remove: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateNonExistentUser(t *testing.T) {
|
||||
_, _, err := api.UpdateUser(getTestUser(), http.StatusNotFound)
|
||||
if err != nil {
|
||||
t.Errorf("unable to update user: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNonExistentUser(t *testing.T) {
|
||||
_, _, err := api.GetUserByID(0, http.StatusNotFound)
|
||||
if err != nil {
|
||||
t.Errorf("unable to get user: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteNonExistentUser(t *testing.T) {
|
||||
_, err := api.RemoveUser(getTestUser(), http.StatusNotFound)
|
||||
if err != nil {
|
||||
t.Errorf("unable to remove user: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddDuplicateUser(t *testing.T) {
|
||||
user, _, err := api.AddUser(getTestUser(), http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to add user: %v", err)
|
||||
}
|
||||
_, _, err = api.AddUser(getTestUser(), http.StatusInternalServerError)
|
||||
if err != nil {
|
||||
t.Errorf("unable to add second user: %v", err)
|
||||
}
|
||||
_, _, err = api.AddUser(getTestUser(), http.StatusOK)
|
||||
if err == nil {
|
||||
t.Errorf("adding a duplicate user must fail")
|
||||
}
|
||||
_, err = api.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to remove user: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUsers(t *testing.T) {
|
||||
user1, _, err := api.AddUser(getTestUser(), http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to add user: %v", err)
|
||||
}
|
||||
u := getTestUser()
|
||||
u.Username = defaultUsername + "1"
|
||||
user2, _, err := api.AddUser(u, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to add second user: %v", err)
|
||||
}
|
||||
users, _, err := api.GetUsers(0, 0, "", http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to get users: %v", err)
|
||||
}
|
||||
if len(users) < 2 {
|
||||
t.Errorf("at least 2 users are expected")
|
||||
}
|
||||
users, _, err = api.GetUsers(1, 0, "", http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to get users: %v", err)
|
||||
}
|
||||
if len(users) != 1 {
|
||||
t.Errorf("1 user is expected")
|
||||
}
|
||||
users, _, err = api.GetUsers(1, 1, "", http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to get users: %v", err)
|
||||
}
|
||||
if len(users) != 1 {
|
||||
t.Errorf("1 user is expected")
|
||||
}
|
||||
_, _, err = api.GetUsers(1, 1, "", http.StatusInternalServerError)
|
||||
if err == nil {
|
||||
t.Errorf("get users must succeed, we requested a fail for a good request")
|
||||
}
|
||||
_, err = api.RemoveUser(user1, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to remove user: %v", err)
|
||||
}
|
||||
_, err = api.RemoveUser(user2, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to remove user: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetQuotaScans(t *testing.T) {
|
||||
_, _, err := api.GetQuotaScans(http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to get quota scans: %v", err)
|
||||
}
|
||||
_, _, err = api.GetQuotaScans(http.StatusInternalServerError)
|
||||
if err == nil {
|
||||
t.Errorf("quota scan request must succeed, we requested to check a wrong status code")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartQuotaScan(t *testing.T) {
|
||||
user, _, err := api.AddUser(getTestUser(), http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to add user: %v", err)
|
||||
}
|
||||
_, err = api.StartQuotaScan(user, http.StatusCreated)
|
||||
if err != nil {
|
||||
t.Errorf("unable to start quota scan: %v", err)
|
||||
}
|
||||
_, err = api.RemoveUser(user, http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to remove user: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVersion(t *testing.T) {
|
||||
_, _, err := api.GetVersion(http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to get sftp version: %v", err)
|
||||
}
|
||||
_, _, err = api.GetVersion(http.StatusInternalServerError)
|
||||
if err == nil {
|
||||
t.Errorf("get version request must succeed, we requested to check a wrong status code")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetConnections(t *testing.T) {
|
||||
_, _, err := api.GetConnections(http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unable to get sftp connections: %v", err)
|
||||
}
|
||||
_, _, err = api.GetConnections(http.StatusInternalServerError)
|
||||
if err == nil {
|
||||
t.Errorf("get sftp connections request must succeed, we requested to check a wrong status code")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloseActiveConnection(t *testing.T) {
|
||||
_, err := api.CloseConnection("non_existent_id", http.StatusNotFound)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error closing non existent sftp connection: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// test using mock http server
|
||||
|
||||
func TestBasicUserHandlingMock(t *testing.T) {
|
||||
user := getTestUser()
|
||||
userAsJSON := getUserAsJSON(t, user)
|
||||
req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||
err := render.DecodeJSON(rr.Body, &user)
|
||||
if err != nil {
|
||||
t.Errorf("Error get user: %v", err)
|
||||
}
|
||||
req, _ = http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusInternalServerError, rr.Code)
|
||||
user.MaxSessions = 10
|
||||
user.UploadBandwidth = 128
|
||||
userAsJSON = getUserAsJSON(t, user)
|
||||
req, _ = http.NewRequest(http.MethodPut, userPath+"/"+strconv.FormatInt(user.ID, 10), bytes.NewBuffer(userAsJSON))
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||
|
||||
req, _ = http.NewRequest(http.MethodGet, userPath+"/"+strconv.FormatInt(user.ID, 10), nil)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||
|
||||
var updatedUser dataprovider.User
|
||||
err = render.DecodeJSON(rr.Body, &updatedUser)
|
||||
if err != nil {
|
||||
t.Errorf("Error decoding updated user: %v", err)
|
||||
}
|
||||
if user.MaxSessions != updatedUser.MaxSessions || user.UploadBandwidth != updatedUser.UploadBandwidth {
|
||||
t.Errorf("Error modifying user actual: %v, %v", updatedUser.MaxSessions, updatedUser.UploadBandwidth)
|
||||
}
|
||||
req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||
}
|
||||
|
||||
func TestGetUserByIdInvalidParamsMock(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodGet, userPath+"/0", nil)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr.Code)
|
||||
req, _ = http.NewRequest(http.MethodGet, userPath+"/a", nil)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr.Code)
|
||||
}
|
||||
|
||||
func TestAddUserNoUsernameMock(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.Username = ""
|
||||
userAsJSON := getUserAsJSON(t, user)
|
||||
req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr.Code)
|
||||
}
|
||||
|
||||
func TestAddUserInvalidHomeDirMock(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.HomeDir = "relative_path"
|
||||
userAsJSON := getUserAsJSON(t, user)
|
||||
req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr.Code)
|
||||
}
|
||||
|
||||
func TestAddUserInvalidPermsMock(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.Permissions = []string{}
|
||||
userAsJSON := getUserAsJSON(t, user)
|
||||
req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr.Code)
|
||||
}
|
||||
|
||||
func TestAddUserInvalidJsonMock(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer([]byte("invalid json")))
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr.Code)
|
||||
}
|
||||
|
||||
func TestUpdateUserInvalidJsonMock(t *testing.T) {
|
||||
user := getTestUser()
|
||||
userAsJSON := getUserAsJSON(t, user)
|
||||
req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||
err := render.DecodeJSON(rr.Body, &user)
|
||||
if err != nil {
|
||||
t.Errorf("Error get user: %v", err)
|
||||
}
|
||||
req, _ = http.NewRequest(http.MethodPut, userPath+"/"+strconv.FormatInt(user.ID, 10), bytes.NewBuffer([]byte("Invalid json")))
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr.Code)
|
||||
req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||
}
|
||||
|
||||
func TestUpdateUserInvalidParamsMock(t *testing.T) {
|
||||
user := getTestUser()
|
||||
userAsJSON := getUserAsJSON(t, user)
|
||||
req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||
err := render.DecodeJSON(rr.Body, &user)
|
||||
if err != nil {
|
||||
t.Errorf("Error get user: %v", err)
|
||||
}
|
||||
user.HomeDir = ""
|
||||
userAsJSON = getUserAsJSON(t, user)
|
||||
req, _ = http.NewRequest(http.MethodPut, userPath+"/"+strconv.FormatInt(user.ID, 10), bytes.NewBuffer(userAsJSON))
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr.Code)
|
||||
userID := user.ID
|
||||
user.ID = 0
|
||||
userAsJSON = getUserAsJSON(t, user)
|
||||
req, _ = http.NewRequest(http.MethodPut, userPath+"/"+strconv.FormatInt(userID, 10), bytes.NewBuffer(userAsJSON))
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr.Code)
|
||||
user.ID = userID
|
||||
req, _ = http.NewRequest(http.MethodPut, userPath+"/0", bytes.NewBuffer(userAsJSON))
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr.Code)
|
||||
req, _ = http.NewRequest(http.MethodPut, userPath+"/a", bytes.NewBuffer(userAsJSON))
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr.Code)
|
||||
req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||
}
|
||||
|
||||
func TestGetUsersMock(t *testing.T) {
|
||||
user := getTestUser()
|
||||
userAsJSON := getUserAsJSON(t, user)
|
||||
req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||
err := render.DecodeJSON(rr.Body, &user)
|
||||
if err != nil {
|
||||
t.Errorf("Error get user: %v", err)
|
||||
}
|
||||
req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=510&offset=0&order=ASC&username="+defaultUsername, nil)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||
var users []dataprovider.User
|
||||
err = render.DecodeJSON(rr.Body, &users)
|
||||
if err != nil {
|
||||
t.Errorf("Error decoding users: %v", err)
|
||||
}
|
||||
if len(users) != 1 {
|
||||
t.Errorf("1 user is expected")
|
||||
}
|
||||
req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=a&offset=0&order=ASC", nil)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr.Code)
|
||||
req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=1&offset=a&order=ASC", nil)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr.Code)
|
||||
req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=1&offset=0&order=ASCa", nil)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr.Code)
|
||||
|
||||
req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||
}
|
||||
|
||||
func TestDeleteUserInvalidParamsMock(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodDelete, userPath+"/0", nil)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr.Code)
|
||||
req, _ = http.NewRequest(http.MethodDelete, userPath+"/a", nil)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr.Code)
|
||||
}
|
||||
|
||||
func TestGetQuotaScansMock(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", quotaScanPath, nil)
|
||||
if err != nil {
|
||||
t.Errorf("error get quota scan: %v", err)
|
||||
}
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||
}
|
||||
|
||||
func TestStartQuotaScanMock(t *testing.T) {
|
||||
user := getTestUser()
|
||||
userAsJSON := getUserAsJSON(t, user)
|
||||
req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||
err := render.DecodeJSON(rr.Body, &user)
|
||||
if err != nil {
|
||||
t.Errorf("Error get user: %v", err)
|
||||
}
|
||||
_, err = os.Stat(user.HomeDir)
|
||||
if err == nil {
|
||||
os.Remove(user.HomeDir)
|
||||
}
|
||||
// simulate a duplicate quota scan
|
||||
userAsJSON = getUserAsJSON(t, user)
|
||||
sftpd.AddQuotaScan(user.Username)
|
||||
req, _ = http.NewRequest(http.MethodPost, quotaScanPath, bytes.NewBuffer(userAsJSON))
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusConflict, rr.Code)
|
||||
sftpd.RemoveQuotaScan(user.Username)
|
||||
|
||||
userAsJSON = getUserAsJSON(t, user)
|
||||
req, _ = http.NewRequest(http.MethodPost, quotaScanPath, bytes.NewBuffer(userAsJSON))
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusCreated, rr.Code)
|
||||
|
||||
req, _ = http.NewRequest(http.MethodGet, quotaScanPath, nil)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||
var scans []sftpd.ActiveQuotaScan
|
||||
err = render.DecodeJSON(rr.Body, &scans)
|
||||
if err != nil {
|
||||
t.Errorf("Error get active scans: %v", err)
|
||||
}
|
||||
for len(scans) > 0 {
|
||||
req, _ = http.NewRequest(http.MethodGet, quotaScanPath, nil)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||
err = render.DecodeJSON(rr.Body, &scans)
|
||||
if err != nil {
|
||||
t.Errorf("Error get active scans: %v", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
_, err = os.Stat(user.HomeDir)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
os.MkdirAll(user.HomeDir, 0777)
|
||||
}
|
||||
req, _ = http.NewRequest(http.MethodPost, quotaScanPath, bytes.NewBuffer(userAsJSON))
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusCreated, rr.Code)
|
||||
req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||
}
|
||||
|
||||
func TestStartQuotaScanBadUserMock(t *testing.T) {
|
||||
user := getTestUser()
|
||||
userAsJSON := getUserAsJSON(t, user)
|
||||
req, _ := http.NewRequest(http.MethodPost, quotaScanPath, bytes.NewBuffer(userAsJSON))
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr.Code)
|
||||
}
|
||||
|
||||
func TestStartQuotaScanNonExistentUserMock(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodPost, quotaScanPath, bytes.NewBuffer([]byte("invalid json")))
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr.Code)
|
||||
}
|
||||
|
||||
func TestGetVersionMock(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodGet, versionPath, nil)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||
}
|
||||
|
||||
func TestGetConnectionsMock(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodGet, activeConnectionsPath, nil)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||
}
|
||||
|
||||
func TestDeleteActiveConnectionMock(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodDelete, activeConnectionsPath+"/connectionID", nil)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr.Code)
|
||||
}
|
||||
|
||||
func TestNotFoundMock(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodGet, "/non/existing/path", nil)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr.Code)
|
||||
}
|
||||
|
||||
func TestMethodNotAllowedMock(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodPost, activeConnectionsPath, nil)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusMethodNotAllowed, rr.Code)
|
||||
}
|
||||
|
||||
func waitTCPListening(address string) {
|
||||
for {
|
||||
conn, err := net.Dial("tcp", address)
|
||||
if err != nil {
|
||||
logger.WarnToConsole("tcp server %v not listening: %v\n", address, err)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
logger.InfoToConsole("tcp server %v now listening\n", address)
|
||||
defer conn.Close()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func getTestUser() dataprovider.User {
|
||||
return dataprovider.User{
|
||||
Username: defaultUsername,
|
||||
Password: defaultPassword,
|
||||
HomeDir: filepath.Join(homeBasePath, defaultUsername),
|
||||
Permissions: defaultPerms,
|
||||
}
|
||||
}
|
||||
|
||||
func getUserAsJSON(t *testing.T, user dataprovider.User) []byte {
|
||||
json, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
t.Errorf("error get user as json: %v", err)
|
||||
return []byte("{}")
|
||||
}
|
||||
return json
|
||||
}
|
||||
|
||||
func executeRequest(req *http.Request) *httptest.ResponseRecorder {
|
||||
rr := httptest.NewRecorder()
|
||||
testServer.Config.Handler.ServeHTTP(rr, req)
|
||||
return rr
|
||||
}
|
||||
|
||||
func checkResponseCode(t *testing.T, expected, actual int) {
|
||||
if expected != actual {
|
||||
t.Errorf("Expected response code %d. Got %d", expected, actual)
|
||||
}
|
||||
}
|
||||
330
api/api_utils.go
@@ -1,330 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/sftpd"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
var (
|
||||
httpBaseURL = "http://127.0.0.1:8080"
|
||||
)
|
||||
|
||||
// SetBaseURL sets the base url to use for HTTP requests, default is "http://127.0.0.1:8080"
|
||||
func SetBaseURL(url string) {
|
||||
httpBaseURL = url
|
||||
}
|
||||
|
||||
// gets an HTTP Client with a timeout
|
||||
func getHTTPClient() *http.Client {
|
||||
return &http.Client{
|
||||
Timeout: 15 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func buildURLRelativeToBase(paths ...string) string {
|
||||
// we need to use path.Join and not filepath.Join
|
||||
// since filepath.Join will use backslash separator on Windows
|
||||
p := path.Join(paths...)
|
||||
return fmt.Sprintf("%s/%s", strings.TrimRight(httpBaseURL, "/"), strings.TrimLeft(p, "/"))
|
||||
}
|
||||
|
||||
// AddUser adds a new user and checks the received HTTP Status code against expectedStatusCode.
|
||||
func AddUser(user dataprovider.User, expectedStatusCode int) (dataprovider.User, []byte, error) {
|
||||
var newUser dataprovider.User
|
||||
var body []byte
|
||||
userAsJSON, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
return newUser, body, err
|
||||
}
|
||||
resp, err := getHTTPClient().Post(buildURLRelativeToBase(userPath), "application/json", bytes.NewBuffer(userAsJSON))
|
||||
if err != nil {
|
||||
return newUser, body, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
err = checkResponse(resp.StatusCode, expectedStatusCode)
|
||||
if expectedStatusCode != http.StatusOK {
|
||||
body, _ = getResponseBody(resp)
|
||||
return newUser, body, err
|
||||
}
|
||||
if err == nil {
|
||||
err = render.DecodeJSON(resp.Body, &newUser)
|
||||
} else {
|
||||
body, _ = getResponseBody(resp)
|
||||
}
|
||||
if err == nil {
|
||||
err = checkUser(user, newUser)
|
||||
}
|
||||
return newUser, body, err
|
||||
}
|
||||
|
||||
// UpdateUser updates an existing user and checks the received HTTP Status code against expectedStatusCode.
|
||||
func UpdateUser(user dataprovider.User, expectedStatusCode int) (dataprovider.User, []byte, error) {
|
||||
var newUser dataprovider.User
|
||||
var body []byte
|
||||
userAsJSON, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
return user, body, err
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPut, buildURLRelativeToBase(userPath, strconv.FormatInt(user.ID, 10)),
|
||||
bytes.NewBuffer(userAsJSON))
|
||||
if err != nil {
|
||||
return user, body, err
|
||||
}
|
||||
resp, err := getHTTPClient().Do(req)
|
||||
if err != nil {
|
||||
return user, body, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ = getResponseBody(resp)
|
||||
err = checkResponse(resp.StatusCode, expectedStatusCode)
|
||||
if expectedStatusCode != http.StatusOK {
|
||||
return newUser, body, err
|
||||
}
|
||||
if err == nil {
|
||||
newUser, body, err = GetUserByID(user.ID, expectedStatusCode)
|
||||
}
|
||||
if err == nil {
|
||||
err = checkUser(user, newUser)
|
||||
}
|
||||
return newUser, body, err
|
||||
}
|
||||
|
||||
// RemoveUser removes an existing user and checks the received HTTP Status code against expectedStatusCode.
|
||||
func RemoveUser(user dataprovider.User, expectedStatusCode int) ([]byte, error) {
|
||||
var body []byte
|
||||
req, err := http.NewRequest(http.MethodDelete, buildURLRelativeToBase(userPath, strconv.FormatInt(user.ID, 10)), nil)
|
||||
if err != nil {
|
||||
return body, err
|
||||
}
|
||||
resp, err := getHTTPClient().Do(req)
|
||||
if err != nil {
|
||||
return body, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ = getResponseBody(resp)
|
||||
return body, checkResponse(resp.StatusCode, expectedStatusCode)
|
||||
}
|
||||
|
||||
// GetUserByID gets an user by database id and checks the received HTTP Status code against expectedStatusCode.
|
||||
func GetUserByID(userID int64, expectedStatusCode int) (dataprovider.User, []byte, error) {
|
||||
var user dataprovider.User
|
||||
var body []byte
|
||||
resp, err := getHTTPClient().Get(buildURLRelativeToBase(userPath, strconv.FormatInt(userID, 10)))
|
||||
if err != nil {
|
||||
return user, body, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
err = checkResponse(resp.StatusCode, expectedStatusCode)
|
||||
if err == nil && expectedStatusCode == http.StatusOK {
|
||||
err = render.DecodeJSON(resp.Body, &user)
|
||||
} else {
|
||||
body, _ = getResponseBody(resp)
|
||||
}
|
||||
return user, body, err
|
||||
}
|
||||
|
||||
// GetUsers allows to get a list of users and checks the received HTTP Status code against expectedStatusCode.
|
||||
// The number of results can be limited specifying a limit.
|
||||
// Some results can be skipped specifying an offset.
|
||||
// The results can be filtered specifying an username, the username filter is an exact match
|
||||
func GetUsers(limit int64, offset int64, username string, expectedStatusCode int) ([]dataprovider.User, []byte, error) {
|
||||
var users []dataprovider.User
|
||||
var body []byte
|
||||
url, err := url.Parse(buildURLRelativeToBase(userPath))
|
||||
if err != nil {
|
||||
return users, body, err
|
||||
}
|
||||
q := url.Query()
|
||||
if limit > 0 {
|
||||
q.Add("limit", strconv.FormatInt(limit, 10))
|
||||
}
|
||||
if offset > 0 {
|
||||
q.Add("offset", strconv.FormatInt(offset, 10))
|
||||
}
|
||||
if len(username) > 0 {
|
||||
q.Add("username", username)
|
||||
}
|
||||
url.RawQuery = q.Encode()
|
||||
resp, err := getHTTPClient().Get(url.String())
|
||||
if err != nil {
|
||||
return users, body, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
err = checkResponse(resp.StatusCode, expectedStatusCode)
|
||||
if err == nil && expectedStatusCode == http.StatusOK {
|
||||
err = render.DecodeJSON(resp.Body, &users)
|
||||
} else {
|
||||
body, _ = getResponseBody(resp)
|
||||
}
|
||||
return users, body, err
|
||||
}
|
||||
|
||||
// GetQuotaScans gets active quota scans and checks the received HTTP Status code against expectedStatusCode.
|
||||
func GetQuotaScans(expectedStatusCode int) ([]sftpd.ActiveQuotaScan, []byte, error) {
|
||||
var quotaScans []sftpd.ActiveQuotaScan
|
||||
var body []byte
|
||||
resp, err := getHTTPClient().Get(buildURLRelativeToBase(quotaScanPath))
|
||||
if err != nil {
|
||||
return quotaScans, body, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
err = checkResponse(resp.StatusCode, expectedStatusCode)
|
||||
if err == nil && expectedStatusCode == http.StatusOK {
|
||||
err = render.DecodeJSON(resp.Body, "aScans)
|
||||
} else {
|
||||
body, _ = getResponseBody(resp)
|
||||
}
|
||||
return quotaScans, body, err
|
||||
}
|
||||
|
||||
// StartQuotaScan start a new quota scan for the given user and checks the received HTTP Status code against expectedStatusCode.
|
||||
func StartQuotaScan(user dataprovider.User, expectedStatusCode int) ([]byte, error) {
|
||||
var body []byte
|
||||
userAsJSON, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
return body, err
|
||||
}
|
||||
resp, err := getHTTPClient().Post(buildURLRelativeToBase(quotaScanPath), "application/json", bytes.NewBuffer(userAsJSON))
|
||||
if err != nil {
|
||||
return body, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ = getResponseBody(resp)
|
||||
return body, checkResponse(resp.StatusCode, expectedStatusCode)
|
||||
}
|
||||
|
||||
// GetConnections returns status and stats for active SFTP/SCP connections
|
||||
func GetConnections(expectedStatusCode int) ([]sftpd.ConnectionStatus, []byte, error) {
|
||||
var connections []sftpd.ConnectionStatus
|
||||
var body []byte
|
||||
resp, err := getHTTPClient().Get(buildURLRelativeToBase(activeConnectionsPath))
|
||||
if err != nil {
|
||||
return connections, body, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
err = checkResponse(resp.StatusCode, expectedStatusCode)
|
||||
if err == nil && expectedStatusCode == http.StatusOK {
|
||||
err = render.DecodeJSON(resp.Body, &connections)
|
||||
} else {
|
||||
body, _ = getResponseBody(resp)
|
||||
}
|
||||
return connections, body, err
|
||||
}
|
||||
|
||||
// CloseConnection closes an active connection identified by connectionID
|
||||
func CloseConnection(connectionID string, expectedStatusCode int) ([]byte, error) {
|
||||
var body []byte
|
||||
req, err := http.NewRequest(http.MethodDelete, buildURLRelativeToBase(activeConnectionsPath, connectionID), nil)
|
||||
if err != nil {
|
||||
return body, err
|
||||
}
|
||||
resp, err := getHTTPClient().Do(req)
|
||||
if err != nil {
|
||||
return body, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
err = checkResponse(resp.StatusCode, expectedStatusCode)
|
||||
body, _ = getResponseBody(resp)
|
||||
return body, err
|
||||
}
|
||||
|
||||
// GetVersion returns version details
|
||||
func GetVersion(expectedStatusCode int) (utils.VersionInfo, []byte, error) {
|
||||
var version utils.VersionInfo
|
||||
var body []byte
|
||||
resp, err := getHTTPClient().Get(buildURLRelativeToBase(versionPath))
|
||||
if err != nil {
|
||||
return version, body, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
err = checkResponse(resp.StatusCode, expectedStatusCode)
|
||||
if err == nil && expectedStatusCode == http.StatusOK {
|
||||
err = render.DecodeJSON(resp.Body, &version)
|
||||
} else {
|
||||
body, _ = getResponseBody(resp)
|
||||
}
|
||||
return version, body, err
|
||||
}
|
||||
|
||||
func checkResponse(actual int, expected int) error {
|
||||
if expected != actual {
|
||||
return fmt.Errorf("wrong status code: got %v want %v", actual, expected)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getResponseBody(resp *http.Response) ([]byte, error) {
|
||||
return ioutil.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
func checkUser(expected dataprovider.User, actual dataprovider.User) error {
|
||||
if len(actual.Password) > 0 {
|
||||
return errors.New("User password must not be visible")
|
||||
}
|
||||
if len(actual.PublicKeys) > 0 {
|
||||
return errors.New("User public keys must not be visible")
|
||||
}
|
||||
if expected.ID <= 0 {
|
||||
if actual.ID <= 0 {
|
||||
return errors.New("actual user ID must be > 0")
|
||||
}
|
||||
} else {
|
||||
if actual.ID != expected.ID {
|
||||
return errors.New("user ID mismatch")
|
||||
}
|
||||
}
|
||||
for _, v := range expected.Permissions {
|
||||
if !utils.IsStringInSlice(v, actual.Permissions) {
|
||||
return errors.New("Permissions contents mismatch")
|
||||
}
|
||||
}
|
||||
return compareEqualsUserFields(expected, actual)
|
||||
}
|
||||
|
||||
func compareEqualsUserFields(expected dataprovider.User, actual dataprovider.User) error {
|
||||
if expected.Username != actual.Username {
|
||||
return errors.New("Username mismatch")
|
||||
}
|
||||
if expected.HomeDir != actual.HomeDir {
|
||||
return errors.New("HomeDir mismatch")
|
||||
}
|
||||
if expected.UID != actual.UID {
|
||||
return errors.New("UID mismatch")
|
||||
}
|
||||
if expected.GID != actual.GID {
|
||||
return errors.New("GID mismatch")
|
||||
}
|
||||
if expected.MaxSessions != actual.MaxSessions {
|
||||
return errors.New("MaxSessions mismatch")
|
||||
}
|
||||
if expected.QuotaSize != actual.QuotaSize {
|
||||
return errors.New("QuotaSize mismatch")
|
||||
}
|
||||
if expected.QuotaFiles != actual.QuotaFiles {
|
||||
return errors.New("QuotaFiles mismatch")
|
||||
}
|
||||
if len(expected.Permissions) != len(actual.Permissions) {
|
||||
return errors.New("Permissions mismatch")
|
||||
}
|
||||
if expected.UploadBandwidth != actual.UploadBandwidth {
|
||||
return errors.New("UploadBandwidth mismatch")
|
||||
}
|
||||
if expected.DownloadBandwidth != actual.DownloadBandwidth {
|
||||
return errors.New("DownloadBandwidth mismatch")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
const (
|
||||
invalidURL = "http://foo\x7f.com/"
|
||||
inactiveURL = "http://127.0.0.1:12345"
|
||||
)
|
||||
|
||||
func TestGetRespStatus(t *testing.T) {
|
||||
var err error
|
||||
err = &dataprovider.MethodDisabledError{}
|
||||
respStatus := getRespStatus(err)
|
||||
if respStatus != http.StatusForbidden {
|
||||
t.Errorf("wrong resp status extected: %d got: %d", http.StatusForbidden, respStatus)
|
||||
}
|
||||
err = fmt.Errorf("generic error")
|
||||
respStatus = getRespStatus(err)
|
||||
if respStatus != http.StatusInternalServerError {
|
||||
t.Errorf("wrong resp status extected: %d got: %d", http.StatusInternalServerError, respStatus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckResponse(t *testing.T) {
|
||||
err := checkResponse(http.StatusOK, http.StatusCreated)
|
||||
if err == nil {
|
||||
t.Errorf("check must fail")
|
||||
}
|
||||
err = checkResponse(http.StatusBadRequest, http.StatusBadRequest)
|
||||
if err != nil {
|
||||
t.Errorf("test must succeed, error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckUser(t *testing.T) {
|
||||
expected := dataprovider.User{}
|
||||
actual := dataprovider.User{}
|
||||
actual.Password = "password"
|
||||
err := checkUser(expected, actual)
|
||||
if err == nil {
|
||||
t.Errorf("actual password must be nil")
|
||||
}
|
||||
actual.Password = ""
|
||||
actual.PublicKeys = []string{"pub key"}
|
||||
err = checkUser(expected, actual)
|
||||
if err == nil {
|
||||
t.Errorf("actual public key must be nil")
|
||||
}
|
||||
actual.PublicKeys = []string{}
|
||||
err = checkUser(expected, actual)
|
||||
if err == nil {
|
||||
t.Errorf("actual ID must be > 0")
|
||||
}
|
||||
expected.ID = 1
|
||||
actual.ID = 2
|
||||
err = checkUser(expected, actual)
|
||||
if err == nil {
|
||||
t.Errorf("actual ID must be equal to expected ID")
|
||||
}
|
||||
expected.ID = 2
|
||||
actual.ID = 2
|
||||
expected.Permissions = []string{dataprovider.PermCreateDirs, dataprovider.PermDelete, dataprovider.PermDownload}
|
||||
actual.Permissions = []string{dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks}
|
||||
err = checkUser(expected, actual)
|
||||
if err == nil {
|
||||
t.Errorf("Permissions are not equal")
|
||||
}
|
||||
expected.Permissions = append(expected.Permissions, dataprovider.PermRename)
|
||||
err = checkUser(expected, actual)
|
||||
if err == nil {
|
||||
t.Errorf("Permissions are not equal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareUserFields(t *testing.T) {
|
||||
expected := dataprovider.User{}
|
||||
actual := dataprovider.User{}
|
||||
expected.Username = "test"
|
||||
err := compareEqualsUserFields(expected, actual)
|
||||
if err == nil {
|
||||
t.Errorf("Username does not match")
|
||||
}
|
||||
expected.Username = ""
|
||||
expected.HomeDir = "homedir"
|
||||
err = compareEqualsUserFields(expected, actual)
|
||||
if err == nil {
|
||||
t.Errorf("HomeDir does not match")
|
||||
}
|
||||
expected.HomeDir = ""
|
||||
expected.UID = 1
|
||||
err = compareEqualsUserFields(expected, actual)
|
||||
if err == nil {
|
||||
t.Errorf("UID does not match")
|
||||
}
|
||||
expected.UID = 0
|
||||
expected.GID = 1
|
||||
err = compareEqualsUserFields(expected, actual)
|
||||
if err == nil {
|
||||
t.Errorf("GID does not match")
|
||||
}
|
||||
expected.GID = 0
|
||||
expected.MaxSessions = 2
|
||||
err = compareEqualsUserFields(expected, actual)
|
||||
if err == nil {
|
||||
t.Errorf("MaxSessions do not match")
|
||||
}
|
||||
expected.MaxSessions = 0
|
||||
expected.QuotaSize = 4096
|
||||
err = compareEqualsUserFields(expected, actual)
|
||||
if err == nil {
|
||||
t.Errorf("QuotaSize does not match")
|
||||
}
|
||||
expected.QuotaSize = 0
|
||||
expected.QuotaFiles = 2
|
||||
err = compareEqualsUserFields(expected, actual)
|
||||
if err == nil {
|
||||
t.Errorf("QuotaFiles do not match")
|
||||
}
|
||||
expected.QuotaFiles = 0
|
||||
expected.Permissions = []string{dataprovider.PermCreateDirs}
|
||||
err = compareEqualsUserFields(expected, actual)
|
||||
if err == nil {
|
||||
t.Errorf("Permissions are not equal")
|
||||
}
|
||||
expected.Permissions = nil
|
||||
expected.UploadBandwidth = 64
|
||||
err = compareEqualsUserFields(expected, actual)
|
||||
if err == nil {
|
||||
t.Errorf("UploadBandwidth does not match")
|
||||
}
|
||||
expected.UploadBandwidth = 0
|
||||
expected.DownloadBandwidth = 128
|
||||
err = compareEqualsUserFields(expected, actual)
|
||||
if err == nil {
|
||||
t.Errorf("DownloadBandwidth does not match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApiCallsWithBadURL(t *testing.T) {
|
||||
oldBaseURL := httpBaseURL
|
||||
SetBaseURL(invalidURL)
|
||||
u := dataprovider.User{}
|
||||
_, _, err := UpdateUser(u, http.StatusBadRequest)
|
||||
if err == nil {
|
||||
t.Errorf("request with invalid URL must fail")
|
||||
}
|
||||
_, err = RemoveUser(u, http.StatusNotFound)
|
||||
if err == nil {
|
||||
t.Errorf("request with invalid URL must fail")
|
||||
}
|
||||
_, _, err = GetUsers(1, 0, "", http.StatusBadRequest)
|
||||
if err == nil {
|
||||
t.Errorf("request with invalid URL must fail")
|
||||
}
|
||||
_, err = CloseConnection("non_existent_id", http.StatusNotFound)
|
||||
if err == nil {
|
||||
t.Errorf("request with invalid URL must fail")
|
||||
}
|
||||
SetBaseURL(oldBaseURL)
|
||||
}
|
||||
|
||||
func TestApiCallToNotListeningServer(t *testing.T) {
|
||||
oldBaseURL := httpBaseURL
|
||||
SetBaseURL(inactiveURL)
|
||||
u := dataprovider.User{}
|
||||
_, _, err := AddUser(u, http.StatusBadRequest)
|
||||
if err == nil {
|
||||
t.Errorf("request to an inactive URL must fail")
|
||||
}
|
||||
_, _, err = UpdateUser(u, http.StatusNotFound)
|
||||
if err == nil {
|
||||
t.Errorf("request to an inactive URL must fail")
|
||||
}
|
||||
_, err = RemoveUser(u, http.StatusNotFound)
|
||||
if err == nil {
|
||||
t.Errorf("request to an inactive URL must fail")
|
||||
}
|
||||
_, _, err = GetUserByID(-1, http.StatusNotFound)
|
||||
if err == nil {
|
||||
t.Errorf("request to an inactive URL must fail")
|
||||
}
|
||||
_, _, err = GetUsers(100, 0, "", http.StatusOK)
|
||||
if err == nil {
|
||||
t.Errorf("request to an inactive URL must fail")
|
||||
}
|
||||
_, _, err = GetQuotaScans(http.StatusOK)
|
||||
if err == nil {
|
||||
t.Errorf("request to an inactive URL must fail")
|
||||
}
|
||||
_, err = StartQuotaScan(u, http.StatusNotFound)
|
||||
if err == nil {
|
||||
t.Errorf("request to an inactive URL must fail")
|
||||
}
|
||||
_, _, err = GetConnections(http.StatusOK)
|
||||
if err == nil {
|
||||
t.Errorf("request to an inactive URL must fail")
|
||||
}
|
||||
_, err = CloseConnection("non_existent_id", http.StatusNotFound)
|
||||
if err == nil {
|
||||
t.Errorf("request to an inactive URL must fail")
|
||||
}
|
||||
_, _, err = GetVersion(http.StatusOK)
|
||||
if err == nil {
|
||||
t.Errorf("request to an inactive URL must fail")
|
||||
}
|
||||
SetBaseURL(oldBaseURL)
|
||||
}
|
||||
|
||||
func TestCloseConnectionHandler(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodDelete, activeConnectionsPath+"/connectionID", nil)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("connectionID", "")
|
||||
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
|
||||
rr := httptest.NewRecorder()
|
||||
handleCloseConnection(rr, req)
|
||||
if rr.Code != http.StatusBadRequest {
|
||||
t.Errorf("Expected response code 400. Got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
44
api/quota.go
@@ -1,44 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/sftpd"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
func getQuotaScans(w http.ResponseWriter, r *http.Request) {
|
||||
render.JSON(w, r, sftpd.GetQuotaScans())
|
||||
}
|
||||
|
||||
func startQuotaScan(w http.ResponseWriter, r *http.Request) {
|
||||
var u dataprovider.User
|
||||
err := render.DecodeJSON(r.Body, &u)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
user, err := dataprovider.UserExists(dataProvider, u.Username)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if sftpd.AddQuotaScan(user.Username) {
|
||||
sendAPIResponse(w, r, err, "Scan started", http.StatusCreated)
|
||||
go func() {
|
||||
numFiles, size, _, err := utils.ScanDirContents(user.HomeDir)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error scanning user home dir %v: %v", user.HomeDir, err)
|
||||
} else {
|
||||
err := dataprovider.UpdateUserQuota(dataProvider, user, numFiles, size, true)
|
||||
logger.Debug(logSender, "user dir scanned, user: %v, dir: %v, error: %v", user.Username, user.HomeDir, err)
|
||||
}
|
||||
sftpd.RemoveQuotaScan(user.Username)
|
||||
}()
|
||||
} else {
|
||||
sendAPIResponse(w, r, err, "Another scan is already in progress", http.StatusConflict)
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/sftpd"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
// GetHTTPRouter returns the configured HTTP handler
|
||||
func GetHTTPRouter() http.Handler {
|
||||
return router
|
||||
}
|
||||
|
||||
func initializeRouter() {
|
||||
router = chi.NewRouter()
|
||||
router.Use(middleware.RequestID)
|
||||
router.Use(middleware.RealIP)
|
||||
router.Use(logger.NewStructuredLogger(logger.GetLogger()))
|
||||
router.Use(middleware.Recoverer)
|
||||
|
||||
router.NotFound(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
sendAPIResponse(w, r, nil, "Not Found", http.StatusNotFound)
|
||||
}))
|
||||
|
||||
router.MethodNotAllowed(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
sendAPIResponse(w, r, nil, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}))
|
||||
|
||||
router.Get(versionPath, func(w http.ResponseWriter, r *http.Request) {
|
||||
render.JSON(w, r, utils.GetAppVersion())
|
||||
})
|
||||
|
||||
router.Get(activeConnectionsPath, func(w http.ResponseWriter, r *http.Request) {
|
||||
render.JSON(w, r, sftpd.GetConnectionsStats())
|
||||
})
|
||||
|
||||
router.Delete(activeConnectionsPath+"/{connectionID}", func(w http.ResponseWriter, r *http.Request) {
|
||||
handleCloseConnection(w, r)
|
||||
})
|
||||
|
||||
router.Get(quotaScanPath, func(w http.ResponseWriter, r *http.Request) {
|
||||
getQuotaScans(w, r)
|
||||
})
|
||||
|
||||
router.Post(quotaScanPath, func(w http.ResponseWriter, r *http.Request) {
|
||||
startQuotaScan(w, r)
|
||||
})
|
||||
|
||||
router.Get(userPath, func(w http.ResponseWriter, r *http.Request) {
|
||||
getUsers(w, r)
|
||||
})
|
||||
|
||||
router.Post(userPath, func(w http.ResponseWriter, r *http.Request) {
|
||||
addUser(w, r)
|
||||
})
|
||||
|
||||
router.Get(userPath+"/{userID}", func(w http.ResponseWriter, r *http.Request) {
|
||||
getUserByID(w, r)
|
||||
})
|
||||
|
||||
router.Put(userPath+"/{userID}", func(w http.ResponseWriter, r *http.Request) {
|
||||
updateUser(w, r)
|
||||
})
|
||||
|
||||
router.Delete(userPath+"/{userID}", func(w http.ResponseWriter, r *http.Request) {
|
||||
deleteUser(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func handleCloseConnection(w http.ResponseWriter, r *http.Request) {
|
||||
connectionID := chi.URLParam(r, "connectionID")
|
||||
if connectionID == "" {
|
||||
sendAPIResponse(w, r, nil, "connectionID is mandatory", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if sftpd.CloseActiveConnection(connectionID) {
|
||||
sendAPIResponse(w, r, nil, "Connection closed", http.StatusOK)
|
||||
} else {
|
||||
sendAPIResponse(w, r, nil, "Not Found", http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
@@ -1,689 +0,0 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: SFTPGo
|
||||
description: 'SFTPGo REST API'
|
||||
version: 1.0.0
|
||||
|
||||
servers:
|
||||
- url: /api/v1
|
||||
paths:
|
||||
/version:
|
||||
get:
|
||||
tags:
|
||||
- version
|
||||
summary: Get version details
|
||||
operationId: get_version
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref : '#/components/schemas/VersionInfo'
|
||||
/connection:
|
||||
get:
|
||||
tags:
|
||||
- connections
|
||||
summary: Get the active users and info about their uploads/downloads
|
||||
operationId: get_connections
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref : '#/components/schemas/ConnectionStatus'
|
||||
/connection/{connectionID}:
|
||||
delete:
|
||||
tags:
|
||||
- connections
|
||||
summary: Terminate an active connection
|
||||
operationId: close_connection
|
||||
parameters:
|
||||
- name: connectionID
|
||||
in: path
|
||||
description: ID of the connection to close
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 200
|
||||
message: "Connection closed"
|
||||
error: ""
|
||||
400:
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 400
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
404:
|
||||
description: Not Found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 404
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
500:
|
||||
description: Internal Server Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 500
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
/quota_scan:
|
||||
get:
|
||||
tags:
|
||||
- quota
|
||||
summary: Get the active quota scans
|
||||
operationId: get_quota_scans
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref : '#/components/schemas/QuotaScan'
|
||||
post:
|
||||
tags:
|
||||
- quota
|
||||
summary: start a new quota scan
|
||||
description: A quota scan update the number of files and their total size for the given user
|
||||
operationId: start_quota_scan
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref : '#/components/schemas/User'
|
||||
responses:
|
||||
201:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 201
|
||||
message: "Scan started"
|
||||
error: ""
|
||||
400:
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 400
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
403:
|
||||
description: Forbidden
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 403
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
404:
|
||||
description: Not Found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 404
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
409:
|
||||
description: Another scan is already in progress for this user
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 409
|
||||
message: "Another scan is already in progress"
|
||||
error: "Error description if any"
|
||||
500:
|
||||
description: Internal Server Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 500
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
/user:
|
||||
get:
|
||||
tags:
|
||||
- users
|
||||
summary: Returns an array with one or more users
|
||||
description: For security reasons password and public key are empty in the response
|
||||
operationId: get_users
|
||||
parameters:
|
||||
- in: query
|
||||
name: offset
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
default: 0
|
||||
required: false
|
||||
- in: query
|
||||
name: limit
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 500
|
||||
default: 100
|
||||
required: false
|
||||
description: The maximum number of items to return. Max value is 500, default is 100
|
||||
- in: query
|
||||
name: order
|
||||
required: false
|
||||
description: Ordering users by username
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- ASC
|
||||
- DESC
|
||||
example: ASC
|
||||
- in: query
|
||||
name: username
|
||||
required: false
|
||||
description: Filter by username, extact match case sensitive
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref : '#/components/schemas/User'
|
||||
400:
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 400
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
403:
|
||||
description: Forbidden
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 403
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
500:
|
||||
description: Internal Server Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 500
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
post:
|
||||
tags:
|
||||
- users
|
||||
summary: Adds a new SFTP/SCP user
|
||||
operationId: add_user
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref : '#/components/schemas/User'
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref : '#/components/schemas/User'
|
||||
400:
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 400
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
403:
|
||||
description: Forbidden
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 403
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
500:
|
||||
description: Internal Server Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 500
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
/user/{userID}:
|
||||
get:
|
||||
tags:
|
||||
- users
|
||||
summary: Find user by ID
|
||||
description: For security reasons password and public key are empty in the response
|
||||
operationId: get_user_by_id
|
||||
parameters:
|
||||
- name: userID
|
||||
in: path
|
||||
description: ID of the user to retrieve
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref : '#/components/schemas/User'
|
||||
400:
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 400
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
403:
|
||||
description: Forbidden
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 403
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
404:
|
||||
description: Not Found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 404
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
500:
|
||||
description: Internal Server Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 500
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
put:
|
||||
tags:
|
||||
- users
|
||||
summary: Update an existing user
|
||||
operationId: update_user
|
||||
parameters:
|
||||
- name: userID
|
||||
in: path
|
||||
description: ID of the user to update
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref : '#/components/schemas/User'
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref : '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 200
|
||||
message: "User updated"
|
||||
error: ""
|
||||
400:
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 400
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
403:
|
||||
description: Forbidden
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 403
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
404:
|
||||
description: Not Found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 404
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
500:
|
||||
description: Internal Server Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 500
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
delete:
|
||||
tags:
|
||||
- users
|
||||
summary: Delete an existing user
|
||||
operationId: delete_user
|
||||
parameters:
|
||||
- name: userID
|
||||
in: path
|
||||
description: ID of the user to delete
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref : '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 200
|
||||
message: "User deleted"
|
||||
error: ""
|
||||
400:
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 400
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
403:
|
||||
description: Forbidden
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 403
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
404:
|
||||
description: Not Found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 404
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
500:
|
||||
description: Internal Server Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
status: 500
|
||||
message: ""
|
||||
error: "Error description if any"
|
||||
components:
|
||||
schemas:
|
||||
Permission:
|
||||
type: string
|
||||
enum:
|
||||
- '*'
|
||||
- list
|
||||
- download
|
||||
- upload
|
||||
- delete
|
||||
- rename
|
||||
- create_dirs
|
||||
- create_symlinks
|
||||
description: >
|
||||
Permissions:
|
||||
* `*` - all permission are granted
|
||||
* `list` - list items is allowed
|
||||
* `download` - download files is allowed
|
||||
* `upload` - upload files is allowed
|
||||
* `delete` - delete files or directories is allowed
|
||||
* `rename` - rename files or directories is allowed
|
||||
* `create_dirs` - create directories is allowed
|
||||
* `create_symlinks` - create links is allowed
|
||||
User:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int32
|
||||
minimum: 1
|
||||
username:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
nullable: true
|
||||
description: password or public key are mandatory. If the password has no known hashing algo prefix it will be stored using argon2id. You can send a password hashed as bcrypt or pbkdf2 and it will be stored as is. For security reasons this field is omitted when you search/get users
|
||||
public_keys:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
description: a password or at least one public key are mandatory. For security reasons this field is omitted when you search/get users.
|
||||
home_dir:
|
||||
type: string
|
||||
description: path to the user home directory. The user cannot upload or download files outside this directory. SFTPGo tries to automatically create this folder if missing. Must be an absolute path
|
||||
uid:
|
||||
type: integer
|
||||
format: int32
|
||||
minimum: 0
|
||||
maximum: 65535
|
||||
description: if you run sftpgo as root user the created files and directories will be assigned to this uid. 0 means no change, the owner will be the user that runs sftpgo. Ignored on windows
|
||||
gid:
|
||||
type: integer
|
||||
format: int32
|
||||
minimum: 0
|
||||
maximum: 65535
|
||||
description: if you run sftpgo as root user the created files and directories will be assigned to this gid. 0 means no change, the group will be the one of the user that runs sftpgo. Ignored on windows
|
||||
max_sessions:
|
||||
type: integer
|
||||
format: int32
|
||||
description: limit the sessions that an user can open. 0 means unlimited
|
||||
quota_size:
|
||||
type: integer
|
||||
format: int64
|
||||
description: quota as size. 0 menas unlimited. Please note that quota is updated if files are added/removed via SFTP/SCP otherwise a quota scan is needed
|
||||
quota_files:
|
||||
type: integer
|
||||
format: int32
|
||||
description: quota as number of files. 0 menas unlimited. Please note that quota is updated if files are added/removed via SFTP/SCP otherwise a quota scan is needed
|
||||
permissions:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Permission'
|
||||
minItems: 1
|
||||
used_quota_size:
|
||||
type: integer
|
||||
format: int64
|
||||
used_quota_file:
|
||||
type: integer
|
||||
format: int32
|
||||
last_quota_update:
|
||||
type: integer
|
||||
format: int64
|
||||
description: last quota update as unix timestamp in milliseconds
|
||||
upload_bandwidth:
|
||||
type: integer
|
||||
format: int32
|
||||
description: Maximum upload bandwidth as KB/s, 0 means unlimited
|
||||
download_bandwidth:
|
||||
type: integer
|
||||
format: int32
|
||||
description: Maximum download bandwidth as KB/s, 0 means unlimited
|
||||
Transfer:
|
||||
type: object
|
||||
properties:
|
||||
operation_type:
|
||||
type: string
|
||||
enum:
|
||||
- upload
|
||||
- download
|
||||
path:
|
||||
type: string
|
||||
description: SFTP/SCP file path for the upload/download
|
||||
start_time:
|
||||
type: integer
|
||||
format: int64
|
||||
description: start time as unix timestamp in milliseconds
|
||||
size:
|
||||
type: integer
|
||||
format: int64
|
||||
description: bytes transferred
|
||||
last_activity:
|
||||
type: integer
|
||||
format: int64
|
||||
description: last transfer activity as unix timestamp in milliseconds
|
||||
ConnectionStatus:
|
||||
type: object
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
description: connected username
|
||||
connection_id:
|
||||
type: string
|
||||
description: unique connection identifier
|
||||
client_version:
|
||||
type: string
|
||||
description: SFTP/SCP client version
|
||||
remote_address:
|
||||
type: string
|
||||
description: Remote address for the connected SFTP/SCP client
|
||||
connection_time:
|
||||
type: integer
|
||||
format: int64
|
||||
description: connection time as unix timestamp in milliseconds
|
||||
last_activity:
|
||||
type: integer
|
||||
format: int64
|
||||
description: last client activity as unix timestamp in milliseconds
|
||||
protocol:
|
||||
type: string
|
||||
enum:
|
||||
- SFTP
|
||||
- SCP
|
||||
active_transfers:
|
||||
type: array
|
||||
items:
|
||||
$ref : '#/components/schemas/Transfer'
|
||||
QuotaScan:
|
||||
type: object
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
description: username with an active scan
|
||||
start_time:
|
||||
type: integer
|
||||
format: int64
|
||||
description: scan start time as unix timestamp in milliseconds
|
||||
ApiResponse:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
format: int32
|
||||
minimum: 200
|
||||
maximum: 500
|
||||
example: 200
|
||||
description: HTTP Status code, for example 200 OK, 400 Bad request and so on
|
||||
message:
|
||||
type: string
|
||||
nullable: true
|
||||
description: additional message if any
|
||||
error:
|
||||
type: string
|
||||
nullable: true
|
||||
description: error description if any
|
||||
VersionInfo:
|
||||
type: object
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
build_date:
|
||||
type: string
|
||||
commit_hash:
|
||||
type: string
|
||||
|
||||
151
api/user.go
@@ -1,151 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
func getUsers(w http.ResponseWriter, r *http.Request) {
|
||||
limit := 100
|
||||
offset := 0
|
||||
order := "ASC"
|
||||
username := ""
|
||||
var err error
|
||||
if _, ok := r.URL.Query()["limit"]; ok {
|
||||
limit, err = strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
if err != nil {
|
||||
err = errors.New("Invalid limit")
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if limit > 500 {
|
||||
limit = 500
|
||||
}
|
||||
}
|
||||
if _, ok := r.URL.Query()["offset"]; ok {
|
||||
offset, err = strconv.Atoi(r.URL.Query().Get("offset"))
|
||||
if err != nil {
|
||||
err = errors.New("Invalid offset")
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
if _, ok := r.URL.Query()["order"]; ok {
|
||||
order = r.URL.Query().Get("order")
|
||||
if order != "ASC" && order != "DESC" {
|
||||
err = errors.New("Invalid order")
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
if _, ok := r.URL.Query()["username"]; ok {
|
||||
username = r.URL.Query().Get("username")
|
||||
}
|
||||
users, err := dataprovider.GetUsers(dataProvider, limit, offset, order, username)
|
||||
if err == nil {
|
||||
render.JSON(w, r, users)
|
||||
} else {
|
||||
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func getUserByID(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64)
|
||||
if err != nil {
|
||||
err = errors.New("Invalid userID")
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
user, err := dataprovider.GetUserByID(dataProvider, userID)
|
||||
if err == nil {
|
||||
user.Password = ""
|
||||
user.PublicKeys = []string{}
|
||||
render.JSON(w, r, user)
|
||||
} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
||||
sendAPIResponse(w, r, err, "", http.StatusNotFound)
|
||||
} else {
|
||||
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func addUser(w http.ResponseWriter, r *http.Request) {
|
||||
var user dataprovider.User
|
||||
err := render.DecodeJSON(r.Body, &user)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = dataprovider.AddUser(dataProvider, user)
|
||||
if err == nil {
|
||||
user, err = dataprovider.UserExists(dataProvider, user.Username)
|
||||
if err == nil {
|
||||
user.Password = ""
|
||||
user.PublicKeys = []string{}
|
||||
render.JSON(w, r, user)
|
||||
} else {
|
||||
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
|
||||
}
|
||||
} else {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
}
|
||||
}
|
||||
|
||||
func updateUser(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64)
|
||||
if err != nil {
|
||||
err = errors.New("Invalid userID")
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
user, err := dataprovider.GetUserByID(dataProvider, userID)
|
||||
if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
||||
sendAPIResponse(w, r, err, "", http.StatusNotFound)
|
||||
return
|
||||
} else if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err = render.DecodeJSON(r.Body, &user)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if user.ID != userID {
|
||||
sendAPIResponse(w, r, err, "user ID in request body does not match user ID in path parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = dataprovider.UpdateUser(dataProvider, user)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
} else {
|
||||
sendAPIResponse(w, r, err, "User updated", http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64)
|
||||
if err != nil {
|
||||
err = errors.New("Invalid userID")
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
user, err := dataprovider.GetUserByID(dataProvider, userID)
|
||||
if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
||||
sendAPIResponse(w, r, err, "", http.StatusNotFound)
|
||||
return
|
||||
} else if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err = dataprovider.DeleteUser(dataProvider, user)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
|
||||
} else {
|
||||
sendAPIResponse(w, r, err, "User deleted", http.StatusOK)
|
||||
}
|
||||
}
|
||||
37
cmd/root.go
@@ -1,37 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
logSender = "cmd"
|
||||
)
|
||||
|
||||
var (
|
||||
rootCmd = &cobra.Command{
|
||||
Use: "sftpgo",
|
||||
Short: "Full featured and highly configurable SFTP server",
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
version := utils.GetAppVersion()
|
||||
rootCmd.Flags().BoolP("version", "v", false, "")
|
||||
rootCmd.Version = version.GetVersionAsString()
|
||||
rootCmd.SetVersionTemplate(`{{printf "SFTPGo version: "}}{{printf "%s" .Version}}
|
||||
`)
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
181
cmd/serve.go
@@ -1,181 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/drakkan/sftpgo/api"
|
||||
"github.com/drakkan/sftpgo/config"
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/sftpd"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
configDirFlag = "config-dir"
|
||||
configDirKey = "config_dir"
|
||||
configFileFlag = "config-file"
|
||||
configFileKey = "config_file"
|
||||
logFilePathFlag = "log-file-path"
|
||||
logFilePathKey = "log_file_path"
|
||||
logMaxSizeFlag = "log-max-size"
|
||||
logMaxSizeKey = "log_max_size"
|
||||
logMaxBackupFlag = "log-max-backups"
|
||||
logMaxBackupKey = "log_max_backups"
|
||||
logMaxAgeFlag = "log-max-age"
|
||||
logMaxAgeKey = "log_max_age"
|
||||
logCompressFlag = "log-compress"
|
||||
logCompressKey = "log_compress"
|
||||
logVerboseFlag = "log-verbose"
|
||||
logVerboseKey = "log_verbose"
|
||||
)
|
||||
|
||||
var (
|
||||
configDir string
|
||||
configFile string
|
||||
logFilePath string
|
||||
logMaxSize int
|
||||
logMaxBackups int
|
||||
logMaxAge int
|
||||
logCompress bool
|
||||
logVerbose bool
|
||||
testVar string
|
||||
serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Start the SFTP Server",
|
||||
Long: `To start the SFTP Server with the default values for the command line flags simply use:
|
||||
|
||||
sftpgo serve
|
||||
|
||||
Please take a look at the usage below to customize the startup options`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
startServe()
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(serveCmd)
|
||||
|
||||
viper.SetDefault(configDirKey, ".")
|
||||
viper.BindEnv(configDirKey, "SFTPGO_CONFIG_DIR")
|
||||
serveCmd.Flags().StringVarP(&configDir, configDirFlag, "c", viper.GetString(configDirKey),
|
||||
"Location for SFTPGo config dir. This directory should contain the \"sftpgo\" configuration file or the configured "+
|
||||
"config-file and it is used as the base for files with a relative path (eg. the private keys for the SFTP server, "+
|
||||
"the SQLite database if you use SQLite as data provider). This flag can be set using SFTPGO_CONFIG_DIR env var too.")
|
||||
viper.BindPFlag(configDirKey, serveCmd.Flags().Lookup(configDirFlag))
|
||||
|
||||
viper.SetDefault(configFileKey, config.DefaultConfigName)
|
||||
viper.BindEnv(configFileKey, "SFTPGO_CONFIG_FILE")
|
||||
serveCmd.Flags().StringVarP(&configFile, configFileFlag, "f", viper.GetString(configFileKey),
|
||||
"Name for SFTPGo configuration file. It must be the name of a file stored in config-dir not the absolute path to the "+
|
||||
"configuration file. The specified file name must have no extension we automatically load JSON, YAML, TOML, HCL and "+
|
||||
"Java properties. Therefore if you set \"sftpgo\" then \"sftpgo.json\", \"sftpgo.yaml\" and so on are searched. "+
|
||||
"This flag can be set using SFTPGO_CONFIG_FILE env var too.")
|
||||
viper.BindPFlag(configFileKey, serveCmd.Flags().Lookup(configFileFlag))
|
||||
|
||||
viper.SetDefault(logFilePathKey, "sftpgo.log")
|
||||
viper.BindEnv(logFilePathKey, "SFTPGO_LOG_FILE_PATH")
|
||||
serveCmd.Flags().StringVarP(&logFilePath, logFilePathFlag, "l", viper.GetString(logFilePathKey),
|
||||
"Location for the log file. This flag can be set using SFTPGO_LOG_FILE_PATH env var too.")
|
||||
viper.BindPFlag(logFilePathKey, serveCmd.Flags().Lookup(logFilePathFlag))
|
||||
|
||||
viper.SetDefault(logMaxSizeKey, 10)
|
||||
viper.BindEnv(logMaxSizeKey, "SFTPGO_LOG_MAX_SIZE")
|
||||
serveCmd.Flags().IntVarP(&logMaxSize, logMaxSizeFlag, "s", viper.GetInt(logMaxSizeKey),
|
||||
"Maximum size in megabytes of the log file before it gets rotated. This flag can be set using SFTPGO_LOG_MAX_SIZE "+
|
||||
"env var too.")
|
||||
viper.BindPFlag(logMaxSizeKey, serveCmd.Flags().Lookup(logMaxSizeFlag))
|
||||
|
||||
viper.SetDefault(logMaxBackupKey, 5)
|
||||
viper.BindEnv(logMaxBackupKey, "SFTPGO_LOG_MAX_BACKUPS")
|
||||
serveCmd.Flags().IntVarP(&logMaxBackups, "log-max-backups", "b", viper.GetInt(logMaxBackupKey),
|
||||
"Maximum number of old log files to retain. This flag can be set using SFTPGO_LOG_MAX_BACKUPS env var too.")
|
||||
viper.BindPFlag(logMaxBackupKey, serveCmd.Flags().Lookup(logMaxBackupFlag))
|
||||
|
||||
viper.SetDefault(logMaxAgeKey, 28)
|
||||
viper.BindEnv(logMaxAgeKey, "SFTPGO_LOG_MAX_AGE")
|
||||
serveCmd.Flags().IntVarP(&logMaxAge, "log-max-age", "a", viper.GetInt(logMaxAgeKey),
|
||||
"Maximum number of days to retain old log files. This flag can be set using SFTPGO_LOG_MAX_AGE env var too.")
|
||||
viper.BindPFlag(logMaxAgeKey, serveCmd.Flags().Lookup(logMaxAgeFlag))
|
||||
|
||||
viper.SetDefault(logCompressKey, false)
|
||||
viper.BindEnv(logCompressKey, "SFTPGO_LOG_COMPRESS")
|
||||
serveCmd.Flags().BoolVarP(&logCompress, logCompressFlag, "z", viper.GetBool(logCompressKey), "Determine if the rotated "+
|
||||
"log files should be compressed using gzip. This flag can be set using SFTPGO_LOG_COMPRESS env var too.")
|
||||
viper.BindPFlag(logCompressKey, serveCmd.Flags().Lookup(logCompressFlag))
|
||||
|
||||
viper.SetDefault(logVerboseKey, true)
|
||||
viper.BindEnv(logVerboseKey, "SFTPGO_LOG_VERBOSE")
|
||||
serveCmd.Flags().BoolVarP(&logVerbose, logVerboseFlag, "v", viper.GetBool(logVerboseKey), "Enable verbose logs. "+
|
||||
"This flag can be set using SFTPGO_LOG_VERBOSE env var too.")
|
||||
viper.BindPFlag(logVerboseKey, serveCmd.Flags().Lookup(logVerboseFlag))
|
||||
}
|
||||
|
||||
func startServe() {
|
||||
logLevel := zerolog.DebugLevel
|
||||
if !logVerbose {
|
||||
logLevel = zerolog.InfoLevel
|
||||
}
|
||||
logger.InitLogger(logFilePath, logMaxSize, logMaxBackups, logMaxAge, logCompress, logLevel)
|
||||
logger.Info(logSender, "starting SFTPGo, config dir: %v, config file: %v, log max size: %v log max backups: %v "+
|
||||
"log max age: %v log verbose: %v, log compress: %v", configDir, configFile, logMaxSize, logMaxBackups, logMaxAge,
|
||||
logVerbose, logCompress)
|
||||
config.LoadConfig(configDir, configFile)
|
||||
providerConf := config.GetProviderConf()
|
||||
|
||||
err := dataprovider.Initialize(providerConf, configDir)
|
||||
if err != nil {
|
||||
logger.Error(logSender, "error initializing data provider: %v", err)
|
||||
logger.ErrorToConsole("error initializing data provider: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
dataProvider := dataprovider.GetProvider()
|
||||
sftpdConf := config.GetSFTPDConfig()
|
||||
httpdConf := config.GetHTTPDConfig()
|
||||
|
||||
sftpd.SetDataProvider(dataProvider)
|
||||
|
||||
shutdown := make(chan bool)
|
||||
|
||||
go func() {
|
||||
logger.Debug(logSender, "initializing SFTP server with config %+v", sftpdConf)
|
||||
if err := sftpdConf.Initialize(configDir); err != nil {
|
||||
logger.Error(logSender, "could not start SFTP server: %v", err)
|
||||
logger.ErrorToConsole("could not start SFTP server: %v", err)
|
||||
}
|
||||
shutdown <- true
|
||||
}()
|
||||
|
||||
if httpdConf.BindPort > 0 {
|
||||
router := api.GetHTTPRouter()
|
||||
api.SetDataProvider(dataProvider)
|
||||
|
||||
go func() {
|
||||
logger.Debug(logSender, "initializing HTTP server with config %+v", httpdConf)
|
||||
s := &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort),
|
||||
Handler: router,
|
||||
ReadTimeout: 300 * time.Second,
|
||||
WriteTimeout: 300 * time.Second,
|
||||
MaxHeaderBytes: 1 << 20, // 1MB
|
||||
}
|
||||
if err := s.ListenAndServe(); err != nil {
|
||||
logger.Error(logSender, "could not start HTTP server: %v", err)
|
||||
logger.ErrorToConsole("could not start HTTP server: %v", err)
|
||||
}
|
||||
shutdown <- true
|
||||
}()
|
||||
} else {
|
||||
logger.Debug(logSender, "HTTP server not started, disabled in config file")
|
||||
logger.DebugToConsole("HTTP server not started, disabled in config file")
|
||||
}
|
||||
|
||||
<-shutdown
|
||||
}
|
||||
134
config/config.go
@@ -1,134 +0,0 @@
|
||||
// Package config manages the configuration.
|
||||
// Configuration is loaded from sftpgo.conf file.
|
||||
// If sftpgo.conf is not found or cannot be readed or decoded as json the default configuration is used.
|
||||
// The default configuration an be found inside the source tree:
|
||||
// https://github.com/drakkan/sftpgo/blob/master/sftpgo.conf
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/drakkan/sftpgo/api"
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/sftpd"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
logSender = "config"
|
||||
defaultBanner = "SFTPGo"
|
||||
// DefaultConfigName defines the name for the default config file.
|
||||
// This is the file name without extension, we use viper and so we
|
||||
// support all the config files format supported by viper
|
||||
DefaultConfigName = "sftpgo"
|
||||
// ConfigEnvPrefix defines a prefix that ENVIRONMENT variables will use
|
||||
configEnvPrefix = "sftpgo"
|
||||
)
|
||||
|
||||
var (
|
||||
globalConf globalConfig
|
||||
)
|
||||
|
||||
type globalConfig struct {
|
||||
SFTPD sftpd.Configuration `json:"sftpd" mapstructure:"sftpd"`
|
||||
ProviderConf dataprovider.Config `json:"data_provider" mapstructure:"data_provider"`
|
||||
HTTPDConfig api.HTTPDConf `json:"httpd" mapstructure:"httpd"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
// create a default configuration to use if no config file is provided
|
||||
globalConf = globalConfig{
|
||||
SFTPD: sftpd.Configuration{
|
||||
Banner: defaultBanner,
|
||||
BindPort: 2022,
|
||||
BindAddress: "",
|
||||
IdleTimeout: 15,
|
||||
MaxAuthTries: 0,
|
||||
Umask: "0022",
|
||||
UploadMode: 0,
|
||||
Actions: sftpd.Actions{
|
||||
ExecuteOn: []string{},
|
||||
Command: "",
|
||||
HTTPNotificationURL: "",
|
||||
},
|
||||
Keys: []sftpd.Key{},
|
||||
IsSCPEnabled: false,
|
||||
},
|
||||
ProviderConf: dataprovider.Config{
|
||||
Driver: "sqlite",
|
||||
Name: "sftpgo.db",
|
||||
Host: "",
|
||||
Port: 5432,
|
||||
Username: "",
|
||||
Password: "",
|
||||
ConnectionString: "",
|
||||
UsersTable: "users",
|
||||
ManageUsers: 1,
|
||||
SSLMode: 0,
|
||||
TrackQuota: 1,
|
||||
},
|
||||
HTTPDConfig: api.HTTPDConf{
|
||||
BindPort: 8080,
|
||||
BindAddress: "127.0.0.1",
|
||||
},
|
||||
}
|
||||
|
||||
viper.SetEnvPrefix(configEnvPrefix)
|
||||
replacer := strings.NewReplacer(".", "__")
|
||||
viper.SetEnvKeyReplacer(replacer)
|
||||
viper.SetConfigName(DefaultConfigName)
|
||||
setViperAdditionalConfigPaths()
|
||||
viper.AddConfigPath(".")
|
||||
viper.AutomaticEnv()
|
||||
}
|
||||
|
||||
// GetSFTPDConfig returns the configuration for the SFTP server
|
||||
func GetSFTPDConfig() sftpd.Configuration {
|
||||
return globalConf.SFTPD
|
||||
}
|
||||
|
||||
// GetHTTPDConfig returns the configuration for the HTTP server
|
||||
func GetHTTPDConfig() api.HTTPDConf {
|
||||
return globalConf.HTTPDConfig
|
||||
}
|
||||
|
||||
//GetProviderConf returns the configuration for the data provider
|
||||
func GetProviderConf() dataprovider.Config {
|
||||
return globalConf.ProviderConf
|
||||
}
|
||||
|
||||
// LoadConfig loads the configuration
|
||||
// configDir will be added to the configuration search paths.
|
||||
// The search path contains by default the current directory and on linux it contains
|
||||
// $HOME/.config/sftpgo and /etc/sftpgo too.
|
||||
// configName is the name of the configuration to search without extension
|
||||
func LoadConfig(configDir, configName string) error {
|
||||
var err error
|
||||
viper.AddConfigPath(configDir)
|
||||
viper.SetConfigName(configName)
|
||||
if err = viper.ReadInConfig(); err != nil {
|
||||
logger.Warn(logSender, "error loading configuration file: %v. Default configuration will be used: %+v", err, globalConf)
|
||||
logger.WarnToConsole("error loading configuration file: %v. Default configuration will be used.", err)
|
||||
return err
|
||||
}
|
||||
err = viper.Unmarshal(&globalConf)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error parsing configuration file: %v. Default configuration will be used: %+v", err, globalConf)
|
||||
logger.WarnToConsole("error parsing configuration file: %v. Default configuration will be used.", err)
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(globalConf.SFTPD.Banner) == "" {
|
||||
globalConf.SFTPD.Banner = defaultBanner
|
||||
}
|
||||
if globalConf.SFTPD.UploadMode < 0 || globalConf.SFTPD.UploadMode > 1 {
|
||||
err = fmt.Errorf("Invalid upload_mode 0 and 1 are supported, configured: %v reset upload_mode to 0",
|
||||
globalConf.SFTPD.UploadMode)
|
||||
globalConf.SFTPD.UploadMode = 0
|
||||
logger.Warn(logSender, "Configuration error: %v", err)
|
||||
logger.WarnToConsole("Configuration error: %v", err)
|
||||
}
|
||||
logger.Debug(logSender, "config file used: '%v', config loaded: %+v", viper.ConfigFileUsed(), globalConf)
|
||||
return err
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// +build linux
|
||||
|
||||
package config
|
||||
|
||||
import "github.com/spf13/viper"
|
||||
|
||||
// linux specific config search path
|
||||
func setViperAdditionalConfigPaths() {
|
||||
viper.AddConfigPath("$HOME/.config/sftpgo")
|
||||
viper.AddConfigPath("/etc/sftpgo")
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// +build !linux
|
||||
|
||||
package config
|
||||
|
||||
func setViperAdditionalConfigPaths() {
|
||||
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/drakkan/sftpgo/api"
|
||||
"github.com/drakkan/sftpgo/config"
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/sftpd"
|
||||
)
|
||||
|
||||
const (
|
||||
tempConfigName = "temp"
|
||||
)
|
||||
|
||||
func TestLoadConfigTest(t *testing.T) {
|
||||
configDir := ".."
|
||||
err := config.LoadConfig(configDir, "")
|
||||
if err != nil {
|
||||
t.Errorf("error loading config")
|
||||
}
|
||||
emptyHTTPDConf := api.HTTPDConf{}
|
||||
if config.GetHTTPDConfig() == emptyHTTPDConf {
|
||||
t.Errorf("error loading httpd conf")
|
||||
}
|
||||
emptyProviderConf := dataprovider.Config{}
|
||||
if config.GetProviderConf() == emptyProviderConf {
|
||||
t.Errorf("error loading provider conf")
|
||||
}
|
||||
emptySFTPDConf := sftpd.Configuration{}
|
||||
if config.GetSFTPDConfig().BindPort == emptySFTPDConf.BindPort {
|
||||
t.Errorf("error loading SFTPD conf")
|
||||
}
|
||||
confName := tempConfigName + ".json"
|
||||
configFilePath := filepath.Join(configDir, confName)
|
||||
err = config.LoadConfig(configDir, tempConfigName)
|
||||
if err == nil {
|
||||
t.Errorf("loading a non existent config file must fail")
|
||||
}
|
||||
ioutil.WriteFile(configFilePath, []byte("{invalid json}"), 0666)
|
||||
err = config.LoadConfig(configDir, tempConfigName)
|
||||
if err == nil {
|
||||
t.Errorf("loading an invalid config file must fail")
|
||||
}
|
||||
ioutil.WriteFile(configFilePath, []byte("{\"sftpd\": {\"bind_port\": \"a\"}}"), 0666)
|
||||
err = config.LoadConfig(configDir, tempConfigName)
|
||||
if err == nil {
|
||||
t.Errorf("loading a config with an invalid bond_port must fail")
|
||||
}
|
||||
os.Remove(configFilePath)
|
||||
}
|
||||
|
||||
func TestEmptyBanner(t *testing.T) {
|
||||
configDir := ".."
|
||||
confName := tempConfigName + ".json"
|
||||
configFilePath := filepath.Join(configDir, confName)
|
||||
config.LoadConfig(configDir, "")
|
||||
sftpdConf := config.GetSFTPDConfig()
|
||||
sftpdConf.Banner = " "
|
||||
c := make(map[string]sftpd.Configuration)
|
||||
c["sftpd"] = sftpdConf
|
||||
jsonConf, _ := json.Marshal(c)
|
||||
err := ioutil.WriteFile(configFilePath, jsonConf, 0666)
|
||||
if err != nil {
|
||||
t.Errorf("error saving temporary configuration")
|
||||
}
|
||||
config.LoadConfig(configDir, tempConfigName)
|
||||
sftpdConf = config.GetSFTPDConfig()
|
||||
if strings.TrimSpace(sftpdConf.Banner) == "" {
|
||||
t.Errorf("SFTPD banner cannot be empty")
|
||||
}
|
||||
os.Remove(configFilePath)
|
||||
}
|
||||
|
||||
func TestInvalidUploadMode(t *testing.T) {
|
||||
configDir := ".."
|
||||
confName := tempConfigName + ".json"
|
||||
configFilePath := filepath.Join(configDir, confName)
|
||||
config.LoadConfig(configDir, "")
|
||||
sftpdConf := config.GetSFTPDConfig()
|
||||
sftpdConf.UploadMode = 10
|
||||
c := make(map[string]sftpd.Configuration)
|
||||
c["sftpd"] = sftpdConf
|
||||
jsonConf, _ := json.Marshal(c)
|
||||
err := ioutil.WriteFile(configFilePath, jsonConf, 0666)
|
||||
if err != nil {
|
||||
t.Errorf("error saving temporary configuration")
|
||||
}
|
||||
err = config.LoadConfig(configDir, tempConfigName)
|
||||
if err == nil {
|
||||
t.Errorf("Loading configuration with invalid upload_mode must fail")
|
||||
}
|
||||
os.Remove(configFilePath)
|
||||
}
|
||||
6
crowdin.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
project_id_env: CROWDIN_PROJECT_ID
|
||||
api_token_env: CROWDIN_PERSONAL_TOKEN
|
||||
files:
|
||||
- source: /static/locales/en/translation.json
|
||||
translation: /static/locales/%two_letters_code%/%original_file_name%
|
||||
type: i18next_json
|
||||
@@ -1,315 +0,0 @@
|
||||
package dataprovider
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
var (
|
||||
usersBucket = []byte("users")
|
||||
usersIDIdxBucket = []byte("users_id_idx")
|
||||
)
|
||||
|
||||
// BoltProvider auth provider for bolt key/value store
|
||||
type BoltProvider struct {
|
||||
dbHandle *bolt.DB
|
||||
}
|
||||
|
||||
func initializeBoltProvider(basePath string) error {
|
||||
var err error
|
||||
dbPath := config.Name
|
||||
if !filepath.IsAbs(dbPath) {
|
||||
dbPath = filepath.Join(basePath, dbPath)
|
||||
}
|
||||
dbHandle, err := bolt.Open(dbPath, 0600, &bolt.Options{
|
||||
NoGrowSync: false,
|
||||
FreelistType: bolt.FreelistArrayType,
|
||||
Timeout: 5 * time.Second})
|
||||
if err == nil {
|
||||
logger.Debug(logSender, "bolt key store handle created")
|
||||
err = dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
_, e := tx.CreateBucketIfNotExists(usersBucket)
|
||||
return e
|
||||
})
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error creating users bucket: %v", err)
|
||||
return err
|
||||
}
|
||||
err = dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
_, e := tx.CreateBucketIfNotExists(usersIDIdxBucket)
|
||||
return e
|
||||
})
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error creating username idx bucket: %v", err)
|
||||
return err
|
||||
}
|
||||
provider = BoltProvider{dbHandle: dbHandle}
|
||||
} else {
|
||||
logger.Warn(logSender, "error creating bolt key/value store handler: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p BoltProvider) validateUserAndPass(username string, password string) (User, error) {
|
||||
var user User
|
||||
if len(password) == 0 {
|
||||
return user, errors.New("Credentials cannot be null or empty")
|
||||
}
|
||||
user, err := p.userExists(username)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error authenticating user: %v, error: %v", username, err)
|
||||
return user, err
|
||||
}
|
||||
return checkUserAndPass(user, password)
|
||||
}
|
||||
|
||||
func (p BoltProvider) validateUserAndPubKey(username string, pubKey string) (User, error) {
|
||||
var user User
|
||||
if len(pubKey) == 0 {
|
||||
return user, errors.New("Credentials cannot be null or empty")
|
||||
}
|
||||
user, err := p.userExists(username)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error authenticating user: %v, error: %v", username, err)
|
||||
return user, err
|
||||
}
|
||||
return checkUserAndPubKey(user, pubKey)
|
||||
}
|
||||
|
||||
func (p BoltProvider) getUserByID(ID int64) (User, error) {
|
||||
var user User
|
||||
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||
bucket, idxBucket, err := getBuckets(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userIDAsBytes := itob(ID)
|
||||
username := idxBucket.Get(userIDAsBytes)
|
||||
if username == nil {
|
||||
return &RecordNotFoundError{err: fmt.Sprintf("user with ID %v does not exist", ID)}
|
||||
}
|
||||
u := bucket.Get(username)
|
||||
if u == nil {
|
||||
return &RecordNotFoundError{err: fmt.Sprintf("username %v and ID: %v does not exist", string(username), ID)}
|
||||
}
|
||||
return json.Unmarshal(u, &user)
|
||||
})
|
||||
|
||||
return user, err
|
||||
}
|
||||
|
||||
func (p BoltProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, _, err := getBuckets(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var u []byte
|
||||
if u = bucket.Get([]byte(username)); u == nil {
|
||||
return &RecordNotFoundError{err: fmt.Sprintf("username %v does not exist, unable to update quota", username)}
|
||||
}
|
||||
var user User
|
||||
err = json.Unmarshal(u, &user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if reset {
|
||||
user.UsedQuotaSize = sizeAdd
|
||||
user.UsedQuotaFiles = filesAdd
|
||||
} else {
|
||||
user.UsedQuotaSize += sizeAdd
|
||||
user.UsedQuotaFiles += filesAdd
|
||||
}
|
||||
user.LastQuotaUpdate = utils.GetTimeAsMsSinceEpoch(time.Now())
|
||||
buf, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bucket.Put([]byte(username), buf)
|
||||
})
|
||||
}
|
||||
|
||||
func (p BoltProvider) getUsedQuota(username string) (int, int64, error) {
|
||||
user, err := p.userExists(username)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "unable to get quota for user '%v' error: %v", username, err)
|
||||
return 0, 0, err
|
||||
}
|
||||
return user.UsedQuotaFiles, user.UsedQuotaSize, err
|
||||
}
|
||||
|
||||
func (p BoltProvider) userExists(username string) (User, error) {
|
||||
var user User
|
||||
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||
bucket, _, err := getBuckets(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u := bucket.Get([]byte(username))
|
||||
if u == nil {
|
||||
return &RecordNotFoundError{err: fmt.Sprintf("username %v does not exist", user.Username)}
|
||||
}
|
||||
return json.Unmarshal(u, &user)
|
||||
})
|
||||
return user, err
|
||||
}
|
||||
|
||||
func (p BoltProvider) addUser(user User) error {
|
||||
err := validateUser(&user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, idxBucket, err := getBuckets(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u := bucket.Get([]byte(user.Username)); u != nil {
|
||||
return fmt.Errorf("username '%v' already exists", user.Username)
|
||||
}
|
||||
id, err := bucket.NextSequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.ID = int64(id)
|
||||
buf, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userIDAsBytes := itob(user.ID)
|
||||
err = bucket.Put([]byte(user.Username), buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return idxBucket.Put(userIDAsBytes, []byte(user.Username))
|
||||
})
|
||||
}
|
||||
|
||||
func (p BoltProvider) updateUser(user User) error {
|
||||
err := validateUser(&user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, _, err := getBuckets(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u := bucket.Get([]byte(user.Username)); u == nil {
|
||||
return &RecordNotFoundError{err: fmt.Sprintf("username '%v' does not exist", user.Username)}
|
||||
}
|
||||
buf, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bucket.Put([]byte(user.Username), buf)
|
||||
})
|
||||
}
|
||||
|
||||
func (p BoltProvider) deleteUser(user User) error {
|
||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, idxBucket, err := getBuckets(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userIDAsBytes := itob(user.ID)
|
||||
userName := idxBucket.Get(userIDAsBytes)
|
||||
if userName == nil {
|
||||
return &RecordNotFoundError{err: fmt.Sprintf("user with id %v does not exist", user.ID)}
|
||||
}
|
||||
err = bucket.Delete(userName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return idxBucket.Delete(userIDAsBytes)
|
||||
})
|
||||
}
|
||||
|
||||
func (p BoltProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) {
|
||||
users := []User{}
|
||||
var err error
|
||||
if len(username) > 0 {
|
||||
if offset == 0 {
|
||||
user, err := p.userExists(username)
|
||||
if err == nil {
|
||||
users = append(users, getUserNoCredentials(&user))
|
||||
}
|
||||
}
|
||||
return users, err
|
||||
}
|
||||
err = p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||
if limit <= 0 {
|
||||
return nil
|
||||
}
|
||||
bucket, _, err := getBuckets(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cursor := bucket.Cursor()
|
||||
itNum := 0
|
||||
if order == "ASC" {
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
itNum++
|
||||
if itNum <= offset {
|
||||
continue
|
||||
}
|
||||
var user User
|
||||
err = json.Unmarshal(v, &user)
|
||||
if err == nil {
|
||||
users = append(users, getUserNoCredentials(&user))
|
||||
}
|
||||
if len(users) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for k, v := cursor.Last(); k != nil; k, v = cursor.Prev() {
|
||||
itNum++
|
||||
if itNum <= offset {
|
||||
continue
|
||||
}
|
||||
var user User
|
||||
err = json.Unmarshal(v, &user)
|
||||
if err == nil {
|
||||
users = append(users, getUserNoCredentials(&user))
|
||||
}
|
||||
if len(users) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
})
|
||||
return users, err
|
||||
}
|
||||
|
||||
func getUserNoCredentials(user *User) User {
|
||||
user.Password = ""
|
||||
user.PublicKeys = []string{}
|
||||
return *user
|
||||
}
|
||||
|
||||
// itob returns an 8-byte big endian representation of v.
|
||||
func itob(v int64) []byte {
|
||||
b := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(b, uint64(v))
|
||||
return b
|
||||
}
|
||||
|
||||
func getBuckets(tx *bolt.Tx) (*bolt.Bucket, *bolt.Bucket, error) {
|
||||
var err error
|
||||
bucket := tx.Bucket(usersBucket)
|
||||
idxBucket := tx.Bucket(usersIDIdxBucket)
|
||||
if bucket == nil || idxBucket == nil {
|
||||
err = fmt.Errorf("Unable to find required buckets, bolt database structure not correcly defined")
|
||||
}
|
||||
return bucket, idxBucket, err
|
||||
}
|
||||
@@ -1,371 +0,0 @@
|
||||
// Package dataprovider provides data access.
|
||||
// It abstract different data providers and exposes a common API.
|
||||
// Currently the supported data providers are: PostreSQL (9+), MySQL (4.1+) and SQLite 3.x
|
||||
package dataprovider
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alexedwards/argon2id"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
// SQLiteDataProviderName name for SQLite database provider
|
||||
SQLiteDataProviderName = "sqlite"
|
||||
// PGSSQLDataProviderName name for PostgreSQL database provider
|
||||
PGSSQLDataProviderName = "postgresql"
|
||||
// MySQLDataProviderName name for MySQL database provider
|
||||
MySQLDataProviderName = "mysql"
|
||||
// BoltDataProviderName name for bbolt key/value store provider
|
||||
BoltDataProviderName = "bolt"
|
||||
|
||||
logSender = "dataProvider"
|
||||
argonPwdPrefix = "$argon2id$"
|
||||
bcryptPwdPrefix = "$2a$"
|
||||
pbkdf2SHA1Prefix = "$pbkdf2-sha1$"
|
||||
pbkdf2SHA256Prefix = "$pbkdf2-sha256$"
|
||||
pbkdf2SHA512Prefix = "$pbkdf2-sha512$"
|
||||
manageUsersDisabledError = "please set manage_users to 1 in sftpgo.conf to enable this method"
|
||||
trackQuotaDisabledError = "please enable track_quota in sftpgo.conf to use this method"
|
||||
)
|
||||
|
||||
var (
|
||||
// SupportedProviders data provider configured in the sftpgo.conf file must match of these strings
|
||||
SupportedProviders = []string{SQLiteDataProviderName, PGSSQLDataProviderName, MySQLDataProviderName, BoltDataProviderName}
|
||||
config Config
|
||||
provider Provider
|
||||
sqlPlaceholders []string
|
||||
validPerms = []string{PermAny, PermListItems, PermDownload, PermUpload, PermDelete, PermRename,
|
||||
PermCreateDirs, PermCreateSymlinks}
|
||||
hashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix}
|
||||
pbkdfPwdPrefixes = []string{pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix}
|
||||
)
|
||||
|
||||
// Config provider configuration
|
||||
type Config struct {
|
||||
// Driver name, must be one of the SupportedProviders
|
||||
Driver string `json:"driver" mapstructure:"driver"`
|
||||
// Database name
|
||||
Name string `json:"name" mapstructure:"name"`
|
||||
// Database host
|
||||
Host string `json:"host" mapstructure:"host"`
|
||||
// Database port
|
||||
Port int `json:"port" mapstructure:"port"`
|
||||
// Database username
|
||||
Username string `json:"username" mapstructure:"username"`
|
||||
// Database password
|
||||
Password string `json:"password" mapstructure:"password"`
|
||||
// Used for drivers mysql and postgresql.
|
||||
// 0 disable SSL/TLS connections.
|
||||
// 1 require ssl.
|
||||
// 2 set ssl mode to verify-ca for driver postgresql and skip-verify for driver mysql.
|
||||
// 3 set ssl mode to verify-full for driver postgresql and preferred for driver mysql.
|
||||
SSLMode int `json:"sslmode" mapstructure:"sslmode"`
|
||||
// Custom database connection string.
|
||||
// If not empty this connection string will be used instead of build one using the previous parameters
|
||||
ConnectionString string `json:"connection_string" mapstructure:"connection_string"`
|
||||
// Database table for SFTP users
|
||||
UsersTable string `json:"users_table" mapstructure:"users_table"`
|
||||
// Set to 0 to disable users management, 1 to enable
|
||||
ManageUsers int `json:"manage_users" mapstructure:"manage_users"`
|
||||
// Set the preferred way to track users quota between the following choices:
|
||||
// 0, disable quota tracking. REST API to scan user dir and update quota will do nothing
|
||||
// 1, quota is updated each time a user upload or delete a file even if the user has no quota restrictions
|
||||
// 2, quota is updated each time a user upload or delete a file but only for users with quota restrictions.
|
||||
// With this configuration the "quota scan" REST API can still be used to periodically update space usage
|
||||
// for users without quota restrictions
|
||||
TrackQuota int `json:"track_quota" mapstructure:"track_quota"`
|
||||
}
|
||||
|
||||
// ValidationError raised if input data is not valid
|
||||
type ValidationError struct {
|
||||
err string
|
||||
}
|
||||
|
||||
// Validation error details
|
||||
func (e *ValidationError) Error() string {
|
||||
return fmt.Sprintf("Validation error: %s", e.err)
|
||||
}
|
||||
|
||||
// MethodDisabledError raised if a method is disabled in config file.
|
||||
// For example, if user management is disabled, this error is raised
|
||||
// every time an user operation is done using the REST API
|
||||
type MethodDisabledError struct {
|
||||
err string
|
||||
}
|
||||
|
||||
// Method disabled error details
|
||||
func (e *MethodDisabledError) Error() string {
|
||||
return fmt.Sprintf("Method disabled error: %s", e.err)
|
||||
}
|
||||
|
||||
// RecordNotFoundError raised if a requested user is not found
|
||||
type RecordNotFoundError struct {
|
||||
err string
|
||||
}
|
||||
|
||||
func (e *RecordNotFoundError) Error() string {
|
||||
return fmt.Sprintf("Not found: %s", e.err)
|
||||
}
|
||||
|
||||
// GetProvider returns the configured provider
|
||||
func GetProvider() Provider {
|
||||
return provider
|
||||
}
|
||||
|
||||
// Provider interface that data providers must implement.
|
||||
type Provider interface {
|
||||
validateUserAndPass(username string, password string) (User, error)
|
||||
validateUserAndPubKey(username string, pubKey string) (User, error)
|
||||
updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error
|
||||
getUsedQuota(username string) (int, int64, error)
|
||||
userExists(username string) (User, error)
|
||||
addUser(user User) error
|
||||
updateUser(user User) error
|
||||
deleteUser(user User) error
|
||||
getUsers(limit int, offset int, order string, username string) ([]User, error)
|
||||
getUserByID(ID int64) (User, error)
|
||||
}
|
||||
|
||||
// Initialize the data provider.
|
||||
// An error is returned if the configured driver is invalid or if the data provider cannot be initialized
|
||||
func Initialize(cnf Config, basePath string) error {
|
||||
config = cnf
|
||||
sqlPlaceholders = getSQLPlaceholders()
|
||||
if config.Driver == SQLiteDataProviderName {
|
||||
return initializeSQLiteProvider(basePath)
|
||||
} else if config.Driver == PGSSQLDataProviderName {
|
||||
return initializePGSQLProvider()
|
||||
} else if config.Driver == MySQLDataProviderName {
|
||||
return initializeMySQLProvider()
|
||||
} else if config.Driver == BoltDataProviderName {
|
||||
return initializeBoltProvider(basePath)
|
||||
}
|
||||
return fmt.Errorf("Unsupported data provider: %v", config.Driver)
|
||||
}
|
||||
|
||||
// CheckUserAndPass retrieves the SFTP user with the given username and password if a match is found or an error
|
||||
func CheckUserAndPass(p Provider, username string, password string) (User, error) {
|
||||
return p.validateUserAndPass(username, password)
|
||||
}
|
||||
|
||||
// CheckUserAndPubKey retrieves the SFTP user with the given username and public key if a match is found or an error
|
||||
func CheckUserAndPubKey(p Provider, username string, pubKey string) (User, error) {
|
||||
return p.validateUserAndPubKey(username, pubKey)
|
||||
}
|
||||
|
||||
// UpdateUserQuota updates the quota for the given SFTP user adding filesAdd and sizeAdd.
|
||||
// If reset is true filesAdd and sizeAdd indicates the total files and the total size instead of the difference.
|
||||
func UpdateUserQuota(p Provider, user User, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
if config.TrackQuota == 0 {
|
||||
return &MethodDisabledError{err: trackQuotaDisabledError}
|
||||
} else if config.TrackQuota == 2 && !reset && !user.HasQuotaRestrictions() {
|
||||
return nil
|
||||
}
|
||||
return p.updateQuota(user.Username, filesAdd, sizeAdd, reset)
|
||||
}
|
||||
|
||||
// GetUsedQuota returns the used quota for the given SFTP user.
|
||||
// TrackQuota must be >=1 to enable this method
|
||||
func GetUsedQuota(p Provider, username string) (int, int64, error) {
|
||||
if config.TrackQuota == 0 {
|
||||
return 0, 0, &MethodDisabledError{err: trackQuotaDisabledError}
|
||||
}
|
||||
return p.getUsedQuota(username)
|
||||
}
|
||||
|
||||
// UserExists checks if the given SFTP username exists, returns an error if no match is found
|
||||
func UserExists(p Provider, username string) (User, error) {
|
||||
return p.userExists(username)
|
||||
}
|
||||
|
||||
// AddUser adds a new SFTP user.
|
||||
// ManageUsers configuration must be set to 1 to enable this method
|
||||
func AddUser(p Provider, user User) error {
|
||||
if config.ManageUsers == 0 {
|
||||
return &MethodDisabledError{err: manageUsersDisabledError}
|
||||
}
|
||||
return p.addUser(user)
|
||||
}
|
||||
|
||||
// UpdateUser updates an existing SFTP user.
|
||||
// ManageUsers configuration must be set to 1 to enable this method
|
||||
func UpdateUser(p Provider, user User) error {
|
||||
if config.ManageUsers == 0 {
|
||||
return &MethodDisabledError{err: manageUsersDisabledError}
|
||||
}
|
||||
return p.updateUser(user)
|
||||
}
|
||||
|
||||
// DeleteUser deletes an existing SFTP user.
|
||||
// ManageUsers configuration must be set to 1 to enable this method
|
||||
func DeleteUser(p Provider, user User) error {
|
||||
if config.ManageUsers == 0 {
|
||||
return &MethodDisabledError{err: manageUsersDisabledError}
|
||||
}
|
||||
return p.deleteUser(user)
|
||||
}
|
||||
|
||||
// GetUsers returns an array of users respecting limit and offset and filtered by username exact match if not empty
|
||||
func GetUsers(p Provider, limit int, offset int, order string, username string) ([]User, error) {
|
||||
return p.getUsers(limit, offset, order, username)
|
||||
}
|
||||
|
||||
// GetUserByID returns the user with the given database ID if a match is found or an error
|
||||
func GetUserByID(p Provider, ID int64) (User, error) {
|
||||
return p.getUserByID(ID)
|
||||
}
|
||||
|
||||
func validateUser(user *User) error {
|
||||
if len(user.Username) == 0 || len(user.HomeDir) == 0 {
|
||||
return &ValidationError{err: "Mandatory parameters missing"}
|
||||
}
|
||||
if len(user.Password) == 0 && len(user.PublicKeys) == 0 {
|
||||
return &ValidationError{err: "Please set password or at least a public_key"}
|
||||
}
|
||||
if len(user.Permissions) == 0 {
|
||||
return &ValidationError{err: "Please grant some permissions to this user"}
|
||||
}
|
||||
if !filepath.IsAbs(user.HomeDir) {
|
||||
return &ValidationError{err: fmt.Sprintf("home_dir must be an absolute path, actual value: %v", user.HomeDir)}
|
||||
}
|
||||
for _, p := range user.Permissions {
|
||||
if !utils.IsStringInSlice(p, validPerms) {
|
||||
return &ValidationError{err: fmt.Sprintf("Invalid permission: %v", p)}
|
||||
}
|
||||
}
|
||||
if len(user.Password) > 0 && !utils.IsStringPrefixInSlice(user.Password, hashPwdPrefixes) {
|
||||
pwd, err := argon2id.CreateHash(user.Password, argon2id.DefaultParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Password = pwd
|
||||
}
|
||||
for i, k := range user.PublicKeys {
|
||||
_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k))
|
||||
if err != nil {
|
||||
return &ValidationError{err: fmt.Sprintf("Could not parse key nr. %d: %s", i, err)}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkUserAndPass(user User, password string) (User, error) {
|
||||
var err error
|
||||
if len(user.Password) == 0 {
|
||||
return user, errors.New("Credentials cannot be null or empty")
|
||||
}
|
||||
var match bool
|
||||
if strings.HasPrefix(user.Password, argonPwdPrefix) {
|
||||
match, err = argon2id.ComparePasswordAndHash(password, user.Password)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error comparing password with argon hash: %v", err)
|
||||
return user, err
|
||||
}
|
||||
} else if strings.HasPrefix(user.Password, bcryptPwdPrefix) {
|
||||
if err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
||||
logger.Warn(logSender, "error comparing password with bcrypt hash: %v", err)
|
||||
return user, err
|
||||
}
|
||||
match = true
|
||||
} else if utils.IsStringPrefixInSlice(user.Password, pbkdfPwdPrefixes) {
|
||||
match, err = comparePbkdf2PasswordAndHash(password, user.Password)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error comparing password with pbkdf2 sha256 hash: %v", err)
|
||||
return user, err
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
err = errors.New("Invalid credentials")
|
||||
}
|
||||
return user, err
|
||||
}
|
||||
|
||||
func checkUserAndPubKey(user User, pubKey string) (User, error) {
|
||||
if len(user.PublicKeys) == 0 {
|
||||
return user, errors.New("Invalid credentials")
|
||||
}
|
||||
for i, k := range user.PublicKeys {
|
||||
storedPubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k))
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error parsing stored public key %d for user %v: %v", i, user.Username, err)
|
||||
return user, err
|
||||
}
|
||||
if string(storedPubKey.Marshal()) == pubKey {
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
return user, errors.New("Invalid credentials")
|
||||
}
|
||||
|
||||
func comparePbkdf2PasswordAndHash(password, hashedPassword string) (bool, error) {
|
||||
vals := strings.Split(hashedPassword, "$")
|
||||
if len(vals) != 5 {
|
||||
return false, fmt.Errorf("pbkdf2: hash is not in the correct format")
|
||||
}
|
||||
var hashFunc func() hash.Hash
|
||||
var hashSize int
|
||||
if strings.HasPrefix(hashedPassword, pbkdf2SHA256Prefix) {
|
||||
hashSize = sha256.Size
|
||||
hashFunc = sha256.New
|
||||
} else if strings.HasPrefix(hashedPassword, pbkdf2SHA512Prefix) {
|
||||
hashSize = sha512.Size
|
||||
hashFunc = sha512.New
|
||||
} else if strings.HasPrefix(hashedPassword, pbkdf2SHA1Prefix) {
|
||||
hashSize = sha1.Size
|
||||
hashFunc = sha1.New
|
||||
} else {
|
||||
return false, fmt.Errorf("pbkdf2: invalid or unsupported hash format %v", vals[1])
|
||||
}
|
||||
iterations, err := strconv.Atoi(vals[2])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
salt := vals[3]
|
||||
expected := vals[4]
|
||||
df := pbkdf2.Key([]byte(password), []byte(salt), iterations, hashSize, hashFunc)
|
||||
buf := make([]byte, base64.StdEncoding.EncodedLen(len(df)))
|
||||
base64.StdEncoding.Encode(buf, df)
|
||||
return subtle.ConstantTimeCompare(buf, []byte(expected)) == 1, nil
|
||||
}
|
||||
|
||||
func getSSLMode() string {
|
||||
if config.Driver == PGSSQLDataProviderName {
|
||||
if config.SSLMode == 0 {
|
||||
return "disable"
|
||||
} else if config.SSLMode == 1 {
|
||||
return "require"
|
||||
} else if config.SSLMode == 2 {
|
||||
return "verify-ca"
|
||||
} else if config.SSLMode == 3 {
|
||||
return "verify-full"
|
||||
}
|
||||
} else if config.Driver == MySQLDataProviderName {
|
||||
if config.SSLMode == 0 {
|
||||
return "false"
|
||||
} else if config.SSLMode == 1 {
|
||||
return "true"
|
||||
} else if config.SSLMode == 2 {
|
||||
return "skip-verify"
|
||||
} else if config.SSLMode == 3 {
|
||||
return "preferred"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package dataprovider
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
)
|
||||
|
||||
// MySQLProvider auth provider for MySQL/MariaDB database
|
||||
type MySQLProvider struct {
|
||||
dbHandle *sql.DB
|
||||
}
|
||||
|
||||
func initializeMySQLProvider() error {
|
||||
var err error
|
||||
var connectionString string
|
||||
if len(config.ConnectionString) == 0 {
|
||||
connectionString = fmt.Sprintf("%v:%v@tcp([%v]:%v)/%v?charset=utf8&interpolateParams=true&timeout=10s&tls=%v",
|
||||
config.Username, config.Password, config.Host, config.Port, config.Name, getSSLMode())
|
||||
} else {
|
||||
connectionString = config.ConnectionString
|
||||
}
|
||||
dbHandle, err := sql.Open("mysql", connectionString)
|
||||
if err == nil {
|
||||
numCPU := runtime.NumCPU()
|
||||
logger.Debug(logSender, "mysql database handle created, connection string: '%v', pool size: %v", connectionString, numCPU)
|
||||
dbHandle.SetMaxIdleConns(numCPU)
|
||||
dbHandle.SetMaxOpenConns(numCPU)
|
||||
dbHandle.SetConnMaxLifetime(1800 * time.Second)
|
||||
provider = MySQLProvider{dbHandle: dbHandle}
|
||||
} else {
|
||||
logger.Warn(logSender, "error creating mysql database handler, connection string: '%v', error: %v", connectionString, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p MySQLProvider) validateUserAndPass(username string, password string) (User, error) {
|
||||
return sqlCommonValidateUserAndPass(username, password, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) validateUserAndPubKey(username string, publicKey string) (User, error) {
|
||||
return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) getUserByID(ID int64) (User, error) {
|
||||
return sqlCommonGetUserByID(ID, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
tx, err := p.dbHandle.Begin()
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error starting transaction to update quota for user %v: %v", username, err)
|
||||
return err
|
||||
}
|
||||
err = sqlCommonUpdateQuota(username, filesAdd, sizeAdd, reset, p.dbHandle)
|
||||
if err == nil {
|
||||
err = tx.Commit()
|
||||
} else {
|
||||
err = tx.Rollback()
|
||||
}
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error closing transaction to update quota for user %v: %v", username, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p MySQLProvider) getUsedQuota(username string) (int, int64, error) {
|
||||
return sqlCommonGetUsedQuota(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) userExists(username string) (User, error) {
|
||||
return sqlCommonCheckUserExists(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) addUser(user User) error {
|
||||
return sqlCommonAddUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) updateUser(user User) error {
|
||||
return sqlCommonUpdateUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) deleteUser(user User) error {
|
||||
return sqlCommonDeleteUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) {
|
||||
return sqlCommonGetUsers(limit, offset, order, username, p.dbHandle)
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package dataprovider
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
)
|
||||
|
||||
// PGSQLProvider auth provider for PostgreSQL database
|
||||
type PGSQLProvider struct {
|
||||
dbHandle *sql.DB
|
||||
}
|
||||
|
||||
func initializePGSQLProvider() error {
|
||||
var err error
|
||||
var connectionString string
|
||||
if len(config.ConnectionString) == 0 {
|
||||
connectionString = fmt.Sprintf("host='%v' port=%v dbname='%v' user='%v' password='%v' sslmode=%v connect_timeout=10",
|
||||
config.Host, config.Port, config.Name, config.Username, config.Password, getSSLMode())
|
||||
} else {
|
||||
connectionString = config.ConnectionString
|
||||
}
|
||||
dbHandle, err := sql.Open("postgres", connectionString)
|
||||
if err == nil {
|
||||
numCPU := runtime.NumCPU()
|
||||
logger.Debug(logSender, "postgres database handle created, connection string: '%v', pool size: %v", connectionString, numCPU)
|
||||
dbHandle.SetMaxIdleConns(numCPU)
|
||||
dbHandle.SetMaxOpenConns(numCPU)
|
||||
provider = PGSQLProvider{dbHandle: dbHandle}
|
||||
} else {
|
||||
logger.Warn(logSender, "error creating postgres database handler, connection string: '%v', error: %v", connectionString, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) validateUserAndPass(username string, password string) (User, error) {
|
||||
return sqlCommonValidateUserAndPass(username, password, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) validateUserAndPubKey(username string, publicKey string) (User, error) {
|
||||
return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) getUserByID(ID int64) (User, error) {
|
||||
return sqlCommonGetUserByID(ID, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
tx, err := p.dbHandle.Begin()
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error starting transaction to update quota for user %v: %v", username, err)
|
||||
return err
|
||||
}
|
||||
err = sqlCommonUpdateQuota(username, filesAdd, sizeAdd, reset, p.dbHandle)
|
||||
if err == nil {
|
||||
err = tx.Commit()
|
||||
} else {
|
||||
err = tx.Rollback()
|
||||
}
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error closing transaction to update quota for user %v: %v", username, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) getUsedQuota(username string) (int, int64, error) {
|
||||
return sqlCommonGetUsedQuota(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) userExists(username string) (User, error) {
|
||||
return sqlCommonCheckUserExists(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) addUser(user User) error {
|
||||
return sqlCommonAddUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) updateUser(user User) error {
|
||||
return sqlCommonUpdateUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) deleteUser(user User) error {
|
||||
return sqlCommonDeleteUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) {
|
||||
return sqlCommonGetUsers(limit, offset, order, username, p.dbHandle)
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
package dataprovider
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
)
|
||||
|
||||
func getUserByUsername(username string, dbHandle *sql.DB) (User, error) {
|
||||
var user User
|
||||
q := getUserByUsernameQuery()
|
||||
stmt, err := dbHandle.Prepare(q)
|
||||
if err != nil {
|
||||
logger.Debug(logSender, "error preparing database query %v: %v", q, err)
|
||||
return user, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
row := stmt.QueryRow(username)
|
||||
return getUserFromDbRow(row, nil)
|
||||
}
|
||||
|
||||
func sqlCommonValidateUserAndPass(username string, password string, dbHandle *sql.DB) (User, error) {
|
||||
var user User
|
||||
if len(password) == 0 {
|
||||
return user, errors.New("Credentials cannot be null or empty")
|
||||
}
|
||||
user, err := getUserByUsername(username, dbHandle)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error authenticating user: %v, error: %v", username, err)
|
||||
return user, err
|
||||
}
|
||||
return checkUserAndPass(user, password)
|
||||
}
|
||||
|
||||
func sqlCommonValidateUserAndPubKey(username string, pubKey string, dbHandle *sql.DB) (User, error) {
|
||||
var user User
|
||||
if len(pubKey) == 0 {
|
||||
return user, errors.New("Credentials cannot be null or empty")
|
||||
}
|
||||
user, err := getUserByUsername(username, dbHandle)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error authenticating user: %v, error: %v", username, err)
|
||||
return user, err
|
||||
}
|
||||
return checkUserAndPubKey(user, pubKey)
|
||||
}
|
||||
|
||||
func sqlCommonGetUserByID(ID int64, dbHandle *sql.DB) (User, error) {
|
||||
var user User
|
||||
q := getUserByIDQuery()
|
||||
stmt, err := dbHandle.Prepare(q)
|
||||
if err != nil {
|
||||
logger.Debug(logSender, "error preparing database query %v: %v", q, err)
|
||||
return user, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
row := stmt.QueryRow(ID)
|
||||
return getUserFromDbRow(row, nil)
|
||||
}
|
||||
|
||||
func sqlCommonUpdateQuota(username string, filesAdd int, sizeAdd int64, reset bool, dbHandle *sql.DB) error {
|
||||
q := getUpdateQuotaQuery(reset)
|
||||
stmt, err := dbHandle.Prepare(q)
|
||||
if err != nil {
|
||||
logger.Debug(logSender, "error preparing database query %v: %v", q, err)
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.Exec(sizeAdd, filesAdd, utils.GetTimeAsMsSinceEpoch(time.Now()), username)
|
||||
if err == nil {
|
||||
logger.Debug(logSender, "quota updated for user %v, files increment: %v size increment: %v is reset? %v",
|
||||
username, filesAdd, sizeAdd, reset)
|
||||
} else {
|
||||
logger.Warn(logSender, "error updating quota for username %v: %v", username, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func sqlCommonGetUsedQuota(username string, dbHandle *sql.DB) (int, int64, error) {
|
||||
q := getQuotaQuery()
|
||||
stmt, err := dbHandle.Prepare(q)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error preparing database query %v: %v", q, err)
|
||||
return 0, 0, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
var usedFiles int
|
||||
var usedSize int64
|
||||
err = stmt.QueryRow(username).Scan(&usedSize, &usedFiles)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error getting user quota: %v, error: %v", username, err)
|
||||
return 0, 0, err
|
||||
}
|
||||
return usedFiles, usedSize, err
|
||||
}
|
||||
|
||||
func sqlCommonCheckUserExists(username string, dbHandle *sql.DB) (User, error) {
|
||||
var user User
|
||||
q := getUserByUsernameQuery()
|
||||
stmt, err := dbHandle.Prepare(q)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error preparing database query %v: %v", q, err)
|
||||
return user, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
row := stmt.QueryRow(username)
|
||||
return getUserFromDbRow(row, nil)
|
||||
}
|
||||
|
||||
func sqlCommonAddUser(user User, dbHandle *sql.DB) error {
|
||||
err := validateUser(&user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := getAddUserQuery()
|
||||
stmt, err := dbHandle.Prepare(q)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error preparing database query %v: %v", q, err)
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
permissions, err := user.GetPermissionsAsJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicKeys, err := user.GetPublicKeysAsJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = stmt.Exec(user.Username, user.Password, string(publicKeys), user.HomeDir, user.UID, user.GID, user.MaxSessions, user.QuotaSize,
|
||||
user.QuotaFiles, string(permissions), user.UploadBandwidth, user.DownloadBandwidth)
|
||||
return err
|
||||
}
|
||||
|
||||
func sqlCommonUpdateUser(user User, dbHandle *sql.DB) error {
|
||||
err := validateUser(&user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := getUpdateUserQuery()
|
||||
stmt, err := dbHandle.Prepare(q)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error preparing database query %v: %v", q, err)
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
permissions, err := user.GetPermissionsAsJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicKeys, err := user.GetPublicKeysAsJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = stmt.Exec(user.Password, string(publicKeys), user.HomeDir, user.UID, user.GID, user.MaxSessions, user.QuotaSize,
|
||||
user.QuotaFiles, string(permissions), user.UploadBandwidth, user.DownloadBandwidth, user.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
func sqlCommonDeleteUser(user User, dbHandle *sql.DB) error {
|
||||
q := getDeleteUserQuery()
|
||||
stmt, err := dbHandle.Prepare(q)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error preparing database query %v: %v", q, err)
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.Exec(user.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
func sqlCommonGetUsers(limit int, offset int, order string, username string, dbHandle *sql.DB) ([]User, error) {
|
||||
users := []User{}
|
||||
q := getUsersQuery(order, username)
|
||||
stmt, err := dbHandle.Prepare(q)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error preparing database query %v: %v", q, err)
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
var rows *sql.Rows
|
||||
if len(username) > 0 {
|
||||
rows, err = stmt.Query(username, limit, offset)
|
||||
} else {
|
||||
rows, err = stmt.Query(limit, offset)
|
||||
}
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
u, err := getUserFromDbRow(nil, rows)
|
||||
// hide password and public key
|
||||
if err == nil {
|
||||
u.Password = ""
|
||||
u.PublicKeys = []string{}
|
||||
users = append(users, u)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return users, err
|
||||
}
|
||||
|
||||
func getUserFromDbRow(row *sql.Row, rows *sql.Rows) (User, error) {
|
||||
var user User
|
||||
var permissions sql.NullString
|
||||
var password sql.NullString
|
||||
var publicKey sql.NullString
|
||||
var err error
|
||||
if row != nil {
|
||||
err = row.Scan(&user.ID, &user.Username, &password, &publicKey, &user.HomeDir, &user.UID, &user.GID, &user.MaxSessions,
|
||||
&user.QuotaSize, &user.QuotaFiles, &permissions, &user.UsedQuotaSize, &user.UsedQuotaFiles, &user.LastQuotaUpdate,
|
||||
&user.UploadBandwidth, &user.DownloadBandwidth)
|
||||
|
||||
} else {
|
||||
err = rows.Scan(&user.ID, &user.Username, &password, &publicKey, &user.HomeDir, &user.UID, &user.GID, &user.MaxSessions,
|
||||
&user.QuotaSize, &user.QuotaFiles, &permissions, &user.UsedQuotaSize, &user.UsedQuotaFiles, &user.LastQuotaUpdate,
|
||||
&user.UploadBandwidth, &user.DownloadBandwidth)
|
||||
}
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return user, &RecordNotFoundError{err: err.Error()}
|
||||
}
|
||||
return user, err
|
||||
}
|
||||
if password.Valid {
|
||||
user.Password = password.String
|
||||
}
|
||||
if publicKey.Valid {
|
||||
var list []string
|
||||
err = json.Unmarshal([]byte(publicKey.String), &list)
|
||||
if err == nil {
|
||||
user.PublicKeys = list
|
||||
}
|
||||
}
|
||||
if permissions.Valid {
|
||||
var list []string
|
||||
err = json.Unmarshal([]byte(permissions.String), &list)
|
||||
if err == nil {
|
||||
user.Permissions = list
|
||||
}
|
||||
}
|
||||
return user, err
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package dataprovider
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
)
|
||||
|
||||
// SQLiteProvider auth provider for SQLite database
|
||||
type SQLiteProvider struct {
|
||||
dbHandle *sql.DB
|
||||
}
|
||||
|
||||
func initializeSQLiteProvider(basePath string) error {
|
||||
var err error
|
||||
var connectionString string
|
||||
if len(config.ConnectionString) == 0 {
|
||||
dbPath := config.Name
|
||||
if !filepath.IsAbs(dbPath) {
|
||||
dbPath = filepath.Join(basePath, dbPath)
|
||||
}
|
||||
fi, err := os.Stat(dbPath)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "sqlite database file does not exists, please be sure to create and initialize"+
|
||||
" a database before starting sftpgo")
|
||||
return err
|
||||
}
|
||||
if fi.Size() == 0 {
|
||||
return errors.New("sqlite database file is invalid, please be sure to create and initialize" +
|
||||
" a database before starting sftpgo")
|
||||
}
|
||||
connectionString = fmt.Sprintf("file:%v?cache=shared", dbPath)
|
||||
} else {
|
||||
connectionString = config.ConnectionString
|
||||
}
|
||||
dbHandle, err := sql.Open("sqlite3", connectionString)
|
||||
if err == nil {
|
||||
logger.Debug(logSender, "sqlite database handle created, connection string: '%v'", connectionString)
|
||||
dbHandle.SetMaxOpenConns(1)
|
||||
provider = SQLiteProvider{dbHandle: dbHandle}
|
||||
} else {
|
||||
logger.Warn(logSender, "error creating sqlite database handler, connection string: '%v', error: %v", connectionString, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) validateUserAndPass(username string, password string) (User, error) {
|
||||
return sqlCommonValidateUserAndPass(username, password, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) validateUserAndPubKey(username string, publicKey string) (User, error) {
|
||||
return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) getUserByID(ID int64) (User, error) {
|
||||
return sqlCommonGetUserByID(ID, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
// we keep only 1 open connection (SetMaxOpenConns(1)) so a transaction is not needed and it could block
|
||||
// the database access since it will try to open a new connection
|
||||
return sqlCommonUpdateQuota(username, filesAdd, sizeAdd, reset, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) getUsedQuota(username string) (int, int64, error) {
|
||||
return sqlCommonGetUsedQuota(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) userExists(username string) (User, error) {
|
||||
return sqlCommonCheckUserExists(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) addUser(user User) error {
|
||||
return sqlCommonAddUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) updateUser(user User) error {
|
||||
return sqlCommonUpdateUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) deleteUser(user User) error {
|
||||
return sqlCommonDeleteUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) {
|
||||
return sqlCommonGetUsers(limit, offset, order, username, p.dbHandle)
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package dataprovider
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
selectUserFields = "id,username,password,public_keys,home_dir,uid,gid,max_sessions,quota_size,quota_files,permissions," +
|
||||
"used_quota_size,used_quota_files,last_quota_update,upload_bandwidth,download_bandwidth"
|
||||
)
|
||||
|
||||
func getSQLPlaceholders() []string {
|
||||
var placeholders []string
|
||||
for i := 1; i <= 20; i++ {
|
||||
if config.Driver == PGSSQLDataProviderName {
|
||||
placeholders = append(placeholders, fmt.Sprintf("$%v", i))
|
||||
} else {
|
||||
placeholders = append(placeholders, "?")
|
||||
}
|
||||
}
|
||||
return placeholders
|
||||
}
|
||||
|
||||
func getUserByUsernameQuery() string {
|
||||
return fmt.Sprintf(`SELECT %v FROM %v WHERE username = %v`, selectUserFields, config.UsersTable, sqlPlaceholders[0])
|
||||
}
|
||||
|
||||
func getUserByIDQuery() string {
|
||||
return fmt.Sprintf(`SELECT %v FROM %v WHERE id = %v`, selectUserFields, config.UsersTable, sqlPlaceholders[0])
|
||||
}
|
||||
|
||||
func getUsersQuery(order string, username string) string {
|
||||
if len(username) > 0 {
|
||||
return fmt.Sprintf(`SELECT %v FROM %v WHERE username = %v ORDER BY username %v LIMIT %v OFFSET %v`,
|
||||
selectUserFields, config.UsersTable, sqlPlaceholders[0], order, sqlPlaceholders[1], sqlPlaceholders[2])
|
||||
}
|
||||
return fmt.Sprintf(`SELECT %v FROM %v ORDER BY username %v LIMIT %v OFFSET %v`, selectUserFields, config.UsersTable,
|
||||
order, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||
}
|
||||
|
||||
func getUpdateQuotaQuery(reset bool) string {
|
||||
if reset {
|
||||
return fmt.Sprintf(`UPDATE %v SET used_quota_size = %v,used_quota_files = %v,last_quota_update = %v
|
||||
WHERE username = %v`, config.UsersTable, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])
|
||||
}
|
||||
return fmt.Sprintf(`UPDATE %v SET used_quota_size = used_quota_size + %v,used_quota_files = used_quota_files + %v,last_quota_update = %v
|
||||
WHERE username = %v`, config.UsersTable, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])
|
||||
}
|
||||
|
||||
func getQuotaQuery() string {
|
||||
return fmt.Sprintf(`SELECT used_quota_size,used_quota_files FROM %v WHERE username = %v`, config.UsersTable,
|
||||
sqlPlaceholders[0])
|
||||
}
|
||||
|
||||
func getAddUserQuery() string {
|
||||
return fmt.Sprintf(`INSERT INTO %v (username,password,public_keys,home_dir,uid,gid,max_sessions,quota_size,quota_files,permissions,
|
||||
used_quota_size,used_quota_files,last_quota_update,upload_bandwidth,download_bandwidth)
|
||||
VALUES (%v,%v,%v,%v,%v,%v,%v,%v,%v,%v,0,0,0,%v,%v)`, config.UsersTable, sqlPlaceholders[0], sqlPlaceholders[1],
|
||||
sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7],
|
||||
sqlPlaceholders[8], sqlPlaceholders[9], sqlPlaceholders[10], sqlPlaceholders[11])
|
||||
}
|
||||
|
||||
func getUpdateUserQuery() string {
|
||||
return fmt.Sprintf(`UPDATE %v SET password=%v,public_keys=%v,home_dir=%v,uid=%v,gid=%v,max_sessions=%v,quota_size=%v,
|
||||
quota_files=%v,permissions=%v,upload_bandwidth=%v,download_bandwidth=%v WHERE id = %v`, config.UsersTable,
|
||||
sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5],
|
||||
sqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9], sqlPlaceholders[10], sqlPlaceholders[11])
|
||||
}
|
||||
|
||||
func getDeleteUserQuery() string {
|
||||
return fmt.Sprintf(`DELETE FROM %v WHERE id = %v`, config.UsersTable, sqlPlaceholders[0])
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package dataprovider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
)
|
||||
|
||||
// Available permissions for SFTP users
|
||||
const (
|
||||
// All permissions are granted
|
||||
PermAny = "*"
|
||||
// List items such as files and directories is allowed
|
||||
PermListItems = "list"
|
||||
// download files is allowed
|
||||
PermDownload = "download"
|
||||
// upload files is allowed
|
||||
PermUpload = "upload"
|
||||
// delete files or directories is allowed
|
||||
PermDelete = "delete"
|
||||
// rename files or directories is allowed
|
||||
PermRename = "rename"
|
||||
// create directories is allowed
|
||||
PermCreateDirs = "create_dirs"
|
||||
// create symbolic links is allowed
|
||||
PermCreateSymlinks = "create_symlinks"
|
||||
)
|
||||
|
||||
// User defines an SFTP user
|
||||
type User struct {
|
||||
// Database unique identifier
|
||||
ID int64 `json:"id"`
|
||||
// Username
|
||||
Username string `json:"username"`
|
||||
// Password used for password authentication.
|
||||
// For users created using SFTPGo REST API the password is be stored using argon2id hashing algo.
|
||||
// Checking passwords stored with bcrypt is supported too.
|
||||
// Currently, as fallback, there is a clear text password checking but you should not store passwords
|
||||
// as clear text and this support could be removed at any time, so please don't depend on it.
|
||||
Password string `json:"password,omitempty"`
|
||||
// PublicKeys used for public key authentication. At least one between password and a public key is mandatory
|
||||
PublicKeys []string `json:"public_keys,omitempty"`
|
||||
// The user cannot upload or download files outside this directory. Must be an absolute path
|
||||
HomeDir string `json:"home_dir"`
|
||||
// If sftpgo runs as root system user then the created files and directories will be assigned to this system UID
|
||||
UID int `json:"uid"`
|
||||
// If sftpgo runs as root system user then the created files and directories will be assigned to this system GID
|
||||
GID int `json:"gid"`
|
||||
// Maximum concurrent sessions. 0 means unlimited
|
||||
MaxSessions int `json:"max_sessions"`
|
||||
// Maximum size allowed as bytes. 0 means unlimited
|
||||
QuotaSize int64 `json:"quota_size"`
|
||||
// Maximum number of files allowed. 0 means unlimited
|
||||
QuotaFiles int `json:"quota_files"`
|
||||
// List of the granted permissions
|
||||
Permissions []string `json:"permissions"`
|
||||
// Used quota as bytes
|
||||
UsedQuotaSize int64 `json:"used_quota_size"`
|
||||
// Used quota as number of files
|
||||
UsedQuotaFiles int `json:"used_quota_files"`
|
||||
// Last quota update as unix timestamp in milliseconds
|
||||
LastQuotaUpdate int64 `json:"last_quota_update"`
|
||||
// Maximum upload bandwidth as KB/s, 0 means unlimited
|
||||
UploadBandwidth int64 `json:"upload_bandwidth"`
|
||||
// Maximum download bandwidth as KB/s, 0 means unlimited
|
||||
DownloadBandwidth int64 `json:"download_bandwidth"`
|
||||
}
|
||||
|
||||
// HasPerm returns true if the user has the given permission or any permission
|
||||
func (u *User) HasPerm(permission string) bool {
|
||||
if utils.IsStringInSlice(PermAny, u.Permissions) {
|
||||
return true
|
||||
}
|
||||
return utils.IsStringInSlice(permission, u.Permissions)
|
||||
}
|
||||
|
||||
// GetPermissionsAsJSON returns the permissions as json byte array
|
||||
func (u *User) GetPermissionsAsJSON() ([]byte, error) {
|
||||
return json.Marshal(u.Permissions)
|
||||
}
|
||||
|
||||
// GetPublicKeysAsJSON returns the public keys as json byte array
|
||||
func (u *User) GetPublicKeysAsJSON() ([]byte, error) {
|
||||
return json.Marshal(u.PublicKeys)
|
||||
}
|
||||
|
||||
// GetUID returns a validate uid, suitable for use with os.Chown
|
||||
func (u *User) GetUID() int {
|
||||
if u.UID <= 0 || u.UID > 65535 {
|
||||
return -1
|
||||
}
|
||||
return u.UID
|
||||
}
|
||||
|
||||
// GetGID returns a validate gid, suitable for use with os.Chown
|
||||
func (u *User) GetGID() int {
|
||||
if u.GID <= 0 || u.GID > 65535 {
|
||||
return -1
|
||||
}
|
||||
return u.GID
|
||||
}
|
||||
|
||||
// GetHomeDir returns the shortest path name equivalent to the user's home directory
|
||||
func (u *User) GetHomeDir() string {
|
||||
return filepath.Clean(u.HomeDir)
|
||||
}
|
||||
|
||||
// HasQuotaRestrictions returns true if there is a quota restriction on number of files or size or both
|
||||
func (u *User) HasQuotaRestrictions() bool {
|
||||
return u.QuotaFiles > 0 || u.QuotaSize > 0
|
||||
}
|
||||
|
||||
// GetRelativePath returns the path for a file relative to the user's home dir.
|
||||
// This is the path as seen by SFTP users
|
||||
func (u *User) GetRelativePath(path string) string {
|
||||
rel, err := filepath.Rel(u.GetHomeDir(), path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return "/" + filepath.ToSlash(rel)
|
||||
}
|
||||
25
docker/scripts/download-plugins.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
ARCH=`uname -m`
|
||||
|
||||
case ${ARCH} in
|
||||
"x86_64")
|
||||
SUFFIX=amd64
|
||||
;;
|
||||
"aarch64")
|
||||
SUFFIX=arm64
|
||||
;;
|
||||
*)
|
||||
SUFFIX=ppc64le
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "download plugins for arch ${SUFFIX}"
|
||||
|
||||
for PLUGIN in geoipfilter kms pubsub eventstore eventsearch auth
|
||||
do
|
||||
echo "download plugin from https://github.com/sftpgo/sftpgo-plugin-${PLUGIN}/releases/latest/download/sftpgo-plugin-${PLUGIN}-linux-${SUFFIX}"
|
||||
curl -L "https://github.com/sftpgo/sftpgo-plugin-${PLUGIN}/releases/latest/download/sftpgo-plugin-${PLUGIN}-linux-${SUFFIX}" --output "/usr/local/bin/sftpgo-plugin-${PLUGIN}"
|
||||
chmod 755 "/usr/local/bin/sftpgo-plugin-${PLUGIN}"
|
||||
done
|
||||
60
examples/OTP/authy/README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Authy
|
||||
|
||||
These example show how-to integrate [Twilio Authy API](https://www.twilio.com/docs/authy/api) for One-Time-Password logins.
|
||||
|
||||
The examples assume that the user has the free [Authy app](https://authy.com/) installed and uses it to generate offline [TOTP](https://en.wikipedia.org/wiki/Time-based_One-time_Password_algorithm) codes (soft tokens).
|
||||
|
||||
You first need to [create an Authy Application in the Twilio Console](https://twilio.com/console/authy/applications?_ga=2.205553366.451688189.1597667213-1526360003.1597667213), then you can create a new Authy user and store a reference to the matching SFTPGo account.
|
||||
|
||||
Verify that your Authy application is successfully registered:
|
||||
|
||||
```bash
|
||||
export AUTHY_API_KEY=<your api key here>
|
||||
curl 'https://api.authy.com/protected/json/app/details' -H "X-Authy-API-Key: $AUTHY_API_KEY"
|
||||
```
|
||||
|
||||
now create an Authy user:
|
||||
|
||||
```bash
|
||||
curl -XPOST "https://api.authy.com/protected/json/users/new" \
|
||||
-H "X-Authy-API-Key: $AUTHY_API_KEY" \
|
||||
--data-urlencode user[email]="user@domain.com" \
|
||||
--data-urlencode user[cellphone]="317-338-9302" \
|
||||
--data-urlencode user[country_code]="54"
|
||||
```
|
||||
|
||||
The response is something like this:
|
||||
|
||||
```json
|
||||
{"message":"User created successfully.","user":{"id":xxxxxxxx},"success":true}
|
||||
```
|
||||
|
||||
Save the user id somewhere and add a reference to the matching SFTPGo account. You could also store this ID in the `additional_info` SFTPGo user field.
|
||||
|
||||
After this step you can use the Authy app installed on your phone to generate TOTP codes.
|
||||
|
||||
Now you can verify the token using an HTTP GET request:
|
||||
|
||||
```bash
|
||||
export TOKEN=<TOTP you read from Authy app>
|
||||
export AUTHY_ID=<user id>
|
||||
curl -i "https://api.authy.com/protected/json/verify/${TOKEN}/${AUTHY_ID}" \
|
||||
-H "X-Authy-API-Key: $AUTHY_API_KEY"
|
||||
```
|
||||
|
||||
So inside your hook you need to check:
|
||||
|
||||
- the HTTP response code for the verify request, it must be `200`
|
||||
- the JSON response body, it must contains the key `success` with the value `true` (as string)
|
||||
|
||||
If these conditions are met the token is valid and you allow the user to login.
|
||||
|
||||
We provide the following examples:
|
||||
|
||||
- [Keyboard interactive authentication](./keyint/README.md) for 2FA using password + Authy one time token.
|
||||
- [External authentication](./extauth/README.md) using Authy one time tokens as passwords.
|
||||
- [Check password hook](./checkpwd/README.md) for 2FA using a password consisting of a fixed string and a One Time Token.
|
||||
|
||||
Please note that these are sample programs not intended for production use, you should write your own hook based on them and you should prefer HTTP based hooks if performance is a concern.
|
||||
|
||||
:warning: SFTPGo has also built-in 2FA support.
|
||||
3
examples/OTP/authy/checkpwd/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Authy 2FA via check password hook
|
||||
|
||||
This example shows how to use 2FA via the check password hook using a password consisting of a fixed part and an Authy TOTP token. The hook will check the TOTP token using the Authy API and SFTPGo will check the fixed part. Please read the [sample code](./main.go), it should be self explanatory.
|
||||
3
examples/OTP/authy/checkpwd/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module github.com/drakkan/sftpgo/authy/checkpwd
|
||||
|
||||
go 1.22.2
|
||||
106
examples/OTP/authy/checkpwd/main.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type userMapping struct {
|
||||
SFTPGoUsername string
|
||||
AuthyID int64
|
||||
AuthyAPIKey string
|
||||
}
|
||||
|
||||
type checkPasswordResponse struct {
|
||||
// 0 KO, 1 OK, 2 partial success
|
||||
Status int `json:"status"`
|
||||
// for status == 2 this is the password that SFTPGo will check against the one stored
|
||||
// inside the data provider
|
||||
ToVerify string `json:"to_verify"`
|
||||
}
|
||||
|
||||
var (
|
||||
mapping []userMapping
|
||||
)
|
||||
|
||||
func init() {
|
||||
// this is for demo only, you probably want to get this mapping dynamically, for example using a database query
|
||||
mapping = append(mapping, userMapping{
|
||||
SFTPGoUsername: "<SFTPGo username>",
|
||||
AuthyID: 1234567,
|
||||
AuthyAPIKey: "<your api key>",
|
||||
})
|
||||
}
|
||||
|
||||
func printResponse(status int, toVerify string) {
|
||||
r := checkPasswordResponse{
|
||||
Status: status,
|
||||
ToVerify: toVerify,
|
||||
}
|
||||
resp, _ := json.Marshal(r)
|
||||
fmt.Printf("%v\n", string(resp))
|
||||
if status > 0 {
|
||||
os.Exit(0)
|
||||
} else {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// get credentials from env vars
|
||||
username := os.Getenv("SFTPGO_AUTHD_USERNAME")
|
||||
password := os.Getenv("SFTPGO_AUTHD_PASSWORD")
|
||||
|
||||
for _, m := range mapping {
|
||||
if m.SFTPGoUsername == username {
|
||||
// Authy token len is 7, we assume that we have the password followed by the token
|
||||
pwdLen := len(password)
|
||||
if pwdLen <= 7 {
|
||||
printResponse(0, "")
|
||||
}
|
||||
pwd := password[:pwdLen-7]
|
||||
authyToken := password[pwdLen-7:]
|
||||
// now verify the authy token and instruct SFTPGo to check the password if the token is OK
|
||||
url := fmt.Sprintf("https://api.authy.com/protected/json/verify/%v/%v", authyToken, m.AuthyID)
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
req.Header.Set("X-Authy-API-Key", m.AuthyAPIKey)
|
||||
httpClient := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
printResponse(0, "")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
// status code 200 is expected
|
||||
printResponse(0, "")
|
||||
}
|
||||
var authyResponse map[string]interface{}
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
printResponse(0, "")
|
||||
}
|
||||
err = json.Unmarshal(respBody, &authyResponse)
|
||||
if err != nil {
|
||||
printResponse(0, "")
|
||||
}
|
||||
if authyResponse["success"].(string) == "true" {
|
||||
printResponse(2, pwd)
|
||||
}
|
||||
printResponse(0, "")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// no mapping found
|
||||
printResponse(0, "")
|
||||
}
|
||||
3
examples/OTP/authy/extauth/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Authy external authentication
|
||||
|
||||
This example shows how to use Authy TOTP token as password for SFTPGo users. Please read the [sample code](./main.go), it should be self explanatory.
|
||||
3
examples/OTP/authy/extauth/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module github.com/drakkan/sftpgo/authy/extauth
|
||||
|
||||
go 1.22.2
|
||||
109
examples/OTP/authy/extauth/main.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type userMapping struct {
|
||||
SFTPGoUsername string
|
||||
AuthyID int64
|
||||
AuthyAPIKey string
|
||||
}
|
||||
|
||||
// we assume that the SFTPGo already exists, we only check the one time token.
|
||||
// If you need to create the SFTPGo user more fields are needed here
|
||||
type minimalSFTPGoUser struct {
|
||||
Status int `json:"status,omitempty"`
|
||||
Username string `json:"username"`
|
||||
HomeDir string `json:"home_dir,omitempty"`
|
||||
Permissions map[string][]string `json:"permissions"`
|
||||
}
|
||||
|
||||
var (
|
||||
mapping []userMapping
|
||||
)
|
||||
|
||||
func init() {
|
||||
// this is for demo only, you probably want to get this mapping dynamically, for example using a database query
|
||||
mapping = append(mapping, userMapping{
|
||||
SFTPGoUsername: "<SFTPGo username>",
|
||||
AuthyID: 1234567,
|
||||
AuthyAPIKey: "<your api key>",
|
||||
})
|
||||
}
|
||||
|
||||
func printResponse(username string) {
|
||||
u := minimalSFTPGoUser{
|
||||
Username: username,
|
||||
Status: 1,
|
||||
HomeDir: filepath.Join(os.TempDir(), username),
|
||||
}
|
||||
u.Permissions = make(map[string][]string)
|
||||
u.Permissions["/"] = []string{"*"}
|
||||
resp, _ := json.Marshal(u)
|
||||
fmt.Printf("%v\n", string(resp))
|
||||
if len(username) > 0 {
|
||||
os.Exit(0)
|
||||
} else {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// get credentials from env vars
|
||||
username := os.Getenv("SFTPGO_AUTHD_USERNAME")
|
||||
password := os.Getenv("SFTPGO_AUTHD_PASSWORD")
|
||||
if len(password) == 0 {
|
||||
// login method is not password
|
||||
printResponse("")
|
||||
return
|
||||
}
|
||||
|
||||
for _, m := range mapping {
|
||||
if m.SFTPGoUsername == username {
|
||||
// mapping found we can now verify the token
|
||||
url := fmt.Sprintf("https://api.authy.com/protected/json/verify/%v/%v", password, m.AuthyID)
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
req.Header.Set("X-Authy-API-Key", m.AuthyAPIKey)
|
||||
httpClient := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
printResponse("")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
// status code 200 is expected
|
||||
printResponse("")
|
||||
}
|
||||
var authyResponse map[string]interface{}
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
printResponse("")
|
||||
}
|
||||
err = json.Unmarshal(respBody, &authyResponse)
|
||||
if err != nil {
|
||||
printResponse("")
|
||||
}
|
||||
if authyResponse["success"].(string) == "true" {
|
||||
printResponse(username)
|
||||
}
|
||||
printResponse("")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// no mapping found
|
||||
printResponse("")
|
||||
}
|
||||
3
examples/OTP/authy/keyint/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Authy 2FA using keyboard interactive authentication
|
||||
|
||||
This example shows how to authenticate SFTP users using 2FA (password + Authy token). Please read the [sample code](./main.go), it should be self explanatory.
|
||||
3
examples/OTP/authy/keyint/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module github.com/drakkan/sftpgo/authy/keyint
|
||||
|
||||
go 1.22.2
|
||||
137
examples/OTP/authy/keyint/main.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type userMapping struct {
|
||||
SFTPGoUsername string
|
||||
AuthyID int64
|
||||
AuthyAPIKey string
|
||||
}
|
||||
|
||||
type keyboardAuthHookResponse struct {
|
||||
Instruction string `json:"instruction,omitempty"`
|
||||
Questions []string `json:"questions,omitempty"`
|
||||
Echos []bool `json:"echos,omitempty"`
|
||||
AuthResult int `json:"auth_result"`
|
||||
CheckPwd int `json:"check_password,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
mapping []userMapping
|
||||
)
|
||||
|
||||
func init() {
|
||||
// this is for demo only, you probably want to get this mapping dynamically, for example using a database query
|
||||
mapping = append(mapping, userMapping{
|
||||
SFTPGoUsername: "<SFTPGo username>",
|
||||
AuthyID: 1234567,
|
||||
AuthyAPIKey: "<your api key>",
|
||||
})
|
||||
}
|
||||
|
||||
func printAuthResponse(result int) {
|
||||
resp, _ := json.Marshal(keyboardAuthHookResponse{
|
||||
AuthResult: result,
|
||||
})
|
||||
fmt.Printf("%v\n", string(resp))
|
||||
if result == 1 {
|
||||
os.Exit(0)
|
||||
} else {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// get credentials from env vars
|
||||
username := os.Getenv("SFTPGO_AUTHD_USERNAME")
|
||||
var userMap userMapping
|
||||
for _, m := range mapping {
|
||||
if m.SFTPGoUsername == username {
|
||||
userMap = m
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if userMap.SFTPGoUsername != username {
|
||||
// no mapping found
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
checkPwdQuestion := keyboardAuthHookResponse{
|
||||
Instruction: "This is a sample keyboard authentication program that ask for your password + Authy token",
|
||||
Questions: []string{"Your password: "},
|
||||
Echos: []bool{false},
|
||||
CheckPwd: 1,
|
||||
AuthResult: 0,
|
||||
}
|
||||
|
||||
q, _ := json.Marshal(checkPwdQuestion)
|
||||
fmt.Printf("%v\n", string(q))
|
||||
|
||||
// in a real world app you probably want to use a read timeout
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
scanner.Scan()
|
||||
if scanner.Err() != nil {
|
||||
printAuthResponse(-1)
|
||||
}
|
||||
response := scanner.Text()
|
||||
if response != "OK" {
|
||||
printAuthResponse(-1)
|
||||
}
|
||||
|
||||
checkTokenQuestion := keyboardAuthHookResponse{
|
||||
Instruction: "",
|
||||
Questions: []string{"Authy token: "},
|
||||
Echos: []bool{false},
|
||||
CheckPwd: 0,
|
||||
AuthResult: 0,
|
||||
}
|
||||
|
||||
q, _ = json.Marshal(checkTokenQuestion)
|
||||
fmt.Printf("%v\n", string(q))
|
||||
scanner.Scan()
|
||||
if scanner.Err() != nil {
|
||||
printAuthResponse(-1)
|
||||
}
|
||||
authyToken := scanner.Text()
|
||||
|
||||
url := fmt.Sprintf("https://api.authy.com/protected/json/verify/%v/%v", authyToken, userMap.AuthyID)
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
printAuthResponse(-1)
|
||||
}
|
||||
req.Header.Set("X-Authy-API-Key", userMap.AuthyAPIKey)
|
||||
httpClient := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
printAuthResponse(-1)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
// status code 200 is expected
|
||||
printAuthResponse(-1)
|
||||
}
|
||||
var authyResponse map[string]interface{}
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
printAuthResponse(-1)
|
||||
}
|
||||
err = json.Unmarshal(respBody, &authyResponse)
|
||||
if err != nil {
|
||||
printAuthResponse(-1)
|
||||
}
|
||||
if authyResponse["success"].(string) == "true" {
|
||||
printAuthResponse(1)
|
||||
}
|
||||
printAuthResponse(-1)
|
||||
}
|
||||
19
examples/backup/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Data Backup
|
||||
|
||||
:warning: Since v2.4.0 you can use the [EventManager](https://sftpgo.github.io/latest/eventmanager/) to schedule backups.
|
||||
|
||||
The `backup` example script shows how to use the SFTPGo REST API to backup your data.
|
||||
|
||||
The script is written in Python and has the following requirements:
|
||||
|
||||
- python3 or python2
|
||||
- python [Requests](https://requests.readthedocs.io/en/master/) module
|
||||
|
||||
The provided example tries to connect to an SFTPGo instance running on `127.0.0.1:8080` using the following credentials:
|
||||
|
||||
- username: `admin`
|
||||
- password: `password`
|
||||
|
||||
and, if you execute it daily, it saves a different backup file for each day of the week. The backups will be saved within the configured `backups_path`.
|
||||
|
||||
Please edit the script according to your needs.
|
||||
36
examples/backup/backup
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from datetime import datetime
|
||||
import sys
|
||||
|
||||
import requests
|
||||
|
||||
try:
|
||||
import urllib.parse as urlparse
|
||||
except ImportError:
|
||||
import urlparse
|
||||
|
||||
# change base_url to point to your SFTPGo installation
|
||||
base_url = "http://127.0.0.1:8080"
|
||||
# set to False if you want to skip TLS certificate validation
|
||||
verify_tls_cert = True
|
||||
# set the credentials for a valid admin here
|
||||
admin_user = "admin"
|
||||
admin_password = "password"
|
||||
|
||||
# get a JWT token
|
||||
auth = requests.auth.HTTPBasicAuth(admin_user, admin_password)
|
||||
r = requests.get(urlparse.urljoin(base_url, "api/v2/token"), auth=auth, verify=verify_tls_cert, timeout=10)
|
||||
if r.status_code != 200:
|
||||
print("error getting access token: {}".format(r.text))
|
||||
sys.exit(1)
|
||||
access_token = r.json()["access_token"]
|
||||
auth_header = {"Authorization": "Bearer " + access_token}
|
||||
|
||||
r = requests.get(urlparse.urljoin(base_url, "api/v2/dumpdata"),
|
||||
params={"output-file":"backup_{}.json".format(datetime.today().strftime('%w'))},
|
||||
headers=auth_header, verify=verify_tls_cert, timeout=10)
|
||||
if r.status_code == 200:
|
||||
print("backup OK")
|
||||
else:
|
||||
print("backup error, status {}, response: {}".format(r.status_code, r.text))
|
||||
17
examples/bulkupdate/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Bulk user update
|
||||
|
||||
The `bulkuserupdate` example script shows how to use the SFTPGo REST API to easily update some common parameters for multiple users while preserving the others.
|
||||
|
||||
The script is written in Python and has the following requirements:
|
||||
|
||||
- python3 or python2
|
||||
- python [Requests](https://requests.readthedocs.io/en/master/) module
|
||||
|
||||
The provided example tries to connect to an SFTPGo instance running on `127.0.0.1:8080` using the following credentials:
|
||||
|
||||
- username: `admin`
|
||||
- password: `password`
|
||||
|
||||
and it updates some fields for `user1`, `user2` and `user3`.
|
||||
|
||||
Please edit the script according to your needs.
|
||||
49
examples/bulkupdate/bulkuserupdate
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import posixpath
|
||||
import sys
|
||||
|
||||
import requests
|
||||
|
||||
try:
|
||||
import urllib.parse as urlparse
|
||||
except ImportError:
|
||||
import urlparse
|
||||
|
||||
# change base_url to point to your SFTPGo installation
|
||||
base_url = "http://127.0.0.1:8080"
|
||||
# set to False if you want to skip TLS certificate validation
|
||||
verify_tls_cert = True
|
||||
# set the credentials for a valid admin here
|
||||
admin_user = "admin"
|
||||
admin_password = "password"
|
||||
# insert here the users you want to update
|
||||
users_to_update = ["user1", "user2", "user3"]
|
||||
# set here the fields you need to update
|
||||
fields_to_update = {"status":0, "quota_files": 1000, "additional_info":"updated using the bulkuserupdate example script"}
|
||||
|
||||
# get a JWT token
|
||||
auth = requests.auth.HTTPBasicAuth(admin_user, admin_password)
|
||||
r = requests.get(urlparse.urljoin(base_url, "api/v2/token"), auth=auth, verify=verify_tls_cert, timeout=10)
|
||||
if r.status_code != 200:
|
||||
print("error getting access token: {}".format(r.text))
|
||||
sys.exit(1)
|
||||
access_token = r.json()["access_token"]
|
||||
auth_header = {"Authorization": "Bearer " + access_token}
|
||||
|
||||
for username in users_to_update:
|
||||
r = requests.get(urlparse.urljoin(base_url, posixpath.join("api/v2/users", username)),
|
||||
headers=auth_header, verify=verify_tls_cert, timeout=10)
|
||||
if r.status_code != 200:
|
||||
print("error getting user {}: {}".format(username, r.text))
|
||||
continue
|
||||
user = r.json()
|
||||
user.update(fields_to_update)
|
||||
r = requests.put(urlparse.urljoin(base_url, posixpath.join("api/v2/users", username)),
|
||||
headers=auth_header, verify=verify_tls_cert, json=user, timeout=10)
|
||||
if r.status_code == 200:
|
||||
print("user {} updated".format(username))
|
||||
else:
|
||||
print("error updating user {}, response code: {} response text: {}".format(username,
|
||||
r.status_code,
|
||||
r.text))
|
||||
51
examples/convertusers/README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Import users from other stores
|
||||
|
||||
`convertusers` is a very simple command line client, written in python, to import users from other stores. It requires `python3` or `python2`.
|
||||
|
||||
Here is the usage:
|
||||
|
||||
```console
|
||||
usage: convertusers [-h] [--min-uid MIN_UID] [--max-uid MAX_UID] [--usernames USERNAMES [USERNAMES ...]]
|
||||
[--force-uid FORCE_UID] [--force-gid FORCE_GID]
|
||||
input_file {unix-passwd,pure-ftpd,proftpd} output_file
|
||||
|
||||
Convert users to a JSON format suitable to use with loadddata
|
||||
|
||||
positional arguments:
|
||||
input_file
|
||||
{unix-passwd,pure-ftpd,proftpd}
|
||||
To import from unix-passwd format you need the permission to read /etc/shadow that is typically
|
||||
granted to the root user only
|
||||
output_file
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--min-uid MIN_UID if >= 0 only import users with UID greater or equal to this value. Default: -1
|
||||
--max-uid MAX_UID if >= 0 only import users with UID lesser or equal to this value. Default: -1
|
||||
--usernames USERNAMES [USERNAMES ...]
|
||||
Only import users with these usernames. Default: []
|
||||
--force-uid FORCE_UID
|
||||
if >= 0 the imported users will have this UID in SFTPGo. Default: -1
|
||||
--force-gid FORCE_GID
|
||||
if >= 0 the imported users will have this GID in SFTPGo. Default: -1
|
||||
```
|
||||
|
||||
Let's see some examples:
|
||||
|
||||
```console
|
||||
python convertusers "" unix-passwd unix_users.json --min-uid 500 --force-uid 1000 --force-gid 1000
|
||||
```
|
||||
|
||||
```console
|
||||
python convertusers pureftpd.passwd pure-ftpd pure_users.json --usernames "user1" "user2"
|
||||
```
|
||||
|
||||
```console
|
||||
python convertusers proftpd.passwd proftpd pro_users.json
|
||||
```
|
||||
|
||||
The generated json file can be used as input for the `loaddata` REST API.
|
||||
|
||||
Please note that when importing Linux/Unix users the input file is not required: `/etc/passwd` and `/etc/shadow` are automatically parsed. `/etc/shadow` read permission is typically granted to the `root` user only, so you need to execute `convertusers` as `root`.
|
||||
|
||||
:warning: SFTPGo does not currently support `yescrypt` hashed passwords.
|
||||
208
examples/convertusers/convertusers
Executable file
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
import pwd
|
||||
import spwd
|
||||
except ImportError:
|
||||
pwd = None
|
||||
|
||||
|
||||
class ConvertUsers:
|
||||
|
||||
def __init__(self, input_file, users_format, output_file, min_uid, max_uid, usernames, force_uid, force_gid):
|
||||
self.input_file = input_file
|
||||
self.users_format = users_format
|
||||
self.output_file = output_file
|
||||
self.min_uid = min_uid
|
||||
self.max_uid = max_uid
|
||||
self.usernames = usernames
|
||||
self.force_uid = force_uid
|
||||
self.force_gid = force_gid
|
||||
self.SFTPGoUsers = []
|
||||
|
||||
def buildUserObject(self, username, password, home_dir, uid, gid, max_sessions, quota_size, quota_files, upload_bandwidth,
|
||||
download_bandwidth, status, expiration_date, allowed_ip=[], denied_ip=[]):
|
||||
return {'id':0, 'username':username, 'password':password, 'home_dir':home_dir, 'uid':uid, 'gid':gid,
|
||||
'max_sessions':max_sessions, 'quota_size':quota_size, 'quota_files':quota_files, 'permissions':{'/':["*"]},
|
||||
'upload_bandwidth':upload_bandwidth, 'download_bandwidth':download_bandwidth,
|
||||
'status':status, 'expiration_date':expiration_date,
|
||||
'filters':{'allowed_ip':allowed_ip, 'denied_ip':denied_ip}}
|
||||
|
||||
def addUser(self, user):
|
||||
user['id'] = len(self.SFTPGoUsers) + 1
|
||||
print('')
|
||||
print('New user imported: {}'.format(user))
|
||||
print('')
|
||||
self.SFTPGoUsers.append(user)
|
||||
|
||||
def saveUsers(self):
|
||||
if self.SFTPGoUsers:
|
||||
data = {'users':self.SFTPGoUsers}
|
||||
jsonData = json.dumps(data)
|
||||
with open(self.output_file, 'w') as f:
|
||||
f.write(jsonData)
|
||||
print()
|
||||
print('Number of users saved to "{}": {}. You can import them using loaddata'.format(self.output_file,
|
||||
len(self.SFTPGoUsers)))
|
||||
print()
|
||||
sys.exit(0)
|
||||
else:
|
||||
print('No user imported')
|
||||
sys.exit(1)
|
||||
|
||||
def convert(self):
|
||||
if self.users_format == 'unix-passwd':
|
||||
self.convertFromUnixPasswd()
|
||||
elif self.users_format == 'pure-ftpd':
|
||||
self.convertFromPureFTPD()
|
||||
else:
|
||||
self.convertFromProFTPD()
|
||||
self.saveUsers()
|
||||
|
||||
def isUserValid(self, username, uid):
|
||||
if self.usernames and not username in self.usernames:
|
||||
return False
|
||||
if self.min_uid >= 0 and uid < self.min_uid:
|
||||
return False
|
||||
if self.max_uid >= 0 and uid > self.max_uid:
|
||||
return False
|
||||
return True
|
||||
|
||||
def convertFromUnixPasswd(self):
|
||||
days_from_epoch_time = time.time() / 86400
|
||||
for user in pwd.getpwall():
|
||||
username = user.pw_name
|
||||
password = user.pw_passwd
|
||||
uid = user.pw_uid
|
||||
gid = user.pw_gid
|
||||
home_dir = user.pw_dir
|
||||
status = 1
|
||||
expiration_date = 0
|
||||
if not self.isUserValid(username, uid):
|
||||
continue
|
||||
if self.force_uid >= 0:
|
||||
uid = self.force_uid
|
||||
if self.force_gid >= 0:
|
||||
gid = self.force_gid
|
||||
# FIXME: if the passwords aren't in /etc/shadow they are probably DES encrypted and we don't support them
|
||||
if password == 'x' or password == '*':
|
||||
user_info = spwd.getspnam(username)
|
||||
password = user_info.sp_pwdp
|
||||
if not password or password == '!!' or password == '!*':
|
||||
print('cannot import user "{}" without a password'.format(username))
|
||||
continue
|
||||
if user_info.sp_inact > 0:
|
||||
last_pwd_change_diff = days_from_epoch_time - user_info.sp_lstchg
|
||||
if last_pwd_change_diff > user_info.sp_inact:
|
||||
status = 0
|
||||
if user_info.sp_expire > 0:
|
||||
expiration_date = user_info.sp_expire * 86400
|
||||
self.addUser(self.buildUserObject(username, password, home_dir, uid, gid, 0, 0, 0, 0, 0, status,
|
||||
expiration_date))
|
||||
|
||||
def convertFromProFTPD(self):
|
||||
with open(self.input_file, 'r') as f:
|
||||
for line in f:
|
||||
fields = line.split(':')
|
||||
if len(fields) > 6:
|
||||
username = fields[0]
|
||||
password = fields[1]
|
||||
uid = int(fields[2])
|
||||
gid = int(fields[3])
|
||||
home_dir = fields[5]
|
||||
if not self.isUserValid(username, uid):
|
||||
continue
|
||||
if self.force_uid >= 0:
|
||||
uid = self.force_uid
|
||||
if self.force_gid >= 0:
|
||||
gid = self.force_gid
|
||||
self.addUser(self.buildUserObject(username, password, home_dir, uid, gid, 0, 0, 0, 0, 0, 1, 0))
|
||||
|
||||
def convertPureFTPDIP(self, fields):
|
||||
result = []
|
||||
if not fields:
|
||||
return result
|
||||
for v in fields.split(','):
|
||||
ip_mask = v.strip()
|
||||
if not ip_mask:
|
||||
continue
|
||||
if ip_mask.count('.') < 3 and ip_mask.count(':') < 3:
|
||||
print('cannot import pure-ftpd IP: {}'.format(ip_mask))
|
||||
continue
|
||||
if '/' not in ip_mask:
|
||||
ip_mask += '/32'
|
||||
result.append(ip_mask)
|
||||
return result
|
||||
|
||||
def convertFromPureFTPD(self):
|
||||
with open(self.input_file, 'r') as f:
|
||||
for line in f:
|
||||
fields = line.split(':')
|
||||
if len(fields) > 16:
|
||||
username = fields[0]
|
||||
password = fields[1]
|
||||
uid = int(fields[2])
|
||||
gid = int(fields[3])
|
||||
home_dir = fields[5]
|
||||
upload_bandwidth = 0
|
||||
if fields[6]:
|
||||
upload_bandwidth = int(int(fields[6]) / 1024)
|
||||
download_bandwidth = 0
|
||||
if fields[7]:
|
||||
download_bandwidth = int(int(fields[7]) / 1024)
|
||||
max_sessions = 0
|
||||
if fields[10]:
|
||||
max_sessions = int(fields[10])
|
||||
quota_files = 0
|
||||
if fields[11]:
|
||||
quota_files = int(fields[11])
|
||||
quota_size = 0
|
||||
if fields[12]:
|
||||
quota_size = int(fields[12])
|
||||
allowed_ip = self.convertPureFTPDIP(fields[15])
|
||||
denied_ip = self.convertPureFTPDIP(fields[16])
|
||||
if not self.isUserValid(username, uid):
|
||||
continue
|
||||
if self.force_uid >= 0:
|
||||
uid = self.force_uid
|
||||
if self.force_gid >= 0:
|
||||
gid = self.force_gid
|
||||
self.addUser(self.buildUserObject(username, password, home_dir, uid, gid, max_sessions, quota_size,
|
||||
quota_files, upload_bandwidth, download_bandwidth, 1, 0, allowed_ip,
|
||||
denied_ip))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=
|
||||
'Convert users to a JSON format suitable to use with loadddata')
|
||||
supportedUsersFormats = []
|
||||
help_text = ''
|
||||
if pwd is not None:
|
||||
supportedUsersFormats.append('unix-passwd')
|
||||
help_text = 'To import from unix-passwd format you need the permission to read /etc/shadow that is typically granted to the root user only'
|
||||
supportedUsersFormats.append('pure-ftpd')
|
||||
supportedUsersFormats.append('proftpd')
|
||||
parser.add_argument('input_file', type=str)
|
||||
parser.add_argument('users_format', type=str, choices=supportedUsersFormats, help=help_text)
|
||||
parser.add_argument('output_file', type=str)
|
||||
parser.add_argument('--min-uid', type=int, default=-1, help='if >= 0 only import users with UID greater or equal ' +
|
||||
'to this value. Default: %(default)s')
|
||||
parser.add_argument('--max-uid', type=int, default=-1, help='if >= 0 only import users with UID lesser or equal ' +
|
||||
'to this value. Default: %(default)s')
|
||||
parser.add_argument('--usernames', type=str, nargs='+', default=[], help='Only import users with these usernames. ' +
|
||||
'Default: %(default)s')
|
||||
parser.add_argument('--force-uid', type=int, default=-1, help='if >= 0 the imported users will have this UID in ' +
|
||||
'SFTPGo. Default: %(default)s')
|
||||
parser.add_argument('--force-gid', type=int, default=-1, help='if >= 0 the imported users will have this GID in ' +
|
||||
'SFTPGo. Default: %(default)s')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
convertUsers = ConvertUsers(args.input_file, args.users_format, args.output_file, args.min_uid, args.max_uid,
|
||||
args.usernames, args.force_uid, args.force_gid)
|
||||
convertUsers.convert()
|
||||
36
examples/data-retention/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# File retention policies
|
||||
|
||||
:warning: Since v2.4.0 you can use the [EventManager](https://sftpgo.github.io/latest/eventmanager/) to schedule data retention checks.
|
||||
|
||||
The `checkretention` example script shows how to use the SFTPGo REST API to manage data retention.
|
||||
|
||||
:warning: Deleting files is an irreversible action, please make sure you fully understand what you are doing before using this feature, you may have users with overlapping home directories or virtual folders shared between multiple users, it is relatively easy to inadvertently delete files you need.
|
||||
|
||||
The example shows how to setup a really simple retention policy, for each user it sends this request:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"path": "/",
|
||||
"retention": 168,
|
||||
"delete_empty_dirs": true,
|
||||
"ignore_user_permissions": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
so alls files with modification time older than 168 hours (7 days) will be deleted. Empty directories will be removed and the check will respect user's permissions, so if the user cannot delete a file/folder it will be skipped.
|
||||
|
||||
You can define different retention policies per-user and per-folder and you can exclude a folder setting the retention to `0`.
|
||||
|
||||
You can use this script as a starting point, please edit it according to your needs.
|
||||
|
||||
The script is written in Python and has the following requirements:
|
||||
|
||||
- python3 or python2
|
||||
- python [Requests](https://requests.readthedocs.io/en/master/) module
|
||||
|
||||
The provided example tries to connect to an SFTPGo instance running on `127.0.0.1:8080` using the following credentials:
|
||||
|
||||
- username: `admin`
|
||||
- password: `password`
|
||||
115
examples/data-retention/checkretention
Executable file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from datetime import datetime
|
||||
import sys
|
||||
import time
|
||||
|
||||
import pytz
|
||||
import requests
|
||||
|
||||
try:
|
||||
import urllib.parse as urlparse
|
||||
except ImportError:
|
||||
import urlparse
|
||||
|
||||
# change base_url to point to your SFTPGo installation
|
||||
base_url = "http://127.0.0.1:8080"
|
||||
# set to False if you want to skip TLS certificate validation
|
||||
verify_tls_cert = True
|
||||
# set the credentials for a valid admin here
|
||||
admin_user = "admin"
|
||||
admin_password = "password"
|
||||
|
||||
|
||||
class CheckRetention:
|
||||
|
||||
def __init__(self):
|
||||
self.limit = 100
|
||||
self.offset = 0
|
||||
self.access_token = ""
|
||||
self.access_token_expiration = None
|
||||
|
||||
def printLog(self, message):
|
||||
print("{} - {}".format(datetime.now(), message))
|
||||
|
||||
def checkAccessToken(self):
|
||||
if self.access_token != "" and self.access_token_expiration:
|
||||
expire_diff = self.access_token_expiration - datetime.now(tz=pytz.UTC)
|
||||
# we don't use total_seconds to be python 2 compatible
|
||||
seconds_to_expire = expire_diff.days * 86400 + expire_diff.seconds
|
||||
if seconds_to_expire > 180:
|
||||
return
|
||||
|
||||
auth = requests.auth.HTTPBasicAuth(admin_user, admin_password)
|
||||
r = requests.get(urlparse.urljoin(base_url, "api/v2/token"), auth=auth, verify=verify_tls_cert, timeout=10)
|
||||
if r.status_code != 200:
|
||||
self.printLog("error getting access token: {}".format(r.text))
|
||||
sys.exit(1)
|
||||
self.access_token = r.json()["access_token"]
|
||||
self.access_token_expiration = pytz.timezone("UTC").localize(datetime.strptime(r.json()["expires_at"],
|
||||
"%Y-%m-%dT%H:%M:%SZ"))
|
||||
|
||||
def getAuthHeader(self):
|
||||
self.checkAccessToken()
|
||||
return {"Authorization": "Bearer " + self.access_token}
|
||||
|
||||
def waitForRentionCheck(self, username):
|
||||
while True:
|
||||
auth_header = self.getAuthHeader()
|
||||
r = requests.get(urlparse.urljoin(base_url, "api/v2/retention/users/checks"), headers=auth_header, verify=verify_tls_cert,
|
||||
timeout=10)
|
||||
if r.status_code != 200:
|
||||
self.printLog("error getting retention checks while waiting for {}: {}".format(username, r.text))
|
||||
sys.exit(1)
|
||||
|
||||
checking = False
|
||||
for check in r.json():
|
||||
if check["username"] == username:
|
||||
checking = True
|
||||
if not checking:
|
||||
break
|
||||
self.printLog("waiting for the retention check to complete for user {}".format(username))
|
||||
time.sleep(2)
|
||||
|
||||
self.printLog("retention check for user {} finished".format(username))
|
||||
|
||||
def checkUserRetention(self, username):
|
||||
self.printLog("starting retention check for user {}".format(username))
|
||||
auth_header = self.getAuthHeader()
|
||||
retention = [
|
||||
{
|
||||
"path": "/",
|
||||
"retention": 168,
|
||||
"delete_empty_dirs": True,
|
||||
"ignore_user_permissions": False
|
||||
}
|
||||
]
|
||||
r = requests.post(urlparse.urljoin(base_url, "api/v2/retention/users/" + username + "/check"), headers=auth_header,
|
||||
json=retention, verify=verify_tls_cert, timeout=10)
|
||||
if r.status_code != 202:
|
||||
self.printLog("error starting retention check for user {}: {}".format(username, r.text))
|
||||
sys.exit(1)
|
||||
self.waitForRentionCheck(username)
|
||||
|
||||
def checkUsersRetention(self):
|
||||
while True:
|
||||
self.printLog("get users, limit {} offset {}".format(self.limit, self.offset))
|
||||
auth_header = self.getAuthHeader()
|
||||
payload = {"limit":self.limit, "offset":self.offset}
|
||||
r = requests.get(urlparse.urljoin(base_url, "api/v2/users"), headers=auth_header, params=payload,
|
||||
verify=verify_tls_cert, timeout=10)
|
||||
if r.status_code != 200:
|
||||
self.printLog("error getting users: {}".format(r.text))
|
||||
sys.exit(1)
|
||||
users = r.json()
|
||||
for user in users:
|
||||
self.checkUserRetention(user["username"])
|
||||
|
||||
self.offset += len(users)
|
||||
if len(users) < self.limit:
|
||||
break
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
c = CheckRetention()
|
||||
c.checkUsersRetention()
|
||||
48
examples/ldapauth/README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# LDAPAuth
|
||||
|
||||
This is an example for an external authentication program. It performs authentication against an LDAP server.
|
||||
It is tested against [389ds](https://directory.fedoraproject.org/) and can be used as starting point to authenticate using any LDAP server including Active Directory.
|
||||
|
||||
You need to change the LDAP connection parameters and the user search query to match your environment.
|
||||
You can build this example using the following command:
|
||||
|
||||
```console
|
||||
go build -ldflags "-s -w" -o ldapauth
|
||||
```
|
||||
|
||||
This program assumes that the 389ds schema was extended to add support for public keys using the following ldif file placed in `/etc/dirsrv/schema/98openssh-ldap.ldif`:
|
||||
|
||||
```console
|
||||
dn: cn=schema
|
||||
changetype: modify
|
||||
add: attributetypes
|
||||
attributetypes: ( 1.3.6.1.4.1.24552.500.1.1.1.13 NAME 'sshPublicKey' DESC 'MANDATORY: OpenSSH Public key' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
|
||||
-
|
||||
add: objectclasses
|
||||
objectClasses: ( 1.3.6.1.4.1.24552.500.1.1.2.0 NAME 'ldapPublicKey' SUP top AUXILIARY DESC 'MANDATORY: OpenSSH LPK objectclass' MUST ( uid ) MAY ( sshPublicKey ) )
|
||||
-
|
||||
|
||||
dn: cn=sshpublickey,cn=default indexes,cn=config,cn=ldbm database,cn=plugins,cn=config
|
||||
changetype: add
|
||||
cn: sshpublickey
|
||||
nsIndexType: eq
|
||||
nsIndexType: pres
|
||||
nsSystemIndex: false
|
||||
objectClass: top
|
||||
objectClass: nsIndex
|
||||
|
||||
dn: cn=sshpublickey_self_manage,ou=groups,dc=example,dc=com
|
||||
changetype: add
|
||||
objectClass: top
|
||||
objectClass: groupofuniquenames
|
||||
cn: sshpublickey_self_manage
|
||||
description: Members of this group gain the ability to edit their own sshPublicKey field
|
||||
|
||||
dn: dc=example,dc=com
|
||||
changetype: modify
|
||||
add: aci
|
||||
aci: (targetattr = "sshPublicKey") (version 3.0; acl "Allow members of sshpublickey_self_manage to edit their keys"; allow(write) (groupdn = "ldap:///cn=sshpublickey_self_manage,ou=groups,dc=example,dc=com" and userdn="ldap:///self" ); )
|
||||
-
|
||||
```
|
||||
|
||||
:warning: A plugin for LDAP/Active Directory authentication is also [available](https://github.com/sftpgo/sftpgo-plugin-auth).
|
||||
15
examples/ldapauth/go.mod
Normal file
@@ -0,0 +1,15 @@
|
||||
module github.com/drakkan/ldapauth
|
||||
|
||||
go 1.22.2
|
||||
|
||||
require (
|
||||
github.com/go-ldap/ldap/v3 v3.4.8
|
||||
golang.org/x/crypto v0.23.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
)
|
||||
99
examples/ldapauth/go.sum
Normal file
@@ -0,0 +1,99 @@
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ=
|
||||
github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
||||
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
175
examples/ldapauth/main.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/syslog"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const (
|
||||
rootDN = "dc=example,dc=com"
|
||||
bindUsername = "cn=sftpgo," + rootDN
|
||||
bindURL = "ldap:///" // That is, the server on the default port of localhost.
|
||||
passwordFile = "/etc/sftpgo/admin-password.txt" // make this file readable only by the server
|
||||
publicDir = "/var/www/webdav/public"
|
||||
)
|
||||
|
||||
type userFilters struct {
|
||||
DeniedLoginMethods []string `json:"denied_login_methods,omitempty"`
|
||||
}
|
||||
|
||||
type minimalSFTPGoUser struct {
|
||||
Status int `json:"status,omitempty"`
|
||||
Username string `json:"username"`
|
||||
HomeDir string `json:"home_dir,omitempty"`
|
||||
UID int `json:"uid,omitempty"`
|
||||
GID int `json:"gid,omitempty"`
|
||||
Permissions map[string][]string `json:"permissions"`
|
||||
Filters userFilters `json:"filters"`
|
||||
}
|
||||
|
||||
func exitError() {
|
||||
log.Printf("exitError\n")
|
||||
u := minimalSFTPGoUser{
|
||||
Username: "",
|
||||
}
|
||||
resp, _ := json.Marshal(u)
|
||||
fmt.Printf("%v\n", string(resp))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func printSuccessResponse(username, homeDir string, uid, gid int, permissions []string) {
|
||||
u := minimalSFTPGoUser{
|
||||
Username: username,
|
||||
HomeDir: homeDir,
|
||||
UID: uid,
|
||||
GID: gid,
|
||||
Status: 1,
|
||||
}
|
||||
u.Permissions = make(map[string][]string)
|
||||
u.Permissions["/"] = permissions
|
||||
// uncomment the next line to require publickey+password authentication
|
||||
//u.Filters.DeniedLoginMethods = []string{"publickey", "password", "keyboard-interactive", "publickey+keyboard-interactive"}
|
||||
resp, _ := json.Marshal(u)
|
||||
log.Printf("%v\n", string(resp))
|
||||
fmt.Printf("%v\n", string(resp))
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func main() {
|
||||
logWriter, err := syslog.New(syslog.LOG_NOTICE, "sftpgo")
|
||||
if err == nil {
|
||||
log.SetOutput(logWriter)
|
||||
}
|
||||
// get credentials from env vars
|
||||
username := os.Getenv("SFTPGO_AUTHD_USERNAME")
|
||||
password := os.Getenv("SFTPGO_AUTHD_PASSWORD")
|
||||
publickey := os.Getenv("SFTPGO_AUTHD_PUBLIC_KEY")
|
||||
if strings.ToLower(username) == "anonymous" {
|
||||
printSuccessResponse("anonymous", publicDir, 0, 0, []string{"list", "download"})
|
||||
return
|
||||
}
|
||||
l, err := ldap.DialURL(bindURL)
|
||||
if err != nil {
|
||||
log.Printf("DialURL: %s\n", err.Error())
|
||||
exitError()
|
||||
}
|
||||
defer l.Close()
|
||||
// bind to the ldap server with an account that can read users
|
||||
bindPassword, err := os.ReadFile(passwordFile)
|
||||
if err != nil {
|
||||
log.Printf("ReadFile(%s): %s\n", passwordFile, err.Error())
|
||||
exitError()
|
||||
}
|
||||
err = l.Bind(bindUsername, string(bindPassword))
|
||||
if err != nil {
|
||||
log.Printf("Bind(%s): %s\n", bindUsername, err.Error())
|
||||
exitError()
|
||||
}
|
||||
|
||||
// search the user trying to login and fetch some attributes, this search string is tested against 389ds using the default configuration
|
||||
log.Printf("username=%s\n", username)
|
||||
searchFilter := fmt.Sprintf("(uid=%s)", ldap.EscapeFilter(username))
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
"ou=people," + rootDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
searchFilter,
|
||||
[]string{"dn", "uid", "homeDirectory", "uidNumber", "gidNumber", "nsSshPublicKey"},
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := l.Search(searchRequest)
|
||||
if err != nil {
|
||||
log.Printf("Search(%s): %s\n", searchFilter, err.Error())
|
||||
exitError()
|
||||
}
|
||||
|
||||
// we expect exactly one user
|
||||
if len(sr.Entries) != 1 {
|
||||
log.Printf("Search(%s): %d entries\n", searchFilter, len(sr.Entries))
|
||||
exitError()
|
||||
}
|
||||
|
||||
if len(publickey) > 0 {
|
||||
// check public key
|
||||
userKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(publickey))
|
||||
if err != nil {
|
||||
log.Printf("ParseAuthorizedKey(%s): %s\n", publickey, err.Error())
|
||||
exitError()
|
||||
}
|
||||
authOk := false
|
||||
for _, k := range sr.Entries[0].GetAttributeValues("nsSshPublicKey") {
|
||||
key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k))
|
||||
// we skip an invalid public key stored inside the LDAP server
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if bytes.Equal(key.Marshal(), userKey.Marshal()) {
|
||||
authOk = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !authOk {
|
||||
log.Printf("publickey %s !authOk\n", publickey)
|
||||
exitError()
|
||||
}
|
||||
} else {
|
||||
// bind to the LDAP server with the user dn and the given password to check the password
|
||||
userdn := sr.Entries[0].DN
|
||||
// log.Printf("password=%s\n", password)
|
||||
err = l.Bind(userdn, password)
|
||||
if err != nil {
|
||||
log.Printf("Bind(%s): %s\n", userdn, err.Error())
|
||||
exitError()
|
||||
}
|
||||
}
|
||||
|
||||
// People in the LDAP directory aren't necessarily Linux users;
|
||||
// so they might not have a uidNumber or gidNumber.
|
||||
uidNumber := sr.Entries[0].GetAttributeValue("uidNumber")
|
||||
uid, err := strconv.Atoi(uidNumber)
|
||||
if err != nil {
|
||||
//log.Printf("uid Atoi(%s) = %s\n", uidNumber, err.Error())
|
||||
uid = 0
|
||||
}
|
||||
gidNumber := sr.Entries[0].GetAttributeValue("gidNumber")
|
||||
gid, err := strconv.Atoi(gidNumber)
|
||||
if err != nil {
|
||||
//log.Printf("gid Atoi(%s) = %s\n", gidNumber, err.Error())
|
||||
gid = 0
|
||||
}
|
||||
homeDir := sr.Entries[0].GetAttributeValue("homeDirectory")
|
||||
if (len(homeDir) <= 0) {
|
||||
homeDir = publicDir // homeDir is a required attribute.
|
||||
}
|
||||
// return the authenticated user
|
||||
printSuccessResponse(sr.Entries[0].GetAttributeValue("uid"), homeDir, uid, gid, []string{"*"})
|
||||
}
|
||||
13
examples/ldapauthserver/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# LDAPAuthServer
|
||||
|
||||
This is an example for an HTTP server to use as external authentication HTTP hook. It performs authentication against an LDAP server.
|
||||
It is tested against [389ds](https://directory.fedoraproject.org/) and can be used as starting point to authenticate using any LDAP server including Active Directory.
|
||||
|
||||
You can configure the server using the [ldapauth.toml](./ldapauth.toml) configuration file.
|
||||
You can build this example using the following command:
|
||||
|
||||
```console
|
||||
go build -ldflags "-s -w" -o ldapauthserver
|
||||
```
|
||||
|
||||
:warning: A plugin for LDAP/Active Directory authentication is also [available](https://github.com/sftpgo/sftpgo-plugin-auth).
|
||||
158
examples/ldapauthserver/cmd/root.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/drakkan/sftpgo/ldapauthserver/config"
|
||||
"github.com/drakkan/sftpgo/ldapauthserver/utils"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
logSender = "cmd"
|
||||
configDirFlag = "config-dir"
|
||||
configDirKey = "config_dir"
|
||||
configFileFlag = "config-file"
|
||||
configFileKey = "config_file"
|
||||
logFilePathFlag = "log-file-path"
|
||||
logFilePathKey = "log_file_path"
|
||||
logMaxSizeFlag = "log-max-size"
|
||||
logMaxSizeKey = "log_max_size"
|
||||
logMaxBackupFlag = "log-max-backups"
|
||||
logMaxBackupKey = "log_max_backups"
|
||||
logMaxAgeFlag = "log-max-age"
|
||||
logMaxAgeKey = "log_max_age"
|
||||
logCompressFlag = "log-compress"
|
||||
logCompressKey = "log_compress"
|
||||
logVerboseFlag = "log-verbose"
|
||||
logVerboseKey = "log_verbose"
|
||||
profilerFlag = "profiler"
|
||||
profilerKey = "profiler"
|
||||
defaultConfigDir = "."
|
||||
defaultConfigName = config.DefaultConfigName
|
||||
defaultLogFile = "ldapauth.log"
|
||||
defaultLogMaxSize = 10
|
||||
defaultLogMaxBackup = 5
|
||||
defaultLogMaxAge = 28
|
||||
defaultLogCompress = false
|
||||
defaultLogVerbose = true
|
||||
)
|
||||
|
||||
var (
|
||||
configDir string
|
||||
configFile string
|
||||
logFilePath string
|
||||
logMaxSize int
|
||||
logMaxBackups int
|
||||
logMaxAge int
|
||||
logCompress bool
|
||||
logVerbose bool
|
||||
|
||||
rootCmd = &cobra.Command{
|
||||
Use: "ldapauthserver",
|
||||
Short: "LDAP Authentication Server for SFTPGo",
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
version := utils.GetAppVersion()
|
||||
rootCmd.Flags().BoolP("version", "v", false, "")
|
||||
rootCmd.Version = version.GetVersionAsString()
|
||||
rootCmd.SetVersionTemplate(`{{printf "LDAP Authentication Server version: "}}{{printf "%s" .Version}}
|
||||
`)
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func addConfigFlags(cmd *cobra.Command) {
|
||||
viper.SetDefault(configDirKey, defaultConfigDir)
|
||||
viper.BindEnv(configDirKey, "LDAPAUTH_CONFIG_DIR")
|
||||
cmd.Flags().StringVarP(&configDir, configDirFlag, "c", viper.GetString(configDirKey),
|
||||
`Location for the config dir. This directory
|
||||
should contain the "ldapauth" configuration
|
||||
file or the configured config-file. This flag
|
||||
can be set using LDAPAUTH_CONFIG_DIR env var too.
|
||||
`)
|
||||
viper.BindPFlag(configDirKey, cmd.Flags().Lookup(configDirFlag))
|
||||
|
||||
viper.SetDefault(configFileKey, defaultConfigName)
|
||||
viper.BindEnv(configFileKey, "LDAPAUTH_CONFIG_FILE")
|
||||
cmd.Flags().StringVarP(&configFile, configFileFlag, "f", viper.GetString(configFileKey),
|
||||
`Name for the configuration file. It must be
|
||||
the name of a file stored in config-dir not
|
||||
the absolute path to the configuration file.
|
||||
The specified file name must have no extension
|
||||
we automatically load JSON, YAML, TOML, HCL and
|
||||
Java properties. Therefore if you set \"ldapauth\"
|
||||
then \"ldapauth.toml\", \"ldapauth.yaml\" and
|
||||
so on are searched. This flag can be set using
|
||||
LDAPAUTH_CONFIG_FILE env var too.
|
||||
`)
|
||||
viper.BindPFlag(configFileKey, cmd.Flags().Lookup(configFileFlag))
|
||||
}
|
||||
|
||||
func addServeFlags(cmd *cobra.Command) {
|
||||
addConfigFlags(cmd)
|
||||
|
||||
viper.SetDefault(logFilePathKey, defaultLogFile)
|
||||
viper.BindEnv(logFilePathKey, "LDAPAUTH_LOG_FILE_PATH")
|
||||
cmd.Flags().StringVarP(&logFilePath, logFilePathFlag, "l", viper.GetString(logFilePathKey),
|
||||
`Location for the log file. Leave empty to write
|
||||
logs to the standard output. This flag can be
|
||||
set using LDAPAUTH_LOG_FILE_PATH env var too.
|
||||
`)
|
||||
viper.BindPFlag(logFilePathKey, cmd.Flags().Lookup(logFilePathFlag))
|
||||
|
||||
viper.SetDefault(logMaxSizeKey, defaultLogMaxSize)
|
||||
viper.BindEnv(logMaxSizeKey, "LDAPAUTH_LOG_MAX_SIZE")
|
||||
cmd.Flags().IntVarP(&logMaxSize, logMaxSizeFlag, "s", viper.GetInt(logMaxSizeKey),
|
||||
`Maximum size in megabytes of the log file
|
||||
before it gets rotated. This flag can be set
|
||||
using LDAPAUTH_LOG_MAX_SIZE env var too. It
|
||||
is unused if log-file-path is empty.`)
|
||||
viper.BindPFlag(logMaxSizeKey, cmd.Flags().Lookup(logMaxSizeFlag))
|
||||
|
||||
viper.SetDefault(logMaxBackupKey, defaultLogMaxBackup)
|
||||
viper.BindEnv(logMaxBackupKey, "LDAPAUTH_LOG_MAX_BACKUPS")
|
||||
cmd.Flags().IntVarP(&logMaxBackups, "log-max-backups", "b", viper.GetInt(logMaxBackupKey),
|
||||
`Maximum number of old log files to retain.
|
||||
This flag can be set using LDAPAUTH_LOG_MAX_BACKUPS
|
||||
env var too. It is unused if log-file-path is
|
||||
empty.`)
|
||||
viper.BindPFlag(logMaxBackupKey, cmd.Flags().Lookup(logMaxBackupFlag))
|
||||
|
||||
viper.SetDefault(logMaxAgeKey, defaultLogMaxAge)
|
||||
viper.BindEnv(logMaxAgeKey, "LDAPAUTH_LOG_MAX_AGE")
|
||||
cmd.Flags().IntVarP(&logMaxAge, "log-max-age", "a", viper.GetInt(logMaxAgeKey),
|
||||
`Maximum number of days to retain old log files.
|
||||
This flag can be set using LDAPAUTH_LOG_MAX_AGE
|
||||
env var too. It is unused if log-file-path is
|
||||
empty.`)
|
||||
viper.BindPFlag(logMaxAgeKey, cmd.Flags().Lookup(logMaxAgeFlag))
|
||||
|
||||
viper.SetDefault(logCompressKey, defaultLogCompress)
|
||||
viper.BindEnv(logCompressKey, "LDAPAUTH_LOG_COMPRESS")
|
||||
cmd.Flags().BoolVarP(&logCompress, logCompressFlag, "z", viper.GetBool(logCompressKey),
|
||||
`Determine if the rotated log files
|
||||
should be compressed using gzip. This flag can
|
||||
be set using LDAPAUTH_LOG_COMPRESS env var too.
|
||||
It is unused if log-file-path is empty.`)
|
||||
viper.BindPFlag(logCompressKey, cmd.Flags().Lookup(logCompressFlag))
|
||||
|
||||
viper.SetDefault(logVerboseKey, defaultLogVerbose)
|
||||
viper.BindEnv(logVerboseKey, "LDAPAUTH_LOG_VERBOSE")
|
||||
cmd.Flags().BoolVarP(&logVerbose, logVerboseFlag, "v", viper.GetBool(logVerboseKey),
|
||||
`Enable verbose logs. This flag can be set
|
||||
using LDAPAUTH_LOG_VERBOSE env var too.
|
||||
`)
|
||||
viper.BindPFlag(logVerboseKey, cmd.Flags().Lookup(logVerboseFlag))
|
||||
}
|
||||
49
examples/ldapauthserver/cmd/serve.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/drakkan/sftpgo/ldapauthserver/config"
|
||||
"github.com/drakkan/sftpgo/ldapauthserver/httpd"
|
||||
"github.com/drakkan/sftpgo/ldapauthserver/logger"
|
||||
"github.com/drakkan/sftpgo/ldapauthserver/utils"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Start the LDAP Authentication Server",
|
||||
Long: `To start the server with the default values for the command line flags simply use:
|
||||
|
||||
ldapauthserver serve
|
||||
|
||||
Please take a look at the usage below to customize the startup options`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
startServer()
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(serveCmd)
|
||||
addServeFlags(serveCmd)
|
||||
}
|
||||
|
||||
func startServer() error {
|
||||
logLevel := zerolog.DebugLevel
|
||||
if !logVerbose {
|
||||
logLevel = zerolog.InfoLevel
|
||||
}
|
||||
if !filepath.IsAbs(logFilePath) && utils.IsFileInputValid(logFilePath) {
|
||||
logFilePath = filepath.Join(configDir, logFilePath)
|
||||
}
|
||||
logger.InitLogger(logFilePath, logMaxSize, logMaxBackups, logMaxAge, logCompress, logLevel)
|
||||
version := utils.GetAppVersion()
|
||||
logger.Info(logSender, "", "starting LDAP Auth Server %v, config dir: %v, config file: %v, log max size: %v log max backups: %v "+
|
||||
"log max age: %v log verbose: %v, log compress: %v", version.GetVersionAsString(), configDir, configFile, logMaxSize,
|
||||
logMaxBackups, logMaxAge, logVerbose, logCompress)
|
||||
config.LoadConfig(configDir, configFile)
|
||||
return httpd.StartHTTPServer(configDir, config.GetHTTPDConfig())
|
||||
}
|
||||
158
examples/ldapauthserver/config/config.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/drakkan/sftpgo/ldapauthserver/logger"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
logSender = "config"
|
||||
// DefaultConfigName defines the name for the default config file.
|
||||
// This is the file name without extension, we use viper and so we
|
||||
// support all the config files format supported by viper
|
||||
DefaultConfigName = "ldapauth"
|
||||
// ConfigEnvPrefix defines a prefix that ENVIRONMENT variables will use
|
||||
configEnvPrefix = "ldapauth"
|
||||
)
|
||||
|
||||
// HTTPDConfig defines configuration for the HTTPD server
|
||||
type HTTPDConfig struct {
|
||||
BindAddress string `mapstructure:"bind_address"`
|
||||
BindPort int `mapstructure:"bind_port"`
|
||||
AuthUserFile string `mapstructure:"auth_user_file"`
|
||||
CertificateFile string `mapstructure:"certificate_file"`
|
||||
CertificateKeyFile string `mapstructure:"certificate_key_file"`
|
||||
}
|
||||
|
||||
// LDAPConfig defines the configuration parameters for LDAP connections and searches
|
||||
type LDAPConfig struct {
|
||||
BaseDN string `mapstructure:"basedn"`
|
||||
BindURL string `mapstructure:"bind_url"`
|
||||
BindUsername string `mapstructure:"bind_username"`
|
||||
BindPassword string `mapstructure:"bind_password"`
|
||||
SearchFilter string `mapstructure:"search_filter"`
|
||||
SearchBaseAttrs []string `mapstructure:"search_base_attrs"`
|
||||
DefaultUID int `mapstructure:"default_uid"`
|
||||
DefaultGID int `mapstructure:"default_gid"`
|
||||
ForceDefaultUID bool `mapstructure:"force_default_uid"`
|
||||
ForceDefaultGID bool `mapstructure:"force_default_gid"`
|
||||
InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"`
|
||||
CACertificates []string `mapstructure:"ca_certificates"`
|
||||
}
|
||||
|
||||
type appConfig struct {
|
||||
HTTPD HTTPDConfig `mapstructure:"httpd"`
|
||||
LDAP LDAPConfig `mapstructure:"ldap"`
|
||||
}
|
||||
|
||||
var conf appConfig
|
||||
|
||||
func init() {
|
||||
conf = appConfig{
|
||||
HTTPD: HTTPDConfig{
|
||||
BindAddress: "",
|
||||
BindPort: 9000,
|
||||
AuthUserFile: "",
|
||||
CertificateFile: "",
|
||||
CertificateKeyFile: "",
|
||||
},
|
||||
LDAP: LDAPConfig{
|
||||
BaseDN: "dc=example,dc=com",
|
||||
BindURL: "ldap://192.168.1.103:389",
|
||||
BindUsername: "cn=Directory Manager",
|
||||
BindPassword: "YOUR_ADMIN_PASSWORD_HERE",
|
||||
SearchFilter: "(&(objectClass=nsPerson)(uid=%s))",
|
||||
SearchBaseAttrs: []string{
|
||||
"dn",
|
||||
"homeDirectory",
|
||||
"uidNumber",
|
||||
"gidNumber",
|
||||
"nsSshPublicKey",
|
||||
},
|
||||
DefaultUID: 0,
|
||||
DefaultGID: 0,
|
||||
ForceDefaultUID: true,
|
||||
ForceDefaultGID: true,
|
||||
InsecureSkipVerify: false,
|
||||
CACertificates: nil,
|
||||
},
|
||||
}
|
||||
viper.SetEnvPrefix(configEnvPrefix)
|
||||
replacer := strings.NewReplacer(".", "__")
|
||||
viper.SetEnvKeyReplacer(replacer)
|
||||
viper.SetConfigName(DefaultConfigName)
|
||||
viper.AutomaticEnv()
|
||||
viper.AllowEmptyEnv(true)
|
||||
}
|
||||
|
||||
// GetHomeDirectory returns the configured name for the LDAP field to use as home directory
|
||||
func (l *LDAPConfig) GetHomeDirectory() string {
|
||||
if len(l.SearchBaseAttrs) > 1 {
|
||||
return l.SearchBaseAttrs[1]
|
||||
}
|
||||
return "homeDirectory"
|
||||
}
|
||||
|
||||
// GetUIDNumber returns the configured name for the LDAP field to use as UID
|
||||
func (l *LDAPConfig) GetUIDNumber() string {
|
||||
if len(l.SearchBaseAttrs) > 2 {
|
||||
return l.SearchBaseAttrs[2]
|
||||
}
|
||||
return "uidNumber"
|
||||
}
|
||||
|
||||
// GetGIDNumber returns the configured name for the LDAP field to use as GID
|
||||
func (l *LDAPConfig) GetGIDNumber() string {
|
||||
if len(l.SearchBaseAttrs) > 3 {
|
||||
return l.SearchBaseAttrs[3]
|
||||
}
|
||||
return "gidNumber"
|
||||
}
|
||||
|
||||
// GetPublicKey returns the configured name for the LDAP field to use as public keys
|
||||
func (l *LDAPConfig) GetPublicKey() string {
|
||||
if len(l.SearchBaseAttrs) > 4 {
|
||||
return l.SearchBaseAttrs[4]
|
||||
}
|
||||
return "nsSshPublicKey"
|
||||
}
|
||||
|
||||
// GetHTTPDConfig returns the configuration for the HTTP server
|
||||
func GetHTTPDConfig() HTTPDConfig {
|
||||
return conf.HTTPD
|
||||
}
|
||||
|
||||
// GetLDAPConfig returns LDAP related settings
|
||||
func GetLDAPConfig() LDAPConfig {
|
||||
return conf.LDAP
|
||||
}
|
||||
|
||||
func getRedactedConf() appConfig {
|
||||
c := conf
|
||||
return c
|
||||
}
|
||||
|
||||
// LoadConfig loads the configuration
|
||||
func LoadConfig(configDir, configName string) error {
|
||||
var err error
|
||||
viper.AddConfigPath(configDir)
|
||||
viper.AddConfigPath(".")
|
||||
viper.SetConfigName(configName)
|
||||
if err = viper.ReadInConfig(); err != nil {
|
||||
logger.Warn(logSender, "", "error loading configuration file: %v. Default configuration will be used: %+v",
|
||||
err, getRedactedConf())
|
||||
logger.WarnToConsole("error loading configuration file: %v. Default configuration will be used.", err)
|
||||
return err
|
||||
}
|
||||
err = viper.Unmarshal(&conf)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "", "error parsing configuration file: %v. Default configuration will be used: %+v",
|
||||
err, getRedactedConf())
|
||||
logger.WarnToConsole("error parsing configuration file: %v. Default configuration will be used.", err)
|
||||
return err
|
||||
}
|
||||
logger.Debug(logSender, "", "config file used: '%q', config loaded: %+v", viper.ConfigFileUsed(), getRedactedConf())
|
||||
return err
|
||||
}
|
||||
43
examples/ldapauthserver/go.mod
Normal file
@@ -0,0 +1,43 @@
|
||||
module github.com/drakkan/sftpgo/ldapauthserver
|
||||
|
||||
go 1.22.2
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.0.12
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/go-ldap/ldap/v3 v3.4.8
|
||||
github.com/nathanaelle/password/v2 v2.0.1
|
||||
github.com/rs/zerolog v1.32.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/viper v1.18.2
|
||||
golang.org/x/crypto v0.23.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
184
examples/ldapauthserver/go.sum
Normal file
@@ -0,0 +1,184 @@
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||
github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ=
|
||||
github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
||||
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/nathanaelle/password/v2 v2.0.1 h1:ItoCTdsuIWzilYmllQPa3DR3YoCXcpfxScWLqr8Ii2s=
|
||||
github.com/nathanaelle/password/v2 v2.0.1/go.mod h1:eaoT+ICQEPNtikBRIAatN8ThWwMhVG+r1jTw60BvPJk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
||||
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
145
examples/ldapauthserver/httpd/auth.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package httpd
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
unixcrypt "github.com/nathanaelle/password/v2"
|
||||
|
||||
"github.com/drakkan/sftpgo/ldapauthserver/logger"
|
||||
"github.com/drakkan/sftpgo/ldapauthserver/utils"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
authenticationHeader = "WWW-Authenticate"
|
||||
authenticationRealm = "LDAP Auth Server"
|
||||
unauthResponse = "Unauthorized"
|
||||
)
|
||||
|
||||
var (
|
||||
md5CryptPwdPrefixes = []string{"$1$", "$apr1$"}
|
||||
bcryptPwdPrefixes = []string{"$2a$", "$2$", "$2x$", "$2y$", "$2b$"}
|
||||
)
|
||||
|
||||
type httpAuthProvider interface {
|
||||
getHashedPassword(username string) (string, bool)
|
||||
isEnabled() bool
|
||||
}
|
||||
|
||||
type basicAuthProvider struct {
|
||||
Path string
|
||||
sync.RWMutex
|
||||
Info os.FileInfo
|
||||
Users map[string]string
|
||||
}
|
||||
|
||||
func newBasicAuthProvider(authUserFile string) (httpAuthProvider, error) {
|
||||
basicAuthProvider := basicAuthProvider{
|
||||
Path: authUserFile,
|
||||
Info: nil,
|
||||
Users: make(map[string]string),
|
||||
}
|
||||
return &basicAuthProvider, basicAuthProvider.loadUsers()
|
||||
}
|
||||
|
||||
func (p *basicAuthProvider) isEnabled() bool {
|
||||
return len(p.Path) > 0
|
||||
}
|
||||
|
||||
func (p *basicAuthProvider) isReloadNeeded(info os.FileInfo) bool {
|
||||
p.RLock()
|
||||
defer p.RUnlock()
|
||||
return p.Info == nil || p.Info.ModTime() != info.ModTime() || p.Info.Size() != info.Size()
|
||||
}
|
||||
|
||||
func (p *basicAuthProvider) loadUsers() error {
|
||||
if !p.isEnabled() {
|
||||
return nil
|
||||
}
|
||||
info, err := os.Stat(p.Path)
|
||||
if err != nil {
|
||||
logger.Debug(logSender, "", "unable to stat basic auth users file: %v", err)
|
||||
return err
|
||||
}
|
||||
if p.isReloadNeeded(info) {
|
||||
r, err := os.Open(p.Path)
|
||||
if err != nil {
|
||||
logger.Debug(logSender, "", "unable to open basic auth users file: %v", err)
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
reader := csv.NewReader(r)
|
||||
reader.Comma = ':'
|
||||
reader.Comment = '#'
|
||||
reader.TrimLeadingSpace = true
|
||||
records, err := reader.ReadAll()
|
||||
if err != nil {
|
||||
logger.Debug(logSender, "", "unable to parse basic auth users file: %v", err)
|
||||
return err
|
||||
}
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
p.Users = make(map[string]string)
|
||||
for _, record := range records {
|
||||
if len(record) == 2 {
|
||||
p.Users[record[0]] = record[1]
|
||||
}
|
||||
}
|
||||
logger.Debug(logSender, "", "number of users loaded for httpd basic auth: %v", len(p.Users))
|
||||
p.Info = info
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *basicAuthProvider) getHashedPassword(username string) (string, bool) {
|
||||
err := p.loadUsers()
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
p.RLock()
|
||||
defer p.RUnlock()
|
||||
pwd, ok := p.Users[username]
|
||||
return pwd, ok
|
||||
}
|
||||
|
||||
func checkAuth(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !validateCredentials(r) {
|
||||
w.Header().Set(authenticationHeader, fmt.Sprintf("Basic realm=\"%v\"", authenticationRealm))
|
||||
sendAPIResponse(w, r, errors.New(unauthResponse), "", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func validateCredentials(r *http.Request) bool {
|
||||
if !httpAuth.isEnabled() {
|
||||
return true
|
||||
}
|
||||
username, password, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if hashedPwd, ok := httpAuth.getHashedPassword(username); ok {
|
||||
if utils.IsStringPrefixInSlice(hashedPwd, bcryptPwdPrefixes) {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPwd), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
if utils.IsStringPrefixInSlice(hashedPwd, md5CryptPwdPrefixes) {
|
||||
crypter, ok := unixcrypt.MD5.CrypterFound(hashedPwd)
|
||||
if !ok {
|
||||
err := errors.New("cannot found matching MD5 crypter")
|
||||
logger.Debug(logSender, "", "error comparing password with MD5 crypt hash: %v", err)
|
||||
return false
|
||||
}
|
||||
return crypter.Verify([]byte(password))
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
149
examples/ldapauthserver/httpd/httpd.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package httpd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/drakkan/sftpgo/ldapauthserver/config"
|
||||
"github.com/drakkan/sftpgo/ldapauthserver/logger"
|
||||
"github.com/drakkan/sftpgo/ldapauthserver/utils"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
const (
|
||||
logSender = "httpd"
|
||||
versionPath = "/api/v1/version"
|
||||
checkAuthPath = "/api/v1/check_auth"
|
||||
maxRequestSize = 1 << 18 // 256KB
|
||||
)
|
||||
|
||||
var (
|
||||
ldapConfig config.LDAPConfig
|
||||
httpAuth httpAuthProvider
|
||||
certMgr *certManager
|
||||
rootCAs *x509.CertPool
|
||||
)
|
||||
|
||||
// StartHTTPServer initializes and starts the HTTP Server
|
||||
func StartHTTPServer(configDir string, httpConfig config.HTTPDConfig) error {
|
||||
var err error
|
||||
authUserFile := getConfigPath(httpConfig.AuthUserFile, configDir)
|
||||
httpAuth, err = newBasicAuthProvider(authUserFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
router := chi.NewRouter()
|
||||
router.Use(middleware.RequestID)
|
||||
router.Use(middleware.RealIP)
|
||||
router.Use(logger.NewStructuredLogger(logger.GetLogger()))
|
||||
router.Use(middleware.Recoverer)
|
||||
|
||||
router.NotFound(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
sendAPIResponse(w, r, nil, "Not Found", http.StatusNotFound)
|
||||
}))
|
||||
|
||||
router.MethodNotAllowed(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
sendAPIResponse(w, r, nil, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}))
|
||||
|
||||
router.Get(versionPath, func(w http.ResponseWriter, r *http.Request) {
|
||||
render.JSON(w, r, utils.GetAppVersion())
|
||||
})
|
||||
|
||||
router.Group(func(router chi.Router) {
|
||||
router.Use(checkAuth)
|
||||
|
||||
router.Post(checkAuthPath, checkSFTPGoUserAuth)
|
||||
})
|
||||
|
||||
ldapConfig = config.GetLDAPConfig()
|
||||
loadCACerts(configDir)
|
||||
|
||||
certificateFile := getConfigPath(httpConfig.CertificateFile, configDir)
|
||||
certificateKeyFile := getConfigPath(httpConfig.CertificateKeyFile, configDir)
|
||||
|
||||
httpServer := &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", httpConfig.BindAddress, httpConfig.BindPort),
|
||||
Handler: router,
|
||||
ReadTimeout: 70 * time.Second,
|
||||
WriteTimeout: 70 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
MaxHeaderBytes: 1 << 16, // 64KB
|
||||
}
|
||||
if len(certificateFile) > 0 && len(certificateKeyFile) > 0 {
|
||||
certMgr, err = newCertManager(certificateFile, certificateKeyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := &tls.Config{
|
||||
GetCertificate: certMgr.GetCertificateFunc(),
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
httpServer.TLSConfig = config
|
||||
return httpServer.ListenAndServeTLS("", "")
|
||||
}
|
||||
return httpServer.ListenAndServe()
|
||||
}
|
||||
|
||||
func sendAPIResponse(w http.ResponseWriter, r *http.Request, err error, message string, code int) {
|
||||
var errorString string
|
||||
if err != nil {
|
||||
errorString = err.Error()
|
||||
}
|
||||
resp := apiResponse{
|
||||
Error: errorString,
|
||||
Message: message,
|
||||
HTTPStatus: code,
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), render.StatusCtxKey, code)
|
||||
render.JSON(w, r.WithContext(ctx), resp)
|
||||
}
|
||||
|
||||
func loadCACerts(configDir string) error {
|
||||
var err error
|
||||
rootCAs, err = x509.SystemCertPool()
|
||||
if err != nil {
|
||||
rootCAs = x509.NewCertPool()
|
||||
}
|
||||
for _, ca := range ldapConfig.CACertificates {
|
||||
caPath := getConfigPath(ca, configDir)
|
||||
certs, err := os.ReadFile(caPath)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "", "error loading ca cert %q: %v", caPath, err)
|
||||
return err
|
||||
}
|
||||
if !rootCAs.AppendCertsFromPEM(certs) {
|
||||
logger.Warn(logSender, "", "unable to add ca cert %q", caPath)
|
||||
} else {
|
||||
logger.Debug(logSender, "", "ca cert %q added to the trusted certificates", caPath)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReloadTLSCertificate reloads the TLS certificate and key from the configured paths
|
||||
func ReloadTLSCertificate() {
|
||||
if certMgr != nil {
|
||||
certMgr.loadCertificate()
|
||||
}
|
||||
}
|
||||
|
||||
func getConfigPath(name, configDir string) string {
|
||||
if !utils.IsFileInputValid(name) {
|
||||
return ""
|
||||
}
|
||||
if len(name) > 0 && !filepath.IsAbs(name) {
|
||||
return filepath.Join(configDir, name)
|
||||
}
|
||||
return name
|
||||
}
|
||||
143
examples/ldapauthserver/httpd/ldapauth.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package httpd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/drakkan/sftpgo/ldapauthserver/logger"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func getSFTPGoUser(entry *ldap.Entry, username string) (SFTPGoUser, error) {
|
||||
var err error
|
||||
var user SFTPGoUser
|
||||
uid := ldapConfig.DefaultUID
|
||||
gid := ldapConfig.DefaultGID
|
||||
status := 1
|
||||
|
||||
if !ldapConfig.ForceDefaultUID {
|
||||
uid, err = strconv.Atoi(entry.GetAttributeValue(ldapConfig.GetUIDNumber()))
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
}
|
||||
|
||||
if !ldapConfig.ForceDefaultGID {
|
||||
uid, err = strconv.Atoi(entry.GetAttributeValue(ldapConfig.GetGIDNumber()))
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
}
|
||||
|
||||
sftpgoUser := SFTPGoUser{
|
||||
Username: username,
|
||||
HomeDir: entry.GetAttributeValue(ldapConfig.GetHomeDirectory()),
|
||||
UID: uid,
|
||||
GID: gid,
|
||||
Status: status,
|
||||
}
|
||||
sftpgoUser.Permissions = make(map[string][]string)
|
||||
sftpgoUser.Permissions["/"] = []string{"*"}
|
||||
return sftpgoUser, nil
|
||||
}
|
||||
|
||||
func checkSFTPGoUserAuth(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
var authReq externalAuthRequest
|
||||
err := render.DecodeJSON(r.Body, &authReq)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, middleware.GetReqID(r.Context()), "error decoding auth request: %v", err)
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
l, err := ldap.DialURL(ldapConfig.BindURL, ldap.DialWithTLSConfig(&tls.Config{
|
||||
InsecureSkipVerify: ldapConfig.InsecureSkipVerify,
|
||||
RootCAs: rootCAs,
|
||||
}))
|
||||
if err != nil {
|
||||
logger.Warn(logSender, middleware.GetReqID(r.Context()), "error connecting to the LDAP server: %v", err)
|
||||
sendAPIResponse(w, r, err, "Error connecting to the LDAP server", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
err = l.Bind(ldapConfig.BindUsername, ldapConfig.BindPassword)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, middleware.GetReqID(r.Context()), "error binding to the LDAP server: %v", err)
|
||||
sendAPIResponse(w, r, err, "Error binding to the LDAP server", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
ldapConfig.BaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
strings.Replace(ldapConfig.SearchFilter, "%s", ldap.EscapeFilter(authReq.Username), 1),
|
||||
ldapConfig.SearchBaseAttrs,
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := l.Search(searchRequest)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, middleware.GetReqID(r.Context()), "error searching LDAP user %q: %v", authReq.Username, err)
|
||||
sendAPIResponse(w, r, err, "Error searching LDAP user", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if len(sr.Entries) != 1 {
|
||||
logger.Warn(logSender, middleware.GetReqID(r.Context()), "expected one user, found: %v", len(sr.Entries))
|
||||
sendAPIResponse(w, r, nil, fmt.Sprintf("Expected one user, found: %v", len(sr.Entries)), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if len(authReq.PublicKey) > 0 {
|
||||
userKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(authReq.PublicKey))
|
||||
if err != nil {
|
||||
logger.Warn(logSender, middleware.GetReqID(r.Context()), "invalid public key for user %q: %v", authReq.Username, err)
|
||||
sendAPIResponse(w, r, err, "Invalid public key", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
authOk := false
|
||||
for _, k := range sr.Entries[0].GetAttributeValues(ldapConfig.GetPublicKey()) {
|
||||
key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k))
|
||||
// we skip an invalid public key stored inside the LDAP server
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if bytes.Equal(key.Marshal(), userKey.Marshal()) {
|
||||
authOk = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !authOk {
|
||||
logger.Warn(logSender, middleware.GetReqID(r.Context()), "public key authentication failed for user: %q", authReq.Username)
|
||||
sendAPIResponse(w, r, nil, "public key authentication failed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// bind to the LDAP server with the user dn and the given password to check the password
|
||||
userdn := sr.Entries[0].DN
|
||||
err = l.Bind(userdn, authReq.Password)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, middleware.GetReqID(r.Context()), "password authentication failed for user: %q", authReq.Username)
|
||||
sendAPIResponse(w, r, nil, "password authentication failed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
user, err := getSFTPGoUser(sr.Entries[0], authReq.Username)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, middleware.GetReqID(r.Context()), "get user from LDAP entry failed for username %q: %v",
|
||||
authReq.Username, err)
|
||||
sendAPIResponse(w, r, err, "mapping LDAP user failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
render.JSON(w, r, user)
|
||||
}
|
||||
109
examples/ldapauthserver/httpd/models.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package httpd
|
||||
|
||||
type apiResponse struct {
|
||||
Error string `json:"error"`
|
||||
Message string `json:"message"`
|
||||
HTTPStatus int `json:"status"`
|
||||
}
|
||||
|
||||
type externalAuthRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
PublicKey string `json:"public_key"`
|
||||
}
|
||||
|
||||
// SFTPGoExtensionsFilter defines filters based on file extensions
|
||||
type SFTPGoExtensionsFilter struct {
|
||||
Path string `json:"path"`
|
||||
AllowedExtensions []string `json:"allowed_extensions,omitempty"`
|
||||
DeniedExtensions []string `json:"denied_extensions,omitempty"`
|
||||
}
|
||||
|
||||
// SFTPGoUserFilters defines additional restrictions for an SFTPGo user
|
||||
type SFTPGoUserFilters struct {
|
||||
AllowedIP []string `json:"allowed_ip,omitempty"`
|
||||
DeniedIP []string `json:"denied_ip,omitempty"`
|
||||
DeniedLoginMethods []string `json:"denied_login_methods,omitempty"`
|
||||
FileExtensions []SFTPGoExtensionsFilter `json:"file_extensions,omitempty"`
|
||||
}
|
||||
|
||||
// S3FsConfig defines the configuration for S3 based filesystem
|
||||
type S3FsConfig struct {
|
||||
Bucket string `json:"bucket,omitempty"`
|
||||
KeyPrefix string `json:"key_prefix,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
AccessKey string `json:"access_key,omitempty"`
|
||||
AccessSecret string `json:"access_secret,omitempty"`
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
StorageClass string `json:"storage_class,omitempty"`
|
||||
UploadPartSize int64 `json:"upload_part_size,omitempty"`
|
||||
UploadConcurrency int `json:"upload_concurrency,omitempty"`
|
||||
}
|
||||
|
||||
// GCSFsConfig defines the configuration for Google Cloud Storage based filesystem
|
||||
type GCSFsConfig struct {
|
||||
Bucket string `json:"bucket,omitempty"`
|
||||
KeyPrefix string `json:"key_prefix,omitempty"`
|
||||
Credentials string `json:"credentials,omitempty"`
|
||||
AutomaticCredentials int `json:"automatic_credentials,omitempty"`
|
||||
StorageClass string `json:"storage_class,omitempty"`
|
||||
}
|
||||
|
||||
// SFTPGoFilesystem defines cloud storage filesystem details
|
||||
type SFTPGoFilesystem struct {
|
||||
// 0 local filesystem, 1 AWS S3 compatible, 2 Google Cloud Storage
|
||||
Provider int `json:"provider"`
|
||||
S3Config S3FsConfig `json:"s3config,omitempty"`
|
||||
GCSConfig GCSFsConfig `json:"gcsconfig,omitempty"`
|
||||
}
|
||||
|
||||
type virtualFolder struct {
|
||||
VirtualPath string `json:"virtual_path"`
|
||||
MappedPath string `json:"mapped_path"`
|
||||
}
|
||||
|
||||
// SFTPGoUser defines an SFTPGo user
|
||||
type SFTPGoUser struct {
|
||||
// Database unique identifier
|
||||
ID int64 `json:"id"`
|
||||
// 1 enabled, 0 disabled (login is not allowed)
|
||||
Status int `json:"status"`
|
||||
// Username
|
||||
Username string `json:"username"`
|
||||
// Account expiration date as unix timestamp in milliseconds. An expired account cannot login.
|
||||
// 0 means no expiration
|
||||
ExpirationDate int64 `json:"expiration_date"`
|
||||
Password string `json:"password,omitempty"`
|
||||
PublicKeys []string `json:"public_keys,omitempty"`
|
||||
HomeDir string `json:"home_dir"`
|
||||
// Mapping between virtual paths and filesystem paths outside the home directory. Supported for local filesystem only
|
||||
VirtualFolders []virtualFolder `json:"virtual_folders,omitempty"`
|
||||
// If sftpgo runs as root system user then the created files and directories will be assigned to this system UID
|
||||
UID int `json:"uid"`
|
||||
// If sftpgo runs as root system user then the created files and directories will be assigned to this system GID
|
||||
GID int `json:"gid"`
|
||||
// Maximum concurrent sessions. 0 means unlimited
|
||||
MaxSessions int `json:"max_sessions"`
|
||||
// Maximum size allowed as bytes. 0 means unlimited
|
||||
QuotaSize int64 `json:"quota_size"`
|
||||
// Maximum number of files allowed. 0 means unlimited
|
||||
QuotaFiles int `json:"quota_files"`
|
||||
// List of the granted permissions
|
||||
Permissions map[string][]string `json:"permissions"`
|
||||
// Used quota as bytes
|
||||
UsedQuotaSize int64 `json:"used_quota_size"`
|
||||
// Used quota as number of files
|
||||
UsedQuotaFiles int `json:"used_quota_files"`
|
||||
// Last quota update as unix timestamp in milliseconds
|
||||
LastQuotaUpdate int64 `json:"last_quota_update"`
|
||||
// Maximum upload bandwidth as KB/s, 0 means unlimited
|
||||
UploadBandwidth int64 `json:"upload_bandwidth"`
|
||||
// Maximum download bandwidth as KB/s, 0 means unlimited
|
||||
DownloadBandwidth int64 `json:"download_bandwidth"`
|
||||
// Last login as unix timestamp in milliseconds
|
||||
LastLogin int64 `json:"last_login"`
|
||||
// Additional restrictions
|
||||
Filters SFTPGoUserFilters `json:"filters"`
|
||||
// Filesystem configuration details
|
||||
FsConfig SFTPGoFilesystem `json:"filesystem"`
|
||||
}
|
||||
49
examples/ldapauthserver/httpd/tlsutils.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package httpd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"sync"
|
||||
|
||||
"github.com/drakkan/sftpgo/ldapauthserver/logger"
|
||||
)
|
||||
|
||||
type certManager struct {
|
||||
certPath string
|
||||
keyPath string
|
||||
sync.RWMutex
|
||||
cert *tls.Certificate
|
||||
}
|
||||
|
||||
func (m *certManager) loadCertificate() error {
|
||||
newCert, err := tls.LoadX509KeyPair(m.certPath, m.keyPath)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "", "unable to load https certificate: %v", err)
|
||||
return err
|
||||
}
|
||||
logger.Debug(logSender, "", "https certificate successfully loaded")
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
m.cert = &newCert
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *certManager) GetCertificateFunc() func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
return func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
return m.cert, nil
|
||||
}
|
||||
}
|
||||
|
||||
func newCertManager(certificateFile, certificateKeyFile string) (*certManager, error) {
|
||||
manager := &certManager{
|
||||
cert: nil,
|
||||
certPath: certificateFile,
|
||||
keyPath: certificateKeyFile,
|
||||
}
|
||||
err := manager.loadCertificate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return manager, nil
|
||||
}
|
||||
33
examples/ldapauthserver/ldapauth.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
[httpd]
|
||||
bind_address = ""
|
||||
bind_port = 9000
|
||||
# Path to a file used to store usernames and passwords for basic authentication. It can be generated using the Apache htpasswd tool
|
||||
auth_user_file = ""
|
||||
# If both the certificate and the private key are provided, the server will expect HTTPS connections
|
||||
certificate_file = ""
|
||||
certificate_key_file = ""
|
||||
|
||||
[ldap]
|
||||
basedn = "dc=example,dc=com"
|
||||
bind_url = "ldap://127.0.0.1:389"
|
||||
bind_username = "cn=Directory Manager"
|
||||
bind_password = "YOUR_ADMIN_PASSWORD_HERE"
|
||||
search_filter = "(&(objectClass=nsPerson)(uid=%s))"
|
||||
# you can change the name of the search base attributes to adapt them to your schema but the order must remain the same
|
||||
search_base_attrs = [
|
||||
"dn",
|
||||
"homeDirectory",
|
||||
"uidNumber",
|
||||
"gidNumber",
|
||||
"nsSshPublicKey"
|
||||
]
|
||||
default_uid = 0
|
||||
default_gid = 0
|
||||
force_default_uid = true
|
||||
force_default_gid = true
|
||||
# if true, ldaps accepts any certificate presented by the LDAP server and any host name in that certificate.
|
||||
# This should be used only for testing
|
||||
insecure_skip_verify = false
|
||||
# list of root CA to use for ldaps connections
|
||||
# If you use a self signed certificate is better to add the root CA to this list than set insecure_skip_verify to true
|
||||
ca_certificates = []
|
||||
126
examples/ldapauthserver/logger/logger.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
dateFormat = "2006-01-02T15:04:05.000" // YYYY-MM-DDTHH:MM:SS.ZZZ
|
||||
)
|
||||
|
||||
var (
|
||||
logger zerolog.Logger
|
||||
consoleLogger zerolog.Logger
|
||||
)
|
||||
|
||||
// GetLogger get the configured logger instance
|
||||
func GetLogger() *zerolog.Logger {
|
||||
return &logger
|
||||
}
|
||||
|
||||
// InitLogger initialize loggers
|
||||
func InitLogger(logFilePath string, logMaxSize, logMaxBackups, logMaxAge int, logCompress bool, level zerolog.Level) {
|
||||
zerolog.TimeFieldFormat = dateFormat
|
||||
if isLogFilePathValid(logFilePath) {
|
||||
logger = zerolog.New(&lumberjack.Logger{
|
||||
Filename: logFilePath,
|
||||
MaxSize: logMaxSize,
|
||||
MaxBackups: logMaxBackups,
|
||||
MaxAge: logMaxAge,
|
||||
Compress: logCompress,
|
||||
})
|
||||
EnableConsoleLogger(level)
|
||||
} else {
|
||||
logger = zerolog.New(&logSyncWrapper{
|
||||
output: os.Stdout,
|
||||
})
|
||||
consoleLogger = zerolog.Nop()
|
||||
}
|
||||
logger.Level(level)
|
||||
}
|
||||
|
||||
// DisableLogger disable the main logger.
|
||||
// ConsoleLogger will not be affected
|
||||
func DisableLogger() {
|
||||
logger = zerolog.Nop()
|
||||
}
|
||||
|
||||
// EnableConsoleLogger enables the console logger
|
||||
func EnableConsoleLogger(level zerolog.Level) {
|
||||
consoleOutput := zerolog.ConsoleWriter{
|
||||
Out: os.Stdout,
|
||||
TimeFormat: dateFormat,
|
||||
NoColor: runtime.GOOS == "windows",
|
||||
}
|
||||
consoleLogger = zerolog.New(consoleOutput).With().Timestamp().Logger().Level(level)
|
||||
}
|
||||
|
||||
// Debug logs at debug level for the specified sender
|
||||
func Debug(prefix, requestID string, format string, v ...interface{}) {
|
||||
logger.Debug().
|
||||
Timestamp().
|
||||
Str("sender", prefix).
|
||||
Str("request_id", requestID).
|
||||
Msg(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
// Info logs at info level for the specified sender
|
||||
func Info(prefix, requestID string, format string, v ...interface{}) {
|
||||
logger.Info().
|
||||
Timestamp().
|
||||
Str("sender", prefix).
|
||||
Str("request_id", requestID).
|
||||
Msg(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
// Warn logs at warn level for the specified sender
|
||||
func Warn(prefix, requestID string, format string, v ...interface{}) {
|
||||
logger.Warn().
|
||||
Timestamp().
|
||||
Str("sender", prefix).
|
||||
Str("request_id", requestID).
|
||||
Msg(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
// Error logs at error level for the specified sender
|
||||
func Error(prefix, requestID string, format string, v ...interface{}) {
|
||||
logger.Error().
|
||||
Timestamp().
|
||||
Str("sender", prefix).
|
||||
Str("request_id", requestID).
|
||||
Msg(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
// DebugToConsole logs at debug level to stdout
|
||||
func DebugToConsole(format string, v ...interface{}) {
|
||||
consoleLogger.Debug().Msg(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
// InfoToConsole logs at info level to stdout
|
||||
func InfoToConsole(format string, v ...interface{}) {
|
||||
consoleLogger.Info().Msg(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
// WarnToConsole logs at info level to stdout
|
||||
func WarnToConsole(format string, v ...interface{}) {
|
||||
consoleLogger.Warn().Msg(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
// ErrorToConsole logs at error level to stdout
|
||||
func ErrorToConsole(format string, v ...interface{}) {
|
||||
consoleLogger.Error().Msg(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
func isLogFilePathValid(logFilePath string) bool {
|
||||
cleanInput := filepath.Clean(logFilePath)
|
||||
if cleanInput == "." || cleanInput == ".." {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -15,12 +15,9 @@ type StructuredLogger struct {
|
||||
Logger *zerolog.Logger
|
||||
}
|
||||
|
||||
// StructuredLoggerEntry defines a log entry.
|
||||
// It implements chi.middleware.LogEntry interface
|
||||
// StructuredLoggerEntry ...
|
||||
type StructuredLoggerEntry struct {
|
||||
// The zerolog logger
|
||||
Logger *zerolog.Logger
|
||||
// fields to write in the log
|
||||
fields map[string]interface{}
|
||||
}
|
||||
|
||||
@@ -53,18 +50,24 @@ func (l *StructuredLogger) NewLogEntry(r *http.Request) middleware.LogEntry {
|
||||
}
|
||||
|
||||
// Write logs a new entry at the end of the HTTP request
|
||||
func (l *StructuredLoggerEntry) Write(status, bytes int, elapsed time.Duration) {
|
||||
l.Logger.Info().Fields(l.fields).Int(
|
||||
"resp_status", status).Int(
|
||||
"resp_size", bytes).Int64(
|
||||
"elapsed_ms", elapsed.Nanoseconds()/1000000).Str(
|
||||
"sender", "httpd").Msg(
|
||||
"")
|
||||
func (l *StructuredLoggerEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) {
|
||||
l.Logger.Info().
|
||||
Timestamp().
|
||||
Str("sender", "httpd").
|
||||
Fields(l.fields).
|
||||
Int("resp_status", status).
|
||||
Int("resp_size", bytes).
|
||||
Int64("elapsed_ms", elapsed.Nanoseconds()/1000000).
|
||||
Send()
|
||||
}
|
||||
|
||||
// Panic logs panics
|
||||
func (l *StructuredLoggerEntry) Panic(v interface{}, stack []byte) {
|
||||
l.Logger.Error().Fields(l.fields).Str(
|
||||
"stack", string(stack)).Str(
|
||||
"panic", fmt.Sprintf("%+v", v)).Msg("")
|
||||
l.Logger.Error().
|
||||
Timestamp().
|
||||
Str("sender", "httpd").
|
||||
Fields(l.fields).
|
||||
Str("stack", string(stack)).
|
||||
Str("panic", fmt.Sprintf("%+v", v)).
|
||||
Send()
|
||||
}
|
||||
17
examples/ldapauthserver/logger/sync_wrapper.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type logSyncWrapper struct {
|
||||
sync.Mutex
|
||||
output *os.File
|
||||
}
|
||||
|
||||
func (l *logSyncWrapper) Write(b []byte) (n int, err error) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
return l.output.Write(b)
|
||||
}
|
||||
7
examples/ldapauthserver/main.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
import "github.com/drakkan/sftpgo/ldapauthserver/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
28
examples/ldapauthserver/utils/utils.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IsFileInputValid returns true this is a valid file name.
|
||||
// This method must be used before joining a file name, generally provided as
|
||||
// user input, with a directory
|
||||
func IsFileInputValid(fileInput string) bool {
|
||||
cleanInput := filepath.Clean(fileInput)
|
||||
if cleanInput == "." || cleanInput == ".." {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// IsStringPrefixInSlice searches a string prefix in a slice and returns true
|
||||
// if a matching prefix is found
|
||||
func IsStringPrefixInSlice(obj string, list []string) bool {
|
||||
for _, v := range list {
|
||||
if strings.HasPrefix(obj, v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package utils
|
||||
|
||||
const version = "0.9.1"
|
||||
const version = "0.1.0-dev"
|
||||
|
||||
var (
|
||||
commit = ""
|
||||
@@ -15,6 +15,14 @@ type VersionInfo struct {
|
||||
CommitHash string `json:"commit_hash"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
versionInfo = VersionInfo{
|
||||
Version: version,
|
||||
CommitHash: commit,
|
||||
BuildDate: date,
|
||||
}
|
||||
}
|
||||
|
||||
// GetVersionAsString returns the string representation of the VersionInfo struct
|
||||
func (v *VersionInfo) GetVersionAsString() string {
|
||||
versionString := v.Version
|
||||
@@ -27,10 +35,7 @@ func (v *VersionInfo) GetVersionAsString() string {
|
||||
return versionString
|
||||
}
|
||||
|
||||
func init() {
|
||||
versionInfo = VersionInfo{
|
||||
Version: version,
|
||||
CommitHash: commit,
|
||||
BuildDate: date,
|
||||
}
|
||||
// GetAppVersion returns VersionInfo struct
|
||||
func GetAppVersion() VersionInfo {
|
||||
return versionInfo
|
||||
}
|
||||
195
examples/php-activedirectory-http-server/README.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# SFTPGo on Windows with Active Directory Integration + Caddy Static File Server Example
|
||||
|
||||
[](https://www.youtube.com/watch?v=M5UcJI8t4AI)
|
||||
|
||||
This is similar to the ldapauthserver example, but is more specific to using Active Directory along with using SFTPGo on a Windows Server.
|
||||
|
||||
The Youtube Walkthrough/Tutorial video above goes into considerable more detail, but in short, it walks through setting up SFTPGo on a new Windows Server, and enables the External Authentication feature within SFTPGo, along with my `sftpgo-ldap-http-server` project, to allow for user authentication into SFTPGo to occur through one or more Active Directory connections.
|
||||
|
||||
Additionally, I go through using the Caddy web server, to help enable serving of static files, if this is something that would be of interest for you.
|
||||
|
||||
To get started, you'll want to download the latest release ZIP package from the [sftpgo-ldap-http-server repository](https://github.com/orware/sftpgo-ldap-http-server).
|
||||
|
||||
The ZIP itself contains the `sftpgo-ldap-http-server.exe` file, along with an `OpenLDAP` folder (mainly to help if you want to use TLS for your LDAP connections), and a `Data` which contains a logs folder, a configuration.example.php file, a functions.php file, and the LICENSE and README files.
|
||||
|
||||
The video above goes through the whole process, but to get started you'll want to install SFTPGo on your server, and then extract the `sftpgo-ldap-http-server` ZIP file on the server as well into a separate folder. Then you'll want to copy the configuration.example.php file and name it `configuration.php` and begin customizing the settings (e.g. add in your own LDAP settings, along with how you may want to have your folders be created). At the very minimum you'll want to make sure that the home directories are set correctly to how you want the folders to be created for your environment (you don't have to use the virtual folders or really any of the other functionality if you don't need it).
|
||||
|
||||
Once configured, from a command prompt window, if you are already in the same folder as where you extracted the `sftpgo-ldap-http-server` ZIP, you may simply call the `sftpgo-ldap-http-server.exe` and it should start up a simple HTTP server on Port 9001 running on localhost (the port can be adjusted via the `configuration.php` file as well). Now all you have to do is point SFTPGo's `external_auth_hook` option to point to `http://localhost:9001/` and you should be able to run some authentication tests (assuming you have all of your settings correct and there are no intermediate issues).
|
||||
|
||||
The video above definitely goes through some troubleshooting situations you might find yourself coming across, so while it is long (at about 1 hour, 42 minutes), it may be helpful to review and avoid some issues and just to learn a bit more about SFTPGo and the integration above.
|
||||
|
||||
## Example Virtual Folders Configuration (Allowing for Both a Public and Private Folder)
|
||||
|
||||
The following can be utilized if you'd like to assign your users both a Private Virtual Folder and Public Virtual Folder.
|
||||
|
||||
By itself, the Public Virtual Folder isn't necessarily public, so keep that in mind. Only by combining things together with the Caddy web server (and Caddyfile example configuration down below) can you be successful in making the `F:\files\public` folder from the example public.
|
||||
|
||||
```php
|
||||
$virtual_folders['example'] = [
|
||||
[
|
||||
//"id" => 0,
|
||||
"name" => "private-#USERNAME#",
|
||||
"mapped_path" => 'F:\files\private\#USERNAME#',
|
||||
//"used_quota_size" => 0,
|
||||
//"used_quota_files" => 0,
|
||||
//"last_quota_update" => 0,
|
||||
"virtual_path" => "/_private",
|
||||
"quota_size" => -1,
|
||||
"quota_files" => -1
|
||||
],
|
||||
[
|
||||
//"id" => 0,
|
||||
"name" => "public-#USERNAME#",
|
||||
"mapped_path" => 'F:\files\public\#USERNAME#',
|
||||
//"used_quota_size" => 0,
|
||||
//"used_quota_files" => 0,
|
||||
//"last_quota_update" => 0,
|
||||
"virtual_path" => "/_public",
|
||||
"quota_size" => -1,
|
||||
"quota_files" => -1
|
||||
]
|
||||
];
|
||||
```
|
||||
|
||||
## Example Connection "Output Object" Allowing For No Files in the User's Home Directory ("Root Directory") but Allowing for Files in the Public/Private Virtual Folders
|
||||
|
||||
The magic here happens in the "permissions" value, by limiting the root/home directory to just the list/download permissions, and then allowing all permissions on the Public/Private virtual folders.
|
||||
|
||||
```php
|
||||
$connection_output_objects['example'] = [
|
||||
'status' => 1,
|
||||
'username' => '',
|
||||
'expiration_date' => 0,
|
||||
'home_dir' => '',
|
||||
'uid' => 0,
|
||||
'gid' => 0,
|
||||
'max_sessions' => 0,
|
||||
'quota_size' => 0,
|
||||
'quota_files' => 100000,
|
||||
'permissions' => [
|
||||
"/" => ["list", "download"],
|
||||
"/_private" => ["*"],
|
||||
"/_public" => ["*"],
|
||||
],
|
||||
'upload_bandwidth' => 0,
|
||||
'download_bandwidth' => 0,
|
||||
'filters' => [
|
||||
'allowed_ip' => [],
|
||||
'denied_ip' => [],
|
||||
],
|
||||
'public_keys' => [],
|
||||
];
|
||||
```
|
||||
|
||||
## Recommended Usage of Automatic Groups Mode (Limiting by Group Prefix)
|
||||
|
||||
The `sftpgo-ldap-http-server` project is able to automatically create virtual folders for any groups your user is a memberof if the automatic mode is turned on. However, by having a specific set of allowed prefixes defined, you can limit things to just those groups that begin with the prefixes you've listed, which can be helpful. The prefix itself will be removed from the group name when added as a virtual folder for the user.
|
||||
|
||||
```php
|
||||
// If automatic groups mode is disabled, then you have to manually add the allowed groups into $allowed_groups down below:
|
||||
// If enabled, then any groups you are a memberof will automatically be added in using the template below.
|
||||
$auto_groups_mode = true;
|
||||
|
||||
$auto_groups_mode_virtual_folder_template = [
|
||||
[
|
||||
//"id" => 0,
|
||||
"name" => "groups-#GROUP#",
|
||||
"mapped_path" => 'F:\files\groups\#GROUP#',
|
||||
//"used_quota_size" => 0,
|
||||
//"used_quota_files" => 0,
|
||||
//"last_quota_update" => 0,
|
||||
"virtual_path" => "/groups/#GROUP#",
|
||||
"quota_size" => 0,
|
||||
"quota_files" => 100000
|
||||
]
|
||||
];
|
||||
|
||||
// Used only when auto groups mode is enabled and will help prevent all your groups from being
|
||||
// added into SFTPGo since only groups with the prefixes defined here will be automatically added
|
||||
// with prefixes automatically removed when listed as a virtual folder (e.g. a group with name
|
||||
// "sftpgo-example" would simply become "example").
|
||||
$allowed_group_prefixes = [
|
||||
'sftpgo-'
|
||||
];
|
||||
```
|
||||
|
||||
## Example Caddyfile Configuration You Can Adapt for Your Needs
|
||||
|
||||
```shell
|
||||
### Re-usable snippets:
|
||||
|
||||
(add_static_file_serving_features) {
|
||||
|
||||
# Allow accessing files without requiring .html:
|
||||
try_files {path} {path}.html
|
||||
|
||||
# Enable Static File Server and Directory Browsing:
|
||||
file_server browse
|
||||
|
||||
# Enable templating functionality:
|
||||
templates
|
||||
|
||||
# Enable Compression for Output:
|
||||
encode zstd gzip
|
||||
|
||||
handle_errors {
|
||||
respond "<pre>{http.error.status_code} {http.error.status_text}</pre>"
|
||||
}
|
||||
}
|
||||
|
||||
(add_hsts_headers) {
|
||||
header {
|
||||
# Enable HTTP Strict Transport Security (HSTS) to force clients to always
|
||||
|
||||
# connect via HTTPS (do not use if only testing)
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
|
||||
# Enable cross-site filter (XSS) and tell browser to block detected attacks
|
||||
X-XSS-Protection "1; mode=block"
|
||||
|
||||
# Prevent some browsers from MIME-sniffing a response away from the declared Content-Type
|
||||
X-Content-Type-Options "nosniff"
|
||||
|
||||
# Disallow the site to be rendered within a frame (clickjacking protection)
|
||||
X-Frame-Options "DENY"
|
||||
|
||||
# keep referrer data off of HTTP connections
|
||||
Referrer-Policy no-referrer-when-downgrade
|
||||
}
|
||||
}
|
||||
|
||||
(add_logging_with_path) {
|
||||
log {
|
||||
output file "{args.0}" {
|
||||
roll_size 100mb
|
||||
roll_keep 5
|
||||
roll_keep_for 720h
|
||||
}
|
||||
|
||||
format json
|
||||
#format console
|
||||
#format single_field common_log
|
||||
}
|
||||
}
|
||||
|
||||
### Site Definitions:
|
||||
|
||||
public.example.com {
|
||||
|
||||
# Site Root:
|
||||
root * F:\files\public
|
||||
|
||||
import add_logging_with_path "F:\caddy\logs\public_example_com_access.log"
|
||||
import add_static_file_serving_features
|
||||
import add_hsts_headers
|
||||
}
|
||||
|
||||
|
||||
### Reverse Proxy Definitions:
|
||||
|
||||
webdav.example.com {
|
||||
reverse_proxy localhost:9000
|
||||
|
||||
import add_logging_with_path "F:\caddy\logs\webdav_example_com_access.log"
|
||||
}
|
||||
```
|
||||
23
examples/quotascan/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Update user quota
|
||||
|
||||
:warning: Since v2.4.0 you can use the [EventManager](https://sftpgo.github.io/latest/eventmanager/) to schedule quota scans.
|
||||
|
||||
The `scanuserquota` example script shows how to use the SFTPGo REST API to update the users' quota.
|
||||
|
||||
The stored quota may be incorrect for several reasons, such as an unexpected shutdown while uploading files, temporary provider failures, files copied outside of SFTPGo, and so on.
|
||||
|
||||
A quota scan updates the number of files and their total size for the specified user and the virtual folders, if any, included in his quota.
|
||||
|
||||
If you want to track quotas, a scheduled quota scan is recommended. You can use this example as a starting point.
|
||||
|
||||
The script is written in Python and has the following requirements:
|
||||
|
||||
- python3 or python2
|
||||
- python [Requests](https://requests.readthedocs.io/en/master/) module
|
||||
|
||||
The provided example tries to connect to an SFTPGo instance running on `127.0.0.1:8080` using the following credentials:
|
||||
|
||||
- username: `admin`
|
||||
- password: `password`
|
||||
|
||||
Please edit the script according to your needs.
|
||||
119
examples/quotascan/scanuserquota
Executable file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from datetime import datetime
|
||||
import sys
|
||||
import time
|
||||
|
||||
import pytz
|
||||
import requests
|
||||
|
||||
try:
|
||||
import urllib.parse as urlparse
|
||||
except ImportError:
|
||||
import urlparse
|
||||
|
||||
# change base_url to point to your SFTPGo installation
|
||||
base_url = "http://127.0.0.1:8080"
|
||||
# set to False if you want to skip TLS certificate validation
|
||||
verify_tls_cert = True
|
||||
# set the credentials for a valid admin here
|
||||
admin_user = "admin"
|
||||
admin_password = "password"
|
||||
|
||||
|
||||
# set your update conditions here
|
||||
def needQuotaUpdate(user):
|
||||
if user["status"] == 0: # inactive user
|
||||
return False
|
||||
if user["quota_size"] == 0 and user["quota_files"] == 0: # no quota restrictions
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class UpdateQuota:
|
||||
|
||||
def __init__(self):
|
||||
self.limit = 100
|
||||
self.offset = 0
|
||||
self.access_token = ""
|
||||
self.access_token_expiration = None
|
||||
|
||||
def printLog(self, message):
|
||||
print("{} - {}".format(datetime.now(), message))
|
||||
|
||||
def checkAccessToken(self):
|
||||
if self.access_token != "" and self.access_token_expiration:
|
||||
expire_diff = self.access_token_expiration - datetime.now(tz=pytz.UTC)
|
||||
# we don't use total_seconds to be python 2 compatible
|
||||
seconds_to_expire = expire_diff.days * 86400 + expire_diff.seconds
|
||||
if seconds_to_expire > 180:
|
||||
return
|
||||
|
||||
auth = requests.auth.HTTPBasicAuth(admin_user, admin_password)
|
||||
r = requests.get(urlparse.urljoin(base_url, "api/v2/token"), auth=auth, verify=verify_tls_cert, timeout=10)
|
||||
if r.status_code != 200:
|
||||
self.printLog("error getting access token: {}".format(r.text))
|
||||
sys.exit(1)
|
||||
self.access_token = r.json()["access_token"]
|
||||
self.access_token_expiration = pytz.timezone("UTC").localize(datetime.strptime(r.json()["expires_at"],
|
||||
"%Y-%m-%dT%H:%M:%SZ"))
|
||||
|
||||
def getAuthHeader(self):
|
||||
self.checkAccessToken()
|
||||
return {"Authorization": "Bearer " + self.access_token}
|
||||
|
||||
def waitForQuotaUpdate(self, username):
|
||||
while True:
|
||||
auth_header = self.getAuthHeader()
|
||||
r = requests.get(urlparse.urljoin(base_url, "api/v2/quotas/users/scans"), headers=auth_header, verify=verify_tls_cert,
|
||||
timeout=10)
|
||||
if r.status_code != 200:
|
||||
self.printLog("error getting quota scans while waiting for {}: {}".format(username, r.text))
|
||||
sys.exit(1)
|
||||
|
||||
scanning = False
|
||||
for scan in r.json():
|
||||
if scan["username"] == username:
|
||||
scanning = True
|
||||
if not scanning:
|
||||
break
|
||||
self.printLog("waiting for the quota scan to complete for user {}".format(username))
|
||||
time.sleep(2)
|
||||
|
||||
self.printLog("quota update for user {} finished".format(username))
|
||||
|
||||
def updateUserQuota(self, username):
|
||||
self.printLog("starting quota update for user {}".format(username))
|
||||
auth_header = self.getAuthHeader()
|
||||
r = requests.post(urlparse.urljoin(base_url, "api/v2/quotas/users/" + username + "/scan"), headers=auth_header,
|
||||
verify=verify_tls_cert, timeout=10)
|
||||
if r.status_code != 202:
|
||||
self.printLog("error starting quota scan for user {}: {}".format(username, r.text))
|
||||
sys.exit(1)
|
||||
self.waitForQuotaUpdate(username)
|
||||
|
||||
def updateUsersQuota(self):
|
||||
while True:
|
||||
self.printLog("get users, limit {} offset {}".format(self.limit, self.offset))
|
||||
auth_header = self.getAuthHeader()
|
||||
payload = {"limit":self.limit, "offset":self.offset}
|
||||
r = requests.get(urlparse.urljoin(base_url, "api/v2/users"), headers=auth_header, params=payload,
|
||||
verify=verify_tls_cert, timeout=10)
|
||||
if r.status_code != 200:
|
||||
self.printLog("error getting users: {}".format(r.text))
|
||||
sys.exit(1)
|
||||
users = r.json()
|
||||
for user in users:
|
||||
if needQuotaUpdate(user):
|
||||
self.updateUserQuota(user["username"])
|
||||
else:
|
||||
self.printLog("user {} does not need a quota update".format(user["username"]))
|
||||
|
||||
self.offset += len(users)
|
||||
if len(users) < self.limit:
|
||||
break
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
q = UpdateQuota()
|
||||
q.updateUsersQuota()
|
||||
209
go.mod
@@ -1,27 +1,190 @@
|
||||
module github.com/drakkan/sftpgo
|
||||
module github.com/drakkan/sftpgo/v2
|
||||
|
||||
go 1.12
|
||||
go 1.22.2
|
||||
|
||||
require (
|
||||
github.com/alexedwards/argon2id v0.0.0-20190612080829-01a59b2b8802
|
||||
github.com/go-chi/chi v4.0.2+incompatible
|
||||
github.com/go-chi/render v1.0.1
|
||||
github.com/go-sql-driver/mysql v1.4.1
|
||||
github.com/lib/pq v1.2.0
|
||||
github.com/magiconair/properties v1.8.1 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.11.0
|
||||
github.com/pelletier/go-toml v1.4.0 // indirect
|
||||
github.com/pkg/sftp v1.10.1
|
||||
github.com/rs/xid v1.2.1
|
||||
github.com/rs/zerolog v1.15.0
|
||||
github.com/spf13/afero v1.2.2 // indirect
|
||||
github.com/spf13/cobra v0.0.5
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/viper v1.4.0
|
||||
go.etcd.io/bbolt v1.3.3
|
||||
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 // indirect
|
||||
golang.org/x/sys v0.0.0-20190830142957-1e83adbbebd0 // indirect
|
||||
google.golang.org/appengine v1.6.2 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
cloud.google.com/go/storage v1.42.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2
|
||||
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
|
||||
github.com/alexedwards/argon2id v1.0.0
|
||||
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.0
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.21
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.21
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.8
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.1
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.22.1
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.56.1
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.31.1
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.29.1
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.3.8
|
||||
github.com/coreos/go-oidc/v3 v3.10.0
|
||||
github.com/drakkan/webdav v0.0.0-20240503091431-218ec83910bb
|
||||
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
|
||||
github.com/fclairamb/ftpserverlib v0.24.1
|
||||
github.com/fclairamb/go-log v0.5.0
|
||||
github.com/go-acme/lego/v4 v4.17.4
|
||||
github.com/go-chi/chi/v5 v5.0.14
|
||||
github.com/go-chi/jwtauth/v5 v5.3.1
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/go-hclog v1.6.3
|
||||
github.com/hashicorp/go-plugin v1.6.1
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7
|
||||
github.com/jackc/pgx/v5 v5.6.0
|
||||
github.com/jlaffaye/ftp v0.2.0
|
||||
github.com/klauspost/compress v1.17.9
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.21
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/mhale/smtpd v0.8.3
|
||||
github.com/minio/sio v0.4.0
|
||||
github.com/otiai10/copy v1.14.0
|
||||
github.com/pires/go-proxyproto v0.7.0
|
||||
github.com/pkg/sftp v1.13.7-0.20240410063531-637088883317
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/prometheus/client_golang v1.19.1
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/rs/cors v1.11.0
|
||||
github.com/rs/xid v1.5.0
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/sftpgo/sdk v0.1.8
|
||||
github.com/shirou/gopsutil/v3 v3.24.5
|
||||
github.com/spf13/afero v1.11.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/viper v1.19.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/studio-b12/gowebdav v0.9.0
|
||||
github.com/subosito/gotenv v1.6.0
|
||||
github.com/unrolled/secure v1.14.0
|
||||
github.com/wagslane/go-password-validator v0.3.0
|
||||
github.com/wneessen/go-mail v0.4.1
|
||||
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
|
||||
go.etcd.io/bbolt v1.3.10
|
||||
go.uber.org/automaxprocs v1.5.3
|
||||
gocloud.dev v0.37.0
|
||||
golang.org/x/crypto v0.24.0
|
||||
golang.org/x/net v0.26.0
|
||||
golang.org/x/oauth2 v0.21.0
|
||||
golang.org/x/sys v0.21.0
|
||||
golang.org/x/term v0.21.0
|
||||
golang.org/x/time v0.5.0
|
||||
google.golang.org/api v0.185.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.115.0 // indirect
|
||||
cloud.google.com/go/auth v0.5.1 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.3.0 // indirect
|
||||
cloud.google.com/go/iam v1.1.8 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0 // indirect
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.21.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.25.1 // indirect
|
||||
github.com/aws/smithy-go v1.20.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/boombuler/barcode v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
||||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.5 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httprc v1.0.5 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.61 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.54.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.6.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||
github.com/tklauser/numcpus v0.8.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect
|
||||
go.opentelemetry.io/otel v1.27.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.27.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.27.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240617180043-68d350f18fd4 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect
|
||||
google.golang.org/grpc v1.64.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/fclairamb/ftpserverlib => github.com/drakkan/ftpserverlib v0.0.0-20240603150004-6a8f643fbf2e
|
||||
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20240430173938-7ba8270c8e7f
|
||||
github.com/robfig/cron/v3 => github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0
|
||||
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20240604170954-7edbaf8467f7
|
||||
)
|
||||
|
||||
706
go.sum
@@ -1,211 +1,565 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
|
||||
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
|
||||
cloud.google.com/go/auth v0.5.1 h1:0QNO7VThG54LUzKiQxv8C6x1YX7lUrzlAa1nVLF8CIw=
|
||||
cloud.google.com/go/auth v0.5.1/go.mod h1:vbZT8GjzDf3AVqCcQmqeeM32U9HBFc32vVVAbwDsa6s=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
|
||||
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
|
||||
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0=
|
||||
cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE=
|
||||
cloud.google.com/go/kms v1.17.1 h1:5k0wXqkxL+YcXd4viQzTqCgzzVKKxzgrK+rCZJytEQs=
|
||||
cloud.google.com/go/kms v1.17.1/go.mod h1:DCMnCF/apA6fZk5Cj4XsD979OyHAqFasPuA5Sd0kGlQ=
|
||||
cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU=
|
||||
cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng=
|
||||
cloud.google.com/go/storage v1.42.0 h1:4QtGpplCVt1wz6g5o1ifXd656P5z+yNgzdw1tVfp0cU=
|
||||
cloud.google.com/go/storage v1.42.0/go.mod h1:HjMXRFq65pGKFn6hxj6x3HCyR41uSB72Z0SO/Vn6JFQ=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0 h1:1nGuui+4POelzDwI7RG56yfQJHCnKvwfMoU7VsEp+Zg=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0/go.mod h1:99EvauvlcJ1U06amZiksfYz/3aFGyIhWGHVyiZXtBAI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0 h1:H+U3Gk9zY56G3u872L82bk4thcsy2Gghb9ExT4Zvm1o=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0/go.mod h1:mgrmMSgaLp9hmax62XQTd0N4aAqSE5E0DulSpVYK7vc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0 h1:AifHbc4mg0x9zW52WOpKbsHaDKuRhlI7TVl47thgQ70=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0/go.mod h1:T5RfihdXtBDxt1Ch2wobif3TvzTdumDy29kahv6AV9A=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2 h1:YUUxeiOWgdAQE3pXt2H7QXzZs0q8UBjgRbl56qo8GYM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2/go.mod h1:dmXQgZuiSubAecswZE+Sm8jkvEa7kQgTPVRvwL/nd0E=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alexedwards/argon2id v0.0.0-20190612080829-01a59b2b8802 h1:RwMM1q/QSKYIGbHfOkf843hE8sSUJtf1dMwFPtEDmm0=
|
||||
github.com/alexedwards/argon2id v0.0.0-20190612080829-01a59b2b8802/go.mod h1:4dsm7ufQm1Gwl8S2ss57u+2J7KlxIL2QUmFGlGtWogY=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI=
|
||||
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec=
|
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
||||
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
|
||||
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 h1:I9YN9WMo3SUh7p/4wKeNvD/IQla3U3SUa61U7ul+xM4=
|
||||
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964/go.mod h1:eFiR01PwTcpbzXtdMces7zxg6utvFM5puiWHpWB8D/k=
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.0 h1:6qAwtzlfcTtcL8NHtbDQAqgM5s6NDipQTkPxyH/6kAA=
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.0/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2/go.mod h1:lPprDr1e6cJdyYeGXnRaJoP4Md+cDBvi2eOj00BlGmg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.21 h1:yPX3pjGCe2hJsetlmGNB4Mngu7UPmvWPzzWCv1+boeM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.21/go.mod h1:4XtlEU6DzNai8RMbjSF5MgGZtYvrhBP/aKZcRtZAVdM=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.21 h1:pjAqgzfgFhTv5grc7xPHtXCAaMapzmwA7aU+c/SZQGw=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.21/go.mod h1:nhK6PtBlfHTUDVmBLr1dg+WHCOCK+1Fu/WQyVHPsgNQ=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.8 h1:FR+oWPFb/8qMVYMWN98bUZAGqPvLHiyqg1wqQGfUAXY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.8/go.mod h1:EgSKcHiuuakEIxJcKGzVNWh5srVAQ3jKaSrBGRYvM48=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.1 h1:D9VqWMuw7lJAX6d5eINfRQ/PkvtcJAK3Qmd6f6xEeUw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.1/go.mod h1:ckvBx7codI4wzc5inOfDp5ZbK7TjMFa7eXwmLvXQrRk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12 h1:SJ04WXGTwnHlWIODtC5kJzKbeuHt+OUNOgKg7nfnUGw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12/go.mod h1:FkpvXhA92gb3GE9LD6Og0pHHycTxW7xGpnEh5E7Opwo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12 h1:hb5KgeYfObi5MHkSSZMEudnIvX30iB+E21evI4r6BnQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12/go.mod h1:CroKe/eWJdyfy9Vx4rljP5wTUjNJfb+fPz1uMYUhEGM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.12 h1:DXFWyt7ymx/l1ygdyTTS0X923e+Q2wXIxConJzrgwc0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.12/go.mod h1:mVOr/LbvaNySK1/BTy4cBOCjhCNY2raWBwK4v+WR5J4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.14 h1:oWccitSnByVU74rQRHac4gLfDqjB6Z1YQGOY/dXKedI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.14/go.mod h1:8SaZBlQdCLrc/2U3CEO48rYj9uR8qRsPRkmzwNM52pM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.14 h1:zSDPny/pVnkqABXYRicYuPf9z2bTqfH13HT3v6UheIk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.14/go.mod h1:3TTcI5JSzda1nw/pkVC9dhgLre0SNBFj2lYS4GctXKI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.12 h1:tzha+v1SCEBpXWEuw6B/+jm4h5z8hZbTpXz0zRZqTnw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.12/go.mod h1:n+nt2qjHGoseWeLHt1vEr6ZRCCxIN2KcNpJxBcYQSwI=
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.22.1 h1:cUDTQYv1AapRcYi7bmSO990q9t3+C6KXcjZxdNexgMs=
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.22.1/go.mod h1:FARw6qpTrHDbxBAJNEdwBzfMQYZj/kHYeEw2ww0V/HM=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.56.1 h1:wsg9Z/vNnCmxWikfGIoOlnExtEU459cR+2d+iDJ8elo=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.56.1/go.mod h1:8rDw3mVwmvIWWX/+LWY3PPIMZuwnQdJMCt0iVFVT3qw=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.31.1 h1:fMhrWVym3nTAcf3eT9XsYcfN1kgQ/7ZuVLGHjPAn6Ms=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.31.1/go.mod h1:tBCf2+VgRT/Lk9KIlKpTxyCunzxHcP8BFPqcck5I9mM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.21.1 h1:sd0BsnAvLH8gsp2e3cbaIr+9D7T1xugueQ7V/zUAsS4=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.21.1/go.mod h1:lcQG/MmxydijbeTOp04hIuJwXGWPZGI3bwdFDGRTv14=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.25.1 h1:1uEFNNskK/I1KoZ9Q8wJxMz5V9jyBlsiaNrM7vA3YUQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.25.1/go.mod h1:z0P8K+cBIsFXUr5rzo/psUeJ20XjPN0+Nn8067Nd+E4=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.29.1 h1:myX5CxqXE0QMZNja6FA1/FSE3Vu1rVmeUmpJMMzeZg0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.29.1/go.mod h1:N2mQiucsO0VwK9CYuS4/c2n6Smeh1v47Rz3dWCPFLdE=
|
||||
github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
|
||||
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
|
||||
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
|
||||
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.3.8 h1:53yoUo4+EtrC1NrAEgnnad4AS3ntNvGup1PAXZ7UmpE=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.3.8/go.mod h1:9uH5jK4yQ3ZQUT9IXe4I2fHzMIF5+JC/oOdzTRgJYJk=
|
||||
github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU=
|
||||
github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs=
|
||||
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
|
||||
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0 h1:EW9gIJRmt9lzk66Fhh4S8VEtURA6QHZqGeSRE9Nb2/U=
|
||||
github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/drakkan/crypto v0.0.0-20240604170954-7edbaf8467f7 h1:4wqhB0oUlzqpP9i5oqmCkBt9PRhwKMh0Fs67kGCrNRg=
|
||||
github.com/drakkan/crypto v0.0.0-20240604170954-7edbaf8467f7/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
github.com/drakkan/ftp v0.0.0-20240430173938-7ba8270c8e7f h1:S9JUlrOzjK58UKoLqqb40YLyVlt0bcIFtYrvnanV3zc=
|
||||
github.com/drakkan/ftp v0.0.0-20240430173938-7ba8270c8e7f/go.mod h1:4p8lUl4vQ80L598CygL+3IFtm+3nggvvW/palOlViwE=
|
||||
github.com/drakkan/ftpserverlib v0.0.0-20240603150004-6a8f643fbf2e h1:VBpqQeChkGXSV1FXCtvd3BJTyB+DcMgiu7SfkpsGuKw=
|
||||
github.com/drakkan/ftpserverlib v0.0.0-20240603150004-6a8f643fbf2e/go.mod h1:aAwyOAC6IIe+IZeeGD1QjuE3GGDzqW/c5Xtn+Dp0JUM=
|
||||
github.com/drakkan/webdav v0.0.0-20240503091431-218ec83910bb h1:067/Uo8cfeY7QC0yzWCr/RImuNcM0rLWAsBUyMks59o=
|
||||
github.com/drakkan/webdav v0.0.0-20240503091431-218ec83910bb/go.mod h1:zOVb1QDhwwqWn2L2qZ0U3swMSO4GTSNyIwXCGO/UGWE=
|
||||
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 h1:/ZshrfQzayqRSBDodmp3rhNCHJCff+utvgBuWRbiqu4=
|
||||
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001/go.mod h1:kltMsfRMTHSFdMbK66XdS8mfMW77+FZA1fGY1xYMF84=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/fclairamb/go-log v0.5.0 h1:Gz9wSamEaA6lta4IU2cjJc2xSq5sV5VYSB5w/SUHhVc=
|
||||
github.com/fclairamb/go-log v0.5.0/go.mod h1:XoRO1dYezpsGmLLkZE9I+sHqpqY65p8JA+Vqblb7k40=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-acme/lego/v4 v4.17.4 h1:h0nePd3ObP6o7kAkndtpTzCw8shOZuWckNYeUQwo36Q=
|
||||
github.com/go-acme/lego/v4 v4.17.4/go.mod h1:dU94SvPNqimEeb7EVilGGSnS0nU1O5Exir0pQ4QFL4U=
|
||||
github.com/go-chi/chi/v5 v5.0.14 h1:PyEwo2Vudraa0x/Wl6eDRRW2NXBvekgfxyydcM0WGE0=
|
||||
github.com/go-chi/chi/v5 v5.0.14/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/jwtauth/v5 v5.3.1 h1:1ePWrjVctvp1tyBq5b/2ER8Th/+RbYc7x4qNsc5rh5A=
|
||||
github.com/go-chi/jwtauth/v5 v5.3.1/go.mod h1:6Fl2RRmWXs3tJYE1IQGX81FsPoGqDwq9c15j52R5q80=
|
||||
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
|
||||
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
||||
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
|
||||
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||
github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
|
||||
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
|
||||
github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA=
|
||||
github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI=
|
||||
github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
|
||||
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
|
||||
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg=
|
||||
github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.10.1 h1:VasscCm72135zRysgrJDKsntdmPN+OuU3+nnHYA9wyc=
|
||||
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
|
||||
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||
github.com/lestrrat-go/httprc v1.0.5 h1:bsTfiH8xaKOJPrg1R+E3iE/AWZr/x0Phj9PBTG/OLUk=
|
||||
github.com/lestrrat-go/httprc v1.0.5/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
||||
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
||||
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.21 h1:jAPKupy4uHgrHFEdjVjNkUgoBKtVDgrQPB/h55FHrR0=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.21/go.mod h1:09mLW8zto6bWL9GbwnqAli+ArLf+5M33QLQPDggkUWM=
|
||||
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
|
||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI=
|
||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mhale/smtpd v0.8.3 h1:8j8YNXajksoSLZja3HdwvYVZPuJSqAxFsib3adzRRt8=
|
||||
github.com/mhale/smtpd v0.8.3/go.mod h1:MQl+y2hwIEQCXtNhe5+55n0GZOjSmeqORDIXbqUL3x4=
|
||||
github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
|
||||
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
|
||||
github.com/minio/sio v0.4.0 h1:u4SWVEm5lXSqU42ZWawV0D9I5AZ5YMmo2RXpEQ/kRhc=
|
||||
github.com/minio/sio v0.4.0/go.mod h1:oBSjJeGbBdRMZZwna07sX9EFzZy+ywu5aofRiV1g79I=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
|
||||
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
|
||||
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
|
||||
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
|
||||
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
|
||||
github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
|
||||
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.7-0.20240410063531-637088883317 h1:kupFhKi4R3XqKmUmqGSHWn/WZbC9CnwSoW421tL1gGw=
|
||||
github.com/pkg/sftp v1.13.7-0.20240410063531-637088883317/go.mod h1:KMKI0t3T6hfA+lTR/ssZdunHo+uwq7ghoN09/FSu3DY=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
|
||||
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8=
|
||||
github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po=
|
||||
github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
|
||||
github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
|
||||
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/sftpgo/sdk v0.1.8 h1:HAywJl9jZnigFGztA/CWLieOW+R+HH6js6o6/qYvuSY=
|
||||
github.com/sftpgo/sdk v0.1.8/go.mod h1:Isl0IEzS/Muvh8Fr4X+NWFsOS/fZQHRD4oPQpoY7C4g=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472 h1:Gv7RPwsi3eZ2Fgewe3CBsuOebPwO27PoXzRpJPsvSSM=
|
||||
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/studio-b12/gowebdav v0.9.0 h1:1j1sc9gQnNxbXXM4M/CebPOX4aXYtr7MojAVcN4dHjU=
|
||||
github.com/studio-b12/gowebdav v0.9.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
||||
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
||||
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
|
||||
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
|
||||
github.com/unrolled/secure v1.14.0 h1:u9vJTU/pR4Bny0ntLUMxdfLtmIRGvQf2sEFuA0TG9AE=
|
||||
github.com/unrolled/secure v1.14.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
|
||||
github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=
|
||||
github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
|
||||
github.com/wneessen/go-mail v0.4.1 h1:m2rSg/sc8FZQCdtrV5M8ymHYOFrC6KJAQAIcgrXvqoo=
|
||||
github.com/wneessen/go-mail v0.4.1/go.mod h1:zxOlafWCP/r6FEhAaRgH4IC1vg2YXxO0Nar9u0IScZ8=
|
||||
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a h1:XfF01GyP+0eWCaVp0y6rNN+kFp7pt9Da4UUYrJ5XPWA=
|
||||
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a/go.mod h1:aXb8yZQEWo1XHGMf1qQfnb83GR/EJ2EBlwtUgAaNBoE=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
|
||||
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0 h1:vS1Ao/R55RNV4O7TA2Qopok8yN+X0LIP6RVWLFkprck=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0/go.mod h1:BMsdeOxN04K0L5FNUBfjFdvwWGNe/rkmSwH4Aelu/X0=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0=
|
||||
go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg=
|
||||
go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ=
|
||||
go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik=
|
||||
go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
|
||||
go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw=
|
||||
go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4=
|
||||
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
|
||||
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
gocloud.dev v0.37.0 h1:XF1rN6R0qZI/9DYjN16Uy0durAmSlf58DHOcb28GPro=
|
||||
gocloud.dev v0.37.0/go.mod h1:7/O4kqdInCNsc6LqgmuFnS0GRew4XNNYWpA44yQnwco=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190830142957-1e83adbbebd0 h1:7z820YPX9pxWR59qM7BE5+fglp4D/mKqAwCvGt11b+8=
|
||||
golang.org/x/sys v0.0.0-20190830142957-1e83adbbebd0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210601080250-7ecdf8ef093b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
google.golang.org/api v0.185.0 h1:ENEKk1k4jW8SmmaT6RE+ZasxmxezCrD5Vw4npvr+pAU=
|
||||
google.golang.org/api v0.185.0/go.mod h1:HNfvIkJGlgrIlrbYkAm9W9IdkmKZjOTVh33YltygGbg=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.6.2 h1:j8RI1yW0SkI+paT6uGwMlrMI/6zwYA6/CFil8rxOzGI=
|
||||
google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20240617180043-68d350f18fd4 h1:CUiCqkPw1nNrNQzCCG4WA65m0nAmQiwXHpub3dNyruU=
|
||||
google.golang.org/genproto v0.0.0-20240617180043-68d350f18fd4/go.mod h1:EvuUDCulqGgV80RvP1BHuom+smhX4qtlhnNatHuroGQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 h1:MuYw1wJzT+ZkybKfaOXKp5hJiZDn2iHaXRw0mRYdHSc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4/go.mod h1:px9SlOOZBg1wM1zdnr8jEL4CNGUBZ+ZKYtNPApNQc4c=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 h1:Di6ANFilr+S60a4S61ZM00vLdw0IrQOSMS2/6mrnOU0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
|
||||
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
||||
BIN
img/7digital.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
img/Aledade_logo.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
img/IDCS.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
img/jumptrading.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
img/logo.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
img/vps2day.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
img/wpengine.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
36
init/com.github.drakkan.sftpgo.plist
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.github.drakkan.sftpgo</string>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>SFTPGO_CONFIG_DIR</key>
|
||||
<string>/usr/local/opt/sftpgo/etc</string>
|
||||
<key>SFTPGO_LOG_FILE_PATH</key>
|
||||
<string>/usr/local/opt/sftpgo/var/log/sftpgo.log</string>
|
||||
<key>SFTPGO_HTTPD__TEMPLATES_PATH</key>
|
||||
<string>/usr/local/opt/sftpgo/usr/share/templates</string>
|
||||
<key>SFTPGO_HTTPD__STATIC_FILES_PATH</key>
|
||||
<string>/usr/local/opt/sftpgo/usr/share/static</string>
|
||||
<key>SFTPGO_HTTPD__OPENAPI_PATH</key>
|
||||
<string>/usr/local/opt/sftpgo/usr/share/openapi</string>
|
||||
<key>SFTPGO_HTTPD__BACKUPS_PATH</key>
|
||||
<string>/usr/local/opt/sftpgo/var/lib/backups</string>
|
||||
<key>SFTPGO_DATA_PROVIDER__CREDENTIALS_PATH</key>
|
||||
<string>/usr/local/opt/sftpgo/var/lib/credentials</string>
|
||||
</dict>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/usr/local/opt/sftpgo/etc</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/local/opt/sftpgo/bin/sftpgo</string>
|
||||
<string>serve</string>
|
||||
</array>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>ThrottleInterval</key>
|
||||
<integer>10</integer>
|
||||
</dict>
|
||||
</plist>
|
||||