mirror of
https://github.com/drakkan/sftpgo.git
synced 2026-03-14 14:25:52 +01:00
Compare commits
469 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb8a8cb791 |
||
|
|
5f072ad8ac |
||
|
|
01a6bf2851 |
||
|
|
c4c95d72b9 |
||
|
|
6ad1f69b2c |
||
|
|
baedf15e0e |
||
|
|
c9601a8976 |
||
|
|
2f092d1289 |
||
|
|
6cef669b8d |
||
|
|
03ae0a1c84 |
||
|
|
4c00f6061c |
||
|
|
2b4c1f32fd |
||
|
|
7071011a5e |
||
|
|
9b58744275 |
||
|
|
35d9242466 |
||
|
|
7a9f5eb50c |
||
|
|
fbbea4a1fa |
||
|
|
96cc3dcf52 |
||
|
|
e48954f05c |
||
|
|
c26cfa364f |
||
|
|
906b0731f1 |
||
|
|
133d2692c4 |
||
|
|
e3b2780655 |
||
|
|
e44ff487e5 |
||
|
|
ed0c1a01ab |
||
|
|
f1022db5c1 |
||
|
|
e861f0f578 |
||
|
|
f90fbccb2c |
||
|
|
1815a098b6 |
||
|
|
3bf7a85325 |
||
|
|
f06a2e8bd4 |
||
|
|
a73f8998a2 |
||
|
|
0be7545d9b |
||
|
|
a8d74a311f | ||
|
|
4a091d6c24 |
||
|
|
891248e7c9 |
||
|
|
23d6e0dc3f |
||
|
|
44828629c5 |
||
|
|
9db27aa782 |
||
|
|
3d549ce702 |
||
|
|
1d9cc1e00f |
||
|
|
babdee5be1 |
||
|
|
a1e45277dd |
||
|
|
375650e9be | ||
|
|
2edd13aef6 | ||
|
|
130fc8e0a2 |
||
|
|
0add546be3 |
||
|
|
d0f4c6423e |
||
|
|
decdb187cf |
||
|
|
21639b963c |
||
|
|
d42bbef16e |
||
|
|
ac3e59562d |
||
|
|
0cf9036f47 |
||
|
|
8c85a722a2 |
||
|
|
e608805b13 |
||
|
|
cbdc48ba7c | ||
|
|
90821ffc23 |
||
|
|
32cc426cb9 |
||
|
|
9fa18c37f7 |
||
|
|
4230da8e7d |
||
|
|
22c875c0a1 |
||
|
|
74f8539247 |
||
|
|
3a42c70021 | ||
|
|
59bd46a227 | ||
|
|
5039a0ee31 | ||
|
|
973b68a383 |
||
|
|
5ce9688780 |
||
|
|
accb9703d0 |
||
|
|
9c42ec34e8 |
||
|
|
29c635a9a6 |
||
|
|
e82d1bbef6 |
||
|
|
2c36077178 |
||
|
|
4e91326124 | ||
|
|
317f14f869 |
||
|
|
a768dac29d |
||
|
|
c4bc88cd2e |
||
|
|
90685d8ef2 |
||
|
|
f21c3d2af2 |
||
|
|
278f522e30 |
||
|
|
fa70ff35c4 |
||
|
|
314bb5c886 |
||
|
|
0ae2354fed |
||
|
|
9ca35c3555 |
||
|
|
69f2c70661 |
||
|
|
35525e22e9 |
||
|
|
cc0ee9f43b |
||
|
|
7dd5757a44 |
||
|
|
3f21db14e4 |
||
|
|
e892748ef4 |
||
|
|
f4092b9f9e |
||
|
|
cdaefbf04a |
||
|
|
5c3aa8278b |
||
|
|
255ad5f6db |
||
|
|
a469dd68a2 |
||
|
|
29e9d95088 |
||
|
|
952df50a98 |
||
|
|
d2ee43585a |
||
|
|
726f1fde19 | ||
|
|
75a9ebcdf9 |
||
|
|
7f03dc0fab |
||
|
|
52ae36f169 |
||
|
|
7ce456edef |
||
|
|
b1208279b7 |
||
|
|
0dca906351 |
||
|
|
20df8ba48b | ||
|
|
b160090866 |
||
|
|
78d93730e0 |
||
|
|
aad4de6001 |
||
|
|
19d1a0e0c1 |
||
|
|
a5dd529d88 |
||
|
|
6bde42fc3f |
||
|
|
917d992231 |
||
|
|
fc111b44d9 | ||
|
|
cdcea54f46 | ||
|
|
a2d3613250 |
||
|
|
81a9813376 |
||
|
|
63366b0007 |
||
|
|
0f6202f059 |
||
|
|
e7a1128574 |
||
|
|
0dec86474e |
||
|
|
b48a90bce9 |
||
|
|
75ad6346c3 |
||
|
|
b2948a5255 |
||
|
|
ddbe40cefa |
||
|
|
9a0137befb |
||
|
|
0bac81816c |
||
|
|
8ae6e5e486 |
||
|
|
c49d76274d |
||
|
|
ae11c81bf8 |
||
|
|
166b87fa3c | ||
|
|
76f6dc06de |
||
|
|
c2835bc19d |
||
|
|
fe78974b47 |
||
|
|
9d20f1744a |
||
|
|
7317674b41 |
||
|
|
bdd097b1c7 |
||
|
|
66a20f34f8 |
||
|
|
bb7891c196 | ||
|
|
1bbc56c3b9 | ||
|
|
1b95468783 |
||
|
|
c9d361d93b |
||
|
|
d0ad528135 |
||
|
|
4c5d9f3a25 |
||
|
|
fb46c28ff2 |
||
|
|
e34c196532 |
||
|
|
5848289756 |
||
|
|
ff5ea7cd40 |
||
|
|
cea5dd665e |
||
|
|
d05250923b |
||
|
|
5ca3522dc0 |
||
|
|
d6fbe97e14 |
||
|
|
0265c4c4a1 |
||
|
|
b6873768b2 |
||
|
|
60af36813b |
||
|
|
3f7533b86a |
||
|
|
e275e8a142 |
||
|
|
6f9729f245 |
||
|
|
f7273ce97e |
||
|
|
392b22219f |
||
|
|
fb97b9f539 |
||
|
|
c5a8d672d2 |
||
|
|
e5d2d26636 |
||
|
|
09e65c8d9f |
||
|
|
9e2230cc33 |
||
|
|
a709b84eef |
||
|
|
1c48e51384 |
||
|
|
5efd232809 |
||
|
|
683d00caec |
||
|
|
c5e76f303a |
||
|
|
513cbe3a77 |
||
|
|
11d8fffd1b |
||
|
|
e1472e9f97 |
||
|
|
0da8adb7ac |
||
|
|
1cf0ed5b7e |
||
|
|
a3a3d2e867 |
||
|
|
17bbe3d297 |
||
|
|
aea036715c |
||
|
|
f41f00fec2 |
||
|
|
01fbf3480f |
||
|
|
5954d4ae20 |
||
|
|
3cae004e6b |
||
|
|
06cd07d67a |
||
|
|
d95d773570 |
||
|
|
cf573fc743 |
||
|
|
2255c5f000 |
||
|
|
6162da7636 |
||
|
|
37d4d1c77f |
||
|
|
38689a71a7 |
||
|
|
a71e53c8c8 |
||
|
|
e590deebe0 |
||
|
|
5a088daf97 |
||
|
|
d4ea6adcc3 |
||
|
|
39ebfab693 |
||
|
|
dfde4d45e2 |
||
|
|
312902b5f5 |
||
|
|
67002ae24d |
||
|
|
51a9cf79bc |
||
|
|
e3b513ccdb |
||
|
|
1e873ff86c |
||
|
|
f096675a2b |
||
|
|
66ec11a19f |
||
|
|
15ac11b575 |
||
|
|
eeee02875a |
||
|
|
e409dc3100 |
||
|
|
40c14607f6 |
||
|
|
c61571ea07 |
||
|
|
cf961afe59 |
||
|
|
2a1374d376 |
||
|
|
dbe31034ce |
||
|
|
aadd5d7d28 |
||
|
|
75c45190ae |
||
|
|
002e819e54 |
||
|
|
38a6b5632a |
||
|
|
5a01ce66f1 |
||
|
|
83cfcde9cb |
||
|
|
152448d116 |
||
|
|
51e487370a |
||
|
|
7c6c81a841 |
||
|
|
0e0cfd62bb |
||
|
|
4dc4ccad37 |
||
|
|
32b7fa2670 |
||
|
|
0013e35b28 |
||
|
|
9fcd12da2e |
||
|
|
b77be826e6 |
||
|
|
519d201e74 |
||
|
|
a3f7405a08 |
||
|
|
1393cf5956 |
||
|
|
69ef36b4d9 |
||
|
|
70f8b4d495 |
||
|
|
48258f6e67 |
||
|
|
83ee977746 |
||
|
|
b686da5e56 |
||
|
|
61aef41bee |
||
|
|
6ab0f22d2d |
||
|
|
c4e80cd5b2 |
||
|
|
ef2f3e51ea |
||
|
|
24215dc734 |
||
|
|
e2b21ad946 |
||
|
|
969faddeee |
||
|
|
e8c5f8ed81 |
||
|
|
04fa242f57 |
||
|
|
de3c987802 |
||
|
|
f2123b4fb0 |
||
|
|
a759789454 |
||
|
|
da68cf3e9d |
||
|
|
5febcdca43 |
||
|
|
b2e9935049 |
||
|
|
1f4cb7077a |
||
|
|
fbf8b1285d |
||
|
|
bf0961458c |
||
|
|
a4a33d4407 |
||
|
|
ff13be4616 |
||
|
|
37f8fb3a0e |
||
|
|
484bda7940 |
||
|
|
deea9ff038 |
||
|
|
91340bbe2f |
||
|
|
e689d52dca |
||
|
|
22f80b97f0 |
||
|
|
dee3f3f87a |
||
|
|
d2c5a6a914 |
||
|
|
1a7f346b51 |
||
|
|
bb579e36db |
||
|
|
843b8c38d3 |
||
|
|
70fc00d7eb |
||
|
|
9f873d1059 |
||
|
|
b0061f570e |
||
|
|
bfe6c58133 |
||
|
|
8c5f92aeb1 |
||
|
|
ec90b61bb4 |
||
|
|
6a72552754 |
||
|
|
1ce408e673 |
||
|
|
d3db80dc32 |
||
|
|
c56be285a5 |
||
|
|
599ee5a58f |
||
|
|
7703f57122 |
||
|
|
b8a4ea50bd |
||
|
|
49f2555914 |
||
|
|
e21c989038 |
||
|
|
f8bdb84e8d |
||
|
|
e161015c67 |
||
|
|
cbd7fc917e |
||
|
|
6a7c8df1ef |
||
|
|
d3e76898cd |
||
|
|
0f9314f900 |
||
|
|
502e3658e0 |
||
|
|
0e77ba9546 |
||
|
|
10b2e5671b |
||
|
|
ebc085da77 |
||
|
|
4a414f0fa4 |
||
|
|
7a12db6cdb |
||
|
|
f30a9a2095 |
||
|
|
ed5ff9c5cc |
||
|
|
59833fba0d |
||
|
|
a79cb30cdc |
||
|
|
e1cd69d5ff |
||
|
|
85333087fa |
||
|
|
5ddac4b3b4 |
||
|
|
c37b7f0493 |
||
|
|
5896c1b7a5 |
||
|
|
0f073a40fd |
||
|
|
618723c457 |
||
|
|
4cb6acefb2 |
||
|
|
f22ec2275f |
||
|
|
7bffed712a |
||
|
|
f30d6ad82a |
||
|
|
b524da11e9 |
||
|
|
3dd412f6e3 |
||
|
|
ef98ee7d11 |
||
|
|
30fb1d6240 |
||
|
|
7aac64531f |
||
|
|
03724d5eb1 |
||
|
|
4eb4ff66ce |
||
|
|
0bff3e1a67 |
||
|
|
82b437c502 |
||
|
|
88b1850b58 |
||
|
|
60558de728 |
||
|
|
beff4432dc |
||
|
|
9ae0bc4ec4 |
||
|
|
21bd8c5660 |
||
|
|
97bb004c12 |
||
|
|
e4e31ec4fb |
||
|
|
259986ed1d |
||
|
|
0c75d234b9 |
||
|
|
ae1487d733 |
||
|
|
c69fbe6bf9 |
||
|
|
8d697bcc94 |
||
|
|
7e7005f5b3 |
||
|
|
12a210e1f6 |
||
|
|
169d8f6223 |
||
|
|
cd3147c654 |
||
|
|
7feeec6941 |
||
|
|
12d888f49d |
||
|
|
ca41b59fc4 |
||
|
|
77b2f8dfb3 |
||
|
|
d8691d1e1a |
||
|
|
5cb1b9c1e9 |
||
|
|
b23e67ae6a |
||
|
|
8e7086ab39 |
||
|
|
dc907c0ba3 |
||
|
|
eba4c93efd |
||
|
|
bdd6de10a5 |
||
|
|
66e1e7ac2b |
||
|
|
4103344989 |
||
|
|
0c470b9202 |
||
|
|
57309dcd5f |
||
|
|
72ba54b5be |
||
|
|
18bf0c6121 |
||
|
|
f88ce014df |
||
|
|
3b2f709aeb |
||
|
|
6626c8846b |
||
|
|
2ecd20d444 |
||
|
|
46e64706ea |
||
|
|
bdc5493593 |
||
|
|
424999dacd |
||
|
|
2ec6aecc5d |
||
|
|
addee12510 |
||
|
|
57c0ca90e5 |
||
|
|
85c65dcad3 |
||
|
|
a34d9bc916 |
||
|
|
27e98b85ce |
||
|
|
ae5ecbe909 |
||
|
|
126cb1ee0d |
||
|
|
eeef23139d |
||
|
|
433d45ed87 |
||
|
|
5f67fcdce5 |
||
|
|
9288010636 |
||
|
|
5162c5de87 |
||
|
|
c2aed5ee92 |
||
|
|
44ebf2f48d |
||
|
|
6896d2bfb1 |
||
|
|
14cabda5c2 |
||
|
|
8cf0491b65 |
||
|
|
1f46df0d60 |
||
|
|
1b928ef6b2 |
||
|
|
db35a55a3d |
||
|
|
fd6126134e |
||
|
|
3b5fba2eec |
||
|
|
eb5ffb940e |
||
|
|
bb422ad5b9 |
||
|
|
53c3905ce3 |
||
|
|
dc42680e1c |
||
|
|
56ef9355da |
||
|
|
d8e4978b61 |
||
|
|
b9b370fbb8 |
||
|
|
dfeca3a972 |
||
|
|
a2934deaa6 |
||
|
|
2fbf608895 |
||
|
|
d783ffc13f |
||
|
|
62426d25da |
||
|
|
fa710b36c2 |
||
|
|
321c3f00d2 |
||
|
|
ec4bf3d76a |
||
|
|
68e62d3d9b |
||
|
|
954c36c0a2 |
||
|
|
81433e00d1 |
||
|
|
a5c5e85144 |
||
|
|
b94451f731 |
||
|
|
4edecc5c77 |
||
|
|
51e9a689a6 |
||
|
|
aa920432f3 |
||
|
|
ce189e5065 |
||
|
|
00155eaaf6 |
||
|
|
d94f80c8da |
||
|
|
bd5eb03d9c |
||
|
|
6ba1198c47 |
||
|
|
b5c821795a |
||
|
|
b2926377b7 |
||
|
|
99f47ca4e7 |
||
|
|
fef388d8cb |
||
|
|
92849ca473 |
||
|
|
0952887157 |
||
|
|
d010b26e1c |
||
|
|
58de410850 |
||
|
|
54bc3ea87d |
||
|
|
64a2f7aa4f |
||
|
|
55be9f0b9c |
||
|
|
97ffa0394f |
||
|
|
dc91ec2056 |
||
|
|
356795f8b0 |
||
|
|
3efcd94e14 |
||
|
|
34bc21b3b7 |
||
|
|
37845c2936 |
||
|
|
47924716c1 |
||
|
|
1d60505629 |
||
|
|
9daf0ba767 |
||
|
|
bdae378569 |
||
|
|
363770ab84 |
||
|
|
8bc08b25dc |
||
|
|
e0c1b974c9 |
||
|
|
39cf9f6943 |
||
|
|
d650defa08 |
||
|
|
c5c42f072b |
||
|
|
bd5b32101f |
||
|
|
8208ac817d |
||
|
|
a99c4879de |
||
|
|
01b666a78f |
||
|
|
8294952474 |
||
|
|
7fb5b1b996 |
||
|
|
2749a98f26 |
||
|
|
08526da153 |
||
|
|
8269adf176 |
||
|
|
0cddcba5a7 |
||
|
|
3bd1eeacc1 |
||
|
|
1698ec2eb3 |
||
|
|
07710ad98d |
||
|
|
f63bf7093c |
||
|
|
0597bf1047 |
||
|
|
5bde4b92a2 |
||
|
|
faa994e3b3 |
||
|
|
68cc1a8e2c |
||
|
|
9c775e2213 |
||
|
|
6c94173ca1 |
||
|
|
d1e0560d28 |
||
|
|
52a94b2593 |
||
|
|
9550fd2921 |
||
|
|
a6549b08f9 |
||
|
|
ba3e2ecb5f |
||
|
|
2bd3b46e3f |
||
|
|
7831ddaede |
||
|
|
613f2f1c24 |
||
|
|
525f33a07a |
||
|
|
3f2604d33f |
||
|
|
b823bb04d2 |
||
|
|
9ba92d9495 |
||
|
|
0127fc188b |
||
|
|
3c7a651d27 |
||
|
|
50a3c0d911 |
||
|
|
b2bea85add |
||
|
|
61bc0065f9 |
310 changed files with 21673 additions and 9758 deletions
12
.cirrus.yml
12
.cirrus.yml
|
|
@ -2,17 +2,17 @@ freebsd_task:
|
|||
name: FreeBSD
|
||||
|
||||
matrix:
|
||||
- name: FreeBSD 14.0
|
||||
- name: FreeBSD 14.3
|
||||
freebsd_instance:
|
||||
image_family: freebsd-14-0
|
||||
image_family: freebsd-14-3
|
||||
|
||||
pkginstall_script:
|
||||
- pkg update -f
|
||||
- pkg install -y go122
|
||||
- pkg install -y go125
|
||||
- pkg install -y git
|
||||
|
||||
setup_script:
|
||||
- ln -s /usr/local/bin/go122 /usr/local/bin/go
|
||||
- ln -s /usr/local/bin/go125 /usr/local/bin/go
|
||||
- pw groupadd sftpgo
|
||||
- pw useradd sftpgo -g sftpgo -w none -m
|
||||
- mkdir /home/sftpgo/sftpgo
|
||||
|
|
@ -20,7 +20,7 @@ freebsd_task:
|
|||
- 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 && go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -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'
|
||||
|
||||
|
|
@ -28,4 +28,4 @@ freebsd_task:
|
|||
- 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'
|
||||
- su sftpgo -c 'cd ~/sftpgo && go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 20m ./... -coverprofile=coverage.txt -covermode=atomic'
|
||||
|
|
|
|||
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -7,8 +7,10 @@ body:
|
|||
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).
|
||||
Before asking for help please check our [support policy](https://github.com/drakkan/sftpgo?tab=readme-ov-file#support).
|
||||
If you are a [commercial user](https://sftpgo.com/) please contact us using the dedicated [email address](mailto:support@sftpgo.com).
|
||||
If you'd like to contribute code, please make sure to read and understand our [Contributor License Agreement (CLA)](https://sftpgo.com/cla.html).
|
||||
You’ll be asked to accept it when submitting a pull request.
|
||||
- type: checkboxes
|
||||
id: before-posting
|
||||
attributes:
|
||||
|
|
|
|||
8
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
8
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
|
|
@ -2,6 +2,14 @@ name: 🚀 Feature request
|
|||
description: Suggest an idea for SFTPGo
|
||||
labels: ["suggestion"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### 👍 Thank you for contributing to our project!
|
||||
Before asking for help please check our [support policy](https://github.com/drakkan/sftpgo?tab=readme-ov-file#support).
|
||||
If you are a [commercial user](https://sftpgo.com/) please contact us using the dedicated [email address](mailto:support@sftpgo.com).
|
||||
If you'd like to contribute code, please make sure to read and understand our [Contributor License Agreement (CLA)](https://sftpgo.com/cla.html).
|
||||
You’ll be asked to accept it when submitting a pull request.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
|
|
|
|||
10
.github/dependabot.yml
vendored
10
.github/dependabot.yml
vendored
|
|
@ -1,11 +1,11 @@
|
|||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 2
|
||||
#- package-ecosystem: "gomod"
|
||||
# directory: "/"
|
||||
# schedule:
|
||||
# interval: "weekly"
|
||||
# open-pull-requests-limit: 2
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
|
|
|
|||
12
.github/workflows/codeql.yml
vendored
12
.github/workflows/codeql.yml
vendored
|
|
@ -15,22 +15,22 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.22'
|
||||
go-version: '1.25'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: go
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
252
.github/workflows/development.yml
vendored
252
.github/workflows/development.yml
vendored
|
|
@ -5,34 +5,32 @@ on:
|
|||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test-deploy:
|
||||
name: Test and deploy
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go: ['1.22']
|
||||
go: ['1.26']
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
upload-coverage: [true]
|
||||
include:
|
||||
- go: '1.22'
|
||||
os: windows-latest
|
||||
upload-coverage: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
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
|
||||
go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -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 -
|
||||
|
|
@ -44,68 +42,35 @@ jobs:
|
|||
|
||||
- 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
|
||||
run: CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 SDKROOT=$(xcrun --sdk macosx --show-sdk-path) go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -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: Run test cases using SQLite provider
|
||||
run: go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -coverprofile=coverage.txt -covermode=atomic
|
||||
run: go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 15m ./... -coverprofile=coverage.txt -covermode=atomic
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
if: ${{ matrix.upload-coverage }}
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
file: ./coverage.txt
|
||||
files: ./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
|
||||
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 2m ./internal/config -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/common -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/httpd -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 8m ./internal/sftpd -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/ftpd -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/webdavd -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 2m ./internal/telemetry -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 2m ./internal/mfa -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -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
|
||||
run: go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 15m ./... -covermode=atomic
|
||||
env:
|
||||
SFTPGO_DATA_PROVIDER__DRIVER: memory
|
||||
SFTPGO_DATA_PROVIDER__NAME: ''
|
||||
|
|
@ -126,15 +91,100 @@ jobs:
|
|||
./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' }}
|
||||
- name: Upload build artifact
|
||||
if: startsWith(matrix.os, 'ubuntu-') != true
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sftpgo-${{ matrix.os }}-go-${{ matrix.go }}
|
||||
path: output
|
||||
|
||||
test-deploy-windows:
|
||||
name: Test and deploy Windows
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.26'
|
||||
|
||||
- name: Run test cases using SQLite provider
|
||||
run: |
|
||||
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 ../..
|
||||
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 15m ./... -coverprofile=coverage.txt -covermode=atomic
|
||||
|
||||
- name: Run test cases using bolt provider
|
||||
run: |
|
||||
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 2m ./internal/config -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/common -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/httpd -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 8m ./internal/sftpd -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/ftpd -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/webdavd -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 2m ./internal/telemetry -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 2m ./internal/mfa -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -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,disable_grpc_modules -p 1 -timeout 15m ./... -covermode=atomic
|
||||
env:
|
||||
SFTPGO_DATA_PROVIDER__DRIVER: memory
|
||||
SFTPGO_DATA_PROVIDER__NAME: ''
|
||||
|
||||
- name: Build
|
||||
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 "2019-2025 Nicola Murino" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
|
||||
go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -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 $LATEST_TAG-dev-$GIT_COMMIT --file-version "$FILE_VERSION" --file-description "SFTPGo server" --product-name SFTPGo --copyright "2019-2025 Nicola Murino" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
|
||||
go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules,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 "2019-2025 Nicola Murino" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
|
||||
go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules,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: Initialize data provider
|
||||
run: |
|
||||
rm sftpgo.db
|
||||
./sftpgo initprovider
|
||||
shell: bash
|
||||
|
||||
- name: Prepare Windows installers
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
run: |
|
||||
choco install innosetup
|
||||
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
|
||||
copy .\NOTICE .\output\NOTICE.txt
|
||||
mkdir output\templates
|
||||
xcopy .\templates .\output\templates\ /E
|
||||
mkdir output\static
|
||||
|
|
@ -145,15 +195,7 @@ jobs:
|
|||
$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
|
||||
iscc .\windows-installer\sftpgo.iss
|
||||
|
||||
rm .\output\sftpgo.exe
|
||||
rm .\output\sftpgo.db
|
||||
|
|
@ -165,40 +207,35 @@ jobs:
|
|||
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
|
||||
iscc .\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 }}
|
||||
iscc .\windows-installer\sftpgo.iss
|
||||
|
||||
- name: Upload Windows installer x86_64 artifact
|
||||
if: ${{ startsWith(matrix.os, 'windows-') && github.event_name != 'pull_request' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
uses: actions/upload-artifact@v7
|
||||
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
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
uses: actions/upload-artifact@v7
|
||||
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
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
uses: actions/upload-artifact@v7
|
||||
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
|
||||
|
|
@ -217,10 +254,9 @@ jobs:
|
|||
xcopy .\openapi .\output\openapi\ /E
|
||||
|
||||
- name: Upload build artifact
|
||||
if: startsWith(matrix.os, 'ubuntu-') != true
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sftpgo-${{ matrix.os }}-go-${{ matrix.go }}
|
||||
name: sftpgo-windows-portable
|
||||
path: output
|
||||
|
||||
test-build-flags:
|
||||
|
|
@ -228,19 +264,19 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.22'
|
||||
go-version: '1.26'
|
||||
|
||||
- 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
|
||||
go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules,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
|
||||
go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules,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:
|
||||
|
|
@ -292,16 +328,16 @@ jobs:
|
|||
- 3308:3306
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.22'
|
||||
go-version: '1.26'
|
||||
|
||||
- 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
|
||||
go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -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 -
|
||||
|
|
@ -313,7 +349,7 @@ jobs:
|
|||
run: |
|
||||
./sftpgo initprovider
|
||||
./sftpgo resetprovider --force
|
||||
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 15m ./... -covermode=atomic
|
||||
env:
|
||||
SFTPGO_DATA_PROVIDER__DRIVER: mysql
|
||||
SFTPGO_DATA_PROVIDER__NAME: sftpgo
|
||||
|
|
@ -326,7 +362,7 @@ jobs:
|
|||
run: |
|
||||
./sftpgo initprovider
|
||||
./sftpgo resetprovider --force
|
||||
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 15m ./... -covermode=atomic
|
||||
env:
|
||||
SFTPGO_DATA_PROVIDER__DRIVER: postgresql
|
||||
SFTPGO_DATA_PROVIDER__NAME: sftpgo
|
||||
|
|
@ -339,7 +375,7 @@ jobs:
|
|||
run: |
|
||||
./sftpgo initprovider
|
||||
./sftpgo resetprovider --force
|
||||
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -covermode=atomic
|
||||
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 15m ./... -covermode=atomic
|
||||
env:
|
||||
SFTPGO_DATA_PROVIDER__DRIVER: mysql
|
||||
SFTPGO_DATA_PROVIDER__NAME: sftpgo
|
||||
|
|
@ -356,7 +392,7 @@ jobs:
|
|||
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
|
||||
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 15m ./... -covermode=atomic
|
||||
docker stop crdb
|
||||
env:
|
||||
SFTPGO_DATA_PROVIDER__DRIVER: cockroachdb
|
||||
|
|
@ -391,7 +427,7 @@ jobs:
|
|||
go: latest
|
||||
go-arch: arm7
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
|
@ -420,7 +456,7 @@ jobs:
|
|||
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
|
||||
echo 'go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -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
|
||||
|
|
@ -436,7 +472,7 @@ jobs:
|
|||
gzip output/man/man1/*
|
||||
cp sftpgo output/
|
||||
|
||||
- uses: uraimo/run-on-arch-action@v2
|
||||
- uses: uraimo/run-on-arch-action@v3
|
||||
if: ${{ matrix.arch != 'amd64' }}
|
||||
name: Build for ${{ matrix.arch }}
|
||||
id: build
|
||||
|
|
@ -471,7 +507,7 @@ jobs:
|
|||
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
|
||||
go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -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/
|
||||
|
|
@ -485,7 +521,7 @@ jobs:
|
|||
cp sftpgo output/
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sftpgo-linux-${{ matrix.arch }}-go-${{ matrix.go }}
|
||||
path: output
|
||||
|
|
@ -500,13 +536,13 @@ jobs:
|
|||
echo "pkg-version=${PKG_VERSION}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload Debian Package
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
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
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-${{ matrix.go-arch }}-rpm
|
||||
path: pkgs/dist/rpm/*
|
||||
|
|
@ -516,11 +552,11 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.22'
|
||||
- uses: actions/checkout@v4
|
||||
go-version: '1.26'
|
||||
- uses: actions/checkout@v6
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: latest
|
||||
|
|
|
|||
14
.github/workflows/docker.yml
vendored
14
.github/workflows/docker.yml
vendored
|
|
@ -33,7 +33,7 @@ jobs:
|
|||
optional_deps: true
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Gather image information
|
||||
id: info
|
||||
|
|
@ -42,7 +42,7 @@ jobs:
|
|||
DOCKERFILE=Dockerfile
|
||||
MINOR=""
|
||||
MAJOR=""
|
||||
FEATURES="nopgxregisterdefaulttypes"
|
||||
FEATURES="nopgxregisterdefaulttypes,disable_grpc_modules"
|
||||
if [ "${{ github.event_name }}" = "schedule" ]; then
|
||||
VERSION=nightly
|
||||
elif [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
|
|
@ -141,21 +141,21 @@ jobs:
|
|||
OPTIONAL_DEPS: ${{ matrix.optional_deps }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up builder
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
id: builder
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
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
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
|
@ -163,7 +163,7 @@ jobs:
|
|||
if: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
builder: ${{ steps.builder.outputs.name }}
|
||||
|
|
|
|||
365
.github/workflows/release.yml
vendored
365
.github/workflows/release.yml
vendored
|
|
@ -4,17 +4,21 @@ on:
|
|||
push:
|
||||
tags: 'v*'
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
|
||||
env:
|
||||
GO_VERSION: 1.22.3
|
||||
GO_VERSION: 1.25.8
|
||||
|
||||
jobs:
|
||||
prepare-sources-with-deps:
|
||||
name: Prepare sources with deps
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
|
|
@ -32,23 +36,20 @@ jobs:
|
|||
SFTPGO_VERSION: ${{ steps.get_version.outputs.VERSION }}
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
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]
|
||||
prepare-windows:
|
||||
name: Prepare Windows binaries
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
|
|
@ -57,46 +58,24 @@ jobs:
|
|||
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-')
|
||||
- name: Build
|
||||
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
|
||||
go-winres simply --arch amd64 --product-version $Env:SFTPGO_VERSION-$GIT_COMMIT --file-version "$FILE_VERSION" --file-description "SFTPGo server" --product-name SFTPGo --copyright "2019-2025 Nicola Murino" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
|
||||
go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -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
|
||||
go-winres simply --arch arm64 --product-version $Env:SFTPGO_VERSION-$GIT_COMMIT --file-version "$FILE_VERSION" --file-description "SFTPGo server" --product-name SFTPGo --copyright "2019-2025 Nicola Murino" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
|
||||
go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules,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
|
||||
go-winres simply --arch 386 --product-version $Env:SFTPGO_VERSION-$GIT_COMMIT --file-version "$FILE_VERSION" --file-description "SFTPGo server" --product-name SFTPGo --copyright "2019-2025 Nicola Murino" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
|
||||
go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules,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
|
||||
|
|
@ -107,14 +86,123 @@ jobs:
|
|||
run: ./sftpgo initprovider
|
||||
shell: bash
|
||||
|
||||
- name: Prepare Release for macOS
|
||||
if: startsWith(matrix.os, 'macos-')
|
||||
- name: Prepare Release
|
||||
run: |
|
||||
mkdir output
|
||||
copy .\sftpgo.exe .\output
|
||||
copy .\sftpgo.json .\output
|
||||
copy .\sftpgo.db .\output
|
||||
copy .\LICENSE .\output\LICENSE.txt
|
||||
copy .\NOTICE .\output\NOTICE.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
|
||||
iscc .\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 .\windows-installer\sftpgo.iss
|
||||
|
||||
rm .\output\sftpgo.exe
|
||||
copy .\x86\sftpgo.exe .\output
|
||||
$Env:SFTPGO_ISS_ARCH='x86'
|
||||
iscc .\windows-installer\sftpgo.iss
|
||||
env:
|
||||
SFTPGO_ISS_VERSION: ${{ steps.get_version.outputs.VERSION }}
|
||||
|
||||
- name: Prepare Portable Release
|
||||
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
|
||||
copy .\NOTICE .\win-portable\NOTICE.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 Windows installer x86_64 artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_windows_x86_64.exe
|
||||
path: ./sftpgo_windows_x86_64.exe
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload Windows installer arm64 artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_windows_arm64.exe
|
||||
path: ./sftpgo_windows_arm64.exe
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload Windows installer x86 artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_windows_x86.exe
|
||||
path: ./sftpgo_windows_x86.exe
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload Windows portable artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_windows_portable.zip
|
||||
path: ./sftpgo_portable.zip
|
||||
retention-days: 1
|
||||
|
||||
prepare-mac:
|
||||
name: Prepare macOS binaries
|
||||
runs-on: macos-14
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
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: Build for macOS x86_64
|
||||
run: go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -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
|
||||
run: CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 SDKROOT=$(xcrun --sdk macosx --show-sdk-path) go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -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: Initialize data provider
|
||||
run: ./sftpgo initprovider
|
||||
shell: bash
|
||||
|
||||
- name: Prepare Release
|
||||
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
|
||||
echo "https://docs.sftpgo.com" >> output/README.txt
|
||||
cp LICENSE output/
|
||||
cp NOTICE output/
|
||||
cp sftpgo output/
|
||||
cp sftpgo.json output/
|
||||
cp sftpgo.db output/sqlite/
|
||||
|
|
@ -127,130 +215,27 @@ jobs:
|
|||
./sftpgo gen man -d output/man/man1
|
||||
gzip output/man/man1/*
|
||||
cd output
|
||||
tar cJvf ../sftpgo_${SFTPGO_VERSION}_${OS}_x86_64.tar.xz *
|
||||
tar cJvf ../sftpgo_${SFTPGO_VERSION}_macOS_x86_64.tar.xz *
|
||||
cd ..
|
||||
cp sftpgo_arm64 output/sftpgo
|
||||
cd output
|
||||
tar cJvf ../sftpgo_${SFTPGO_VERSION}_${OS}_arm64.tar.xz *
|
||||
tar cJvf ../sftpgo_${SFTPGO_VERSION}_macOS_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
|
||||
uses: actions/upload-artifact@v7
|
||||
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
|
||||
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_macOS_x86_64.tar.xz
|
||||
path: ./sftpgo_${{ steps.get_version.outputs.VERSION }}_macOS_x86_64.tar.xz
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload macOS arm64 artifact
|
||||
if: startsWith(matrix.os, 'macos-')
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
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
|
||||
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_macOS_arm64.tar.xz
|
||||
path: ./sftpgo_${{ steps.get_version.outputs.VERSION }}_macOS_arm64.tar.xz
|
||||
retention-days: 1
|
||||
|
||||
prepare-linux:
|
||||
|
|
@ -285,7 +270,7 @@ jobs:
|
|||
tar-arch: armv7
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Get versions
|
||||
id: get_version
|
||||
|
|
@ -310,7 +295,7 @@ jobs:
|
|||
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
|
||||
echo 'go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -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
|
||||
|
|
@ -319,6 +304,7 @@ jobs:
|
|||
echo "" >> output/README.txt
|
||||
echo "https://github.com/drakkan/sftpgo/blob/${SFTPGO_VERSION}/README.md" >> output/README.txt
|
||||
cp LICENSE output/
|
||||
cp NOTICE output/
|
||||
cp sftpgo.json output/
|
||||
cp -r templates output/
|
||||
cp -r static output/
|
||||
|
|
@ -337,7 +323,7 @@ jobs:
|
|||
env:
|
||||
SFTPGO_VERSION: ${{ steps.get_version.outputs.SFTPGO_VERSION }}
|
||||
|
||||
- uses: uraimo/run-on-arch-action@v2
|
||||
- uses: uraimo/run-on-arch-action@v3
|
||||
if: ${{ matrix.arch != 'amd64' }}
|
||||
name: Build for ${{ matrix.arch }}
|
||||
id: build
|
||||
|
|
@ -362,12 +348,13 @@ jobs:
|
|||
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
|
||||
go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -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 NOTICE output/
|
||||
cp sftpgo.json output/
|
||||
cp -r templates output/
|
||||
cp -r static output/
|
||||
|
|
@ -385,7 +372,7 @@ jobs:
|
|||
cd ..
|
||||
|
||||
- name: Upload build artifact for ${{ matrix.arch }}
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
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
|
||||
|
|
@ -403,14 +390,14 @@ jobs:
|
|||
SFTPGO_VERSION: ${{ steps.get_version.outputs.SFTPGO_VERSION }}
|
||||
|
||||
- name: Upload Deb Package
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
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
|
||||
uses: actions/upload-artifact@v7
|
||||
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
|
||||
|
|
@ -429,22 +416,22 @@ jobs:
|
|||
shell: bash
|
||||
|
||||
- name: Download amd64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_x86_64.tar.xz
|
||||
|
||||
- name: Download arm64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_arm64.tar.xz
|
||||
|
||||
- name: Download ppc64le artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_ppc64le.tar.xz
|
||||
|
||||
- name: Download armv7 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_armv7.tar.xz
|
||||
|
||||
|
|
@ -467,7 +454,7 @@ jobs:
|
|||
SFTPGO_VERSION: ${{ steps.get_version.outputs.SFTPGO_VERSION }}
|
||||
|
||||
- name: Upload Linux bundle
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
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
|
||||
|
|
@ -475,11 +462,11 @@ jobs:
|
|||
|
||||
create-release:
|
||||
name: Release
|
||||
needs: [prepare-linux-bundle, prepare-sources-with-deps, prepare-window-mac]
|
||||
needs: [prepare-linux-bundle, prepare-sources-with-deps, prepare-mac, prepare-windows]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Get versions
|
||||
id: get_version
|
||||
run: |
|
||||
|
|
@ -490,102 +477,102 @@ jobs:
|
|||
shell: bash
|
||||
|
||||
- name: Download amd64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_x86_64.tar.xz
|
||||
|
||||
- name: Download arm64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_arm64.tar.xz
|
||||
|
||||
- name: Download ppc64le artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_ppc64le.tar.xz
|
||||
|
||||
- name: Download armv7 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_armv7.tar.xz
|
||||
|
||||
- name: Download Linux bundle artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_bundle.tar.xz
|
||||
|
||||
- name: Download Deb amd64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.PKG_VERSION }}-1_amd64.deb
|
||||
|
||||
- name: Download Deb arm64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.PKG_VERSION }}-1_arm64.deb
|
||||
|
||||
- name: Download Deb ppc64le artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.PKG_VERSION }}-1_ppc64el.deb
|
||||
|
||||
- name: Download Deb armv7 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.PKG_VERSION }}-1_armhf.deb
|
||||
|
||||
- name: Download RPM x86_64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sftpgo-${{ steps.get_version.outputs.PKG_VERSION }}-1.x86_64.rpm
|
||||
|
||||
- name: Download RPM aarch64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sftpgo-${{ steps.get_version.outputs.PKG_VERSION }}-1.aarch64.rpm
|
||||
|
||||
- name: Download RPM ppc64le artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sftpgo-${{ steps.get_version.outputs.PKG_VERSION }}-1.ppc64le.rpm
|
||||
|
||||
- name: Download RPM armv7 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sftpgo-${{ steps.get_version.outputs.PKG_VERSION }}-1.armv7hl.rpm
|
||||
|
||||
- name: Download macOS x86_64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_macOS_x86_64.tar.xz
|
||||
|
||||
- name: Download macOS arm64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
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
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_windows_x86_64.exe
|
||||
|
||||
- name: Download Windows installer arm64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_windows_arm64.exe
|
||||
|
||||
- name: Download Windows installer x86 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_windows_x86.exe
|
||||
|
||||
- name: Download Windows portable artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_windows_portable.zip
|
||||
|
||||
- name: Download source with deps artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_src_with_deps.tar.xz
|
||||
|
||||
|
|
|
|||
100
.golangci.yml
100
.golangci.yml
|
|
@ -1,52 +1,66 @@
|
|||
version: "2"
|
||||
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
|
||||
- dogsled
|
||||
- dupl
|
||||
- goconst
|
||||
- gocyclo
|
||||
- misspell
|
||||
- whitespace
|
||||
- dupl
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- dogsled
|
||||
- govet
|
||||
- unconvert
|
||||
- unparam
|
||||
- whitespace
|
||||
settings:
|
||||
dupl:
|
||||
threshold: 150
|
||||
errcheck:
|
||||
check-type-assertions: false
|
||||
check-blank: false
|
||||
goconst:
|
||||
min-len: 3
|
||||
min-occurrences: 3
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
# https://golangci-lint.run/usage/linters/#revive
|
||||
revive:
|
||||
rules:
|
||||
- name: var-naming
|
||||
severity: warning
|
||||
disabled: true
|
||||
exclude: [""]
|
||||
arguments:
|
||||
- ["ID"] # AllowList
|
||||
- ["VM"] # DenyList
|
||||
- - upper-case-const: true
|
||||
- - skip-package-name-checks: true
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
settings:
|
||||
gofmt:
|
||||
simplify: true
|
||||
goimports:
|
||||
local-prefixes:
|
||||
- github.com/drakkan/sftpgo
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
|
|
|||
10
Dockerfile
10
Dockerfile
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1.22-bookworm as builder
|
||||
FROM golang:1.26-trixie AS builder
|
||||
|
||||
ENV GOFLAGS="-mod=readonly"
|
||||
|
||||
|
|
@ -10,7 +10,7 @@ WORKDIR /workspace
|
|||
ARG GOPROXY
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
RUN go mod download && go mod verify
|
||||
|
||||
ARG COMMIT_SHA
|
||||
|
||||
|
|
@ -30,14 +30,14 @@ 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
|
||||
FROM debian:trixie-slim
|
||||
|
||||
# Set to "true" to install jq and the optional git and rsync dependencies
|
||||
# Set to "true" to install jq
|
||||
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 if [ "${INSTALL_OPTIONAL_PACKAGES}" = "true" ]; then apt-get update && apt-get install --no-install-recommends -y jq && rm -rf /var/lib/apt/lists/*; fi
|
||||
|
||||
RUN mkdir -p /etc/sftpgo /var/lib/sftpgo /usr/share/sftpgo /srv/sftpgo/data /srv/sftpgo/backups
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1.22-alpine3.19 AS builder
|
||||
FROM golang:1.26-alpine3.23 AS builder
|
||||
|
||||
ENV GOFLAGS="-mod=readonly"
|
||||
|
||||
|
|
@ -10,7 +10,7 @@ WORKDIR /workspace
|
|||
ARG GOPROXY
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
RUN go mod download && go mod verify
|
||||
|
||||
ARG COMMIT_SHA
|
||||
|
||||
|
|
@ -25,14 +25,14 @@ 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
|
||||
FROM alpine:3.23
|
||||
|
||||
# Set to "true" to install jq and the optional git and rsync dependencies
|
||||
# Set to "true" to install jq
|
||||
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 if [ "${INSTALL_OPTIONAL_PACKAGES}" = "true" ]; then apk add --update --no-cache jq; fi
|
||||
|
||||
RUN mkdir -p /etc/sftpgo /var/lib/sftpgo /usr/share/sftpgo /srv/sftpgo/data /srv/sftpgo/backups
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1.22-bookworm as builder
|
||||
FROM golang:1.26-trixie AS builder
|
||||
|
||||
ENV CGO_ENABLED=0 GOFLAGS="-mod=readonly"
|
||||
|
||||
|
|
@ -10,7 +10,7 @@ WORKDIR /workspace
|
|||
ARG GOPROXY
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
RUN go mod download && go mod verify
|
||||
|
||||
ARG COMMIT_SHA
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ RUN sed -i 's|"users_base_dir": "",|"users_base_dir": "/srv/sftpgo/data",|' sftp
|
|||
|
||||
RUN mkdir /etc/sftpgo /var/lib/sftpgo /srv/sftpgo
|
||||
|
||||
FROM gcr.io/distroless/static-debian12
|
||||
FROM gcr.io/distroless/static-debian13
|
||||
|
||||
COPY --from=builder --chown=1000:1000 /etc/sftpgo /etc/sftpgo
|
||||
COPY --from=builder --chown=1000:1000 /srv/sftpgo /srv/sftpgo
|
||||
|
|
|
|||
12
NOTICE
Normal file
12
NOTICE
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
Additional terms under GNU AGPL version 3 section 7.3(b) and 13.1:
|
||||
|
||||
If you have included SFTPGo so that it is offered through any network
|
||||
interactions, including by means of an external user interface, or
|
||||
any other integration, even without modifying its source code and then
|
||||
SFTPGo is partially, fully or optionally configured via your frontend,
|
||||
you must provide reasonable but clear attribution to the SFTPGo project
|
||||
and its author(s), not imply any endorsement by or affiliation with the
|
||||
SFTPGo project, and you must prominently offer all users interacting
|
||||
with it remotely through a computer network an opportunity to receive
|
||||
the Corresponding Source of the SFTPGo version you include by providing
|
||||
a link to the Corresponding Source in the SFTPGo source code repository.
|
||||
98
README.md
98
README.md
|
|
@ -1,31 +1,51 @@
|
|||
# SFTPGo
|
||||
|
||||
[](https://github.com/drakkan/sftpgo/workflows/CI/badge.svg?branch=main&event=push)
|
||||
[](https://codecov.io/gh/drakkan/sftpgo/branch/main)
|
||||
[](https://github.com/drakkan/sftpgo/workflows/CI/badge.svg)
|
||||
[](https://www.gnu.org/licenses/agpl-3.0)
|
||||
[](https://github.com/avelino/awesome-go)
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
The WebAdmin UI allows to easily create and manage your users, folders, groups and other resources.
|
||||
## Project Status & Editions
|
||||
|
||||
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.
|
||||
SFTPGo is an open-source project with a sustainable business model. We offer two editions to suit different requirements, ensuring the project remains healthy and maintained for everyone.
|
||||
|
||||
### Open Source (Community)
|
||||
|
||||
Free, Copyleft (AGPLv3), Community Supported. The Community edition is a fully functional, production-ready solution widely adopted worldwide. It includes all the core protocols, storage backends, and the WebAdmin/WebClient UIs. It is ideal for:
|
||||
|
||||
- Standard file transfer needs.
|
||||
- Integrating storage backends (S3, GCS, Azure Blob) with legacy protocols.
|
||||
- Projects that are comfortable with AGPLv3 licensing.
|
||||
|
||||
### SFTPGo Enterprise
|
||||
|
||||
Commercial License, Professional Support, ISO 27001 Vendor. The Enterprise edition is built on the same core but extends it for mission-critical environments, compliance-heavy industries, and advanced workflows. It is a drop-in replacement (seamless upgrade).
|
||||
|
||||
| Feature | Open Source (Community) | Enterprise Edition |
|
||||
| :--- | :--- | :--- |
|
||||
| **License Type** | AGPLv3 (Copyleft) | **Commercial License**<br/>Proprietary/No Copyleft |
|
||||
| **Vendor Compliance** | Not Applicable<br/>Community Project | **Certified Vendor**<br/>ISO 27001 & Supply Chain Validation |
|
||||
| **Support** | Community (GitHub) | **Direct from Authors** |
|
||||
| **Cloud Storage Engine** | Standard | **High Performance & Scalable**<br/>In-memory streaming (no local temp files) and up to 70% faster |
|
||||
| **High Availability (HA)** | Standard<br/>Shared DB & Storage | **Advanced**<br/>Enhanced event handling and optimized instance coordination |
|
||||
| **Automation Logic** | Simple Placeholders | **Dynamic Logic & Virtual Folders**<br/>Conditions, loops, route data across storage backends |
|
||||
| **Data Lifecycle** | Delete / Retain | **Smart Archiving**<br/>Move data to external Cloud/SFTP storage via Virtual Folders |
|
||||
| **Email Data Ingestion** | - | **Native IMAP Integration**<br/>Auto-extract attachments from email to storage |
|
||||
| **Public Sharing** | Standard Links | **Advanced & Collaborative**<br/>Email Authentication & Group Delegation |
|
||||
| **Data Protection** | - | **Encryption & Scanning**<br/>Automated PGP, Antivirus & DLP via ICAP |
|
||||
| **Advanced Identity (SSO)** | Standard | **Extended Controls**<br/>Advanced Single Sign-On parameters |
|
||||
| **Document Editing** | - | **Included**<br/>View, edit, and co-author in browser |
|
||||
|
||||
**Note**: We are committed to keeping the Open Source edition powerful and maintained. The Enterprise edition helps fund the development of the entire SFTPGo ecosystem.
|
||||
|
||||
## Sponsors
|
||||
|
||||
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).
|
||||
If you rely on SFTPGo in your projects, consider becoming a [sponsor](https://github.com/sponsors/drakkan).
|
||||
|
||||
We also provide [professional services](https://sftpgo.com/#pricing) to support you in using SFTPGo to the fullest.
|
||||
|
||||
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?
|
||||
|
||||
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.
|
||||
|
||||
You should support the project for its ongoing maintenance, even if you don't have any questions or need new features. If SFTPGo is no longer maintained you will have troubles and your company will lose money: bugs and security vulnerabilities will no longer be fixed, new algorithms will not be added to support newer clients, and so on. You will be forced to switch to a similar proprietary product and pay for its license and the migration cost.
|
||||
Your sponsorship helps cover maintenance, security updates and ongoing development of the open-source edition.
|
||||
|
||||
### Thank you to our sponsors
|
||||
|
||||
|
|
@ -45,23 +65,39 @@ You should support the project for its ongoing maintenance, even if you don't ha
|
|||
|
||||
[<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/)
|
||||
|
||||
## Support policy
|
||||
|
||||
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.
|
||||
|
||||
Use [discussions](https://github.com/drakkan/sftpgo/discussions) to ask questions and get support from the community.
|
||||
|
||||
If you report an invalid issue and/or ask for step-by-step support, your issue will be closed as invalid without further explanation. Invalid bug reports left open may confuse other users. Thanks for understanding.
|
||||
[<img src="./img/servinga.png" alt="servinga logo" width="258" height="56">](https://servinga.com/)
|
||||
</br></br>
|
||||
[<img src="./img/reui.png" alt="ReUI logo" width="151" height="56">](https://www.reui.io/)
|
||||
|
||||
## Documentation
|
||||
|
||||
You can read more about supported features and documentation at [sftpgo.github.io](https://sftpgo.github.io/).
|
||||
You can explore all supported features and configuration options at [docs.sftpgo.com](https://docs.sftpgo.com/latest/).
|
||||
|
||||
**Note:** The link above refers to the **Community Edition**.
|
||||
For details on **Enterprise Edition**, please refer to the [Enterprise Documentation](https://docs.sftpgo.com/enterprise/).
|
||||
|
||||
## Support
|
||||
|
||||
- **Community Support**: use [GitHub Discussions](https://github.com/drakkan/sftpgo/discussions) to ask questions, share feedback, and engage with other users.
|
||||
- **Commercial Support**: If you require guaranteed SLAs, expert guidance, or the advanced features listed above, check out [SFTPGo Enterprise](https://sftpgo.com).
|
||||
|
||||
SFTPGo Enterprise is available as:
|
||||
|
||||
- On-premises: Full control on your infrastructure. More details: [sftpgo.com/on-premises](https://sftpgo.com/on-premises)
|
||||
- Fully managed SaaS: We handle the infrastructure. More details: [sftpgo.com/saas](https://sftpgo.com/saas)
|
||||
|
||||
## Internationalization
|
||||
|
||||
The translations are available via [Crowdin](https://crowdin.com/project/sftpgo), who have granted us an open source license.
|
||||
|
||||
Before translating please take a look at our contribution [guidelines](https://docs.sftpgo.com/latest/web-interfaces/#internationalization).
|
||||
|
||||
## Release Cadence
|
||||
|
||||
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 releases per year.
|
||||
SFTPGo follows a feature-driven release cycle.
|
||||
|
||||
- Enterprise Edition: Receives major new features first and follows a faster [release cadence](https://docs.sftpgo.com/enterprise/changelog/).
|
||||
- Community Edition: Remains maintained, receiving bug fixes, security updates, and updates to core features.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
|
|
@ -71,7 +107,7 @@ We are very grateful to all the people who contributed with ideas and/or pull re
|
|||
|
||||
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 [KeenThemes](https://keenthemes.com/) for granting us a custom license to use their amazing [themes](https://keenthemes.com/bootstrap-templates) for the SFTPGo WebAdmin and WebClient user interfaces, across both the Open Source and Open Core versions.
|
||||
|
||||
Thank you to [Crowdin](https://crowdin.com/) for granting us an Open Source License.
|
||||
|
||||
|
|
@ -79,15 +115,17 @@ Thank you to [Incode](https://www.incode.it/) for helping us to improve the UI/U
|
|||
|
||||
## License
|
||||
|
||||
SFTPGo source code is licensed under the GNU AGPL-3.0-only.
|
||||
SFTPGo source code is licensed under the GNU AGPL-3.0-only with [additional terms](./NOTICE).
|
||||
|
||||
The [theme](https://keenthemes.com/products/templates-mega-bundle) used in WebAdmin and WebClient user interfaces is proprietary, this means:
|
||||
The [theme](https://keenthemes.com/bootstrap-templates) 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).
|
||||
|
||||
**Note:** We do not provide legal advice. If you have questions about license compliance or whether your use case is permitted under the license terms, please consult your legal team.
|
||||
|
||||
## Copyright
|
||||
|
||||
Copyright (C) 2019 Nicola Murino
|
||||
Copyright (C) 2019 - 2026 Nicola Murino
|
||||
|
|
|
|||
14
SECURITY.md
14
SECURITY.md
|
|
@ -2,8 +2,18 @@
|
|||
|
||||
## 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.
|
||||
We actively maintain the latest stable release of SFTPGo. While we strive to keep the Open Source version secure and up-to-date, maintenance is performed on a best-effort basis by the community and contributors.
|
||||
|
||||
## Scope and Dependency Policy
|
||||
|
||||
Our security advisories focus on vulnerabilities found within the **SFTPGo codebase itself**.
|
||||
|
||||
To ensure the long-term sustainability of the project, we handle upstream dependencies (like the Go standard library, external packages, or Docker base images) as follows:
|
||||
|
||||
- Community Updates: For the Open Source version, vulnerabilities in upstream components (such as the Go standard library or third-party packages) are addressed during our **regular release cycles**. We generally do not provide immediate, out-of-band or ad-hoc releases to address dependency-only CVEs.
|
||||
- Empowering Users: One of the strengths of SFTPGo being open-source is that you have full control. If your security scanners require an immediate fix, you can always rebuild the project using the latest patched Go toolchain or updated dependencies.
|
||||
- Compatibility: We are committed to keeping SFTPGo compatible with the latest stable Go compiler. If an upstream fix breaks SFTPGo, fixing that becomes a priority for us.
|
||||
- Professional Needs: We understand that some organizations have strict compliance requirements or internal SLAs that require guaranteed, immediate response times and out-of-band patches. For these cases, we offer [SFTPGo Enterprise](https://sftpgo.com/on-premises) to cover the additional maintenance and support overhead.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
#!/usr/bin/env bash
|
||||
set -e
|
||||
set -euo pipefail
|
||||
|
||||
ARCH=`uname -m`
|
||||
ARCH=$(uname -m)
|
||||
|
||||
case ${ARCH} in
|
||||
"x86_64")
|
||||
x86_64)
|
||||
SUFFIX=amd64
|
||||
;;
|
||||
"aarch64")
|
||||
aarch64)
|
||||
SUFFIX=arm64
|
||||
;;
|
||||
*)
|
||||
|
|
@ -15,11 +15,21 @@ case ${ARCH} in
|
|||
;;
|
||||
esac
|
||||
|
||||
echo "download plugins for arch ${SUFFIX}"
|
||||
echo "Downloading 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}"
|
||||
PLUGINS=(geoipfilter kms pubsub eventstore eventsearch auth)
|
||||
|
||||
for PLUGIN in "${PLUGINS[@]}"; do
|
||||
URL="https://github.com/sftpgo/sftpgo-plugin-${PLUGIN}/releases/latest/download/sftpgo-plugin-${PLUGIN}-linux-${SUFFIX}"
|
||||
DEST="/usr/local/bin/sftpgo-plugin-${PLUGIN}"
|
||||
|
||||
echo "Downloading ${PLUGIN}..."
|
||||
if curl --fail --silent --show-error -L "${URL}" --output "${DEST}"; then
|
||||
chmod 755 "${DEST}"
|
||||
else
|
||||
echo "Error: Failed to download ${PLUGIN}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "All plugins downloaded successfully"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Data Backup
|
||||
|
||||
:warning: Since v2.4.0 you can use the [EventManager](https://sftpgo.github.io/latest/eventmanager/) to schedule backups.
|
||||
:warning: Since v2.4.0 you can use the [EventManager](https://docs.sftpgo.com/latest/eventmanager/) to schedule backups.
|
||||
|
||||
The `backup` example script shows how to use the SFTPGo REST API to backup your data.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
# 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`
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
#!/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()
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
module github.com/drakkan/ldapauth
|
||||
|
||||
go 1.22.2
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/go-ldap/ldap/v3 v3.4.8
|
||||
golang.org/x/crypto v0.23.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.12
|
||||
golang.org/x/crypto v0.45.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/Azure/go-ntlmssp v0.1.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,20 +1,15 @@
|
|||
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/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
|
||||
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/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/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
|
||||
github.com/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=
|
||||
|
|
@ -31,69 +26,15 @@ github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZ
|
|||
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=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
|
|
@ -1,43 +1,37 @@
|
|||
module github.com/drakkan/sftpgo/ldapauthserver
|
||||
|
||||
go 1.22.2
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.0.12
|
||||
github.com/go-chi/chi/v5 v5.2.3
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/go-ldap/ldap/v3 v3.4.8
|
||||
github.com/go-ldap/ldap/v3 v3.4.12
|
||||
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
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/viper v1.21.0
|
||||
golang.org/x/crypto v0.45.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.1.0 // 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/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // 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-colorable v0.1.14 // 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/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // 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
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,40 +1,34 @@
|
|||
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/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
|
||||
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||
github.com/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/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/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/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
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/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/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
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/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
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=
|
||||
|
|
@ -53,132 +47,68 @@ 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-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
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/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/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/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
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/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/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=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
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/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
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/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
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/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
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=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
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=
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Update user quota
|
||||
|
||||
:warning: Since v2.4.0 you can use the [EventManager](https://sftpgo.github.io/latest/eventmanager/) to schedule quota scans.
|
||||
:warning: Since v2.4.0 you can use the [EventManager](https://docs.sftpgo.com/latest/eventmanager/) to schedule quota scans.
|
||||
|
||||
The `scanuserquota` example script shows how to use the SFTPGo REST API to update the users' quota.
|
||||
|
||||
|
|
|
|||
283
go.mod
283
go.mod
|
|
@ -1,190 +1,185 @@
|
|||
module github.com/drakkan/sftpgo/v2
|
||||
|
||||
go 1.22.2
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
cloud.google.com/go/storage v1.41.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2
|
||||
cloud.google.com/go/storage v1.60.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4
|
||||
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.26.1
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.13
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.13
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.18
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.21.5
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.54.0
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.7
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.7
|
||||
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.0
|
||||
github.com/fclairamb/go-log v0.5.0
|
||||
github.com/go-acme/lego/v4 v4.16.1
|
||||
github.com/go-chi/chi/v5 v5.0.12
|
||||
github.com/go-chi/jwtauth/v5 v5.3.1
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.3
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.11
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.11
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.4.3
|
||||
github.com/coreos/go-oidc/v3 v3.17.0
|
||||
github.com/drakkan/webdav v0.0.0-20241026165615-b8b8f74ae71b
|
||||
github.com/eikenb/pipeat v0.0.0-20251030185646-385cd3c3e07b
|
||||
github.com/fclairamb/ftpserverlib v0.30.0
|
||||
github.com/go-acme/lego/v4 v4.32.0
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/go-jose/go-jose/v4 v4.1.3
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
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.6
|
||||
github.com/jackc/pgx/v5 v5.5.5
|
||||
github.com/hashicorp/go-plugin v1.7.0
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8
|
||||
github.com/jackc/pgx/v5 v5.8.0
|
||||
github.com/jlaffaye/ftp v0.2.0
|
||||
github.com/klauspost/compress v1.17.8
|
||||
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/klauspost/compress v1.18.4
|
||||
github.com/lithammer/shortuuid/v4 v4.2.0
|
||||
github.com/mattn/go-sqlite3 v1.14.34
|
||||
github.com/mhale/smtpd v0.8.3
|
||||
github.com/minio/sio v0.3.1
|
||||
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/minio/sio v0.4.3
|
||||
github.com/otiai10/copy v1.14.1
|
||||
github.com/pires/go-proxyproto v0.11.0
|
||||
github.com/pkg/sftp v1.13.10
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
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.32.0
|
||||
github.com/sftpgo/sdk v0.1.7
|
||||
github.com/shirou/gopsutil/v3 v3.24.4
|
||||
github.com/spf13/afero v1.11.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/viper v1.18.2
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/studio-b12/gowebdav v0.9.0
|
||||
github.com/rs/cors v1.11.1
|
||||
github.com/rs/xid v1.6.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/sftpgo/sdk v0.1.9
|
||||
github.com/shirou/gopsutil/v3 v3.24.5
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/studio-b12/gowebdav v0.12.0
|
||||
github.com/subosito/gotenv v1.6.0
|
||||
github.com/unrolled/secure v1.14.0
|
||||
github.com/unrolled/secure v1.17.0
|
||||
github.com/wagslane/go-password-validator v0.3.0
|
||||
github.com/wneessen/go-mail v0.4.1
|
||||
github.com/wneessen/go-mail v0.7.2
|
||||
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.23.0
|
||||
golang.org/x/net v0.25.0
|
||||
golang.org/x/oauth2 v0.20.0
|
||||
golang.org/x/sys v0.20.0
|
||||
golang.org/x/term v0.20.0
|
||||
golang.org/x/time v0.5.0
|
||||
google.golang.org/api v0.180.0
|
||||
go.etcd.io/bbolt v1.4.3
|
||||
gocloud.dev v0.45.0
|
||||
golang.org/x/crypto v0.49.0
|
||||
golang.org/x/net v0.52.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sys v0.42.0
|
||||
golang.org/x/term v0.41.0
|
||||
golang.org/x/time v0.15.0
|
||||
google.golang.org/api v0.271.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.113.0 // indirect
|
||||
cloud.google.com/go/auth v0.4.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.8.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.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // 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.5 // 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.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0 // indirect
|
||||
github.com/aws/smithy-go v1.20.2 // indirect
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
cloud.google.com/go/auth v0.18.2 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
cloud.google.com/go/iam v1.5.3 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.3 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
|
||||
github.com/ajg/form v1.7.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect
|
||||
github.com/aws/smithy-go v1.24.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/boombuler/barcode v1.1.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // 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/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // 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/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
|
||||
github.com/fatih/color v1.18.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.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // 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.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // 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.4 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.18.0 // 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/hashicorp/yamux v0.1.2 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // 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/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.59 // 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/miekg/dns v1.1.72 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/oklog/run v1.2.0 // indirect
|
||||
github.com/otiai10/mint v1.6.3 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // 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.53.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.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/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.2.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.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.51.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect
|
||||
go.opentelemetry.io/otel v1.26.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.26.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.26.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
golang.org/x/tools v0.21.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240513163218-0867130af1f8 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 // indirect
|
||||
google.golang.org/grpc v1.64.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
|
||||
go.opentelemetry.io/otel v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.42.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/mod v0.34.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/tools v0.43.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/genproto v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
google.golang.org/grpc v1.79.2 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/fclairamb/ftpserverlib => github.com/drakkan/ftpserverlib v0.0.0-20240510125431-4617586dfa1c
|
||||
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-20240509175024-33071fb6437f
|
||||
)
|
||||
|
|
|
|||
747
go.sum
747
go.sum
|
|
@ -1,222 +1,198 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.113.0 h1:g3C70mn3lWfckKBiCVsAshabrDg01pQ0pnX1MNtnMkA=
|
||||
cloud.google.com/go v0.113.0/go.mod h1:glEqlogERKYeePz6ZdkcLJ28Q2I6aERgDDErBg9GzO8=
|
||||
cloud.google.com/go/auth v0.4.1 h1:Z7YNIhlWRtrnKlZke7z3GMqzvuYzdc2z98F9D1NV5Hg=
|
||||
cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro=
|
||||
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 v1.26.0 h1:uHf0NN2nvxl1Gh4QO83yRCOdMK4zivtMS5gv0dEX0hg=
|
||||
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.16.0 h1:1yZsRPhmargZOmY+fVAh8IKiR9HzCb0U1zsxb5g2nRY=
|
||||
cloud.google.com/go/kms v1.16.0/go.mod h1:olQUXy2Xud+1GzYfiBO9N0RhjsJk5IJLU6n/ethLXVc=
|
||||
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.41.0 h1:RusiwatSu6lHeEXe3kglxakAmAbfV+rhtPqA6i8RBx0=
|
||||
cloud.google.com/go/storage v1.41.0/go.mod h1:J1WCa/Z2FcgdEDuPUY8DxT5I+d9mFKsCepp5vR6Sq80=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
|
||||
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
|
||||
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
|
||||
cloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU=
|
||||
cloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58=
|
||||
cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA=
|
||||
cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak=
|
||||
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
|
||||
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
|
||||
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
|
||||
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
|
||||
cloud.google.com/go/storage v1.60.0 h1:oBfZrSOCimggVNz9Y/bXY35uUcts7OViubeddTTVzQ8=
|
||||
cloud.google.com/go/storage v1.60.0/go.mod h1:q+5196hXfejkctrnx+VYU8RKQr/L3c0cBIlrjmiAKE0=
|
||||
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
|
||||
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
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.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
|
||||
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/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 h1:jWQK1GI+LeGGUKBADtcH2rRqPxYB1Ljwms5gFA2LqrM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4/go.mod h1:8mwH4klAm9DUgR2EEHyEEAQlRDvLPyg5fQry3y+cDew=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0 h1:4iB+IesclUXdP0ICgAabvq2FYLXrJWKx1fJQ+GxSo3Y=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
||||
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/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/ajg/form v1.7.1 h1:OsnBDzTkrWdrxvEnO68I72ZVGJGNaMwPhoAm0V+llgc=
|
||||
github.com/ajg/form v1.7.1/go.mod h1:HL757PzLyNkj5AIfptT6L+iGNeXTlnrr/oDePGc/y7Q=
|
||||
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.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA=
|
||||
github.com/aws/aws-sdk-go-v2 v1.26.1/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.13 h1:WbKW8hOzrWoOA/+35S5okqO/2Ap8hkkFUzoW8Hzq24A=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.13/go.mod h1:XLiyiTMnguytjRER7u5RIkhIqS8Nyz41SwAWb4xEjxs=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.13 h1:XDCJDzk/u5cN7Aple7D/MiAhx1Rjo/0nueJ0La8mRuE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.13/go.mod h1:FMNcjQrmuBYvOTZDtOLCIu0esmxjF7RuA/89iSXWzQI=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.17 h1:9b1Os1s11mF5qTIKLgSsyPG810di2+ySSLIIt9bwe9I=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.17/go.mod h1:9Wp7tDOMhv0+sb/FTRAkbHNQ7abYDnoJRzm5AAtCnTc=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.18 h1:fUHit8Pe+2dWEHtxpOVDTOSQR257iH24HjT17DAz6qs=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.18/go.mod h1:IX1n1o870YYxzqN56w26s7FrO5Zaw/hdatxhJDiEf2U=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc=
|
||||
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.5 h1:81KE7vaZzrl7yHBYHVEzYB8sypz11NMOZ40YlWvPxsU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5/go.mod h1:LIt2rg7Mcgn09Ygbdh/RdIm0rQ+3BNkbP1gyVMFtRK0=
|
||||
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.7 h1:ZMeFZ5yk+Ek+jNr1+uwCd2tG89t6oTS5yVWpa6yy2es=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7/go.mod h1:mxV05U+4JiHqIpGqqYXOHLPKUC6bDXC44bsUhNjOEwY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 h1:f9RyWNtS8oH7cZlbn+/JNPpjUk5+5fLd5lM9M0i49Ys=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5/go.mod h1:h5CoMZV2VF297/VLhRhO1WF+XYWOzXo+4HsObA4HjBQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.21.5 h1:p2PxN+OO28p2bCCXE79sJfFBaSohwxa24bQdjuyPZCs=
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.21.5/go.mod h1:Q01yJLephuOzv6IYzcknrpVAriOqB66+qtGnpqgw9UE=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.2 h1:rq2hglTQM3yHZvOPVMtNvLS5x6hijx7JvRDgKiTNDGQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.2/go.mod h1:qmdkIIAC+GCLASF7R2whgNrJADz0QZPX+Seiw/i4S3o=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.54.0 h1:Ls94RY3P6HtB88JkzXo1lHrXzonHPpNR//OSAV63mSE=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.54.0/go.mod h1:qmdkIIAC+GCLASF7R2whgNrJADz0QZPX+Seiw/i4S3o=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.7 h1:4cziOtpDwtgcb+wTYRzz8C+GoH1XySy0p7j4oBbqPQE=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.7/go.mod h1:3Ba++UwWd154xtP4FRX5pUK3Gt4up5sDHCve6kVfE+g=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.6 h1:o5cTaeunSpfXiLTIBx5xo2enQmiChtu1IBbzXnfU9Hs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.6/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0 h1:Qe0r0lVURDDeBQJ4yP+BOrJkvkiCo/3FH/t+wY11dmw=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.7 h1:et3Ta53gotFR4ERLXXHIHl/Uuk1qYpP5uU7cvNql8ns=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.7/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw=
|
||||
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/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 h1:N4lRUXZpZ1KVEUn6hxtco/1d2lgYhNn1fHkkl8WhlyQ=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 h1:qi3e/dmpdONhj1RyIZdi6DKKpDXS5Lb8ftr3p7cyHJc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20/go.mod h1:V1K+TeJVD5JOk3D9e5tsX2KUdL7BlB+FV6cBhdobN8c=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 h1:BYf7XNsJMzl4mObARUBUib+j2tf0U//JAAtTnYqvqCw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11/go.mod h1:aEUS4WrNk/+FxkBZZa7tVgp4pGH+kFGW40Y8rCPqt5g=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 h1:JnQeStZvPHFHeyky/7LbMlyQjUa+jIBj36OlWm0pzIk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19/go.mod h1:HGyasyHvYdFQeJhvDHfH7HXkHh57htcJGKDZ+7z+I24=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4 h1:4ExZyubQ6LQQVuF2Qp9OsfEvsTdAWh5Gfwf6PgIdLdk=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4/go.mod h1:NF3JcMGOiARAss1ld3WGORCw71+4ExDD2cbbdKS5PpA=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI=
|
||||
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
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/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0/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/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
|
||||
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
|
||||
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
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/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/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.4.3 h1:LJO3K3jC5WXvMePRQSJE1NsIGoFGcEx1LW83W6RAlhw=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.4.3/go.mod h1:9U179XbCx4qFWtNhc7BiWLPfuyMVQ7qdAhfrwLz1vH0=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||
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/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/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
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/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-20240509175024-33071fb6437f h1:4+0I7deWH0/8dTS1xVgFrNSq7aaNvKrfaqLlfFBNV64=
|
||||
github.com/drakkan/crypto v0.0.0-20240509175024-33071fb6437f/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
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-20240510125431-4617586dfa1c h1:cO3eqB2Bjv8WM8HUDfajAt3bFFGj6FUQ2eIxsxVvyC8=
|
||||
github.com/drakkan/ftpserverlib v0.0.0-20240510125431-4617586dfa1c/go.mod h1:+9afJRWESpCq4/O8Vr00Q2jfinRxP6PiCpXph6CgGuc=
|
||||
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/drakkan/webdav v0.0.0-20241026165615-b8b8f74ae71b h1:Y1tLiQ8fnxM5f3wiBjAXsHzHNwiY9BR+mXZA75nZwrs=
|
||||
github.com/drakkan/webdav v0.0.0-20241026165615-b8b8f74ae71b/go.mod h1:zOVb1QDhwwqWn2L2qZ0U3swMSO4GTSNyIwXCGO/UGWE=
|
||||
github.com/eikenb/pipeat v0.0.0-20251030185646-385cd3c3e07b h1:G2Mm3YhlyjkFrNnvu5E6LtNcPJtggXL1i5ekDV4hDD4=
|
||||
github.com/eikenb/pipeat v0.0.0-20251030185646-385cd3c3e07b/go.mod h1:XccPiThW83W5pzeOCsJAylEUtWeH+3zQVwiO402FXXc=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
|
||||
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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fclairamb/ftpserverlib v0.30.0 h1:caB9sDn1Au//q0j2ev/icPn388qPuk4k1ajSvglDcMQ=
|
||||
github.com/fclairamb/ftpserverlib v0.30.0/go.mod h1:QmogtltTOgkihyKza0GNo37Mu4AEzbJ+sH6W9Y0MBIQ=
|
||||
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.16.1 h1:JxZ93s4KG0jL27rZ30UsIgxap6VGzKuREsSkkyzeoCQ=
|
||||
github.com/go-acme/lego/v4 v4.16.1/go.mod h1:AVvwdPned/IWpD/ihHhMsKnveF7HHYAz/CmtXi7OZoE=
|
||||
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/jwtauth/v5 v5.3.1 h1:1ePWrjVctvp1tyBq5b/2ER8Th/+RbYc7x4qNsc5rh5A=
|
||||
github.com/go-chi/jwtauth/v5 v5.3.1/go.mod h1:6Fl2RRmWXs3tJYE1IQGX81FsPoGqDwq9c15j52R5q80=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-acme/lego/v4 v4.32.0 h1:z7Ss7aa1noabhKj+DBzhNCO2SM96xhE3b0ucVW3x8Tc=
|
||||
github.com/go-acme/lego/v4 v4.32.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI=
|
||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
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-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
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.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
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-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.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/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
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/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.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
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/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
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.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg=
|
||||
github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI=
|
||||
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
||||
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
||||
github.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI=
|
||||
github.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE=
|
||||
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=
|
||||
|
|
@ -226,33 +202,28 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1
|
|||
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.6 h1:TwRYfx2z2C4cLbXmT8I5PgP/xmuqASDyiVuGYfs9GZM=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.6/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/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=
|
||||
github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
||||
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
|
||||
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
|
||||
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-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||
github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0=
|
||||
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.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
||||
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
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.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
|
||||
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
|
||||
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
|
||||
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
||||
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
|
|
@ -261,217 +232,185 @@ 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-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
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/lithammer/shortuuid/v4 v4.2.0 h1:LMFOzVB3996a7b8aBuEXxqOBflbfPQAiVzkIcHO0h8c=
|
||||
github.com/lithammer/shortuuid/v4 v4.2.0/go.mod h1:D5noHZ2oFw/YaKCfGy0YxyE7M0wMbezmMjPdhyEFe6Y=
|
||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=
|
||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
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-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
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/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
|
||||
github.com/mattn/go-sqlite3 v1.14.34/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.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
|
||||
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
|
||||
github.com/minio/sio v0.3.1 h1:d59r5RTHb1OsQaSl1EaTWurzMMDRLA5fgNmjzD4eVu4=
|
||||
github.com/minio/sio v0.3.1/go.mod h1:S0ovgVgc+sTlQyhiXA1ppBLv7REM7TYi5yyq2qL/Y6o=
|
||||
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/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/minio/sio v0.4.3 h1:JqyID1XM86KwBZox5RAdLD4MLPIDoCY2cke2CXCJCkg=
|
||||
github.com/minio/sio v0.4.3/go.mod h1:4ANoe4CCXqnt1FCiLM0+vlBUhhWZzVOhYCz0069KtFc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E=
|
||||
github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk=
|
||||
github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
|
||||
github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
|
||||
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
|
||||
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
|
||||
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
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/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
|
||||
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
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.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE=
|
||||
github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
|
||||
github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI5Ek=
|
||||
github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk=
|
||||
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.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
||||
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
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.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/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||
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.7 h1:lzOKBDnKb1PpSMlskqCPxBYKxVWz34uMBhT78r/13iA=
|
||||
github.com/sftpgo/sdk v0.1.7/go.mod h1:ler/KG6kMLlsOs/8s6dVN3oom+z+NkbXBVWO//Cv/WA=
|
||||
github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU=
|
||||
github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8=
|
||||
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.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/sftpgo/sdk v0.1.9 h1:onBWfibCt34xHeKC2KFYPZ1DBqXGl9um/cAw+AVdgzY=
|
||||
github.com/sftpgo/sdk v0.1.9/go.mod h1:ehimvlTP+XTEiE3t1CPwWx9n7+6A6OGvMGlZ7ouvKFk=
|
||||
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.2.0 h1:t4GNqvPZ84Vjtpboo/kT3pIkbaK3vc+JIlD/Wz1zSFY=
|
||||
github.com/shoenig/go-m1cpu v0.2.0/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w=
|
||||
github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=
|
||||
github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.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/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/studio-b12/gowebdav v0.12.0 h1:kFRtQECt8jmVAvA6RHBz3geXUGJHUZA6/IKpOVUs5kM=
|
||||
github.com/studio-b12/gowebdav v0.12.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.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
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.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
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/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||
github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU=
|
||||
github.com/unrolled/secure v1.17.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/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
|
||||
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
|
||||
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.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc=
|
||||
go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
|
||||
go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4=
|
||||
go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30=
|
||||
go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4=
|
||||
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.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
|
||||
go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0=
|
||||
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-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
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=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
|
||||
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
|
||||
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI=
|
||||
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
|
||||
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
|
||||
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
|
||||
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
gocloud.dev v0.45.0 h1:WknIK8IbRdmynDvara3Q7G6wQhmEiOGwpgJufbM39sY=
|
||||
gocloud.dev v0.45.0/go.mod h1:0kXKmkCLG6d31N7NyLZWzt7jDSQura9zD/mWgiB6THI=
|
||||
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.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
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.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.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-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
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-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.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
|
||||
golang.org/x/oauth2 v0.20.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/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
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.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/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-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=
|
||||
|
|
@ -483,86 +422,56 @@ 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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.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.19.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/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
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.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
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/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
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.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/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/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
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-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.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
|
||||
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
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.180.0 h1:M2D87Yo0rGBPWpo1orwfCLehUUL6E7/TYe5gvMQWDh4=
|
||||
google.golang.org/api v0.180.0/go.mod h1:51AiyoEg1MJPSZ9zvklA8VnRILPXxn1iVen9v25XHAE=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
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-20240513163218-0867130af1f8 h1:XpH03M6PDRKTo1oGfZBXu2SzwcbfxUokgobVinuUZoU=
|
||||
google.golang.org/genproto v0.0.0-20240513163218-0867130af1f8/go.mod h1:OLh2Ylz+WlYAJaSBRpJIJLP8iQP+8da+fpxbwNEAV/o=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 h1:W5Xj/70xIA4x60O/IFyXivR5MGqblAb8R3w26pnD6No=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8/go.mod h1:vPrPUTsDCYxXWjP7clS81mZ6/803D8K4iM9Ma27VKas=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 h1:mxSlqyb8ZAHsYDCfiXN1EDdNTdvjUJSLY+OnAUtYNYA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
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.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
|
||||
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
|
||||
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.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY=
|
||||
google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q=
|
||||
google.golang.org/genproto v0.0.0-20260311181403-84a4fc48630c h1:ZhFDeBMmFc/4g8/GwxnJ4rzB3O4GwQVNr+8Mh7Y5z4g=
|
||||
google.golang.org/genproto v0.0.0-20260311181403-84a4fc48630c/go.mod h1:hf4r/rBuzaTkLUWRO03771Xvcs6P5hwdQK3UUEJjqo0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c h1:OyQPd6I3pN/9gDxz6L13kYGJgqkpdrAohJRBeXyxlgI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c/go.mod h1:X2gu9Qwng7Nn009s/r3RUxqkzQNqOrAy79bluY7ojIg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/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/logo.png
BIN
img/logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 29 KiB |
BIN
img/reui.png
Normal file
BIN
img/reui.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
BIN
img/servinga.png
Normal file
BIN
img/servinga.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
BIN
img/vps2day.png
BIN
img/vps2day.png
Binary file not shown.
|
Before Width: | Height: | Size: 8 KiB |
102
init/sftpgo
Executable file
102
init/sftpgo
Executable file
|
|
@ -0,0 +1,102 @@
|
|||
#! /bin/sh
|
||||
|
||||
### BEGIN INIT INFO
|
||||
# Provides: SFTPGo
|
||||
# Required-Start: $remote_fs $syslog
|
||||
# Required-Stop: $remote_fs $syslog
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop:
|
||||
# Short-Description: SFTPGo server
|
||||
### END INIT INFO
|
||||
|
||||
set -e
|
||||
|
||||
# /etc/init.d/sftpgo: start and stop the SFTPGo "server" daemon
|
||||
|
||||
SFTPGO_USER="sftpgo"
|
||||
SFTPGO_GROUP="sftpgo"
|
||||
SFTPGO_BIN_NAME="sftpgo"
|
||||
SFTPGO_BIN="/usr/bin/sftpgo"
|
||||
SFTPGO_PID="/run/sftpgo.pid"
|
||||
SFTPGO_CONF_DIR="/etc/sftpgo"
|
||||
SFTPGO_CONF_FILE="sftpgo.json"
|
||||
SFTPGO_OPTS="serve -c $SFTPGO_CONF_DIR --config-file $SFTPGO_CONF_FILE"
|
||||
|
||||
umask 022
|
||||
|
||||
test -x $SFTPGO_BIN || exit 0
|
||||
|
||||
|
||||
if test -f /etc/default/$SFTPGO_BIN_NAME; then
|
||||
. /etc/default/$SFTPGO_BIN_NAME
|
||||
fi
|
||||
|
||||
. /lib/lsb/init-functions
|
||||
|
||||
if [ -n "$2" ]; then
|
||||
SFTPGO_OPTS="$SFTPGO_OPTS $2"
|
||||
fi
|
||||
|
||||
# Are we running from init?
|
||||
run_by_init() {
|
||||
([ "$previous" ] && [ "$runlevel" ]) || [ "$runlevel" = S ]
|
||||
}
|
||||
|
||||
check_dev_null() {
|
||||
if [ ! -c /dev/null ]; then
|
||||
if [ "$1" = log_end_msg ]; then
|
||||
log_end_msg 1 || true
|
||||
fi
|
||||
if ! run_by_init; then
|
||||
log_action_msg "/dev/null is not a character device!" || true
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
write_pid() {
|
||||
sleep 0.25
|
||||
echo $(/bin/pidof $SFTPGO_BIN_NAME) > $SFTPGO_PID
|
||||
}
|
||||
|
||||
export PATH="${PATH:+$PATH:}/usr/sbin:/sbin"
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
check_dev_null
|
||||
log_daemon_msg "Starting SFTPGo server" "$SFTPGO_BIN_NAME" || true
|
||||
if start-stop-daemon --start --background --quiet --oknodo --chuid $SFTPGO_USER:$SFTPGO_GROUP --pidfile $SFTPGO_PID --exec $SFTPGO_BIN -- $SFTPGO_OPTS; then
|
||||
log_end_msg 0 || true
|
||||
write_pid
|
||||
else
|
||||
log_end_msg 1 || true
|
||||
fi
|
||||
;;
|
||||
stop)
|
||||
log_daemon_msg "Stopping SFTPGo server" "$SFTPGO_BIN_NAME" || true
|
||||
if start-stop-daemon --stop --quiet --oknodo --pidfile $SFTPGO_PID --exec $SFTPGO_BIN; then
|
||||
log_end_msg 0 || true
|
||||
else
|
||||
log_end_msg 1 || true
|
||||
fi
|
||||
;;
|
||||
|
||||
reload)
|
||||
log_daemon_msg "Reloading SFTPGo server" "$SFTPGO_BIN_NAME" || true
|
||||
if start-stop-daemon --stop --signal 1 --quiet --oknodo --pidfile $SFTPGO_PID --exec $SFTPGO_BIN; then
|
||||
log_end_msg 0 || true
|
||||
else
|
||||
log_end_msg 1 || true
|
||||
fi
|
||||
;;
|
||||
|
||||
status)
|
||||
status_of_proc -p $SFTPGO_PID $SFTPGO_BIN $SFTPGO_BIN_NAME && exit 0 || exit $?
|
||||
;;
|
||||
|
||||
*)
|
||||
log_action_msg "Usage: /etc/init.d/$SFTPGO_BIN_NAME {start|stop|reload|status}" || true
|
||||
exit 1
|
||||
esac
|
||||
|
||||
exit 0
|
||||
|
|
@ -30,6 +30,7 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -43,6 +44,7 @@ import (
|
|||
"github.com/go-acme/lego/v4/log"
|
||||
"github.com/go-acme/lego/v4/providers/http/webroot"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
"github.com/robfig/cron/v3"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/common"
|
||||
|
|
@ -249,7 +251,7 @@ func (c *Configuration) Initialize(configDir string) error {
|
|||
if c.RenewDays < 1 {
|
||||
return fmt.Errorf("invalid number of days remaining before renewal: %d", c.RenewDays)
|
||||
}
|
||||
if !util.Contains(supportedKeyTypes, c.KeyType) {
|
||||
if !slices.Contains(supportedKeyTypes, c.KeyType) {
|
||||
return fmt.Errorf("invalid key type %q", c.KeyType)
|
||||
}
|
||||
caURL, err := url.Parse(c.CAEndpoint)
|
||||
|
|
@ -489,7 +491,16 @@ func (c *Configuration) setup() (*account, *lego.Client, error) {
|
|||
config := lego.NewConfig(&account)
|
||||
config.CADirURL = c.CAEndpoint
|
||||
config.Certificate.KeyType = certcrypto.KeyType(c.KeyType)
|
||||
config.Certificate.OverallRequestLimit = 6
|
||||
config.UserAgent = version.GetServerVersion("/", false)
|
||||
|
||||
retryClient := retryablehttp.NewClient()
|
||||
retryClient.Logger = &logger.LeveledLogger{Sender: "RetryableHTTPClient"}
|
||||
retryClient.RetryMax = 5
|
||||
retryClient.HTTPClient = config.HTTPClient
|
||||
|
||||
config.HTTPClient = retryClient.StandardClient()
|
||||
|
||||
client, err := lego.NewClient(config)
|
||||
if err != nil {
|
||||
acmeLog(logger.LevelError, "unable to get ACME client: %v", err)
|
||||
|
|
@ -557,6 +568,13 @@ func (c *Configuration) tryRecoverRegistration(privateKey crypto.PrivateKey) (*r
|
|||
config.CADirURL = c.CAEndpoint
|
||||
config.UserAgent = version.GetServerVersion("/", false)
|
||||
|
||||
retryClient := retryablehttp.NewClient()
|
||||
retryClient.Logger = &logger.LeveledLogger{Sender: "RetryableHTTPClient"}
|
||||
retryClient.RetryMax = 5
|
||||
retryClient.HTTPClient = config.HTTPClient
|
||||
|
||||
config.HTTPClient = retryClient.StandardClient()
|
||||
|
||||
client, err := lego.NewClient(config)
|
||||
if err != nil {
|
||||
acmeLog(logger.LevelError, "unable to get the ACME client: %v", err)
|
||||
|
|
@ -671,7 +689,7 @@ func (c *Configuration) notifyCertificateRenewal(domain string, err error) {
|
|||
params := common.EventParams{
|
||||
Name: domain,
|
||||
Event: "Certificate renewal",
|
||||
Timestamp: time.Now().UnixNano(),
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
if err != nil {
|
||||
params.Status = 2
|
||||
|
|
@ -749,7 +767,7 @@ func (c *Configuration) renewCertificates() error {
|
|||
|
||||
func isDomainValid(domain string) (string, bool) {
|
||||
isValid := false
|
||||
for _, d := range strings.Split(domain, ",") {
|
||||
for d := range strings.SplitSeq(domain, ",") {
|
||||
d = strings.TrimSpace(d)
|
||||
if d != "" {
|
||||
isValid = true
|
||||
|
|
@ -767,7 +785,7 @@ func getDomains(domain string) []string {
|
|||
delimiter = " "
|
||||
}
|
||||
|
||||
for _, d := range strings.Split(domain, delimiter) {
|
||||
for d := range strings.SplitSeq(domain, delimiter) {
|
||||
d = strings.TrimSpace(d)
|
||||
if d != "" {
|
||||
domains = append(domains, d)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build bundle
|
||||
// +build bundle
|
||||
|
||||
package bundle
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import (
|
|||
"github.com/drakkan/sftpgo/v2/internal/config"
|
||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
"github.com/drakkan/sftpgo/v2/internal/plugin"
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
)
|
||||
|
||||
|
|
@ -56,6 +57,15 @@ renewed by the SFTPGo service
|
|||
logger.ErrorToConsole("unable to initialize KMS: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if config.HasKMSPlugin() {
|
||||
if err := plugin.Initialize(config.GetPluginsConfig(), "debug"); err != nil {
|
||||
logger.ErrorToConsole("unable to initialize plugin system: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
registerSignals()
|
||||
defer plugin.Handler.Cleanup()
|
||||
}
|
||||
|
||||
mfaConfig := config.GetMFAConfig()
|
||||
err = mfaConfig.Initialize()
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
// Copyright (C) 2019 Nicola Murino
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, version 3.
|
||||
//
|
||||
// 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 Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
//go:build awscontainer
|
||||
// +build awscontainer
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func addAWSContainerFlags(cmd *cobra.Command) {
|
||||
viper.SetDefault("disable_aws_installation_code", false)
|
||||
viper.BindEnv("disable_aws_installation_code", "SFTPGO_DISABLE_AWS_INSTALLATION_CODE") //nolint:errcheck
|
||||
cmd.Flags().BoolVar(&disableAWSInstallationCode, "disable-aws-installation-code", viper.GetBool("disable_aws_installation_code"),
|
||||
`Disable installation code for the AWS container.
|
||||
This flag can be set using
|
||||
SFTPGO_DISABLE_AWS_INSTALLATION_CODE env var too.
|
||||
`)
|
||||
viper.BindPFlag("disable_aws_installation_code", cmd.Flags().Lookup("disable-aws-installation-code")) //nolint:errcheck
|
||||
}
|
||||
|
|
@ -21,9 +21,11 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/common"
|
||||
"github.com/drakkan/sftpgo/v2/internal/config"
|
||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
"github.com/drakkan/sftpgo/v2/internal/plugin"
|
||||
"github.com/drakkan/sftpgo/v2/internal/service"
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
)
|
||||
|
|
@ -65,6 +67,15 @@ Please take a look at the usage below to customize the options.`,
|
|||
logger.ErrorToConsole("Unable to initialize KMS: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if config.HasKMSPlugin() {
|
||||
if err := plugin.Initialize(config.GetPluginsConfig(), "debug"); err != nil {
|
||||
logger.ErrorToConsole("unable to initialize plugin system: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
registerSignals()
|
||||
defer plugin.Handler.Cleanup()
|
||||
}
|
||||
|
||||
mfaConfig := config.GetMFAConfig()
|
||||
err = mfaConfig.Initialize()
|
||||
if err != nil {
|
||||
|
|
@ -78,15 +89,20 @@ Please take a look at the usage below to customize the options.`,
|
|||
providerConf.Actions.ExecuteOn = nil
|
||||
logger.InfoToConsole("Initializing provider: %q config file: %q", providerConf.Driver, viper.ConfigFileUsed())
|
||||
err = dataprovider.InitializeDatabase(providerConf, configDir)
|
||||
if err == nil {
|
||||
switch err {
|
||||
case nil:
|
||||
logger.InfoToConsole("Data provider successfully initialized/updated")
|
||||
} else if err == dataprovider.ErrNoInitRequired {
|
||||
case dataprovider.ErrNoInitRequired:
|
||||
logger.InfoToConsole("%v", err.Error())
|
||||
} else {
|
||||
default:
|
||||
logger.ErrorToConsole("Unable to initialize/update the data provider: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if providerConf.Driver != dataprovider.MemoryDataProviderName && loadDataFrom != "" {
|
||||
if err := common.Initialize(config.GetCommonConfig(), providerConf.GetShared()); err != nil {
|
||||
logger.ErrorToConsole("%v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
service := service.Service{
|
||||
LoadDataFrom: loadDataFrom,
|
||||
LoadDataMode: loadDataMode,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build !noportable
|
||||
// +build !noportable
|
||||
|
||||
package cmd
|
||||
|
||||
|
|
@ -497,7 +496,7 @@ func getPatternsFilterValues(value string) (string, []string) {
|
|||
if len(dirExts) > 1 {
|
||||
dir := strings.TrimSpace(dirExts[0])
|
||||
exts := []string{}
|
||||
for _, e := range strings.Split(dirExts[1], ",") {
|
||||
for e := range strings.SplitSeq(dirExts[1], ",") {
|
||||
cleanedExt := strings.TrimSpace(e)
|
||||
if cleanedExt != "" {
|
||||
exts = append(exts, cleanedExt)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build noportable
|
||||
// +build noportable
|
||||
|
||||
package cmd
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import (
|
|||
"github.com/drakkan/sftpgo/v2/internal/config"
|
||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
"github.com/drakkan/sftpgo/v2/internal/plugin"
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
)
|
||||
|
||||
|
|
@ -37,6 +38,7 @@ var (
|
|||
Short: "Reset the password for the specified administrator",
|
||||
Long: `This command reads the data provider connection details from the specified
|
||||
configuration file and resets the password for the specified administrator.
|
||||
Two-factor authentication is also disabled.
|
||||
This command is not supported for the memory provider.
|
||||
For embedded providers like bolt and SQLite you should stop the running SFTPGo
|
||||
instance to avoid database corruption.
|
||||
|
|
@ -57,6 +59,15 @@ Please take a look at the usage below to customize the options.`,
|
|||
logger.ErrorToConsole("unable to initialize KMS: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if config.HasKMSPlugin() {
|
||||
if err := plugin.Initialize(config.GetPluginsConfig(), "debug"); err != nil {
|
||||
logger.ErrorToConsole("unable to initialize plugin system: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
registerSignals()
|
||||
defer plugin.Handler.Cleanup()
|
||||
}
|
||||
|
||||
mfaConfig := config.GetMFAConfig()
|
||||
err = mfaConfig.Initialize()
|
||||
if err != nil {
|
||||
|
|
@ -98,6 +109,7 @@ Please take a look at the usage below to customize the options.`,
|
|||
os.Exit(1)
|
||||
}
|
||||
admin.Password = string(pwd)
|
||||
admin.Filters.TOTPConfig.Enabled = false
|
||||
if err := dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSystem, "", ""); err != nil {
|
||||
logger.ErrorToConsole("Unable to update password: %v", err)
|
||||
os.Exit(1)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import (
|
|||
"github.com/drakkan/sftpgo/v2/internal/config"
|
||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
"github.com/drakkan/sftpgo/v2/internal/plugin"
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
)
|
||||
|
||||
|
|
@ -40,8 +41,8 @@ Please take a look at the usage below to customize the options.`,
|
|||
Run: func(_ *cobra.Command, _ []string) {
|
||||
logger.DisableLogger()
|
||||
logger.EnableConsoleLogger(zerolog.DebugLevel)
|
||||
if revertProviderTargetVersion != 28 {
|
||||
logger.WarnToConsole("Unsupported target version, 28 is the only supported one")
|
||||
if revertProviderTargetVersion != 33 {
|
||||
logger.WarnToConsole("Unsupported target version, 33 is the only supported one")
|
||||
os.Exit(1)
|
||||
}
|
||||
configDir = util.CleanDirInput(configDir)
|
||||
|
|
@ -56,6 +57,21 @@ Please take a look at the usage below to customize the options.`,
|
|||
logger.ErrorToConsole("unable to initialize KMS: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if config.HasKMSPlugin() {
|
||||
if err := plugin.Initialize(config.GetPluginsConfig(), "debug"); err != nil {
|
||||
logger.ErrorToConsole("unable to initialize plugin system: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
registerSignals()
|
||||
defer plugin.Handler.Cleanup()
|
||||
}
|
||||
|
||||
mfaConfig := config.GetMFAConfig()
|
||||
err = mfaConfig.Initialize()
|
||||
if err != nil {
|
||||
logger.ErrorToConsole("Unable to initialize MFA: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
providerConf := config.GetProviderConf()
|
||||
logger.InfoToConsole("Reverting provider: %q config file: %q target version %d", providerConf.Driver,
|
||||
viper.ConfigFileUsed(), revertProviderTargetVersion)
|
||||
|
|
@ -71,7 +87,7 @@ Please take a look at the usage below to customize the options.`,
|
|||
|
||||
func init() {
|
||||
addConfigFlags(revertProviderCmd)
|
||||
revertProviderCmd.Flags().IntVar(&revertProviderTargetVersion, "to-version", 28, `28 means the version supported in v2.5.x`)
|
||||
revertProviderCmd.Flags().IntVar(&revertProviderTargetVersion, "to-version", 33, `33 means the version supported in v2.7.x`)
|
||||
|
||||
rootCmd.AddCommand(revertProviderCmd)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,8 +85,6 @@ var (
|
|||
loadDataQuotaScan int
|
||||
loadDataClean bool
|
||||
graceTime int
|
||||
// used if awscontainer build tag is enabled
|
||||
disableAWSInstallationCode bool
|
||||
|
||||
rootCmd = &cobra.Command{
|
||||
Use: "sftpgo",
|
||||
|
|
|
|||
|
|
@ -16,13 +16,21 @@ package cmd
|
|||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/subosito/gotenv"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/service"
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
)
|
||||
|
||||
const (
|
||||
envFileMaxSize = 1048576
|
||||
)
|
||||
|
||||
var (
|
||||
serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
|
|
@ -34,9 +42,11 @@ $ sftpgo serve
|
|||
|
||||
Please take a look at the usage below to customize the startup options`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
configDir := util.CleanDirInput(configDir)
|
||||
checkServeParamsFromEnvFiles(configDir)
|
||||
service.SetGraceTime(graceTime)
|
||||
service := service.Service{
|
||||
ConfigDir: util.CleanDirInput(configDir),
|
||||
ConfigDir: configDir,
|
||||
ConfigFile: configFile,
|
||||
LogFilePath: logFilePath,
|
||||
LogMaxSize: logMaxSize,
|
||||
|
|
@ -51,7 +61,7 @@ Please take a look at the usage below to customize the startup options`,
|
|||
LoadDataClean: loadDataClean,
|
||||
Shutdown: make(chan bool),
|
||||
}
|
||||
if err := service.Start(disableAWSInstallationCode); err == nil {
|
||||
if err := service.Start(); err == nil {
|
||||
service.Wait()
|
||||
if service.Error == nil {
|
||||
os.Exit(0)
|
||||
|
|
@ -62,8 +72,76 @@ Please take a look at the usage below to customize the startup options`,
|
|||
}
|
||||
)
|
||||
|
||||
func setIntFromEnv(receiver *int, val string) {
|
||||
converted, err := strconv.Atoi(val)
|
||||
if err == nil {
|
||||
*receiver = converted
|
||||
}
|
||||
}
|
||||
|
||||
func setBoolFromEnv(receiver *bool, val string) {
|
||||
converted, err := strconv.ParseBool(strings.TrimSpace(val))
|
||||
if err == nil {
|
||||
*receiver = converted
|
||||
}
|
||||
}
|
||||
|
||||
func checkServeParamsFromEnvFiles(configDir string) { //nolint:gocyclo
|
||||
// The logger is not yet initialized here, we have no way to report errors.
|
||||
envd := filepath.Join(configDir, "env.d")
|
||||
entries, err := os.ReadDir(envd)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, entry := range entries {
|
||||
info, err := entry.Info()
|
||||
if err == nil && info.Mode().IsRegular() {
|
||||
envFile := filepath.Join(envd, entry.Name())
|
||||
if info.Size() > envFileMaxSize {
|
||||
continue
|
||||
}
|
||||
envVars, err := gotenv.Read(envFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for k, v := range envVars {
|
||||
if _, isSet := os.LookupEnv(k); isSet {
|
||||
continue
|
||||
}
|
||||
switch k {
|
||||
case "SFTPGO_LOG_FILE_PATH":
|
||||
logFilePath = v
|
||||
case "SFTPGO_LOG_MAX_SIZE":
|
||||
setIntFromEnv(&logMaxSize, v)
|
||||
case "SFTPGO_LOG_MAX_BACKUPS":
|
||||
setIntFromEnv(&logMaxBackups, v)
|
||||
case "SFTPGO_LOG_MAX_AGE":
|
||||
setIntFromEnv(&logMaxAge, v)
|
||||
case "SFTPGO_LOG_COMPRESS":
|
||||
setBoolFromEnv(&logCompress, v)
|
||||
case "SFTPGO_LOG_LEVEL":
|
||||
logLevel = v
|
||||
case "SFTPGO_LOG_UTC_TIME":
|
||||
setBoolFromEnv(&logUTCTime, v)
|
||||
case "SFTPGO_CONFIG_FILE":
|
||||
configFile = v
|
||||
case "SFTPGO_LOADDATA_FROM":
|
||||
loadDataFrom = v
|
||||
case "SFTPGO_LOADDATA_MODE":
|
||||
setIntFromEnv(&loadDataMode, v)
|
||||
case "SFTPGO_LOADDATA_CLEAN":
|
||||
setBoolFromEnv(&loadDataClean, v)
|
||||
case "SFTPGO_LOADDATA_QUOTA_SCAN":
|
||||
setIntFromEnv(&loadDataQuotaScan, v)
|
||||
case "SFTPGO_GRACE_TIME":
|
||||
setIntFromEnv(&graceTime, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(serveCmd)
|
||||
addServeFlags(serveCmd)
|
||||
addAWSContainerFlags(serveCmd)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (C) 2019 Nicola Murino
|
||||
// Copyright (C) 2025 Nicola Murino
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
|
|
@ -13,26 +13,29 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package sftpd
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
"github.com/drakkan/sftpgo/v2/internal/plugin"
|
||||
)
|
||||
|
||||
var (
|
||||
processUID = os.Geteuid()
|
||||
processGID = os.Getegid()
|
||||
)
|
||||
|
||||
func wrapCmd(cmd *exec.Cmd, uid, gid int) *exec.Cmd {
|
||||
isCurrentUser := processUID == uid && processGID == gid
|
||||
if (uid > 0 || gid > 0) && !isCurrentUser {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}
|
||||
}
|
||||
return cmd
|
||||
func registerSignals() {
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
for sig := range c {
|
||||
switch sig {
|
||||
case syscall.SIGINT, syscall.SIGTERM:
|
||||
logger.DebugToConsole("Received interrupt request")
|
||||
plugin.Handler.Cleanup()
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (C) 2019 Nicola Murino
|
||||
// Copyright (C) 2025 Nicola Murino
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
|
|
@ -12,13 +12,25 @@
|
|||
// 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/>.
|
||||
|
||||
//go:build !awscontainer
|
||||
// +build !awscontainer
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
"github.com/drakkan/sftpgo/v2/internal/plugin"
|
||||
)
|
||||
|
||||
func addAWSContainerFlags(_ *cobra.Command) {}
|
||||
func registerSignals() {
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
|
||||
go func() {
|
||||
for range c {
|
||||
logger.DebugToConsole("Received interrupt request")
|
||||
plugin.Handler.Cleanup()
|
||||
os.Exit(0)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
@ -17,7 +17,6 @@ package cmd
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
|
|
@ -31,21 +30,23 @@ var (
|
|||
Short: "Start the SFTPGo Windows Service",
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
configDir = util.CleanDirInput(configDir)
|
||||
if !filepath.IsAbs(logFilePath) && util.IsFileInputValid(logFilePath) {
|
||||
logFilePath = filepath.Join(configDir, logFilePath)
|
||||
}
|
||||
checkServeParamsFromEnvFiles(configDir)
|
||||
service.SetGraceTime(graceTime)
|
||||
s := service.Service{
|
||||
ConfigDir: configDir,
|
||||
ConfigFile: configFile,
|
||||
LogFilePath: logFilePath,
|
||||
LogMaxSize: logMaxSize,
|
||||
LogMaxBackups: logMaxBackups,
|
||||
LogMaxAge: logMaxAge,
|
||||
LogCompress: logCompress,
|
||||
LogLevel: logLevel,
|
||||
LogUTCTime: logUTCTime,
|
||||
Shutdown: make(chan bool),
|
||||
ConfigDir: configDir,
|
||||
ConfigFile: configFile,
|
||||
LogFilePath: logFilePath,
|
||||
LogMaxSize: logMaxSize,
|
||||
LogMaxBackups: logMaxBackups,
|
||||
LogMaxAge: logMaxAge,
|
||||
LogCompress: logCompress,
|
||||
LogLevel: logLevel,
|
||||
LogUTCTime: logUTCTime,
|
||||
LoadDataFrom: loadDataFrom,
|
||||
LoadDataMode: loadDataMode,
|
||||
LoadDataQuotaScan: loadDataQuotaScan,
|
||||
LoadDataClean: loadDataClean,
|
||||
Shutdown: make(chan bool),
|
||||
}
|
||||
winService := service.WindowsService{
|
||||
Service: s,
|
||||
|
|
|
|||
|
|
@ -1,227 +0,0 @@
|
|||
// Copyright (C) 2019 Nicola Murino
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, version 3.
|
||||
//
|
||||
// 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 Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/rs/xid"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/common"
|
||||
"github.com/drakkan/sftpgo/v2/internal/config"
|
||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
"github.com/drakkan/sftpgo/v2/internal/plugin"
|
||||
"github.com/drakkan/sftpgo/v2/internal/sftpd"
|
||||
"github.com/drakkan/sftpgo/v2/internal/version"
|
||||
)
|
||||
|
||||
var (
|
||||
logJournalD = false
|
||||
preserveHomeDir = false
|
||||
baseHomeDir = ""
|
||||
subsystemCmd = &cobra.Command{
|
||||
Use: "startsubsys",
|
||||
Short: "Use sftpgo as SFTP file transfer subsystem",
|
||||
Long: `In this mode SFTPGo speaks the server side of SFTP protocol to stdout and
|
||||
expects client requests from stdin.
|
||||
This mode is not intended to be called directly, but from sshd using the
|
||||
Subsystem option.
|
||||
For example adding a line like this one in "/etc/ssh/sshd_config":
|
||||
|
||||
Subsystem sftp sftpgo startsubsys
|
||||
|
||||
Command-line flags should be specified in the Subsystem declaration.
|
||||
`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
logSender := "startsubsys"
|
||||
connectionID := xid.New().String()
|
||||
var zeroLogLevel zerolog.Level
|
||||
switch logLevel {
|
||||
case "info":
|
||||
zeroLogLevel = zerolog.InfoLevel
|
||||
case "warn":
|
||||
zeroLogLevel = zerolog.WarnLevel
|
||||
case "error":
|
||||
zeroLogLevel = zerolog.ErrorLevel
|
||||
default:
|
||||
zeroLogLevel = zerolog.DebugLevel
|
||||
}
|
||||
logger.SetLogTime(logUTCTime)
|
||||
if logJournalD {
|
||||
logger.InitJournalDLogger(zeroLogLevel)
|
||||
} else {
|
||||
logger.InitStdErrLogger(zeroLogLevel)
|
||||
}
|
||||
osUser, err := user.Current()
|
||||
if err != nil {
|
||||
logger.Error(logSender, connectionID, "unable to get the current user: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
username := osUser.Username
|
||||
homedir := osUser.HomeDir
|
||||
logger.Info(logSender, connectionID, "starting SFTPGo %v as subsystem, user %q home dir %q config dir %q base home dir %q",
|
||||
version.Get(), username, homedir, configDir, baseHomeDir)
|
||||
err = config.LoadConfig(configDir, configFile)
|
||||
if err != nil {
|
||||
logger.Error(logSender, connectionID, "unable to load configuration: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
kmsConfig := config.GetKMSConfig()
|
||||
if err := kmsConfig.Initialize(); err != nil {
|
||||
logger.Error(logSender, connectionID, "unable to initialize KMS: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
mfaConfig := config.GetMFAConfig()
|
||||
err = mfaConfig.Initialize()
|
||||
if err != nil {
|
||||
logger.Error(logSender, "", "unable to initialize MFA: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
dataProviderConf := config.GetProviderConf()
|
||||
if dataProviderConf.Driver == dataprovider.SQLiteDataProviderName || dataProviderConf.Driver == dataprovider.BoltDataProviderName {
|
||||
logger.Debug(logSender, connectionID, "data provider %q not supported in subsystem mode, using %q provider",
|
||||
dataProviderConf.Driver, dataprovider.MemoryDataProviderName)
|
||||
dataProviderConf.Driver = dataprovider.MemoryDataProviderName
|
||||
dataProviderConf.Name = ""
|
||||
}
|
||||
config.SetProviderConf(dataProviderConf)
|
||||
err = dataprovider.Initialize(dataProviderConf, configDir, false)
|
||||
if err != nil {
|
||||
logger.Error(logSender, connectionID, "unable to initialize the data provider: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := plugin.Initialize(config.GetPluginsConfig(), logLevel); err != nil {
|
||||
logger.Error(logSender, connectionID, "unable to initialize plugin system: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
smtpConfig := config.GetSMTPConfig()
|
||||
err = smtpConfig.Initialize(configDir, false)
|
||||
if err != nil {
|
||||
logger.Error(logSender, connectionID, "unable to initialize SMTP configuration: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
commonConfig := config.GetCommonConfig()
|
||||
// idle connection are managed externally
|
||||
commonConfig.IdleTimeout = 0
|
||||
config.SetCommonConfig(commonConfig)
|
||||
if err := common.Initialize(config.GetCommonConfig(), dataProviderConf.GetShared()); err != nil {
|
||||
logger.Error(logSender, connectionID, "%v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
httpConfig := config.GetHTTPConfig()
|
||||
if err := httpConfig.Initialize(configDir); err != nil {
|
||||
logger.Error(logSender, connectionID, "unable to initialize http client: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
commandConfig := config.GetCommandConfig()
|
||||
if err := commandConfig.Initialize(); err != nil {
|
||||
logger.Error(logSender, connectionID, "unable to initialize commands configuration: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
user, err := dataprovider.UserExists(username, "")
|
||||
if err == nil {
|
||||
if user.HomeDir != filepath.Clean(homedir) && !preserveHomeDir {
|
||||
// update the user
|
||||
user.HomeDir = filepath.Clean(homedir)
|
||||
err = dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSystem, "", "")
|
||||
if err != nil {
|
||||
logger.Error(logSender, connectionID, "unable to update user %q: %v", username, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
user.Username = username
|
||||
if baseHomeDir != "" && filepath.IsAbs(baseHomeDir) {
|
||||
user.HomeDir = filepath.Join(baseHomeDir, username)
|
||||
} else {
|
||||
user.HomeDir = filepath.Clean(homedir)
|
||||
}
|
||||
logger.Debug(logSender, connectionID, "home dir for new user %q", user.HomeDir)
|
||||
user.Password = connectionID
|
||||
user.Permissions = make(map[string][]string)
|
||||
user.Permissions["/"] = []string{dataprovider.PermAny}
|
||||
err = dataprovider.AddUser(&user, dataprovider.ActionExecutorSystem, "", "")
|
||||
if err != nil {
|
||||
logger.Error(logSender, connectionID, "unable to add user %q: %v", username, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
err = user.LoadAndApplyGroupSettings()
|
||||
if err != nil {
|
||||
logger.Error(logSender, connectionID, "unable to apply group settings for user %q: %v", username, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = sftpd.ServeSubSystemConnection(&user, connectionID, os.Stdin, os.Stdout)
|
||||
if err != nil && err != io.EOF {
|
||||
logger.Warn(logSender, connectionID, "serving subsystem finished with error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
logger.Info(logSender, connectionID, "serving subsystem finished")
|
||||
plugin.Handler.Cleanup()
|
||||
os.Exit(0)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
subsystemCmd.Flags().BoolVarP(&preserveHomeDir, "preserve-home", "p", false, `If the user already exists, the existing home
|
||||
directory will not be changed`)
|
||||
subsystemCmd.Flags().StringVarP(&baseHomeDir, "base-home-dir", "d", "", `If the user does not exist specify an alternate
|
||||
starting directory. The home directory for a new
|
||||
user will be:
|
||||
|
||||
[base-home-dir]/[username]
|
||||
|
||||
base-home-dir must be an absolute path.`)
|
||||
subsystemCmd.Flags().BoolVarP(&logJournalD, "log-to-journald", "j", false, `Send logs to journald. Only available on Linux.
|
||||
Use:
|
||||
|
||||
$ journalctl -o verbose -f
|
||||
|
||||
To see full logs.
|
||||
If not set, the logs will be sent to the standard
|
||||
error`)
|
||||
|
||||
addConfigFlags(subsystemCmd)
|
||||
|
||||
viper.SetDefault(logLevelKey, defaultLogLevel)
|
||||
viper.BindEnv(logLevelKey, "SFTPGO_LOG_LEVEL") //nolint:errcheck
|
||||
subsystemCmd.Flags().StringVar(&logLevel, logLevelFlag, viper.GetString(logLevelKey),
|
||||
`Set the log level. Supported values:
|
||||
|
||||
debug, info, warn, error.
|
||||
|
||||
This flag can be set
|
||||
using SFTPGO_LOG_LEVEL env var too.
|
||||
`)
|
||||
viper.BindPFlag(logLevelKey, subsystemCmd.Flags().Lookup(logLevelFlag)) //nolint:errcheck
|
||||
|
||||
viper.SetDefault(logUTCTimeKey, defaultLogUTCTime)
|
||||
viper.BindEnv(logUTCTimeKey, "SFTPGO_LOG_UTC_TIME") //nolint:errcheck
|
||||
subsystemCmd.Flags().BoolVar(&logUTCTime, logUTCTimeFlag, viper.GetBool(logUTCTimeKey),
|
||||
`Use UTC time for logging. This flag can be set
|
||||
using SFTPGO_LOG_UTC_TIME env var too.
|
||||
`)
|
||||
viper.BindPFlag(logUTCTimeKey, subsystemCmd.Flags().Lookup(logUTCTimeFlag)) //nolint:errcheck
|
||||
|
||||
rootCmd.AddCommand(subsystemCmd)
|
||||
}
|
||||
|
|
@ -17,10 +17,9 @@ package command
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -36,7 +35,6 @@ const (
|
|||
HookStartup = "startup"
|
||||
HookPostConnect = "post_connect"
|
||||
HookPostDisconnect = "post_disconnect"
|
||||
HookDataRetention = "data_retention"
|
||||
HookCheckPassword = "check_password"
|
||||
HookPreLogin = "pre_login"
|
||||
HookPostLogin = "post_login"
|
||||
|
|
@ -47,7 +45,7 @@ const (
|
|||
var (
|
||||
config Config
|
||||
supportedHooks = []string{HookFsActions, HookProviderActions, HookStartup, HookPostConnect, HookPostDisconnect,
|
||||
HookDataRetention, HookCheckPassword, HookPreLogin, HookPostLogin, HookExternalAuth, HookKeyboardInteractive}
|
||||
HookCheckPassword, HookPreLogin, HookPostLogin, HookExternalAuth, HookKeyboardInteractive}
|
||||
)
|
||||
|
||||
// Command define the configuration for a specific commands
|
||||
|
|
@ -117,7 +115,7 @@ func (c Config) Initialize() error {
|
|||
}
|
||||
// don't validate args, we allow to pass empty arguments
|
||||
if cmd.Hook != "" {
|
||||
if !util.Contains(supportedHooks, cmd.Hook) {
|
||||
if !slices.Contains(supportedHooks, cmd.Hook) {
|
||||
return fmt.Errorf("invalid hook name %q, supported values: %+v", cmd.Hook, supportedHooks)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import (
|
|||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
|
@ -37,7 +38,6 @@ import (
|
|||
"github.com/drakkan/sftpgo/v2/internal/httpclient"
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
"github.com/drakkan/sftpgo/v2/internal/plugin"
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -86,13 +86,14 @@ func InitializeActionHandler(handler ActionHandler) {
|
|||
func ExecutePreAction(conn *BaseConnection, operation, filePath, virtualPath string, fileSize int64, openFlags int) (int, error) {
|
||||
var event *notifier.FsEvent
|
||||
hasNotifiersPlugin := plugin.Handler.HasNotifiers()
|
||||
hasHook := util.Contains(Config.Actions.ExecuteOn, operation)
|
||||
hasHook := slices.Contains(Config.Actions.ExecuteOn, operation)
|
||||
hasRules := eventManager.hasFsRules()
|
||||
if !hasHook && !hasNotifiersPlugin && !hasRules {
|
||||
return 0, nil
|
||||
}
|
||||
dateTime := time.Now()
|
||||
event = newActionNotification(&conn.User, operation, filePath, virtualPath, "", "", "",
|
||||
conn.protocol, conn.GetRemoteIP(), conn.ID, fileSize, openFlags, conn.getNotificationStatus(nil), 0, nil)
|
||||
conn.protocol, conn.GetRemoteIP(), conn.ID, fileSize, openFlags, conn.getNotificationStatus(nil), 0, dateTime, nil)
|
||||
if hasNotifiersPlugin {
|
||||
plugin.Handler.NotifyFsEvent(event)
|
||||
}
|
||||
|
|
@ -112,7 +113,7 @@ func ExecutePreAction(conn *BaseConnection, operation, filePath, virtualPath str
|
|||
Protocol: event.Protocol,
|
||||
IP: event.IP,
|
||||
Role: event.Role,
|
||||
Timestamp: event.Timestamp,
|
||||
Timestamp: dateTime,
|
||||
Email: conn.User.Email,
|
||||
Object: nil,
|
||||
}
|
||||
|
|
@ -132,13 +133,14 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua
|
|||
fileSize int64, err error, elapsed int64, metadata map[string]string,
|
||||
) error {
|
||||
hasNotifiersPlugin := plugin.Handler.HasNotifiers()
|
||||
hasHook := util.Contains(Config.Actions.ExecuteOn, operation)
|
||||
hasHook := slices.Contains(Config.Actions.ExecuteOn, operation)
|
||||
hasRules := eventManager.hasFsRules()
|
||||
if !hasHook && !hasNotifiersPlugin && !hasRules {
|
||||
return nil
|
||||
}
|
||||
dateTime := time.Now()
|
||||
notification := newActionNotification(&conn.User, operation, filePath, virtualPath, target, virtualTarget, sshCmd,
|
||||
conn.protocol, conn.GetRemoteIP(), conn.ID, fileSize, 0, conn.getNotificationStatus(err), elapsed, metadata)
|
||||
conn.protocol, conn.GetRemoteIP(), conn.ID, fileSize, 0, conn.getNotificationStatus(err), elapsed, dateTime, metadata)
|
||||
if hasNotifiersPlugin {
|
||||
plugin.Handler.NotifyFsEvent(notification)
|
||||
}
|
||||
|
|
@ -159,7 +161,7 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua
|
|||
Protocol: notification.Protocol,
|
||||
IP: notification.IP,
|
||||
Role: notification.Role,
|
||||
Timestamp: notification.Timestamp,
|
||||
Timestamp: dateTime,
|
||||
Email: conn.User.Email,
|
||||
Object: nil,
|
||||
Metadata: metadata,
|
||||
|
|
@ -173,7 +175,7 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua
|
|||
}
|
||||
}
|
||||
if hasHook {
|
||||
if util.Contains(Config.Actions.ExecuteSync, operation) {
|
||||
if slices.Contains(Config.Actions.ExecuteSync, operation) {
|
||||
_, err := actionHandler.Handle(notification)
|
||||
return err
|
||||
}
|
||||
|
|
@ -197,6 +199,7 @@ func newActionNotification(
|
|||
operation, filePath, virtualPath, target, virtualTarget, sshCmd, protocol, ip, sessionID string,
|
||||
fileSize int64,
|
||||
openFlags, status int, elapsed int64,
|
||||
datetime time.Time,
|
||||
metadata map[string]string,
|
||||
) *notifier.FsEvent {
|
||||
var bucket, endpoint string
|
||||
|
|
@ -238,7 +241,7 @@ func newActionNotification(
|
|||
SessionID: sessionID,
|
||||
OpenFlags: openFlags,
|
||||
Role: user.Role,
|
||||
Timestamp: time.Now().UnixNano(),
|
||||
Timestamp: datetime.UnixNano(),
|
||||
Elapsed: elapsed,
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
|
@ -247,7 +250,7 @@ func newActionNotification(
|
|||
type defaultActionHandler struct{}
|
||||
|
||||
func (h *defaultActionHandler) Handle(event *notifier.FsEvent) (int, error) {
|
||||
if !util.Contains(Config.Actions.ExecuteOn, event.Action) {
|
||||
if !slices.Contains(Config.Actions.ExecuteOn, event.Action) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
|
|
@ -345,7 +348,7 @@ func notificationAsEnvVars(event *notifier.FsEvent) []string {
|
|||
if len(event.Metadata) > 0 {
|
||||
data, err := json.Marshal(event.Metadata)
|
||||
if err == nil {
|
||||
result = append(result, fmt.Sprintf("SFTPGO_ACTION_METADATA=%s", util.BytesToString(data)))
|
||||
result = append(result, fmt.Sprintf("SFTPGO_ACTION_METADATA=%s", data))
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -22,8 +22,9 @@ import (
|
|||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
"github.com/lithammer/shortuuid/v4"
|
||||
"github.com/rs/xid"
|
||||
"github.com/sftpgo/sdk"
|
||||
"github.com/sftpgo/sdk/plugin/notifier"
|
||||
|
|
@ -71,7 +72,7 @@ func TestNewActionNotification(t *testing.T) {
|
|||
c := NewBaseConnection("id", ProtocolSSH, "", "", user)
|
||||
sessionID := xid.New().String()
|
||||
a := newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSFTP, "", sessionID,
|
||||
123, 0, c.getNotificationStatus(errors.New("fake error")), 0, nil)
|
||||
123, 0, c.getNotificationStatus(errors.New("fake error")), 0, time.Now(), nil)
|
||||
assert.Equal(t, user.Username, a.Username)
|
||||
assert.Equal(t, 0, len(a.Bucket))
|
||||
assert.Equal(t, 0, len(a.Endpoint))
|
||||
|
|
@ -79,38 +80,38 @@ func TestNewActionNotification(t *testing.T) {
|
|||
|
||||
user.FsConfig.Provider = sdk.S3FilesystemProvider
|
||||
a = newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSSH, "", sessionID,
|
||||
123, 0, c.getNotificationStatus(nil), 0, nil)
|
||||
123, 0, c.getNotificationStatus(nil), 0, time.Now(), nil)
|
||||
assert.Equal(t, "s3bucket", a.Bucket)
|
||||
assert.Equal(t, "endpoint", a.Endpoint)
|
||||
assert.Equal(t, 1, a.Status)
|
||||
|
||||
user.FsConfig.Provider = sdk.GCSFilesystemProvider
|
||||
a = newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSCP, "", sessionID,
|
||||
123, 0, c.getNotificationStatus(ErrQuotaExceeded), 0, nil)
|
||||
123, 0, c.getNotificationStatus(ErrQuotaExceeded), 0, time.Now(), nil)
|
||||
assert.Equal(t, "gcsbucket", a.Bucket)
|
||||
assert.Equal(t, 0, len(a.Endpoint))
|
||||
assert.Equal(t, 3, a.Status)
|
||||
a = newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSCP, "", sessionID,
|
||||
123, 0, c.getNotificationStatus(fmt.Errorf("wrapper quota error: %w", ErrQuotaExceeded)), 0, nil)
|
||||
123, 0, c.getNotificationStatus(fmt.Errorf("wrapper quota error: %w", ErrQuotaExceeded)), 0, time.Now(), nil)
|
||||
assert.Equal(t, "gcsbucket", a.Bucket)
|
||||
assert.Equal(t, 0, len(a.Endpoint))
|
||||
assert.Equal(t, 3, a.Status)
|
||||
|
||||
user.FsConfig.Provider = sdk.HTTPFilesystemProvider
|
||||
a = newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSSH, "", sessionID,
|
||||
123, 0, c.getNotificationStatus(nil), 0, nil)
|
||||
123, 0, c.getNotificationStatus(nil), 0, time.Now(), nil)
|
||||
assert.Equal(t, "httpendpoint", a.Endpoint)
|
||||
assert.Equal(t, 1, a.Status)
|
||||
|
||||
user.FsConfig.Provider = sdk.AzureBlobFilesystemProvider
|
||||
a = newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSCP, "", sessionID,
|
||||
123, 0, c.getNotificationStatus(nil), 0, nil)
|
||||
123, 0, c.getNotificationStatus(nil), 0, time.Now(), nil)
|
||||
assert.Equal(t, "azcontainer", a.Bucket)
|
||||
assert.Equal(t, "azendpoint", a.Endpoint)
|
||||
assert.Equal(t, 1, a.Status)
|
||||
|
||||
a = newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSCP, "", sessionID,
|
||||
123, os.O_APPEND, c.getNotificationStatus(nil), 0, nil)
|
||||
123, os.O_APPEND, c.getNotificationStatus(nil), 0, time.Now(), nil)
|
||||
assert.Equal(t, "azcontainer", a.Bucket)
|
||||
assert.Equal(t, "azendpoint", a.Endpoint)
|
||||
assert.Equal(t, 1, a.Status)
|
||||
|
|
@ -118,7 +119,7 @@ func TestNewActionNotification(t *testing.T) {
|
|||
|
||||
user.FsConfig.Provider = sdk.SFTPFilesystemProvider
|
||||
a = newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSFTP, "", sessionID,
|
||||
123, 0, c.getNotificationStatus(nil), 0, nil)
|
||||
123, 0, c.getNotificationStatus(nil), 0, time.Now(), nil)
|
||||
assert.Equal(t, "sftpendpoint", a.Endpoint)
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +136,7 @@ func TestActionHTTP(t *testing.T) {
|
|||
},
|
||||
}
|
||||
a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSFTP, "",
|
||||
xid.New().String(), 123, 0, 1, 0, nil)
|
||||
xid.New().String(), 123, 0, 1, 0, time.Now(), nil)
|
||||
status, err := actionHandler.Handle(a)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, status)
|
||||
|
|
@ -175,7 +176,7 @@ func TestActionCMD(t *testing.T) {
|
|||
}
|
||||
sessionID := shortuuid.New()
|
||||
a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSFTP, "", sessionID,
|
||||
123, 0, 1, 0, map[string]string{"key": "value"})
|
||||
123, 0, 1, 0, time.Now(), map[string]string{"key": "value"})
|
||||
status, err := actionHandler.Handle(a)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, status)
|
||||
|
|
@ -208,7 +209,7 @@ func TestWrongActions(t *testing.T) {
|
|||
}
|
||||
|
||||
a := newActionNotification(user, operationUpload, "", "", "", "", "", ProtocolSFTP, "", xid.New().String(),
|
||||
123, 0, 1, 0, nil)
|
||||
123, 0, 1, 0, time.Now(), nil)
|
||||
status, err := actionHandler.Handle(a)
|
||||
assert.Error(t, err, "action with bad command must fail")
|
||||
assert.Equal(t, 1, status)
|
||||
|
|
|
|||
|
|
@ -19,12 +19,14 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
|
@ -124,6 +126,9 @@ func init() {
|
|||
Connections.clients = clientsMap{
|
||||
clients: make(map[string]int),
|
||||
}
|
||||
Connections.transfers = clientsMap{
|
||||
clients: make(map[string]int),
|
||||
}
|
||||
Connections.perUserConns = make(map[string]int)
|
||||
Connections.mapping = make(map[string]int)
|
||||
Connections.sshMapping = make(map[string]int)
|
||||
|
|
@ -163,13 +168,20 @@ var (
|
|||
rateLimiters map[string][]*rateLimiter
|
||||
isShuttingDown atomic.Bool
|
||||
ftpLoginCommands = []string{"PASS", "USER"}
|
||||
fnUpdateBranding func(*dataprovider.BrandingConfigs)
|
||||
)
|
||||
|
||||
// SetUpdateBrandingFn sets the function to call to update branding configs.
|
||||
func SetUpdateBrandingFn(fn func(*dataprovider.BrandingConfigs)) {
|
||||
fnUpdateBranding = fn
|
||||
}
|
||||
|
||||
// Initialize sets the common configuration
|
||||
func Initialize(c Configuration, isShared int) error {
|
||||
isShuttingDown.Store(false)
|
||||
util.SetUmask(c.Umask)
|
||||
version.SetConfig(c.ServerVersion)
|
||||
dataprovider.SetTZ(c.TZ)
|
||||
Config = c
|
||||
Config.Actions.ExecuteOn = util.RemoveDuplicates(Config.Actions.ExecuteOn, true)
|
||||
Config.Actions.ExecuteSync = util.RemoveDuplicates(Config.Actions.ExecuteSync, true)
|
||||
|
|
@ -200,7 +212,7 @@ func Initialize(c Configuration, isShared int) error {
|
|||
Config.rateLimitersList = rateLimitersList
|
||||
}
|
||||
if c.DefenderConfig.Enabled {
|
||||
if !util.Contains(supportedDefenderDrivers, c.DefenderConfig.Driver) {
|
||||
if !slices.Contains(supportedDefenderDrivers, c.DefenderConfig.Driver) {
|
||||
return fmt.Errorf("unsupported defender driver %q", c.DefenderConfig.Driver)
|
||||
}
|
||||
var defender Defender
|
||||
|
|
@ -228,6 +240,9 @@ func Initialize(c Configuration, isShared int) error {
|
|||
if err := c.initializeProxyProtocol(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.EventManager.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
vfs.SetTempPath(c.TempPath)
|
||||
dataprovider.SetTempPath(c.TempPath)
|
||||
vfs.SetAllowSelfConnections(c.AllowSelfConnections)
|
||||
|
|
@ -236,6 +251,7 @@ func Initialize(c Configuration, isShared int) error {
|
|||
vfs.SetResumeMaxSize(c.ResumeMaxSize)
|
||||
vfs.SetUploadMode(c.UploadMode)
|
||||
dataprovider.SetAllowSelfConnections(c.AllowSelfConnections)
|
||||
dataprovider.EnabledActionCommands = c.EventManager.EnabledCommands
|
||||
transfersChecker = getTransfersChecker(isShared)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -327,6 +343,13 @@ func Reload() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// DelayLogin applies the configured login delay
|
||||
func DelayLogin(err error) {
|
||||
if Config.defender != nil {
|
||||
Config.defender.DelayLogin(err)
|
||||
}
|
||||
}
|
||||
|
||||
// IsBanned returns true if the specified IP address is banned
|
||||
func IsBanned(ip, protocol string) bool {
|
||||
if plugin.Handler.IsIPBanned(ip, protocol) {
|
||||
|
|
@ -395,6 +418,23 @@ func AddDefenderEvent(ip, protocol string, event HostEvent) bool {
|
|||
return Config.defender.AddEvent(ip, protocol, event)
|
||||
}
|
||||
|
||||
func reloadProviderConfigs() {
|
||||
configs, err := dataprovider.GetConfigs()
|
||||
if err != nil {
|
||||
logger.Error(logSender, "", "unable to load config from provider: %v", err)
|
||||
return
|
||||
}
|
||||
configs.SetNilsToEmpty()
|
||||
if fnUpdateBranding != nil {
|
||||
fnUpdateBranding(configs.Branding)
|
||||
}
|
||||
if err := configs.SMTP.TryDecrypt(); err != nil {
|
||||
logger.Error(logSender, "", "unable to decrypt smtp config: %v", err)
|
||||
return
|
||||
}
|
||||
smtp.Activate(configs.SMTP)
|
||||
}
|
||||
|
||||
func startPeriodicChecks(duration time.Duration, isShared int) {
|
||||
startEventScheduler()
|
||||
spec := fmt.Sprintf("@every %s", duration)
|
||||
|
|
@ -403,7 +443,7 @@ func startPeriodicChecks(duration time.Duration, isShared int) {
|
|||
logger.Info(logSender, "", "scheduled overquota transfers check, schedule %q", spec)
|
||||
if isShared == 1 {
|
||||
logger.Info(logSender, "", "add reload configs task")
|
||||
_, err := eventScheduler.AddFunc("@every 10m", smtp.ReloadProviderConf)
|
||||
_, err := eventScheduler.AddFunc("@every 10m", reloadProviderConfigs)
|
||||
util.PanicOnError(err)
|
||||
}
|
||||
if Config.IdleTimeout > 0 {
|
||||
|
|
@ -423,6 +463,7 @@ type ActiveTransfer interface {
|
|||
GetDownloadedSize() int64
|
||||
GetUploadedSize() int64
|
||||
GetVirtualPath() string
|
||||
GetFsPath() string
|
||||
GetStartTime() time.Time
|
||||
SignalClose(err error)
|
||||
Truncate(fsPath string, size int64) (int64, error)
|
||||
|
|
@ -477,6 +518,23 @@ type ConnectionTransfer struct {
|
|||
DLSize int64 `json:"-"`
|
||||
}
|
||||
|
||||
// EventManagerConfig defines the configuration for the EventManager
|
||||
type EventManagerConfig struct {
|
||||
// EnabledCommands defines the system commands that can be executed via EventManager,
|
||||
// an empty list means that any command is allowed to be executed.
|
||||
// Commands must be set as an absolute path
|
||||
EnabledCommands []string `json:"enabled_commands" mapstructure:"enabled_commands"`
|
||||
}
|
||||
|
||||
func (c *EventManagerConfig) validate() error {
|
||||
for _, c := range c.EnabledCommands {
|
||||
if !filepath.IsAbs(c) {
|
||||
return fmt.Errorf("invalid command %q: it must be an absolute path", c)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MetadataConfig defines how to handle metadata for cloud storage backends
|
||||
type MetadataConfig struct {
|
||||
// If not zero the metadata will be read before downloads and will be
|
||||
|
|
@ -558,9 +616,6 @@ type Configuration struct {
|
|||
// Absolute path to an external program or an HTTP URL to invoke after an SSH/FTP connection ends.
|
||||
// Leave empty do disable.
|
||||
PostDisconnectHook string `json:"post_disconnect_hook" mapstructure:"post_disconnect_hook"`
|
||||
// Absolute path to an external program or an HTTP URL to invoke after a data retention check completes.
|
||||
// Leave empty do disable.
|
||||
DataRetentionHook string `json:"data_retention_hook" mapstructure:"data_retention_hook"`
|
||||
// Maximum number of concurrent client connections. 0 means unlimited
|
||||
MaxTotalConnections int `json:"max_total_connections" mapstructure:"max_total_connections"`
|
||||
// Maximum number of concurrent client connections from the same host (IP). 0 means unlimited
|
||||
|
|
@ -581,8 +636,14 @@ type Configuration struct {
|
|||
Umask string `json:"umask" mapstructure:"umask"`
|
||||
// Defines the server version
|
||||
ServerVersion string `json:"server_version" mapstructure:"server_version"`
|
||||
// TZ defines the time zone to use for the EventManager scheduler and to
|
||||
// control time-based access restrictions. Set to "local" to use the
|
||||
// server's local time, otherwise UTC will be used.
|
||||
TZ string `json:"tz" mapstructure:"tz"`
|
||||
// Metadata configuration
|
||||
Metadata MetadataConfig `json:"metadata" mapstructure:"metadata"`
|
||||
Metadata MetadataConfig `json:"metadata" mapstructure:"metadata"`
|
||||
// EventManager configuration
|
||||
EventManager EventManagerConfig `json:"event_manager" mapstructure:"event_manager"`
|
||||
idleTimeoutAsDuration time.Duration
|
||||
idleLoginTimeout time.Duration
|
||||
defender Defender
|
||||
|
|
@ -615,7 +676,7 @@ func (c *Configuration) initializeProxyProtocol() error {
|
|||
|
||||
// GetProxyListener returns a wrapper for the given listener that supports the
|
||||
// HAProxy Proxy Protocol
|
||||
func (c *Configuration) GetProxyListener(listener net.Listener) (*proxyproto.Listener, error) {
|
||||
func (c *Configuration) GetProxyListener(listener net.Listener) (net.Listener, error) {
|
||||
if c.ProxyProtocol > 0 {
|
||||
defaultPolicy := proxyproto.REQUIRE
|
||||
if c.ProxyProtocol == 1 {
|
||||
|
|
@ -624,7 +685,7 @@ func (c *Configuration) GetProxyListener(listener net.Listener) (*proxyproto.Lis
|
|||
|
||||
return &proxyproto.Listener{
|
||||
Listener: listener,
|
||||
Policy: getProxyPolicy(c.proxyAllowed, c.proxySkipped, defaultPolicy),
|
||||
ConnPolicy: getProxyPolicy(c.proxyAllowed, c.proxySkipped, defaultPolicy),
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -742,7 +803,7 @@ func (c *Configuration) checkPostDisconnectHook(remoteAddr, protocol, username,
|
|||
if c.PostDisconnectHook == "" {
|
||||
return
|
||||
}
|
||||
if !util.Contains(disconnHookProtocols, protocol) {
|
||||
if !slices.Contains(disconnHookProtocols, protocol) {
|
||||
return
|
||||
}
|
||||
go c.executePostDisconnectHook(remoteAddr, protocol, username, connID, connectionTime)
|
||||
|
|
@ -799,12 +860,14 @@ func (c *Configuration) ExecutePostConnectHook(ipAddr, protocol string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func getProxyPolicy(allowed, skipped []func(net.IP) bool, def proxyproto.Policy) proxyproto.PolicyFunc {
|
||||
return func(upstream net.Addr) (proxyproto.Policy, error) {
|
||||
upstreamIP, err := util.GetIPFromNetAddr(upstream)
|
||||
func getProxyPolicy(allowed, skipped []func(net.IP) bool, def proxyproto.Policy) proxyproto.ConnPolicyFunc {
|
||||
return func(connPolicyOptions proxyproto.ConnPolicyOptions) (proxyproto.Policy, error) {
|
||||
upstreamIP, err := util.GetIPFromNetAddr(connPolicyOptions.Upstream)
|
||||
if err != nil {
|
||||
// something is wrong with the source IP, better reject the connection
|
||||
return proxyproto.REJECT, err
|
||||
// Something is wrong with the source IP, better reject the
|
||||
// connection.
|
||||
logger.Error(logSender, "", "reject connection from ip %q, err: %v", connPolicyOptions.Upstream, err)
|
||||
return proxyproto.REJECT, proxyproto.ErrInvalidUpstream
|
||||
}
|
||||
|
||||
for _, skippedFrom := range skipped {
|
||||
|
|
@ -822,6 +885,11 @@ func getProxyPolicy(allowed, skipped []func(net.IP) bool, def proxyproto.Policy)
|
|||
}
|
||||
}
|
||||
|
||||
if def == proxyproto.REQUIRE {
|
||||
logger.Debug(logSender, "", "reject connection from ip %q: proxy protocol signature required and not set",
|
||||
upstreamIP)
|
||||
return proxyproto.REJECT, proxyproto.ErrInvalidUpstream
|
||||
}
|
||||
return def, nil
|
||||
}
|
||||
}
|
||||
|
|
@ -830,12 +898,12 @@ func getProxyPolicy(allowed, skipped []func(net.IP) bool, def proxyproto.Policy)
|
|||
// Each SSH connection can open several channels for SFTP or SSH commands
|
||||
type SSHConnection struct {
|
||||
id string
|
||||
conn net.Conn
|
||||
conn io.Closer
|
||||
lastActivity atomic.Int64
|
||||
}
|
||||
|
||||
// NewSSHConnection returns a new SSHConnection
|
||||
func NewSSHConnection(id string, conn net.Conn) *SSHConnection {
|
||||
func NewSSHConnection(id string, conn io.Closer) *SSHConnection {
|
||||
c := &SSHConnection{
|
||||
id: id,
|
||||
conn: conn,
|
||||
|
|
@ -868,7 +936,9 @@ func (c *SSHConnection) Close() error {
|
|||
type ActiveConnections struct {
|
||||
// clients contains both authenticated and estabilished connections and the ones waiting
|
||||
// for authentication
|
||||
clients clientsMap
|
||||
clients clientsMap
|
||||
// transfers contains active transfers, total and per-user
|
||||
transfers clientsMap
|
||||
transfersCheckStatus atomic.Bool
|
||||
sync.RWMutex
|
||||
connections []ActiveConnection
|
||||
|
|
@ -919,6 +989,9 @@ func (conns *ActiveConnections) Add(c ActiveConnection) error {
|
|||
if val := conns.perUserConns[username]; val >= maxSessions {
|
||||
return fmt.Errorf("too many open sessions: %d/%d", val, maxSessions)
|
||||
}
|
||||
if val := conns.transfers.getTotalFrom(username); val >= maxSessions {
|
||||
return fmt.Errorf("too many open transfers: %d/%d", val, maxSessions)
|
||||
}
|
||||
}
|
||||
conns.addUserConnection(username)
|
||||
}
|
||||
|
|
@ -980,7 +1053,7 @@ func (conns *ActiveConnections) Remove(connectionID string) {
|
|||
metric.UpdateActiveConnectionsSize(lastIdx)
|
||||
logger.Debug(conn.GetProtocol(), conn.GetID(), "connection removed, local address %q, remote address %q close fs error: %v, num open connections: %d",
|
||||
conn.GetLocalAddress(), conn.GetRemoteAddress(), err, lastIdx)
|
||||
if conn.GetProtocol() == ProtocolFTP && conn.GetUsername() == "" && !util.Contains(ftpLoginCommands, conn.GetCommand()) {
|
||||
if conn.GetProtocol() == ProtocolFTP && conn.GetUsername() == "" && !slices.Contains(ftpLoginCommands, conn.GetCommand()) {
|
||||
ip := util.GetIPFromRemoteAddress(conn.GetRemoteAddress())
|
||||
logger.ConnectionFailedLog("", ip, dataprovider.LoginMethodNoAuthTried, ProtocolFTP,
|
||||
dataprovider.ErrNoAuthTried.Error())
|
||||
|
|
@ -1089,7 +1162,7 @@ func (conns *ActiveConnections) checkIdles() {
|
|||
logger.Debug(conn.GetProtocol(), conn.GetID(), "close idle connection, idle time: %s, username: %q close err: %v",
|
||||
time.Since(conn.GetLastActivity()), conn.GetUsername(), err)
|
||||
}(c)
|
||||
} else if !c.isAccessAllowed() {
|
||||
} else if !isUnauthenticatedFTPUser && !c.isAccessAllowed() {
|
||||
defer func(conn ActiveConnection) {
|
||||
err := conn.Disconnect()
|
||||
logger.Info(conn.GetProtocol(), conn.GetID(), "access conditions not met for user: %q close connection err: %v",
|
||||
|
|
@ -1179,6 +1252,35 @@ func (conns *ActiveConnections) GetClientConnections() int32 {
|
|||
return conns.clients.getTotal()
|
||||
}
|
||||
|
||||
// GetTotalTransfers returns the total number of active transfers
|
||||
func (conns *ActiveConnections) GetTotalTransfers() int32 {
|
||||
return conns.transfers.getTotal()
|
||||
}
|
||||
|
||||
// IsNewTransferAllowed returns an error if the maximum number of concurrent allowed
|
||||
// transfers is exceeded
|
||||
func (conns *ActiveConnections) IsNewTransferAllowed(username string) error {
|
||||
if isShuttingDown.Load() {
|
||||
return ErrShuttingDown
|
||||
}
|
||||
if Config.MaxTotalConnections == 0 && Config.MaxPerHostConnections == 0 {
|
||||
return nil
|
||||
}
|
||||
if Config.MaxPerHostConnections > 0 {
|
||||
if transfers := conns.transfers.getTotalFrom(username); transfers >= Config.MaxPerHostConnections {
|
||||
logger.Info(logSender, "", "active transfers from user %q: %d/%d", username, transfers, Config.MaxPerHostConnections)
|
||||
return ErrConnectionDenied
|
||||
}
|
||||
}
|
||||
if Config.MaxTotalConnections > 0 {
|
||||
if transfers := conns.transfers.getTotal(); transfers >= int32(Config.MaxTotalConnections) {
|
||||
logger.Info(logSender, "", "active transfers %d/%d", transfers, Config.MaxTotalConnections)
|
||||
return ErrConnectionDenied
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsNewConnectionAllowed returns an error if the maximum number of concurrent allowed
|
||||
// connections is exceeded or a whitelist is defined and the specified ipAddr is not listed
|
||||
// or the service is shutting down
|
||||
|
|
@ -1219,7 +1321,11 @@ func (conns *ActiveConnections) IsNewConnectionAllowed(ipAddr, protocol string)
|
|||
}
|
||||
|
||||
// on a single SFTP connection we could have multiple SFTP channels or commands
|
||||
// so we check the estabilished connections too
|
||||
// so we check the estabilished connections and active uploads too
|
||||
if transfers := conns.transfers.getTotal(); transfers >= int32(Config.MaxTotalConnections) {
|
||||
logger.Info(logSender, "", "active transfers %d/%d", transfers, Config.MaxTotalConnections)
|
||||
return ErrConnectionDenied
|
||||
}
|
||||
|
||||
conns.RLock()
|
||||
defer conns.RUnlock()
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import (
|
|||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
|
@ -63,7 +64,7 @@ func (c *fakeConnection) AddUser(user dataprovider.User) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.BaseConnection.User = user
|
||||
c.User = user
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -216,6 +217,33 @@ func TestConnections(t *testing.T) {
|
|||
Connections.RUnlock()
|
||||
}
|
||||
|
||||
func TestEventManagerCommandsInitialization(t *testing.T) {
|
||||
configCopy := Config
|
||||
|
||||
c := Configuration{
|
||||
EventManager: EventManagerConfig{
|
||||
EnabledCommands: []string{"ls"}, // not an absolute path
|
||||
},
|
||||
}
|
||||
err := Initialize(c, 0)
|
||||
assert.ErrorContains(t, err, "invalid command")
|
||||
|
||||
var commands []string
|
||||
if runtime.GOOS == osWindows {
|
||||
commands = []string{"C:\\command"}
|
||||
} else {
|
||||
commands = []string{"/bin/ls"}
|
||||
}
|
||||
|
||||
c.EventManager.EnabledCommands = commands
|
||||
err = Initialize(c, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, commands, dataprovider.EnabledActionCommands)
|
||||
|
||||
dataprovider.EnabledActionCommands = configCopy.EventManager.EnabledCommands
|
||||
Config = configCopy
|
||||
}
|
||||
|
||||
func TestInitializationProxyErrors(t *testing.T) {
|
||||
configCopy := Config
|
||||
|
||||
|
|
@ -419,6 +447,9 @@ func TestDefenderIntegration(t *testing.T) {
|
|||
ObservationTime: 15,
|
||||
EntriesSoftLimit: 100,
|
||||
EntriesHardLimit: 150,
|
||||
LoginDelay: LoginDelay{
|
||||
PasswordFailed: 200,
|
||||
},
|
||||
}
|
||||
err = Initialize(Config, 0)
|
||||
// ScoreInvalid cannot be greater than threshold
|
||||
|
|
@ -477,6 +508,16 @@ func TestDefenderIntegration(t *testing.T) {
|
|||
assert.Nil(t, banTime)
|
||||
assert.False(t, DeleteDefenderHost(ip))
|
||||
|
||||
startTime := time.Now()
|
||||
DelayLogin(nil)
|
||||
elapsed := time.Since(startTime)
|
||||
assert.Less(t, elapsed, time.Millisecond*50)
|
||||
|
||||
startTime = time.Now()
|
||||
DelayLogin(ErrInternalFailure)
|
||||
elapsed = time.Since(startTime)
|
||||
assert.Greater(t, elapsed, time.Millisecond*150)
|
||||
|
||||
Config = configCopy
|
||||
}
|
||||
|
||||
|
|
@ -612,11 +653,17 @@ func TestMaxConnections(t *testing.T) {
|
|||
|
||||
ipAddr := "192.168.7.8"
|
||||
assert.NoError(t, Connections.IsNewConnectionAllowed(ipAddr, ProtocolFTP))
|
||||
assert.NoError(t, Connections.IsNewTransferAllowed(userTestUsername))
|
||||
|
||||
Config.MaxTotalConnections = 1
|
||||
Config.MaxPerHostConnections = perHost
|
||||
|
||||
assert.NoError(t, Connections.IsNewConnectionAllowed(ipAddr, ProtocolHTTP))
|
||||
assert.NoError(t, Connections.IsNewTransferAllowed(userTestUsername))
|
||||
isShuttingDown.Store(true)
|
||||
assert.ErrorIs(t, Connections.IsNewTransferAllowed(userTestUsername), ErrShuttingDown)
|
||||
isShuttingDown.Store(false)
|
||||
|
||||
c := NewBaseConnection("id", ProtocolSFTP, "", "", dataprovider.User{})
|
||||
fakeConn := &fakeConnection{
|
||||
BaseConnection: c,
|
||||
|
|
@ -625,6 +672,10 @@ func TestMaxConnections(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Len(t, Connections.GetStats(""), 1)
|
||||
assert.Error(t, Connections.IsNewConnectionAllowed(ipAddr, ProtocolSSH))
|
||||
Connections.transfers.add(userTestUsername)
|
||||
assert.Error(t, Connections.IsNewTransferAllowed(userTestUsername))
|
||||
Connections.transfers.remove(userTestUsername)
|
||||
assert.Equal(t, int32(0), Connections.GetTotalTransfers())
|
||||
|
||||
res := Connections.Close(fakeConn.GetID(), "")
|
||||
assert.True(t, res)
|
||||
|
|
@ -636,6 +687,9 @@ func TestMaxConnections(t *testing.T) {
|
|||
assert.Error(t, Connections.IsNewConnectionAllowed(ipAddr, ProtocolSSH))
|
||||
Connections.RemoveClientConnection(ipAddr)
|
||||
assert.NoError(t, Connections.IsNewConnectionAllowed(ipAddr, ProtocolWebDAV))
|
||||
Connections.transfers.add(userTestUsername)
|
||||
assert.Error(t, Connections.IsNewConnectionAllowed(ipAddr, ProtocolSSH))
|
||||
Connections.transfers.remove(userTestUsername)
|
||||
Connections.RemoveClientConnection(ipAddr)
|
||||
|
||||
Config.MaxTotalConnections = oldValue
|
||||
|
|
@ -774,11 +828,7 @@ func TestIdleConnections(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, Connections.GetActiveSessions(username), 2)
|
||||
|
||||
cFTP := NewBaseConnection("id2", ProtocolFTP, "", "", dataprovider.User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
Status: 1,
|
||||
},
|
||||
})
|
||||
cFTP := NewBaseConnection("id2", ProtocolFTP, "", "", dataprovider.User{})
|
||||
cFTP.lastActivity.Store(time.Now().UnixNano())
|
||||
fakeConn = &fakeConnection{
|
||||
BaseConnection: cFTP,
|
||||
|
|
@ -945,9 +995,10 @@ func TestConnectionStatus(t *testing.T) {
|
|||
assert.Len(t, stats, 3)
|
||||
for _, stat := range stats {
|
||||
assert.Equal(t, stat.Username, username)
|
||||
if stat.ConnectionID == "SFTP_id1" {
|
||||
switch stat.ConnectionID {
|
||||
case "SFTP_id1":
|
||||
assert.Len(t, stat.Transfers, 2)
|
||||
} else if stat.ConnectionID == "DAV_id3" {
|
||||
case "DAV_id3":
|
||||
assert.Len(t, stat.Transfers, 1)
|
||||
}
|
||||
}
|
||||
|
|
@ -1028,9 +1079,13 @@ func TestQuotaScansRole(t *testing.T) {
|
|||
|
||||
func TestProxyPolicy(t *testing.T) {
|
||||
addr := net.TCPAddr{}
|
||||
downstream := net.TCPAddr{IP: net.ParseIP("1.1.1.1")}
|
||||
p := getProxyPolicy(nil, nil, proxyproto.IGNORE)
|
||||
policy, err := p(&addr)
|
||||
assert.Error(t, err)
|
||||
policy, err := p(proxyproto.ConnPolicyOptions{
|
||||
Upstream: &addr,
|
||||
Downstream: &downstream,
|
||||
})
|
||||
assert.ErrorIs(t, err, proxyproto.ErrInvalidUpstream)
|
||||
assert.Equal(t, proxyproto.REJECT, policy)
|
||||
ip1 := net.ParseIP("10.8.1.1")
|
||||
ip2 := net.ParseIP("10.8.1.2")
|
||||
|
|
@ -1040,31 +1095,55 @@ func TestProxyPolicy(t *testing.T) {
|
|||
skipped, err := util.ParseAllowedIPAndRanges([]string{ip2.String(), ip3.String()})
|
||||
assert.NoError(t, err)
|
||||
p = getProxyPolicy(allowed, skipped, proxyproto.IGNORE)
|
||||
policy, err = p(&net.TCPAddr{IP: ip1})
|
||||
policy, err = p(proxyproto.ConnPolicyOptions{
|
||||
Upstream: &net.TCPAddr{IP: ip1},
|
||||
Downstream: &downstream,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, proxyproto.USE, policy)
|
||||
policy, err = p(&net.TCPAddr{IP: ip2})
|
||||
policy, err = p(proxyproto.ConnPolicyOptions{
|
||||
Upstream: &net.TCPAddr{IP: ip2},
|
||||
Downstream: &downstream,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, proxyproto.SKIP, policy)
|
||||
policy, err = p(&net.TCPAddr{IP: ip3})
|
||||
policy, err = p(proxyproto.ConnPolicyOptions{
|
||||
Upstream: &net.TCPAddr{IP: ip3},
|
||||
Downstream: &downstream,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, proxyproto.SKIP, policy)
|
||||
policy, err = p(&net.TCPAddr{IP: net.ParseIP("10.8.1.4")})
|
||||
policy, err = p(proxyproto.ConnPolicyOptions{
|
||||
Upstream: &net.TCPAddr{IP: net.ParseIP("10.8.1.4")},
|
||||
Downstream: &downstream,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, proxyproto.IGNORE, policy)
|
||||
p = getProxyPolicy(allowed, skipped, proxyproto.REQUIRE)
|
||||
policy, err = p(&net.TCPAddr{IP: ip1})
|
||||
policy, err = p(proxyproto.ConnPolicyOptions{
|
||||
Upstream: &net.TCPAddr{IP: ip1},
|
||||
Downstream: &downstream,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, proxyproto.REQUIRE, policy)
|
||||
policy, err = p(&net.TCPAddr{IP: ip2})
|
||||
policy, err = p(proxyproto.ConnPolicyOptions{
|
||||
Upstream: &net.TCPAddr{IP: ip2},
|
||||
Downstream: &downstream,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, proxyproto.SKIP, policy)
|
||||
policy, err = p(&net.TCPAddr{IP: ip3})
|
||||
policy, err = p(proxyproto.ConnPolicyOptions{
|
||||
Upstream: &net.TCPAddr{IP: ip3},
|
||||
Downstream: &downstream,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, proxyproto.SKIP, policy)
|
||||
policy, err = p(&net.TCPAddr{IP: net.ParseIP("10.8.1.5")})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, proxyproto.REQUIRE, policy)
|
||||
policy, err = p(proxyproto.ConnPolicyOptions{
|
||||
Upstream: &net.TCPAddr{IP: net.ParseIP("10.8.1.5")},
|
||||
Downstream: &downstream,
|
||||
})
|
||||
assert.ErrorIs(t, err, proxyproto.ErrInvalidUpstream)
|
||||
assert.Equal(t, proxyproto.REJECT, policy)
|
||||
}
|
||||
|
||||
func TestProxyProtocolVersion(t *testing.T) {
|
||||
|
|
@ -1076,14 +1155,18 @@ func TestProxyProtocolVersion(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "proxy protocol not configured")
|
||||
}
|
||||
c.ProxyProtocol = 1
|
||||
proxyListener, err := c.GetProxyListener(nil)
|
||||
listener, err := c.GetProxyListener(nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, proxyListener.Policy)
|
||||
proxyListener, ok := listener.(*proxyproto.Listener)
|
||||
require.True(t, ok)
|
||||
assert.NotNil(t, proxyListener.ConnPolicy)
|
||||
|
||||
c.ProxyProtocol = 2
|
||||
proxyListener, err = c.GetProxyListener(nil)
|
||||
listener, err = c.GetProxyListener(nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, proxyListener.Policy)
|
||||
proxyListener, ok = listener.(*proxyproto.Listener)
|
||||
require.True(t, ok)
|
||||
assert.NotNil(t, proxyListener.ConnPolicy)
|
||||
}
|
||||
|
||||
func TestStartupHook(t *testing.T) {
|
||||
|
|
@ -1213,8 +1296,8 @@ func TestFolderCopy(t *testing.T) {
|
|||
folder.ID = 2
|
||||
folder.Users = []string{"user3"}
|
||||
require.Len(t, folderCopy.Users, 2)
|
||||
require.True(t, util.Contains(folderCopy.Users, "user1"))
|
||||
require.True(t, util.Contains(folderCopy.Users, "user2"))
|
||||
require.True(t, slices.Contains(folderCopy.Users, "user1"))
|
||||
require.True(t, slices.Contains(folderCopy.Users, "user2"))
|
||||
require.Equal(t, int64(1), folderCopy.ID)
|
||||
require.Equal(t, folder.Name, folderCopy.Name)
|
||||
require.Equal(t, folder.MappedPath, folderCopy.MappedPath)
|
||||
|
|
@ -1230,7 +1313,7 @@ func TestFolderCopy(t *testing.T) {
|
|||
folderCopy = folder.GetACopy()
|
||||
folder.FsConfig.CryptConfig.Passphrase = kms.NewEmptySecret()
|
||||
require.Len(t, folderCopy.Users, 1)
|
||||
require.True(t, util.Contains(folderCopy.Users, "user3"))
|
||||
require.True(t, slices.Contains(folderCopy.Users, "user3"))
|
||||
require.Equal(t, int64(2), folderCopy.ID)
|
||||
require.Equal(t, folder.Name, folderCopy.Name)
|
||||
require.Equal(t, folder.MappedPath, folderCopy.MappedPath)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
|
@ -62,7 +63,7 @@ type BaseConnection struct {
|
|||
// NewBaseConnection returns a new BaseConnection
|
||||
func NewBaseConnection(id, protocol, localAddr, remoteAddr string, user dataprovider.User) *BaseConnection {
|
||||
connID := id
|
||||
if util.Contains(supportedProtocols, protocol) {
|
||||
if slices.Contains(supportedProtocols, protocol) {
|
||||
connID = fmt.Sprintf("%s_%s", protocol, id)
|
||||
}
|
||||
user.UploadBandwidth, user.DownloadBandwidth = user.GetBandwidthForIP(util.GetIPFromRemoteAddress(remoteAddr), connID)
|
||||
|
|
@ -131,7 +132,7 @@ func (c *BaseConnection) GetRemoteIP() string {
|
|||
// SetProtocol sets the protocol for this connection
|
||||
func (c *BaseConnection) SetProtocol(protocol string) {
|
||||
c.protocol = protocol
|
||||
if util.Contains(supportedProtocols, c.protocol) {
|
||||
if slices.Contains(supportedProtocols, c.protocol) {
|
||||
c.ID = fmt.Sprintf("%v_%v", c.protocol, c.ID)
|
||||
}
|
||||
}
|
||||
|
|
@ -158,6 +159,8 @@ func (c *BaseConnection) CloseFS() error {
|
|||
|
||||
// AddTransfer associates a new transfer to this connection
|
||||
func (c *BaseConnection) AddTransfer(t ActiveTransfer) {
|
||||
Connections.transfers.add(c.User.Username)
|
||||
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
|
|
@ -189,6 +192,8 @@ func (c *BaseConnection) AddTransfer(t ActiveTransfer) {
|
|||
|
||||
// RemoveTransfer removes the specified transfer from the active ones
|
||||
func (c *BaseConnection) RemoveTransfer(t ActiveTransfer) {
|
||||
Connections.transfers.remove(c.User.Username)
|
||||
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
|
|
@ -291,6 +296,20 @@ func (c *BaseConnection) setTimes(fsPath string, atime time.Time, mtime time.Tim
|
|||
return false
|
||||
}
|
||||
|
||||
// getInfoForOngoingUpload returns upload statistics for an upload currently in
|
||||
// progress on this connection.
|
||||
func (c *BaseConnection) getInfoForOngoingUpload(fsPath string) (os.FileInfo, error) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
|
||||
for _, t := range c.activeTransfers {
|
||||
if t.GetType() == TransferUpload && t.GetFsPath() == fsPath {
|
||||
return vfs.NewFileInfo(t.GetVirtualPath(), false, t.GetSize(), t.GetStartTime(), false), nil
|
||||
}
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
func (c *BaseConnection) truncateOpenHandle(fsPath string, size int64) (int64, error) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
|
|
@ -321,10 +340,9 @@ func (c *BaseConnection) ListDir(virtualPath string) (*DirListerAt, error) {
|
|||
}
|
||||
return &DirListerAt{
|
||||
virtualPath: virtualPath,
|
||||
user: &c.User,
|
||||
conn: c,
|
||||
fs: fs,
|
||||
info: c.User.GetVirtualFoldersInfo(virtualPath),
|
||||
id: c.ID,
|
||||
protocol: c.protocol,
|
||||
lister: lister,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -449,10 +467,7 @@ func (c *BaseConnection) RemoveFile(fs vfs.Fs, fsPath, virtualPath string, info
|
|||
if updateQuota && info.Mode()&os.ModeSymlink == 0 {
|
||||
vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(virtualPath))
|
||||
if err == nil {
|
||||
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, -1, -size, false) //nolint:errcheck
|
||||
if vfolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.User, -1, -size, false) //nolint:errcheck
|
||||
}
|
||||
dataprovider.UpdateUserFolderQuota(&vfolder, &c.User, -1, -size, false)
|
||||
} else {
|
||||
dataprovider.UpdateUserQuota(&c.User, -1, -size, false) //nolint:errcheck
|
||||
}
|
||||
|
|
@ -616,13 +631,15 @@ func (c *BaseConnection) checkCopy(srcInfo, dstInfo os.FileInfo, virtualSource,
|
|||
if dstInfo != nil && dstInfo.IsDir() {
|
||||
return fmt.Errorf("cannot overwrite file %q with dir %q: %w", virtualSource, virtualTarget, c.GetOpUnsupportedError())
|
||||
}
|
||||
if fsSourcePath == fsTargetPath {
|
||||
return fmt.Errorf("the copy source and target cannot be the same: %w", c.GetOpUnsupportedError())
|
||||
if c.IsSameResource(virtualSource, virtualTarget) {
|
||||
if fsSourcePath == fsTargetPath {
|
||||
return fmt.Errorf("the copy source and target cannot be the same: %w", c.GetOpUnsupportedError())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *BaseConnection) copyFile(virtualSourcePath, virtualTargetPath string, srcSize int64) error {
|
||||
func (c *BaseConnection) copyFile(virtualSourcePath, virtualTargetPath string, srcInfo os.FileInfo) error {
|
||||
if !c.User.HasPerm(dataprovider.PermCopy, virtualSourcePath) || !c.User.HasPerm(dataprovider.PermCopy, virtualTargetPath) {
|
||||
return c.GetPermissionDeniedError()
|
||||
}
|
||||
|
|
@ -640,12 +657,12 @@ func (c *BaseConnection) copyFile(virtualSourcePath, virtualTargetPath string, s
|
|||
return err
|
||||
}
|
||||
startTime := time.Now()
|
||||
numFiles, sizeDiff, err := copier.CopyFile(fsSourcePath, fsTargetPath, srcSize)
|
||||
numFiles, sizeDiff, err := copier.CopyFile(fsSourcePath, fsTargetPath, srcInfo)
|
||||
elapsed := time.Since(startTime).Nanoseconds() / 1000000
|
||||
updateUserQuotaAfterFileWrite(c, virtualTargetPath, numFiles, sizeDiff)
|
||||
logger.CommandLog(copyLogSender, fsSourcePath, fsTargetPath, c.User.Username, "", c.ID, c.protocol, -1, -1,
|
||||
"", "", "", srcSize, c.localAddr, c.remoteAddr, elapsed)
|
||||
ExecuteActionNotification(c, operationCopy, fsSourcePath, virtualSourcePath, fsTargetPath, virtualTargetPath, "", srcSize, err, elapsed, nil) //nolint:errcheck
|
||||
"", "", "", srcInfo.Size(), c.localAddr, c.remoteAddr, elapsed)
|
||||
ExecuteActionNotification(c, operationCopy, fsSourcePath, virtualSourcePath, fsTargetPath, virtualTargetPath, "", srcInfo.Size(), err, elapsed, nil) //nolint:errcheck
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -657,7 +674,7 @@ func (c *BaseConnection) copyFile(virtualSourcePath, virtualTargetPath string, s
|
|||
defer rCancelFn()
|
||||
defer reader.Close()
|
||||
|
||||
writer, numFiles, truncatedSize, wCancelFn, err := getFileWriter(c, virtualTargetPath, srcSize)
|
||||
writer, numFiles, truncatedSize, wCancelFn, err := getFileWriter(c, virtualTargetPath, srcInfo.Size())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get writer for path %q: %w", virtualTargetPath, err)
|
||||
}
|
||||
|
|
@ -708,7 +725,7 @@ func (c *BaseConnection) doRecursiveCopy(virtualSourcePath, virtualTargetPath st
|
|||
return nil
|
||||
}
|
||||
|
||||
return c.copyFile(virtualSourcePath, virtualTargetPath, srcInfo.Size())
|
||||
return c.copyFile(virtualSourcePath, virtualTargetPath, srcInfo)
|
||||
}
|
||||
|
||||
func (c *BaseConnection) recursiveCopyEntries(virtualSourcePath, virtualTargetPath string, entries []os.FileInfo, recursion int) error {
|
||||
|
|
@ -770,29 +787,27 @@ func (c *BaseConnection) Copy(virtualSourcePath, virtualTargetPath string) error
|
|||
return err
|
||||
}
|
||||
}
|
||||
createTargetDir := true
|
||||
if dstInfo != nil && dstInfo.IsDir() {
|
||||
createTargetDir = false
|
||||
}
|
||||
createTargetDir := dstInfo == nil || !dstInfo.IsDir()
|
||||
if err := c.checkCopy(srcInfo, dstInfo, virtualSourcePath, destPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.CheckParentDirs(path.Dir(destPath)); err != nil {
|
||||
return err
|
||||
}
|
||||
done := make(chan bool)
|
||||
defer close(done)
|
||||
go keepConnectionAlive(c, done, 2*time.Minute)
|
||||
stopKeepAlive := keepConnectionAlive(c, 2*time.Minute)
|
||||
defer stopKeepAlive()
|
||||
|
||||
return c.doRecursiveCopy(virtualSourcePath, destPath, srcInfo, createTargetDir, 0)
|
||||
}
|
||||
|
||||
// Rename renames (moves) virtualSourcePath to virtualTargetPath
|
||||
func (c *BaseConnection) Rename(virtualSourcePath, virtualTargetPath string) error {
|
||||
return c.renameInternal(virtualSourcePath, virtualTargetPath, false)
|
||||
return c.renameInternal(virtualSourcePath, virtualTargetPath, false, vfs.CheckParentDir)
|
||||
}
|
||||
|
||||
func (c *BaseConnection) renameInternal(virtualSourcePath, virtualTargetPath string, checkParentDestination bool) error {
|
||||
func (c *BaseConnection) renameInternal(virtualSourcePath, virtualTargetPath string, //nolint:gocyclo
|
||||
checkParentDestination bool, checks int,
|
||||
) error {
|
||||
if virtualSourcePath == virtualTargetPath {
|
||||
return fmt.Errorf("the rename source and target cannot be the same: %w", c.GetOpUnsupportedError())
|
||||
}
|
||||
|
|
@ -813,7 +828,11 @@ func (c *BaseConnection) renameInternal(virtualSourcePath, virtualTargetPath str
|
|||
return c.GetPermissionDeniedError()
|
||||
}
|
||||
initialSize := int64(-1)
|
||||
if dstInfo, err := fsDst.Lstat(fsTargetPath); err == nil {
|
||||
dstInfo, err := fsDst.Lstat(fsTargetPath)
|
||||
if err != nil && !fsDst.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if err == nil {
|
||||
checkParentDestination = false
|
||||
if dstInfo.IsDir() {
|
||||
c.Log(logger.LevelWarn, "attempted to rename %q overwriting an existing directory %q",
|
||||
|
|
@ -835,18 +854,17 @@ func (c *BaseConnection) renameInternal(virtualSourcePath, virtualTargetPath str
|
|||
return err
|
||||
}
|
||||
}
|
||||
if !c.hasSpaceForRename(fsSrc, virtualSourcePath, virtualTargetPath, initialSize, fsSourcePath) {
|
||||
if !c.hasSpaceForRename(fsSrc, virtualSourcePath, virtualTargetPath, initialSize, fsSourcePath, srcInfo) {
|
||||
c.Log(logger.LevelInfo, "denying cross rename due to space limit")
|
||||
return c.GetGenericError(ErrQuotaExceeded)
|
||||
}
|
||||
if checkParentDestination {
|
||||
c.CheckParentDirs(path.Dir(virtualTargetPath)) //nolint:errcheck
|
||||
}
|
||||
done := make(chan bool)
|
||||
defer close(done)
|
||||
go keepConnectionAlive(c, done, 2*time.Minute)
|
||||
stopKeepAlive := keepConnectionAlive(c, 2*time.Minute)
|
||||
defer stopKeepAlive()
|
||||
|
||||
files, size, err := fsDst.Rename(fsSourcePath, fsTargetPath)
|
||||
files, size, err := fsDst.Rename(fsSourcePath, fsTargetPath, checks)
|
||||
if err != nil {
|
||||
c.Log(logger.LevelError, "failed to rename %q -> %q: %+v", fsSourcePath, fsTargetPath, err)
|
||||
return c.GetFsError(fsSrc, err)
|
||||
|
|
@ -918,16 +936,6 @@ func (c *BaseConnection) CreateSymlink(virtualSourcePath, virtualTargetPath stri
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *BaseConnection) getPathForSetStatPerms(fs vfs.Fs, fsPath, virtualPath string) string {
|
||||
pathForPerms := virtualPath
|
||||
if fi, err := fs.Lstat(fsPath); err == nil {
|
||||
if fi.IsDir() {
|
||||
pathForPerms = path.Dir(virtualPath)
|
||||
}
|
||||
}
|
||||
return pathForPerms
|
||||
}
|
||||
|
||||
func (c *BaseConnection) doStatInternal(virtualPath string, mode int, checkFilePatterns,
|
||||
convertResult bool,
|
||||
) (os.FileInfo, error) {
|
||||
|
|
@ -958,7 +966,19 @@ func (c *BaseConnection) doStatInternal(virtualPath string, mode int, checkFileP
|
|||
info, err = fs.Stat(c.getRealFsPath(fsPath))
|
||||
}
|
||||
if err != nil {
|
||||
if !fs.IsNotExist(err) {
|
||||
isNotExist := fs.IsNotExist(err)
|
||||
if isNotExist {
|
||||
// This is primarily useful for atomic storage backends, where files
|
||||
// become visible only after they are closed. However, since we may
|
||||
// be proxying (for example) an SFTP server backed by atomic
|
||||
// storage, and this search only inspects transfers active on the
|
||||
// current connection (typically just one), the check is inexpensive
|
||||
// and safe to perform unconditionally.
|
||||
if info, err := c.getInfoForOngoingUpload(fsPath); err == nil {
|
||||
return info, nil
|
||||
}
|
||||
}
|
||||
if !isNotExist {
|
||||
c.Log(logger.LevelWarn, "stat error for path %q: %+v", virtualPath, err)
|
||||
}
|
||||
return nil, c.GetFsError(fs, err)
|
||||
|
|
@ -1064,7 +1084,7 @@ func (c *BaseConnection) SetStat(virtualPath string, attributes *StatAttributes)
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pathForPerms := c.getPathForSetStatPerms(fs, fsPath, virtualPath)
|
||||
pathForPerms := path.Dir(virtualPath)
|
||||
|
||||
if attributes.Flags&StatAttrTimes != 0 {
|
||||
if err = c.handleChtimes(fs, fsPath, pathForPerms, attributes); err != nil {
|
||||
|
|
@ -1121,10 +1141,7 @@ func (c *BaseConnection) truncateFile(fs vfs.Fs, fsPath, virtualPath string, siz
|
|||
sizeDiff := initialSize - size
|
||||
vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(virtualPath))
|
||||
if err == nil {
|
||||
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, 0, -sizeDiff, false) //nolint:errcheck
|
||||
if vfolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.User, 0, -sizeDiff, false) //nolint:errcheck
|
||||
}
|
||||
dataprovider.UpdateUserFolderQuota(&vfolder, &c.User, 0, -sizeDiff, false)
|
||||
} else {
|
||||
dataprovider.UpdateUserQuota(&c.User, 0, -sizeDiff, false) //nolint:errcheck
|
||||
}
|
||||
|
|
@ -1133,11 +1150,11 @@ func (c *BaseConnection) truncateFile(fs vfs.Fs, fsPath, virtualPath string, siz
|
|||
}
|
||||
|
||||
func (c *BaseConnection) checkRecursiveRenameDirPermissions(fsSrc, fsDst vfs.Fs, sourcePath, targetPath,
|
||||
virtualSourcePath, virtualTargetPath string, fi os.FileInfo,
|
||||
virtualSourcePath, virtualTargetPath string, srcInfo os.FileInfo,
|
||||
) error {
|
||||
if !c.User.HasPermissionsInside(virtualSourcePath) &&
|
||||
!c.User.HasPermissionsInside(virtualTargetPath) {
|
||||
if !c.isRenamePermitted(fsSrc, fsDst, sourcePath, targetPath, virtualSourcePath, virtualTargetPath, fi) {
|
||||
if !c.isRenamePermitted(fsSrc, fsDst, sourcePath, targetPath, virtualSourcePath, virtualTargetPath, srcInfo) {
|
||||
c.Log(logger.LevelInfo, "rename %q -> %q is not allowed, virtual destination path: %q",
|
||||
sourcePath, targetPath, virtualTargetPath)
|
||||
return c.GetPermissionDeniedError()
|
||||
|
|
@ -1197,7 +1214,7 @@ func (c *BaseConnection) hasRenamePerms(virtualSourcePath, virtualTargetPath str
|
|||
}
|
||||
|
||||
func (c *BaseConnection) checkFolderRename(fsSrc, fsDst vfs.Fs, fsSourcePath, fsTargetPath, virtualSourcePath,
|
||||
virtualTargetPath string, fi os.FileInfo) error {
|
||||
virtualTargetPath string, srcInfo os.FileInfo) error {
|
||||
if util.IsDirOverlapped(virtualSourcePath, virtualTargetPath, true, "/") {
|
||||
c.Log(logger.LevelDebug, "renaming the folder %q->%q is not supported: nested folders",
|
||||
virtualSourcePath, virtualTargetPath)
|
||||
|
|
@ -1221,7 +1238,7 @@ func (c *BaseConnection) checkFolderRename(fsSrc, fsDst vfs.Fs, fsSourcePath, fs
|
|||
return fmt.Errorf("folder %q has virtual folders inside it: %w", virtualTargetPath, c.GetOpUnsupportedError())
|
||||
}
|
||||
if err := c.checkRecursiveRenameDirPermissions(fsSrc, fsDst, fsSourcePath, fsTargetPath,
|
||||
virtualSourcePath, virtualTargetPath, fi); err != nil {
|
||||
virtualSourcePath, virtualTargetPath, srcInfo); err != nil {
|
||||
c.Log(logger.LevelDebug, "error checking recursive permissions before renaming %q: %+v", fsSourcePath, err)
|
||||
return err
|
||||
}
|
||||
|
|
@ -1229,7 +1246,7 @@ func (c *BaseConnection) checkFolderRename(fsSrc, fsDst vfs.Fs, fsSourcePath, fs
|
|||
}
|
||||
|
||||
func (c *BaseConnection) isRenamePermitted(fsSrc, fsDst vfs.Fs, fsSourcePath, fsTargetPath, virtualSourcePath,
|
||||
virtualTargetPath string, fi os.FileInfo,
|
||||
virtualTargetPath string, srcInfo os.FileInfo,
|
||||
) bool {
|
||||
if !c.IsSameResource(virtualSourcePath, virtualTargetPath) {
|
||||
c.Log(logger.LevelInfo, "rename %q->%q is not allowed: the paths must be on the same resource",
|
||||
|
|
@ -1259,11 +1276,11 @@ func (c *BaseConnection) isRenamePermitted(fsSrc, fsDst vfs.Fs, fsSourcePath, fs
|
|||
virtualTargetPath)
|
||||
return false
|
||||
}
|
||||
return c.hasRenamePerms(virtualSourcePath, virtualTargetPath, fi)
|
||||
return c.hasRenamePerms(virtualSourcePath, virtualTargetPath, srcInfo)
|
||||
}
|
||||
|
||||
func (c *BaseConnection) hasSpaceForRename(fs vfs.Fs, virtualSourcePath, virtualTargetPath string, initialSize int64,
|
||||
fsSourcePath string) bool {
|
||||
sourcePath string, srcInfo os.FileInfo) bool {
|
||||
if dataprovider.GetQuotaTracking() == 0 {
|
||||
return true
|
||||
}
|
||||
|
|
@ -1293,30 +1310,28 @@ func (c *BaseConnection) hasSpaceForRename(fs vfs.Fs, virtualSourcePath, virtual
|
|||
// no quota restrictions
|
||||
return true
|
||||
}
|
||||
return c.hasSpaceForCrossRename(fs, quotaResult, initialSize, fsSourcePath)
|
||||
return c.hasSpaceForCrossRename(fs, quotaResult, initialSize, sourcePath, srcInfo)
|
||||
}
|
||||
|
||||
// hasSpaceForCrossRename checks the quota after a rename between different folders
|
||||
func (c *BaseConnection) hasSpaceForCrossRename(fs vfs.Fs, quotaResult vfs.QuotaCheckResult, initialSize int64, sourcePath string) bool {
|
||||
func (c *BaseConnection) hasSpaceForCrossRename(fs vfs.Fs, quotaResult vfs.QuotaCheckResult, initialSize int64,
|
||||
sourcePath string, srcInfo os.FileInfo,
|
||||
) bool {
|
||||
if !quotaResult.HasSpace && initialSize == -1 {
|
||||
// we are over quota and this is not a file replace
|
||||
return false
|
||||
}
|
||||
fi, err := fs.Lstat(sourcePath)
|
||||
if err != nil {
|
||||
c.Log(logger.LevelError, "cross rename denied, stat error for path %q: %v", sourcePath, err)
|
||||
return false
|
||||
}
|
||||
var sizeDiff int64
|
||||
var filesDiff int
|
||||
if fi.Mode().IsRegular() {
|
||||
sizeDiff = fi.Size()
|
||||
var err error
|
||||
if srcInfo.Mode().IsRegular() {
|
||||
sizeDiff = srcInfo.Size()
|
||||
filesDiff = 1
|
||||
if initialSize != -1 {
|
||||
sizeDiff -= initialSize
|
||||
filesDiff = 0
|
||||
}
|
||||
} else if fi.IsDir() {
|
||||
} else if srcInfo.IsDir() {
|
||||
filesDiff, sizeDiff, err = fs.GetDirSize(sourcePath)
|
||||
if err != nil {
|
||||
c.Log(logger.LevelError, "cross rename denied, error getting size for directory %q: %v", sourcePath, err)
|
||||
|
|
@ -1343,7 +1358,7 @@ func (c *BaseConnection) hasSpaceForCrossRename(fs vfs.Fs, quotaResult vfs.Quota
|
|||
}
|
||||
if quotaResult.QuotaSize > 0 {
|
||||
remainingSize := quotaResult.GetRemainingSize()
|
||||
c.Log(logger.LevelDebug, "cross rename, source %q remaining size %d to add %d", sourcePath,
|
||||
c.Log(logger.LevelDebug, "cross rename, source %q remaining size %d to add %d", srcInfo.Name(),
|
||||
remainingSize, sizeDiff)
|
||||
if remainingSize < sizeDiff {
|
||||
return false
|
||||
|
|
@ -1518,61 +1533,40 @@ func (c *BaseConnection) updateQuotaMoveBetweenVFolders(sourceFolder, dstFolder
|
|||
if sourceFolder.Name == dstFolder.Name {
|
||||
// both files are inside the same virtual folder
|
||||
if initialSize != -1 {
|
||||
dataprovider.UpdateVirtualFolderQuota(&dstFolder.BaseVirtualFolder, -numFiles, -initialSize, false) //nolint:errcheck
|
||||
if dstFolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.User, -numFiles, -initialSize, false) //nolint:errcheck
|
||||
}
|
||||
dataprovider.UpdateUserFolderQuota(dstFolder, &c.User, -numFiles, -initialSize, false)
|
||||
}
|
||||
return
|
||||
}
|
||||
// files are inside different virtual folders
|
||||
dataprovider.UpdateVirtualFolderQuota(&sourceFolder.BaseVirtualFolder, -numFiles, -filesSize, false) //nolint:errcheck
|
||||
if sourceFolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.User, -numFiles, -filesSize, false) //nolint:errcheck
|
||||
}
|
||||
dataprovider.UpdateUserFolderQuota(sourceFolder, &c.User, -numFiles, -filesSize, false)
|
||||
if initialSize == -1 {
|
||||
dataprovider.UpdateVirtualFolderQuota(&dstFolder.BaseVirtualFolder, numFiles, filesSize, false) //nolint:errcheck
|
||||
if dstFolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.User, numFiles, filesSize, false) //nolint:errcheck
|
||||
}
|
||||
} else {
|
||||
// we cannot have a directory here, initialSize != -1 only for files
|
||||
dataprovider.UpdateVirtualFolderQuota(&dstFolder.BaseVirtualFolder, 0, filesSize-initialSize, false) //nolint:errcheck
|
||||
if dstFolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.User, 0, filesSize-initialSize, false) //nolint:errcheck
|
||||
}
|
||||
dataprovider.UpdateUserFolderQuota(dstFolder, &c.User, numFiles, filesSize, false)
|
||||
return
|
||||
}
|
||||
// we cannot have a directory here, initialSize != -1 only for files
|
||||
dataprovider.UpdateUserFolderQuota(dstFolder, &c.User, 0, filesSize-initialSize, false)
|
||||
}
|
||||
|
||||
func (c *BaseConnection) updateQuotaMoveFromVFolder(sourceFolder *vfs.VirtualFolder, initialSize, filesSize int64, numFiles int) {
|
||||
// move between a virtual folder and the user home dir
|
||||
dataprovider.UpdateVirtualFolderQuota(&sourceFolder.BaseVirtualFolder, -numFiles, -filesSize, false) //nolint:errcheck
|
||||
if sourceFolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.User, -numFiles, -filesSize, false) //nolint:errcheck
|
||||
}
|
||||
dataprovider.UpdateUserFolderQuota(sourceFolder, &c.User, -numFiles, -filesSize, false)
|
||||
if initialSize == -1 {
|
||||
dataprovider.UpdateUserQuota(&c.User, numFiles, filesSize, false) //nolint:errcheck
|
||||
} else {
|
||||
// we cannot have a directory here, initialSize != -1 only for files
|
||||
dataprovider.UpdateUserQuota(&c.User, 0, filesSize-initialSize, false) //nolint:errcheck
|
||||
return
|
||||
}
|
||||
// we cannot have a directory here, initialSize != -1 only for files
|
||||
dataprovider.UpdateUserQuota(&c.User, 0, filesSize-initialSize, false) //nolint:errcheck
|
||||
}
|
||||
|
||||
func (c *BaseConnection) updateQuotaMoveToVFolder(dstFolder *vfs.VirtualFolder, initialSize, filesSize int64, numFiles int) {
|
||||
// move between the user home dir and a virtual folder
|
||||
dataprovider.UpdateUserQuota(&c.User, -numFiles, -filesSize, false) //nolint:errcheck
|
||||
if initialSize == -1 {
|
||||
dataprovider.UpdateVirtualFolderQuota(&dstFolder.BaseVirtualFolder, numFiles, filesSize, false) //nolint:errcheck
|
||||
if dstFolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.User, numFiles, filesSize, false) //nolint:errcheck
|
||||
}
|
||||
} else {
|
||||
// we cannot have a directory here, initialSize != -1 only for files
|
||||
dataprovider.UpdateVirtualFolderQuota(&dstFolder.BaseVirtualFolder, 0, filesSize-initialSize, false) //nolint:errcheck
|
||||
if dstFolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&c.User, 0, filesSize-initialSize, false) //nolint:errcheck
|
||||
}
|
||||
dataprovider.UpdateUserFolderQuota(dstFolder, &c.User, numFiles, filesSize, false)
|
||||
return
|
||||
}
|
||||
// we cannot have a directory here, initialSize != -1 only for files
|
||||
dataprovider.UpdateUserFolderQuota(dstFolder, &c.User, 0, filesSize-initialSize, false)
|
||||
}
|
||||
|
||||
func (c *BaseConnection) updateQuotaAfterRename(fs vfs.Fs, virtualSourcePath, virtualTargetPath, targetPath string,
|
||||
|
|
@ -1814,20 +1808,19 @@ func (c *BaseConnection) GetFsAndResolvedPath(virtualPath string) (vfs.Fs, strin
|
|||
// DirListerAt defines a directory lister implementing the ListAt method.
|
||||
type DirListerAt struct {
|
||||
virtualPath string
|
||||
user *dataprovider.User
|
||||
conn *BaseConnection
|
||||
fs vfs.Fs
|
||||
info []os.FileInfo
|
||||
id string
|
||||
protocol string
|
||||
mu sync.Mutex
|
||||
lister vfs.DirLister
|
||||
}
|
||||
|
||||
// Add adds the given os.FileInfo to the internal cache
|
||||
func (l *DirListerAt) Add(fi os.FileInfo) {
|
||||
// Prepend adds the given os.FileInfo as first element of the internal cache
|
||||
func (l *DirListerAt) Prepend(fi os.FileInfo) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
l.info = append(l.info, fi)
|
||||
l.info = slices.Insert(l.info, 0, fi)
|
||||
}
|
||||
|
||||
// ListAt implements sftp.ListerAt
|
||||
|
|
@ -1840,10 +1833,10 @@ func (l *DirListerAt) ListAt(f []os.FileInfo, _ int64) (int, error) {
|
|||
}
|
||||
if len(f) <= len(l.info) {
|
||||
files := make([]os.FileInfo, 0, len(f))
|
||||
for idx := len(l.info) - 1; idx >= 0; idx-- {
|
||||
for idx := range l.info {
|
||||
files = append(files, l.info[idx])
|
||||
if len(files) == len(f) {
|
||||
l.info = l.info[:idx]
|
||||
l.info = l.info[idx+1:]
|
||||
n := copy(f, files)
|
||||
return n, nil
|
||||
}
|
||||
|
|
@ -1860,14 +1853,12 @@ func (l *DirListerAt) Next(limit int) ([]os.FileInfo, error) {
|
|||
for {
|
||||
files, err := l.lister.Next(limit)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
logger.Debug(l.protocol, l.id, "error retrieving directory entries: %+v", err)
|
||||
return files, err
|
||||
l.conn.Log(logger.LevelDebug, "error retrieving directory entries: %+v", err)
|
||||
return files, l.conn.GetFsError(l.fs, err)
|
||||
}
|
||||
files = l.user.FilterListDir(files, l.virtualPath)
|
||||
files = l.conn.User.FilterListDir(files, l.virtualPath)
|
||||
if len(l.info) > 0 {
|
||||
for _, fi := range l.info {
|
||||
files = util.PrependFileInfo(files, fi)
|
||||
}
|
||||
files = slices.Concat(l.info, files)
|
||||
l.info = nil
|
||||
}
|
||||
if err != nil || len(files) > 0 {
|
||||
|
|
@ -1902,18 +1893,22 @@ func getPermissionDeniedError(protocol string) error {
|
|||
}
|
||||
}
|
||||
|
||||
func keepConnectionAlive(c *BaseConnection, done chan bool, interval time.Duration) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
}()
|
||||
func keepConnectionAlive(c *BaseConnection, interval time.Duration) func() {
|
||||
var timer *time.Timer
|
||||
var closed atomic.Bool
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
c.UpdateLastActivity()
|
||||
task := func() {
|
||||
c.UpdateLastActivity()
|
||||
|
||||
if !closed.Load() {
|
||||
timer.Reset(interval)
|
||||
}
|
||||
}
|
||||
|
||||
timer = time.AfterFunc(interval, task)
|
||||
|
||||
return func() {
|
||||
closed.Store(true)
|
||||
timer.Stop()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import (
|
|||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
|
@ -197,25 +198,24 @@ func TestRecursiveRenameWalkError(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCrossRenameFsErrors(t *testing.T) {
|
||||
if runtime.GOOS == osWindows {
|
||||
t.Skip("this test is not available on Windows")
|
||||
}
|
||||
fs := vfs.NewOsFs("", os.TempDir(), "", nil)
|
||||
conn := NewBaseConnection("", ProtocolWebDAV, "", "", dataprovider.User{})
|
||||
res := conn.hasSpaceForCrossRename(fs, vfs.QuotaCheckResult{}, 1, "missingsource")
|
||||
dirPath := filepath.Join(os.TempDir(), "d")
|
||||
err := os.Mkdir(dirPath, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
err = os.Chmod(dirPath, 0001)
|
||||
assert.NoError(t, err)
|
||||
srcInfo := vfs.NewFileInfo(filepath.Base(dirPath), true, 0, time.Now(), false)
|
||||
res := conn.hasSpaceForCrossRename(fs, vfs.QuotaCheckResult{}, 1, dirPath, srcInfo)
|
||||
assert.False(t, res)
|
||||
if runtime.GOOS != osWindows {
|
||||
dirPath := filepath.Join(os.TempDir(), "d")
|
||||
err := os.Mkdir(dirPath, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
err = os.Chmod(dirPath, 0001)
|
||||
assert.NoError(t, err)
|
||||
|
||||
res = conn.hasSpaceForCrossRename(fs, vfs.QuotaCheckResult{}, 1, dirPath)
|
||||
assert.False(t, res)
|
||||
|
||||
err = os.Chmod(dirPath, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(dirPath)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
err = os.Chmod(dirPath, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(dirPath)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRenameVirtualFolders(t *testing.T) {
|
||||
|
|
@ -389,7 +389,7 @@ func TestErrorsMapping(t *testing.T) {
|
|||
err := conn.GetFsError(fs, os.ErrNotExist)
|
||||
if protocol == ProtocolSFTP {
|
||||
assert.ErrorIs(t, err, sftp.ErrSSHFxNoSuchFile)
|
||||
} else if util.Contains(osErrorsProtocols, protocol) {
|
||||
} else if slices.Contains(osErrorsProtocols, protocol) {
|
||||
assert.EqualError(t, err, os.ErrNotExist.Error())
|
||||
} else {
|
||||
assert.EqualError(t, err, ErrNotExist.Error())
|
||||
|
|
@ -627,12 +627,11 @@ func TestErrorResolvePath(t *testing.T) {
|
|||
func TestConnectionKeepAlive(t *testing.T) {
|
||||
conn := NewBaseConnection("", ProtocolWebDAV, "", "", dataprovider.User{})
|
||||
lastActivity := conn.GetLastActivity()
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
close(done)
|
||||
}()
|
||||
keepConnectionAlive(conn, done, 50*time.Millisecond)
|
||||
|
||||
stop := keepConnectionAlive(conn, 50*time.Millisecond)
|
||||
defer stop()
|
||||
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
assert.Greater(t, conn.GetLastActivity(), lastActivity)
|
||||
}
|
||||
|
||||
|
|
@ -1047,6 +1046,37 @@ func TestFilePatterns(t *testing.T) {
|
|||
require.Len(t, filtered, 1)
|
||||
}
|
||||
|
||||
func TestStatForOngoingTransfers(t *testing.T) {
|
||||
user := dataprovider.User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
Username: xid.New().String(),
|
||||
Password: xid.New().String(),
|
||||
HomeDir: filepath.Clean(os.TempDir()),
|
||||
Status: 1,
|
||||
Permissions: map[string][]string{
|
||||
"/": {"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
fileName := "file.txt"
|
||||
conn := NewBaseConnection(xid.New().String(), ProtocolSFTP, "", "", user)
|
||||
fs := vfs.NewOsFs("", os.TempDir(), "", nil)
|
||||
tr := NewBaseTransfer(nil, conn, nil, filepath.Join(os.TempDir(), fileName), filepath.Join(os.TempDir(), fileName),
|
||||
fileName, TransferUpload, 0, 0, 0, 0, true, fs, dataprovider.TransferQuota{})
|
||||
_, err := conn.DoStat("/file.txt", 0, false)
|
||||
assert.NoError(t, err)
|
||||
err = tr.Close()
|
||||
assert.NoError(t, err)
|
||||
tr = NewBaseTransfer(nil, conn, nil, filepath.Join(os.TempDir(), fileName), filepath.Join(os.TempDir(), fileName),
|
||||
fileName, TransferDownload, 0, 0, 0, 0, true, fs, dataprovider.TransferQuota{})
|
||||
_, err = conn.DoStat("/file.txt", 0, false)
|
||||
assert.Error(t, err)
|
||||
err = tr.Close()
|
||||
assert.NoError(t, err)
|
||||
err = conn.CloseFS()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestListerAt(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
user := dataprovider.User{
|
||||
|
|
@ -1090,7 +1120,7 @@ func TestListerAt(t *testing.T) {
|
|||
require.ErrorIs(t, err, io.EOF)
|
||||
require.Len(t, files, 0)
|
||||
_, err = lister.Next(-1)
|
||||
require.ErrorContains(t, err, "invalid limit")
|
||||
require.ErrorContains(t, err, conn.GetGenericError(err).Error())
|
||||
err = lister.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -1134,8 +1164,8 @@ func TestListerAt(t *testing.T) {
|
|||
require.Equal(t, 0, n)
|
||||
lister, err = conn.ListDir("/")
|
||||
require.NoError(t, err)
|
||||
lister.Add(vfs.NewFileInfo("..", true, 0, time.Unix(0, 0), false))
|
||||
lister.Add(vfs.NewFileInfo(".", true, 0, time.Unix(0, 0), false))
|
||||
lister.Prepend(vfs.NewFileInfo("..", true, 0, time.Unix(0, 0), false))
|
||||
lister.Prepend(vfs.NewFileInfo(".", true, 0, time.Unix(0, 0), false))
|
||||
files = make([]os.FileInfo, 1)
|
||||
n, err = lister.ListAt(files, 0)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -1159,3 +1189,348 @@ func TestListerAt(t *testing.T) {
|
|||
err = lister.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGetFsAndResolvedPath(t *testing.T) {
|
||||
homeDir := filepath.Join(os.TempDir(), "home_test")
|
||||
localVdir := filepath.Join(os.TempDir(), "local_mount_test")
|
||||
|
||||
err := os.MkdirAll(homeDir, 0777)
|
||||
require.NoError(t, err)
|
||||
err = os.MkdirAll(localVdir, 0777)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
os.RemoveAll(homeDir)
|
||||
os.RemoveAll(localVdir)
|
||||
})
|
||||
|
||||
user := dataprovider.User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
Username: xid.New().String(),
|
||||
Status: 1,
|
||||
HomeDir: homeDir,
|
||||
},
|
||||
VirtualFolders: []vfs.VirtualFolder{
|
||||
{
|
||||
BaseVirtualFolder: vfs.BaseVirtualFolder{
|
||||
Name: "s3",
|
||||
MappedPath: "",
|
||||
FsConfig: vfs.Filesystem{
|
||||
Provider: sdk.S3FilesystemProvider,
|
||||
S3Config: vfs.S3FsConfig{
|
||||
BaseS3FsConfig: sdk.BaseS3FsConfig{
|
||||
Bucket: "my-test-bucket",
|
||||
Region: "us-east-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
VirtualPath: "/s3",
|
||||
},
|
||||
{
|
||||
BaseVirtualFolder: vfs.BaseVirtualFolder{
|
||||
Name: "local",
|
||||
MappedPath: localVdir,
|
||||
FsConfig: vfs.Filesystem{
|
||||
Provider: sdk.LocalFilesystemProvider,
|
||||
},
|
||||
},
|
||||
VirtualPath: "/local",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
conn := NewBaseConnection(xid.New().String(), ProtocolSFTP, "", "", user)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
inputVirtualPath string
|
||||
expectedFsType string
|
||||
expectedPhyPath string // The resolved path on the target FS
|
||||
expectedRelativePath string
|
||||
}{
|
||||
{
|
||||
name: "Root File",
|
||||
inputVirtualPath: "/file.txt",
|
||||
expectedFsType: "osfs",
|
||||
expectedPhyPath: filepath.Join(homeDir, "file.txt"),
|
||||
expectedRelativePath: "/file.txt",
|
||||
},
|
||||
{
|
||||
name: "Standard S3 File",
|
||||
inputVirtualPath: "/s3/image.png",
|
||||
expectedFsType: "S3Fs",
|
||||
expectedPhyPath: "image.png",
|
||||
expectedRelativePath: "/s3/image.png",
|
||||
},
|
||||
{
|
||||
name: "Standard Local Mount File",
|
||||
inputVirtualPath: "/local/config.json",
|
||||
expectedFsType: "osfs",
|
||||
expectedPhyPath: filepath.Join(localVdir, "config.json"),
|
||||
expectedRelativePath: "/local/config.json",
|
||||
},
|
||||
|
||||
{
|
||||
name: "Backslash Separator -> Should hit S3",
|
||||
inputVirtualPath: "\\s3\\doc.txt",
|
||||
expectedFsType: "S3Fs",
|
||||
expectedPhyPath: "doc.txt",
|
||||
expectedRelativePath: "/s3/doc.txt",
|
||||
},
|
||||
{
|
||||
name: "Mixed Separators -> Should hit Local Mount",
|
||||
inputVirtualPath: "/local\\subdir/test.txt",
|
||||
expectedFsType: "osfs",
|
||||
expectedPhyPath: filepath.Join(localVdir, "subdir", "test.txt"),
|
||||
expectedRelativePath: "/local/subdir/test.txt",
|
||||
},
|
||||
{
|
||||
name: "Double Slash -> Should normalize and hit S3",
|
||||
inputVirtualPath: "//s3//dir @1/data.csv",
|
||||
expectedFsType: "S3Fs",
|
||||
expectedPhyPath: "dir @1/data.csv",
|
||||
expectedRelativePath: "/s3/dir @1/data.csv",
|
||||
},
|
||||
|
||||
{
|
||||
name: "Local Mount Traversal (Attempt to escape)",
|
||||
inputVirtualPath: "/local/../../etc/passwd",
|
||||
expectedFsType: "osfs",
|
||||
expectedPhyPath: filepath.Join(homeDir, "/etc/passwd"),
|
||||
expectedRelativePath: "/etc/passwd",
|
||||
},
|
||||
{
|
||||
name: "Traversal Out of S3 (Valid)",
|
||||
inputVirtualPath: "/s3/../../secret.txt",
|
||||
expectedFsType: "osfs",
|
||||
expectedPhyPath: filepath.Join(homeDir, "secret.txt"),
|
||||
expectedRelativePath: "/secret.txt",
|
||||
},
|
||||
{
|
||||
name: "Traversal Inside S3",
|
||||
inputVirtualPath: "/s3/subdir/../image.png",
|
||||
expectedFsType: "S3Fs",
|
||||
expectedPhyPath: "image.png",
|
||||
expectedRelativePath: "/s3/image.png",
|
||||
},
|
||||
{
|
||||
name: "Mount Point Bypass -> Target Local Mount",
|
||||
inputVirtualPath: "/s3\\..\\local\\secret.txt",
|
||||
expectedFsType: "osfs",
|
||||
expectedPhyPath: filepath.Join(localVdir, "secret.txt"),
|
||||
expectedRelativePath: "/local/secret.txt",
|
||||
},
|
||||
{
|
||||
name: "Dirty Relative Path (Your Case)",
|
||||
inputVirtualPath: "test\\..\\..\\oops/file.txt",
|
||||
expectedFsType: "osfs",
|
||||
expectedPhyPath: filepath.Join(homeDir, "oops", "file.txt"),
|
||||
expectedRelativePath: "/oops/file.txt",
|
||||
},
|
||||
{
|
||||
name: "Relative Path targeting S3 (No leading slash)",
|
||||
inputVirtualPath: "s3//sub/../image.png",
|
||||
expectedFsType: "S3Fs",
|
||||
expectedPhyPath: "image.png",
|
||||
expectedRelativePath: "/s3/image.png",
|
||||
},
|
||||
{
|
||||
name: "Windows Path starting with Backslash",
|
||||
inputVirtualPath: "\\s3\\doc/dir\\doc.txt",
|
||||
expectedFsType: "S3Fs",
|
||||
expectedPhyPath: "doc/dir/doc.txt",
|
||||
expectedRelativePath: "/s3/doc/dir/doc.txt",
|
||||
},
|
||||
{
|
||||
name: "Filesystem Juggling (Relative)",
|
||||
inputVirtualPath: "local/../s3/file.txt",
|
||||
expectedFsType: "S3Fs",
|
||||
expectedPhyPath: "file.txt",
|
||||
expectedRelativePath: "/s3/file.txt",
|
||||
},
|
||||
{
|
||||
name: "Triple Dot Filename (Valid Name)",
|
||||
inputVirtualPath: "/...hidden/secret",
|
||||
expectedFsType: "osfs",
|
||||
expectedPhyPath: filepath.Join(homeDir, "...hidden", "secret"),
|
||||
expectedRelativePath: "/...hidden/secret",
|
||||
},
|
||||
{
|
||||
name: "Dot Slash Prefix",
|
||||
inputVirtualPath: "./local/file.txt",
|
||||
expectedFsType: "osfs",
|
||||
expectedPhyPath: filepath.Join(localVdir, "file.txt"),
|
||||
expectedRelativePath: "/local/file.txt",
|
||||
},
|
||||
{
|
||||
name: "Root of Local Mount Exactly",
|
||||
inputVirtualPath: "/local/",
|
||||
expectedFsType: "osfs",
|
||||
expectedPhyPath: localVdir,
|
||||
expectedRelativePath: "/local",
|
||||
},
|
||||
{
|
||||
name: "Root of S3 Mount Exactly",
|
||||
inputVirtualPath: "/s3/",
|
||||
expectedFsType: "S3Fs",
|
||||
expectedPhyPath: "",
|
||||
expectedRelativePath: "/s3",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// The input path is sanitized by the protocol handler
|
||||
// implementations before reaching GetFsAndResolvedPath.
|
||||
cleanInput := util.CleanPath(tc.inputVirtualPath)
|
||||
fs, resolvedPath, err := conn.GetFsAndResolvedPath(cleanInput)
|
||||
if assert.NoError(t, err, "did not expect error for path: %q, got: %v", tc.inputVirtualPath, err) {
|
||||
assert.Contains(t, fs.Name(), tc.expectedFsType,
|
||||
"routing error: input %q but expected fs %q, got %q", tc.inputVirtualPath, tc.expectedFsType, fs.Name())
|
||||
assert.Equal(t, tc.expectedPhyPath, resolvedPath,
|
||||
"resolution error: input %q resolved to %q expected %q", tc.inputVirtualPath, resolvedPath, tc.expectedPhyPath)
|
||||
relativePath := fs.GetRelativePath(resolvedPath)
|
||||
assert.Equal(t, tc.expectedRelativePath, relativePath,
|
||||
"relative path error, input %q, got %q, expected %q", tc.inputVirtualPath, tc.expectedRelativePath, relativePath)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOsFsGetRelativePath(t *testing.T) {
|
||||
homeDir := filepath.Join(os.TempDir(), "home_test")
|
||||
localVdir := filepath.Join(os.TempDir(), "local_mount_test")
|
||||
|
||||
err := os.MkdirAll(homeDir, 0777)
|
||||
require.NoError(t, err)
|
||||
err = os.MkdirAll(localVdir, 0777)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
os.RemoveAll(homeDir)
|
||||
os.RemoveAll(localVdir)
|
||||
})
|
||||
|
||||
user := dataprovider.User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
Username: xid.New().String(),
|
||||
Status: 1,
|
||||
HomeDir: homeDir,
|
||||
},
|
||||
VirtualFolders: []vfs.VirtualFolder{
|
||||
{
|
||||
BaseVirtualFolder: vfs.BaseVirtualFolder{
|
||||
Name: "local",
|
||||
MappedPath: localVdir,
|
||||
FsConfig: vfs.Filesystem{
|
||||
Provider: sdk.LocalFilesystemProvider,
|
||||
},
|
||||
},
|
||||
VirtualPath: "/local",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
connID := xid.New().String()
|
||||
rootFs, err := user.GetFilesystemForPath("/", connID)
|
||||
require.NoError(t, err)
|
||||
|
||||
localFs, err := user.GetFilesystemForPath("/local", connID)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fs vfs.Fs
|
||||
inputPath string // The physical path to reverse-map
|
||||
expectedRel string // The expected virtual path
|
||||
}{
|
||||
{
|
||||
name: "Root FS - Inside root",
|
||||
fs: rootFs,
|
||||
inputPath: filepath.Join(homeDir, "docs", "file.txt"),
|
||||
expectedRel: "/docs/file.txt",
|
||||
},
|
||||
{
|
||||
name: "Root FS - Exact root directory",
|
||||
fs: rootFs,
|
||||
inputPath: homeDir,
|
||||
expectedRel: "/",
|
||||
},
|
||||
{
|
||||
name: "Root FS - External absolute path (Jail to /)",
|
||||
fs: rootFs,
|
||||
inputPath: "/etc/passwd",
|
||||
expectedRel: "/",
|
||||
},
|
||||
{
|
||||
name: "Root FS - Traversal escape (Jail to /)",
|
||||
fs: rootFs,
|
||||
inputPath: filepath.Join(homeDir, "..", "escaped.txt"),
|
||||
expectedRel: "/",
|
||||
},
|
||||
{
|
||||
name: "Root FS - Valid file named with triple dots",
|
||||
fs: rootFs,
|
||||
inputPath: filepath.Join(homeDir, "..."),
|
||||
expectedRel: "/...",
|
||||
},
|
||||
{
|
||||
name: "Local FS - Up path in dir",
|
||||
fs: rootFs,
|
||||
inputPath: homeDir + "/../" + filepath.Base(homeDir) + "/dir/test.txt",
|
||||
expectedRel: "/dir/test.txt",
|
||||
},
|
||||
|
||||
{
|
||||
name: "Local FS - Inside mount",
|
||||
fs: localFs,
|
||||
inputPath: filepath.Join(localVdir, "data", "config.json"),
|
||||
expectedRel: "/local/data/config.json",
|
||||
},
|
||||
{
|
||||
name: "Local FS - Exact mount directory",
|
||||
fs: localFs,
|
||||
inputPath: localVdir,
|
||||
expectedRel: "/local",
|
||||
},
|
||||
{
|
||||
name: "Local FS - External absolute path (Jail to /local)",
|
||||
fs: localFs,
|
||||
inputPath: "/var/log/syslog",
|
||||
expectedRel: "/local",
|
||||
},
|
||||
{
|
||||
name: "Local FS - Traversal escape (Jail to /local)",
|
||||
fs: localFs,
|
||||
inputPath: filepath.Join(localVdir, "..", "..", "etc", "passwd"),
|
||||
expectedRel: "/local",
|
||||
},
|
||||
{
|
||||
name: "Local FS - Partial prefix (Jail to /local)",
|
||||
fs: localFs,
|
||||
inputPath: localVdir + "_backup",
|
||||
expectedRel: "/local",
|
||||
},
|
||||
{
|
||||
name: "Local FS - Relative traversal matching virual dir",
|
||||
fs: localFs,
|
||||
inputPath: localVdir + "/../" + filepath.Base(localVdir) + "/dir/test.txt",
|
||||
expectedRel: "/local/dir/test.txt",
|
||||
},
|
||||
{
|
||||
name: "Local FS - Valid file starting with two dots",
|
||||
fs: localFs,
|
||||
inputPath: filepath.Join(localVdir, "..hidden_file.txt"),
|
||||
expectedRel: "/local/..hidden_file.txt",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actualRel := tc.fs.GetRelativePath(tc.inputPath)
|
||||
assert.Equal(t, tc.expectedRel, actualRel,
|
||||
"Failed mapping physical path %q on FS %q", tc.inputPath, tc.fs.Name())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,44 +15,20 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/wneessen/go-mail"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/command"
|
||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/internal/httpclient"
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
"github.com/drakkan/sftpgo/v2/internal/smtp"
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
"github.com/drakkan/sftpgo/v2/internal/vfs"
|
||||
)
|
||||
|
||||
// RetentionCheckNotification defines the supported notification methods for a retention check result
|
||||
type RetentionCheckNotification = string
|
||||
|
||||
// Supported notification methods
|
||||
const (
|
||||
// notify results using the defined "data_retention_hook"
|
||||
RetentionCheckNotificationHook = "Hook"
|
||||
// notify results by email
|
||||
RetentionCheckNotificationEmail = "Email"
|
||||
)
|
||||
|
||||
var (
|
||||
// RetentionChecks is the list of active retention checks
|
||||
RetentionChecks ActiveRetentionChecks
|
||||
|
|
@ -74,14 +50,10 @@ func (c *ActiveRetentionChecks) Get(role string) []RetentionCheck {
|
|||
if role == "" || role == check.Role {
|
||||
foldersCopy := make([]dataprovider.FolderRetention, len(check.Folders))
|
||||
copy(foldersCopy, check.Folders)
|
||||
notificationsCopy := make([]string, len(check.Notifications))
|
||||
copy(notificationsCopy, check.Notifications)
|
||||
checks = append(checks, RetentionCheck{
|
||||
Username: check.Username,
|
||||
StartTime: check.StartTime,
|
||||
Notifications: notificationsCopy,
|
||||
Email: check.Email,
|
||||
Folders: foldersCopy,
|
||||
Username: check.Username,
|
||||
StartTime: check.StartTime,
|
||||
Folders: foldersCopy,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -150,54 +122,10 @@ type RetentionCheck struct {
|
|||
StartTime int64 `json:"start_time"`
|
||||
// affected folders
|
||||
Folders []dataprovider.FolderRetention `json:"folders"`
|
||||
// how cleanup results will be notified
|
||||
Notifications []RetentionCheckNotification `json:"notifications,omitempty"`
|
||||
// email to use if the notification method is set to email
|
||||
Email string `json:"email,omitempty"`
|
||||
Role string `json:"-"`
|
||||
Role string `json:"-"`
|
||||
// Cleanup results
|
||||
results []folderRetentionCheckResult `json:"-"`
|
||||
conn *BaseConnection
|
||||
}
|
||||
|
||||
// Validate returns an error if the specified folders are not valid
|
||||
func (c *RetentionCheck) Validate() error {
|
||||
folderPaths := make(map[string]bool)
|
||||
nothingToDo := true
|
||||
for idx := range c.Folders {
|
||||
f := &c.Folders[idx]
|
||||
if err := f.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if f.Retention > 0 {
|
||||
nothingToDo = false
|
||||
}
|
||||
if _, ok := folderPaths[f.Path]; ok {
|
||||
return util.NewValidationError(fmt.Sprintf("duplicated folder path %q", f.Path))
|
||||
}
|
||||
folderPaths[f.Path] = true
|
||||
}
|
||||
if nothingToDo {
|
||||
return util.NewValidationError("nothing to delete!")
|
||||
}
|
||||
for _, notification := range c.Notifications {
|
||||
switch notification {
|
||||
case RetentionCheckNotificationEmail:
|
||||
if !smtp.IsEnabled() {
|
||||
return util.NewValidationError("in order to notify results via email you must configure an SMTP server")
|
||||
}
|
||||
if c.Email == "" {
|
||||
return util.NewValidationError("in order to notify results via email you must add a valid email address to your profile")
|
||||
}
|
||||
case RetentionCheckNotificationHook:
|
||||
if Config.DataRetentionHook == "" {
|
||||
return util.NewValidationError("in order to notify results via hook you must define a data_retention_hook")
|
||||
}
|
||||
default:
|
||||
return util.NewValidationError(fmt.Sprintf("invalid notification %q", notification))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
conn *BaseConnection `json:"-"`
|
||||
}
|
||||
|
||||
func (c *RetentionCheck) updateUserPermissions() {
|
||||
|
|
@ -269,7 +197,7 @@ func (c *RetentionCheck) cleanupFolder(folderPath string, recursion int) error {
|
|||
return nil
|
||||
}
|
||||
result.Error = fmt.Sprintf("unable to get lister for directory %q", folderPath)
|
||||
c.conn.Log(logger.LevelError, result.Error)
|
||||
c.conn.Log(logger.LevelError, "%s", result.Error)
|
||||
return err
|
||||
}
|
||||
defer lister.Close()
|
||||
|
|
@ -359,130 +287,13 @@ func (c *RetentionCheck) Start() error {
|
|||
for _, folder := range c.Folders {
|
||||
if folder.Retention > 0 {
|
||||
if err := c.cleanupFolder(folder.Path, 0); err != nil {
|
||||
c.conn.Log(logger.LevelError, "retention check failed, unable to cleanup folder %q", folder.Path)
|
||||
c.sendNotifications(time.Since(startTime), err)
|
||||
c.conn.Log(logger.LevelError, "retention check failed, unable to cleanup folder %q, elapsed: %s",
|
||||
folder.Path, time.Since(startTime))
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.conn.Log(logger.LevelInfo, "retention check completed")
|
||||
c.sendNotifications(time.Since(startTime), nil)
|
||||
c.conn.Log(logger.LevelInfo, "retention check completed, elapsed: %s", time.Since(startTime))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *RetentionCheck) sendNotifications(elapsed time.Duration, err error) {
|
||||
for _, notification := range c.Notifications {
|
||||
switch notification {
|
||||
case RetentionCheckNotificationEmail:
|
||||
c.sendEmailNotification(err) //nolint:errcheck
|
||||
case RetentionCheckNotificationHook:
|
||||
c.sendHookNotification(elapsed, err) //nolint:errcheck
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RetentionCheck) sendEmailNotification(errCheck error) error {
|
||||
params := EventParams{}
|
||||
if len(c.results) > 0 || errCheck != nil {
|
||||
params.retentionChecks = append(params.retentionChecks, executedRetentionCheck{
|
||||
Username: c.conn.User.Username,
|
||||
ActionName: "Retention check",
|
||||
Results: c.results,
|
||||
})
|
||||
}
|
||||
var files []*mail.File
|
||||
f, err := params.getRetentionReportsAsMailAttachment()
|
||||
if err != nil {
|
||||
c.conn.Log(logger.LevelError, "unable to get retention report as mail attachment: %v", err)
|
||||
return err
|
||||
}
|
||||
f.Name = "retention-report.zip"
|
||||
files = append(files, f)
|
||||
|
||||
startTime := time.Now()
|
||||
var subject string
|
||||
if errCheck == nil {
|
||||
subject = fmt.Sprintf("Successful retention check for user %q", c.conn.User.Username)
|
||||
} else {
|
||||
subject = fmt.Sprintf("Retention check failed for user %q", c.conn.User.Username)
|
||||
}
|
||||
body := "Further details attached."
|
||||
err = smtp.SendEmail([]string{c.Email}, nil, subject, body, smtp.EmailContentTypeTextPlain, files...)
|
||||
if err != nil {
|
||||
c.conn.Log(logger.LevelError, "unable to notify retention check result via email: %v, elapsed: %s", err,
|
||||
time.Since(startTime))
|
||||
return err
|
||||
}
|
||||
c.conn.Log(logger.LevelInfo, "retention check result successfully notified via email, elapsed: %s", time.Since(startTime))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *RetentionCheck) sendHookNotification(elapsed time.Duration, errCheck error) error {
|
||||
startNewHook()
|
||||
defer hookEnded()
|
||||
|
||||
data := make(map[string]any)
|
||||
totalDeletedFiles := 0
|
||||
totalDeletedSize := int64(0)
|
||||
for _, result := range c.results {
|
||||
totalDeletedFiles += result.DeletedFiles
|
||||
totalDeletedSize += result.DeletedSize
|
||||
}
|
||||
data["username"] = c.conn.User.Username
|
||||
data["start_time"] = c.StartTime
|
||||
data["elapsed"] = elapsed.Milliseconds()
|
||||
if errCheck == nil {
|
||||
data["status"] = 1
|
||||
} else {
|
||||
data["status"] = 0
|
||||
}
|
||||
data["total_deleted_files"] = totalDeletedFiles
|
||||
data["total_deleted_size"] = totalDeletedSize
|
||||
data["details"] = c.results
|
||||
jsonData, _ := json.Marshal(data)
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
if strings.HasPrefix(Config.DataRetentionHook, "http") {
|
||||
var url *url.URL
|
||||
url, err := url.Parse(Config.DataRetentionHook)
|
||||
if err != nil {
|
||||
c.conn.Log(logger.LevelError, "invalid data retention hook %q: %v", Config.DataRetentionHook, err)
|
||||
return err
|
||||
}
|
||||
respCode := 0
|
||||
|
||||
resp, err := httpclient.RetryablePost(url.String(), "application/json", bytes.NewBuffer(jsonData))
|
||||
if err == nil {
|
||||
respCode = resp.StatusCode
|
||||
resp.Body.Close()
|
||||
|
||||
if respCode != http.StatusOK {
|
||||
err = errUnexpectedHTTResponse
|
||||
}
|
||||
}
|
||||
|
||||
c.conn.Log(logger.LevelDebug, "notified result to URL: %q, status code: %v, elapsed: %v err: %v",
|
||||
url.Redacted(), respCode, time.Since(startTime), err)
|
||||
|
||||
return err
|
||||
}
|
||||
if !filepath.IsAbs(Config.DataRetentionHook) {
|
||||
err := fmt.Errorf("invalid data retention hook %q", Config.DataRetentionHook)
|
||||
c.conn.Log(logger.LevelError, "%v", err)
|
||||
return err
|
||||
}
|
||||
timeout, env, args := command.GetConfig(Config.DataRetentionHook, command.HookDataRetention)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, Config.DataRetentionHook, args...)
|
||||
cmd.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_DATA_RETENTION_RESULT=%s", util.BytesToString(jsonData)))
|
||||
err := cmd.Run()
|
||||
|
||||
c.conn.Log(logger.LevelDebug, "notified result using command: %q, elapsed: %s err: %v",
|
||||
Config.DataRetentionHook, time.Since(startTime), err)
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,228 +15,17 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sftpgo/sdk"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/internal/smtp"
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
)
|
||||
|
||||
func TestRetentionValidation(t *testing.T) {
|
||||
check := RetentionCheck{}
|
||||
check.Folders = []dataprovider.FolderRetention{
|
||||
{
|
||||
Path: "/",
|
||||
Retention: -1,
|
||||
},
|
||||
}
|
||||
err := check.Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid folder retention")
|
||||
|
||||
check.Folders = []dataprovider.FolderRetention{
|
||||
{
|
||||
Path: "/ab/..",
|
||||
Retention: 0,
|
||||
},
|
||||
}
|
||||
err = check.Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "nothing to delete")
|
||||
assert.Equal(t, "/", check.Folders[0].Path)
|
||||
|
||||
check.Folders = append(check.Folders, dataprovider.FolderRetention{
|
||||
Path: "/../..",
|
||||
Retention: 24,
|
||||
})
|
||||
err = check.Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), `duplicated folder path "/"`)
|
||||
|
||||
check.Folders = []dataprovider.FolderRetention{
|
||||
{
|
||||
Path: "/dir1",
|
||||
Retention: 48,
|
||||
},
|
||||
{
|
||||
Path: "/dir2",
|
||||
Retention: 96,
|
||||
},
|
||||
}
|
||||
err = check.Validate()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, check.Notifications, 0)
|
||||
assert.Empty(t, check.Email)
|
||||
|
||||
check.Notifications = []RetentionCheckNotification{RetentionCheckNotificationEmail}
|
||||
err = check.Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "you must configure an SMTP server")
|
||||
|
||||
smtpCfg := smtp.Config{
|
||||
Host: "mail.example.com",
|
||||
Port: 25,
|
||||
From: "notification@example.com",
|
||||
TemplatesPath: "templates",
|
||||
}
|
||||
err = smtpCfg.Initialize(configDir, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = check.Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "you must add a valid email address")
|
||||
|
||||
check.Email = "admin@example.com"
|
||||
err = check.Validate()
|
||||
assert.NoError(t, err)
|
||||
|
||||
smtpCfg = smtp.Config{}
|
||||
err = smtpCfg.Initialize(configDir, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
check.Notifications = []RetentionCheckNotification{RetentionCheckNotificationHook}
|
||||
err = check.Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "data_retention_hook")
|
||||
|
||||
check.Notifications = []string{"not valid"}
|
||||
err = check.Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid notification")
|
||||
}
|
||||
|
||||
func TestRetentionEmailNotifications(t *testing.T) {
|
||||
smtpCfg := smtp.Config{
|
||||
Host: "127.0.0.1",
|
||||
Port: 2525,
|
||||
From: "notification@example.com",
|
||||
TemplatesPath: "templates",
|
||||
}
|
||||
err := smtpCfg.Initialize(configDir, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
user := dataprovider.User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
Username: "user1",
|
||||
},
|
||||
}
|
||||
user.Permissions = make(map[string][]string)
|
||||
user.Permissions["/"] = []string{dataprovider.PermAny}
|
||||
check := RetentionCheck{
|
||||
Notifications: []RetentionCheckNotification{RetentionCheckNotificationEmail},
|
||||
Email: "notification@example.com",
|
||||
results: []folderRetentionCheckResult{
|
||||
{
|
||||
Path: "/",
|
||||
Retention: 24,
|
||||
DeletedFiles: 10,
|
||||
DeletedSize: 32657,
|
||||
Elapsed: 10 * time.Second,
|
||||
},
|
||||
},
|
||||
}
|
||||
conn := NewBaseConnection("", "", "", "", user)
|
||||
conn.SetProtocol(ProtocolDataRetention)
|
||||
conn.ID = fmt.Sprintf("data_retention_%v", user.Username)
|
||||
check.conn = conn
|
||||
check.sendNotifications(1*time.Second, nil)
|
||||
err = check.sendEmailNotification(nil)
|
||||
assert.NoError(t, err)
|
||||
err = check.sendEmailNotification(errors.New("test error"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
check.results = nil
|
||||
err = check.sendEmailNotification(nil)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "no data retention report available")
|
||||
}
|
||||
|
||||
smtpCfg.Port = 2626
|
||||
err = smtpCfg.Initialize(configDir, true)
|
||||
require.NoError(t, err)
|
||||
err = check.sendEmailNotification(nil)
|
||||
assert.Error(t, err)
|
||||
check.results = []folderRetentionCheckResult{
|
||||
{
|
||||
Path: "/",
|
||||
Retention: 24,
|
||||
DeletedFiles: 20,
|
||||
DeletedSize: 456789,
|
||||
Elapsed: 12 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
smtpCfg = smtp.Config{}
|
||||
err = smtpCfg.Initialize(configDir, true)
|
||||
require.NoError(t, err)
|
||||
err = check.sendEmailNotification(nil)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestRetentionHookNotifications(t *testing.T) {
|
||||
dataRetentionHook := Config.DataRetentionHook
|
||||
|
||||
Config.DataRetentionHook = fmt.Sprintf("http://%v", httpAddr)
|
||||
user := dataprovider.User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
Username: "user2",
|
||||
},
|
||||
}
|
||||
user.Permissions = make(map[string][]string)
|
||||
user.Permissions["/"] = []string{dataprovider.PermAny}
|
||||
check := RetentionCheck{
|
||||
Notifications: []RetentionCheckNotification{RetentionCheckNotificationHook},
|
||||
results: []folderRetentionCheckResult{
|
||||
{
|
||||
Path: "/",
|
||||
Retention: 24,
|
||||
DeletedFiles: 10,
|
||||
DeletedSize: 32657,
|
||||
Elapsed: 10 * time.Second,
|
||||
},
|
||||
},
|
||||
}
|
||||
conn := NewBaseConnection("", "", "", "", user)
|
||||
conn.SetProtocol(ProtocolDataRetention)
|
||||
conn.ID = fmt.Sprintf("data_retention_%v", user.Username)
|
||||
check.conn = conn
|
||||
check.sendNotifications(1*time.Second, nil)
|
||||
err := check.sendHookNotification(1*time.Second, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
Config.DataRetentionHook = fmt.Sprintf("http://%v/404", httpAddr)
|
||||
err = check.sendHookNotification(1*time.Second, nil)
|
||||
assert.ErrorIs(t, err, errUnexpectedHTTResponse)
|
||||
|
||||
Config.DataRetentionHook = "http://foo\x7f.com/retention"
|
||||
err = check.sendHookNotification(1*time.Second, err)
|
||||
assert.Error(t, err)
|
||||
|
||||
Config.DataRetentionHook = "relativepath"
|
||||
err = check.sendHookNotification(1*time.Second, err)
|
||||
assert.Error(t, err)
|
||||
|
||||
if runtime.GOOS != osWindows {
|
||||
hookCmd, err := exec.LookPath("true")
|
||||
assert.NoError(t, err)
|
||||
|
||||
Config.DataRetentionHook = hookCmd
|
||||
err = check.sendHookNotification(1*time.Second, err)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
Config.DataRetentionHook = dataRetentionHook
|
||||
}
|
||||
|
||||
func TestRetentionPermissionsAndGetFolder(t *testing.T) {
|
||||
user := dataprovider.User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
|
|
@ -311,7 +100,6 @@ func TestRetentionCheckAddRemove(t *testing.T) {
|
|||
Retention: 48,
|
||||
},
|
||||
},
|
||||
Notifications: []RetentionCheckNotification{RetentionCheckNotificationHook},
|
||||
}
|
||||
assert.NotNil(t, RetentionChecks.Add(check, &user))
|
||||
checks := RetentionChecks.Get("")
|
||||
|
|
@ -321,8 +109,6 @@ func TestRetentionCheckAddRemove(t *testing.T) {
|
|||
require.Len(t, checks[0].Folders, 1)
|
||||
assert.Equal(t, check.Folders[0].Path, checks[0].Folders[0].Path)
|
||||
assert.Equal(t, check.Folders[0].Retention, checks[0].Folders[0].Retention)
|
||||
require.Len(t, checks[0].Notifications, 1)
|
||||
assert.Equal(t, RetentionCheckNotificationHook, checks[0].Notifications[0])
|
||||
|
||||
assert.Nil(t, RetentionChecks.Add(check, &user))
|
||||
assert.True(t, RetentionChecks.remove(username))
|
||||
|
|
@ -349,7 +135,6 @@ func TestRetentionCheckRole(t *testing.T) {
|
|||
Retention: 48,
|
||||
},
|
||||
},
|
||||
Notifications: []RetentionCheckNotification{RetentionCheckNotificationHook},
|
||||
}
|
||||
assert.NotNil(t, RetentionChecks.Add(check, &user))
|
||||
checks := RetentionChecks.Get("")
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ type Defender interface {
|
|||
GetBanTime(ip string) (*time.Time, error)
|
||||
GetScore(ip string) (int, error)
|
||||
DeleteHost(ip string) bool
|
||||
DelayLogin(err error)
|
||||
}
|
||||
|
||||
// DefenderConfig defines the "defender" configuration
|
||||
|
|
@ -90,6 +91,16 @@ type DefenderConfig struct {
|
|||
// to return when you request for the entire host list from the defender
|
||||
EntriesSoftLimit int `json:"entries_soft_limit" mapstructure:"entries_soft_limit"`
|
||||
EntriesHardLimit int `json:"entries_hard_limit" mapstructure:"entries_hard_limit"`
|
||||
// Configuration to impose a delay between login attempts
|
||||
LoginDelay LoginDelay `json:"login_delay" mapstructure:"login_delay"`
|
||||
}
|
||||
|
||||
// LoginDelay defines the delays to impose between login attempts.
|
||||
type LoginDelay struct {
|
||||
// The number of milliseconds to pause prior to allowing a successful login
|
||||
Success int `json:"success" mapstructure:"success"`
|
||||
// The number of milliseconds to pause prior to reporting a failed login
|
||||
PasswordFailed int `json:"password_failed" mapstructure:"password_failed"`
|
||||
}
|
||||
|
||||
type baseDefender struct {
|
||||
|
|
@ -163,6 +174,19 @@ func (d *baseDefender) logBan(ip, protocol string) {
|
|||
Send()
|
||||
}
|
||||
|
||||
// DelayLogin applies the configured login delay.
|
||||
func (d *baseDefender) DelayLogin(err error) {
|
||||
if err == nil {
|
||||
if d.config.LoginDelay.Success > 0 {
|
||||
time.Sleep(time.Duration(d.config.LoginDelay.Success) * time.Millisecond)
|
||||
}
|
||||
return
|
||||
}
|
||||
if d.config.LoginDelay.PasswordFailed > 0 {
|
||||
time.Sleep(time.Duration(d.config.LoginDelay.PasswordFailed) * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
type hostEvent struct {
|
||||
dateTime time.Time
|
||||
score int
|
||||
|
|
|
|||
|
|
@ -435,6 +435,31 @@ func TestDefenderCleanup(t *testing.T) {
|
|||
assert.Equal(t, 0, score)
|
||||
}
|
||||
|
||||
func TestDefenderDelay(t *testing.T) {
|
||||
d := memoryDefender{
|
||||
baseDefender: baseDefender{
|
||||
config: &DefenderConfig{
|
||||
ObservationTime: 1,
|
||||
EntriesSoftLimit: 2,
|
||||
EntriesHardLimit: 3,
|
||||
LoginDelay: LoginDelay{
|
||||
Success: 50,
|
||||
PasswordFailed: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
startTime := time.Now()
|
||||
d.DelayLogin(nil)
|
||||
elapsed := time.Since(startTime)
|
||||
assert.Less(t, elapsed, time.Millisecond*100)
|
||||
|
||||
startTime = time.Now()
|
||||
d.DelayLogin(ErrInternalFailure)
|
||||
elapsed = time.Since(startTime)
|
||||
assert.Greater(t, elapsed, time.Millisecond*150)
|
||||
}
|
||||
|
||||
func TestDefenderConfig(t *testing.T) {
|
||||
c := DefenderConfig{}
|
||||
err := c.validate()
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ func (d *dbDefender) GetHost(ip string) (dataprovider.DefenderEntry, error) {
|
|||
// and increase ban time if the IP is found.
|
||||
// This method must be called as soon as the client connects
|
||||
func (d *dbDefender) IsBanned(ip, protocol string) bool {
|
||||
if d.baseDefender.isBanned(ip, protocol) {
|
||||
if d.isBanned(ip, protocol) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -95,22 +95,22 @@ func (d *dbDefender) AddEvent(ip, protocol string, event HostEvent) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
score := d.baseDefender.getScore(event)
|
||||
score := d.getScore(event)
|
||||
|
||||
host, err := dataprovider.AddDefenderEvent(ip, score, d.getStartObservationTime())
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
d.baseDefender.logEvent(ip, protocol, event, host.Score)
|
||||
d.logEvent(ip, protocol, event, host.Score)
|
||||
if host.Score > d.config.Threshold {
|
||||
d.baseDefender.logBan(ip, protocol)
|
||||
d.logBan(ip, protocol)
|
||||
banTime := time.Now().Add(time.Duration(d.config.BanTime) * time.Minute)
|
||||
err = dataprovider.SetDefenderBanTime(ip, util.GetTimeAsMsSinceEpoch(banTime))
|
||||
if err == nil {
|
||||
eventManager.handleIPBlockedEvent(EventParams{
|
||||
Event: ipBlockedEventName,
|
||||
IP: ip,
|
||||
Timestamp: time.Now().UnixNano(),
|
||||
Timestamp: time.Now(),
|
||||
Status: 1,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ func (d *memoryDefender) IsBanned(ip, protocol string) bool {
|
|||
|
||||
defer d.RUnlock()
|
||||
|
||||
return d.baseDefender.isBanned(ip, protocol)
|
||||
return d.isBanned(ip, protocol)
|
||||
}
|
||||
|
||||
// DeleteHost removes the specified IP from the defender lists
|
||||
|
|
@ -188,7 +188,7 @@ func (d *memoryDefender) AddEvent(ip, protocol string, event HostEvent) bool {
|
|||
delete(d.banned, ip)
|
||||
}
|
||||
|
||||
score := d.baseDefender.getScore(event)
|
||||
score := d.getScore(event)
|
||||
|
||||
ev := hostEvent{
|
||||
dateTime: time.Now(),
|
||||
|
|
@ -207,25 +207,25 @@ func (d *memoryDefender) AddEvent(ip, protocol string, event HostEvent) bool {
|
|||
idx++
|
||||
}
|
||||
}
|
||||
d.baseDefender.logEvent(ip, protocol, event, hs.TotalScore)
|
||||
d.logEvent(ip, protocol, event, hs.TotalScore)
|
||||
|
||||
hs.Events = hs.Events[:idx]
|
||||
if hs.TotalScore >= d.config.Threshold {
|
||||
d.baseDefender.logBan(ip, protocol)
|
||||
d.logBan(ip, protocol)
|
||||
d.banned[ip] = time.Now().Add(time.Duration(d.config.BanTime) * time.Minute)
|
||||
delete(d.hosts, ip)
|
||||
d.cleanupBanned()
|
||||
eventManager.handleIPBlockedEvent(EventParams{
|
||||
Event: ipBlockedEventName,
|
||||
IP: ip,
|
||||
Timestamp: time.Now().UnixNano(),
|
||||
Timestamp: time.Now(),
|
||||
Status: 1,
|
||||
})
|
||||
} else {
|
||||
d.hosts[ip] = hs
|
||||
}
|
||||
} else {
|
||||
d.baseDefender.logEvent(ip, protocol, event, ev.score)
|
||||
d.logEvent(ip, protocol, event, ev.score)
|
||||
d.hosts[ip] = hostScore{
|
||||
TotalScore: ev.score,
|
||||
Events: []hostEvent{ev},
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
|
|
@ -31,6 +32,7 @@ import (
|
|||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
|
@ -55,8 +57,9 @@ import (
|
|||
const (
|
||||
ipBlockedEventName = "IP Blocked"
|
||||
maxAttachmentsSize = int64(10 * 1024 * 1024)
|
||||
objDataPlaceholder = "{{ObjectData}}"
|
||||
objDataPlaceholderString = "{{ObjectDataString}}"
|
||||
objDataPlaceholder = "{{.ObjectData}}"
|
||||
objDataPlaceholderString = "{{.ObjectDataString}}"
|
||||
dateTimeMillisFormat = "2006-01-02T15:04:05.000"
|
||||
)
|
||||
|
||||
// Supported IDP login events
|
||||
|
|
@ -69,6 +72,9 @@ var (
|
|||
// eventManager handle the supported event rules actions
|
||||
eventManager eventRulesContainer
|
||||
multipartQuoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
|
||||
fsEventsWithSize = []string{operationPreDelete, OperationPreUpload, operationDelete,
|
||||
operationCopy, operationDownload, operationFirstUpload, operationFirstDownload,
|
||||
operationUpload}
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
|
@ -88,11 +94,12 @@ func init() {
|
|||
ObjectType: objectType,
|
||||
IP: ip,
|
||||
Role: role,
|
||||
Timestamp: time.Now().UnixNano(),
|
||||
Timestamp: time.Now(),
|
||||
Object: object,
|
||||
}
|
||||
if u, ok := object.(*dataprovider.User); ok {
|
||||
p.Email = u.Email
|
||||
p.Groups = u.Groups
|
||||
} else if a, ok := object.(*dataprovider.Admin); ok {
|
||||
p.Email = a.Email
|
||||
}
|
||||
|
|
@ -271,7 +278,8 @@ func (r *eventRulesContainer) addUpdateRuleInternal(rule dataprovider.EventRule)
|
|||
func (r *eventRulesContainer) loadRules() {
|
||||
eventManagerLog(logger.LevelDebug, "loading updated rules")
|
||||
modTime := util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
rules, err := dataprovider.GetRecentlyUpdatedRules(r.getLastLoadTime())
|
||||
lastLoadTime := r.getLastLoadTime()
|
||||
rules, err := dataprovider.GetRecentlyUpdatedRules(lastLoadTime)
|
||||
if err != nil {
|
||||
eventManagerLog(logger.LevelError, "unable to load event rules: %v", err)
|
||||
return
|
||||
|
|
@ -307,23 +315,26 @@ func (*eventRulesContainer) checkIPDLoginEventMatch(conditions *dataprovider.Eve
|
|||
}
|
||||
|
||||
func (*eventRulesContainer) checkProviderEventMatch(conditions *dataprovider.EventConditions, params *EventParams) bool {
|
||||
if !util.Contains(conditions.ProviderEvents, params.Event) {
|
||||
if !slices.Contains(conditions.ProviderEvents, params.Event) {
|
||||
return false
|
||||
}
|
||||
if !checkEventConditionPatterns(params.Name, conditions.Options.Names) {
|
||||
return false
|
||||
}
|
||||
if !checkEventGroupConditionPatterns(params.Groups, conditions.Options.GroupNames) {
|
||||
return false
|
||||
}
|
||||
if !checkEventConditionPatterns(params.Role, conditions.Options.RoleNames) {
|
||||
return false
|
||||
}
|
||||
if len(conditions.Options.ProviderObjects) > 0 && !util.Contains(conditions.Options.ProviderObjects, params.ObjectType) {
|
||||
if len(conditions.Options.ProviderObjects) > 0 && !slices.Contains(conditions.Options.ProviderObjects, params.ObjectType) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (*eventRulesContainer) checkFsEventMatch(conditions *dataprovider.EventConditions, params *EventParams) bool {
|
||||
if !util.Contains(conditions.FsEvents, params.Event) {
|
||||
if !slices.Contains(conditions.FsEvents, params.Event) {
|
||||
return false
|
||||
}
|
||||
if !checkEventConditionPatterns(params.Name, conditions.Options.Names) {
|
||||
|
|
@ -338,10 +349,10 @@ func (*eventRulesContainer) checkFsEventMatch(conditions *dataprovider.EventCond
|
|||
if !checkEventConditionPatterns(params.VirtualPath, conditions.Options.FsPaths) {
|
||||
return false
|
||||
}
|
||||
if len(conditions.Options.Protocols) > 0 && !util.Contains(conditions.Options.Protocols, params.Protocol) {
|
||||
if len(conditions.Options.Protocols) > 0 && !slices.Contains(conditions.Options.Protocols, params.Protocol) {
|
||||
return false
|
||||
}
|
||||
if params.Event == operationUpload || params.Event == operationDownload {
|
||||
if slices.Contains(fsEventsWithSize, params.Event) {
|
||||
if conditions.Options.MinFileSize > 0 {
|
||||
if params.FileSize < conditions.Options.MinFileSize {
|
||||
return false
|
||||
|
|
@ -555,7 +566,7 @@ type EventParams struct {
|
|||
IP string
|
||||
Role string
|
||||
Email string
|
||||
Timestamp int64
|
||||
Timestamp time.Time
|
||||
UID string
|
||||
IDPCustomFields *map[string]string
|
||||
Object plugin.Renderer
|
||||
|
|
@ -639,7 +650,7 @@ func (p *EventParams) setBackupParams(backupPath string) {
|
|||
p.FsPath = backupPath
|
||||
p.ObjectName = filepath.Base(backupPath)
|
||||
p.VirtualPath = "/" + p.ObjectName
|
||||
p.Timestamp = time.Now().UnixNano()
|
||||
p.Timestamp = time.Now()
|
||||
info, err := os.Stat(backupPath)
|
||||
if err == nil {
|
||||
p.FileSize = info.Size()
|
||||
|
|
@ -765,46 +776,70 @@ func (p *EventParams) getRetentionReportsAsMailAttachment() (*mail.File, error)
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (*EventParams) getStringReplacement(val string, jsonEscaped bool) string {
|
||||
if jsonEscaped {
|
||||
func (*EventParams) getStringReplacement(val string, escapeMode int) string {
|
||||
switch escapeMode {
|
||||
case 1:
|
||||
return util.JSONEscape(val)
|
||||
case 2:
|
||||
return html.EscapeString(val)
|
||||
default:
|
||||
return val
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (p *EventParams) getStringReplacements(addObjectData, jsonEscaped bool) []string {
|
||||
func (p *EventParams) getStringReplacements(addObjectData bool, escapeMode int) []string {
|
||||
var dateTimeString string
|
||||
if Config.TZ == "local" {
|
||||
dateTimeString = p.Timestamp.Local().Format(dateTimeMillisFormat)
|
||||
} else {
|
||||
dateTimeString = p.Timestamp.UTC().Format(dateTimeMillisFormat)
|
||||
}
|
||||
year := dateTimeString[0:4]
|
||||
month := dateTimeString[5:7]
|
||||
day := dateTimeString[8:10]
|
||||
hour := dateTimeString[11:13]
|
||||
minute := dateTimeString[14:16]
|
||||
|
||||
replacements := []string{
|
||||
"{{Name}}", p.getStringReplacement(p.Name, jsonEscaped),
|
||||
"{{Event}}", p.Event,
|
||||
"{{Status}}", fmt.Sprintf("%d", p.Status),
|
||||
"{{VirtualPath}}", p.getStringReplacement(p.VirtualPath, jsonEscaped),
|
||||
"{{FsPath}}", p.getStringReplacement(p.FsPath, jsonEscaped),
|
||||
"{{VirtualTargetPath}}", p.getStringReplacement(p.VirtualTargetPath, jsonEscaped),
|
||||
"{{FsTargetPath}}", p.getStringReplacement(p.FsTargetPath, jsonEscaped),
|
||||
"{{ObjectName}}", p.getStringReplacement(p.ObjectName, jsonEscaped),
|
||||
"{{ObjectType}}", p.ObjectType,
|
||||
"{{FileSize}}", strconv.FormatInt(p.FileSize, 10),
|
||||
"{{Elapsed}}", strconv.FormatInt(p.Elapsed, 10),
|
||||
"{{Protocol}}", p.Protocol,
|
||||
"{{IP}}", p.IP,
|
||||
"{{Role}}", p.getStringReplacement(p.Role, jsonEscaped),
|
||||
"{{Email}}", p.getStringReplacement(p.Email, jsonEscaped),
|
||||
"{{Timestamp}}", strconv.FormatInt(p.Timestamp, 10),
|
||||
"{{StatusString}}", p.getStatusString(),
|
||||
"{{UID}}", p.getStringReplacement(p.UID, jsonEscaped),
|
||||
"{{Ext}}", p.getStringReplacement(p.Extension, jsonEscaped),
|
||||
"{{.Name}}", p.getStringReplacement(p.Name, escapeMode),
|
||||
"{{.Event}}", p.Event,
|
||||
"{{.Status}}", fmt.Sprintf("%d", p.Status),
|
||||
"{{.VirtualPath}}", p.getStringReplacement(p.VirtualPath, escapeMode),
|
||||
"{{.EscapedVirtualPath}}", p.getStringReplacement(url.QueryEscape(p.VirtualPath), escapeMode),
|
||||
"{{.FsPath}}", p.getStringReplacement(p.FsPath, escapeMode),
|
||||
"{{.VirtualTargetPath}}", p.getStringReplacement(p.VirtualTargetPath, escapeMode),
|
||||
"{{.FsTargetPath}}", p.getStringReplacement(p.FsTargetPath, escapeMode),
|
||||
"{{.ObjectName}}", p.getStringReplacement(p.ObjectName, escapeMode),
|
||||
"{{.ObjectBaseName}}", p.getStringReplacement(strings.TrimSuffix(p.ObjectName, p.Extension), escapeMode),
|
||||
"{{.ObjectType}}", p.ObjectType,
|
||||
"{{.FileSize}}", strconv.FormatInt(p.FileSize, 10),
|
||||
"{{.Elapsed}}", strconv.FormatInt(p.Elapsed, 10),
|
||||
"{{.Protocol}}", p.Protocol,
|
||||
"{{.IP}}", p.IP,
|
||||
"{{.Role}}", p.getStringReplacement(p.Role, escapeMode),
|
||||
"{{.Email}}", p.getStringReplacement(p.Email, escapeMode),
|
||||
"{{.Timestamp}}", strconv.FormatInt(p.Timestamp.UnixNano(), 10),
|
||||
"{{.DateTime}}", dateTimeString,
|
||||
"{{.Year}}", year,
|
||||
"{{.Month}}", month,
|
||||
"{{.Day}}", day,
|
||||
"{{.Hour}}", hour,
|
||||
"{{.Minute}}", minute,
|
||||
"{{.StatusString}}", p.getStatusString(),
|
||||
"{{.UID}}", p.getStringReplacement(p.UID, escapeMode),
|
||||
"{{.Ext}}", p.getStringReplacement(p.Extension, escapeMode),
|
||||
}
|
||||
if p.VirtualPath != "" {
|
||||
replacements = append(replacements, "{{VirtualDirPath}}", p.getStringReplacement(path.Dir(p.VirtualPath), jsonEscaped))
|
||||
replacements = append(replacements, "{{.VirtualDirPath}}", p.getStringReplacement(path.Dir(p.VirtualPath), escapeMode))
|
||||
}
|
||||
if p.VirtualTargetPath != "" {
|
||||
replacements = append(replacements, "{{VirtualTargetDirPath}}", p.getStringReplacement(path.Dir(p.VirtualTargetPath), jsonEscaped))
|
||||
replacements = append(replacements, "{{TargetName}}", p.getStringReplacement(path.Base(p.VirtualTargetPath), jsonEscaped))
|
||||
replacements = append(replacements, "{{.VirtualTargetDirPath}}", p.getStringReplacement(path.Dir(p.VirtualTargetPath), escapeMode))
|
||||
replacements = append(replacements, "{{.TargetName}}", p.getStringReplacement(path.Base(p.VirtualTargetPath), escapeMode))
|
||||
}
|
||||
if len(p.errors) > 0 {
|
||||
replacements = append(replacements, "{{ErrorString}}", p.getStringReplacement(strings.Join(p.errors, ", "), jsonEscaped))
|
||||
replacements = append(replacements, "{{.ErrorString}}", p.getStringReplacement(strings.Join(p.errors, ", "), escapeMode))
|
||||
} else {
|
||||
replacements = append(replacements, "{{ErrorString}}", "")
|
||||
replacements = append(replacements, "{{.ErrorString}}", "")
|
||||
}
|
||||
replacements = append(replacements, objDataPlaceholder, "{}")
|
||||
replacements = append(replacements, objDataPlaceholderString, "")
|
||||
|
|
@ -812,23 +847,23 @@ func (p *EventParams) getStringReplacements(addObjectData, jsonEscaped bool) []s
|
|||
data, err := p.Object.RenderAsJSON(p.Event != operationDelete)
|
||||
if err == nil {
|
||||
dataString := util.BytesToString(data)
|
||||
replacements[len(replacements)-3] = p.getStringReplacement(dataString, false)
|
||||
replacements[len(replacements)-1] = p.getStringReplacement(dataString, true)
|
||||
replacements[len(replacements)-3] = p.getStringReplacement(dataString, 0)
|
||||
replacements[len(replacements)-1] = p.getStringReplacement(dataString, 1)
|
||||
}
|
||||
}
|
||||
if p.IDPCustomFields != nil {
|
||||
for k, v := range *p.IDPCustomFields {
|
||||
replacements = append(replacements, fmt.Sprintf("{{IDPField%s}}", k), p.getStringReplacement(v, jsonEscaped))
|
||||
replacements = append(replacements, fmt.Sprintf("{{.IDPField%s}}", k), p.getStringReplacement(v, escapeMode))
|
||||
}
|
||||
}
|
||||
replacements = append(replacements, "{{Metadata}}", "{}")
|
||||
replacements = append(replacements, "{{MetadataString}}", "")
|
||||
replacements = append(replacements, "{{.Metadata}}", "{}")
|
||||
replacements = append(replacements, "{{.MetadataString}}", "")
|
||||
if len(p.Metadata) > 0 {
|
||||
data, err := json.Marshal(p.Metadata)
|
||||
if err == nil {
|
||||
dataString := util.BytesToString(data)
|
||||
replacements[len(replacements)-3] = p.getStringReplacement(dataString, false)
|
||||
replacements[len(replacements)-1] = p.getStringReplacement(dataString, true)
|
||||
replacements[len(replacements)-3] = p.getStringReplacement(dataString, 0)
|
||||
replacements[len(replacements)-1] = p.getStringReplacement(dataString, 1)
|
||||
}
|
||||
}
|
||||
return replacements
|
||||
|
|
@ -908,10 +943,7 @@ func updateUserQuotaAfterFileWrite(conn *BaseConnection, virtualPath string, num
|
|||
dataprovider.UpdateUserQuota(&conn.User, numFiles, fileSize, false) //nolint:errcheck
|
||||
return
|
||||
}
|
||||
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, numFiles, fileSize, false) //nolint:errcheck
|
||||
if vfolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&conn.User, numFiles, fileSize, false) //nolint:errcheck
|
||||
}
|
||||
dataprovider.UpdateUserFolderQuota(&vfolder, &conn.User, numFiles, fileSize, false)
|
||||
}
|
||||
|
||||
func checkWriterPermsAndQuota(conn *BaseConnection, virtualPath string, numFiles int, expectedSize, truncatedSize int64) error {
|
||||
|
|
@ -988,7 +1020,7 @@ func getFileWriter(conn *BaseConnection, virtualPath string, expectedSize int64)
|
|||
return w, numFiles, truncatedSize, cancelFn, nil
|
||||
}
|
||||
|
||||
func addZipEntry(wr *zipWriterWrapper, conn *BaseConnection, entryPath, baseDir string, recursion int) error {
|
||||
func addZipEntry(wr *zipWriterWrapper, conn *BaseConnection, entryPath, baseDir string, info os.FileInfo, recursion int) error { //nolint:gocyclo
|
||||
if entryPath == wr.Name {
|
||||
// skip the archive itself
|
||||
return nil
|
||||
|
|
@ -998,10 +1030,13 @@ func addZipEntry(wr *zipWriterWrapper, conn *BaseConnection, entryPath, baseDir
|
|||
return util.ErrRecursionTooDeep
|
||||
}
|
||||
recursion++
|
||||
info, err := conn.DoStat(entryPath, 1, false)
|
||||
if err != nil {
|
||||
eventManagerLog(logger.LevelError, "unable to add zip entry %q, stat error: %v", entryPath, err)
|
||||
return err
|
||||
var err error
|
||||
if info == nil {
|
||||
info, err = conn.DoStat(entryPath, 1, false)
|
||||
if err != nil {
|
||||
eventManagerLog(logger.LevelError, "unable to add zip entry %q, stat error: %v", entryPath, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
entryName, err := getZipEntryName(entryPath, baseDir)
|
||||
if err != nil {
|
||||
|
|
@ -1039,7 +1074,7 @@ func addZipEntry(wr *zipWriterWrapper, conn *BaseConnection, entryPath, baseDir
|
|||
}
|
||||
for _, info := range contents {
|
||||
fullPath := util.CleanPath(path.Join(entryPath, info.Name()))
|
||||
if err := addZipEntry(wr, conn, fullPath, baseDir, recursion); err != nil {
|
||||
if err := addZipEntry(wr, conn, fullPath, baseDir, info, recursion); err != nil {
|
||||
eventManagerLog(logger.LevelError, "unable to add zip entry: %v", err)
|
||||
return err
|
||||
}
|
||||
|
|
@ -1163,7 +1198,7 @@ func getMailAttachments(conn *BaseConnection, attachments []string, replacer *st
|
|||
}
|
||||
|
||||
func replaceWithReplacer(input string, replacer *strings.Replacer) string {
|
||||
if !strings.Contains(input, "{{") {
|
||||
if !strings.Contains(input, "{{.") {
|
||||
return input
|
||||
}
|
||||
return replacer.Replace(input)
|
||||
|
|
@ -1250,7 +1285,7 @@ func getHTTPRuleActionEndpoint(c *dataprovider.EventActionHTTPConfig, replacer *
|
|||
if err != nil {
|
||||
return "", fmt.Errorf("invalid endpoint: %w", err)
|
||||
}
|
||||
if strings.Contains(u.Path, "{{") {
|
||||
if strings.Contains(u.Path, "{{.") {
|
||||
pathComponents := strings.Split(u.Path, "/")
|
||||
for idx := range pathComponents {
|
||||
part := replaceWithReplacer(pathComponents[idx], replacer)
|
||||
|
|
@ -1284,7 +1319,7 @@ func writeHTTPPart(m *multipart.Writer, part dataprovider.HTTPPart, h textproto.
|
|||
if part.Body != "" {
|
||||
cType := h.Get("Content-Type")
|
||||
if strings.Contains(strings.ToLower(cType), "application/json") {
|
||||
replacements := params.getStringReplacements(addObjectData, true)
|
||||
replacements := params.getStringReplacements(addObjectData, 1)
|
||||
jsonReplacer := strings.NewReplacer(replacements...)
|
||||
_, err = partWriter.Write(util.StringToBytes(replaceWithReplacer(part.Body, jsonReplacer)))
|
||||
} else {
|
||||
|
|
@ -1316,7 +1351,7 @@ func writeHTTPPart(m *multipart.Writer, part dataprovider.HTTPPart, h textproto.
|
|||
return nil
|
||||
}
|
||||
|
||||
func getHTTPRuleActionBody(c *dataprovider.EventActionHTTPConfig, replacer *strings.Replacer,
|
||||
func getHTTPRuleActionBody(c *dataprovider.EventActionHTTPConfig, replacer *strings.Replacer, //nolint:gocyclo
|
||||
cancel context.CancelFunc, user dataprovider.User, params *EventParams, addObjectData bool,
|
||||
) (io.Reader, string, error) {
|
||||
var body io.Reader
|
||||
|
|
@ -1332,7 +1367,7 @@ func getHTTPRuleActionBody(c *dataprovider.EventActionHTTPConfig, replacer *stri
|
|||
return bytes.NewBuffer(data), "", nil
|
||||
}
|
||||
if c.HasJSONBody() {
|
||||
replacements := params.getStringReplacements(addObjectData, true)
|
||||
replacements := params.getStringReplacements(addObjectData, 1)
|
||||
jsonReplacer := strings.NewReplacer(replacements...)
|
||||
return bytes.NewBufferString(replaceWithReplacer(c.Body, jsonReplacer)), "", nil
|
||||
}
|
||||
|
|
@ -1345,8 +1380,7 @@ func getHTTPRuleActionBody(c *dataprovider.EventActionHTTPConfig, replacer *stri
|
|||
var conn *BaseConnection
|
||||
if user.Username != "" {
|
||||
var err error
|
||||
user, err = getUserForEventAction(user)
|
||||
if err != nil {
|
||||
if err := getUserForEventAction(&user); err != nil {
|
||||
return body, "", err
|
||||
}
|
||||
connectionID := fmt.Sprintf("%s_%s", protocolEventAction, xid.New().String())
|
||||
|
|
@ -1362,6 +1396,9 @@ func getHTTPRuleActionBody(c *dataprovider.EventActionHTTPConfig, replacer *stri
|
|||
go func() {
|
||||
defer w.Close()
|
||||
defer user.CloseFs() //nolint:errcheck
|
||||
if conn != nil {
|
||||
defer conn.CloseFS() //nolint:errcheck
|
||||
}
|
||||
|
||||
for _, part := range c.Parts {
|
||||
h := make(textproto.MIMEHeader)
|
||||
|
|
@ -1417,7 +1454,7 @@ func executeHTTPRuleAction(c dataprovider.EventActionHTTPConfig, params *EventPa
|
|||
addObjectData = c.HasObjectData()
|
||||
}
|
||||
|
||||
replacements := params.getStringReplacements(addObjectData, false)
|
||||
replacements := params.getStringReplacements(addObjectData, 0)
|
||||
replacer := strings.NewReplacer(replacements...)
|
||||
endpoint, err := getHTTPRuleActionEndpoint(&c, replacer)
|
||||
if err != nil {
|
||||
|
|
@ -1467,7 +1504,7 @@ func executeHTTPRuleAction(c dataprovider.EventActionHTTPConfig, params *EventPa
|
|||
if resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusNoContent {
|
||||
if rb, err := io.ReadAll(io.LimitReader(resp.Body, 2048)); err == nil {
|
||||
eventManagerLog(logger.LevelDebug, "error notification response from endpoint %q: %s",
|
||||
endpoint, util.BytesToString(rb))
|
||||
endpoint, rb)
|
||||
}
|
||||
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
|
@ -1476,6 +1513,9 @@ func executeHTTPRuleAction(c dataprovider.EventActionHTTPConfig, params *EventPa
|
|||
}
|
||||
|
||||
func executeCommandRuleAction(c dataprovider.EventActionCommandConfig, params *EventParams) error {
|
||||
if !dataprovider.IsActionCommandAllowed(c.Cmd) {
|
||||
return fmt.Errorf("command %q is not allowed", c.Cmd)
|
||||
}
|
||||
addObjectData := false
|
||||
if params.Object != nil {
|
||||
for _, k := range c.EnvVars {
|
||||
|
|
@ -1485,7 +1525,7 @@ func executeCommandRuleAction(c dataprovider.EventActionCommandConfig, params *E
|
|||
}
|
||||
}
|
||||
}
|
||||
replacements := params.getStringReplacements(addObjectData, false)
|
||||
replacements := params.getStringReplacements(addObjectData, 0)
|
||||
replacer := strings.NewReplacer(replacements...)
|
||||
|
||||
args := make([]string, 0, len(c.Args))
|
||||
|
|
@ -1499,7 +1539,7 @@ func executeCommandRuleAction(c dataprovider.EventActionCommandConfig, params *E
|
|||
cmd := exec.CommandContext(ctx, c.Cmd, args...)
|
||||
cmd.Env = []string{}
|
||||
for _, keyVal := range c.EnvVars {
|
||||
if keyVal.Value == "$" {
|
||||
if keyVal.Value == "$" && !strings.HasPrefix(strings.ToUpper(keyVal.Key), "SFTPGO_") {
|
||||
val := os.Getenv(keyVal.Key)
|
||||
if val == "" {
|
||||
eventManagerLog(logger.LevelDebug, "empty value for environment variable %q", keyVal.Key)
|
||||
|
|
@ -1540,9 +1580,16 @@ func executeEmailRuleAction(c dataprovider.EventActionEmailConfig, params *Event
|
|||
addObjectData = true
|
||||
}
|
||||
}
|
||||
replacements := params.getStringReplacements(addObjectData, false)
|
||||
replacements := params.getStringReplacements(addObjectData, 0)
|
||||
replacer := strings.NewReplacer(replacements...)
|
||||
body := replaceWithReplacer(c.Body, replacer)
|
||||
var body string
|
||||
if c.ContentType == 1 {
|
||||
replacements := params.getStringReplacements(addObjectData, 2)
|
||||
bodyReplacer := strings.NewReplacer(replacements...)
|
||||
body = replaceWithReplacer(c.Body, bodyReplacer)
|
||||
} else {
|
||||
body = replaceWithReplacer(c.Body, replacer)
|
||||
}
|
||||
subject := replaceWithReplacer(c.Subject, replacer)
|
||||
recipients := getEmailAddressesWithReplacer(c.Recipients, replacer)
|
||||
bcc := getEmailAddressesWithReplacer(c.Bcc, replacer)
|
||||
|
|
@ -1565,8 +1612,7 @@ func executeEmailRuleAction(c dataprovider.EventActionEmailConfig, params *Event
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user, err = getUserForEventAction(user)
|
||||
if err != nil {
|
||||
if err := getUserForEventAction(&user); err != nil {
|
||||
return err
|
||||
}
|
||||
connectionID := fmt.Sprintf("%s_%s", protocolEventAction, xid.New().String())
|
||||
|
|
@ -1576,6 +1622,8 @@ func executeEmailRuleAction(c dataprovider.EventActionEmailConfig, params *Event
|
|||
return fmt.Errorf("error getting email attachments, unable to check root fs for user %q: %w", user.Username, err)
|
||||
}
|
||||
conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user)
|
||||
defer conn.CloseFS() //nolint:errcheck
|
||||
|
||||
res, err := getMailAttachments(conn, fileAttachments, replacer)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -1591,11 +1639,11 @@ func executeEmailRuleAction(c dataprovider.EventActionEmailConfig, params *Event
|
|||
return nil
|
||||
}
|
||||
|
||||
func getUserForEventAction(user dataprovider.User) (dataprovider.User, error) {
|
||||
func getUserForEventAction(user *dataprovider.User) error {
|
||||
err := user.LoadAndApplyGroupSettings()
|
||||
if err != nil {
|
||||
eventManagerLog(logger.LevelError, "unable to get group for user %q: %+v", user.Username, err)
|
||||
return dataprovider.User{}, fmt.Errorf("unable to get groups for user %q", user.Username)
|
||||
return fmt.Errorf("unable to get groups for user %q", user.Username)
|
||||
}
|
||||
user.UploadDataTransfer = 0
|
||||
user.UploadBandwidth = 0
|
||||
|
|
@ -1606,7 +1654,7 @@ func getUserForEventAction(user dataprovider.User) (dataprovider.User, error) {
|
|||
for k := range user.Permissions {
|
||||
user.Permissions[k] = []string{dataprovider.PermAny}
|
||||
}
|
||||
return user, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func replacePathsPlaceholders(paths []string, replacer *strings.Replacer) []string {
|
||||
|
|
@ -1626,17 +1674,18 @@ func executeDeleteFileFsAction(conn *BaseConnection, item string, info os.FileIn
|
|||
}
|
||||
|
||||
func executeDeleteFsActionForUser(deletes []string, replacer *strings.Replacer, user dataprovider.User) error {
|
||||
user, err := getUserForEventAction(user)
|
||||
if err != nil {
|
||||
if err := getUserForEventAction(&user); err != nil {
|
||||
return err
|
||||
}
|
||||
connectionID := fmt.Sprintf("%s_%s", protocolEventAction, xid.New().String())
|
||||
err = user.CheckFsRoot(connectionID)
|
||||
err := user.CheckFsRoot(connectionID)
|
||||
defer user.CloseFs() //nolint:errcheck
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete error, unable to check root fs for user %q: %w", user.Username, err)
|
||||
}
|
||||
conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user)
|
||||
defer conn.CloseFS() //nolint:errcheck
|
||||
|
||||
for _, item := range replacePathsPlaceholders(deletes, replacer) {
|
||||
info, err := conn.DoStat(item, 0, false)
|
||||
if err != nil {
|
||||
|
|
@ -1694,17 +1743,18 @@ func executeDeleteFsRuleAction(deletes []string, replacer *strings.Replacer,
|
|||
}
|
||||
|
||||
func executeMkDirsFsActionForUser(dirs []string, replacer *strings.Replacer, user dataprovider.User) error {
|
||||
user, err := getUserForEventAction(user)
|
||||
if err != nil {
|
||||
if err := getUserForEventAction(&user); err != nil {
|
||||
return err
|
||||
}
|
||||
connectionID := fmt.Sprintf("%s_%s", protocolEventAction, xid.New().String())
|
||||
err = user.CheckFsRoot(connectionID)
|
||||
err := user.CheckFsRoot(connectionID)
|
||||
defer user.CloseFs() //nolint:errcheck
|
||||
if err != nil {
|
||||
return fmt.Errorf("mkdir error, unable to check root fs for user %q: %w", user.Username, err)
|
||||
}
|
||||
conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user)
|
||||
defer conn.CloseFS() //nolint:errcheck
|
||||
|
||||
for _, item := range replacePathsPlaceholders(dirs, replacer) {
|
||||
if err = conn.CheckParentDirs(path.Dir(item)); err != nil {
|
||||
return fmt.Errorf("unable to check parent dirs for %q, user %q: %w", item, user.Username, err)
|
||||
|
|
@ -1750,24 +1800,29 @@ func executeMkdirFsRuleAction(dirs []string, replacer *strings.Replacer,
|
|||
return nil
|
||||
}
|
||||
|
||||
func executeRenameFsActionForUser(renames []dataprovider.KeyValue, replacer *strings.Replacer,
|
||||
func executeRenameFsActionForUser(renames []dataprovider.RenameConfig, replacer *strings.Replacer,
|
||||
user dataprovider.User,
|
||||
) error {
|
||||
user, err := getUserForEventAction(user)
|
||||
if err != nil {
|
||||
if err := getUserForEventAction(&user); err != nil {
|
||||
return err
|
||||
}
|
||||
connectionID := fmt.Sprintf("%s_%s", protocolEventAction, xid.New().String())
|
||||
err = user.CheckFsRoot(connectionID)
|
||||
err := user.CheckFsRoot(connectionID)
|
||||
defer user.CloseFs() //nolint:errcheck
|
||||
if err != nil {
|
||||
return fmt.Errorf("rename error, unable to check root fs for user %q: %w", user.Username, err)
|
||||
}
|
||||
conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user)
|
||||
defer conn.CloseFS() //nolint:errcheck
|
||||
|
||||
for _, item := range renames {
|
||||
source := util.CleanPath(replaceWithReplacer(item.Key, replacer))
|
||||
target := util.CleanPath(replaceWithReplacer(item.Value, replacer))
|
||||
if err = conn.renameInternal(source, target, true); err != nil {
|
||||
checks := 0
|
||||
if item.UpdateModTime {
|
||||
checks += vfs.CheckUpdateModTime
|
||||
}
|
||||
if err = conn.renameInternal(source, target, true, checks); err != nil {
|
||||
return fmt.Errorf("unable to rename %q->%q, user %q: %w", source, target, user.Username, err)
|
||||
}
|
||||
eventManagerLog(logger.LevelDebug, "rename %q->%q ok, user %q", source, target, user.Username)
|
||||
|
|
@ -1775,21 +1830,22 @@ func executeRenameFsActionForUser(renames []dataprovider.KeyValue, replacer *str
|
|||
return nil
|
||||
}
|
||||
|
||||
func executeCopyFsActionForUser(copy []dataprovider.KeyValue, replacer *strings.Replacer,
|
||||
func executeCopyFsActionForUser(keyVals []dataprovider.KeyValue, replacer *strings.Replacer,
|
||||
user dataprovider.User,
|
||||
) error {
|
||||
user, err := getUserForEventAction(user)
|
||||
if err != nil {
|
||||
if err := getUserForEventAction(&user); err != nil {
|
||||
return err
|
||||
}
|
||||
connectionID := fmt.Sprintf("%s_%s", protocolEventAction, xid.New().String())
|
||||
err = user.CheckFsRoot(connectionID)
|
||||
err := user.CheckFsRoot(connectionID)
|
||||
defer user.CloseFs() //nolint:errcheck
|
||||
if err != nil {
|
||||
return fmt.Errorf("copy error, unable to check root fs for user %q: %w", user.Username, err)
|
||||
}
|
||||
conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user)
|
||||
for _, item := range copy {
|
||||
defer conn.CloseFS() //nolint:errcheck
|
||||
|
||||
for _, item := range keyVals {
|
||||
source := util.CleanPath(replaceWithReplacer(item.Key, replacer))
|
||||
target := util.CleanPath(replaceWithReplacer(item.Value, replacer))
|
||||
if strings.HasSuffix(item.Key, "/") {
|
||||
|
|
@ -1809,17 +1865,18 @@ func executeCopyFsActionForUser(copy []dataprovider.KeyValue, replacer *strings.
|
|||
func executeExistFsActionForUser(exist []string, replacer *strings.Replacer,
|
||||
user dataprovider.User,
|
||||
) error {
|
||||
user, err := getUserForEventAction(user)
|
||||
if err != nil {
|
||||
if err := getUserForEventAction(&user); err != nil {
|
||||
return err
|
||||
}
|
||||
connectionID := fmt.Sprintf("%s_%s", protocolEventAction, xid.New().String())
|
||||
err = user.CheckFsRoot(connectionID)
|
||||
err := user.CheckFsRoot(connectionID)
|
||||
defer user.CloseFs() //nolint:errcheck
|
||||
if err != nil {
|
||||
return fmt.Errorf("existence check error, unable to check root fs for user %q: %w", user.Username, err)
|
||||
}
|
||||
conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user)
|
||||
defer conn.CloseFS() //nolint:errcheck
|
||||
|
||||
for _, item := range replacePathsPlaceholders(exist, replacer) {
|
||||
if _, err = conn.DoStat(item, 0, false); err != nil {
|
||||
return fmt.Errorf("error checking existence for path %q, user %q: %w", item, user.Username, err)
|
||||
|
|
@ -1829,7 +1886,7 @@ func executeExistFsActionForUser(exist []string, replacer *strings.Replacer,
|
|||
return nil
|
||||
}
|
||||
|
||||
func executeRenameFsRuleAction(renames []dataprovider.KeyValue, replacer *strings.Replacer,
|
||||
func executeRenameFsRuleAction(renames []dataprovider.RenameConfig, replacer *strings.Replacer,
|
||||
conditions dataprovider.ConditionOptions, params *EventParams,
|
||||
) error {
|
||||
users, err := params.getUsers()
|
||||
|
|
@ -1863,7 +1920,7 @@ func executeRenameFsRuleAction(renames []dataprovider.KeyValue, replacer *string
|
|||
return nil
|
||||
}
|
||||
|
||||
func executeCopyFsRuleAction(copy []dataprovider.KeyValue, replacer *strings.Replacer,
|
||||
func executeCopyFsRuleAction(keyVals []dataprovider.KeyValue, replacer *strings.Replacer,
|
||||
conditions dataprovider.ConditionOptions, params *EventParams,
|
||||
) error {
|
||||
users, err := params.getUsers()
|
||||
|
|
@ -1882,7 +1939,7 @@ func executeCopyFsRuleAction(copy []dataprovider.KeyValue, replacer *strings.Rep
|
|||
}
|
||||
}
|
||||
executed++
|
||||
if err = executeCopyFsActionForUser(copy, replacer, user); err != nil {
|
||||
if err = executeCopyFsActionForUser(keyVals, replacer, user); err != nil {
|
||||
failures = append(failures, user.Username)
|
||||
params.AddError(err)
|
||||
}
|
||||
|
|
@ -1967,17 +2024,18 @@ func estimateZipSize(conn *BaseConnection, zipPath string, paths []string) (int6
|
|||
func executeCompressFsActionForUser(c dataprovider.EventActionFsCompress, replacer *strings.Replacer,
|
||||
user dataprovider.User,
|
||||
) error {
|
||||
user, err := getUserForEventAction(user)
|
||||
if err != nil {
|
||||
if err := getUserForEventAction(&user); err != nil {
|
||||
return err
|
||||
}
|
||||
connectionID := fmt.Sprintf("%s_%s", protocolEventAction, xid.New().String())
|
||||
err = user.CheckFsRoot(connectionID)
|
||||
err := user.CheckFsRoot(connectionID)
|
||||
defer user.CloseFs() //nolint:errcheck
|
||||
if err != nil {
|
||||
return fmt.Errorf("compress error, unable to check root fs for user %q: %w", user.Username, err)
|
||||
}
|
||||
conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user)
|
||||
defer conn.CloseFS() //nolint:errcheck
|
||||
|
||||
name := util.CleanPath(replaceWithReplacer(c.Name, replacer))
|
||||
conn.CheckParentDirs(path.Dir(name)) //nolint:errcheck
|
||||
paths := make([]string, 0, len(c.Paths))
|
||||
|
|
@ -2011,7 +2069,7 @@ func executeCompressFsActionForUser(c dataprovider.EventActionFsCompress, replac
|
|||
}
|
||||
startTime := time.Now()
|
||||
for _, item := range paths {
|
||||
if err := addZipEntry(zipWriter, conn, item, baseDir, 0); err != nil {
|
||||
if err := addZipEntry(zipWriter, conn, item, baseDir, nil, 0); err != nil {
|
||||
closeWriterAndUpdateQuota(writer, conn, name, "", numFiles, truncatedSize, err, operationUpload, startTime) //nolint:errcheck
|
||||
return err
|
||||
}
|
||||
|
|
@ -2096,7 +2154,7 @@ func executeFsRuleAction(c dataprovider.EventActionFilesystemConfig, conditions
|
|||
params *EventParams,
|
||||
) error {
|
||||
addObjectData := false
|
||||
replacements := params.getStringReplacements(addObjectData, false)
|
||||
replacements := params.getStringReplacements(addObjectData, 0)
|
||||
replacer := strings.NewReplacer(replacements...)
|
||||
switch c.Type {
|
||||
case dataprovider.FilesystemActionRename:
|
||||
|
|
@ -2448,7 +2506,7 @@ func executePwdExpirationCheckForUser(user *dataprovider.User, config dataprovid
|
|||
}
|
||||
subject := "SFTPGo password expiration notification"
|
||||
startTime := time.Now()
|
||||
if err := smtp.SendEmail([]string{user.Email}, nil, subject, body.String(), smtp.EmailContentTypeTextHTML); err != nil {
|
||||
if err := smtp.SendEmail(user.GetEmailAddresses(), nil, subject, body.String(), smtp.EmailContentTypeTextHTML); err != nil {
|
||||
eventManagerLog(logger.LevelError, "unable to notify password expiration for user %s: %v, elapsed: %s",
|
||||
user.Username, err, time.Since(startTime))
|
||||
return err
|
||||
|
|
@ -2496,7 +2554,7 @@ func executeAdminCheckAction(c *dataprovider.EventActionIDPAccountCheck, params
|
|||
return nil, err
|
||||
}
|
||||
|
||||
replacements := params.getStringReplacements(false, true)
|
||||
replacements := params.getStringReplacements(false, 1)
|
||||
replacer := strings.NewReplacer(replacements...)
|
||||
data := replaceWithReplacer(c.TemplateAdmin, replacer)
|
||||
|
||||
|
|
@ -2505,19 +2563,57 @@ func executeAdminCheckAction(c *dataprovider.EventActionIDPAccountCheck, params
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if newAdmin.Password == "" {
|
||||
newAdmin.Password = util.GenerateUniqueID()
|
||||
}
|
||||
if exists {
|
||||
eventManagerLog(logger.LevelDebug, "updating admin %q after IDP login", params.Name)
|
||||
// Not sure if this makes sense, but it shouldn't hurt.
|
||||
if newAdmin.Password == "" {
|
||||
newAdmin.Password = admin.Password
|
||||
}
|
||||
newAdmin.Filters.TOTPConfig = admin.Filters.TOTPConfig
|
||||
newAdmin.Filters.RecoveryCodes = admin.Filters.RecoveryCodes
|
||||
err = dataprovider.UpdateAdmin(&newAdmin, dataprovider.ActionExecutorSystem, "", "")
|
||||
} else {
|
||||
eventManagerLog(logger.LevelDebug, "creating admin %q after IDP login", params.Name)
|
||||
if newAdmin.Password == "" {
|
||||
newAdmin.Password = util.GenerateUniqueID()
|
||||
}
|
||||
err = dataprovider.AddAdmin(&newAdmin, dataprovider.ActionExecutorSystem, "", "")
|
||||
}
|
||||
return &newAdmin, err
|
||||
}
|
||||
|
||||
func preserveUserProfile(user, newUser *dataprovider.User) {
|
||||
if newUser.CanChangePassword() && user.Password != "" {
|
||||
newUser.Password = user.Password
|
||||
}
|
||||
if newUser.CanManagePublicKeys() && len(user.PublicKeys) > 0 {
|
||||
newUser.PublicKeys = user.PublicKeys
|
||||
}
|
||||
if newUser.CanManageTLSCerts() {
|
||||
if len(user.Filters.TLSCerts) > 0 {
|
||||
newUser.Filters.TLSCerts = user.Filters.TLSCerts
|
||||
}
|
||||
}
|
||||
if newUser.CanChangeInfo() {
|
||||
if user.Description != "" {
|
||||
newUser.Description = user.Description
|
||||
}
|
||||
if user.Email != "" {
|
||||
newUser.Email = user.Email
|
||||
}
|
||||
if len(user.Filters.AdditionalEmails) > 0 {
|
||||
newUser.Filters.AdditionalEmails = user.Filters.AdditionalEmails
|
||||
}
|
||||
}
|
||||
if newUser.CanChangeAPIKeyAuth() {
|
||||
newUser.Filters.AllowAPIKeyAuth = user.Filters.AllowAPIKeyAuth
|
||||
}
|
||||
newUser.Filters.RecoveryCodes = user.Filters.RecoveryCodes
|
||||
newUser.Filters.TOTPConfig = user.Filters.TOTPConfig
|
||||
newUser.LastPasswordChange = user.LastPasswordChange
|
||||
newUser.SetEmptySecretsIfNil()
|
||||
}
|
||||
|
||||
func executeUserCheckAction(c *dataprovider.EventActionIDPAccountCheck, params *EventParams) (*dataprovider.User, error) {
|
||||
user, err := dataprovider.UserExists(params.Name, "")
|
||||
exists := err == nil
|
||||
|
|
@ -2528,7 +2624,7 @@ func executeUserCheckAction(c *dataprovider.EventActionIDPAccountCheck, params *
|
|||
if err != nil && !errors.Is(err, util.ErrNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
replacements := params.getStringReplacements(false, true)
|
||||
replacements := params.getStringReplacements(false, 1)
|
||||
replacer := strings.NewReplacer(replacements...)
|
||||
data := replaceWithReplacer(c.TemplateUser, replacer)
|
||||
|
||||
|
|
@ -2539,6 +2635,7 @@ func executeUserCheckAction(c *dataprovider.EventActionIDPAccountCheck, params *
|
|||
}
|
||||
if exists {
|
||||
eventManagerLog(logger.LevelDebug, "updating user %q after IDP login", params.Name)
|
||||
preserveUserProfile(&user, &newUser)
|
||||
err = dataprovider.UpdateUser(&newUser, dataprovider.ActionExecutorSystem, "", "")
|
||||
} else {
|
||||
eventManagerLog(logger.LevelDebug, "creating user %q after IDP login", params.Name)
|
||||
|
|
@ -2551,9 +2648,14 @@ func executeUserCheckAction(c *dataprovider.EventActionIDPAccountCheck, params *
|
|||
return &u, err
|
||||
}
|
||||
|
||||
func executeRuleAction(action dataprovider.BaseEventAction, params *EventParams,
|
||||
func executeRuleAction(action dataprovider.BaseEventAction, params *EventParams, //nolint:gocyclo
|
||||
conditions dataprovider.ConditionOptions,
|
||||
) error {
|
||||
if len(conditions.EventStatuses) > 0 && !slices.Contains(conditions.EventStatuses, params.Status) {
|
||||
eventManagerLog(logger.LevelDebug, "skipping action %s, event status %d does not match: %v",
|
||||
action.Name, params.Status, conditions.EventStatuses)
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
|
||||
switch action.Type {
|
||||
|
|
@ -2585,6 +2687,8 @@ func executeRuleAction(action dataprovider.BaseEventAction, params *EventParams,
|
|||
err = executeUserExpirationCheckRuleAction(conditions, params)
|
||||
case dataprovider.ActionTypeUserInactivityCheck:
|
||||
err = executeUserInactivityCheckRuleAction(action.Options.UserInactivityConfig, conditions, params, time.Now())
|
||||
case dataprovider.ActionTypeRotateLogs:
|
||||
err = logger.RotateLogFile()
|
||||
default:
|
||||
err = fmt.Errorf("unsupported action type: %d", action.Type)
|
||||
}
|
||||
|
|
@ -2742,6 +2846,16 @@ func (j *eventCronJob) getTask(rule *dataprovider.EventRule) (dataprovider.Task,
|
|||
return dataprovider.Task{}, nil
|
||||
}
|
||||
|
||||
func (j *eventCronJob) getEventParams() EventParams {
|
||||
return EventParams{
|
||||
Event: "Schedule",
|
||||
Name: j.ruleName,
|
||||
Status: 1,
|
||||
Timestamp: time.Now(),
|
||||
updateStatusFromError: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (j *eventCronJob) Run() {
|
||||
eventManagerLog(logger.LevelDebug, "executing scheduled rule %q", j.ruleName)
|
||||
rule, err := dataprovider.EventRuleExists(j.ruleName)
|
||||
|
|
@ -2792,9 +2906,9 @@ func (j *eventCronJob) Run() {
|
|||
}
|
||||
}(task.Name)
|
||||
|
||||
executeAsyncRulesActions([]dataprovider.EventRule{rule}, EventParams{Status: 1, updateStatusFromError: true})
|
||||
executeAsyncRulesActions([]dataprovider.EventRule{rule}, j.getEventParams())
|
||||
} else {
|
||||
executeAsyncRulesActions([]dataprovider.EventRule{rule}, EventParams{Status: 1, updateStatusFromError: true})
|
||||
executeAsyncRulesActions([]dataprovider.EventRule{rule}, j.getEventParams())
|
||||
}
|
||||
eventManagerLog(logger.LevelDebug, "execution for scheduled rule %q finished", j.ruleName)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -800,6 +800,32 @@ func TestEventManagerErrors(t *testing.T) {
|
|||
stopEventScheduler()
|
||||
}
|
||||
|
||||
func TestDateTimePlaceholder(t *testing.T) {
|
||||
oldTZ := Config.TZ
|
||||
|
||||
Config.TZ = ""
|
||||
dateTime := time.Now()
|
||||
params := EventParams{
|
||||
Timestamp: dateTime,
|
||||
}
|
||||
replacements := params.getStringReplacements(false, 0)
|
||||
r := strings.NewReplacer(replacements...)
|
||||
res := r.Replace("{{.DateTime}}")
|
||||
assert.Equal(t, dateTime.UTC().Format(dateTimeMillisFormat), res)
|
||||
res = r.Replace("{{.Year}}-{{.Month}}-{{.Day}}T{{.Hour}}:{{.Minute}}")
|
||||
assert.Equal(t, dateTime.UTC().Format(dateTimeMillisFormat)[:16], res)
|
||||
|
||||
Config.TZ = "local"
|
||||
replacements = params.getStringReplacements(false, 0)
|
||||
r = strings.NewReplacer(replacements...)
|
||||
res = r.Replace("{{.DateTime}}")
|
||||
assert.Equal(t, dateTime.Local().Format(dateTimeMillisFormat), res)
|
||||
res = r.Replace("{{.Year}}-{{.Month}}-{{.Day}}T{{.Hour}}:{{.Minute}}")
|
||||
assert.Equal(t, dateTime.Local().Format(dateTimeMillisFormat)[:16], res)
|
||||
|
||||
Config.TZ = oldTZ
|
||||
}
|
||||
|
||||
func TestEventRuleActions(t *testing.T) {
|
||||
actionName := "test rule action"
|
||||
action := dataprovider.BaseEventAction{
|
||||
|
|
@ -819,7 +845,7 @@ func TestEventRuleActions(t *testing.T) {
|
|||
HTTPConfig: dataprovider.EventActionHTTPConfig{
|
||||
Endpoint: "http://foo\x7f.com/", // invalid URL
|
||||
SkipTLSVerify: true,
|
||||
Body: `"data": "{{ObjectDataString}}"`,
|
||||
Body: `"data": "{{.ObjectDataString}}"`,
|
||||
Method: http.MethodPost,
|
||||
QueryParameters: []dataprovider.KeyValue{
|
||||
{
|
||||
|
|
@ -887,7 +913,7 @@ func TestEventRuleActions(t *testing.T) {
|
|||
assert.Contains(t, getErrorString(err), "error getting user")
|
||||
|
||||
action.Options.HTTPConfig.Parts = nil
|
||||
action.Options.HTTPConfig.Body = "{{ObjectData}}"
|
||||
action.Options.HTTPConfig.Body = "{{.ObjectData}}"
|
||||
// test disk and transfer quota reset
|
||||
username1 := "user1"
|
||||
username2 := "user2"
|
||||
|
|
@ -1166,10 +1192,12 @@ func TestEventRuleActions(t *testing.T) {
|
|||
action.Options = dataprovider.BaseEventActionOptions{
|
||||
FsConfig: dataprovider.EventActionFilesystemConfig{
|
||||
Type: dataprovider.FilesystemActionRename,
|
||||
Renames: []dataprovider.KeyValue{
|
||||
Renames: []dataprovider.RenameConfig{
|
||||
{
|
||||
Key: "/source",
|
||||
Value: "/target",
|
||||
KeyValue: dataprovider.KeyValue{
|
||||
Key: "/source",
|
||||
Value: "/target",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -1221,7 +1249,7 @@ func TestEventRuleActions(t *testing.T) {
|
|||
Type: dataprovider.FilesystemActionCompress,
|
||||
Compress: dataprovider.EventActionFsCompress{
|
||||
Name: "test.zip",
|
||||
Paths: []string{"/{{VirtualPath}}"},
|
||||
Paths: []string{"/{{.VirtualPath}}"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -1386,6 +1414,31 @@ func TestIDPAccountCheckRule(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, username, user.Username)
|
||||
assert.Equal(t, 1, user.Status)
|
||||
assert.Empty(t, user.Password)
|
||||
assert.Len(t, user.PublicKeys, 0)
|
||||
assert.Len(t, user.Filters.TLSCerts, 0)
|
||||
assert.Empty(t, user.Email)
|
||||
assert.Empty(t, user.Description)
|
||||
// Update the profile attribute and make sure they are preserved
|
||||
user.Password = "secret"
|
||||
user.Email = "example@example.com"
|
||||
user.Filters.AdditionalEmails = []string{"alias@example.com"}
|
||||
user.Description = "some desc"
|
||||
user.Filters.TLSCerts = []string{serverCert}
|
||||
user.PublicKeys = []string{"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0= nicola@p1"}
|
||||
err = dataprovider.UpdateUser(user, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
user, err = executeUserCheckAction(c, params)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, username, user.Username)
|
||||
assert.Equal(t, 1, user.Status)
|
||||
assert.NotEmpty(t, user.Password)
|
||||
assert.Len(t, user.PublicKeys, 1)
|
||||
assert.Len(t, user.Filters.TLSCerts, 1)
|
||||
assert.NotEmpty(t, user.Email)
|
||||
assert.Len(t, user.Filters.AdditionalEmails, 1)
|
||||
assert.NotEmpty(t, user.Description)
|
||||
|
||||
err = dataprovider.DeleteUser(username, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -1686,10 +1739,12 @@ func TestFilesystemActionErrors(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = dataprovider.AddUser(&user, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
err = executeRenameFsActionForUser([]dataprovider.KeyValue{
|
||||
err = executeRenameFsActionForUser([]dataprovider.RenameConfig{
|
||||
{
|
||||
Key: "/p1",
|
||||
Value: "/p1",
|
||||
KeyValue: dataprovider.KeyValue{
|
||||
Key: "/p1",
|
||||
Value: "/p1",
|
||||
},
|
||||
},
|
||||
}, testReplacer, user)
|
||||
if assert.Error(t, err) {
|
||||
|
|
@ -1700,10 +1755,12 @@ func TestFilesystemActionErrors(t *testing.T) {
|
|||
Options: dataprovider.BaseEventActionOptions{
|
||||
FsConfig: dataprovider.EventActionFilesystemConfig{
|
||||
Type: dataprovider.FilesystemActionRename,
|
||||
Renames: []dataprovider.KeyValue{
|
||||
Renames: []dataprovider.RenameConfig{
|
||||
{
|
||||
Key: "/p2",
|
||||
Value: "/p2",
|
||||
KeyValue: dataprovider.KeyValue{
|
||||
Key: "/p2",
|
||||
Value: "/p2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -1789,7 +1846,7 @@ func TestFilesystemActionErrors(t *testing.T) {
|
|||
Writer: zip.NewWriter(bytes.NewBuffer(nil)),
|
||||
Entries: map[string]bool{},
|
||||
}
|
||||
err = addZipEntry(wr, conn, "/adir/sub/f.dat", "/adir/sub/sub", 0)
|
||||
err = addZipEntry(wr, conn, "/adir/sub/f.dat", "/adir/sub/sub", nil, 0)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, getErrorString(err), "is outside base dir")
|
||||
}
|
||||
|
|
@ -1799,7 +1856,7 @@ func TestFilesystemActionErrors(t *testing.T) {
|
|||
Writer: zip.NewWriter(bytes.NewBuffer(nil)),
|
||||
Entries: map[string]bool{},
|
||||
}
|
||||
err = addZipEntry(wr, conn, "/p1", "/", 2000)
|
||||
err = addZipEntry(wr, conn, "/p1", "/", nil, 2000)
|
||||
assert.ErrorIs(t, err, util.ErrRecursionTooDeep)
|
||||
|
||||
err = dataprovider.DeleteUser(username, "", "", "")
|
||||
|
|
@ -1897,12 +1954,27 @@ func TestScheduledActions(t *testing.T) {
|
|||
backupsPath := filepath.Join(os.TempDir(), "backups")
|
||||
err := os.RemoveAll(backupsPath)
|
||||
assert.NoError(t, err)
|
||||
now := time.Now().UTC().Format(dateTimeMillisFormat)
|
||||
// The backup action sets the home directory to the backup path.
|
||||
expectedDirPath := filepath.Join(backupsPath, fmt.Sprintf("%s_%s_%s", now[0:4], now[5:7], now[8:10]))
|
||||
|
||||
action := &dataprovider.BaseEventAction{
|
||||
Name: "action",
|
||||
action1 := &dataprovider.BaseEventAction{
|
||||
Name: "action1",
|
||||
Type: dataprovider.ActionTypeBackup,
|
||||
}
|
||||
err = dataprovider.AddEventAction(action, "", "", "")
|
||||
err = dataprovider.AddEventAction(action1, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
action2 := &dataprovider.BaseEventAction{
|
||||
Name: "action2",
|
||||
Type: dataprovider.ActionTypeFilesystem,
|
||||
Options: dataprovider.BaseEventActionOptions{
|
||||
FsConfig: dataprovider.EventActionFilesystemConfig{
|
||||
Type: dataprovider.FilesystemActionMkdirs,
|
||||
MkDirs: []string{"{{.Year}}_{{.Month}}_{{.Day}}"},
|
||||
},
|
||||
},
|
||||
}
|
||||
err = dataprovider.AddEventAction(action2, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
rule := &dataprovider.EventRule{
|
||||
Name: "rule",
|
||||
|
|
@ -1921,10 +1993,16 @@ func TestScheduledActions(t *testing.T) {
|
|||
Actions: []dataprovider.EventAction{
|
||||
{
|
||||
BaseEventAction: dataprovider.BaseEventAction{
|
||||
Name: action.Name,
|
||||
Name: action1.Name,
|
||||
},
|
||||
Order: 1,
|
||||
},
|
||||
{
|
||||
BaseEventAction: dataprovider.BaseEventAction{
|
||||
Name: action2.Name,
|
||||
},
|
||||
Order: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -1939,9 +2017,10 @@ func TestScheduledActions(t *testing.T) {
|
|||
|
||||
job.Run()
|
||||
assert.DirExists(t, backupsPath)
|
||||
assert.DirExists(t, expectedDirPath)
|
||||
|
||||
action.Type = dataprovider.ActionTypeEmail
|
||||
action.Options = dataprovider.BaseEventActionOptions{
|
||||
action1.Type = dataprovider.ActionTypeEmail
|
||||
action1.Options = dataprovider.BaseEventActionOptions{
|
||||
EmailConfig: dataprovider.EventActionEmailConfig{
|
||||
Recipients: []string{"example@example.com"},
|
||||
Subject: "test with attachments",
|
||||
|
|
@ -1949,16 +2028,19 @@ func TestScheduledActions(t *testing.T) {
|
|||
Attachments: []string{"/file1.txt"},
|
||||
},
|
||||
}
|
||||
err = dataprovider.UpdateEventAction(action, "", "", "")
|
||||
err = dataprovider.UpdateEventAction(action1, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
job.Run() // action is not compatible with a scheduled rule
|
||||
|
||||
err = dataprovider.DeleteEventRule(rule.Name, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
err = dataprovider.DeleteEventAction(action.Name, "", "", "")
|
||||
err = dataprovider.DeleteEventAction(action1.Name, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
err = dataprovider.DeleteEventAction(action2.Name, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(backupsPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
stopEventScheduler()
|
||||
}
|
||||
|
||||
|
|
@ -2077,11 +2159,11 @@ func TestWriteHTTPPartsError(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestReplacePathsPlaceholders(t *testing.T) {
|
||||
replacer := strings.NewReplacer("{{VirtualPath}}", "/path1")
|
||||
paths := []string{"{{VirtualPath}}", "/path1"}
|
||||
replacer := strings.NewReplacer("{{.VirtualPath}}", "/path1")
|
||||
paths := []string{"{{.VirtualPath}}", "/path1"}
|
||||
paths = replacePathsPlaceholders(paths, replacer)
|
||||
assert.Equal(t, []string{"/path1"}, paths)
|
||||
paths = []string{"{{VirtualPath}}", "/path2"}
|
||||
paths = []string{"{{.VirtualPath}}", "/path2"}
|
||||
paths = replacePathsPlaceholders(paths, replacer)
|
||||
assert.Equal(t, []string{"/path1", "/path2"}, paths)
|
||||
}
|
||||
|
|
@ -2174,7 +2256,7 @@ func TestOnDemandRule(t *testing.T) {
|
|||
Recipients: []string{"example@example.org"},
|
||||
Subject: "subject",
|
||||
Body: "body",
|
||||
Attachments: []string{"/{{VirtualPath}}"},
|
||||
Attachments: []string{"/{{.VirtualPath}}"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -2215,21 +2297,21 @@ func getErrorString(err error) string {
|
|||
|
||||
func TestHTTPEndpointWithPlaceholders(t *testing.T) {
|
||||
c := dataprovider.EventActionHTTPConfig{
|
||||
Endpoint: "http://127.0.0.1:8080/base/url/{{Name}}/{{VirtualPath}}/upload",
|
||||
Endpoint: "http://127.0.0.1:8080/base/url/{{.Name}}/{{.VirtualPath}}/upload",
|
||||
QueryParameters: []dataprovider.KeyValue{
|
||||
{
|
||||
Key: "u",
|
||||
Value: "{{Name}}",
|
||||
Value: "{{.Name}}",
|
||||
},
|
||||
{
|
||||
Key: "p",
|
||||
Value: "{{VirtualPath}}",
|
||||
Value: "{{.VirtualPath}}",
|
||||
},
|
||||
},
|
||||
}
|
||||
name := "uname"
|
||||
vPath := "/a dir/@ file.txt"
|
||||
replacer := strings.NewReplacer("{{Name}}", name, "{{VirtualPath}}", vPath)
|
||||
replacer := strings.NewReplacer("{{.Name}}", name, "{{.VirtualPath}}", vPath)
|
||||
u, err := getHTTPRuleActionEndpoint(&c, replacer)
|
||||
assert.NoError(t, err)
|
||||
expected := "http://127.0.0.1:8080/base/url/" + url.PathEscape(name) + "/" + url.PathEscape(vPath) +
|
||||
|
|
@ -2249,9 +2331,9 @@ func TestMetadataReplacement(t *testing.T) {
|
|||
"key": "value",
|
||||
},
|
||||
}
|
||||
replacements := params.getStringReplacements(false, false)
|
||||
replacements := params.getStringReplacements(false, 0)
|
||||
replacer := strings.NewReplacer(replacements...)
|
||||
reader, _, err := getHTTPRuleActionBody(&dataprovider.EventActionHTTPConfig{Body: "{{Metadata}} {{MetadataString}}"}, replacer, nil, dataprovider.User{}, params, false)
|
||||
reader, _, err := getHTTPRuleActionBody(&dataprovider.EventActionHTTPConfig{Body: "{{.Metadata}} {{.MetadataString}}"}, replacer, nil, dataprovider.User{}, params, false)
|
||||
require.NoError(t, err)
|
||||
data, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import (
|
|||
|
||||
"github.com/robfig/cron/v3"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
)
|
||||
|
||||
|
|
@ -36,7 +38,15 @@ func stopEventScheduler() {
|
|||
func startEventScheduler() {
|
||||
stopEventScheduler()
|
||||
|
||||
eventScheduler = cron.New(cron.WithLocation(time.UTC), cron.WithLogger(cron.DiscardLogger))
|
||||
options := []cron.Option{
|
||||
cron.WithLogger(cron.DiscardLogger),
|
||||
}
|
||||
if !dataprovider.UseLocalTime() {
|
||||
eventManagerLog(logger.LevelDebug, "use UTC time for the scheduler")
|
||||
options = append(options, cron.WithLocation(time.UTC))
|
||||
}
|
||||
|
||||
eventScheduler = cron.New(options...)
|
||||
eventManager.loadRules()
|
||||
_, err := eventScheduler.AddFunc("@every 10m", eventManager.loadRules)
|
||||
util.PanicOnError(err)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -17,6 +17,7 @@ package common
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
|
@ -94,7 +95,7 @@ func (r *RateLimiterConfig) validate() error {
|
|||
}
|
||||
r.Protocols = util.RemoveDuplicates(r.Protocols, true)
|
||||
for _, protocol := range r.Protocols {
|
||||
if !util.Contains(rateLimiterProtocolValues, protocol) {
|
||||
if !slices.Contains(rateLimiterProtocolValues, protocol) {
|
||||
return fmt.Errorf("invalid protocol %q", protocol)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import (
|
|||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
|
|
@ -96,7 +97,7 @@ func (m *CertManager) loadCertificates() error {
|
|||
}
|
||||
logger.Debug(m.logSender, "", "TLS certificate %q successfully loaded, id %v", keyPair.Cert, keyPair.ID)
|
||||
certs[keyPair.ID] = &newCert
|
||||
if !util.Contains(m.monitorList, keyPair.Cert) {
|
||||
if !slices.Contains(m.monitorList, keyPair.Cert) {
|
||||
m.monitorList = append(m.monitorList, keyPair.Cert)
|
||||
}
|
||||
}
|
||||
|
|
@ -190,7 +191,7 @@ func (m *CertManager) LoadCRLs() error {
|
|||
|
||||
logger.Debug(m.logSender, "", "CRL %q successfully loaded", revocationList)
|
||||
crls = append(crls, crl)
|
||||
if !util.Contains(m.monitorList, revocationList) {
|
||||
if !slices.Contains(m.monitorList, revocationList) {
|
||||
m.monitorList = append(m.monitorList, revocationList)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ var (
|
|||
)
|
||||
|
||||
// BaseTransfer contains protocols common transfer details for an upload or a download.
|
||||
type BaseTransfer struct { //nolint:maligned
|
||||
type BaseTransfer struct {
|
||||
ID int64
|
||||
BytesSent atomic.Int64
|
||||
BytesReceived atomic.Int64
|
||||
|
|
@ -329,6 +329,21 @@ func (t *BaseTransfer) getUploadFileSize() (int64, int, error) {
|
|||
var fileSize int64
|
||||
var deletedFiles int
|
||||
|
||||
switch dataprovider.GetQuotaTracking() {
|
||||
case 0:
|
||||
return fileSize, deletedFiles, errors.New("quota tracking disabled")
|
||||
case 2:
|
||||
if !t.Connection.User.HasQuotaRestrictions() {
|
||||
vfolder, err := t.Connection.User.GetVirtualFolderForPath(path.Dir(t.requestPath))
|
||||
if err != nil {
|
||||
return fileSize, deletedFiles, errors.New("quota tracking disabled for this user")
|
||||
}
|
||||
if vfolder.IsIncludedInUserQuota() {
|
||||
return fileSize, deletedFiles, errors.New("quota tracking disabled for this user and folder included in user quota")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info, err := t.Fs.Stat(t.fsPath)
|
||||
if err == nil {
|
||||
fileSize = info.Size()
|
||||
|
|
@ -352,6 +367,9 @@ func (t *BaseTransfer) checkUploadOutsideHomeDir(err error) int {
|
|||
if err == nil {
|
||||
return 0
|
||||
}
|
||||
if t.ErrTransfer == nil {
|
||||
t.ErrTransfer = err
|
||||
}
|
||||
if Config.TempPath == "" {
|
||||
return 0
|
||||
}
|
||||
|
|
@ -391,7 +409,7 @@ func (t *BaseTransfer) Close() error {
|
|||
t.effectiveFsPath, err)
|
||||
} else if t.isAtomicUpload() {
|
||||
if t.ErrTransfer == nil || Config.UploadMode&UploadModeAtomicWithResume != 0 {
|
||||
_, _, err = t.Fs.Rename(t.effectiveFsPath, t.fsPath)
|
||||
_, _, err = t.Fs.Rename(t.effectiveFsPath, t.fsPath, 0)
|
||||
t.Connection.Log(logger.LevelDebug, "atomic upload completed, rename: %q -> %q, error: %v",
|
||||
t.effectiveFsPath, t.fsPath, err)
|
||||
// the file must be removed if it is uploaded to a path outside the home dir and cannot be renamed
|
||||
|
|
@ -410,7 +428,8 @@ func (t *BaseTransfer) Close() error {
|
|||
var uploadFileSize int64
|
||||
if t.transferType == TransferDownload {
|
||||
logger.TransferLog(downloadLogSender, t.fsPath, elapsed, t.BytesSent.Load(), t.Connection.User.Username,
|
||||
t.Connection.ID, t.Connection.protocol, t.Connection.localAddr, t.Connection.remoteAddr, t.ftpMode)
|
||||
t.Connection.ID, t.Connection.protocol, t.Connection.localAddr, t.Connection.remoteAddr, t.ftpMode,
|
||||
t.ErrTransfer)
|
||||
ExecuteActionNotification(t.Connection, operationDownload, t.fsPath, t.requestPath, "", "", "", //nolint:errcheck
|
||||
t.BytesSent.Load(), t.ErrTransfer, elapsed, t.metadata)
|
||||
} else {
|
||||
|
|
@ -431,7 +450,8 @@ func (t *BaseTransfer) Close() error {
|
|||
t.updateQuota(numFiles, uploadFileSize)
|
||||
t.updateTimes()
|
||||
logger.TransferLog(uploadLogSender, t.fsPath, elapsed, t.BytesReceived.Load(), t.Connection.User.Username,
|
||||
t.Connection.ID, t.Connection.protocol, t.Connection.localAddr, t.Connection.remoteAddr, t.ftpMode)
|
||||
t.Connection.ID, t.Connection.protocol, t.Connection.localAddr, t.Connection.remoteAddr, t.ftpMode,
|
||||
t.ErrTransfer)
|
||||
}
|
||||
if t.ErrTransfer != nil {
|
||||
t.Connection.Log(logger.LevelError, "transfer error: %v, path: %q", t.ErrTransfer, t.fsPath)
|
||||
|
|
@ -516,11 +536,8 @@ func (t *BaseTransfer) updateQuota(numFiles int, fileSize int64) bool {
|
|||
if t.transferType == TransferUpload && (numFiles != 0 || sizeDiff != 0) {
|
||||
vfolder, err := t.Connection.User.GetVirtualFolderForPath(path.Dir(t.requestPath))
|
||||
if err == nil {
|
||||
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, numFiles, //nolint:errcheck
|
||||
dataprovider.UpdateUserFolderQuota(&vfolder, &t.Connection.User, numFiles,
|
||||
sizeDiff, false)
|
||||
if vfolder.IsIncludedInUserQuota() {
|
||||
dataprovider.UpdateUserQuota(&t.Connection.User, numFiles, sizeDiff, false) //nolint:errcheck
|
||||
}
|
||||
} else {
|
||||
dataprovider.UpdateUserQuota(&t.Connection.User, numFiles, sizeDiff, false) //nolint:errcheck
|
||||
}
|
||||
|
|
|
|||
|
|
@ -306,8 +306,9 @@ func TestRemovePartialCryptoFile(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
u := dataprovider.User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
Username: "test",
|
||||
HomeDir: os.TempDir(),
|
||||
Username: "test",
|
||||
HomeDir: os.TempDir(),
|
||||
QuotaFiles: 1000000,
|
||||
},
|
||||
}
|
||||
conn := NewBaseConnection(fs.ConnectionID(), ProtocolSFTP, "", "", u)
|
||||
|
|
@ -323,6 +324,9 @@ func TestRemovePartialCryptoFile(t *testing.T) {
|
|||
assert.Equal(t, int64(0), size)
|
||||
assert.Equal(t, 1, deletedFiles)
|
||||
assert.NoFileExists(t, testFile)
|
||||
err = transfer.Close()
|
||||
assert.Error(t, err)
|
||||
assert.Len(t, conn.GetTransfers(), 0)
|
||||
}
|
||||
|
||||
func TestFTPMode(t *testing.T) {
|
||||
|
|
@ -434,6 +438,11 @@ func TestTransferQuota(t *testing.T) {
|
|||
}
|
||||
err = transfer.CheckWrite()
|
||||
assert.True(t, conn.IsQuotaExceededError(err))
|
||||
|
||||
err = transfer.Close()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, conn.GetTransfers(), 0)
|
||||
assert.Equal(t, int32(0), Connections.GetTotalTransfers())
|
||||
}
|
||||
|
||||
func TestUploadOutsideHomeRenameError(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -250,6 +250,7 @@ func TestTransfersCheckerDiskQuota(t *testing.T) {
|
|||
Connections.Remove(fakeConn5.GetID())
|
||||
stats := Connections.GetStats("")
|
||||
assert.Len(t, stats, 0)
|
||||
assert.Equal(t, int32(0), Connections.GetTotalTransfers())
|
||||
|
||||
err = dataprovider.DeleteUser(user.Username, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -368,11 +369,16 @@ func TestTransferCheckerTransferQuota(t *testing.T) {
|
|||
if assert.Error(t, transfer4.errAbort) {
|
||||
assert.Contains(t, transfer4.errAbort.Error(), ErrReadQuotaExceeded.Error())
|
||||
}
|
||||
err = transfer3.Close()
|
||||
assert.NoError(t, err)
|
||||
err = transfer4.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
Connections.Remove(fakeConn3.GetID())
|
||||
Connections.Remove(fakeConn4.GetID())
|
||||
stats := Connections.GetStats("")
|
||||
assert.Len(t, stats, 0)
|
||||
assert.Equal(t, int32(0), Connections.GetTotalTransfers())
|
||||
|
||||
err = dataprovider.DeleteUser(user.Username, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -20,9 +20,11 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
kmsplugin "github.com/sftpgo/sdk/plugin/kms"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/subosito/gotenv"
|
||||
|
||||
|
|
@ -91,30 +93,35 @@ var (
|
|||
TLSCipherSuites: nil,
|
||||
Protocols: nil,
|
||||
Prefix: "",
|
||||
ProxyMode: 0,
|
||||
ProxyAllowed: nil,
|
||||
ClientIPProxyHeader: "",
|
||||
ClientIPHeaderDepth: 0,
|
||||
DisableWWWAuthHeader: false,
|
||||
}
|
||||
defaultHTTPDBinding = httpd.Binding{
|
||||
Address: "",
|
||||
Port: 8080,
|
||||
EnableWebAdmin: true,
|
||||
EnableWebClient: true,
|
||||
EnableRESTAPI: true,
|
||||
EnabledLoginMethods: 0,
|
||||
EnableHTTPS: false,
|
||||
CertificateFile: "",
|
||||
CertificateKeyFile: "",
|
||||
MinTLSVersion: 12,
|
||||
ClientAuthType: 0,
|
||||
TLSCipherSuites: nil,
|
||||
Protocols: nil,
|
||||
ProxyAllowed: nil,
|
||||
ClientIPProxyHeader: "",
|
||||
ClientIPHeaderDepth: 0,
|
||||
HideLoginURL: 0,
|
||||
RenderOpenAPI: true,
|
||||
Address: "",
|
||||
Port: 8080,
|
||||
EnableWebAdmin: true,
|
||||
EnableWebClient: true,
|
||||
EnableRESTAPI: true,
|
||||
EnabledLoginMethods: 0,
|
||||
DisabledLoginMethods: 0,
|
||||
EnableHTTPS: false,
|
||||
CertificateFile: "",
|
||||
CertificateKeyFile: "",
|
||||
MinTLSVersion: 12,
|
||||
ClientAuthType: 0,
|
||||
TLSCipherSuites: nil,
|
||||
Protocols: nil,
|
||||
ProxyMode: 0,
|
||||
ProxyAllowed: nil,
|
||||
ClientIPProxyHeader: "",
|
||||
ClientIPHeaderDepth: 0,
|
||||
HideLoginURL: 0,
|
||||
RenderOpenAPI: true,
|
||||
BaseURL: "",
|
||||
Languages: []string{"en"},
|
||||
OIDC: httpd.OIDC{
|
||||
ClientID: "",
|
||||
ClientSecret: "",
|
||||
|
|
@ -130,20 +137,23 @@ var (
|
|||
Debug: false,
|
||||
},
|
||||
Security: httpd.SecurityConf{
|
||||
Enabled: false,
|
||||
AllowedHosts: nil,
|
||||
AllowedHostsAreRegex: false,
|
||||
HostsProxyHeaders: nil,
|
||||
HTTPSRedirect: false,
|
||||
HTTPSHost: "",
|
||||
HTTPSProxyHeaders: nil,
|
||||
STSSeconds: 0,
|
||||
STSIncludeSubdomains: false,
|
||||
STSPreload: false,
|
||||
ContentTypeNosniff: false,
|
||||
ContentSecurityPolicy: "",
|
||||
PermissionsPolicy: "",
|
||||
CrossOriginOpenerPolicy: "",
|
||||
Enabled: false,
|
||||
AllowedHosts: nil,
|
||||
AllowedHostsAreRegex: false,
|
||||
HostsProxyHeaders: nil,
|
||||
HTTPSRedirect: false,
|
||||
HTTPSHost: "",
|
||||
HTTPSProxyHeaders: nil,
|
||||
STSSeconds: 0,
|
||||
STSIncludeSubdomains: false,
|
||||
STSPreload: false,
|
||||
ContentTypeNosniff: false,
|
||||
ContentSecurityPolicy: "",
|
||||
PermissionsPolicy: "",
|
||||
CrossOriginOpenerPolicy: "",
|
||||
CrossOriginResourcePolicy: "",
|
||||
CrossOriginEmbedderPolicy: "",
|
||||
CacheControl: "",
|
||||
},
|
||||
Branding: httpd.Branding{},
|
||||
}
|
||||
|
|
@ -208,7 +218,6 @@ func Init() {
|
|||
ProxySkipped: []string{},
|
||||
PostConnectHook: "",
|
||||
PostDisconnectHook: "",
|
||||
DataRetentionHook: "",
|
||||
MaxTotalConnections: 0,
|
||||
MaxPerHostConnections: 20,
|
||||
AllowListStatus: 0,
|
||||
|
|
@ -226,13 +235,21 @@ func Init() {
|
|||
ObservationTime: 30,
|
||||
EntriesSoftLimit: 100,
|
||||
EntriesHardLimit: 150,
|
||||
LoginDelay: common.LoginDelay{
|
||||
Success: 0,
|
||||
PasswordFailed: 1000,
|
||||
},
|
||||
},
|
||||
RateLimitersConfig: []common.RateLimiterConfig{defaultRateLimiter},
|
||||
Umask: "",
|
||||
ServerVersion: "",
|
||||
TZ: "",
|
||||
Metadata: common.MetadataConfig{
|
||||
Read: 0,
|
||||
},
|
||||
EventManager: common.EventManagerConfig{
|
||||
EnabledCommands: []string{},
|
||||
},
|
||||
},
|
||||
ACME: acme.Configuration{
|
||||
Email: "",
|
||||
|
|
@ -262,6 +279,8 @@ func Init() {
|
|||
PublicKeyAlgorithms: []string{},
|
||||
TrustedUserCAKeys: []string{},
|
||||
RevokedUserCertsFile: "",
|
||||
OPKSSHPath: "",
|
||||
OPKSSHChecksum: "",
|
||||
LoginBannerFile: "",
|
||||
EnabledSSHCommands: []string{},
|
||||
KeyboardInteractiveAuthentication: true,
|
||||
|
|
@ -390,6 +409,9 @@ func Init() {
|
|||
SigningPassphrase: "",
|
||||
SigningPassphraseFile: "",
|
||||
TokenValidation: 0,
|
||||
CookieLifetime: 20,
|
||||
ShareCookieLifetime: 120,
|
||||
JWTLifetime: 20,
|
||||
MaxUploadFileSize: 0,
|
||||
Cors: httpd.CorsConfig{
|
||||
Enabled: false,
|
||||
|
|
@ -568,6 +590,16 @@ func SetPluginsConfig(config []plugin.Config) {
|
|||
globalConf.PluginsConfig = config
|
||||
}
|
||||
|
||||
// HasKMSPlugin returns true if at least one KMS plugin is configured.
|
||||
func HasKMSPlugin() bool {
|
||||
for _, c := range globalConf.PluginsConfig {
|
||||
if c.Type == kmsplugin.PluginName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetMFAConfig returns multi-factor authentication config
|
||||
func GetMFAConfig() mfa.Config {
|
||||
return globalConf.MFAConfig
|
||||
|
|
@ -614,7 +646,6 @@ func getRedactedGlobalConf() globalConfig {
|
|||
conf.Common.StartupHook = util.GetRedactedURL(conf.Common.StartupHook)
|
||||
conf.Common.PostConnectHook = util.GetRedactedURL(conf.Common.PostConnectHook)
|
||||
conf.Common.PostDisconnectHook = util.GetRedactedURL(conf.Common.PostDisconnectHook)
|
||||
conf.Common.DataRetentionHook = util.GetRedactedURL(conf.Common.DataRetentionHook)
|
||||
conf.SFTPD.KeyboardInteractiveHook = util.GetRedactedURL(conf.SFTPD.KeyboardInteractiveHook)
|
||||
conf.HTTPDConfig.SigningPassphrase = getRedactedPassword(conf.HTTPDConfig.SigningPassphrase)
|
||||
conf.HTTPDConfig.Setup.InstallationCode = getRedactedPassword(conf.HTTPDConfig.Setup.InstallationCode)
|
||||
|
|
@ -631,6 +662,16 @@ func getRedactedGlobalConf() globalConfig {
|
|||
binding.OIDC.ClientSecret = getRedactedPassword(binding.OIDC.ClientSecret)
|
||||
conf.HTTPDConfig.Bindings = append(conf.HTTPDConfig.Bindings, binding)
|
||||
}
|
||||
conf.KMSConfig.Secrets.MasterKeyString = getRedactedPassword(conf.KMSConfig.Secrets.MasterKeyString)
|
||||
conf.PluginsConfig = nil
|
||||
for _, plugin := range globalConf.PluginsConfig {
|
||||
var args []string
|
||||
for _, arg := range plugin.Args {
|
||||
args = append(args, getRedactedPassword(arg))
|
||||
}
|
||||
plugin.Args = args
|
||||
conf.PluginsConfig = append(conf.PluginsConfig, plugin)
|
||||
}
|
||||
return conf
|
||||
}
|
||||
|
||||
|
|
@ -701,7 +742,7 @@ func checkOverrideDefaultSettings() {
|
|||
}
|
||||
}
|
||||
|
||||
if util.Contains(viper.AllKeys(), "mfa.totp") {
|
||||
if slices.Contains(viper.AllKeys(), "mfa.totp") {
|
||||
globalConf.MFAConfig.TOTP = nil
|
||||
}
|
||||
}
|
||||
|
|
@ -787,6 +828,12 @@ func resetInvalidConfigs() {
|
|||
logger.WarnToConsole("Non-fatal configuration error: %v", warn)
|
||||
}
|
||||
}
|
||||
if globalConf.Common.RenameMode < 0 || globalConf.Common.RenameMode > 1 {
|
||||
warn := fmt.Sprintf("invalid rename mode %d, reset to 0", globalConf.Common.RenameMode)
|
||||
globalConf.Common.RenameMode = 0
|
||||
logger.Warn(logSender, "", "Non-fatal configuration error: %v", warn)
|
||||
logger.WarnToConsole("Non-fatal configuration error: %v", warn)
|
||||
}
|
||||
}
|
||||
|
||||
func loadBindingsFromEnv() {
|
||||
|
|
@ -859,13 +906,13 @@ func getRateLimitersFromEnv(idx int) {
|
|||
isSet = true
|
||||
}
|
||||
|
||||
burst, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_COMMON__RATE_LIMITERS__%v__BURST", idx), 0)
|
||||
burst, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_COMMON__RATE_LIMITERS__%v__BURST", idx), 32)
|
||||
if ok {
|
||||
rtlConfig.Burst = int(burst)
|
||||
isSet = true
|
||||
}
|
||||
|
||||
rtlType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_COMMON__RATE_LIMITERS__%v__TYPE", idx), 0)
|
||||
rtlType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_COMMON__RATE_LIMITERS__%v__TYPE", idx), 32)
|
||||
if ok {
|
||||
rtlConfig.Type = int(rtlType)
|
||||
isSet = true
|
||||
|
|
@ -883,13 +930,13 @@ func getRateLimitersFromEnv(idx int) {
|
|||
isSet = true
|
||||
}
|
||||
|
||||
softLimit, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_COMMON__RATE_LIMITERS__%v__ENTRIES_SOFT_LIMIT", idx), 0)
|
||||
softLimit, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_COMMON__RATE_LIMITERS__%v__ENTRIES_SOFT_LIMIT", idx), 32)
|
||||
if ok {
|
||||
rtlConfig.EntriesSoftLimit = int(softLimit)
|
||||
isSet = true
|
||||
}
|
||||
|
||||
hardLimit, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_COMMON__RATE_LIMITERS__%v__ENTRIES_HARD_LIMIT", idx), 0)
|
||||
hardLimit, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_COMMON__RATE_LIMITERS__%v__ENTRIES_HARD_LIMIT", idx), 32)
|
||||
if ok {
|
||||
rtlConfig.EntriesHardLimit = int(hardLimit)
|
||||
isSet = true
|
||||
|
|
@ -925,7 +972,7 @@ func getKMSPluginFromEnv(idx int, pluginConfig *plugin.Config) bool {
|
|||
func getAuthPluginFromEnv(idx int, pluginConfig *plugin.Config) bool {
|
||||
isSet := false
|
||||
|
||||
authScope, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__AUTH_OPTIONS__SCOPE", idx), 0)
|
||||
authScope, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__AUTH_OPTIONS__SCOPE", idx), 32)
|
||||
if ok {
|
||||
pluginConfig.AuthOptions.Scope = int(authScope)
|
||||
isSet = true
|
||||
|
|
@ -970,13 +1017,13 @@ func getNotifierPluginFromEnv(idx int, pluginConfig *plugin.Config) bool {
|
|||
}
|
||||
}
|
||||
|
||||
notifierRetryMaxTime, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__NOTIFIER_OPTIONS__RETRY_MAX_TIME", idx), 0)
|
||||
notifierRetryMaxTime, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__NOTIFIER_OPTIONS__RETRY_MAX_TIME", idx), 32)
|
||||
if ok {
|
||||
pluginConfig.NotifierOptions.RetryMaxTime = int(notifierRetryMaxTime)
|
||||
isSet = true
|
||||
}
|
||||
|
||||
notifierRetryQueueMaxSize, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__NOTIFIER_OPTIONS__RETRY_QUEUE_MAX_SIZE", idx), 0)
|
||||
notifierRetryQueueMaxSize, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__NOTIFIER_OPTIONS__RETRY_QUEUE_MAX_SIZE", idx), 32)
|
||||
if ok {
|
||||
pluginConfig.NotifierOptions.RetryQueueMaxSize = int(notifierRetryQueueMaxSize)
|
||||
isSet = true
|
||||
|
|
@ -1064,7 +1111,7 @@ func getSFTPDBindindFromEnv(idx int) {
|
|||
|
||||
isSet := false
|
||||
|
||||
port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_SFTPD__BINDINGS__%v__PORT", idx), 0)
|
||||
port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_SFTPD__BINDINGS__%v__PORT", idx), 32)
|
||||
if ok {
|
||||
binding.Port = int(port)
|
||||
isSet = true
|
||||
|
|
@ -1151,19 +1198,13 @@ func getFTPDBindingSecurityFromEnv(idx int, binding *ftpd.Binding) bool {
|
|||
isSet = true
|
||||
}
|
||||
|
||||
tlsMode, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__TLS_MODE", idx), 0)
|
||||
tlsMode, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__TLS_MODE", idx), 32)
|
||||
if ok {
|
||||
binding.TLSMode = int(tlsMode)
|
||||
isSet = true
|
||||
}
|
||||
|
||||
tlsSessionReuse, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__TLS_SESSION_REUSE", idx), 0)
|
||||
if ok {
|
||||
binding.TLSSessionReuse = int(tlsSessionReuse)
|
||||
isSet = true
|
||||
}
|
||||
|
||||
tlsVer, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__MIN_TLS_VERSION", idx), 0)
|
||||
tlsVer, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__MIN_TLS_VERSION", idx), 32)
|
||||
if ok {
|
||||
binding.MinTLSVersion = int(tlsVer)
|
||||
isSet = true
|
||||
|
|
@ -1175,30 +1216,24 @@ func getFTPDBindingSecurityFromEnv(idx int, binding *ftpd.Binding) bool {
|
|||
isSet = true
|
||||
}
|
||||
|
||||
clientAuthType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__CLIENT_AUTH_TYPE", idx), 0)
|
||||
clientAuthType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__CLIENT_AUTH_TYPE", idx), 32)
|
||||
if ok {
|
||||
binding.ClientAuthType = int(clientAuthType)
|
||||
isSet = true
|
||||
}
|
||||
|
||||
pasvSecurity, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__PASSIVE_CONNECTIONS_SECURITY", idx), 0)
|
||||
pasvSecurity, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__PASSIVE_CONNECTIONS_SECURITY", idx), 32)
|
||||
if ok {
|
||||
binding.PassiveConnectionsSecurity = int(pasvSecurity)
|
||||
isSet = true
|
||||
}
|
||||
|
||||
activeSecurity, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__ACTIVE_CONNECTIONS_SECURITY", idx), 0)
|
||||
activeSecurity, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__ACTIVE_CONNECTIONS_SECURITY", idx), 32)
|
||||
if ok {
|
||||
binding.ActiveConnectionsSecurity = int(activeSecurity)
|
||||
isSet = true
|
||||
}
|
||||
|
||||
ignoreASCIITransferType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%d__IGNORE_ASCII_TRANSFER_TYPE", idx), 0)
|
||||
if ok {
|
||||
binding.IgnoreASCIITransferType = int(ignoreASCIITransferType)
|
||||
isSet = true
|
||||
}
|
||||
|
||||
return isSet
|
||||
}
|
||||
|
||||
|
|
@ -1206,7 +1241,7 @@ func getFTPDBindingFromEnv(idx int) {
|
|||
binding := getDefaultFTPDBinding(idx)
|
||||
isSet := false
|
||||
|
||||
port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__PORT", idx), 0)
|
||||
port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__PORT", idx), 32)
|
||||
if ok {
|
||||
binding.Port = int(port)
|
||||
isSet = true
|
||||
|
|
@ -1286,13 +1321,13 @@ func getWebDAVBindingHTTPSConfigsFromEnv(idx int, binding *webdavd.Binding) bool
|
|||
isSet = true
|
||||
}
|
||||
|
||||
tlsVer, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__MIN_TLS_VERSION", idx), 0)
|
||||
tlsVer, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__MIN_TLS_VERSION", idx), 32)
|
||||
if ok {
|
||||
binding.MinTLSVersion = int(tlsVer)
|
||||
isSet = true
|
||||
}
|
||||
|
||||
clientAuthType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__CLIENT_AUTH_TYPE", idx), 0)
|
||||
clientAuthType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__CLIENT_AUTH_TYPE", idx), 32)
|
||||
if ok {
|
||||
binding.ClientAuthType = int(clientAuthType)
|
||||
isSet = true
|
||||
|
|
@ -1316,6 +1351,12 @@ func getWebDAVBindingHTTPSConfigsFromEnv(idx int, binding *webdavd.Binding) bool
|
|||
func getWebDAVDBindingProxyConfigsFromEnv(idx int, binding *webdavd.Binding) bool {
|
||||
isSet := false
|
||||
|
||||
proxyMode, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__PROXY_MODE", idx), 32)
|
||||
if ok {
|
||||
binding.ProxyMode = int(proxyMode)
|
||||
isSet = true
|
||||
}
|
||||
|
||||
proxyAllowed, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__PROXY_ALLOWED", idx))
|
||||
if ok {
|
||||
binding.ProxyAllowed = proxyAllowed
|
||||
|
|
@ -1328,7 +1369,7 @@ func getWebDAVDBindingProxyConfigsFromEnv(idx int, binding *webdavd.Binding) boo
|
|||
isSet = true
|
||||
}
|
||||
|
||||
clientIPHeaderDepth, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__CLIENT_IP_HEADER_DEPTH", idx), 0)
|
||||
clientIPHeaderDepth, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__CLIENT_IP_HEADER_DEPTH", idx), 32)
|
||||
if ok {
|
||||
binding.ClientIPHeaderDepth = int(clientIPHeaderDepth)
|
||||
isSet = true
|
||||
|
|
@ -1366,7 +1407,7 @@ func getWebDAVDBindingFromEnv(idx int) {
|
|||
|
||||
isSet := false
|
||||
|
||||
port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__PORT", idx), 0)
|
||||
port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__PORT", idx), 32)
|
||||
if ok {
|
||||
binding.Port = int(port)
|
||||
isSet = true
|
||||
|
|
@ -1527,9 +1568,33 @@ func getHTTPDSecurityConfFromEnv(idx int) (httpd.SecurityConf, bool) { //nolint:
|
|||
isSet = true
|
||||
}
|
||||
|
||||
crossOriginOpenedPolicy, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__CROSS_ORIGIN_OPENER_POLICY", idx))
|
||||
crossOriginOpenerPolicy, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__CROSS_ORIGIN_OPENER_POLICY", idx))
|
||||
if ok {
|
||||
result.CrossOriginOpenerPolicy = crossOriginOpenedPolicy
|
||||
result.CrossOriginOpenerPolicy = crossOriginOpenerPolicy
|
||||
isSet = true
|
||||
}
|
||||
|
||||
crossOriginResourcePolicy, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__CROSS_ORIGIN_RESOURCE_POLICY", idx))
|
||||
if ok {
|
||||
result.CrossOriginResourcePolicy = crossOriginResourcePolicy
|
||||
isSet = true
|
||||
}
|
||||
|
||||
crossOriginEmbedderPolicy, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__CROSS_ORIGIN_EMBEDDER_POLICY", idx))
|
||||
if ok {
|
||||
result.CrossOriginEmbedderPolicy = crossOriginEmbedderPolicy
|
||||
isSet = true
|
||||
}
|
||||
|
||||
referredPolicy, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__REFERRER_POLICY", idx))
|
||||
if ok {
|
||||
result.ReferrerPolicy = referredPolicy
|
||||
isSet = true
|
||||
}
|
||||
|
||||
cacheControl, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__CACHE_CONTROL", idx))
|
||||
if ok {
|
||||
result.CacheControl = cacheControl
|
||||
isSet = true
|
||||
}
|
||||
|
||||
|
|
@ -1645,12 +1710,6 @@ func getHTTPDUIBrandingFromEnv(prefix string, branding httpd.UIBranding) (httpd.
|
|||
isSet = true
|
||||
}
|
||||
|
||||
loginImagePath, ok := os.LookupEnv(fmt.Sprintf("%s__LOGIN_IMAGE_PATH", prefix))
|
||||
if ok {
|
||||
branding.LoginImagePath = loginImagePath
|
||||
isSet = true
|
||||
}
|
||||
|
||||
disclaimerName, ok := os.LookupEnv(fmt.Sprintf("%s__DISCLAIMER_NAME", prefix))
|
||||
if ok {
|
||||
branding.DisclaimerName = disclaimerName
|
||||
|
|
@ -1737,6 +1796,12 @@ func getHTTPDNestedObjectsFromEnv(idx int, binding *httpd.Binding) bool {
|
|||
func getHTTPDBindingProxyConfigsFromEnv(idx int, binding *httpd.Binding) bool {
|
||||
isSet := false
|
||||
|
||||
proxyMode, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__PROXY_MODE", idx), 32)
|
||||
if ok {
|
||||
binding.ProxyMode = int(proxyMode)
|
||||
isSet = true
|
||||
}
|
||||
|
||||
proxyAllowed, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__PROXY_ALLOWED", idx))
|
||||
if ok {
|
||||
binding.ProxyAllowed = proxyAllowed
|
||||
|
|
@ -1749,7 +1814,7 @@ func getHTTPDBindingProxyConfigsFromEnv(idx int, binding *httpd.Binding) bool {
|
|||
isSet = true
|
||||
}
|
||||
|
||||
clientIPHeaderDepth, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__CLIENT_IP_HEADER_DEPTH", idx), 0)
|
||||
clientIPHeaderDepth, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__CLIENT_IP_HEADER_DEPTH", idx), 32)
|
||||
if ok {
|
||||
binding.ClientIPHeaderDepth = int(clientIPHeaderDepth)
|
||||
isSet = true
|
||||
|
|
@ -1762,7 +1827,7 @@ func getHTTPDBindingFromEnv(idx int) { //nolint:gocyclo
|
|||
binding := getDefaultHTTPBinding(idx)
|
||||
isSet := false
|
||||
|
||||
port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__PORT", idx), 0)
|
||||
port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__PORT", idx), 32)
|
||||
if ok {
|
||||
binding.Port = int(port)
|
||||
isSet = true
|
||||
|
|
@ -1804,31 +1869,49 @@ func getHTTPDBindingFromEnv(idx int) { //nolint:gocyclo
|
|||
isSet = true
|
||||
}
|
||||
|
||||
enabledLoginMethods, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__ENABLED_LOGIN_METHODS", idx), 0)
|
||||
enabledLoginMethods, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__ENABLED_LOGIN_METHODS", idx), 32)
|
||||
if ok {
|
||||
binding.EnabledLoginMethods = int(enabledLoginMethods)
|
||||
isSet = true
|
||||
}
|
||||
|
||||
disabledLoginMethods, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__DISABLED_LOGIN_METHODS", idx), 32)
|
||||
if ok {
|
||||
binding.DisabledLoginMethods = int(disabledLoginMethods)
|
||||
isSet = true
|
||||
}
|
||||
|
||||
renderOpenAPI, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__RENDER_OPENAPI", idx))
|
||||
if ok {
|
||||
binding.RenderOpenAPI = renderOpenAPI
|
||||
isSet = true
|
||||
}
|
||||
|
||||
baseURL, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%d__BASE_URL", idx))
|
||||
if ok {
|
||||
binding.BaseURL = baseURL
|
||||
isSet = true
|
||||
}
|
||||
|
||||
languages, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%d__LANGUAGES", idx))
|
||||
if ok {
|
||||
binding.Languages = languages
|
||||
isSet = true
|
||||
}
|
||||
|
||||
enableHTTPS, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__ENABLE_HTTPS", idx))
|
||||
if ok {
|
||||
binding.EnableHTTPS = enableHTTPS
|
||||
isSet = true
|
||||
}
|
||||
|
||||
tlsVer, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__MIN_TLS_VERSION", idx), 0)
|
||||
tlsVer, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__MIN_TLS_VERSION", idx), 32)
|
||||
if ok {
|
||||
binding.MinTLSVersion = int(tlsVer)
|
||||
isSet = true
|
||||
}
|
||||
|
||||
clientAuthType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__CLIENT_AUTH_TYPE", idx), 0)
|
||||
clientAuthType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__CLIENT_AUTH_TYPE", idx), 32)
|
||||
if ok {
|
||||
binding.ClientAuthType = int(clientAuthType)
|
||||
isSet = true
|
||||
|
|
@ -1850,7 +1933,7 @@ func getHTTPDBindingFromEnv(idx int) { //nolint:gocyclo
|
|||
isSet = true
|
||||
}
|
||||
|
||||
hideLoginURL, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__HIDE_LOGIN_URL", idx), 0)
|
||||
hideLoginURL, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__HIDE_LOGIN_URL", idx), 32)
|
||||
if ok {
|
||||
binding.HideLoginURL = int(hideLoginURL)
|
||||
isSet = true
|
||||
|
|
@ -1939,7 +2022,7 @@ func getCommandConfigsFromEnv(idx int) {
|
|||
cfg.Path = path
|
||||
}
|
||||
|
||||
timeout, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_COMMAND__COMMANDS__%v__TIMEOUT", idx), 0)
|
||||
timeout, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_COMMAND__COMMANDS__%v__TIMEOUT", idx), 32)
|
||||
if ok {
|
||||
cfg.Timeout = int(timeout)
|
||||
}
|
||||
|
|
@ -1978,7 +2061,6 @@ func setViperDefaults() {
|
|||
viper.SetDefault("common.proxy_skipped", globalConf.Common.ProxySkipped)
|
||||
viper.SetDefault("common.post_connect_hook", globalConf.Common.PostConnectHook)
|
||||
viper.SetDefault("common.post_disconnect_hook", globalConf.Common.PostDisconnectHook)
|
||||
viper.SetDefault("common.data_retention_hook", globalConf.Common.DataRetentionHook)
|
||||
viper.SetDefault("common.max_total_connections", globalConf.Common.MaxTotalConnections)
|
||||
viper.SetDefault("common.max_per_host_connections", globalConf.Common.MaxPerHostConnections)
|
||||
viper.SetDefault("common.allowlist_status", globalConf.Common.AllowListStatus)
|
||||
|
|
@ -1995,9 +2077,13 @@ func setViperDefaults() {
|
|||
viper.SetDefault("common.defender.observation_time", globalConf.Common.DefenderConfig.ObservationTime)
|
||||
viper.SetDefault("common.defender.entries_soft_limit", globalConf.Common.DefenderConfig.EntriesSoftLimit)
|
||||
viper.SetDefault("common.defender.entries_hard_limit", globalConf.Common.DefenderConfig.EntriesHardLimit)
|
||||
viper.SetDefault("common.defender.login_delay.success", globalConf.Common.DefenderConfig.LoginDelay.Success)
|
||||
viper.SetDefault("common.defender.login_delay.password_failed", globalConf.Common.DefenderConfig.LoginDelay.PasswordFailed)
|
||||
viper.SetDefault("common.umask", globalConf.Common.Umask)
|
||||
viper.SetDefault("common.server_version", globalConf.Common.ServerVersion)
|
||||
viper.SetDefault("common.tz", globalConf.Common.TZ)
|
||||
viper.SetDefault("common.metadata.read", globalConf.Common.Metadata.Read)
|
||||
viper.SetDefault("common.event_manager.enabled_commands", globalConf.Common.EventManager.EnabledCommands)
|
||||
viper.SetDefault("acme.email", globalConf.ACME.Email)
|
||||
viper.SetDefault("acme.key_type", globalConf.ACME.KeyType)
|
||||
viper.SetDefault("acme.certs_path", globalConf.ACME.CertsPath)
|
||||
|
|
@ -2018,6 +2104,8 @@ func setViperDefaults() {
|
|||
viper.SetDefault("sftpd.public_key_algorithms", globalConf.SFTPD.PublicKeyAlgorithms)
|
||||
viper.SetDefault("sftpd.trusted_user_ca_keys", globalConf.SFTPD.TrustedUserCAKeys)
|
||||
viper.SetDefault("sftpd.revoked_user_certs_file", globalConf.SFTPD.RevokedUserCertsFile)
|
||||
viper.SetDefault("sftpd.opkssh_path", globalConf.SFTPD.OPKSSHPath)
|
||||
viper.SetDefault("sftpd.opkssh_checksum", globalConf.SFTPD.OPKSSHChecksum)
|
||||
viper.SetDefault("sftpd.login_banner_file", globalConf.SFTPD.LoginBannerFile)
|
||||
viper.SetDefault("sftpd.enabled_ssh_commands", sftpd.GetDefaultSSHCommands())
|
||||
viper.SetDefault("sftpd.keyboard_interactive_authentication", globalConf.SFTPD.KeyboardInteractiveAuthentication)
|
||||
|
|
@ -2109,6 +2197,9 @@ func setViperDefaults() {
|
|||
viper.SetDefault("httpd.signing_passphrase", globalConf.HTTPDConfig.SigningPassphrase)
|
||||
viper.SetDefault("httpd.signing_passphrase_file", globalConf.HTTPDConfig.SigningPassphraseFile)
|
||||
viper.SetDefault("httpd.token_validation", globalConf.HTTPDConfig.TokenValidation)
|
||||
viper.SetDefault("httpd.cookie_lifetime", globalConf.HTTPDConfig.CookieLifetime)
|
||||
viper.SetDefault("httpd.share_cookie_lifetime", globalConf.HTTPDConfig.ShareCookieLifetime)
|
||||
viper.SetDefault("httpd.jwt_lifetime", globalConf.HTTPDConfig.JWTLifetime)
|
||||
viper.SetDefault("httpd.max_upload_file_size", globalConf.HTTPDConfig.MaxUploadFileSize)
|
||||
viper.SetDefault("httpd.cors.enabled", globalConf.HTTPDConfig.Cors.Enabled)
|
||||
viper.SetDefault("httpd.cors.allowed_origins", globalConf.HTTPDConfig.Cors.AllowedOrigins)
|
||||
|
|
@ -2182,7 +2273,7 @@ func lookupStringListFromEnv(envName string) ([]string, bool) {
|
|||
value, ok := os.LookupEnv(envName)
|
||||
if ok {
|
||||
var result []string
|
||||
for _, v := range strings.Split(value, ",") {
|
||||
for v := range strings.SplitSeq(value, ",") {
|
||||
val := strings.TrimSpace(v)
|
||||
if val != "" {
|
||||
result = append(result, val)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package config
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build !linux && !darwin
|
||||
// +build !linux,!darwin
|
||||
|
||||
package config
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package config
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import (
|
|||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/sftpgo/sdk/kms"
|
||||
|
|
@ -36,7 +37,6 @@ import (
|
|||
"github.com/drakkan/sftpgo/v2/internal/plugin"
|
||||
"github.com/drakkan/sftpgo/v2/internal/sftpd"
|
||||
"github.com/drakkan/sftpgo/v2/internal/smtp"
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
"github.com/drakkan/sftpgo/v2/internal/webdavd"
|
||||
)
|
||||
|
||||
|
|
@ -243,6 +243,26 @@ func TestInvalidInstallationHint(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestInvalidRenameMode(t *testing.T) {
|
||||
reset()
|
||||
|
||||
confName := tempConfigName + ".json"
|
||||
configFilePath := filepath.Join(configDir, confName)
|
||||
commonConfig := config.GetCommonConfig()
|
||||
commonConfig.RenameMode = 10
|
||||
c := make(map[string]any)
|
||||
c["common"] = commonConfig
|
||||
jsonConf, err := json.Marshal(c)
|
||||
assert.NoError(t, err)
|
||||
err = os.WriteFile(configFilePath, jsonConf, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
err = config.LoadConfig(configDir, confName)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, config.GetCommonConfig().RenameMode)
|
||||
err = os.Remove(configFilePath)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDefenderProviderDriver(t *testing.T) {
|
||||
if config.GetProviderConf().Driver != dataprovider.SQLiteDataProviderName {
|
||||
t.Skip("this test is not supported with the current database provider")
|
||||
|
|
@ -323,6 +343,18 @@ func TestSetGetConfig(t *testing.T) {
|
|||
if assert.Len(t, config.GetPluginsConfig(), 1) {
|
||||
assert.Equal(t, pluginConf[0].Type, config.GetPluginsConfig()[0].Type)
|
||||
}
|
||||
assert.False(t, config.HasKMSPlugin())
|
||||
pluginConf = []plugin.Config{
|
||||
{
|
||||
Type: "notifier",
|
||||
},
|
||||
{
|
||||
Type: "kms",
|
||||
},
|
||||
}
|
||||
config.SetPluginsConfig(pluginConf)
|
||||
assert.Len(t, config.GetPluginsConfig(), 2)
|
||||
assert.True(t, config.HasKMSPlugin())
|
||||
}
|
||||
|
||||
func TestServiceToStart(t *testing.T) {
|
||||
|
|
@ -508,7 +540,7 @@ func TestOverrideSliceValues(t *testing.T) {
|
|||
|
||||
c = make(map[string]any)
|
||||
c["httpd"] = httpd.Conf{
|
||||
Bindings: []httpd.Binding{},
|
||||
Bindings: nil,
|
||||
}
|
||||
jsonConf, err = json.Marshal(c)
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -679,8 +711,8 @@ func TestPluginsFromEnv(t *testing.T) {
|
|||
pluginConf := pluginsConf[0]
|
||||
require.Equal(t, "notifier", pluginConf.Type)
|
||||
require.Len(t, pluginConf.NotifierOptions.FsEvents, 2)
|
||||
require.True(t, util.Contains(pluginConf.NotifierOptions.FsEvents, "upload"))
|
||||
require.True(t, util.Contains(pluginConf.NotifierOptions.FsEvents, "download"))
|
||||
require.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, "upload"))
|
||||
require.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, "download"))
|
||||
require.Len(t, pluginConf.NotifierOptions.ProviderEvents, 2)
|
||||
require.Equal(t, "add", pluginConf.NotifierOptions.ProviderEvents[0])
|
||||
require.Equal(t, "update", pluginConf.NotifierOptions.ProviderEvents[1])
|
||||
|
|
@ -729,8 +761,8 @@ func TestPluginsFromEnv(t *testing.T) {
|
|||
pluginConf = pluginsConf[0]
|
||||
require.Equal(t, "notifier", pluginConf.Type)
|
||||
require.Len(t, pluginConf.NotifierOptions.FsEvents, 2)
|
||||
require.True(t, util.Contains(pluginConf.NotifierOptions.FsEvents, "upload"))
|
||||
require.True(t, util.Contains(pluginConf.NotifierOptions.FsEvents, "download"))
|
||||
require.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, "upload"))
|
||||
require.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, "download"))
|
||||
require.Len(t, pluginConf.NotifierOptions.ProviderEvents, 2)
|
||||
require.Equal(t, "add", pluginConf.NotifierOptions.ProviderEvents[0])
|
||||
require.Equal(t, "update", pluginConf.NotifierOptions.ProviderEvents[1])
|
||||
|
|
@ -787,8 +819,8 @@ func TestRateLimitersFromEnv(t *testing.T) {
|
|||
require.Equal(t, 2, limiters[0].Type)
|
||||
protocols := limiters[0].Protocols
|
||||
require.Len(t, protocols, 2)
|
||||
require.True(t, util.Contains(protocols, common.ProtocolFTP))
|
||||
require.True(t, util.Contains(protocols, common.ProtocolSSH))
|
||||
require.True(t, slices.Contains(protocols, common.ProtocolFTP))
|
||||
require.True(t, slices.Contains(protocols, common.ProtocolSSH))
|
||||
require.True(t, limiters[0].GenerateDefenderEvents)
|
||||
require.Equal(t, 50, limiters[0].EntriesSoftLimit)
|
||||
require.Equal(t, 100, limiters[0].EntriesHardLimit)
|
||||
|
|
@ -799,10 +831,10 @@ func TestRateLimitersFromEnv(t *testing.T) {
|
|||
require.Equal(t, 2, limiters[1].Type)
|
||||
protocols = limiters[1].Protocols
|
||||
require.Len(t, protocols, 4)
|
||||
require.True(t, util.Contains(protocols, common.ProtocolFTP))
|
||||
require.True(t, util.Contains(protocols, common.ProtocolSSH))
|
||||
require.True(t, util.Contains(protocols, common.ProtocolWebDAV))
|
||||
require.True(t, util.Contains(protocols, common.ProtocolHTTP))
|
||||
require.True(t, slices.Contains(protocols, common.ProtocolFTP))
|
||||
require.True(t, slices.Contains(protocols, common.ProtocolSSH))
|
||||
require.True(t, slices.Contains(protocols, common.ProtocolWebDAV))
|
||||
require.True(t, slices.Contains(protocols, common.ProtocolHTTP))
|
||||
require.False(t, limiters[1].GenerateDefenderEvents)
|
||||
require.Equal(t, 100, limiters[1].EntriesSoftLimit)
|
||||
require.Equal(t, 150, limiters[1].EntriesHardLimit)
|
||||
|
|
@ -910,7 +942,6 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
|
|||
os.Setenv("SFTPGO_FTPD__BINDINGS__0__PORT", "2200")
|
||||
os.Setenv("SFTPGO_FTPD__BINDINGS__0__APPLY_PROXY_CONFIG", "f")
|
||||
os.Setenv("SFTPGO_FTPD__BINDINGS__0__TLS_MODE", "2")
|
||||
os.Setenv("SFTPGO_FTPD__BINDINGS__0__TLS_SESSION_REUSE", "1")
|
||||
os.Setenv("SFTPGO_FTPD__BINDINGS__0__FORCE_PASSIVE_IP", "127.0.1.2")
|
||||
os.Setenv("SFTPGO_FTPD__BINDINGS__0__PASSIVE_IP_OVERRIDES__0__IP", "172.16.1.1")
|
||||
os.Setenv("SFTPGO_FTPD__BINDINGS__0__PASSIVE_HOST", "127.0.1.3")
|
||||
|
|
@ -935,7 +966,6 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
|
|||
os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__PORT")
|
||||
os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__APPLY_PROXY_CONFIG")
|
||||
os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__TLS_MODE")
|
||||
os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__TLS_SESSION_REUSE")
|
||||
os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__FORCE_PASSIVE_IP")
|
||||
os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__PASSIVE_IP_OVERRIDES__0__IP")
|
||||
os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__PASSIVE_HOST")
|
||||
|
|
@ -964,7 +994,6 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
|
|||
require.Equal(t, "127.0.0.1", bindings[0].Address)
|
||||
require.False(t, bindings[0].ApplyProxyConfig)
|
||||
require.Equal(t, 2, bindings[0].TLSMode)
|
||||
require.Equal(t, 1, bindings[0].TLSSessionReuse)
|
||||
require.Equal(t, 12, bindings[0].MinTLSVersion)
|
||||
require.Equal(t, "127.0.1.2", bindings[0].ForcePassiveIP)
|
||||
require.Len(t, bindings[0].PassiveIPOverrides, 0)
|
||||
|
|
@ -976,12 +1005,10 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
|
|||
require.False(t, bindings[0].Debug)
|
||||
require.Equal(t, 1, bindings[0].PassiveConnectionsSecurity)
|
||||
require.Equal(t, 0, bindings[0].ActiveConnectionsSecurity)
|
||||
require.Equal(t, 0, bindings[0].IgnoreASCIITransferType)
|
||||
require.Equal(t, 2203, bindings[1].Port)
|
||||
require.Equal(t, "127.0.1.1", bindings[1].Address)
|
||||
require.True(t, bindings[1].ApplyProxyConfig) // default value
|
||||
require.Equal(t, 1, bindings[1].TLSMode)
|
||||
require.Equal(t, 0, bindings[1].TLSSessionReuse)
|
||||
require.Equal(t, 13, bindings[1].MinTLSVersion)
|
||||
require.Equal(t, "127.0.1.1", bindings[1].ForcePassiveIP)
|
||||
require.Empty(t, bindings[1].PassiveHost)
|
||||
|
|
@ -994,7 +1021,6 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
|
|||
require.Nil(t, bindings[1].TLSCipherSuites)
|
||||
require.Equal(t, 0, bindings[1].PassiveConnectionsSecurity)
|
||||
require.Equal(t, 1, bindings[1].ActiveConnectionsSecurity)
|
||||
require.Equal(t, 1, bindings[1].IgnoreASCIITransferType)
|
||||
require.True(t, bindings[1].Debug)
|
||||
require.Equal(t, "cert.crt", bindings[1].CertificateFile)
|
||||
require.Equal(t, "cert.key", bindings[1].CertificateKeyFile)
|
||||
|
|
@ -1074,6 +1100,7 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
|
|||
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__ENABLE_HTTPS", "0")
|
||||
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__TLS_CIPHER_SUITES", "TLS_RSA_WITH_AES_128_CBC_SHA ")
|
||||
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__TLS_PROTOCOLS", "http/1.1 ")
|
||||
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__PROXY_MODE", "1")
|
||||
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__PROXY_ALLOWED", "192.168.10.1")
|
||||
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__CLIENT_IP_PROXY_HEADER", "X-Forwarded-For")
|
||||
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__CLIENT_IP_HEADER_DEPTH", "2")
|
||||
|
|
@ -1093,6 +1120,7 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
|
|||
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__ENABLE_HTTPS")
|
||||
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__TLS_CIPHER_SUITES")
|
||||
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__TLS_PROTOCOLS")
|
||||
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__PROXY_MODE")
|
||||
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__PROXY_ALLOWED")
|
||||
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__CLIENT_IP_PROXY_HEADER")
|
||||
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__CLIENT_IP_HEADER_DEPTH")
|
||||
|
|
@ -1117,6 +1145,7 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
|
|||
require.Equal(t, 12, bindings[0].MinTLSVersion)
|
||||
require.Len(t, bindings[0].TLSCipherSuites, 0)
|
||||
require.Len(t, bindings[0].Protocols, 0)
|
||||
require.Equal(t, 0, bindings[0].ProxyMode)
|
||||
require.Empty(t, bindings[0].Prefix)
|
||||
require.Equal(t, 0, bindings[0].ClientIPHeaderDepth)
|
||||
require.False(t, bindings[0].DisableWWWAuthHeader)
|
||||
|
|
@ -1129,6 +1158,7 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
|
|||
require.Equal(t, "TLS_RSA_WITH_AES_128_CBC_SHA", bindings[1].TLSCipherSuites[0])
|
||||
require.Len(t, bindings[1].Protocols, 1)
|
||||
assert.Equal(t, "http/1.1", bindings[1].Protocols[0])
|
||||
require.Equal(t, 1, bindings[1].ProxyMode)
|
||||
require.Equal(t, "192.168.10.1", bindings[1].ProxyAllowed[0])
|
||||
require.Equal(t, "X-Forwarded-For", bindings[1].ClientIPProxyHeader)
|
||||
require.Equal(t, 2, bindings[1].ClientIPHeaderDepth)
|
||||
|
|
@ -1139,6 +1169,7 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
|
|||
require.True(t, bindings[2].EnableHTTPS)
|
||||
require.Equal(t, 13, bindings[2].MinTLSVersion)
|
||||
require.Equal(t, 1, bindings[2].ClientAuthType)
|
||||
require.Equal(t, 0, bindings[2].ProxyMode)
|
||||
require.Nil(t, bindings[2].TLSCipherSuites)
|
||||
require.Equal(t, "/dav2", bindings[2].Prefix)
|
||||
require.Equal(t, "webdav.crt", bindings[2].CertificateFile)
|
||||
|
|
@ -1167,12 +1198,16 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_CLIENT", "0")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_REST_API", "0")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLED_LOGIN_METHODS", "3")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__DISABLED_LOGIN_METHODS", "12")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__RENDER_OPENAPI", "0")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BASE_URL", "https://example.com")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__LANGUAGES", "en,es")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_HTTPS", "1 ")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__MIN_TLS_VERSION", "13")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_AUTH_TYPE", "1")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__TLS_CIPHER_SUITES", " TLS_AES_256_GCM_SHA384 , TLS_CHACHA20_POLY1305_SHA256")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__TLS_PROTOCOLS", "h2, http/1.1")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__PROXY_MODE", "1")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__PROXY_ALLOWED", " 192.168.9.1 , 172.16.25.0/24")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_IP_PROXY_HEADER", "X-Real-IP")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_IP_HEADER_DEPTH", "2")
|
||||
|
|
@ -1203,11 +1238,14 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CONTENT_SECURITY_POLICY", "script-src $NONCE")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__PERMISSIONS_POLICY", "fullscreen=(), geolocation=()")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_OPENER_POLICY", "same-origin")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_RESOURCE_POLICY", "same-site")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_EMBEDDER_POLICY", "require-corp")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CACHE_CONTROL", "private")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__REFERRER_POLICY", "no-referrer")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__0__PATH", "path1")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__1__PATH", "path2")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__FAVICON_PATH", "favicon.ico")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__LOGO_PATH", "logo.png")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__LOGIN_IMAGE_PATH", "login_image.png")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DISCLAIMER_NAME", "disclaimer")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__DISCLAIMER_PATH", "disclaimer.html")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DEFAULT_CSS", "default.css")
|
||||
|
|
@ -1234,10 +1272,14 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_CLIENT")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_REST_API")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLED_LOGIN_METHODS")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__DISABLED_LOGIN_METHODS")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__RENDER_OPENAPI")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BASE_URL")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__LANGUAGES")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_AUTH_TYPE")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__TLS_CIPHER_SUITES")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__TLS_PROTOCOLS")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__PROXY_MODE")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__PROXY_ALLOWED")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_IP_PROXY_HEADER")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_IP_HEADER_DEPTH")
|
||||
|
|
@ -1268,11 +1310,14 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CONTENT_SECURITY_POLICY")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__PERMISSIONS_POLICY")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_OPENER_POLICY")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_RESOURCE_POLICY")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_EMBEDDER_POLICY")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CACHE_CONTROL")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__REFERRER_POLICY")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__0__PATH")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__1__PATH")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__FAVICON_PATH")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__LOGO_PATH")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__LOGIN_IMAGE_PATH")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DISCLAIMER_NAME")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__DISCLAIMER_PATH")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DEFAULT_CSS")
|
||||
|
|
@ -1294,8 +1339,13 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
require.True(t, bindings[0].EnableWebClient)
|
||||
require.True(t, bindings[0].EnableRESTAPI)
|
||||
require.Equal(t, 0, bindings[0].EnabledLoginMethods)
|
||||
require.Equal(t, 0, bindings[0].DisabledLoginMethods)
|
||||
require.True(t, bindings[0].RenderOpenAPI)
|
||||
require.Empty(t, bindings[0].BaseURL)
|
||||
require.Len(t, bindings[0].Languages, 1)
|
||||
assert.Contains(t, bindings[0].Languages, "en")
|
||||
require.Len(t, bindings[0].TLSCipherSuites, 1)
|
||||
require.Equal(t, 0, bindings[0].ProxyMode)
|
||||
require.Empty(t, bindings[0].OIDC.ConfigURL)
|
||||
require.Equal(t, "TLS_AES_128_GCM_SHA256", bindings[0].TLSCipherSuites[0])
|
||||
require.Equal(t, 0, bindings[0].HideLoginURL)
|
||||
|
|
@ -1304,6 +1354,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
require.Len(t, bindings[0].OIDC.Scopes, 3)
|
||||
require.False(t, bindings[0].OIDC.InsecureSkipSignatureCheck)
|
||||
require.False(t, bindings[0].OIDC.Debug)
|
||||
require.Empty(t, bindings[0].Security.ReferrerPolicy)
|
||||
require.Equal(t, 8000, bindings[1].Port)
|
||||
require.Equal(t, "127.0.0.1", bindings[1].Address)
|
||||
require.False(t, bindings[1].EnableHTTPS)
|
||||
|
|
@ -1312,7 +1363,11 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
require.True(t, bindings[1].EnableWebClient)
|
||||
require.True(t, bindings[1].EnableRESTAPI)
|
||||
require.Equal(t, 0, bindings[1].EnabledLoginMethods)
|
||||
require.Equal(t, 0, bindings[1].DisabledLoginMethods)
|
||||
require.True(t, bindings[1].RenderOpenAPI)
|
||||
require.Empty(t, bindings[1].BaseURL)
|
||||
require.Len(t, bindings[1].Languages, 1)
|
||||
assert.Contains(t, bindings[1].Languages, "en")
|
||||
require.Nil(t, bindings[1].TLSCipherSuites)
|
||||
require.Equal(t, 1, bindings[1].HideLoginURL)
|
||||
require.Empty(t, bindings[1].OIDC.ClientID)
|
||||
|
|
@ -1322,6 +1377,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
require.False(t, bindings[1].Security.Enabled)
|
||||
require.Equal(t, "Web Admin", bindings[1].Branding.WebAdmin.Name)
|
||||
require.Equal(t, "WebClient", bindings[1].Branding.WebClient.ShortName)
|
||||
require.Equal(t, 0, bindings[1].ProxyMode)
|
||||
require.Equal(t, 0, bindings[1].ClientIPHeaderDepth)
|
||||
require.Equal(t, 9000, bindings[2].Port)
|
||||
require.Equal(t, "127.0.1.1", bindings[2].Address)
|
||||
|
|
@ -1331,7 +1387,12 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
require.False(t, bindings[2].EnableWebClient)
|
||||
require.False(t, bindings[2].EnableRESTAPI)
|
||||
require.Equal(t, 3, bindings[2].EnabledLoginMethods)
|
||||
require.Equal(t, 12, bindings[2].DisabledLoginMethods)
|
||||
require.False(t, bindings[2].RenderOpenAPI)
|
||||
require.Equal(t, "https://example.com", bindings[2].BaseURL)
|
||||
require.Len(t, bindings[2].Languages, 2)
|
||||
assert.Contains(t, bindings[2].Languages, "en")
|
||||
assert.Contains(t, bindings[2].Languages, "es")
|
||||
require.Equal(t, 1, bindings[2].ClientAuthType)
|
||||
require.Len(t, bindings[2].TLSCipherSuites, 2)
|
||||
require.Equal(t, "TLS_AES_256_GCM_SHA384", bindings[2].TLSCipherSuites[0])
|
||||
|
|
@ -1339,6 +1400,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
require.Len(t, bindings[2].Protocols, 2)
|
||||
require.Equal(t, "h2", bindings[2].Protocols[0])
|
||||
require.Equal(t, "http/1.1", bindings[2].Protocols[1])
|
||||
require.Equal(t, 1, bindings[2].ProxyMode)
|
||||
require.Len(t, bindings[2].ProxyAllowed, 2)
|
||||
require.Equal(t, "192.168.9.1", bindings[2].ProxyAllowed[0])
|
||||
require.Equal(t, "172.16.25.0/24", bindings[2].ProxyAllowed[1])
|
||||
|
|
@ -1378,9 +1440,12 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
require.Equal(t, "script-src $NONCE", bindings[2].Security.ContentSecurityPolicy)
|
||||
require.Equal(t, "fullscreen=(), geolocation=()", bindings[2].Security.PermissionsPolicy)
|
||||
require.Equal(t, "same-origin", bindings[2].Security.CrossOriginOpenerPolicy)
|
||||
require.Equal(t, "same-site", bindings[2].Security.CrossOriginResourcePolicy)
|
||||
require.Equal(t, "require-corp", bindings[2].Security.CrossOriginEmbedderPolicy)
|
||||
require.Equal(t, "private", bindings[2].Security.CacheControl)
|
||||
require.Equal(t, "no-referrer", bindings[2].Security.ReferrerPolicy)
|
||||
require.Equal(t, "favicon.ico", bindings[2].Branding.WebAdmin.FaviconPath)
|
||||
require.Equal(t, "logo.png", bindings[2].Branding.WebClient.LogoPath)
|
||||
require.Equal(t, "login_image.png", bindings[2].Branding.WebAdmin.LoginImagePath)
|
||||
require.Equal(t, "disclaimer", bindings[2].Branding.WebClient.DisclaimerName)
|
||||
require.Equal(t, "disclaimer.html", bindings[2].Branding.WebAdmin.DisclaimerPath)
|
||||
require.Equal(t, []string{"default.css"}, bindings[2].Branding.WebClient.DefaultCSS)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"net/url"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -78,8 +79,8 @@ func executeAction(operation, executor, ip, objectType, objectName, role string,
|
|||
if config.Actions.Hook == "" {
|
||||
return
|
||||
}
|
||||
if !util.Contains(config.Actions.ExecuteOn, operation) ||
|
||||
!util.Contains(config.Actions.ExecuteFor, objectType) {
|
||||
if !slices.Contains(config.Actions.ExecuteOn, operation) ||
|
||||
!slices.Contains(config.Actions.ExecuteFor, objectType) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -141,14 +142,14 @@ func executeNotificationCommand(operation, executor, ip, objectType, objectName,
|
|||
|
||||
cmd := exec.CommandContext(ctx, config.Actions.Hook, args...)
|
||||
cmd.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_ACTION=%vs", operation),
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_ACTION=%s", operation),
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT_TYPE=%s", objectType),
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT_NAME=%s", objectName),
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_USERNAME=%s", executor),
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_IP=%s", ip),
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_ROLE=%s", role),
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_TIMESTAMP=%d", util.GetTimeAsMsSinceEpoch(time.Now())),
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT=%s", util.BytesToString(objectAsJSON)))
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT=%s", objectAsJSON))
|
||||
|
||||
startTime := time.Now()
|
||||
err := cmd.Run()
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
|
@ -44,19 +45,12 @@ const (
|
|||
PermAdminViewConnections = "view_conns"
|
||||
PermAdminCloseConnections = "close_conns"
|
||||
PermAdminViewServerStatus = "view_status"
|
||||
PermAdminManageAdmins = "manage_admins"
|
||||
PermAdminManageGroups = "manage_groups"
|
||||
PermAdminManageFolders = "manage_folders"
|
||||
PermAdminManageAPIKeys = "manage_apikeys"
|
||||
PermAdminQuotaScans = "quota_scans"
|
||||
PermAdminManageSystem = "manage_system"
|
||||
PermAdminManageDefender = "manage_defender"
|
||||
PermAdminViewDefender = "view_defender"
|
||||
PermAdminRetentionChecks = "retention_checks"
|
||||
PermAdminViewEvents = "view_events"
|
||||
PermAdminManageEventRules = "manage_event_rules"
|
||||
PermAdminManageRoles = "manage_roles"
|
||||
PermAdminManageIPLists = "manage_ip_lists"
|
||||
PermAdminDisableMFA = "disable_mfa"
|
||||
)
|
||||
|
||||
|
|
@ -72,12 +66,9 @@ const (
|
|||
var (
|
||||
validAdminPerms = []string{PermAdminAny, PermAdminAddUsers, PermAdminChangeUsers, PermAdminDeleteUsers,
|
||||
PermAdminViewUsers, PermAdminManageFolders, PermAdminManageGroups, PermAdminViewConnections,
|
||||
PermAdminCloseConnections, PermAdminViewServerStatus, PermAdminManageAdmins, PermAdminManageRoles,
|
||||
PermAdminManageEventRules, PermAdminManageAPIKeys, PermAdminQuotaScans, PermAdminManageSystem,
|
||||
PermAdminManageDefender, PermAdminViewDefender, PermAdminManageIPLists, PermAdminRetentionChecks,
|
||||
PermAdminViewEvents, PermAdminDisableMFA}
|
||||
forbiddenPermsForRoleAdmins = []string{PermAdminAny, PermAdminManageAdmins, PermAdminManageSystem,
|
||||
PermAdminManageEventRules, PermAdminManageIPLists, PermAdminManageRoles}
|
||||
PermAdminCloseConnections, PermAdminViewServerStatus, PermAdminQuotaScans,
|
||||
PermAdminManageDefender, PermAdminViewDefender, PermAdminViewEvents, PermAdminDisableMFA}
|
||||
forbiddenPermsForRoleAdmins = []string{PermAdminAny}
|
||||
)
|
||||
|
||||
// AdminTOTPConfig defines the time-based one time password configuration
|
||||
|
|
@ -96,7 +87,7 @@ func (c *AdminTOTPConfig) validate(username string) error {
|
|||
if c.ConfigName == "" {
|
||||
return util.NewValidationError("totp: config name is mandatory")
|
||||
}
|
||||
if !util.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {
|
||||
if !slices.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {
|
||||
return util.NewValidationError(fmt.Sprintf("totp: config name %q not found", c.ConfigName))
|
||||
}
|
||||
if c.Secret.IsEmpty() {
|
||||
|
|
@ -265,12 +256,7 @@ type Admin struct {
|
|||
// Last login as unix timestamp in milliseconds
|
||||
LastLogin int64 `json:"last_login"`
|
||||
// Role name. If set the admin can only administer users with the same role.
|
||||
// Role admins cannot have the following permissions:
|
||||
// - manage_admins
|
||||
// - manage_apikeys
|
||||
// - manage_system
|
||||
// - manage_event_rules
|
||||
// - manage_roles
|
||||
// Role admins cannot be super administrators
|
||||
Role string `json:"role,omitempty"`
|
||||
}
|
||||
|
||||
|
|
@ -337,22 +323,18 @@ func (a *Admin) validatePermissions() error {
|
|||
util.I18nErrorPermissionsRequired,
|
||||
)
|
||||
}
|
||||
if util.Contains(a.Permissions, PermAdminAny) {
|
||||
if slices.Contains(a.Permissions, PermAdminAny) {
|
||||
a.Permissions = []string{PermAdminAny}
|
||||
}
|
||||
for _, perm := range a.Permissions {
|
||||
if !util.Contains(validAdminPerms, perm) {
|
||||
if !slices.Contains(validAdminPerms, perm) {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid permission: %q", perm))
|
||||
}
|
||||
if a.Role != "" {
|
||||
if util.Contains(forbiddenPermsForRoleAdmins, perm) {
|
||||
deniedPerms := strings.Join(forbiddenPermsForRoleAdmins, ",")
|
||||
if slices.Contains(forbiddenPermsForRoleAdmins, perm) {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("a role admin cannot have the following permissions: %q", deniedPerms)),
|
||||
util.NewValidationError("a role admin cannot be a super admin"),
|
||||
util.I18nErrorRoleAdminPerms,
|
||||
util.I18nErrorArgs(map[string]any{
|
||||
"val": deniedPerms,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -382,11 +364,24 @@ func (a *Admin) validateGroups() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (a *Admin) validate() error {
|
||||
func (a *Admin) applyNamingRules() {
|
||||
a.Username = config.convertName(a.Username)
|
||||
a.Role = config.convertName(a.Role)
|
||||
for idx := range a.Groups {
|
||||
a.Groups[idx].Name = config.convertName(a.Groups[idx].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Admin) validate() error { //nolint:gocyclo
|
||||
a.SetEmptySecretsIfNil()
|
||||
a.applyNamingRules()
|
||||
a.Password = strings.TrimSpace(a.Password)
|
||||
if a.Username == "" {
|
||||
return util.NewI18nError(util.NewValidationError("username is mandatory"), util.I18nErrorUsernameRequired)
|
||||
}
|
||||
if !util.IsNameValid(a.Username) {
|
||||
return util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)
|
||||
}
|
||||
if err := checkReservedUsernames(a.Username); err != nil {
|
||||
return util.NewI18nError(err, util.I18nErrorReservedUsername)
|
||||
}
|
||||
|
|
@ -499,7 +494,7 @@ func (a *Admin) checkUserAndPass(password, ip string) error {
|
|||
if err := a.CanLogin(ip); err != nil {
|
||||
return err
|
||||
}
|
||||
if a.Password == "" || password == "" {
|
||||
if a.Password == "" || strings.TrimSpace(password) == "" {
|
||||
return errors.New("credentials cannot be null or empty")
|
||||
}
|
||||
match, err := a.CheckPassword(password)
|
||||
|
|
@ -559,10 +554,20 @@ func (a *Admin) SetNilSecretsIfEmpty() {
|
|||
|
||||
// HasPermission returns true if the admin has the specified permission
|
||||
func (a *Admin) HasPermission(perm string) bool {
|
||||
if util.Contains(a.Permissions, PermAdminAny) {
|
||||
if slices.Contains(a.Permissions, PermAdminAny) {
|
||||
return true
|
||||
}
|
||||
return util.Contains(a.Permissions, perm)
|
||||
return slices.Contains(a.Permissions, perm)
|
||||
}
|
||||
|
||||
// HasPermissions returns true if the admin has all the specified permissions
|
||||
func (a *Admin) HasPermissions(perms ...string) bool {
|
||||
for _, perm := range perms {
|
||||
if !a.HasPermission(perm) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return len(perms) > 0
|
||||
}
|
||||
|
||||
// GetAllowedIPAsString returns the allowed IP as comma separated string
|
||||
|
|
|
|||
|
|
@ -148,6 +148,9 @@ func (k *APIKey) validate() error {
|
|||
if k.Name == "" {
|
||||
return util.NewValidationError("name is mandatory")
|
||||
}
|
||||
if !util.IsNameValid(k.Name) {
|
||||
return util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)
|
||||
}
|
||||
if k.Scope != APIKeyScopeAdmin && k.Scope != APIKeyScopeUser {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid scope: %v", k.Scope))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build !nobolt
|
||||
// +build !nobolt
|
||||
|
||||
package dataprovider
|
||||
|
||||
|
|
@ -25,10 +24,13 @@ import (
|
|||
"fmt"
|
||||
"net/netip"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
bolt "go.etcd.io/bbolt"
|
||||
bolterrors "go.etcd.io/bbolt/errors"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
|
|
@ -37,7 +39,7 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
boltDatabaseVersion = 29
|
||||
boltDatabaseVersion = 34
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -181,6 +183,50 @@ func (p *BoltProvider) updateAPIKeyLastUse(keyID string) error {
|
|||
})
|
||||
}
|
||||
|
||||
func (p *BoltProvider) getAdminSignature(username string) (string, error) {
|
||||
var updatedAt int64
|
||||
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||
bucket, err := p.getAdminsBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u := bucket.Get([]byte(username))
|
||||
var admin Admin
|
||||
err = json.Unmarshal(u, &admin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updatedAt = admin.UpdatedAt
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strconv.FormatInt(updatedAt, 10), nil
|
||||
}
|
||||
|
||||
func (p *BoltProvider) getUserSignature(username string) (string, error) {
|
||||
var updatedAt int64
|
||||
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||
bucket, err := p.getUsersBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u := bucket.Get([]byte(username))
|
||||
var user User
|
||||
err = json.Unmarshal(u, &user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updatedAt = user.UpdatedAt
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strconv.FormatInt(updatedAt, 10), nil
|
||||
}
|
||||
|
||||
func (p *BoltProvider) setUpdatedAt(username string) {
|
||||
p.dbHandle.Update(func(tx *bolt.Tx) error { //nolint:errcheck
|
||||
bucket, err := p.getUsersBucket(tx)
|
||||
|
|
@ -400,9 +446,6 @@ func (p *BoltProvider) addAdmin(admin *Admin) error {
|
|||
admin.LastLogin = 0
|
||||
admin.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
admin.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
sort.Slice(admin.Groups, func(i, j int) bool {
|
||||
return admin.Groups[i].Name < admin.Groups[j].Name
|
||||
})
|
||||
for idx := range admin.Groups {
|
||||
err = p.addAdminToGroupMapping(admin.Username, admin.Groups[idx].Name, groupBucket)
|
||||
if err != nil {
|
||||
|
|
@ -461,9 +504,6 @@ func (p *BoltProvider) updateAdmin(admin *Admin) error {
|
|||
if err = p.addAdminToRole(admin.Username, admin.Role, rolesBucket); err != nil {
|
||||
return err
|
||||
}
|
||||
sort.Slice(admin.Groups, func(i, j int) bool {
|
||||
return admin.Groups[i].Name < admin.Groups[j].Name
|
||||
})
|
||||
for idx := range admin.Groups {
|
||||
err = p.addAdminToGroupMapping(admin.Username, admin.Groups[idx].Name, groupBucket)
|
||||
if err != nil {
|
||||
|
|
@ -675,18 +715,12 @@ func (p *BoltProvider) addUser(user *User) error {
|
|||
if err := p.addUserToRole(user.Username, user.Role, rolesBucket); err != nil {
|
||||
return err
|
||||
}
|
||||
sort.Slice(user.VirtualFolders, func(i, j int) bool {
|
||||
return user.VirtualFolders[i].Name < user.VirtualFolders[j].Name
|
||||
})
|
||||
for idx := range user.VirtualFolders {
|
||||
err = p.addRelationToFolderMapping(user.VirtualFolders[idx].Name, user, nil, foldersBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
sort.Slice(user.Groups, func(i, j int) bool {
|
||||
return user.Groups[i].Name < user.Groups[j].Name
|
||||
})
|
||||
for idx := range user.Groups {
|
||||
err = p.addUserToGroupMapping(user.Username, user.Groups[idx].Name, groupBucket)
|
||||
if err != nil {
|
||||
|
|
@ -1458,9 +1492,6 @@ func (p *BoltProvider) addGroup(group *Group) error {
|
|||
group.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
group.Users = nil
|
||||
group.Admins = nil
|
||||
sort.Slice(group.VirtualFolders, func(i, j int) bool {
|
||||
return group.VirtualFolders[i].Name < group.VirtualFolders[j].Name
|
||||
})
|
||||
for idx := range group.VirtualFolders {
|
||||
err = p.addRelationToFolderMapping(group.VirtualFolders[idx].Name, nil, group, foldersBucket)
|
||||
if err != nil {
|
||||
|
|
@ -1503,9 +1534,6 @@ func (p *BoltProvider) updateGroup(group *Group) error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
sort.Slice(group.VirtualFolders, func(i, j int) bool {
|
||||
return group.VirtualFolders[i].Name < group.VirtualFolders[j].Name
|
||||
})
|
||||
for idx := range group.VirtualFolders {
|
||||
err = p.addRelationToFolderMapping(group.VirtualFolders[idx].Name, nil, group, foldersBucket)
|
||||
if err != nil {
|
||||
|
|
@ -2088,11 +2116,11 @@ func (p *BoltProvider) addSharedSession(_ Session) error {
|
|||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
func (p *BoltProvider) deleteSharedSession(_ string) error {
|
||||
func (p *BoltProvider) deleteSharedSession(_ string, _ SessionType) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
func (p *BoltProvider) getSharedSession(_ string) (Session, error) {
|
||||
func (p *BoltProvider) getSharedSession(_ string, _ SessionType) (Session, error) {
|
||||
return Session{}, ErrNotImplemented
|
||||
}
|
||||
|
||||
|
|
@ -3134,15 +3162,16 @@ func (p *BoltProvider) migrateDatabase() error {
|
|||
case version == boltDatabaseVersion:
|
||||
providerLog(logger.LevelDebug, "bolt database is up to date, current version: %d", version)
|
||||
return ErrNoInitRequired
|
||||
case version < 28:
|
||||
err = fmt.Errorf("database schema version %d is too old, please see the upgrading docs", version)
|
||||
case version < 33:
|
||||
err = errSchemaVersionTooOld(version)
|
||||
providerLog(logger.LevelError, "%v", err)
|
||||
logger.ErrorToConsole("%v", err)
|
||||
return err
|
||||
case version == 28:
|
||||
logger.InfoToConsole("updating database schema version: %d -> 29", version)
|
||||
providerLog(logger.LevelInfo, "updating database schema version: %d -> 29", version)
|
||||
return updateBoltDatabaseVersion(p.dbHandle, 29)
|
||||
case version == 33:
|
||||
logger.InfoToConsole("updating database schema version: %d -> 34", version)
|
||||
providerLog(logger.LevelInfo, "updating database schema version: %d -> 34", version)
|
||||
return updateBoltDatabaseVersion(p.dbHandle, 34)
|
||||
|
||||
default:
|
||||
if version > boltDatabaseVersion {
|
||||
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
|
||||
|
|
@ -3155,7 +3184,7 @@ func (p *BoltProvider) migrateDatabase() error {
|
|||
}
|
||||
}
|
||||
|
||||
func (p *BoltProvider) revertDatabase(targetVersion int) error { //nolint:gocyclo
|
||||
func (p *BoltProvider) revertDatabase(targetVersion int) error {
|
||||
dbVersion, err := getBoltDatabaseVersion(p.dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -3164,10 +3193,11 @@ func (p *BoltProvider) revertDatabase(targetVersion int) error { //nolint:gocycl
|
|||
return errors.New("current version match target version, nothing to do")
|
||||
}
|
||||
switch dbVersion.Version {
|
||||
case 29:
|
||||
logger.InfoToConsole("downgrading database schema version: %d -> 28", dbVersion.Version)
|
||||
providerLog(logger.LevelInfo, "downgrading database schema version: %d -> 28", dbVersion.Version)
|
||||
return updateBoltDatabaseVersion(p.dbHandle, 28)
|
||||
case 34:
|
||||
logger.InfoToConsole("downgrading database schema version: %d -> 33", dbVersion.Version)
|
||||
providerLog(logger.LevelInfo, "downgrading database schema version: %d -> 33", dbVersion.Version)
|
||||
return updateBoltDatabaseVersion(p.dbHandle, 33)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("database schema version not handled: %v", dbVersion.Version)
|
||||
}
|
||||
|
|
@ -3177,7 +3207,7 @@ func (p *BoltProvider) resetDatabase() error {
|
|||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
for _, bucketName := range boltBuckets {
|
||||
err := tx.DeleteBucket(bucketName)
|
||||
if err != nil && !errors.Is(err, bolt.ErrBucketNotFound) {
|
||||
if err != nil && !errors.Is(err, bolterrors.ErrBucketNotFound) {
|
||||
return fmt.Errorf("unable to remove bucket %v: %w", bucketName, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -3328,7 +3358,7 @@ func (p *BoltProvider) addAdminToRole(username, roleName string, bucket *bolt.Bu
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !util.Contains(role.Admins, username) {
|
||||
if !slices.Contains(role.Admins, username) {
|
||||
role.Admins = append(role.Admins, username)
|
||||
buf, err := json.Marshal(role)
|
||||
if err != nil {
|
||||
|
|
@ -3353,7 +3383,7 @@ func (p *BoltProvider) removeAdminFromRole(username, roleName string, bucket *bo
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if util.Contains(role.Admins, username) {
|
||||
if slices.Contains(role.Admins, username) {
|
||||
var admins []string
|
||||
for _, admin := range role.Admins {
|
||||
if admin != username {
|
||||
|
|
@ -3383,7 +3413,7 @@ func (p *BoltProvider) addUserToRole(username, roleName string, bucket *bolt.Buc
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !util.Contains(role.Users, username) {
|
||||
if !slices.Contains(role.Users, username) {
|
||||
role.Users = append(role.Users, username)
|
||||
buf, err := json.Marshal(role)
|
||||
if err != nil {
|
||||
|
|
@ -3408,7 +3438,7 @@ func (p *BoltProvider) removeUserFromRole(username, roleName string, bucket *bol
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if util.Contains(role.Users, username) {
|
||||
if slices.Contains(role.Users, username) {
|
||||
var users []string
|
||||
for _, user := range role.Users {
|
||||
if user != username {
|
||||
|
|
@ -3436,7 +3466,7 @@ func (p *BoltProvider) addRuleToActionMapping(ruleName, actionName string, bucke
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !util.Contains(action.Rules, ruleName) {
|
||||
if !slices.Contains(action.Rules, ruleName) {
|
||||
action.Rules = append(action.Rules, ruleName)
|
||||
buf, err := json.Marshal(action)
|
||||
if err != nil {
|
||||
|
|
@ -3458,7 +3488,7 @@ func (p *BoltProvider) removeRuleFromActionMapping(ruleName, actionName string,
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if util.Contains(action.Rules, ruleName) {
|
||||
if slices.Contains(action.Rules, ruleName) {
|
||||
var rules []string
|
||||
for _, r := range action.Rules {
|
||||
if r != ruleName {
|
||||
|
|
@ -3485,7 +3515,7 @@ func (p *BoltProvider) addUserToGroupMapping(username, groupname string, bucket
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !util.Contains(group.Users, username) {
|
||||
if !slices.Contains(group.Users, username) {
|
||||
group.Users = append(group.Users, username)
|
||||
buf, err := json.Marshal(group)
|
||||
if err != nil {
|
||||
|
|
@ -3530,7 +3560,7 @@ func (p *BoltProvider) addAdminToGroupMapping(username, groupname string, bucket
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !util.Contains(group.Admins, username) {
|
||||
if !slices.Contains(group.Admins, username) {
|
||||
group.Admins = append(group.Admins, username)
|
||||
buf, err := json.Marshal(group)
|
||||
if err != nil {
|
||||
|
|
@ -3601,11 +3631,11 @@ func (p *BoltProvider) addRelationToFolderMapping(folderName string, user *User,
|
|||
return err
|
||||
}
|
||||
updated := false
|
||||
if user != nil && !util.Contains(folder.Users, user.Username) {
|
||||
if user != nil && !slices.Contains(folder.Users, user.Username) {
|
||||
folder.Users = append(folder.Users, user.Username)
|
||||
updated = true
|
||||
}
|
||||
if group != nil && !util.Contains(folder.Groups, group.Name) {
|
||||
if group != nil && !slices.Contains(folder.Groups, group.Name) {
|
||||
folder.Groups = append(folder.Groups, group.Name)
|
||||
updated = true
|
||||
}
|
||||
|
|
@ -3691,18 +3721,12 @@ func (p *BoltProvider) updateUserRelations(tx *bolt.Tx, user *User, oldUser User
|
|||
if err = p.removeUserFromRole(oldUser.Username, oldUser.Role, rolesBucket); err != nil {
|
||||
return err
|
||||
}
|
||||
sort.Slice(user.VirtualFolders, func(i, j int) bool {
|
||||
return user.VirtualFolders[i].Name < user.VirtualFolders[j].Name
|
||||
})
|
||||
for idx := range user.VirtualFolders {
|
||||
err = p.addRelationToFolderMapping(user.VirtualFolders[idx].Name, user, nil, foldersBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
sort.Slice(user.Groups, func(i, j int) bool {
|
||||
return user.Groups[i].Name < user.Groups[j].Name
|
||||
})
|
||||
for idx := range user.Groups {
|
||||
err = p.addUserToGroupMapping(user.Username, user.Groups[idx].Name, groupsBucket)
|
||||
if err != nil {
|
||||
|
|
@ -3899,7 +3923,7 @@ func getBoltDatabaseVersion(dbHandle *bolt.DB) (schemaVersion, error) {
|
|||
v := bucket.Get(dbVersionKey)
|
||||
if v == nil {
|
||||
dbVersion = schemaVersion{
|
||||
Version: 28,
|
||||
Version: 33,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build nobolt
|
||||
// +build nobolt
|
||||
|
||||
package dataprovider
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,12 @@
|
|||
package dataprovider
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image/png"
|
||||
"net/url"
|
||||
"slices"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
|
|
@ -28,18 +32,18 @@ import (
|
|||
// Supported values for host keys, KEXs, ciphers, MACs
|
||||
var (
|
||||
supportedHostKeyAlgos = []string{ssh.KeyAlgoRSA}
|
||||
supportedPublicKeyAlgos = []string{ssh.KeyAlgoRSA, ssh.InsecureKeyAlgoDSA}
|
||||
supportedPublicKeyAlgos = []string{ssh.KeyAlgoRSA, ssh.InsecureKeyAlgoDSA} //nolint:staticcheck
|
||||
supportedKexAlgos = []string{
|
||||
ssh.KeyExchangeDH16SHA512, ssh.InsecureKeyExchangeDH14SHA1, ssh.InsecureKeyExchangeDH1SHA1,
|
||||
ssh.InsecureKeyExchangeDHGEXSHA1,
|
||||
}
|
||||
supportedCiphers = []string{
|
||||
ssh.InsecureCipherAES128CBC, ssh.InsecureCipherAES192CBC, ssh.InsecureCipherAES256CBC,
|
||||
ssh.InsecureCipherAES128CBC,
|
||||
ssh.InsecureCipherTripleDESCBC,
|
||||
}
|
||||
supportedMACs = []string{
|
||||
ssh.HMACSHA512ETM, ssh.HMACSHA512,
|
||||
ssh.InsecureHMACSHA1, ssh.InsecureHMACSHA196,
|
||||
ssh.HMACSHA1, ssh.InsecureHMACSHA196,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -102,7 +106,7 @@ func (c *SFTPDConfigs) validate() error {
|
|||
if algo == ssh.CertAlgoRSAv01 {
|
||||
continue
|
||||
}
|
||||
if !util.Contains(supportedHostKeyAlgos, algo) {
|
||||
if !slices.Contains(supportedHostKeyAlgos, algo) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported host key algorithm %q", algo))
|
||||
}
|
||||
hostKeyAlgos = append(hostKeyAlgos, algo)
|
||||
|
|
@ -113,24 +117,27 @@ func (c *SFTPDConfigs) validate() error {
|
|||
if algo == "diffie-hellman-group18-sha512" || algo == ssh.KeyExchangeDHGEXSHA256 {
|
||||
continue
|
||||
}
|
||||
if !util.Contains(supportedKexAlgos, algo) {
|
||||
if !slices.Contains(supportedKexAlgos, algo) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported KEX algorithm %q", algo))
|
||||
}
|
||||
kexAlgos = append(kexAlgos, algo)
|
||||
}
|
||||
c.KexAlgorithms = kexAlgos
|
||||
for _, cipher := range c.Ciphers {
|
||||
if !util.Contains(supportedCiphers, cipher) {
|
||||
if slices.Contains([]string{"aes192-cbc", "aes256-cbc"}, cipher) {
|
||||
continue
|
||||
}
|
||||
if !slices.Contains(supportedCiphers, cipher) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported cipher %q", cipher))
|
||||
}
|
||||
}
|
||||
for _, mac := range c.MACs {
|
||||
if !util.Contains(supportedMACs, mac) {
|
||||
if !slices.Contains(supportedMACs, mac) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported MAC algorithm %q", mac))
|
||||
}
|
||||
}
|
||||
for _, algo := range c.PublicKeyAlgos {
|
||||
if !util.Contains(supportedPublicKeyAlgos, algo) {
|
||||
if !slices.Contains(supportedPublicKeyAlgos, algo) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported public key algorithm %q", algo))
|
||||
}
|
||||
}
|
||||
|
|
@ -193,19 +200,19 @@ func (c *SMTPOAuth2) validate() error {
|
|||
if c.ClientID == "" {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("smtp oauth2: client id is required"),
|
||||
util.I18nErrorSMTPClientIDRequired,
|
||||
util.I18nErrorClientIDRequired,
|
||||
)
|
||||
}
|
||||
if c.ClientSecret == nil {
|
||||
if c.ClientSecret == nil || c.ClientSecret.IsEmpty() {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("smtp oauth2: client secret is required"),
|
||||
util.I18nErrorSMTPClientSecretRequired,
|
||||
util.I18nErrorClientSecretRequired,
|
||||
)
|
||||
}
|
||||
if c.RefreshToken == nil {
|
||||
if c.RefreshToken == nil || c.RefreshToken.IsEmpty() {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("smtp oauth2: refresh token is required"),
|
||||
util.I18nErrorSMTPRefreshTokenRequired,
|
||||
util.I18nErrorRefreshTokenRequired,
|
||||
)
|
||||
}
|
||||
if err := validateSMTPSecret(c.ClientSecret, "oauth2 client secret"); err != nil {
|
||||
|
|
@ -305,6 +312,27 @@ func (c *SMTPConfigs) TryDecrypt() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *SMTPConfigs) prepareForRendering() {
|
||||
if c.Password != nil {
|
||||
c.Password.Hide()
|
||||
if c.Password.IsEmpty() {
|
||||
c.Password = nil
|
||||
}
|
||||
}
|
||||
if c.OAuth2.ClientSecret != nil {
|
||||
c.OAuth2.ClientSecret.Hide()
|
||||
if c.OAuth2.ClientSecret.IsEmpty() {
|
||||
c.OAuth2.ClientSecret = nil
|
||||
}
|
||||
}
|
||||
if c.OAuth2.RefreshToken != nil {
|
||||
c.OAuth2.RefreshToken.Hide()
|
||||
if c.OAuth2.RefreshToken.IsEmpty() {
|
||||
c.OAuth2.RefreshToken = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SMTPConfigs) getACopy() *SMTPConfigs {
|
||||
var password *kms.Secret
|
||||
if c.Password != nil {
|
||||
|
|
@ -387,13 +415,137 @@ func (c *ACMEConfigs) getACopy() *ACMEConfigs {
|
|||
}
|
||||
}
|
||||
|
||||
// BrandingConfig defines the branding configuration
|
||||
type BrandingConfig struct {
|
||||
Name string `json:"name"`
|
||||
ShortName string `json:"short_name"`
|
||||
Logo []byte `json:"logo"`
|
||||
Favicon []byte `json:"favicon"`
|
||||
DisclaimerName string `json:"disclaimer_name"`
|
||||
DisclaimerURL string `json:"disclaimer_url"`
|
||||
}
|
||||
|
||||
func (c *BrandingConfig) isEmpty() bool {
|
||||
if c.Name != "" {
|
||||
return false
|
||||
}
|
||||
if c.ShortName != "" {
|
||||
return false
|
||||
}
|
||||
if len(c.Logo) > 0 {
|
||||
return false
|
||||
}
|
||||
if len(c.Favicon) > 0 {
|
||||
return false
|
||||
}
|
||||
if c.DisclaimerName != "" && c.DisclaimerURL != "" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (*BrandingConfig) validatePNG(b []byte, maxWidth, maxHeight int) error {
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
// DecodeConfig is more efficient, but I'm not sure if this would lead to
|
||||
// accepting invalid images in some edge cases and performance does not
|
||||
// matter here.
|
||||
img, err := png.Decode(bytes.NewBuffer(b))
|
||||
if err != nil {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("invalid PNG image"),
|
||||
util.I18nErrorInvalidPNG,
|
||||
)
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
if bounds.Dx() > maxWidth || bounds.Dy() > maxHeight {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("invalid PNG image size"),
|
||||
util.I18nErrorInvalidPNGSize,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *BrandingConfig) validateDisclaimerURL() error {
|
||||
if c.DisclaimerURL == "" {
|
||||
return nil
|
||||
}
|
||||
u, err := url.Parse(c.DisclaimerURL)
|
||||
if err != nil {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("invalid disclaimer URL"),
|
||||
util.I18nErrorInvalidDisclaimerURL,
|
||||
)
|
||||
}
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("invalid disclaimer URL scheme"),
|
||||
util.I18nErrorInvalidDisclaimerURL,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *BrandingConfig) validate() error {
|
||||
if err := c.validateDisclaimerURL(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.validatePNG(c.Logo, 512, 512); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.validatePNG(c.Favicon, 256, 256)
|
||||
}
|
||||
|
||||
func (c *BrandingConfig) getACopy() BrandingConfig {
|
||||
logo := make([]byte, len(c.Logo))
|
||||
copy(logo, c.Logo)
|
||||
favicon := make([]byte, len(c.Favicon))
|
||||
copy(favicon, c.Favicon)
|
||||
|
||||
return BrandingConfig{
|
||||
Name: c.Name,
|
||||
ShortName: c.ShortName,
|
||||
Logo: logo,
|
||||
Favicon: favicon,
|
||||
DisclaimerName: c.DisclaimerName,
|
||||
DisclaimerURL: c.DisclaimerURL,
|
||||
}
|
||||
}
|
||||
|
||||
// BrandingConfigs defines the branding configuration for WebAdmin and WebClient UI
|
||||
type BrandingConfigs struct {
|
||||
WebAdmin BrandingConfig
|
||||
WebClient BrandingConfig
|
||||
}
|
||||
|
||||
func (c *BrandingConfigs) isEmpty() bool {
|
||||
return c.WebAdmin.isEmpty() && c.WebClient.isEmpty()
|
||||
}
|
||||
|
||||
func (c *BrandingConfigs) validate() error {
|
||||
if err := c.WebAdmin.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.WebClient.validate()
|
||||
}
|
||||
|
||||
func (c *BrandingConfigs) getACopy() *BrandingConfigs {
|
||||
return &BrandingConfigs{
|
||||
WebAdmin: c.WebAdmin.getACopy(),
|
||||
WebClient: c.WebClient.getACopy(),
|
||||
}
|
||||
}
|
||||
|
||||
// Configs allows to set configuration keys disabled by default without
|
||||
// modifying the config file or setting env vars
|
||||
type Configs struct {
|
||||
SFTPD *SFTPDConfigs `json:"sftpd,omitempty"`
|
||||
SMTP *SMTPConfigs `json:"smtp,omitempty"`
|
||||
ACME *ACMEConfigs `json:"acme,omitempty"`
|
||||
UpdatedAt int64 `json:"updated_at,omitempty"`
|
||||
SFTPD *SFTPDConfigs `json:"sftpd,omitempty"`
|
||||
SMTP *SMTPConfigs `json:"smtp,omitempty"`
|
||||
ACME *ACMEConfigs `json:"acme,omitempty"`
|
||||
Branding *BrandingConfigs `json:"branding,omitempty"`
|
||||
UpdatedAt int64 `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Configs) validate() error {
|
||||
|
|
@ -412,6 +564,11 @@ func (c *Configs) validate() error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
if c.Branding != nil {
|
||||
if err := c.Branding.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -428,25 +585,11 @@ func (c *Configs) PrepareForRendering() {
|
|||
if c.ACME != nil && c.ACME.isEmpty() {
|
||||
c.ACME = nil
|
||||
}
|
||||
if c.Branding != nil && c.Branding.isEmpty() {
|
||||
c.Branding = nil
|
||||
}
|
||||
if c.SMTP != nil {
|
||||
if c.SMTP.Password != nil {
|
||||
c.SMTP.Password.Hide()
|
||||
if c.SMTP.Password.IsEmpty() {
|
||||
c.SMTP.Password = nil
|
||||
}
|
||||
}
|
||||
if c.SMTP.OAuth2.ClientSecret != nil {
|
||||
c.SMTP.OAuth2.ClientSecret.Hide()
|
||||
if c.SMTP.OAuth2.ClientSecret.IsEmpty() {
|
||||
c.SMTP.OAuth2.ClientSecret = nil
|
||||
}
|
||||
}
|
||||
if c.SMTP.OAuth2.RefreshToken != nil {
|
||||
c.SMTP.OAuth2.RefreshToken.Hide()
|
||||
if c.SMTP.OAuth2.RefreshToken.IsEmpty() {
|
||||
c.SMTP.OAuth2.RefreshToken = nil
|
||||
}
|
||||
}
|
||||
c.SMTP.prepareForRendering()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -470,6 +613,9 @@ func (c *Configs) SetNilsToEmpty() {
|
|||
if c.ACME == nil {
|
||||
c.ACME = &ACMEConfigs{}
|
||||
}
|
||||
if c.Branding == nil {
|
||||
c.Branding = &BrandingConfigs{}
|
||||
}
|
||||
}
|
||||
|
||||
// RenderAsJSON implements the renderer interface used within plugins
|
||||
|
|
@ -498,6 +644,9 @@ func (c *Configs) getACopy() Configs {
|
|||
if c.ACME != nil {
|
||||
result.ACME = c.ACME.getACopy()
|
||||
}
|
||||
if c.Branding != nil {
|
||||
result.Branding = c.Branding.getACopy()
|
||||
}
|
||||
result.UpdatedAt = c.UpdatedAt
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import (
|
|||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
|
@ -89,7 +90,7 @@ const (
|
|||
CockroachDataProviderName = "cockroachdb"
|
||||
// DumpVersion defines the version for the dump.
|
||||
// For restore/load we support the current version and the previous one
|
||||
DumpVersion = 16
|
||||
DumpVersion = 17
|
||||
|
||||
argonPwdPrefix = "$argon2id$"
|
||||
bcryptPwdPrefix = "$2a$"
|
||||
|
|
@ -111,7 +112,6 @@ const (
|
|||
operationDelete = "delete"
|
||||
sqlPrefixValidChars = "abcdefghijklmnopqrstuvwxyz_0123456789"
|
||||
maxHookResponseSize = 1048576 // 1MB
|
||||
iso8601UTCFormat = "2006-01-02T15:04:05Z"
|
||||
)
|
||||
|
||||
// Supported algorithms for hashing passwords.
|
||||
|
|
@ -187,6 +187,8 @@ var (
|
|||
ErrDuplicatedKey = errors.New("duplicated key not allowed")
|
||||
// ErrForeignKeyViolated occurs when there is a foreign key constraint violation
|
||||
ErrForeignKeyViolated = errors.New("violates foreign key constraint")
|
||||
errInvalidInput = util.NewValidationError("Invalid input. Slashes (/ ), colons (:), control characters, and reserved system names are not allowed")
|
||||
tz = ""
|
||||
isAdminCreated atomic.Bool
|
||||
validTLSUsernames = []string{string(sdk.TLSUsernameNone), string(sdk.TLSUsernameCN)}
|
||||
config Config
|
||||
|
|
@ -209,6 +211,7 @@ var (
|
|||
sqlTableAdmins string
|
||||
sqlTableAPIKeys string
|
||||
sqlTableShares string
|
||||
sqlTableSharesGroupsMapping string
|
||||
sqlTableDefenderHosts string
|
||||
sqlTableDefenderEvents string
|
||||
sqlTableActiveTransfers string
|
||||
|
|
@ -243,6 +246,7 @@ func initSQLTables() {
|
|||
sqlTableAdmins = "admins"
|
||||
sqlTableAPIKeys = "api_keys"
|
||||
sqlTableShares = "shares"
|
||||
sqlTableSharesGroupsMapping = "shares_groups_mapping"
|
||||
sqlTableDefenderHosts = "defender_hosts"
|
||||
sqlTableDefenderEvents = "defender_events"
|
||||
sqlTableActiveTransfers = "active_transfers"
|
||||
|
|
@ -518,7 +522,7 @@ type Config struct {
|
|||
// GetShared returns the provider share mode.
|
||||
// This method is called before the provider is initialized
|
||||
func (c *Config) GetShared() int {
|
||||
if !util.Contains(sharedProviders, c.Driver) {
|
||||
if !slices.Contains(sharedProviders, c.Driver) {
|
||||
return 0
|
||||
}
|
||||
return c.IsShared
|
||||
|
|
@ -590,6 +594,16 @@ func (c *Config) doBackup() (string, error) {
|
|||
return outputFile, nil
|
||||
}
|
||||
|
||||
// SetTZ sets the configured timezone.
|
||||
func SetTZ(val string) {
|
||||
tz = val
|
||||
}
|
||||
|
||||
// UseLocalTime returns true if local time should be used instead of UTC.
|
||||
func UseLocalTime() bool {
|
||||
return tz == "local"
|
||||
}
|
||||
|
||||
// ExecuteBackup executes a backup
|
||||
func ExecuteBackup() (string, error) {
|
||||
return config.doBackup()
|
||||
|
|
@ -759,6 +773,8 @@ type Provider interface {
|
|||
updateLastLogin(username string) error
|
||||
updateAdminLastLogin(username string) error
|
||||
setUpdatedAt(username string)
|
||||
getAdminSignature(username string) (string, error)
|
||||
getUserSignature(username string) (string, error)
|
||||
getFolders(limit, offset int, order string, minimal bool) ([]vfs.BaseVirtualFolder, error)
|
||||
getFolderByName(name string) (vfs.BaseVirtualFolder, error)
|
||||
addFolder(folder *vfs.BaseVirtualFolder) error
|
||||
|
|
@ -810,8 +826,8 @@ type Provider interface {
|
|||
cleanupActiveTransfers(before time.Time) error
|
||||
getActiveTransfers(from time.Time) ([]ActiveTransfer, error)
|
||||
addSharedSession(session Session) error
|
||||
deleteSharedSession(key string) error
|
||||
getSharedSession(key string) (Session, error)
|
||||
deleteSharedSession(key string, sessionType SessionType) error
|
||||
getSharedSession(key string, sessionType SessionType) (Session, error)
|
||||
cleanupSharedSessions(sessionType SessionType, before int64) error
|
||||
getEventActions(limit, offset int, order string, minimal bool) ([]BaseEventAction, error)
|
||||
dumpEventActions() ([]BaseEventAction, error)
|
||||
|
|
@ -874,7 +890,7 @@ func SetTempPath(fsPath string) {
|
|||
}
|
||||
|
||||
func checkSharedMode() {
|
||||
if !util.Contains(sharedProviders, config.Driver) {
|
||||
if !slices.Contains(sharedProviders, config.Driver) {
|
||||
config.IsShared = 0
|
||||
}
|
||||
}
|
||||
|
|
@ -929,12 +945,13 @@ func checkDatabase(checkAdmins bool) error {
|
|||
if config.UpdateMode == 0 {
|
||||
err := provider.initializeDatabase()
|
||||
if err != nil && err != ErrNoInitRequired {
|
||||
logger.WarnToConsole("Unable to initialize data provider: %v", err)
|
||||
providerLog(logger.LevelError, "Unable to initialize data provider: %v", err)
|
||||
logger.WarnToConsole("unable to initialize data provider: %v", err)
|
||||
providerLog(logger.LevelError, "unable to initialize data provider: %v", err)
|
||||
return err
|
||||
}
|
||||
if err == nil {
|
||||
logger.DebugToConsole("Data provider successfully initialized")
|
||||
logger.DebugToConsole("data provider successfully initialized")
|
||||
providerLog(logger.LevelInfo, "data provider successfully initialized")
|
||||
}
|
||||
err = provider.migrateDatabase()
|
||||
if err != nil && err != ErrNoInitRequired {
|
||||
|
|
@ -1040,6 +1057,7 @@ func validateSQLTablesPrefix() error {
|
|||
sqlTableAdmins = config.SQLTablesPrefix + sqlTableAdmins
|
||||
sqlTableAPIKeys = config.SQLTablesPrefix + sqlTableAPIKeys
|
||||
sqlTableShares = config.SQLTablesPrefix + sqlTableShares
|
||||
sqlTableSharesGroupsMapping = config.SQLTablesPrefix + sqlTableSharesGroupsMapping
|
||||
sqlTableDefenderEvents = config.SQLTablesPrefix + sqlTableDefenderEvents
|
||||
sqlTableDefenderHosts = config.SQLTablesPrefix + sqlTableDefenderHosts
|
||||
sqlTableActiveTransfers = config.SQLTablesPrefix + sqlTableActiveTransfers
|
||||
|
|
@ -1061,12 +1079,12 @@ func validateSQLTablesPrefix() error {
|
|||
"api keys %q shares %q defender hosts %q defender events %q transfers %q groups %q "+
|
||||
"users groups mapping %q admins groups mapping %q groups folders mapping %q shared sessions %q "+
|
||||
"schema version %q events actions %q events rules %q rules actions mapping %q tasks %q nodes %q roles %q"+
|
||||
"ip lists %q configs %q",
|
||||
"ip lists %q share groups mapping %q configs %q",
|
||||
sqlTableUsers, sqlTableFolders, sqlTableUsersFoldersMapping, sqlTableAdmins, sqlTableAPIKeys,
|
||||
sqlTableShares, sqlTableDefenderHosts, sqlTableDefenderEvents, sqlTableActiveTransfers, sqlTableGroups,
|
||||
sqlTableUsersGroupsMapping, sqlTableAdminsGroupsMapping, sqlTableGroupsFoldersMapping, sqlTableSharedSessions,
|
||||
sqlTableSchemaVersion, sqlTableEventsActions, sqlTableEventsRules, sqlTableRulesActionsMapping,
|
||||
sqlTableTasks, sqlTableNodes, sqlTableRoles, sqlTableIPLists, sqlTableConfigs)
|
||||
sqlTableTasks, sqlTableNodes, sqlTableRoles, sqlTableIPLists, sqlTableSharesGroupsMapping, sqlTableConfigs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1503,6 +1521,15 @@ func UpdateUserQuota(user *User, filesAdd int, sizeAdd int64, reset bool) error
|
|||
return nil
|
||||
}
|
||||
|
||||
// UpdateUserFolderQuota updates the quota for the given user and virtual folder.
|
||||
func UpdateUserFolderQuota(folder *vfs.VirtualFolder, user *User, filesAdd int, sizeAdd int64, reset bool) {
|
||||
if folder.IsIncludedInUserQuota() {
|
||||
UpdateUserQuota(user, filesAdd, sizeAdd, reset) //nolint:errcheck
|
||||
return
|
||||
}
|
||||
UpdateVirtualFolderQuota(&folder.BaseVirtualFolder, filesAdd, sizeAdd, reset) //nolint:errcheck
|
||||
}
|
||||
|
||||
// UpdateVirtualFolderQuota updates the quota for the given virtual folder adding filesAdd and sizeAdd.
|
||||
// If reset is true filesAdd and sizeAdd indicates the total files and the total size instead of the difference.
|
||||
func UpdateVirtualFolderQuota(vfolder *vfs.BaseVirtualFolder, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
|
|
@ -1693,7 +1720,7 @@ func IPListEntryExists(ipOrNet string, listType IPListType) (IPListEntry, error)
|
|||
|
||||
// GetIPListEntries returns the IP list entries applying the specified criteria and search limit
|
||||
func GetIPListEntries(listType IPListType, filter, from, order string, limit int) ([]IPListEntry, error) {
|
||||
if !util.Contains(supportedIPListType, listType) {
|
||||
if !slices.Contains(supportedIPListType, listType) {
|
||||
return nil, util.NewValidationError(fmt.Sprintf("invalid list type %d", listType))
|
||||
}
|
||||
return provider.getIPListEntries(listType, filter, from, order, limit)
|
||||
|
|
@ -2064,6 +2091,20 @@ func UserExists(username, role string) (User, error) {
|
|||
return provider.userExists(username, role)
|
||||
}
|
||||
|
||||
// GetAdminSignature returns the signature for the admin with the specified
|
||||
// username.
|
||||
func GetAdminSignature(username string) (string, error) {
|
||||
username = config.convertName(username)
|
||||
return provider.getAdminSignature(username)
|
||||
}
|
||||
|
||||
// GetUserSignature returns the signature for the user with the specified
|
||||
// username.
|
||||
func GetUserSignature(username string) (string, error) {
|
||||
username = config.convertName(username)
|
||||
return provider.getUserSignature(username)
|
||||
}
|
||||
|
||||
// GetUserWithGroupSettings tries to return the user with the specified username
|
||||
// loading also the group settings
|
||||
func GetUserWithGroupSettings(username, role string) (User, error) {
|
||||
|
|
@ -2106,9 +2147,6 @@ func UpdateUserPassword(username, plainPwd, executor, ipAddress, role string) er
|
|||
return err
|
||||
}
|
||||
userCopy := user.getACopy()
|
||||
if err := userCopy.LoadAndApplyGroupSettings(); err != nil {
|
||||
return err
|
||||
}
|
||||
userCopy.Password = plainPwd
|
||||
if err := createUserPasswordHash(&userCopy); err != nil {
|
||||
return err
|
||||
|
|
@ -2206,8 +2244,8 @@ func AddSharedSession(session Session) error {
|
|||
}
|
||||
|
||||
// DeleteSharedSession deletes the session with the specified key
|
||||
func DeleteSharedSession(key string) error {
|
||||
err := provider.deleteSharedSession(key)
|
||||
func DeleteSharedSession(key string, sessionType SessionType) error {
|
||||
err := provider.deleteSharedSession(key, sessionType)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "unable to add shared session, key %q, err: %v", key, err)
|
||||
}
|
||||
|
|
@ -2215,8 +2253,8 @@ func DeleteSharedSession(key string) error {
|
|||
}
|
||||
|
||||
// GetSharedSession retrieves the session with the specified key
|
||||
func GetSharedSession(key string) (Session, error) {
|
||||
return provider.getSharedSession(key)
|
||||
func GetSharedSession(key string, sessionType SessionType) (Session, error) {
|
||||
return provider.getSharedSession(key, sessionType)
|
||||
}
|
||||
|
||||
// CleanupSharedSessions removes the shared session with the specified type and
|
||||
|
|
@ -2352,7 +2390,7 @@ func GetFolders(limit, offset int, order string, minimal bool) ([]vfs.BaseVirtua
|
|||
}
|
||||
|
||||
func dumpUsers(data *BackupData, scopes []string) error {
|
||||
if len(scopes) == 0 || util.Contains(scopes, DumpScopeUsers) {
|
||||
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeUsers) {
|
||||
users, err := provider.dumpUsers()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -2363,7 +2401,7 @@ func dumpUsers(data *BackupData, scopes []string) error {
|
|||
}
|
||||
|
||||
func dumpFolders(data *BackupData, scopes []string) error {
|
||||
if len(scopes) == 0 || util.Contains(scopes, DumpScopeFolders) {
|
||||
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeFolders) {
|
||||
folders, err := provider.dumpFolders()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -2374,7 +2412,7 @@ func dumpFolders(data *BackupData, scopes []string) error {
|
|||
}
|
||||
|
||||
func dumpGroups(data *BackupData, scopes []string) error {
|
||||
if len(scopes) == 0 || util.Contains(scopes, DumpScopeGroups) {
|
||||
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeGroups) {
|
||||
groups, err := provider.dumpGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -2385,7 +2423,7 @@ func dumpGroups(data *BackupData, scopes []string) error {
|
|||
}
|
||||
|
||||
func dumpAdmins(data *BackupData, scopes []string) error {
|
||||
if len(scopes) == 0 || util.Contains(scopes, DumpScopeAdmins) {
|
||||
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeAdmins) {
|
||||
admins, err := provider.dumpAdmins()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -2396,7 +2434,7 @@ func dumpAdmins(data *BackupData, scopes []string) error {
|
|||
}
|
||||
|
||||
func dumpAPIKeys(data *BackupData, scopes []string) error {
|
||||
if len(scopes) == 0 || util.Contains(scopes, DumpScopeAPIKeys) {
|
||||
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeAPIKeys) {
|
||||
apiKeys, err := provider.dumpAPIKeys()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -2407,7 +2445,7 @@ func dumpAPIKeys(data *BackupData, scopes []string) error {
|
|||
}
|
||||
|
||||
func dumpShares(data *BackupData, scopes []string) error {
|
||||
if len(scopes) == 0 || util.Contains(scopes, DumpScopeShares) {
|
||||
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeShares) {
|
||||
shares, err := provider.dumpShares()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -2418,7 +2456,7 @@ func dumpShares(data *BackupData, scopes []string) error {
|
|||
}
|
||||
|
||||
func dumpActions(data *BackupData, scopes []string) error {
|
||||
if len(scopes) == 0 || util.Contains(scopes, DumpScopeActions) {
|
||||
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeActions) {
|
||||
actions, err := provider.dumpEventActions()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -2429,7 +2467,7 @@ func dumpActions(data *BackupData, scopes []string) error {
|
|||
}
|
||||
|
||||
func dumpRules(data *BackupData, scopes []string) error {
|
||||
if len(scopes) == 0 || util.Contains(scopes, DumpScopeRules) {
|
||||
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeRules) {
|
||||
rules, err := provider.dumpEventRules()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -2440,7 +2478,7 @@ func dumpRules(data *BackupData, scopes []string) error {
|
|||
}
|
||||
|
||||
func dumpRoles(data *BackupData, scopes []string) error {
|
||||
if len(scopes) == 0 || util.Contains(scopes, DumpScopeRoles) {
|
||||
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeRoles) {
|
||||
roles, err := provider.dumpRoles()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -2451,7 +2489,7 @@ func dumpRoles(data *BackupData, scopes []string) error {
|
|||
}
|
||||
|
||||
func dumpIPLists(data *BackupData, scopes []string) error {
|
||||
if len(scopes) == 0 || util.Contains(scopes, DumpScopeIPLists) {
|
||||
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeIPLists) {
|
||||
ipLists, err := provider.dumpIPListEntries()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -2462,7 +2500,7 @@ func dumpIPLists(data *BackupData, scopes []string) error {
|
|||
}
|
||||
|
||||
func dumpConfigs(data *BackupData, scopes []string) error {
|
||||
if len(scopes) == 0 || util.Contains(scopes, DumpScopeConfigs) {
|
||||
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeConfigs) {
|
||||
configs, err := provider.getConfigs()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -2519,6 +2557,17 @@ func DumpData(scopes []string) (BackupData, error) {
|
|||
func ParseDumpData(data []byte) (BackupData, error) {
|
||||
var dump BackupData
|
||||
err := json.Unmarshal(data, &dump)
|
||||
if err != nil {
|
||||
return dump, err
|
||||
}
|
||||
if dump.Version < 17 {
|
||||
providerLog(logger.LevelInfo, "updating placeholders for actions restored from dump version %d", dump.Version)
|
||||
eventActions, err := updateEventActionPlaceholders(dump.EventActions)
|
||||
if err != nil {
|
||||
return dump, fmt.Errorf("unable to update event action placeholders for dump version %d: %w", dump.Version, err)
|
||||
}
|
||||
dump.EventActions = eventActions
|
||||
}
|
||||
return dump, err
|
||||
}
|
||||
|
||||
|
|
@ -2568,9 +2617,8 @@ func createProvider(basePath string) error {
|
|||
return initializeBoltProvider(basePath)
|
||||
case MemoryDataProviderName:
|
||||
if err := initializeMemoryProvider(basePath); err != nil {
|
||||
msg := fmt.Sprintf("provider initialized but data loading failed: %v", err)
|
||||
logger.Warn(logSender, "", msg)
|
||||
logger.WarnToConsole(msg)
|
||||
logger.Warn(logSender, "", "provider initialized but data loading failed: %v", err)
|
||||
logger.WarnToConsole("provider initialized but data loading failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
|
|
@ -2684,7 +2732,7 @@ func validateUserGroups(user *User) error {
|
|||
groupNames := make(map[string]bool)
|
||||
|
||||
for _, g := range user.Groups {
|
||||
if g.Type < sdk.GroupTypePrimary && g.Type > sdk.GroupTypeMembership {
|
||||
if g.Type < sdk.GroupTypePrimary || g.Type > sdk.GroupTypeMembership {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid group type: %v", g.Type))
|
||||
}
|
||||
if g.Type == sdk.GroupTypePrimary {
|
||||
|
|
@ -2715,6 +2763,7 @@ func validateAssociatedVirtualFolders(vfolders []vfs.VirtualFolder) ([]vfs.Virtu
|
|||
folderNames := make(map[string]bool)
|
||||
|
||||
for _, v := range vfolders {
|
||||
v.Name = config.convertName(v.Name)
|
||||
if v.VirtualPath == "" {
|
||||
return nil, util.NewI18nError(
|
||||
util.NewValidationError("mount/virtual path is mandatory"),
|
||||
|
|
@ -2766,7 +2815,7 @@ func validateUserTOTPConfig(c *UserTOTPConfig, username string) error {
|
|||
if c.ConfigName == "" {
|
||||
return util.NewValidationError("totp: config name is mandatory")
|
||||
}
|
||||
if !util.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {
|
||||
if !slices.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {
|
||||
return util.NewValidationError(fmt.Sprintf("totp: config name %q not found", c.ConfigName))
|
||||
}
|
||||
if c.Secret.IsEmpty() {
|
||||
|
|
@ -2782,7 +2831,7 @@ func validateUserTOTPConfig(c *UserTOTPConfig, username string) error {
|
|||
return util.NewValidationError("totp: specify at least one protocol")
|
||||
}
|
||||
for _, protocol := range c.Protocols {
|
||||
if !util.Contains(MFAProtocols, protocol) {
|
||||
if !slices.Contains(MFAProtocols, protocol) {
|
||||
return util.NewValidationError(fmt.Sprintf("totp: invalid protocol %q", protocol))
|
||||
}
|
||||
}
|
||||
|
|
@ -2815,7 +2864,7 @@ func validateUserPermissions(permsToCheck map[string][]string) (map[string][]str
|
|||
return permissions, util.NewValidationError("invalid permissions")
|
||||
}
|
||||
for _, p := range perms {
|
||||
if !util.Contains(ValidPerms, p) {
|
||||
if !slices.Contains(ValidPerms, p) {
|
||||
return permissions, util.NewValidationError(fmt.Sprintf("invalid permission: %q", p))
|
||||
}
|
||||
}
|
||||
|
|
@ -2829,7 +2878,7 @@ func validateUserPermissions(permsToCheck map[string][]string) (map[string][]str
|
|||
if dir != cleanedDir && cleanedDir == "/" {
|
||||
return permissions, util.NewValidationError(fmt.Sprintf("cannot set permissions for invalid subdirectory: %q is an alias for \"/\"", dir))
|
||||
}
|
||||
if util.Contains(perms, PermAny) {
|
||||
if slices.Contains(perms, PermAny) {
|
||||
permissions[cleanedDir] = []string{PermAny}
|
||||
} else {
|
||||
permissions[cleanedDir] = util.RemoveDuplicates(perms, false)
|
||||
|
|
@ -2870,11 +2919,18 @@ func validatePublicKeys(user *User) error {
|
|||
util.I18nErrorPubKeyInvalid,
|
||||
)
|
||||
}
|
||||
if out.Type() == ssh.InsecureKeyAlgoDSA { //nolint:staticcheck
|
||||
providerLog(logger.LevelError, "dsa public key not accepted, position: %d", idx)
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("DSA key format is insecure and it is not allowed for key at position %d", idx)),
|
||||
util.I18nErrorKeyInsecure,
|
||||
)
|
||||
}
|
||||
if k, ok := out.(ssh.CryptoPublicKey); ok {
|
||||
cryptoKey := k.CryptoPublicKey()
|
||||
if rsaKey, ok := cryptoKey.(*rsa.PublicKey); ok {
|
||||
if size := rsaKey.N.BitLen(); size < 2048 {
|
||||
providerLog(logger.LevelError, "rsa key with size %d not accepted, minimum 2048", size)
|
||||
providerLog(logger.LevelError, "rsa key with size %d at position %d not accepted, minimum 2048", size, idx)
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("invalid size %d for rsa key at position %d, minimum 2048",
|
||||
size, idx)),
|
||||
|
|
@ -2905,7 +2961,7 @@ func validateFiltersPatternExtensions(baseFilters *sdk.BaseUserFilters) error {
|
|||
util.I18nErrorFilePatternPathInvalid,
|
||||
)
|
||||
}
|
||||
if util.Contains(filteredPaths, cleanedPath) {
|
||||
if slices.Contains(filteredPaths, cleanedPath) {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("duplicate file patterns filter for path %q", f.Path)),
|
||||
util.I18nErrorFilePatternDuplicated,
|
||||
|
|
@ -3024,13 +3080,13 @@ func validateFilterProtocols(filters *sdk.BaseUserFilters) error {
|
|||
return util.NewValidationError("invalid denied_protocols")
|
||||
}
|
||||
for _, p := range filters.DeniedProtocols {
|
||||
if !util.Contains(ValidProtocols, p) {
|
||||
if !slices.Contains(ValidProtocols, p) {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid denied protocol %q", p))
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range filters.TwoFactorAuthProtocols {
|
||||
if !util.Contains(MFAProtocols, p) {
|
||||
if !slices.Contains(MFAProtocols, p) {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid two factor protocol %q", p))
|
||||
}
|
||||
}
|
||||
|
|
@ -3086,7 +3142,7 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
|
|||
return util.NewValidationError("invalid denied_login_methods")
|
||||
}
|
||||
for _, loginMethod := range filters.DeniedLoginMethods {
|
||||
if !util.Contains(ValidLoginMethods, loginMethod) {
|
||||
if !slices.Contains(ValidLoginMethods, loginMethod) {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid login method: %q", loginMethod))
|
||||
}
|
||||
}
|
||||
|
|
@ -3094,7 +3150,7 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
|
|||
return err
|
||||
}
|
||||
if filters.TLSUsername != "" {
|
||||
if !util.Contains(validTLSUsernames, string(filters.TLSUsername)) {
|
||||
if !slices.Contains(validTLSUsernames, string(filters.TLSUsername)) {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid TLS username: %q", filters.TLSUsername))
|
||||
}
|
||||
}
|
||||
|
|
@ -3104,7 +3160,7 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
|
|||
}
|
||||
filters.TLSCerts = certs
|
||||
for _, opts := range filters.WebClient {
|
||||
if !util.Contains(sdk.WebClientOptions, opts) {
|
||||
if !slices.Contains(sdk.WebClientOptions, opts) {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid web client options %q", opts))
|
||||
}
|
||||
}
|
||||
|
|
@ -3172,19 +3228,19 @@ func validateAccessTimeFilters(filters *sdk.BaseUserFilters) error {
|
|||
}
|
||||
|
||||
func validateCombinedUserFilters(user *User) error {
|
||||
if user.Filters.TOTPConfig.Enabled && util.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
|
||||
if user.Filters.TOTPConfig.Enabled && slices.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("two-factor authentication cannot be disabled for a user with an active configuration"),
|
||||
util.I18nErrorDisableActive2FA,
|
||||
)
|
||||
}
|
||||
if user.Filters.RequirePasswordChange && util.Contains(user.Filters.WebClient, sdk.WebClientPasswordChangeDisabled) {
|
||||
if user.Filters.RequirePasswordChange && slices.Contains(user.Filters.WebClient, sdk.WebClientPasswordChangeDisabled) {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("you cannot require password change and at the same time disallow it"),
|
||||
util.I18nErrorPwdChangeConflict,
|
||||
)
|
||||
}
|
||||
if len(user.Filters.TwoFactorAuthProtocols) > 0 && util.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
|
||||
if len(user.Filters.TwoFactorAuthProtocols) > 0 && slices.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("you cannot require two-factor authentication and at the same time disallow it"),
|
||||
util.I18nError2FAConflict,
|
||||
|
|
@ -3193,19 +3249,37 @@ func validateCombinedUserFilters(user *User) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func validateBaseParams(user *User) error {
|
||||
if user.Username == "" {
|
||||
return util.NewI18nError(util.NewValidationError("username is mandatory"), util.I18nErrorUsernameRequired)
|
||||
}
|
||||
if err := checkReservedUsernames(user.Username); err != nil {
|
||||
return util.NewI18nError(err, util.I18nErrorReservedUsername)
|
||||
}
|
||||
func validateEmails(user *User) error {
|
||||
if user.Email != "" && !util.IsEmailValid(user.Email) {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("email %q is not valid", user.Email)),
|
||||
util.I18nErrorInvalidEmail,
|
||||
)
|
||||
}
|
||||
for _, email := range user.Filters.AdditionalEmails {
|
||||
if !util.IsEmailValid(email) {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("email %q is not valid", email)),
|
||||
util.I18nErrorInvalidEmail,
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateBaseParams(user *User) error {
|
||||
if user.Username == "" {
|
||||
return util.NewI18nError(util.NewValidationError("username is mandatory"), util.I18nErrorUsernameRequired)
|
||||
}
|
||||
if !util.IsNameValid(user.Username) {
|
||||
return util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)
|
||||
}
|
||||
if err := checkReservedUsernames(user.Username); err != nil {
|
||||
return util.NewI18nError(err, util.I18nErrorReservedUsername)
|
||||
}
|
||||
if err := validateEmails(user); err != nil {
|
||||
return err
|
||||
}
|
||||
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(user.Username) {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("username %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", user.Username)),
|
||||
|
|
@ -3266,6 +3340,19 @@ func hashPlainPassword(plainPwd string) (string, error) {
|
|||
|
||||
func createUserPasswordHash(user *User) error {
|
||||
if user.Password != "" && !user.IsPasswordHashed() {
|
||||
for _, g := range user.Groups {
|
||||
if g.Type == sdk.GroupTypePrimary {
|
||||
group, err := GroupExists(g.Name)
|
||||
if err != nil {
|
||||
return errors.New("unable to load group password policies")
|
||||
}
|
||||
if minEntropy := group.UserSettings.Filters.PasswordStrength; minEntropy > 0 {
|
||||
if err := passwordvalidator.Validate(user.Password, float64(minEntropy)); err != nil {
|
||||
return util.NewI18nError(util.NewValidationError(err.Error()), util.I18nErrorPasswordComplexity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if minEntropy := user.getMinPasswordEntropy(); minEntropy > 0 {
|
||||
if err := passwordvalidator.Validate(user.Password, minEntropy); err != nil {
|
||||
return util.NewI18nError(util.NewValidationError(err.Error()), util.I18nErrorPasswordComplexity)
|
||||
|
|
@ -3288,6 +3375,9 @@ func ValidateFolder(folder *vfs.BaseVirtualFolder) error {
|
|||
if folder.Name == "" {
|
||||
return util.NewI18nError(util.NewValidationError("folder name is mandatory"), util.I18nErrorNameRequired)
|
||||
}
|
||||
if !util.IsNameValid(folder.Name) {
|
||||
return util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)
|
||||
}
|
||||
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(folder.Name) {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("folder name %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", folder.Name)),
|
||||
|
|
@ -3317,6 +3407,7 @@ func ValidateUser(user *User) error {
|
|||
user.OIDCCustomFields = nil
|
||||
user.HasPassword = false
|
||||
user.SetEmptySecretsIfNil()
|
||||
user.applyNamingRules()
|
||||
buildUserHomeDir(user)
|
||||
if err := validateBaseParams(user); err != nil {
|
||||
return err
|
||||
|
|
@ -3466,7 +3557,7 @@ func checkUserAndPass(user *User, password, ip, protocol string) (User, error) {
|
|||
if err != nil {
|
||||
return *user, ErrInvalidCredentials
|
||||
}
|
||||
if user.Password == "" || password == "" {
|
||||
if user.Password == "" || strings.TrimSpace(password) == "" {
|
||||
return *user, errors.New("credentials cannot be null or empty")
|
||||
}
|
||||
if !user.Filters.Hooks.CheckPasswordDisabled {
|
||||
|
|
@ -3505,7 +3596,7 @@ func checkUserPasscode(user *User, password, protocol string) (string, error) {
|
|||
if user.Filters.TOTPConfig.Enabled {
|
||||
switch protocol {
|
||||
case protocolFTP:
|
||||
if util.Contains(user.Filters.TOTPConfig.Protocols, protocol) {
|
||||
if slices.Contains(user.Filters.TOTPConfig.Protocols, protocol) {
|
||||
// the TOTP passcode has six digits
|
||||
pwdLen := len(password)
|
||||
if pwdLen < 7 {
|
||||
|
|
@ -3641,7 +3732,8 @@ func comparePbkdf2PasswordAndHash(password, hashedPassword string) (bool, error)
|
|||
}
|
||||
|
||||
func getSSLMode() string {
|
||||
if config.Driver == PGSQLDataProviderName || config.Driver == CockroachDataProviderName {
|
||||
switch config.Driver {
|
||||
case PGSQLDataProviderName, CockroachDataProviderName:
|
||||
switch config.SSLMode {
|
||||
case 0:
|
||||
return "disable"
|
||||
|
|
@ -3656,7 +3748,7 @@ func getSSLMode() string {
|
|||
case 5:
|
||||
return "allow"
|
||||
}
|
||||
} else if config.Driver == MySQLDataProviderName {
|
||||
case MySQLDataProviderName:
|
||||
if config.requireCustomTLSForMySQL() {
|
||||
return "custom"
|
||||
}
|
||||
|
|
@ -3711,7 +3803,7 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
|
|||
if err := user.LoadAndApplyGroupSettings(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
hasSecondFactor := user.Filters.TOTPConfig.Enabled && util.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH)
|
||||
hasSecondFactor := user.Filters.TOTPConfig.Enabled && slices.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH)
|
||||
if !isPartialAuth || !hasSecondFactor {
|
||||
answers, err := client("", "", []string{"Password: "}, []bool{false})
|
||||
if err != nil {
|
||||
|
|
@ -3729,7 +3821,7 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
|
|||
}
|
||||
|
||||
func checkKeyboardInteractiveSecondFactor(user *User, client ssh.KeyboardInteractiveChallenge, protocol string) (int, error) {
|
||||
if !user.Filters.TOTPConfig.Enabled || !util.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
|
||||
if !user.Filters.TOTPConfig.Enabled || !slices.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
|
||||
return 1, nil
|
||||
}
|
||||
err := user.Filters.TOTPConfig.Secret.TryDecrypt()
|
||||
|
|
@ -3853,7 +3945,7 @@ func getKeyboardInteractiveAnswers(client ssh.KeyboardInteractiveChallenge, resp
|
|||
}
|
||||
if len(answers) == 1 && response.CheckPwd > 0 {
|
||||
if response.CheckPwd == 2 {
|
||||
if !user.Filters.TOTPConfig.Enabled || !util.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
|
||||
if !user.Filters.TOTPConfig.Enabled || !slices.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
|
||||
providerLog(logger.LevelInfo, "keyboard interactive auth error: unable to check TOTP passcode, TOTP is not enabled for user %q",
|
||||
user.Username)
|
||||
return answers, errors.New("TOTP not enabled for SSH protocol")
|
||||
|
|
@ -4059,7 +4151,7 @@ func getPasswordHookResponse(username, password, ip, protocol string) ([]byte, e
|
|||
fmt.Sprintf("SFTPGO_AUTHD_IP=%s", ip),
|
||||
fmt.Sprintf("SFTPGO_AUTHD_PROTOCOL=%s", protocol),
|
||||
)
|
||||
return cmd.Output()
|
||||
return getCmdOutput(cmd, "check_password_hook")
|
||||
}
|
||||
|
||||
func executeCheckPasswordHook(username, password, ip, protocol string) (checkPasswordResponse, error) {
|
||||
|
|
@ -4115,15 +4207,17 @@ func getPreLoginHookResponse(loginMethod, ip, protocol string, userAsJSON []byte
|
|||
|
||||
cmd := exec.CommandContext(ctx, config.PreLoginHook, args...)
|
||||
cmd.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_LOGIND_USER=%s", util.BytesToString(userAsJSON)),
|
||||
fmt.Sprintf("SFTPGO_LOGIND_USER=%s", userAsJSON),
|
||||
fmt.Sprintf("SFTPGO_LOGIND_METHOD=%s", loginMethod),
|
||||
fmt.Sprintf("SFTPGO_LOGIND_IP=%s", ip),
|
||||
fmt.Sprintf("SFTPGO_LOGIND_PROTOCOL=%s", protocol),
|
||||
)
|
||||
return cmd.Output()
|
||||
return getCmdOutput(cmd, "pre_login_hook")
|
||||
}
|
||||
|
||||
func executePreLoginHook(username, loginMethod, ip, protocol string, oidcTokenFields *map[string]any) (User, error) {
|
||||
var user User
|
||||
|
||||
u, mergedUser, userAsJSON, err := getUserAndJSONForHook(username, oidcTokenFields)
|
||||
if err != nil {
|
||||
return u, err
|
||||
|
|
@ -4146,55 +4240,41 @@ func executePreLoginHook(username, loginMethod, ip, protocol string, oidcTokenFi
|
|||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
userID := u.ID
|
||||
userUsedQuotaSize := u.UsedQuotaSize
|
||||
userUsedQuotaFiles := u.UsedQuotaFiles
|
||||
userUsedDownloadTransfer := u.UsedDownloadDataTransfer
|
||||
userUsedUploadTransfer := u.UsedUploadDataTransfer
|
||||
userLastQuotaUpdate := u.LastQuotaUpdate
|
||||
userLastLogin := u.LastLogin
|
||||
userFirstDownload := u.FirstDownload
|
||||
userFirstUpload := u.FirstUpload
|
||||
userLastPwdChange := u.LastPasswordChange
|
||||
userCreatedAt := u.CreatedAt
|
||||
totpConfig := u.Filters.TOTPConfig
|
||||
recoveryCodes := u.Filters.RecoveryCodes
|
||||
err = json.Unmarshal(out, &u)
|
||||
err = json.Unmarshal(out, &user)
|
||||
if err != nil {
|
||||
return u, fmt.Errorf("invalid pre-login hook response %q, error: %v", util.BytesToString(out), err)
|
||||
return u, fmt.Errorf("invalid pre-login hook response %q, error: %v", out, err)
|
||||
}
|
||||
u.ID = userID
|
||||
u.UsedQuotaSize = userUsedQuotaSize
|
||||
u.UsedQuotaFiles = userUsedQuotaFiles
|
||||
u.UsedUploadDataTransfer = userUsedUploadTransfer
|
||||
u.UsedDownloadDataTransfer = userUsedDownloadTransfer
|
||||
u.LastQuotaUpdate = userLastQuotaUpdate
|
||||
u.LastLogin = userLastLogin
|
||||
u.LastPasswordChange = userLastPwdChange
|
||||
u.FirstDownload = userFirstDownload
|
||||
u.FirstUpload = userFirstUpload
|
||||
u.CreatedAt = userCreatedAt
|
||||
if userID == 0 {
|
||||
err = provider.addUser(&u)
|
||||
} else {
|
||||
u.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
if u.ID > 0 {
|
||||
user.ID = u.ID
|
||||
user.UsedQuotaSize = u.UsedQuotaSize
|
||||
user.UsedQuotaFiles = u.UsedQuotaFiles
|
||||
user.UsedUploadDataTransfer = u.UsedUploadDataTransfer
|
||||
user.UsedDownloadDataTransfer = u.UsedDownloadDataTransfer
|
||||
user.LastQuotaUpdate = u.LastQuotaUpdate
|
||||
user.LastLogin = u.LastLogin
|
||||
user.LastPasswordChange = u.LastPasswordChange
|
||||
user.FirstDownload = u.FirstDownload
|
||||
user.FirstUpload = u.FirstUpload
|
||||
// preserve TOTP config and recovery codes
|
||||
u.Filters.TOTPConfig = totpConfig
|
||||
u.Filters.RecoveryCodes = recoveryCodes
|
||||
err = provider.updateUser(&u)
|
||||
if err == nil {
|
||||
webDAVUsersCache.swap(&u, "")
|
||||
user.Filters.TOTPConfig = u.Filters.TOTPConfig
|
||||
user.Filters.RecoveryCodes = u.Filters.RecoveryCodes
|
||||
if err := provider.updateUser(&user); err != nil {
|
||||
return u, err
|
||||
}
|
||||
} else {
|
||||
if err := provider.addUser(&user); err != nil {
|
||||
return u, err
|
||||
}
|
||||
}
|
||||
user, err = provider.userExists(user.Username, "")
|
||||
if err != nil {
|
||||
return u, err
|
||||
}
|
||||
providerLog(logger.LevelDebug, "user %q added/updated from pre-login hook response, id: %d", username, userID)
|
||||
if userID == 0 {
|
||||
return provider.userExists(username, "")
|
||||
providerLog(logger.LevelDebug, "user %q added/updated from pre-login hook response, id: %d", username, u.ID)
|
||||
if u.ID > 0 {
|
||||
webDAVUsersCache.swap(&user, "")
|
||||
}
|
||||
return u, nil
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// ExecutePostLoginHook executes the post login hook if defined
|
||||
|
|
@ -4257,7 +4337,7 @@ func ExecutePostLoginHook(user *User, loginMethod, ip, protocol string, err erro
|
|||
|
||||
cmd := exec.CommandContext(ctx, config.PostLoginHook, args...)
|
||||
cmd.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_LOGIND_USER=%s", util.BytesToString(userAsJSON)),
|
||||
fmt.Sprintf("SFTPGO_LOGIND_USER=%s", userAsJSON),
|
||||
fmt.Sprintf("SFTPGO_LOGIND_IP=%s", ip),
|
||||
fmt.Sprintf("SFTPGO_LOGIND_METHOD=%s", loginMethod),
|
||||
fmt.Sprintf("SFTPGO_LOGIND_STATUS=%s", status),
|
||||
|
|
@ -4326,7 +4406,7 @@ func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip,
|
|||
cmd := exec.CommandContext(ctx, config.ExternalAuthHook, args...)
|
||||
cmd.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%s", username),
|
||||
fmt.Sprintf("SFTPGO_AUTHD_USER=%s", util.BytesToString(userAsJSON)),
|
||||
fmt.Sprintf("SFTPGO_AUTHD_USER=%s", userAsJSON),
|
||||
fmt.Sprintf("SFTPGO_AUTHD_IP=%s", ip),
|
||||
fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%s", password),
|
||||
fmt.Sprintf("SFTPGO_AUTHD_PUBLIC_KEY=%s", pkey),
|
||||
|
|
@ -4334,7 +4414,7 @@ func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip,
|
|||
fmt.Sprintf("SFTPGO_AUTHD_TLS_CERT=%s", strings.ReplaceAll(tlsCert, "\n", "\\n")),
|
||||
fmt.Sprintf("SFTPGO_AUTHD_KEYBOARD_INTERACTIVE=%v", keyboardInteractive))
|
||||
|
||||
return cmd.Output()
|
||||
return getCmdOutput(cmd, "external_auth_hook")
|
||||
}
|
||||
|
||||
func updateUserFromExtAuthResponse(user *User, password, pkey string) {
|
||||
|
|
@ -4442,7 +4522,7 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv
|
|||
// preserve TOTP config and recovery codes
|
||||
user.Filters.TOTPConfig = u.Filters.TOTPConfig
|
||||
user.Filters.RecoveryCodes = u.Filters.RecoveryCodes
|
||||
err = provider.updateUser(&user)
|
||||
user, err = updateUserAfterExternalAuth(&user)
|
||||
if err == nil {
|
||||
if protocol != protocolWebDAV {
|
||||
webDAVUsersCache.swap(&user, password)
|
||||
|
|
@ -4514,7 +4594,7 @@ func doPluginAuth(username, password string, pubKey []byte, ip, protocol string,
|
|||
// preserve TOTP config and recovery codes
|
||||
user.Filters.TOTPConfig = u.Filters.TOTPConfig
|
||||
user.Filters.RecoveryCodes = u.Filters.RecoveryCodes
|
||||
err = provider.updateUser(&user)
|
||||
user, err = updateUserAfterExternalAuth(&user)
|
||||
if err == nil {
|
||||
if protocol != protocolWebDAV {
|
||||
webDAVUsersCache.swap(&user, password)
|
||||
|
|
@ -4530,6 +4610,13 @@ func doPluginAuth(username, password string, pubKey []byte, ip, protocol string,
|
|||
return provider.userExists(user.Username, "")
|
||||
}
|
||||
|
||||
func updateUserAfterExternalAuth(user *User) (User, error) {
|
||||
if err := provider.updateUser(user); err != nil {
|
||||
return *user, err
|
||||
}
|
||||
return provider.userExists(user.Username, "")
|
||||
}
|
||||
|
||||
func getUserForHook(username string, oidcTokenFields *map[string]any) (User, User, error) {
|
||||
u, err := provider.userExists(username, "")
|
||||
if err != nil {
|
||||
|
|
@ -4601,6 +4688,59 @@ func isExternalAuthConfigured(loginMethod string) bool {
|
|||
}
|
||||
}
|
||||
|
||||
func replaceTemplateVars(input string) string {
|
||||
var result strings.Builder
|
||||
i := 0
|
||||
for i < len(input) {
|
||||
if i+2 <= len(input) && input[i:i+2] == "{{" {
|
||||
if i+2 < len(input) {
|
||||
nextChar := input[i+2]
|
||||
if nextChar == ' ' || nextChar == '.' || nextChar == '-' {
|
||||
// Don't replace if followed by space, dot or minus.
|
||||
result.WriteString("{{")
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Find the closing "}}"
|
||||
closing := strings.Index(input[i:], "}}")
|
||||
if closing != -1 {
|
||||
// Replace with {{. only if it's a proper template variable.
|
||||
result.WriteString("{{.")
|
||||
result.WriteString(input[i+2 : i+closing])
|
||||
result.WriteString("}}")
|
||||
i += closing + 2
|
||||
continue
|
||||
}
|
||||
}
|
||||
result.WriteByte(input[i])
|
||||
i++
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func updateEventActionPlaceholders(actions []BaseEventAction) ([]BaseEventAction, error) {
|
||||
var result []BaseEventAction
|
||||
|
||||
for _, action := range actions {
|
||||
options, err := json.Marshal(action.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
convertedOptions := replaceTemplateVars(string(options))
|
||||
var opts BaseEventActionOptions
|
||||
err = json.Unmarshal([]byte(convertedOptions), &opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
action.Options = opts
|
||||
result = append(result, action)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getConfigPath(name, configDir string) string {
|
||||
if !util.IsFileInputValid(name) {
|
||||
return ""
|
||||
|
|
@ -4612,12 +4752,47 @@ func getConfigPath(name, configDir string) string {
|
|||
}
|
||||
|
||||
func checkReservedUsernames(username string) error {
|
||||
if util.Contains(reservedUsers, username) {
|
||||
if slices.Contains(reservedUsers, username) {
|
||||
return util.NewValidationError("this username is reserved")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func errSchemaVersionTooOld(version int) error {
|
||||
return fmt.Errorf("database schema version %d is too old, please see the upgrading docs: https://docs.sftpgo.com/latest/data-provider/#upgrading", version)
|
||||
}
|
||||
|
||||
func getCmdOutput(cmd *exec.Cmd, sender string) ([]byte, error) {
|
||||
var stdout bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(stderr)
|
||||
|
||||
go func() {
|
||||
for scanner.Scan() {
|
||||
if out := scanner.Text(); out != "" {
|
||||
logger.Log(logger.LevelWarn, sender, "", "%s", out)
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
logger.Log(logger.LevelError, sender, "", "error reading stderr: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = cmd.Wait()
|
||||
return stdout.Bytes(), err
|
||||
}
|
||||
|
||||
func providerLog(level logger.LogLevel, format string, v ...any) {
|
||||
logger.Log(level, logSender, "", format, v...)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import (
|
|||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -49,17 +50,21 @@ const (
|
|||
ActionTypeUserExpirationCheck
|
||||
ActionTypeIDPAccountCheck
|
||||
ActionTypeUserInactivityCheck
|
||||
ActionTypeRotateLogs
|
||||
)
|
||||
|
||||
var (
|
||||
supportedEventActions = []int{ActionTypeHTTP, ActionTypeCommand, ActionTypeEmail, ActionTypeFilesystem,
|
||||
ActionTypeBackup, ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,
|
||||
ActionTypeDataRetentionCheck, ActionTypePasswordExpirationCheck,
|
||||
ActionTypeUserExpirationCheck, ActionTypeUserInactivityCheck, ActionTypeIDPAccountCheck}
|
||||
ActionTypeDataRetentionCheck, ActionTypePasswordExpirationCheck, ActionTypeUserExpirationCheck,
|
||||
ActionTypeUserInactivityCheck, ActionTypeIDPAccountCheck, ActionTypeRotateLogs}
|
||||
// EnabledActionCommands defines the system commands that can be executed via EventManager,
|
||||
// an empty list means that no command is allowed to be executed.
|
||||
EnabledActionCommands []string
|
||||
)
|
||||
|
||||
func isActionTypeValid(action int) bool {
|
||||
return util.Contains(supportedEventActions, action)
|
||||
return slices.Contains(supportedEventActions, action)
|
||||
}
|
||||
|
||||
func getActionTypeAsString(action int) string {
|
||||
|
|
@ -88,6 +93,8 @@ func getActionTypeAsString(action int) string {
|
|||
return util.I18nActionTypeUserInactivityCheck
|
||||
case ActionTypeIDPAccountCheck:
|
||||
return util.I18nActionTypeIDPCheck
|
||||
case ActionTypeRotateLogs:
|
||||
return util.I18nActionTypeRotateLogs
|
||||
default:
|
||||
return util.I18nActionTypeCommand
|
||||
}
|
||||
|
|
@ -112,7 +119,7 @@ var (
|
|||
)
|
||||
|
||||
func isEventTriggerValid(trigger int) bool {
|
||||
return util.Contains(supportedEventTriggers, trigger)
|
||||
return slices.Contains(supportedEventTriggers, trigger)
|
||||
}
|
||||
|
||||
func getTriggerTypeAsString(trigger int) string {
|
||||
|
|
@ -166,7 +173,7 @@ var (
|
|||
)
|
||||
|
||||
func isFilesystemActionValid(value int) bool {
|
||||
return util.Contains(supportedFsActions, value)
|
||||
return slices.Contains(supportedFsActions, value)
|
||||
}
|
||||
|
||||
func getFsActionTypeAsString(value int) string {
|
||||
|
|
@ -335,7 +342,7 @@ func (c *EventActionHTTPConfig) validateMultiparts() error {
|
|||
)
|
||||
}
|
||||
for _, k := range c.Headers {
|
||||
if strings.ToLower(k.Key) == "content-type" {
|
||||
if strings.EqualFold(k.Key, "content-type") {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("content type is automatically set for multipart requests"),
|
||||
util.I18nErrorMultipartCType,
|
||||
|
|
@ -377,7 +384,7 @@ func (c *EventActionHTTPConfig) validate(additionalData string) error {
|
|||
return util.NewValidationError(fmt.Sprintf("could not encrypt HTTP password: %v", err))
|
||||
}
|
||||
}
|
||||
if !util.Contains(SupportedHTTPActionMethods, c.Method) {
|
||||
if !slices.Contains(SupportedHTTPActionMethods, c.Method) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported HTTP method: %s", c.Method))
|
||||
}
|
||||
for _, kv := range c.QueryParameters {
|
||||
|
|
@ -398,11 +405,11 @@ func (c *EventActionHTTPConfig) GetContext() (context.Context, context.CancelFun
|
|||
|
||||
// HasObjectData returns true if the {{ObjectData}} placeholder is defined
|
||||
func (c *EventActionHTTPConfig) HasObjectData() bool {
|
||||
if strings.Contains(c.Body, "{{ObjectData}}") {
|
||||
if strings.Contains(c.Body, "{{ObjectData}}") || strings.Contains(c.Body, "{{ObjectDataString}}") {
|
||||
return true
|
||||
}
|
||||
for _, part := range c.Parts {
|
||||
if strings.Contains(part.Body, "{{ObjectData}}") {
|
||||
if strings.Contains(part.Body, "{{ObjectData}}") || strings.Contains(part.Body, "{{ObjectDataString}}") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -446,6 +453,11 @@ func (c *EventActionHTTPConfig) GetHTTPClient() *http.Client {
|
|||
return client
|
||||
}
|
||||
|
||||
// IsActionCommandAllowed returns true if the specified command is allowed
|
||||
func IsActionCommandAllowed(cmd string) bool {
|
||||
return slices.Contains(EnabledActionCommands, cmd)
|
||||
}
|
||||
|
||||
// EventActionCommandConfig defines the configuration for a command event target
|
||||
type EventActionCommandConfig struct {
|
||||
Cmd string `json:"cmd,omitempty"`
|
||||
|
|
@ -458,6 +470,9 @@ func (c *EventActionCommandConfig) validate() error {
|
|||
if c.Cmd == "" {
|
||||
return util.NewI18nError(util.NewValidationError("command is required"), util.I18nErrorCommandRequired)
|
||||
}
|
||||
if !IsActionCommandAllowed(c.Cmd) {
|
||||
return util.NewValidationError(fmt.Sprintf("command %q is not allowed", c.Cmd))
|
||||
}
|
||||
if !filepath.IsAbs(c.Cmd) {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("invalid command, it must be an absolute path"),
|
||||
|
|
@ -656,12 +671,21 @@ func (c *EventActionFsCompress) validate() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// RenameConfig defines the configuration for a filesystem rename
|
||||
type RenameConfig struct {
|
||||
// key is the source and target the value
|
||||
KeyValue
|
||||
// This setting only applies to storage providers that support
|
||||
// changing modification times.
|
||||
UpdateModTime bool `json:"update_modtime,omitempty"`
|
||||
}
|
||||
|
||||
// EventActionFilesystemConfig defines the configuration for filesystem actions
|
||||
type EventActionFilesystemConfig struct {
|
||||
// Filesystem actions, see the above enum
|
||||
Type int `json:"type,omitempty"`
|
||||
// files/dirs to rename, key is the source and target the value
|
||||
Renames []KeyValue `json:"renames,omitempty"`
|
||||
// files/dirs to rename
|
||||
Renames []RenameConfig `json:"renames,omitempty"`
|
||||
// directories to create
|
||||
MkDirs []string `json:"mkdirs,omitempty"`
|
||||
// files/dirs to delete
|
||||
|
|
@ -702,9 +726,9 @@ func (c *EventActionFilesystemConfig) validateRenames() error {
|
|||
if len(c.Renames) == 0 {
|
||||
return util.NewI18nError(util.NewValidationError("no path to rename specified"), util.I18nErrorPathRequired)
|
||||
}
|
||||
for idx, kv := range c.Renames {
|
||||
key := strings.TrimSpace(kv.Key)
|
||||
value := strings.TrimSpace(kv.Value)
|
||||
for idx, cfg := range c.Renames {
|
||||
key := strings.TrimSpace(cfg.Key)
|
||||
value := strings.TrimSpace(cfg.Value)
|
||||
if key == "" || value == "" {
|
||||
return util.NewValidationError("invalid paths to rename")
|
||||
}
|
||||
|
|
@ -722,9 +746,12 @@ func (c *EventActionFilesystemConfig) validateRenames() error {
|
|||
util.I18nErrorRootNotAllowed,
|
||||
)
|
||||
}
|
||||
c.Renames[idx] = KeyValue{
|
||||
Key: key,
|
||||
Value: value,
|
||||
c.Renames[idx] = RenameConfig{
|
||||
KeyValue: KeyValue{
|
||||
Key: key,
|
||||
Value: value,
|
||||
},
|
||||
UpdateModTime: cfg.UpdateModTime,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
@ -888,7 +915,7 @@ func (c *EventActionFilesystemConfig) getACopy() EventActionFilesystemConfig {
|
|||
|
||||
return EventActionFilesystemConfig{
|
||||
Type: c.Type,
|
||||
Renames: cloneKeyValues(c.Renames),
|
||||
Renames: cloneRenameConfigs(c.Renames),
|
||||
MkDirs: mkdirs,
|
||||
Deletes: deletes,
|
||||
Exist: exist,
|
||||
|
|
@ -1226,6 +1253,15 @@ func (a *BaseEventAction) validate() error {
|
|||
if a.Name == "" {
|
||||
return util.NewI18nError(util.NewValidationError("name is mandatory"), util.I18nErrorNameRequired)
|
||||
}
|
||||
if !util.IsNameValid(a.Name) {
|
||||
return util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)
|
||||
}
|
||||
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(a.Name) {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("name %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", a.Name)),
|
||||
util.I18nErrorInvalidUser,
|
||||
)
|
||||
}
|
||||
if !isActionTypeValid(a.Type) {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid action type: %d", a.Type))
|
||||
}
|
||||
|
|
@ -1277,7 +1313,7 @@ func (a *EventAction) validateAssociation(trigger int, fsEvents []string) error
|
|||
}
|
||||
if trigger == EventTriggerFsEvent {
|
||||
for _, ev := range fsEvents {
|
||||
if !util.Contains(allowedSyncFsEvents, ev) {
|
||||
if !slices.Contains(allowedSyncFsEvents, ev) {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("sync execution is only supported for upload and pre-* events"),
|
||||
util.I18nErrorEvSyncUnsupportedFs,
|
||||
|
|
@ -1320,6 +1356,7 @@ type ConditionOptions struct {
|
|||
ProviderObjects []string `json:"provider_objects,omitempty"`
|
||||
MinFileSize int64 `json:"min_size,omitempty"`
|
||||
MaxFileSize int64 `json:"max_size,omitempty"`
|
||||
EventStatuses []int `json:"event_statuses,omitempty"`
|
||||
// allow to execute scheduled tasks concurrently from multiple instances
|
||||
ConcurrentExecution bool `json:"concurrent_execution,omitempty"`
|
||||
}
|
||||
|
|
@ -1329,6 +1366,8 @@ func (f *ConditionOptions) getACopy() ConditionOptions {
|
|||
copy(protocols, f.Protocols)
|
||||
providerObjects := make([]string, len(f.ProviderObjects))
|
||||
copy(providerObjects, f.ProviderObjects)
|
||||
statuses := make([]int, len(f.EventStatuses))
|
||||
copy(statuses, f.EventStatuses)
|
||||
|
||||
return ConditionOptions{
|
||||
Names: cloneConditionPatterns(f.Names),
|
||||
|
|
@ -1339,10 +1378,20 @@ func (f *ConditionOptions) getACopy() ConditionOptions {
|
|||
ProviderObjects: providerObjects,
|
||||
MinFileSize: f.MinFileSize,
|
||||
MaxFileSize: f.MaxFileSize,
|
||||
EventStatuses: statuses,
|
||||
ConcurrentExecution: f.ConcurrentExecution,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *ConditionOptions) validateStatuses() error {
|
||||
for _, status := range f.EventStatuses {
|
||||
if status < 0 || status > 3 {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid event_status %d", status))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *ConditionOptions) validate() error {
|
||||
if err := validateConditionPatterns(f.Names); err != nil {
|
||||
return err
|
||||
|
|
@ -1358,12 +1407,12 @@ func (f *ConditionOptions) validate() error {
|
|||
}
|
||||
|
||||
for _, p := range f.Protocols {
|
||||
if !util.Contains(SupportedRuleConditionProtocols, p) {
|
||||
if !slices.Contains(SupportedRuleConditionProtocols, p) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported rule condition protocol: %q", p))
|
||||
}
|
||||
}
|
||||
for _, p := range f.ProviderObjects {
|
||||
if !util.Contains(SupporteRuleConditionProviderObjects, p) {
|
||||
if !slices.Contains(SupporteRuleConditionProviderObjects, p) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported provider object: %q", p))
|
||||
}
|
||||
}
|
||||
|
|
@ -1373,6 +1422,9 @@ func (f *ConditionOptions) validate() error {
|
|||
util.ByteCountSI(f.MaxFileSize), util.ByteCountSI(f.MinFileSize)))
|
||||
}
|
||||
}
|
||||
if err := f.validateStatuses(); err != nil {
|
||||
return err
|
||||
}
|
||||
if config.IsShared == 0 {
|
||||
f.ConcurrentExecution = false
|
||||
}
|
||||
|
|
@ -1465,16 +1517,16 @@ func (c *EventConditions) validate(trigger int) error {
|
|||
)
|
||||
}
|
||||
for _, ev := range c.FsEvents {
|
||||
if !util.Contains(SupportedFsEvents, ev) {
|
||||
if !slices.Contains(SupportedFsEvents, ev) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported fs event: %q", ev))
|
||||
}
|
||||
}
|
||||
case EventTriggerProviderEvent:
|
||||
c.FsEvents = nil
|
||||
c.Schedules = nil
|
||||
c.Options.GroupNames = nil
|
||||
c.Options.FsPaths = nil
|
||||
c.Options.Protocols = nil
|
||||
c.Options.EventStatuses = nil
|
||||
c.Options.MinFileSize = 0
|
||||
c.Options.MaxFileSize = 0
|
||||
c.IDPLoginEvent = 0
|
||||
|
|
@ -1485,7 +1537,7 @@ func (c *EventConditions) validate(trigger int) error {
|
|||
)
|
||||
}
|
||||
for _, ev := range c.ProviderEvents {
|
||||
if !util.Contains(SupportedProviderEvents, ev) {
|
||||
if !slices.Contains(SupportedProviderEvents, ev) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported provider event: %q", ev))
|
||||
}
|
||||
}
|
||||
|
|
@ -1494,6 +1546,7 @@ func (c *EventConditions) validate(trigger int) error {
|
|||
c.ProviderEvents = nil
|
||||
c.Options.FsPaths = nil
|
||||
c.Options.Protocols = nil
|
||||
c.Options.EventStatuses = nil
|
||||
c.Options.MinFileSize = 0
|
||||
c.Options.MaxFileSize = 0
|
||||
c.Options.ProviderObjects = nil
|
||||
|
|
@ -1509,6 +1562,7 @@ func (c *EventConditions) validate(trigger int) error {
|
|||
c.Options.RoleNames = nil
|
||||
c.Options.FsPaths = nil
|
||||
c.Options.Protocols = nil
|
||||
c.Options.EventStatuses = nil
|
||||
c.Options.MinFileSize = 0
|
||||
c.Options.MaxFileSize = 0
|
||||
c.Schedules = nil
|
||||
|
|
@ -1518,6 +1572,7 @@ func (c *EventConditions) validate(trigger int) error {
|
|||
c.ProviderEvents = nil
|
||||
c.Options.FsPaths = nil
|
||||
c.Options.Protocols = nil
|
||||
c.Options.EventStatuses = nil
|
||||
c.Options.MinFileSize = 0
|
||||
c.Options.MaxFileSize = 0
|
||||
c.Options.ProviderObjects = nil
|
||||
|
|
@ -1531,10 +1586,11 @@ func (c *EventConditions) validate(trigger int) error {
|
|||
c.Options.RoleNames = nil
|
||||
c.Options.FsPaths = nil
|
||||
c.Options.Protocols = nil
|
||||
c.Options.EventStatuses = nil
|
||||
c.Options.MinFileSize = 0
|
||||
c.Options.MaxFileSize = 0
|
||||
c.Schedules = nil
|
||||
if !util.Contains(supportedIDPLoginEvents, c.IDPLoginEvent) {
|
||||
if !slices.Contains(supportedIDPLoginEvents, c.IDPLoginEvent) {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid Identity Provider login event %d", c.IDPLoginEvent))
|
||||
}
|
||||
default:
|
||||
|
|
@ -1544,6 +1600,7 @@ func (c *EventConditions) validate(trigger int) error {
|
|||
c.Options.RoleNames = nil
|
||||
c.Options.FsPaths = nil
|
||||
c.Options.Protocols = nil
|
||||
c.Options.EventStatuses = nil
|
||||
c.Options.MinFileSize = 0
|
||||
c.Options.MaxFileSize = 0
|
||||
c.Schedules = nil
|
||||
|
|
@ -1624,10 +1681,19 @@ func (r *EventRule) isStatusValid() bool {
|
|||
return r.Status >= 0 && r.Status <= 1
|
||||
}
|
||||
|
||||
func (r *EventRule) validate() error {
|
||||
func (r *EventRule) validate() error { //nolint:gocyclo
|
||||
if r.Name == "" {
|
||||
return util.NewI18nError(util.NewValidationError("name is mandatory"), util.I18nErrorNameRequired)
|
||||
}
|
||||
if !util.IsNameValid(r.Name) {
|
||||
return util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)
|
||||
}
|
||||
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(r.Name) {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("name %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", r.Name)),
|
||||
util.I18nErrorInvalidUser,
|
||||
)
|
||||
}
|
||||
if !r.isStatusValid() {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid event rule status: %d", r.Status))
|
||||
}
|
||||
|
|
@ -1687,7 +1753,7 @@ func (r *EventRule) validateMandatorySyncActions() error {
|
|||
return nil
|
||||
}
|
||||
for _, ev := range r.Conditions.FsEvents {
|
||||
if util.Contains(mandatorySyncFsEvents, ev) {
|
||||
if slices.Contains(mandatorySyncFsEvents, ev) {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("event %q requires at least a sync action", ev)),
|
||||
util.I18nErrorRuleSyncActionRequired,
|
||||
|
|
@ -1705,7 +1771,7 @@ func (r *EventRule) checkIPBlockedAndCertificateActions() error {
|
|||
ActionTypeDataRetentionCheck, ActionTypeFilesystem, ActionTypePasswordExpirationCheck,
|
||||
ActionTypeUserExpirationCheck}
|
||||
for _, action := range r.Actions {
|
||||
if util.Contains(unavailableActions, action.Type) {
|
||||
if slices.Contains(unavailableActions, action.Type) {
|
||||
return fmt.Errorf("action %q, type %q is not supported for event trigger %q",
|
||||
action.Name, getActionTypeAsString(action.Type), getTriggerTypeAsString(r.Trigger))
|
||||
}
|
||||
|
|
@ -1721,7 +1787,7 @@ func (r *EventRule) checkProviderEventActions(providerObjectType string) error {
|
|||
ActionTypeDataRetentionCheck, ActionTypeFilesystem,
|
||||
ActionTypePasswordExpirationCheck, ActionTypeUserExpirationCheck}
|
||||
for _, action := range r.Actions {
|
||||
if util.Contains(userSpecificActions, action.Type) && providerObjectType != actionObjectUser {
|
||||
if slices.Contains(userSpecificActions, action.Type) && providerObjectType != actionObjectUser {
|
||||
return fmt.Errorf("action %q, type %q is only supported for provider user events",
|
||||
action.Name, getActionTypeAsString(action.Type))
|
||||
}
|
||||
|
|
@ -1829,6 +1895,20 @@ func (r *EventRule) RenderAsJSON(reload bool) ([]byte, error) {
|
|||
return json.Marshal(r)
|
||||
}
|
||||
|
||||
func cloneRenameConfigs(renames []RenameConfig) []RenameConfig {
|
||||
res := make([]RenameConfig, 0, len(renames))
|
||||
for _, c := range renames {
|
||||
res = append(res, RenameConfig{
|
||||
KeyValue: KeyValue{
|
||||
Key: c.Key,
|
||||
Value: c.Value,
|
||||
},
|
||||
UpdateModTime: c.UpdateModTime,
|
||||
})
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func cloneKeyValues(keyVals []KeyValue) []KeyValue {
|
||||
res := make([]KeyValue, 0, len(keyVals))
|
||||
for _, kv := range keyVals {
|
||||
|
|
|
|||
|
|
@ -132,11 +132,22 @@ func (g *Group) hasRedactedSecret() bool {
|
|||
return g.UserSettings.FsConfig.HasRedactedSecret()
|
||||
}
|
||||
|
||||
func (g *Group) applyNamingRules() {
|
||||
g.Name = config.convertName(g.Name)
|
||||
for idx := range g.VirtualFolders {
|
||||
g.VirtualFolders[idx].Name = config.convertName(g.VirtualFolders[idx].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Group) validate() error {
|
||||
g.SetEmptySecretsIfNil()
|
||||
g.applyNamingRules()
|
||||
if g.Name == "" {
|
||||
return util.NewI18nError(util.NewValidationError("name is mandatory"), util.I18nErrorNameRequired)
|
||||
}
|
||||
if !util.IsNameValid(g.Name) {
|
||||
return util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)
|
||||
}
|
||||
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(g.Name) {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("name %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", g.Name)),
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
|
@ -85,7 +86,7 @@ var (
|
|||
|
||||
// CheckIPListType returns an error if the provided IP list type is not valid
|
||||
func CheckIPListType(t IPListType) error {
|
||||
if !util.Contains(supportedIPListType, t) {
|
||||
if !slices.Contains(supportedIPListType, t) {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid list type %d", t))
|
||||
}
|
||||
return nil
|
||||
|
|
@ -417,6 +418,10 @@ func (l *IPList) IsListed(ip, protocol string) (bool, int, error) {
|
|||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
|
||||
if l.Ranges.Len() == 0 {
|
||||
return false, 0, nil
|
||||
}
|
||||
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP == nil {
|
||||
return false, 0, fmt.Errorf("invalid IP %s", ip)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ import (
|
|||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -206,6 +208,32 @@ func (p *MemoryProvider) updateAPIKeyLastUse(keyID string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) getAdminSignature(username string) (string, error) {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
return "", errMemoryProviderClosed
|
||||
}
|
||||
admin, err := p.adminExistsInternal(username)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strconv.FormatInt(admin.UpdatedAt, 10), nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) getUserSignature(username string) (string, error) {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
return "", errMemoryProviderClosed
|
||||
}
|
||||
user, err := p.userExistsInternal(username)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strconv.FormatInt(user.UpdatedAt, 10), nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) setUpdatedAt(username string) {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
|
|
@ -348,9 +376,6 @@ func (p *MemoryProvider) addUser(user *User) error {
|
|||
if err := p.addUserToRole(user.Username, user.Role); err != nil {
|
||||
return err
|
||||
}
|
||||
sort.Slice(user.Groups, func(i, j int) bool {
|
||||
return user.Groups[i].Name < user.Groups[j].Name
|
||||
})
|
||||
var mappedGroups []string
|
||||
for idx := range user.Groups {
|
||||
if err = p.addUserToGroupMapping(user.Username, user.Groups[idx].Name); err != nil {
|
||||
|
|
@ -362,9 +387,6 @@ func (p *MemoryProvider) addUser(user *User) error {
|
|||
}
|
||||
mappedGroups = append(mappedGroups, user.Groups[idx].Name)
|
||||
}
|
||||
sort.Slice(user.VirtualFolders, func(i, j int) bool {
|
||||
return user.VirtualFolders[i].Name < user.VirtualFolders[j].Name
|
||||
})
|
||||
var mappedFolders []string
|
||||
for idx := range user.VirtualFolders {
|
||||
if err = p.addUserToFolderMapping(user.Username, user.VirtualFolders[idx].Name); err != nil {
|
||||
|
|
@ -410,9 +432,6 @@ func (p *MemoryProvider) updateUser(user *User) error { //nolint:gocyclo
|
|||
for idx := range u.Groups {
|
||||
p.removeUserFromGroupMapping(u.Username, u.Groups[idx].Name)
|
||||
}
|
||||
sort.Slice(user.Groups, func(i, j int) bool {
|
||||
return user.Groups[i].Name < user.Groups[j].Name
|
||||
})
|
||||
for idx := range user.Groups {
|
||||
if err = p.addUserToGroupMapping(user.Username, user.Groups[idx].Name); err != nil {
|
||||
// try to add old mapping
|
||||
|
|
@ -428,9 +447,6 @@ func (p *MemoryProvider) updateUser(user *User) error { //nolint:gocyclo
|
|||
for _, oldFolder := range u.VirtualFolders {
|
||||
p.removeRelationFromFolderMapping(oldFolder.Name, u.Username, "")
|
||||
}
|
||||
sort.Slice(user.VirtualFolders, func(i, j int) bool {
|
||||
return user.VirtualFolders[i].Name < user.VirtualFolders[j].Name
|
||||
})
|
||||
for idx := range user.VirtualFolders {
|
||||
if err = p.addUserToFolderMapping(user.Username, user.VirtualFolders[idx].Name); err != nil {
|
||||
// try to add old mapping
|
||||
|
|
@ -743,9 +759,6 @@ func (p *MemoryProvider) addAdmin(admin *Admin) error {
|
|||
return err
|
||||
}
|
||||
var mappedAdmins []string
|
||||
sort.Slice(admin.Groups, func(i, j int) bool {
|
||||
return admin.Groups[i].Name < admin.Groups[j].Name
|
||||
})
|
||||
for idx := range admin.Groups {
|
||||
if err = p.addAdminToGroupMapping(admin.Username, admin.Groups[idx].Name); err != nil {
|
||||
// try to remove group mapping
|
||||
|
|
@ -788,9 +801,6 @@ func (p *MemoryProvider) updateAdmin(admin *Admin) error {
|
|||
for idx := range a.Groups {
|
||||
p.removeAdminFromGroupMapping(a.Username, a.Groups[idx].Name)
|
||||
}
|
||||
sort.Slice(admin.Groups, func(i, j int) bool {
|
||||
return admin.Groups[i].Name < admin.Groups[j].Name
|
||||
})
|
||||
for idx := range admin.Groups {
|
||||
if err = p.addAdminToGroupMapping(admin.Username, admin.Groups[idx].Name); err != nil {
|
||||
// try to add old mapping
|
||||
|
|
@ -1054,9 +1064,6 @@ func (p *MemoryProvider) addGroup(group *Group) error {
|
|||
group.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
group.Users = nil
|
||||
group.Admins = nil
|
||||
sort.Slice(group.VirtualFolders, func(i, j int) bool {
|
||||
return group.VirtualFolders[i].Name < group.VirtualFolders[j].Name
|
||||
})
|
||||
var mappedFolders []string
|
||||
for idx := range group.VirtualFolders {
|
||||
if err = p.addGroupToFolderMapping(group.Name, group.VirtualFolders[idx].Name); err != nil {
|
||||
|
|
@ -1090,9 +1097,6 @@ func (p *MemoryProvider) updateGroup(group *Group) error {
|
|||
for _, oldFolder := range g.VirtualFolders {
|
||||
p.removeRelationFromFolderMapping(oldFolder.Name, "", g.Name)
|
||||
}
|
||||
sort.Slice(group.VirtualFolders, func(i, j int) bool {
|
||||
return group.VirtualFolders[i].Name < group.VirtualFolders[j].Name
|
||||
})
|
||||
for idx := range group.VirtualFolders {
|
||||
if err = p.addGroupToFolderMapping(group.Name, group.VirtualFolders[idx].Name); err != nil {
|
||||
// try to add old mapping
|
||||
|
|
@ -1210,7 +1214,7 @@ func (p *MemoryProvider) addRuleToActionMapping(ruleName, actionName string) err
|
|||
if err != nil {
|
||||
return util.NewGenericError(fmt.Sprintf("action %q does not exist", actionName))
|
||||
}
|
||||
if !util.Contains(a.Rules, ruleName) {
|
||||
if !slices.Contains(a.Rules, ruleName) {
|
||||
a.Rules = append(a.Rules, ruleName)
|
||||
p.dbHandle.actions[actionName] = a
|
||||
}
|
||||
|
|
@ -1223,7 +1227,7 @@ func (p *MemoryProvider) removeRuleFromActionMapping(ruleName, actionName string
|
|||
providerLog(logger.LevelWarn, "action %q does not exist, cannot remove from mapping", actionName)
|
||||
return
|
||||
}
|
||||
if util.Contains(a.Rules, ruleName) {
|
||||
if slices.Contains(a.Rules, ruleName) {
|
||||
var rules []string
|
||||
for _, r := range a.Rules {
|
||||
if r != ruleName {
|
||||
|
|
@ -1240,7 +1244,7 @@ func (p *MemoryProvider) addAdminToGroupMapping(username, groupname string) erro
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !util.Contains(g.Admins, username) {
|
||||
if !slices.Contains(g.Admins, username) {
|
||||
g.Admins = append(g.Admins, username)
|
||||
p.dbHandle.groups[groupname] = g
|
||||
}
|
||||
|
|
@ -1283,7 +1287,7 @@ func (p *MemoryProvider) addUserToGroupMapping(username, groupname string) error
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !util.Contains(g.Users, username) {
|
||||
if !slices.Contains(g.Users, username) {
|
||||
g.Users = append(g.Users, username)
|
||||
p.dbHandle.groups[groupname] = g
|
||||
}
|
||||
|
|
@ -1313,7 +1317,7 @@ func (p *MemoryProvider) addAdminToRole(username, role string) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("%w: role %q does not exist", ErrForeignKeyViolated, role)
|
||||
}
|
||||
if !util.Contains(r.Admins, username) {
|
||||
if !slices.Contains(r.Admins, username) {
|
||||
r.Admins = append(r.Admins, username)
|
||||
p.dbHandle.roles[role] = r
|
||||
}
|
||||
|
|
@ -1347,7 +1351,7 @@ func (p *MemoryProvider) addUserToRole(username, role string) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("%w: role %q does not exist", ErrForeignKeyViolated, role)
|
||||
}
|
||||
if !util.Contains(r.Users, username) {
|
||||
if !slices.Contains(r.Users, username) {
|
||||
r.Users = append(r.Users, username)
|
||||
p.dbHandle.roles[role] = r
|
||||
}
|
||||
|
|
@ -1378,7 +1382,7 @@ func (p *MemoryProvider) addUserToFolderMapping(username, foldername string) err
|
|||
if err != nil {
|
||||
return util.NewGenericError(fmt.Sprintf("unable to get folder %q: %v", foldername, err))
|
||||
}
|
||||
if !util.Contains(f.Users, username) {
|
||||
if !slices.Contains(f.Users, username) {
|
||||
f.Users = append(f.Users, username)
|
||||
p.dbHandle.vfolders[foldername] = f
|
||||
}
|
||||
|
|
@ -1390,7 +1394,7 @@ func (p *MemoryProvider) addGroupToFolderMapping(name, foldername string) error
|
|||
if err != nil {
|
||||
return util.NewGenericError(fmt.Sprintf("unable to get folder %q: %v", foldername, err))
|
||||
}
|
||||
if !util.Contains(f.Groups, name) {
|
||||
if !slices.Contains(f.Groups, name) {
|
||||
f.Groups = append(f.Groups, name)
|
||||
p.dbHandle.vfolders[foldername] = f
|
||||
}
|
||||
|
|
@ -2089,11 +2093,11 @@ func (p *MemoryProvider) addSharedSession(_ Session) error {
|
|||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) deleteSharedSession(_ string) error {
|
||||
func (p *MemoryProvider) deleteSharedSession(_ string, _ SessionType) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) getSharedSession(_ string) (Session, error) {
|
||||
func (p *MemoryProvider) getSharedSession(_ string, _ SessionType) (Session, error) {
|
||||
return Session{}, ErrNotImplemented
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build !nomysql
|
||||
// +build !nomysql
|
||||
|
||||
package dataprovider
|
||||
|
||||
|
|
@ -39,11 +38,11 @@ import (
|
|||
|
||||
const (
|
||||
mysqlResetSQL = "DROP TABLE IF EXISTS `{{api_keys}}` CASCADE;" +
|
||||
"DROP TABLE IF EXISTS `{{folders_mapping}}` CASCADE;" +
|
||||
"DROP TABLE IF EXISTS `{{users_folders_mapping}}` CASCADE;" +
|
||||
"DROP TABLE IF EXISTS `{{users_groups_mapping}}` CASCADE;" +
|
||||
"DROP TABLE IF EXISTS `{{admins_groups_mapping}}` CASCADE;" +
|
||||
"DROP TABLE IF EXISTS `{{groups_folders_mapping}}` CASCADE;" +
|
||||
"DROP TABLE IF EXISTS `{{shares_groups_mapping}}` CASCADE;" +
|
||||
"DROP TABLE IF EXISTS `{{admins}}` CASCADE;" +
|
||||
"DROP TABLE IF EXISTS `{{folders}}` CASCADE;" +
|
||||
"DROP TABLE IF EXISTS `{{shares}}` CASCADE;" +
|
||||
|
|
@ -84,8 +83,8 @@ const (
|
|||
"CREATE TABLE `{{groups}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, " +
|
||||
"`name` varchar(255) NOT NULL UNIQUE, `description` varchar(512) NULL, `created_at` bigint NOT NULL, " +
|
||||
"`updated_at` bigint NOT NULL, `user_settings` longtext NULL);" +
|
||||
"CREATE TABLE `{{shared_sessions}}` (`key` varchar(128) NOT NULL PRIMARY KEY, " +
|
||||
"`data` longtext NOT NULL, `type` integer NOT NULL, `timestamp` bigint NOT NULL);" +
|
||||
"CREATE TABLE `{{shared_sessions}}` (`key` varchar(128) NOT NULL, `type` integer NOT NULL, `data` longtext NOT NULL, " +
|
||||
"`timestamp` bigint NOT NULL, PRIMARY KEY (`key`, `type`));" +
|
||||
"CREATE TABLE `{{users}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `username` varchar(255) NOT NULL UNIQUE, " +
|
||||
"`status` integer NOT NULL, `expiration_date` bigint NOT NULL, `description` varchar(512) NULL, `password` longtext NULL, " +
|
||||
"`public_keys` longtext NULL, `home_dir` longtext NOT NULL, `uid` bigint NOT NULL, `gid` bigint NOT NULL, " +
|
||||
|
|
@ -95,38 +94,41 @@ const (
|
|||
"`last_login` bigint NOT NULL, `filters` longtext NULL, `filesystem` longtext NULL, `additional_info` longtext NULL, " +
|
||||
"`created_at` bigint NOT NULL, `updated_at` bigint NOT NULL, `email` varchar(255) NULL, " +
|
||||
"`upload_data_transfer` integer NOT NULL, `download_data_transfer` integer NOT NULL, " +
|
||||
"`total_data_transfer` integer NOT NULL, `used_upload_data_transfer` integer NOT NULL, " +
|
||||
"`used_download_data_transfer` integer NOT NULL, `deleted_at` bigint NOT NULL, `first_download` bigint NOT NULL, " +
|
||||
"`total_data_transfer` integer NOT NULL, `used_upload_data_transfer` bigint NOT NULL, " +
|
||||
"`used_download_data_transfer` bigint NOT NULL, `deleted_at` bigint NOT NULL, `first_download` bigint NOT NULL, " +
|
||||
"`first_upload` bigint NOT NULL, `last_password_change` bigint NOT NULL, `role_id` integer NULL);" +
|
||||
"CREATE TABLE `{{groups_folders_mapping}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, " +
|
||||
"`group_id` integer NOT NULL, `folder_id` integer NOT NULL, " +
|
||||
"`virtual_path` longtext NOT NULL, `quota_size` bigint NOT NULL, `quota_files` integer NOT NULL);" +
|
||||
"`virtual_path` longtext NOT NULL, `quota_size` bigint NOT NULL, `quota_files` integer NOT NULL, `sort_order` integer NOT NULL);" +
|
||||
"CREATE TABLE `{{users_groups_mapping}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, " +
|
||||
"`user_id` integer NOT NULL, `group_id` integer NOT NULL, `group_type` integer NOT NULL);" +
|
||||
"`user_id` integer NOT NULL, `group_id` integer NOT NULL, `group_type` integer NOT NULL, `sort_order` integer NOT NULL);" +
|
||||
"CREATE TABLE `{{users_folders_mapping}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `virtual_path` longtext NOT NULL, " +
|
||||
"`quota_size` bigint NOT NULL, `quota_files` integer NOT NULL, `folder_id` integer NOT NULL, `user_id` integer NOT NULL);" +
|
||||
"`quota_size` bigint NOT NULL, `quota_files` integer NOT NULL, `folder_id` integer NOT NULL, `user_id` integer NOT NULL, `sort_order` integer NOT NULL);" +
|
||||
"ALTER TABLE `{{users_folders_mapping}}` ADD CONSTRAINT `{{prefix}}unique_user_folder_mapping` " +
|
||||
"UNIQUE (`user_id`, `folder_id`);" +
|
||||
"ALTER TABLE `{{users_folders_mapping}}` ADD CONSTRAINT `{{prefix}}users_folders_mapping_user_id_fk_users_id` " +
|
||||
"FOREIGN KEY (`user_id`) REFERENCES `{{users}}` (`id`) ON DELETE CASCADE;" +
|
||||
"ALTER TABLE `{{users_folders_mapping}}` ADD CONSTRAINT `{{prefix}}users_folders_mapping_folder_id_fk_folders_id` " +
|
||||
"FOREIGN KEY (`folder_id`) REFERENCES `{{folders}}` (`id`) ON DELETE CASCADE;" +
|
||||
"CREATE INDEX `{{prefix}}users_folders_mapping_sort_order_idx` ON `{{users_folders_mapping}}` (`sort_order`);" +
|
||||
"ALTER TABLE `{{users_groups_mapping}}` ADD CONSTRAINT `{{prefix}}unique_user_group_mapping` UNIQUE (`user_id`, `group_id`);" +
|
||||
"ALTER TABLE `{{groups_folders_mapping}}` ADD CONSTRAINT `{{prefix}}unique_group_folder_mapping` UNIQUE (`group_id`, `folder_id`);" +
|
||||
"ALTER TABLE `{{users_groups_mapping}}` ADD CONSTRAINT `{{prefix}}users_groups_mapping_group_id_fk_groups_id` " +
|
||||
"FOREIGN KEY (`group_id`) REFERENCES `{{groups}}` (`id`) ON DELETE NO ACTION;" +
|
||||
"ALTER TABLE `{{users_groups_mapping}}` ADD CONSTRAINT `{{prefix}}users_groups_mapping_user_id_fk_users_id` " +
|
||||
"FOREIGN KEY (`user_id`) REFERENCES `{{users}}` (`id`) ON DELETE CASCADE; " +
|
||||
"CREATE INDEX `{{prefix}}users_groups_mapping_sort_order_idx` ON `{{users_groups_mapping}}` (`sort_order`);" +
|
||||
"ALTER TABLE `{{groups_folders_mapping}}` ADD CONSTRAINT `{{prefix}}groups_folders_mapping_folder_id_fk_folders_id` " +
|
||||
"FOREIGN KEY (`folder_id`) REFERENCES `{{folders}}` (`id`) ON DELETE CASCADE;" +
|
||||
"ALTER TABLE `{{groups_folders_mapping}}` ADD CONSTRAINT `{{prefix}}groups_folders_mapping_group_id_fk_groups_id` " +
|
||||
"FOREIGN KEY (`group_id`) REFERENCES `{{groups}}` (`id`) ON DELETE CASCADE;" +
|
||||
"CREATE INDEX `{{prefix}}groups_folders_mapping_sort_order_idx` ON `{{groups_folders_mapping}}` (`sort_order`); " +
|
||||
"CREATE TABLE `{{shares}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, " +
|
||||
"`share_id` varchar(60) NOT NULL UNIQUE, `name` varchar(255) NOT NULL, `description` varchar(512) NULL, " +
|
||||
"`scope` integer NOT NULL, `paths` longtext NOT NULL, `created_at` bigint NOT NULL, " +
|
||||
"`updated_at` bigint NOT NULL, `last_use_at` bigint NOT NULL, `expires_at` bigint NOT NULL, " +
|
||||
"`password` longtext NULL, `max_tokens` integer NOT NULL, `used_tokens` integer NOT NULL, " +
|
||||
"`allow_from` longtext NULL, `user_id` integer NOT NULL);" +
|
||||
"`allow_from` longtext NULL, `options` longtext NULL, `user_id` integer NOT NULL);" +
|
||||
"ALTER TABLE `{{shares}}` ADD CONSTRAINT `{{prefix}}shares_user_id_fk_users_id` " +
|
||||
"FOREIGN KEY (`user_id`) REFERENCES `{{users}}` (`id`) ON DELETE CASCADE;" +
|
||||
"CREATE TABLE `{{api_keys}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` varchar(255) NOT NULL, `key_id` varchar(50) NOT NULL UNIQUE," +
|
||||
|
|
@ -150,13 +152,14 @@ const (
|
|||
"ALTER TABLE `{{rules_actions_mapping}}` ADD CONSTRAINT `{{prefix}}rules_actions_mapping_action_id_fk_events_targets_id` " +
|
||||
"FOREIGN KEY (`action_id`) REFERENCES `{{events_actions}}` (`id`) ON DELETE NO ACTION;" +
|
||||
"CREATE TABLE `{{admins_groups_mapping}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, " +
|
||||
" `admin_id` integer NOT NULL, `group_id` integer NOT NULL, `options` longtext NOT NULL);" +
|
||||
" `admin_id` integer NOT NULL, `group_id` integer NOT NULL, `options` longtext NOT NULL, `sort_order` integer NOT NULL);" +
|
||||
"ALTER TABLE `{{admins_groups_mapping}}` ADD CONSTRAINT `{{prefix}}unique_admin_group_mapping` " +
|
||||
"UNIQUE (`admin_id`, `group_id`);" +
|
||||
"ALTER TABLE `{{admins_groups_mapping}}` ADD CONSTRAINT `{{prefix}}admins_groups_mapping_admin_id_fk_admins_id` " +
|
||||
"FOREIGN KEY (`admin_id`) REFERENCES `{{admins}}` (`id`) ON DELETE CASCADE;" +
|
||||
"ALTER TABLE `{{admins_groups_mapping}}` ADD CONSTRAINT `{{prefix}}admins_groups_mapping_group_id_fk_groups_id` " +
|
||||
"FOREIGN KEY (`group_id`) REFERENCES `{{groups}}` (`id`) ON DELETE CASCADE;" +
|
||||
"CREATE INDEX `{{prefix}}admins_groups_mapping_sort_order_idx` ON `{{admins_groups_mapping}}` (`sort_order`); " +
|
||||
"CREATE TABLE `{{nodes}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, " +
|
||||
"`name` varchar(255) NOT NULL UNIQUE, `data` longtext NOT NULL, `created_at` bigint NOT NULL, " +
|
||||
"`updated_at` bigint NOT NULL);" +
|
||||
|
|
@ -193,11 +196,17 @@ const (
|
|||
"CREATE INDEX `{{prefix}}ip_lists_updated_at_idx` ON `{{ip_lists}}` (`updated_at`);" +
|
||||
"CREATE INDEX `{{prefix}}ip_lists_deleted_at_idx` ON `{{ip_lists}}` (`deleted_at`);" +
|
||||
"CREATE INDEX `{{prefix}}ip_lists_first_last_idx` ON `{{ip_lists}}` (`first`, `last`);" +
|
||||
"INSERT INTO {{schema_version}} (version) VALUES (28);"
|
||||
mysqlV29SQL = "ALTER TABLE `{{users}}` MODIFY `used_download_data_transfer` bigint NOT NULL;" +
|
||||
"ALTER TABLE `{{users}}` MODIFY `used_upload_data_transfer` bigint NOT NULL;"
|
||||
mysqlV29DownSQL = "ALTER TABLE `{{users}}` MODIFY `used_upload_data_transfer` integer NOT NULL;" +
|
||||
"ALTER TABLE `{{users}}` MODIFY `used_download_data_transfer` integer NOT NULL;"
|
||||
"INSERT INTO {{schema_version}} (version) VALUES (33);"
|
||||
mysqlV34SQL = "CREATE TABLE `{{shares_groups_mapping}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY," +
|
||||
"`share_id` integer NOT NULL, `group_id` integer NOT NULL, `permissions` integer NOT NULL," +
|
||||
"`sort_order` integer NOT NULL," +
|
||||
"CONSTRAINT `{{prefix}}unique_share_group_mapping` UNIQUE (`share_id`, `group_id`)," +
|
||||
"CONSTRAINT `{{prefix}}shares_groups_mapping_share_id_fk` FOREIGN KEY (`share_id`) REFERENCES `{{shares}}` (`id`) ON DELETE CASCADE," +
|
||||
"CONSTRAINT `{{prefix}}shares_groups_mapping_group_id_fk` FOREIGN KEY (`group_id`) REFERENCES `{{groups}}` (`id`) ON DELETE CASCADE); " +
|
||||
"CREATE INDEX `{{prefix}}shares_groups_mapping_sort_order_idx` ON `{{shares_groups_mapping}}` (`sort_order`); " +
|
||||
"CREATE INDEX `{{prefix}}shares_groups_mapping_share_id_idx` ON `{{shares_groups_mapping}}` (`share_id`); " +
|
||||
"CREATE INDEX `{{prefix}}shares_groups_mapping_group_id_idx` ON `{{shares_groups_mapping}}` (`group_id`);"
|
||||
mysqlV34DownSQL = "DROP TABLE IF EXISTS `{{shares_groups_mapping}}`;"
|
||||
)
|
||||
|
||||
// MySQLProvider defines the auth provider for MySQL/MariaDB database
|
||||
|
|
@ -329,6 +338,14 @@ func (p *MySQLProvider) getUsedQuota(username string) (int, int64, int64, int64,
|
|||
return sqlCommonGetUsedQuota(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) getAdminSignature(username string) (string, error) {
|
||||
return sqlCommonGetAdminSignature(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) getUserSignature(username string) (string, error) {
|
||||
return sqlCommonGetUserSignature(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) setUpdatedAt(username string) {
|
||||
sqlCommonSetUpdatedAt(username, p.dbHandle)
|
||||
}
|
||||
|
|
@ -583,12 +600,12 @@ func (p *MySQLProvider) addSharedSession(session Session) error {
|
|||
return sqlCommonAddSession(session, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) deleteSharedSession(key string) error {
|
||||
return sqlCommonDeleteSession(key, p.dbHandle)
|
||||
func (p *MySQLProvider) deleteSharedSession(key string, sessionType SessionType) error {
|
||||
return sqlCommonDeleteSession(key, sessionType, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) getSharedSession(key string) (Session, error) {
|
||||
return sqlCommonGetSession(key, p.dbHandle)
|
||||
func (p *MySQLProvider) getSharedSession(key string, sessionType SessionType) (Session, error) {
|
||||
return sqlCommonGetSession(key, sessionType, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) cleanupSharedSessions(sessionType SessionType, before int64) error {
|
||||
|
|
@ -776,11 +793,11 @@ func (p *MySQLProvider) initializeDatabase() error {
|
|||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return errSchemaVersionEmpty
|
||||
}
|
||||
logger.InfoToConsole("creating initial database schema, version 28")
|
||||
providerLog(logger.LevelInfo, "creating initial database schema, version 28")
|
||||
logger.InfoToConsole("creating initial database schema, version 33")
|
||||
providerLog(logger.LevelInfo, "creating initial database schema, version 33")
|
||||
initialSQL := sqlReplaceAll(mysqlInitialSQL)
|
||||
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, strings.Split(initialSQL, ";"), 28, true)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, strings.Split(initialSQL, ";"), 33, true)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) migrateDatabase() error {
|
||||
|
|
@ -793,13 +810,13 @@ func (p *MySQLProvider) migrateDatabase() error {
|
|||
case version == sqlDatabaseVersion:
|
||||
providerLog(logger.LevelDebug, "sql database is up to date, current version: %d", version)
|
||||
return ErrNoInitRequired
|
||||
case version < 28:
|
||||
err = fmt.Errorf("database schema version %d is too old, please see the upgrading docs", version)
|
||||
case version < 33:
|
||||
err = errSchemaVersionTooOld(version)
|
||||
providerLog(logger.LevelError, "%v", err)
|
||||
logger.ErrorToConsole("%v", err)
|
||||
return err
|
||||
case version == 28:
|
||||
return updateMySQLDatabaseFrom28To29(p.dbHandle)
|
||||
case version == 33:
|
||||
return updateMySQLDatabaseFromV33(p.dbHandle)
|
||||
default:
|
||||
if version > sqlDatabaseVersion {
|
||||
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
|
||||
|
|
@ -822,8 +839,8 @@ func (p *MySQLProvider) revertDatabase(targetVersion int) error {
|
|||
}
|
||||
|
||||
switch dbVersion.Version {
|
||||
case 29:
|
||||
return downgradeMySQLDatabaseFrom29To28(p.dbHandle)
|
||||
case 34:
|
||||
return downgradeMySQLDatabaseFromV34(p.dbHandle)
|
||||
default:
|
||||
return fmt.Errorf("database schema version not handled: %d", dbVersion.Version)
|
||||
}
|
||||
|
|
@ -862,18 +879,29 @@ func (p *MySQLProvider) normalizeError(err error, fieldType int) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func updateMySQLDatabaseFrom28To29(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database schema version: 28 -> 29")
|
||||
providerLog(logger.LevelInfo, "updating database schema version: 28 -> 29")
|
||||
|
||||
sql := strings.ReplaceAll(mysqlV29SQL, "{{users}}", sqlTableUsers)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 29, true)
|
||||
func updateMySQLDatabaseFromV33(dbHandle *sql.DB) error {
|
||||
return updateMySQLDatabaseFrom33To34(dbHandle)
|
||||
}
|
||||
|
||||
func downgradeMySQLDatabaseFrom29To28(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database schema version: 29 -> 28")
|
||||
providerLog(logger.LevelInfo, "downgrading database schema version: 29 -> 28")
|
||||
|
||||
sql := strings.ReplaceAll(mysqlV29DownSQL, "{{users}}", sqlTableUsers)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 28, false)
|
||||
func downgradeMySQLDatabaseFromV34(dbHandle *sql.DB) error {
|
||||
return downgradeMySQLDatabaseFrom34To33(dbHandle)
|
||||
}
|
||||
|
||||
func updateMySQLDatabaseFrom33To34(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database schema version: 33 -> 34")
|
||||
providerLog(logger.LevelInfo, "updating database schema version: 33 -> 34")
|
||||
|
||||
sql := strings.ReplaceAll(mysqlV34SQL, "{{prefix}}", config.SQLTablesPrefix)
|
||||
sql = strings.ReplaceAll(sql, "{{shares}}", sqlTableShares)
|
||||
sql = strings.ReplaceAll(sql, "{{shares_groups_mapping}}", sqlTableSharesGroupsMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{groups}}", sqlTableGroups)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 34, true)
|
||||
}
|
||||
|
||||
func downgradeMySQLDatabaseFrom34To33(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database schema version: 34 -> 33")
|
||||
providerLog(logger.LevelInfo, "downgrading database schema version: 34 -> 33")
|
||||
|
||||
sql := strings.ReplaceAll(mysqlV34DownSQL, "{{shares_groups_mapping}}", sqlTableSharesGroupsMapping)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 33, false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build nomysql
|
||||
// +build nomysql
|
||||
|
||||
package dataprovider
|
||||
|
||||
|
|
|
|||
|
|
@ -17,21 +17,21 @@ package dataprovider
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/rs/xid"
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/httpclient"
|
||||
"github.com/drakkan/sftpgo/v2/internal/jwt"
|
||||
"github.com/drakkan/sftpgo/v2/internal/kms"
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
|
|
@ -45,7 +45,8 @@ const (
|
|||
|
||||
const (
|
||||
// NodeTokenHeader defines the header to use for the node auth token
|
||||
NodeTokenHeader = "X-SFTPGO-Node"
|
||||
NodeTokenHeader = "X-SFTPGO-Node"
|
||||
nodeTokenAudience = "node"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -99,7 +100,7 @@ func (n *NodeData) validate() error {
|
|||
if n.Proto != NodeProtoHTTP && n.Proto != NodeProtoHTTPS {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid node proto: %s", n.Proto))
|
||||
}
|
||||
n.Key = kms.NewPlainSecret(util.BytesToString(util.GenerateRandomBytes(32)))
|
||||
n.Key = kms.NewPlainSecret(util.GenerateOpaqueString())
|
||||
n.Key.SetAdditionalData(n.Host)
|
||||
if err := n.Key.Encrypt(); err != nil {
|
||||
return fmt.Errorf("unable to encrypt node key: %w", err)
|
||||
|
|
@ -108,12 +109,12 @@ func (n *NodeData) validate() error {
|
|||
}
|
||||
|
||||
func (n *NodeData) getNodeName() string {
|
||||
h := fnv.New64a()
|
||||
h := sha256.New()
|
||||
var b bytes.Buffer
|
||||
|
||||
b.WriteString(fmt.Sprintf("%s:%d", n.Host, n.Port))
|
||||
fmt.Fprintf(&b, "%s:%d", n.Host, n.Port)
|
||||
h.Write(b.Bytes())
|
||||
return strconv.FormatUint(h.Sum64(), 10)
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// Node defines a cluster node
|
||||
|
|
@ -131,33 +132,26 @@ func (n *Node) validate() error {
|
|||
return n.Data.validate()
|
||||
}
|
||||
|
||||
func (n *Node) authenticate(token string) (string, string, error) {
|
||||
func (n *Node) authenticate(token string) (*jwt.Claims, error) {
|
||||
if err := n.Data.Key.TryDecrypt(); err != nil {
|
||||
providerLog(logger.LevelError, "unable to decrypt node key: %v", err)
|
||||
return "", "", err
|
||||
return nil, err
|
||||
}
|
||||
if token == "" {
|
||||
return "", "", ErrInvalidCredentials
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
t, err := jwt.Parse([]byte(token), jwt.WithKey(jwa.HS256, []byte(n.Data.Key.GetPayload())), jwt.WithValidate(true))
|
||||
claims, err := jwt.VerifyTokenWithKey(token, []jose.SignatureAlgorithm{jose.HS256}, []byte(n.Data.Key.GetPayload()))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("unable to parse and validate token: %v", err)
|
||||
return nil, fmt.Errorf("unable to parse and validate token: %v", err)
|
||||
}
|
||||
var adminUsername, role string
|
||||
if admin, ok := t.Get("admin"); ok {
|
||||
if val, ok := admin.(string); ok && val != "" {
|
||||
adminUsername = val
|
||||
}
|
||||
if claims.Username == "" {
|
||||
return nil, errors.New("no admin username associated with node token")
|
||||
}
|
||||
if adminUsername == "" {
|
||||
return "", "", errors.New("no admin username associated with node token")
|
||||
if !claims.Audience.Contains(nodeTokenAudience) {
|
||||
return nil, errors.New("invalid node token audience")
|
||||
}
|
||||
if r, ok := t.Get("role"); ok {
|
||||
if val, ok := r.(string); ok && val != "" {
|
||||
role = val
|
||||
}
|
||||
}
|
||||
return adminUsername, role, nil
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// getBaseURL returns the base URL for this node
|
||||
|
|
@ -174,35 +168,37 @@ func (n *Node) getBaseURL() string {
|
|||
}
|
||||
|
||||
// generateAuthToken generates a new auth token
|
||||
func (n *Node) generateAuthToken(username, role string) (string, error) {
|
||||
func (n *Node) generateAuthToken(username, role string, permissions []string) (string, error) {
|
||||
if err := n.Data.Key.TryDecrypt(); err != nil {
|
||||
return "", fmt.Errorf("unable to decrypt node key: %w", err)
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
|
||||
t := jwt.New()
|
||||
t.Set("admin", username) //nolint:errcheck
|
||||
t.Set("role", role) //nolint:errcheck
|
||||
t.Set(jwt.JwtIDKey, xid.New().String()) //nolint:errcheck
|
||||
t.Set(jwt.NotBeforeKey, now.Add(-30*time.Second)) //nolint:errcheck
|
||||
t.Set(jwt.ExpirationKey, now.Add(1*time.Minute)) //nolint:errcheck
|
||||
|
||||
payload, err := jwt.Sign(t, jwt.WithKey(jwa.HS256, []byte(n.Data.Key.GetPayload())))
|
||||
signer, err := jwt.NewSigner(jose.HS256, []byte(n.Data.Key.GetPayload()))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to create signer: %w", err)
|
||||
}
|
||||
claims := &jwt.Claims{
|
||||
Username: username,
|
||||
Role: role,
|
||||
Permissions: permissions,
|
||||
}
|
||||
claims.Audience = []string{nodeTokenAudience}
|
||||
claims.SetExpiry(time.Now().Add(1 * time.Minute))
|
||||
payload, err := signer.Sign(claims)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to sign authentication token: %w", err)
|
||||
}
|
||||
return util.BytesToString(payload), nil
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func (n *Node) prepareRequest(ctx context.Context, username, role, relativeURL, method string,
|
||||
body io.Reader,
|
||||
permissions []string, body io.Reader,
|
||||
) (*http.Request, error) {
|
||||
url := fmt.Sprintf("%s%s", n.getBaseURL(), relativeURL)
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token, err := n.generateAuthToken(username, role)
|
||||
token, err := n.generateAuthToken(username, role, permissions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -212,11 +208,11 @@ func (n *Node) prepareRequest(ctx context.Context, username, role, relativeURL,
|
|||
|
||||
// SendGetRequest sends an HTTP GET request to this node.
|
||||
// The responseHolder must be a pointer
|
||||
func (n *Node) SendGetRequest(username, role, relativeURL string, responseHolder any) error {
|
||||
func (n *Node) SendGetRequest(username, role, relativeURL string, permissions []string, responseHolder any) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), nodeReqTimeout)
|
||||
defer cancel()
|
||||
|
||||
req, err := n.prepareRequest(ctx, username, role, relativeURL, http.MethodGet, nil)
|
||||
req, err := n.prepareRequest(ctx, username, role, relativeURL, http.MethodGet, permissions, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -244,11 +240,11 @@ func (n *Node) SendGetRequest(username, role, relativeURL string, responseHolder
|
|||
}
|
||||
|
||||
// SendDeleteRequest sends an HTTP DELETE request to this node
|
||||
func (n *Node) SendDeleteRequest(username, role, relativeURL string) error {
|
||||
func (n *Node) SendDeleteRequest(username, role, relativeURL string, permissions []string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), nodeReqTimeout)
|
||||
defer cancel()
|
||||
|
||||
req, err := n.prepareRequest(ctx, username, role, relativeURL, http.MethodDelete, nil)
|
||||
req, err := n.prepareRequest(ctx, username, role, relativeURL, http.MethodDelete, permissions, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -268,9 +264,9 @@ func (n *Node) SendDeleteRequest(username, role, relativeURL string) error {
|
|||
}
|
||||
|
||||
// AuthenticateNodeToken check the validity of the provided token
|
||||
func AuthenticateNodeToken(token string) (string, string, error) {
|
||||
func AuthenticateNodeToken(token string) (*jwt.Claims, error) {
|
||||
if currentNode == nil {
|
||||
return "", "", errNoClusterNodes
|
||||
return nil, errNoClusterNodes
|
||||
}
|
||||
return currentNode.authenticate(token)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build !nopgsql
|
||||
// +build !nopgsql
|
||||
|
||||
package dataprovider
|
||||
|
||||
|
|
@ -24,6 +23,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -40,11 +40,11 @@ import (
|
|||
|
||||
const (
|
||||
pgsqlResetSQL = `DROP TABLE IF EXISTS "{{api_keys}}" CASCADE;
|
||||
DROP TABLE IF EXISTS "{{folders_mapping}}" CASCADE;
|
||||
DROP TABLE IF EXISTS "{{users_folders_mapping}}" CASCADE;
|
||||
DROP TABLE IF EXISTS "{{users_groups_mapping}}" CASCADE;
|
||||
DROP TABLE IF EXISTS "{{admins_groups_mapping}}" CASCADE;
|
||||
DROP TABLE IF EXISTS "{{groups_folders_mapping}}" CASCADE;
|
||||
DROP TABLE IF EXISTS "{{shares_groups_mapping}}" CASCADE;
|
||||
DROP TABLE IF EXISTS "{{admins}}" CASCADE;
|
||||
DROP TABLE IF EXISTS "{{folders}}" CASCADE;
|
||||
DROP TABLE IF EXISTS "{{shares}}" CASCADE;
|
||||
|
|
@ -85,8 +85,8 @@ CREATE TABLE "{{folders}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS A
|
|||
"filesystem" text NULL);
|
||||
CREATE TABLE "{{groups}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, "name" varchar(255) NOT NULL UNIQUE,
|
||||
"description" varchar(512) NULL, "created_at" bigint NOT NULL, "updated_at" bigint NOT NULL, "user_settings" text NULL);
|
||||
CREATE TABLE "{{shared_sessions}}" ("key" varchar(128) NOT NULL PRIMARY KEY,
|
||||
"data" text NOT NULL, "type" integer NOT NULL, "timestamp" bigint NOT NULL);
|
||||
CREATE TABLE "{{shared_sessions}}" ("key" varchar(128) NOT NULL, "type" integer NOT NULL,
|
||||
"data" text NOT NULL, "timestamp" bigint NOT NULL, PRIMARY KEY ("key", "type"));
|
||||
CREATE TABLE "{{users}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, "username" varchar(255) NOT NULL UNIQUE, "status" integer NOT NULL,
|
||||
"expiration_date" bigint NOT NULL, "description" varchar(512) NULL, "password" text NULL, "public_keys" text NULL,
|
||||
"home_dir" text NOT NULL, "uid" bigint NOT NULL, "gid" bigint NOT NULL, "max_sessions" integer NOT NULL,
|
||||
|
|
@ -95,14 +95,14 @@ CREATE TABLE "{{users}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS
|
|||
"download_bandwidth" integer NOT NULL, "last_login" bigint NOT NULL, "filters" text NULL, "filesystem" text NULL,
|
||||
"additional_info" text NULL, "created_at" bigint NOT NULL, "updated_at" bigint NOT NULL, "email" varchar(255) NULL,
|
||||
"upload_data_transfer" integer NOT NULL, "download_data_transfer" integer NOT NULL, "total_data_transfer" integer NOT NULL,
|
||||
"used_upload_data_transfer" integer NOT NULL, "used_download_data_transfer" integer NOT NULL, "deleted_at" bigint NOT NULL,
|
||||
"used_upload_data_transfer" bigint NOT NULL, "used_download_data_transfer" bigint NOT NULL, "deleted_at" bigint NOT NULL,
|
||||
"first_download" bigint NOT NULL, "first_upload" bigint NOT NULL, "last_password_change" bigint NOT NULL, "role_id" integer NULL);
|
||||
CREATE TABLE "{{groups_folders_mapping}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, "group_id" integer NOT NULL,
|
||||
"folder_id" integer NOT NULL, "virtual_path" text NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL);
|
||||
"folder_id" integer NOT NULL, "virtual_path" text NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL, "sort_order" integer NOT NULL);
|
||||
CREATE TABLE "{{users_groups_mapping}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, "user_id" integer NOT NULL,
|
||||
"group_id" integer NOT NULL, "group_type" integer NOT NULL);
|
||||
"group_id" integer NOT NULL, "group_type" integer NOT NULL, "sort_order" integer NOT NULL);
|
||||
CREATE TABLE "{{users_folders_mapping}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, "virtual_path" text NOT NULL,
|
||||
"quota_size" bigint NOT NULL, "quota_files" integer NOT NULL, "folder_id" integer NOT NULL, "user_id" integer NOT NULL);
|
||||
"quota_size" bigint NOT NULL, "quota_files" integer NOT NULL, "sort_order" integer NOT NULL, "folder_id" integer NOT NULL, "user_id" integer NOT NULL);
|
||||
ALTER TABLE "{{users_folders_mapping}}" ADD CONSTRAINT "{{prefix}}unique_user_folder_mapping" UNIQUE ("user_id", "folder_id");
|
||||
ALTER TABLE "{{users_folders_mapping}}" ADD CONSTRAINT "{{prefix}}users_folders_mapping_folder_id_fk_folders_id"
|
||||
FOREIGN KEY ("folder_id") REFERENCES "{{folders}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;
|
||||
|
|
@ -112,7 +112,7 @@ CREATE TABLE "{{shares}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS
|
|||
"share_id" varchar(60) NOT NULL UNIQUE, "name" varchar(255) NOT NULL, "description" varchar(512) NULL,
|
||||
"scope" integer NOT NULL, "paths" text NOT NULL, "created_at" bigint NOT NULL, "updated_at" bigint NOT NULL,
|
||||
"last_use_at" bigint NOT NULL, "expires_at" bigint NOT NULL, "password" text NULL,
|
||||
"max_tokens" integer NOT NULL, "used_tokens" integer NOT NULL, "allow_from" text NULL,
|
||||
"max_tokens" integer NOT NULL, "used_tokens" integer NOT NULL, "allow_from" text NULL, "options" text NULL,
|
||||
"user_id" integer NOT NULL);
|
||||
ALTER TABLE "{{shares}}" ADD CONSTRAINT "{{prefix}}shares_user_id_fk_users_id" FOREIGN KEY ("user_id")
|
||||
REFERENCES "{{users}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;
|
||||
|
|
@ -132,12 +132,14 @@ FOREIGN KEY ("group_id") REFERENCES "{{groups}}" ("id") MATCH SIMPLE ON UPDATE N
|
|||
CREATE INDEX "{{prefix}}users_groups_mapping_user_id_idx" ON "{{users_groups_mapping}}" ("user_id");
|
||||
ALTER TABLE "{{users_groups_mapping}}" ADD CONSTRAINT "{{prefix}}users_groups_mapping_user_id_fk_users_id"
|
||||
FOREIGN KEY ("user_id") REFERENCES "{{users}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;
|
||||
CREATE INDEX "{{prefix}}users_groups_mapping_sort_order_idx" ON "{{users_groups_mapping}}" ("sort_order");
|
||||
CREATE INDEX "{{prefix}}groups_folders_mapping_folder_id_idx" ON "{{groups_folders_mapping}}" ("folder_id");
|
||||
ALTER TABLE "{{groups_folders_mapping}}" ADD CONSTRAINT "{{prefix}}groups_folders_mapping_folder_id_fk_folders_id"
|
||||
FOREIGN KEY ("folder_id") REFERENCES "{{folders}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;
|
||||
CREATE INDEX "{{prefix}}groups_folders_mapping_group_id_idx" ON "{{groups_folders_mapping}}" ("group_id");
|
||||
ALTER TABLE "{{groups_folders_mapping}}" ADD CONSTRAINT "{{prefix}}groups_folders_mapping_group_id_fk_groups_id"
|
||||
FOREIGN KEY ("group_id") REFERENCES "{{groups}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;
|
||||
CREATE INDEX "{{prefix}}groups_folders_mapping_sort_order_idx" ON "{{groups_folders_mapping}}" ("sort_order");
|
||||
CREATE TABLE "{{events_rules}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, "name" varchar(255) NOT NULL UNIQUE,
|
||||
"status" integer NOT NULL, "description" varchar(512) NULL, "created_at" bigint NOT NULL, "updated_at" bigint NOT NULL,
|
||||
"trigger" integer NOT NULL, "conditions" text NOT NULL, "deleted_at" bigint NOT NULL);
|
||||
|
|
@ -153,7 +155,7 @@ FOREIGN KEY ("rule_id") REFERENCES "{{events_rules}}" ("id") MATCH SIMPLE ON UPD
|
|||
ALTER TABLE "{{rules_actions_mapping}}" ADD CONSTRAINT "{{prefix}}rules_actions_mapping_action_id_fk_events_targets_id"
|
||||
FOREIGN KEY ("action_id") REFERENCES "{{events_actions}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||
CREATE TABLE "{{admins_groups_mapping}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
"admin_id" integer NOT NULL, "group_id" integer NOT NULL, "options" text NOT NULL);
|
||||
"admin_id" integer NOT NULL, "group_id" integer NOT NULL, "options" text NOT NULL, "sort_order" integer NOT NULL);
|
||||
ALTER TABLE "{{admins_groups_mapping}}" ADD CONSTRAINT "{{prefix}}unique_admin_group_mapping" UNIQUE ("admin_id", "group_id");
|
||||
ALTER TABLE "{{admins_groups_mapping}}" ADD CONSTRAINT "{{prefix}}admins_groups_mapping_admin_id_fk_admins_id"
|
||||
FOREIGN KEY ("admin_id") REFERENCES "{{admins}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;
|
||||
|
|
@ -176,6 +178,7 @@ CREATE TABLE "{{configs}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS A
|
|||
INSERT INTO {{configs}} (configs) VALUES ('{}');
|
||||
CREATE INDEX "{{prefix}}users_folders_mapping_folder_id_idx" ON "{{users_folders_mapping}}" ("folder_id");
|
||||
CREATE INDEX "{{prefix}}users_folders_mapping_user_id_idx" ON "{{users_folders_mapping}}" ("user_id");
|
||||
CREATE INDEX "{{prefix}}users_folders_mapping_sort_order_idx" ON "{{users_folders_mapping}}" ("sort_order");
|
||||
CREATE INDEX "{{prefix}}api_keys_admin_id_idx" ON "{{api_keys}}" ("admin_id");
|
||||
CREATE INDEX "{{prefix}}api_keys_user_id_idx" ON "{{api_keys}}" ("user_id");
|
||||
CREATE INDEX "{{prefix}}users_updated_at_idx" ON "{{users}}" ("updated_at");
|
||||
|
|
@ -198,6 +201,7 @@ CREATE INDEX "{{prefix}}rules_actions_mapping_action_id_idx" ON "{{rules_actions
|
|||
CREATE INDEX "{{prefix}}rules_actions_mapping_order_idx" ON "{{rules_actions_mapping}}" ("order");
|
||||
CREATE INDEX "{{prefix}}admins_groups_mapping_admin_id_idx" ON "{{admins_groups_mapping}}" ("admin_id");
|
||||
CREATE INDEX "{{prefix}}admins_groups_mapping_group_id_idx" ON "{{admins_groups_mapping}}" ("group_id");
|
||||
CREATE INDEX "{{prefix}}admins_groups_mapping_sort_order_idx" ON "{{admins_groups_mapping}}" ("sort_order");
|
||||
CREATE INDEX "{{prefix}}admins_role_id_idx" ON "{{admins}}" ("role_id");
|
||||
CREATE INDEX "{{prefix}}users_role_id_idx" ON "{{users}}" ("role_id");
|
||||
CREATE INDEX "{{prefix}}ip_lists_type_idx" ON "{{ip_lists}}" ("type");
|
||||
|
|
@ -205,16 +209,24 @@ CREATE INDEX "{{prefix}}ip_lists_ipornet_idx" ON "{{ip_lists}}" ("ipornet");
|
|||
CREATE INDEX "{{prefix}}ip_lists_updated_at_idx" ON "{{ip_lists}}" ("updated_at");
|
||||
CREATE INDEX "{{prefix}}ip_lists_deleted_at_idx" ON "{{ip_lists}}" ("deleted_at");
|
||||
CREATE INDEX "{{prefix}}ip_lists_first_last_idx" ON "{{ip_lists}}" ("first", "last");
|
||||
INSERT INTO {{schema_version}} (version) VALUES (28);
|
||||
INSERT INTO {{schema_version}} (version) VALUES (33);
|
||||
`
|
||||
// not supported in CockroachDB
|
||||
ipListsLikeIndex = `CREATE INDEX "{{prefix}}ip_lists_ipornet_like_idx" ON "{{ip_lists}}" ("ipornet" varchar_pattern_ops);`
|
||||
pgsqlV29SQL = `ALTER TABLE "{{users}}" ALTER COLUMN "used_download_data_transfer" TYPE bigint;
|
||||
ALTER TABLE "{{users}}" ALTER COLUMN "used_upload_data_transfer" TYPE bigint;
|
||||
`
|
||||
pgsqlV29DownSQL = `ALTER TABLE "{{users}}" ALTER COLUMN "used_upload_data_transfer" TYPE integer;
|
||||
ALTER TABLE "{{users}}" ALTER COLUMN "used_download_data_transfer" TYPE integer;
|
||||
pgsqlV34SQL = `CREATE TABLE "{{shares_groups_mapping}}" (
|
||||
"id" integer NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
|
||||
"share_id" integer NOT NULL,
|
||||
"group_id" integer NOT NULL,
|
||||
"permissions" integer NOT NULL,
|
||||
"sort_order" integer NOT NULL,
|
||||
CONSTRAINT "{{prefix}}unique_share_group_mapping" UNIQUE ("share_id", "group_id"),
|
||||
CONSTRAINT "{{prefix}}shares_groups_mapping_share_id_fk" FOREIGN KEY ("share_id") REFERENCES "{{shares}}"("id") ON DELETE CASCADE,
|
||||
CONSTRAINT "{{prefix}}shares_groups_mapping_group_id_fk" FOREIGN KEY ("group_id") REFERENCES "{{groups}}"("id") ON DELETE CASCADE);
|
||||
CREATE INDEX "{{prefix}}shares_groups_mapping_sort_order_idx" ON "{{shares_groups_mapping}}" ("sort_order");
|
||||
CREATE INDEX "{{prefix}}shares_groups_mapping_share_id_idx" ON "{{shares_groups_mapping}}" ("share_id");
|
||||
CREATE INDEX "{{prefix}}shares_groups_mapping_group_id_idx" ON "{{shares_groups_mapping}}" ("group_id");
|
||||
`
|
||||
pgsqlV34DownSQL = `DROP TABLE IF EXISTS "{{shares_groups_mapping}}";`
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -274,7 +286,7 @@ func getPGSQLHostsAndPorts(configHost string, configPort int) (string, string) {
|
|||
defaultPort = "5432"
|
||||
}
|
||||
|
||||
for _, hostport := range strings.Split(configHost, ",") {
|
||||
for hostport := range strings.SplitSeq(configHost, ",") {
|
||||
hostport = strings.TrimSpace(hostport)
|
||||
if hostport == "" {
|
||||
continue
|
||||
|
|
@ -311,7 +323,7 @@ func getPGSQLConnectionString(redactedPwd bool) string {
|
|||
if config.DisableSNI {
|
||||
connectionString += " sslsni=0"
|
||||
}
|
||||
if util.Contains(pgSQLTargetSessionAttrs, config.TargetSessionAttrs) {
|
||||
if slices.Contains(pgSQLTargetSessionAttrs, config.TargetSessionAttrs) {
|
||||
connectionString += fmt.Sprintf(" target_session_attrs='%s'", config.TargetSessionAttrs)
|
||||
}
|
||||
} else {
|
||||
|
|
@ -348,6 +360,14 @@ func (p *PGSQLProvider) getUsedQuota(username string) (int, int64, int64, int64,
|
|||
return sqlCommonGetUsedQuota(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) getAdminSignature(username string) (string, error) {
|
||||
return sqlCommonGetAdminSignature(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) getUserSignature(username string) (string, error) {
|
||||
return sqlCommonGetUserSignature(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) setUpdatedAt(username string) {
|
||||
sqlCommonSetUpdatedAt(username, p.dbHandle)
|
||||
}
|
||||
|
|
@ -602,12 +622,12 @@ func (p *PGSQLProvider) addSharedSession(session Session) error {
|
|||
return sqlCommonAddSession(session, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) deleteSharedSession(key string) error {
|
||||
return sqlCommonDeleteSession(key, p.dbHandle)
|
||||
func (p *PGSQLProvider) deleteSharedSession(key string, sessionType SessionType) error {
|
||||
return sqlCommonDeleteSession(key, sessionType, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) getSharedSession(key string) (Session, error) {
|
||||
return sqlCommonGetSession(key, p.dbHandle)
|
||||
func (p *PGSQLProvider) getSharedSession(key string, sessionType SessionType) (Session, error) {
|
||||
return sqlCommonGetSession(key, sessionType, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) cleanupSharedSessions(sessionType SessionType, before int64) error {
|
||||
|
|
@ -795,8 +815,8 @@ func (p *PGSQLProvider) initializeDatabase() error {
|
|||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return errSchemaVersionEmpty
|
||||
}
|
||||
logger.InfoToConsole("creating initial database schema, version 28")
|
||||
providerLog(logger.LevelInfo, "creating initial database schema, version 28")
|
||||
logger.InfoToConsole("creating initial database schema, version 33")
|
||||
providerLog(logger.LevelInfo, "creating initial database schema, version 33")
|
||||
var initialSQL string
|
||||
if config.Driver == CockroachDataProviderName {
|
||||
initialSQL = sqlReplaceAll(pgsqlInitial)
|
||||
|
|
@ -805,10 +825,10 @@ func (p *PGSQLProvider) initializeDatabase() error {
|
|||
initialSQL = sqlReplaceAll(pgsqlInitial + ipListsLikeIndex)
|
||||
}
|
||||
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{initialSQL}, 28, true)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{initialSQL}, 33, true)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) migrateDatabase() error { //nolint:dupl
|
||||
func (p *PGSQLProvider) migrateDatabase() error {
|
||||
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -818,13 +838,13 @@ func (p *PGSQLProvider) migrateDatabase() error { //nolint:dupl
|
|||
case version == sqlDatabaseVersion:
|
||||
providerLog(logger.LevelDebug, "sql database is up to date, current version: %d", version)
|
||||
return ErrNoInitRequired
|
||||
case version < 28:
|
||||
err = fmt.Errorf("database schema version %d is too old, please see the upgrading docs", version)
|
||||
case version < 33:
|
||||
err = errSchemaVersionTooOld(version)
|
||||
providerLog(logger.LevelError, "%v", err)
|
||||
logger.ErrorToConsole("%v", err)
|
||||
return err
|
||||
case version == 28:
|
||||
return updatePGSQLDatabaseFrom28To29(p.dbHandle)
|
||||
case version == 33:
|
||||
return updatePGSQLDatabaseFromV33(p.dbHandle)
|
||||
default:
|
||||
if version > sqlDatabaseVersion {
|
||||
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
|
||||
|
|
@ -847,8 +867,8 @@ func (p *PGSQLProvider) revertDatabase(targetVersion int) error {
|
|||
}
|
||||
|
||||
switch dbVersion.Version {
|
||||
case 29:
|
||||
return downgradePGSQLDatabaseFrom29To28(p.dbHandle)
|
||||
case 34:
|
||||
return downgradePGSQLDatabaseFromV34(p.dbHandle)
|
||||
default:
|
||||
return fmt.Errorf("database schema version not handled: %d", dbVersion.Version)
|
||||
}
|
||||
|
|
@ -887,18 +907,29 @@ func (p *PGSQLProvider) normalizeError(err error, fieldType int) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func updatePGSQLDatabaseFrom28To29(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database schema version: 28 -> 29")
|
||||
providerLog(logger.LevelInfo, "updating database schema version: 28 -> 29")
|
||||
|
||||
sql := strings.ReplaceAll(pgsqlV29SQL, "{{users}}", sqlTableUsers)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 29, true)
|
||||
func updatePGSQLDatabaseFromV33(dbHandle *sql.DB) error {
|
||||
return updatePGSQLDatabaseFrom33To34(dbHandle)
|
||||
}
|
||||
|
||||
func downgradePGSQLDatabaseFrom29To28(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database schema version: 29 -> 28")
|
||||
providerLog(logger.LevelInfo, "downgrading database schema version: 29 -> 28")
|
||||
|
||||
sql := strings.ReplaceAll(pgsqlV29DownSQL, "{{users}}", sqlTableUsers)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 28, false)
|
||||
func downgradePGSQLDatabaseFromV34(dbHandle *sql.DB) error {
|
||||
return downgradePGSQLDatabaseFrom34To33(dbHandle)
|
||||
}
|
||||
|
||||
func updatePGSQLDatabaseFrom33To34(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database schema version: 33 -> 34")
|
||||
providerLog(logger.LevelInfo, "updating database schema version: 33 -> 34")
|
||||
|
||||
sql := strings.ReplaceAll(pgsqlV34SQL, "{{prefix}}", config.SQLTablesPrefix)
|
||||
sql = strings.ReplaceAll(sql, "{{shares}}", sqlTableShares)
|
||||
sql = strings.ReplaceAll(sql, "{{shares_groups_mapping}}", sqlTableSharesGroupsMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{groups}}", sqlTableGroups)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 34, true)
|
||||
}
|
||||
|
||||
func downgradePGSQLDatabaseFrom34To33(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database schema version: 34 -> 33")
|
||||
providerLog(logger.LevelInfo, "downgrading database schema version: 34 -> 33")
|
||||
|
||||
sql := strings.ReplaceAll(pgsqlV34DownSQL, "{{shares_groups_mapping}}", sqlTableSharesGroupsMapping)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 33, false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build nopgsql
|
||||
// +build nopgsql
|
||||
|
||||
package dataprovider
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,9 @@ func (r *Role) validate() error {
|
|||
if r.Name == "" {
|
||||
return util.NewI18nError(util.NewValidationError("name is mandatory"), util.I18nErrorNameRequired)
|
||||
}
|
||||
if !util.IsNameValid(r.Name) {
|
||||
return util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)
|
||||
}
|
||||
if len(r.Name) > 255 {
|
||||
return util.NewValidationError("name is too long, 255 is the maximum length allowed")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,11 +84,14 @@ func addScheduledCacheUpdates() error {
|
|||
|
||||
func checkDataprovider() {
|
||||
if currentNode != nil {
|
||||
if err := provider.updateNodeTimestamp(); err != nil {
|
||||
err := provider.updateNodeTimestamp()
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "unable to update node timestamp: %v", err)
|
||||
} else {
|
||||
providerLog(logger.LevelDebug, "node timestamp updated")
|
||||
}
|
||||
metric.UpdateDataProviderAvailability(err)
|
||||
return
|
||||
}
|
||||
err := provider.checkAvailability()
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -89,6 +89,11 @@ func (s *Share) GetAllowedFromAsString() string {
|
|||
return strings.Join(s.AllowFrom, ",")
|
||||
}
|
||||
|
||||
// IsPasswordHashed returns true if the password is hashed
|
||||
func (s *Share) IsPasswordHashed() bool {
|
||||
return util.IsStringPrefixInSlice(s.Password, hashPwdPrefixes)
|
||||
}
|
||||
|
||||
func (s *Share) getACopy() Share {
|
||||
allowFrom := make([]string, len(s.AllowFrom))
|
||||
copy(allowFrom, s.AllowFrom)
|
||||
|
|
@ -141,7 +146,7 @@ func (s *Share) HasRedactedPassword() bool {
|
|||
|
||||
func (s *Share) hashPassword() error {
|
||||
if s.Password != "" && !util.IsStringPrefixInSlice(s.Password, internalHashPwdPrefixes) {
|
||||
user, err := UserExists(s.Username, "")
|
||||
user, err := GetUserWithGroupSettings(s.Username, "")
|
||||
if err != nil {
|
||||
return util.NewGenericError(fmt.Sprintf("unable to validate user: %v", err))
|
||||
}
|
||||
|
|
@ -201,13 +206,16 @@ func (s *Share) validatePaths() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Share) validate() error {
|
||||
func (s *Share) validate() error { //nolint:gocyclo
|
||||
if s.ShareID == "" {
|
||||
return util.NewValidationError("share_id is mandatory")
|
||||
}
|
||||
if s.Name == "" {
|
||||
return util.NewI18nError(util.NewValidationError("name is mandatory"), util.I18nErrorNameRequired)
|
||||
}
|
||||
if !util.IsNameValid(s.Name) {
|
||||
return util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)
|
||||
}
|
||||
if s.Scope < ShareScopeRead || s.Scope > ShareScopeReadWrite {
|
||||
return util.NewI18nError(util.NewValidationError(fmt.Sprintf("invalid scope: %v", s.Scope)), util.I18nErrorShareScope)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import (
|
|||
"fmt"
|
||||
"net/netip"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -35,7 +36,7 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
sqlDatabaseVersion = 29
|
||||
sqlDatabaseVersion = 34
|
||||
defaultSQLQueryTimeout = 10 * time.Second
|
||||
longSQLQueryTimeout = 60 * time.Second
|
||||
)
|
||||
|
|
@ -70,6 +71,7 @@ func sqlReplaceAll(sql string) string {
|
|||
sql = strings.ReplaceAll(sql, "{{groups_folders_mapping}}", sqlTableGroupsFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{api_keys}}", sqlTableAPIKeys)
|
||||
sql = strings.ReplaceAll(sql, "{{shares}}", sqlTableShares)
|
||||
sql = strings.ReplaceAll(sql, "{{shares_groups_mapping}}", sqlTableSharesGroupsMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{defender_events}}", sqlTableDefenderEvents)
|
||||
sql = strings.ReplaceAll(sql, "{{defender_hosts}}", sqlTableDefenderHosts)
|
||||
sql = strings.ReplaceAll(sql, "{{active_transfers}}", sqlTableActiveTransfers)
|
||||
|
|
@ -1248,6 +1250,32 @@ func sqlCommonUpdateQuota(username string, filesAdd int, sizeAdd int64, reset bo
|
|||
return err
|
||||
}
|
||||
|
||||
func sqlCommonGetAdminSignature(username string, dbHandle *sql.DB) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
q := getAdminSignatureQuery()
|
||||
var updatedAt int64
|
||||
err := dbHandle.QueryRowContext(ctx, q, username).Scan(&updatedAt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strconv.FormatInt(updatedAt, 10), nil
|
||||
}
|
||||
|
||||
func sqlCommonGetUserSignature(username string, dbHandle *sql.DB) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
q := getUserSignatureQuery()
|
||||
var updatedAt int64
|
||||
err := dbHandle.QueryRowContext(ctx, q, username).Scan(&updatedAt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strconv.FormatInt(updatedAt, 10), nil
|
||||
}
|
||||
|
||||
func sqlCommonGetUsedQuota(username string, dbHandle *sql.DB) (int, int64, int64, int64, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
|
@ -2496,9 +2524,9 @@ func sqlCommonClearUserGroupMapping(ctx context.Context, user *User, dbHandle sq
|
|||
return err
|
||||
}
|
||||
|
||||
func sqlCommonAddUserFolderMapping(ctx context.Context, user *User, folder *vfs.VirtualFolder, dbHandle sqlQuerier) error {
|
||||
func sqlCommonAddUserFolderMapping(ctx context.Context, user *User, folder *vfs.VirtualFolder, sortOrder int, dbHandle sqlQuerier) error {
|
||||
q := getAddUserFolderMappingQuery()
|
||||
_, err := dbHandle.ExecContext(ctx, q, folder.VirtualPath, folder.QuotaSize, folder.QuotaFiles, folder.Name, user.Username)
|
||||
_, err := dbHandle.ExecContext(ctx, q, folder.VirtualPath, folder.QuotaSize, folder.QuotaFiles, folder.Name, user.Username, sortOrder)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -2508,27 +2536,29 @@ func sqlCommonClearAdminGroupMapping(ctx context.Context, admin *Admin, dbHandle
|
|||
return err
|
||||
}
|
||||
|
||||
func sqlCommonAddGroupFolderMapping(ctx context.Context, group *Group, folder *vfs.VirtualFolder, dbHandle sqlQuerier) error {
|
||||
func sqlCommonAddGroupFolderMapping(ctx context.Context, group *Group, folder *vfs.VirtualFolder, sortOrder int,
|
||||
dbHandle sqlQuerier,
|
||||
) error {
|
||||
q := getAddGroupFolderMappingQuery()
|
||||
_, err := dbHandle.ExecContext(ctx, q, folder.VirtualPath, folder.QuotaSize, folder.QuotaFiles, folder.Name, group.Name)
|
||||
_, err := dbHandle.ExecContext(ctx, q, folder.VirtualPath, folder.QuotaSize, folder.QuotaFiles, folder.Name, group.Name, sortOrder)
|
||||
return err
|
||||
}
|
||||
|
||||
func sqlCommonAddUserGroupMapping(ctx context.Context, username, groupName string, groupType int, dbHandle sqlQuerier) error {
|
||||
func sqlCommonAddUserGroupMapping(ctx context.Context, username, groupName string, groupType, sortOrder int, dbHandle sqlQuerier) error {
|
||||
q := getAddUserGroupMappingQuery()
|
||||
_, err := dbHandle.ExecContext(ctx, q, username, groupName, groupType)
|
||||
_, err := dbHandle.ExecContext(ctx, q, username, groupName, groupType, sortOrder)
|
||||
return err
|
||||
}
|
||||
|
||||
func sqlCommonAddAdminGroupMapping(ctx context.Context, username, groupName string, mappingOptions AdminGroupMappingOptions,
|
||||
dbHandle sqlQuerier,
|
||||
sortOrder int, dbHandle sqlQuerier,
|
||||
) error {
|
||||
options, err := json.Marshal(mappingOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := getAddAdminGroupMappingQuery()
|
||||
_, err = dbHandle.ExecContext(ctx, q, username, groupName, options)
|
||||
_, err = dbHandle.ExecContext(ctx, q, username, groupName, options, sortOrder)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -2539,7 +2569,7 @@ func generateGroupVirtualFoldersMapping(ctx context.Context, group *Group, dbHan
|
|||
}
|
||||
for idx := range group.VirtualFolders {
|
||||
vfolder := &group.VirtualFolders[idx]
|
||||
err = sqlCommonAddGroupFolderMapping(ctx, group, vfolder, dbHandle)
|
||||
err = sqlCommonAddGroupFolderMapping(ctx, group, vfolder, idx, dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -2554,7 +2584,7 @@ func generateUserVirtualFoldersMapping(ctx context.Context, user *User, dbHandle
|
|||
}
|
||||
for idx := range user.VirtualFolders {
|
||||
vfolder := &user.VirtualFolders[idx]
|
||||
err = sqlCommonAddUserFolderMapping(ctx, user, vfolder, dbHandle)
|
||||
err = sqlCommonAddUserFolderMapping(ctx, user, vfolder, idx, dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -2567,8 +2597,8 @@ func generateUserGroupMapping(ctx context.Context, user *User, dbHandle sqlQueri
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, group := range user.Groups {
|
||||
err = sqlCommonAddUserGroupMapping(ctx, user.Username, group.Name, group.Type, dbHandle)
|
||||
for idx, group := range user.Groups {
|
||||
err = sqlCommonAddUserGroupMapping(ctx, user.Username, group.Name, group.Type, idx, dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -2581,8 +2611,8 @@ func generateAdminGroupMapping(ctx context.Context, admin *Admin, dbHandle sqlQu
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, group := range admin.Groups {
|
||||
err = sqlCommonAddAdminGroupMapping(ctx, admin.Username, group.Name, group.Options, dbHandle)
|
||||
for idx, group := range admin.Groups {
|
||||
err = sqlCommonAddAdminGroupMapping(ctx, admin.Username, group.Name, group.Options, idx, dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -3238,14 +3268,14 @@ func sqlCommonAddSession(session Session, dbHandle *sql.DB) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func sqlCommonGetSession(key string, dbHandle sqlQuerier) (Session, error) {
|
||||
func sqlCommonGetSession(key string, sessionType SessionType, dbHandle sqlQuerier) (Session, error) {
|
||||
var session Session
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
q := getSessionQuery()
|
||||
var data []byte // type hint, some driver will use string instead of []byte if the type is any
|
||||
err := dbHandle.QueryRowContext(ctx, q, key).Scan(&session.Key, &data, &session.Type, &session.Timestamp)
|
||||
err := dbHandle.QueryRowContext(ctx, q, key, sessionType).Scan(&session.Key, &data, &session.Type, &session.Timestamp)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return session, util.NewRecordNotFoundError(err.Error())
|
||||
|
|
@ -3256,12 +3286,12 @@ func sqlCommonGetSession(key string, dbHandle sqlQuerier) (Session, error) {
|
|||
return session, nil
|
||||
}
|
||||
|
||||
func sqlCommonDeleteSession(key string, dbHandle *sql.DB) error {
|
||||
func sqlCommonDeleteSession(key string, sessionType SessionType, dbHandle *sql.DB) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
q := getDeleteSessionQuery()
|
||||
res, err := dbHandle.ExecContext(ctx, q, key)
|
||||
res, err := dbHandle.ExecContext(ctx, q, key, sessionType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -3936,16 +3966,22 @@ func sqlCommonUpdateDatabaseVersion(ctx context.Context, dbHandle sqlQuerier, ve
|
|||
}
|
||||
|
||||
func sqlCommonExecSQLAndUpdateDBVersion(dbHandle *sql.DB, sqlQueries []string, newVersion int, isUp bool) error {
|
||||
if err := sqlAcquireLock(dbHandle); err != nil {
|
||||
return err
|
||||
}
|
||||
defer sqlReleaseLock(dbHandle)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
conn, err := dbHandle.Conn(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get connection from pool: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if err := sqlAcquireLock(conn); err != nil {
|
||||
return err
|
||||
}
|
||||
defer sqlReleaseLock(conn)
|
||||
|
||||
if newVersion > 0 {
|
||||
currentVersion, err := sqlCommonGetDatabaseVersion(dbHandle, false)
|
||||
currentVersion, err := sqlCommonGetDatabaseVersion(conn, false)
|
||||
if err == nil {
|
||||
if (isUp && currentVersion.Version >= newVersion) || (!isUp && currentVersion.Version <= newVersion) {
|
||||
providerLog(logger.LevelInfo, "current schema version: %v, requested: %v, did you execute simultaneous migrations?",
|
||||
|
|
@ -3955,7 +3991,7 @@ func sqlCommonExecSQLAndUpdateDBVersion(dbHandle *sql.DB, sqlQueries []string, n
|
|||
}
|
||||
}
|
||||
|
||||
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
|
||||
return sqlCommonExecuteTxOnConn(ctx, conn, func(tx *sql.Tx) error {
|
||||
for _, q := range sqlQueries {
|
||||
if strings.TrimSpace(q) == "" {
|
||||
continue
|
||||
|
|
@ -3972,7 +4008,7 @@ func sqlCommonExecSQLAndUpdateDBVersion(dbHandle *sql.DB, sqlQueries []string, n
|
|||
})
|
||||
}
|
||||
|
||||
func sqlAcquireLock(dbHandle *sql.DB) error {
|
||||
func sqlAcquireLock(dbHandle *sql.Conn) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
|
|
@ -4001,7 +4037,7 @@ func sqlAcquireLock(dbHandle *sql.DB) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func sqlReleaseLock(dbHandle *sql.DB) {
|
||||
func sqlReleaseLock(dbHandle *sql.Conn) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
|
|
@ -4023,6 +4059,20 @@ func sqlReleaseLock(dbHandle *sql.DB) {
|
|||
}
|
||||
}
|
||||
|
||||
func sqlCommonExecuteTxOnConn(ctx context.Context, conn *sql.Conn, txFn func(*sql.Tx) error) error {
|
||||
tx, err := conn.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = txFn(tx)
|
||||
if err != nil {
|
||||
tx.Rollback() //nolint:errcheck
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func sqlCommonExecuteTx(ctx context.Context, dbHandle *sql.DB, txFn func(*sql.Tx) error) error {
|
||||
if config.Driver == CockroachDataProviderName {
|
||||
return crdb.ExecuteTx(ctx, dbHandle, nil, txFn)
|
||||
|
|
|
|||
|
|
@ -12,8 +12,7 @@
|
|||
// 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/>.
|
||||
|
||||
//go:build !nosqlite
|
||||
// +build !nosqlite
|
||||
//go:build !nosqlite && cgo
|
||||
|
||||
package dataprovider
|
||||
|
||||
|
|
@ -24,6 +23,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattn/go-sqlite3"
|
||||
|
|
@ -36,11 +36,11 @@ import (
|
|||
|
||||
const (
|
||||
sqliteResetSQL = `DROP TABLE IF EXISTS "{{api_keys}}";
|
||||
DROP TABLE IF EXISTS "{{folders_mapping}}";
|
||||
DROP TABLE IF EXISTS "{{users_folders_mapping}}";
|
||||
DROP TABLE IF EXISTS "{{users_groups_mapping}}";
|
||||
DROP TABLE IF EXISTS "{{admins_groups_mapping}}";
|
||||
DROP TABLE IF EXISTS "{{groups_folders_mapping}}";
|
||||
DROP TABLE IF EXISTS "{{shares_groups_mapping}}";
|
||||
DROP TABLE IF EXISTS "{{admins}}";
|
||||
DROP TABLE IF EXISTS "{{folders}}";
|
||||
DROP TABLE IF EXISTS "{{shares}}";
|
||||
|
|
@ -82,8 +82,8 @@ CREATE TABLE "{{folders}}" ("id" integer NOT NULL PRIMARY KEY, "name" varchar(25
|
|||
"last_quota_update" bigint NOT NULL, "filesystem" text NULL);
|
||||
CREATE TABLE "{{groups}}" ("id" integer NOT NULL PRIMARY KEY, "name" varchar(255) NOT NULL UNIQUE,
|
||||
"description" varchar(512) NULL, "created_at" bigint NOT NULL, "updated_at" bigint NOT NULL, "user_settings" text NULL);
|
||||
CREATE TABLE "{{shared_sessions}}" ("key" varchar(128) NOT NULL PRIMARY KEY, "data" text NOT NULL,
|
||||
"type" integer NOT NULL, "timestamp" bigint NOT NULL);
|
||||
CREATE TABLE "{{shared_sessions}}" ("key" varchar(128) NOT NULL, "type" integer NOT NULL,
|
||||
"data" text NOT NULL, "timestamp" bigint NOT NULL, PRIMARY KEY ("key", "type"));
|
||||
CREATE TABLE "{{users}}" ("id" integer NOT NULL PRIMARY KEY, "username" varchar(255) NOT NULL UNIQUE,
|
||||
"status" integer NOT NULL, "expiration_date" bigint NOT NULL, "description" varchar(512) NULL, "password" text NULL,
|
||||
"public_keys" text NULL, "home_dir" text NOT NULL, "uid" bigint NOT NULL, "gid" bigint NOT NULL,
|
||||
|
|
@ -98,21 +98,21 @@ CREATE TABLE "{{users}}" ("id" integer NOT NULL PRIMARY KEY, "username" varchar(
|
|||
CREATE TABLE "{{groups_folders_mapping}}" ("id" integer NOT NULL PRIMARY KEY,
|
||||
"folder_id" integer NOT NULL REFERENCES "{{folders}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
|
||||
"group_id" integer NOT NULL REFERENCES "{{groups}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
|
||||
"virtual_path" text NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL,
|
||||
"virtual_path" text NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL, "sort_order" integer NOT NULL,
|
||||
CONSTRAINT "{{prefix}}unique_group_folder_mapping" UNIQUE ("group_id", "folder_id"));
|
||||
CREATE TABLE "{{users_groups_mapping}}" ("id" integer NOT NULL PRIMARY KEY,
|
||||
"user_id" integer NOT NULL REFERENCES "{{users}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
|
||||
"group_id" integer NOT NULL REFERENCES "{{groups}}" ("id") ON DELETE NO ACTION,
|
||||
"group_type" integer NOT NULL, CONSTRAINT "{{prefix}}unique_user_group_mapping" UNIQUE ("user_id", "group_id"));
|
||||
"group_type" integer NOT NULL, "sort_order" integer NOT NULL, CONSTRAINT "{{prefix}}unique_user_group_mapping" UNIQUE ("user_id", "group_id"));
|
||||
CREATE TABLE "{{users_folders_mapping}}" ("id" integer NOT NULL PRIMARY KEY,
|
||||
"user_id" integer NOT NULL REFERENCES "{{users}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
|
||||
"folder_id" integer NOT NULL REFERENCES "{{folders}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
|
||||
"virtual_path" text NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL,
|
||||
"virtual_path" text NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL, "sort_order" integer NOT NULL,
|
||||
CONSTRAINT "{{prefix}}unique_user_folder_mapping" UNIQUE ("user_id", "folder_id"));
|
||||
CREATE TABLE "{{shares}}" ("id" integer NOT NULL PRIMARY KEY, "share_id" varchar(60) NOT NULL UNIQUE,
|
||||
"name" varchar(255) NOT NULL, "description" varchar(512) NULL, "scope" integer NOT NULL, "paths" text NOT NULL,
|
||||
"created_at" bigint NOT NULL, "updated_at" bigint NOT NULL, "last_use_at" bigint NOT NULL, "expires_at" bigint NOT NULL,
|
||||
"password" text NULL, "max_tokens" integer NOT NULL, "used_tokens" integer NOT NULL, "allow_from" text NULL,
|
||||
"password" text NULL, "max_tokens" integer NOT NULL, "used_tokens" integer NOT NULL, "allow_from" text NULL, "options" text NULL,
|
||||
"user_id" integer NOT NULL REFERENCES "{{users}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED);
|
||||
CREATE TABLE "{{api_keys}}" ("id" integer NOT NULL PRIMARY KEY, "name" varchar(255) NOT NULL,
|
||||
"key_id" varchar(50) NOT NULL UNIQUE, "api_key" varchar(255) NOT NULL UNIQUE, "scope" integer NOT NULL,
|
||||
|
|
@ -134,7 +134,7 @@ CREATE TABLE "{{tasks}}" ("id" integer NOT NULL PRIMARY KEY, "name" varchar(255)
|
|||
CREATE TABLE "{{admins_groups_mapping}}" ("id" integer NOT NULL PRIMARY KEY,
|
||||
"admin_id" integer NOT NULL REFERENCES "{{admins}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
|
||||
"group_id" integer NOT NULL REFERENCES "{{groups}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
|
||||
"options" text NOT NULL, CONSTRAINT "{{prefix}}unique_admin_group_mapping" UNIQUE ("admin_id", "group_id"));
|
||||
"options" text NOT NULL, "sort_order" integer NOT NULL, CONSTRAINT "{{prefix}}unique_admin_group_mapping" UNIQUE ("admin_id", "group_id"));
|
||||
CREATE TABLE "{{ip_lists}}" ("id" integer NOT NULL PRIMARY KEY,
|
||||
"type" integer NOT NULL, "ipornet" varchar(50) NOT NULL, "mode" integer NOT NULL, "description" varchar(512) NULL,
|
||||
"first" BLOB NOT NULL, "last" BLOB NOT NULL, "ip_type" integer NOT NULL, "protocols" integer NOT NULL,
|
||||
|
|
@ -144,10 +144,13 @@ CREATE TABLE "{{configs}}" ("id" integer NOT NULL PRIMARY KEY, "configs" text NO
|
|||
INSERT INTO {{configs}} (configs) VALUES ('{}');
|
||||
CREATE INDEX "{{prefix}}users_folders_mapping_folder_id_idx" ON "{{users_folders_mapping}}" ("folder_id");
|
||||
CREATE INDEX "{{prefix}}users_folders_mapping_user_id_idx" ON "{{users_folders_mapping}}" ("user_id");
|
||||
CREATE INDEX "{{prefix}}users_folders_mapping_sort_order_idx" ON "{{users_folders_mapping}}" ("sort_order");
|
||||
CREATE INDEX "{{prefix}}users_groups_mapping_group_id_idx" ON "{{users_groups_mapping}}" ("group_id");
|
||||
CREATE INDEX "{{prefix}}users_groups_mapping_user_id_idx" ON "{{users_groups_mapping}}" ("user_id");
|
||||
CREATE INDEX "{{prefix}}users_groups_mapping_sort_order_idx" ON "{{users_groups_mapping}}" ("sort_order");
|
||||
CREATE INDEX "{{prefix}}groups_folders_mapping_folder_id_idx" ON "{{groups_folders_mapping}}" ("folder_id");
|
||||
CREATE INDEX "{{prefix}}groups_folders_mapping_group_id_idx" ON "{{groups_folders_mapping}}" ("group_id");
|
||||
CREATE INDEX "{{prefix}}groups_folders_mapping_sort_order_idx" ON "{{groups_folders_mapping}}" ("sort_order");
|
||||
CREATE INDEX "{{prefix}}api_keys_admin_id_idx" ON "{{api_keys}}" ("admin_id");
|
||||
CREATE INDEX "{{prefix}}api_keys_user_id_idx" ON "{{api_keys}}" ("user_id");
|
||||
CREATE INDEX "{{prefix}}users_updated_at_idx" ON "{{users}}" ("updated_at");
|
||||
|
|
@ -170,6 +173,7 @@ CREATE INDEX "{{prefix}}rules_actions_mapping_action_id_idx" ON "{{rules_actions
|
|||
CREATE INDEX "{{prefix}}rules_actions_mapping_order_idx" ON "{{rules_actions_mapping}}" ("order");
|
||||
CREATE INDEX "{{prefix}}admins_groups_mapping_admin_id_idx" ON "{{admins_groups_mapping}}" ("admin_id");
|
||||
CREATE INDEX "{{prefix}}admins_groups_mapping_group_id_idx" ON "{{admins_groups_mapping}}" ("group_id");
|
||||
CREATE INDEX "{{prefix}}admins_groups_mapping_sort_order_idx" ON "{{admins_groups_mapping}}" ("sort_order");
|
||||
CREATE INDEX "{{prefix}}users_role_id_idx" ON "{{users}}" ("role_id");
|
||||
CREATE INDEX "{{prefix}}admins_role_id_idx" ON "{{admins}}" ("role_id");
|
||||
CREATE INDEX "{{prefix}}ip_lists_type_idx" ON "{{ip_lists}}" ("type");
|
||||
|
|
@ -178,8 +182,22 @@ CREATE INDEX "{{prefix}}ip_lists_ip_type_idx" ON "{{ip_lists}}" ("ip_type");
|
|||
CREATE INDEX "{{prefix}}ip_lists_ip_updated_at_idx" ON "{{ip_lists}}" ("updated_at");
|
||||
CREATE INDEX "{{prefix}}ip_lists_ip_deleted_at_idx" ON "{{ip_lists}}" ("deleted_at");
|
||||
CREATE INDEX "{{prefix}}ip_lists_first_last_idx" ON "{{ip_lists}}" ("first", "last");
|
||||
INSERT INTO {{schema_version}} (version) VALUES (28);
|
||||
INSERT INTO {{schema_version}} (version) VALUES (33);
|
||||
`
|
||||
sqliteV34SQL = `
|
||||
CREATE TABLE "{{shares_groups_mapping}}" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"share_id" integer NOT NULL REFERENCES "{{shares}}" ("id") ON DELETE CASCADE,
|
||||
"group_id" integer NOT NULL REFERENCES "{{groups}}" ("id") ON DELETE CASCADE,
|
||||
"permissions" integer NOT NULL,
|
||||
"sort_order" integer NOT NULL,
|
||||
CONSTRAINT "{{prefix}}unique_share_group_mapping" UNIQUE ("share_id", "group_id")
|
||||
);
|
||||
CREATE INDEX "{{prefix}}shares_groups_mapping_sort_order_idx" ON "{{shares_groups_mapping}}" ("sort_order");
|
||||
CREATE INDEX "{{prefix}}shares_groups_mapping_group_id_idx" ON "{{shares_groups_mapping}}" ("group_id");
|
||||
CREATE INDEX "{{prefix}}shares_groups_mapping_share_id_idx" ON "{{shares_groups_mapping}}" ("share_id");
|
||||
`
|
||||
sqliteV34DownSQL = `DROP TABLE IF EXISTS "{{shares_groups_mapping}}";`
|
||||
)
|
||||
|
||||
// SQLiteProvider defines the auth provider for SQLite database
|
||||
|
|
@ -215,7 +233,7 @@ func initializeSQLiteProvider(basePath string) error {
|
|||
providerLog(logger.LevelDebug, "sqlite database handle created, connection string: %q", connectionString)
|
||||
dbHandle.SetMaxOpenConns(1)
|
||||
provider = &SQLiteProvider{dbHandle: dbHandle}
|
||||
return nil
|
||||
return executePragmaOptimize(dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) checkAvailability() error {
|
||||
|
|
@ -246,6 +264,14 @@ func (p *SQLiteProvider) getUsedQuota(username string) (int, int64, int64, int64
|
|||
return sqlCommonGetUsedQuota(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) getAdminSignature(username string) (string, error) {
|
||||
return sqlCommonGetAdminSignature(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) getUserSignature(username string) (string, error) {
|
||||
return sqlCommonGetUserSignature(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) setUpdatedAt(username string) {
|
||||
sqlCommonSetUpdatedAt(username, p.dbHandle)
|
||||
}
|
||||
|
|
@ -500,12 +526,12 @@ func (p *SQLiteProvider) addSharedSession(session Session) error {
|
|||
return sqlCommonAddSession(session, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) deleteSharedSession(key string) error {
|
||||
return sqlCommonDeleteSession(key, p.dbHandle)
|
||||
func (p *SQLiteProvider) deleteSharedSession(key string, sessionType SessionType) error {
|
||||
return sqlCommonDeleteSession(key, sessionType, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) getSharedSession(key string) (Session, error) {
|
||||
return sqlCommonGetSession(key, p.dbHandle)
|
||||
func (p *SQLiteProvider) getSharedSession(key string, sessionType SessionType) (Session, error) {
|
||||
return sqlCommonGetSession(key, sessionType, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) cleanupSharedSessions(sessionType SessionType, before int64) error {
|
||||
|
|
@ -693,13 +719,13 @@ func (p *SQLiteProvider) initializeDatabase() error {
|
|||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return errSchemaVersionEmpty
|
||||
}
|
||||
logger.InfoToConsole("creating initial database schema, version 28")
|
||||
providerLog(logger.LevelInfo, "creating initial database schema, version 28")
|
||||
logger.InfoToConsole("creating initial database schema, version 33")
|
||||
providerLog(logger.LevelInfo, "creating initial database schema, version 33")
|
||||
sql := sqlReplaceAll(sqliteInitialSQL)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{sql}, 28, true)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{sql}, 33, true)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) migrateDatabase() error { //nolint:dupl
|
||||
func (p *SQLiteProvider) migrateDatabase() error {
|
||||
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -709,13 +735,13 @@ func (p *SQLiteProvider) migrateDatabase() error { //nolint:dupl
|
|||
case version == sqlDatabaseVersion:
|
||||
providerLog(logger.LevelDebug, "sql database is up to date, current version: %d", version)
|
||||
return ErrNoInitRequired
|
||||
case version < 28:
|
||||
err = fmt.Errorf("database schema version %d is too old, please see the upgrading docs", version)
|
||||
case version < 33:
|
||||
err = errSchemaVersionTooOld(version)
|
||||
providerLog(logger.LevelError, "%v", err)
|
||||
logger.ErrorToConsole("%v", err)
|
||||
return err
|
||||
case version == 28:
|
||||
return updateSQLiteDatabaseFrom28To29(p.dbHandle)
|
||||
case version == 33:
|
||||
return updateSQLiteDatabaseFromV33(p.dbHandle)
|
||||
default:
|
||||
if version > sqlDatabaseVersion {
|
||||
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
|
||||
|
|
@ -738,8 +764,8 @@ func (p *SQLiteProvider) revertDatabase(targetVersion int) error {
|
|||
}
|
||||
|
||||
switch dbVersion.Version {
|
||||
case 29:
|
||||
return downgradeSQLiteDatabaseFrom29To28(p.dbHandle)
|
||||
case 34:
|
||||
return downgradeSQLiteDatabaseFromV34(p.dbHandle)
|
||||
default:
|
||||
return fmt.Errorf("database schema version not handled: %d", dbVersion.Version)
|
||||
}
|
||||
|
|
@ -777,24 +803,39 @@ func (p *SQLiteProvider) normalizeError(err error, fieldType int) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func updateSQLiteDatabaseFrom28To29(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database schema version: 28 -> 29")
|
||||
providerLog(logger.LevelInfo, "updating database schema version: 28 -> 29")
|
||||
|
||||
func executePragmaOptimize(dbHandle *sql.DB) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
return sqlCommonUpdateDatabaseVersion(ctx, dbHandle, 29)
|
||||
_, err := dbHandle.ExecContext(ctx, "PRAGMA optimize;")
|
||||
return err
|
||||
}
|
||||
|
||||
func downgradeSQLiteDatabaseFrom29To28(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database schema version: 29 -> 28")
|
||||
providerLog(logger.LevelInfo, "downgrading database schema version: 29 -> 28")
|
||||
func updateSQLiteDatabaseFromV33(dbHandle *sql.DB) error {
|
||||
return updateSQLiteDatabaseFrom33To34(dbHandle)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
func downgradeSQLiteDatabaseFromV34(dbHandle *sql.DB) error {
|
||||
return downgradeSQLiteDatabaseFrom34To33(dbHandle)
|
||||
}
|
||||
|
||||
return sqlCommonUpdateDatabaseVersion(ctx, dbHandle, 28)
|
||||
func updateSQLiteDatabaseFrom33To34(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database schema version: 33 -> 34")
|
||||
providerLog(logger.LevelInfo, "updating database schema version: 33 -> 34")
|
||||
|
||||
sql := strings.ReplaceAll(sqliteV34SQL, "{{prefix}}", config.SQLTablesPrefix)
|
||||
sql = strings.ReplaceAll(sql, "{{shares}}", sqlTableShares)
|
||||
sql = strings.ReplaceAll(sql, "{{shares_groups_mapping}}", sqlTableSharesGroupsMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{groups}}", sqlTableGroups)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 34, true)
|
||||
}
|
||||
|
||||
func downgradeSQLiteDatabaseFrom34To33(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database schema version: 34 -> 33")
|
||||
providerLog(logger.LevelInfo, "downgrading database schema version: 34 -> 33")
|
||||
|
||||
sql := strings.ReplaceAll(sqliteV34DownSQL, "{{shares_groups_mapping}}", sqlTableSharesGroupsMapping)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 33, false)
|
||||
}
|
||||
|
||||
/*func setPragmaFK(dbHandle *sql.DB, value string) error {
|
||||
|
|
|
|||
|
|
@ -12,8 +12,7 @@
|
|||
// 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/>.
|
||||
|
||||
//go:build nosqlite
|
||||
// +build nosqlite
|
||||
//go:build nosqlite || !cgo
|
||||
|
||||
package dataprovider
|
||||
|
||||
|
|
|
|||
|
|
@ -81,25 +81,27 @@ func getAddSessionQuery() string {
|
|||
"ON DUPLICATE KEY UPDATE `data`=VALUES(`data`), `timestamp`=VALUES(`timestamp`)",
|
||||
sqlTableSharedSessions, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])
|
||||
}
|
||||
return fmt.Sprintf(`INSERT INTO %s (key,data,type,timestamp) VALUES (%s,%s,%s,%s) ON CONFLICT(key) DO UPDATE SET data=
|
||||
return fmt.Sprintf(`INSERT INTO %s (key,data,type,timestamp) VALUES (%s,%s,%s,%s) ON CONFLICT(key,type) DO UPDATE SET data=
|
||||
EXCLUDED.data, timestamp=EXCLUDED.timestamp`,
|
||||
sqlTableSharedSessions, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])
|
||||
}
|
||||
|
||||
func getDeleteSessionQuery() string {
|
||||
if config.Driver == MySQLDataProviderName {
|
||||
return fmt.Sprintf("DELETE FROM %s WHERE `key` = %s", sqlTableSharedSessions, sqlPlaceholders[0])
|
||||
return fmt.Sprintf("DELETE FROM %s WHERE `key` = %s AND `type` = %s",
|
||||
sqlTableSharedSessions, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||
}
|
||||
return fmt.Sprintf(`DELETE FROM %s WHERE key = %s`, sqlTableSharedSessions, sqlPlaceholders[0])
|
||||
return fmt.Sprintf(`DELETE FROM %s WHERE key = %s AND type = %s`,
|
||||
sqlTableSharedSessions, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||
}
|
||||
|
||||
func getSessionQuery() string {
|
||||
if config.Driver == MySQLDataProviderName {
|
||||
return fmt.Sprintf("SELECT `key`,`data`,`type`,`timestamp` FROM %s WHERE `key` = %s", sqlTableSharedSessions,
|
||||
sqlPlaceholders[0])
|
||||
return fmt.Sprintf("SELECT `key`,`data`,`type`,`timestamp` FROM %s WHERE `key` = %s AND `type` = %s",
|
||||
sqlTableSharedSessions, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||
}
|
||||
return fmt.Sprintf(`SELECT key,data,type,timestamp FROM %s WHERE key = %s`, sqlTableSharedSessions,
|
||||
sqlPlaceholders[0])
|
||||
return fmt.Sprintf(`SELECT key,data,type,timestamp FROM %s WHERE key = %s AND type = %s`,
|
||||
sqlTableSharedSessions, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||
}
|
||||
|
||||
func getCleanupSessionsQuery() string {
|
||||
|
|
@ -650,6 +652,14 @@ func getUpdateQuotaQuery(reset bool) string {
|
|||
WHERE username = %s`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])
|
||||
}
|
||||
|
||||
func getAdminSignatureQuery() string {
|
||||
return fmt.Sprintf(`SELECT updated_at FROM %s WHERE username = %s`, sqlTableAdmins, sqlPlaceholders[0])
|
||||
}
|
||||
|
||||
func getUserSignatureQuery() string {
|
||||
return fmt.Sprintf(`SELECT updated_at FROM %s WHERE username = %s`, sqlTableUsers, sqlPlaceholders[0])
|
||||
}
|
||||
|
||||
func getSetUpdateAtQuery() string {
|
||||
return fmt.Sprintf(`UPDATE %s SET updated_at = %s WHERE username = %s`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||
}
|
||||
|
|
@ -757,10 +767,10 @@ func getClearUserGroupMappingQuery() string {
|
|||
}
|
||||
|
||||
func getAddUserGroupMappingQuery() string {
|
||||
return fmt.Sprintf(`INSERT INTO %s (user_id,group_id,group_type) VALUES ((SELECT id FROM %s WHERE username = %s),
|
||||
(SELECT id FROM %s WHERE name = %s),%s)`,
|
||||
return fmt.Sprintf(`INSERT INTO %s (user_id,group_id,group_type,sort_order) VALUES ((SELECT id FROM %s WHERE username = %s),
|
||||
(SELECT id FROM %s WHERE name = %s),%s,%s)`,
|
||||
sqlTableUsersGroupsMapping, sqlTableUsers, sqlPlaceholders[0], getSQLQuotedName(sqlTableGroups),
|
||||
sqlPlaceholders[1], sqlPlaceholders[2])
|
||||
sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])
|
||||
}
|
||||
|
||||
func getClearAdminGroupMappingQuery() string {
|
||||
|
|
@ -769,10 +779,10 @@ func getClearAdminGroupMappingQuery() string {
|
|||
}
|
||||
|
||||
func getAddAdminGroupMappingQuery() string {
|
||||
return fmt.Sprintf(`INSERT INTO %s (admin_id,group_id,options) VALUES ((SELECT id FROM %s WHERE username = %s),
|
||||
(SELECT id FROM %s WHERE name = %s),%s)`,
|
||||
return fmt.Sprintf(`INSERT INTO %s (admin_id,group_id,options,sort_order) VALUES ((SELECT id FROM %s WHERE username = %s),
|
||||
(SELECT id FROM %s WHERE name = %s),%s,%s)`,
|
||||
sqlTableAdminsGroupsMapping, sqlTableAdmins, sqlPlaceholders[0], getSQLQuotedName(sqlTableGroups),
|
||||
sqlPlaceholders[1], sqlPlaceholders[2])
|
||||
sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])
|
||||
}
|
||||
|
||||
func getClearGroupFolderMappingQuery() string {
|
||||
|
|
@ -781,10 +791,10 @@ func getClearGroupFolderMappingQuery() string {
|
|||
}
|
||||
|
||||
func getAddGroupFolderMappingQuery() string {
|
||||
return fmt.Sprintf(`INSERT INTO %s (virtual_path,quota_size,quota_files,folder_id,group_id)
|
||||
VALUES (%s,%s,%s,(SELECT id FROM %s WHERE name = %s),(SELECT id FROM %s WHERE name = %s))`,
|
||||
return fmt.Sprintf(`INSERT INTO %s (virtual_path,quota_size,quota_files,folder_id,group_id,sort_order)
|
||||
VALUES (%s,%s,%s,(SELECT id FROM %s WHERE name = %s),(SELECT id FROM %s WHERE name = %s),%s)`,
|
||||
sqlTableGroupsFoldersMapping, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlTableFolders,
|
||||
sqlPlaceholders[3], getSQLQuotedName(sqlTableGroups), sqlPlaceholders[4])
|
||||
sqlPlaceholders[3], getSQLQuotedName(sqlTableGroups), sqlPlaceholders[4], sqlPlaceholders[5])
|
||||
}
|
||||
|
||||
func getClearUserFolderMappingQuery() string {
|
||||
|
|
@ -793,10 +803,10 @@ func getClearUserFolderMappingQuery() string {
|
|||
}
|
||||
|
||||
func getAddUserFolderMappingQuery() string {
|
||||
return fmt.Sprintf(`INSERT INTO %s (virtual_path,quota_size,quota_files,folder_id,user_id)
|
||||
VALUES (%s,%s,%s,(SELECT id FROM %s WHERE name = %s),(SELECT id FROM %s WHERE username = %s))`,
|
||||
return fmt.Sprintf(`INSERT INTO %s (virtual_path,quota_size,quota_files,folder_id,user_id,sort_order)
|
||||
VALUES (%s,%s,%s,(SELECT id FROM %s WHERE name = %s),(SELECT id FROM %s WHERE username = %s),%s)`,
|
||||
sqlTableUsersFoldersMapping, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlTableFolders,
|
||||
sqlPlaceholders[3], sqlTableUsers, sqlPlaceholders[4])
|
||||
sqlPlaceholders[3], sqlTableUsers, sqlPlaceholders[4], sqlPlaceholders[5])
|
||||
}
|
||||
|
||||
func getFoldersQuery(order string, minimal bool) string {
|
||||
|
|
@ -838,7 +848,7 @@ func getRelatedGroupsForUsersQuery(users []User) string {
|
|||
sb.WriteString(")")
|
||||
}
|
||||
return fmt.Sprintf(`SELECT g.name,ug.group_type,ug.user_id FROM %s g INNER JOIN %s ug ON g.id = ug.group_id WHERE
|
||||
ug.user_id IN %s ORDER BY g.name`, getSQLQuotedName(sqlTableGroups), sqlTableUsersGroupsMapping, sb.String())
|
||||
ug.user_id IN %s ORDER BY ug.sort_order`, getSQLQuotedName(sqlTableGroups), sqlTableUsersGroupsMapping, sb.String())
|
||||
}
|
||||
|
||||
func getRelatedGroupsForAdminsQuery(admins []Admin) string {
|
||||
|
|
@ -855,7 +865,7 @@ func getRelatedGroupsForAdminsQuery(admins []Admin) string {
|
|||
sb.WriteString(")")
|
||||
}
|
||||
return fmt.Sprintf(`SELECT g.name,ag.options,ag.admin_id FROM %s g INNER JOIN %s ag ON g.id = ag.group_id WHERE
|
||||
ag.admin_id IN %s ORDER BY g.name`, getSQLQuotedName(sqlTableGroups), sqlTableAdminsGroupsMapping, sb.String())
|
||||
ag.admin_id IN %s ORDER BY ag.sort_order`, getSQLQuotedName(sqlTableGroups), sqlTableAdminsGroupsMapping, sb.String())
|
||||
}
|
||||
|
||||
func getRelatedFoldersForUsersQuery(users []User) string {
|
||||
|
|
@ -873,7 +883,7 @@ func getRelatedFoldersForUsersQuery(users []User) string {
|
|||
}
|
||||
return fmt.Sprintf(`SELECT f.id,f.name,f.path,f.used_quota_size,f.used_quota_files,f.last_quota_update,fm.virtual_path,
|
||||
fm.quota_size,fm.quota_files,fm.user_id,f.filesystem,f.description FROM %s f INNER JOIN %s fm ON f.id = fm.folder_id WHERE
|
||||
fm.user_id IN %s ORDER BY f.name`, sqlTableFolders, sqlTableUsersFoldersMapping, sb.String())
|
||||
fm.user_id IN %s ORDER BY fm.sort_order`, sqlTableFolders, sqlTableUsersFoldersMapping, sb.String())
|
||||
}
|
||||
|
||||
func getRelatedUsersForFoldersQuery(folders []vfs.BaseVirtualFolder) string {
|
||||
|
|
@ -960,7 +970,7 @@ func getRelatedFoldersForGroupsQuery(groups []Group) string {
|
|||
}
|
||||
return fmt.Sprintf(`SELECT f.id,f.name,f.path,f.used_quota_size,f.used_quota_files,f.last_quota_update,fm.virtual_path,
|
||||
fm.quota_size,fm.quota_files,fm.group_id,f.filesystem,f.description FROM %s f INNER JOIN %s fm ON f.id = fm.folder_id WHERE
|
||||
fm.group_id IN %s ORDER BY f.name`, sqlTableFolders, sqlTableGroupsFoldersMapping, sb.String())
|
||||
fm.group_id IN %s ORDER BY fm.sort_order`, sqlTableFolders, sqlTableGroupsFoldersMapping, sb.String())
|
||||
}
|
||||
|
||||
func getActiveTransfersQuery() string {
|
||||
|
|
|
|||
|
|
@ -12,8 +12,7 @@
|
|||
// 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/>.
|
||||
|
||||
//go:build unixcrypt
|
||||
// +build unixcrypt
|
||||
//go:build unixcrypt && cgo
|
||||
|
||||
package dataprovider
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,7 @@
|
|||
// 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/>.
|
||||
|
||||
//go:build !unixcrypt
|
||||
// +build !unixcrypt
|
||||
//go:build !unixcrypt || !cgo
|
||||
|
||||
package dataprovider
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import (
|
|||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -123,6 +125,8 @@ type UserFilters struct {
|
|||
sdk.BaseUserFilters
|
||||
// User must change password from WebClient/REST API at next login.
|
||||
RequirePasswordChange bool `json:"require_password_change,omitempty"`
|
||||
// AdditionalEmails defines additional email addresses
|
||||
AdditionalEmails []string `json:"additional_emails,omitempty"`
|
||||
// Time-based one time passwords configuration
|
||||
TOTPConfig UserTOTPConfig `json:"totp_config,omitempty"`
|
||||
// Recovery codes to use if the user loses access to their second factor auth device.
|
||||
|
|
@ -342,7 +346,11 @@ func (u *User) isTimeBasedAccessAllowed(when time.Time) bool {
|
|||
if when.IsZero() {
|
||||
when = time.Now()
|
||||
}
|
||||
when = when.UTC()
|
||||
if UseLocalTime() {
|
||||
when = when.Local()
|
||||
} else {
|
||||
when = when.UTC()
|
||||
}
|
||||
weekDay := when.Weekday()
|
||||
hhMM := when.Format("15:04")
|
||||
for _, p := range u.Filters.AccessTime {
|
||||
|
|
@ -399,6 +407,15 @@ func (u *User) CheckMaxShareExpiration(expiresAt time.Time) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// GetEmailAddresses returns all the email addresses.
|
||||
func (u *User) GetEmailAddresses() []string {
|
||||
var res []string
|
||||
if u.Email != "" {
|
||||
res = append(res, u.Email)
|
||||
}
|
||||
return slices.Concat(res, u.Filters.AdditionalEmails)
|
||||
}
|
||||
|
||||
// GetSubDirPermissions returns permissions for sub directories
|
||||
func (u *User) GetSubDirPermissions() []sdk.DirectoryPermissions {
|
||||
var result []sdk.DirectoryPermissions
|
||||
|
|
@ -840,20 +857,20 @@ func (u *User) HasPermissionsInside(virtualPath string) bool {
|
|||
// HasPerm returns true if the user has the given permission or any permission
|
||||
func (u *User) HasPerm(permission, path string) bool {
|
||||
perms := u.GetPermissionsForPath(path)
|
||||
if util.Contains(perms, PermAny) {
|
||||
if slices.Contains(perms, PermAny) {
|
||||
return true
|
||||
}
|
||||
return util.Contains(perms, permission)
|
||||
return slices.Contains(perms, permission)
|
||||
}
|
||||
|
||||
// HasAnyPerm returns true if the user has at least one of the given permissions
|
||||
func (u *User) HasAnyPerm(permissions []string, path string) bool {
|
||||
perms := u.GetPermissionsForPath(path)
|
||||
if util.Contains(perms, PermAny) {
|
||||
if slices.Contains(perms, PermAny) {
|
||||
return true
|
||||
}
|
||||
for _, permission := range permissions {
|
||||
if util.Contains(perms, permission) {
|
||||
if slices.Contains(perms, permission) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -863,11 +880,11 @@ func (u *User) HasAnyPerm(permissions []string, path string) bool {
|
|||
// HasPerms returns true if the user has all the given permissions
|
||||
func (u *User) HasPerms(permissions []string, path string) bool {
|
||||
perms := u.GetPermissionsForPath(path)
|
||||
if util.Contains(perms, PermAny) {
|
||||
if slices.Contains(perms, PermAny) {
|
||||
return true
|
||||
}
|
||||
for _, permission := range permissions {
|
||||
if !util.Contains(perms, permission) {
|
||||
if !slices.Contains(perms, permission) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -927,11 +944,11 @@ func (u *User) IsLoginMethodAllowed(loginMethod, protocol string) bool {
|
|||
if len(u.Filters.DeniedLoginMethods) == 0 {
|
||||
return true
|
||||
}
|
||||
if util.Contains(u.Filters.DeniedLoginMethods, loginMethod) {
|
||||
if slices.Contains(u.Filters.DeniedLoginMethods, loginMethod) {
|
||||
return false
|
||||
}
|
||||
if protocol == protocolSSH && loginMethod == LoginMethodPassword {
|
||||
if util.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {
|
||||
if slices.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -965,10 +982,10 @@ func (u *User) IsPartialAuth() bool {
|
|||
method == SSHLoginMethodPassword {
|
||||
continue
|
||||
}
|
||||
if method == LoginMethodPassword && util.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {
|
||||
if method == LoginMethodPassword && slices.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {
|
||||
continue
|
||||
}
|
||||
if !util.Contains(SSHMultiStepsLoginMethods, method) {
|
||||
if !slices.Contains(SSHMultiStepsLoginMethods, method) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -982,7 +999,7 @@ func (u *User) GetAllowedLoginMethods() []string {
|
|||
if method == SSHLoginMethodPassword {
|
||||
continue
|
||||
}
|
||||
if !util.Contains(u.Filters.DeniedLoginMethods, method) {
|
||||
if !slices.Contains(u.Filters.DeniedLoginMethods, method) {
|
||||
allowedMethods = append(allowedMethods, method)
|
||||
}
|
||||
}
|
||||
|
|
@ -1052,7 +1069,7 @@ func (u *User) IsFileAllowed(virtualPath string) (bool, int) {
|
|||
|
||||
// CanManageMFA returns true if the user can add a multi-factor authentication configuration
|
||||
func (u *User) CanManageMFA() bool {
|
||||
if util.Contains(u.Filters.WebClient, sdk.WebClientMFADisabled) {
|
||||
if slices.Contains(u.Filters.WebClient, sdk.WebClientMFADisabled) {
|
||||
return false
|
||||
}
|
||||
return len(mfa.GetAvailableTOTPConfigs()) > 0
|
||||
|
|
@ -1073,39 +1090,39 @@ func (u *User) skipExternalAuth() bool {
|
|||
|
||||
// CanManageShares returns true if the user can add, update and list shares
|
||||
func (u *User) CanManageShares() bool {
|
||||
return !util.Contains(u.Filters.WebClient, sdk.WebClientSharesDisabled)
|
||||
return !slices.Contains(u.Filters.WebClient, sdk.WebClientSharesDisabled)
|
||||
}
|
||||
|
||||
// CanResetPassword returns true if this user is allowed to reset its password
|
||||
func (u *User) CanResetPassword() bool {
|
||||
return !util.Contains(u.Filters.WebClient, sdk.WebClientPasswordResetDisabled)
|
||||
return !slices.Contains(u.Filters.WebClient, sdk.WebClientPasswordResetDisabled)
|
||||
}
|
||||
|
||||
// CanChangePassword returns true if this user is allowed to change its password
|
||||
func (u *User) CanChangePassword() bool {
|
||||
return !util.Contains(u.Filters.WebClient, sdk.WebClientPasswordChangeDisabled)
|
||||
return !slices.Contains(u.Filters.WebClient, sdk.WebClientPasswordChangeDisabled)
|
||||
}
|
||||
|
||||
// CanChangeAPIKeyAuth returns true if this user is allowed to enable/disable API key authentication
|
||||
func (u *User) CanChangeAPIKeyAuth() bool {
|
||||
return !util.Contains(u.Filters.WebClient, sdk.WebClientAPIKeyAuthChangeDisabled)
|
||||
return !slices.Contains(u.Filters.WebClient, sdk.WebClientAPIKeyAuthChangeDisabled)
|
||||
}
|
||||
|
||||
// CanChangeInfo returns true if this user is allowed to change its info such as email and description
|
||||
func (u *User) CanChangeInfo() bool {
|
||||
return !util.Contains(u.Filters.WebClient, sdk.WebClientInfoChangeDisabled)
|
||||
return !slices.Contains(u.Filters.WebClient, sdk.WebClientInfoChangeDisabled)
|
||||
}
|
||||
|
||||
// CanManagePublicKeys returns true if this user is allowed to manage public keys
|
||||
// from the WebClient. Used in WebClient UI
|
||||
func (u *User) CanManagePublicKeys() bool {
|
||||
return !util.Contains(u.Filters.WebClient, sdk.WebClientPubKeyChangeDisabled)
|
||||
return !slices.Contains(u.Filters.WebClient, sdk.WebClientPubKeyChangeDisabled)
|
||||
}
|
||||
|
||||
// CanManageTLSCerts returns true if this user is allowed to manage TLS certificates
|
||||
// from the WebClient. Used in WebClient UI
|
||||
func (u *User) CanManageTLSCerts() bool {
|
||||
return !util.Contains(u.Filters.WebClient, sdk.WebClientTLSCertChangeDisabled)
|
||||
return !slices.Contains(u.Filters.WebClient, sdk.WebClientTLSCertChangeDisabled)
|
||||
}
|
||||
|
||||
// CanUpdateProfile returns true if the user is allowed to update the profile.
|
||||
|
|
@ -1117,7 +1134,7 @@ func (u *User) CanUpdateProfile() bool {
|
|||
// CanAddFilesFromWeb returns true if the client can add files from the web UI.
|
||||
// The specified target is the directory where the files must be uploaded
|
||||
func (u *User) CanAddFilesFromWeb(target string) bool {
|
||||
if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
|
||||
if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
|
||||
return false
|
||||
}
|
||||
return u.HasPerm(PermUpload, target) || u.HasPerm(PermOverwrite, target)
|
||||
|
|
@ -1126,7 +1143,7 @@ func (u *User) CanAddFilesFromWeb(target string) bool {
|
|||
// CanAddDirsFromWeb returns true if the client can add directories from the web UI.
|
||||
// The specified target is the directory where the new directory must be created
|
||||
func (u *User) CanAddDirsFromWeb(target string) bool {
|
||||
if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
|
||||
if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
|
||||
return false
|
||||
}
|
||||
return u.HasPerm(PermCreateDirs, target)
|
||||
|
|
@ -1135,7 +1152,7 @@ func (u *User) CanAddDirsFromWeb(target string) bool {
|
|||
// CanRenameFromWeb returns true if the client can rename objects from the web UI.
|
||||
// The specified src and dest are the source and target directories for the rename.
|
||||
func (u *User) CanRenameFromWeb(src, dest string) bool {
|
||||
if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
|
||||
if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
|
||||
return false
|
||||
}
|
||||
return u.HasAnyPerm(permsRenameAny, src) && u.HasAnyPerm(permsRenameAny, dest)
|
||||
|
|
@ -1144,7 +1161,7 @@ func (u *User) CanRenameFromWeb(src, dest string) bool {
|
|||
// CanDeleteFromWeb returns true if the client can delete objects from the web UI.
|
||||
// The specified target is the parent directory for the object to delete
|
||||
func (u *User) CanDeleteFromWeb(target string) bool {
|
||||
if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
|
||||
if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
|
||||
return false
|
||||
}
|
||||
return u.HasAnyPerm(permsDeleteAny, target)
|
||||
|
|
@ -1153,7 +1170,7 @@ func (u *User) CanDeleteFromWeb(target string) bool {
|
|||
// CanCopyFromWeb returns true if the client can copy objects from the web UI.
|
||||
// The specified src and dest are the source and target directories for the copy.
|
||||
func (u *User) CanCopyFromWeb(src, dest string) bool {
|
||||
if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
|
||||
if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
|
||||
return false
|
||||
}
|
||||
if !u.HasPerm(PermListItems, src) {
|
||||
|
|
@ -1213,7 +1230,7 @@ func (u *User) MustSetSecondFactor() bool {
|
|||
return true
|
||||
}
|
||||
for _, p := range u.Filters.TwoFactorAuthProtocols {
|
||||
if !util.Contains(u.Filters.TOTPConfig.Protocols, p) {
|
||||
if !slices.Contains(u.Filters.TOTPConfig.Protocols, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -1224,11 +1241,11 @@ func (u *User) MustSetSecondFactor() bool {
|
|||
// MustSetSecondFactorForProtocol returns true if the user must set a second factor authentication
|
||||
// for the specified protocol
|
||||
func (u *User) MustSetSecondFactorForProtocol(protocol string) bool {
|
||||
if util.Contains(u.Filters.TwoFactorAuthProtocols, protocol) {
|
||||
if slices.Contains(u.Filters.TwoFactorAuthProtocols, protocol) {
|
||||
if !u.Filters.TOTPConfig.Enabled {
|
||||
return true
|
||||
}
|
||||
if !util.Contains(u.Filters.TOTPConfig.Protocols, protocol) {
|
||||
if !slices.Contains(u.Filters.TOTPConfig.Protocols, protocol) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -1571,7 +1588,7 @@ func (u *User) mergeCryptFsConfig(group *Group) {
|
|||
|
||||
func (u *User) mergeWithPrimaryGroup(group *Group, replacer *strings.Replacer) {
|
||||
if group.UserSettings.HomeDir != "" {
|
||||
u.HomeDir = u.replacePlaceholder(group.UserSettings.HomeDir, replacer)
|
||||
u.HomeDir = filepath.Clean(u.replacePlaceholder(group.UserSettings.HomeDir, replacer))
|
||||
}
|
||||
if group.UserSettings.FsConfig.Provider != 0 {
|
||||
u.FsConfig = u.replaceFsConfigPlaceholders(group.UserSettings.FsConfig, replacer)
|
||||
|
|
@ -1748,6 +1765,17 @@ func (u *User) hasRole(role string) bool {
|
|||
return role == u.Role
|
||||
}
|
||||
|
||||
func (u *User) applyNamingRules() {
|
||||
u.Username = config.convertName(u.Username)
|
||||
u.Role = config.convertName(u.Role)
|
||||
for idx := range u.Groups {
|
||||
u.Groups[idx].Name = config.convertName(u.Groups[idx].Name)
|
||||
}
|
||||
for idx := range u.VirtualFolders {
|
||||
u.VirtualFolders[idx].Name = config.convertName(u.VirtualFolders[idx].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) getACopy() User {
|
||||
u.SetEmptySecretsIfNil()
|
||||
pubKeys := make([]string, len(u.PublicKeys))
|
||||
|
|
@ -1779,6 +1807,8 @@ func (u *User) getACopy() User {
|
|||
filters.TOTPConfig.Secret = u.Filters.TOTPConfig.Secret.Clone()
|
||||
filters.TOTPConfig.Protocols = make([]string, len(u.Filters.TOTPConfig.Protocols))
|
||||
copy(filters.TOTPConfig.Protocols, u.Filters.TOTPConfig.Protocols)
|
||||
filters.AdditionalEmails = make([]string, len(u.Filters.AdditionalEmails))
|
||||
copy(filters.AdditionalEmails, u.Filters.AdditionalEmails)
|
||||
filters.RecoveryCodes = make([]RecoveryCode, 0, len(u.Filters.RecoveryCodes))
|
||||
for _, code := range u.Filters.RecoveryCodes {
|
||||
if code.Secret == nil {
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ func TestBasicFTPHandlingCryptFs(t *testing.T) {
|
|||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats("")) == 0 }, 1*time.Second, 50*time.Millisecond)
|
||||
assert.Eventually(t, func() bool { return common.Connections.GetClientConnections() == 0 }, 1000*time.Millisecond,
|
||||
50*time.Millisecond)
|
||||
assert.Equal(t, int32(0), common.Connections.GetTotalTransfers())
|
||||
}
|
||||
|
||||
func TestBufferedCryptFs(t *testing.T) {
|
||||
|
|
@ -179,6 +180,7 @@ func TestBufferedCryptFs(t *testing.T) {
|
|||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats("")) == 0 }, 1*time.Second, 50*time.Millisecond)
|
||||
assert.Eventually(t, func() bool { return common.Connections.GetClientConnections() == 0 }, 1000*time.Millisecond,
|
||||
50*time.Millisecond)
|
||||
assert.Equal(t, int32(0), common.Connections.GetTotalTransfers())
|
||||
}
|
||||
|
||||
func TestZeroBytesTransfersCryptFs(t *testing.T) {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue