Compare commits

...

848 Commits

Author SHA1 Message Date
Nicola Murino
a503feaab6 set version to 2.5.2
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-17 18:33:56 +02:00
Nicola Murino
cba894987c WebClient: show user quota
Also remove per-source data transfer limits. This was an
oversight

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-16 21:31:15 +02:00
Nicola Murino
1d120bdd26 WebAdmin: don't show hidden deny policy for allowed patterns
The deny policy only applies to denied patterns, showing an allowed
pattern as hidden will confuse users

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-14 19:01:22 +02:00
Nicola Murino
7245710b31 CI: fix MariaDB initialization
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-12 20:04:48 +02:00
Nicola Murino
3a3df5670d WebAdmin: relax key prefix validation
try to automatically fix leading and trailing slashes

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-12 19:15:02 +02:00
Nicola Murino
97bbf37af4 branch 2.5.x
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-09 19:47:17 +02:00
Nicola Murino
d120957736 CI: set Go version to 1.20.5
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-08 19:44:19 +02:00
Nicola Murino
324d695d93 try to fix a randomly failing test case
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-08 19:41:58 +02:00
Nicola Murino
9d60972743 WebClient: redirect to the requested URL after login
This feature is only useful and enabled for file manager urls

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-08 18:14:47 +02:00
Nicola Murino
f938af5a61 WebClient: fix sorting by size
Fixes #1313

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-04 21:45:31 +02:00
Nicola Murino
9ccdc3a597 add code of conduct
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-04 09:03:58 +02:00
Nicola Murino
3499edd5c2 WebUI: remove leading and trailing spaces from user-submitted input
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-04 08:45:17 +02:00
Nicola Murino
9470cd6e69 multi-node installations: use a different backup path for each node
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-03 17:54:24 +02:00
Nicola Murino
1f7433e798 getting started guide: add a link to the available installation methods
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-03 17:29:12 +02:00
Nicola Murino
4ba3d026b4 update README
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-03 17:19:51 +02:00
Nicola Murino
98c639579f add issue templates
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-03 17:13:43 +02:00
Nicola Murino
74e5999c63 added support for verifying sha256/sha512 passwords hash
this simplifies the migration of users from some proprietary products

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-03 16:58:45 +02:00
Nicola Murino
48939b2b4f add XOAUTH2
start the countdown, let's see how long it takes for your favorite
Go-based proprietary SFTP server to notice this change, copy the SFTPGo
code and thus violate its license, and announce the same feature :)

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-03 16:17:32 +02:00
Nicola Murino
8339fee69d smtp: add debug option
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-30 19:11:28 +02:00
Nicola Murino
a2fc7d3cc5 update security policy
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-26 19:22:37 +02:00
Nicola Murino
ae7954eee2 WebUIs: fix disclaimer paths
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-26 17:59:38 +02:00
Nicola Murino
8f934f7c82 email action: allow to configure Bcc
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-25 19:55:27 +02:00
Nicola Murino
b2781e0bfc WebAdmin: Set TLS username to empty string if disabled
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-25 18:24:51 +02:00
Nicola Murino
e11473cf52 config: limit the size for env files
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-25 05:25:28 +02:00
Nicola Murino
f8f8962ccb file patterns: evaluate allowed filters before the denied ones
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-24 19:56:53 +02:00
Nicola Murino
2238043efd EventManager: add email field placeholder
Fixes #1288

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-24 19:08:51 +02:00
Nicola Murino
d9426cef20 docker: update docs
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-23 13:06:42 +02:00
Nicola Murino
052d586364 docker: remove distroless
Fix #1295

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-23 13:04:42 +02:00
Nicola Murino
11ba41e903 Revert "Docker: try to add CAP_NET_BIND_SERVICE to the binary"
This reverts commit 8d12872608.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-23 12:59:49 +02:00
Nicola Murino
255985b7b0 Windows: start the service in a goroutine
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-23 12:59:27 +02:00
Nicola Murino
2b77709a04 back to development
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-21 09:23:24 +02:00
Nicola Murino
5b4a1bda2e set version to 2.5.1
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-20 17:39:23 +02:00
Nicola Murino
3f94f6d0e7 proxy protocol: fix require policy in some edge cases
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-20 16:08:57 +02:00
Nicola Murino
d28a53a6cf webdav: fix caching with external auth/plugins
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-20 12:39:07 +02:00
Nicola Murino
963cec124e oidc docs: fix typo
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-18 18:43:46 +02:00
Nicola Murino
bbaca578cd EventManager: add content type option for email config
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-17 19:28:13 +02:00
Nicola Murino
da30389989 fix OpenAPI schema, update js deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-17 18:24:33 +02:00
Nicola Murino
52ec36dbd6 update pwd reset template. Update deps and use new features from the OIDC library
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-17 18:10:57 +02:00
Artem Kajalainen
b524d178dd docs: add info about IRSA for S3 authn
Signed-off-by: Artem Kajalainen <artem@iki.fi>
2023-05-16 19:35:13 +02:00
Nicola Murino
e0d9b8bddf WebClient: update password change timestamp after password reset
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-16 19:15:45 +02:00
Nicola Murino
19da923369 webdav: add support for parsing more time formats
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-16 18:51:42 +02:00
Nicola Murino
824a70b22d Docker Alpine: update to 3.18
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-16 18:23:41 +02:00
Nicola Murino
cea70d5d6b update security policy
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-16 18:17:35 +02:00
Nicola Murino
adad8e658b osfs: add optional buffering
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-16 18:08:14 +02:00
Nicola Murino
e10487ad57 EventManager: improve automatic detection of JSON body
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-12 19:22:50 +02:00
Nicola Murino
4eded56d5f add support for log events
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-12 18:34:59 +02:00
Krasimir Popov
43d011f125 Fixing double the typo in README
Signed-off-by: Krasimir Popov <kjpopovbg@gmail.com>
2023-05-08 15:50:28 +02:00
Daniel Hammer
a292044501 Aligned help example with v2.5.0 output
Signed-off-by: Daniel Hammer <daniel.hammer+oss@gmail.com>
2023-05-06 13:11:48 +02:00
Nicola Murino
05c54614b2 back to development
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-05 19:12:50 +02:00
Nicola Murino
32020e236f set version to 2.5.0
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-05-03 13:07:48 +02:00
Nicola Murino
b9cf6e5083 Add the link to the new Azure offer for Windows
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-04-26 13:53:05 +02:00
Nicola Murino
ee5b7290a0 EventManager: add more debug logs for HTTP actions
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-04-25 20:27:40 +02:00
Nicola Murino
fd6a44c562 OpenAPI: fix filesystem action types enum
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-04-23 14:46:09 +02:00
Nicola Murino
8d12872608 Docker: try to add CAP_NET_BIND_SERVICE to the binary
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-04-19 13:41:59 +02:00
Nicola Murino
712f2053a4 REST API dumpdata: allow to specify the resources to dump
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-04-18 18:11:23 +02:00
Nicola Murino
54462c26f2 WebAdmin: display undefined js objects as empty string
This is probably something that changed in the recent datatables update,
before it was handled automatically

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-04-16 15:38:49 +02:00
Nicola Murino
d0a171558d fix test cases for system commands
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-04-15 16:09:53 +02:00
Nicola Murino
1ade850557 add a log to better debug a randomically failing test case
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-04-15 15:08:42 +02:00
Nicola Murino
466f2e88b3 WebClient: fix rename
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-04-15 14:16:26 +02:00
Nicola Murino
3cb53b2c33 fix cross folder copy
also update css/js deps and other minor changes

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-04-13 18:23:42 +02:00
Nicola Murino
6279216c2e webdav: fix GET as PROPFIND if a prefix is defined
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-04-09 20:17:37 +02:00
Nicola Murino
5219c1fdd1 back to development
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-04-08 19:00:05 +02:00
Nicola Murino
4294659785 try harder to convert transfer errors in well-known error types
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-04-08 14:55:04 +02:00
Nicola Murino
f03f1b0156 improve test cases coverage
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-04-08 10:01:48 +02:00
Nicola Murino
184b99d500 user: add a field to indicate whether the password is set
A structure similar to the one used for secrets would be better,
but we don't want to break backwards compatibility.

Also document that omitting the password field in the request body
will preserve the current password when updating a user using the
REST API. Added a test case for this.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-04-06 18:22:09 +02:00
Nicola Murino
74f05e5305 EventManager: check the parent directory before creating a zip
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-04-03 18:53:13 +02:00
Nicola Murino
aefa7f77c2 add a link to the Terraform provider
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-04-02 18:48:56 +02:00
Nicola Murino
084d4109b8 WebAdmin: ensure to sanitize data before rendering
Thanks to Polina Zvorykina, VK for reporting this issue

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-03-28 12:28:38 +02:00
Nicola Murino
b60d3f680e user as JSON: rename 2fa_protocols to two_factor_protocols
This is a breaking change, but it is necessary to make JSON serialization of
users more compatible.
For example, Terraform does not allow JSON fields starting with numbers

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-03-26 15:57:53 +02:00
Nicola Murino
ee90bfb506 add unixcrypt build tag
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-03-26 10:33:30 +02:00
Nicola Murino
e17068a76f postgres provider: add support for load balancing
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-03-25 09:29:13 +01:00
Nicola Murino
354fc9b3d6 OIDC: allow to extract custom fields from sub-structs
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-03-23 18:15:07 +01:00
Nicola Murino
e29f6857db EventManager: add IDP login trigger and check account action
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-03-22 19:02:54 +01:00
Nicola Murino
40344ec0ff CI FreeBSD: compile and run tests using the same user
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-03-17 12:51:37 +01:00
Nicola Murino
783dff369b CI FreeBSD: install git
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-03-15 20:20:52 +01:00
Nicola Murino
72e0325d05 run test cases also on FreeBSD
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-03-15 19:44:45 +01:00
Nicola Murino
2710207779 update jquery, go deps, actions/setup-go to v4
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-03-15 18:44:08 +01:00
Nicola Murino
b719d03ebe WebAdmin: improve fs config layout
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-03-12 15:08:32 +01:00
Nicola Murino
84396343da fix some codeql warnings
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-03-10 17:30:06 +01:00
Nicola Murino
14242b59a2 oidc docs: add env vars config
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-03-09 18:58:36 +01:00
Nicola Murino
dad346cee8 add codeql
update deps

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-03-05 16:38:29 +01:00
Nicola Murino
04282f94a4 update js and css deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-03-04 16:14:16 +01:00
Nicola Murino
0423e8f157 httpd: generate defender events for failed 2fa and password resets
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-03-04 13:55:48 +01:00
Nicola Murino
bdcee06665 WebClient: remove the default upload size limit
Users who want a limit can still set it.
By default, we want to allow uploads of any size

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-03-02 18:26:21 +01:00
Nicola Murino
ae90ed2ba0 Docker: try again to add armv7 support
Let's see if the actions are more stable now

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-03-02 18:11:26 +01:00
Nicola Murino
4ba3ae876d allow to set password strength at user/group level
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-03-02 09:11:30 +01:00
Nicola Murino
662164c7ff smtp: require templates only if a server is configured or in service mode
This regression was introduced after recent changes to allow setting the SMTP
settings from the WebAdmin UI.

Fixes #1217

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-03-01 18:31:02 +01:00
Nicola Murino
fad6af11e5 don't expose error messages from pre-actions and post connect hooks
always return a generic error instead to avoid leaking internal info

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-28 18:01:09 +01:00
Nicola Murino
dba088daed printf: replace %#v with the more explicit %q
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-27 19:19:57 +01:00
Nicola Murino
a23fdea9e3 ftpd: allow hostnames as passive IP
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-27 19:19:50 +01:00
Nicola Murino
561976bcd0 WebClient: return proper status code for http.MaxBytesError
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-27 11:03:05 +01:00
Nicola Murino
874776bd12 also capture logs for pre-login and check-password commands
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-26 15:15:34 +01:00
Felix Eckhofer
ec67b67e9e Send output from external_auth_hook to logs
Signed-off-by: Felix Eckhofer <felix@eckhofer.com>
2023-02-26 07:39:34 +01:00
Felix Eckhofer
71f691b208 Fix potential ldap injection
Signed-off-by: Felix Eckhofer <felix@eckhofer.com>
2023-02-26 07:10:58 +01:00
Nicola Murino
e0cbb966f0 eventmanager: skip password expiration check for expired users
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-25 16:33:39 +01:00
Nicola Murino
df9d47900a eventmanager: add user/folders as comma separated string in errors
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-25 15:26:38 +01:00
Nicola Murino
b8496c4d6e eventmanager: add user expiration check
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-25 13:06:09 +01:00
Nicola Murino
b0cfaf189c portable mode: allow to read the password from a file
Fixes #1206

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-25 10:24:23 +01:00
Nicola Murino
195cb9f081 enable keyboard interactive authentication by default
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-24 20:22:32 +01:00
Nicola Murino
9a10740218 allow ACME HTTP-01 challenge with https redirect from port 80
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-24 20:08:14 +01:00
Nicola Murino
7bcd79a70a telemetry: improve test cases
remove an unnecessary nil check in tlsutils added as workaround
to make telemetry test cases work

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-24 11:05:46 +01:00
Nicola Murino
beb8822df4 examples: update deps
to silence dependabot alerts

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-23 19:25:30 +01:00
Nicola Murino
8805d85377 configs: add ACME section
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-23 19:25:20 +01:00
Nicola Murino
fcf9a8c673 scheduler: disable verbose logs
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-21 18:18:24 +01:00
Nicola Murino
2c1319985d sql providers: remove unnecessary []byte to string conversion
always check affected rows for updates

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-20 18:14:02 +01:00
Nicola Murino
a3fff56da5 WebAdmin: add configs section
Setting configurations is an experimental feature and is not currently
supported in the REST API

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-19 19:03:45 +01:00
Nicola Murino
14961a573f examples: update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-18 13:46:06 +01:00
Nicola Murino
78cd5d8eba groups: add expiration date override
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-13 19:32:36 +01:00
Nicola Murino
2df2803a37 ipfilter plugin: add protocol
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-13 13:45:45 +01:00
Nicola Murino
7738faa040 events: add elapsed to UI and exports
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-13 12:58:21 +01:00
Nicola Murino
157d1db0b1 fs events: add elapsed field to notifications
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-12 18:56:53 +01:00
Nicola Murino
7e85356325 WebClient shares: replace basic auth with a login form
basic auth will continue to work for REST API

Fixes #1166

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-12 08:29:53 +01:00
Nicola Murino
a3d0cf5ddf fix lint errors
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-10 19:59:03 +01:00
Nicola Murino
04ab8e72f6 WebUI: make error messages user dismissible
Fixes #1171

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-10 18:07:23 +01:00
Nicola Murino
e0c3a13ac5 azblob: update to the latest SDK
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-09 13:04:12 +01:00
Nicola Murino
1b1745b7f7 move IP/Network lists to the data provider
this is a backward incompatible change, all previous file based IP/network
lists will not work anymore

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-09 09:33:33 +01:00
Nicola Murino
2412a0a369 add Dendi to the sponsors section, thank you!!!
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-02 18:12:21 +01:00
Nicola Murino
1e14d006b1 defender: set score_no_auth to 0 as default
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-29 18:00:27 +01:00
Nicola Murino
27c4ffd663 sftpd: fix duplicate defender error introduced in the previous commit
improve the defender test cases by verifying the expected score

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-25 21:57:27 +01:00
Nicola Murino
c0fe08b597 defender: allow to set a different score for "no auth tried" events
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-25 18:49:03 +01:00
Nicola Murino
5550a5d2c0 update users: also disconnect users from remote nodes when requested
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-24 18:53:34 +01:00
Nicola Murino
2066ad7c83 WebDAV: allow to define custom MIME type mappings
Fixes #1154

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-23 18:43:25 +01:00
Nicola Murino
61199172d0 add support for monitoring and reloading externally provided TLS certs
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-22 18:31:14 +01:00
Nicola Murino
3ce4d04b27 EventManager: support placeholders within URL paths
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-22 08:46:58 +01:00
Nicola Murino
707729ee61 acme: allow to separate multiple domains with spaces
This change is required to be able to set multiple domains for the same
certificate using env vars.
The change is backward compatible for general use cases but may be
backward incompatible in some edge cases, for example:

- "sftpgo.com,www.sftpgo.com" will work as before
- "sftpgo.com, www.sftpgo.com" will not work anymore

Check the logs to see if you are affected and rename the certificate and key
to fix

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-21 18:00:23 +01:00
Nicola Murino
7b5bebc588 EventManager: add "on-demand" trigger
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-21 15:41:24 +01:00
Nicola Murino
53f17b5715 allow to disable event rules
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-19 18:33:04 +01:00
Nicola Murino
496c8bc785 allow to start if only httpd service is enabled
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-17 18:22:04 +01:00
Nicola Murino
396d67bb2c web: add spellcheck hint to some more fields
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-16 19:45:12 +01:00
Daniel Hammer
bbebd9b163 "Spell-Jacking" mitigation ~ prevent sensitive data leak from spell checker.
@see https://www.otto-js.com/news/article/chrome-and-edge-enhanced-spellcheck-features-expose-pii-even-your-passwords

Signed-off-by: Daniel Hammer <daniel.hammer+oss@gmail.com>
2023-01-16 19:23:43 +01:00
Nicola Murino
c8d94f0a27 add a health check command
Useful in restricted environments where commands like curl and such
are not available.

Fixes #1129

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-16 18:54:42 +01:00
Nicola Murino
8be8343fee README: fix link to Fs interface
Fixes #1142

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-15 15:28:55 +01:00
Nicola Murino
f3995901e3 OpenAPI: fix group settings documentation
the OpenAPI docs should really be improved, but nobody seems interested
enough to sponsor this work

Fixes #1141

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-15 15:28:52 +01:00
Nicola Murino
f2618e7de6 switch from go-simple-mail to go-mail
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-15 15:28:31 +01:00
Nicola Murino
6afbd77fd5 update css and js deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-07 18:11:46 +01:00
Nicola Murino
93e5cb36df copy: use server side copy if available
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-07 16:28:46 +01:00
Nicola Murino
09dea57850 back to development
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-07 13:07:41 +01:00
Nicola Murino
8cad436421 conditional support for recursive renaming for cloud providers
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-06 12:33:50 +01:00
Nicola Murino
f0dedbfabf eventmanager: auto-create destination folder for renames
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-03 18:13:01 +01:00
Nicola Murino
51f0ded222 update test certificates
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-03 11:48:08 +01:00
Jon Bendtsen
6b555cf0d8 metrics only available in telemetry server
I do not know which version removed /metrics from the HTTP server, but it does not seem to be available in 2.4.2, so I updated the metrics documentation to reflect this. Replaced with links to telemetry configuration.

Signed-off-by: Jon Bendtsen <github@jonb.dk>
2023-01-03 10:27:15 +01:00
Nicola Murino
0190d0b849 update Copyright year
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-03 10:18:30 +01:00
Nicola Murino
9977c64459 docs eventmanager: update index
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-02 19:22:26 +01:00
Nicola Murino
20706e45b0 docs: basic example for a Recycle Bin function
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-02 18:51:45 +01:00
Jon Bendtsen
53864fd8c1 Add warning of docker grace vs. SFTPGo grace
Dockers default grace period is only 10 seconds, so added a warning to alert users to those cases where their SFTPGO_GRACE_TIME is larger than the docker grace
2023-01-02 17:09:59 +01:00
Nicola Murino
7fa0959af4 eventmanager: add support for global star path matching
This introduce a backward incompatible change for filesystem path matching
in the Event Manager, now patterns like "*.txt" will no longer match any
file with the "txt" suffix, you need to change them to "/**/*.txt".

Also change pre-delete behaviour, now if an error is returned the client
will get a permission denied error. This is the same as the other pre-*
action. Previously it was not possible to deny deletion of a file.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-02 15:59:00 +01:00
Nicola Murino
2611dd2c98 eventmanager: add support for pre-* actions
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-01-01 17:59:41 +01:00
Nicola Murino
6cebc037a0 eventmanager: check disk quota before executing the compress action
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-31 16:41:32 +01:00
Nicola Murino
15ad31da54 WebClient: add copy action
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-30 19:30:16 +01:00
Nicola Murino
fe9904a54d docs full-configuration: improve formatting
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-28 18:51:25 +01:00
Nicola Murino
831851c0c3 change the default value for naming rules
WebAdmin does not work properly is trimming trailing and leading white
spaces is disabled

Fixes #1119

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-27 18:57:48 +01:00
Nicola Murino
ea4c4dd57f eventmanager: add copy action
refactor sftpgo-copy and sftpgo-remove commands

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-27 18:51:53 +01:00
Nicola Murino
e5a8220b8a REST API: add location header to 201 responses
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-23 13:08:04 +01:00
Jon Bendtsen
ed949604d3 Added graceful shutdown description to docker (#1112)
* Added graceful shutdown description to docker

Describing how to use the graceful shutdown period in a docker SFTPGO container and giving some examples of what happens with both existing and new connections.

Signed-off-by: Jon Bendtsen <github@jonb.dk>
2022-12-23 12:11:15 +01:00
Nicola Murino
0841c7d7bd REST API: remove merging of fields on updates
we use PUT verb not PATCH. We keep merging only to allow to preserve
hidden/encrypted fields.

This is a backward incompatible change, but is necessary to avoid unexpected
issues.
You have to pass complete objects on updates.

Fixes #1088

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-23 09:36:20 +01:00
Nicola Murino
e17975ed7d dataprovider: include port in node name and make it a hash
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-20 16:40:32 +01:00
Nicola Murino
f4eb9e7cd6 OpenAPI: set charset also for text/plain responses
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-19 18:38:23 +01:00
Nicola Murino
1085f9e5ec httpfs OpenAPI: added charset=utf-8 to application/json content type
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-19 18:35:06 +01:00
Jon Bendtsen
37eceffed9 OpenAPI: added charset=utf-8 to application/json content type (#1108)
* Added charset=utf-8 to application/json content type

This change is linked to https://github.com/drakkan/sftpgo/issues/1101 and should partially alleviate the need to change the content type in the files generated by openapi-generator-cli

Signed-off-by: Jon Bendtsen <github@jonb.dk>

* extra newline

Signed-off-by: Jon Bendtsen <github@jonb.dk>

* Signed-off-by: Jon Bendtsen github@jonb.dk

Signed-off-by: Jon Bendtsen github@jonb.dk
Signed-off-by: Jon Bendtsen <github@jonb.dk>

* This change is linked to #1101 and should partially alleviate the need to change the content type in the files generated by openapi-generator-cli.

Signed-off-by: Jon Bendtsen <github@jonb.dk>

Signed-off-by: Jon Bendtsen <github@jonb.dk>
Signed-off-by: Jon Bendtsen github@jonb.dk
2022-12-19 18:30:27 +01:00
Nicola Murino
6270b2c2d3 eventmanager: log a get task error only when required
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-19 18:10:40 +01:00
Nicola Murino
ad5bd18dd0 CI: add nosqlite build tag when CGO is disabled
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-18 15:21:32 +01:00
Nicola Murino
0296e0cafa gcsfs: allow to customize upload part size/time
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-18 11:51:46 +01:00
Nicola Murino
147ad3b230 respect token validation mode for CSRF header
Fixes #1104

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-16 19:14:56 +01:00
Nicola Murino
2da3eabc12 eventmanager: add password notification check action
this action allow to send an email notification to users whose
password is about to expire

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-16 18:51:29 +01:00
Nicola Murino
ac91170d65 S3: improve "directories" detection
Fixes #1097

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-13 08:55:01 +01:00
Nicola Murino
f13b901f2d local fs: fixed paths validation for some Windows specific edge cases
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-12 10:40:04 +01:00
Nicola Murino
c23c73ed34 update OpenAPI definition
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-11 17:53:41 +01:00
Nicola Murino
ad5d657a1a add support for password policies
you can now set a password expiration and the password change requirement

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-11 17:15:34 +01:00
Nicola Murino
e2bebc99d1 AzureBlobs: update SDK to v0.6.1
Remove path escape for blob names, this issue is now fixed within
the SDK

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-10 09:44:14 +01:00
Nicola Murino
926dcbbc63 add a CLI command to reset admin passwords
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-09 18:28:16 +01:00
Nicola Murino
a7f9581d99 provider events: add support for omit_object_data search param
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-08 10:02:12 +01:00
Nicola Murino
75d911f29e WebAdmin: allow to search and export event logs
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-07 18:47:38 +01:00
Nicola Murino
91e4a54385 fix build with some features disabled
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-04 08:44:45 +01:00
Nicola Murino
221a4878aa eventmanager: allow to filter based on role name
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-03 17:47:43 +01:00
Nicola Murino
2ea43647ed ftpd: check the TYPE parameter in a case-insensitive manner
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-03 13:09:25 +01:00
Nicola Murino
04bdd3a5e4 docker: bump alpine to 3.17
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-03 12:30:53 +01:00
Nicola Murino
1f9cf194fe add role to events
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-12-03 11:45:27 +01:00
Nicola Murino
e87118d2a8 allow WebClient login with multi-step auth enabled
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-29 18:43:48 +01:00
Nicola Murino
fe888729f9 back to development
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-27 12:15:56 +01:00
Nicola Murino
d7cd2ac803 add CODEOWNERS file
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-24 18:53:59 +01:00
Nicola Murino
ba9fe38b8b azblob: handle dirs metadata
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-24 18:14:24 +01:00
Nicola Murino
7b00fe3d5a update nfpm to 2.22.1
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-20 15:23:45 +01:00
Nicola Murino
fc1ba36ae5 fix SeaweedFS rename compatibility
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-20 13:06:58 +01:00
Nicola Murino
2290137868 WebDAV: add support for X-OC-Mtime header
it is used by Nextcloud compatible clients to set the modification time

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-19 19:39:28 +01:00
Nicola Murino
6ebe7691db WebClient: add drag and drop upload UI
thanks to @wooneusean for the help

Fixes #951

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-19 12:31:03 +01:00
Nicola Murino
29d1993a3b Docker: add a default moduli file
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-18 18:13:03 +01:00
Nicola Murino
81c693de4e Ignore denied patterns for stat on "/"
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-18 18:12:37 +01:00
Nicola Murino
2017cb60e9 Per-directory permissions: add wildcards support
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-18 18:12:04 +01:00
Nicola Murino
ec4cc33364 WebAdmin users form: trim spaces from some form fields
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-17 18:26:19 +01:00
Nicola Murino
a22282f275 add support for DHGEX
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-17 18:15:53 +01:00
Nicola Murino
67de4c9c07 check more mime types for SeaweedFS dirs
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-16 21:38:27 +01:00
Amir.h Yeganemehr
6591769a07 Handle empty directories with mimetype
Signed-off-by: Amir.h Yeganemehr <yeganemehr@jeyserver.com>
2022-11-16 19:47:22 +01:00
Nicola Murino
5a222807b7 add roles
Fixes #837

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-16 19:04:50 +01:00
Nicola Murino
a9207857cf webdav: add a test case for PROPFIND with infinity Depth
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-06 07:33:56 +01:00
Nicola Murino
37ffa3b55a portable mode: remove support for services discovery via multicast DNS
The library used for mDNS doesn't seem well maintained and I think this
feature is rarely used

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-05 18:32:36 +01:00
Nicola Murino
048591553a allow to set a default expiration for newly created users
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-05 18:01:24 +01:00
Nicola Murino
33bfd61a0c plugins: fix hash check
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-04 20:25:01 +01:00
Nicola Murino
965d059400 WebUI: try harder to prevent browsers from auto-filling in password fields
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-03 19:57:43 +01:00
Nicola Murino
676286182a webdav: always open files for reading in lazy mode
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-03 08:31:40 +01:00
Nicola Murino
3b2002d9ef shared providers: allow to immediately re-add soft-deleted event rules
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-01 17:39:53 +01:00
Nicola Murino
9d7e30807d WebDAV: make test cases more robust
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-01 13:42:42 +01:00
Nicola Murino
91fae5c4d4 shared providers: allow to immediately re-add soft-deleted users
there is no need to wait for cache updates

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-01 12:53:08 +01:00
Nicola Murino
e3e85867b1 sftpfs: reuse connections
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-01 12:22:54 +01:00
Nicola Murino
5618b95372 improve some docs
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-30 08:34:16 +01:00
Nicola Murino
bf45d04600 eventmanager: add placeholder to get the parent directory
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-29 15:49:24 +02:00
Nicola Murino
80244bd83b eventmanager: allow to access the backup file
so it can be used in email and other actions

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-29 14:04:31 +02:00
Nicola Murino
9a9e7d1a7f squash database migrations
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-28 14:28:37 +02:00
Nicola Murino
6f422c3d8b WebClient: make folder deletion recursive
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-27 08:27:44 +02:00
Nicola Murino
222f0c735b back to development
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-23 09:20:00 +02:00
Nicola Murino
63bf8eb1a1 set version to 2.4.0
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-23 04:47:41 +02:00
Nicola Murino
db0e58ae7e Add support for graceful shutdown
Fixes #1014

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-22 11:56:41 +02:00
Nicola Murino
87045284cc make connections lookups constant time
Performance improves if there are many active connections.
For a few connections there is a small (unnoticeable) performance
degradation

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-20 18:17:13 +02:00
Nicola Murino
f3ee20980a fix build in bundle mode
added bunlde mode build to CI to prevent this from happening again
in the future

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-20 07:58:34 +02:00
Nicola Murino
54f1946aba OIDC: allow to skip JWT signature validation
It's intended for special cases where providers,such as Azure,
use the "none" algorithm

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-19 18:38:09 +02:00
Nicola Murino
47842ae614 script based hooks: don't propagate global env vars
env vars must be explicitly set

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-19 09:29:40 +02:00
Nicola Murino
7e0b62b703 update swagger-ui, codemirror, video-js
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-16 18:29:42 +02:00
Nicola Murino
15b4194e8f event rules: allow to set min/max file size using "human" notation
10MB or 1GB instead of the size in bytes

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-16 15:28:47 +02:00
Nicola Murino
5a199acbb2 howto: add event manager
add groups section in the getting started guide.
Suggest to prefer configuration with env vars instead of modifying
the default configuration file

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-16 08:26:03 +02:00
Nicola Murino
07b3f2f4d6 config: fix for slices with default values
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-14 16:45:20 +02:00
Nicola Murino
13ee236884 Allow to read env vars from files inside the "env.d" directory
This makes it easier to set environment variables on some operating systems.
Setting configuration options from environment variables is recommended if
you want to avoid the time-consuming task of merging your changes with the
default configuration file after upgrading SFTPGo

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-13 18:43:58 +02:00
Nicola Murino
3822b7d3f7 workflows: replace deprecated set-output command
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-13 13:11:22 +02:00
Nicola Murino
2b2b69fb23 back to development
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-12 22:03:47 +02:00
Nicola Murino
4b4edef0ad disable self connections by default
now that the event manager can create files, self connections may create
even more issues than before

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-12 18:12:12 +02:00
Nicola Murino
aa1e73326f FTPD: fix APPE to new files
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-12 11:37:31 +02:00
Nicola Murino
07012aa812 WebDAV: allow to set last modification time
This commit add a minimal dead properties implementation

Fixes #1018

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-11 19:20:58 +02:00
Nicola Murino
0e54fa5655 cryptfs: fix quota for overwrites if upload fails
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-10 19:34:15 +02:00
Nicola Murino
3e44a1dd2d eventmanager: add support for file/directory compression
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-10 18:53:58 +02:00
Nicola Murino
a417df60b3 azblob: use UUIDs as block IDs
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-07 06:54:26 +02:00
Nicola Murino
2067c5c527 azblob: rename method to initialize from SAS URL
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-02 15:03:38 +02:00
Nicola Murino
8a43486730 postgres driver: add multi hosts support
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-02 12:43:26 +02:00
Nicola Murino
2636fedce8 node token: add/pars admin username
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-02 09:51:47 +02:00
Nicola Murino
a42e9ffa6b azblob: add support for the latest SDK
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-10-01 14:04:53 +02:00
Nicola Murino
0e8c41bbd1 sftpd: fix relative symlinks handling
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-30 19:23:54 +02:00
Nicola Murino
1e21aa9453 add support for checking sha256crypt passwords
they will be converted to the configured password hashing algorithm after
the first user login

Fixes #1000

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-28 19:15:02 +02:00
Nicola Murino
f9eadd7f04 API data retention check: send CSV reports for email notifications
replace the HTML email with the same CSV report used in the
event manager

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-28 18:37:32 +02:00
Nicola Murino
04dc97072b eventmanager: add metadata check
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-26 19:00:34 +02:00
Nicola Murino
ddda0b5ece SQLite provider: remove code only used for shared providers
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-25 20:29:43 +02:00
Nicola Murino
76e89d07d4 add support for inter-node communications
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-25 19:48:55 +02:00
Nicola Murino
a538255034 httpclient: add leaf certificates
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-23 17:49:42 +02:00
Nicola Murino
4ad2a9c1fa WebClient: validate PDF files before rendering
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-22 20:41:28 +02:00
Nicola Murino
7ae9303c99 allow to disable REST API
Fixes #987

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-22 17:27:00 +02:00
Nicola Murino
6c7b3ac5bb oidc: update user after token refresh
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-22 08:30:22 +02:00
Nicola Murino
bd294bb3cf WebAdmin: allow to simplify the user page
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-21 19:36:08 +02:00
Nicola Murino
7349598b19 command hooks: allow to pass custom arguments
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-20 13:58:44 +02:00
Nicola Murino
7f19f9f39c WebClient: allow partial download of shared files
each partial download will count as a share usage

Fixes #970

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-19 19:58:35 +02:00
Nicola Murino
f19691250d zip downloads: make zip entries relative to the current dir when possible
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-19 17:06:42 +02:00
Nicola Murino
554a1cb1f4 back to development
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-18 13:08:48 +02:00
Nicola Murino
e54237ff70 allow a client if its IP is both allowed and denied
this allows you to define a group deny policy that can be overridden
on a per-user basis.

This is a backward incompatible change

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-15 19:51:17 +02:00
Nicola Murino
e58709c822 WebAdmin: allow to specify quota and upload size in human format
For example 1 GB

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-14 21:18:32 +02:00
Nicola Murino
5eca73a399 give some hints if we fail to load HTML templates
Fixes #986

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-14 12:25:19 +02:00
Nicola Murino
f8a19f747d WebUI: improve HTML escaping
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-13 19:16:07 +02:00
Nicola Murino
ea3c1d7a3b WebAdmin: allow to pre-select groups on add user page
The admin will still be able to choose different groups

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-13 18:04:27 +02:00
Nicola Murino
bd585d8e52 CI: add commit info in vendored sources
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-08 17:32:28 +02:00
Nicola Murino
a40fa93d7b CI: use the shortened 8-digit commit hash everywhere
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-08 13:44:14 +02:00
Nicola Murino
4498bbf2e4 CI: use Docker to build x86_64 Linux packages
therefore Linux packages are compiled with Docker for all supported
architectures

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-08 09:42:36 +02:00
Nicola Murino
63e3891808 WebClient/HTTP API: ensure to check home dir, when needed, in multi-node setups
Behind a load balancer with no sticky sessions enabled is not enough to check
the home dir only when the client logs in

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-07 16:23:56 +02:00
Nicola Murino
3ebdfa9b2d data providers: allow to disable SNI for TLS connections
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-07 14:31:50 +02:00
Nicola Murino
8debde842c eventmanager: improve docs
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-06 19:46:06 +02:00
Nicola Murino
3e5cf56460 eventmanager: add data retention reports
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-06 19:09:23 +02:00
Nicola Murino
f264b005ff event rules: allow filtering based on group names
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-04 17:48:09 +02:00
Nicola Murino
bf76b0b158 docs external auth: clarify the meaning of the empty response from the hooks
Fixes #961

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-03 19:46:08 +02:00
Nicola Murino
c2a65a9a74 http actions: add multipart support
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-09-03 16:29:07 +02:00
Nicola Murino
3267a50ae3 MFA: allow recovery codes only if two-factor auth is enabled
Fixes #965

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-31 09:29:39 +02:00
Nicola Murino
f0839519a8 FTP: always generate a defender event if the client does not authenticate
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-30 17:14:57 +02:00
Nicola Murino
95e9106902 use the new atomic types introduced in Go 1.19
we depend on Go 1.19 anyway

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-30 15:47:41 +02:00
Nicola Murino
da03f6c4e3 eventmanager commands: allow to pass custom arguments
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-30 12:37:18 +02:00
Nicola Murino
9e77cd1a26 clarify support policy
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-29 19:14:48 +02:00
Nicola Murino
56bf51277c eventmanager placeholders: add StatusString and ErrorString
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-29 19:03:31 +02:00
Nicola Murino
37d98ca290 users: add a setting to set the default expiration for shares
Fixes #960

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-28 14:41:42 +02:00
Nicola Murino
9473dc3937 WebAdmin: fix saving email event actions without attachments
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-22 20:17:45 +02:00
Nicola Murino
6777008aec eventmanager: allow to add attachments to email actions
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-22 19:04:17 +02:00
Nicola Murino
3e8254e398 fs actions: add first upload/download
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-21 19:01:08 +02:00
Nicola Murino
9ddd2d3588 eventmanager: add path exists filesystem action
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-20 14:13:43 +02:00
Nicola Murino
57935f585c eventmanager: allow to execute fs actions based on schedules
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-19 15:04:00 +02:00
Nicola Murino
2b463d61e3 use epoch timestamp instead of current timestamp for unknown modification times
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-16 17:59:13 +02:00
Nicola Murino
ced4206c5f allow cross folder renaming if the underlying resource is the same
this was only allowed for the local filesystem before this change

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-15 21:39:04 +02:00
Nicola Murino
c86db09cd8 event manager: add Certificate renewal trigger
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-12 17:37:29 +02:00
Nicola Murino
194c3c13ac event manager: add IP blocked trigger
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-11 20:09:53 +02:00
Nicola Murino
d65c00728a docker: add a variant with official plugins included
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-11 11:27:35 +02:00
Nicola Murino
526f6e0f6b cloud storage providers: remove head bucket requests
let's just assume the bucket exists on "stat" requests for the "/" path

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-11 08:31:51 +02:00
Nicola Murino
a61211d32c OIDC: allow to get the role field from a sub-struct
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-10 21:42:58 +02:00
Nicola Murino
78f75cdcb9 eventmanager: don't fail if a directory to be created already exists
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-10 19:33:02 +02:00
Nicola Murino
4cd340e07f eventmanager: add support for filesystem actions
Fixes #931

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-10 18:41:59 +02:00
Nicola Murino
890dde0e00 back to development
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-05 18:48:58 +02:00
Nicola Murino
b1efe8d0b5 eventmanager: add support for data retention checks
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-04 21:50:38 +02:00
Nicola Murino
71fff28d29 add Aledade to the sponsors section, thank you!!!
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-03 21:47:13 +02:00
Nicola Murino
6bfdf941bc webdav: allow to disable the WWW-Authenticate header
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-02 19:06:49 +02:00
Nicola Murino
fdc10aa6c7 CORS: add support for more parameters
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-02 18:44:34 +02:00
Nicola Murino
455bb550ee azblob: fix SAS URL with embedded container name
Fixes #944

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-01 21:32:40 +02:00
Nicola Murino
2a827544ef allow to edit profile to users logged in via OIDC
Fixes #942

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-01 19:41:18 +02:00
Nicola Murino
9d2b5dc07d refactor: move eventmanager to common package
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-08-01 18:48:54 +02:00
Nicola Murino
3ca62d76d7 back to development
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-30 10:07:09 +02:00
Nicola Murino
00b9280834 docs: some improvements and clarifications
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-27 20:49:22 +02:00
Nicola Murino
ef0a3bc571 add support for anonymous users
Fixes #935

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-27 18:54:25 +02:00
Nicola Murino
e3c5cf981f download as zip: improve filename
include username and also filename/directory name if the user downloads
a single file/directory

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-26 19:05:42 +02:00
Nicola Murino
ec5da8b4a5 ftpd: allow to require TLS on a per-user basis
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-26 18:51:39 +02:00
Nicola Murino
81de7d271e add support for embedding templates and other static resources
This feature is disabled by default and can be enabled using the
"bundle" build tag

Fixes #823

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-24 20:02:37 +02:00
Nicola Murino
c8158e14e0 move SFTPGo package to the internal folder
SFTPGo is a daemon and command line tool, not a library.

The public API are provided by the SDK

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-24 16:18:54 +02:00
Nicola Murino
e96ae5ca51 add folders to data provider actions
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-24 08:10:23 +02:00
Nicola Murino
e059197398 WebClient: show images as gallery
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-23 21:12:16 +02:00
Nicola Murino
a2e73228d2 initprovider: don't execute actions
we are not running as service here

Fixes #932

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-23 19:38:15 +02:00
Nicola Murino
1470018054 web UI: allow to enable OIDC login and/or login forms
any combination is now supported

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-22 20:55:33 +02:00
Nicola Murino
e6bfbcd489 OIDC: allow to debug the received id_token
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-22 11:11:35 +02:00
Nicola Murino
a0bbcf6ebb web client: add HTML5 player
See #914

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-21 18:42:22 +02:00
Nicola Murino
7f5a13d185 fix unused parameter lint warnings
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-19 23:28:33 +02:00
Nicola Murino
d5946da1e2 OIDC: allow to enable only OIDC login for Web UIs
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-19 22:25:00 +02:00
Nicola Murino
21682d1c1d add license header to source files
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-17 20:16:00 +02:00
Nicola Murino
fd52475ae2 shared mode: ensure to clear webdav cache for deleted users
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-17 18:48:41 +02:00
Nicola Murino
55b47cf741 sftp realpath: resolve symlinks
Fixes #890

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-17 16:02:45 +02:00
Nicola Murino
e0ce2e2e8a allow to customize the log level
The old log-verbose flag is not appropriate anymore.
You should now use the log-level flag to set your preferred log level.
The default level is "debug" as before, you can also set "info", "warn",
"error"

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-13 10:40:24 +02:00
Nicola Murino
8fc4971df1 ftpd: fix wildcards handling for backend with virtual dirs
Fixes #915

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-12 12:04:01 +02:00
Nicola Murino
20e8cb898a always check root dir in multi node setups
Fixes #920

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-12 08:32:31 +02:00
Nicola Murino
b5894b257f try to better highlight donations and sponsorships options ...
... and to better explain why they are required.

Please don't say "someone else will help the project, I'll just use it"

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-11 16:14:44 +02:00
Nicola Murino
cb517a3595 SFTPGo is now available on Elest.io
Purchasing from there will help keep SFTPGo a long-term sustainable project.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-11 09:39:01 +02:00
Nicola Murino
1b8f94c08f add event manager
auto backup removed from setting. You can now schedule backups with
the event manager

Fixes #762

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-07-11 08:17:36 +02:00
Nicola Murino
e46051299f s3: improve rename performance
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-30 18:22:58 +02:00
maximethebault
bf2dcfe307 S3: Fix timeout error when renaming large files (#899)
Remove AWS SDK Transport ResponseHeaderTimeout (finer-grained timeout are already handled by the callers)
Lower the threshold for MultipartCopy (5GB -> 500MB) to improve copy performance and reduce chance of hitting Single part copy timeout

Fixes #898

Signed-off-by: Maxime Thébault <contact@maximethebault.me>
2022-06-30 10:23:39 +02:00
Nicola Murino
719f6077ab don't expose underlying errors to clients
log them and return a generic failure

Fixes #896

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-28 22:08:16 +02:00
Nicola Murino
101783ee86 config: fix replace from env vars for some sub list
ensure to merge configuration from files with configuration from env for
all the sub lists

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-28 19:44:12 +02:00
Nicola Murino
6843402d2e fix get branding from env
Fixes #895

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-28 10:48:15 +02:00
Nicola Murino
88feda6bf9 clarify licensing
Fixes #891

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-27 18:04:24 +02:00
Nicola Murino
5c446ff645 subsystem mode: advertise the supported extensions
Fixes #889

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-25 18:05:13 +02:00
Nicola Murino
9a6b1a1315 Fix issues found in PR #887
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-25 10:42:17 +02:00
Andre Mainka
90009a649d Allow OAuth Scope to be configured (#887)
Signed-off-by: BobSilent <andre_1@gmx.net>
2022-06-25 10:40:39 +02:00
Nicola Murino
8762628481 backup: include folders set on group
also fix sql tables prefix handling and add the sql prefix to CI

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-24 13:16:45 +02:00
Nicola Murino
a5e41c9336 S3: allow empty region
the region may be embedded within the endpoint for some S3 compatible
object storage

Fixes #884

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-20 19:55:01 +02:00
Nicola Murino
729f30aebf allow to refuse an upload from a sync upload hook
Fixes #880

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-16 18:42:17 +02:00
Nicola Murino
1da213a6e3 CI: test resetprovider
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-15 20:08:31 +02:00
Nicola Murino
2b0b19da9e add metrics for httpgs and sftpfs
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-14 19:37:25 +02:00
Nicola Murino
686166f2ce remove deprecated APIs
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-14 18:30:57 +02:00
Nicola Murino
93ce593ed0 squash database migrations and remove the credentials_path setting
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-13 20:08:49 +02:00
Nicola Murino
6f4475ff72 httpfs: add support for UNIX domain sockets
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-12 18:29:49 +02:00
Nicola Murino
0b9a96ec6b restore fast path for recursive permissions check and update some docs
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-12 12:04:48 +02:00
Nicola Murino
f0f5ee392b OpenAPI schema: improve compatibility with some generators
Fixes #875

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-11 19:07:05 +02:00
Nicola Murino
dadaca141a sql providers: remove prepared statements
preparing a statement without reusing it is useless

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-11 11:57:06 +02:00
Nicola Murino
7ab30099dd add httpfs
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-11 10:41:34 +02:00
Nicola Murino
3170991aa8 MySQL: groups is a reserved keyword since MySQL 8.0.2
add MySQL to CI, testing with MariaDB is not enough

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-10 18:08:15 +02:00
Nicola Murino
118744a860 parse IP proxy header also if listening on UNIX domain socket
Fixes #867

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-09 08:37:37 +02:00
Nicola Murino
fe6a3f2ce8 web UIs: fix date formatting on Safari
Fixes #869

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-09 07:37:50 +02:00
Nicola Murino
75efaa9741 APT and YUM repo are now available
This is possible thanks to the Oregon State University's free
mirroring service

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-09 07:30:09 +02:00
Nicola Murino
560e7f316a allow to set an additional shared data path at build time
This is useful to simplify brew package

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-04 21:24:50 +02:00
Nicola Murino
eee5d74e87 back to development
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-04 19:29:46 +02:00
Nicola Murino
9ae473fcdc set version to 2.3.0
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-04 05:01:48 +02:00
Nicola Murino
b774289c6d change default value for naming_rules to 1
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-03 16:09:02 +02:00
Nicola Murino
ecf715880f update docs
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-03 14:36:38 +02:00
Nicola Murino
b2e28fe3a2 groups: apply placeholders to the fs config of virtual folders
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-06-02 09:45:01 +02:00
Nicola Murino
cc2f23bd89 trim values for string lists which can be set as env vars
See #857

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-31 18:22:18 +02:00
Nicola Murino
7329cd804b Fixes #855
update OpenAPI definition, add test cases, fix lint

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-30 19:01:12 +02:00
sunilke
84e3132ed1 Feat private key passphrase for sftpfs (#855)
Signed-off-by: Sunil Keswani <sunilke@zeta.tech>
2022-05-30 19:00:39 +02:00
Nicola Murino
f6b11c2d01 httpd/webdav: allow to configure trusted proxy header and depth
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-28 19:47:23 +02:00
Nicola Murino
32da923dfe httpd: add a setting to customize tokens validation
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-28 13:28:50 +02:00
Nicola Murino
91dfa501f8 improve some docs
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-27 10:09:53 +02:00
Nicola Murino
7c724e18fe add support for ACME compliant certificate authorities
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-27 07:39:55 +02:00
Nicola Murino
302f83c7a4 CI: fix for cockroach 22
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-24 11:31:49 +02:00
Nicola Murino
984ca1fb7e web UIs: update js and css deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-23 19:14:39 +02:00
Nicola Murino
87f6a18476 web admin UI: add column visibility control to the groups table as well
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-22 19:19:14 +02:00
Nicola Murino
90c21458b8 OIDC: add support for implicit roles
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-22 14:38:25 +02:00
Nicola Murino
f536c64043 admin UI: allow to control columns visibility and ordering
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-22 11:45:49 +02:00
Nicola Murino
1a33b5bb53 allow different TLS certificates for each binding
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-21 16:34:47 +02:00
Nicola Murino
0ecaa862bd web UIs: allow to replace the default CSS
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-21 11:05:58 +02:00
Nicola Murino
751946f47a allow to customize timeout and env vars for program based hooks
Fixes #847

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-20 19:30:54 +02:00
Nicola Murino
796ea1dde9 allow to store temporary sessions within the data provider
so we can persist password reset codes, OIDC auth sessions and tokens.
These features will also work in multi-node setups without sicky
sessions now

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-19 19:49:51 +02:00
Tim Birkett
a87aa9b98e feat: make MFA status visible in WebAdmin (#844)
Signed-off-by: Tim Birkett <tim.birkett@sainsburys.co.uk>
2022-05-17 19:27:12 +02:00
Nicola Murino
9abd186166 external auth http hook: properly serialize the user in the POST body
For historical reasons we send the json serialized user as a string field.
I Initially copied the code used in the script hook where it is appropriate
to convert the JSON user to string.

After some time I have noticed this error, I know that changing it now might
break existing external authentication hooks but we cannot continue with
this mistake, new users are surprised by this behavior, sorry

Fixes #836

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-15 18:26:07 +02:00
Nicola Murino
18d0bf9dc3 execute db migrations holding a database-level lock
so migrations cannot be executed concurrently if you run them from multiple
SFTPGo instances at the same time.

CockroachDB doesn't support database-level locks

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-15 15:25:12 +02:00
Nicola Murino
c9bd08cf9c UI branding: use the short name on the login pages
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-15 07:30:36 +02:00
Nicola Murino
d2f4edcdb6 sftpd statvfs: check the virtual quota against that of the filesystem
if the virtual quota limit is greater than the filesystem available space,
we need to return the filesystem limits

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-14 14:53:26 +02:00
Nicola Murino
67abf03fe3 web UIs: move common css to a separate template file
so we can reuse it instead of copying the same CSS every time

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-14 11:54:55 +02:00
Nicola Murino
5d7f6960f3 web UIs: add branding support
Fixes #829

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-13 19:40:52 +02:00
Paul Laffitte
4bea9ed760 add sftpgo logo on login pages (#835)
Signed-off-by: Paul Laffitte <paul.laffitte@enix.fr>
2022-05-13 17:12:52 +02:00
Nicola Murino
4995cf1b02 defender: allow to load blocklist/safelist also from config/env vars
Fixes #831

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-13 14:46:07 +02:00
Tim Birkett
a5d0cbbe44 chore: fix a linting error (#834)
Signed-off-by: Tim Birkett <tim.birkett@sainsburys.co.uk>
2022-05-13 11:14:38 +02:00
Tim Birkett
7b1a0d3cd3 chore: disallow all crawlers with robots.txt (#833)
Signed-off-by: Tim Birkett <tim.birkett@sainsburys.co.uk>
2022-05-13 09:23:43 +02:00
Nicola Murino
1e0b3a2a8c web client: add share mode read/write
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-09 19:09:43 +02:00
Nicola Murino
e72bb1e124 allow building with bolt provider disabled
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-09 11:19:29 +02:00
Nicola Murino
164621289c awscontainer: add a flag to disable the installation code
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-07 12:50:49 +02:00
Nicola Murino
737109b2b8 sftpfs: add more ciphers, KEXs and MACs
they are negotiated according to the order.
Restrictions are generally configured server side.
I want to avoid to expose other settings for now.

Fixes #817

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-06 09:21:57 +02:00
Herbert He
8b8e27b702 docs(cn): support README translation for Simplified Chinese (#818)
Signed-off-by: Herbert <herbert.he0229@gmail.com>
2022-05-05 19:15:49 +02:00
Dylan Legendre
4b099640de Updating typos in openapi/swagger documentation as well as various markdown documentation files (#816)
Signed-off-by: Dylan Legendre <dylanlegendre09@gmail.com>
2022-05-05 18:26:22 +02:00
Nicola Murino
80da2dc722 try to automatically find shared data dirs in system-wide paths
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-05 11:27:19 +02:00
Nicola Murino
61947e67ae ftpd: add basic wildcard support
this is the minimal implementation to allow mget and similar commands with
wildcards.

We only support wildcard for the last path level, for example:

- mget *.xml is supported
- mget dir/*.xml is supported
- mget */*.xml is not supported

Removed . and .. from FTP directory listing

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-04 19:32:02 +02:00
Nicola Murino
9a37e3d159 back to development
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-01 21:51:58 +02:00
Nicola Murino
14fb6c4038 always check recently updated users
also fix the query to get users for quota check for sql based providers

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-04-30 11:59:36 +02:00
Nicola Murino
dd9c5b2149 sql provider: enhanced folder mapping query using an upsert
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-04-28 14:49:57 +02:00
Nicola Murino
ecd488a840 data provider: remove prefer_database_credentials
Google Cloud Storage credentials are now always stored within the data
provider.

Added a migration to read credentials from disk and store them inside the
data provider.

After v2.3 we can also remove credentials_path

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-04-28 12:55:01 +02:00
Nicola Murino
4a44a7dfe1 improved readlink handling
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-04-27 18:38:46 +02:00
Nicola Murino
16a44a144b webclient: don't restore checkbox status
Fixes #807

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-04-26 09:15:26 +02:00
Nicola Murino
97f8142b1e azblobfs: update to the latest sdk and fix compatibility
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-04-25 17:34:52 +02:00
Nicola Murino
504cd3efda add groups support
Using groups simplifies the administration of multiple accounts by
letting you assign settings once to a group, instead of multiple
times to each individual user.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-04-25 15:49:11 +02:00
zemsten
857b6cc10a Update full-configuration.md (#799)
NGINX spelling

Signed-off-by: Samuel Zarn <samz@localhost.localdomain>
2022-04-22 09:22:36 +02:00
Nicola Murino
002a06629e refactoring of user session counters
Fixes #792

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-04-14 19:07:41 +02:00
Nicola Murino
5bc0f4f8af linux pkgs: disable vcs stamping
it seems to create build issues since go 1.18.1.

I need to investigate better

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-04-13 10:25:29 +02:00
Nicola Murino
cacfffc5bf OIDC: add support for custom fields
These fields can be used in the pre-login hook to implement custom
logics

Fixes #787

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-04-12 19:31:25 +02:00
dependabot[bot]
87d7854453 Bump codecov/codecov-action from 2 to 3 (#789)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 2 to 3.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-11 09:24:54 +02:00
dependabot[bot]
aa34388de0 Bump actions/download-artifact from 2 to 3 (#790)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 2 to 3.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-11 09:24:09 +02:00
Nicola Murino
a3f50029ba update moment.js to v2.29.2
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-04-09 10:05:15 +02:00
Nicola Murino
f9d8b83c2a sshd: disable by default ssh-rsa host key algo
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-04-04 18:52:19 +02:00
Nicola Murino
7c8bb5b18a fix quota for uploads outside home dir if rename fails
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-04-03 13:48:56 +02:00
Nicola Murino
254b2ae87f add support for AWS container
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-04-03 08:52:36 +02:00
Nicola Murino
5a40f998ae check and update the password hashing algorithm on user login
also add ldap md5 variant as per-user request

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-04-02 22:20:21 +02:00
Nicola Murino
77f3400161 allow to mount virtual folders on root (/) path
Fixes #783

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-04-02 18:32:46 +02:00
Nicola Murino
3521bacc4a web user templates: ensure we can save valid users
users with no public key and password are now valid after the recent
changes

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-04-01 09:47:54 +02:00
Nicola Murino
55f8171dd1 sshd: add support for host key certificates
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-04-01 08:03:56 +02:00
Nicola Murino
a7b159aebb ssh user certs: add a revoked list
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-31 21:49:06 +02:00
Nicola Murino
5c114b28e3 sshd: we don't need the user certificate
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-31 18:16:50 +02:00
Nicola Murino
e079444e8a update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-31 09:54:59 +02:00
Nicola Murino
3cb23ac956 be sure to close an SSH connection if all channels are idle
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-30 10:59:25 +02:00
Nicola Murino
8fb256ac91 add link to an external Traefik tutorial
update deps

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-29 18:13:43 +02:00
Nicola Murino
ca32cd5e0e allow placeholders for add/update users and folders
remove session token for S3, a temporary token is useless for our usage

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-27 16:32:21 +02:00
Nicola Murino
e0defafa26 azblob: fix the error returned in fs.Stat
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-26 11:47:12 +01:00
Nicola Murino
5cccb872bb add support to redirect HTTP to HTTPS
Fixes #777

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-26 10:00:02 +01:00
Nicola Murino
aaf940edab enforce CSRF token usage by the same IP for which it was issued
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-26 08:41:50 +01:00
ismail BASKIN
853086b942 Add role field array support (#774)
oidc: add array role field support

Signed-off-by: Ismail Baskin <ismailbaskin5@gmail.com>
2022-03-25 10:36:35 +01:00
Nicola Murino
81bdba6782 docker: re-add ppc64le
The alpine image for Go 1.18 is now available

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-25 09:32:48 +01:00
Nicola Murino
d955ddcef9 check that the jwt token is used by the same IP for which it
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-24 22:03:17 +01:00
Nicola Murino
4bbb195711 plugin: reload IP filter plugin on demand
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-24 10:21:13 +01:00
Nicola Murino
a193089646 add jq to full docker image variants
Fixes #767

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-23 11:35:23 +01:00
Nicola Murino
9bfdc10172 add support for ipfilter plugins
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-23 10:58:01 +01:00
Nicola Murino
b062b38ef4 docker: add rsync to "full" images
there are better alternatives and rsync will only work on local
filesystem, but it can still be useful to some people

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-22 17:29:14 +01:00
Nicola Murino
a31a9dc32c update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-21 17:52:18 +01:00
Pr0pHesyer
fa43791ea9 Optimized typography for better readability
Signed-off-by: Pr0pHesyer <proskire@protonmail.com>
2022-03-21 15:05:43 +01:00
Nicola Murino
93b9c1617e web UI: allow to load custom css
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-19 21:44:27 +01:00
Nicola Murino
4c710d731f update to Go 1.18
temporarily disabled docker image for ppcle64 as alpine image
is not yet available

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-18 21:52:00 +01:00
Nicola Murino
d9f30e7ac5 add a global whitelist
if defined only the listed IPs/networks can access the configured
services, all other client connections will be dropped before they
even try to authenticate

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-17 22:10:52 +01:00
Nicola Murino
03da7f696c SFTPGo is now listed on Azure Marketplace
Fixes #684

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-17 14:59:02 +01:00
Nicola Murino
883a3dceaf db defender: fix getHost query and add more test cases
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-16 18:22:08 +01:00
lucatiozzo91
7b86e2ac59 always show banned host in ui
When an host is banned but the updated_time field is in the past the ui
didn't show the record.

Fixes #758

Signed-off-by: lucatiozzo91 <luca.tiozzo91@gmail.com>
2022-03-16 17:38:29 +01:00
Nicola Murino
8502d7b051 improve transfer quota limits test case
ReadAll can read more bytes than the effective size, for this test
io.Copy is better

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-15 22:13:07 +01:00
Nicola Murino
6f8b71b89f s3fs: migrate to AWS SDK V2
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-15 19:16:50 +01:00
Nicola Murino
7e7f662a23 ensure that defaults defined in code match the default config file
Fixes #754

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-14 10:42:14 +01:00
Nicola Murino
0bec1c6012 change the default value for prefer_database_credentials to true ...
... and deprecate this setting.

In the future we'll remove prefer_database_credentials and
credentials_path and we will not allow the credentials to be saved on
the filesystem

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-13 14:29:11 +01:00
Nicola Murino
5582f5c811 data provider: add automatic backups
Automatic backup are enabled by default, a new backup will be saved
each day at midnight.

The backups_path setting was moved from the httpd section to the
data_provider one, please adjust your configuration file and or your
env vars

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-13 13:45:07 +01:00
Nicola Murino
48ed3dab1f update docs and deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-11 17:11:49 +01:00
Nicola Murino
d8de0faef5 allow to require two-factor auth for users
Fixes #721

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-06 16:57:13 +01:00
Nicola Murino
df828b6021 gcsfs: use pagers when listing bucket objects
Hopefully fixes #746

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-04 18:46:17 +01:00
Nicola Murino
056daaddfc always execute fs checks for users not logged in after an update
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-03 19:31:54 +01:00
Nicola Murino
5c2fd8d52a add support for a start directory
Fixes #705

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-03-03 12:44:56 +01:00
Nicola Murino
4519bffa39 S3: add support for assume role
Fixes #736

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-28 20:19:13 +01:00
Nicola Murino
1ea7429921 initprovider: add load data options
Fixes #741

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-28 17:05:18 +01:00
dependabot[bot]
816c174036 Bump github.com/mattn/go-sqlite3 from 1.14.11 to 1.14.12
Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 1.14.11 to 1.14.12.
- [Release notes](https://github.com/mattn/go-sqlite3/releases)
- [Commits](https://github.com/mattn/go-sqlite3/compare/v1.14.11...v1.14.12)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-sqlite3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-28 09:26:17 +01:00
Nicola Murino
79857a8733 config: restore defaults for smtp templates path
It was mistakenly deleted in the previous commit

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-27 14:16:38 +01:00
Nicola Murino
dcc3292dbc web setup: add an optional installation code
The purpose of this code is to prevent anyone who can access to
the initial setup screen from creating an admin user

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-27 13:08:47 +01:00
Nicola Murino
7f674a7fb3 add more details to the server status page
add all supported fields to the OpenAPI docs

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-26 16:43:29 +01:00
Nicola Murino
b64d3c2fbf simplify rename permission
before this patch we allow a rename in the following cases:

- the user has rename permission on both source and target path
- the user has delete permission on source path and create/upload on
  target path

we now check only the rename/rename_files/rename_dirs permissions.
This is what SFTPGo users expect.

This is a backward incompatible change and it will not backported to
the 2.2.x branch

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-26 12:19:09 +01:00
Nicola Murino
7fc5cb80d6 deb/rpm packages: attempt to set the cap_net_bind_service capability
so the service can bind to privileged ports without running as root user

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-26 10:10:51 +01:00
Andrea Mattia
92460f811f Simplify sed commands in Dockerfile(s)
Closes #740

Signed-off-by: Andrea Mattia <andrea.mattia@kireygroup.com>
2022-02-25 20:58:26 +01:00
Nicola Murino
e18ad55067 S3: add support for session tokens
Fixes #736

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-25 15:30:04 +01:00
Nicola Murino
4e9dae6fa4 allow to cache external authentications
Fixes #733

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-25 11:51:10 +01:00
Nicola Murino
f5a0559be6 don't execute fs check if the user has recent activity
The check could be expensive with some backends and is generally
only required the first time that a user logs in

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-24 16:11:35 +01:00
Nicola Murino
670018f05e OIDC: add profile and email scope to OAuth2 config
Fixes #728

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-22 10:20:14 +01:00
Nicola Murino
8bbf54d2b6 azure blobs: add support for multipart downloads
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-21 19:01:31 +01:00
Nicola Murino
d31cccf85f azblob: switch to the new azure-go-sdk
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-20 14:43:24 +01:00
Nicola Murino
c19b03a3f7 shares: add permission to deny sharing without password
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-19 13:31:58 +01:00
Nicola Murino
c6b8644828 OIDC: execute pre-login hook after IDP authentication
so the SFTPGo users can be auto-created using the hook

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-19 10:53:35 +01:00
Nicola Murino
f1a255aa6c httpd: allow to restrict allowed hosts ...
... and to add security headers to the responses

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-17 18:22:27 +01:00
Nicola Murino
876bf8aa4f sftpfs: improve remove
we know if the client asks to remove a file or directory so let's
use the appropriate command without letting the sftp library guess
the appropriate behavior

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-16 16:46:28 +01:00
Nicola Murino
900e519ff1 SFTP: respect file open flags also for file creation
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-16 16:05:56 +01:00
Nicola Murino
f1832d4478 shares: add an upload form for shares with write scope
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-15 19:19:25 +01:00
Nicola Murino
ebbbf81e65 logger: fix UTC time func
Fixes #719

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-14 12:30:00 +01:00
Nicola Murino
1fccd05e9e allow to configure the minimum version of TLS to be enabled
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-13 15:56:07 +01:00
Nicola Murino
66945c0a02 Web UIs: add OpenID Connect support
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-13 14:30:20 +01:00
Nicola Murino
fa0ca8fe89 quota summary and docs improvements
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-08 12:43:08 +01:00
clach04
c478c7dae9 Docker readme typo
Signed-off-by: clach04 <clach04@gmail.com>
2022-02-06 19:21:35 +01:00
Nicola Murino
9382db751c make HTTP shares browsable
if you share a single folder with read scope, you can now browse the share
and download single files

Fixes #674
See #677

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-06 16:46:43 +01:00
Nicola Murino
7e2a8e70c9 update zerolog deps
The updated version avoid to always create a socket connected to the
journald on application start.

Now the socket is only created if we log to the journald

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-03 17:55:36 +01:00
Nicola Murino
cd35636939 S3: add a timeout for single part uploads
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-02-01 12:15:56 +01:00
Nicola Murino
d51adb041e update data transfer quota only if the current IP has some limits
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-31 19:30:25 +01:00
Nicola Murino
02db00d008 dataprovider: add naming rules
naming rules allow to support case insensitive usernames, trim trailing
and leading white spaces, and accept any valid UTF-8 characters in
usernames.

If you were enabling `skip_natural_keys_validation` now you need to
set `naming_rules` to `1`

Fixes #687

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-31 18:01:37 +01:00
Nicola Murino
fb2d59ec92 data provider: add config options for certs validation/authentication
Fixes #682

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-30 18:04:03 +01:00
Nicola Murino
1df1225eed add support for data transfer bandwidth limits
with total limit or separate settings for uploads and downloads and
overrides based on the client's IP address.

Limits can be reset using the REST API

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-30 11:42:36 +01:00
Moroi
aca71bff7a Fixed typo
Signed-off-by: Moroi <4635854+Rango-dz@users.noreply.github.com>
2022-01-26 22:00:59 +01:00
Jeremy Clerc
9709aed5e6 httpd: webpath redirect using status found (302)
301 MovedPermanently is cached by the browser which can
be annoying when it is is on base path like / while one
may reuse the domain (e.g. localhost) for other apps/tests.

Fixes #695

Signed-off-by: Jeremy Clerc <jeremy@clerc.io>
2022-01-26 21:50:37 +01:00
Nicola Murino
d2a4178846 check quota usage between ongoing transfers
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-20 18:19:20 +01:00
Nicola Murino
d73be7aee5 remove the use of some unnecessary pointers
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-16 12:09:17 +01:00
Nicola Murino
ffe7f7ff16 doc improvements and minor changes
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-16 09:50:23 +01:00
Nicola Murino
a6ed6fc721 pattern filters: don't allow files in hidden dirs
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-15 19:06:02 +01:00
Nicola Murino
c3831de94e add hide policy to pattern filters
Disallowed files/dirs can be completly hidden. This may cause performance
issues for large directories

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-15 17:16:49 +01:00
Marc
9b6b9cca3d systemd-security: add some easy wins
We can tighten security by adding the following to
the systemd service file:

* NoNewPrivileges: should never be needed
* DevicePolicy: only basics required
* PrivateDevices: only needs mounted stuff, never devs
* ProtectSystem: no need to change boot
* RestrictAddressFamilies: INET, UNIX only

Signed-off-by: Marc <mail@lpcvoid.com>
2022-01-15 13:31:59 +01:00
Nicola Murino
64d1ea2d89 update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-13 18:48:08 +01:00
Nicola Murino
1c51239da8 Admin UI: allow to create multiple users/folders from templates
the clone button is not needed anymore, you can select a user and
click on template to generate one or more similar users or you can
create users/folders from an empty template

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-12 19:01:19 +01:00
Nicola Murino
51c15de892 web admin: simplify user page
The page to add/edit users should be less less intimidating now.
All the advanced settings are hidden by default. Permissions are set
to any, so if you also have a users base dir set, to add a user
you have to simply set username, password or public key and save

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-10 19:44:16 +01:00
Nicola Murino
b8efb1b8ec squash database migrations.
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-09 12:25:53 +01:00
Nicola Murino
ec1d20f46f sshd: improve docs about supported ciphers, KEX and MACs
also added a check to ensure that the configured values are valid

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-06 18:09:49 +01:00
Nicola Murino
1f619d5ea6 make the sdk a separate module
The SFTPGo SDK now is at the following URL

https://github.com/sftpgo/sdk

Fixes #657

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-06 11:54:43 +01:00
Nicola Murino
6d3d94a01f move kms implementation outside the sdk package
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-06 10:11:47 +01:00
Nicola Murino
0a3d94f73d log at info level the service configurations
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-05 13:22:49 +01:00
Nicola Murino
7c68b03d07 move plugin handling outside the sdk package
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-05 11:37:45 +01:00
Nicola Murino
2912b2e92e sdk: add a logger interface
we are now ready to make the sdk a separate module

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-04 16:07:41 +01:00
Nicola Murino
a6fe802370 move kms definitions to the sdk package
This is the first step to make the sdk a separate module

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-04 12:49:30 +01:00
Nicola Murino
ad483b7581 httpd: switch back to chi Recoverer now that the required patch is merged
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-04 09:48:16 +01:00
Nicola Murino
df86955f28 eventsearcher plugin: add support to search for provider, bucket, endpoint
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-03 17:02:52 +01:00
Nicola Murino
00ec426a80 notifier plugins: add provider, bucket and endpoint to nottifier params
Fixes #656

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-02 19:22:44 +01:00
Nicola Murino
222db53410 notifiers plugin: replace params with a struct
Fixes #658

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-02 15:16:35 +01:00
Nicola Murino
4d85dc108f document that SFTPGo is also available as a winget package
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-01-01 18:19:48 +01:00
Nicola Murino
6d582a821b back to development
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2021-12-31 16:01:23 +01:00
Nicola Murino
794afbf85e update release workflow
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2021-12-31 14:17:51 +01:00
Nicola Murino
e3f3997c5e set version to 2.2.1
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2021-12-31 13:42:03 +01:00
Nicola Murino
f78090e47f update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2021-12-29 18:11:00 +01:00
Nicola Murino
4d7a4aa99a check rename source and target 2021-12-28 12:03:52 +01:00
Nicola Murino
c36217c654 improve some docs 2021-12-26 14:54:29 +01:00
Nicola Murino
59bb578b89 web client: allow to move files between folders
Fixes #653
2021-12-25 17:13:23 +01:00
Nicola Murino
7d8823307f defender: add provider driver
Fixes #616
2021-12-25 12:08:07 +01:00
Nicola Murino
8174349032 console logger: enable colors on Windows too ...
... now that zerolog supports this feature
2021-12-20 18:47:18 +01:00
Nicola Murino
00a02dc14d howto: add two-factor authentication 2021-12-19 18:08:12 +01:00
Nicola Murino
ced73ed04e REST API: add an option to create missing dirs 2021-12-19 12:14:53 +01:00
Nicola Murino
cc73bb811b change log level from warn to error where appropriate
Fixes #649
2021-12-16 19:53:00 +01:00
Nicola Murino
a587228cf0 add support for metadata plugins 2021-12-16 18:18:36 +01:00
Nicola Murino
1472a0f415 hooks: preserve MFA related configs
if a user is updated using pre-login or external auth hook we need to
preserve the MFA related configs in the same way we do if the user is
updated using the REST API
2021-12-11 11:08:20 +01:00
Nicola Murino
0bb141960f add support for different bandwidth limits based on client IP 2021-12-10 18:43:26 +01:00
Nicola Murino
c153330ab8 web client: use fetch to upload files
also add REST API to upload a single file as POST body
2021-12-08 19:25:22 +01:00
Nicola Murino
5b4ef0ee3b windows installer: rename the sample configuration with the default values
The previous name sftpgo.json.default could create confusion for Windows
users
2021-12-05 07:58:53 +01:00
Nicola Murino
9632b6ee94 events search: improve test cases 2021-12-04 18:18:59 +01:00
Nicola Murino
78eb1c1166 update OpenAPI schema 2021-12-04 17:57:48 +01:00
Nicola Murino
a7c0b07a2a add session id to notifier plugins/hook 2021-12-04 17:27:24 +01:00
Nicola Murino
dc1cc88a46 keyboard interactive hooks: allow to validate passcode 2021-12-04 15:14:44 +01:00
Nicola Murino
3f5451eab6 web client: save/restore file list preferences 2021-12-04 07:58:49 +01:00
Nicola Murino
30d98326ca docker: update alpine image to 3.15 2021-12-03 19:33:37 +01:00
Nicola Murino
bedc8e288b web client: add support for integrating external viewers/editors 2021-12-03 18:33:08 +01:00
Nicola Murino
6092b6628e logs: use info level for login related messages
so enabling debug level is not required, for example only to understand
that a user exceeded the allowed sessions.

Also set the cache update frequency as documented
2021-12-02 19:36:42 +01:00
Nicola Murino
6ee51c5cc1 kms: remove support for compat secrets
also document how to activate the deprecated builtin provider
2021-12-01 17:53:19 +01:00
Nicola Murino
4df0ae82ac web client: allow downloading of single shared files without compression
Fixes #629
2021-11-30 20:32:10 +01:00
Nicola Murino
5db31f0fb3 web client: allow to upload/delete multiple files 2021-11-30 18:40:50 +01:00
Nicola Murino
0f8170c10f improve some docs and disable telemetry server by default 2021-11-29 17:58:10 +01:00
Nicola Murino
3c24cb773f SFTP: log users connections at info level
uniform SFTP and FTP logs

Fixes #626
2021-11-29 10:15:46 +01:00
Nicola Murino
bec54ac8ae CI: add windows x86
there still seem to be people using x86 on Windows ...
2021-11-28 21:30:31 +01:00
Nicola Murino
c330ac8418 CI: add windows arm64 2021-11-28 18:56:30 +01:00
Nicola Murino
3e478f42ea update lint rules and fix some warnings 2021-11-27 17:04:13 +01:00
Nicola Murino
18ab757216 back to development 2021-11-27 15:07:31 +01:00
Nicola Murino
b6bcf0cd94 set version to 2.2.0 2021-11-27 11:46:05 +01:00
Nicola Murino
015aa36c56 loaddata: improve shares restore
usage and timestamps are now preserved
2021-11-27 11:12:51 +01:00
Nicola Murino
f2480ce5c9 improve chtimes handling on open files 2021-11-26 19:00:44 +01:00
Vincent Murphy
f828c58dca Add --s3-force-path-style to portable 2021-11-26 17:40:23 +01:00
Nicola Murino
dc19921b0c web client: don't show the link for expired shares 2021-11-25 20:09:11 +01:00
Nicola Murino
3f3591bae0 web client: allow to preview images and pdf
pdf depends on browser support. It does not work on mobile devices.
2021-11-25 19:24:32 +01:00
Nicola Murino
fc048728d9 add 7digital to the sponsors section 2021-11-25 13:49:32 +01:00
Nicola Murino
aeb4675196 web admin: use a textarea for allowed/denied ip mask fields
Fixes #621
2021-11-25 13:08:12 +01:00
Nicola Murino
4652f9ede8 FTPD: allow to set different passive IPs based on the client's IP address 2021-11-25 12:45:09 +01:00
Nicola Murino
531cb5b5a1 sftpd: handle setstat requests with multiple attrs 2021-11-24 11:55:14 +01:00
Nicola Murino
9fb43b2c46 docs: clarify how multi-step auth works with external authentication
Fixes #617
2021-11-24 11:27:32 +01:00
Nicola Murino
8a8298ad46 web client: improve file upload 2021-11-22 12:25:36 +01:00
Nicola Murino
3d6b09e949 REST API: expose OpenAPI schema and render it using Swagger UI
Fixes #609
2021-11-21 09:32:51 +01:00
Nicola Murino
fb8f013ea7 web: update permissions on cookie refresh 2021-11-20 10:48:39 +01:00
Nicola Murino
c41319bb7a CI: sign windows installer and executable 2021-11-19 22:44:50 +01:00
Nicola Murino
46157ebbb6 CI docker: remove armv7 support
CI is still unreliable if we enable armv7 support
2021-11-16 09:07:10 +01:00
Nicola Murino
200b1d08c7 docker: add armv7 2021-11-15 21:58:35 +01:00
Nicola Murino
24b0352eb6 GCS: add ACL support 2021-11-15 21:57:41 +01:00
Nicola Murino
52f3a98cc8 preserve GCS credentials on update if not set
credentials were not preserved if "prefer_database_credentials" was
set to true

Fixes #613
2021-11-15 19:12:58 +01:00
Nicola Murino
e29a3efd39 add resetprovider sub-command
Fixes #608
2021-11-15 18:40:31 +01:00
Nicola Murino
ca730e77a5 add separate permissions to delete and rename files and dirs
perm_delete and perm_rename still exist for backward compatibility,
now they are an alias to assign both new split permissions
2021-11-14 16:23:33 +01:00
Nicola Murino
0833b4698e httpd service: add CORS support 2021-11-13 23:14:50 +01:00
Nicola Murino
ee5c5e033d S3: add ACL support
Fixes #610
2021-11-13 16:05:40 +01:00
Nicola Murino
78233ff9a3 web UI/REST API: add password reset
In order to reset the password from the admin/client user interface,
an SMTP configuration must be added and the user/admin must have an email
address.
You can prohibit the reset functionality on a per-user basis by using a
specific restriction.

Fixes #597
2021-11-13 13:25:43 +01:00
Nicola Murino
b331dc5686 web client: show share last use and used tokens 2021-11-07 09:53:35 +01:00
Nicola Murino
dfcfcee208 Windows: fix UTC time logging 2021-11-06 16:27:01 +01:00
Nicola Murino
094ee1522e logger: add a flag to use UTC time for logging 2021-11-06 15:18:16 +01:00
Nicola Murino
3bc58f5988 WebClient/REST API: add sharing support 2021-11-06 14:13:20 +01:00
Martijn Pieters
f6938e76dc Parse auth plugin information from env 2021-11-02 11:36:30 +01:00
Nicola Murino
570964deb3 add post-disconnect hook
Fixes #587
2021-10-29 19:55:18 +02:00
Nicola Murino
31984ffec1 update logo and add it to windows exe and installer
thanks to @asheroto for donating the new logo
2021-10-23 19:27:39 +02:00
Nicola Murino
74fc3aaf37 REST API: add events search 2021-10-23 15:47:21 +02:00
Nicola Murino
97d0a48557 plugins: improve notifier and searcher 2021-10-20 19:39:49 +02:00
Nicola Murino
3bbe67571f plugins: add eventsearcher 2021-10-17 16:43:05 +02:00
Nicola Murino
f131ef130b add a link to the new events store plugin 2021-10-16 17:08:34 +02:00
Nicola Murino
4a6a4ce28d sftpfs: map path resolution error to permission denied
we do the same for os fs so that the problematic directory is excluded
from the webdav listing instead of failing the whole directory listing
2021-10-16 10:32:18 +02:00
Nicola Murino
a80ac80fcd pkgs: update nfpm to 2.7 and use xz as compression for both deb and rpm 2021-10-13 09:15:04 +02:00
Nicola Murino
4aa9686e3b refactor custom actions
SFTPGo is now fully auditable, all fs and provider events that change
something are notified and can be collected using hooks/plugins.

There are some backward incompatible changes for command hooks
2021-10-10 13:08:05 +02:00
Nicola Murino
64e87d64bd web client UI: allow to edit plain text files
Fixes #567
2021-10-09 14:17:28 +02:00
Nicola Murino
9ca0b46f30 UI connections page: add a refresh button 2021-10-07 18:28:31 +02:00
Nicola Murino
6eb154bb74 webdav: add support for lock discovery 2021-10-06 09:11:56 +02:00
Nicola Murino
ea01c3a125 rate limiting: allow to exclude IP addresses/ranges
Fixes #563
2021-10-03 20:50:05 +02:00
Nicola Murino
1b4a1fbbe5 add data retention check hook 2021-10-03 15:17:49 +02:00
Nicola Murino
ec81a7ac29 actions: add a specific protocol for data retention 2021-10-03 10:22:47 +02:00
Nicola Murino
22d28a37b6 cmd: improve completion sub-commands 2021-10-03 08:14:57 +02:00
Nicola Murino
cc134cad9a data retention: allow to notify results via e-mail 2021-10-02 22:25:41 +02:00
Nicola Murino
1459150024 WebDAV: improve logs 2021-10-01 20:37:23 +02:00
root
87751e562e Flesh out examples/ldapauth, specifically:
Support 'virtual' users who have no homeDirectory, uidNumber or gidNumber.
Permit read-only access by a user named "anonymous", with any password.
Assume a conventional DIT with users under ou=people,dc=example,dc=com.
Read the LDAP bindPassword from a file (not baked into the code).
Log progress and problems to syslog.
2021-10-01 09:10:13 +02:00
Nicola Murino
e6f969cb04 web UI: update js and css deps 2021-09-30 10:23:25 +02:00
Nicola Murino
ba1febba73 rework user and admin profiles
users and admins can now also update their email and description
2021-09-29 18:46:15 +02:00
Nicola Murino
af8fa7ff81 Docker: remove rsync from default images
it's time to encourage people to switch to more modern alternatives like
rclone
2021-09-27 11:34:11 +02:00
Nicola Murino
4ab2e4088a CI docker: remove armv7 support
building docker images now takes too long and often fails with random
errors. I have to restart the build several times to be able to push
the images to docker hub and gcr
2021-09-27 10:25:21 +02:00
Nicola Murino
da0ccc6426 add SMTP support
it will be used in future update to add email sending capabilities
2021-09-26 20:25:37 +02:00
Maharanjan
0661876e99 Added email field for user account 2021-09-25 19:06:13 +02:00
Nicola Murino
cd72ac4fc9 CI: add armv7 support 2021-09-25 14:14:21 +02:00
Nicola Murino
da5a061b65 add basic REST APIs for data retention
Fixes #495
2021-09-25 12:20:31 +02:00
Nicola Murino
65948a47f1 systemd unit: set LimitNOFILE to 8192 2021-09-19 17:37:18 +02:00
Nicola Murino
bf4b3e6840 httpd: move the check connection middleware before the logger middleware
Fixes #543
2021-09-19 08:14:59 +02:00
Nicola Murino
6ea38188e8 minor fixes and doc improvements 2021-09-18 10:50:17 +02:00
Nicola Murino
b5639a51fd don't generate defender events for HTTP/WebDAV requests with no auth
it is quite common for HTTP clients to send a first request without
the Authorization header and then send the credentials after receiving
a 401 response. We don't want to generate defender events in this case
2021-09-11 18:23:11 +02:00
Nicola Murino
5c34d814d6 fix a possible nil pointer dereference
it can happen by upgrading from very old versions
2021-09-11 14:19:17 +02:00
Nicola Murino
0eca4f1866 update deps 2021-09-08 12:29:47 +02:00
Nicola Murino
b52f829f05 docker: replace mime-support package with media-types
This way the size of the slim image is similar to the previous buster
based images
2021-09-07 21:04:46 +02:00
Nicola Murino
90f64c9f63 distroless image: minor changes 2021-09-07 19:52:28 +02:00
Oleksandr Shvets
c106498dd8 docker: added distroless image 2021-09-06 19:10:28 +02:00
Nicola Murino
7bad65a43e user: add a permission to disable changing api key authentication
also implement the missing APIs to enable/disable api key authentication
2021-09-06 18:46:35 +02:00
Nicola Murino
101c2962ab web client UI: add a permission to disable password change
Fixes #528
2021-09-05 18:49:13 +02:00
Nicola Murino
59140a6d51 add additional data to MFA secrets and fix pointers management 2021-09-05 14:10:12 +02:00
Nicola Murino
b1d54f69d9 admin: fix possible nil pointer dereference
this possible bug was introduced in the previous commit
2021-09-04 13:56:29 +02:00
Nicola Murino
374de07c7b update deps 2021-09-04 13:30:23 +02:00
Nicola Murino
8a4c21b64a add builtin two-factor auth support
The builtin two-factor authentication is based on time-based one time
passwords (RFC 6238) which works with Authy, Google Authenticator and
other compatible apps.
2021-09-04 12:11:04 +02:00
Nicola Murino
16ba7ddb34 CI: also runs test cases using GOARCH 386
This way we can detect unaligned 64-bit atomic operations that only happen
on 32 bit platforms
2021-08-28 12:03:23 +02:00
Nicola Murino
bd9506da42 BaseConnection struct: ensure 64 bit alignment
Fixes #516
2021-08-28 10:06:49 +02:00
Nicola Murino
b903a6e46f data provider: remove default admin
you need to load initial data or set "create_default_admin" to true
and the appropriate env vars if you don't want to use the web admin
setup screen to create the default admin
2021-08-20 10:37:51 +02:00
Nicola Murino
bcf088f586 data provider: update internal caches if the data provider is shared 2021-08-20 09:35:06 +02:00
Nicola Murino
be3857d572 dataprovider: add timestamp fields for users and admins 2021-08-19 15:51:43 +02:00
Nicola Murino
b99d4ce82e fix folders validation
Fixes #510
2021-08-19 11:28:53 +02:00
Nicola Murino
0a558203da improve proxy documentation
Fixes #507
2021-08-18 15:27:07 +02:00
Nicola Murino
5a549a88fe update to Go 1.17 2021-08-18 14:39:56 +02:00
Nicola Murino
fe953d6b38 REST API: add support for API key authentication 2021-08-17 18:08:32 +02:00
erwiese
05c62b9f40 add documentation for defender scores (#500)
Co-authored-by: Erwin Wiesensarter <erwin.wiesensarter@bkg.bund.de>
2021-08-13 15:40:33 +02:00
Nicola Murino
555dc3b0c0 transfer logs: add FTP mode 2021-08-10 13:07:38 +02:00
Nicola Murino
0de0d3308c improve error messages for generic failures 2021-08-08 19:30:21 +02:00
Nicola Murino
a20373b613 add support for auth plugins 2021-08-08 17:09:48 +02:00
Nicola Murino
ced2e16f41 add support for password validation rules
Fixes #494
2021-08-06 18:56:07 +02:00
Nicola Murino
3ac832c8dd docker: bump Alpine to 3.14 2021-08-05 19:38:30 +02:00
Nicola Murino
a3c087456b ftpd: add some security checks 2021-08-05 18:38:15 +02:00
Nicola Murino
419774158a remove PayPal link
I'm having some issues with my PayPal account, remove it for now
2021-08-03 20:36:10 +02:00
Nicola Murino
0503215e7a web client: try to prevent browsers from caching requests
Fixes #493
2021-08-03 19:58:03 +02:00
dependabot[bot]
9541843ff7 Bump github.com/shirou/gopsutil/v3 from 3.21.6 to 3.21.7 (#491)
Bumps [github.com/shirou/gopsutil/v3](https://github.com/shirou/gopsutil) from 3.21.6 to 3.21.7.
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v3.21.6...v3.21.7)

---
updated-dependencies:
- dependency-name: github.com/shirou/gopsutil/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-02 10:11:09 +02:00
dependabot[bot]
98f22ba110 Bump uraimo/run-on-arch-action from 2.1.0 to 2.1.1 (#490)
Bumps [uraimo/run-on-arch-action](https://github.com/uraimo/run-on-arch-action) from 2.1.0 to 2.1.1.
- [Release notes](https://github.com/uraimo/run-on-arch-action/releases)
- [Commits](https://github.com/uraimo/run-on-arch-action/compare/v2.1.0...v2.1.1)

---
updated-dependencies:
- dependency-name: uraimo/run-on-arch-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-02 10:10:24 +02:00
Nicola Murino
1e9a19e326 add a howto to use SFTPGo as OpenSSH's SFTP subsystem 2021-07-31 19:09:09 +02:00
mmcgeefeedo
0046c9960a add support to override default admin credentials via env vars 2021-07-31 10:39:53 +02:00
Nicola Murino
7640612a95 update deps 2021-07-31 10:22:38 +02:00
Nicola Murino
a26962f367 add dot and dot dot directories to sftp/ftp file listing 2021-07-31 09:42:23 +02:00
Nicola Murino
f778e47d22 sftpd: minor improvements and docs for the prefix middleware 2021-07-29 20:12:23 +02:00
Nicola Murino
4781921336 fix loading enabled_ssh_commands config key 2021-07-29 00:54:22 +02:00
mmcgeefeedo
3ae8abda9e sftpd: add folder prefix middleware 2021-07-29 00:32:55 +02:00
Nicola Murino
90b324d707 Add a link on the login pages to switch between admin and web client login
The links are hidden if only the web admin or only thw web client is
enabled and can also be controlled using the "hide_login_url" setting

Fixes #485
2021-07-27 18:43:00 +02:00
Nicola Murino
3a22aae34f web UI: add support for upload, create dirs, rename, delete 2021-07-26 20:55:49 +02:00
dependabot[bot]
45a0473fec Bump codecov/codecov-action from 1 to 2.0.2 (#486)
* Bump codecov/codecov-action from 1 to 2

Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 1 to 2.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v1...v2.0.2)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nicola Murino <nicola.murino@gmail.com>
2021-07-26 11:08:48 +02:00
Nicola Murino
a7313e4492 webdav: add new test cases and fix some lock related issues
Our net/webdav branch now include the following patches:

https://github.com/golang/net/pull/92
https://github.com/golang/net/pull/93
https://github.com/golang/net/pull/94
2021-07-25 09:55:14 +02:00
Nicola Murino
c41ae116eb improve logging
Fixes #381
2021-07-24 20:11:17 +02:00
Nicola Murino
83c7453957 user API: allow to disable writes ...
... even if the user has permissions for these actions
2021-07-23 21:41:02 +02:00
Nicola Murino
85a47810ff S3: expose more properties, possible backward incompatible change
Before these changes we implictly set S3ForcePathStyle if an endpoint
was provided.

This can cause issues with some S3 compatible object storages and must
be explicitly set now.

AWS is also deprecating this setting

https://aws.amazon.com/it/blogs/aws/amazon-s3-path-deprecation-plan-the-rest-of-the-story/
2021-07-23 16:56:48 +02:00
Nicola Murino
c997ef876c S3: fix Ceph compatibility
This hack will no longer be needed once Ceph tags a new version and vendors
using it update their servers.

This code is taken from rclone, thank you!

Fixes #483
2021-07-23 11:41:31 +02:00
Nicola Murino
ae8ccadad2 users API: add API to create, delete, rename files and directories 2021-07-23 10:19:27 +02:00
Nicola Murino
5967aa1aa5 FTP: enable ftpserverlib logging and make debug mode configurable 2021-07-20 17:22:08 +02:00
Nicola Murino
c900cde8e4 notifiers plugin: add settings to retry unhandled events 2021-07-20 12:51:21 +02:00
Nicola Murino
13183a9f76 deps cleanup 2021-07-17 15:42:59 +02:00
Nicola Murino
5a568b4077 KMS: allow to provide the master encryption key as string 2021-07-17 15:34:48 +02:00
Nicola Murino
030507a2ce add some docs for the plugin system 2021-07-17 14:14:42 +02:00
Nicola Murino
338301955f move cloud KMS providers to an external plugin 2021-07-17 13:08:05 +02:00
Nicola Murino
6d313f6d8f expose KMS as plugin 2021-07-16 18:22:42 +02:00
Nicola Murino
776dffcf12 kms: improve modularity 2021-07-13 21:17:21 +02:00
Nicola Murino
e1a2451c22 s3: allow to configure the chunk download timeout 2021-07-11 18:39:45 +02:00
Nicola Murino
7344366ce8 sftpd: remove workarounds for directory listing
The underlying issue was fixed in pkg/sftp 1.13.2
2021-07-11 16:26:40 +02:00
Nicola Murino
bd5191dfc5 add experimental plugin system 2021-07-11 15:26:51 +02:00
Nicola Murino
bfa4085932 improve docs 2021-07-03 18:23:36 +02:00
Nicola Murino
302ec2558c add notifications for mkdir/rmdir 2021-07-03 18:07:55 +02:00
Nicola Murino
ff19879ffd allow to use a persistent signing key for JWT and CSRF tokens
Fixes #466
2021-07-01 20:17:40 +02:00
Nicola Murino
04001f7ad3 FTP: try to return more specific error codes/messages for some errors
We now return 552 code for quota exceeded errors and 553 in the following
cases:

- filename denied by a filter
- no upload permission
- no overwrite permission
- pre upload hook error

Fixes #442
2021-06-28 19:40:04 +02:00
Nicola Murino
076b2f0ee0 modules: add v2 support 2021-06-26 07:31:41 +02:00
Nicola Murino
93dfb03eaf GCS: add a trailing / to "directories"
This way SFTPGo should be compatible with Google Cloud console.

This change should be backward compatibile, testing is welcome

Fixes #464
2021-06-24 19:36:01 +02:00
Nicola Murino
e09bdd43d4 defender: fix GetHost for blocklist entries too 2021-06-20 21:57:19 +02:00
Nicola Murino
ac8d8a3da1 update portable mode docs 2021-06-19 19:40:53 +02:00
Manuel Reithuber
a4157e83e9 template fsconfig: updated form-group css classes so we can further improve onFilesystemChanged()
it doesn't reference any vfs providers at all anymore :)
2021-06-19 19:27:54 +02:00
Manuel Reithuber
13f23838a1 template fsconfig.html: using string provider name in onFilesystemChanged() 2021-06-19 19:27:54 +02:00
Manuel Reithuber
fd4c388b23 added vfs.ListProviders() and using it in template fsconfig.html (added a new ListFSProviders template function for that) 2021-06-19 19:27:54 +02:00
Manuel Reithuber
88b10da596 updated utils.LoadTemplate() to call template.ParseFiles() directly and added a way to specify a base template (will be used in the next commit) 2021-06-19 19:27:54 +02:00
Manuel Reithuber
c07dc74d48 template fsconfig.html: simplified code in onFilesystemChanged() 2021-06-19 19:27:54 +02:00
Manuel Reithuber
b48e01155c FilesystemProvider: added .Name() which reverses vfs.GetProviderByName(), and added .ShortInfo(); using .ShortInfo() in User.GetInfoString() 2021-06-19 19:27:54 +02:00
Manuel Reithuber
0ff010cc94 added vfs.GetProviderByName(), using it in for sftpgo portable and for parsing the webadmin form field 2021-06-19 19:27:54 +02:00
Nicola Murino
81aac15a6c defender: don't return expired hosts/banned ip in GetHost too 2021-06-19 18:51:33 +02:00
Nicola Murino
c1b862394d move other errors to utils package 2021-06-19 13:06:01 +02:00
Manuel Reithuber
f19937b715 move Filesystem config validation to vfs 2021-06-19 12:24:43 +02:00
Nicola Murino
f2f612b450 defender: don't return expired hosts/banned ip 2021-06-19 11:02:46 +02:00
Nicola Murino
0c2640bbab update deps 2021-06-19 09:56:49 +02:00
Nicola Murino
3bb0ca1d2b config: remove deprecated configuration keys 2021-06-19 09:47:06 +02:00
Nicola Murino
d5b42f72e2 squash database migrations, remove compat data provider code 2021-06-19 09:03:20 +02:00
Nicola Murino
62744e081b get HTTPD binding from env: respect the documented default 2021-06-17 15:57:41 +02:00
Nicola Murino
9dcaf1555f back to development 2021-06-16 19:28:25 +02:00
Nicola Murino
a09cf5c8b9 set version to 2.1.0 2021-06-16 17:45:09 +02:00
Nicola Murino
47ebe42375 FTP: fix LIST on files 2021-06-15 06:38:56 +02:00
Nicola Murino
4d97ab9eb9 Let's Encrypt tutorial: use sudo where appropriate 2021-06-14 22:35:08 +02:00
Nicola Murino
8ed13dc4a9 docs: document how to use Let's Encrypt Certificates 2021-06-14 22:05:55 +02:00
Nicola Murino
3b66dd0873 Linux packages: fix static resources copy 2021-06-14 14:18:15 +02:00
Nicola Murino
d992f0ffcc update deps 2021-06-13 08:54:22 +02:00
Nicola Murino
6c5a7e8f13 improve installation docs, add paypal link to fundings 2021-06-12 10:05:25 +02:00
Nicola Murino
9d3d7db29c azblob: store SAS URL as kms.Secret 2021-06-11 22:27:36 +02:00
Nicola Murino
8607788975 s3fs: use "application/x-directory" as folder mime type
This change improve s3fs-fuse compatibility

Fixes #451
2021-06-08 13:52:36 +02:00
Nicola Murino
4be6307d87 webadmin: add defender page 2021-06-08 13:24:28 +02:00
Nicola Murino
feec2118bb improve defender and quotas REST API 2021-06-07 21:52:43 +02:00
Nicola Murino
43182fc25e OpenAPI: add users API
These new APIs match the web client features.

I'm aware that some API do not follow REST best practises.

I want to avoid things likes "/user/folders/<path>"

where "path" must be encoded and making it optional create issues, so
I defined resources as query parameters instead of path parameters
2021-06-05 16:07:09 +02:00
Nicola Murino
976f588863 improve docs to enable FTP/WebDAV
Fixes #447
2021-06-02 09:49:31 +02:00
Nicola Murino
575bcf1f03 add remote address to transfer and commands logs 2021-06-01 22:28:43 +02:00
Nicola Murino
969c992bfd pre-upload: execute the hook just before opening the target file 2021-05-31 22:40:47 +02:00
Nicola Murino
c1239fbf59 pre-upload action: add file open flags
Reading the flags the hook receiver can detect if the client wants to
truncate the target file
2021-05-31 22:33:23 +02:00
Nicola Murino
c63b923ec3 cryptfs: add support for atomic uploads 2021-05-31 21:45:29 +02:00
dependabot[bot]
574c4029fc Bump uraimo/run-on-arch-action from 2.0.9 to 2.0.10 (#444)
Bumps [uraimo/run-on-arch-action](https://github.com/uraimo/run-on-arch-action) from 2.0.9 to 2.0.10.
- [Release notes](https://github.com/uraimo/run-on-arch-action/releases)
- [Commits](https://github.com/uraimo/run-on-arch-action/compare/v2.0.9...v2.0.10)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-31 10:05:25 +02:00
Nicola Murino
423d8306be webclient: allow to download multiple files as zip 2021-05-30 23:07:46 +02:00
Nicola Murino
fc7066a25c cross device rename: remove the source if copy suceeded 2021-05-27 22:23:14 +02:00
Nicola Murino
e1bf46c6a5 local fs rename: if it fails with a cross device error try a copy
I don't want to add a new setting for this, at least until we get the
first complain for a slow rename :)

Fixes #440
2021-05-27 20:14:12 +02:00
Nicola Murino
3b46e6a6fb add support for a global temp path
Fixes #436
2021-05-27 15:38:27 +02:00
Nicola Murino
7a85c66ee7 webclient: defer file list rendering
combined with server side processing I can now list a directory with
about 100.000 files in less than 2 seconds without losing client side
filtering and pagination
2021-05-27 09:40:46 +02:00
Nicola Murino
25a44030f9 actions: add pre-download and pre-upload
Downloads and uploads can be denied based on hook response
2021-05-26 07:48:37 +02:00
Nicola Murino
600268ebb8 httpclient: allow to set custom headers 2021-05-25 08:36:01 +02:00
Nicola Murino
1223957f91 webclient: use different icons based on the file extension 2021-05-24 19:09:03 +02:00
Nicola Murino
15cde2dd1a improve test coverage 2021-05-23 22:29:55 +02:00
Nicola Murino
50e441849a try to make the web admin more user friendly
removed all the textarea with fields separated using "::".
This should, hopefully, improve user experience
2021-05-23 22:02:01 +02:00
Nicola Murino
02bb09ec01 remove deprecated file extensions filters
these filters were deprecated a long time ago, everyone should use
patterns filters now
2021-05-22 12:28:05 +02:00
Nicola Murino
402947a43c update deps 2021-05-22 10:42:30 +02:00
Nicola Murino
b9bc8d722d try to improve web client credentials page
I should do the same for the admin page too
2021-05-22 09:54:27 +02:00
Nicola Murino
0cb5c49cf3 map path resolution errors to Permission errors
this way the affected paths will be ignored in WebDAV

Fixes #432
2021-05-21 13:04:22 +02:00
Nicola Murino
9fc4be6d40 minor doc fixes 2021-05-20 18:34:38 +02:00
Nicola Murino
ecfed4dc04 Add a Getting Started Guide 2021-05-20 18:16:27 +02:00
dependabot[bot]
b415e4d98f Bump github.com/lib/pq from 1.10.1 to 1.10.2 (#429)
Bumps [github.com/lib/pq](https://github.com/lib/pq) from 1.10.1 to 1.10.2.
- [Release notes](https://github.com/lib/pq/releases)
- [Commits](https://github.com/lib/pq/compare/v1.10.1...v1.10.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-17 09:27:16 +02:00
Nicola Murino
7d059efe06 add an example backup script 2021-05-16 22:28:08 +02:00
Nicola Murino
60cfbd2989 setup: auto login after creating the first admin 2021-05-16 21:36:57 +02:00
Nicola Murino
8ecf64f481 httpclient: accepts timeouts as float
Fixes #428
2021-05-16 12:50:06 +02:00
Nicola Murino
019b0f2fd5 http cookie: add max-age and samesite
update deps too
2021-05-16 09:13:00 +02:00
Nicola Murino
15d6cd144a another try to better understand the random webdav test case failure 2021-05-15 08:56:36 +02:00
Nicola Murino
f59f62317e sftpd: fix file upload resume detection
WinSCP does not set the APPEND flag while resuming a file upload,
so we detect a file upload resume if the TRUNCATE flag is not set.
The APPEND flag is now ignored.

Fixes #420
2021-05-15 08:39:01 +02:00
Nicola Murino
f2b93c0402 add a setup screen to create the first admin user
If you prefer to auto-create the first admin you can enable the
"create_default_admin" configuration key and SFTPGo will work as before.

You can also create the first admin by loading initial data: now you can
set both username and password, before you could only change the password
2021-05-14 19:21:15 +02:00
Nicola Murino
0540b8780e redact credentials within hooks
go-retryablehttp does not redact credentials, so we still log them
when we use it

https://github.com/hashicorp/go-retryablehttp/pull/133
2021-05-12 22:44:17 +02:00
Nicola Murino
fa45c9c138 allow to execute actions for file operations and SSH commands synchronously
The actions to run synchronously can be configured via the `execute_sync`
configuration key.

Executing an action synchronously means that SFTPGo will not return a result
code to the client until your hook have completed its execution.

Fixes #409
2021-05-11 12:45:14 +02:00
Nicola Murino
b67cd0d3df ensure no client is connected before running max connections test cases 2021-05-11 08:04:57 +02:00
Nicola Murino
c8f7fc9bc9 httpd/webdav: add a list of hosts allowed to send proxy headers
X-Forwarded-For, X-Real-IP and X-Forwarded-Proto headers will be ignored
for hosts not included in this list.

This is a backward incompatible change, before the proxy headers were
always used
2021-05-11 06:54:06 +02:00
dependabot[bot]
f1b998ce16 Bump github.com/otiai10/copy from 1.5.1 to 1.6.0 (#414)
Bumps [github.com/otiai10/copy](https://github.com/otiai10/copy) from 1.5.1 to 1.6.0.
- [Release notes](https://github.com/otiai10/copy/releases)
- [Commits](https://github.com/otiai10/copy/compare/v1.5.1...v1.6.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 14:02:09 +02:00
dependabot[bot]
aaa758e978 Bump github.com/minio/sio from 0.2.1 to 0.3.0 (#412)
Bumps [github.com/minio/sio](https://github.com/minio/sio) from 0.2.1 to 0.3.0.
- [Release notes](https://github.com/minio/sio/releases)
- [Commits](https://github.com/minio/sio/compare/v0.2.1...v0.3.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 11:34:01 +02:00
dependabot[bot]
716946a148 Bump github.com/aws/aws-sdk-go from 1.38.35 to 1.38.36 (#413)
Bumps [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) from 1.38.35 to 1.38.36.
- [Release notes](https://github.com/aws/aws-sdk-go/releases)
- [Changelog](https://github.com/aws/aws-sdk-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-go/compare/v1.38.35...v1.38.36)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 11:10:58 +02:00
Nicola Murino
15934d72cc webdav test: increase log size
the latest 10 lines are not enough to understand the issue, try with 20
2021-05-09 10:09:25 +02:00
Nicola Murino
8f6cdacd00 allow to limit the number of per-host connections 2021-05-08 19:45:21 +02:00
Nicola Murino
8f736da4b8 webdav test: add some more logs
QuotaLimits test case sometime fails when running in CI, try to
understand the reason
2021-05-07 22:24:06 +02:00
Nicola Murino
4ea4202b99 httpd/webdav: use a custom listener with read and write deadlines 2021-05-07 20:41:20 +02:00
Nicola Murino
d4bfc3f6b5 fix lint configuration and a warning 2021-05-06 22:06:22 +02:00
Nicola Murino
23d9ebfc91 add a basic front-end web interface for end-users
Fixes #339 #321 #398
2021-05-06 21:35:43 +02:00
dependabot[bot]
5c99f4fb60 Bump github.com/shirou/gopsutil/v3 from 3.21.3 to 3.21.4 (#406)
Bumps [github.com/shirou/gopsutil/v3](https://github.com/shirou/gopsutil) from 3.21.3 to 3.21.4.
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v3.21.3...v3.21.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-03 14:44:07 +02:00
dependabot[bot]
2263c7e20f Bump github.com/hashicorp/go-retryablehttp from 0.6.8 to 0.7.0 (#405)
Bumps [github.com/hashicorp/go-retryablehttp](https://github.com/hashicorp/go-retryablehttp) from 0.6.8 to 0.7.0.
- [Release notes](https://github.com/hashicorp/go-retryablehttp/releases)
- [Commits](https://github.com/hashicorp/go-retryablehttp/compare/v0.6.8...v0.7.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-03 14:43:53 +02:00
dependabot[bot]
515b2d917e Bump github.com/fclairamb/ftpserverlib from 0.13.0 to 0.13.1 (#404)
Bumps [github.com/fclairamb/ftpserverlib](https://github.com/fclairamb/ftpserverlib) from 0.13.0 to 0.13.1.
- [Release notes](https://github.com/fclairamb/ftpserverlib/releases)
- [Commits](https://github.com/fclairamb/ftpserverlib/compare/v0.13.0...v0.13.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-03 13:29:54 +02:00
dependabot[bot]
af4723356d Bump github.com/lestrrat-go/jwx from 1.1.7 to 1.2.0 (#403)
Bumps [github.com/lestrrat-go/jwx](https://github.com/lestrrat-go/jwx) from 1.1.7 to 1.2.0.
- [Release notes](https://github.com/lestrrat-go/jwx/releases)
- [Changelog](https://github.com/lestrrat-go/jwx/blob/main/Changes)
- [Commits](https://github.com/lestrrat-go/jwx/compare/v1.1.7...v1.2.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-03 13:29:16 +02:00
dependabot[bot]
068dd34a38 Bump github.com/aws/aws-sdk-go from 1.38.25 to 1.38.30 (#402)
Bumps [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) from 1.38.25 to 1.38.30.
- [Release notes](https://github.com/aws/aws-sdk-go/releases)
- [Changelog](https://github.com/aws/aws-sdk-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-go/compare/v1.38.25...v1.38.30)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-03 11:41:25 +02:00
dependabot[bot]
b16a5c2caf Bump github.com/go-chi/chi/v5 from 5.0.2 to 5.0.3 (#401)
Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.0.2 to 5.0.3.
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-chi/chi/compare/v5.0.2...v5.0.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-03 11:41:09 +02:00
Nicola Murino
a383957cfa OpenAPI: document that also folder-quota-update supports partial updates 2021-04-28 19:33:32 +02:00
Nicola Murino
00f97aabb4 OpenAPI: document that quota-update support partial updates
If the update mode is "add" and you pass only used_quota_size or only
used_quota_files the missing field will remain unchanged
2021-04-28 19:16:15 +02:00
Nicola Murino
32db0787bb add an example script for scheduled quota updates 2021-04-26 21:53:09 +02:00
Nicola Murino
1275328fdf Authentication errors: try to avoid user enumeration
Fixes #395
2021-04-26 19:48:21 +02:00
Nicola Murino
7778716fa7 update crypto and net dependencies 2021-04-25 18:12:02 +02:00
dependabot[bot]
77476d0f56 Bump github.com/aws/aws-sdk-go from 1.38.21 to 1.38.25 (#394)
Bumps [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) from 1.38.21 to 1.38.25.
- [Release notes](https://github.com/aws/aws-sdk-go/releases)
- [Changelog](https://github.com/aws/aws-sdk-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-go/compare/v1.38.21...v1.38.25)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-25 17:07:59 +02:00
dependabot[bot]
c7a1fc2996 Bump cloud.google.com/go/storage from 1.14.0 to 1.15.0 (#392)
Bumps [cloud.google.com/go/storage](https://github.com/googleapis/google-cloud-go) from 1.14.0 to 1.15.0.
- [Release notes](https://github.com/googleapis/google-cloud-go/releases)
- [Changelog](https://github.com/googleapis/google-cloud-go/blob/master/CHANGES.md)
- [Commits](https://github.com/googleapis/google-cloud-go/compare/spanner/v1.14.0...spanner/v1.15.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-25 17:07:36 +02:00
dependabot[bot]
e7d8e73be8 Bump github.com/lib/pq from 1.10.0 to 1.10.1 (#391)
Bumps [github.com/lib/pq](https://github.com/lib/pq) from 1.10.0 to 1.10.1.
- [Release notes](https://github.com/lib/pq/releases)
- [Commits](https://github.com/lib/pq/compare/v1.10.0...v1.10.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-25 17:07:26 +02:00
dependabot[bot]
3ee27f4370 Bump golangci/golangci-lint-action from v2 to v2.5.2 (#389)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from v2 to v2.5.2.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v2...5c56cd6c9dc07901af25baab6f2b0d9f3b7c3018)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-25 16:41:17 +02:00
Nicola Murino
92424cd1c2 dependabot: limit the number of open pull requests 2021-04-25 16:39:41 +02:00
Nicola Murino
0190dad984 docker: update github script to v4 2021-04-25 15:59:29 +02:00
Nicola Murino
198258f4e7 add dependabot
Fixes #388
2021-04-25 15:54:19 +02:00
Nicola Murino
5be4b6bd44 localfs: fix subdir check if the user has the root dir as home 2021-04-25 14:36:29 +02:00
Nicola Murino
3941255733 docs: fix a typo 2021-04-25 09:42:19 +02:00
Nicola Murino
46998252e5 use bcrypt as default password hashing algo
argon2id has a high memory cost and, if not properly tuned, it can lead to
resource starvation.

Advanced users can still configure and use argon2id.
Passwords stored as argon2id will continue to work
2021-04-25 09:38:33 +02:00
Nicola Murino
74b51f0ad3 update nfpm 2021-04-23 22:53:13 +02:00
Nicola Murino
b11865f971 CI: add support for darwin/arm64
I have no way to test the produced binaries on a real Silicon M1
2021-04-20 23:00:27 +02:00
Nicola Murino
f4369cdbef fix max connections check
Also make sure to close the ssh client connection in test cases
2021-04-20 18:12:16 +02:00
Nicola Murino
92638ce93d add support for hashing password using bcrypt
argon2id remains the default
2021-04-20 13:55:09 +02:00
Nicola Murino
6ef85d6026 add, optional, in memory password caching
Verifying argon2 passwords has a high memory and computational cost,
by enabling, in memory, password caching you reduce this cost
2021-04-20 09:39:36 +02:00
Nicola Murino
bc88503f25 sql providers: reuse the same context where appropriate 2021-04-19 18:58:53 +02:00
Nicola Murino
47317bed9b make sure that Retry-After header has a value greater than zero 2021-04-19 09:16:27 +02:00
Nicola Murino
f45c89fc46 add rate limiting support for REST API/web admin too 2021-04-19 08:14:04 +02:00
Nicola Murino
112e3b2fc2 add rate limiting support 2021-04-18 12:31:06 +02:00
Nicola Murino
124c471a2b FTPD: make sure that the passive ip, if provided, is valid
The server will refuse to start if the provided passive ip is not a
valid IPv4 address.

Fixes #376
2021-04-16 15:08:10 +02:00
Nicola Murino
683ba6cd5b get binding from env: respect the documented default
Fixes #377
2021-04-16 13:35:13 +02:00
Nicola Murino
21fbcf4556 FTP: add support for TLS session resumption on the data connection
Fixes #374
2021-04-16 09:00:40 +02:00
Nicola Murino
2ffefbeb33 add sql_tables_prefix also to indexes and constraints
This allows you to reuse the same database for multiple SFTPGo instances

Fixes #372
2021-04-12 20:00:49 +02:00
Nicola Murino
c844fc7477 add support for delayed quota update
If there are a lot of close uploads, accumulating quota updates can
save you many queries to the data provider
2021-04-11 08:38:43 +02:00
Nicola Murino
4b98f37df1 back to development 2021-04-10 09:40:02 +02:00
Nicola Murino
0bc4db9950 web admin: make base url configurable 2021-04-09 22:02:48 +02:00
Nicola Murino
5acf29dae6 CI: replace deprecated actions with gh CLI 2021-04-08 21:29:09 +02:00
Nicola Murino
e9a42cd508 release workflow: re-add build Linux bundle
it is used as source for PPA packages
2021-04-08 08:38:51 +02:00
Nicola Murino
ed26d68948 portable mode: add SFTP buffer size 2021-04-07 19:47:39 +02:00
Nicola Murino
b389f93d97 allow to select sha256-simd using an env var 2021-04-07 16:25:58 +02:00
Nicola Murino
150aebf8d2 CI: replace xgo with QEMU
currently xgo don't allow to choose the building OS, this could cause
unexpected issues, for example v2.0.3 packages for arm64 and ppc64
don't run on Ubuntu 18.04
2021-04-07 15:12:09 +02:00
Nicola Murino
74e0223eb9 remove sha256-simd usage
sha256-simd is now deprecated

https://github.com/minio/sha256-simd/issues/58

This could slow down sha256 computation on some CPU
2021-04-05 18:23:40 +02:00
Nicola Murino
0823928f98 allow to disable login filesystem checks
SFTPGo requires that the user's home directory, virtual folder root,
and intermediate paths to virtual folders exist to work properly.
If you already know that the required directories exist, disabling
these checks will speed up login.
2021-04-05 17:57:30 +02:00
Nicola Murino
f895059660 web: add responsive table style to connections too
Fixed a small issue for sftpfs too
2021-04-05 11:28:28 +02:00
Nicola Murino
acb4310c11 add a startup hook 2021-04-05 10:07:59 +02:00
Nicola Murino
fdf3f23df5 allow to disable some hooks on a per-user basis
This way you can, for example, mix external and internal users
2021-04-04 22:32:25 +02:00
Nicola Murino
d92861a8e8 sftpfs: disable buffering for downloads if concurrent reads are disabled 2021-04-04 09:53:29 +02:00
Nicola Murino
1ee843757d fix OpenAPI schema 2021-04-03 17:09:08 +02:00
Nicola Murino
ea26d7786c sftpfs: add buffering support
this way we improve performance over high latency networks
2021-04-03 16:00:55 +02:00
Nicola Murino
6eb43baf3d web: fix content type for folders form
Fixes #367
2021-04-01 19:42:18 +02:00
Nicola Murino
2f56375121 improve SFTP loop detection 2021-04-01 18:53:48 +02:00
Nicola Murino
3bfd7e4d17 sftpfs: try to detect if an SFTP user point to itself
this will cause an infinite loop on login. The check should be improved
2021-03-29 21:53:44 +02:00
Nicola Murino
e1c66d96a1 back to development 2021-03-28 22:25:24 +02:00
Nicola Murino
a43854ae9b OpenAPI: document that secrets are automatically encrypted before saving 2021-03-28 11:23:06 +02:00
Nicola Murino
183bedd6ed webui: add responsive extension 2021-03-28 11:02:11 +02:00
Nicola Murino
2a89a8f664 webui: minor improvements 2021-03-27 22:23:01 +01:00
Nicola Murino
5cd27ce529 document Cockroach driver name 2021-03-27 19:41:00 +01:00
Nicola Murino
cee2e18caf convertusers: fix permissions
Fixes #363
2021-03-27 19:18:01 +01:00
Nicola Murino
9ad750da54 WebDAV: try to preserve the lock fs as much as possible 2021-03-27 19:10:27 +01:00
Nicola Murino
5f49af1780 external auth: allow to inspect and preserve an existing user 2021-03-26 15:19:01 +01:00
Nicola Murino
d5f092284a improve signals handling 2021-03-25 19:31:21 +01:00
Nicola Murino
0e50310a66 add a test case for UID/GID limits 2021-03-25 17:30:39 +01:00
Mike Unitskyi
5939ac4801 Increase uid:gid limits (#362)
Fixes #361
2021-03-25 17:11:42 +01:00
Nicola Murino
db274f1093 crdb: fix transactions handling 2021-03-25 09:07:56 +01:00
Nicola Murino
6bc5c64a3a webdav: ignore path, perm and not exist errors in PROPFIND
Fixes #340
2021-03-24 13:32:20 +01:00
Nicola Murino
70e035315e data provider: add CockroachDB support 2021-03-23 19:14:15 +01:00
Nicola Murino
8a1249878a OpenAPI schema: remove some superfluous required definitions
Fixes #356
2021-03-22 19:22:41 +01:00
Nicola Murino
5e375f56dd kms: add a lock, secrets could be modified concurrently for cached users
also reduce the size of the JSON payload omitting empty secrets
2021-03-22 19:03:25 +01:00
Nicola Murino
28f1d66ae5 link the Active Directory example in the howto section 2021-03-22 09:52:05 +01:00
Omar Ramos
79060d37a7 Added in a first draft of the page related to sftpgo-ldap-http-server. 2021-03-22 08:59:29 +01:00
Nicola Murino
800e64404b update deps 2021-03-22 08:55:35 +01:00
Nicola Murino
54c0c1b80d Windows: manually check if we can bind on the configured port/ports
Windows allows the coexistence of three types of sockets on the same
transport-layer service port, for example, 127.0.0.1:8080, [::1]:8080
and [::ffff:0.0.0.0]:8080

Go don't properly handles this, so we use a ugly hack

Fixes #350
2021-03-21 22:21:04 +01:00
Nicola Murino
f7c7e2951d initialize argon params before creating the data provider
Fixes #349
2021-03-21 19:58:57 +01:00
Nicola Murino
f249286cb1 docs: add some notes about the new virtual folders support
fixe a failing test case for the memory provider
2021-03-21 19:47:11 +01:00
Nicola Murino
d6dc3a507e extend virtual folders support to all storage backends
Fixes #241
2021-03-21 19:15:47 +01:00
Nicola Murino
0286da2356 try to auto create virtual folders if missing 2021-03-10 22:30:56 +01:00
Nicola Murino
76c08baaa0 httpclient: load CA certificates only when required
on Windows x509.SystemCertPool is not implemented and therefore we end
uo with an empty certificate pool if we load the CA certificates
unconditionally
2021-03-10 21:45:48 +01:00
Nicola Murino
67ea75cf03 improve OpenAPI schema so it is better rendered on Stoplight 2021-03-07 18:41:56 +01:00
Nicola Murino
4c658bb6f0 webdav: add prefix support 2021-03-07 17:10:45 +01:00
Nicola Murino
1ab02d5891 OpenAPI: improve schema
Fix some lint warnings
2021-03-06 17:08:24 +01:00
Nicola Murino
055506e518 sftpfs: add an option to disable concurrent reads 2021-03-06 15:41:40 +01:00
Nicola Murino
88122ba2f8 update jwtauth to v5 2021-03-05 18:50:45 +01:00
Nicola Murino
bfe0c18976 portable mode: fix WebDAV support 2021-03-05 08:41:24 +01:00
Nicola Murino
df41f0c556 add a setting to skip natural keys validation
Enabling the "skip_natural_keys_validation" data provider setting,
the natural keys for REST API/Web Admin as usernames, admin names,
folder names are not restricted to unreserved URI chars

Fixes #334 #308
2021-03-04 09:48:53 +01:00
Nicola Murino
561c5021dd add Segmed to the sponsors section 2021-03-03 18:55:47 +01:00
Nicola Murino
ad07fc78eb update nfpm and deps 2021-03-03 18:39:58 +01:00
Nicola Murino
3243181c5f Add a link to the OpenAPI schema where relevant
Fixes #329
2021-03-01 22:22:05 +01:00
Nicola Murino
895117718e SSH system command: add os separator to the resolved path when appropriate
Fixes #327
2021-03-01 22:10:45 +01:00
Nicola Murino
534b253c20 WebDAV: improve TLS certificate authentication
For each user you can now configure:

- TLS certificate auth
- TLS certificate auth and password
- Password auth

For TLS certificate auth, the certificate common name is used as
username
2021-03-01 19:28:11 +01:00
Nicola Murino
901cafc6da metrics: reduce complexity for AddLoginResult method
fix a gocyclo warning
2021-02-28 12:23:48 +01:00
Nicola Murino
a6e36e7cad FTP: improve TLS certificate authentication
For each user you can now configure:

- TLS certificate auth
- TLS certificate auth and password
- Password auth

For TLS auth, the certificate common name must match the name provided
using the "USER" FTP command
2021-02-28 12:10:40 +01:00
Nicola Murino
b566457e12 change license to AGPL-3 2021-02-26 19:47:48 +01:00
Nicola Murino
ca3e15578e Use new methods in the io and os packages instead of ioutil ones
ioutil is deprecated in Go 1.16 and SFTPGo is an application, not
a library, we have no reason to keep compatibility with old Go
versions.

Go 1.16 fix some cifs related issues too.
2021-02-25 21:53:04 +01:00
Nicola Murino
4b2edff6dd update deps 2021-02-24 22:27:52 +01:00
Nicola Murino
2146b83343 data providers: add filesystem to folder ...
... and some descriptive fields.
The filesystem support for virtual folders will be implemented in
future commits
2021-02-24 19:40:29 +01:00
Nicola Murino
3e1b07324d GCS: remove compat code 2021-02-22 22:06:23 +01:00
Nicola Murino
8cc2dfe5c2 update pkg/sftp
we don't need my branch anymore now that all the required features for
the sftpfs are available upstream too
2021-02-22 16:27:45 +01:00
Nicola Murino
78a837e8f1 remove other compat code 2021-02-22 09:13:26 +01:00
Nicola Murino
49830516be squash database migrations and remove compat code 2021-02-22 08:37:50 +01:00
Nicola Murino
41e1d9e68a use Go 1.16 for CI and Docker images 2021-02-21 12:01:37 +01:00
Nicola Murino
5da4f931c5 TLS: allow to configure cipher suites
Fixes #316
2021-02-18 20:17:16 +01:00
Nicola Murino
552a96533e back to development 2021-02-17 09:45:20 +01:00
Nicola Murino
cebd069c77 set version to 2.0.2 2021-02-17 08:10:17 +01:00
Nicola Murino
be9230e85b micro optimizations spotted using the go-critic linter 2021-02-16 19:11:36 +01:00
Nicola Murino
b1ce6eb85b web admin: allow to set an empty password for SFTPGo users 2021-02-15 19:38:53 +01:00
Nicola Murino
46176a54b4 minor doc fixes 2021-02-14 22:08:08 +01:00
Nicola Murino
a21ccad174 web hooks: add mutual TLS support 2021-02-13 14:41:37 +01:00
Nicola Murino
1129a868a5 Improve powershell completion
cobra 1.1.3 has much better powershell support
2021-02-13 09:10:35 +01:00
Nicola Murino
1ac66d27b6 Use IEC units for byte counting everywhere 2021-02-12 22:16:35 +01:00
Nicola Murino
6a6e8fffbc web hooks: improve resilience by adding a configurable retry
the retryable http client is used for hooks that notify events
2021-02-12 21:42:49 +01:00
Nicola Murino
51f110bc7b sftpd: add statvfs@openssh.com support 2021-02-11 19:45:52 +01:00
Nicola Murino
4ddfe41f23 loaddata: restore admins too 2021-02-11 08:33:32 +01:00
Nicola Murino
ddd06fc2ac docker: add permissions to data dirs
This way data and backup dirs can be mounted as separate volumes.

Based on the proof of concept submitted by

Mark Sagi-Kazar <mark.sagikazar@gmail.com>

See #305
2021-02-10 19:04:06 +01:00
Nicola Murino
1bccb93fcb rename default branch from master to main 2021-02-09 19:53:03 +01:00
Nicola Murino
db80781716 validation: improve error message for invalid chars 2021-02-08 21:32:59 +01:00
Nicola Murino
a2a99f9b57 merge full and slim dockerfiles
Fixes #232
2021-02-07 21:49:04 +01:00
Nicola Murino
cd4a68cc96 set version to 2.0.1 2021-02-06 15:28:30 +01:00
Nicola Murino
b37eb68993 docker alpine: revert to 3.12 since we have to release 2.0.1 2021-02-06 14:58:19 +01:00
Nicola Murino
b13958a8d6 docker: fix httpd address 2021-02-06 14:51:55 +01:00
Nicola Murino
17e2b234a0 dataprovider: fix migration with old mysql versions
Fixes #298
2021-02-06 14:33:51 +01:00
Nicola Murino
4ef1775e9a docker: switch to Alpine 3.13 2021-02-06 12:54:13 +01:00
Nicola Murino
363977b474 back to development 2021-02-06 12:23:26 +01:00
679 changed files with 195734 additions and 55107 deletions

30
.cirrus.yml Normal file
View File

@@ -0,0 +1,30 @@
freebsd_task:
name: FreeBSD
matrix:
- name: FreeBSD 13.2
freebsd_instance:
image_family: freebsd-13-2
pkginstall_script:
- pkg update -f
- pkg install -y go
- pkg install -y git
setup_script:
- pw groupadd sftpgo
- pw useradd sftpgo -g sftpgo -w none -m
- mkdir /home/sftpgo/sftpgo
- cp -R . /home/sftpgo/sftpgo
- chown -R sftpgo:sftpgo /home/sftpgo/sftpgo
compile_script:
- su sftpgo -c 'cd ~/sftpgo && go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo'
- su sftpgo -c 'cd ~/sftpgo/tests/eventsearcher && go build -trimpath -ldflags "-s -w" -o eventsearcher'
- su sftpgo -c 'cd ~/sftpgo/tests/ipfilter && go build -trimpath -ldflags "-s -w" -o ipfilter'
check_script:
- su sftpgo -c 'cd ~/sftpgo && ./sftpgo initprovider && ./sftpgo resetprovider --force'
test_script:
- su sftpgo -c 'cd ~/sftpgo && go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 20m ./... -coverprofile=coverage.txt -covermode=atomic'

106
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,106 @@
name: Open Source Bug Report
description: "Submit a report and help us improve SFTPGo"
title: "[Bug]: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
### 👍 Thank you for contributing to our project!
Before asking for help please check the [support policy](https://github.com/drakkan/sftpgo#support-policy).
If you are a commercial user or a project sponsor please contact us using the dedicated [email address](mailto:support@sftpgo.com).
- type: checkboxes
id: before-posting
attributes:
label: "⚠️ This issue respects the following points: ⚠️"
description: All conditions are **required**.
options:
- label: This is a **bug**, not a question or a configuration issue.
required: true
- label: This issue is **not** already reported on Github _(I've searched it)_.
required: true
- type: textarea
id: bug-description
attributes:
label: Bug description
description: |
Provide a description of the bug you're experiencing.
Don't just expect someone will guess what your specific problem is and provide full details.
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Steps to reproduce
description: |
Describe the steps to reproduce the bug.
The better your description is the fastest you'll get an _(accurate)_ answer.
value: |
1.
2.
3.
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: Describe what you expected to happen instead.
validations:
required: true
- type: input
id: version
attributes:
label: SFTPGo version
validations:
required: true
- type: input
id: data-provider
attributes:
label: Data provider
validations:
required: true
- type: dropdown
id: install-method
attributes:
label: Installation method
description: |
Select installation method you've used.
_Describe the method in the "Additional info" section if you chose "Other"._
options:
- "Community Docker image"
- "Community Deb package"
- "Community RPM package"
- "Other"
validations:
required: true
- type: textarea
attributes:
label: Configuration
description: "Describe your customizations to the configuration: both config file changes and overrides via environment variables"
value: config
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
- type: dropdown
id: usecase
attributes:
label: What are you using SFTPGo for?
description: We'd like to understand your SFTPGo usecase more
multiple: true
options:
- "Private user, home usecase (home backup/VPS)"
- "Professional user, 1 person business"
- "Small business (3-person firm with file exchange?)"
- "Medium business"
- "Enterprise"
- type: textarea
id: additional-info
attributes:
label: Additional info
description: Any additional information related to the issue.

9
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,9 @@
blank_issues_enabled: false
contact_links:
- name: Commercial Support
url: https://sftpgo.com/
about: >
If you need Professional support, so your reports are prioritized and resolved more quickly.
- name: GitHub Community Discussions
url: https://github.com/drakkan/sftpgo/discussions
about: Please ask and answer questions here.

View File

@@ -0,0 +1,40 @@
name: 🚀 Feature request
description: Suggest an idea for SFTPGo
labels: ["suggestion"]
body:
- type: textarea
attributes:
label: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is.
validations:
required: false
- type: textarea
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: false
- type: dropdown
id: usecase
attributes:
label: What are you using SFTPGo for?
description: We'd like to understand your SFTPGo usecase more
multiple: true
options:
- "Private user, home usecase (home backup/VPS)"
- "Professional user, 1 person business"
- "Small business (3-person firm with file exchange?)"
- "Medium business"
- "Enterprise"
- type: textarea
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
validations:
required: false

20
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 2
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 2
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 2

36
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: "Code scanning - action"
on:
push:
pull_request:
schedule:
- cron: '30 1 * * 6'
jobs:
CodeQL-Build:
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.20'
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: go
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View File

@@ -2,7 +2,7 @@ name: CI
on:
push:
branches: [master]
branches: [2.5.x]
pull_request:
jobs:
@@ -11,193 +11,239 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
go: [1.15]
go: ['1.20']
os: [ubuntu-latest, macos-latest]
upload-coverage: [true]
include:
#- go: 1.14
# os: ubuntu-latest
# upload-coverage: false
- go: 1.15
- go: '1.20'
os: windows-latest
upload-coverage: false
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go }}
- name: Build for Linux/macOS
- name: Build for Linux/macOS x86_64
if: startsWith(matrix.os, 'windows-') != true
run: go build -ldflags "-s -w -X github.com/drakkan/sftpgo/version.commit=`git describe --always --dirty` -X github.com/drakkan/sftpgo/version.date=`date -u +%FT%TZ`" -o sftpgo
run: |
go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
cd tests/eventsearcher
go build -trimpath -ldflags "-s -w" -o eventsearcher
cd -
cd tests/ipfilter
go build -trimpath -ldflags "-s -w" -o ipfilter
cd -
./sftpgo initprovider
./sftpgo resetprovider --force
- name: Build for macOS arm64
if: startsWith(matrix.os, 'macos-') == true
run: CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 SDKROOT=$(xcrun --sdk macosx --show-sdk-path) go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo_arm64
- name: Build for Windows
if: startsWith(matrix.os, 'windows-')
run: |
$GIT_COMMIT = (git describe --always --dirty) | Out-String
$GIT_COMMIT = (git describe --always --abbrev=8 --dirty) | Out-String
$DATE_TIME = ([datetime]::Now.ToUniversalTime().toString("yyyy-MM-ddTHH:mm:ssZ")) | Out-String
go build -ldflags "-s -w -X github.com/drakkan/sftpgo/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/version.date=$DATE_TIME" -o sftpgo.exe
$LATEST_TAG = ((git describe --tags $(git rev-list --tags --max-count=1)) | Out-String).Trim()
$REV_LIST=$LATEST_TAG+"..HEAD"
$COMMITS_FROM_TAG= ((git rev-list $REV_LIST --count) | Out-String).Trim()
$FILE_VERSION = $LATEST_TAG.substring(1) + "." + $COMMITS_FROM_TAG
go install github.com/tc-hib/go-winres@latest
go-winres simply --arch amd64 --product-version $LATEST_TAG-dev-$GIT_COMMIT --file-version $FILE_VERSION --file-description "SFTPGo server" --product-name SFTPGo --copyright "AGPL-3.0" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o sftpgo.exe
cd tests/eventsearcher
go build -trimpath -ldflags "-s -w" -o eventsearcher.exe
cd ../..
cd tests/ipfilter
go build -trimpath -ldflags "-s -w" -o ipfilter.exe
cd ../..
mkdir arm64
$Env:CGO_ENABLED='0'
$Env:GOOS='windows'
$Env:GOARCH='arm64'
go-winres simply --arch arm64 --product-version $LATEST_TAG-dev-$GIT_COMMIT --file-version $FILE_VERSION --file-description "SFTPGo server" --product-name SFTPGo --copyright "AGPL-3.0" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
go build -trimpath -tags nopgxregisterdefaulttypes,nosqlite -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o .\arm64\sftpgo.exe
mkdir x86
$Env:GOARCH='386'
go-winres simply --arch 386 --product-version $LATEST_TAG-dev-$GIT_COMMIT --file-version $FILE_VERSION --file-description "SFTPGo server" --product-name SFTPGo --copyright "AGPL-3.0" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
go build -trimpath -tags nopgxregisterdefaulttypes,nosqlite -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o .\x86\sftpgo.exe
Remove-Item Env:\CGO_ENABLED
Remove-Item Env:\GOOS
Remove-Item Env:\GOARCH
- name: Run test cases using SQLite provider
run: go test -v -p 1 -timeout 10m ./... -coverprofile=coverage.txt -covermode=atomic
run: go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -coverprofile=coverage.txt -covermode=atomic
- name: Upload coverage to Codecov
if: ${{ matrix.upload-coverage }}
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v3
with:
file: ./coverage.txt
fail_ci_if_error: false
- name: Run test cases using bolt provider
run: |
go test -v -p 1 -timeout 2m ./config -covermode=atomic
go test -v -p 1 -timeout 2m ./common -covermode=atomic
go test -v -p 1 -timeout 3m ./httpd -covermode=atomic
go test -v -p 1 -timeout 8m ./sftpd -covermode=atomic
go test -v -p 1 -timeout 2m ./ftpd -covermode=atomic
go test -v -p 1 -timeout 2m ./webdavd -covermode=atomic
go test -v -p 1 -timeout 2m ./telemetry -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 2m ./internal/config -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 5m ./internal/common -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 5m ./internal/httpd -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 8m ./internal/sftpd -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 5m ./internal/ftpd -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 5m ./internal/webdavd -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 2m ./internal/telemetry -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 2m ./internal/mfa -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 2m ./internal/command -covermode=atomic
env:
SFTPGO_DATA_PROVIDER__DRIVER: bolt
SFTPGO_DATA_PROVIDER__NAME: 'sftpgo_bolt.db'
- name: Run test cases using memory provider
run: go test -v -p 1 -timeout 10m ./... -covermode=atomic
run: go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -covermode=atomic
env:
SFTPGO_DATA_PROVIDER__DRIVER: memory
SFTPGO_DATA_PROVIDER__NAME: ''
- name: Gather cross build info
id: cross_info
if: ${{ matrix.upload-coverage && startsWith(matrix.os, 'ubuntu-') }}
- name: Prepare build artifact for macOS
if: startsWith(matrix.os, 'macos-') == true
run: |
GIT_COMMIT=$(git describe --always)
BUILD_DATE=$(date -u +%FT%TZ)
echo ::set-output name=sha::${GIT_COMMIT}
echo ::set-output name=created::${BUILD_DATE}
- name: Cross build with xgo
if: ${{ matrix.upload-coverage && startsWith(matrix.os, 'ubuntu-') }}
uses: crazy-max/ghaction-xgo@v1
with:
dest: cross
prefix: sftpgo
targets: linux/arm64,linux/ppc64le
v: true
x: false
race: false
ldflags: -s -w -X github.com/drakkan/sftpgo/version.commit=${{ steps.cross_info.outputs.sha }} -X github.com/drakkan/sftpgo/version.date=${{ steps.cross_info.outputs.created }}
buildmode: default
- name: Prepare build artifact for Linux/macOS
if: startsWith(matrix.os, 'windows-') != true
run: |
mkdir -p output/{bash_completion,zsh_completion}
cp sftpgo output/
mkdir -p output/{init,bash_completion,zsh_completion}
cp sftpgo output/sftpgo_x86_64
cp sftpgo_arm64 output/
cp sftpgo.json output/
cp -r templates output/
cp -r static output/
cp -r init output/
cp -r openapi output/
cp init/com.github.drakkan.sftpgo.plist output/init/
./sftpgo gen completion bash > output/bash_completion/sftpgo
./sftpgo gen completion zsh > output/zsh_completion/_sftpgo
./sftpgo gen man -d output/man/man1
gzip output/man/man1/*
- name: Copy cross compiled Linux binaries
if: ${{ matrix.upload-coverage && startsWith(matrix.os, 'ubuntu-') }}
run: |
cp cross/sftpgo-linux-arm64 output/
cp cross/sftpgo-linux-ppc64le output/
- name: Prepare build artifact for Windows
if: startsWith(matrix.os, 'windows-')
- name: Prepare Windows installer
if: ${{ startsWith(matrix.os, 'windows-') && github.event_name != 'pull_request' }}
run: |
Remove-Item -LiteralPath "output" -Force -Recurse -ErrorAction Ignore
mkdir output
copy .\sftpgo.exe .\output
copy .\sftpgo.json .\output
copy .\sftpgo.db .\output
copy .\LICENSE .\output\LICENSE.txt
mkdir output\templates
xcopy .\templates .\output\templates\ /E
mkdir output\static
xcopy .\static .\output\static\ /E
mkdir output\openapi
xcopy .\openapi .\output\openapi\ /E
$LATEST_TAG = ((git describe --tags $(git rev-list --tags --max-count=1)) | Out-String).Trim()
$REV_LIST=$LATEST_TAG+"..HEAD"
$COMMITS_FROM_TAG= ((git rev-list $REV_LIST --count) | Out-String).Trim()
$Env:SFTPGO_ISS_DEV_VERSION = $LATEST_TAG + "." + $COMMITS_FROM_TAG
$CERT_PATH=(Get-Location -PSProvider FileSystem).ProviderPath + "\cert.pfx"
[IO.File]::WriteAllBytes($CERT_PATH,[System.Convert]::FromBase64String($Env:CERT_DATA))
certutil -f -p "$Env:CERT_PASS" -importpfx MY "$CERT_PATH"
rm "$CERT_PATH"
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.20348.0/x86/signtool.exe' sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n "Nicola Murino" /d "SFTPGo" .\sftpgo.exe
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.20348.0/x86/signtool.exe' sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n "Nicola Murino" /d "SFTPGo" .\arm64\sftpgo.exe
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.20348.0/x86/signtool.exe' sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n "Nicola Murino" /d "SFTPGo" .\x86\sftpgo.exe
$INNO_S='/Ssigntool=$qC:/Program Files (x86)/Windows Kits/10/bin/10.0.20348.0/x86/signtool.exe$q sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n $qNicola Murino$q /d $qSFTPGo$q $f'
iscc "$INNO_S" .\windows-installer\sftpgo.iss
rm .\output\sftpgo.exe
rm .\output\sftpgo.db
copy .\arm64\sftpgo.exe .\output
(Get-Content .\output\sftpgo.json).replace('"sqlite"', '"bolt"') | Set-Content .\output\sftpgo.json
$Env:SFTPGO_DATA_PROVIDER__DRIVER='bolt'
$Env:SFTPGO_DATA_PROVIDER__NAME='.\output\sftpgo.db'
.\sftpgo.exe initprovider
Remove-Item Env:\SFTPGO_DATA_PROVIDER__DRIVER
Remove-Item Env:\SFTPGO_DATA_PROVIDER__NAME
$Env:SFTPGO_ISS_ARCH='arm64'
iscc "$INNO_S" .\windows-installer\sftpgo.iss
rm .\output\sftpgo.exe
copy .\x86\sftpgo.exe .\output
$Env:SFTPGO_ISS_ARCH='x86'
iscc "$INNO_S" .\windows-installer\sftpgo.iss
certutil -delstore MY "Nicola Murino"
env:
CERT_DATA: ${{ secrets.CERT_DATA }}
CERT_PASS: ${{ secrets.CERT_PASS }}
- name: Upload Windows installer x86_64 artifact
if: ${{ startsWith(matrix.os, 'windows-') && github.event_name != 'pull_request' }}
uses: actions/upload-artifact@v3
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@v3
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@v3
with:
name: sftpgo_windows_installer_x86
path: ./sftpgo_windows_x86.exe
- name: Prepare build artifact for Windows
if: startsWith(matrix.os, 'windows-')
run: |
Remove-Item -LiteralPath "output" -Force -Recurse -ErrorAction Ignore
mkdir output
copy .\sftpgo.exe .\output
mkdir output\arm64
copy .\arm64\sftpgo.exe .\output\arm64
mkdir output\x86
copy .\x86\sftpgo.exe .\output\x86
copy .\sftpgo.json .\output
(Get-Content .\output\sftpgo.json).replace('"sqlite"', '"bolt"') | Set-Content .\output\sftpgo.json
mkdir output\templates
xcopy .\templates .\output\templates\ /E
mkdir output\static
xcopy .\static .\output\static\ /E
mkdir output\openapi
xcopy .\openapi .\output\openapi\ /E
- name: Upload build artifact
uses: actions/upload-artifact@v2
if: startsWith(matrix.os, 'ubuntu-') != true
uses: actions/upload-artifact@v3
with:
name: sftpgo-${{ matrix.os }}-go${{ matrix.go }}
name: sftpgo-${{ matrix.os }}-go-${{ matrix.go }}
path: output
- name: Build Linux Packages
id: build_linux_pkgs
if: ${{ matrix.upload-coverage && startsWith(matrix.os, 'ubuntu-') }}
test-build-flags:
name: Test build flags
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.20'
- name: Build
run: |
cp -r pkgs pkgs_arm64
cp -r pkgs pkgs_ppc64le
cd pkgs
./build.sh
cd ..
export NFPM_ARCH=arm64
export BIN_SUFFIX=-linux-arm64
cp cross/sftpgo${BIN_SUFFIX} .
cd pkgs_arm64
./build.sh
cd ..
export NFPM_ARCH=ppc64le
export BIN_SUFFIX=-linux-ppc64le
cp cross/sftpgo${BIN_SUFFIX} .
cd pkgs_ppc64le
./build.sh
PKG_VERSION=$(cat dist/version)
echo "::set-output name=pkg-version::${PKG_VERSION}"
go build -trimpath -tags nopgxregisterdefaulttypes,nogcs,nos3,noportable,nobolt,nomysql,nopgsql,nosqlite,nometrics,noazblob,unixcrypt -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
./sftpgo -v
cp -r openapi static templates internal/bundle/
go build -trimpath -tags nopgxregisterdefaulttypes,bundle -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
./sftpgo -v
- name: Upload Debian Package
if: ${{ matrix.upload-coverage && startsWith(matrix.os, 'ubuntu-') }}
uses: actions/upload-artifact@v2
with:
name: sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-x86_64-deb
path: pkgs/dist/deb/*
- name: Upload RPM Package
if: ${{ matrix.upload-coverage && startsWith(matrix.os, 'ubuntu-') }}
uses: actions/upload-artifact@v2
with:
name: sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-x86_64-rpm
path: pkgs/dist/rpm/*
- name: Upload Debian Package arm64
if: ${{ matrix.upload-coverage && startsWith(matrix.os, 'ubuntu-') }}
uses: actions/upload-artifact@v2
with:
name: sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-arm64-deb
path: pkgs_arm64/dist/deb/*
- name: Upload RPM Package arm64
if: ${{ matrix.upload-coverage && startsWith(matrix.os, 'ubuntu-') }}
uses: actions/upload-artifact@v2
with:
name: sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-arm64-rpm
path: pkgs_arm64/dist/rpm/*
- name: Upload Debian Package ppc64le
if: ${{ matrix.upload-coverage && startsWith(matrix.os, 'ubuntu-') }}
uses: actions/upload-artifact@v2
with:
name: sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-ppc64le-deb
path: pkgs_ppc64le/dist/deb/*
- name: Upload RPM Package ppc64le
if: ${{ matrix.upload-coverage && startsWith(matrix.os, 'ubuntu-') }}
uses: actions/upload-artifact@v2
with:
name: sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-ppc64le-rpm
path: pkgs_ppc64le/dist/rpm/*
test-postgresql-mysql:
name: Test with PostgreSQL/MySQL
test-postgresql-mysql-crdb:
name: Test with PgSQL/MySQL/Cockroach
runs-on: ubuntu-latest
services:
@@ -222,27 +268,64 @@ jobs:
MYSQL_USER: sftpgo
MYSQL_PASSWORD: sftpgo
options: >-
--health-cmd "mysqladmin status -h 127.0.0.1 -P 3306 -u root -p$MYSQL_ROOT_PASSWORD"
--health-cmd "mariadb-admin status -h 127.0.0.1 -P 3306 -u root -p$MYSQL_ROOT_PASSWORD"
--health-interval 10s
--health-timeout 5s
--health-retries 6
ports:
- 3307:3306
mysql:
image: mysql:latest
env:
MYSQL_ROOT_PASSWORD: mysql
MYSQL_DATABASE: sftpgo
MYSQL_USER: sftpgo
MYSQL_PASSWORD: sftpgo
options: >-
--health-cmd "mysqladmin status -h 127.0.0.1 -P 3306 -u root -p$MYSQL_ROOT_PASSWORD"
--health-interval 10s
--health-timeout 5s
--health-retries 6
ports:
- 3308:3306
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: 1.15
go-version: '1.20'
- name: Build
run: go build -ldflags "-s -w -X github.com/drakkan/sftpgo/version.commit=`git describe --always --dirty` -X github.com/drakkan/sftpgo/version.date=`date -u +%FT%TZ`" -o sftpgo
run: |
go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
cd tests/eventsearcher
go build -trimpath -ldflags "-s -w" -o eventsearcher
cd -
cd tests/ipfilter
go build -trimpath -ldflags "-s -w" -o ipfilter
cd -
- name: Run tests using MySQL provider
run: |
./sftpgo initprovider
./sftpgo resetprovider --force
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -covermode=atomic
env:
SFTPGO_DATA_PROVIDER__DRIVER: mysql
SFTPGO_DATA_PROVIDER__NAME: sftpgo
SFTPGO_DATA_PROVIDER__HOST: localhost
SFTPGO_DATA_PROVIDER__PORT: 3308
SFTPGO_DATA_PROVIDER__USERNAME: sftpgo
SFTPGO_DATA_PROVIDER__PASSWORD: sftpgo
- name: Run tests using PostgreSQL provider
run: |
go test -v -p 1 -timeout 10m ./... -covermode=atomic
./sftpgo initprovider
./sftpgo resetprovider --force
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -covermode=atomic
env:
SFTPGO_DATA_PROVIDER__DRIVER: postgresql
SFTPGO_DATA_PROVIDER__NAME: sftpgo
@@ -251,9 +334,11 @@ jobs:
SFTPGO_DATA_PROVIDER__USERNAME: postgres
SFTPGO_DATA_PROVIDER__PASSWORD: postgres
- name: Run tests using MySQL provider
- name: Run tests using MariaDB provider
run: |
go test -v -p 1 -timeout 10m ./... -covermode=atomic
./sftpgo initprovider
./sftpgo resetprovider --force
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -covermode=atomic
env:
SFTPGO_DATA_PROVIDER__DRIVER: mysql
SFTPGO_DATA_PROVIDER__NAME: sftpgo
@@ -261,13 +346,180 @@ jobs:
SFTPGO_DATA_PROVIDER__PORT: 3307
SFTPGO_DATA_PROVIDER__USERNAME: sftpgo
SFTPGO_DATA_PROVIDER__PASSWORD: sftpgo
SFTPGO_DATA_PROVIDER__SQL_TABLES_PREFIX: prefix_
- name: Run tests using CockroachDB provider
run: |
docker run --rm --name crdb --health-cmd "curl -I http://127.0.0.1:8080" --health-interval 10s --health-timeout 5s --health-retries 6 -p 26257:26257 -d cockroachdb/cockroach:latest start-single-node --insecure --listen-addr :26257
sleep 10
docker exec crdb cockroach sql --insecure -e 'create database "sftpgo"'
./sftpgo initprovider
./sftpgo resetprovider --force
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -covermode=atomic
docker stop crdb
env:
SFTPGO_DATA_PROVIDER__DRIVER: cockroachdb
SFTPGO_DATA_PROVIDER__NAME: sftpgo
SFTPGO_DATA_PROVIDER__HOST: localhost
SFTPGO_DATA_PROVIDER__PORT: 26257
SFTPGO_DATA_PROVIDER__USERNAME: root
SFTPGO_DATA_PROVIDER__PASSWORD:
SFTPGO_DATA_PROVIDER__TARGET_SESSION_ATTRS: any
SFTPGO_DATA_PROVIDER__SQL_TABLES_PREFIX: prefix_
build-linux-packages:
name: Build Linux packages
runs-on: ubuntu-latest
strategy:
matrix:
include:
- arch: amd64
distro: ubuntu:18.04
go: latest
go-arch: amd64
- arch: aarch64
distro: ubuntu18.04
go: latest
go-arch: arm64
- arch: ppc64le
distro: ubuntu18.04
go: latest
go-arch: ppc64le
- arch: armv7
distro: ubuntu18.04
go: latest
go-arch: arm7
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get commit SHA
id: get_commit
run: echo "COMMIT=${GITHUB_SHA::8}" >> $GITHUB_OUTPUT
shell: bash
- name: Build on amd64
if: ${{ matrix.arch == 'amd64' }}
run: |
echo '#!/bin/bash' > build.sh
echo '' >> build.sh
echo 'set -e' >> build.sh
echo 'apt-get update -q -y' >> build.sh
echo 'apt-get install -q -y curl gcc' >> build.sh
if [ ${{ matrix.go }} == 'latest' ]
then
echo 'GO_VERSION=$(curl -L https://go.dev/VERSION?m=text)' >> build.sh
else
echo 'GO_VERSION=${{ matrix.go }}' >> build.sh
fi
echo 'GO_DOWNLOAD_ARCH=${{ matrix.go-arch }}' >> build.sh
echo 'curl --retry 5 --retry-delay 2 --connect-timeout 10 -o go.tar.gz -L https://go.dev/dl/${GO_VERSION}.linux-${GO_DOWNLOAD_ARCH}.tar.gz' >> build.sh
echo 'tar -C /usr/local -xzf go.tar.gz' >> build.sh
echo 'export PATH=$PATH:/usr/local/go/bin' >> build.sh
echo 'go version' >> build.sh
echo 'cd /usr/local/src' >> build.sh
echo 'go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${{ steps.get_commit.outputs.COMMIT }} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo' >> build.sh
chmod 755 build.sh
docker run --rm --name ubuntu-build --mount type=bind,source=`pwd`,target=/usr/local/src ${{ matrix.distro }} /usr/local/src/build.sh
mkdir -p output/{init,bash_completion,zsh_completion}
cp sftpgo.json output/
cp -r templates output/
cp -r static output/
cp -r openapi output/
cp init/sftpgo.service output/init/
./sftpgo gen completion bash > output/bash_completion/sftpgo
./sftpgo gen completion zsh > output/zsh_completion/_sftpgo
./sftpgo gen man -d output/man/man1
gzip output/man/man1/*
cp sftpgo output/
- uses: uraimo/run-on-arch-action@v2
if: ${{ matrix.arch != 'amd64' }}
name: Build for ${{ matrix.arch }}
id: build
with:
arch: ${{ matrix.arch }}
distro: ${{ matrix.distro }}
setup: |
mkdir -p "${PWD}/output"
dockerRunArgs: |
--volume "${PWD}/output:/output"
shell: /bin/bash
install: |
apt-get update -q -y
apt-get install -q -y curl gcc
if [ ${{ matrix.go }} == 'latest' ]
then
GO_VERSION=$(curl -L https://go.dev/VERSION?m=text)
else
GO_VERSION=${{ matrix.go }}
fi
GO_DOWNLOAD_ARCH=${{ matrix.go-arch }}
if [ ${{ matrix.arch}} == 'armv7' ]
then
GO_DOWNLOAD_ARCH=armv6l
fi
curl --retry 5 --retry-delay 2 --connect-timeout 10 -o go.tar.gz -L https://go.dev/dl/${GO_VERSION}.linux-${GO_DOWNLOAD_ARCH}.tar.gz
tar -C /usr/local -xzf go.tar.gz
run: |
export PATH=$PATH:/usr/local/go/bin
go version
if [ ${{ matrix.arch}} == 'armv7' ]
then
export GOARM=7
fi
go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${{ steps.get_commit.outputs.COMMIT }} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
mkdir -p output/{init,bash_completion,zsh_completion}
cp sftpgo.json output/
cp -r templates output/
cp -r static output/
cp -r openapi output/
cp init/sftpgo.service output/init/
./sftpgo gen completion bash > output/bash_completion/sftpgo
./sftpgo gen completion zsh > output/zsh_completion/_sftpgo
./sftpgo gen man -d output/man/man1
gzip output/man/man1/*
cp sftpgo output/
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
name: sftpgo-linux-${{ matrix.arch }}-go-${{ matrix.go }}
path: output
- name: Build Packages
id: build_linux_pkgs
run: |
export NFPM_ARCH=${{ matrix.go-arch }}
cd pkgs
./build.sh
PKG_VERSION=$(cat dist/version)
echo "pkg-version=${PKG_VERSION}" >> $GITHUB_OUTPUT
- name: Upload Debian Package
uses: actions/upload-artifact@v3
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@v3
with:
name: sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-${{ matrix.go-arch }}-rpm
path: pkgs/dist/rpm/*
golangci-lint:
name: golangci-lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.20'
- uses: actions/checkout@v3
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v2
uses: golangci/golangci-lint-action@v3
with:
version: latest

View File

@@ -5,7 +5,7 @@ on:
# - cron: '0 4 * * *' # everyday at 4:00 AM UTC
push:
branches:
- master
- 2.5.x
tags:
- v*
pull_request:
@@ -21,26 +21,25 @@ jobs:
docker_pkg:
- debian
- alpine
optional_deps:
- true
- false
include:
- os: ubuntu-latest
docker_pkg: debian-plugins
optional_deps: true
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Repo metadata
id: repo
uses: actions/github-script@v3
with:
script: |
const repo = await github.repos.get(context.repo)
return repo.data
uses: actions/checkout@v3
- name: Gather image information
id: info
run: |
VERSION=noop
DOCKERFILE_SLIM=Dockerfile
DOCKERFILE=Dockerfile.full
DOCKERFILE=Dockerfile
MINOR=""
MAJOR=""
FEATURES="nopgxregisterdefaulttypes"
if [ "${{ github.event_name }}" = "schedule" ]; then
VERSION=nightly
elif [[ $GITHUB_REF == refs/tags/* ]]; then
@@ -61,14 +60,17 @@ jobs:
if [[ $DOCKER_PKG == alpine ]]; then
VERSION="${VERSION}-alpine"
VERSION_SLIM="${VERSION}-slim"
DOCKERFILE_SLIM=Dockerfile.alpine
DOCKERFILE=Dockerfile.full.alpine
DOCKERFILE=Dockerfile.alpine
elif [[ $DOCKER_PKG == debian-plugins ]]; then
VERSION="${VERSION}-plugins"
VERSION_SLIM="${VERSION}-slim"
FEATURES="${FEATURES},unixcrypt"
elif [[ $DOCKER_PKG == debian ]]; then
FEATURES="${FEATURES},unixcrypt"
fi
DOCKER_IMAGES=("drakkan/sftpgo" "ghcr.io/drakkan/sftpgo")
TAGS="${DOCKER_IMAGES[0]}:${VERSION}"
TAGS_SLIM="${DOCKER_IMAGES[0]}:${VERSION_SLIM}"
BASE_IMAGE="${TAGS_SLIM}"
for DOCKER_IMAGE in ${DOCKER_IMAGES[@]}; do
if [[ ${DOCKER_IMAGE} != ${DOCKER_IMAGES[0]} ]]; then
@@ -83,6 +85,13 @@ jobs:
fi
TAGS="${TAGS},${DOCKER_IMAGE}:latest"
TAGS_SLIM="${TAGS_SLIM},${DOCKER_IMAGE}:slim"
elif [[ $DOCKER_PKG == debian-plugins ]]; then
if [[ -n $MAJOR && -n $MINOR ]]; then
TAGS="${TAGS},${DOCKER_IMAGE}:${MINOR}-plugins,${DOCKER_IMAGE}:${MAJOR}-plugins"
TAGS_SLIM="${TAGS_SLIM},${DOCKER_IMAGE}:${MINOR}-plugins-slim,${DOCKER_IMAGE}:${MAJOR}-plugins-slim"
fi
TAGS="${TAGS},${DOCKER_IMAGE}:plugins"
TAGS_SLIM="${TAGS_SLIM},${DOCKER_IMAGE}:plugins-slim"
else
if [[ -n $MAJOR && -n $MINOR ]]; then
TAGS="${TAGS},${DOCKER_IMAGE}:${MINOR}-alpine,${DOCKER_IMAGE}:${MAJOR}-alpine"
@@ -94,84 +103,71 @@ jobs:
fi
done
echo ::set-output name=dockerfile::${DOCKERFILE}
echo ::set-output name=dockerfile-slim::${DOCKERFILE_SLIM}
echo ::set-output name=version::${VERSION}
echo ::set-output name=version-slim::${VERSION_SLIM}
echo ::set-output name=tags::${TAGS}
echo ::set-output name=tags-slim::${TAGS_SLIM}
echo ::set-output name=base-image::${BASE_IMAGE}
echo ::set-output name=created::$(date -u +'%Y-%m-%dT%H:%M:%SZ')
echo ::set-output name=sha::${GITHUB_SHA::8}
if [[ $OPTIONAL_DEPS == true ]]; then
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
echo "full=true" >> $GITHUB_OUTPUT
else
echo "version=${VERSION_SLIM}" >> $GITHUB_OUTPUT
echo "tags=${TAGS_SLIM}" >> $GITHUB_OUTPUT
echo "full=false" >> $GITHUB_OUTPUT
fi
if [[ $DOCKER_PKG == debian-plugins ]]; then
echo "plugins=true" >> $GITHUB_OUTPUT
else
echo "plugins=false" >> $GITHUB_OUTPUT
fi
echo "dockerfile=${DOCKERFILE}" >> $GITHUB_OUTPUT
echo "features=${FEATURES}" >> $GITHUB_OUTPUT
echo "created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
echo "sha=${GITHUB_SHA::8}" >> $GITHUB_OUTPUT
env:
DOCKER_PKG: ${{ matrix.docker_pkg }}
OPTIONAL_DEPS: ${{ matrix.optional_deps }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
- name: Set up builder slim
uses: docker/setup-buildx-action@v1
id: builder-slim
- name: Set up builder full
uses: docker/setup-buildx-action@v1
id: builder-full
- name: Set up builder
uses: docker/setup-buildx-action@v2
id: builder
- name: Login to Docker Hub
uses: docker/login-action@v1
uses: docker/login-action@v2
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@v1
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.CR_PAT }}
password: ${{ secrets.GITHUB_TOKEN }}
if: ${{ github.event_name != 'pull_request' }}
- name: Build and push slim
uses: docker/build-push-action@v2
- name: Build and push
uses: docker/build-push-action@v4
with:
builder: ${{ steps.builder-slim.outputs.name }}
file: ./${{ steps.info.outputs.dockerfile-slim }}
platforms: linux/amd64,linux/arm64,linux/ppc64le
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.info.outputs.tags-slim }}
build-args: |
COMMIT_SHA=${{ steps.info.outputs.sha }}
labels: |
org.opencontainers.image.title=SFTPGo
org.opencontainers.image.description=Fully featured and highly configurable SFTP server with optional FTP/S and WebDAV support
org.opencontainers.image.url=${{ fromJson(steps.repo.outputs.result).html_url }}
org.opencontainers.image.documentation=${{ fromJson(steps.repo.outputs.result).html_url }}/blob/${{ github.sha }}/docker/README.md
org.opencontainers.image.source=${{ fromJson(steps.repo.outputs.result).html_url }}
org.opencontainers.image.version=${{ steps.info.outputs.version }}
org.opencontainers.image.created=${{ steps.info.outputs.created }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.licenses=${{ fromJson(steps.repo.outputs.result).license.spdx_id }}
- name: Build and push full
if: ${{ github.event_name != 'pull_request' }}
uses: docker/build-push-action@v2
with:
builder: ${{ steps.builder-full.outputs.name }}
context: .
builder: ${{ steps.builder.outputs.name }}
file: ./${{ steps.info.outputs.dockerfile }}
platforms: linux/amd64,linux/arm64,linux/ppc64le
platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/arm/v7
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.info.outputs.tags }}
build-args: |
COMMIT_SHA=${{ steps.info.outputs.sha }}
BASE_IMAGE=${{ steps.info.outputs.base-image }}
INSTALL_OPTIONAL_PACKAGES=${{ steps.info.outputs.full }}
DOWNLOAD_PLUGINS=${{ steps.info.outputs.plugins }}
FEATURES=${{ steps.info.outputs.features }}
labels: |
org.opencontainers.image.title=SFTPGo
org.opencontainers.image.description=Fully featured and highly configurable SFTP server with optional FTP/S and WebDAV support
org.opencontainers.image.url=${{ fromJson(steps.repo.outputs.result).html_url }}
org.opencontainers.image.documentation=${{ fromJson(steps.repo.outputs.result).html_url }}/blob/${{ github.sha }}/docker/README.md
org.opencontainers.image.source=${{ fromJson(steps.repo.outputs.result).html_url }}
org.opencontainers.image.description=Fully featured and highly configurable SFTP server with optional HTTP, FTP/S and WebDAV support
org.opencontainers.image.url=https://github.com/drakkan/sftpgo
org.opencontainers.image.documentation=https://github.com/drakkan/sftpgo/blob/${{ github.sha }}/docker/README.md
org.opencontainers.image.source=https://github.com/drakkan/sftpgo
org.opencontainers.image.version=${{ steps.info.outputs.version }}
org.opencontainers.image.created=${{ steps.info.outputs.created }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.licenses=${{ fromJson(steps.repo.outputs.result).license.spdx_id }}
org.opencontainers.image.licenses=AGPL-3.0-only

View File

@@ -5,155 +5,110 @@ on:
tags: 'v*'
env:
GO_VERSION: 1.15.8
GO_VERSION: 1.20.5
jobs:
create-release:
name: Create
prepare-sources-with-deps:
name: Prepare sources with deps
runs-on: ubuntu-latest
steps:
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: false
prerelease: false
- name: Save release upload URL
run: echo "${{ steps.create_release.outputs.upload_url }}" > ./upload_url.txt
shell: bash
- name: Store release upload URL
uses: actions/upload-artifact@v2
with:
name: upload_url
path: ./upload_url.txt
release-sources-with-deps:
name: Publish sources
needs: create-release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- name: Get SFTPGo version
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
run: echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
- name: Prepare release
run: |
go mod vendor
echo "${SFTPGO_VERSION}" > VERSION.txt
echo "${GITHUB_SHA::8}" >> VERSION.txt
tar cJvf sftpgo_${SFTPGO_VERSION}_src_with_deps.tar.xz *
env:
SFTPGO_VERSION: ${{ steps.get_version.outputs.VERSION }}
- name: Download release upload URL
uses: actions/download-artifact@v2
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
name: upload_url
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
- name: Get release upload URL
id: upload_url
run: |
URL=$(cat upload_url.txt)
echo "::set-output name=url::${URL}"
shell: bash
- name: Upload Release
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.upload_url.outputs.url }}
asset_path: ./sftpgo_${{ steps.get_version.outputs.VERSION }}_src_with_deps.tar.xz
asset_name: sftpgo_${{ steps.get_version.outputs.VERSION }}_src_with_deps.tar.xz
asset_content_type: application/x-xz
publish:
name: Publish binary
needs: create-release
prepare-window-mac:
name: Prepare binaries
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
os: [macos-11, windows-2022]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- name: Build for Linux/macOS
if: startsWith(matrix.os, 'windows-') != true
run: go build -ldflags "-s -w -X github.com/drakkan/sftpgo/version.commit=`git describe --always --dirty` -X github.com/drakkan/sftpgo/version.date=`date -u +%FT%TZ`" -o sftpgo
- name: Build for Windows
if: startsWith(matrix.os, 'windows-')
run: |
$GIT_COMMIT = (git describe --always --dirty) | Out-String
$DATE_TIME = ([datetime]::Now.ToUniversalTime().toString("yyyy-MM-ddTHH:mm:ssZ")) | Out-String
go build -ldflags "-s -w -X github.com/drakkan/sftpgo/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/version.date=$DATE_TIME" -o sftpgo.exe
- name: Initialize data provider
run: ./sftpgo initprovider
shell: bash
- name: Get SFTPGo version
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
run: echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
shell: bash
- name: Get OS name
id: get_os_name
run: |
if [ $MATRIX_OS == 'ubuntu-latest' ]
if [[ $MATRIX_OS =~ ^macos.* ]]
then
echo ::set-output name=OS::linux
elif [ $MATRIX_OS == 'macos-latest' ]
then
echo ::set-output name=OS::macOS
echo "OS=macOS" >> $GITHUB_OUTPUT
else
echo ::set-output name=OS::windows
echo "OS=windows" >> $GITHUB_OUTPUT
fi
shell: bash
env:
MATRIX_OS: ${{ matrix.os }}
- name: Gather cross build info
id: cross_info
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
GIT_COMMIT=$(git describe --always)
BUILD_DATE=$(date -u +%FT%TZ)
echo ::set-output name=sha::${GIT_COMMIT}
echo ::set-output name=created::${BUILD_DATE}
- name: Cross build with xgo
if: ${{ matrix.os == 'ubuntu-latest' }}
uses: crazy-max/ghaction-xgo@v1
with:
dest: cross
prefix: sftpgo
targets: linux/arm64,linux/ppc64le
v: true
x: false
race: false
ldflags: -s -w -X github.com/drakkan/sftpgo/version.commit=${{ steps.cross_info.outputs.sha }} -X github.com/drakkan/sftpgo/version.date=${{ steps.cross_info.outputs.created }}
buildmode: default
- name: Prepare Release for Linux/macOS
- name: Build for macOS x86_64
if: startsWith(matrix.os, 'windows-') != true
run: go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
- name: Build for macOS arm64
if: startsWith(matrix.os, 'macos-') == true
run: CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 SDKROOT=$(xcrun --sdk macosx --show-sdk-path) go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo_arm64
- name: Build for Windows
if: startsWith(matrix.os, 'windows-')
run: |
$GIT_COMMIT = (git describe --always --abbrev=8 --dirty) | Out-String
$DATE_TIME = ([datetime]::Now.ToUniversalTime().toString("yyyy-MM-ddTHH:mm:ssZ")) | Out-String
$FILE_VERSION = $Env:SFTPGO_VERSION.substring(1) + ".0"
go install github.com/tc-hib/go-winres@latest
go-winres simply --arch amd64 --product-version $Env:SFTPGO_VERSION-$GIT_COMMIT --file-version $FILE_VERSION --file-description "SFTPGo server" --product-name SFTPGo --copyright "AGPL-3.0" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o sftpgo.exe
mkdir arm64
$Env:CGO_ENABLED='0'
$Env:GOOS='windows'
$Env:GOARCH='arm64'
go-winres simply --arch arm64 --product-version $Env:SFTPGO_VERSION-$GIT_COMMIT --file-version $FILE_VERSION --file-description "SFTPGo server" --product-name SFTPGo --copyright "AGPL-3.0" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
go build -trimpath -tags nopgxregisterdefaulttypes,nosqlite -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o .\arm64\sftpgo.exe
mkdir x86
$Env:GOARCH='386'
go-winres simply --arch 386 --product-version $Env:SFTPGO_VERSION-$GIT_COMMIT --file-version $FILE_VERSION --file-description "SFTPGo server" --product-name SFTPGo --copyright "AGPL-3.0" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
go build -trimpath -tags nopgxregisterdefaulttypes,nosqlite -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o .\x86\sftpgo.exe
Remove-Item Env:\CGO_ENABLED
Remove-Item Env:\GOOS
Remove-Item Env:\GOARCH
env:
SFTPGO_VERSION: ${{ steps.get_version.outputs.VERSION }}
- name: Initialize data provider
run: ./sftpgo initprovider
shell: bash
- name: Prepare Release for macOS
if: startsWith(matrix.os, 'macos-')
run: |
mkdir -p output/{init,sqlite,bash_completion,zsh_completion}
echo "For documentation please take a look here:" > output/README.txt
@@ -164,73 +119,24 @@ jobs:
cp sftpgo.json output/
cp sftpgo.db output/sqlite/
cp -r static output/
cp -r openapi output/
cp -r templates output/
if [ $OS == 'linux' ]
then
cp init/sftpgo.service output/init/
else
cp init/com.github.drakkan.sftpgo.plist output/init/
fi
cp init/com.github.drakkan.sftpgo.plist output/init/
./sftpgo gen completion bash > output/bash_completion/sftpgo
./sftpgo gen completion zsh > output/zsh_completion/_sftpgo
./sftpgo gen man -d output/man/man1
gzip output/man/man1/*
if [ $OS == 'linux' ]
then
cp -r output output_arm64
cp -r output output_ppc64le
cp -r output output_all
fi
cd output
tar cJvf sftpgo_${SFTPGO_VERSION}_${OS}_x86_64.tar.xz *
tar cJvf ../sftpgo_${SFTPGO_VERSION}_${OS}_x86_64.tar.xz *
cd ..
cp sftpgo_arm64 output/sftpgo
cd output
tar cJvf ../sftpgo_${SFTPGO_VERSION}_${OS}_arm64.tar.xz *
cd ..
if [ $OS == 'linux' ]
then
cp cross/sftpgo-linux-arm64 output_arm64/sftpgo
cd output_arm64
tar cJvf sftpgo_${SFTPGO_VERSION}_${OS}_arm64.tar.xz *
cd ..
cp cross/sftpgo-linux-ppc64le output_ppc64le/sftpgo
cd output_ppc64le
tar cJvf sftpgo_${SFTPGO_VERSION}_${OS}_ppc64le.tar.xz *
cd ..
mkdir output_all/{arm64,ppc64le}
cp cross/sftpgo-linux-arm64 output_all/arm64/sftpgo
cp cross/sftpgo-linux-ppc64le output_all/ppc64le/sftpgo
cd output_all
tar cJvf sftpgo_${SFTPGO_VERSION}_${OS}_bundle.tar.xz *
cd ..
fi
env:
SFTPGO_VERSION: ${{ steps.get_version.outputs.VERSION }}
OS: ${{ steps.get_os_name.outputs.OS }}
- name: Prepare Linux Packages
id: build_linux_pkgs
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
cp -r pkgs pkgs_arm64
cp -r pkgs pkgs_ppc64le
cd pkgs
./build.sh
cd ..
export NFPM_ARCH=arm64
export BIN_SUFFIX=-linux-arm64
cp cross/sftpgo${BIN_SUFFIX} .
cd pkgs_arm64
./build.sh
cd ..
export NFPM_ARCH=ppc64le
export BIN_SUFFIX=-linux-ppc64le
cp cross/sftpgo${BIN_SUFFIX} .
cd pkgs_ppc64le
./build.sh
cd ..
PKG_VERSION=${SFTPGO_VERSION:1}
echo "::set-output name=pkg-version::${PKG_VERSION}"
env:
SFTPGO_VERSION: ${{ steps.get_version.outputs.VERSION }}
- name: Prepare Release for Windows
if: startsWith(matrix.os, 'windows-')
run: |
@@ -243,168 +149,459 @@ jobs:
xcopy .\templates .\output\templates\ /E
mkdir output\static
xcopy .\static .\output\static\ /E
iscc windows-installer\sftpgo.iss
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
copy .\sftpgo.db .\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
Compress-Archive .\win-portable\* sftpgo_portable_x86_64.zip
env:
SFTPGO_VERSION: ${{ steps.get_version.outputs.VERSION }}
OS: ${{ steps.get_os_name.outputs.OS }}
mkdir win-portable\openapi
xcopy .\openapi .\win-portable\openapi\ /E
Compress-Archive .\win-portable\* sftpgo_portable.zip
- name: Download release upload URL
uses: actions/download-artifact@v2
- name: Upload macOS x86_64 artifact
if: startsWith(matrix.os, 'macos-')
uses: actions/upload-artifact@v3
with:
name: upload_url
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_x86_64.tar.xz
path: ./sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_x86_64.tar.xz
retention-days: 1
- name: Get release upload URL
id: upload_url
- name: Upload macOS arm64 artifact
if: startsWith(matrix.os, 'macos-')
uses: actions/upload-artifact@v3
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@v3
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@v3
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@v3
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@v3
with:
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_portable.zip
path: ./sftpgo_portable.zip
retention-days: 1
prepare-linux:
name: Prepare Linux binaries
runs-on: ubuntu-latest
strategy:
matrix:
include:
- arch: amd64
distro: ubuntu:18.04
go-arch: amd64
deb-arch: amd64
rpm-arch: x86_64
tar-arch: x86_64
- arch: aarch64
distro: ubuntu18.04
go-arch: arm64
deb-arch: arm64
rpm-arch: aarch64
tar-arch: arm64
- arch: ppc64le
distro: ubuntu18.04
go-arch: ppc64le
deb-arch: ppc64el
rpm-arch: ppc64le
tar-arch: ppc64le
- arch: armv7
distro: ubuntu18.04
go-arch: arm7
deb-arch: armhf
rpm-arch: armv7hl
tar-arch: armv7
steps:
- uses: actions/checkout@v3
- name: Get versions
id: get_version
run: |
URL=$(cat upload_url.txt)
echo "::set-output name=url::${URL}"
echo "SFTPGO_VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
echo "GO_VERSION=${GO_VERSION}" >> $GITHUB_OUTPUT
echo "COMMIT=${GITHUB_SHA::8}" >> $GITHUB_OUTPUT
shell: bash
- name: Upload Linux/macOS Release
if: startsWith(matrix.os, 'windows-') != true
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.upload_url.outputs.url }}
asset_path: ./output/sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_x86_64.tar.xz
asset_name: sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_x86_64.tar.xz
asset_content_type: application/x-xz
GO_VERSION: ${{ env.GO_VERSION }}
- name: Upload Linux/arm64 Release
if: ${{ matrix.os == 'ubuntu-latest' }}
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.upload_url.outputs.url }}
asset_path: ./output_arm64/sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_arm64.tar.xz
asset_name: sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_arm64.tar.xz
asset_content_type: application/x-xz
- name: Build on amd64
if: ${{ matrix.arch == 'amd64' }}
run: |
echo '#!/bin/bash' > build.sh
echo '' >> build.sh
echo 'set -e' >> build.sh
echo 'apt-get update -q -y' >> build.sh
echo 'apt-get install -q -y curl gcc' >> build.sh
echo 'curl --retry 5 --retry-delay 2 --connect-timeout 10 -o go.tar.gz -L https://go.dev/dl/go${{ steps.get_version.outputs.GO_VERSION }}.linux-${{ matrix.go-arch }}.tar.gz' >> build.sh
echo 'tar -C /usr/local -xzf go.tar.gz' >> build.sh
echo 'export PATH=$PATH:/usr/local/go/bin' >> build.sh
echo 'go version' >> build.sh
echo 'cd /usr/local/src' >> build.sh
echo 'go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${{ steps.get_version.outputs.COMMIT }} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo' >> build.sh
- name: Upload Linux/ppc64le Release
if: ${{ matrix.os == 'ubuntu-latest' }}
uses: actions/upload-release-asset@v1
chmod 755 build.sh
docker run --rm --name ubuntu-build --mount type=bind,source=`pwd`,target=/usr/local/src ${{ matrix.distro }} /usr/local/src/build.sh
mkdir -p output/{init,sqlite,bash_completion,zsh_completion}
echo "For documentation please take a look here:" > output/README.txt
echo "" >> output/README.txt
echo "https://github.com/drakkan/sftpgo/blob/${SFTPGO_VERSION}/README.md" >> output/README.txt
cp LICENSE output/
cp sftpgo.json output/
cp -r templates output/
cp -r static output/
cp -r openapi output/
cp init/sftpgo.service output/init/
./sftpgo initprovider
./sftpgo gen completion bash > output/bash_completion/sftpgo
./sftpgo gen completion zsh > output/zsh_completion/_sftpgo
./sftpgo gen man -d output/man/man1
gzip output/man/man1/*
cp sftpgo output/
cp sftpgo.db output/sqlite/
cd output
tar cJvf sftpgo_${SFTPGO_VERSION}_linux_${{ matrix.tar-arch }}.tar.xz *
cd ..
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.upload_url.outputs.url }}
asset_path: ./output_ppc64le/sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_ppc64le.tar.xz
asset_name: sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_ppc64le.tar.xz
asset_content_type: application/x-xz
SFTPGO_VERSION: ${{ steps.get_version.outputs.SFTPGO_VERSION }}
- name: Upload Linux Bundle Release
if: ${{ matrix.os == 'ubuntu-latest' }}
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: uraimo/run-on-arch-action@v2
if: ${{ matrix.arch != 'amd64' }}
name: Build for ${{ matrix.arch }}
id: build
with:
upload_url: ${{ steps.upload_url.outputs.url }}
asset_path: ./output_all/sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_bundle.tar.xz
asset_name: sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_bundle.tar.xz
asset_content_type: application/x-xz
arch: ${{ matrix.arch }}
distro: ${{ matrix.distro }}
setup: |
mkdir -p "${PWD}/output"
dockerRunArgs: |
--volume "${PWD}/output:/output"
shell: /bin/bash
install: |
apt-get update -q -y
apt-get install -q -y curl gcc xz-utils
GO_DOWNLOAD_ARCH=${{ matrix.go-arch }}
if [ ${{ matrix.arch}} == 'armv7' ]
then
GO_DOWNLOAD_ARCH=armv6l
fi
curl --retry 5 --retry-delay 2 --connect-timeout 10 -o go.tar.gz -L https://go.dev/dl/go${{ steps.get_version.outputs.GO_VERSION }}.linux-${GO_DOWNLOAD_ARCH}.tar.gz
tar -C /usr/local -xzf go.tar.gz
run: |
export PATH=$PATH:/usr/local/go/bin
go version
go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${{ steps.get_version.outputs.COMMIT }} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
mkdir -p output/{init,sqlite,bash_completion,zsh_completion}
echo "For documentation please take a look here:" > output/README.txt
echo "" >> output/README.txt
echo "https://github.com/drakkan/sftpgo/blob/${{ steps.get_version.outputs.SFTPGO_VERSION }}/README.md" >> output/README.txt
cp LICENSE output/
cp sftpgo.json output/
cp -r templates output/
cp -r static output/
cp -r openapi output/
cp init/sftpgo.service output/init/
./sftpgo initprovider
./sftpgo gen completion bash > output/bash_completion/sftpgo
./sftpgo gen completion zsh > output/zsh_completion/_sftpgo
./sftpgo gen man -d output/man/man1
gzip output/man/man1/*
cp sftpgo output/
cp sftpgo.db output/sqlite/
cd output
tar cJvf sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_${{ matrix.tar-arch }}.tar.xz *
cd ..
- name: Upload Windows Release
if: startsWith(matrix.os, 'windows-')
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload build artifact for ${{ matrix.arch }}
uses: actions/upload-artifact@v3
with:
upload_url: ${{ steps.upload_url.outputs.url }}
asset_path: ./sftpgo_windows_x86_64.exe
asset_name: sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_x86_64.exe
asset_content_type: application/x-dosexec
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_${{ matrix.tar-arch }}.tar.xz
path: ./output/sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_${{ matrix.tar-arch }}.tar.xz
retention-days: 1
- name: Upload Portable Windows Release
if: startsWith(matrix.os, 'windows-')
uses: actions/upload-release-asset@v1
- name: Build Packages
id: build_linux_pkgs
run: |
export NFPM_ARCH=${{ matrix.go-arch }}
cd pkgs
./build.sh
PKG_VERSION=${SFTPGO_VERSION:1}
echo "pkg-version=${PKG_VERSION}" >> $GITHUB_OUTPUT
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.upload_url.outputs.url }}
asset_path: ./sftpgo_portable_x86_64.zip
asset_name: sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_portable_x86_64.zip
asset_content_type: application/zip
SFTPGO_VERSION: ${{ steps.get_version.outputs.SFTPGO_VERSION }}
- name: Upload Debian Package
if: ${{ matrix.os == 'ubuntu-latest' }}
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Deb Package
uses: actions/upload-artifact@v3
with:
upload_url: ${{ steps.upload_url.outputs.url }}
asset_path: ./pkgs/dist/deb/sftpgo_${{ steps.build_linux_pkgs.outputs.pkg-version }}-1_amd64.deb
asset_name: sftpgo_${{ steps.build_linux_pkgs.outputs.pkg-version }}-1_amd64.deb
asset_content_type: application/vnd.debian.binary-package
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
if: ${{ matrix.os == 'ubuntu-latest' }}
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/upload-artifact@v3
with:
upload_url: ${{ steps.upload_url.outputs.url }}
asset_path: ./pkgs/dist/rpm/sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-1.x86_64.rpm
asset_name: sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-1.x86_64.rpm
asset_content_type: application/x-rpm
name: sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-1.${{ matrix.rpm-arch}}.rpm
path: ./pkgs/dist/rpm/sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-1.${{ matrix.rpm-arch}}.rpm
retention-days: 1
- name: Upload Debian Package arm64
if: ${{ matrix.os == 'ubuntu-latest' }}
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.upload_url.outputs.url }}
asset_path: ./pkgs_arm64/dist/deb/sftpgo_${{ steps.build_linux_pkgs.outputs.pkg-version }}-1_arm64.deb
asset_name: sftpgo_${{ steps.build_linux_pkgs.outputs.pkg-version }}-1_arm64.deb
asset_content_type: application/vnd.debian.binary-package
prepare-linux-bundle:
name: Prepare Linux bundle
needs: prepare-linux
runs-on: ubuntu-latest
- name: Upload RPM Package arm64
if: ${{ matrix.os == 'ubuntu-latest' }}
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.upload_url.outputs.url }}
asset_path: ./pkgs_arm64/dist/rpm/sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-1.aarch64.rpm
asset_name: sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-1.aarch64.rpm
asset_content_type: application/x-rpm
steps:
- name: Get versions
id: get_version
run: |
echo "SFTPGO_VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
shell: bash
- name: Upload Debian Package ppc64le
if: ${{ matrix.os == 'ubuntu-latest' }}
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download amd64 artifact
uses: actions/download-artifact@v3
with:
upload_url: ${{ steps.upload_url.outputs.url }}
asset_path: ./pkgs_ppc64le/dist/deb/sftpgo_${{ steps.build_linux_pkgs.outputs.pkg-version }}-1_ppc64el.deb
asset_name: sftpgo_${{ steps.build_linux_pkgs.outputs.pkg-version }}-1_ppc64el.deb
asset_content_type: application/vnd.debian.binary-package
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_x86_64.tar.xz
- name: Upload RPM Package ppc64le
if: ${{ matrix.os == 'ubuntu-latest' }}
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download arm64 artifact
uses: actions/download-artifact@v3
with:
upload_url: ${{ steps.upload_url.outputs.url }}
asset_path: ./pkgs_ppc64le/dist/rpm/sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-1.ppc64le.rpm
asset_name: sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-1.ppc64le.rpm
asset_content_type: application/x-rpm
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_arm64.tar.xz
- name: Download ppc64le artifact
uses: actions/download-artifact@v3
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_ppc64le.tar.xz
- name: Download armv7 artifact
uses: actions/download-artifact@v3
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_armv7.tar.xz
- name: Build bundle
shell: bash
run: |
mkdir -p bundle/{arm64,ppc64le,armv7}
cd bundle
tar xvf ../sftpgo_${SFTPGO_VERSION}_linux_x86_64.tar.xz
cd arm64
tar xvf ../../sftpgo_${SFTPGO_VERSION}_linux_arm64.tar.xz sftpgo
cd ../ppc64le
tar xvf ../../sftpgo_${SFTPGO_VERSION}_linux_ppc64le.tar.xz sftpgo
cd ../armv7
tar xvf ../../sftpgo_${SFTPGO_VERSION}_linux_armv7.tar.xz sftpgo
cd ..
tar cJvf sftpgo_${SFTPGO_VERSION}_linux_bundle.tar.xz *
cd ..
env:
SFTPGO_VERSION: ${{ steps.get_version.outputs.SFTPGO_VERSION }}
- name: Upload Linux bundle
uses: actions/upload-artifact@v3
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_bundle.tar.xz
path: ./bundle/sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_bundle.tar.xz
retention-days: 1
create-release:
name: Release
needs: [prepare-linux-bundle, prepare-sources-with-deps, prepare-window-mac]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Get versions
id: get_version
run: |
SFTPGO_VERSION=${GITHUB_REF/refs\/tags\//}
PKG_VERSION=${SFTPGO_VERSION:1}
echo "SFTPGO_VERSION=${SFTPGO_VERSION}" >> $GITHUB_OUTPUT
echo "PKG_VERSION=${PKG_VERSION}" >> $GITHUB_OUTPUT
shell: bash
- name: Download amd64 artifact
uses: actions/download-artifact@v3
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_x86_64.tar.xz
- name: Download arm64 artifact
uses: actions/download-artifact@v3
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_arm64.tar.xz
- name: Download ppc64le artifact
uses: actions/download-artifact@v3
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_ppc64le.tar.xz
- name: Download armv7 artifact
uses: actions/download-artifact@v3
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_armv7.tar.xz
- name: Download Linux bundle artifact
uses: actions/download-artifact@v3
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_bundle.tar.xz
- name: Download Deb amd64 artifact
uses: actions/download-artifact@v3
with:
name: sftpgo_${{ steps.get_version.outputs.PKG_VERSION }}-1_amd64.deb
- name: Download Deb arm64 artifact
uses: actions/download-artifact@v3
with:
name: sftpgo_${{ steps.get_version.outputs.PKG_VERSION }}-1_arm64.deb
- name: Download Deb ppc64le artifact
uses: actions/download-artifact@v3
with:
name: sftpgo_${{ steps.get_version.outputs.PKG_VERSION }}-1_ppc64el.deb
- name: Download Deb armv7 artifact
uses: actions/download-artifact@v3
with:
name: sftpgo_${{ steps.get_version.outputs.PKG_VERSION }}-1_armhf.deb
- name: Download RPM x86_64 artifact
uses: actions/download-artifact@v3
with:
name: sftpgo-${{ steps.get_version.outputs.PKG_VERSION }}-1.x86_64.rpm
- name: Download RPM aarch64 artifact
uses: actions/download-artifact@v3
with:
name: sftpgo-${{ steps.get_version.outputs.PKG_VERSION }}-1.aarch64.rpm
- name: Download RPM ppc64le artifact
uses: actions/download-artifact@v3
with:
name: sftpgo-${{ steps.get_version.outputs.PKG_VERSION }}-1.ppc64le.rpm
- name: Download RPM armv7 artifact
uses: actions/download-artifact@v3
with:
name: sftpgo-${{ steps.get_version.outputs.PKG_VERSION }}-1.armv7hl.rpm
- name: Download macOS x86_64 artifact
uses: actions/download-artifact@v3
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_macOS_x86_64.tar.xz
- name: Download macOS arm64 artifact
uses: actions/download-artifact@v3
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_macOS_arm64.tar.xz
- name: Download Windows installer x86_64 artifact
uses: actions/download-artifact@v3
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_windows_x86_64.exe
- name: Download Windows installer arm64 artifact
uses: actions/download-artifact@v3
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_windows_arm64.exe
- name: Download Windows installer x86 artifact
uses: actions/download-artifact@v3
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_windows_x86.exe
- name: Download Windows portable artifact
uses: actions/download-artifact@v3
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_windows_portable.zip
- name: Download source with deps artifact
uses: actions/download-artifact@v3
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_src_with_deps.tar.xz
- name: Create release
run: |
mv sftpgo_windows_x86_64.exe sftpgo_${SFTPGO_VERSION}_windows_x86_64.exe
mv sftpgo_windows_arm64.exe sftpgo_${SFTPGO_VERSION}_windows_arm64.exe
mv sftpgo_windows_x86.exe sftpgo_${SFTPGO_VERSION}_windows_x86.exe
mv sftpgo_portable.zip sftpgo_${SFTPGO_VERSION}_windows_portable.zip
gh release create "${SFTPGO_VERSION}" -t "${SFTPGO_VERSION}"
gh release upload "${SFTPGO_VERSION}" sftpgo_*.xz --clobber
gh release upload "${SFTPGO_VERSION}" sftpgo-*.rpm --clobber
gh release upload "${SFTPGO_VERSION}" sftpgo_*.deb --clobber
gh release upload "${SFTPGO_VERSION}" sftpgo_*.exe --clobber
gh release upload "${SFTPGO_VERSION}" sftpgo_*.zip --clobber
gh release view "${SFTPGO_VERSION}"
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
SFTPGO_VERSION: ${{ steps.get_version.outputs.SFTPGO_VERSION }}

View File

@@ -1,5 +1,5 @@
run:
timeout: 5m
timeout: 10m
issues-exit-code: 1
tests: true
@@ -19,8 +19,19 @@ linters-settings:
simplify: true
goimports:
local-prefixes: github.com/drakkan/sftpgo
maligned:
suggest-new: true
#govet:
# report about shadowed variables
#check-shadowing: true
#enable:
# - fieldalignment
issues:
include:
- EXC0002
- EXC0012
- EXC0013
- EXC0014
- EXC0015
linters:
enable:
@@ -28,15 +39,14 @@ linters:
- errcheck
- gofmt
- goimports
- golint
- revive
- unconvert
- unparam
- bodyclose
- gocyclo
- misspell
- maligned
- whitespace
- dupl
- scopelint
- rowserrcheck
- dogsled
- dogsled
- govet

1
CODEOWNERS Normal file
View File

@@ -0,0 +1 @@
* @drakkan

128
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
support@sftpgo.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

34
DCO Normal file
View File

@@ -0,0 +1,34 @@
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.

View File

@@ -1,4 +1,4 @@
FROM golang:1.15 as builder
FROM golang:1.20-bullseye as builder
ENV GOFLAGS="-mod=readonly"
@@ -20,14 +20,26 @@ ARG FEATURES
COPY . .
RUN set -xe && \
export COMMIT_SHA=${COMMIT_SHA:-$(git describe --always --dirty)} && \
go build $(if [ -n "${FEATURES}" ]; then echo "-tags ${FEATURES}"; fi) -ldflags "-s -w -X github.com/drakkan/sftpgo/version.commit=${COMMIT_SHA} -X github.com/drakkan/sftpgo/version.date=`date -u +%FT%TZ`" -v -o sftpgo
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 debian:buster-slim
# Set to "true" to download the "official" plugins in /usr/local/bin
ARG DOWNLOAD_PLUGINS=false
RUN apt-get update && apt-get install --no-install-recommends -y ca-certificates mime-support && rm -rf /var/lib/apt/lists/*
RUN if [ "${DOWNLOAD_PLUGINS}" = "true" ]; then apt-get update && apt-get install --no-install-recommends -y curl && ./docker/scripts/download-plugins.sh; fi
RUN mkdir -p /etc/sftpgo /var/lib/sftpgo /usr/share/sftpgo /srv/sftpgo
RUN apt-get update && apt-get install --no-install-recommends -y openssh-server && rm -rf /var/lib/apt/lists/*
FROM debian:bullseye-slim
# Set to "true" to install jq and the optional git and rsync dependencies
ARG INSTALL_OPTIONAL_PACKAGES=false
RUN apt-get update && apt-get install --no-install-recommends -y ca-certificates media-types && rm -rf /var/lib/apt/lists/*
RUN if [ "${INSTALL_OPTIONAL_PACKAGES}" = "true" ]; then apt-get update && apt-get install --no-install-recommends -y jq git rsync && rm -rf /var/lib/apt/lists/*; fi
RUN mkdir -p /etc/sftpgo /var/lib/sftpgo /usr/share/sftpgo /srv/sftpgo/data /srv/sftpgo/backups
RUN groupadd --system -g 1000 sftpgo && \
useradd --system --gid sftpgo --no-create-home \
@@ -35,24 +47,20 @@ RUN groupadd --system -g 1000 sftpgo && \
--comment "SFTPGo user" --uid 1000 sftpgo
COPY --from=builder /workspace/sftpgo.json /etc/sftpgo/sftpgo.json
COPY --from=builder /etc/ssh/moduli /etc/sftpgo/moduli
COPY --from=builder /workspace/templates /usr/share/sftpgo/templates
COPY --from=builder /workspace/static /usr/share/sftpgo/static
COPY --from=builder /workspace/sftpgo /usr/local/bin/
COPY --from=builder /workspace/openapi /usr/share/sftpgo/openapi
COPY --from=builder /workspace/sftpgo /usr/local/bin/sftpgo-plugin-* /usr/local/bin/
# Log to the stdout so the logs will be available using docker logs
ENV SFTPGO_LOG_FILE_PATH=""
# templates and static paths are inside the container
ENV SFTPGO_HTTPD__TEMPLATES_PATH=/usr/share/sftpgo/templates
ENV SFTPGO_HTTPD__STATIC_FILES_PATH=/usr/share/sftpgo/static
# Modify the default configuration file
RUN sed -i "s|\"users_base_dir\": \"\",|\"users_base_dir\": \"/srv/sftpgo/data\",|" /etc/sftpgo/sftpgo.json && \
sed -i "s|\"backups\"|\"/srv/sftpgo/backups\"|" /etc/sftpgo/sftpgo.json && \
sed -i "s|\"bind_address\": \"127.0.0.1\",|\"bind_address\": \"\",|" /etc/sftpgo/sftpgo.json
RUN sed -i 's|"users_base_dir": "",|"users_base_dir": "/srv/sftpgo/data",|' /etc/sftpgo/sftpgo.json && \
sed -i 's|"backups"|"/srv/sftpgo/backups"|' /etc/sftpgo/sftpgo.json
COPY ./docker/scripts/entrypoint.sh /docker-entrypoint.sh
RUN chown -R sftpgo:sftpgo /etc/sftpgo && chown sftpgo:sftpgo /var/lib/sftpgo /srv/sftpgo
RUN chown -R sftpgo:sftpgo /etc/sftpgo /srv/sftpgo && chown sftpgo:sftpgo /var/lib/sftpgo && chmod 700 /srv/sftpgo/backups
WORKDIR /var/lib/sftpgo
USER 1000:1000

View File

@@ -1,4 +1,4 @@
FROM golang:1.15-alpine AS builder
FROM golang:1.20-alpine3.18 AS builder
ENV GOFLAGS="-mod=readonly"
@@ -22,40 +22,40 @@ ARG FEATURES
COPY . .
RUN set -xe && \
export COMMIT_SHA=${COMMIT_SHA:-$(git describe --always --dirty)} && \
go build $(if [ -n "${FEATURES}" ]; then echo "-tags ${FEATURES}"; fi) -ldflags "-s -w -X github.com/drakkan/sftpgo/version.commit=${COMMIT_SHA} -X github.com/drakkan/sftpgo/version.date=`date -u +%FT%TZ`" -v -o sftpgo
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
RUN apk add --update --no-cache openssh-client-common
FROM alpine:3.12
FROM alpine:3.18
# Set to "true" to install jq and the optional git and rsync dependencies
ARG INSTALL_OPTIONAL_PACKAGES=false
RUN apk add --update --no-cache ca-certificates tzdata mailcap
# set up nsswitch.conf for Go's "netgo" implementation
# https://github.com/gliderlabs/docker-alpine/issues/367#issuecomment-424546457
RUN test ! -e /etc/nsswitch.conf && echo 'hosts: files dns' > /etc/nsswitch.conf
RUN if [ "${INSTALL_OPTIONAL_PACKAGES}" = "true" ]; then apk add --update --no-cache jq git rsync; fi
RUN mkdir -p /etc/sftpgo /var/lib/sftpgo /usr/share/sftpgo /srv/sftpgo
RUN mkdir -p /etc/sftpgo /var/lib/sftpgo /usr/share/sftpgo /srv/sftpgo/data /srv/sftpgo/backups
RUN addgroup -g 1000 -S sftpgo && \
adduser -u 1000 -h /var/lib/sftpgo -s /sbin/nologin -G sftpgo -S -D -H -g "SFTPGo user" sftpgo
COPY --from=builder /workspace/sftpgo.json /etc/sftpgo/sftpgo.json
COPY --from=builder /etc/ssh/moduli /etc/sftpgo/moduli
COPY --from=builder /workspace/templates /usr/share/sftpgo/templates
COPY --from=builder /workspace/static /usr/share/sftpgo/static
COPY --from=builder /workspace/openapi /usr/share/sftpgo/openapi
COPY --from=builder /workspace/sftpgo /usr/local/bin/
# Log to the stdout so the logs will be available using docker logs
ENV SFTPGO_LOG_FILE_PATH=""
# templates and static paths are inside the container
ENV SFTPGO_HTTPD__TEMPLATES_PATH=/usr/share/sftpgo/templates
ENV SFTPGO_HTTPD__STATIC_FILES_PATH=/usr/share/sftpgo/static
# Modify the default configuration file
RUN sed -i "s|\"users_base_dir\": \"\",|\"users_base_dir\": \"/srv/sftpgo/data\",|" /etc/sftpgo/sftpgo.json && \
sed -i "s|\"backups\"|\"/srv/sftpgo/backups\"|" /etc/sftpgo/sftpgo.json && \
sed -i "s|\"bind_address\": \"127.0.0.1\",|\"bind_address\": \"\",|" /etc/sftpgo/sftpgo.json
RUN sed -i 's|"users_base_dir": "",|"users_base_dir": "/srv/sftpgo/data",|' /etc/sftpgo/sftpgo.json && \
sed -i 's|"backups"|"/srv/sftpgo/backups"|' /etc/sftpgo/sftpgo.json
RUN chown -R sftpgo:sftpgo /etc/sftpgo && chown sftpgo:sftpgo /var/lib/sftpgo /srv/sftpgo
RUN chown -R sftpgo:sftpgo /etc/sftpgo /srv/sftpgo && chown sftpgo:sftpgo /var/lib/sftpgo && chmod 700 /srv/sftpgo/backups
WORKDIR /var/lib/sftpgo
USER 1000:1000

View File

@@ -1,10 +0,0 @@
ARG BASE_IMAGE
FROM ${BASE_IMAGE}
USER root
# Install some optional packages used by SFTPGo features
RUN apt-get update && apt-get install --no-install-recommends -y git rsync && rm -rf /var/lib/apt/lists/*
USER 1000:1000

View File

@@ -1,10 +0,0 @@
ARG BASE_IMAGE
FROM ${BASE_IMAGE}
USER root
# Install some optional packages used by SFTPGo features
RUN apk add --update --no-cache rsync git
USER 1000:1000

145
LICENSE
View File

@@ -1,5 +1,5 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
@@ -7,17 +7,15 @@
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
@@ -72,7 +60,7 @@ modification follow.
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
@@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
GNU Affero General Public License for more details.
You should have received a copy of the GNU General Public License
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

271
README.md
View File

@@ -1,67 +1,119 @@
# SFTPGo
![CI Status](https://github.com/drakkan/sftpgo/workflows/CI/badge.svg?branch=master&event=push)
[![Code Coverage](https://codecov.io/gh/drakkan/sftpgo/branch/master/graph/badge.svg)](https://codecov.io/gh/drakkan/sftpgo/branch/master)
[![Go Report Card](https://goreportcard.com/badge/github.com/drakkan/sftpgo)](https://goreportcard.com/report/github.com/drakkan/sftpgo)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![CI Status](https://github.com/drakkan/sftpgo/workflows/CI/badge.svg?branch=main&event=push)](https://github.com/drakkan/sftpgo/workflows/CI/badge.svg?branch=main&event=push)
[![Code Coverage](https://codecov.io/gh/drakkan/sftpgo/branch/main/graph/badge.svg)](https://codecov.io/gh/drakkan/sftpgo/branch/main)
[![License: AGPL-3.0-only](https://img.shields.io/badge/License-AGPLv3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
[![Docker Pulls](https://img.shields.io/docker/pulls/drakkan/sftpgo)](https://hub.docker.com/r/drakkan/sftpgo)
[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)
Fully featured and highly configurable SFTP server with optional FTP/S and WebDAV support, written in Go.
[English](./README.md) | [简体中文](./README.zh_CN.md)
Fully featured and highly configurable SFTP server with optional HTTP/S, FTP/S and WebDAV support.
Several storage backends are supported: local filesystem, encrypted local filesystem, S3 (compatible) Object Storage, Google Cloud Storage, Azure Blob Storage, SFTP.
## Sponsors
If you find SFTPGo useful please consider supporting this Open Source project.
Maintaining and evolving SFTPGo is a lot of work - easily the equivalent of a full time job - for me.
I'd like to make SFTPGo into a sustainable long term project and would not like to introduce a dual licensing option and limit some features to the proprietary version only.
If you use SFTPGo, it is in your best interest to ensure that the project you rely on stays healthy and well maintained.
This can only happen with your donations and [sponsorships](https://github.com/sponsors/drakkan) :heart:
You can also purchase support plans from the [SFTPGo website](https://sftpgo.com/#pricing).
With sponsorships/donations or support plans we establish a channel for reciprocal access, ensuring better outcomes for both you and the project.
### Thank you to our sponsors
#### Platinum sponsors
[<img src="./img/Aledade_logo.png" alt="Aledade logo" width="202" height="70">](https://www.aledade.com/)
#### Silver sponsors
[<img src="./img/Dendi_logo.png" alt="Dendi logo" width="212" height="66">](https://dendisoftware.com/)
#### Bronze sponsors
[<img src="https://www.7digital.com/wp-content/themes/sevendigital/images/top_logo.png" alt="7digital logo">](https://www.7digital.com/)
## Support policy
SFTPGo is an Open Source project and you can of course use it for free but please don't ask for free support as well.
We will check the reported issues to see if you are experiencing a bug and if so, it may or may not be fixed, we only provide support to project [sponsors/donors](#sponsors).
If you report an invalid issue or ask for step-by-step support, your issue will remain open with no answer or will be closed as invalid without further explanation. Thanks for understanding.
## Features
- SFTPGo uses virtual accounts stored inside a "data provider".
- SQLite, MySQL, PostgreSQL, bbolt (key/value store in pure Go) and in-memory data providers are supported.
- Each local account is chrooted in its home directory, for cloud-based accounts you can restrict access to a certain base path.
- Public key and password authentication. Multiple public keys per user are supported.
- Support for serving local filesystem, encrypted local filesystem, S3 Compatible Object Storage, Google Cloud Storage, Azure Blob Storage or other SFTP accounts over SFTP/SCP/FTP/WebDAV.
- Virtual folders are supported: a virtual folder can use any of the supported storage backends. So you can have, for example, a user with the S3 backend mapping a GCS bucket (or part of it) on a specified path and an encrypted local filesystem on another one. Virtual folders can be private or shared among multiple users, for shared virtual folders you can define different quota limits for each user.
- Configurable [custom commands and/or HTTP hooks](./docs/custom-actions.md) on upload, pre-upload, download, pre-download, delete, pre-delete, rename, mkdir, rmdir on SSH commands and on user add, update and delete.
- Virtual accounts stored within a "data provider".
- SQLite, MySQL, PostgreSQL, CockroachDB, Bolt (key/value store in pure Go) and in-memory data providers are supported.
- Chroot isolation for local accounts. Cloud-based accounts can be restricted to a certain base path.
- Per-user and per-directory virtual permissions, for each path you can allow or deny: directory listing, upload, overwrite, download, delete, rename, create directories, create symlinks, change owner/group/file mode and modification time.
- [REST API](./docs/rest-api.md) for users and folders management, data retention, backup, restore and real time reports of the active connections with possibility of forcibly closing a connection.
- The [Event Manager](./docs/eventmanager.md) allows to define custom workflows based on server events or schedules.
- [Web based administration interface](./docs/web-admin.md) to easily manage users, folders and connections.
- [Web client interface](./docs/web-client.md) so that end users can change their credentials, manage and share their files in the browser.
- Public key and password authentication. Multiple public keys per-user are supported.
- SSH user [certificate authentication](https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?rev=1.8).
- Keyboard interactive authentication. You can easily setup a customizable multi-factor authentication.
- Partial authentication. You can configure multi-step authentication requiring, for example, the user password after successful public key authentication.
- Per user authentication methods. You can configure the allowed authentication methods for each user.
- Custom authentication via external programs/HTTP API is supported.
- [Data At Rest Encryption](./docs/dare.md) is supported.
- Dynamic user modification before login via external programs/HTTP API is supported.
- Quota support: accounts can have individual quota expressed as max total size and/or max number of files.
- Bandwidth throttling is supported, with distinct settings for upload and download.
- Per user maximum concurrent sessions.
- Per user and per directory permission management: list directory contents, upload, overwrite, download, delete, rename, create directories, create symlinks, change owner/group and mode, change access and modification times.
- Per user files/folders ownership mapping: you can map all the users to the system account that runs SFTPGo (all platforms are supported) or you can run SFTPGo as root user and map each user or group of users to a different system account (\*NIX only).
- Per user IP filters are supported: login can be restricted to specific ranges of IP addresses or to a specific IP address.
- Per user and per directory shell like patterns filters are supported: files can be allowed or denied based on shell like patterns.
- Virtual folders are supported: directories outside the user home directory can be exposed as virtual folders.
- Configurable custom commands and/or HTTP notifications on file upload, download, pre-delete, delete, rename, on SSH commands and on user add, update and delete.
- Per-user authentication methods.
- [Two-factor authentication](./docs/howto/two-factor-authentication.md) based on time-based one time passwords (RFC 6238) which works with Authy, Google Authenticator, Microsoft Authenticator and other compatible apps.
- Simplified user administrations using [groups](./docs/groups.md).
- [Roles](./docs/roles.md) allow to create limited administrators who can only create and manage users with their role.
- Custom authentication via [external programs/HTTP API](./docs/external-auth.md).
- Web Client and Web Admin user interfaces support [OpenID Connect](https://openid.net/connect/) authentication and so they can be integrated with identity providers such as [Keycloak](https://www.keycloak.org/). You can find more details [here](./docs/oidc.md).
- [Data At Rest Encryption](./docs/dare.md).
- Dynamic user modification before login via [external programs/HTTP API](./docs/dynamic-user-mod.md).
- Quota support: accounts can have individual disk quota expressed as max total size and/or max number of files.
- Bandwidth throttling, with separate settings for upload and download and overrides based on the client's IP address.
- Data transfer bandwidth limits, with total limit or separate settings for uploads and downloads and overrides based on the client's IP address. Limits can be reset using the REST API.
- Per-protocol [rate limiting](./docs/rate-limiting.md) is supported and can be optionally connected to the built-in defender to automatically block hosts that repeatedly exceed the configured limit.
- Per-user maximum concurrent sessions.
- Per-user and global IP filters: login can be restricted to specific ranges of IP addresses or to a specific IP address.
- Per-user and per-directory shell like patterns filters: files can be allowed, denied and optionally hidden based on shell like patterns.
- Automatically terminating idle connections.
- Automatic blocklist management is supported using the built-in [defender](./docs/defender.md).
- Automatic blocklist management using the built-in [defender](./docs/defender.md).
- Geo-IP filtering using a [plugin](https://github.com/sftpgo/sftpgo-plugin-geoipfilter).
- Atomic uploads are configurable.
- Per-user files/folders ownership mapping: you can map all the users to the system account that runs SFTPGo (all platforms are supported) or you can run SFTPGo as root user and map each user or group of users to a different system account (\*NIX only).
- Support for Git repositories over SSH.
- SCP and rsync are supported.
- FTP/S is supported. You can configure the FTP service to require TLS for both control and data connections.
- [WebDAV](./docs/webdav.md) is supported.
- ACME protocol is supported. SFTPGo can obtain and automatically renew TLS certificates for HTTPS, WebDAV and FTPS from `Let's Encrypt` or other ACME compliant certificate authorities, using the `HTTP-01` or `TLS-ALPN-01` [challenge types](https://letsencrypt.org/docs/challenge-types/).
- Two-Way TLS authentication, aka TLS with client certificate authentication, is supported for REST API/Web Admin, FTPS and WebDAV over HTTPS.
- Support for serving local filesystem, encrypted local filesystem, S3 Compatible Object Storage, Google Cloud Storage, Azure Blob Storage or other SFTP accounts over SFTP/SCP/FTP/WebDAV.
- Per user protocols restrictions. You can configure the allowed protocols (SSH/FTP/WebDAV) for each user.
- [Prometheus metrics](./docs/metrics.md) are exposed.
- Support for HAProxy PROXY protocol: you can proxy and/or load balance the SFTP/SCP/FTP/WebDAV service without losing the information about the client's address.
- [REST API](./docs/rest-api.md) for users and folders management, backup, restore and real time reports of the active connections with possibility of forcibly closing a connection.
- [Web based administration interface](./docs/web-admin.md) to easily manage users, folders and connections.
- Per-user protocols restrictions. You can configure the allowed protocols (SSH/HTTP/FTP/WebDAV) for each user.
- [Prometheus metrics](./docs/metrics.md) are supported.
- Support for HAProxy PROXY protocol: you can proxy and/or load balance the SFTP/SCP/FTP service without losing the information about the client's address.
- Easy [migration](./examples/convertusers) from Linux system user accounts.
- [Portable mode](./docs/portable-mode.md): a convenient way to share a single directory on demand.
- [SFTP subsystem mode](./docs/sftp-subsystem.md): you can use SFTPGo as OpenSSH's SFTP subsystem.
- Performance analysis using built-in [profiler](./docs/profiling.md).
- Configuration format is at your choice: JSON, TOML, YAML, HCL, envfile are supported.
- Log files are accurate and they are saved in the easily parsable JSON format ([more information](./docs/logs.md)).
- SFTPGo supports a [plugin system](./docs/plugins.md) and therefore can be extended using external plugins.
- Infrastructure as Code (IaC) support using the [Terraform provider](https://registry.terraform.io/providers/drakkan/sftpgo/latest).
## Platforms
SFTPGo is developed and tested on Linux. After each commit, the code is automatically built and tested on Linux, macOS and Windows using a [GitHub Action](./.github/workflows/development.yml). The test cases are regularly manually executed and passed on FreeBSD. Other *BSD variants should work too.
SFTPGo is developed and tested on Linux. After each commit, the code is automatically built and tested on Linux, macOS, Windows and FreeBSD. Other *BSD variants should work too.
## Requirements
- Go 1.15 or higher as build only dependency.
- A suitable SQL server to use as data provider: PostgreSQL 9.4+ or MySQL 5.6+ or SQLite 3.x.
- The SQL server is optional: you can choose to use an embedded bolt database as key/value store or an in memory data provider.
- Go as build only dependency. We support the Go version(s) used in [continuous integration workflows](./.github/workflows).
- A suitable SQL server to use as data provider:
- upstream supported versions of PostgreSQL, MySQL and MariaDB.
- CockroachDB stable.
- The SQL server is optional: you can choose to use an embedded SQLite, bolt or in memory data provider.
## Installation
@@ -69,24 +121,54 @@ Binary releases for Linux, macOS, and Windows are available. Please visit the [r
An official Docker image is available. Documentation is [here](./docker/README.md).
Some Linux distro packages are available:
<details>
<summary>Some Linux distro packages are available</summary>
- For Arch Linux via AUR:
- [sftpgo](https://aur.archlinux.org/packages/sftpgo/). This package follows stable releases. It requires `git`, `gcc` and `go` to build.
- [sftpgo-bin](https://aur.archlinux.org/packages/sftpgo-bin/). This package follows stable releases downloading the prebuilt linux binary from GitHub. It does not require `git`, `gcc` and `go` to build.
- [sftpgo-git](https://aur.archlinux.org/packages/sftpgo-git/). This package builds and installs the latest git master. It requires `git`, `gcc` and `go` to build.
- [sftpgo-git](https://aur.archlinux.org/packages/sftpgo-git/). This package builds and installs the latest git `main` branch. It requires `git`, `gcc` and `go` to build.
- Deb and RPM packages are built after each commit and for each release.
- For Ubuntu a PPA is available [here](https://launchpad.net/~sftpgo/+archive/ubuntu/sftpgo).
- Void Linux provides an [official package](https://github.com/void-linux/void-packages/tree/master/srcpkgs/sftpgo).
</details>
APT and YUM repositories are [available](./docs/repo.md).
SFTPGo is also available on some marketplaces:
- [AWS Marketplace](https://aws.amazon.com/marketplace/seller-profile?id=6e849ab8-70a6-47de-9a43-13c3fa849335)
- Azure Marketplace: [SFTPGo for Linux](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/eliamarzia1667381463185.sftpgo_linux), [SFTPGo for Windows](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/eliamarzia1667381463185.sftpgo_windows)
- [Elest.io](https://elest.io/open-source/sftpgo)
Purchasing from there will help keep SFTPGo a long-term sustainable project.
<details><summary>Windows packages</summary>
- The Windows installer to install and run SFTPGo as a Windows service.
- The portable package to start SFTPGo on demand.
- The [winget](https://docs.microsoft.com/en-us/windows/package-manager/winget/install) package to install and run SFTPGo as a Windows service: `winget install SFTPGo`.
- The [Chocolatey package](https://community.chocolatey.org/packages/sftpgo) to install and run SFTPGo as a Windows service.
</details>
On macOS you can install from the Homebrew [Formula](https://formulae.brew.sh/formula/sftpgo).
On FreeBSD you can install from the [SFTPGo port](https://www.freshports.org/ftp/sftpgo).
On DragonFlyBSD you can install SFTPGo from [DPorts](https://github.com/DragonFlyBSD/DPorts/tree/master/ftp/sftpgo).
You can easily test new features selecting a commit from the [Actions](https://github.com/drakkan/sftpgo/actions) page and downloading the matching build artifacts for Linux, macOS or Windows. GitHub stores artifacts for 90 days.
Alternately, you can [build from source](./docs/build-from-source.md).
[Getting Started Guide for the Impatient](./docs/howto/getting-started.md).
## Configuration
A full explanation of all configuration methods can be found [here](./docs/full-configuration.md).
Please make sure to [initialize the data provider](#data-provider-initialization-and-management) before running the daemon!
Please make sure to [initialize the data provider](#data-provider-initialization-and-management) before running the daemon.
To start SFTPGo with the default settings, simply run:
@@ -100,7 +182,7 @@ Check out [this documentation](./docs/service.md) if you want to run SFTPGo as a
Before starting the SFTPGo server please ensure that the configured data provider is properly initialized/updated.
For PostgreSQL and MySQL providers, you need to create the configured database. For SQLite, the configured database will be automatically created at startup. Memory and bolt data providers do not require an initialization but they could require an update to the existing data after upgrading SFTPGo.
For PostgreSQL, MySQL and CockroachDB providers, you need to create the configured database. For SQLite, the configured database will be automatically created at startup. Memory and bolt data providers do not require an initialization but they could require an update to the existing data after upgrading SFTPGo.
SFTPGo will attempt to automatically detect if the data provider is initialized/updated and if not, will attempt to initialize/ update it on startup as needed.
@@ -120,35 +202,70 @@ sftpgo initprovider --help
You can disable automatic data provider checks/updates at startup by setting the `update_mode` configuration key to `1`.
If for some reason you want to downgrade SFTPGo, you may need to downgrade your data provider schema and data as well. You can use the `revertprovider` command for this task.
We support the follwing schema versions:
- `6`, this is the latest version
- `4`, this is the schema for v1.0.0-v1.2.x
So, if you plan to downgrade from git master to 1.2.x, you can prepare your data provider executing the following command from the configuration directory:
```shell
sftpgo revertprovider --to-version 4
```
Take a look at the CLI usage to learn how to specify a different configuration file:
You can also reset your provider by using the `resetprovider` sub-command. Take a look at the CLI usage for more details:
```bash
sftpgo resetprovider --help
```
:warning: Please note that some data providers (e.g. MySQL and CockroachDB) do not support schema changes within a transaction, this means that you may end up with an inconsistent schema if migrations are forcibly aborted. CockroachDB doesn't support database-level locks, so make sure you don't execute migrations concurrently.
## Create the first admin
To start using SFTPGo you need to create an admin user, you can do it in several ways:
- by using the web admin interface. The default URL is [http://127.0.0.1:8080/web/admin](http://127.0.0.1:8080/web/admin)
- by loading initial data
- by enabling `create_default_admin` in your configuration file and setting the environment variables `SFTPGO_DEFAULT_ADMIN_USERNAME` and `SFTPGO_DEFAULT_ADMIN_PASSWORD`
## Upgrading
SFTPGo supports upgrading from the previous release branch to the current one.
Some examples for supported upgrade paths are:
- from 2.1.x to 2.2.x
- from 2.2.x to 2.3.x and so on.
For supported upgrade paths, the data and schema are migrated automatically when SFTPGo starts, alternatively you can use the `initprovider` command before starting SFTPGo.
So if, for example, you want to upgrade from 2.0.x to 2.2.x, you must first install version 2.1.x, update the data provider (automatically, by starting SFTPGo or manually using the `initprovider` command) and finally install the version 2.2.x. It is recommended to always install the latest available minor version, ie do not install 2.1.0 if 2.1.2 is available.
Loading data from a provider independent JSON dump is supported from the previous release branch to the current one too. After upgrading SFTPGo it is advisable to regenerate the JSON dump from the new version.
## Downgrading
If for some reason you want to downgrade SFTPGo, you may need to downgrade your data provider schema and data as well. You can use the `revertprovider` command for this task.
As for upgrading, SFTPGo supports downgrading from the previous release branch to the current one.
So, if you plan to downgrade from 2.3.x to 2.2.x, before uninstalling 2.3.x version, you can prepare your data provider executing the following command from the configuration directory:
```shell
sftpgo revertprovider
```
Take a look at the CLI usage to learn how to specify a configuration file:
```shell
sftpgo revertprovider --help
```
The `revertprovider` command is not supported for the memory provider.
## Users and folders management
Please note that we only support the current release branch and the current main branch, if you find a bug it is better to report it rather than downgrading to an older unsupported version.
After starting SFTPGo you can manage users and folders using:
## Users, groups, folders and other resource management
- the [web based administration interface](./docs/web-admin.md)
After starting SFTPGo you can manage users, groups, folders and other resources using:
- the [WebAdmin UI](./docs/web-admin.md)
- the [REST API](./docs/rest-api.md)
To support embedded data providers like `bolt` and `SQLite` we can't have a CLI that directly write users and folders to the data provider, we always have to use the REST API.
To support embedded data providers like `bolt` and `SQLite`, which do not support concurrent connections, we can't have a CLI that directly write users and other resources to the data provider, we always have to use the REST API.
Full details for users, groups, folders, admins and other resources are documented in the [OpenAPI](./openapi/openapi.yaml) schema. If you want to render the schema without importing it manually, you can explore it on [Stoplight](https://sftpgo.stoplight.io/docs/sftpgo/openapi.yaml).
:warning: SFTPGo users, groups and folders are virtual and therefore unrelated to the system ones. There is no need to create system-wide users and groups.
## Tutorials
@@ -156,30 +273,34 @@ Some step-to-step tutorials can be found inside the source tree [howto](./docs/h
## Authentication options
### External Authentication
<details><summary> External Authentication</summary>
Custom authentication methods can easily be added. SFTPGo supports external authentication modules, and writing a new backend can be as simple as a few lines of shell script. More information can be found [here](./docs/external-auth.md).
### Keyboard Interactive Authentication
</details>
<details><summary> Keyboard Interactive Authentication</summary>
Keyboard interactive authentication is, in general, a series of questions asked by the server with responses provided by the client.
This authentication method is typically used for multi-factor authentication.
More information can be found [here](./docs/keyboard-interactive.md).
</details>
## Dynamic user creation or modification
A user can be created or modified by an external program just before the login. More information about this can be found [here](./docs/dynamic-user-mod.md).
## Custom Actions
SFTPGo allows to configure custom commands and/or HTTP notifications on file upload, download, delete, rename, on SSH commands and on user add, update and delete.
SFTPGo allows you to configure custom commands and/or HTTP hooks to receive notifications about file uploads, deletions and several other events.
More information about custom actions can be found [here](./docs/custom-actions.md).
## Virtual folders
Directories outside the user home directory can be exposed as virtual folders, more information [here](./docs/virtual-folders.md).
Directories outside the user home directory or based on a different storage provider can be mapped as virtual folders, more information [here](./docs/virtual-folders.md).
## Other hooks
@@ -188,17 +309,9 @@ You can use your own hook to [check passwords](./docs/check-password-hook.md).
## Storage backends
### S3 Compatible Object Storage backends
### S3/GCP/Azure
Each user can be mapped to the whole bucket or to a bucket virtual folder. This way, the mapped bucket/virtual folder is exposed over SFTP/SCP/FTP/WebDAV. More information about S3 integration can be found [here](./docs/s3.md).
### Google Cloud Storage backend
Each user can be mapped with a Google Cloud Storage bucket or a bucket virtual folder. This way, the mapped bucket/virtual folder is exposed over SFTP/SCP/FTP/WebDAV. More information about Google Cloud Storage integration can be found [here](./docs/google-cloud-storage.md).
### Azure Blob Storage backend
Each user can be mapped with an Azure Blob Storage container or a container virtual folder. This way, the mapped container/virtual folder is exposed over SFTP/SCP/FTP/WebDAV. More information about Azure Blob Storage integration can be found [here](./docs/azure-blob-storage.md).
Each user can be mapped with a [S3 Compatible Object Storage](./docs/s3.md) /[Google Cloud Storage](./docs/google-cloud-storage.md)/[Azure Blob Storage](./docs/azure-blob-storage.md) bucket or a bucket virtual folder.
### SFTP backend
@@ -208,22 +321,26 @@ Each user can be mapped to another SFTP server account or a subfolder of it. Mor
Data at-rest encryption is supported via the [cryptfs backend](./docs/dare.md).
### HTTP/S backend
HTTP/S backend allows you to write your own custom storage backend by implementing a REST API. More information can be found [here](./docs/httpfs.md).
### Other Storage backends
Adding new storage backends is quite easy:
- implement the [Fs interface](./vfs/vfs.go#L28 "interface for filesystem backends").
- implement the [Fs interface](./internal/vfs/vfs.go#L86 "interface for filesystem backends").
- update the user method `GetFilesystem` to return the new backend
- update the web interface and the REST API CLI
- add the flags for the new storage backed to the `portable` mode
Anyway, some backends require a pay per use account (or they offer free account for a limited time period only). To be able to add support for such backends or to review pull requests, please provide a test account. The test account must be available for enough time to be able to maintain the backend and do basic tests before each new release.
Anyway, some backends require a pay per-use account (or they offer free account for a limited time period only). To be able to add support for such backends or to review pull requests, please provide a test account. The test account must be available for enough time to be able to maintain the backend and do basic tests before each new release.
## Brute force protection
The [connection failed logs](./docs/logs.md) can be used for integration in tools such as [Fail2ban](http://www.fail2ban.org/). Example of [jails](./fail2ban/jails) and [filters](./fail2ban/filters) working with `systemd`/`journald` are available in fail2ban directory.
SFTPGo supports a built-in [defender](./docs/defender.md).
You can also use the built-in [defender](./docs/defender.md).
Alternately you can use the [connection failed logs](./docs/logs.md) for integration in tools such as [Fail2ban](http://www.fail2ban.org/). Example of [jails](./fail2ban/jails) and [filters](./fail2ban/filters) working with `systemd`/`journald` are available in fail2ban directory.
## Account's configuration properties
@@ -247,12 +364,6 @@ We are very grateful to all the people who contributed with ideas and/or pull re
Thank you [ysura](https://www.ysura.com/) for granting me stable access to a test AWS S3 account.
## Sponsors
I'd like to make SFTPGo into a sustainable long term project and your [sponsorship](https://github.com/sponsors/drakkan) will really help :heart:
Bronze, Silver and Gold sponsors will be listed here (if they wish).
## License
GNU GPLv3
GNU AGPL-3.0-only

357
README.zh_CN.md Normal file
View File

@@ -0,0 +1,357 @@
# SFTPGo
[![CI Status](https://github.com/drakkan/sftpgo/workflows/CI/badge.svg?branch=main&event=push)](https://github.com/drakkan/sftpgo/workflows/CI/badge.svg?branch=main&event=push)
[![Code Coverage](https://codecov.io/gh/drakkan/sftpgo/branch/main/graph/badge.svg)](https://codecov.io/gh/drakkan/sftpgo/branch/main)
[![License: AGPL-3.0-only](https://img.shields.io/badge/License-AGPLv3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
[![Docker Pulls](https://img.shields.io/docker/pulls/drakkan/sftpgo)](https://hub.docker.com/r/drakkan/sftpgo)
[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)
[English](./README.md) | [简体中文](./README.zh_CN.md)
功能齐全、高度可配置化、支持自定义 HTTP/SFTP/S 和 WebDAV 的 SFTP 服务。
一些存储后端支持本地文件系统、加密本地文件系统、S3兼容对象存储Google Cloud 存储Azure Blob 存储SFTP。
:warning: 我無法自己維護中文翻譯,這個文檔可能已經過時了
## 赞助商
如果您发现 SFTPGo 有用,请考虑支持这个开源项目。
维护和发展 SFTPGo 对我来说是一项繁重的工作——很容易相当于一份全职工作。
我想让 SFTPGo 成为一个可持续的长期项目,并且不想引入双重许可选项并将某些功能仅限于专有版本。
如果您使用 SFTPGo确保您所依赖的项目保持健康和良好维护符合您的最大利益。
这只能通过您的捐赠和[赞助](https://github.com/sponsors/drakkan) 实现:心:
您还可以从 [SFTPGo 网站](https://sftpgo.com/#pricing) 购买支持计划。
通过赞助/捐赠或支持计划,我们建立了一个互惠渠道,确保您和项目取得更好的成果。
### 感谢我们的赞助商
#### 白金赞助商
[<img src="./img/Aledade_logo.png" alt="Aledade logo" width="202" height="70">](https://www.aledade.com/)
#### 銀牌贊助商
[<img src="./img/Dendi_logo.png" alt="Dendi logo" width="212" height="66">](https://dendisoftware.com/)
#### 铜牌赞助商
[<img src="https://www.7digital.com/wp-content/themes/sevendigital/images/top_logo.png" alt="7digital logo">](https://www.7digital.com/)
## 支持政策
SFTPGo 是一个开源项目,您当然可以免费使用它,但也请不要要求免费支持。
我们将检查报告的问题以查看您是否遇到错误,如果是,我们将修复它,但只会为项目赞助商/捐助者提供支持。
如果您报告无效问题或要求逐步支持,您的问题将保持打开状态而没有答案,或者将被关闭为无效而无需进一步解释。 感谢您的理解。
## 特性
- 支持服务本地文件系统、加密本地文件系统、S3 兼容对象存储、Google Cloud 存储、Azure Blob 存储或其它基于 SFTP/SCP/FTP/WebDAV 协议的 SFTP 账户。
- 虚拟目录支持:一个虚拟目录可以用于支持的存储后端。你可以,比如,一个 S3 用户暴露了一个 GCS bucket或者其中一部分在特定的路径下、一个加密本地文件系统在另一个。虚拟目录可以对于大量用户作为私密或者共享分享虚拟目录你可以为每个用户定义不同的配额。
- 可配置的 [自定义命令 和/或 HTTP 钩子](./docs/custom-actions.md) 在 SSH 命令的 upload, pre-upload, download, pre-download, delete, pre-delete, rename, mkdir, rmdir 阶段,和用户添加、更新、删除阶段。
- 存储在 “数据提供程序” 中的虚拟账户。
- 支持 SQLite, MySQL, PostgreSQL, CockroachDB, Bolt (Go 原生键/值存储) 和内存数据提供程序。
- 为本地账户提供 Chroot 隔离。云端账户可以限制为特定的基本路径。
- 每个用户和每个目录虚拟权限,对于每个暴露的路径你可以允许或禁止:目录展示、上传、覆盖、下载、删除、重命名、创建文件夹、创建软连接、修改 owner/group/file 模式和更改时间。
- 为用户和目录管理提供、数据保留、备份、恢复和即时活动连接的实时报告,可能会强制关闭连接,提供 [REST API](./docs/rest-api.md)。
- [基于 Web 的管理员界面](./docs/web-admin.md) 可以容易地管理用户、目录和连接。
- [Web 客户端界面](./docs/web-client.md) 以便终端用户可以在浏览器中更改他们的凭据、管理和共享他们的文件。
- 公钥和密码认证。支持每个用户多个公钥。
- SSH 用户 [证书认证](https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?rev=1.8).
- 键盘交互认证。您可以轻松设置可定制的多因素身份认证。
- 部分验证。你可以配置多步验证请求,例如,用户密码在公钥验证之后。
- 每个用户的身份验证方法。
- [双重验证](./docs/howto/two-factor-authentication.md) 基于实现一次性密码 (RFC 6238) 可以与 Authy、Google Authenticator 和其他兼容的应用程序配合使用。
- 通过 [群组](./docs/groups.md) 精简用户管理。
- 通过外部 程序/HTTP API 自定义验证。
- Web 客户端和 Web 管理员他用户界面支持 [OpenID Connect](https://openid.net/connect/) 验证,所以它们很容易被集成在诸如 [Keycloak](https://www.keycloak.org/) 之类的身份认证程序。你可以在 [](./docs/oidc.md) 获取更多信息。
- [静态数据加密](./docs/dare.md)。
- 在登录之前通过 程序/HTTP API 进行动态用户修改。
- 配额支持:账户拥有独立的磁盘配额表示为总计最大体积 和/或 最大文件数量。
- 带宽节流,基于客户端 IP 地址独立设置上传、下载和覆盖。
- 数据传输带宽限制,限制总量或基于客户端 IP 地址设置上传、下载和覆盖。限制可以通过 REST API 重置。
- 支持每个协议[限速](./docs/rate-limiting.md),可以可选与内置的防护连接实现自动封禁重复超过设置限制的主机。
- 每个用户的最大并发会话。
- 每个用户和全局 IP 过滤:登录可以被限制在特定的 IP 段和指定的 IP 地址。
- 每个用户和每个文件夹类似于 shell 的模式过滤:文件可以被允许、禁止和隐藏基于类 shell 模式。
- 自动使 idle 连接终止。
- 通过内置的 [防护](./docs/defender.md) 自动管理禁止名单。
- 通过 [插件](https://github.com/sftpgo/sftpgo-plugin-geoipfilter) 实现 地理-IP 过滤。
- 原子上传是可配置的。
- 每个用户 文件/目录 所有权映射:你可以将所有用户映射到运行 SFEPGo 的系统账户(所有的平台都是支持的),或者你可以使用 root 用户运行 SFTPGo 并且映射每个用户或用户组到一个不同系统账户(仅支持 \*NIX
- 通过 SSH 支持 Git 仓库。
- 支持 SCP 和 rsync。
- 支持 FTP/S。你可以配置 FTP 服务为控制和数据连接都需要 TLS。
- [WebDAV](./docs/webdav.md) 是支持的。
- 两步 TLS 验证,具有客户端证书身份验证的 aka TLS支持 REST API/Web Admin、FTPS 和 基于 HTTPS 的 WebDAV。
- 每个用户协议限制。你可以为每个用户配置允许的协议(SSH/HTTP/FTP/WebDAV)。
- 暴露 [输出指标](./docs/metrics.md)。
- 支持 HAProxy PROXY 协议:你可以不需要丢失客户端地址信息代理 和/或 负载平衡 SFTP/SCP/FTP 服务。
- 简单从 Linux 系统用户账户进行 [迁移](./examples/convertusers)。
- [可携带模式](./docs/portable-mode.md):按需共享单个目录的便捷方式。
- [SFTP 子系统模式](./docs/sftp-subsystem.md):你可以使用 SFTPGo 作为 OpenSSH 的 SFTP 子系统。
- 性能分析基于内置的 [分析器](./docs/profiling.md)。
- 配置项格式基于你的选择JSON, TOML, YAML, HCL, envfile 都是支持的。
- 日志文件是精确的,它们被存储为易被解析的 JSON 格式。([更多信息](./docs/logs.md)
- SFTPGo 支持 [插件系统](./docs/plugins.md),因此可以使用外部插件拓展。
## 平台
SFTPGo 基于 Linux 开发和创建。在每一次提交之后,代码会自动通过 [GitHub Actions](./.github/workflows/development.yml) 在 Linux、macOS 和 Windows 构建和测试。测试用例定期手动在 FreeBSD 执行,其他的 *BSD 变体同样适用。
## 要求
- Go 作为构建仅有的依赖。我们支持 [持续集成工作流](./.github/workflows) 中使用的 Go 版本。
- 使用适配的 SQL 服务作为数据提供程序PostgreSQL 9.4+, MySQL 5.6+, SQLite 3.x, CockroachDB stable.
- SQL 服务是可选的:你可以使用一个内置的 bolt 数据库以 键/值 存储,或者一个内存中的数据提供程序。
## 安装
为 Linux、macOS 和 Windows 提供的二进制发行版是可用的。请参考 [发行版](https://github.com/drakkan/sftpgo/releases "releases") 页面。
一个官方的 Docker 镜像是可用的。文档参考 [Docker](./docker/README.md)。
<details>
<summary>一些 Linux 分支包是可用的</summary>
- Arch Linux 通过 AUR:
- [sftpgo](https://aur.archlinux.org/packages/sftpgo/)。这个包跟随稳定的发行版。需要 `git``gcc``go` 进行构建。
- [sftpgo-bin](https://aur.archlinux.org/packages/sftpgo-bin/)。这个包跟随稳定的发行版从 GitHub 下载预构建 Linux 二进制文件。不需要 `git``gcc``go` 进行构建。
- [sftpgo-git](https://aur.archlinux.org/packages/sftpgo-git/)。这个包构建和下载基于最新的 `git` 主分支。需要 `git``gcc``go` 进行构建。
- Deb and RPM 包在每次提交和发行之后构建。
- Ubuntu PPA 在 [](https://launchpad.net/~sftpgo/+archive/ubuntu/sftpgo) 可用。
- Void Linux 提供一个 [官方包](https://github.com/void-linux/void-packages/tree/master/srcpkgs/sftpgo)。
</details>
SFTPGo 在 [AWS Marketplace](https://aws.amazon.com/marketplace/seller-profile?id=6e849ab8-70a6-47de-9a43-13c3fa849335) 和 [Azure Marketplace](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/eliamarzia1667381463185.sftpgo_linux) 同样可用,在此付费可以帮助 SFTPGo 成为一个可持续发展的长期项目。
<details><summary>Windows 包</summary>
- Windows installer 安装和运行 SFTPGo 作为一个 Windows 服务。
- 开箱即用的包启动按需使用的 SFTPGo。
- [winget](https://docs.microsoft.com/en-us/windows/package-manager/winget/install) 包下载和运行 SFTPGo 作为一个 Windows 服务:`winget install SFTPGo`
- [Chocolatey 包](https://community.chocolatey.org/packages/sftpgo) 下载和运行 SFTPGo 作为一个 Windows 服务。
</details>
在 FreeBSD你可以从 [SFTPGo port](https://www.freshports.org/ftp/sftpgo) 下载。
在 DragonFlyBSD你可以从 [DPorts](https://github.com/DragonFlyBSD/DPorts/tree/master/ftp/sftpgo) 下载。
您可以从 [Actions](https://github.com/drakkan/sftpgo/Actions) 页面选择一个 commit 并下载 Linux、macOS 或 Windows 的匹配构建从而轻松测试新特性。GitHub 存储 90 天。
另外,你可以 [从源码构建](./docs/build-from-source.md)。
[不耐烦的快速上手指南](./docs/howto/getting-started.md).
## 配置项
可以完整的配置项方法说明可以参考 [配置项](./docs/full-configuration.md)。
请确保按需运行之前,[初始化数据提供程序](#数据提供程序初始化和管理)。
默认配置启动 STFPGo运行
```bash
sftpgo serve
```
如果你将 SFTPGo作为服务请参阅 [这篇文档](./docs/service.md)。
### 数据提供程序初始化和管理
在启动 SFTPGo 服务之前,请确保配置的数据提供程序已经被适当的 初始化/更新。
对于 PostgreSQL, MySQL 和 CockroachDB 提供,你需要创建一个配置数据库。对于 SQLite配置数据库将会在启动时被自动创建。内存和 bolt 数据提供程序不需要初始化,但是它们需要在升级 SFTPGo 之后更新现有的数据。
SFTPGo 会尝试自动探测数据提供程序是否被 初始化/更新;如果没有,将会在启动时尝试 初始化/更新。
或者,你可以通过 `initprovider` 命令自行 创建/更新 需要的数据提供程序结构。
比如,你可以执行在配置文件目录下面的命令:
```bash
sftpgo initprovider
```
看一看 CLI 用法学习如何指定一个不同的配置文件:
```bash
sftpgo initprovider --help
```
你可以在启动阶段通过设置 `update_mode` 配置项为 `1`,禁止自动数据提供程序 检查/更新。
你可以通过使用 `resetprovider` 子命令重置你的数据提供程序。看一看 CLI 用法获取更多细节信息:
```bash
sftpgo resetprovider --help
```
:warning: 请注意一些数据提供程序(比如 MySQL 和 CockroachDB不支持事务内的方案更改这意味着如果迁移被强制中止或由多个实例同时运行您可能会得到不一致的方案。
## 创建第一个管理员
开始使用 SFTPGo你需要创建一个管理员用户你可以通过不同的方式进行实现
- 通过 web 管理员界面。默认 URL 是 [http://127.0.0.1:8080/web/admin](http://127.0.0.1:8080/web/admin)
- 通过加载初始数据
- 通过在你的配置文件启用 `create_default_admin` 并设置环境变量 `SFTPGO_DEFAULT_ADMIN_USERNAME``SFTPGO_DEFAULT_ADMIN_PASSWORD`
## 升级
SFTPGo 支持从之前的发行版分支升级到当前分支。
一些支持的升级路径如下:
- 从 1.2.x 到 2.0.x
- 从 2.0.x 到 2.1.x 等。
对支持的升级路径,数据和方案将会自动迁移,你可以使用 `initprovider` 命令作为替代。
所以,比如,你想从 1.2.x 之前的版本升级到 2.0.x你必须首先安装 1.2.x 版本,升级数据提供程序并最终安装版本 2.0.x。建议安装最新的可用小版本如果 1.2.2 可用就不要安装 1.2.0 版本。
从以前发行版分支到当前版本,都支持从独立于数据提供程序的 JSON 转储中加载数据。升级 SFTPGo 后,建议从新版本重新生成 JSON 转储。
## 降级
如果因为一些原因你想降级 SFTPGo你可能需要降级你的用户数据提供程序方案和数据。你可以使用 `revertprovider` 命令执行这项任务。
对于升级SFTPGo 支持从先前的发行版分支降级到当前分支。
所以,如果你有计划从 2.0.x 降级到 1.2.x之前先卸载 2.0.x 版本,你可以通过从配置目录执行以下命令来准备你的数据提供程序:
```shell
sftpgo revertprovider --to-version 4
```
看一看 CLI 的用法、了解 `--to-version` 参数支持的参数,了解如何去指定一个不同的配置文件:
```shell
sftpgo revertprovider --help
```
`revertprovider` 命令不支持内存数据提供程序。
请注意我们只支持当前发行版分支和当前主分支,如果你发现了个 bug最好是报告这个问题而不是降级到一个老的、不被支持的版本。
## 用户和目录管理
在启动 SFTPGo 之后,你可以管理用户和目录使用:
- [基于 Web 的管理员界面](./docs/web-admin.md)
- [REST API](./docs/rest-api.md)
支持内置的数据提供程序比如 `bolt``SQLite`。我们不能使用 CLI 直接将用户和文件夹写到数据提供程序,通常使用 REAST API。
对于用户、目录、管理员和其它资源的细节,都记录在 [OpenAPI](./openapi/openapi.yaml) 方案。如果你想在不手动引入的情况下渲染方案,你可以在 [Stoplight](https://sftpgo.stoplight.io/docs/sftpgo/openapi.yaml) 上暴露它。
## 教程
一些手把手教程可以在源码文件树中的 [howto](./docs/howto "How-to") 目录找到。
## 认证选项
<details><summary>外部认证</summary>
自定义认证方法可以很容易被添加。SFTPGo 支持外部认证模块,编写一个后端可以如编写几行 shell 脚本那样简单。更多的信息可以参考 [外部认证](./docs/external-auth.md)。
</details>
<details><summary>键盘交互认证</summary>
一般来说,键盘交互身份验证是服务器提出的一系列问题,由客户端提供响应。
这种身份认证方法通常用于多因素身份认证。
更多信息参考 [键盘交互](./docs/keyboard-interactive.md)。
</details>
## 动态用户创建或修改
一个用户可以通过外部程序在登录之前被创建和修改。更多关于此可以参考 [动态用户修改](./docs/dynamic-user-mod.md)。
## 自定义动作
SFTPGo 允许你配置自定义的命令 和/或 HTTP 钩子去获取关于文件上传、删除和一些其它操作的通知。
更多关于自定义动作的信息你可以参考 [自定义动作](./docs/custom-actions.md)。
## 虚拟目录
用户 home 文件夹外或者基于不同存储提供的目录,可以作为虚拟目录进行暴露,详细信息参考 [虚拟目录](./docs/virtual-folders.md)。
## 其它钩子
你可以使用 [Post-connect 钩子](./docs/post-connect-hook.md) 及时获取新的连接建立,使用 [Post-login hook](./docs/post-login-hook.md) 获取每次登录之后的通知。你可以使用你自己的钩子去 [验证密码](./docs/check-password-hook.md)。
## 存储后端
### S3/GCP/Azure
每个用户可以被映射到 [S3 兼容对象存储](./docs/s3.md) /[Google Cloud 存储](./docs/google-cloud-storage.md)/[Azure Blob 存储](./docs/azure-blob-storage.md) bucket 或者一个 bucket 虚拟目录,通过 SFTP/SCP/FTP/WebDAV 进行暴露。
### SFTP 后端
每个用户可以被映射到另一个 SFTP 服务器账户或者它的子目录。更多的信息可以参考 [sftpfs](./docs/sftpfs.md)。
### 加密后端
数据静态加密通过 [cryptfs 后端](./docs/dare.md) 进行支持。
### 其它存储后端
添加新的存储后端非常简单:
- 实现 [Fs 接口](./vfs/vfs.go#L28 "interface for filesystem backends")
- 更新用户方法 `GetFilesystem` 返回新的后端
- 更新 web 接口和 REST API CLI
- 为新的存储后端添加向 `portable` 模式添加 flags
无论如何,一些后端需要按次付费账户(或者他们提供限制期限内提供免费账户)。为了能够添加这些账户支持或者预览 PRs请提供一个测试账户。测试账户必须在提供足够长时间维护此后端并且支持每一次新的发行版之前做基本测试。
## 强力保护
SFTPGo 支持内置 [防护](./docs/defender.md)。
你可以使用 [连接失败日志](./docs/logs.md) 在诸如 [Fail2ban](http://www.fail2ban.org/) 进行工具内集成。[jails](./fail2ban/jails) 和 [filters](./fail2ban/filters) 示例,在 fail2ban 目录中与 `systemd`/`journald` 是可以同时工作的。
## 账户配置属性
关于账户配置属性的细节信息,请参考 [账户](./docs/account.md)。
## 性能
SFTPGo 在没有特殊配置的情况下,可以实现低端硬件轻松达到 GB 量级连接,对于大多数场景足够使用了。
更多深度性能分析可以参考 [性能](./docs/performance.md)。
## 发行节奏
STFPGo 发行版是特性驱动的,我们没有基于计划的固定时间。粗略估计,你可以每年期待一到两个新的发行版。
## 感谢
SFTPGo 使用了 [go.mod](./go.mod) 中列出的第三方库。
我们非常感激所有贡献想法 和/或 PRs。
感谢 [ysura](https://www.ysura.com/) 给予我测试 AWS S3 账户的稳定权限。
## 赞助者
我希望可以使 STFPGo 成为一个可持续发展的长期项目,你的 [赞助](https://github.com/sponsors/drakkan) 对我很有帮助!:heart:
感谢我们的赞助者!
[<img src="https://www.7digital.com/wp-content/themes/sevendigital/images/top_logo.png" alt="7digital logo">](https://www.7digital.com/)
## 许可证
GNU AGPL-3.0-only

View File

@@ -2,11 +2,9 @@
## Supported Versions
Only the current release of the software is actively supported. If you need
help backporting fixes into an older release, feel free to ask.
Only the current release of the software is actively supported.
[Contact us](mailto:support@sftpgo.com) if you need early security patches and enterprise-grade security.
## Reporting a Vulnerability
Email your vulnerability information to SFTPGo's maintainer:
Nicola Murino <nicola.murino@gmail.com>
To report (possible) security issues in SFTPGo, please either send a mail to the [SFTPGo Team](mailto:support@sftpgo.com) or use Github's [private reporting feature](https://github.com/drakkan/sftpgo/security/advisories/new).

View File

@@ -1,12 +0,0 @@
package cmd
import "github.com/spf13/cobra"
var genCmd = &cobra.Command{
Use: "gen",
Short: "A collection of useful generators",
}
func init() {
rootCmd.AddCommand(genCmd)
}

View File

@@ -1,76 +0,0 @@
package cmd
import (
"os"
"github.com/rs/zerolog"
"github.com/spf13/cobra"
"github.com/drakkan/sftpgo/logger"
)
var genCompletionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate shell completion script",
Long: `To load completions:
Bash:
$ source <(sftpgo gen completion bash)
To load completions for each session, execute once:
Linux:
$ sudo sftpgo gen completion bash > /usr/share/bash-completion/completions/sftpgo
MacOS:
$ sudo sftpgo gen completion bash > /usr/local/etc/bash_completion.d/sftpgo
Zsh:
If shell completion is not already enabled in your environment you will need
to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
To load completions for each session, execute once:
$ sftpgo gen completion zsh > "${fpath[1]}/_sftpgo"
Fish:
$ sftpgo gen completion fish | source
To load completions for each session, execute once:
$ sftpgo gen completion fish > ~/.config/fish/completions/sftpgo.fish
`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.ExactValidArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var err error
logger.DisableLogger()
logger.EnableConsoleLogger(zerolog.DebugLevel)
switch args[0] {
case "bash":
err = cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
err = cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
err = cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
err = cmd.Root().GenPowerShellCompletion(os.Stdout)
}
if err != nil {
logger.WarnToConsole("Unable to generate shell completion script: %v", err)
os.Exit(1)
}
},
}
func init() {
genCmd.AddCommand(genCompletionCmd)
}

View File

@@ -1,52 +0,0 @@
package cmd
import (
"fmt"
"os"
"github.com/rs/zerolog"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/version"
)
var (
manDir string
genManCmd = &cobra.Command{
Use: "man",
Short: "Generate man pages for SFTPGo CLI",
Long: `This command automatically generates up-to-date man pages of SFTPGo's
command-line interface. By default, it creates the man page files
in the "man" directory under the current directory.
`,
Run: func(cmd *cobra.Command, args []string) {
logger.DisableLogger()
logger.EnableConsoleLogger(zerolog.DebugLevel)
if _, err := os.Stat(manDir); os.IsNotExist(err) {
err = os.MkdirAll(manDir, os.ModePerm)
if err != nil {
logger.WarnToConsole("Unable to generate man page files: %v", err)
os.Exit(1)
}
}
header := &doc.GenManHeader{
Section: "1",
Manual: "SFTPGo Manual",
Source: fmt.Sprintf("SFTPGo %v", version.Get().Version),
}
cmd.Root().DisableAutoGenTag = true
err := doc.GenManTree(cmd.Root(), header, manDir)
if err != nil {
logger.WarnToConsole("Unable to generate man page files: %v", err)
os.Exit(1)
}
},
}
)
func init() {
genManCmd.Flags().StringVarP(&manDir, "dir", "d", "man", "The directory to write the man pages")
genCmd.AddCommand(genManCmd)
}

View File

@@ -1,70 +0,0 @@
package cmd
import (
"os"
"github.com/rs/zerolog"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/drakkan/sftpgo/config"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/utils"
)
var (
initProviderCmd = &cobra.Command{
Use: "initprovider",
Short: "Initializes and/or updates the configured data provider",
Long: `This command reads the data provider connection details from the specified
configuration file and creates the initial structure or update the existing one,
as needed.
Some data providers such as bolt and memory does not require an initialization
but they could require an update to the existing data after upgrading SFTPGo.
For SQLite/bolt providers the database file will be auto-created if missing.
For PostgreSQL and MySQL providers you need to create the configured database,
this command will create/update the required tables as needed.
To initialize/update the data provider from the configuration directory simply use:
$ sftpgo initprovider
Please take a look at the usage below to customize the options.`,
Run: func(cmd *cobra.Command, args []string) {
logger.DisableLogger()
logger.EnableConsoleLogger(zerolog.DebugLevel)
configDir = utils.CleanDirInput(configDir)
err := config.LoadConfig(configDir, configFile)
if err != nil {
logger.WarnToConsole("Unable to initialize data provider, config load error: %v", err)
return
}
kmsConfig := config.GetKMSConfig()
err = kmsConfig.Initialize()
if err != nil {
logger.ErrorToConsole("unable to initialize KMS: %v", err)
os.Exit(1)
}
providerConf := config.GetProviderConf()
logger.InfoToConsole("Initializing provider: %#v config file: %#v", providerConf.Driver, viper.ConfigFileUsed())
err = dataprovider.InitializeDatabase(providerConf, configDir)
if err == nil {
logger.InfoToConsole("Data provider successfully initialized/updated")
} else if err == dataprovider.ErrNoInitRequired {
logger.InfoToConsole("%v", err.Error())
} else {
logger.WarnToConsole("Unable to initialize/update the data provider: %v", err)
os.Exit(1)
}
},
}
)
func init() {
rootCmd.AddCommand(initProviderCmd)
addConfigFlags(initProviderCmd)
}

View File

@@ -1,392 +0,0 @@
// +build !noportable
package cmd
import (
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/drakkan/sftpgo/common"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/kms"
"github.com/drakkan/sftpgo/service"
"github.com/drakkan/sftpgo/sftpd"
"github.com/drakkan/sftpgo/version"
"github.com/drakkan/sftpgo/vfs"
)
var (
directoryToServe string
portableSFTPDPort int
portableAdvertiseService bool
portableAdvertiseCredentials bool
portableUsername string
portablePassword string
portableLogFile string
portableLogVerbose bool
portablePublicKeys []string
portablePermissions []string
portableSSHCommands []string
portableAllowedPatterns []string
portableDeniedPatterns []string
portableFsProvider int
portableS3Bucket string
portableS3Region string
portableS3AccessKey string
portableS3AccessSecret string
portableS3Endpoint string
portableS3StorageClass string
portableS3KeyPrefix string
portableS3ULPartSize int
portableS3ULConcurrency int
portableGCSBucket string
portableGCSCredentialsFile string
portableGCSAutoCredentials int
portableGCSStorageClass string
portableGCSKeyPrefix string
portableFTPDPort int
portableFTPSCert string
portableFTPSKey string
portableWebDAVPort int
portableWebDAVCert string
portableWebDAVKey string
portableAzContainer string
portableAzAccountName string
portableAzAccountKey string
portableAzEndpoint string
portableAzAccessTier string
portableAzSASURL string
portableAzKeyPrefix string
portableAzULPartSize int
portableAzULConcurrency int
portableAzUseEmulator bool
portableCryptPassphrase string
portableSFTPEndpoint string
portableSFTPUsername string
portableSFTPPassword string
portableSFTPPrivateKeyPath string
portableSFTPFingerprints []string
portableSFTPPrefix string
portableCmd = &cobra.Command{
Use: "portable",
Short: "Serve a single directory",
Long: `To serve the current working directory with auto generated credentials simply
use:
$ sftpgo portable
Please take a look at the usage below to customize the serving parameters`,
Run: func(cmd *cobra.Command, args []string) {
portableDir := directoryToServe
fsProvider := dataprovider.FilesystemProvider(portableFsProvider)
if !filepath.IsAbs(portableDir) {
if fsProvider == dataprovider.LocalFilesystemProvider {
portableDir, _ = filepath.Abs(portableDir)
} else {
portableDir = os.TempDir()
}
}
permissions := make(map[string][]string)
permissions["/"] = portablePermissions
portableGCSCredentials := ""
if fsProvider == dataprovider.GCSFilesystemProvider && portableGCSCredentialsFile != "" {
contents, err := getFileContents(portableGCSCredentialsFile)
if err != nil {
fmt.Printf("Unable to get GCS credentials: %v\n", err)
os.Exit(1)
}
portableGCSCredentials = contents
portableGCSAutoCredentials = 0
}
portableSFTPPrivateKey := ""
if fsProvider == dataprovider.SFTPFilesystemProvider && portableSFTPPrivateKeyPath != "" {
contents, err := getFileContents(portableSFTPPrivateKeyPath)
if err != nil {
fmt.Printf("Unable to get SFTP private key: %v\n", err)
os.Exit(1)
}
portableSFTPPrivateKey = contents
}
if portableFTPDPort >= 0 && len(portableFTPSCert) > 0 && len(portableFTPSKey) > 0 {
_, err := common.NewCertManager(portableFTPSCert, portableFTPSKey, filepath.Clean(defaultConfigDir),
"FTP portable")
if err != nil {
fmt.Printf("Unable to load FTPS key pair, cert file %#v key file %#v error: %v\n",
portableFTPSCert, portableFTPSKey, err)
os.Exit(1)
}
}
if portableWebDAVPort > 0 && len(portableWebDAVCert) > 0 && len(portableWebDAVKey) > 0 {
_, err := common.NewCertManager(portableWebDAVCert, portableWebDAVKey, filepath.Clean(defaultConfigDir),
"WebDAV portable")
if err != nil {
fmt.Printf("Unable to load WebDAV key pair, cert file %#v key file %#v error: %v\n",
portableWebDAVCert, portableWebDAVKey, err)
os.Exit(1)
}
}
service := service.Service{
ConfigDir: filepath.Clean(defaultConfigDir),
ConfigFile: defaultConfigFile,
LogFilePath: portableLogFile,
LogMaxSize: defaultLogMaxSize,
LogMaxBackups: defaultLogMaxBackup,
LogMaxAge: defaultLogMaxAge,
LogCompress: defaultLogCompress,
LogVerbose: portableLogVerbose,
Shutdown: make(chan bool),
PortableMode: 1,
PortableUser: dataprovider.User{
Username: portableUsername,
Password: portablePassword,
PublicKeys: portablePublicKeys,
Permissions: permissions,
HomeDir: portableDir,
Status: 1,
FsConfig: dataprovider.Filesystem{
Provider: dataprovider.FilesystemProvider(portableFsProvider),
S3Config: vfs.S3FsConfig{
Bucket: portableS3Bucket,
Region: portableS3Region,
AccessKey: portableS3AccessKey,
AccessSecret: kms.NewPlainSecret(portableS3AccessSecret),
Endpoint: portableS3Endpoint,
StorageClass: portableS3StorageClass,
KeyPrefix: portableS3KeyPrefix,
UploadPartSize: int64(portableS3ULPartSize),
UploadConcurrency: portableS3ULConcurrency,
},
GCSConfig: vfs.GCSFsConfig{
Bucket: portableGCSBucket,
Credentials: kms.NewPlainSecret(portableGCSCredentials),
AutomaticCredentials: portableGCSAutoCredentials,
StorageClass: portableGCSStorageClass,
KeyPrefix: portableGCSKeyPrefix,
},
AzBlobConfig: vfs.AzBlobFsConfig{
Container: portableAzContainer,
AccountName: portableAzAccountName,
AccountKey: kms.NewPlainSecret(portableAzAccountKey),
Endpoint: portableAzEndpoint,
AccessTier: portableAzAccessTier,
SASURL: portableAzSASURL,
KeyPrefix: portableAzKeyPrefix,
UseEmulator: portableAzUseEmulator,
UploadPartSize: int64(portableAzULPartSize),
UploadConcurrency: portableAzULConcurrency,
},
CryptConfig: vfs.CryptFsConfig{
Passphrase: kms.NewPlainSecret(portableCryptPassphrase),
},
SFTPConfig: vfs.SFTPFsConfig{
Endpoint: portableSFTPEndpoint,
Username: portableSFTPUsername,
Password: kms.NewPlainSecret(portableSFTPPassword),
PrivateKey: kms.NewPlainSecret(portableSFTPPrivateKey),
Fingerprints: portableSFTPFingerprints,
Prefix: portableSFTPPrefix,
},
},
Filters: dataprovider.UserFilters{
FilePatterns: parsePatternsFilesFilters(),
},
},
}
if err := service.StartPortableMode(portableSFTPDPort, portableFTPDPort, portableWebDAVPort, portableSSHCommands, portableAdvertiseService,
portableAdvertiseCredentials, portableFTPSCert, portableFTPSKey, portableWebDAVCert, portableWebDAVKey); err == nil {
service.Wait()
if service.Error == nil {
os.Exit(0)
}
}
os.Exit(1)
},
}
)
func init() {
version.AddFeature("+portable")
portableCmd.Flags().StringVarP(&directoryToServe, "directory", "d", ".", `Path to the directory to serve.
This can be an absolute path or a path
relative to the current directory
`)
portableCmd.Flags().IntVarP(&portableSFTPDPort, "sftpd-port", "s", 0, `0 means a random unprivileged port,
< 0 disabled`)
portableCmd.Flags().IntVar(&portableFTPDPort, "ftpd-port", -1, `0 means a random unprivileged port,
< 0 disabled`)
portableCmd.Flags().IntVar(&portableWebDAVPort, "webdav-port", -1, `0 means a random unprivileged port,
< 0 disabled`)
portableCmd.Flags().StringSliceVarP(&portableSSHCommands, "ssh-commands", "c", sftpd.GetDefaultSSHCommands(),
`SSH commands to enable.
"*" means any supported SSH command
including scp
`)
portableCmd.Flags().StringVarP(&portableUsername, "username", "u", "", `Leave empty to use an auto generated
value`)
portableCmd.Flags().StringVarP(&portablePassword, "password", "p", "", `Leave empty to use an auto generated
value`)
portableCmd.Flags().StringVarP(&portableLogFile, logFilePathFlag, "l", "", "Leave empty to disable logging")
portableCmd.Flags().BoolVarP(&portableLogVerbose, logVerboseFlag, "v", false, "Enable verbose logs")
portableCmd.Flags().StringSliceVarP(&portablePublicKeys, "public-key", "k", []string{}, "")
portableCmd.Flags().StringSliceVarP(&portablePermissions, "permissions", "g", []string{"list", "download"},
`User's permissions. "*" means any
permission`)
portableCmd.Flags().StringArrayVar(&portableAllowedPatterns, "allowed-patterns", []string{},
`Allowed file patterns case insensitive.
The format is:
/dir::pattern1,pattern2.
For example: "/somedir::*.jpg,a*b?.png"`)
portableCmd.Flags().StringArrayVar(&portableDeniedPatterns, "denied-patterns", []string{},
`Denied file patterns case insensitive.
The format is:
/dir::pattern1,pattern2.
For example: "/somedir::*.jpg,a*b?.png"`)
portableCmd.Flags().BoolVarP(&portableAdvertiseService, "advertise-service", "S", false,
`Advertise configured services using
multicast DNS`)
portableCmd.Flags().BoolVarP(&portableAdvertiseCredentials, "advertise-credentials", "C", false,
`If the SFTP/FTP service is
advertised via multicast DNS, this
flag allows to put username/password
inside the advertised TXT record`)
portableCmd.Flags().IntVarP(&portableFsProvider, "fs-provider", "f", int(dataprovider.LocalFilesystemProvider), `0 => local filesystem
1 => AWS S3 compatible
2 => Google Cloud Storage
3 => Azure Blob Storage
4 => Encrypted local filesystem
5 => SFTP`)
portableCmd.Flags().StringVar(&portableS3Bucket, "s3-bucket", "", "")
portableCmd.Flags().StringVar(&portableS3Region, "s3-region", "", "")
portableCmd.Flags().StringVar(&portableS3AccessKey, "s3-access-key", "", "")
portableCmd.Flags().StringVar(&portableS3AccessSecret, "s3-access-secret", "", "")
portableCmd.Flags().StringVar(&portableS3Endpoint, "s3-endpoint", "", "")
portableCmd.Flags().StringVar(&portableS3StorageClass, "s3-storage-class", "", "")
portableCmd.Flags().StringVar(&portableS3KeyPrefix, "s3-key-prefix", "", `Allows to restrict access to the
virtual folder identified by this
prefix and its contents`)
portableCmd.Flags().IntVar(&portableS3ULPartSize, "s3-upload-part-size", 5, `The buffer size for multipart uploads
(MB)`)
portableCmd.Flags().IntVar(&portableS3ULConcurrency, "s3-upload-concurrency", 2, `How many parts are uploaded in
parallel`)
portableCmd.Flags().StringVar(&portableGCSBucket, "gcs-bucket", "", "")
portableCmd.Flags().StringVar(&portableGCSStorageClass, "gcs-storage-class", "", "")
portableCmd.Flags().StringVar(&portableGCSKeyPrefix, "gcs-key-prefix", "", `Allows to restrict access to the
virtual folder identified by this
prefix and its contents`)
portableCmd.Flags().StringVar(&portableGCSCredentialsFile, "gcs-credentials-file", "", `Google Cloud Storage JSON credentials
file`)
portableCmd.Flags().IntVar(&portableGCSAutoCredentials, "gcs-automatic-credentials", 1, `0 means explicit credentials using
a JSON credentials file, 1 automatic
`)
portableCmd.Flags().StringVar(&portableFTPSCert, "ftpd-cert", "", "Path to the certificate file for FTPS")
portableCmd.Flags().StringVar(&portableFTPSKey, "ftpd-key", "", "Path to the key file for FTPS")
portableCmd.Flags().StringVar(&portableWebDAVCert, "webdav-cert", "", `Path to the certificate file for WebDAV
over HTTPS`)
portableCmd.Flags().StringVar(&portableWebDAVKey, "webdav-key", "", `Path to the key file for WebDAV over
HTTPS`)
portableCmd.Flags().StringVar(&portableAzContainer, "az-container", "", "")
portableCmd.Flags().StringVar(&portableAzAccountName, "az-account-name", "", "")
portableCmd.Flags().StringVar(&portableAzAccountKey, "az-account-key", "", "")
portableCmd.Flags().StringVar(&portableAzSASURL, "az-sas-url", "", `Shared access signature URL`)
portableCmd.Flags().StringVar(&portableAzEndpoint, "az-endpoint", "", `Leave empty to use the default:
"blob.core.windows.net"`)
portableCmd.Flags().StringVar(&portableAzAccessTier, "az-access-tier", "", `Leave empty to use the default
container setting`)
portableCmd.Flags().StringVar(&portableAzKeyPrefix, "az-key-prefix", "", `Allows to restrict access to the
virtual folder identified by this
prefix and its contents`)
portableCmd.Flags().IntVar(&portableAzULPartSize, "az-upload-part-size", 4, `The buffer size for multipart uploads
(MB)`)
portableCmd.Flags().IntVar(&portableAzULConcurrency, "az-upload-concurrency", 2, `How many parts are uploaded in
parallel`)
portableCmd.Flags().BoolVar(&portableAzUseEmulator, "az-use-emulator", false, "")
portableCmd.Flags().StringVar(&portableCryptPassphrase, "crypto-passphrase", "", `Passphrase for encryption/decryption`)
portableCmd.Flags().StringVar(&portableSFTPEndpoint, "sftp-endpoint", "", `SFTP endpoint as host:port for SFTP
provider`)
portableCmd.Flags().StringVar(&portableSFTPUsername, "sftp-username", "", `SFTP user for SFTP provider`)
portableCmd.Flags().StringVar(&portableSFTPPassword, "sftp-password", "", `SFTP password for SFTP provider`)
portableCmd.Flags().StringVar(&portableSFTPPrivateKeyPath, "sftp-key-path", "", `SFTP private key path for SFTP provider`)
portableCmd.Flags().StringSliceVar(&portableSFTPFingerprints, "sftp-fingerprints", []string{}, `SFTP fingerprints to verify remote host
key for SFTP provider`)
portableCmd.Flags().StringVar(&portableSFTPPrefix, "sftp-prefix", "", `SFTP prefix allows restrict all
operations to a given path within the
remote SFTP server`)
rootCmd.AddCommand(portableCmd)
}
func parsePatternsFilesFilters() []dataprovider.PatternsFilter {
var patterns []dataprovider.PatternsFilter
for _, val := range portableAllowedPatterns {
p, exts := getPatternsFilterValues(strings.TrimSpace(val))
if len(p) > 0 {
patterns = append(patterns, dataprovider.PatternsFilter{
Path: path.Clean(p),
AllowedPatterns: exts,
DeniedPatterns: []string{},
})
}
}
for _, val := range portableDeniedPatterns {
p, exts := getPatternsFilterValues(strings.TrimSpace(val))
if len(p) > 0 {
found := false
for index, e := range patterns {
if path.Clean(e.Path) == path.Clean(p) {
patterns[index].DeniedPatterns = append(patterns[index].DeniedPatterns, exts...)
found = true
break
}
}
if !found {
patterns = append(patterns, dataprovider.PatternsFilter{
Path: path.Clean(p),
AllowedPatterns: []string{},
DeniedPatterns: exts,
})
}
}
}
return patterns
}
func getPatternsFilterValues(value string) (string, []string) {
if strings.Contains(value, "::") {
dirExts := strings.Split(value, "::")
if len(dirExts) > 1 {
dir := strings.TrimSpace(dirExts[0])
exts := []string{}
for _, e := range strings.Split(dirExts[1], ",") {
cleanedExt := strings.TrimSpace(e)
if len(cleanedExt) > 0 {
exts = append(exts, cleanedExt)
}
}
if dir != "" && len(exts) > 0 {
return dir, exts
}
}
}
return "", nil
}
func getFileContents(name string) (string, error) {
fi, err := os.Stat(name)
if err != nil {
return "", err
}
if fi.Size() > 1048576 {
return "", fmt.Errorf("%#v is too big %v/1048576 bytes", name, fi.Size())
}
contents, err := ioutil.ReadFile(name)
if err != nil {
return "", err
}
return string(contents), nil
}

View File

@@ -1,9 +0,0 @@
// +build noportable
package cmd
import "github.com/drakkan/sftpgo/version"
func init() {
version.AddFeature("-portable")
}

View File

@@ -1,35 +0,0 @@
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/drakkan/sftpgo/service"
)
var (
reloadCmd = &cobra.Command{
Use: "reload",
Short: "Reload the SFTPGo Windows Service sending a \"paramchange\" request",
Run: func(cmd *cobra.Command, args []string) {
s := service.WindowsService{
Service: service.Service{
Shutdown: make(chan bool),
},
}
err := s.Reload()
if err != nil {
fmt.Printf("Error sending reload signal: %v\r\n", err)
os.Exit(1)
} else {
fmt.Printf("Reload signal sent!\r\n")
}
},
}
)
func init() {
serviceCmd.AddCommand(reloadCmd)
}

View File

@@ -1,35 +0,0 @@
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/drakkan/sftpgo/service"
)
var (
rotateLogCmd = &cobra.Command{
Use: "rotatelogs",
Short: "Signal to the running service to rotate the logs",
Run: func(cmd *cobra.Command, args []string) {
s := service.WindowsService{
Service: service.Service{
Shutdown: make(chan bool),
},
}
err := s.RotateLogFile()
if err != nil {
fmt.Printf("Error sending rotate log file signal to the service: %v\r\n", err)
os.Exit(1)
} else {
fmt.Printf("Rotate log file signal sent!\r\n")
}
},
}
)
func init() {
serviceCmd.AddCommand(rotateLogCmd)
}

View File

@@ -1,52 +0,0 @@
package cmd
import (
"os"
"github.com/spf13/cobra"
"github.com/drakkan/sftpgo/service"
"github.com/drakkan/sftpgo/utils"
)
var (
serveCmd = &cobra.Command{
Use: "serve",
Short: "Start the SFTP Server",
Long: `To start the SFTPGo with the default values for the command line flags simply
use:
$ sftpgo serve
Please take a look at the usage below to customize the startup options`,
Run: func(cmd *cobra.Command, args []string) {
service := service.Service{
ConfigDir: utils.CleanDirInput(configDir),
ConfigFile: configFile,
LogFilePath: logFilePath,
LogMaxSize: logMaxSize,
LogMaxBackups: logMaxBackups,
LogMaxAge: logMaxAge,
LogCompress: logCompress,
LogVerbose: logVerbose,
LoadDataFrom: loadDataFrom,
LoadDataMode: loadDataMode,
LoadDataQuotaScan: loadDataQuotaScan,
LoadDataClean: loadDataClean,
Shutdown: make(chan bool),
}
if err := service.Start(); err == nil {
service.Wait()
if service.Error == nil {
os.Exit(0)
}
}
os.Exit(1)
},
}
)
func init() {
rootCmd.AddCommand(serveCmd)
addServeFlags(serveCmd)
}

View File

@@ -1,16 +0,0 @@
package cmd
import (
"github.com/spf13/cobra"
)
var (
serviceCmd = &cobra.Command{
Use: "service",
Short: "Manage SFTPGo Windows Service",
}
)
func init() {
rootCmd.AddCommand(serviceCmd)
}

View File

@@ -1,51 +0,0 @@
package cmd
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/cobra"
"github.com/drakkan/sftpgo/service"
"github.com/drakkan/sftpgo/utils"
)
var (
startCmd = &cobra.Command{
Use: "start",
Short: "Start SFTPGo Windows Service",
Run: func(cmd *cobra.Command, args []string) {
configDir = utils.CleanDirInput(configDir)
if !filepath.IsAbs(logFilePath) && utils.IsFileInputValid(logFilePath) {
logFilePath = filepath.Join(configDir, logFilePath)
}
s := service.Service{
ConfigDir: configDir,
ConfigFile: configFile,
LogFilePath: logFilePath,
LogMaxSize: logMaxSize,
LogMaxBackups: logMaxBackups,
LogMaxAge: logMaxAge,
LogCompress: logCompress,
LogVerbose: logVerbose,
Shutdown: make(chan bool),
}
winService := service.WindowsService{
Service: s,
}
err := winService.RunService()
if err != nil {
fmt.Printf("Error starting service: %v\r\n", err)
os.Exit(1)
} else {
fmt.Printf("Service started!\r\n")
}
},
}
)
func init() {
serviceCmd.AddCommand(startCmd)
addServeFlags(startCmd)
}

View File

@@ -1,35 +0,0 @@
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/drakkan/sftpgo/service"
)
var (
statusCmd = &cobra.Command{
Use: "status",
Short: "Retrieve the status for the SFTPGo Windows Service",
Run: func(cmd *cobra.Command, args []string) {
s := service.WindowsService{
Service: service.Service{
Shutdown: make(chan bool),
},
}
status, err := s.Status()
if err != nil {
fmt.Printf("Error querying service status: %v\r\n", err)
os.Exit(1)
} else {
fmt.Printf("Service status: %#v\r\n", status.String())
}
},
}
)
func init() {
serviceCmd.AddCommand(statusCmd)
}

View File

@@ -1,35 +0,0 @@
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/drakkan/sftpgo/service"
)
var (
stopCmd = &cobra.Command{
Use: "stop",
Short: "Stop SFTPGo Windows Service",
Run: func(cmd *cobra.Command, args []string) {
s := service.WindowsService{
Service: service.Service{
Shutdown: make(chan bool),
},
}
err := s.Stop()
if err != nil {
fmt.Printf("Error stopping service: %v\r\n", err)
os.Exit(1)
} else {
fmt.Printf("Service stopped!\r\n")
}
},
}
)
func init() {
serviceCmd.AddCommand(stopCmd)
}

View File

@@ -1,35 +0,0 @@
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/drakkan/sftpgo/service"
)
var (
uninstallCmd = &cobra.Command{
Use: "uninstall",
Short: "Uninstall SFTPGo Windows Service",
Run: func(cmd *cobra.Command, args []string) {
s := service.WindowsService{
Service: service.Service{
Shutdown: make(chan bool),
},
}
err := s.Uninstall()
if err != nil {
fmt.Printf("Error removing service: %v\r\n", err)
os.Exit(1)
} else {
fmt.Printf("Service uninstalled\r\n")
}
},
}
)
func init() {
serviceCmd.AddCommand(uninstallCmd)
}

View File

@@ -1,205 +0,0 @@
package common
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/httpclient"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/utils"
)
var (
errUnconfiguredAction = errors.New("no hook is configured for this action")
errNoHook = errors.New("unable to execute action, no hook defined")
errUnexpectedHTTResponse = errors.New("unexpected HTTP response code")
)
// ProtocolActions defines the action to execute on file operations and SSH commands
type ProtocolActions struct {
// Valid values are download, upload, pre-delete, delete, rename, ssh_cmd. Empty slice to disable
ExecuteOn []string `json:"execute_on" mapstructure:"execute_on"`
// Absolute path to an external program or an HTTP URL
Hook string `json:"hook" mapstructure:"hook"`
}
var actionHandler ActionHandler = defaultActionHandler{}
// InitializeActionHandler lets the user choose an action handler implementation.
//
// Do NOT call this function after application initialization.
func InitializeActionHandler(handler ActionHandler) {
actionHandler = handler
}
// SSHCommandActionNotification executes the defined action for the specified SSH command.
func SSHCommandActionNotification(user *dataprovider.User, filePath, target, sshCmd string, err error) {
notification := newActionNotification(user, operationSSHCmd, filePath, target, sshCmd, ProtocolSSH, 0, err)
go actionHandler.Handle(notification) // nolint:errcheck
}
// ActionHandler handles a notification for a Protocol Action.
type ActionHandler interface {
Handle(notification ActionNotification) error
}
// ActionNotification defines a notification for a Protocol Action.
type ActionNotification struct {
Action string `json:"action"`
Username string `json:"username"`
Path string `json:"path"`
TargetPath string `json:"target_path,omitempty"`
SSHCmd string `json:"ssh_cmd,omitempty"`
FileSize int64 `json:"file_size,omitempty"`
FsProvider int `json:"fs_provider"`
Bucket string `json:"bucket,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
Status int `json:"status"`
Protocol string `json:"protocol"`
}
func newActionNotification(
user *dataprovider.User,
operation, filePath, target, sshCmd, protocol string,
fileSize int64,
err error,
) ActionNotification {
var bucket, endpoint string
status := 1
if user.FsConfig.Provider == dataprovider.S3FilesystemProvider {
bucket = user.FsConfig.S3Config.Bucket
endpoint = user.FsConfig.S3Config.Endpoint
} else if user.FsConfig.Provider == dataprovider.GCSFilesystemProvider {
bucket = user.FsConfig.GCSConfig.Bucket
} else if user.FsConfig.Provider == dataprovider.AzureBlobFilesystemProvider {
bucket = user.FsConfig.AzBlobConfig.Container
if user.FsConfig.AzBlobConfig.SASURL != "" {
endpoint = user.FsConfig.AzBlobConfig.SASURL
} else {
endpoint = user.FsConfig.AzBlobConfig.Endpoint
}
}
if err == ErrQuotaExceeded {
status = 2
} else if err != nil {
status = 0
}
return ActionNotification{
Action: operation,
Username: user.Username,
Path: filePath,
TargetPath: target,
SSHCmd: sshCmd,
FileSize: fileSize,
FsProvider: int(user.FsConfig.Provider),
Bucket: bucket,
Endpoint: endpoint,
Status: status,
Protocol: protocol,
}
}
type defaultActionHandler struct{}
func (h defaultActionHandler) Handle(notification ActionNotification) error {
if !utils.IsStringInSlice(notification.Action, Config.Actions.ExecuteOn) {
return errUnconfiguredAction
}
if Config.Actions.Hook == "" {
logger.Warn(notification.Protocol, "", "Unable to send notification, no hook is defined")
return errNoHook
}
if strings.HasPrefix(Config.Actions.Hook, "http") {
return h.handleHTTP(notification)
}
return h.handleCommand(notification)
}
func (h defaultActionHandler) handleHTTP(notification ActionNotification) error {
u, err := url.Parse(Config.Actions.Hook)
if err != nil {
logger.Warn(notification.Protocol, "", "Invalid hook %#v for operation %#v: %v", Config.Actions.Hook, notification.Action, err)
return err
}
startTime := time.Now()
respCode := 0
httpClient := httpclient.GetHTTPClient()
var b bytes.Buffer
_ = json.NewEncoder(&b).Encode(notification)
resp, err := httpClient.Post(u.String(), "application/json", &b)
if err == nil {
respCode = resp.StatusCode
resp.Body.Close()
if respCode != http.StatusOK {
err = errUnexpectedHTTResponse
}
}
logger.Debug(notification.Protocol, "", "notified operation %#v to URL: %v status code: %v, elapsed: %v err: %v", notification.Action, u.String(), respCode, time.Since(startTime), err)
return err
}
func (h defaultActionHandler) handleCommand(notification ActionNotification) error {
if !filepath.IsAbs(Config.Actions.Hook) {
err := fmt.Errorf("invalid notification command %#v", Config.Actions.Hook)
logger.Warn(notification.Protocol, "", "unable to execute notification command: %v", err)
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, Config.Actions.Hook, notification.Action, notification.Username, notification.Path, notification.TargetPath, notification.SSHCmd)
cmd.Env = append(os.Environ(), notificationAsEnvVars(notification)...)
startTime := time.Now()
err := cmd.Run()
logger.Debug(notification.Protocol, "", "executed command %#v with arguments: %#v, %#v, %#v, %#v, %#v, elapsed: %v, error: %v",
Config.Actions.Hook, notification.Action, notification.Username, notification.Path, notification.TargetPath, notification.SSHCmd, time.Since(startTime), err)
return err
}
func notificationAsEnvVars(notification ActionNotification) []string {
return []string{
fmt.Sprintf("SFTPGO_ACTION=%v", notification.Action),
fmt.Sprintf("SFTPGO_ACTION_USERNAME=%v", notification.Username),
fmt.Sprintf("SFTPGO_ACTION_PATH=%v", notification.Path),
fmt.Sprintf("SFTPGO_ACTION_TARGET=%v", notification.TargetPath),
fmt.Sprintf("SFTPGO_ACTION_SSH_CMD=%v", notification.SSHCmd),
fmt.Sprintf("SFTPGO_ACTION_FILE_SIZE=%v", notification.FileSize),
fmt.Sprintf("SFTPGO_ACTION_FS_PROVIDER=%v", notification.FsProvider),
fmt.Sprintf("SFTPGO_ACTION_BUCKET=%v", notification.Bucket),
fmt.Sprintf("SFTPGO_ACTION_ENDPOINT=%v", notification.Endpoint),
fmt.Sprintf("SFTPGO_ACTION_STATUS=%v", notification.Status),
fmt.Sprintf("SFTPGO_ACTION_PROTOCOL=%v", notification.Protocol),
}
}

View File

@@ -1,222 +0,0 @@
package common
import (
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/vfs"
)
func TestNewActionNotification(t *testing.T) {
user := &dataprovider.User{
Username: "username",
}
user.FsConfig.Provider = dataprovider.LocalFilesystemProvider
user.FsConfig.S3Config = vfs.S3FsConfig{
Bucket: "s3bucket",
Endpoint: "endpoint",
}
user.FsConfig.GCSConfig = vfs.GCSFsConfig{
Bucket: "gcsbucket",
}
user.FsConfig.AzBlobConfig = vfs.AzBlobFsConfig{
Container: "azcontainer",
SASURL: "azsasurl",
Endpoint: "azendpoint",
}
a := newActionNotification(user, operationDownload, "path", "target", "", ProtocolSFTP, 123, errors.New("fake error"))
assert.Equal(t, user.Username, a.Username)
assert.Equal(t, 0, len(a.Bucket))
assert.Equal(t, 0, len(a.Endpoint))
assert.Equal(t, 0, a.Status)
user.FsConfig.Provider = dataprovider.S3FilesystemProvider
a = newActionNotification(user, operationDownload, "path", "target", "", ProtocolSSH, 123, nil)
assert.Equal(t, "s3bucket", a.Bucket)
assert.Equal(t, "endpoint", a.Endpoint)
assert.Equal(t, 1, a.Status)
user.FsConfig.Provider = dataprovider.GCSFilesystemProvider
a = newActionNotification(user, operationDownload, "path", "target", "", ProtocolSCP, 123, ErrQuotaExceeded)
assert.Equal(t, "gcsbucket", a.Bucket)
assert.Equal(t, 0, len(a.Endpoint))
assert.Equal(t, 2, a.Status)
user.FsConfig.Provider = dataprovider.AzureBlobFilesystemProvider
a = newActionNotification(user, operationDownload, "path", "target", "", ProtocolSCP, 123, nil)
assert.Equal(t, "azcontainer", a.Bucket)
assert.Equal(t, "azsasurl", a.Endpoint)
assert.Equal(t, 1, a.Status)
user.FsConfig.AzBlobConfig.SASURL = ""
a = newActionNotification(user, operationDownload, "path", "target", "", ProtocolSCP, 123, nil)
assert.Equal(t, "azcontainer", a.Bucket)
assert.Equal(t, "azendpoint", a.Endpoint)
assert.Equal(t, 1, a.Status)
}
func TestActionHTTP(t *testing.T) {
actionsCopy := Config.Actions
Config.Actions = ProtocolActions{
ExecuteOn: []string{operationDownload},
Hook: fmt.Sprintf("http://%v", httpAddr),
}
user := &dataprovider.User{
Username: "username",
}
a := newActionNotification(user, operationDownload, "path", "target", "", ProtocolSFTP, 123, nil)
err := actionHandler.Handle(a)
assert.NoError(t, err)
Config.Actions.Hook = "http://invalid:1234"
err = actionHandler.Handle(a)
assert.Error(t, err)
Config.Actions.Hook = fmt.Sprintf("http://%v/404", httpAddr)
err = actionHandler.Handle(a)
if assert.Error(t, err) {
assert.EqualError(t, err, errUnexpectedHTTResponse.Error())
}
Config.Actions = actionsCopy
}
func TestActionCMD(t *testing.T) {
if runtime.GOOS == osWindows {
t.Skip("this test is not available on Windows")
}
actionsCopy := Config.Actions
hookCmd, err := exec.LookPath("true")
assert.NoError(t, err)
Config.Actions = ProtocolActions{
ExecuteOn: []string{operationDownload},
Hook: hookCmd,
}
user := &dataprovider.User{
Username: "username",
}
a := newActionNotification(user, operationDownload, "path", "target", "", ProtocolSFTP, 123, nil)
err = actionHandler.Handle(a)
assert.NoError(t, err)
SSHCommandActionNotification(user, "path", "target", "sha1sum", nil)
Config.Actions = actionsCopy
}
func TestWrongActions(t *testing.T) {
actionsCopy := Config.Actions
badCommand := "/bad/command"
if runtime.GOOS == osWindows {
badCommand = "C:\\bad\\command"
}
Config.Actions = ProtocolActions{
ExecuteOn: []string{operationUpload},
Hook: badCommand,
}
user := &dataprovider.User{
Username: "username",
}
a := newActionNotification(user, operationUpload, "", "", "", ProtocolSFTP, 123, nil)
err := actionHandler.Handle(a)
assert.Error(t, err, "action with bad command must fail")
a.Action = operationDelete
err = actionHandler.Handle(a)
assert.EqualError(t, err, errUnconfiguredAction.Error())
Config.Actions.Hook = "http://foo\x7f.com/"
a.Action = operationUpload
err = actionHandler.Handle(a)
assert.Error(t, err, "action with bad url must fail")
Config.Actions.Hook = ""
err = actionHandler.Handle(a)
if assert.Error(t, err) {
assert.EqualError(t, err, errNoHook.Error())
}
Config.Actions.Hook = "relative path"
err = actionHandler.Handle(a)
if assert.Error(t, err) {
assert.EqualError(t, err, fmt.Sprintf("invalid notification command %#v", Config.Actions.Hook))
}
Config.Actions = actionsCopy
}
func TestPreDeleteAction(t *testing.T) {
if runtime.GOOS == osWindows {
t.Skip("this test is not available on Windows")
}
actionsCopy := Config.Actions
hookCmd, err := exec.LookPath("true")
assert.NoError(t, err)
Config.Actions = ProtocolActions{
ExecuteOn: []string{operationPreDelete},
Hook: hookCmd,
}
homeDir := filepath.Join(os.TempDir(), "test_user")
err = os.MkdirAll(homeDir, os.ModePerm)
assert.NoError(t, err)
user := dataprovider.User{
Username: "username",
HomeDir: homeDir,
}
user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny}
fs := vfs.NewOsFs("id", homeDir, nil)
c := NewBaseConnection("id", ProtocolSFTP, user, fs)
testfile := filepath.Join(user.HomeDir, "testfile")
err = ioutil.WriteFile(testfile, []byte("test"), os.ModePerm)
assert.NoError(t, err)
info, err := os.Stat(testfile)
assert.NoError(t, err)
err = c.RemoveFile(testfile, "testfile", info)
assert.NoError(t, err)
assert.FileExists(t, testfile)
os.RemoveAll(homeDir)
Config.Actions = actionsCopy
}
type actionHandlerStub struct {
called bool
}
func (h *actionHandlerStub) Handle(notification ActionNotification) error {
h.called = true
return nil
}
func TestInitializeActionHandler(t *testing.T) {
handler := &actionHandlerStub{}
InitializeActionHandler(handler)
t.Cleanup(func() {
InitializeActionHandler(defaultActionHandler{})
})
err := actionHandler.Handle(ActionNotification{})
assert.NoError(t, err)
assert.True(t, handler.called)
}

View File

@@ -1,834 +0,0 @@
// Package common defines code shared among file transfer packages and protocols
package common
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/pires/go-proxyproto"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/httpclient"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/metrics"
"github.com/drakkan/sftpgo/utils"
)
// constants
const (
logSender = "common"
uploadLogSender = "Upload"
downloadLogSender = "Download"
renameLogSender = "Rename"
rmdirLogSender = "Rmdir"
mkdirLogSender = "Mkdir"
symlinkLogSender = "Symlink"
removeLogSender = "Remove"
chownLogSender = "Chown"
chmodLogSender = "Chmod"
chtimesLogSender = "Chtimes"
truncateLogSender = "Truncate"
operationDownload = "download"
operationUpload = "upload"
operationDelete = "delete"
operationPreDelete = "pre-delete"
operationRename = "rename"
operationSSHCmd = "ssh_cmd"
chtimesFormat = "2006-01-02T15:04:05" // YYYY-MM-DDTHH:MM:SS
idleTimeoutCheckInterval = 3 * time.Minute
)
// Stat flags
const (
StatAttrUIDGID = 1
StatAttrPerms = 2
StatAttrTimes = 4
StatAttrSize = 8
)
// Transfer types
const (
TransferUpload = iota
TransferDownload
)
// Supported protocols
const (
ProtocolSFTP = "SFTP"
ProtocolSCP = "SCP"
ProtocolSSH = "SSH"
ProtocolFTP = "FTP"
ProtocolWebDAV = "DAV"
)
// Upload modes
const (
UploadModeStandard = iota
UploadModeAtomic
UploadModeAtomicWithResume
)
// errors definitions
var (
ErrPermissionDenied = errors.New("permission denied")
ErrNotExist = errors.New("no such file or directory")
ErrOpUnsupported = errors.New("operation unsupported")
ErrGenericFailure = errors.New("failure")
ErrQuotaExceeded = errors.New("denying write due to space limit")
ErrSkipPermissionsCheck = errors.New("permission check skipped")
ErrConnectionDenied = errors.New("you are not allowed to connect")
ErrNoBinding = errors.New("no binding configured")
ErrCrtRevoked = errors.New("your certificate has been revoked")
errNoTransfer = errors.New("requested transfer not found")
errTransferMismatch = errors.New("transfer mismatch")
)
var (
// Config is the configuration for the supported protocols
Config Configuration
// Connections is the list of active connections
Connections ActiveConnections
// QuotaScans is the list of active quota scans
QuotaScans ActiveScans
idleTimeoutTicker *time.Ticker
idleTimeoutTickerDone chan bool
supportedProtocols = []string{ProtocolSFTP, ProtocolSCP, ProtocolSSH, ProtocolFTP, ProtocolWebDAV}
)
// Initialize sets the common configuration
func Initialize(c Configuration) error {
Config = c
Config.idleLoginTimeout = 2 * time.Minute
Config.idleTimeoutAsDuration = time.Duration(Config.IdleTimeout) * time.Minute
if Config.IdleTimeout > 0 {
startIdleTimeoutTicker(idleTimeoutCheckInterval)
}
Config.defender = nil
if c.DefenderConfig.Enabled {
defender, err := newInMemoryDefender(&c.DefenderConfig)
if err != nil {
return fmt.Errorf("defender initialization error: %v", err)
}
logger.Info(logSender, "", "defender initialized with config %+v", c.DefenderConfig)
Config.defender = defender
}
return nil
}
// ReloadDefender reloads the defender's block and safe lists
func ReloadDefender() error {
if Config.defender == nil {
return nil
}
return Config.defender.Reload()
}
// IsBanned returns true if the specified IP address is banned
func IsBanned(ip string) bool {
if Config.defender == nil {
return false
}
return Config.defender.IsBanned(ip)
}
// GetDefenderBanTime returns the ban time for the given IP
// or nil if the IP is not banned or the defender is disabled
func GetDefenderBanTime(ip string) *time.Time {
if Config.defender == nil {
return nil
}
return Config.defender.GetBanTime(ip)
}
// Unban removes the specified IP address from the banned ones
func Unban(ip string) bool {
if Config.defender == nil {
return false
}
return Config.defender.Unban(ip)
}
// GetDefenderScore returns the score for the given IP
func GetDefenderScore(ip string) int {
if Config.defender == nil {
return 0
}
return Config.defender.GetScore(ip)
}
// AddDefenderEvent adds the specified defender event for the given IP
func AddDefenderEvent(ip string, event HostEvent) {
if Config.defender == nil {
return
}
Config.defender.AddEvent(ip, event)
}
// the ticker cannot be started/stopped from multiple goroutines
func startIdleTimeoutTicker(duration time.Duration) {
stopIdleTimeoutTicker()
idleTimeoutTicker = time.NewTicker(duration)
idleTimeoutTickerDone = make(chan bool)
go func() {
for {
select {
case <-idleTimeoutTickerDone:
return
case <-idleTimeoutTicker.C:
Connections.checkIdles()
}
}
}()
}
func stopIdleTimeoutTicker() {
if idleTimeoutTicker != nil {
idleTimeoutTicker.Stop()
idleTimeoutTickerDone <- true
idleTimeoutTicker = nil
}
}
// ActiveTransfer defines the interface for the current active transfers
type ActiveTransfer interface {
GetID() uint64
GetType() int
GetSize() int64
GetVirtualPath() string
GetStartTime() time.Time
SignalClose()
Truncate(fsPath string, size int64) (int64, error)
GetRealFsPath(fsPath string) string
}
// ActiveConnection defines the interface for the current active connections
type ActiveConnection interface {
GetID() string
GetUsername() string
GetRemoteAddress() string
GetClientVersion() string
GetProtocol() string
GetConnectionTime() time.Time
GetLastActivity() time.Time
GetCommand() string
Disconnect() error
AddTransfer(t ActiveTransfer)
RemoveTransfer(t ActiveTransfer)
GetTransfers() []ConnectionTransfer
CloseFS() error
}
// StatAttributes defines the attributes for set stat commands
type StatAttributes struct {
Mode os.FileMode
Atime time.Time
Mtime time.Time
UID int
GID int
Flags int
Size int64
}
// ConnectionTransfer defines the trasfer details to expose
type ConnectionTransfer struct {
ID uint64 `json:"-"`
OperationType string `json:"operation_type"`
StartTime int64 `json:"start_time"`
Size int64 `json:"size"`
VirtualPath string `json:"path"`
}
func (t *ConnectionTransfer) getConnectionTransferAsString() string {
result := ""
switch t.OperationType {
case operationUpload:
result += "UL "
case operationDownload:
result += "DL "
}
result += fmt.Sprintf("%#v ", t.VirtualPath)
if t.Size > 0 {
elapsed := time.Since(utils.GetTimeFromMsecSinceEpoch(t.StartTime))
speed := float64(t.Size) / float64(utils.GetTimeAsMsSinceEpoch(time.Now())-t.StartTime)
result += fmt.Sprintf("Size: %#v Elapsed: %#v Speed: \"%.1f KB/s\"", utils.ByteCountSI(t.Size),
utils.GetDurationAsString(elapsed), speed)
}
return result
}
// Configuration defines configuration parameters common to all supported protocols
type Configuration struct {
// Maximum idle timeout as minutes. If a client is idle for a time that exceeds this setting it will be disconnected.
// 0 means disabled
IdleTimeout int `json:"idle_timeout" mapstructure:"idle_timeout"`
// UploadMode 0 means standard, the files are uploaded directly to the requested path.
// 1 means atomic: the files are uploaded to a temporary path and renamed to the requested path
// when the client ends the upload. Atomic mode avoid problems such as a web server that
// serves partial files when the files are being uploaded.
// In atomic mode if there is an upload error the temporary file is deleted and so the requested
// upload path will not contain a partial file.
// 2 means atomic with resume support: as atomic but if there is an upload error the temporary
// file is renamed to the requested path and not deleted, this way a client can reconnect and resume
// the upload.
UploadMode int `json:"upload_mode" mapstructure:"upload_mode"`
// Actions to execute for SFTP file operations and SSH commands
Actions ProtocolActions `json:"actions" mapstructure:"actions"`
// SetstatMode 0 means "normal mode": requests for changing permissions and owner/group are executed.
// 1 means "ignore mode": requests for changing permissions and owner/group are silently ignored.
// 2 means "ignore mode for cloud fs": requests for changing permissions and owner/group/time are
// silently ignored for cloud based filesystem such as S3, GCS, Azure Blob
SetstatMode int `json:"setstat_mode" mapstructure:"setstat_mode"`
// Support for HAProxy PROXY protocol.
// If you are running SFTPGo behind a proxy server such as HAProxy, AWS ELB or NGNIX, you can enable
// the proxy protocol. It provides a convenient way to safely transport connection information
// such as a client's address across multiple layers of NAT or TCP proxies to get the real
// client IP address instead of the proxy IP. Both protocol versions 1 and 2 are supported.
// - 0 means disabled
// - 1 means proxy protocol enabled. Proxy header will be used and requests without proxy header will be accepted.
// - 2 means proxy protocol required. Proxy header will be used and requests without proxy header will be rejected.
// If the proxy protocol is enabled in SFTPGo then you have to enable the protocol in your proxy configuration too,
// for example for HAProxy add "send-proxy" or "send-proxy-v2" to each server configuration line.
ProxyProtocol int `json:"proxy_protocol" mapstructure:"proxy_protocol"`
// List of IP addresses and IP ranges allowed to send the proxy header.
// If proxy protocol is set to 1 and we receive a proxy header from an IP that is not in the list then the
// connection will be accepted and the header will be ignored.
// If proxy protocol is set to 2 and we receive a proxy header from an IP that is not in the list then the
// connection will be rejected.
ProxyAllowed []string `json:"proxy_allowed" mapstructure:"proxy_allowed"`
// Absolute path to an external program or an HTTP URL to invoke after a user connects
// and before he tries to login. It allows you to reject the connection based on the source
// ip address. Leave empty do disable.
PostConnectHook string `json:"post_connect_hook" mapstructure:"post_connect_hook"`
// Maximum number of concurrent client connections. 0 means unlimited
MaxTotalConnections int `json:"max_total_connections" mapstructure:"max_total_connections"`
// Defender configuration
DefenderConfig DefenderConfig `json:"defender" mapstructure:"defender"`
idleTimeoutAsDuration time.Duration
idleLoginTimeout time.Duration
defender Defender
}
// IsAtomicUploadEnabled returns true if atomic upload is enabled
func (c *Configuration) IsAtomicUploadEnabled() bool {
return c.UploadMode == UploadModeAtomic || c.UploadMode == UploadModeAtomicWithResume
}
// GetProxyListener returns a wrapper for the given listener that supports the
// HAProxy Proxy Protocol or nil if the proxy protocol is not configured
func (c *Configuration) GetProxyListener(listener net.Listener) (*proxyproto.Listener, error) {
var proxyListener *proxyproto.Listener
var err error
if c.ProxyProtocol > 0 {
var policyFunc func(upstream net.Addr) (proxyproto.Policy, error)
if c.ProxyProtocol == 1 && len(c.ProxyAllowed) > 0 {
policyFunc, err = proxyproto.LaxWhiteListPolicy(c.ProxyAllowed)
if err != nil {
return nil, err
}
}
if c.ProxyProtocol == 2 {
if len(c.ProxyAllowed) == 0 {
policyFunc = func(upstream net.Addr) (proxyproto.Policy, error) {
return proxyproto.REQUIRE, nil
}
} else {
policyFunc, err = proxyproto.StrictWhiteListPolicy(c.ProxyAllowed)
if err != nil {
return nil, err
}
}
}
proxyListener = &proxyproto.Listener{
Listener: listener,
Policy: policyFunc,
}
}
return proxyListener, nil
}
// ExecutePostConnectHook executes the post connect hook if defined
func (c *Configuration) ExecutePostConnectHook(ipAddr, protocol string) error {
if c.PostConnectHook == "" {
return nil
}
if strings.HasPrefix(c.PostConnectHook, "http") {
var url *url.URL
url, err := url.Parse(c.PostConnectHook)
if err != nil {
logger.Warn(protocol, "", "Login from ip %#v denied, invalid post connect hook %#v: %v",
ipAddr, c.PostConnectHook, err)
return err
}
httpClient := httpclient.GetHTTPClient()
q := url.Query()
q.Add("ip", ipAddr)
q.Add("protocol", protocol)
url.RawQuery = q.Encode()
resp, err := httpClient.Get(url.String())
if err != nil {
logger.Warn(protocol, "", "Login from ip %#v denied, error executing post connect hook: %v", ipAddr, err)
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
logger.Warn(protocol, "", "Login from ip %#v denied, post connect hook response code: %v", ipAddr, resp.StatusCode)
return errUnexpectedHTTResponse
}
return nil
}
if !filepath.IsAbs(c.PostConnectHook) {
err := fmt.Errorf("invalid post connect hook %#v", c.PostConnectHook)
logger.Warn(protocol, "", "Login from ip %#v denied: %v", ipAddr, err)
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, c.PostConnectHook)
cmd.Env = append(os.Environ(),
fmt.Sprintf("SFTPGO_CONNECTION_IP=%v", ipAddr),
fmt.Sprintf("SFTPGO_CONNECTION_PROTOCOL=%v", protocol))
err := cmd.Run()
if err != nil {
logger.Warn(protocol, "", "Login from ip %#v denied, connect hook error: %v", ipAddr, err)
}
return err
}
// SSHConnection defines an ssh connection.
// Each SSH connection can open several channels for SFTP or SSH commands
type SSHConnection struct {
id string
conn net.Conn
lastActivity int64
}
// NewSSHConnection returns a new SSHConnection
func NewSSHConnection(id string, conn net.Conn) *SSHConnection {
return &SSHConnection{
id: id,
conn: conn,
lastActivity: time.Now().UnixNano(),
}
}
// GetID returns the ID for this SSHConnection
func (c *SSHConnection) GetID() string {
return c.id
}
// UpdateLastActivity updates last activity for this connection
func (c *SSHConnection) UpdateLastActivity() {
atomic.StoreInt64(&c.lastActivity, time.Now().UnixNano())
}
// GetLastActivity returns the last connection activity
func (c *SSHConnection) GetLastActivity() time.Time {
return time.Unix(0, atomic.LoadInt64(&c.lastActivity))
}
// Close closes the underlying network connection
func (c *SSHConnection) Close() error {
return c.conn.Close()
}
// ActiveConnections holds the currect active connections with the associated transfers
type ActiveConnections struct {
sync.RWMutex
connections []ActiveConnection
sshConnections []*SSHConnection
}
// GetActiveSessions returns the number of active sessions for the given username.
// We return the open sessions for any protocol
func (conns *ActiveConnections) GetActiveSessions(username string) int {
conns.RLock()
defer conns.RUnlock()
numSessions := 0
for _, c := range conns.connections {
if c.GetUsername() == username {
numSessions++
}
}
return numSessions
}
// Add adds a new connection to the active ones
func (conns *ActiveConnections) Add(c ActiveConnection) {
conns.Lock()
defer conns.Unlock()
conns.connections = append(conns.connections, c)
metrics.UpdateActiveConnectionsSize(len(conns.connections))
logger.Debug(c.GetProtocol(), c.GetID(), "connection added, num open connections: %v", len(conns.connections))
}
// Swap replaces an existing connection with the given one.
// This method is useful if you have to change some connection details
// for example for FTP is used to update the connection once the user
// authenticates
func (conns *ActiveConnections) Swap(c ActiveConnection) error {
conns.Lock()
defer conns.Unlock()
for idx, conn := range conns.connections {
if conn.GetID() == c.GetID() {
conn = nil
conns.connections[idx] = c
return nil
}
}
return errors.New("connection to swap not found")
}
// Remove removes a connection from the active ones
func (conns *ActiveConnections) Remove(connectionID string) {
conns.Lock()
defer conns.Unlock()
for idx, conn := range conns.connections {
if conn.GetID() == connectionID {
err := conn.CloseFS()
lastIdx := len(conns.connections) - 1
conns.connections[idx] = conns.connections[lastIdx]
conns.connections[lastIdx] = nil
conns.connections = conns.connections[:lastIdx]
metrics.UpdateActiveConnectionsSize(lastIdx)
logger.Debug(conn.GetProtocol(), conn.GetID(), "connection removed, close fs error: %v, num open connections: %v",
err, lastIdx)
return
}
}
logger.Warn(logSender, "", "connection id %#v to remove not found!", connectionID)
}
// Close closes an active connection.
// It returns true on success
func (conns *ActiveConnections) Close(connectionID string) bool {
conns.RLock()
result := false
for _, c := range conns.connections {
if c.GetID() == connectionID {
defer func(conn ActiveConnection) {
err := conn.Disconnect()
logger.Debug(conn.GetProtocol(), conn.GetID(), "close connection requested, close err: %v", err)
}(c)
result = true
break
}
}
conns.RUnlock()
return result
}
// AddSSHConnection adds a new ssh connection to the active ones
func (conns *ActiveConnections) AddSSHConnection(c *SSHConnection) {
conns.Lock()
defer conns.Unlock()
conns.sshConnections = append(conns.sshConnections, c)
logger.Debug(logSender, c.GetID(), "ssh connection added, num open connections: %v", len(conns.sshConnections))
}
// RemoveSSHConnection removes a connection from the active ones
func (conns *ActiveConnections) RemoveSSHConnection(connectionID string) {
conns.Lock()
defer conns.Unlock()
for idx, conn := range conns.sshConnections {
if conn.GetID() == connectionID {
lastIdx := len(conns.sshConnections) - 1
conns.sshConnections[idx] = conns.sshConnections[lastIdx]
conns.sshConnections[lastIdx] = nil
conns.sshConnections = conns.sshConnections[:lastIdx]
logger.Debug(logSender, conn.GetID(), "ssh connection removed, num open ssh connections: %v", lastIdx)
return
}
}
logger.Warn(logSender, "", "ssh connection to remove with id %#v not found!", connectionID)
}
func (conns *ActiveConnections) checkIdles() {
conns.RLock()
for _, sshConn := range conns.sshConnections {
idleTime := time.Since(sshConn.GetLastActivity())
if idleTime > Config.idleTimeoutAsDuration {
// we close the an ssh connection if it has no active connections associated
idToMatch := fmt.Sprintf("_%v_", sshConn.GetID())
toClose := true
for _, conn := range conns.connections {
if strings.Contains(conn.GetID(), idToMatch) {
toClose = false
break
}
}
if toClose {
defer func(c *SSHConnection) {
err := c.Close()
logger.Debug(logSender, c.GetID(), "close idle SSH connection, idle time: %v, close err: %v",
time.Since(c.GetLastActivity()), err)
}(sshConn)
}
}
}
for _, c := range conns.connections {
idleTime := time.Since(c.GetLastActivity())
isUnauthenticatedFTPUser := (c.GetProtocol() == ProtocolFTP && c.GetUsername() == "")
if idleTime > Config.idleTimeoutAsDuration || (isUnauthenticatedFTPUser && idleTime > Config.idleLoginTimeout) {
defer func(conn ActiveConnection, isFTPNoAuth bool) {
err := conn.Disconnect()
logger.Debug(conn.GetProtocol(), conn.GetID(), "close idle connection, idle time: %v, username: %#v close err: %v",
time.Since(conn.GetLastActivity()), conn.GetUsername(), err)
if isFTPNoAuth {
ip := utils.GetIPFromRemoteAddress(c.GetRemoteAddress())
logger.ConnectionFailedLog("", ip, dataprovider.LoginMethodNoAuthTryed, c.GetProtocol(), "client idle")
metrics.AddNoAuthTryed()
AddDefenderEvent(ip, HostEventNoLoginTried)
dataprovider.ExecutePostLoginHook(&dataprovider.User{}, dataprovider.LoginMethodNoAuthTryed, ip, c.GetProtocol(),
dataprovider.ErrNoAuthTryed)
}
}(c, isUnauthenticatedFTPUser)
}
}
conns.RUnlock()
}
// IsNewConnectionAllowed returns false if the maximum number of concurrent allowed connections is exceeded
func (conns *ActiveConnections) IsNewConnectionAllowed() bool {
if Config.MaxTotalConnections == 0 {
return true
}
conns.RLock()
defer conns.RUnlock()
return len(conns.connections) < Config.MaxTotalConnections
}
// GetStats returns stats for active connections
func (conns *ActiveConnections) GetStats() []ConnectionStatus {
conns.RLock()
defer conns.RUnlock()
stats := make([]ConnectionStatus, 0, len(conns.connections))
for _, c := range conns.connections {
stat := ConnectionStatus{
Username: c.GetUsername(),
ConnectionID: c.GetID(),
ClientVersion: c.GetClientVersion(),
RemoteAddress: c.GetRemoteAddress(),
ConnectionTime: utils.GetTimeAsMsSinceEpoch(c.GetConnectionTime()),
LastActivity: utils.GetTimeAsMsSinceEpoch(c.GetLastActivity()),
Protocol: c.GetProtocol(),
Command: c.GetCommand(),
Transfers: c.GetTransfers(),
}
stats = append(stats, stat)
}
return stats
}
// ConnectionStatus returns the status for an active connection
type ConnectionStatus struct {
// Logged in username
Username string `json:"username"`
// Unique identifier for the connection
ConnectionID string `json:"connection_id"`
// client's version string
ClientVersion string `json:"client_version,omitempty"`
// Remote address for this connection
RemoteAddress string `json:"remote_address"`
// Connection time as unix timestamp in milliseconds
ConnectionTime int64 `json:"connection_time"`
// Last activity as unix timestamp in milliseconds
LastActivity int64 `json:"last_activity"`
// Protocol for this connection
Protocol string `json:"protocol"`
// active uploads/downloads
Transfers []ConnectionTransfer `json:"active_transfers,omitempty"`
// SSH command or WebDAV method
Command string `json:"command,omitempty"`
}
// GetConnectionDuration returns the connection duration as string
func (c ConnectionStatus) GetConnectionDuration() string {
elapsed := time.Since(utils.GetTimeFromMsecSinceEpoch(c.ConnectionTime))
return utils.GetDurationAsString(elapsed)
}
// GetConnectionInfo returns connection info.
// Protocol,Client Version and RemoteAddress are returned.
func (c ConnectionStatus) GetConnectionInfo() string {
var result strings.Builder
result.WriteString(fmt.Sprintf("%v. Client: %#v From: %#v", c.Protocol, c.ClientVersion, c.RemoteAddress))
if c.Command == "" {
return result.String()
}
switch c.Protocol {
case ProtocolSSH, ProtocolFTP:
result.WriteString(fmt.Sprintf(". Command: %#v", c.Command))
case ProtocolWebDAV:
result.WriteString(fmt.Sprintf(". Method: %#v", c.Command))
}
return result.String()
}
// GetTransfersAsString returns the active transfers as string
func (c ConnectionStatus) GetTransfersAsString() string {
result := ""
for _, t := range c.Transfers {
if len(result) > 0 {
result += ". "
}
result += t.getConnectionTransferAsString()
}
return result
}
// ActiveQuotaScan defines an active quota scan for a user home dir
type ActiveQuotaScan struct {
// Username to which the quota scan refers
Username string `json:"username"`
// quota scan start time as unix timestamp in milliseconds
StartTime int64 `json:"start_time"`
}
// ActiveVirtualFolderQuotaScan defines an active quota scan for a virtual folder
type ActiveVirtualFolderQuotaScan struct {
// folder name to which the quota scan refers
Name string `json:"name"`
// quota scan start time as unix timestamp in milliseconds
StartTime int64 `json:"start_time"`
}
// ActiveScans holds the active quota scans
type ActiveScans struct {
sync.RWMutex
UserHomeScans []ActiveQuotaScan
FolderScans []ActiveVirtualFolderQuotaScan
}
// GetUsersQuotaScans returns the active quota scans for users home directories
func (s *ActiveScans) GetUsersQuotaScans() []ActiveQuotaScan {
s.RLock()
defer s.RUnlock()
scans := make([]ActiveQuotaScan, len(s.UserHomeScans))
copy(scans, s.UserHomeScans)
return scans
}
// AddUserQuotaScan adds a user to the ones with active quota scans.
// Returns false if the user has a quota scan already running
func (s *ActiveScans) AddUserQuotaScan(username string) bool {
s.Lock()
defer s.Unlock()
for _, scan := range s.UserHomeScans {
if scan.Username == username {
return false
}
}
s.UserHomeScans = append(s.UserHomeScans, ActiveQuotaScan{
Username: username,
StartTime: utils.GetTimeAsMsSinceEpoch(time.Now()),
})
return true
}
// RemoveUserQuotaScan removes a user from the ones with active quota scans.
// Returns false if the user has no active quota scans
func (s *ActiveScans) RemoveUserQuotaScan(username string) bool {
s.Lock()
defer s.Unlock()
indexToRemove := -1
for i, scan := range s.UserHomeScans {
if scan.Username == username {
indexToRemove = i
break
}
}
if indexToRemove >= 0 {
s.UserHomeScans[indexToRemove] = s.UserHomeScans[len(s.UserHomeScans)-1]
s.UserHomeScans = s.UserHomeScans[:len(s.UserHomeScans)-1]
return true
}
return false
}
// GetVFoldersQuotaScans returns the active quota scans for virtual folders
func (s *ActiveScans) GetVFoldersQuotaScans() []ActiveVirtualFolderQuotaScan {
s.RLock()
defer s.RUnlock()
scans := make([]ActiveVirtualFolderQuotaScan, len(s.FolderScans))
copy(scans, s.FolderScans)
return scans
}
// AddVFolderQuotaScan adds a virtual folder to the ones with active quota scans.
// Returns false if the folder has a quota scan already running
func (s *ActiveScans) AddVFolderQuotaScan(folderName string) bool {
s.Lock()
defer s.Unlock()
for _, scan := range s.FolderScans {
if scan.Name == folderName {
return false
}
}
s.FolderScans = append(s.FolderScans, ActiveVirtualFolderQuotaScan{
Name: folderName,
StartTime: utils.GetTimeAsMsSinceEpoch(time.Now()),
})
return true
}
// RemoveVFolderQuotaScan removes a folder from the ones with active quota scans.
// Returns false if the folder has no active quota scans
func (s *ActiveScans) RemoveVFolderQuotaScan(folderName string) bool {
s.Lock()
defer s.Unlock()
indexToRemove := -1
for i, scan := range s.FolderScans {
if scan.Name == folderName {
indexToRemove = i
break
}
}
if indexToRemove >= 0 {
s.FolderScans[indexToRemove] = s.FolderScans[len(s.FolderScans)-1]
s.FolderScans = s.FolderScans[:len(s.FolderScans)-1]
return true
}
return false
}

View File

@@ -1,652 +0,0 @@
package common
import (
"fmt"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/rs/zerolog"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/httpclient"
"github.com/drakkan/sftpgo/kms"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/utils"
"github.com/drakkan/sftpgo/vfs"
)
const (
logSenderTest = "common_test"
httpAddr = "127.0.0.1:9999"
httpProxyAddr = "127.0.0.1:7777"
configDir = ".."
osWindows = "windows"
userTestUsername = "common_test_username"
userTestPwd = "common_test_pwd"
)
type providerConf struct {
Config dataprovider.Config `json:"data_provider" mapstructure:"data_provider"`
}
type fakeConnection struct {
*BaseConnection
command string
}
func (c *fakeConnection) AddUser(user dataprovider.User) error {
fs, err := user.GetFilesystem(c.GetID())
if err != nil {
return err
}
c.BaseConnection.User = user
c.BaseConnection.Fs = fs
return nil
}
func (c *fakeConnection) Disconnect() error {
Connections.Remove(c.GetID())
return nil
}
func (c *fakeConnection) GetClientVersion() string {
return ""
}
func (c *fakeConnection) GetCommand() string {
return c.command
}
func (c *fakeConnection) GetRemoteAddress() string {
return ""
}
type customNetConn struct {
net.Conn
id string
isClosed bool
}
func (c *customNetConn) Close() error {
Connections.RemoveSSHConnection(c.id)
c.isClosed = true
return c.Conn.Close()
}
func TestMain(m *testing.M) {
logfilePath := "common_test.log"
logger.InitLogger(logfilePath, 5, 1, 28, false, zerolog.DebugLevel)
viper.SetEnvPrefix("sftpgo")
replacer := strings.NewReplacer(".", "__")
viper.SetEnvKeyReplacer(replacer)
viper.SetConfigName("sftpgo")
viper.AutomaticEnv()
viper.AllowEmptyEnv(true)
driver, err := initializeDataprovider(-1)
if err != nil {
logger.WarnToConsole("error initializing data provider: %v", err)
os.Exit(1)
}
logger.InfoToConsole("Starting COMMON tests, provider: %v", driver)
err = Initialize(Configuration{})
if err != nil {
logger.WarnToConsole("error initializing common: %v", err)
os.Exit(1)
}
httpConfig := httpclient.Config{
Timeout: 5,
}
httpConfig.Initialize(configDir)
go func() {
// start a test HTTP server to receive action notifications
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "OK\n")
})
http.HandleFunc("/404", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "Not found\n")
})
if err := http.ListenAndServe(httpAddr, nil); err != nil {
logger.ErrorToConsole("could not start HTTP notification server: %v", err)
os.Exit(1)
}
}()
go func() {
Config.ProxyProtocol = 2
listener, err := net.Listen("tcp", httpProxyAddr)
if err != nil {
logger.ErrorToConsole("error creating listener for proxy protocol server: %v", err)
os.Exit(1)
}
proxyListener, err := Config.GetProxyListener(listener)
if err != nil {
logger.ErrorToConsole("error creating proxy protocol listener: %v", err)
os.Exit(1)
}
Config.ProxyProtocol = 0
s := &http.Server{}
if err := s.Serve(proxyListener); err != nil {
logger.ErrorToConsole("could not start HTTP proxy protocol server: %v", err)
os.Exit(1)
}
}()
waitTCPListening(httpAddr)
waitTCPListening(httpProxyAddr)
exitCode := m.Run()
os.Remove(logfilePath) //nolint:errcheck
os.Exit(exitCode)
}
func waitTCPListening(address string) {
for {
conn, err := net.Dial("tcp", address)
if err != nil {
logger.WarnToConsole("tcp server %v not listening: %v\n", address, err)
time.Sleep(100 * time.Millisecond)
continue
}
logger.InfoToConsole("tcp server %v now listening\n", address)
conn.Close()
break
}
}
func initializeDataprovider(trackQuota int) (string, error) {
configDir := ".."
viper.AddConfigPath(configDir)
if err := viper.ReadInConfig(); err != nil {
return "", err
}
var cfg providerConf
if err := viper.Unmarshal(&cfg); err != nil {
return "", err
}
if trackQuota >= 0 && trackQuota <= 2 {
cfg.Config.TrackQuota = trackQuota
}
return cfg.Config.Driver, dataprovider.Initialize(cfg.Config, configDir, true)
}
func closeDataprovider() error {
return dataprovider.Close()
}
func TestSSHConnections(t *testing.T) {
conn1, conn2 := net.Pipe()
now := time.Now()
sshConn1 := NewSSHConnection("id1", conn1)
sshConn2 := NewSSHConnection("id2", conn2)
sshConn3 := NewSSHConnection("id3", conn2)
assert.Equal(t, "id1", sshConn1.GetID())
assert.Equal(t, "id2", sshConn2.GetID())
assert.Equal(t, "id3", sshConn3.GetID())
sshConn1.UpdateLastActivity()
assert.GreaterOrEqual(t, sshConn1.GetLastActivity().UnixNano(), now.UnixNano())
Connections.AddSSHConnection(sshConn1)
Connections.AddSSHConnection(sshConn2)
Connections.AddSSHConnection(sshConn3)
Connections.RLock()
assert.Len(t, Connections.sshConnections, 3)
Connections.RUnlock()
Connections.RemoveSSHConnection(sshConn1.id)
Connections.RLock()
assert.Len(t, Connections.sshConnections, 2)
assert.Equal(t, sshConn3.id, Connections.sshConnections[0].id)
assert.Equal(t, sshConn2.id, Connections.sshConnections[1].id)
Connections.RUnlock()
Connections.RemoveSSHConnection(sshConn1.id)
Connections.RLock()
assert.Len(t, Connections.sshConnections, 2)
assert.Equal(t, sshConn3.id, Connections.sshConnections[0].id)
assert.Equal(t, sshConn2.id, Connections.sshConnections[1].id)
Connections.RUnlock()
Connections.RemoveSSHConnection(sshConn2.id)
Connections.RLock()
assert.Len(t, Connections.sshConnections, 1)
assert.Equal(t, sshConn3.id, Connections.sshConnections[0].id)
Connections.RUnlock()
Connections.RemoveSSHConnection(sshConn3.id)
Connections.RLock()
assert.Len(t, Connections.sshConnections, 0)
Connections.RUnlock()
assert.NoError(t, sshConn1.Close())
assert.NoError(t, sshConn2.Close())
assert.NoError(t, sshConn3.Close())
}
func TestDefenderIntegration(t *testing.T) {
// by default defender is nil
configCopy := Config
ip := "127.1.1.1"
assert.Nil(t, ReloadDefender())
AddDefenderEvent(ip, HostEventNoLoginTried)
assert.False(t, IsBanned(ip))
assert.Nil(t, GetDefenderBanTime(ip))
assert.False(t, Unban(ip))
assert.Equal(t, 0, GetDefenderScore(ip))
Config.DefenderConfig = DefenderConfig{
Enabled: true,
BanTime: 10,
BanTimeIncrement: 50,
Threshold: 0,
ScoreInvalid: 2,
ScoreValid: 1,
ObservationTime: 15,
EntriesSoftLimit: 100,
EntriesHardLimit: 150,
}
err := Initialize(Config)
assert.Error(t, err)
Config.DefenderConfig.Threshold = 3
err = Initialize(Config)
assert.NoError(t, err)
assert.Nil(t, ReloadDefender())
AddDefenderEvent(ip, HostEventNoLoginTried)
assert.False(t, IsBanned(ip))
assert.Equal(t, 2, GetDefenderScore(ip))
assert.False(t, Unban(ip))
assert.Nil(t, GetDefenderBanTime(ip))
AddDefenderEvent(ip, HostEventLoginFailed)
assert.True(t, IsBanned(ip))
assert.Equal(t, 0, GetDefenderScore(ip))
assert.NotNil(t, GetDefenderBanTime(ip))
assert.True(t, Unban(ip))
assert.Nil(t, GetDefenderBanTime(ip))
assert.False(t, Unban(ip))
Config = configCopy
}
func TestMaxConnections(t *testing.T) {
oldValue := Config.MaxTotalConnections
Config.MaxTotalConnections = 1
assert.True(t, Connections.IsNewConnectionAllowed())
c := NewBaseConnection("id", ProtocolSFTP, dataprovider.User{}, nil)
fakeConn := &fakeConnection{
BaseConnection: c,
}
Connections.Add(fakeConn)
assert.Len(t, Connections.GetStats(), 1)
assert.False(t, Connections.IsNewConnectionAllowed())
res := Connections.Close(fakeConn.GetID())
assert.True(t, res)
assert.Eventually(t, func() bool { return len(Connections.GetStats()) == 0 }, 300*time.Millisecond, 50*time.Millisecond)
Config.MaxTotalConnections = oldValue
}
func TestIdleConnections(t *testing.T) {
configCopy := Config
Config.IdleTimeout = 1
err := Initialize(Config)
assert.NoError(t, err)
conn1, conn2 := net.Pipe()
customConn1 := &customNetConn{
Conn: conn1,
id: "id1",
}
customConn2 := &customNetConn{
Conn: conn2,
id: "id2",
}
sshConn1 := NewSSHConnection(customConn1.id, customConn1)
sshConn2 := NewSSHConnection(customConn2.id, customConn2)
username := "test_user"
user := dataprovider.User{
Username: username,
}
c := NewBaseConnection(sshConn1.id+"_1", ProtocolSFTP, user, nil)
c.lastActivity = time.Now().Add(-24 * time.Hour).UnixNano()
fakeConn := &fakeConnection{
BaseConnection: c,
}
// both ssh connections are expired but they should get removed only
// if there is no associated connection
sshConn1.lastActivity = c.lastActivity
sshConn2.lastActivity = c.lastActivity
Connections.AddSSHConnection(sshConn1)
Connections.Add(fakeConn)
assert.Equal(t, Connections.GetActiveSessions(username), 1)
c = NewBaseConnection(sshConn2.id+"_1", ProtocolSSH, user, nil)
fakeConn = &fakeConnection{
BaseConnection: c,
}
Connections.AddSSHConnection(sshConn2)
Connections.Add(fakeConn)
assert.Equal(t, Connections.GetActiveSessions(username), 2)
cFTP := NewBaseConnection("id2", ProtocolFTP, dataprovider.User{}, nil)
cFTP.lastActivity = time.Now().UnixNano()
fakeConn = &fakeConnection{
BaseConnection: cFTP,
}
Connections.Add(fakeConn)
assert.Equal(t, Connections.GetActiveSessions(username), 2)
assert.Len(t, Connections.GetStats(), 3)
Connections.RLock()
assert.Len(t, Connections.sshConnections, 2)
Connections.RUnlock()
startIdleTimeoutTicker(100 * time.Millisecond)
assert.Eventually(t, func() bool { return Connections.GetActiveSessions(username) == 1 }, 1*time.Second, 200*time.Millisecond)
assert.Eventually(t, func() bool {
Connections.RLock()
defer Connections.RUnlock()
return len(Connections.sshConnections) == 1
}, 1*time.Second, 200*time.Millisecond)
stopIdleTimeoutTicker()
assert.Len(t, Connections.GetStats(), 2)
c.lastActivity = time.Now().Add(-24 * time.Hour).UnixNano()
cFTP.lastActivity = time.Now().Add(-24 * time.Hour).UnixNano()
sshConn2.lastActivity = c.lastActivity
startIdleTimeoutTicker(100 * time.Millisecond)
assert.Eventually(t, func() bool { return len(Connections.GetStats()) == 0 }, 1*time.Second, 200*time.Millisecond)
assert.Eventually(t, func() bool {
Connections.RLock()
defer Connections.RUnlock()
return len(Connections.sshConnections) == 0
}, 1*time.Second, 200*time.Millisecond)
stopIdleTimeoutTicker()
assert.True(t, customConn1.isClosed)
assert.True(t, customConn2.isClosed)
Config = configCopy
}
func TestCloseConnection(t *testing.T) {
c := NewBaseConnection("id", ProtocolSFTP, dataprovider.User{}, nil)
fakeConn := &fakeConnection{
BaseConnection: c,
}
assert.True(t, Connections.IsNewConnectionAllowed())
Connections.Add(fakeConn)
assert.Len(t, Connections.GetStats(), 1)
res := Connections.Close(fakeConn.GetID())
assert.True(t, res)
assert.Eventually(t, func() bool { return len(Connections.GetStats()) == 0 }, 300*time.Millisecond, 50*time.Millisecond)
res = Connections.Close(fakeConn.GetID())
assert.False(t, res)
Connections.Remove(fakeConn.GetID())
}
func TestSwapConnection(t *testing.T) {
c := NewBaseConnection("id", ProtocolFTP, dataprovider.User{}, nil)
fakeConn := &fakeConnection{
BaseConnection: c,
}
Connections.Add(fakeConn)
if assert.Len(t, Connections.GetStats(), 1) {
assert.Equal(t, "", Connections.GetStats()[0].Username)
}
c = NewBaseConnection("id", ProtocolFTP, dataprovider.User{
Username: userTestUsername,
}, nil)
fakeConn = &fakeConnection{
BaseConnection: c,
}
err := Connections.Swap(fakeConn)
assert.NoError(t, err)
if assert.Len(t, Connections.GetStats(), 1) {
assert.Equal(t, userTestUsername, Connections.GetStats()[0].Username)
}
res := Connections.Close(fakeConn.GetID())
assert.True(t, res)
assert.Eventually(t, func() bool { return len(Connections.GetStats()) == 0 }, 300*time.Millisecond, 50*time.Millisecond)
err = Connections.Swap(fakeConn)
assert.Error(t, err)
}
func TestAtomicUpload(t *testing.T) {
configCopy := Config
Config.UploadMode = UploadModeStandard
assert.False(t, Config.IsAtomicUploadEnabled())
Config.UploadMode = UploadModeAtomic
assert.True(t, Config.IsAtomicUploadEnabled())
Config.UploadMode = UploadModeAtomicWithResume
assert.True(t, Config.IsAtomicUploadEnabled())
Config = configCopy
}
func TestConnectionStatus(t *testing.T) {
username := "test_user"
user := dataprovider.User{
Username: username,
}
fs := vfs.NewOsFs("", os.TempDir(), nil)
c1 := NewBaseConnection("id1", ProtocolSFTP, user, fs)
fakeConn1 := &fakeConnection{
BaseConnection: c1,
}
t1 := NewBaseTransfer(nil, c1, nil, "/p1", "/r1", TransferUpload, 0, 0, 0, true, fs)
t1.BytesReceived = 123
t2 := NewBaseTransfer(nil, c1, nil, "/p2", "/r2", TransferDownload, 0, 0, 0, true, fs)
t2.BytesSent = 456
c2 := NewBaseConnection("id2", ProtocolSSH, user, nil)
fakeConn2 := &fakeConnection{
BaseConnection: c2,
command: "md5sum",
}
c3 := NewBaseConnection("id3", ProtocolWebDAV, user, nil)
fakeConn3 := &fakeConnection{
BaseConnection: c3,
command: "PROPFIND",
}
t3 := NewBaseTransfer(nil, c3, nil, "/p2", "/r2", TransferDownload, 0, 0, 0, true, fs)
Connections.Add(fakeConn1)
Connections.Add(fakeConn2)
Connections.Add(fakeConn3)
stats := Connections.GetStats()
assert.Len(t, stats, 3)
for _, stat := range stats {
assert.Equal(t, stat.Username, username)
assert.True(t, strings.HasPrefix(stat.GetConnectionInfo(), stat.Protocol))
assert.True(t, strings.HasPrefix(stat.GetConnectionDuration(), "00:"))
if stat.ConnectionID == "SFTP_id1" {
assert.Len(t, stat.Transfers, 2)
assert.Greater(t, len(stat.GetTransfersAsString()), 0)
for _, tr := range stat.Transfers {
if tr.OperationType == operationDownload {
assert.True(t, strings.HasPrefix(tr.getConnectionTransferAsString(), "DL"))
} else if tr.OperationType == operationUpload {
assert.True(t, strings.HasPrefix(tr.getConnectionTransferAsString(), "UL"))
}
}
} else if stat.ConnectionID == "DAV_id3" {
assert.Len(t, stat.Transfers, 1)
assert.Greater(t, len(stat.GetTransfersAsString()), 0)
} else {
assert.Equal(t, 0, len(stat.GetTransfersAsString()))
}
}
err := t1.Close()
assert.NoError(t, err)
err = t2.Close()
assert.NoError(t, err)
err = fakeConn3.SignalTransfersAbort()
assert.NoError(t, err)
assert.Equal(t, int32(1), atomic.LoadInt32(&t3.AbortTransfer))
err = t3.Close()
assert.NoError(t, err)
err = fakeConn3.SignalTransfersAbort()
assert.Error(t, err)
Connections.Remove(fakeConn1.GetID())
stats = Connections.GetStats()
assert.Len(t, stats, 2)
assert.Equal(t, fakeConn3.GetID(), stats[0].ConnectionID)
assert.Equal(t, fakeConn2.GetID(), stats[1].ConnectionID)
Connections.Remove(fakeConn2.GetID())
stats = Connections.GetStats()
assert.Len(t, stats, 1)
assert.Equal(t, fakeConn3.GetID(), stats[0].ConnectionID)
Connections.Remove(fakeConn3.GetID())
stats = Connections.GetStats()
assert.Len(t, stats, 0)
}
func TestQuotaScans(t *testing.T) {
username := "username"
assert.True(t, QuotaScans.AddUserQuotaScan(username))
assert.False(t, QuotaScans.AddUserQuotaScan(username))
if assert.Len(t, QuotaScans.GetUsersQuotaScans(), 1) {
assert.Equal(t, QuotaScans.GetUsersQuotaScans()[0].Username, username)
}
assert.True(t, QuotaScans.RemoveUserQuotaScan(username))
assert.False(t, QuotaScans.RemoveUserQuotaScan(username))
assert.Len(t, QuotaScans.GetUsersQuotaScans(), 0)
folderName := "folder"
assert.True(t, QuotaScans.AddVFolderQuotaScan(folderName))
assert.False(t, QuotaScans.AddVFolderQuotaScan(folderName))
if assert.Len(t, QuotaScans.GetVFoldersQuotaScans(), 1) {
assert.Equal(t, QuotaScans.GetVFoldersQuotaScans()[0].Name, folderName)
}
assert.True(t, QuotaScans.RemoveVFolderQuotaScan(folderName))
assert.False(t, QuotaScans.RemoveVFolderQuotaScan(folderName))
assert.Len(t, QuotaScans.GetVFoldersQuotaScans(), 0)
}
func TestProxyProtocolVersion(t *testing.T) {
c := Configuration{
ProxyProtocol: 1,
}
proxyListener, err := c.GetProxyListener(nil)
assert.NoError(t, err)
assert.Nil(t, proxyListener.Policy)
c.ProxyProtocol = 2
proxyListener, err = c.GetProxyListener(nil)
assert.NoError(t, err)
assert.NotNil(t, proxyListener.Policy)
c.ProxyProtocol = 1
c.ProxyAllowed = []string{"invalid"}
_, err = c.GetProxyListener(nil)
assert.Error(t, err)
c.ProxyProtocol = 2
_, err = c.GetProxyListener(nil)
assert.Error(t, err)
}
func TestProxyProtocol(t *testing.T) {
httpClient := httpclient.GetHTTPClient()
resp, err := httpClient.Get(fmt.Sprintf("http://%v", httpProxyAddr))
if assert.NoError(t, err) {
defer resp.Body.Close()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
}
}
func TestPostConnectHook(t *testing.T) {
Config.PostConnectHook = ""
ipAddr := "127.0.0.1"
assert.NoError(t, Config.ExecutePostConnectHook(ipAddr, ProtocolFTP))
Config.PostConnectHook = "http://foo\x7f.com/"
assert.Error(t, Config.ExecutePostConnectHook(ipAddr, ProtocolSFTP))
Config.PostConnectHook = "http://invalid:1234/"
assert.Error(t, Config.ExecutePostConnectHook(ipAddr, ProtocolSFTP))
Config.PostConnectHook = fmt.Sprintf("http://%v/404", httpAddr)
assert.Error(t, Config.ExecutePostConnectHook(ipAddr, ProtocolFTP))
Config.PostConnectHook = fmt.Sprintf("http://%v", httpAddr)
assert.NoError(t, Config.ExecutePostConnectHook(ipAddr, ProtocolFTP))
Config.PostConnectHook = "invalid"
assert.Error(t, Config.ExecutePostConnectHook(ipAddr, ProtocolFTP))
if runtime.GOOS == osWindows {
Config.PostConnectHook = "C:\\bad\\command"
assert.Error(t, Config.ExecutePostConnectHook(ipAddr, ProtocolSFTP))
} else {
Config.PostConnectHook = "/invalid/path"
assert.Error(t, Config.ExecutePostConnectHook(ipAddr, ProtocolSFTP))
hookCmd, err := exec.LookPath("true")
assert.NoError(t, err)
Config.PostConnectHook = hookCmd
assert.NoError(t, Config.ExecutePostConnectHook(ipAddr, ProtocolSFTP))
}
Config.PostConnectHook = ""
}
func TestCryptoConvertFileInfo(t *testing.T) {
name := "name"
fs, err := vfs.NewCryptFs("connID1", os.TempDir(), vfs.CryptFsConfig{Passphrase: kms.NewPlainSecret("secret")})
require.NoError(t, err)
cryptFs := fs.(*vfs.CryptFs)
info := vfs.NewFileInfo(name, true, 48, time.Now(), false)
assert.Equal(t, info, cryptFs.ConvertFileInfo(info))
info = vfs.NewFileInfo(name, false, 48, time.Now(), false)
assert.NotEqual(t, info.Size(), cryptFs.ConvertFileInfo(info).Size())
info = vfs.NewFileInfo(name, false, 33, time.Now(), false)
assert.Equal(t, int64(0), cryptFs.ConvertFileInfo(info).Size())
info = vfs.NewFileInfo(name, false, 1, time.Now(), false)
assert.Equal(t, int64(0), cryptFs.ConvertFileInfo(info).Size())
}
func TestFolderCopy(t *testing.T) {
folder := vfs.BaseVirtualFolder{
ID: 1,
Name: "name",
MappedPath: filepath.Clean(os.TempDir()),
UsedQuotaSize: 4096,
UsedQuotaFiles: 2,
LastQuotaUpdate: utils.GetTimeAsMsSinceEpoch(time.Now()),
Users: []string{"user1", "user2"},
}
folderCopy := folder.GetACopy()
folder.ID = 2
folder.Users = []string{"user3"}
require.Len(t, folderCopy.Users, 2)
require.True(t, utils.IsStringInSlice("user1", folderCopy.Users))
require.True(t, utils.IsStringInSlice("user2", folderCopy.Users))
require.Equal(t, int64(1), folderCopy.ID)
require.Equal(t, folder.Name, folderCopy.Name)
require.Equal(t, folder.MappedPath, folderCopy.MappedPath)
require.Equal(t, folder.UsedQuotaSize, folderCopy.UsedQuotaSize)
require.Equal(t, folder.UsedQuotaFiles, folderCopy.UsedQuotaFiles)
require.Equal(t, folder.LastQuotaUpdate, folderCopy.LastQuotaUpdate)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,472 +0,0 @@
package common
import (
"encoding/json"
"fmt"
"io/ioutil"
"net"
"os"
"sort"
"sync"
"time"
"github.com/yl2chen/cidranger"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/utils"
)
// HostEvent is the enumerable for the support host event
type HostEvent int
// Supported host events
const (
HostEventLoginFailed HostEvent = iota
HostEventUserNotFound
HostEventNoLoginTried
)
// Defender defines the interface that a defender must implements
type Defender interface {
AddEvent(ip string, event HostEvent)
IsBanned(ip string) bool
GetBanTime(ip string) *time.Time
GetScore(ip string) int
Unban(ip string) bool
Reload() error
}
// DefenderConfig defines the "defender" configuration
type DefenderConfig struct {
// Set to true to enable the defender
Enabled bool `json:"enabled" mapstructure:"enabled"`
// BanTime is the number of minutes that a host is banned
BanTime int `json:"ban_time" mapstructure:"ban_time"`
// Percentage increase of the ban time if a banned host tries to connect again
BanTimeIncrement int `json:"ban_time_increment" mapstructure:"ban_time_increment"`
// Threshold value for banning a client
Threshold int `json:"threshold" mapstructure:"threshold"`
// Score for invalid login attempts, eg. non-existent user accounts or
// client disconnected for inactivity without authentication attempts
ScoreInvalid int `json:"score_invalid" mapstructure:"score_invalid"`
// Score for valid login attempts, eg. user accounts that exist
ScoreValid int `json:"score_valid" mapstructure:"score_valid"`
// Defines the time window, in minutes, for tracking client errors.
// A host is banned if it has exceeded the defined threshold during
// the last observation time minutes
ObservationTime int `json:"observation_time" mapstructure:"observation_time"`
// The number of banned IPs and host scores kept in memory will vary between the
// soft and hard limit
EntriesSoftLimit int `json:"entries_soft_limit" mapstructure:"entries_soft_limit"`
EntriesHardLimit int `json:"entries_hard_limit" mapstructure:"entries_hard_limit"`
// Path to a file containing a list of ip addresses and/or networks to never ban
SafeListFile string `json:"safelist_file" mapstructure:"safelist_file"`
// Path to a file containing a list of ip addresses and/or networks to always ban
BlockListFile string `json:"blocklist_file" mapstructure:"blocklist_file"`
}
type memoryDefender struct {
config *DefenderConfig
sync.RWMutex
// IP addresses of the clients trying to connected are stored inside hosts,
// they are added to banned once the thresold is reached.
// A violation from a banned host will increase the ban time
// based on the configured BanTimeIncrement
hosts map[string]hostScore // the key is the host IP
banned map[string]time.Time // the key is the host IP
safeList *HostList
blockList *HostList
}
// HostListFile defines the structure expected for safe/block list files
type HostListFile struct {
IPAddresses []string `json:"addresses"`
CIDRNetworks []string `json:"networks"`
}
// HostList defines the structure used to keep the HostListFile in memory
type HostList struct {
IPAddresses map[string]bool
Ranges cidranger.Ranger
}
func (h *HostList) isListed(ip string) bool {
if _, ok := h.IPAddresses[ip]; ok {
return true
}
ok, err := h.Ranges.Contains(net.ParseIP(ip))
if err != nil {
return false
}
return ok
}
type hostEvent struct {
dateTime time.Time
score int
}
type hostScore struct {
TotalScore int
Events []hostEvent
}
// validate returns an error if the configuration is invalid
func (c *DefenderConfig) validate() error {
if !c.Enabled {
return nil
}
if c.ScoreInvalid >= c.Threshold {
return fmt.Errorf("score_invalid %v cannot be greater than threshold %v", c.ScoreInvalid, c.Threshold)
}
if c.ScoreValid >= c.Threshold {
return fmt.Errorf("score_valid %v cannot be greater than threshold %v", c.ScoreValid, c.Threshold)
}
if c.BanTime <= 0 {
return fmt.Errorf("invalid ban_time %v", c.BanTime)
}
if c.BanTimeIncrement <= 0 {
return fmt.Errorf("invalid ban_time_increment %v", c.BanTimeIncrement)
}
if c.ObservationTime <= 0 {
return fmt.Errorf("invalid observation_time %v", c.ObservationTime)
}
if c.EntriesSoftLimit <= 0 {
return fmt.Errorf("invalid entries_soft_limit %v", c.EntriesSoftLimit)
}
if c.EntriesHardLimit <= c.EntriesSoftLimit {
return fmt.Errorf("invalid entries_hard_limit %v must be > %v", c.EntriesHardLimit, c.EntriesSoftLimit)
}
return nil
}
func newInMemoryDefender(config *DefenderConfig) (Defender, error) {
err := config.validate()
if err != nil {
return nil, err
}
defender := &memoryDefender{
config: config,
hosts: make(map[string]hostScore),
banned: make(map[string]time.Time),
}
if err := defender.Reload(); err != nil {
return nil, err
}
return defender, nil
}
// Reload reloads block and safe lists
func (d *memoryDefender) Reload() error {
blockList, err := loadHostListFromFile(d.config.BlockListFile)
if err != nil {
return err
}
d.Lock()
d.blockList = blockList
d.Unlock()
safeList, err := loadHostListFromFile(d.config.SafeListFile)
if err != nil {
return err
}
d.Lock()
d.safeList = safeList
d.Unlock()
return nil
}
// IsBanned returns true if the specified IP is banned
// and increase ban time if the IP is found.
// This method must be called as soon as the client connects
func (d *memoryDefender) IsBanned(ip string) bool {
d.RLock()
if banTime, ok := d.banned[ip]; ok {
if banTime.After(time.Now()) {
increment := d.config.BanTime * d.config.BanTimeIncrement / 100
if increment == 0 {
increment++
}
d.RUnlock()
// we can save an earlier ban time if there are contemporary updates
// but this should not make much difference. I prefer to hold a read lock
// until possible for performance reasons, this method is called each
// time a new client connects and it must be as fast as possible
d.Lock()
d.banned[ip] = banTime.Add(time.Duration(increment) * time.Minute)
d.Unlock()
return true
}
}
defer d.RUnlock()
if d.blockList != nil && d.blockList.isListed(ip) {
// permanent ban
return true
}
return false
}
// Unban removes the specified IP address from the banned ones
func (d *memoryDefender) Unban(ip string) bool {
d.Lock()
defer d.Unlock()
if _, ok := d.banned[ip]; ok {
delete(d.banned, ip)
return true
}
return false
}
// AddEvent adds an event for the given IP.
// This method must be called for clients not yet banned
func (d *memoryDefender) AddEvent(ip string, event HostEvent) {
d.Lock()
defer d.Unlock()
if d.safeList != nil && d.safeList.isListed(ip) {
return
}
var score int
switch event {
case HostEventLoginFailed:
score = d.config.ScoreValid
case HostEventUserNotFound, HostEventNoLoginTried:
score = d.config.ScoreInvalid
}
ev := hostEvent{
dateTime: time.Now(),
score: score,
}
if hs, ok := d.hosts[ip]; ok {
hs.Events = append(hs.Events, ev)
hs.TotalScore = 0
idx := 0
for _, event := range hs.Events {
if event.dateTime.Add(time.Duration(d.config.ObservationTime) * time.Minute).After(time.Now()) {
hs.Events[idx] = event
hs.TotalScore += event.score
idx++
}
}
hs.Events = hs.Events[:idx]
if hs.TotalScore >= d.config.Threshold {
d.banned[ip] = time.Now().Add(time.Duration(d.config.BanTime) * time.Minute)
delete(d.hosts, ip)
d.cleanupBanned()
} else {
d.hosts[ip] = hs
}
} else {
d.hosts[ip] = hostScore{
TotalScore: ev.score,
Events: []hostEvent{ev},
}
d.cleanupHosts()
}
}
func (d *memoryDefender) countBanned() int {
d.RLock()
defer d.RUnlock()
return len(d.banned)
}
func (d *memoryDefender) countHosts() int {
d.RLock()
defer d.RUnlock()
return len(d.hosts)
}
// GetBanTime returns the ban time for the given IP or nil if the IP is not banned
func (d *memoryDefender) GetBanTime(ip string) *time.Time {
d.RLock()
defer d.RUnlock()
if banTime, ok := d.banned[ip]; ok {
return &banTime
}
return nil
}
// GetScore returns the score for the given IP
func (d *memoryDefender) GetScore(ip string) int {
d.RLock()
defer d.RUnlock()
score := 0
if hs, ok := d.hosts[ip]; ok {
for _, event := range hs.Events {
if event.dateTime.Add(time.Duration(d.config.ObservationTime) * time.Minute).After(time.Now()) {
score += event.score
}
}
}
return score
}
func (d *memoryDefender) cleanupBanned() {
if len(d.banned) > d.config.EntriesHardLimit {
kvList := make(kvList, 0, len(d.banned))
for k, v := range d.banned {
if v.Before(time.Now()) {
delete(d.banned, k)
}
kvList = append(kvList, kv{
Key: k,
Value: v.UnixNano(),
})
}
// we removed expired ip addresses, if any, above, this could be enough
numToRemove := len(d.banned) - d.config.EntriesSoftLimit
if numToRemove <= 0 {
return
}
sort.Sort(kvList)
for idx, kv := range kvList {
if idx >= numToRemove {
break
}
delete(d.banned, kv.Key)
}
}
}
func (d *memoryDefender) cleanupHosts() {
if len(d.hosts) > d.config.EntriesHardLimit {
kvList := make(kvList, 0, len(d.hosts))
for k, v := range d.hosts {
value := int64(0)
if len(v.Events) > 0 {
value = v.Events[len(v.Events)-1].dateTime.UnixNano()
}
kvList = append(kvList, kv{
Key: k,
Value: value,
})
}
sort.Sort(kvList)
numToRemove := len(d.hosts) - d.config.EntriesSoftLimit
for idx, kv := range kvList {
if idx >= numToRemove {
break
}
delete(d.hosts, kv.Key)
}
}
}
func loadHostListFromFile(name string) (*HostList, error) {
if name == "" {
return nil, nil
}
if !utils.IsFileInputValid(name) {
return nil, fmt.Errorf("invalid host list file name %#v", name)
}
info, err := os.Stat(name)
if err != nil {
return nil, err
}
// opinionated max size, you should avoid big host lists
if info.Size() > 1048576*5 { // 5MB
return nil, fmt.Errorf("host list file %#v is too big: %v bytes", name, info.Size())
}
content, err := ioutil.ReadFile(name)
if err != nil {
return nil, fmt.Errorf("unable to read input file %#v: %v", name, err)
}
var hostList HostListFile
err = json.Unmarshal(content, &hostList)
if err != nil {
return nil, err
}
if len(hostList.CIDRNetworks) > 0 || len(hostList.IPAddresses) > 0 {
result := &HostList{
IPAddresses: make(map[string]bool),
Ranges: cidranger.NewPCTrieRanger(),
}
ipCount := 0
cdrCount := 0
for _, ip := range hostList.IPAddresses {
if net.ParseIP(ip) == nil {
logger.Warn(logSender, "", "unable to parse IP %#v", ip)
continue
}
result.IPAddresses[ip] = true
ipCount++
}
for _, cidrNet := range hostList.CIDRNetworks {
_, network, err := net.ParseCIDR(cidrNet)
if err != nil {
logger.Warn(logSender, "", "unable to parse CIDR network %#v", cidrNet)
continue
}
err = result.Ranges.Insert(cidranger.NewBasicRangerEntry(*network))
if err == nil {
cdrCount++
}
}
logger.Info(logSender, "", "list %#v loaded, ip addresses loaded: %v/%v networks loaded: %v/%v",
name, ipCount, len(hostList.IPAddresses), cdrCount, len(hostList.CIDRNetworks))
return result, nil
}
return nil, nil
}
type kv struct {
Key string
Value int64
}
type kvList []kv
func (p kvList) Len() int { return len(p) }
func (p kvList) Less(i, j int) bool { return p[i].Value < p[j].Value }
func (p kvList) Swap(i, j int) { p[i], p[j] = p[j], p[i] }

View File

@@ -1,523 +0,0 @@
package common
import (
"crypto/rand"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"os"
"path/filepath"
"runtime"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/yl2chen/cidranger"
)
func TestBasicDefender(t *testing.T) {
bl := HostListFile{
IPAddresses: []string{"172.16.1.1", "172.16.1.2"},
CIDRNetworks: []string{"10.8.0.0/24"},
}
sl := HostListFile{
IPAddresses: []string{"172.16.1.3", "172.16.1.4"},
CIDRNetworks: []string{"192.168.8.0/24"},
}
blFile := filepath.Join(os.TempDir(), "bl.json")
slFile := filepath.Join(os.TempDir(), "sl.json")
data, err := json.Marshal(bl)
assert.NoError(t, err)
err = ioutil.WriteFile(blFile, data, os.ModePerm)
assert.NoError(t, err)
data, err = json.Marshal(sl)
assert.NoError(t, err)
err = ioutil.WriteFile(slFile, data, os.ModePerm)
assert.NoError(t, err)
config := &DefenderConfig{
Enabled: true,
BanTime: 10,
BanTimeIncrement: 2,
Threshold: 5,
ScoreInvalid: 2,
ScoreValid: 1,
ObservationTime: 15,
EntriesSoftLimit: 1,
EntriesHardLimit: 2,
SafeListFile: "slFile",
BlockListFile: "blFile",
}
_, err = newInMemoryDefender(config)
assert.Error(t, err)
config.BlockListFile = blFile
_, err = newInMemoryDefender(config)
assert.Error(t, err)
config.SafeListFile = slFile
d, err := newInMemoryDefender(config)
assert.NoError(t, err)
defender := d.(*memoryDefender)
assert.True(t, defender.IsBanned("172.16.1.1"))
assert.False(t, defender.IsBanned("172.16.1.10"))
assert.False(t, defender.IsBanned("10.8.2.3"))
assert.True(t, defender.IsBanned("10.8.0.3"))
assert.False(t, defender.IsBanned("invalid ip"))
assert.Equal(t, 0, defender.countBanned())
assert.Equal(t, 0, defender.countHosts())
defender.AddEvent("172.16.1.4", HostEventLoginFailed)
defender.AddEvent("192.168.8.4", HostEventUserNotFound)
assert.Equal(t, 0, defender.countHosts())
testIP := "12.34.56.78"
defender.AddEvent(testIP, HostEventLoginFailed)
assert.Equal(t, 1, defender.countHosts())
assert.Equal(t, 0, defender.countBanned())
assert.Equal(t, 1, defender.GetScore(testIP))
assert.Nil(t, defender.GetBanTime(testIP))
defender.AddEvent(testIP, HostEventNoLoginTried)
assert.Equal(t, 1, defender.countHosts())
assert.Equal(t, 0, defender.countBanned())
assert.Equal(t, 3, defender.GetScore(testIP))
defender.AddEvent(testIP, HostEventNoLoginTried)
assert.Equal(t, 0, defender.countHosts())
assert.Equal(t, 1, defender.countBanned())
assert.Equal(t, 0, defender.GetScore(testIP))
assert.NotNil(t, defender.GetBanTime(testIP))
// now test cleanup, testIP is already banned
testIP1 := "12.34.56.79"
testIP2 := "12.34.56.80"
testIP3 := "12.34.56.81"
defender.AddEvent(testIP1, HostEventNoLoginTried)
defender.AddEvent(testIP2, HostEventNoLoginTried)
assert.Equal(t, 2, defender.countHosts())
time.Sleep(20 * time.Millisecond)
defender.AddEvent(testIP3, HostEventNoLoginTried)
assert.Equal(t, defender.config.EntriesSoftLimit, defender.countHosts())
// testIP1 and testIP2 should be removed
assert.Equal(t, defender.config.EntriesSoftLimit, defender.countHosts())
assert.Equal(t, 0, defender.GetScore(testIP1))
assert.Equal(t, 0, defender.GetScore(testIP2))
assert.Equal(t, 2, defender.GetScore(testIP3))
defender.AddEvent(testIP3, HostEventNoLoginTried)
defender.AddEvent(testIP3, HostEventNoLoginTried)
// IP3 is now banned
assert.NotNil(t, defender.GetBanTime(testIP3))
assert.Equal(t, 0, defender.countHosts())
time.Sleep(20 * time.Millisecond)
for i := 0; i < 3; i++ {
defender.AddEvent(testIP1, HostEventNoLoginTried)
}
assert.Equal(t, 0, defender.countHosts())
assert.Equal(t, config.EntriesSoftLimit, defender.countBanned())
assert.Nil(t, defender.GetBanTime(testIP))
assert.Nil(t, defender.GetBanTime(testIP3))
assert.NotNil(t, defender.GetBanTime(testIP1))
for i := 0; i < 3; i++ {
defender.AddEvent(testIP, HostEventNoLoginTried)
time.Sleep(10 * time.Millisecond)
defender.AddEvent(testIP3, HostEventNoLoginTried)
}
assert.Equal(t, 0, defender.countHosts())
assert.Equal(t, defender.config.EntriesSoftLimit, defender.countBanned())
banTime := defender.GetBanTime(testIP3)
if assert.NotNil(t, banTime) {
assert.True(t, defender.IsBanned(testIP3))
// ban time should increase
newBanTime := defender.GetBanTime(testIP3)
assert.True(t, newBanTime.After(*banTime))
}
assert.True(t, defender.Unban(testIP3))
assert.False(t, defender.Unban(testIP3))
err = os.Remove(slFile)
assert.NoError(t, err)
err = os.Remove(blFile)
assert.NoError(t, err)
}
func TestLoadHostListFromFile(t *testing.T) {
_, err := loadHostListFromFile(".")
assert.Error(t, err)
hostsFilePath := filepath.Join(os.TempDir(), "hostfile")
content := make([]byte, 1048576*6)
_, err = rand.Read(content)
assert.NoError(t, err)
err = ioutil.WriteFile(hostsFilePath, content, os.ModePerm)
assert.NoError(t, err)
_, err = loadHostListFromFile(hostsFilePath)
assert.Error(t, err)
hl := HostListFile{
IPAddresses: []string{},
CIDRNetworks: []string{},
}
asJSON, err := json.Marshal(hl)
assert.NoError(t, err)
err = ioutil.WriteFile(hostsFilePath, asJSON, os.ModePerm)
assert.NoError(t, err)
hostList, err := loadHostListFromFile(hostsFilePath)
assert.NoError(t, err)
assert.Nil(t, hostList)
hl.IPAddresses = append(hl.IPAddresses, "invalidip")
asJSON, err = json.Marshal(hl)
assert.NoError(t, err)
err = ioutil.WriteFile(hostsFilePath, asJSON, os.ModePerm)
assert.NoError(t, err)
hostList, err = loadHostListFromFile(hostsFilePath)
assert.NoError(t, err)
assert.Len(t, hostList.IPAddresses, 0)
hl.IPAddresses = nil
hl.CIDRNetworks = append(hl.CIDRNetworks, "invalid net")
asJSON, err = json.Marshal(hl)
assert.NoError(t, err)
err = ioutil.WriteFile(hostsFilePath, asJSON, os.ModePerm)
assert.NoError(t, err)
hostList, err = loadHostListFromFile(hostsFilePath)
assert.NoError(t, err)
assert.NotNil(t, hostList)
assert.Len(t, hostList.IPAddresses, 0)
assert.Equal(t, 0, hostList.Ranges.Len())
if runtime.GOOS != "windows" {
err = os.Chmod(hostsFilePath, 0111)
assert.NoError(t, err)
_, err = loadHostListFromFile(hostsFilePath)
assert.Error(t, err)
err = os.Chmod(hostsFilePath, 0644)
assert.NoError(t, err)
}
err = ioutil.WriteFile(hostsFilePath, []byte("non json content"), os.ModePerm)
assert.NoError(t, err)
_, err = loadHostListFromFile(hostsFilePath)
assert.Error(t, err)
err = os.Remove(hostsFilePath)
assert.NoError(t, err)
}
func TestDefenderCleanup(t *testing.T) {
d := memoryDefender{
banned: make(map[string]time.Time),
hosts: make(map[string]hostScore),
config: &DefenderConfig{
ObservationTime: 1,
EntriesSoftLimit: 2,
EntriesHardLimit: 3,
},
}
d.banned["1.1.1.1"] = time.Now().Add(-24 * time.Hour)
d.banned["1.1.1.2"] = time.Now().Add(-24 * time.Hour)
d.banned["1.1.1.3"] = time.Now().Add(-24 * time.Hour)
d.banned["1.1.1.4"] = time.Now().Add(-24 * time.Hour)
d.cleanupBanned()
assert.Equal(t, 0, d.countBanned())
d.banned["2.2.2.2"] = time.Now().Add(2 * time.Minute)
d.banned["2.2.2.3"] = time.Now().Add(1 * time.Minute)
d.banned["2.2.2.4"] = time.Now().Add(3 * time.Minute)
d.banned["2.2.2.5"] = time.Now().Add(4 * time.Minute)
d.cleanupBanned()
assert.Equal(t, d.config.EntriesSoftLimit, d.countBanned())
assert.Nil(t, d.GetBanTime("2.2.2.3"))
d.hosts["3.3.3.3"] = hostScore{
TotalScore: 0,
Events: []hostEvent{
{
dateTime: time.Now().Add(-5 * time.Minute),
score: 1,
},
{
dateTime: time.Now().Add(-3 * time.Minute),
score: 1,
},
{
dateTime: time.Now(),
score: 1,
},
},
}
d.hosts["3.3.3.4"] = hostScore{
TotalScore: 1,
Events: []hostEvent{
{
dateTime: time.Now().Add(-3 * time.Minute),
score: 1,
},
},
}
d.hosts["3.3.3.5"] = hostScore{
TotalScore: 1,
Events: []hostEvent{
{
dateTime: time.Now().Add(-2 * time.Minute),
score: 1,
},
},
}
d.hosts["3.3.3.6"] = hostScore{
TotalScore: 1,
Events: []hostEvent{
{
dateTime: time.Now().Add(-1 * time.Minute),
score: 1,
},
},
}
assert.Equal(t, 1, d.GetScore("3.3.3.3"))
d.cleanupHosts()
assert.Equal(t, d.config.EntriesSoftLimit, d.countHosts())
assert.Equal(t, 0, d.GetScore("3.3.3.4"))
}
func TestDefenderConfig(t *testing.T) {
c := DefenderConfig{}
err := c.validate()
require.NoError(t, err)
c.Enabled = true
c.Threshold = 10
c.ScoreInvalid = 10
err = c.validate()
require.Error(t, err)
c.ScoreInvalid = 2
c.ScoreValid = 10
err = c.validate()
require.Error(t, err)
c.ScoreValid = 1
c.BanTime = 0
err = c.validate()
require.Error(t, err)
c.BanTime = 30
c.BanTimeIncrement = 0
err = c.validate()
require.Error(t, err)
c.BanTimeIncrement = 50
c.ObservationTime = 0
err = c.validate()
require.Error(t, err)
c.ObservationTime = 30
err = c.validate()
require.Error(t, err)
c.EntriesSoftLimit = 10
err = c.validate()
require.Error(t, err)
c.EntriesHardLimit = 10
err = c.validate()
require.Error(t, err)
c.EntriesHardLimit = 20
err = c.validate()
require.NoError(t, err)
}
func BenchmarkDefenderBannedSearch(b *testing.B) {
d := getDefenderForBench()
ip, ipnet, err := net.ParseCIDR("10.8.0.0/12") // 1048574 ip addresses
if err != nil {
panic(err)
}
for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) {
d.banned[ip.String()] = time.Now().Add(10 * time.Minute)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
d.IsBanned("192.168.1.1")
}
}
func BenchmarkCleanup(b *testing.B) {
d := getDefenderForBench()
ip, ipnet, err := net.ParseCIDR("192.168.4.0/24")
if err != nil {
panic(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) {
d.AddEvent(ip.String(), HostEventLoginFailed)
if d.countHosts() > d.config.EntriesHardLimit {
panic("too many hosts")
}
if d.countBanned() > d.config.EntriesSoftLimit {
panic("too many ip banned")
}
}
}
}
func BenchmarkDefenderBannedSearchWithBlockList(b *testing.B) {
d := getDefenderForBench()
d.blockList = &HostList{
IPAddresses: make(map[string]bool),
Ranges: cidranger.NewPCTrieRanger(),
}
ip, ipnet, err := net.ParseCIDR("129.8.0.0/12") // 1048574 ip addresses
if err != nil {
panic(err)
}
for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) {
d.banned[ip.String()] = time.Now().Add(10 * time.Minute)
d.blockList.IPAddresses[ip.String()] = true
}
for i := 0; i < 255; i++ {
cidr := fmt.Sprintf("10.8.%v.1/24", i)
_, network, _ := net.ParseCIDR(cidr)
if err := d.blockList.Ranges.Insert(cidranger.NewBasicRangerEntry(*network)); err != nil {
panic(err)
}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
d.IsBanned("192.168.1.1")
}
}
func BenchmarkHostListSearch(b *testing.B) {
hostlist := &HostList{
IPAddresses: make(map[string]bool),
Ranges: cidranger.NewPCTrieRanger(),
}
ip, ipnet, _ := net.ParseCIDR("172.16.0.0/16")
for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) {
hostlist.IPAddresses[ip.String()] = true
}
for i := 0; i < 255; i++ {
cidr := fmt.Sprintf("10.8.%v.1/24", i)
_, network, _ := net.ParseCIDR(cidr)
if err := hostlist.Ranges.Insert(cidranger.NewBasicRangerEntry(*network)); err != nil {
panic(err)
}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
if hostlist.isListed("192.167.1.2") {
panic("should not be listed")
}
}
}
func BenchmarkCIDRanger(b *testing.B) {
ranger := cidranger.NewPCTrieRanger()
for i := 0; i < 255; i++ {
cidr := fmt.Sprintf("192.168.%v.1/24", i)
_, network, _ := net.ParseCIDR(cidr)
if err := ranger.Insert(cidranger.NewBasicRangerEntry(*network)); err != nil {
panic(err)
}
}
ipToMatch := net.ParseIP("192.167.1.2")
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := ranger.Contains(ipToMatch); err != nil {
panic(err)
}
}
}
func BenchmarkNetContains(b *testing.B) {
var nets []*net.IPNet
for i := 0; i < 255; i++ {
cidr := fmt.Sprintf("192.168.%v.1/24", i)
_, network, _ := net.ParseCIDR(cidr)
nets = append(nets, network)
}
ipToMatch := net.ParseIP("192.167.1.1")
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, n := range nets {
n.Contains(ipToMatch)
}
}
}
func getDefenderForBench() *memoryDefender {
config := &DefenderConfig{
Enabled: true,
BanTime: 30,
BanTimeIncrement: 50,
Threshold: 10,
ScoreInvalid: 2,
ScoreValid: 2,
ObservationTime: 30,
EntriesSoftLimit: 50,
EntriesHardLimit: 100,
}
return &memoryDefender{
config: config,
hosts: make(map[string]hostScore),
banned: make(map[string]time.Time),
}
}
func inc(ip net.IP) {
for j := len(ip) - 1; j >= 0; j-- {
ip[j]++
if ip[j] > 0 {
break
}
}
}

View File

@@ -1,200 +0,0 @@
package common
import (
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"io/ioutil"
"path/filepath"
"sync"
"time"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/utils"
)
// CertManager defines a TLS certificate manager
type CertManager struct {
certPath string
keyPath string
configDir string
logSender string
sync.RWMutex
caCertificates []string
caRevocationLists []string
cert *tls.Certificate
rootCAs *x509.CertPool
crls []*pkix.CertificateList
}
// Reload tries to reload certificate and CRLs
func (m *CertManager) Reload() error {
errCrt := m.loadCertificate()
errCRLs := m.LoadCRLs()
if errCrt != nil {
return errCrt
}
return errCRLs
}
// LoadCertificate loads the configured x509 key pair
func (m *CertManager) loadCertificate() error {
newCert, err := tls.LoadX509KeyPair(m.certPath, m.keyPath)
if err != nil {
logger.Warn(m.logSender, "", "unable to load X509 key pair, cert file %#v key file %#v error: %v",
m.certPath, m.keyPath, err)
return err
}
logger.Debug(m.logSender, "", "TLS certificate %#v successfully loaded", m.certPath)
m.Lock()
defer m.Unlock()
m.cert = &newCert
return nil
}
// GetCertificateFunc returns the loaded certificate
func (m *CertManager) GetCertificateFunc() func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
m.RLock()
defer m.RUnlock()
return m.cert, nil
}
}
// IsRevoked returns true if the specified certificate has been revoked
func (m *CertManager) IsRevoked(crt *x509.Certificate, caCrt *x509.Certificate) bool {
m.RLock()
defer m.RUnlock()
if crt == nil || caCrt == nil {
logger.Warn(m.logSender, "", "unable to verify crt %v ca crt %v", crt, caCrt)
return len(m.crls) > 0
}
for _, crl := range m.crls {
if !crl.HasExpired(time.Now()) && caCrt.CheckCRLSignature(crl) == nil {
for _, rc := range crl.TBSCertList.RevokedCertificates {
if rc.SerialNumber.Cmp(crt.SerialNumber) == 0 {
return true
}
}
}
}
return false
}
// LoadCRLs tries to load certificate revocation lists from the given paths
func (m *CertManager) LoadCRLs() error {
if len(m.caRevocationLists) == 0 {
return nil
}
var crls []*pkix.CertificateList
for _, revocationList := range m.caRevocationLists {
if !utils.IsFileInputValid(revocationList) {
return fmt.Errorf("invalid root CA revocation list %#v", revocationList)
}
if revocationList != "" && !filepath.IsAbs(revocationList) {
revocationList = filepath.Join(m.configDir, revocationList)
}
crlBytes, err := ioutil.ReadFile(revocationList)
if err != nil {
logger.Warn(m.logSender, "unable to read revocation list %#v", revocationList)
return err
}
crl, err := x509.ParseCRL(crlBytes)
if err != nil {
logger.Warn(m.logSender, "unable to parse revocation list %#v", revocationList)
return err
}
logger.Debug(m.logSender, "", "CRL %#v successfully loaded", revocationList)
crls = append(crls, crl)
}
m.Lock()
defer m.Unlock()
m.crls = crls
return nil
}
// GetRootCAs returns the set of root certificate authorities that servers
// use if required to verify a client certificate
func (m *CertManager) GetRootCAs() *x509.CertPool {
m.RLock()
defer m.RUnlock()
return m.rootCAs
}
// LoadRootCAs tries to load root CA certificate authorities from the given paths
func (m *CertManager) LoadRootCAs() error {
if len(m.caCertificates) == 0 {
return nil
}
rootCAs := x509.NewCertPool()
for _, rootCA := range m.caCertificates {
if !utils.IsFileInputValid(rootCA) {
return fmt.Errorf("invalid root CA certificate %#v", rootCA)
}
if rootCA != "" && !filepath.IsAbs(rootCA) {
rootCA = filepath.Join(m.configDir, rootCA)
}
crt, err := ioutil.ReadFile(rootCA)
if err != nil {
return err
}
if rootCAs.AppendCertsFromPEM(crt) {
logger.Debug(m.logSender, "", "TLS certificate authority %#v successfully loaded", rootCA)
} else {
err := fmt.Errorf("unable to load TLS certificate authority %#v", rootCA)
logger.Warn(m.logSender, "", "%v", err)
return err
}
}
m.Lock()
defer m.Unlock()
m.rootCAs = rootCAs
return nil
}
// SetCACertificates sets the root CA authorities file paths.
// This should not be changed at runtime
func (m *CertManager) SetCACertificates(caCertificates []string) {
m.caCertificates = caCertificates
}
// SetCARevocationLists sets the CA revocation lists file paths.
// This should not be changed at runtime
func (m *CertManager) SetCARevocationLists(caRevocationLists []string) {
m.caRevocationLists = caRevocationLists
}
// NewCertManager creates a new certificate manager
func NewCertManager(certificateFile, certificateKeyFile, configDir, logSender string) (*CertManager, error) {
manager := &CertManager{
cert: nil,
certPath: certificateFile,
keyPath: certificateKeyFile,
configDir: configDir,
logSender: logSender,
}
err := manager.loadCertificate()
if err != nil {
return nil, err
}
return manager, nil
}

View File

@@ -1,387 +0,0 @@
package common
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
const (
serverCert = `-----BEGIN CERTIFICATE-----
MIIEIDCCAgigAwIBAgIRAPOR9zTkX35vSdeyGpF8Rn8wDQYJKoZIhvcNAQELBQAw
EzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjEwMTAyMjEyMjU1WhcNMjIwNzAyMjEz
MDUxWjARMQ8wDQYDVQQDEwZzZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
ggEKAoIBAQCte0PJhCTNqTiqdwk/s4JanKIMKUVWr2u94a+JYy5gJ9xYXrQ49SeN
m+fwhTAOqctP5zNVkFqxlBytJZg3pqCKqRoOOl1qVgL3F3o7JdhZGi67aw8QMLPx
tLPpYWnnrlUQoXRJdTlqkDqO8lOZl9HO5oZeidPZ7r5BVD6ZiujAC6Zg0jIc+EPt
qhaUJ1CStoAeRf1rNWKmDsLv5hEaDWoaHF9sNVzDQg6atZ3ici00qQj+uvEZo8mL
k6egg3rqsTv9ml2qlrRgFumt99J60hTt3tuQaAruHY80O9nGy3SCXC11daa7gszH
ElCRvhUVoOxRtB54YBEtJ0gEpFnTO9J1AgMBAAGjcTBvMA4GA1UdDwEB/wQEAwID
uDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFAgDXwPV
nhztNz+H20iNWgoIx8adMB8GA1UdIwQYMBaAFO1yCNAGr/zQTJIi8lw3w5OiuBvM
MA0GCSqGSIb3DQEBCwUAA4ICAQCR5kgIb4vAtrtsXD24n6RtU1yIXHPLNmDStVrH
uaMYNnHlLhRlQFCjHhjWvZ89FQC7FeNOITc3FpibJySyw7JfnsyEOGxEbcAS4uLB
2pdAiJPqdQtxIVcyi5vu53m1T5tm0sy8sBrGxU466aDQ8VGqjcjfTwNIyoFMd3p/
ezFRvg2BudwU9hqApgfHfLi4WCuI3hLO2tbmgDinyH0HI0YYNNweGpiBYbTLF4Tx
H6vHgD9USMZeu4+HX0IIsBiHQD7TTIe5ceREkPcNPd5qTpIvT3zKQ/KwwT90/zjP
aWmz6pLxBfjRu7MY/bDfxfRUqsrLYJCVBoaDVRWR9rhiPIFkC5JzoWD/4hdj2iis
N0+OOaJ77L+/ArFprE+7Fu3cSdYlfiNjV8R5kE29cAxKLI92CjAiTKrEuxKcQPKO
+taWNKIYYjEDZwVnzlkTIl007X0RBuzu9gh4w5NwJdt8ZOJAp0JV0Cq+UvG+FC/v
lYk82E6j1HKhf4CXmrjsrD1Fyu41mpVFOpa2ATiFGvms913MkXuyO8g99IllmDw1
D7/PN4Qe9N6Zm7yoKZM0IUw2v+SUMIdOAZ7dptO9ZjtYOfiAIYN3jM8R4JYgPiuD
DGSM9LJBJxCxI/DiO1y1Z3n9TcdDQYut8Gqdi/aYXw2YeqyHXosX5Od3vcK/O5zC
pOJTYQ==
-----END CERTIFICATE-----`
serverKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEArXtDyYQkzak4qncJP7OCWpyiDClFVq9rveGviWMuYCfcWF60
OPUnjZvn8IUwDqnLT+czVZBasZQcrSWYN6agiqkaDjpdalYC9xd6OyXYWRouu2sP
EDCz8bSz6WFp565VEKF0SXU5apA6jvJTmZfRzuaGXonT2e6+QVQ+mYrowAumYNIy
HPhD7aoWlCdQkraAHkX9azVipg7C7+YRGg1qGhxfbDVcw0IOmrWd4nItNKkI/rrx
GaPJi5OnoIN66rE7/Zpdqpa0YBbprffSetIU7d7bkGgK7h2PNDvZxst0glwtdXWm
u4LMxxJQkb4VFaDsUbQeeGARLSdIBKRZ0zvSdQIDAQABAoIBAF4sI8goq7HYwqIG
rEagM4rsrCrd3H4KC/qvoJJ7/JjGCp8OCddBfY8pquat5kCPe4aMgxlXm2P6evaj
CdZr5Ypf8Xz3we4PctyfKgMhsCfuRqAGpc6sIYJ8DY4LC2pxAExe2LlnoRtv39np
QeiGuaYPDbIUL6SGLVFZYgIHngFhbDYfL83q3Cb/PnivUGFvUVQCfRBUKO2d8KYq
TrVB5BWD2GrHor24ApQmci1OOqfbkIevkK6bk8HUfSZiZGI9LUQiPHMxi5k2x43J
nIwhZnW2N28dorKnWHg2vh7viGvinVRZ3MEyX150oCw/L6SYM4fqR6t2ZSBgNQHT
ZNoDtwECgYEA4lXMgtYqKuSlZ3TKfxAj03tJ/gbRdKcUCEGXEbdpY70tTu6KESZS
etid4Ut/sWEoPTJsgYiGbgJl571t1O8oR1UZYgh9hBGHLV6UEIt9n2PbExhE2vL3
SB7+LfO+tMvM4qKUBN+uy4GpU0NiyEEecw4x4S7MRSyHFRIDR7B6RV0CgYEAxDgS
mDaNUfSdfB5mXekLUJAwqeKRdL9RjXYaHbnoZ5kIwQ73tFikRwyTsLQwMhjE1l3z
MItTzIAyTf/BlK3dsp6bHTaT7hXIjHBsuKATN5qAuUpzTrg9+QaCawVSlQgNeF3a
iyfD4dVp66Bzn3gO757TWqmroBZ2e1owbAQvF/kCgYAKT/Jze6KMNcK7hfy78VZQ
imuCoXjlob8t6R8i9YJdwv7Pe9rakS5s3nXDEBePU2fr8eIzvK6zUHSoLF9WtlbV
eTEg4FYnsEzCam7AmjptCrWulwp8F1ng9ViLa3Gi9y4snU+1MSPbrdqzKnzTtvPW
Ni1bnzA7bp3w/dMcbxQDGQKBgB50hY5SiUS7LuZg4YqZ7UOn3aXAoMr6FvJZ7lvG
yyepPQ6aACBh0b2lWhcHIKPl7EdJdcGHHo6TJzusAqPNCKf8rh6upe9COkpx+K3/
SnxK4sffol4JgrTwKbXqsZKoGU8hYhZPKbwXn8UOtmN+AvN2N1/PDfBfDCzBJtrd
G2IhAoGBAN19976xAMDjKb2+wd/mQYA2fR7E8lodxdX3LDnblYmndTKY67nVo94M
FHPKZSN590HkFJ+wmChnOrqjtosY+N25CKMS7939EUIDrq+B+bYTWM/gcwdLXNUk
Rygw/078Z3ZDJamXmyez5WpeLFrrbmI8sLnBBmSjQvMb6vCEtQ2Z
-----END RSA PRIVATE KEY-----`
caCRT = `-----BEGIN CERTIFICATE-----
MIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0
QXV0aDAeFw0yMTAxMDIyMTIwNTVaFw0yMjA3MDIyMTMwNTJaMBMxETAPBgNVBAMT
CENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4Tiho5xW
AC15JRkMwfp3/TJwI2As7MY5dele5cmdr5bHAE+sRKqC+Ti88OJWCV5saoyax/1S
CjxJlQMZMl169P1QYJskKjdG2sdv6RLWLMgwSNRRjxp/Bw9dHdiEb9MjLgu28Jro
9peQkHcRHeMf5hM9WvlIJGrdzbC4hUehmqggcqgARainBkYjf0SwuWxHeu4nMqkp
Ak5tcSTLCjHfEFHZ9Te0TIPG5YkWocQKyeLgu4lvuU+DD2W2lym+YVUtRMGs1Env
k7p+N0DcGU26qfzZ2sF5ZXkqm7dBsGQB9pIxwc2Q8T1dCIyP9OQCKVILdc5aVFf1
cryQFHYzYNNZXFlIBims5VV5Mgfp8ESHQSue+v6n6ykecLEyKt1F1Y/MWY/nWUSI
8zdq83jdBAZVjo9MSthxVn57/06s/hQca65IpcTZV2gX0a+eRlAVqaRbAhL3LaZe
bYsW3WHKoUOftwemuep3nL51TzlXZVL7Oz/ClGaEOsnGG9KFO6jh+W768qC0zLQI
CdE7v2Zex98sZteHCg9fGJHIaYoF0aJG5P3WI5oZf2fy7UIYN9ADLFZiorCXAZEh
CSU6mDoRViZ4RGR9GZxbDZ9KYn7O8M/KCR72bkQg73TlMsk1zSXEw0MKLUjtsw6c
rZ0Jt8t3sRatHO3JrYHALMt9vZfyNCZp0IsCAwEAAaNFMEMwDgYDVR0PAQH/BAQD
AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFO1yCNAGr/zQTJIi8lw3
w5OiuBvMMA0GCSqGSIb3DQEBCwUAA4ICAQA6gCNuM7r8mnx674dm31GxBjQy5ZwB
7CxDzYEvL/oiZ3Tv3HlPfN2LAAsJUfGnghh9DOytenL2CTZWjl/emP5eijzmlP+9
zva5I6CIMCf/eDDVsRdO244t0o4uG7+At0IgSDM3bpVaVb4RHZNjEziYChsEYY8d
HK6iwuRSvFniV6yhR/Vj1Ymi9yZ5xclqseLXiQnUB0PkfIk23+7s42cXB16653fH
O/FsPyKBLiKJArizLYQc12aP3QOrYoYD9+fAzIIzew7A5C0aanZCGzkuFpO6TRlD
Tb7ry9Gf0DfPpCgxraH8tOcmnqp/ka3hjqo/SRnnTk0IFrmmLdarJvjD46rKwBo4
MjyAIR1mQ5j8GTlSFBmSgETOQ/EYvO3FPLmra1Fh7L+DvaVzTpqI9fG3TuyyY+Ri
Fby4ycTOGSZOe5Fh8lqkX5Y47mCUJ3zHzOA1vUJy2eTlMRGpu47Eb1++Vm6EzPUP
2EF5aD+zwcssh+atZvQbwxpgVqVcyLt91RSkKkmZQslh0rnlTb68yxvUnD3zw7So
o6TAf9UvwVMEvdLT9NnFd6hwi2jcNte/h538GJwXeBb8EkfpqLKpTKyicnOdkamZ
7E9zY8SHNRYMwB9coQ/W8NvufbCgkvOoLyMXk5edbXofXl3PhNGOlraWbghBnzf5
r3rwjFsQOoZotA==
-----END CERTIFICATE-----`
caKey = `-----BEGIN RSA PRIVATE KEY-----
MIIJKQIBAAKCAgEA4Tiho5xWAC15JRkMwfp3/TJwI2As7MY5dele5cmdr5bHAE+s
RKqC+Ti88OJWCV5saoyax/1SCjxJlQMZMl169P1QYJskKjdG2sdv6RLWLMgwSNRR
jxp/Bw9dHdiEb9MjLgu28Jro9peQkHcRHeMf5hM9WvlIJGrdzbC4hUehmqggcqgA
RainBkYjf0SwuWxHeu4nMqkpAk5tcSTLCjHfEFHZ9Te0TIPG5YkWocQKyeLgu4lv
uU+DD2W2lym+YVUtRMGs1Envk7p+N0DcGU26qfzZ2sF5ZXkqm7dBsGQB9pIxwc2Q
8T1dCIyP9OQCKVILdc5aVFf1cryQFHYzYNNZXFlIBims5VV5Mgfp8ESHQSue+v6n
6ykecLEyKt1F1Y/MWY/nWUSI8zdq83jdBAZVjo9MSthxVn57/06s/hQca65IpcTZ
V2gX0a+eRlAVqaRbAhL3LaZebYsW3WHKoUOftwemuep3nL51TzlXZVL7Oz/ClGaE
OsnGG9KFO6jh+W768qC0zLQICdE7v2Zex98sZteHCg9fGJHIaYoF0aJG5P3WI5oZ
f2fy7UIYN9ADLFZiorCXAZEhCSU6mDoRViZ4RGR9GZxbDZ9KYn7O8M/KCR72bkQg
73TlMsk1zSXEw0MKLUjtsw6crZ0Jt8t3sRatHO3JrYHALMt9vZfyNCZp0IsCAwEA
AQKCAgAV+ElERYbaI5VyufvVnFJCH75ypPoc6sVGLEq2jbFVJJcq/5qlZCC8oP1F
Xj7YUR6wUiDzK1Hqb7EZ2SCHGjlZVrCVi+y+NYAy7UuMZ+r+mVSkdhmypPoJPUVv
GOTqZ6VB46Cn3eSl0WknvoWr7bD555yPmEuiSc5zNy74yWEJTidEKAFGyknowcTK
sG+w1tAuPLcUKQ44DGB+rgEkcHL7C5EAa7upzx0C3RmZFB+dTAVyJdkBMbFuOhTS
sB7DLeTplR7/4mp9da7EQw51ZXC1DlZOEZt++4/desXsqATNAbva1OuzrLG7mMKe
N/PCBh/aERQcsCvgUmaXqGQgqN1Jhw8kbXnjZnVd9iE7TAh7ki3VqNy1OMgTwOex
bBYWaCqHuDYIxCjeW0qLJcn0cKQ13FVYrxgInf4Jp82SQht5b/zLL3IRZEyKcLJF
kL6g1wlmTUTUX0z8eZzlM0ZCrqtExjgElMO/rV971nyNV5WU8Og3NmE8/slqMrmJ
DlrQr9q0WJsDKj1IMe46EUM6ix7bbxC5NIfJ96dgdxZDn6ghjca6iZYqqUACvmUj
cq08s3R4Ouw9/87kn11wwGBx2yDueCwrjKEGc0RKjweGbwu0nBxOrkJ8JXz6bAv7
1OKfYaX3afI9B8x4uaiuRs38oBQlg9uAYFfl4HNBPuQikGLmsQKCAQEA8VjFOsaz
y6NMZzKXi7WZ48uu3ed5x3Kf6RyDr1WvQ1jkBMv9b6b8Gp1CRnPqviRBto9L8QAg
bCXZTqnXzn//brskmW8IZgqjAlf89AWa53piucu9/hgidrHRZobs5gTqev28uJdc
zcuw1g8c3nCpY9WeTjHODzX5NXYRLFpkazLfYa6c8Q9jZR4KKrpdM+66fxL0JlOd
7dN0oQtEqEAugsd3cwkZgvWhY4oM7FGErrZoDLy273ZdJzi/vU+dThyVzfD8Ab8u
VxxuobVMT/S608zbe+uaiUdov5s96OkCl87403UNKJBH+6LNb3rjBBLE9NPN5ET9
JLQMrYd+zj8jQwKCAQEA7uU5I9MOufo9bIgJqjY4Ie1+Ex9DZEMUYFAvGNCJCVcS
mwOdGF8AWzIavTLACmEDJO7t/OrBdoo4L7IEsCNjgA3WiIwIMiWUVqveAGUMEXr6
TRI5EolV6FTqqIP6AS+BAeBq7G1ELgsTrWNHh11rW3+3kBMuOCn77PUQ8WHwcq/r
teZcZn4Ewcr6P7cBODgVvnBPhe/J8xHS0HFVCeS1CvaiNYgees5yA80Apo9IPjDJ
YWawLjmH5wUBI5yDFVp067wjqJnoKPSoKwWkZXqUk+zgFXx5KT0gh/c5yh1frASp
q6oaYnHEVC5qj2SpT1GFLonTcrQUXiSkiUudvNu1GQKCAQEAmko+5GFtRe0ihgLQ
4S76r6diJli6AKil1Fg3U1r6zZpBQ1PJtJxTJQyN9w5Z7q6tF/GqAesrzxevQdvQ
rCImAPtA3ZofC2UXawMnIjWHHx6diNvYnV1+gtUQ4nO1dSOFZ5VZFcUmPiZO6boF
oaryj3FcX+71JcJCjEvrlKhA9Es0hXUkvfMxfs5if4he1zlyHpTWYr4oA4egUugq
P0mwskikc3VIyvEO+NyjgFxo72yLPkFSzemkidN8uKDyFqKtnlfGM7OuA2CY1WZa
3+67lXWshx9KzyJIs92iCYkU8EoPxtdYzyrV6efdX7x27v60zTOut5TnJJS6WiF6
Do5MkwKCAQAxoR9IyP0DN/BwzqYrXU42Bi+t603F04W1KJNQNWpyrUspNwv41yus
xnD1o0hwH41Wq+h3JZIBfV+E0RfWO9Pc84MBJQ5C1LnHc7cQH+3s575+Km3+4tcd
CB8j2R8kBeloKWYtLdn/Mr/ownpGreqyvIq2/LUaZ+Z1aMgXTYB1YwS16mCBzmZQ
mEl62RsAwe4KfSyYJ6OtwqMoOJMxFfliiLBULK4gVykqjvk2oQeiG+KKQJoTUFJi
dRCyhD5bPkqR+qjxyt+HOqSBI4/uoROi05AOBqjpH1DVzk+MJKQOiX1yM0l98CKY
Vng+x+vAla/0Zh+ucajVkgk4mKPxazdpAoIBAQC17vWk4KYJpF2RC3pKPcQ0PdiX
bN35YNlvyhkYlSfDNdyH3aDrGiycUyW2mMXUgEDFsLRxHMTL+zPC6efqO6sTAJDY
cBptsW4drW/qo8NTx3dNOisLkW+mGGJOR/w157hREFr29ymCVMYu/Z7fVWIeSpCq
p3u8YX8WTljrxwSczlGjvpM7uJx3SfYRM4TUoy+8wU8bK74LywLa5f60bQY6Dye0
Gqd9O6OoPfgcQlwjC5MiAofeqwPJvU0hQOPoehZyNLAmOCWXTYWaTP7lxO1r6+NE
M3hGYqW3W8Ixua71OskCypBZg/HVlIP/lzjRzdx+VOB2hbWVth2Iup/Z1egW
-----END RSA PRIVATE KEY-----`
caCRL = `-----BEGIN X509 CRL-----
MIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN
MjEwMTAyMjEzNDA1WhcNMjMwMTAyMjEzNDA1WjAkMCICEQC+l04DbHWMyC3fG09k
VXf+Fw0yMTAxMDIyMTM0MDVaoCMwITAfBgNVHSMEGDAWgBTtcgjQBq/80EySIvJc
N8OTorgbzDANBgkqhkiG9w0BAQsFAAOCAgEAEJ7z+uNc8sqtxlOhSdTGDzX/xput
E857kFQkSlMnU2whQ8c+XpYrBLA5vIZJNSSwohTpM4+zVBX/bJpmu3wqqaArRO9/
YcW5mQk9Anvb4WjQW1cHmtNapMTzoC9AiYt/OWPfy+P6JCgCr4Hy6LgQyIRL6bM9
VYTalolOm1qa4Y5cIeT7iHq/91mfaqo8/6MYRjLl8DOTROpmw8OS9bCXkzGKdCat
AbAzwkQUSauyoCQ10rpX+Y64w9ng3g4Dr20aCqPf5osaqplEJ2HTK8ljDTidlslv
9anQj8ax3Su89vI8+hK+YbfVQwrThabgdSjQsn+veyx8GlP8WwHLAQ379KjZjWg+
OlOSwBeU1vTdP0QcB8X5C2gVujAyuQekbaV86xzIBOj7vZdfHZ6ee30TZ2FKiMyg
7/N2OqW0w77ChsjB4MSHJCfuTgIeg62GzuZXLM+Q2Z9LBdtm4Byg+sm/P52adOEg
gVb2Zf4KSvsAmA0PIBlu449/QXUFcMxzLFy7mwTeZj2B4Ln0Hm0szV9f9R8MwMtB
SyLYxVH+mgqaR6Jkk22Q/yYyLPaELfafX5gp/AIXG8n0zxfVaTvK3auSgb1Q6ZLS
5QH9dSIsmZHlPq7GoSXmKpMdjUL8eaky/IMteioyXgsBiATzl5L2dsw6MTX3MDF0
QbDK+MzhmbKfDxs=
-----END X509 CRL-----`
client1Crt = `-----BEGIN CERTIFICATE-----
MIIEITCCAgmgAwIBAgIRAIppZHoj1hM80D7WzTEKLuAwDQYJKoZIhvcNAQELBQAw
EzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjEwMTAyMjEyMzEwWhcNMjIwNzAyMjEz
MDUxWjASMRAwDgYDVQQDEwdjbGllbnQxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAoKbYY9MdF2kF/nhBESIiZTdVYtA8XL9xrIZyDj9EnCiTxHiVbJtH
XVwszqSl5TRrotPmnmAQcX3r8OCk+z+RQZ0QQj257P3kG6q4rNnOcWCS5xEd20jP
yhQ3m+hMGfZsotNTQze1ochuQgLUN6IPyPxZkH22ia3jX4iu1eo/QxeLYHj1UHw4
3Cii9yE+j5kPUC21xmnrGKdUrB55NYLXHx6yTIqYR5znSOVB8oJi18/hwdZmH859
DHhm0Hx1HrS+jbjI3+CMorZJ3WUyNf+CkiVLD3xYutPbxzEpwiqkG/XYzLH0habT
cDcILo18n+o3jvem2KWBrDhyairjIDscwQIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC
A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBSJ5GIv
zIrE4ZSQt2+CGblKTDswizAfBgNVHSMEGDAWgBTtcgjQBq/80EySIvJcN8OTorgb
zDANBgkqhkiG9w0BAQsFAAOCAgEALh4f5GhvNYNou0Ab04iQBbLEdOu2RlbK1B5n
K9P/umYenBHMY/z6HT3+6tpcHsDuqE8UVdq3f3Gh4S2Gu9m8PRitT+cJ3gdo9Plm
3rD4ufn/s6rGg3ppydXcedm17492tbccUDWOBZw3IO/ASVq13WPgT0/Kev7cPq0k
sSdSNhVeXqx8Myc2/d+8GYyzbul2Kpfa7h9i24sK49E9ftnSmsIvngONo08eT1T0
3wAOyK2981LIsHaAWcneShKFLDB6LeXIT9oitOYhiykhFlBZ4M1GNlSNfhQ8IIQP
xbqMNXCLkW4/BtLhGEEcg0QVso6Kudl9rzgTfQknrdF7pHp6rS46wYUjoSyIY6dl
oLmnoAVJX36J3QPWelePI9e07X2wrTfiZWewwgw3KNRWjd6/zfPLe7GoqXnK1S2z
PT8qMfCaTwKTtUkzXuTFvQ8bAo2My/mS8FOcpkt2oQWeOsADHAUX7fz5BCoa2DL3
k/7Mh4gVT+JYZEoTwCFuYHgMWFWe98naqHi9lB4yR981p1QgXgxO7qBeipagKY1F
LlH1iwXUqZ3MZnkNA+4e1Fglsw3sa/rC+L98HnznJ/YbTfQbCP6aQ1qcOymrjMud
7MrFwqZjtd/SK4Qx1VpK6jGEAtPgWBTUS3p9ayg6lqjMBjsmySWfvRsDQbq6P5Ct
O/e3EH8=
-----END CERTIFICATE-----`
client1Key = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAoKbYY9MdF2kF/nhBESIiZTdVYtA8XL9xrIZyDj9EnCiTxHiV
bJtHXVwszqSl5TRrotPmnmAQcX3r8OCk+z+RQZ0QQj257P3kG6q4rNnOcWCS5xEd
20jPyhQ3m+hMGfZsotNTQze1ochuQgLUN6IPyPxZkH22ia3jX4iu1eo/QxeLYHj1
UHw43Cii9yE+j5kPUC21xmnrGKdUrB55NYLXHx6yTIqYR5znSOVB8oJi18/hwdZm
H859DHhm0Hx1HrS+jbjI3+CMorZJ3WUyNf+CkiVLD3xYutPbxzEpwiqkG/XYzLH0
habTcDcILo18n+o3jvem2KWBrDhyairjIDscwQIDAQABAoIBAEBSjVFqtbsp0byR
aXvyrtLX1Ng7h++at2jca85Ihq//jyqbHTje8zPuNAKI6eNbmb0YGr5OuEa4pD9N
ssDmMsKSoG/lRwwcm7h4InkSvBWpFShvMgUaohfHAHzsBYxfnh+TfULsi0y7c2n6
t/2OZcOTRkkUDIITnXYiw93ibHHv2Mv2bBDu35kGrcK+c2dN5IL5ZjTjMRpbJTe2
44RBJbdTxHBVSgoGBnugF+s2aEma6Ehsj70oyfoVpM6Aed5kGge0A5zA1JO7WCn9
Ay/DzlULRXHjJIoRWd2NKvx5n3FNppUc9vJh2plRHalRooZ2+MjSf8HmXlvG2Hpb
ScvmWgECgYEA1G+A/2KnxWsr/7uWIJ7ClcGCiNLdk17Pv3DZ3G4qUsU2ITftfIbb
tU0Q/b19na1IY8Pjy9ptP7t74/hF5kky97cf1FA8F+nMj/k4+wO8QDI8OJfzVzh9
PwielA5vbE+xmvis5Hdp8/od1Yrc/rPSy2TKtPFhvsqXjqoUmOAjDP8CgYEAwZjH
9dt1sc2lx/rMxihlWEzQ3JPswKW9/LJAmbRBoSWF9FGNjbX7uhWtXRKJkzb8ZAwa
88azluNo2oftbDD/+jw8b2cDgaJHlLAkSD4O1D1RthW7/LKD15qZ/oFsRb13NV85
ZNKtwslXGbfVNyGKUVFm7fVA8vBAOUey+LKDFj8CgYEAg8WWstOzVdYguMTXXuyb
ruEV42FJaDyLiSirOvxq7GTAKuLSQUg1yMRBIeQEo2X1XU0JZE3dLodRVhuO4EXP
g7Dn4X7Th9HSvgvNuIacowWGLWSz4Qp9RjhGhXhezUSx2nseY6le46PmFavJYYSR
4PBofMyt4PcyA6Cknh+KHmkCgYEAnTriG7ETE0a7v4DXUpB4TpCEiMCy5Xs2o8Z5
ZNva+W+qLVUWq+MDAIyechqeFSvxK6gRM69LJ96lx+XhU58wJiFJzAhT9rK/g+jS
bsHH9WOfu0xHkuHA5hgvvV2Le9B2wqgFyva4HJy82qxMxCu/VG/SMqyfBS9OWbb7
ibQhdq0CgYAl53LUWZsFSZIth1vux2LVOsI8C3X1oiXDGpnrdlQ+K7z57hq5EsRq
GC+INxwXbvKNqp5h0z2MvmKYPDlGVTgw8f8JjM7TkN17ERLcydhdRrMONUryZpo8
1xTob+8blyJgfxZUIAKbMbMbIiU0WAF0rfD/eJJwS4htOW/Hfv4TGA==
-----END RSA PRIVATE KEY-----`
// client 2 crt is revoked
client2Crt = `-----BEGIN CERTIFICATE-----
MIIEITCCAgmgAwIBAgIRAL6XTgNsdYzILd8bT2RVd/4wDQYJKoZIhvcNAQELBQAw
EzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjEwMTAyMjEyMzIwWhcNMjIwNzAyMjEz
MDUxWjASMRAwDgYDVQQDEwdjbGllbnQyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEA6xjW5KQR3/OFQtV5M75WINqQ4AzXSu6DhSz/yumaaQZP/UxY+6hi
jcrFzGo9MMie/Sza8DhkXOFAl2BelUubrOeB2cl+/Gr8OCyRi2Gv6j3zCsuN/4jQ
tNaoez/IbkDvI3l/ZpzBtnuNY2RiemGgHuORXHRVf3qVlsw+npBIRW5rM2HkO/xG
oZjeBErWVu390Lyn+Gvk2TqQDnkutWnxUC60/zPlHhXZ4BwaFAekbSnjsSDB1YFM
s8HwW4oBryoxdj3/+/qLrBHt75IdLw3T7/V1UDJQM3EvSQOr12w4egpldhtsC871
nnBQZeY6qA5feffIwwg/6lJm70o6S6OX6wIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC
A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBTB84v5
t9HqhLhMODbn6oYkEQt3KzAfBgNVHSMEGDAWgBTtcgjQBq/80EySIvJcN8OTorgb
zDANBgkqhkiG9w0BAQsFAAOCAgEALGtBCve5k8tToL3oLuXp/oSik6ovIB/zq4I/
4zNMYPU31+ZWz6aahysgx1JL1yqTa3Qm8o2tu52MbnV10dM7CIw7c/cYa+c+OPcG
5LF97kp13X+r2axy+CmwM86b4ILaDGs2Qyai6VB6k7oFUve+av5o7aUrNFpqGCJz
HWdtHZSVA3JMATzy0TfWanwkzreqfdw7qH0yZ9bDURlBKAVWrqnCstva9jRuv+AI
eqxr/4Ro986TFjJdoAP3Vr16CPg7/B6GA/KmsBWJrpeJdPWq4i2gpLKvYZoy89qD
mUZf34RbzcCtV4NvV1DadGnt4us0nvLrvS5rL2+2uWD09kZYq9RbLkvgzF/cY0fz
i7I1bi5XQ+alWe0uAk5ZZL/D+GTRYUX1AWwCqwJxmHrMxcskMyO9pXvLyuSWRDLo
YNBrbX9nLcfJzVCp+X+9sntTHjs4l6Cw+fLepJIgtgqdCHtbhTiv68vSM6cgb4br
6n2xrXRKuioiWFOrTSRr+oalZh8dGJ/xvwY8IbWknZAvml9mf1VvfE7Ma5P777QM
fsbYVTq0Y3R/5hIWsC3HA5z6MIM8L1oRe/YyhP3CTmrCHkVKyDOosGXpGz+JVcyo
cfYkY5A3yFKB2HaCwZSfwFmRhxkrYWGEbHv3Cd9YkZs1J3hNhGFZyVMC9Uh0S85a
6zdDidU=
-----END CERTIFICATE-----`
client2Key = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA6xjW5KQR3/OFQtV5M75WINqQ4AzXSu6DhSz/yumaaQZP/UxY
+6hijcrFzGo9MMie/Sza8DhkXOFAl2BelUubrOeB2cl+/Gr8OCyRi2Gv6j3zCsuN
/4jQtNaoez/IbkDvI3l/ZpzBtnuNY2RiemGgHuORXHRVf3qVlsw+npBIRW5rM2Hk
O/xGoZjeBErWVu390Lyn+Gvk2TqQDnkutWnxUC60/zPlHhXZ4BwaFAekbSnjsSDB
1YFMs8HwW4oBryoxdj3/+/qLrBHt75IdLw3T7/V1UDJQM3EvSQOr12w4egpldhts
C871nnBQZeY6qA5feffIwwg/6lJm70o6S6OX6wIDAQABAoIBAFatstVb1KdQXsq0
cFpui8zTKOUiduJOrDkWzTygAmlEhYtrccdfXu7OWz0x0lvBLDVGK3a0I/TGrAzj
4BuFY+FM/egxTVt9in6fmA3et4BS1OAfCryzUdfK6RV//8L+t+zJZ/qKQzWnugpy
QYjDo8ifuMFwtvEoXizaIyBNLAhEp9hnrv+Tyi2O2gahPvCHsD48zkyZRCHYRstD
NH5cIrwz9/RJgPO1KI+QsJE7Nh7stR0sbr+5TPU4fnsL2mNhMUF2TJrwIPrc1yp+
YIUjdnh3SO88j4TQT3CIrWi8i4pOy6N0dcVn3gpCRGaqAKyS2ZYUj+yVtLO4KwxZ
SZ1lNvECgYEA78BrF7f4ETfWSLcBQ3qxfLs7ibB6IYo2x25685FhZjD+zLXM1AKb
FJHEXUm3mUYrFJK6AFEyOQnyGKBOLs3S6oTAswMPbTkkZeD1Y9O6uv0AHASLZnK6
pC6ub0eSRF5LUyTQ55Jj8D7QsjXJueO8v+G5ihWhNSN9tB2UA+8NBmkCgYEA+weq
cvoeMIEMBQHnNNLy35bwfqrceGyPIRBcUIvzQfY1vk7KW6DYOUzC7u+WUzy/hA52
DjXVVhua2eMQ9qqtOav7djcMc2W9RbLowxvno7K5qiCss013MeWk64TCWy+WMp5A
AVAtOliC3hMkIKqvR2poqn+IBTh1449agUJQqTMCgYEAu06IHGq1GraV6g9XpGF5
wqoAlMzUTdnOfDabRilBf/YtSr+J++ThRcuwLvXFw7CnPZZ4TIEjDJ7xjj3HdxeE
fYYjineMmNd40UNUU556F1ZLvJfsVKizmkuCKhwvcMx+asGrmA+tlmds4p3VMS50
KzDtpKzLWlmU/p/RINWlRmkCgYBy0pHTn7aZZx2xWKqCDg+L2EXPGqZX6wgZDpu7
OBifzlfM4ctL2CmvI/5yPmLbVgkgBWFYpKUdiujsyyEiQvWTUKhn7UwjqKDHtcsk
G6p7xS+JswJrzX4885bZJ9Oi1AR2yM3sC9l0O7I4lDbNPmWIXBLeEhGMmcPKv/Kc
91Ff4wKBgQCF3ur+Vt0PSU0ucrPVHjCe7tqazm0LJaWbPXL1Aw0pzdM2EcNcW/MA
w0kqpr7MgJ94qhXCBcVcfPuFN9fBOadM3UBj1B45Cz3pptoK+ScI8XKno6jvVK/p
xr5cb9VBRBtB9aOKVfuRhpatAfS2Pzm2Htae9lFn7slGPUmu2hkjDw==
-----END RSA PRIVATE KEY-----`
)
func TestLoadCertificate(t *testing.T) {
caCrtPath := filepath.Join(os.TempDir(), "testca.crt")
caCrlPath := filepath.Join(os.TempDir(), "testcrl.crt")
certPath := filepath.Join(os.TempDir(), "test.crt")
keyPath := filepath.Join(os.TempDir(), "test.key")
err := ioutil.WriteFile(caCrtPath, []byte(caCRT), os.ModePerm)
assert.NoError(t, err)
err = ioutil.WriteFile(caCrlPath, []byte(caCRL), os.ModePerm)
assert.NoError(t, err)
err = ioutil.WriteFile(certPath, []byte(serverCert), os.ModePerm)
assert.NoError(t, err)
err = ioutil.WriteFile(keyPath, []byte(serverKey), os.ModePerm)
assert.NoError(t, err)
certManager, err := NewCertManager(certPath, keyPath, configDir, logSenderTest)
assert.NoError(t, err)
certFunc := certManager.GetCertificateFunc()
if assert.NotNil(t, certFunc) {
hello := &tls.ClientHelloInfo{
ServerName: "localhost",
CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305},
}
cert, err := certFunc(hello)
assert.NoError(t, err)
assert.Equal(t, certManager.cert, cert)
}
certManager.SetCACertificates(nil)
err = certManager.LoadRootCAs()
assert.NoError(t, err)
certManager.SetCACertificates([]string{""})
err = certManager.LoadRootCAs()
assert.Error(t, err)
certManager.SetCACertificates([]string{"invalid"})
err = certManager.LoadRootCAs()
assert.Error(t, err)
// laoding the key as root CA must fail
certManager.SetCACertificates([]string{keyPath})
err = certManager.LoadRootCAs()
assert.Error(t, err)
certManager.SetCACertificates([]string{certPath})
err = certManager.LoadRootCAs()
assert.NoError(t, err)
rootCa := certManager.GetRootCAs()
assert.NotNil(t, rootCa)
err = certManager.Reload()
assert.NoError(t, err)
certManager.SetCARevocationLists(nil)
err = certManager.LoadCRLs()
assert.NoError(t, err)
certManager.SetCARevocationLists([]string{""})
err = certManager.LoadCRLs()
assert.Error(t, err)
certManager.SetCARevocationLists([]string{"invalid crl"})
err = certManager.LoadCRLs()
assert.Error(t, err)
// this is not a crl and must fail
certManager.SetCARevocationLists([]string{caCrtPath})
err = certManager.LoadCRLs()
assert.Error(t, err)
certManager.SetCARevocationLists([]string{caCrlPath})
err = certManager.LoadCRLs()
assert.NoError(t, err)
crt, err := tls.X509KeyPair([]byte(caCRT), []byte(caKey))
assert.NoError(t, err)
x509CAcrt, err := x509.ParseCertificate(crt.Certificate[0])
assert.NoError(t, err)
crt, err = tls.X509KeyPair([]byte(client1Crt), []byte(client1Key))
assert.NoError(t, err)
x509crt, err := x509.ParseCertificate(crt.Certificate[0])
if assert.NoError(t, err) {
assert.False(t, certManager.IsRevoked(x509crt, x509CAcrt))
}
crt, err = tls.X509KeyPair([]byte(client2Crt), []byte(client2Key))
assert.NoError(t, err)
x509crt, err = x509.ParseCertificate(crt.Certificate[0])
if assert.NoError(t, err) {
assert.True(t, certManager.IsRevoked(x509crt, x509CAcrt))
}
assert.True(t, certManager.IsRevoked(nil, nil))
err = os.Remove(caCrlPath)
assert.NoError(t, err)
err = certManager.Reload()
assert.Error(t, err)
err = os.Remove(certPath)
assert.NoError(t, err)
err = os.Remove(keyPath)
assert.NoError(t, err)
err = certManager.Reload()
assert.Error(t, err)
err = os.Remove(caCrtPath)
assert.NoError(t, err)
}
func TestLoadInvalidCert(t *testing.T) {
certManager, err := NewCertManager("test.crt", "test.key", configDir, logSenderTest)
assert.Error(t, err)
assert.Nil(t, certManager)
}

View File

@@ -1,303 +0,0 @@
package common
import (
"errors"
"path"
"sync"
"sync/atomic"
"time"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/metrics"
"github.com/drakkan/sftpgo/vfs"
)
var (
// ErrTransferClosed defines the error returned for a closed transfer
ErrTransferClosed = errors.New("transfer already closed")
)
// BaseTransfer contains protocols common transfer details for an upload or a download.
type BaseTransfer struct { //nolint:maligned
ID uint64
BytesSent int64
BytesReceived int64
Fs vfs.Fs
File vfs.File
Connection *BaseConnection
cancelFn func()
fsPath string
requestPath string
start time.Time
MaxWriteSize int64
MinWriteOffset int64
InitialSize int64
isNewFile bool
transferType int
AbortTransfer int32
sync.Mutex
ErrTransfer error
}
// NewBaseTransfer returns a new BaseTransfer and adds it to the given connection
func NewBaseTransfer(file vfs.File, conn *BaseConnection, cancelFn func(), fsPath, requestPath string, transferType int,
minWriteOffset, initialSize, maxWriteSize int64, isNewFile bool, fs vfs.Fs) *BaseTransfer {
t := &BaseTransfer{
ID: conn.GetTransferID(),
File: file,
Connection: conn,
cancelFn: cancelFn,
fsPath: fsPath,
start: time.Now(),
transferType: transferType,
MinWriteOffset: minWriteOffset,
InitialSize: initialSize,
isNewFile: isNewFile,
requestPath: requestPath,
BytesSent: 0,
BytesReceived: 0,
MaxWriteSize: maxWriteSize,
AbortTransfer: 0,
Fs: fs,
}
conn.AddTransfer(t)
return t
}
// GetID returns the transfer ID
func (t *BaseTransfer) GetID() uint64 {
return t.ID
}
// GetType returns the transfer type
func (t *BaseTransfer) GetType() int {
return t.transferType
}
// GetSize returns the transferred size
func (t *BaseTransfer) GetSize() int64 {
if t.transferType == TransferDownload {
return atomic.LoadInt64(&t.BytesSent)
}
return atomic.LoadInt64(&t.BytesReceived)
}
// GetStartTime returns the start time
func (t *BaseTransfer) GetStartTime() time.Time {
return t.start
}
// SignalClose signals that the transfer should be closed.
// For same protocols, for example WebDAV, we have no
// access to the network connection, so we use this method
// to make the next read or write to fail
func (t *BaseTransfer) SignalClose() {
atomic.StoreInt32(&(t.AbortTransfer), 1)
}
// GetVirtualPath returns the transfer virtual path
func (t *BaseTransfer) GetVirtualPath() string {
return t.requestPath
}
// GetFsPath returns the transfer filesystem path
func (t *BaseTransfer) GetFsPath() string {
return t.fsPath
}
// GetRealFsPath returns the real transfer filesystem path.
// If atomic uploads are enabled this differ from fsPath
func (t *BaseTransfer) GetRealFsPath(fsPath string) string {
if fsPath == t.GetFsPath() {
if t.File != nil {
return t.File.Name()
}
return t.fsPath
}
return ""
}
// SetCancelFn sets the cancel function for the transfer
func (t *BaseTransfer) SetCancelFn(cancelFn func()) {
t.cancelFn = cancelFn
}
// Truncate changes the size of the opened file.
// Supported for local fs only
func (t *BaseTransfer) Truncate(fsPath string, size int64) (int64, error) {
if fsPath == t.GetFsPath() {
if t.File != nil {
initialSize := t.InitialSize
err := t.File.Truncate(size)
if err == nil {
t.Lock()
t.InitialSize = size
if t.MaxWriteSize > 0 {
sizeDiff := initialSize - size
t.MaxWriteSize += sizeDiff
metrics.TransferCompleted(atomic.LoadInt64(&t.BytesSent), atomic.LoadInt64(&t.BytesReceived), t.transferType, t.ErrTransfer)
atomic.StoreInt64(&t.BytesReceived, 0)
}
t.Unlock()
}
t.Connection.Log(logger.LevelDebug, "file %#v truncated to size %v max write size %v new initial size %v err: %v",
fsPath, size, t.MaxWriteSize, t.InitialSize, err)
return initialSize, err
}
if size == 0 && atomic.LoadInt64(&t.BytesSent) == 0 {
// for cloud providers the file is always truncated to zero, we don't support append/resume for uploads
return 0, nil
}
return 0, ErrOpUnsupported
}
return 0, errTransferMismatch
}
// TransferError is called if there is an unexpected error.
// For example network or client issues
func (t *BaseTransfer) TransferError(err error) {
t.Lock()
defer t.Unlock()
if t.ErrTransfer != nil {
return
}
t.ErrTransfer = err
if t.cancelFn != nil {
t.cancelFn()
}
elapsed := time.Since(t.start).Nanoseconds() / 1000000
t.Connection.Log(logger.LevelWarn, "Unexpected error for transfer, path: %#v, error: \"%v\" bytes sent: %v, "+
"bytes received: %v transfer running since %v ms", t.fsPath, t.ErrTransfer, atomic.LoadInt64(&t.BytesSent),
atomic.LoadInt64(&t.BytesReceived), elapsed)
}
func (t *BaseTransfer) getUploadFileSize() (int64, error) {
var fileSize int64
info, err := t.Fs.Stat(t.fsPath)
if err == nil {
fileSize = info.Size()
}
if vfs.IsCryptOsFs(t.Fs) && t.ErrTransfer != nil {
errDelete := t.Connection.Fs.Remove(t.fsPath, false)
if errDelete != nil {
t.Connection.Log(logger.LevelWarn, "error removing partial crypto file %#v: %v", t.fsPath, errDelete)
}
}
return fileSize, err
}
// Close it is called when the transfer is completed.
// It logs the transfer info, updates the user quota (for uploads)
// and executes any defined action.
// If there is an error no action will be executed and, in atomic mode,
// we try to delete the temporary file
func (t *BaseTransfer) Close() error {
defer t.Connection.RemoveTransfer(t)
var err error
numFiles := 0
if t.isNewFile {
numFiles = 1
}
metrics.TransferCompleted(atomic.LoadInt64(&t.BytesSent), atomic.LoadInt64(&t.BytesReceived), t.transferType, t.ErrTransfer)
if t.ErrTransfer == ErrQuotaExceeded && t.File != nil {
// if quota is exceeded we try to remove the partial file for uploads to local filesystem
err = t.Connection.Fs.Remove(t.File.Name(), false)
if err == nil {
numFiles--
atomic.StoreInt64(&t.BytesReceived, 0)
t.MinWriteOffset = 0
}
t.Connection.Log(logger.LevelWarn, "upload denied due to space limit, delete temporary file: %#v, deletion error: %v",
t.File.Name(), err)
} else if t.transferType == TransferUpload && t.File != nil && t.File.Name() != t.fsPath {
if t.ErrTransfer == nil || Config.UploadMode == UploadModeAtomicWithResume {
err = t.Connection.Fs.Rename(t.File.Name(), t.fsPath)
t.Connection.Log(logger.LevelDebug, "atomic upload completed, rename: %#v -> %#v, error: %v",
t.File.Name(), t.fsPath, err)
} else {
err = t.Connection.Fs.Remove(t.File.Name(), false)
t.Connection.Log(logger.LevelWarn, "atomic upload completed with error: \"%v\", delete temporary file: %#v, "+
"deletion error: %v", t.ErrTransfer, t.File.Name(), err)
if err == nil {
numFiles--
atomic.StoreInt64(&t.BytesReceived, 0)
t.MinWriteOffset = 0
}
}
}
elapsed := time.Since(t.start).Nanoseconds() / 1000000
if t.transferType == TransferDownload {
logger.TransferLog(downloadLogSender, t.fsPath, elapsed, atomic.LoadInt64(&t.BytesSent), t.Connection.User.Username,
t.Connection.ID, t.Connection.protocol)
action := newActionNotification(&t.Connection.User, operationDownload, t.fsPath, "", "", t.Connection.protocol,
atomic.LoadInt64(&t.BytesSent), t.ErrTransfer)
go actionHandler.Handle(action) //nolint:errcheck
} else {
fileSize := atomic.LoadInt64(&t.BytesReceived) + t.MinWriteOffset
if statSize, err := t.getUploadFileSize(); err == nil {
fileSize = statSize
}
t.Connection.Log(logger.LevelDebug, "uploaded file size %v", fileSize)
t.updateQuota(numFiles, fileSize)
logger.TransferLog(uploadLogSender, t.fsPath, elapsed, atomic.LoadInt64(&t.BytesReceived), t.Connection.User.Username,
t.Connection.ID, t.Connection.protocol)
action := newActionNotification(&t.Connection.User, operationUpload, t.fsPath, "", "", t.Connection.protocol,
fileSize, t.ErrTransfer)
go actionHandler.Handle(action) //nolint:errcheck
}
if t.ErrTransfer != nil {
t.Connection.Log(logger.LevelWarn, "transfer error: %v, path: %#v", t.ErrTransfer, t.fsPath)
if err == nil {
err = t.ErrTransfer
}
}
return err
}
func (t *BaseTransfer) updateQuota(numFiles int, fileSize int64) bool {
// S3 uploads are atomic, if there is an error nothing is uploaded
if t.File == nil && t.ErrTransfer != nil {
return false
}
sizeDiff := fileSize - t.InitialSize
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
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
}
return true
}
return false
}
// HandleThrottle manage bandwidth throttling
func (t *BaseTransfer) HandleThrottle() {
var wantedBandwidth int64
var trasferredBytes int64
if t.transferType == TransferDownload {
wantedBandwidth = t.Connection.User.DownloadBandwidth
trasferredBytes = atomic.LoadInt64(&t.BytesSent)
} else {
wantedBandwidth = t.Connection.User.UploadBandwidth
trasferredBytes = atomic.LoadInt64(&t.BytesReceived)
}
if wantedBandwidth > 0 {
// real and wanted elapsed as milliseconds, bytes as kilobytes
realElapsed := time.Since(t.start).Nanoseconds() / 1000000
// trasferredBytes / 1024 = KB/s, we multiply for 1000 to get milliseconds
wantedElapsed := 1000 * (trasferredBytes / 1024) / wantedBandwidth
if wantedElapsed > realElapsed {
toSleep := time.Duration(wantedElapsed - realElapsed)
time.Sleep(toSleep * time.Millisecond)
}
}
}

View File

@@ -1,276 +0,0 @@
package common
import (
"errors"
"io/ioutil"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/kms"
"github.com/drakkan/sftpgo/vfs"
)
func TestTransferUpdateQuota(t *testing.T) {
conn := NewBaseConnection("", ProtocolSFTP, dataprovider.User{}, nil)
transfer := BaseTransfer{
Connection: conn,
transferType: TransferUpload,
BytesReceived: 123,
Fs: vfs.NewOsFs("", os.TempDir(), nil),
}
errFake := errors.New("fake error")
transfer.TransferError(errFake)
assert.False(t, transfer.updateQuota(1, 0))
err := transfer.Close()
if assert.Error(t, err) {
assert.EqualError(t, err, errFake.Error())
}
mappedPath := filepath.Join(os.TempDir(), "vdir")
vdirPath := "/vdir"
conn.User.VirtualFolders = append(conn.User.VirtualFolders, vfs.VirtualFolder{
BaseVirtualFolder: vfs.BaseVirtualFolder{
MappedPath: mappedPath,
},
VirtualPath: vdirPath,
QuotaFiles: -1,
QuotaSize: -1,
})
transfer.ErrTransfer = nil
transfer.BytesReceived = 1
transfer.requestPath = "/vdir/file"
assert.True(t, transfer.updateQuota(1, 0))
err = transfer.Close()
assert.NoError(t, err)
}
func TestTransferThrottling(t *testing.T) {
u := dataprovider.User{
Username: "test",
UploadBandwidth: 50,
DownloadBandwidth: 40,
}
fs := vfs.NewOsFs("", os.TempDir(), nil)
testFileSize := int64(131072)
wantedUploadElapsed := 1000 * (testFileSize / 1024) / u.UploadBandwidth
wantedDownloadElapsed := 1000 * (testFileSize / 1024) / u.DownloadBandwidth
// some tolerance
wantedUploadElapsed -= wantedDownloadElapsed / 10
wantedDownloadElapsed -= wantedDownloadElapsed / 10
conn := NewBaseConnection("id", ProtocolSCP, u, nil)
transfer := NewBaseTransfer(nil, conn, nil, "", "", TransferUpload, 0, 0, 0, true, fs)
transfer.BytesReceived = testFileSize
transfer.Connection.UpdateLastActivity()
startTime := transfer.Connection.GetLastActivity()
transfer.HandleThrottle()
elapsed := time.Since(startTime).Nanoseconds() / 1000000
assert.GreaterOrEqual(t, elapsed, wantedUploadElapsed, "upload bandwidth throttling not respected")
err := transfer.Close()
assert.NoError(t, err)
transfer = NewBaseTransfer(nil, conn, nil, "", "", TransferDownload, 0, 0, 0, true, fs)
transfer.BytesSent = testFileSize
transfer.Connection.UpdateLastActivity()
startTime = transfer.Connection.GetLastActivity()
transfer.HandleThrottle()
elapsed = time.Since(startTime).Nanoseconds() / 1000000
assert.GreaterOrEqual(t, elapsed, wantedDownloadElapsed, "download bandwidth throttling not respected")
err = transfer.Close()
assert.NoError(t, err)
}
func TestRealPath(t *testing.T) {
testFile := filepath.Join(os.TempDir(), "afile.txt")
fs := vfs.NewOsFs("123", os.TempDir(), nil)
u := dataprovider.User{
Username: "user",
HomeDir: os.TempDir(),
}
u.Permissions = make(map[string][]string)
u.Permissions["/"] = []string{dataprovider.PermAny}
file, err := os.Create(testFile)
require.NoError(t, err)
conn := NewBaseConnection(fs.ConnectionID(), ProtocolSFTP, u, fs)
transfer := NewBaseTransfer(file, conn, nil, testFile, "/transfer_test_file", TransferUpload, 0, 0, 0, true, fs)
rPath := transfer.GetRealFsPath(testFile)
assert.Equal(t, testFile, rPath)
rPath = conn.getRealFsPath(testFile)
assert.Equal(t, testFile, rPath)
err = transfer.Close()
assert.NoError(t, err)
err = file.Close()
assert.NoError(t, err)
transfer.File = nil
rPath = transfer.GetRealFsPath(testFile)
assert.Equal(t, testFile, rPath)
rPath = transfer.GetRealFsPath("")
assert.Empty(t, rPath)
err = os.Remove(testFile)
assert.NoError(t, err)
assert.Len(t, conn.GetTransfers(), 0)
}
func TestTruncate(t *testing.T) {
testFile := filepath.Join(os.TempDir(), "transfer_test_file")
fs := vfs.NewOsFs("123", os.TempDir(), nil)
u := dataprovider.User{
Username: "user",
HomeDir: os.TempDir(),
}
u.Permissions = make(map[string][]string)
u.Permissions["/"] = []string{dataprovider.PermAny}
file, err := os.Create(testFile)
if !assert.NoError(t, err) {
assert.FailNow(t, "unable to open test file")
}
_, err = file.Write([]byte("hello"))
assert.NoError(t, err)
conn := NewBaseConnection(fs.ConnectionID(), ProtocolSFTP, u, fs)
transfer := NewBaseTransfer(file, conn, nil, testFile, "/transfer_test_file", TransferUpload, 0, 5, 100, false, fs)
err = conn.SetStat(testFile, "/transfer_test_file", &StatAttributes{
Size: 2,
Flags: StatAttrSize,
})
assert.NoError(t, err)
assert.Equal(t, int64(103), transfer.MaxWriteSize)
err = transfer.Close()
assert.NoError(t, err)
err = file.Close()
assert.NoError(t, err)
fi, err := os.Stat(testFile)
if assert.NoError(t, err) {
assert.Equal(t, int64(2), fi.Size())
}
transfer = NewBaseTransfer(file, conn, nil, testFile, "/transfer_test_file", TransferUpload, 0, 0, 100, true, fs)
// file.Stat will fail on a closed file
err = conn.SetStat(testFile, "/transfer_test_file", &StatAttributes{
Size: 2,
Flags: StatAttrSize,
})
assert.Error(t, err)
err = transfer.Close()
assert.NoError(t, err)
transfer = NewBaseTransfer(nil, conn, nil, testFile, "", TransferUpload, 0, 0, 0, true, fs)
_, err = transfer.Truncate("mismatch", 0)
assert.EqualError(t, err, errTransferMismatch.Error())
_, err = transfer.Truncate(testFile, 0)
assert.NoError(t, err)
_, err = transfer.Truncate(testFile, 1)
assert.EqualError(t, err, ErrOpUnsupported.Error())
err = transfer.Close()
assert.NoError(t, err)
err = os.Remove(testFile)
assert.NoError(t, err)
assert.Len(t, conn.GetTransfers(), 0)
}
func TestTransferErrors(t *testing.T) {
isCancelled := false
cancelFn := func() {
isCancelled = true
}
testFile := filepath.Join(os.TempDir(), "transfer_test_file")
fs := vfs.NewOsFs("id", os.TempDir(), nil)
u := dataprovider.User{
Username: "test",
HomeDir: os.TempDir(),
}
err := ioutil.WriteFile(testFile, []byte("test data"), os.ModePerm)
assert.NoError(t, err)
file, err := os.Open(testFile)
if !assert.NoError(t, err) {
assert.FailNow(t, "unable to open test file")
}
conn := NewBaseConnection("id", ProtocolSFTP, u, fs)
transfer := NewBaseTransfer(file, conn, nil, testFile, "/transfer_test_file", TransferUpload, 0, 0, 0, true, fs)
assert.Nil(t, transfer.cancelFn)
assert.Equal(t, testFile, transfer.GetFsPath())
transfer.SetCancelFn(cancelFn)
errFake := errors.New("err fake")
transfer.BytesReceived = 9
transfer.TransferError(ErrQuotaExceeded)
assert.True(t, isCancelled)
transfer.TransferError(errFake)
assert.Error(t, transfer.ErrTransfer, ErrQuotaExceeded.Error())
// the file is closed from the embedding struct before to call close
err = file.Close()
assert.NoError(t, err)
err = transfer.Close()
if assert.Error(t, err) {
assert.Error(t, err, ErrQuotaExceeded.Error())
}
assert.NoFileExists(t, testFile)
err = ioutil.WriteFile(testFile, []byte("test data"), os.ModePerm)
assert.NoError(t, err)
file, err = os.Open(testFile)
if !assert.NoError(t, err) {
assert.FailNow(t, "unable to open test file")
}
fsPath := filepath.Join(os.TempDir(), "test_file")
transfer = NewBaseTransfer(file, conn, nil, fsPath, "/test_file", TransferUpload, 0, 0, 0, true, fs)
transfer.BytesReceived = 9
transfer.TransferError(errFake)
assert.Error(t, transfer.ErrTransfer, errFake.Error())
// the file is closed from the embedding struct before to call close
err = file.Close()
assert.NoError(t, err)
err = transfer.Close()
if assert.Error(t, err) {
assert.Error(t, err, errFake.Error())
}
assert.NoFileExists(t, testFile)
err = ioutil.WriteFile(testFile, []byte("test data"), os.ModePerm)
assert.NoError(t, err)
file, err = os.Open(testFile)
if !assert.NoError(t, err) {
assert.FailNow(t, "unable to open test file")
}
transfer = NewBaseTransfer(file, conn, nil, fsPath, "/test_file", TransferUpload, 0, 0, 0, true, fs)
transfer.BytesReceived = 9
// the file is closed from the embedding struct before to call close
err = file.Close()
assert.NoError(t, err)
err = transfer.Close()
assert.NoError(t, err)
assert.NoFileExists(t, testFile)
assert.FileExists(t, fsPath)
err = os.Remove(fsPath)
assert.NoError(t, err)
assert.Len(t, conn.GetTransfers(), 0)
}
func TestRemovePartialCryptoFile(t *testing.T) {
testFile := filepath.Join(os.TempDir(), "transfer_test_file")
fs, err := vfs.NewCryptFs("id", os.TempDir(), vfs.CryptFsConfig{Passphrase: kms.NewPlainSecret("secret")})
require.NoError(t, err)
u := dataprovider.User{
Username: "test",
HomeDir: os.TempDir(),
}
conn := NewBaseConnection(fs.ConnectionID(), ProtocolSFTP, u, fs)
transfer := NewBaseTransfer(nil, conn, nil, testFile, "/transfer_test_file", TransferUpload, 0, 0, 0, true, fs)
transfer.ErrTransfer = errors.New("test error")
_, err = transfer.getUploadFileSize()
assert.Error(t, err)
err = ioutil.WriteFile(testFile, []byte("test data"), os.ModePerm)
assert.NoError(t, err)
size, err := transfer.getUploadFileSize()
assert.NoError(t, err)
assert.Equal(t, int64(9), size)
assert.NoFileExists(t, testFile)
}

View File

@@ -1,885 +0,0 @@
// Package config manages the configuration
package config
import (
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/spf13/viper"
"github.com/drakkan/sftpgo/common"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/ftpd"
"github.com/drakkan/sftpgo/httpclient"
"github.com/drakkan/sftpgo/httpd"
"github.com/drakkan/sftpgo/kms"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/sftpd"
"github.com/drakkan/sftpgo/telemetry"
"github.com/drakkan/sftpgo/utils"
"github.com/drakkan/sftpgo/version"
"github.com/drakkan/sftpgo/webdavd"
)
const (
logSender = "config"
// configName defines the name for config file.
// This name does not include the extension, viper will search for files
// with supported extensions such as "sftpgo.json", "sftpgo.yaml" and so on
configName = "sftpgo"
// ConfigEnvPrefix defines a prefix that environment variables will use
configEnvPrefix = "sftpgo"
)
var (
globalConf globalConfig
defaultSFTPDBanner = fmt.Sprintf("SFTPGo_%v", version.Get().Version)
defaultFTPDBanner = fmt.Sprintf("SFTPGo %v ready", version.Get().Version)
defaultSFTPDBinding = sftpd.Binding{
Address: "",
Port: 2022,
ApplyProxyConfig: true,
}
defaultFTPDBinding = ftpd.Binding{
Address: "",
Port: 0,
ApplyProxyConfig: true,
TLSMode: 0,
ForcePassiveIP: "",
ClientAuthType: 0,
}
defaultWebDAVDBinding = webdavd.Binding{
Address: "",
Port: 0,
EnableHTTPS: false,
ClientAuthType: 0,
}
defaultHTTPDBinding = httpd.Binding{
Address: "127.0.0.1",
Port: 8080,
EnableWebAdmin: true,
EnableHTTPS: false,
ClientAuthType: 0,
}
)
type globalConfig struct {
Common common.Configuration `json:"common" mapstructure:"common"`
SFTPD sftpd.Configuration `json:"sftpd" mapstructure:"sftpd"`
FTPD ftpd.Configuration `json:"ftpd" mapstructure:"ftpd"`
WebDAVD webdavd.Configuration `json:"webdavd" mapstructure:"webdavd"`
ProviderConf dataprovider.Config `json:"data_provider" mapstructure:"data_provider"`
HTTPDConfig httpd.Conf `json:"httpd" mapstructure:"httpd"`
HTTPConfig httpclient.Config `json:"http" mapstructure:"http"`
KMSConfig kms.Configuration `json:"kms" mapstructure:"kms"`
TelemetryConfig telemetry.Conf `json:"telemetry" mapstructure:"telemetry"`
}
func init() {
Init()
}
// Init initializes the global configuration.
// It is not supposed to be called outside of this package.
// It is exported to minimize refactoring efforts. Will eventually disappear.
func Init() {
// create a default configuration to use if no config file is provided
globalConf = globalConfig{
Common: common.Configuration{
IdleTimeout: 15,
UploadMode: 0,
Actions: common.ProtocolActions{
ExecuteOn: []string{},
Hook: "",
},
SetstatMode: 0,
ProxyProtocol: 0,
ProxyAllowed: []string{},
PostConnectHook: "",
MaxTotalConnections: 0,
DefenderConfig: common.DefenderConfig{
Enabled: false,
BanTime: 30,
BanTimeIncrement: 50,
Threshold: 15,
ScoreInvalid: 2,
ScoreValid: 1,
ObservationTime: 30,
EntriesSoftLimit: 100,
EntriesHardLimit: 150,
SafeListFile: "",
BlockListFile: "",
},
},
SFTPD: sftpd.Configuration{
Banner: defaultSFTPDBanner,
Bindings: []sftpd.Binding{defaultSFTPDBinding},
MaxAuthTries: 0,
HostKeys: []string{},
KexAlgorithms: []string{},
Ciphers: []string{},
MACs: []string{},
TrustedUserCAKeys: []string{},
LoginBannerFile: "",
EnabledSSHCommands: sftpd.GetDefaultSSHCommands(),
KeyboardInteractiveHook: "",
PasswordAuthentication: true,
},
FTPD: ftpd.Configuration{
Bindings: []ftpd.Binding{defaultFTPDBinding},
Banner: defaultFTPDBanner,
BannerFile: "",
ActiveTransfersPortNon20: true,
PassivePortRange: ftpd.PortRange{
Start: 50000,
End: 50100,
},
DisableActiveMode: false,
EnableSite: false,
HASHSupport: 0,
CombineSupport: 0,
CertificateFile: "",
CertificateKeyFile: "",
CACertificates: []string{},
CARevocationLists: []string{},
},
WebDAVD: webdavd.Configuration{
Bindings: []webdavd.Binding{defaultWebDAVDBinding},
CertificateFile: "",
CertificateKeyFile: "",
CACertificates: []string{},
CARevocationLists: []string{},
Cors: webdavd.Cors{
Enabled: false,
AllowedOrigins: []string{},
AllowedMethods: []string{},
AllowedHeaders: []string{},
ExposedHeaders: []string{},
AllowCredentials: false,
MaxAge: 0,
},
Cache: webdavd.Cache{
Users: webdavd.UsersCacheConfig{
ExpirationTime: 0,
MaxSize: 50,
},
MimeTypes: webdavd.MimeCacheConfig{
Enabled: true,
MaxSize: 1000,
},
},
},
ProviderConf: dataprovider.Config{
Driver: "sqlite",
Name: "sftpgo.db",
Host: "",
Port: 5432,
Username: "",
Password: "",
ConnectionString: "",
SQLTablesPrefix: "",
SSLMode: 0,
TrackQuota: 1,
PoolSize: 0,
UsersBaseDir: "",
Actions: dataprovider.UserActions{
ExecuteOn: []string{},
Hook: "",
},
ExternalAuthHook: "",
ExternalAuthScope: 0,
CredentialsPath: "credentials",
PreLoginHook: "",
PostLoginHook: "",
PostLoginScope: 0,
CheckPasswordHook: "",
CheckPasswordScope: 0,
PasswordHashing: dataprovider.PasswordHashing{
Argon2Options: dataprovider.Argon2Options{
Memory: 65536,
Iterations: 1,
Parallelism: 2,
},
},
UpdateMode: 0,
PreferDatabaseCredentials: false,
},
HTTPDConfig: httpd.Conf{
Bindings: []httpd.Binding{defaultHTTPDBinding},
TemplatesPath: "templates",
StaticFilesPath: "static",
BackupsPath: "backups",
CertificateFile: "",
CertificateKeyFile: "",
},
HTTPConfig: httpclient.Config{
Timeout: 20,
CACertificates: nil,
SkipTLSVerify: false,
},
KMSConfig: kms.Configuration{
Secrets: kms.Secrets{
URL: "",
MasterKeyPath: "",
},
},
TelemetryConfig: telemetry.Conf{
BindPort: 10000,
BindAddress: "127.0.0.1",
EnableProfiler: false,
AuthUserFile: "",
CertificateFile: "",
CertificateKeyFile: "",
},
}
viper.SetEnvPrefix(configEnvPrefix)
replacer := strings.NewReplacer(".", "__")
viper.SetEnvKeyReplacer(replacer)
viper.SetConfigName(configName)
setViperDefaults()
viper.AutomaticEnv()
viper.AllowEmptyEnv(true)
}
// GetCommonConfig returns the common protocols configuration
func GetCommonConfig() common.Configuration {
return globalConf.Common
}
// SetCommonConfig sets the common protocols configuration
func SetCommonConfig(config common.Configuration) {
globalConf.Common = config
}
// GetSFTPDConfig returns the configuration for the SFTP server
func GetSFTPDConfig() sftpd.Configuration {
return globalConf.SFTPD
}
// SetSFTPDConfig sets the configuration for the SFTP server
func SetSFTPDConfig(config sftpd.Configuration) {
globalConf.SFTPD = config
}
// GetFTPDConfig returns the configuration for the FTP server
func GetFTPDConfig() ftpd.Configuration {
return globalConf.FTPD
}
// SetFTPDConfig sets the configuration for the FTP server
func SetFTPDConfig(config ftpd.Configuration) {
globalConf.FTPD = config
}
// GetWebDAVDConfig returns the configuration for the WebDAV server
func GetWebDAVDConfig() webdavd.Configuration {
return globalConf.WebDAVD
}
// SetWebDAVDConfig sets the configuration for the WebDAV server
func SetWebDAVDConfig(config webdavd.Configuration) {
globalConf.WebDAVD = config
}
// GetHTTPDConfig returns the configuration for the HTTP server
func GetHTTPDConfig() httpd.Conf {
return globalConf.HTTPDConfig
}
// SetHTTPDConfig sets the configuration for the HTTP server
func SetHTTPDConfig(config httpd.Conf) {
globalConf.HTTPDConfig = config
}
// GetProviderConf returns the configuration for the data provider
func GetProviderConf() dataprovider.Config {
return globalConf.ProviderConf
}
// SetProviderConf sets the configuration for the data provider
func SetProviderConf(config dataprovider.Config) {
globalConf.ProviderConf = config
}
// GetHTTPConfig returns the configuration for HTTP clients
func GetHTTPConfig() httpclient.Config {
return globalConf.HTTPConfig
}
// GetKMSConfig returns the KMS configuration
func GetKMSConfig() kms.Configuration {
return globalConf.KMSConfig
}
// SetKMSConfig sets the kms configuration
func SetKMSConfig(config kms.Configuration) {
globalConf.KMSConfig = config
}
// GetTelemetryConfig returns the telemetry configuration
func GetTelemetryConfig() telemetry.Conf {
return globalConf.TelemetryConfig
}
// SetTelemetryConfig sets the telemetry configuration
func SetTelemetryConfig(config telemetry.Conf) {
globalConf.TelemetryConfig = config
}
// HasServicesToStart returns true if the config defines at least a service to start.
// Supported services are SFTP, FTP and WebDAV
func HasServicesToStart() bool {
if globalConf.SFTPD.ShouldBind() {
return true
}
if globalConf.FTPD.ShouldBind() {
return true
}
if globalConf.WebDAVD.ShouldBind() {
return true
}
return false
}
func getRedactedGlobalConf() globalConfig {
conf := globalConf
conf.ProviderConf.Password = "[redacted]"
return conf
}
func setConfigFile(configDir, configFile string) {
if configFile == "" {
return
}
if !filepath.IsAbs(configFile) && utils.IsFileInputValid(configFile) {
configFile = filepath.Join(configDir, configFile)
}
viper.SetConfigFile(configFile)
}
// LoadConfig loads the configuration
// configDir will be added to the configuration search paths.
// The search path contains by default the current directory and on linux it contains
// $HOME/.config/sftpgo and /etc/sftpgo too.
// configFile is an absolute or relative path (to the config dir) to the configuration file.
func LoadConfig(configDir, configFile string) error {
var err error
viper.AddConfigPath(configDir)
setViperAdditionalConfigPaths()
viper.AddConfigPath(".")
setConfigFile(configDir, configFile)
if err = viper.ReadInConfig(); err != nil {
// if the user specify a configuration file we get os.ErrNotExist.
// viper.ConfigFileNotFoundError is returned if viper is unable
// to find sftpgo.{json,yaml, etc..} in any of the search paths
if errors.As(err, &viper.ConfigFileNotFoundError{}) {
logger.Debug(logSender, "", "no configuration file found")
} else {
// should we return the error and not start here?
logger.Warn(logSender, "", "error loading configuration file: %v", err)
logger.WarnToConsole("error loading configuration file: %v", err)
}
}
err = viper.Unmarshal(&globalConf)
if err != nil {
logger.Warn(logSender, "", "error parsing configuration file: %v", err)
logger.WarnToConsole("error parsing configuration file: %v", err)
return err
}
// viper only supports slice of strings from env vars, so we use our custom method
loadBindingsFromEnv()
checkCommonParamsCompatibility()
if strings.TrimSpace(globalConf.SFTPD.Banner) == "" {
globalConf.SFTPD.Banner = defaultSFTPDBanner
}
if strings.TrimSpace(globalConf.FTPD.Banner) == "" {
globalConf.FTPD.Banner = defaultFTPDBanner
}
if len(globalConf.ProviderConf.UsersBaseDir) > 0 && !utils.IsFileInputValid(globalConf.ProviderConf.UsersBaseDir) {
err = fmt.Errorf("invalid users base dir %#v will be ignored", globalConf.ProviderConf.UsersBaseDir)
globalConf.ProviderConf.UsersBaseDir = ""
logger.Warn(logSender, "", "Configuration error: %v", err)
logger.WarnToConsole("Configuration error: %v", err)
}
if globalConf.Common.UploadMode < 0 || globalConf.Common.UploadMode > 2 {
warn := fmt.Sprintf("invalid upload_mode 0, 1 and 2 are supported, configured: %v reset upload_mode to 0",
globalConf.Common.UploadMode)
globalConf.Common.UploadMode = 0
logger.Warn(logSender, "", "Configuration error: %v", warn)
logger.WarnToConsole("Configuration error: %v", warn)
}
if globalConf.Common.ProxyProtocol < 0 || globalConf.Common.ProxyProtocol > 2 {
warn := fmt.Sprintf("invalid proxy_protocol 0, 1 and 2 are supported, configured: %v reset proxy_protocol to 0",
globalConf.Common.ProxyProtocol)
globalConf.Common.ProxyProtocol = 0
logger.Warn(logSender, "", "Configuration error: %v", warn)
logger.WarnToConsole("Configuration error: %v", warn)
}
if globalConf.ProviderConf.ExternalAuthScope < 0 || globalConf.ProviderConf.ExternalAuthScope > 7 {
warn := fmt.Sprintf("invalid external_auth_scope: %v reset to 0", globalConf.ProviderConf.ExternalAuthScope)
globalConf.ProviderConf.ExternalAuthScope = 0
logger.Warn(logSender, "", "Configuration error: %v", warn)
logger.WarnToConsole("Configuration error: %v", warn)
}
if globalConf.ProviderConf.CredentialsPath == "" {
warn := "invalid credentials path, reset to \"credentials\""
globalConf.ProviderConf.CredentialsPath = "credentials"
logger.Warn(logSender, "", "Configuration error: %v", warn)
logger.WarnToConsole("Configuration error: %v", warn)
}
checkHostKeyCompatibility()
logger.Debug(logSender, "", "config file used: '%#v', config loaded: %+v", viper.ConfigFileUsed(), getRedactedGlobalConf())
return nil
}
func checkHostKeyCompatibility() {
// we copy deprecated fields to new ones to keep backward compatibility so lint is disabled
if len(globalConf.SFTPD.Keys) > 0 && len(globalConf.SFTPD.HostKeys) == 0 { //nolint:staticcheck
logger.Warn(logSender, "", "keys is deprecated, please use host_keys")
logger.WarnToConsole("keys is deprecated, please use host_keys")
for _, k := range globalConf.SFTPD.Keys { //nolint:staticcheck
globalConf.SFTPD.HostKeys = append(globalConf.SFTPD.HostKeys, k.PrivateKey)
}
}
}
func checkCommonParamsCompatibility() {
// we copy deprecated fields to new ones to keep backward compatibility so lint is disabled
if globalConf.SFTPD.IdleTimeout > 0 { //nolint:staticcheck
logger.Warn(logSender, "", "sftpd.idle_timeout is deprecated, please use common.idle_timeout")
logger.WarnToConsole("sftpd.idle_timeout is deprecated, please use common.idle_timeout")
globalConf.Common.IdleTimeout = globalConf.SFTPD.IdleTimeout //nolint:staticcheck
}
if len(globalConf.SFTPD.Actions.Hook) > 0 && len(globalConf.Common.Actions.Hook) == 0 { //nolint:staticcheck
logger.Warn(logSender, "", "sftpd.actions is deprecated, please use common.actions")
logger.WarnToConsole("sftpd.actions is deprecated, please use common.actions")
globalConf.Common.Actions.ExecuteOn = globalConf.SFTPD.Actions.ExecuteOn //nolint:staticcheck
globalConf.Common.Actions.Hook = globalConf.SFTPD.Actions.Hook //nolint:staticcheck
}
if globalConf.SFTPD.SetstatMode > 0 && globalConf.Common.SetstatMode == 0 { //nolint:staticcheck
logger.Warn(logSender, "", "sftpd.setstat_mode is deprecated, please use common.setstat_mode")
logger.WarnToConsole("sftpd.setstat_mode is deprecated, please use common.setstat_mode")
globalConf.Common.SetstatMode = globalConf.SFTPD.SetstatMode //nolint:staticcheck
}
if globalConf.SFTPD.UploadMode > 0 && globalConf.Common.UploadMode == 0 { //nolint:staticcheck
logger.Warn(logSender, "", "sftpd.upload_mode is deprecated, please use common.upload_mode")
logger.WarnToConsole("sftpd.upload_mode is deprecated, please use common.upload_mode")
globalConf.Common.UploadMode = globalConf.SFTPD.UploadMode //nolint:staticcheck
}
if globalConf.SFTPD.ProxyProtocol > 0 && globalConf.Common.ProxyProtocol == 0 { //nolint:staticcheck
logger.Warn(logSender, "", "sftpd.proxy_protocol is deprecated, please use common.proxy_protocol")
logger.WarnToConsole("sftpd.proxy_protocol is deprecated, please use common.proxy_protocol")
globalConf.Common.ProxyProtocol = globalConf.SFTPD.ProxyProtocol //nolint:staticcheck
globalConf.Common.ProxyAllowed = globalConf.SFTPD.ProxyAllowed //nolint:staticcheck
}
}
func checkSFTPDBindingsCompatibility() {
if globalConf.SFTPD.BindPort == 0 { //nolint:staticcheck
return
}
// we copy deprecated fields to new ones to keep backward compatibility so lint is disabled
binding := sftpd.Binding{
ApplyProxyConfig: true,
}
if globalConf.SFTPD.BindPort > 0 { //nolint:staticcheck
binding.Port = globalConf.SFTPD.BindPort //nolint:staticcheck
}
if globalConf.SFTPD.BindAddress != "" { //nolint:staticcheck
binding.Address = globalConf.SFTPD.BindAddress //nolint:staticcheck
}
globalConf.SFTPD.Bindings = []sftpd.Binding{binding}
}
func checkFTPDBindingCompatibility() {
if globalConf.FTPD.BindPort == 0 { //nolint:staticcheck
return
}
binding := ftpd.Binding{
ApplyProxyConfig: true,
}
if globalConf.FTPD.BindPort > 0 { //nolint:staticcheck
binding.Port = globalConf.FTPD.BindPort //nolint:staticcheck
}
if globalConf.FTPD.BindAddress != "" { //nolint:staticcheck
binding.Address = globalConf.FTPD.BindAddress //nolint:staticcheck
}
if globalConf.FTPD.TLSMode > 0 { //nolint:staticcheck
binding.TLSMode = globalConf.FTPD.TLSMode //nolint:staticcheck
}
if globalConf.FTPD.ForcePassiveIP != "" { //nolint:staticcheck
binding.ForcePassiveIP = globalConf.FTPD.ForcePassiveIP //nolint:staticcheck
}
globalConf.FTPD.Bindings = []ftpd.Binding{binding}
}
func checkWebDAVDBindingCompatibility() {
if globalConf.WebDAVD.BindPort == 0 { //nolint:staticcheck
return
}
binding := webdavd.Binding{
EnableHTTPS: globalConf.WebDAVD.CertificateFile != "" && globalConf.WebDAVD.CertificateKeyFile != "",
}
if globalConf.WebDAVD.BindPort > 0 { //nolint:staticcheck
binding.Port = globalConf.WebDAVD.BindPort //nolint:staticcheck
}
if globalConf.WebDAVD.BindAddress != "" { //nolint:staticcheck
binding.Address = globalConf.WebDAVD.BindAddress //nolint:staticcheck
}
globalConf.WebDAVD.Bindings = []webdavd.Binding{binding}
}
func checkHTTPDBindingCompatibility() {
if globalConf.HTTPDConfig.BindPort == 0 { //nolint:staticcheck
return
}
binding := httpd.Binding{
EnableWebAdmin: globalConf.HTTPDConfig.StaticFilesPath != "" && globalConf.HTTPDConfig.TemplatesPath != "",
EnableHTTPS: globalConf.HTTPDConfig.CertificateFile != "" && globalConf.HTTPDConfig.CertificateKeyFile != "",
}
if globalConf.HTTPDConfig.BindPort > 0 { //nolint:staticcheck
binding.Port = globalConf.HTTPDConfig.BindPort //nolint:staticcheck
}
if globalConf.HTTPDConfig.BindAddress != "" { //nolint:staticcheck
binding.Address = globalConf.HTTPDConfig.BindAddress //nolint:staticcheck
}
globalConf.HTTPDConfig.Bindings = []httpd.Binding{binding}
}
func loadBindingsFromEnv() {
checkSFTPDBindingsCompatibility()
checkFTPDBindingCompatibility()
checkWebDAVDBindingCompatibility()
checkHTTPDBindingCompatibility()
maxBindings := make([]int, 10)
for idx := range maxBindings {
getSFTPDBindindFromEnv(idx)
getFTPDBindingFromEnv(idx)
getWebDAVDBindingFromEnv(idx)
getHTTPDBindingFromEnv(idx)
}
}
func getSFTPDBindindFromEnv(idx int) {
binding := sftpd.Binding{}
if len(globalConf.SFTPD.Bindings) > idx {
binding = globalConf.SFTPD.Bindings[idx]
}
isSet := false
port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_SFTPD__BINDINGS__%v__PORT", idx))
if ok {
binding.Port = port
isSet = true
}
address, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_SFTPD__BINDINGS__%v__ADDRESS", idx))
if ok {
binding.Address = address
isSet = true
}
applyProxyConfig, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_SFTPD__BINDINGS__%v__APPLY_PROXY_CONFIG", idx))
if ok {
binding.ApplyProxyConfig = applyProxyConfig
isSet = true
}
if isSet {
if len(globalConf.SFTPD.Bindings) > idx {
globalConf.SFTPD.Bindings[idx] = binding
} else {
globalConf.SFTPD.Bindings = append(globalConf.SFTPD.Bindings, binding)
}
}
}
func getFTPDBindingFromEnv(idx int) {
binding := ftpd.Binding{}
if len(globalConf.FTPD.Bindings) > idx {
binding = globalConf.FTPD.Bindings[idx]
}
isSet := false
port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__PORT", idx))
if ok {
binding.Port = port
isSet = true
}
address, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__ADDRESS", idx))
if ok {
binding.Address = address
isSet = true
}
applyProxyConfig, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__APPLY_PROXY_CONFIG", idx))
if ok {
binding.ApplyProxyConfig = applyProxyConfig
isSet = true
}
tlsMode, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__TLS_MODE", idx))
if ok {
binding.TLSMode = tlsMode
isSet = true
}
passiveIP, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__FORCE_PASSIVE_IP", idx))
if ok {
binding.ForcePassiveIP = passiveIP
isSet = true
}
clientAuthType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__CLIENT_AUTH_TYPE", idx))
if ok {
binding.ClientAuthType = clientAuthType
isSet = true
}
if isSet {
if len(globalConf.FTPD.Bindings) > idx {
globalConf.FTPD.Bindings[idx] = binding
} else {
globalConf.FTPD.Bindings = append(globalConf.FTPD.Bindings, binding)
}
}
}
func getWebDAVDBindingFromEnv(idx int) {
binding := webdavd.Binding{}
if len(globalConf.WebDAVD.Bindings) > idx {
binding = globalConf.WebDAVD.Bindings[idx]
}
isSet := false
port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__PORT", idx))
if ok {
binding.Port = port
isSet = true
}
address, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__ADDRESS", idx))
if ok {
binding.Address = address
isSet = true
}
enableHTTPS, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__ENABLE_HTTPS", idx))
if ok {
binding.EnableHTTPS = enableHTTPS
isSet = true
}
clientAuthType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__CLIENT_AUTH_TYPE", idx))
if ok {
binding.ClientAuthType = clientAuthType
isSet = true
}
if isSet {
if len(globalConf.WebDAVD.Bindings) > idx {
globalConf.WebDAVD.Bindings[idx] = binding
} else {
globalConf.WebDAVD.Bindings = append(globalConf.WebDAVD.Bindings, binding)
}
}
}
func getHTTPDBindingFromEnv(idx int) {
binding := httpd.Binding{}
if len(globalConf.HTTPDConfig.Bindings) > idx {
binding = globalConf.HTTPDConfig.Bindings[idx]
}
isSet := false
port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__PORT", idx))
if ok {
binding.Port = port
isSet = true
}
address, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__ADDRESS", idx))
if ok {
binding.Address = address
isSet = true
}
enableWebAdmin, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__ENABLE_WEB_ADMIN", idx))
if ok {
binding.EnableWebAdmin = enableWebAdmin
isSet = true
}
enableHTTPS, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__ENABLE_HTTPS", idx))
if ok {
binding.EnableHTTPS = enableHTTPS
isSet = true
}
clientAuthType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__CLIENT_AUTH_TYPE", idx))
if ok {
binding.ClientAuthType = clientAuthType
isSet = true
}
if isSet {
if len(globalConf.HTTPDConfig.Bindings) > idx {
globalConf.HTTPDConfig.Bindings[idx] = binding
} else {
globalConf.HTTPDConfig.Bindings = append(globalConf.HTTPDConfig.Bindings, binding)
}
}
}
func setViperDefaults() {
viper.SetDefault("common.idle_timeout", globalConf.Common.IdleTimeout)
viper.SetDefault("common.upload_mode", globalConf.Common.UploadMode)
viper.SetDefault("common.actions.execute_on", globalConf.Common.Actions.ExecuteOn)
viper.SetDefault("common.actions.hook", globalConf.Common.Actions.Hook)
viper.SetDefault("common.setstat_mode", globalConf.Common.SetstatMode)
viper.SetDefault("common.proxy_protocol", globalConf.Common.ProxyProtocol)
viper.SetDefault("common.proxy_allowed", globalConf.Common.ProxyAllowed)
viper.SetDefault("common.post_connect_hook", globalConf.Common.PostConnectHook)
viper.SetDefault("common.max_total_connections", globalConf.Common.MaxTotalConnections)
viper.SetDefault("common.defender.enabled", globalConf.Common.DefenderConfig.Enabled)
viper.SetDefault("common.defender.ban_time", globalConf.Common.DefenderConfig.BanTime)
viper.SetDefault("common.defender.ban_time_increment", globalConf.Common.DefenderConfig.BanTimeIncrement)
viper.SetDefault("common.defender.threshold", globalConf.Common.DefenderConfig.Threshold)
viper.SetDefault("common.defender.score_invalid", globalConf.Common.DefenderConfig.ScoreInvalid)
viper.SetDefault("common.defender.score_valid", globalConf.Common.DefenderConfig.ScoreValid)
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.safelist_file", globalConf.Common.DefenderConfig.SafeListFile)
viper.SetDefault("common.defender.blocklist_file", globalConf.Common.DefenderConfig.BlockListFile)
viper.SetDefault("sftpd.max_auth_tries", globalConf.SFTPD.MaxAuthTries)
viper.SetDefault("sftpd.banner", globalConf.SFTPD.Banner)
viper.SetDefault("sftpd.host_keys", globalConf.SFTPD.HostKeys)
viper.SetDefault("sftpd.kex_algorithms", globalConf.SFTPD.KexAlgorithms)
viper.SetDefault("sftpd.ciphers", globalConf.SFTPD.Ciphers)
viper.SetDefault("sftpd.macs", globalConf.SFTPD.MACs)
viper.SetDefault("sftpd.trusted_user_ca_keys", globalConf.SFTPD.TrustedUserCAKeys)
viper.SetDefault("sftpd.login_banner_file", globalConf.SFTPD.LoginBannerFile)
viper.SetDefault("sftpd.enabled_ssh_commands", globalConf.SFTPD.EnabledSSHCommands)
viper.SetDefault("sftpd.keyboard_interactive_auth_hook", globalConf.SFTPD.KeyboardInteractiveHook)
viper.SetDefault("sftpd.password_authentication", globalConf.SFTPD.PasswordAuthentication)
viper.SetDefault("ftpd.banner", globalConf.FTPD.Banner)
viper.SetDefault("ftpd.banner_file", globalConf.FTPD.BannerFile)
viper.SetDefault("ftpd.active_transfers_port_non_20", globalConf.FTPD.ActiveTransfersPortNon20)
viper.SetDefault("ftpd.passive_port_range.start", globalConf.FTPD.PassivePortRange.Start)
viper.SetDefault("ftpd.passive_port_range.end", globalConf.FTPD.PassivePortRange.End)
viper.SetDefault("ftpd.disable_active_mode", globalConf.FTPD.DisableActiveMode)
viper.SetDefault("ftpd.enable_site", globalConf.FTPD.EnableSite)
viper.SetDefault("ftpd.hash_support", globalConf.FTPD.HASHSupport)
viper.SetDefault("ftpd.combine_support", globalConf.FTPD.CombineSupport)
viper.SetDefault("ftpd.certificate_file", globalConf.FTPD.CertificateFile)
viper.SetDefault("ftpd.certificate_key_file", globalConf.FTPD.CertificateKeyFile)
viper.SetDefault("ftpd.ca_certificates", globalConf.FTPD.CACertificates)
viper.SetDefault("ftpd.ca_revocation_lists", globalConf.FTPD.CARevocationLists)
viper.SetDefault("webdavd.certificate_file", globalConf.WebDAVD.CertificateFile)
viper.SetDefault("webdavd.certificate_key_file", globalConf.WebDAVD.CertificateKeyFile)
viper.SetDefault("webdavd.ca_certificates", globalConf.WebDAVD.CACertificates)
viper.SetDefault("webdavd.ca_revocation_lists", globalConf.WebDAVD.CARevocationLists)
viper.SetDefault("webdavd.cors.enabled", globalConf.WebDAVD.Cors.Enabled)
viper.SetDefault("webdavd.cors.allowed_origins", globalConf.WebDAVD.Cors.AllowedOrigins)
viper.SetDefault("webdavd.cors.allowed_methods", globalConf.WebDAVD.Cors.AllowedMethods)
viper.SetDefault("webdavd.cors.allowed_headers", globalConf.WebDAVD.Cors.AllowedHeaders)
viper.SetDefault("webdavd.cors.exposed_headers", globalConf.WebDAVD.Cors.ExposedHeaders)
viper.SetDefault("webdavd.cors.allow_credentials", globalConf.WebDAVD.Cors.AllowCredentials)
viper.SetDefault("webdavd.cors.max_age", globalConf.WebDAVD.Cors.MaxAge)
viper.SetDefault("webdavd.cache.users.expiration_time", globalConf.WebDAVD.Cache.Users.ExpirationTime)
viper.SetDefault("webdavd.cache.users.max_size", globalConf.WebDAVD.Cache.Users.MaxSize)
viper.SetDefault("webdavd.cache.mime_types.enabled", globalConf.WebDAVD.Cache.MimeTypes.Enabled)
viper.SetDefault("webdavd.cache.mime_types.max_size", globalConf.WebDAVD.Cache.MimeTypes.MaxSize)
viper.SetDefault("data_provider.driver", globalConf.ProviderConf.Driver)
viper.SetDefault("data_provider.name", globalConf.ProviderConf.Name)
viper.SetDefault("data_provider.host", globalConf.ProviderConf.Host)
viper.SetDefault("data_provider.port", globalConf.ProviderConf.Port)
viper.SetDefault("data_provider.username", globalConf.ProviderConf.Username)
viper.SetDefault("data_provider.password", globalConf.ProviderConf.Password)
viper.SetDefault("data_provider.sslmode", globalConf.ProviderConf.SSLMode)
viper.SetDefault("data_provider.connection_string", globalConf.ProviderConf.ConnectionString)
viper.SetDefault("data_provider.sql_tables_prefix", globalConf.ProviderConf.SQLTablesPrefix)
viper.SetDefault("data_provider.track_quota", globalConf.ProviderConf.TrackQuota)
viper.SetDefault("data_provider.pool_size", globalConf.ProviderConf.PoolSize)
viper.SetDefault("data_provider.users_base_dir", globalConf.ProviderConf.UsersBaseDir)
viper.SetDefault("data_provider.actions.execute_on", globalConf.ProviderConf.Actions.ExecuteOn)
viper.SetDefault("data_provider.actions.hook", globalConf.ProviderConf.Actions.Hook)
viper.SetDefault("data_provider.external_auth_hook", globalConf.ProviderConf.ExternalAuthHook)
viper.SetDefault("data_provider.external_auth_scope", globalConf.ProviderConf.ExternalAuthScope)
viper.SetDefault("data_provider.credentials_path", globalConf.ProviderConf.CredentialsPath)
viper.SetDefault("data_provider.prefer_database_credentials", globalConf.ProviderConf.PreferDatabaseCredentials)
viper.SetDefault("data_provider.pre_login_hook", globalConf.ProviderConf.PreLoginHook)
viper.SetDefault("data_provider.post_login_hook", globalConf.ProviderConf.PostLoginHook)
viper.SetDefault("data_provider.post_login_scope", globalConf.ProviderConf.PostLoginScope)
viper.SetDefault("data_provider.check_password_hook", globalConf.ProviderConf.CheckPasswordHook)
viper.SetDefault("data_provider.check_password_scope", globalConf.ProviderConf.CheckPasswordScope)
viper.SetDefault("data_provider.password_hashing.argon2_options.memory", globalConf.ProviderConf.PasswordHashing.Argon2Options.Memory)
viper.SetDefault("data_provider.password_hashing.argon2_options.iterations", globalConf.ProviderConf.PasswordHashing.Argon2Options.Iterations)
viper.SetDefault("data_provider.password_hashing.argon2_options.parallelism", globalConf.ProviderConf.PasswordHashing.Argon2Options.Parallelism)
viper.SetDefault("data_provider.update_mode", globalConf.ProviderConf.UpdateMode)
viper.SetDefault("httpd.templates_path", globalConf.HTTPDConfig.TemplatesPath)
viper.SetDefault("httpd.static_files_path", globalConf.HTTPDConfig.StaticFilesPath)
viper.SetDefault("httpd.backups_path", globalConf.HTTPDConfig.BackupsPath)
viper.SetDefault("httpd.certificate_file", globalConf.HTTPDConfig.CertificateFile)
viper.SetDefault("httpd.certificate_key_file", globalConf.HTTPDConfig.CertificateKeyFile)
viper.SetDefault("httpd.ca_certificates", globalConf.HTTPDConfig.CACertificates)
viper.SetDefault("httpd.ca_revocation_lists", globalConf.HTTPDConfig.CARevocationLists)
viper.SetDefault("http.timeout", globalConf.HTTPConfig.Timeout)
viper.SetDefault("http.ca_certificates", globalConf.HTTPConfig.CACertificates)
viper.SetDefault("http.skip_tls_verify", globalConf.HTTPConfig.SkipTLSVerify)
viper.SetDefault("kms.secrets.url", globalConf.KMSConfig.Secrets.URL)
viper.SetDefault("kms.secrets.master_key_path", globalConf.KMSConfig.Secrets.MasterKeyPath)
viper.SetDefault("telemetry.bind_port", globalConf.TelemetryConfig.BindPort)
viper.SetDefault("telemetry.bind_address", globalConf.TelemetryConfig.BindAddress)
viper.SetDefault("telemetry.enable_profiler", globalConf.TelemetryConfig.EnableProfiler)
viper.SetDefault("telemetry.auth_user_file", globalConf.TelemetryConfig.AuthUserFile)
viper.SetDefault("telemetry.certificate_file", globalConf.TelemetryConfig.CertificateFile)
viper.SetDefault("telemetry.certificate_key_file", globalConf.TelemetryConfig.CertificateKeyFile)
}
func lookupBoolFromEnv(envName string) (bool, bool) {
value, ok := os.LookupEnv(envName)
if ok {
converted, err := strconv.ParseBool(value)
if err == nil {
return converted, ok
}
}
return false, false
}
func lookupIntFromEnv(envName string) (int, bool) {
value, ok := os.LookupEnv(envName)
if ok {
converted, err := strconv.ParseInt(value, 10, 16)
if err == nil {
return int(converted), ok
}
}
return 0, false
}

View File

@@ -1,11 +0,0 @@
// +build linux
package config
import "github.com/spf13/viper"
// linux specific config search path
func setViperAdditionalConfigPaths() {
viper.AddConfigPath("$HOME/.config/sftpgo")
viper.AddConfigPath("/etc/sftpgo")
}

View File

@@ -1,7 +0,0 @@
// +build !linux
package config
func setViperAdditionalConfigPaths() {
}

View File

@@ -1,702 +0,0 @@
package config_test
import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/drakkan/sftpgo/common"
"github.com/drakkan/sftpgo/config"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/ftpd"
"github.com/drakkan/sftpgo/httpclient"
"github.com/drakkan/sftpgo/httpd"
"github.com/drakkan/sftpgo/sftpd"
"github.com/drakkan/sftpgo/utils"
"github.com/drakkan/sftpgo/webdavd"
)
const (
tempConfigName = "temp"
)
func reset() {
viper.Reset()
config.Init()
}
func TestLoadConfigTest(t *testing.T) {
reset()
configDir := ".."
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
assert.NotEqual(t, httpd.Conf{}, config.GetHTTPConfig())
assert.NotEqual(t, dataprovider.Config{}, config.GetProviderConf())
assert.NotEqual(t, sftpd.Configuration{}, config.GetSFTPDConfig())
assert.NotEqual(t, httpclient.Config{}, config.GetHTTPConfig())
confName := tempConfigName + ".json"
configFilePath := filepath.Join(configDir, confName)
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
err = ioutil.WriteFile(configFilePath, []byte("{invalid json}"), os.ModePerm)
assert.NoError(t, err)
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
err = ioutil.WriteFile(configFilePath, []byte("{\"sftpd\": {\"bind_port\": \"a\"}}"), os.ModePerm)
assert.NoError(t, err)
err = config.LoadConfig(configDir, confName)
assert.Error(t, err)
err = os.Remove(configFilePath)
assert.NoError(t, err)
}
func TestLoadConfigFileNotFound(t *testing.T) {
reset()
viper.SetConfigName("configfile")
err := config.LoadConfig(os.TempDir(), "")
assert.NoError(t, err)
}
func TestEmptyBanner(t *testing.T) {
reset()
configDir := ".."
confName := tempConfigName + ".json"
configFilePath := filepath.Join(configDir, confName)
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
sftpdConf := config.GetSFTPDConfig()
sftpdConf.Banner = " "
c := make(map[string]sftpd.Configuration)
c["sftpd"] = sftpdConf
jsonConf, _ := json.Marshal(c)
err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm)
assert.NoError(t, err)
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
sftpdConf = config.GetSFTPDConfig()
assert.NotEmpty(t, strings.TrimSpace(sftpdConf.Banner))
err = os.Remove(configFilePath)
assert.NoError(t, err)
ftpdConf := config.GetFTPDConfig()
ftpdConf.Banner = " "
c1 := make(map[string]ftpd.Configuration)
c1["ftpd"] = ftpdConf
jsonConf, _ = json.Marshal(c1)
err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm)
assert.NoError(t, err)
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
ftpdConf = config.GetFTPDConfig()
assert.NotEmpty(t, strings.TrimSpace(ftpdConf.Banner))
err = os.Remove(configFilePath)
assert.NoError(t, err)
}
func TestInvalidUploadMode(t *testing.T) {
reset()
configDir := ".."
confName := tempConfigName + ".json"
configFilePath := filepath.Join(configDir, confName)
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
commonConf := config.GetCommonConfig()
commonConf.UploadMode = 10
c := make(map[string]common.Configuration)
c["common"] = commonConf
jsonConf, err := json.Marshal(c)
assert.NoError(t, err)
err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm)
assert.NoError(t, err)
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
assert.Equal(t, 0, config.GetCommonConfig().UploadMode)
err = os.Remove(configFilePath)
assert.NoError(t, err)
}
func TestInvalidExternalAuthScope(t *testing.T) {
reset()
configDir := ".."
confName := tempConfigName + ".json"
configFilePath := filepath.Join(configDir, confName)
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
providerConf := config.GetProviderConf()
providerConf.ExternalAuthScope = 10
c := make(map[string]dataprovider.Config)
c["data_provider"] = providerConf
jsonConf, err := json.Marshal(c)
assert.NoError(t, err)
err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm)
assert.NoError(t, err)
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
assert.Equal(t, 0, config.GetProviderConf().ExternalAuthScope)
err = os.Remove(configFilePath)
assert.NoError(t, err)
}
func TestInvalidCredentialsPath(t *testing.T) {
reset()
configDir := ".."
confName := tempConfigName + ".json"
configFilePath := filepath.Join(configDir, confName)
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
providerConf := config.GetProviderConf()
providerConf.CredentialsPath = ""
c := make(map[string]dataprovider.Config)
c["data_provider"] = providerConf
jsonConf, err := json.Marshal(c)
assert.NoError(t, err)
err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm)
assert.NoError(t, err)
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
assert.Equal(t, "credentials", config.GetProviderConf().CredentialsPath)
err = os.Remove(configFilePath)
assert.NoError(t, err)
}
func TestInvalidProxyProtocol(t *testing.T) {
reset()
configDir := ".."
confName := tempConfigName + ".json"
configFilePath := filepath.Join(configDir, confName)
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
commonConf := config.GetCommonConfig()
commonConf.ProxyProtocol = 10
c := make(map[string]common.Configuration)
c["common"] = commonConf
jsonConf, err := json.Marshal(c)
assert.NoError(t, err)
err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm)
assert.NoError(t, err)
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
assert.Equal(t, 0, config.GetCommonConfig().ProxyProtocol)
err = os.Remove(configFilePath)
assert.NoError(t, err)
}
func TestInvalidUsersBaseDir(t *testing.T) {
reset()
configDir := ".."
confName := tempConfigName + ".json"
configFilePath := filepath.Join(configDir, confName)
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
providerConf := config.GetProviderConf()
providerConf.UsersBaseDir = "."
c := make(map[string]dataprovider.Config)
c["data_provider"] = providerConf
jsonConf, err := json.Marshal(c)
assert.NoError(t, err)
err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm)
assert.NoError(t, err)
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
assert.Empty(t, config.GetProviderConf().UsersBaseDir)
err = os.Remove(configFilePath)
assert.NoError(t, err)
}
func TestCommonParamsCompatibility(t *testing.T) {
reset()
configDir := ".."
confName := tempConfigName + ".json"
configFilePath := filepath.Join(configDir, confName)
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
sftpdConf := config.GetSFTPDConfig()
sftpdConf.IdleTimeout = 21 //nolint:staticcheck
sftpdConf.Actions.Hook = "http://hook"
sftpdConf.Actions.ExecuteOn = []string{"upload"}
sftpdConf.SetstatMode = 1 //nolint:staticcheck
sftpdConf.UploadMode = common.UploadModeAtomicWithResume //nolint:staticcheck
sftpdConf.ProxyProtocol = 1 //nolint:staticcheck
sftpdConf.ProxyAllowed = []string{"192.168.1.1"} //nolint:staticcheck
c := make(map[string]sftpd.Configuration)
c["sftpd"] = sftpdConf
jsonConf, err := json.Marshal(c)
assert.NoError(t, err)
err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm)
assert.NoError(t, err)
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
commonConf := config.GetCommonConfig()
assert.Equal(t, 21, commonConf.IdleTimeout)
assert.Equal(t, "http://hook", commonConf.Actions.Hook)
assert.Len(t, commonConf.Actions.ExecuteOn, 1)
assert.True(t, utils.IsStringInSlice("upload", commonConf.Actions.ExecuteOn))
assert.Equal(t, 1, commonConf.SetstatMode)
assert.Equal(t, 1, commonConf.ProxyProtocol)
assert.Len(t, commonConf.ProxyAllowed, 1)
assert.True(t, utils.IsStringInSlice("192.168.1.1", commonConf.ProxyAllowed))
err = os.Remove(configFilePath)
assert.NoError(t, err)
}
func TestHostKeyCompatibility(t *testing.T) {
reset()
configDir := ".."
confName := tempConfigName + ".json"
configFilePath := filepath.Join(configDir, confName)
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
sftpdConf := config.GetSFTPDConfig()
sftpdConf.Keys = []sftpd.Key{ //nolint:staticcheck
{
PrivateKey: "rsa",
},
{
PrivateKey: "ecdsa",
},
}
c := make(map[string]sftpd.Configuration)
c["sftpd"] = sftpdConf
jsonConf, err := json.Marshal(c)
assert.NoError(t, err)
err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm)
assert.NoError(t, err)
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
sftpdConf = config.GetSFTPDConfig()
assert.Equal(t, 2, len(sftpdConf.HostKeys))
assert.True(t, utils.IsStringInSlice("rsa", sftpdConf.HostKeys))
assert.True(t, utils.IsStringInSlice("ecdsa", sftpdConf.HostKeys))
err = os.Remove(configFilePath)
assert.NoError(t, err)
}
func TestSetGetConfig(t *testing.T) {
reset()
sftpdConf := config.GetSFTPDConfig()
sftpdConf.MaxAuthTries = 10
config.SetSFTPDConfig(sftpdConf)
assert.Equal(t, sftpdConf.MaxAuthTries, config.GetSFTPDConfig().MaxAuthTries)
dataProviderConf := config.GetProviderConf()
dataProviderConf.Host = "test host"
config.SetProviderConf(dataProviderConf)
assert.Equal(t, dataProviderConf.Host, config.GetProviderConf().Host)
httpdConf := config.GetHTTPDConfig()
httpdConf.Bindings = append(httpdConf.Bindings, httpd.Binding{Address: "0.0.0.0"})
config.SetHTTPDConfig(httpdConf)
assert.Equal(t, httpdConf.Bindings[0].Address, config.GetHTTPDConfig().Bindings[0].Address)
commonConf := config.GetCommonConfig()
commonConf.IdleTimeout = 10
config.SetCommonConfig(commonConf)
assert.Equal(t, commonConf.IdleTimeout, config.GetCommonConfig().IdleTimeout)
ftpdConf := config.GetFTPDConfig()
ftpdConf.CertificateFile = "cert"
ftpdConf.CertificateKeyFile = "key"
config.SetFTPDConfig(ftpdConf)
assert.Equal(t, ftpdConf.CertificateFile, config.GetFTPDConfig().CertificateFile)
assert.Equal(t, ftpdConf.CertificateKeyFile, config.GetFTPDConfig().CertificateKeyFile)
webDavConf := config.GetWebDAVDConfig()
webDavConf.CertificateFile = "dav_cert"
webDavConf.CertificateKeyFile = "dav_key"
config.SetWebDAVDConfig(webDavConf)
assert.Equal(t, webDavConf.CertificateFile, config.GetWebDAVDConfig().CertificateFile)
assert.Equal(t, webDavConf.CertificateKeyFile, config.GetWebDAVDConfig().CertificateKeyFile)
kmsConf := config.GetKMSConfig()
kmsConf.Secrets.MasterKeyPath = "apath"
kmsConf.Secrets.URL = "aurl"
config.SetKMSConfig(kmsConf)
assert.Equal(t, kmsConf.Secrets.MasterKeyPath, config.GetKMSConfig().Secrets.MasterKeyPath)
assert.Equal(t, kmsConf.Secrets.URL, config.GetKMSConfig().Secrets.URL)
telemetryConf := config.GetTelemetryConfig()
telemetryConf.BindPort = 10001
telemetryConf.BindAddress = "0.0.0.0"
config.SetTelemetryConfig(telemetryConf)
assert.Equal(t, telemetryConf.BindPort, config.GetTelemetryConfig().BindPort)
assert.Equal(t, telemetryConf.BindAddress, config.GetTelemetryConfig().BindAddress)
}
func TestServiceToStart(t *testing.T) {
reset()
configDir := ".."
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
assert.True(t, config.HasServicesToStart())
sftpdConf := config.GetSFTPDConfig()
sftpdConf.Bindings[0].Port = 0
config.SetSFTPDConfig(sftpdConf)
assert.False(t, config.HasServicesToStart())
ftpdConf := config.GetFTPDConfig()
ftpdConf.Bindings[0].Port = 2121
config.SetFTPDConfig(ftpdConf)
assert.True(t, config.HasServicesToStart())
ftpdConf.Bindings[0].Port = 0
config.SetFTPDConfig(ftpdConf)
webdavdConf := config.GetWebDAVDConfig()
webdavdConf.Bindings[0].Port = 9000
config.SetWebDAVDConfig(webdavdConf)
assert.True(t, config.HasServicesToStart())
webdavdConf.Bindings[0].Port = 0
config.SetWebDAVDConfig(webdavdConf)
assert.False(t, config.HasServicesToStart())
sftpdConf.Bindings[0].Port = 2022
config.SetSFTPDConfig(sftpdConf)
assert.True(t, config.HasServicesToStart())
}
func TestSFTPDBindingsCompatibility(t *testing.T) {
reset()
configDir := ".."
confName := tempConfigName + ".json"
configFilePath := filepath.Join(configDir, confName)
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
sftpdConf := config.GetSFTPDConfig()
require.Len(t, sftpdConf.Bindings, 1)
sftpdConf.Bindings = nil
sftpdConf.BindPort = 9022 //nolint:staticcheck
sftpdConf.BindAddress = "127.0.0.1" //nolint:staticcheck
c := make(map[string]sftpd.Configuration)
c["sftpd"] = sftpdConf
jsonConf, err := json.Marshal(c)
assert.NoError(t, err)
err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm)
assert.NoError(t, err)
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
sftpdConf = config.GetSFTPDConfig()
// the default binding should be replaced with the deprecated configuration
require.Len(t, sftpdConf.Bindings, 1)
require.Equal(t, 9022, sftpdConf.Bindings[0].Port)
require.Equal(t, "127.0.0.1", sftpdConf.Bindings[0].Address)
require.True(t, sftpdConf.Bindings[0].ApplyProxyConfig)
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
sftpdConf = config.GetSFTPDConfig()
require.Len(t, sftpdConf.Bindings, 1)
require.Equal(t, 9022, sftpdConf.Bindings[0].Port)
require.Equal(t, "127.0.0.1", sftpdConf.Bindings[0].Address)
require.True(t, sftpdConf.Bindings[0].ApplyProxyConfig)
err = os.Remove(configFilePath)
assert.NoError(t, err)
}
func TestFTPDBindingsCompatibility(t *testing.T) {
reset()
configDir := ".."
confName := tempConfigName + ".json"
configFilePath := filepath.Join(configDir, confName)
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
ftpdConf := config.GetFTPDConfig()
require.Len(t, ftpdConf.Bindings, 1)
ftpdConf.Bindings = nil
ftpdConf.BindPort = 9022 //nolint:staticcheck
ftpdConf.BindAddress = "127.1.0.1" //nolint:staticcheck
ftpdConf.ForcePassiveIP = "127.1.1.1" //nolint:staticcheck
ftpdConf.TLSMode = 2 //nolint:staticcheck
c := make(map[string]ftpd.Configuration)
c["ftpd"] = ftpdConf
jsonConf, err := json.Marshal(c)
assert.NoError(t, err)
err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm)
assert.NoError(t, err)
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
ftpdConf = config.GetFTPDConfig()
// the default binding should be replaced with the deprecated configuration
require.Len(t, ftpdConf.Bindings, 1)
require.Equal(t, 9022, ftpdConf.Bindings[0].Port)
require.Equal(t, "127.1.0.1", ftpdConf.Bindings[0].Address)
require.True(t, ftpdConf.Bindings[0].ApplyProxyConfig)
require.Equal(t, 2, ftpdConf.Bindings[0].TLSMode)
require.Equal(t, "127.1.1.1", ftpdConf.Bindings[0].ForcePassiveIP)
err = os.Remove(configFilePath)
assert.NoError(t, err)
}
func TestWebDAVDBindingsCompatibility(t *testing.T) {
reset()
configDir := ".."
confName := tempConfigName + ".json"
configFilePath := filepath.Join(configDir, confName)
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
webdavConf := config.GetWebDAVDConfig()
require.Len(t, webdavConf.Bindings, 1)
webdavConf.Bindings = nil
webdavConf.BindPort = 9080 //nolint:staticcheck
webdavConf.BindAddress = "127.0.0.1" //nolint:staticcheck
c := make(map[string]webdavd.Configuration)
c["webdavd"] = webdavConf
jsonConf, err := json.Marshal(c)
assert.NoError(t, err)
err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm)
assert.NoError(t, err)
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
webdavConf = config.GetWebDAVDConfig()
// the default binding should be replaced with the deprecated configuration
require.Len(t, webdavConf.Bindings, 1)
require.Equal(t, 9080, webdavConf.Bindings[0].Port)
require.Equal(t, "127.0.0.1", webdavConf.Bindings[0].Address)
require.False(t, webdavConf.Bindings[0].EnableHTTPS)
err = os.Remove(configFilePath)
assert.NoError(t, err)
}
func TestHTTPDBindingsCompatibility(t *testing.T) {
reset()
configDir := ".."
confName := tempConfigName + ".json"
configFilePath := filepath.Join(configDir, confName)
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
httpdConf := config.GetHTTPDConfig()
require.Len(t, httpdConf.Bindings, 1)
httpdConf.Bindings = nil
httpdConf.BindPort = 9080 //nolint:staticcheck
httpdConf.BindAddress = "127.1.1.1" //nolint:staticcheck
c := make(map[string]httpd.Conf)
c["httpd"] = httpdConf
jsonConf, err := json.Marshal(c)
assert.NoError(t, err)
err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm)
assert.NoError(t, err)
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
httpdConf = config.GetHTTPDConfig()
// the default binding should be replaced with the deprecated configuration
require.Len(t, httpdConf.Bindings, 1)
require.Equal(t, 9080, httpdConf.Bindings[0].Port)
require.Equal(t, "127.1.1.1", httpdConf.Bindings[0].Address)
require.False(t, httpdConf.Bindings[0].EnableHTTPS)
require.True(t, httpdConf.Bindings[0].EnableWebAdmin)
err = os.Remove(configFilePath)
assert.NoError(t, err)
}
func TestSFTPDBindingsFromEnv(t *testing.T) {
reset()
os.Setenv("SFTPGO_SFTPD__BINDINGS__0__ADDRESS", "127.0.0.1")
os.Setenv("SFTPGO_SFTPD__BINDINGS__0__PORT", "2200")
os.Setenv("SFTPGO_SFTPD__BINDINGS__0__APPLY_PROXY_CONFIG", "false")
os.Setenv("SFTPGO_SFTPD__BINDINGS__3__ADDRESS", "127.0.1.1")
os.Setenv("SFTPGO_SFTPD__BINDINGS__3__PORT", "2203")
os.Setenv("SFTPGO_SFTPD__BINDINGS__3__APPLY_PROXY_CONFIG", "1")
t.Cleanup(func() {
os.Unsetenv("SFTPGO_SFTPD__BINDINGS__0__ADDRESS")
os.Unsetenv("SFTPGO_SFTPD__BINDINGS__0__PORT")
os.Unsetenv("SFTPGO_SFTPD__BINDINGS__0__APPLY_PROXY_CONFIG")
os.Unsetenv("SFTPGO_SFTPD__BINDINGS__3__ADDRESS")
os.Unsetenv("SFTPGO_SFTPD__BINDINGS__3__PORT")
os.Unsetenv("SFTPGO_SFTPD__BINDINGS__3__APPLY_PROXY_CONFIG")
})
configDir := ".."
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
bindings := config.GetSFTPDConfig().Bindings
require.Len(t, bindings, 2)
require.Equal(t, 2200, bindings[0].Port)
require.Equal(t, "127.0.0.1", bindings[0].Address)
require.False(t, bindings[0].ApplyProxyConfig)
require.Equal(t, 2203, bindings[1].Port)
require.Equal(t, "127.0.1.1", bindings[1].Address)
require.True(t, bindings[1].ApplyProxyConfig)
}
func TestFTPDBindingsFromEnv(t *testing.T) {
reset()
os.Setenv("SFTPGO_FTPD__BINDINGS__0__ADDRESS", "127.0.0.1")
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__FORCE_PASSIVE_IP", "127.0.1.2")
os.Setenv("SFTPGO_FTPD__BINDINGS__9__ADDRESS", "127.0.1.1")
os.Setenv("SFTPGO_FTPD__BINDINGS__9__PORT", "2203")
os.Setenv("SFTPGO_FTPD__BINDINGS__9__APPLY_PROXY_CONFIG", "t")
os.Setenv("SFTPGO_FTPD__BINDINGS__9__TLS_MODE", "1")
os.Setenv("SFTPGO_FTPD__BINDINGS__9__FORCE_PASSIVE_IP", "127.0.1.1")
os.Setenv("SFTPGO_FTPD__BINDINGS__9__CLIENT_AUTH_TYPE", "1")
t.Cleanup(func() {
os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__ADDRESS")
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__FORCE_PASSIVE_IP")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__ADDRESS")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__PORT")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__APPLY_PROXY_CONFIG")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__TLS_MODE")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__FORCE_PASSIVE_IP")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__CLIENT_AUTH_TYPE")
})
configDir := ".."
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
bindings := config.GetFTPDConfig().Bindings
require.Len(t, bindings, 2)
require.Equal(t, 2200, bindings[0].Port)
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, "127.0.1.2", bindings[0].ForcePassiveIP)
require.Equal(t, 0, bindings[0].ClientAuthType)
require.Equal(t, 2203, bindings[1].Port)
require.Equal(t, "127.0.1.1", bindings[1].Address)
require.True(t, bindings[1].ApplyProxyConfig)
require.Equal(t, 1, bindings[1].TLSMode)
require.Equal(t, "127.0.1.1", bindings[1].ForcePassiveIP)
require.Equal(t, 1, bindings[1].ClientAuthType)
}
func TestWebDAVBindingsFromEnv(t *testing.T) {
reset()
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__ADDRESS", "127.0.0.1")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__PORT", "8000")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__ENABLE_HTTPS", "0")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__ADDRESS", "127.0.1.1")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__PORT", "9000")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__ENABLE_HTTPS", "1")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__CLIENT_AUTH_TYPE", "1")
t.Cleanup(func() {
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__ADDRESS")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__PORT")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__ENABLE_HTTPS")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__ADDRESS")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__PORT")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__ENABLE_HTTPS")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__CLIENT_AUTH_TYPE")
})
configDir := ".."
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
bindings := config.GetWebDAVDConfig().Bindings
require.Len(t, bindings, 3)
require.Equal(t, 0, bindings[0].Port)
require.Empty(t, bindings[0].Address)
require.False(t, bindings[0].EnableHTTPS)
require.Equal(t, 8000, bindings[1].Port)
require.Equal(t, "127.0.0.1", bindings[1].Address)
require.False(t, bindings[1].EnableHTTPS)
require.Equal(t, 0, bindings[1].ClientAuthType)
require.Equal(t, 9000, bindings[2].Port)
require.Equal(t, "127.0.1.1", bindings[2].Address)
require.True(t, bindings[2].EnableHTTPS)
require.Equal(t, 1, bindings[2].ClientAuthType)
}
func TestHTTPDBindingsFromEnv(t *testing.T) {
reset()
sockPath := filepath.Clean(os.TempDir())
os.Setenv("SFTPGO_HTTPD__BINDINGS__0__ADDRESS", sockPath)
os.Setenv("SFTPGO_HTTPD__BINDINGS__0__PORT", "0")
os.Setenv("SFTPGO_HTTPD__BINDINGS__1__ADDRESS", "127.0.0.1")
os.Setenv("SFTPGO_HTTPD__BINDINGS__1__PORT", "8000")
os.Setenv("SFTPGO_HTTPD__BINDINGS__1__ENABLE_HTTPS", "0")
os.Setenv("SFTPGO_HTTPD__BINDINGS__1__ENABLE_WEB_ADMIN", "1")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ADDRESS", "127.0.1.1")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__PORT", "9000")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_ADMIN", "0")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_HTTPS", "1")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_AUTH_TYPE", "1")
t.Cleanup(func() {
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__0__ADDRESS")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__0__PORT")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__1__ADDRESS")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__1__PORT")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__1__ENABLE_HTTPS")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__1__ENABLE_WEB_ADMIN")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ADDRESS")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__PORT")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_HTTPS")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_ADMIN")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_AUTH_TYPE")
})
configDir := ".."
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
bindings := config.GetHTTPDConfig().Bindings
require.Len(t, bindings, 3)
require.Equal(t, 0, bindings[0].Port)
require.Equal(t, sockPath, bindings[0].Address)
require.False(t, bindings[0].EnableHTTPS)
require.True(t, bindings[0].EnableWebAdmin)
require.Equal(t, 8000, bindings[1].Port)
require.Equal(t, "127.0.0.1", bindings[1].Address)
require.False(t, bindings[1].EnableHTTPS)
require.True(t, bindings[1].EnableWebAdmin)
require.Equal(t, 9000, bindings[2].Port)
require.Equal(t, "127.0.1.1", bindings[2].Address)
require.True(t, bindings[2].EnableHTTPS)
require.False(t, bindings[2].EnableWebAdmin)
require.Equal(t, 1, bindings[2].ClientAuthType)
}
func TestConfigFromEnv(t *testing.T) {
reset()
os.Setenv("SFTPGO_SFTPD__BINDINGS__0__ADDRESS", "127.0.0.1")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__0__PORT", "12000")
os.Setenv("SFTPGO_DATA_PROVIDER__PASSWORD_HASHING__ARGON2_OPTIONS__ITERATIONS", "41")
os.Setenv("SFTPGO_DATA_PROVIDER__POOL_SIZE", "10")
os.Setenv("SFTPGO_DATA_PROVIDER__ACTIONS__EXECUTE_ON", "add")
os.Setenv("SFTPGO_KMS__SECRETS__URL", "local")
os.Setenv("SFTPGO_KMS__SECRETS__MASTER_KEY_PATH", "path")
t.Cleanup(func() {
os.Unsetenv("SFTPGO_SFTPD__BINDINGS__0__ADDRESS")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__0__PORT")
os.Unsetenv("SFTPGO_DATA_PROVIDER__PASSWORD_HASHING__ARGON2_OPTIONS__ITERATIONS")
os.Unsetenv("SFTPGO_DATA_PROVIDER__POOL_SIZE")
os.Unsetenv("SFTPGO_DATA_PROVIDER__ACTIONS__EXECUTE_ON")
os.Unsetenv("SFTPGO_KMS__SECRETS__URL")
os.Unsetenv("SFTPGO_KMS__SECRETS__MASTER_KEY_PATH")
})
err := config.LoadConfig(".", "invalid config")
assert.NoError(t, err)
sftpdConfig := config.GetSFTPDConfig()
assert.Equal(t, "127.0.0.1", sftpdConfig.Bindings[0].Address)
assert.Equal(t, 12000, config.GetWebDAVDConfig().Bindings[0].Port)
dataProviderConf := config.GetProviderConf()
assert.Equal(t, uint32(41), dataProviderConf.PasswordHashing.Argon2Options.Iterations)
assert.Equal(t, 10, dataProviderConf.PoolSize)
assert.Len(t, dataProviderConf.Actions.ExecuteOn, 1)
assert.Contains(t, dataProviderConf.Actions.ExecuteOn, "add")
kmsConfig := config.GetKMSConfig()
assert.Equal(t, "local", kmsConfig.Secrets.URL)
assert.Equal(t, "path", kmsConfig.Secrets.MasterKeyPath)
}

View File

@@ -1,228 +0,0 @@
package dataprovider
import (
"encoding/base64"
"errors"
"fmt"
"net"
"regexp"
"strings"
"github.com/alexedwards/argon2id"
"github.com/minio/sha256-simd"
"github.com/drakkan/sftpgo/utils"
)
// Available permissions for SFTPGo admins
const (
PermAdminAny = "*"
PermAdminAddUsers = "add_users"
PermAdminChangeUsers = "edit_users"
PermAdminDeleteUsers = "del_users"
PermAdminViewUsers = "view_users"
PermAdminViewConnections = "view_conns"
PermAdminCloseConnections = "close_conns"
PermAdminViewServerStatus = "view_status"
PermAdminManageAdmins = "manage_admins"
PermAdminQuotaScans = "quota_scans"
PermAdminManageSystem = "manage_system"
PermAdminManageDefender = "manage_defender"
PermAdminViewDefender = "view_defender"
)
var (
emailRegex = regexp.MustCompile("^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$")
validAdminPerms = []string{PermAdminAny, PermAdminAddUsers, PermAdminChangeUsers, PermAdminDeleteUsers,
PermAdminViewUsers, PermAdminViewConnections, PermAdminCloseConnections, PermAdminViewServerStatus,
PermAdminManageAdmins, PermAdminQuotaScans, PermAdminManageSystem, PermAdminManageDefender,
PermAdminViewDefender}
)
// AdminFilters defines additional restrictions for SFTPGo admins
type AdminFilters struct {
// only clients connecting from these IP/Mask are allowed.
// IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291
// for example "192.0.2.0/24" or "2001:db8::/32"
AllowList []string `json:"allow_list,omitempty"`
}
// Admin defines a SFTPGo admin
type Admin struct {
// Database unique identifier
ID int64 `json:"id"`
// 1 enabled, 0 disabled (login is not allowed)
Status int `json:"status"`
// Username
Username string `json:"username"`
Password string `json:"password,omitempty"`
Email string `json:"email"`
Permissions []string `json:"permissions"`
Filters AdminFilters `json:"filters,omitempty"`
AdditionalInfo string `json:"additional_info,omitempty"`
}
func (a *Admin) validate() error {
if a.Username == "" {
return &ValidationError{err: "username is mandatory"}
}
if a.Password == "" {
return &ValidationError{err: "please set a password"}
}
if !usernameRegex.MatchString(a.Username) {
return &ValidationError{err: fmt.Sprintf("username %#v is not valid", a.Username)}
}
if a.Password != "" && !strings.HasPrefix(a.Password, argonPwdPrefix) {
pwd, err := argon2id.CreateHash(a.Password, argon2Params)
if err != nil {
return err
}
a.Password = pwd
}
a.Permissions = utils.RemoveDuplicates(a.Permissions)
if len(a.Permissions) == 0 {
return &ValidationError{err: "please grant some permissions to this admin"}
}
if utils.IsStringInSlice(PermAdminAny, a.Permissions) {
a.Permissions = []string{PermAdminAny}
}
for _, perm := range a.Permissions {
if !utils.IsStringInSlice(perm, validAdminPerms) {
return &ValidationError{err: fmt.Sprintf("invalid permission: %#v", perm)}
}
}
if a.Email != "" && !emailRegex.MatchString(a.Email) {
return &ValidationError{err: fmt.Sprintf("email %#v is not valid", a.Email)}
}
for _, IPMask := range a.Filters.AllowList {
_, _, err := net.ParseCIDR(IPMask)
if err != nil {
return &ValidationError{err: fmt.Sprintf("could not parse allow list entry %#v : %v", IPMask, err)}
}
}
return nil
}
// CheckPassword verifies the admin password
func (a *Admin) CheckPassword(password string) (bool, error) {
return argon2id.ComparePasswordAndHash(password, a.Password)
}
// CanLoginFromIP returns true if login from the given IP is allowed
func (a *Admin) CanLoginFromIP(ip string) bool {
if len(a.Filters.AllowList) == 0 {
return true
}
parsedIP := net.ParseIP(ip)
if parsedIP == nil {
return len(a.Filters.AllowList) == 0
}
for _, ipMask := range a.Filters.AllowList {
_, network, err := net.ParseCIDR(ipMask)
if err != nil {
continue
}
if network.Contains(parsedIP) {
return true
}
}
return false
}
func (a *Admin) checkUserAndPass(password, ip string) error {
if a.Status != 1 {
return fmt.Errorf("admin %#v is disabled", a.Username)
}
if a.Password == "" || password == "" {
return errors.New("credentials cannot be null or empty")
}
match, err := a.CheckPassword(password)
if err != nil {
return err
}
if !match {
return ErrInvalidCredentials
}
if !a.CanLoginFromIP(ip) {
return fmt.Errorf("login from IP %v not allowed", ip)
}
return nil
}
// HideConfidentialData hides admin confidential data
func (a *Admin) HideConfidentialData() {
a.Password = ""
}
// HasPermission returns true if the admin has the specified permission
func (a *Admin) HasPermission(perm string) bool {
if utils.IsStringInSlice(PermAdminAny, a.Permissions) {
return true
}
return utils.IsStringInSlice(perm, a.Permissions)
}
// GetPermissionsAsString returns permission as string
func (a *Admin) GetPermissionsAsString() string {
return strings.Join(a.Permissions, ", ")
}
// GetAllowedIPAsString returns the allowed IP as comma separated string
func (a *Admin) GetAllowedIPAsString() string {
return strings.Join(a.Filters.AllowList, ",")
}
// GetValidPerms returns the allowed admin permissions
func (a *Admin) GetValidPerms() []string {
return validAdminPerms
}
// GetInfoString returns admin's info as string.
func (a *Admin) GetInfoString() string {
var result string
if a.Email != "" {
result = fmt.Sprintf("Email: %v. ", a.Email)
}
if len(a.Filters.AllowList) > 0 {
result += fmt.Sprintf("Allowed IP/Mask: %v. ", len(a.Filters.AllowList))
}
return result
}
// GetSignature returns a signature for this admin.
// It could change after an update
func (a *Admin) GetSignature() string {
data := []byte(a.Username)
data = append(data, []byte(a.Password)...)
signature := sha256.Sum256(data)
return base64.StdEncoding.EncodeToString(signature[:])
}
func (a *Admin) getACopy() Admin {
permissions := make([]string, len(a.Permissions))
copy(permissions, a.Permissions)
filters := AdminFilters{}
filters.AllowList = make([]string, len(a.Filters.AllowList))
copy(filters.AllowList, a.Filters.AllowList)
return Admin{
ID: a.ID,
Status: a.Status,
Username: a.Username,
Password: a.Password,
Email: a.Email,
Permissions: permissions,
Filters: filters,
AdditionalInfo: a.AdditionalInfo,
}
}
// setDefaults sets the appropriate value for the default admin
func (a *Admin) setDefaults() {
a.Username = "admin"
a.Password = "password"
a.Status = 1
a.Permissions = []string{PermAdminAny}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +0,0 @@
// +build nobolt
package dataprovider
import (
"errors"
"github.com/drakkan/sftpgo/version"
)
func init() {
version.AddFeature("-bolt")
}
func initializeBoltProvider(basePath string) error {
return errors.New("bolt disabled at build time")
}

View File

@@ -1,358 +0,0 @@
package dataprovider
import (
"encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
"github.com/drakkan/sftpgo/kms"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/utils"
"github.com/drakkan/sftpgo/vfs"
)
type compatUserV2 struct {
ID int64 `json:"id"`
Username string `json:"username"`
Password string `json:"password,omitempty"`
PublicKeys []string `json:"public_keys,omitempty"`
HomeDir string `json:"home_dir"`
UID int `json:"uid"`
GID int `json:"gid"`
MaxSessions int `json:"max_sessions"`
QuotaSize int64 `json:"quota_size"`
QuotaFiles int `json:"quota_files"`
Permissions []string `json:"permissions"`
UsedQuotaSize int64 `json:"used_quota_size"`
UsedQuotaFiles int `json:"used_quota_files"`
LastQuotaUpdate int64 `json:"last_quota_update"`
UploadBandwidth int64 `json:"upload_bandwidth"`
DownloadBandwidth int64 `json:"download_bandwidth"`
ExpirationDate int64 `json:"expiration_date"`
LastLogin int64 `json:"last_login"`
Status int `json:"status"`
}
type compatS3FsConfigV4 struct {
Bucket string `json:"bucket,omitempty"`
KeyPrefix string `json:"key_prefix,omitempty"`
Region string `json:"region,omitempty"`
AccessKey string `json:"access_key,omitempty"`
AccessSecret string `json:"access_secret,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
StorageClass string `json:"storage_class,omitempty"`
UploadPartSize int64 `json:"upload_part_size,omitempty"`
UploadConcurrency int `json:"upload_concurrency,omitempty"`
}
type compatGCSFsConfigV4 struct {
Bucket string `json:"bucket,omitempty"`
KeyPrefix string `json:"key_prefix,omitempty"`
CredentialFile string `json:"-"`
Credentials []byte `json:"credentials,omitempty"`
AutomaticCredentials int `json:"automatic_credentials,omitempty"`
StorageClass string `json:"storage_class,omitempty"`
}
type compatAzBlobFsConfigV4 struct {
Container string `json:"container,omitempty"`
AccountName string `json:"account_name,omitempty"`
AccountKey string `json:"account_key,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
SASURL string `json:"sas_url,omitempty"`
KeyPrefix string `json:"key_prefix,omitempty"`
UploadPartSize int64 `json:"upload_part_size,omitempty"`
UploadConcurrency int `json:"upload_concurrency,omitempty"`
UseEmulator bool `json:"use_emulator,omitempty"`
AccessTier string `json:"access_tier,omitempty"`
}
type compatFilesystemV4 struct {
Provider FilesystemProvider `json:"provider"`
S3Config compatS3FsConfigV4 `json:"s3config,omitempty"`
GCSConfig compatGCSFsConfigV4 `json:"gcsconfig,omitempty"`
AzBlobConfig compatAzBlobFsConfigV4 `json:"azblobconfig,omitempty"`
}
type compatUserV4 struct {
ID int64 `json:"id"`
Status int `json:"status"`
Username string `json:"username"`
ExpirationDate int64 `json:"expiration_date"`
Password string `json:"password,omitempty"`
PublicKeys []string `json:"public_keys,omitempty"`
HomeDir string `json:"home_dir"`
VirtualFolders []vfs.VirtualFolder `json:"virtual_folders,omitempty"`
UID int `json:"uid"`
GID int `json:"gid"`
MaxSessions int `json:"max_sessions"`
QuotaSize int64 `json:"quota_size"`
QuotaFiles int `json:"quota_files"`
Permissions map[string][]string `json:"permissions"`
UsedQuotaSize int64 `json:"used_quota_size"`
UsedQuotaFiles int `json:"used_quota_files"`
LastQuotaUpdate int64 `json:"last_quota_update"`
UploadBandwidth int64 `json:"upload_bandwidth"`
DownloadBandwidth int64 `json:"download_bandwidth"`
LastLogin int64 `json:"last_login"`
Filters UserFilters `json:"filters"`
FsConfig compatFilesystemV4 `json:"filesystem"`
}
type backupDataV4Compat struct {
Users []compatUserV4 `json:"users"`
Folders []vfs.BaseVirtualFolder `json:"folders"`
}
func createUserFromV4(u compatUserV4, fsConfig Filesystem) User {
user := User{
ID: u.ID,
Status: u.Status,
Username: u.Username,
ExpirationDate: u.ExpirationDate,
Password: u.Password,
PublicKeys: u.PublicKeys,
HomeDir: u.HomeDir,
VirtualFolders: u.VirtualFolders,
UID: u.UID,
GID: u.GID,
MaxSessions: u.MaxSessions,
QuotaSize: u.QuotaSize,
QuotaFiles: u.QuotaFiles,
Permissions: u.Permissions,
UsedQuotaSize: u.UsedQuotaSize,
UsedQuotaFiles: u.UsedQuotaFiles,
LastQuotaUpdate: u.LastQuotaUpdate,
UploadBandwidth: u.UploadBandwidth,
DownloadBandwidth: u.DownloadBandwidth,
LastLogin: u.LastLogin,
Filters: u.Filters,
}
user.FsConfig = fsConfig
user.SetEmptySecretsIfNil()
return user
}
func convertUserToV4(u User, fsConfig compatFilesystemV4) compatUserV4 {
user := compatUserV4{
ID: u.ID,
Status: u.Status,
Username: u.Username,
ExpirationDate: u.ExpirationDate,
Password: u.Password,
PublicKeys: u.PublicKeys,
HomeDir: u.HomeDir,
VirtualFolders: u.VirtualFolders,
UID: u.UID,
GID: u.GID,
MaxSessions: u.MaxSessions,
QuotaSize: u.QuotaSize,
QuotaFiles: u.QuotaFiles,
Permissions: u.Permissions,
UsedQuotaSize: u.UsedQuotaSize,
UsedQuotaFiles: u.UsedQuotaFiles,
LastQuotaUpdate: u.LastQuotaUpdate,
UploadBandwidth: u.UploadBandwidth,
DownloadBandwidth: u.DownloadBandwidth,
LastLogin: u.LastLogin,
Filters: u.Filters,
}
user.FsConfig = fsConfig
return user
}
func getCGSCredentialsFromV4(config compatGCSFsConfigV4) (*kms.Secret, error) {
secret := kms.NewEmptySecret()
var err error
if len(config.Credentials) > 0 {
secret = kms.NewPlainSecret(string(config.Credentials))
return secret, nil
}
if config.CredentialFile != "" {
creds, err := ioutil.ReadFile(config.CredentialFile)
if err != nil {
return secret, err
}
secret = kms.NewPlainSecret(string(creds))
return secret, nil
}
return secret, err
}
func getCGSCredentialsFromV6(config vfs.GCSFsConfig, username string) (string, error) {
if config.Credentials == nil {
config.Credentials = kms.NewEmptySecret()
}
if config.Credentials.IsEmpty() {
config.CredentialFile = filepath.Join(credentialsDirPath, fmt.Sprintf("%v_gcs_credentials.json",
username))
creds, err := ioutil.ReadFile(config.CredentialFile)
if err != nil {
return "", err
}
err = json.Unmarshal(creds, &config.Credentials)
if err != nil {
return "", err
}
}
if config.Credentials.IsEncrypted() {
err := config.Credentials.Decrypt()
if err != nil {
return "", err
}
// in V4 GCS credentials were not encrypted
return config.Credentials.GetPayload(), nil
}
return "", nil
}
func convertFsConfigToV4(fs Filesystem, username string) (compatFilesystemV4, error) {
fsV4 := compatFilesystemV4{
Provider: fs.Provider,
S3Config: compatS3FsConfigV4{},
AzBlobConfig: compatAzBlobFsConfigV4{},
GCSConfig: compatGCSFsConfigV4{},
}
switch fs.Provider {
case S3FilesystemProvider:
fsV4.S3Config = compatS3FsConfigV4{
Bucket: fs.S3Config.Bucket,
KeyPrefix: fs.S3Config.KeyPrefix,
Region: fs.S3Config.Region,
AccessKey: fs.S3Config.AccessKey,
AccessSecret: "",
Endpoint: fs.S3Config.Endpoint,
StorageClass: fs.S3Config.StorageClass,
UploadPartSize: fs.S3Config.UploadPartSize,
UploadConcurrency: fs.S3Config.UploadConcurrency,
}
if fs.S3Config.AccessSecret.IsEncrypted() {
err := fs.S3Config.AccessSecret.Decrypt()
if err != nil {
return fsV4, err
}
secretV4, err := utils.EncryptData(fs.S3Config.AccessSecret.GetPayload())
if err != nil {
return fsV4, err
}
fsV4.S3Config.AccessSecret = secretV4
}
case AzureBlobFilesystemProvider:
fsV4.AzBlobConfig = compatAzBlobFsConfigV4{
Container: fs.AzBlobConfig.Container,
AccountName: fs.AzBlobConfig.AccountName,
AccountKey: "",
Endpoint: fs.AzBlobConfig.Endpoint,
SASURL: fs.AzBlobConfig.SASURL,
KeyPrefix: fs.AzBlobConfig.KeyPrefix,
UploadPartSize: fs.AzBlobConfig.UploadPartSize,
UploadConcurrency: fs.AzBlobConfig.UploadConcurrency,
UseEmulator: fs.AzBlobConfig.UseEmulator,
AccessTier: fs.AzBlobConfig.AccessTier,
}
if fs.AzBlobConfig.AccountKey.IsEncrypted() {
err := fs.AzBlobConfig.AccountKey.Decrypt()
if err != nil {
return fsV4, err
}
secretV4, err := utils.EncryptData(fs.AzBlobConfig.AccountKey.GetPayload())
if err != nil {
return fsV4, err
}
fsV4.AzBlobConfig.AccountKey = secretV4
}
case GCSFilesystemProvider:
fsV4.GCSConfig = compatGCSFsConfigV4{
Bucket: fs.GCSConfig.Bucket,
KeyPrefix: fs.GCSConfig.KeyPrefix,
CredentialFile: fs.GCSConfig.CredentialFile,
AutomaticCredentials: fs.GCSConfig.AutomaticCredentials,
StorageClass: fs.GCSConfig.StorageClass,
}
if fs.GCSConfig.AutomaticCredentials == 0 {
creds, err := getCGSCredentialsFromV6(fs.GCSConfig, username)
if err != nil {
return fsV4, err
}
fsV4.GCSConfig.Credentials = []byte(creds)
}
default:
// a provider not supported in v4, the configuration will be lost
providerLog(logger.LevelWarn, "provider %v was not supported in v4, the configuration for the user %#v will be lost",
fs.Provider, username)
fsV4.Provider = 0
}
return fsV4, nil
}
func convertFsConfigFromV4(compatFs compatFilesystemV4, username string) (Filesystem, error) {
fsConfig := Filesystem{
Provider: compatFs.Provider,
S3Config: vfs.S3FsConfig{},
AzBlobConfig: vfs.AzBlobFsConfig{},
GCSConfig: vfs.GCSFsConfig{},
}
switch compatFs.Provider {
case S3FilesystemProvider:
fsConfig.S3Config = vfs.S3FsConfig{
Bucket: compatFs.S3Config.Bucket,
KeyPrefix: compatFs.S3Config.KeyPrefix,
Region: compatFs.S3Config.Region,
AccessKey: compatFs.S3Config.AccessKey,
AccessSecret: kms.NewEmptySecret(),
Endpoint: compatFs.S3Config.Endpoint,
StorageClass: compatFs.S3Config.StorageClass,
UploadPartSize: compatFs.S3Config.UploadPartSize,
UploadConcurrency: compatFs.S3Config.UploadConcurrency,
}
if compatFs.S3Config.AccessSecret != "" {
secret, err := kms.GetSecretFromCompatString(compatFs.S3Config.AccessSecret)
if err != nil {
providerLog(logger.LevelError, "unable to convert v4 filesystem for user %#v: %v", username, err)
return fsConfig, err
}
fsConfig.S3Config.AccessSecret = secret
}
case AzureBlobFilesystemProvider:
fsConfig.AzBlobConfig = vfs.AzBlobFsConfig{
Container: compatFs.AzBlobConfig.Container,
AccountName: compatFs.AzBlobConfig.AccountName,
AccountKey: kms.NewEmptySecret(),
Endpoint: compatFs.AzBlobConfig.Endpoint,
SASURL: compatFs.AzBlobConfig.SASURL,
KeyPrefix: compatFs.AzBlobConfig.KeyPrefix,
UploadPartSize: compatFs.AzBlobConfig.UploadPartSize,
UploadConcurrency: compatFs.AzBlobConfig.UploadConcurrency,
UseEmulator: compatFs.AzBlobConfig.UseEmulator,
AccessTier: compatFs.AzBlobConfig.AccessTier,
}
if compatFs.AzBlobConfig.AccountKey != "" {
secret, err := kms.GetSecretFromCompatString(compatFs.AzBlobConfig.AccountKey)
if err != nil {
providerLog(logger.LevelError, "unable to convert v4 filesystem for user %#v: %v", username, err)
return fsConfig, err
}
fsConfig.AzBlobConfig.AccountKey = secret
}
case GCSFilesystemProvider:
fsConfig.GCSConfig = vfs.GCSFsConfig{
Bucket: compatFs.GCSConfig.Bucket,
KeyPrefix: compatFs.GCSConfig.KeyPrefix,
CredentialFile: compatFs.GCSConfig.CredentialFile,
AutomaticCredentials: compatFs.GCSConfig.AutomaticCredentials,
StorageClass: compatFs.GCSConfig.StorageClass,
}
if compatFs.GCSConfig.AutomaticCredentials == 0 {
compatFs.GCSConfig.CredentialFile = filepath.Join(credentialsDirPath, fmt.Sprintf("%v_gcs_credentials.json",
username))
}
secret, err := getCGSCredentialsFromV4(compatFs.GCSConfig)
if err != nil {
providerLog(logger.LevelError, "unable to convert v4 filesystem for user %#v: %v", username, err)
return fsConfig, err
}
fsConfig.GCSConfig.Credentials = secret
}
return fsConfig, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,902 +0,0 @@
package dataprovider
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"sync"
"time"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/utils"
"github.com/drakkan/sftpgo/vfs"
)
var (
errMemoryProviderClosed = errors.New("memory provider is closed")
)
type memoryProviderHandle struct {
// configuration file to use for loading users
configFile string
sync.Mutex
isClosed bool
// slice with ordered usernames
usernames []string
// map for users, username is the key
users map[string]User
// map for virtual folders, folder name is the key
vfolders map[string]vfs.BaseVirtualFolder
// slice with ordered folder names
vfoldersNames []string
// map for admins, username is the key
admins map[string]Admin
// slice with ordered admins
adminsUsernames []string
}
// MemoryProvider auth provider for a memory store
type MemoryProvider struct {
dbHandle *memoryProviderHandle
}
func initializeMemoryProvider(basePath string) {
logSender = fmt.Sprintf("dataprovider_%v", MemoryDataProviderName)
configFile := ""
if utils.IsFileInputValid(config.Name) {
configFile = config.Name
if !filepath.IsAbs(configFile) {
configFile = filepath.Join(basePath, configFile)
}
}
provider = &MemoryProvider{
dbHandle: &memoryProviderHandle{
isClosed: false,
usernames: []string{},
users: make(map[string]User),
vfolders: make(map[string]vfs.BaseVirtualFolder),
vfoldersNames: []string{},
admins: make(map[string]Admin),
adminsUsernames: []string{},
configFile: configFile,
},
}
if err := provider.reloadConfig(); err != nil {
logger.Error(logSender, "", "unable to load initial data: %v", err)
logger.ErrorToConsole("unable to load initial data: %v", err)
}
}
func (p *MemoryProvider) checkAvailability() error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
return nil
}
func (p *MemoryProvider) close() error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
p.dbHandle.isClosed = true
return nil
}
func (p *MemoryProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
var user User
if password == "" {
return user, errors.New("Credentials cannot be null or empty")
}
user, err := p.userExists(username)
if err != nil {
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
return user, err
}
return checkUserAndPass(user, password, ip, protocol)
}
func (p *MemoryProvider) validateUserAndPubKey(username string, pubKey []byte) (User, string, error) {
var user User
if len(pubKey) == 0 {
return user, "", errors.New("Credentials cannot be null or empty")
}
user, err := p.userExists(username)
if err != nil {
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
return user, "", err
}
return checkUserAndPubKey(user, pubKey)
}
func (p *MemoryProvider) validateAdminAndPass(username, password, ip string) (Admin, error) {
admin, err := p.adminExists(username)
if err != nil {
providerLog(logger.LevelWarn, "error authenticating admin %#v: %v", username, err)
return admin, err
}
err = admin.checkUserAndPass(password, ip)
return admin, err
}
func (p *MemoryProvider) updateLastLogin(username 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
}
user.LastLogin = utils.GetTimeAsMsSinceEpoch(time.Now())
p.dbHandle.users[user.Username] = user
return nil
}
func (p *MemoryProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
user, err := p.userExistsInternal(username)
if err != nil {
providerLog(logger.LevelWarn, "unable to update quota for user %#v error: %v", username, err)
return err
}
if reset {
user.UsedQuotaSize = sizeAdd
user.UsedQuotaFiles = filesAdd
} else {
user.UsedQuotaSize += sizeAdd
user.UsedQuotaFiles += filesAdd
}
user.LastQuotaUpdate = utils.GetTimeAsMsSinceEpoch(time.Now())
providerLog(logger.LevelDebug, "quota updated for user %#v, files increment: %v size increment: %v is reset? %v",
username, filesAdd, sizeAdd, reset)
p.dbHandle.users[user.Username] = user
return nil
}
func (p *MemoryProvider) getUsedQuota(username string) (int, int64, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return 0, 0, errMemoryProviderClosed
}
user, err := p.userExistsInternal(username)
if err != nil {
providerLog(logger.LevelWarn, "unable to get quota for user %#v error: %v", username, err)
return 0, 0, err
}
return user.UsedQuotaFiles, user.UsedQuotaSize, err
}
func (p *MemoryProvider) addUser(user *User) error {
// we can query virtual folder while validating a user
// so we have to check without holding the lock
err := ValidateUser(user)
if err != nil {
return err
}
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
_, err = p.userExistsInternal(user.Username)
if err == nil {
return fmt.Errorf("username %#v already exists", user.Username)
}
user.ID = p.getNextID()
user.LastQuotaUpdate = 0
user.UsedQuotaSize = 0
user.UsedQuotaFiles = 0
user.LastLogin = 0
user.VirtualFolders = p.joinVirtualFoldersFields(user)
p.dbHandle.users[user.Username] = user.getACopy()
p.dbHandle.usernames = append(p.dbHandle.usernames, user.Username)
sort.Strings(p.dbHandle.usernames)
return nil
}
func (p *MemoryProvider) updateUser(user *User) error {
// we can query virtual folder while validating a user
// so we have to check without holding the lock
err := ValidateUser(user)
if err != nil {
return err
}
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
u, err := p.userExistsInternal(user.Username)
if err != nil {
return err
}
for _, oldFolder := range u.VirtualFolders {
p.removeUserFromFolderMapping(oldFolder.Name, u.Username)
}
user.VirtualFolders = p.joinVirtualFoldersFields(user)
user.LastQuotaUpdate = u.LastQuotaUpdate
user.UsedQuotaSize = u.UsedQuotaSize
user.UsedQuotaFiles = u.UsedQuotaFiles
user.LastLogin = u.LastLogin
user.ID = u.ID
// pre-login and external auth hook will use the passed *user so save a copy
p.dbHandle.users[user.Username] = user.getACopy()
return nil
}
func (p *MemoryProvider) deleteUser(user *User) error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
u, err := p.userExistsInternal(user.Username)
if err != nil {
return err
}
for _, oldFolder := range u.VirtualFolders {
p.removeUserFromFolderMapping(oldFolder.Name, u.Username)
}
delete(p.dbHandle.users, user.Username)
// this could be more efficient
p.dbHandle.usernames = make([]string, 0, len(p.dbHandle.users))
for username := range p.dbHandle.users {
p.dbHandle.usernames = append(p.dbHandle.usernames, username)
}
sort.Strings(p.dbHandle.usernames)
return nil
}
func (p *MemoryProvider) dumpUsers() ([]User, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
users := make([]User, 0, len(p.dbHandle.usernames))
var err error
if p.dbHandle.isClosed {
return users, errMemoryProviderClosed
}
for _, username := range p.dbHandle.usernames {
u := p.dbHandle.users[username]
user := u.getACopy()
err = addCredentialsToUser(&user)
if err != nil {
return users, err
}
users = append(users, user)
}
return users, err
}
func (p *MemoryProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
folders := make([]vfs.BaseVirtualFolder, 0, len(p.dbHandle.vfoldersNames))
if p.dbHandle.isClosed {
return folders, errMemoryProviderClosed
}
for _, f := range p.dbHandle.vfolders {
folders = append(folders, f)
}
return folders, nil
}
func (p *MemoryProvider) getUsers(limit int, offset int, order string) ([]User, error) {
users := make([]User, 0, limit)
var err error
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return users, errMemoryProviderClosed
}
if limit <= 0 {
return users, err
}
itNum := 0
if order == OrderASC {
for _, username := range p.dbHandle.usernames {
itNum++
if itNum <= offset {
continue
}
u := p.dbHandle.users[username]
user := u.getACopy()
user.HideConfidentialData()
users = append(users, user)
if len(users) >= limit {
break
}
}
} else {
for i := len(p.dbHandle.usernames) - 1; i >= 0; i-- {
itNum++
if itNum <= offset {
continue
}
username := p.dbHandle.usernames[i]
u := p.dbHandle.users[username]
user := u.getACopy()
user.HideConfidentialData()
users = append(users, user)
if len(users) >= limit {
break
}
}
}
return users, err
}
func (p *MemoryProvider) userExists(username string) (User, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return User{}, errMemoryProviderClosed
}
return p.userExistsInternal(username)
}
func (p *MemoryProvider) userExistsInternal(username string) (User, error) {
if val, ok := p.dbHandle.users[username]; ok {
return val.getACopy(), nil
}
return User{}, &RecordNotFoundError{err: fmt.Sprintf("username %#v does not exist", username)}
}
func (p *MemoryProvider) addAdmin(admin *Admin) error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
err := admin.validate()
if err != nil {
return err
}
_, err = p.adminExistsInternal(admin.Username)
if err == nil {
return fmt.Errorf("admin %#v already exists", admin.Username)
}
admin.ID = p.getNextAdminID()
p.dbHandle.admins[admin.Username] = admin.getACopy()
p.dbHandle.adminsUsernames = append(p.dbHandle.adminsUsernames, admin.Username)
sort.Strings(p.dbHandle.adminsUsernames)
return nil
}
func (p *MemoryProvider) updateAdmin(admin *Admin) error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
err := admin.validate()
if err != nil {
return err
}
a, err := p.adminExistsInternal(admin.Username)
if err != nil {
return err
}
admin.ID = a.ID
p.dbHandle.admins[admin.Username] = admin.getACopy()
return nil
}
func (p *MemoryProvider) deleteAdmin(admin *Admin) error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
_, err := p.adminExistsInternal(admin.Username)
if err != nil {
return err
}
delete(p.dbHandle.admins, admin.Username)
// this could be more efficient
p.dbHandle.adminsUsernames = make([]string, 0, len(p.dbHandle.admins))
for username := range p.dbHandle.admins {
p.dbHandle.adminsUsernames = append(p.dbHandle.adminsUsernames, username)
}
sort.Strings(p.dbHandle.adminsUsernames)
return nil
}
func (p *MemoryProvider) adminExists(username string) (Admin, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return Admin{}, errMemoryProviderClosed
}
return p.adminExistsInternal(username)
}
func (p *MemoryProvider) adminExistsInternal(username string) (Admin, error) {
if val, ok := p.dbHandle.admins[username]; ok {
return val.getACopy(), nil
}
return Admin{}, &RecordNotFoundError{err: fmt.Sprintf("admin %#v does not exist", username)}
}
func (p *MemoryProvider) dumpAdmins() ([]Admin, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
admins := make([]Admin, 0, len(p.dbHandle.admins))
if p.dbHandle.isClosed {
return admins, errMemoryProviderClosed
}
for _, admin := range p.dbHandle.admins {
admins = append(admins, admin)
}
return admins, nil
}
func (p *MemoryProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) {
admins := make([]Admin, 0, limit)
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return admins, errMemoryProviderClosed
}
if limit <= 0 {
return admins, nil
}
itNum := 0
if order == OrderASC {
for _, username := range p.dbHandle.adminsUsernames {
itNum++
if itNum <= offset {
continue
}
a := p.dbHandle.admins[username]
admin := a.getACopy()
admin.HideConfidentialData()
admins = append(admins, admin)
if len(admins) >= limit {
break
}
}
} else {
for i := len(p.dbHandle.adminsUsernames) - 1; i >= 0; i-- {
itNum++
if itNum <= offset {
continue
}
username := p.dbHandle.adminsUsernames[i]
a := p.dbHandle.admins[username]
admin := a.getACopy()
admin.HideConfidentialData()
admins = append(admins, admin)
if len(admins) >= limit {
break
}
}
}
return admins, nil
}
func (p *MemoryProvider) updateFolderQuota(name string, filesAdd int, sizeAdd int64, reset bool) error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
folder, err := p.folderExistsInternal(name)
if err != nil {
providerLog(logger.LevelWarn, "unable to update quota for folder %#v error: %v", name, err)
return err
}
if reset {
folder.UsedQuotaSize = sizeAdd
folder.UsedQuotaFiles = filesAdd
} else {
folder.UsedQuotaSize += sizeAdd
folder.UsedQuotaFiles += filesAdd
}
folder.LastQuotaUpdate = utils.GetTimeAsMsSinceEpoch(time.Now())
p.dbHandle.vfolders[name] = folder
return nil
}
func (p *MemoryProvider) getUsedFolderQuota(name string) (int, int64, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return 0, 0, errMemoryProviderClosed
}
folder, err := p.folderExistsInternal(name)
if err != nil {
providerLog(logger.LevelWarn, "unable to get quota for folder %#v error: %v", name, err)
return 0, 0, err
}
return folder.UsedQuotaFiles, folder.UsedQuotaSize, err
}
func (p *MemoryProvider) joinVirtualFoldersFields(user *User) []vfs.VirtualFolder {
var folders []vfs.VirtualFolder
for _, folder := range user.VirtualFolders {
f, err := p.addOrGetFolderInternal(folder.Name, folder.MappedPath, user.Username)
if err == nil {
folder.UsedQuotaFiles = f.UsedQuotaFiles
folder.UsedQuotaSize = f.UsedQuotaSize
folder.LastQuotaUpdate = f.LastQuotaUpdate
folder.ID = f.ID
folder.MappedPath = f.MappedPath
folders = append(folders, folder)
}
}
return folders
}
func (p *MemoryProvider) removeUserFromFolderMapping(folderName, username string) {
folder, err := p.folderExistsInternal(folderName)
if err == nil {
var usernames []string
for _, user := range folder.Users {
if user != username {
usernames = append(usernames, user)
}
}
folder.Users = usernames
p.dbHandle.vfolders[folder.Name] = folder
}
}
func (p *MemoryProvider) updateFoldersMappingInternal(folder vfs.BaseVirtualFolder) {
p.dbHandle.vfolders[folder.Name] = folder
if !utils.IsStringInSlice(folder.Name, p.dbHandle.vfoldersNames) {
p.dbHandle.vfoldersNames = append(p.dbHandle.vfoldersNames, folder.Name)
sort.Strings(p.dbHandle.vfoldersNames)
}
}
func (p *MemoryProvider) addOrGetFolderInternal(folderName, folderMappedPath, username string) (vfs.BaseVirtualFolder, error) {
folder, err := p.folderExistsInternal(folderName)
if _, ok := err.(*RecordNotFoundError); ok {
folder := vfs.BaseVirtualFolder{
ID: p.getNextFolderID(),
Name: folderName,
MappedPath: folderMappedPath,
UsedQuotaSize: 0,
UsedQuotaFiles: 0,
LastQuotaUpdate: 0,
Users: []string{username},
}
p.updateFoldersMappingInternal(folder)
return folder, nil
}
if err == nil && !utils.IsStringInSlice(username, folder.Users) {
folder.Users = append(folder.Users, username)
p.updateFoldersMappingInternal(folder)
}
return folder, err
}
func (p *MemoryProvider) folderExistsInternal(name string) (vfs.BaseVirtualFolder, error) {
if val, ok := p.dbHandle.vfolders[name]; ok {
return val, nil
}
return vfs.BaseVirtualFolder{}, &RecordNotFoundError{err: fmt.Sprintf("folder %#v does not exist", name)}
}
func (p *MemoryProvider) getFolders(limit, offset int, order string) ([]vfs.BaseVirtualFolder, error) {
folders := make([]vfs.BaseVirtualFolder, 0, limit)
var err error
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return folders, errMemoryProviderClosed
}
if limit <= 0 {
return folders, err
}
itNum := 0
if order == OrderASC {
for _, name := range p.dbHandle.vfoldersNames {
itNum++
if itNum <= offset {
continue
}
folder := p.dbHandle.vfolders[name]
folders = append(folders, folder)
if len(folders) >= limit {
break
}
}
} else {
for i := len(p.dbHandle.vfoldersNames) - 1; i >= 0; i-- {
itNum++
if itNum <= offset {
continue
}
name := p.dbHandle.vfoldersNames[i]
folder := p.dbHandle.vfolders[name]
folders = append(folders, folder)
if len(folders) >= limit {
break
}
}
}
return folders, err
}
func (p *MemoryProvider) getFolderByName(name string) (vfs.BaseVirtualFolder, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return vfs.BaseVirtualFolder{}, errMemoryProviderClosed
}
return p.folderExistsInternal(name)
}
func (p *MemoryProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
err := ValidateFolder(folder)
if err != nil {
return err
}
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
_, err = p.folderExistsInternal(folder.Name)
if err == nil {
return fmt.Errorf("folder %#v already exists", folder.Name)
}
folder.ID = p.getNextFolderID()
folder.Users = nil
p.dbHandle.vfolders[folder.Name] = folder.GetACopy()
p.dbHandle.vfoldersNames = append(p.dbHandle.vfoldersNames, folder.Name)
sort.Strings(p.dbHandle.vfoldersNames)
return nil
}
func (p *MemoryProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {
err := ValidateFolder(folder)
if err != nil {
return err
}
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
f, err := p.folderExistsInternal(folder.Name)
if err != nil {
return err
}
folder.ID = f.ID
folder.LastQuotaUpdate = f.LastQuotaUpdate
folder.UsedQuotaFiles = f.UsedQuotaFiles
folder.UsedQuotaSize = f.UsedQuotaSize
folder.Users = f.Users
p.dbHandle.vfolders[folder.Name] = folder.GetACopy()
return nil
}
func (p *MemoryProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
_, err := p.folderExistsInternal(folder.Name)
if err != nil {
return err
}
for _, username := range folder.Users {
user, err := p.userExistsInternal(username)
if err == nil {
var folders []vfs.VirtualFolder
for _, userFolder := range user.VirtualFolders {
if folder.Name != userFolder.Name {
folders = append(folders, userFolder)
}
}
user.VirtualFolders = folders
p.dbHandle.users[user.Username] = user
}
}
delete(p.dbHandle.vfolders, folder.Name)
p.dbHandle.vfoldersNames = []string{}
for name := range p.dbHandle.vfolders {
p.dbHandle.vfoldersNames = append(p.dbHandle.vfoldersNames, name)
}
sort.Strings(p.dbHandle.vfoldersNames)
return nil
}
func (p *MemoryProvider) getNextID() int64 {
nextID := int64(1)
for _, v := range p.dbHandle.users {
if v.ID >= nextID {
nextID = v.ID + 1
}
}
return nextID
}
func (p *MemoryProvider) getNextFolderID() int64 {
nextID := int64(1)
for _, v := range p.dbHandle.vfolders {
if v.ID >= nextID {
nextID = v.ID + 1
}
}
return nextID
}
func (p *MemoryProvider) getNextAdminID() int64 {
nextID := int64(1)
for _, a := range p.dbHandle.admins {
if a.ID >= nextID {
nextID = a.ID + 1
}
}
return nextID
}
func (p *MemoryProvider) clear() {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
p.dbHandle.usernames = []string{}
p.dbHandle.users = make(map[string]User)
p.dbHandle.vfoldersNames = []string{}
p.dbHandle.vfolders = make(map[string]vfs.BaseVirtualFolder)
p.dbHandle.admins = make(map[string]Admin)
p.dbHandle.adminsUsernames = []string{}
}
func (p *MemoryProvider) reloadConfig() error {
if p.dbHandle.configFile == "" {
providerLog(logger.LevelDebug, "no dump configuration file defined")
return nil
}
providerLog(logger.LevelDebug, "loading dump from file: %#v", p.dbHandle.configFile)
fi, err := os.Stat(p.dbHandle.configFile)
if err != nil {
providerLog(logger.LevelWarn, "error loading dump: %v", err)
return err
}
if fi.Size() == 0 {
err = errors.New("dump configuration file is invalid, its size must be > 0")
providerLog(logger.LevelWarn, "error loading dump: %v", err)
return err
}
if fi.Size() > 10485760 {
err = errors.New("dump configuration file is invalid, its size must be <= 10485760 bytes")
providerLog(logger.LevelWarn, "error loading dump: %v", err)
return err
}
content, err := ioutil.ReadFile(p.dbHandle.configFile)
if err != nil {
providerLog(logger.LevelWarn, "error loading dump: %v", err)
return err
}
dump, err := ParseDumpData(content)
if err != nil {
providerLog(logger.LevelWarn, "error loading dump: %v", err)
return err
}
p.clear()
if err := p.restoreFolders(&dump); err != nil {
return err
}
if err := p.restoreUsers(&dump); err != nil {
return err
}
if err := p.restoreAdmins(&dump); err != nil {
return err
}
providerLog(logger.LevelDebug, "config loaded from file: %#v", p.dbHandle.configFile)
return nil
}
func (p *MemoryProvider) restoreAdmins(dump *BackupData) error {
for _, admin := range dump.Admins {
a, err := p.adminExists(admin.Username)
admin := admin // pin
if err == nil {
admin.ID = a.ID
err = p.updateAdmin(&admin)
if err != nil {
providerLog(logger.LevelWarn, "error updating admin %#v: %v", admin.Username, err)
return err
}
} else {
err = p.addAdmin(&admin)
if err != nil {
providerLog(logger.LevelWarn, "error adding admin %#v: %v", admin.Username, err)
return err
}
}
}
return nil
}
func (p *MemoryProvider) restoreFolders(dump *BackupData) error {
for _, folder := range dump.Folders {
folder := folder // pin
f, err := p.getFolderByName(folder.Name)
if err == nil {
folder.ID = f.ID
err = p.updateFolder(&folder)
if err != nil {
providerLog(logger.LevelWarn, "error updating folder %#v: %v", folder.Name, err)
return err
}
} else {
folder.Users = nil
err = p.addFolder(&folder)
if err != nil {
providerLog(logger.LevelWarn, "error adding folder %#v: %v", folder.Name, err)
return err
}
}
}
return nil
}
func (p *MemoryProvider) restoreUsers(dump *BackupData) error {
for _, user := range dump.Users {
user := user // pin
u, err := p.userExists(user.Username)
if err == nil {
user.ID = u.ID
err = p.updateUser(&user)
if err != nil {
providerLog(logger.LevelWarn, "error updating user %#v: %v", user.Username, err)
return err
}
} else {
err = p.addUser(&user)
if err != nil {
providerLog(logger.LevelWarn, "error adding user %#v: %v", user.Username, err)
return err
}
}
}
return nil
}
// initializeDatabase does nothing, no initilization is needed for memory provider
func (p *MemoryProvider) initializeDatabase() error {
return ErrNoInitRequired
}
func (p *MemoryProvider) migrateDatabase() error {
return ErrNoInitRequired
}
func (p *MemoryProvider) revertDatabase(targetVersion int) error {
return errors.New("memory provider does not store data, revert not possible")
}

View File

@@ -1,451 +0,0 @@
// +build !nomysql
package dataprovider
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
// we import go-sql-driver/mysql here to be able to disable MySQL support using a build tag
_ "github.com/go-sql-driver/mysql"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/version"
"github.com/drakkan/sftpgo/vfs"
)
const (
mysqlUsersTableSQL = "CREATE TABLE `{{users}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, " +
"`username` varchar(255) NOT NULL UNIQUE, `password` varchar(255) NULL, `public_keys` longtext NULL, " +
"`home_dir` varchar(255) NOT NULL, `uid` integer NOT NULL, `gid` integer NOT NULL, `max_sessions` integer NOT NULL, " +
" `quota_size` bigint NOT NULL, `quota_files` integer NOT NULL, `permissions` longtext NOT NULL, " +
"`used_quota_size` bigint NOT NULL, `used_quota_files` integer NOT NULL, `last_quota_update` bigint NOT NULL, " +
"`upload_bandwidth` integer NOT NULL, `download_bandwidth` integer NOT NULL, `expiration_date` bigint(20) NOT NULL, " +
"`last_login` bigint(20) NOT NULL, `status` int(11) NOT NULL, `filters` longtext DEFAULT NULL, " +
"`filesystem` longtext DEFAULT NULL);"
mysqlSchemaTableSQL = "CREATE TABLE `{{schema_version}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `version` integer NOT NULL);"
mysqlV2SQL = "ALTER TABLE `{{users}}` ADD COLUMN `virtual_folders` longtext NULL;"
mysqlV3SQL = "ALTER TABLE `{{users}}` MODIFY `password` longtext NULL;"
mysqlV4SQL = "CREATE TABLE `{{folders}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `path` varchar(512) NOT NULL UNIQUE," +
"`used_quota_size` bigint NOT NULL, `used_quota_files` integer NOT NULL, `last_quota_update` bigint NOT NULL);" +
"ALTER TABLE `{{users}}` MODIFY `home_dir` varchar(512) NOT NULL;" +
"ALTER TABLE `{{users}}` DROP COLUMN `virtual_folders`;" +
"CREATE TABLE `{{folders_mapping}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `virtual_path` varchar(512) NOT NULL, " +
"`quota_size` bigint NOT NULL, `quota_files` integer NOT NULL, `folder_id` integer NOT NULL, `user_id` integer NOT NULL);" +
"ALTER TABLE `{{folders_mapping}}` ADD CONSTRAINT `unique_mapping` UNIQUE (`user_id`, `folder_id`);" +
"ALTER TABLE `{{folders_mapping}}` ADD CONSTRAINT `folders_mapping_folder_id_fk_folders_id` FOREIGN KEY (`folder_id`) REFERENCES `{{folders}}` (`id`) ON DELETE CASCADE;" +
"ALTER TABLE `{{folders_mapping}}` ADD CONSTRAINT `folders_mapping_user_id_fk_users_id` FOREIGN KEY (`user_id`) REFERENCES `{{users}}` (`id`) ON DELETE CASCADE;"
mysqlV6SQL = "ALTER TABLE `{{users}}` ADD COLUMN `additional_info` longtext NULL;"
mysqlV6DownSQL = "ALTER TABLE `{{users}}` DROP COLUMN `additional_info`;"
mysqlV7SQL = "CREATE TABLE `{{admins}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `username` varchar(255) NOT NULL UNIQUE, " +
"`password` varchar(255) NOT NULL, `email` varchar(255) NULL, `status` integer NOT NULL, `permissions` longtext NOT NULL, " +
"`filters` longtext NULL, `additional_info` longtext NULL);"
mysqlV7DownSQL = "DROP TABLE `{{admins}}` CASCADE;"
mysqlV8SQL = "ALTER TABLE `{{folders}}` ADD COLUMN `name` varchar(255) NULL;" +
"ALTER TABLE `{{folders}}` MODIFY `path` varchar(512) NULL;" +
"ALTER TABLE `{{folders}}` DROP INDEX `path`;" +
"UPDATE `{{folders}}` f1 SET name = (SELECT CONCAT('folder',f2.id) FROM `{{folders}}` f2 WHERE f2.id = f1.id);" +
"ALTER TABLE `{{folders}}` MODIFY `name` varchar(255) NOT NULL;" +
"ALTER TABLE `folders` ADD CONSTRAINT `name` UNIQUE (`name`);"
mysqlV8DownSQL = "ALTER TABLE `{{folders}}` DROP COLUMN `name`;" +
"ALTER TABLE `{{folders}}` MODIFY `path` varchar(512) NOT NULL;" +
"ALTER TABLE `{{folders}}` ADD CONSTRAINT `path` UNIQUE (`path`);"
)
// MySQLProvider auth provider for MySQL/MariaDB database
type MySQLProvider struct {
dbHandle *sql.DB
}
func init() {
version.AddFeature("+mysql")
}
func initializeMySQLProvider() error {
var err error
logSender = fmt.Sprintf("dataprovider_%v", MySQLDataProviderName)
dbHandle, err := sql.Open("mysql", getMySQLConnectionString(false))
if err == nil {
providerLog(logger.LevelDebug, "mysql database handle created, connection string: %#v, pool size: %v",
getMySQLConnectionString(true), config.PoolSize)
dbHandle.SetMaxOpenConns(config.PoolSize)
if config.PoolSize > 0 {
dbHandle.SetMaxIdleConns(config.PoolSize)
} else {
dbHandle.SetMaxIdleConns(2)
}
dbHandle.SetConnMaxLifetime(240 * time.Second)
provider = &MySQLProvider{dbHandle: dbHandle}
} else {
providerLog(logger.LevelWarn, "error creating mysql database handler, connection string: %#v, error: %v",
getMySQLConnectionString(true), err)
}
return err
}
func getMySQLConnectionString(redactedPwd bool) string {
var connectionString string
if config.ConnectionString == "" {
password := config.Password
if redactedPwd {
password = "[redacted]"
}
connectionString = fmt.Sprintf("%v:%v@tcp([%v]:%v)/%v?charset=utf8&interpolateParams=true&timeout=10s&tls=%v&writeTimeout=10s&readTimeout=10s",
config.Username, password, config.Host, config.Port, config.Name, getSSLMode())
} else {
connectionString = config.ConnectionString
}
return connectionString
}
func (p *MySQLProvider) checkAvailability() error {
return sqlCommonCheckAvailability(p.dbHandle)
}
func (p *MySQLProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
return sqlCommonValidateUserAndPass(username, password, ip, protocol, p.dbHandle)
}
func (p *MySQLProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) {
return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle)
}
func (p *MySQLProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
return sqlCommonUpdateQuota(username, filesAdd, sizeAdd, reset, p.dbHandle)
}
func (p *MySQLProvider) getUsedQuota(username string) (int, int64, error) {
return sqlCommonGetUsedQuota(username, p.dbHandle)
}
func (p *MySQLProvider) updateLastLogin(username string) error {
return sqlCommonUpdateLastLogin(username, p.dbHandle)
}
func (p *MySQLProvider) userExists(username string) (User, error) {
return sqlCommonGetUserByUsername(username, p.dbHandle)
}
func (p *MySQLProvider) addUser(user *User) error {
return sqlCommonAddUser(user, p.dbHandle)
}
func (p *MySQLProvider) updateUser(user *User) error {
return sqlCommonUpdateUser(user, p.dbHandle)
}
func (p *MySQLProvider) deleteUser(user *User) error {
return sqlCommonDeleteUser(user, p.dbHandle)
}
func (p *MySQLProvider) dumpUsers() ([]User, error) {
return sqlCommonDumpUsers(p.dbHandle)
}
func (p *MySQLProvider) getUsers(limit int, offset int, order string) ([]User, error) {
return sqlCommonGetUsers(limit, offset, order, p.dbHandle)
}
func (p *MySQLProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
return sqlCommonDumpFolders(p.dbHandle)
}
func (p *MySQLProvider) getFolders(limit, offset int, order string) ([]vfs.BaseVirtualFolder, error) {
return sqlCommonGetFolders(limit, offset, order, p.dbHandle)
}
func (p *MySQLProvider) getFolderByName(name string) (vfs.BaseVirtualFolder, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
return sqlCommonGetFolderByName(ctx, name, p.dbHandle)
}
func (p *MySQLProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
return sqlCommonAddFolder(folder, p.dbHandle)
}
func (p *MySQLProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {
return sqlCommonUpdateFolder(folder, p.dbHandle)
}
func (p *MySQLProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
return sqlCommonDeleteFolder(folder, p.dbHandle)
}
func (p *MySQLProvider) updateFolderQuota(name string, filesAdd int, sizeAdd int64, reset bool) error {
return sqlCommonUpdateFolderQuota(name, filesAdd, sizeAdd, reset, p.dbHandle)
}
func (p *MySQLProvider) getUsedFolderQuota(name string) (int, int64, error) {
return sqlCommonGetFolderUsedQuota(name, p.dbHandle)
}
func (p *MySQLProvider) adminExists(username string) (Admin, error) {
return sqlCommonGetAdminByUsername(username, p.dbHandle)
}
func (p *MySQLProvider) addAdmin(admin *Admin) error {
return sqlCommonAddAdmin(admin, p.dbHandle)
}
func (p *MySQLProvider) updateAdmin(admin *Admin) error {
return sqlCommonUpdateAdmin(admin, p.dbHandle)
}
func (p *MySQLProvider) deleteAdmin(admin *Admin) error {
return sqlCommonDeleteAdmin(admin, p.dbHandle)
}
func (p *MySQLProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) {
return sqlCommonGetAdmins(limit, offset, order, p.dbHandle)
}
func (p *MySQLProvider) dumpAdmins() ([]Admin, error) {
return sqlCommonDumpAdmins(p.dbHandle)
}
func (p *MySQLProvider) validateAdminAndPass(username, password, ip string) (Admin, error) {
return sqlCommonValidateAdminAndPass(username, password, ip, p.dbHandle)
}
func (p *MySQLProvider) close() error {
return p.dbHandle.Close()
}
func (p *MySQLProvider) reloadConfig() error {
return nil
}
// initializeDatabase creates the initial database structure
func (p *MySQLProvider) initializeDatabase() error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, false)
if err == nil && dbVersion.Version > 0 {
return ErrNoInitRequired
}
sqlUsers := strings.Replace(mysqlUsersTableSQL, "{{users}}", sqlTableUsers, 1)
tx, err := p.dbHandle.Begin()
if err != nil {
return err
}
_, err = tx.Exec(sqlUsers)
if err != nil {
sqlCommonRollbackTransaction(tx)
return err
}
_, err = tx.Exec(strings.Replace(mysqlSchemaTableSQL, "{{schema_version}}", sqlTableSchemaVersion, 1))
if err != nil {
sqlCommonRollbackTransaction(tx)
return err
}
_, err = tx.Exec(strings.Replace(initialDBVersionSQL, "{{schema_version}}", sqlTableSchemaVersion, 1))
if err != nil {
sqlCommonRollbackTransaction(tx)
return err
}
return tx.Commit()
}
func (p *MySQLProvider) migrateDatabase() error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
if err != nil {
return err
}
if dbVersion.Version == sqlDatabaseVersion {
providerLog(logger.LevelDebug, "sql database is up to date, current version: %v", dbVersion.Version)
return ErrNoInitRequired
}
switch dbVersion.Version {
case 1:
return updateMySQLDatabaseFromV1(p.dbHandle)
case 2:
return updateMySQLDatabaseFromV2(p.dbHandle)
case 3:
return updateMySQLDatabaseFromV3(p.dbHandle)
case 4:
return updateMySQLDatabaseFromV4(p.dbHandle)
case 5:
return updateMySQLDatabaseFromV5(p.dbHandle)
case 6:
return updateMySQLDatabaseFromV6(p.dbHandle)
case 7:
return updateMySQLDatabaseFromV7(p.dbHandle)
default:
if dbVersion.Version > sqlDatabaseVersion {
providerLog(logger.LevelWarn, "database version %v is newer than the supported: %v", dbVersion.Version,
sqlDatabaseVersion)
logger.WarnToConsole("database version %v is newer than the supported: %v", dbVersion.Version,
sqlDatabaseVersion)
return nil
}
return fmt.Errorf("Database version not handled: %v", dbVersion.Version)
}
}
//nolint:dupl
func (p *MySQLProvider) revertDatabase(targetVersion int) error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
if err != nil {
return err
}
if dbVersion.Version == targetVersion {
return fmt.Errorf("current version match target version, nothing to do")
}
switch dbVersion.Version {
case 8:
err = downgradeMySQLDatabaseFrom8To7(p.dbHandle)
if err != nil {
return err
}
err = downgradeMySQLDatabaseFrom7To6(p.dbHandle)
if err != nil {
return err
}
err = downgradeMySQLDatabaseFrom6To5(p.dbHandle)
if err != nil {
return err
}
return downgradeMySQLDatabaseFrom5To4(p.dbHandle)
case 7:
err = downgradeMySQLDatabaseFrom7To6(p.dbHandle)
if err != nil {
return err
}
err = downgradeMySQLDatabaseFrom6To5(p.dbHandle)
if err != nil {
return err
}
return downgradeMySQLDatabaseFrom5To4(p.dbHandle)
case 6:
err = downgradeMySQLDatabaseFrom6To5(p.dbHandle)
if err != nil {
return err
}
return downgradeMySQLDatabaseFrom5To4(p.dbHandle)
case 5:
return downgradeMySQLDatabaseFrom5To4(p.dbHandle)
default:
return fmt.Errorf("Database version not handled: %v", dbVersion.Version)
}
}
func updateMySQLDatabaseFromV1(dbHandle *sql.DB) error {
err := updateMySQLDatabaseFrom1To2(dbHandle)
if err != nil {
return err
}
return updateMySQLDatabaseFromV2(dbHandle)
}
func updateMySQLDatabaseFromV2(dbHandle *sql.DB) error {
err := updateMySQLDatabaseFrom2To3(dbHandle)
if err != nil {
return err
}
return updateMySQLDatabaseFromV3(dbHandle)
}
func updateMySQLDatabaseFromV3(dbHandle *sql.DB) error {
err := updateMySQLDatabaseFrom3To4(dbHandle)
if err != nil {
return err
}
return updateMySQLDatabaseFromV4(dbHandle)
}
func updateMySQLDatabaseFromV4(dbHandle *sql.DB) error {
err := updateMySQLDatabaseFrom4To5(dbHandle)
if err != nil {
return err
}
return updateMySQLDatabaseFromV5(dbHandle)
}
func updateMySQLDatabaseFromV5(dbHandle *sql.DB) error {
err := updateMySQLDatabaseFrom5To6(dbHandle)
if err != nil {
return err
}
return updateMySQLDatabaseFromV6(dbHandle)
}
func updateMySQLDatabaseFromV6(dbHandle *sql.DB) error {
err := updateMySQLDatabaseFrom6To7(dbHandle)
if err != nil {
return err
}
return updateMySQLDatabaseFromV7(dbHandle)
}
func updateMySQLDatabaseFromV7(dbHandle *sql.DB) error {
return updateMySQLDatabaseFrom7To8(dbHandle)
}
func updateMySQLDatabaseFrom1To2(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 1 -> 2")
providerLog(logger.LevelInfo, "updating database version: 1 -> 2")
sql := strings.Replace(mysqlV2SQL, "{{users}}", sqlTableUsers, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 2)
}
func updateMySQLDatabaseFrom2To3(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 2 -> 3")
providerLog(logger.LevelInfo, "updating database version: 2 -> 3")
sql := strings.Replace(mysqlV3SQL, "{{users}}", sqlTableUsers, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 3)
}
func updateMySQLDatabaseFrom3To4(dbHandle *sql.DB) error {
return sqlCommonUpdateDatabaseFrom3To4(mysqlV4SQL, dbHandle)
}
func updateMySQLDatabaseFrom4To5(dbHandle *sql.DB) error {
return sqlCommonUpdateDatabaseFrom4To5(dbHandle)
}
func updateMySQLDatabaseFrom5To6(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 5 -> 6")
providerLog(logger.LevelInfo, "updating database version: 5 -> 6")
sql := strings.Replace(mysqlV6SQL, "{{users}}", sqlTableUsers, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
}
func updateMySQLDatabaseFrom6To7(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 6 -> 7")
providerLog(logger.LevelInfo, "updating database version: 6 -> 7")
sql := strings.Replace(mysqlV7SQL, "{{admins}}", sqlTableAdmins, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 7)
}
func updateMySQLDatabaseFrom7To8(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 7 -> 8")
providerLog(logger.LevelInfo, "updating database version: 7 -> 8")
sql := strings.ReplaceAll(mysqlV8SQL, "{{folders}}", sqlTableFolders)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 8)
}
func downgradeMySQLDatabaseFrom8To7(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 8 -> 7")
providerLog(logger.LevelInfo, "downgrading database version: 8 -> 7")
sql := strings.ReplaceAll(mysqlV8DownSQL, "{{folders}}", sqlTableFolders)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 7)
}
func downgradeMySQLDatabaseFrom7To6(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 7 -> 6")
providerLog(logger.LevelInfo, "downgrading database version: 7 -> 6")
sql := strings.Replace(mysqlV7DownSQL, "{{admins}}", sqlTableAdmins, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
}
func downgradeMySQLDatabaseFrom6To5(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 6 -> 5")
providerLog(logger.LevelInfo, "downgrading database version: 6 -> 5")
sql := strings.Replace(mysqlV6DownSQL, "{{users}}", sqlTableUsers, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 5)
}
func downgradeMySQLDatabaseFrom5To4(dbHandle *sql.DB) error {
return sqlCommonDowngradeDatabaseFrom5To4(dbHandle)
}

View File

@@ -1,17 +0,0 @@
// +build nomysql
package dataprovider
import (
"errors"
"github.com/drakkan/sftpgo/version"
)
func init() {
version.AddFeature("-mysql")
}
func initializeMySQLProvider() error {
return errors.New("MySQL disabled at build time")
}

View File

@@ -1,455 +0,0 @@
// +build !nopgsql
package dataprovider
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
// we import lib/pq here to be able to disable PostgreSQL support using a build tag
_ "github.com/lib/pq"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/version"
"github.com/drakkan/sftpgo/vfs"
)
const (
pgsqlUsersTableSQL = `CREATE TABLE "{{users}}" ("id" serial NOT NULL PRIMARY KEY, "username" varchar(255) NOT NULL UNIQUE,
"password" varchar(255) NULL, "public_keys" text NULL, "home_dir" varchar(255) NOT NULL, "uid" integer NOT NULL,
"gid" integer NOT NULL, "max_sessions" integer NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL,
"permissions" text NOT NULL, "used_quota_size" bigint NOT NULL, "used_quota_files" integer NOT NULL,
"last_quota_update" bigint NOT NULL, "upload_bandwidth" integer NOT NULL, "download_bandwidth" integer NOT NULL,
"expiration_date" bigint NOT NULL, "last_login" bigint NOT NULL, "status" integer NOT NULL, "filters" text NULL,
"filesystem" text NULL);`
pgsqlSchemaTableSQL = `CREATE TABLE "{{schema_version}}" ("id" serial NOT NULL PRIMARY KEY, "version" integer NOT NULL);`
pgsqlV2SQL = `ALTER TABLE "{{users}}" ADD COLUMN "virtual_folders" text NULL;`
pgsqlV3SQL = `ALTER TABLE "{{users}}" ALTER COLUMN "password" TYPE text USING "password"::text;`
pgsqlV4SQL = `CREATE TABLE "{{folders}}" ("id" serial NOT NULL PRIMARY KEY, "path" varchar(512) NOT NULL UNIQUE, "used_quota_size" bigint NOT NULL, "used_quota_files" integer NOT NULL, "last_quota_update" bigint NOT NULL);
ALTER TABLE "{{users}}" ALTER COLUMN "home_dir" TYPE varchar(512) USING "home_dir"::varchar(512);
ALTER TABLE "{{users}}" DROP COLUMN "virtual_folders" CASCADE;
CREATE TABLE "{{folders_mapping}}" ("id" serial NOT NULL PRIMARY KEY, "virtual_path" varchar(512) NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL, "folder_id" integer NOT NULL, "user_id" integer NOT NULL);
ALTER TABLE "{{folders_mapping}}" ADD CONSTRAINT "unique_mapping" UNIQUE ("user_id", "folder_id");
ALTER TABLE "{{folders_mapping}}" ADD CONSTRAINT "folders_mapping_folder_id_fk_folders_id" FOREIGN KEY ("folder_id") REFERENCES "{{folders}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE "{{folders_mapping}}" ADD CONSTRAINT "folders_mapping_user_id_fk_users_id" FOREIGN KEY ("user_id") REFERENCES "{{users}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
CREATE INDEX "folders_mapping_folder_id_idx" ON "{{folders_mapping}}" ("folder_id");
CREATE INDEX "folders_mapping_user_id_idx" ON "{{folders_mapping}}" ("user_id");
`
pgsqlV6SQL = `ALTER TABLE "{{users}}" ADD COLUMN "additional_info" text NULL;`
pgsqlV6DownSQL = `ALTER TABLE "{{users}}" DROP COLUMN "additional_info" CASCADE;`
pgsqlV7SQL = `CREATE TABLE "{{admins}}" ("id" serial NOT NULL PRIMARY KEY, "username" varchar(255) NOT NULL UNIQUE,
"password" varchar(255) NOT NULL, "email" varchar(255) NULL, "status" integer NOT NULL, "permissions" text NOT NULL,
"filters" text NULL, "additional_info" text NULL);
`
pgsqlV7DownSQL = `DROP TABLE "{{admins}}" CASCADE;`
pgsqlV8SQL = `ALTER TABLE "{{folders}}" ADD COLUMN "name" varchar(255) NULL;
ALTER TABLE "folders" ALTER COLUMN "path" DROP NOT NULL;
ALTER TABLE "{{folders}}" DROP CONSTRAINT IF EXISTS folders_path_key;
UPDATE "{{folders}}" f1 SET name = (SELECT CONCAT('folder',f2.id) FROM "{{folders}}" f2 WHERE f2.id = f1.id);
ALTER TABLE "{{folders}}" ALTER COLUMN "name" SET NOT NULL;
ALTER TABLE "{{folders}}" ADD CONSTRAINT "folders_name_uniq" UNIQUE ("name");
`
pgsqlV8DownSQL = `ALTER TABLE "{{folders}}" DROP COLUMN "name" CASCADE;
ALTER TABLE "{{folders}}" ALTER COLUMN "path" SET NOT NULL;
ALTER TABLE "{{folders}}" ADD CONSTRAINT folders_path_key UNIQUE (path);
`
)
// PGSQLProvider auth provider for PostgreSQL database
type PGSQLProvider struct {
dbHandle *sql.DB
}
func init() {
version.AddFeature("+pgsql")
}
func initializePGSQLProvider() error {
var err error
logSender = fmt.Sprintf("dataprovider_%v", PGSQLDataProviderName)
dbHandle, err := sql.Open("postgres", getPGSQLConnectionString(false))
if err == nil {
providerLog(logger.LevelDebug, "postgres database handle created, connection string: %#v, pool size: %v",
getPGSQLConnectionString(true), config.PoolSize)
dbHandle.SetMaxOpenConns(config.PoolSize)
if config.PoolSize > 0 {
dbHandle.SetMaxIdleConns(config.PoolSize)
} else {
dbHandle.SetMaxIdleConns(2)
}
dbHandle.SetConnMaxLifetime(240 * time.Second)
provider = &PGSQLProvider{dbHandle: dbHandle}
} else {
providerLog(logger.LevelWarn, "error creating postgres database handler, connection string: %#v, error: %v",
getPGSQLConnectionString(true), err)
}
return err
}
func getPGSQLConnectionString(redactedPwd bool) string {
var connectionString string
if config.ConnectionString == "" {
password := config.Password
if redactedPwd {
password = "[redacted]"
}
connectionString = fmt.Sprintf("host='%v' port=%v dbname='%v' user='%v' password='%v' sslmode=%v connect_timeout=10",
config.Host, config.Port, config.Name, config.Username, password, getSSLMode())
} else {
connectionString = config.ConnectionString
}
return connectionString
}
func (p *PGSQLProvider) checkAvailability() error {
return sqlCommonCheckAvailability(p.dbHandle)
}
func (p *PGSQLProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
return sqlCommonValidateUserAndPass(username, password, ip, protocol, p.dbHandle)
}
func (p *PGSQLProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) {
return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle)
}
func (p *PGSQLProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
return sqlCommonUpdateQuota(username, filesAdd, sizeAdd, reset, p.dbHandle)
}
func (p *PGSQLProvider) getUsedQuota(username string) (int, int64, error) {
return sqlCommonGetUsedQuota(username, p.dbHandle)
}
func (p *PGSQLProvider) updateLastLogin(username string) error {
return sqlCommonUpdateLastLogin(username, p.dbHandle)
}
func (p *PGSQLProvider) userExists(username string) (User, error) {
return sqlCommonGetUserByUsername(username, p.dbHandle)
}
func (p *PGSQLProvider) addUser(user *User) error {
return sqlCommonAddUser(user, p.dbHandle)
}
func (p *PGSQLProvider) updateUser(user *User) error {
return sqlCommonUpdateUser(user, p.dbHandle)
}
func (p *PGSQLProvider) deleteUser(user *User) error {
return sqlCommonDeleteUser(user, p.dbHandle)
}
func (p *PGSQLProvider) dumpUsers() ([]User, error) {
return sqlCommonDumpUsers(p.dbHandle)
}
func (p *PGSQLProvider) getUsers(limit int, offset int, order string) ([]User, error) {
return sqlCommonGetUsers(limit, offset, order, p.dbHandle)
}
func (p *PGSQLProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
return sqlCommonDumpFolders(p.dbHandle)
}
func (p *PGSQLProvider) getFolders(limit, offset int, order string) ([]vfs.BaseVirtualFolder, error) {
return sqlCommonGetFolders(limit, offset, order, p.dbHandle)
}
func (p *PGSQLProvider) getFolderByName(name string) (vfs.BaseVirtualFolder, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
return sqlCommonGetFolderByName(ctx, name, p.dbHandle)
}
func (p *PGSQLProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
return sqlCommonAddFolder(folder, p.dbHandle)
}
func (p *PGSQLProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {
return sqlCommonUpdateFolder(folder, p.dbHandle)
}
func (p *PGSQLProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
return sqlCommonDeleteFolder(folder, p.dbHandle)
}
func (p *PGSQLProvider) updateFolderQuota(name string, filesAdd int, sizeAdd int64, reset bool) error {
return sqlCommonUpdateFolderQuota(name, filesAdd, sizeAdd, reset, p.dbHandle)
}
func (p *PGSQLProvider) getUsedFolderQuota(name string) (int, int64, error) {
return sqlCommonGetFolderUsedQuota(name, p.dbHandle)
}
func (p *PGSQLProvider) adminExists(username string) (Admin, error) {
return sqlCommonGetAdminByUsername(username, p.dbHandle)
}
func (p *PGSQLProvider) addAdmin(admin *Admin) error {
return sqlCommonAddAdmin(admin, p.dbHandle)
}
func (p *PGSQLProvider) updateAdmin(admin *Admin) error {
return sqlCommonUpdateAdmin(admin, p.dbHandle)
}
func (p *PGSQLProvider) deleteAdmin(admin *Admin) error {
return sqlCommonDeleteAdmin(admin, p.dbHandle)
}
func (p *PGSQLProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) {
return sqlCommonGetAdmins(limit, offset, order, p.dbHandle)
}
func (p *PGSQLProvider) dumpAdmins() ([]Admin, error) {
return sqlCommonDumpAdmins(p.dbHandle)
}
func (p *PGSQLProvider) validateAdminAndPass(username, password, ip string) (Admin, error) {
return sqlCommonValidateAdminAndPass(username, password, ip, p.dbHandle)
}
func (p *PGSQLProvider) close() error {
return p.dbHandle.Close()
}
func (p *PGSQLProvider) reloadConfig() error {
return nil
}
// initializeDatabase creates the initial database structure
func (p *PGSQLProvider) initializeDatabase() error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, false)
if err == nil && dbVersion.Version > 0 {
return ErrNoInitRequired
}
sqlUsers := strings.Replace(pgsqlUsersTableSQL, "{{users}}", sqlTableUsers, 1)
tx, err := p.dbHandle.Begin()
if err != nil {
return err
}
_, err = tx.Exec(sqlUsers)
if err != nil {
sqlCommonRollbackTransaction(tx)
return err
}
_, err = tx.Exec(strings.Replace(pgsqlSchemaTableSQL, "{{schema_version}}", sqlTableSchemaVersion, 1))
if err != nil {
sqlCommonRollbackTransaction(tx)
return err
}
_, err = tx.Exec(strings.Replace(initialDBVersionSQL, "{{schema_version}}", sqlTableSchemaVersion, 1))
if err != nil {
sqlCommonRollbackTransaction(tx)
return err
}
return tx.Commit()
}
func (p *PGSQLProvider) migrateDatabase() error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
if err != nil {
return err
}
if dbVersion.Version == sqlDatabaseVersion {
providerLog(logger.LevelDebug, "sql database is up to date, current version: %v", dbVersion.Version)
return ErrNoInitRequired
}
switch dbVersion.Version {
case 1:
return updatePGSQLDatabaseFromV1(p.dbHandle)
case 2:
return updatePGSQLDatabaseFromV2(p.dbHandle)
case 3:
return updatePGSQLDatabaseFromV3(p.dbHandle)
case 4:
return updatePGSQLDatabaseFromV4(p.dbHandle)
case 5:
return updatePGSQLDatabaseFromV5(p.dbHandle)
case 6:
return updatePGSQLDatabaseFromV6(p.dbHandle)
case 7:
return updatePGSQLDatabaseFromV7(p.dbHandle)
default:
if dbVersion.Version > sqlDatabaseVersion {
providerLog(logger.LevelWarn, "database version %v is newer than the supported: %v", dbVersion.Version,
sqlDatabaseVersion)
logger.WarnToConsole("database version %v is newer than the supported: %v", dbVersion.Version,
sqlDatabaseVersion)
return nil
}
return fmt.Errorf("Database version not handled: %v", dbVersion.Version)
}
}
//nolint:dupl
func (p *PGSQLProvider) revertDatabase(targetVersion int) error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
if err != nil {
return err
}
if dbVersion.Version == targetVersion {
return fmt.Errorf("current version match target version, nothing to do")
}
switch dbVersion.Version {
case 8:
err = downgradePGSQLDatabaseFrom8To7(p.dbHandle)
if err != nil {
return err
}
err = downgradePGSQLDatabaseFrom7To6(p.dbHandle)
if err != nil {
return err
}
err = downgradePGSQLDatabaseFrom6To5(p.dbHandle)
if err != nil {
return err
}
return downgradePGSQLDatabaseFrom5To4(p.dbHandle)
case 7:
err = downgradePGSQLDatabaseFrom7To6(p.dbHandle)
if err != nil {
return err
}
err = downgradePGSQLDatabaseFrom6To5(p.dbHandle)
if err != nil {
return err
}
return downgradePGSQLDatabaseFrom5To4(p.dbHandle)
case 6:
err = downgradePGSQLDatabaseFrom6To5(p.dbHandle)
if err != nil {
return err
}
return downgradePGSQLDatabaseFrom5To4(p.dbHandle)
case 5:
return downgradePGSQLDatabaseFrom5To4(p.dbHandle)
default:
return fmt.Errorf("Database version not handled: %v", dbVersion.Version)
}
}
func updatePGSQLDatabaseFromV1(dbHandle *sql.DB) error {
err := updatePGSQLDatabaseFrom1To2(dbHandle)
if err != nil {
return err
}
return updatePGSQLDatabaseFromV2(dbHandle)
}
func updatePGSQLDatabaseFromV2(dbHandle *sql.DB) error {
err := updatePGSQLDatabaseFrom2To3(dbHandle)
if err != nil {
return err
}
return updatePGSQLDatabaseFromV3(dbHandle)
}
func updatePGSQLDatabaseFromV3(dbHandle *sql.DB) error {
err := updatePGSQLDatabaseFrom3To4(dbHandle)
if err != nil {
return err
}
return updatePGSQLDatabaseFromV4(dbHandle)
}
func updatePGSQLDatabaseFromV4(dbHandle *sql.DB) error {
err := updatePGSQLDatabaseFrom4To5(dbHandle)
if err != nil {
return err
}
return updatePGSQLDatabaseFromV5(dbHandle)
}
func updatePGSQLDatabaseFromV5(dbHandle *sql.DB) error {
err := updatePGSQLDatabaseFrom5To6(dbHandle)
if err != nil {
return err
}
return updatePGSQLDatabaseFromV6(dbHandle)
}
func updatePGSQLDatabaseFromV6(dbHandle *sql.DB) error {
err := updatePGSQLDatabaseFrom6To7(dbHandle)
if err != nil {
return err
}
return updatePGSQLDatabaseFromV7(dbHandle)
}
func updatePGSQLDatabaseFromV7(dbHandle *sql.DB) error {
return updatePGSQLDatabaseFrom7To8(dbHandle)
}
func updatePGSQLDatabaseFrom1To2(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 1 -> 2")
providerLog(logger.LevelInfo, "updating database version: 1 -> 2")
sql := strings.Replace(pgsqlV2SQL, "{{users}}", sqlTableUsers, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 2)
}
func updatePGSQLDatabaseFrom2To3(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 2 -> 3")
providerLog(logger.LevelInfo, "updating database version: 2 -> 3")
sql := strings.Replace(pgsqlV3SQL, "{{users}}", sqlTableUsers, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 3)
}
func updatePGSQLDatabaseFrom3To4(dbHandle *sql.DB) error {
return sqlCommonUpdateDatabaseFrom3To4(pgsqlV4SQL, dbHandle)
}
func updatePGSQLDatabaseFrom4To5(dbHandle *sql.DB) error {
return sqlCommonUpdateDatabaseFrom4To5(dbHandle)
}
func updatePGSQLDatabaseFrom5To6(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 5 -> 6")
providerLog(logger.LevelInfo, "updating database version: 5 -> 6")
sql := strings.Replace(pgsqlV6SQL, "{{users}}", sqlTableUsers, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
}
func updatePGSQLDatabaseFrom6To7(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 6 -> 7")
providerLog(logger.LevelInfo, "updating database version: 6 -> 7")
sql := strings.Replace(pgsqlV7SQL, "{{admins}}", sqlTableAdmins, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 7)
}
func updatePGSQLDatabaseFrom7To8(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 7 -> 8")
providerLog(logger.LevelInfo, "updating database version: 7 -> 8")
sql := strings.ReplaceAll(pgsqlV8SQL, "{{folders}}", sqlTableFolders)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 8)
}
func downgradePGSQLDatabaseFrom8To7(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 8 -> 7")
providerLog(logger.LevelInfo, "downgrading database version: 8 -> 7")
sql := strings.ReplaceAll(pgsqlV8DownSQL, "{{folders}}", sqlTableAdmins)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 7)
}
func downgradePGSQLDatabaseFrom7To6(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 7 -> 6")
providerLog(logger.LevelInfo, "downgrading database version: 7 -> 6")
sql := strings.Replace(pgsqlV7DownSQL, "{{admins}}", sqlTableAdmins, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
}
func downgradePGSQLDatabaseFrom6To5(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 6 -> 5")
providerLog(logger.LevelInfo, "downgrading database version: 6 -> 5")
sql := strings.Replace(pgsqlV6DownSQL, "{{users}}", sqlTableUsers, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 5)
}
func downgradePGSQLDatabaseFrom5To4(dbHandle *sql.DB) error {
return sqlCommonDowngradeDatabaseFrom5To4(dbHandle)
}

View File

@@ -1,17 +0,0 @@
// +build nopgsql
package dataprovider
import (
"errors"
"github.com/drakkan/sftpgo/version"
)
func init() {
version.AddFeature("-pgsql")
}
func initializePGSQLProvider() error {
return errors.New("PostgreSQL disabled at build time")
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,510 +0,0 @@
// +build !nosqlite
package dataprovider
import (
"context"
"database/sql"
"fmt"
"path/filepath"
"strings"
// we import go-sqlite3 here to be able to disable SQLite support using a build tag
_ "github.com/mattn/go-sqlite3"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/utils"
"github.com/drakkan/sftpgo/version"
"github.com/drakkan/sftpgo/vfs"
)
const (
sqliteUsersTableSQL = `CREATE TABLE "{{users}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "username" varchar(255)
NOT NULL UNIQUE, "password" varchar(255) NULL, "public_keys" text NULL, "home_dir" varchar(255) NOT NULL, "uid" integer NOT NULL,
"gid" integer NOT NULL, "max_sessions" integer NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL,
"permissions" text NOT NULL, "used_quota_size" bigint NOT NULL, "used_quota_files" integer NOT NULL,
"last_quota_update" bigint NOT NULL, "upload_bandwidth" integer NOT NULL, "download_bandwidth" integer NOT NULL,
"expiration_date" bigint NOT NULL, "last_login" bigint NOT NULL, "status" integer NOT NULL, "filters" text NULL,
"filesystem" text NULL);`
sqliteSchemaTableSQL = `CREATE TABLE "{{schema_version}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "version" integer NOT NULL);`
sqliteV2SQL = `ALTER TABLE "{{users}}" ADD COLUMN "virtual_folders" text NULL;`
sqliteV3SQL = `CREATE TABLE "new__users" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "username" varchar(255) NOT NULL UNIQUE,
"password" text NULL, "public_keys" text NULL, "home_dir" varchar(255) NOT NULL, "uid" integer NOT NULL,
"gid" integer NOT NULL, "max_sessions" integer NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL,
"permissions" text NOT NULL, "used_quota_size" bigint NOT NULL, "used_quota_files" integer NOT NULL, "last_quota_update" bigint NOT NULL,
"upload_bandwidth" integer NOT NULL, "download_bandwidth" integer NOT NULL, "expiration_date" bigint NOT NULL, "last_login" bigint NOT NULL,
"status" integer NOT NULL, "filters" text NULL, "filesystem" text NULL, "virtual_folders" text NULL);
INSERT INTO "new__users" ("id", "username", "public_keys", "home_dir", "uid", "gid", "max_sessions", "quota_size", "quota_files",
"permissions", "used_quota_size", "used_quota_files", "last_quota_update", "upload_bandwidth", "download_bandwidth", "expiration_date",
"last_login", "status", "filters", "filesystem", "virtual_folders", "password") SELECT "id", "username", "public_keys", "home_dir",
"uid", "gid", "max_sessions", "quota_size", "quota_files", "permissions", "used_quota_size", "used_quota_files", "last_quota_update",
"upload_bandwidth", "download_bandwidth", "expiration_date", "last_login", "status", "filters", "filesystem", "virtual_folders",
"password" FROM "{{users}}";
DROP TABLE "{{users}}";
ALTER TABLE "new__users" RENAME TO "{{users}}";`
sqliteV4SQL = `CREATE TABLE "{{folders}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "path" varchar(512) NOT NULL UNIQUE,
"used_quota_size" bigint NOT NULL, "used_quota_files" integer NOT NULL, "last_quota_update" bigint NOT NULL);
CREATE TABLE "{{folders_mapping}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "virtual_path" varchar(512) NOT NULL,
"quota_size" bigint NOT NULL, "quota_files" integer NOT NULL, "folder_id" integer NOT NULL REFERENCES "{{folders}}" ("id")
ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, "user_id" integer NOT NULL REFERENCES "{{users}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
CONSTRAINT "unique_mapping" UNIQUE ("user_id", "folder_id"));
CREATE TABLE "new__users" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "username" varchar(255) NOT NULL UNIQUE, "password" text NULL,
"public_keys" text NULL, "home_dir" varchar(512) NOT NULL, "uid" integer NOT NULL, "gid" integer NOT NULL, "max_sessions" integer NOT NULL,
"quota_size" bigint NOT NULL, "quota_files" integer NOT NULL, "permissions" text NOT NULL, "used_quota_size" bigint NOT NULL,
"used_quota_files" integer NOT NULL, "last_quota_update" bigint NOT NULL, "upload_bandwidth" integer NOT NULL, "download_bandwidth" integer NOT NULL,
"expiration_date" bigint NOT NULL, "last_login" bigint NOT NULL, "status" integer NOT NULL, "filters" text NULL, "filesystem" text NULL);
INSERT INTO "new__users" ("id", "username", "password", "public_keys", "home_dir", "uid", "gid", "max_sessions", "quota_size", "quota_files",
"permissions", "used_quota_size", "used_quota_files", "last_quota_update", "upload_bandwidth", "download_bandwidth", "expiration_date",
"last_login", "status", "filters", "filesystem") SELECT "id", "username", "password", "public_keys", "home_dir", "uid", "gid", "max_sessions",
"quota_size", "quota_files", "permissions", "used_quota_size", "used_quota_files", "last_quota_update", "upload_bandwidth", "download_bandwidth",
"expiration_date", "last_login", "status", "filters", "filesystem" FROM "{{users}}";
DROP TABLE "{{users}}";
ALTER TABLE "new__users" RENAME TO "{{users}}";
CREATE INDEX "folders_mapping_folder_id_idx" ON "{{folders_mapping}}" ("folder_id");
CREATE INDEX "folders_mapping_user_id_idx" ON "{{folders_mapping}}" ("user_id");
`
sqliteV6SQL = `ALTER TABLE "{{users}}" ADD COLUMN "additional_info" text NULL;`
sqliteV6DownSQL = `CREATE TABLE "new__users" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "username" varchar(255) NOT NULL UNIQUE,
"password" text NULL, "public_keys" text NULL, "home_dir" varchar(512) NOT NULL, "uid" integer NOT NULL, "gid" integer NOT NULL,
"max_sessions" integer NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL, "permissions" text NOT NULL,
"used_quota_size" bigint NOT NULL, "used_quota_files" integer NOT NULL, "last_quota_update" bigint NOT NULL, "upload_bandwidth" integer NOT NULL,
"download_bandwidth" integer NOT NULL, "expiration_date" bigint NOT NULL, "last_login" bigint NOT NULL, "status" integer NOT NULL,
"filters" text NULL, "filesystem" text NULL);
INSERT INTO "new__users" ("id", "username", "password", "public_keys", "home_dir", "uid", "gid", "max_sessions", "quota_size", "quota_files",
"permissions", "used_quota_size", "used_quota_files", "last_quota_update", "upload_bandwidth", "download_bandwidth", "expiration_date",
"last_login", "status", "filters", "filesystem") SELECT "id", "username", "password", "public_keys", "home_dir", "uid", "gid", "max_sessions",
"quota_size", "quota_files", "permissions", "used_quota_size", "used_quota_files", "last_quota_update", "upload_bandwidth", "download_bandwidth",
"expiration_date", "last_login", "status", "filters", "filesystem" FROM "{{users}}";
DROP TABLE "{{users}}";
ALTER TABLE "new__users" RENAME TO "{{users}}";
`
sqliteV7SQL = `CREATE TABLE "{{admins}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "username" varchar(255) NOT NULL UNIQUE,
"password" varchar(255) NOT NULL, "email" varchar(255) NULL, "status" integer NOT NULL, "permissions" text NOT NULL, "filters" text NULL,
"additional_info" text NULL);`
sqliteV7DownSQL = `DROP TABLE "{{admins}}";`
sqliteV8SQL = `CREATE TABLE "new__folders" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" varchar(255) NOT NULL UNIQUE, "path" varchar(512) NULL, "used_quota_size" bigint NOT NULL,
"used_quota_files" integer NOT NULL, "last_quota_update" bigint NOT NULL);
INSERT INTO "new__folders" ("id", "path", "used_quota_size", "used_quota_files", "last_quota_update", "name")
SELECT "id", "path", "used_quota_size", "used_quota_files", "last_quota_update", ('folder' || "id") FROM "{{folders}}";
DROP TABLE "{{folders}}";
ALTER TABLE "new__folders" RENAME TO "{{folders}}";
`
sqliteV8DownSQL = `CREATE TABLE "new__folders" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"path" varchar(512) NOT NULL UNIQUE, "used_quota_size" bigint NOT NULL, "used_quota_files" integer NOT NULL,
"last_quota_update" bigint NOT NULL);
INSERT INTO "new__folders" ("id", "path", "used_quota_size", "used_quota_files", "last_quota_update")
SELECT "id", "path", "used_quota_size", "used_quota_files", "last_quota_update" FROM "{{folders}}";
DROP TABLE "{{folders}}";
ALTER TABLE "new__folders" RENAME TO "{{folders}}";
`
)
// SQLiteProvider auth provider for SQLite database
type SQLiteProvider struct {
dbHandle *sql.DB
}
func init() {
version.AddFeature("+sqlite")
}
func initializeSQLiteProvider(basePath string) error {
var err error
var connectionString string
logSender = fmt.Sprintf("dataprovider_%v", SQLiteDataProviderName)
if config.ConnectionString == "" {
dbPath := config.Name
if !utils.IsFileInputValid(dbPath) {
return fmt.Errorf("Invalid database path: %#v", dbPath)
}
if !filepath.IsAbs(dbPath) {
dbPath = filepath.Join(basePath, dbPath)
}
connectionString = fmt.Sprintf("file:%v?cache=shared&_foreign_keys=1", dbPath)
} else {
connectionString = config.ConnectionString
}
dbHandle, err := sql.Open("sqlite3", connectionString)
if err == nil {
providerLog(logger.LevelDebug, "sqlite database handle created, connection string: %#v", connectionString)
dbHandle.SetMaxOpenConns(1)
provider = &SQLiteProvider{dbHandle: dbHandle}
} else {
providerLog(logger.LevelWarn, "error creating sqlite database handler, connection string: %#v, error: %v",
connectionString, err)
}
return err
}
func (p *SQLiteProvider) checkAvailability() error {
return sqlCommonCheckAvailability(p.dbHandle)
}
func (p *SQLiteProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
return sqlCommonValidateUserAndPass(username, password, ip, protocol, p.dbHandle)
}
func (p *SQLiteProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) {
return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle)
}
func (p *SQLiteProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
return sqlCommonUpdateQuota(username, filesAdd, sizeAdd, reset, p.dbHandle)
}
func (p *SQLiteProvider) getUsedQuota(username string) (int, int64, error) {
return sqlCommonGetUsedQuota(username, p.dbHandle)
}
func (p *SQLiteProvider) updateLastLogin(username string) error {
return sqlCommonUpdateLastLogin(username, p.dbHandle)
}
func (p *SQLiteProvider) userExists(username string) (User, error) {
return sqlCommonGetUserByUsername(username, p.dbHandle)
}
func (p *SQLiteProvider) addUser(user *User) error {
return sqlCommonAddUser(user, p.dbHandle)
}
func (p *SQLiteProvider) updateUser(user *User) error {
return sqlCommonUpdateUser(user, p.dbHandle)
}
func (p *SQLiteProvider) deleteUser(user *User) error {
return sqlCommonDeleteUser(user, p.dbHandle)
}
func (p *SQLiteProvider) dumpUsers() ([]User, error) {
return sqlCommonDumpUsers(p.dbHandle)
}
func (p *SQLiteProvider) getUsers(limit int, offset int, order string) ([]User, error) {
return sqlCommonGetUsers(limit, offset, order, p.dbHandle)
}
func (p *SQLiteProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
return sqlCommonDumpFolders(p.dbHandle)
}
func (p *SQLiteProvider) getFolders(limit, offset int, order string) ([]vfs.BaseVirtualFolder, error) {
return sqlCommonGetFolders(limit, offset, order, p.dbHandle)
}
func (p *SQLiteProvider) getFolderByName(name string) (vfs.BaseVirtualFolder, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
return sqlCommonGetFolderByName(ctx, name, p.dbHandle)
}
func (p *SQLiteProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
return sqlCommonAddFolder(folder, p.dbHandle)
}
func (p *SQLiteProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {
return sqlCommonUpdateFolder(folder, p.dbHandle)
}
func (p *SQLiteProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
return sqlCommonDeleteFolder(folder, p.dbHandle)
}
func (p *SQLiteProvider) updateFolderQuota(name string, filesAdd int, sizeAdd int64, reset bool) error {
return sqlCommonUpdateFolderQuota(name, filesAdd, sizeAdd, reset, p.dbHandle)
}
func (p *SQLiteProvider) getUsedFolderQuota(name string) (int, int64, error) {
return sqlCommonGetFolderUsedQuota(name, p.dbHandle)
}
func (p *SQLiteProvider) adminExists(username string) (Admin, error) {
return sqlCommonGetAdminByUsername(username, p.dbHandle)
}
func (p *SQLiteProvider) addAdmin(admin *Admin) error {
return sqlCommonAddAdmin(admin, p.dbHandle)
}
func (p *SQLiteProvider) updateAdmin(admin *Admin) error {
return sqlCommonUpdateAdmin(admin, p.dbHandle)
}
func (p *SQLiteProvider) deleteAdmin(admin *Admin) error {
return sqlCommonDeleteAdmin(admin, p.dbHandle)
}
func (p *SQLiteProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) {
return sqlCommonGetAdmins(limit, offset, order, p.dbHandle)
}
func (p *SQLiteProvider) dumpAdmins() ([]Admin, error) {
return sqlCommonDumpAdmins(p.dbHandle)
}
func (p *SQLiteProvider) validateAdminAndPass(username, password, ip string) (Admin, error) {
return sqlCommonValidateAdminAndPass(username, password, ip, p.dbHandle)
}
func (p *SQLiteProvider) close() error {
return p.dbHandle.Close()
}
func (p *SQLiteProvider) reloadConfig() error {
return nil
}
// initializeDatabase creates the initial database structure
func (p *SQLiteProvider) initializeDatabase() error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, false)
if err == nil && dbVersion.Version > 0 {
return ErrNoInitRequired
}
sqlUsers := strings.Replace(sqliteUsersTableSQL, "{{users}}", sqlTableUsers, 1)
tx, err := p.dbHandle.Begin()
if err != nil {
return err
}
_, err = tx.Exec(sqlUsers)
if err != nil {
sqlCommonRollbackTransaction(tx)
return err
}
_, err = tx.Exec(strings.Replace(sqliteSchemaTableSQL, "{{schema_version}}", sqlTableSchemaVersion, 1))
if err != nil {
sqlCommonRollbackTransaction(tx)
return err
}
_, err = tx.Exec(strings.Replace(initialDBVersionSQL, "{{schema_version}}", sqlTableSchemaVersion, 1))
if err != nil {
sqlCommonRollbackTransaction(tx)
return err
}
return tx.Commit()
}
func (p *SQLiteProvider) migrateDatabase() error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
if err != nil {
return err
}
if dbVersion.Version == sqlDatabaseVersion {
providerLog(logger.LevelDebug, "sql database is up to date, current version: %v", dbVersion.Version)
return ErrNoInitRequired
}
switch dbVersion.Version {
case 1:
return updateSQLiteDatabaseFromV1(p.dbHandle)
case 2:
return updateSQLiteDatabaseFromV2(p.dbHandle)
case 3:
return updateSQLiteDatabaseFromV3(p.dbHandle)
case 4:
return updateSQLiteDatabaseFromV4(p.dbHandle)
case 5:
return updateSQLiteDatabaseFromV5(p.dbHandle)
case 6:
return updateSQLiteDatabaseFromV6(p.dbHandle)
case 7:
return updateSQLiteDatabaseFromV7(p.dbHandle)
default:
if dbVersion.Version > sqlDatabaseVersion {
providerLog(logger.LevelWarn, "database version %v is newer than the supported: %v", dbVersion.Version,
sqlDatabaseVersion)
logger.WarnToConsole("database version %v is newer than the supported: %v", dbVersion.Version,
sqlDatabaseVersion)
return nil
}
return fmt.Errorf("Database version not handled: %v", dbVersion.Version)
}
}
//nolint:dupl
func (p *SQLiteProvider) revertDatabase(targetVersion int) error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
if err != nil {
return err
}
if dbVersion.Version == targetVersion {
return fmt.Errorf("current version match target version, nothing to do")
}
switch dbVersion.Version {
case 8:
err = downgradeSQLiteDatabaseFrom8To7(p.dbHandle)
if err != nil {
return err
}
err = downgradeSQLiteDatabaseFrom7To6(p.dbHandle)
if err != nil {
return err
}
err = downgradeSQLiteDatabaseFrom6To5(p.dbHandle)
if err != nil {
return err
}
return downgradeSQLiteDatabaseFrom5To4(p.dbHandle)
case 7:
err = downgradeSQLiteDatabaseFrom7To6(p.dbHandle)
if err != nil {
return err
}
err = downgradeSQLiteDatabaseFrom6To5(p.dbHandle)
if err != nil {
return err
}
return downgradeSQLiteDatabaseFrom5To4(p.dbHandle)
case 6:
err = downgradeSQLiteDatabaseFrom6To5(p.dbHandle)
if err != nil {
return err
}
return downgradeSQLiteDatabaseFrom5To4(p.dbHandle)
case 5:
return downgradeSQLiteDatabaseFrom5To4(p.dbHandle)
default:
return fmt.Errorf("Database version not handled: %v", dbVersion.Version)
}
}
func updateSQLiteDatabaseFromV1(dbHandle *sql.DB) error {
err := updateSQLiteDatabaseFrom1To2(dbHandle)
if err != nil {
return err
}
return updateSQLiteDatabaseFromV2(dbHandle)
}
func updateSQLiteDatabaseFromV2(dbHandle *sql.DB) error {
err := updateSQLiteDatabaseFrom2To3(dbHandle)
if err != nil {
return err
}
return updateSQLiteDatabaseFromV3(dbHandle)
}
func updateSQLiteDatabaseFromV3(dbHandle *sql.DB) error {
err := updateSQLiteDatabaseFrom3To4(dbHandle)
if err != nil {
return err
}
return updateSQLiteDatabaseFromV4(dbHandle)
}
func updateSQLiteDatabaseFromV4(dbHandle *sql.DB) error {
err := updateSQLiteDatabaseFrom4To5(dbHandle)
if err != nil {
return err
}
return updateSQLiteDatabaseFromV5(dbHandle)
}
func updateSQLiteDatabaseFromV5(dbHandle *sql.DB) error {
err := updateSQLiteDatabaseFrom5To6(dbHandle)
if err != nil {
return err
}
return updateSQLiteDatabaseFromV6(dbHandle)
}
func updateSQLiteDatabaseFromV6(dbHandle *sql.DB) error {
err := updateSQLiteDatabaseFrom6To7(dbHandle)
if err != nil {
return err
}
return updateSQLiteDatabaseFromV7(dbHandle)
}
func updateSQLiteDatabaseFromV7(dbHandle *sql.DB) error {
return updateSQLiteDatabaseFrom7To8(dbHandle)
}
func updateSQLiteDatabaseFrom1To2(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 1 -> 2")
providerLog(logger.LevelInfo, "updating database version: 1 -> 2")
sql := strings.Replace(sqliteV2SQL, "{{users}}", sqlTableUsers, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 2)
}
func updateSQLiteDatabaseFrom2To3(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 2 -> 3")
providerLog(logger.LevelInfo, "updating database version: 2 -> 3")
sql := strings.ReplaceAll(sqliteV3SQL, "{{users}}", sqlTableUsers)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 3)
}
func updateSQLiteDatabaseFrom3To4(dbHandle *sql.DB) error {
return sqlCommonUpdateDatabaseFrom3To4(sqliteV4SQL, dbHandle)
}
func updateSQLiteDatabaseFrom4To5(dbHandle *sql.DB) error {
return sqlCommonUpdateDatabaseFrom4To5(dbHandle)
}
func updateSQLiteDatabaseFrom5To6(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 5 -> 6")
providerLog(logger.LevelInfo, "updating database version: 5 -> 6")
sql := strings.Replace(sqliteV6SQL, "{{users}}", sqlTableUsers, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
}
func updateSQLiteDatabaseFrom6To7(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 6 -> 7")
providerLog(logger.LevelInfo, "updating database version: 6 -> 7")
sql := strings.Replace(sqliteV7SQL, "{{admins}}", sqlTableAdmins, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 7)
}
func updateSQLiteDatabaseFrom7To8(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 7 -> 8")
providerLog(logger.LevelInfo, "updating database version: 7 -> 8")
if err := setPragmaFK(dbHandle, "OFF"); err != nil {
return err
}
sql := strings.ReplaceAll(sqliteV8SQL, "{{folders}}", sqlTableFolders)
if err := sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 8); err != nil {
return err
}
return setPragmaFK(dbHandle, "ON")
}
func setPragmaFK(dbHandle *sql.DB, value string) error {
ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)
defer cancel()
sql := fmt.Sprintf("PRAGMA foreign_keys=%v;", value)
_, err := dbHandle.ExecContext(ctx, sql)
return err
}
func downgradeSQLiteDatabaseFrom8To7(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 8 -> 7")
providerLog(logger.LevelInfo, "downgrading database version: 8 -> 7")
if err := setPragmaFK(dbHandle, "OFF"); err != nil {
return err
}
sql := strings.ReplaceAll(sqliteV8DownSQL, "{{folders}}", sqlTableFolders)
if err := sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 7); err != nil {
return err
}
return setPragmaFK(dbHandle, "ON")
}
func downgradeSQLiteDatabaseFrom7To6(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 7 -> 6")
providerLog(logger.LevelInfo, "downgrading database version: 7 -> 6")
sql := strings.Replace(sqliteV7DownSQL, "{{admins}}", sqlTableAdmins, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
}
func downgradeSQLiteDatabaseFrom6To5(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 6 -> 5")
providerLog(logger.LevelInfo, "downgrading database version: 6 -> 5")
sql := strings.ReplaceAll(sqliteV6DownSQL, "{{users}}", sqlTableUsers)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 5)
}
func downgradeSQLiteDatabaseFrom5To4(dbHandle *sql.DB) error {
return sqlCommonDowngradeDatabaseFrom5To4(dbHandle)
}

View File

@@ -1,17 +0,0 @@
// +build nosqlite
package dataprovider
import (
"errors"
"github.com/drakkan/sftpgo/version"
)
func init() {
version.AddFeature("-sqlite")
}
func initializeSQLiteProvider(basePath string) error {
return errors.New("SQLite disabled at build time")
}

View File

@@ -1,217 +0,0 @@
package dataprovider
import (
"fmt"
"strconv"
"strings"
"github.com/drakkan/sftpgo/vfs"
)
const (
selectUserFields = "id,username,password,public_keys,home_dir,uid,gid,max_sessions,quota_size,quota_files,permissions,used_quota_size," +
"used_quota_files,last_quota_update,upload_bandwidth,download_bandwidth,expiration_date,last_login,status,filters,filesystem,additional_info"
selectFolderFields = "id,path,used_quota_size,used_quota_files,last_quota_update,name"
selectAdminFields = "id,username,password,status,email,permissions,filters,additional_info"
)
func getSQLPlaceholders() []string {
var placeholders []string
for i := 1; i <= 20; i++ {
if config.Driver == PGSQLDataProviderName {
placeholders = append(placeholders, fmt.Sprintf("$%v", i))
} else {
placeholders = append(placeholders, "?")
}
}
return placeholders
}
func getAdminByUsernameQuery() string {
return fmt.Sprintf(`SELECT %v FROM %v WHERE username = %v`, selectAdminFields, sqlTableAdmins, sqlPlaceholders[0])
}
func getAdminsQuery(order string) string {
return fmt.Sprintf(`SELECT %v FROM %v ORDER BY username %v LIMIT %v OFFSET %v`, selectAdminFields, sqlTableAdmins,
order, sqlPlaceholders[0], sqlPlaceholders[1])
}
func getDumpAdminsQuery() string {
return fmt.Sprintf(`SELECT %v FROM %v`, selectAdminFields, sqlTableAdmins)
}
func getAddAdminQuery() string {
return fmt.Sprintf(`INSERT INTO %v (username,password,status,email,permissions,filters,additional_info)
VALUES (%v,%v,%v,%v,%v,%v,%v)`, sqlTableAdmins, sqlPlaceholders[0], sqlPlaceholders[1],
sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6])
}
func getUpdateAdminQuery() string {
return fmt.Sprintf(`UPDATE %v SET password=%v,status=%v,email=%v,permissions=%v,filters=%v,additional_info=%v
WHERE username = %v`, sqlTableAdmins, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2],
sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6])
}
func getDeleteAdminQuery() string {
return fmt.Sprintf(`DELETE FROM %v WHERE username = %v`, sqlTableAdmins, sqlPlaceholders[0])
}
func getUserByUsernameQuery() string {
return fmt.Sprintf(`SELECT %v FROM %v WHERE username = %v`, selectUserFields, sqlTableUsers, sqlPlaceholders[0])
}
func getUsersQuery(order string) string {
return fmt.Sprintf(`SELECT %v FROM %v ORDER BY username %v LIMIT %v OFFSET %v`, selectUserFields, sqlTableUsers,
order, sqlPlaceholders[0], sqlPlaceholders[1])
}
func getDumpUsersQuery() string {
return fmt.Sprintf(`SELECT %v FROM %v`, selectUserFields, sqlTableUsers)
}
func getDumpFoldersQuery() string {
return fmt.Sprintf(`SELECT %v FROM %v`, selectFolderFields, sqlTableFolders)
}
func getUpdateQuotaQuery(reset bool) string {
if reset {
return fmt.Sprintf(`UPDATE %v SET used_quota_size = %v,used_quota_files = %v,last_quota_update = %v
WHERE username = %v`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])
}
return fmt.Sprintf(`UPDATE %v SET used_quota_size = used_quota_size + %v,used_quota_files = used_quota_files + %v,last_quota_update = %v
WHERE username = %v`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])
}
func getUpdateLastLoginQuery() string {
return fmt.Sprintf(`UPDATE %v SET last_login = %v WHERE username = %v`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1])
}
func getQuotaQuery() string {
return fmt.Sprintf(`SELECT used_quota_size,used_quota_files FROM %v WHERE username = %v`, sqlTableUsers,
sqlPlaceholders[0])
}
func getAddUserQuery() string {
return fmt.Sprintf(`INSERT INTO %v (username,password,public_keys,home_dir,uid,gid,max_sessions,quota_size,quota_files,permissions,
used_quota_size,used_quota_files,last_quota_update,upload_bandwidth,download_bandwidth,status,last_login,expiration_date,filters,
filesystem,additional_info)
VALUES (%v,%v,%v,%v,%v,%v,%v,%v,%v,%v,0,0,0,%v,%v,%v,0,%v,%v,%v,%v)`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1],
sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7],
sqlPlaceholders[8], sqlPlaceholders[9], sqlPlaceholders[10], sqlPlaceholders[11], sqlPlaceholders[12], sqlPlaceholders[13],
sqlPlaceholders[14], sqlPlaceholders[15], sqlPlaceholders[16])
}
func getUpdateUserQuery() string {
return fmt.Sprintf(`UPDATE %v SET password=%v,public_keys=%v,home_dir=%v,uid=%v,gid=%v,max_sessions=%v,quota_size=%v,
quota_files=%v,permissions=%v,upload_bandwidth=%v,download_bandwidth=%v,status=%v,expiration_date=%v,filters=%v,filesystem=%v,
additional_info=%v WHERE id = %v`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3],
sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9],
sqlPlaceholders[10], sqlPlaceholders[11], sqlPlaceholders[12], sqlPlaceholders[13], sqlPlaceholders[14], sqlPlaceholders[15],
sqlPlaceholders[16])
}
func getDeleteUserQuery() string {
return fmt.Sprintf(`DELETE FROM %v WHERE id = %v`, sqlTableUsers, sqlPlaceholders[0])
}
func getFolderByNameQuery() string {
return fmt.Sprintf(`SELECT %v FROM %v WHERE name = %v`, selectFolderFields, sqlTableFolders, sqlPlaceholders[0])
}
func getAddFolderQuery() string {
return fmt.Sprintf(`INSERT INTO %v (path,used_quota_size,used_quota_files,last_quota_update,name) VALUES (%v,%v,%v,%v,%v)`,
sqlTableFolders, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4])
}
func getUpdateFolderQuery() string {
return fmt.Sprintf(`UPDATE %v SET path = %v WHERE name = %v`, sqlTableFolders, sqlPlaceholders[0], sqlPlaceholders[1])
}
func getDeleteFolderQuery() string {
return fmt.Sprintf(`DELETE FROM %v WHERE id = %v`, sqlTableFolders, sqlPlaceholders[0])
}
func getClearFolderMappingQuery() string {
return fmt.Sprintf(`DELETE FROM %v WHERE user_id = (SELECT id FROM %v WHERE username = %v)`, sqlTableFoldersMapping,
sqlTableUsers, sqlPlaceholders[0])
}
func getAddFolderMappingQuery() string {
return fmt.Sprintf(`INSERT INTO %v (virtual_path,quota_size,quota_files,folder_id,user_id)
VALUES (%v,%v,%v,%v,(SELECT id FROM %v WHERE username = %v))`, sqlTableFoldersMapping, sqlPlaceholders[0],
sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlTableUsers, sqlPlaceholders[4])
}
func getFoldersQuery(order string) string {
return fmt.Sprintf(`SELECT %v FROM %v ORDER BY name %v LIMIT %v OFFSET %v`, selectFolderFields, sqlTableFolders,
order, sqlPlaceholders[0], sqlPlaceholders[1])
}
func getUpdateFolderQuotaQuery(reset bool) string {
if reset {
return fmt.Sprintf(`UPDATE %v SET used_quota_size = %v,used_quota_files = %v,last_quota_update = %v
WHERE name = %v`, sqlTableFolders, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])
}
return fmt.Sprintf(`UPDATE %v SET used_quota_size = used_quota_size + %v,used_quota_files = used_quota_files + %v,last_quota_update = %v
WHERE name = %v`, sqlTableFolders, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])
}
func getQuotaFolderQuery() string {
return fmt.Sprintf(`SELECT used_quota_size,used_quota_files FROM %v WHERE name = %v`, sqlTableFolders,
sqlPlaceholders[0])
}
func getRelatedFoldersForUsersQuery(users []User) string {
var sb strings.Builder
for _, u := range users {
if sb.Len() == 0 {
sb.WriteString("(")
} else {
sb.WriteString(",")
}
sb.WriteString(strconv.FormatInt(u.ID, 10))
}
if sb.Len() > 0 {
sb.WriteString(")")
}
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
FROM %v f INNER JOIN %v fm ON f.id = fm.folder_id WHERE fm.user_id IN %v ORDER BY fm.user_id`, sqlTableFolders,
sqlTableFoldersMapping, sb.String())
}
func getRelatedUsersForFoldersQuery(folders []vfs.BaseVirtualFolder) string {
var sb strings.Builder
for _, f := range folders {
if sb.Len() == 0 {
sb.WriteString("(")
} else {
sb.WriteString(",")
}
sb.WriteString(strconv.FormatInt(f.ID, 10))
}
if sb.Len() > 0 {
sb.WriteString(")")
}
return fmt.Sprintf(`SELECT fm.folder_id,u.username FROM %v fm INNER JOIN %v u ON fm.user_id = u.id
WHERE fm.folder_id IN %v ORDER BY fm.folder_id`, sqlTableFoldersMapping, sqlTableUsers, sb.String())
}
func getDatabaseVersionQuery() string {
return fmt.Sprintf("SELECT version from %v LIMIT 1", sqlTableSchemaVersion)
}
func getUpdateDBVersionQuery() string {
return fmt.Sprintf(`UPDATE %v SET version=%v`, sqlTableSchemaVersion, sqlPlaceholders[0])
}
/*func getCompatVirtualFoldersQuery() string {
return fmt.Sprintf(`SELECT id,username,virtual_folders FROM %v`, sqlTableUsers)
}*/
func getCompatV4FsConfigQuery() string {
return fmt.Sprintf(`SELECT id,username,filesystem FROM %v`, sqlTableUsers)
}
func updateCompatV4FsConfigQuery() string {
return fmt.Sprintf(`UPDATE %v SET filesystem=%v WHERE id=%v`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1])
}

View File

@@ -1,960 +0,0 @@
package dataprovider
import (
"encoding/json"
"errors"
"fmt"
"net"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"time"
"golang.org/x/net/webdav"
"github.com/drakkan/sftpgo/kms"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/utils"
"github.com/drakkan/sftpgo/vfs"
)
// Available permissions for SFTPGo users
const (
// All permissions are granted
PermAny = "*"
// List items such as files and directories is allowed
PermListItems = "list"
// download files is allowed
PermDownload = "download"
// upload files is allowed
PermUpload = "upload"
// overwrite an existing file, while uploading, is allowed
// upload permission is required to allow file overwrite
PermOverwrite = "overwrite"
// delete files or directories is allowed
PermDelete = "delete"
// rename files or directories is allowed
PermRename = "rename"
// create directories is allowed
PermCreateDirs = "create_dirs"
// create symbolic links is allowed
PermCreateSymlinks = "create_symlinks"
// changing file or directory permissions is allowed
PermChmod = "chmod"
// changing file or directory owner and group is allowed
PermChown = "chown"
// changing file or directory access and modification time is allowed
PermChtimes = "chtimes"
)
// Available login methods
const (
LoginMethodNoAuthTryed = "no_auth_tryed"
LoginMethodPassword = "password"
SSHLoginMethodPublicKey = "publickey"
SSHLoginMethodKeyboardInteractive = "keyboard-interactive"
SSHLoginMethodKeyAndPassword = "publickey+password"
SSHLoginMethodKeyAndKeyboardInt = "publickey+keyboard-interactive"
)
var (
errNoMatchingVirtualFolder = errors.New("no matching virtual folder found")
)
// CachedUser adds fields useful for caching to a SFTPGo user
type CachedUser struct {
User User
Expiration time.Time
Password string
LockSystem webdav.LockSystem
}
// IsExpired returns true if the cached user is expired
func (c *CachedUser) IsExpired() bool {
if c.Expiration.IsZero() {
return false
}
return c.Expiration.Before(time.Now())
}
// ExtensionsFilter defines filters based on file extensions.
// These restrictions do not apply to files listing for performance reasons, so
// a denied file cannot be downloaded/overwritten/renamed but will still be
// in the list of files.
// System commands such as Git and rsync interacts with the filesystem directly
// and they are not aware about these restrictions so they are not allowed
// inside paths with extensions filters
type ExtensionsFilter struct {
// Virtual path, if no other specific filter is defined, the filter apply for
// sub directories too.
// For example if filters are defined for the paths "/" and "/sub" then the
// filters for "/" are applied for any file outside the "/sub" directory
Path string `json:"path"`
// only files with these, case insensitive, extensions are allowed.
// Shell like expansion is not supported so you have to specify ".jpg" and
// not "*.jpg". If you want shell like patterns use pattern filters
AllowedExtensions []string `json:"allowed_extensions,omitempty"`
// files with these, case insensitive, extensions are not allowed.
// Denied file extensions are evaluated before the allowed ones
DeniedExtensions []string `json:"denied_extensions,omitempty"`
}
// PatternsFilter defines filters based on shell like patterns.
// These restrictions do not apply to files listing for performance reasons, so
// a denied file cannot be downloaded/overwritten/renamed but will still be
// in the list of files.
// System commands such as Git and rsync interacts with the filesystem directly
// and they are not aware about these restrictions so they are not allowed
// inside paths with extensions filters
type PatternsFilter struct {
// Virtual path, if no other specific filter is defined, the filter apply for
// sub directories too.
// For example if filters are defined for the paths "/" and "/sub" then the
// filters for "/" are applied for any file outside the "/sub" directory
Path string `json:"path"`
// files with these, case insensitive, patterns are allowed.
// Denied file patterns are evaluated before the allowed ones
AllowedPatterns []string `json:"allowed_patterns,omitempty"`
// files with these, case insensitive, patterns are not allowed.
// Denied file patterns are evaluated before the allowed ones
DeniedPatterns []string `json:"denied_patterns,omitempty"`
}
// UserFilters defines additional restrictions for a user
type UserFilters struct {
// only clients connecting from these IP/Mask are allowed.
// IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291
// for example "192.0.2.0/24" or "2001:db8::/32"
AllowedIP []string `json:"allowed_ip,omitempty"`
// clients connecting from these IP/Mask are not allowed.
// Denied rules will be evaluated before allowed ones
DeniedIP []string `json:"denied_ip,omitempty"`
// these login methods are not allowed.
// If null or empty any available login method is allowed
DeniedLoginMethods []string `json:"denied_login_methods,omitempty"`
// these protocols are not allowed.
// If null or empty any available protocol is allowed
DeniedProtocols []string `json:"denied_protocols,omitempty"`
// filters based on file extensions.
// Please note that these restrictions can be easily bypassed.
FileExtensions []ExtensionsFilter `json:"file_extensions,omitempty"`
// filter based on shell patterns
FilePatterns []PatternsFilter `json:"file_patterns,omitempty"`
// max size allowed for a single upload, 0 means unlimited
MaxUploadFileSize int64 `json:"max_upload_file_size,omitempty"`
}
// FilesystemProvider defines the supported storages
type FilesystemProvider int
// supported values for FilesystemProvider
const (
LocalFilesystemProvider FilesystemProvider = iota // Local
S3FilesystemProvider // AWS S3 compatible
GCSFilesystemProvider // Google Cloud Storage
AzureBlobFilesystemProvider // Azure Blob Storage
CryptedFilesystemProvider // Local encrypted
SFTPFilesystemProvider // SFTP
)
// Filesystem defines cloud storage filesystem details
type Filesystem struct {
Provider FilesystemProvider `json:"provider"`
S3Config vfs.S3FsConfig `json:"s3config,omitempty"`
GCSConfig vfs.GCSFsConfig `json:"gcsconfig,omitempty"`
AzBlobConfig vfs.AzBlobFsConfig `json:"azblobconfig,omitempty"`
CryptConfig vfs.CryptFsConfig `json:"cryptconfig,omitempty"`
SFTPConfig vfs.SFTPFsConfig `json:"sftpconfig,omitempty"`
}
// User defines a SFTPGo user
type User struct {
// Database unique identifier
ID int64 `json:"id"`
// 1 enabled, 0 disabled (login is not allowed)
Status int `json:"status"`
// Username
Username string `json:"username"`
// Account expiration date as unix timestamp in milliseconds. An expired account cannot login.
// 0 means no expiration
ExpirationDate int64 `json:"expiration_date"`
// Password used for password authentication.
// For users created using SFTPGo REST API the password is be stored using argon2id hashing algo.
// Checking passwords stored with bcrypt, pbkdf2, md5crypt and sha512crypt is supported too.
Password string `json:"password,omitempty"`
// PublicKeys used for public key authentication. At least one between password and a public key is mandatory
PublicKeys []string `json:"public_keys,omitempty"`
// The user cannot upload or download files outside this directory. Must be an absolute path
HomeDir string `json:"home_dir"`
// Mapping between virtual paths and filesystem paths outside the home directory.
// Supported for local filesystem only
VirtualFolders []vfs.VirtualFolder `json:"virtual_folders,omitempty"`
// If sftpgo runs as root system user then the created files and directories will be assigned to this system UID
UID int `json:"uid"`
// If sftpgo runs as root system user then the created files and directories will be assigned to this system GID
GID int `json:"gid"`
// Maximum concurrent sessions. 0 means unlimited
MaxSessions int `json:"max_sessions"`
// Maximum size allowed as bytes. 0 means unlimited
QuotaSize int64 `json:"quota_size"`
// Maximum number of files allowed. 0 means unlimited
QuotaFiles int `json:"quota_files"`
// List of the granted permissions
Permissions map[string][]string `json:"permissions"`
// Used quota as bytes
UsedQuotaSize int64 `json:"used_quota_size"`
// Used quota as number of files
UsedQuotaFiles int `json:"used_quota_files"`
// Last quota update as unix timestamp in milliseconds
LastQuotaUpdate int64 `json:"last_quota_update"`
// Maximum upload bandwidth as KB/s, 0 means unlimited
UploadBandwidth int64 `json:"upload_bandwidth"`
// Maximum download bandwidth as KB/s, 0 means unlimited
DownloadBandwidth int64 `json:"download_bandwidth"`
// Last login as unix timestamp in milliseconds
LastLogin int64 `json:"last_login"`
// Additional restrictions
Filters UserFilters `json:"filters"`
// Filesystem configuration details
FsConfig Filesystem `json:"filesystem"`
// free form text field for external systems
AdditionalInfo string `json:"additional_info,omitempty"`
}
// GetFilesystem returns the filesystem for this user
func (u *User) GetFilesystem(connectionID string) (vfs.Fs, error) {
switch u.FsConfig.Provider {
case S3FilesystemProvider:
return vfs.NewS3Fs(connectionID, u.GetHomeDir(), u.FsConfig.S3Config)
case GCSFilesystemProvider:
config := u.FsConfig.GCSConfig
config.CredentialFile = u.getGCSCredentialsFilePath()
return vfs.NewGCSFs(connectionID, u.GetHomeDir(), config)
case AzureBlobFilesystemProvider:
return vfs.NewAzBlobFs(connectionID, u.GetHomeDir(), u.FsConfig.AzBlobConfig)
case CryptedFilesystemProvider:
return vfs.NewCryptFs(connectionID, u.GetHomeDir(), u.FsConfig.CryptConfig)
case SFTPFilesystemProvider:
return vfs.NewSFTPFs(connectionID, u.FsConfig.SFTPConfig)
default:
return vfs.NewOsFs(connectionID, u.GetHomeDir(), u.VirtualFolders), nil
}
}
// HideConfidentialData hides user confidential data
func (u *User) HideConfidentialData() {
u.Password = ""
switch u.FsConfig.Provider {
case S3FilesystemProvider:
u.FsConfig.S3Config.AccessSecret.Hide()
case GCSFilesystemProvider:
u.FsConfig.GCSConfig.Credentials.Hide()
case AzureBlobFilesystemProvider:
u.FsConfig.AzBlobConfig.AccountKey.Hide()
case CryptedFilesystemProvider:
u.FsConfig.CryptConfig.Passphrase.Hide()
case SFTPFilesystemProvider:
u.FsConfig.SFTPConfig.Password.Hide()
u.FsConfig.SFTPConfig.PrivateKey.Hide()
}
}
// SetEmptySecrets sets to empty any user secret
func (u *User) SetEmptySecrets() {
u.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret()
u.FsConfig.GCSConfig.Credentials = kms.NewEmptySecret()
u.FsConfig.AzBlobConfig.AccountKey = kms.NewEmptySecret()
u.FsConfig.CryptConfig.Passphrase = kms.NewEmptySecret()
u.FsConfig.SFTPConfig.Password = kms.NewEmptySecret()
u.FsConfig.SFTPConfig.PrivateKey = kms.NewEmptySecret()
}
// DecryptSecrets tries to decrypts kms secrets
func (u *User) DecryptSecrets() error {
switch u.FsConfig.Provider {
case S3FilesystemProvider:
if u.FsConfig.S3Config.AccessSecret.IsEncrypted() {
return u.FsConfig.S3Config.AccessSecret.Decrypt()
}
case GCSFilesystemProvider:
if u.FsConfig.GCSConfig.Credentials.IsEncrypted() {
return u.FsConfig.GCSConfig.Credentials.Decrypt()
}
case AzureBlobFilesystemProvider:
if u.FsConfig.AzBlobConfig.AccountKey.IsEncrypted() {
return u.FsConfig.AzBlobConfig.AccountKey.Decrypt()
}
case CryptedFilesystemProvider:
if u.FsConfig.CryptConfig.Passphrase.IsEncrypted() {
return u.FsConfig.CryptConfig.Passphrase.Decrypt()
}
case SFTPFilesystemProvider:
if u.FsConfig.SFTPConfig.Password.IsEncrypted() {
if err := u.FsConfig.SFTPConfig.Password.Decrypt(); err != nil {
return err
}
}
if u.FsConfig.SFTPConfig.PrivateKey.IsEncrypted() {
if err := u.FsConfig.SFTPConfig.PrivateKey.Decrypt(); err != nil {
return err
}
}
}
return nil
}
// GetPermissionsForPath returns the permissions for the given path.
// The path must be an SFTP path
func (u *User) GetPermissionsForPath(p string) []string {
permissions := []string{}
if perms, ok := u.Permissions["/"]; ok {
// if only root permissions are defined returns them unconditionally
if len(u.Permissions) == 1 {
return perms
}
// fallback permissions
permissions = perms
}
dirsForPath := utils.GetDirsForSFTPPath(p)
// dirsForPath contains all the dirs for a given path in reverse order
// for example if the path is: /1/2/3/4 it contains:
// [ "/1/2/3/4", "/1/2/3", "/1/2", "/1", "/" ]
// so the first match is the one we are interested to
for _, val := range dirsForPath {
if perms, ok := u.Permissions[val]; ok {
permissions = perms
break
}
}
return permissions
}
// GetVirtualFolderForPath returns the virtual folder containing the specified sftp path.
// If the path is not inside a virtual folder an error is returned
func (u *User) GetVirtualFolderForPath(sftpPath string) (vfs.VirtualFolder, error) {
var folder vfs.VirtualFolder
if len(u.VirtualFolders) == 0 || u.FsConfig.Provider != LocalFilesystemProvider {
return folder, errNoMatchingVirtualFolder
}
dirsForPath := utils.GetDirsForSFTPPath(sftpPath)
for _, val := range dirsForPath {
for _, v := range u.VirtualFolders {
if v.VirtualPath == val {
return v, nil
}
}
}
return folder, errNoMatchingVirtualFolder
}
// AddVirtualDirs adds virtual folders, if defined, to the given files list
func (u *User) AddVirtualDirs(list []os.FileInfo, sftpPath string) []os.FileInfo {
if len(u.VirtualFolders) == 0 {
return list
}
for _, v := range u.VirtualFolders {
if path.Dir(v.VirtualPath) == sftpPath {
fi := vfs.NewFileInfo(v.VirtualPath, true, 0, time.Now(), false)
found := false
for index, f := range list {
if f.Name() == fi.Name() {
list[index] = fi
found = true
break
}
}
if !found {
list = append(list, fi)
}
}
}
return list
}
// IsMappedPath returns true if the specified filesystem path has a virtual folder mapping.
// The filesystem path must be cleaned before calling this method
func (u *User) IsMappedPath(fsPath string) bool {
for _, v := range u.VirtualFolders {
if fsPath == v.MappedPath {
return true
}
}
return false
}
// IsVirtualFolder returns true if the specified sftp path is a virtual folder
func (u *User) IsVirtualFolder(sftpPath string) bool {
for _, v := range u.VirtualFolders {
if sftpPath == v.VirtualPath {
return true
}
}
return false
}
// HasVirtualFoldersInside returns true if there are virtual folders inside the
// specified SFTP path. We assume that path are cleaned
func (u *User) HasVirtualFoldersInside(sftpPath string) bool {
if sftpPath == "/" && len(u.VirtualFolders) > 0 {
return true
}
for _, v := range u.VirtualFolders {
if len(v.VirtualPath) > len(sftpPath) {
if strings.HasPrefix(v.VirtualPath, sftpPath+"/") {
return true
}
}
}
return false
}
// HasPermissionsInside returns true if the specified sftpPath has no permissions itself and
// no subdirs with defined permissions
func (u *User) HasPermissionsInside(sftpPath string) bool {
for dir := range u.Permissions {
if dir == sftpPath {
return true
} else if len(dir) > len(sftpPath) {
if strings.HasPrefix(dir, sftpPath+"/") {
return true
}
}
}
return false
}
// HasOverlappedMappedPaths returns true if this user has virtual folders with overlapped mapped paths
func (u *User) HasOverlappedMappedPaths() bool {
if len(u.VirtualFolders) <= 1 {
return false
}
for _, v1 := range u.VirtualFolders {
for _, v2 := range u.VirtualFolders {
if v1.VirtualPath == v2.VirtualPath {
continue
}
if isMappedDirOverlapped(v1.MappedPath, v2.MappedPath) {
return true
}
}
}
return false
}
// 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 utils.IsStringInSlice(PermAny, perms) {
return true
}
return utils.IsStringInSlice(permission, perms)
}
// HasPerms return true if the user has all the given permissions
func (u *User) HasPerms(permissions []string, path string) bool {
perms := u.GetPermissionsForPath(path)
if utils.IsStringInSlice(PermAny, perms) {
return true
}
for _, permission := range permissions {
if !utils.IsStringInSlice(permission, perms) {
return false
}
}
return true
}
// HasNoQuotaRestrictions returns true if no quota restrictions need to be applyed
func (u *User) HasNoQuotaRestrictions(checkFiles bool) bool {
if u.QuotaSize == 0 && (!checkFiles || u.QuotaFiles == 0) {
return true
}
return false
}
// IsLoginMethodAllowed returns true if the specified login method is allowed
func (u *User) IsLoginMethodAllowed(loginMethod string, partialSuccessMethods []string) bool {
if len(u.Filters.DeniedLoginMethods) == 0 {
return true
}
if len(partialSuccessMethods) == 1 {
for _, method := range u.GetNextAuthMethods(partialSuccessMethods, true) {
if method == loginMethod {
return true
}
}
}
if utils.IsStringInSlice(loginMethod, u.Filters.DeniedLoginMethods) {
return false
}
return true
}
// GetNextAuthMethods returns the list of authentications methods that
// can continue for multi-step authentication
func (u *User) GetNextAuthMethods(partialSuccessMethods []string, isPasswordAuthEnabled bool) []string {
var methods []string
if len(partialSuccessMethods) != 1 {
return methods
}
if partialSuccessMethods[0] != SSHLoginMethodPublicKey {
return methods
}
for _, method := range u.GetAllowedLoginMethods() {
if method == SSHLoginMethodKeyAndPassword && isPasswordAuthEnabled {
methods = append(methods, LoginMethodPassword)
}
if method == SSHLoginMethodKeyAndKeyboardInt {
methods = append(methods, SSHLoginMethodKeyboardInteractive)
}
}
return methods
}
// IsPartialAuth returns true if the specified login method is a step for
// a multi-step Authentication.
// We support publickey+password and publickey+keyboard-interactive, so
// only publickey can returns partial success.
// We can have partial success if only multi-step Auth methods are enabled
func (u *User) IsPartialAuth(loginMethod string) bool {
if loginMethod != SSHLoginMethodPublicKey {
return false
}
for _, method := range u.GetAllowedLoginMethods() {
if !utils.IsStringInSlice(method, SSHMultiStepsLoginMethods) {
return false
}
}
return true
}
// GetAllowedLoginMethods returns the allowed login methods
func (u *User) GetAllowedLoginMethods() []string {
var allowedMethods []string
for _, method := range ValidSSHLoginMethods {
if !utils.IsStringInSlice(method, u.Filters.DeniedLoginMethods) {
allowedMethods = append(allowedMethods, method)
}
}
return allowedMethods
}
// IsFileAllowed returns true if the specified file is allowed by the file restrictions filters
func (u *User) IsFileAllowed(virtualPath string) bool {
return u.isFilePatternAllowed(virtualPath) && u.isFileExtensionAllowed(virtualPath)
}
func (u *User) isFileExtensionAllowed(virtualPath string) bool {
if len(u.Filters.FileExtensions) == 0 {
return true
}
dirsForPath := utils.GetDirsForSFTPPath(path.Dir(virtualPath))
var filter ExtensionsFilter
for _, dir := range dirsForPath {
for _, f := range u.Filters.FileExtensions {
if f.Path == dir {
filter = f
break
}
}
if filter.Path != "" {
break
}
}
if filter.Path != "" {
toMatch := strings.ToLower(virtualPath)
for _, denied := range filter.DeniedExtensions {
if strings.HasSuffix(toMatch, denied) {
return false
}
}
for _, allowed := range filter.AllowedExtensions {
if strings.HasSuffix(toMatch, allowed) {
return true
}
}
return len(filter.AllowedExtensions) == 0
}
return true
}
func (u *User) isFilePatternAllowed(virtualPath string) bool {
if len(u.Filters.FilePatterns) == 0 {
return true
}
dirsForPath := utils.GetDirsForSFTPPath(path.Dir(virtualPath))
var filter PatternsFilter
for _, dir := range dirsForPath {
for _, f := range u.Filters.FilePatterns {
if f.Path == dir {
filter = f
break
}
}
if filter.Path != "" {
break
}
}
if filter.Path != "" {
toMatch := strings.ToLower(path.Base(virtualPath))
for _, denied := range filter.DeniedPatterns {
matched, err := path.Match(denied, toMatch)
if err != nil || matched {
return false
}
}
for _, allowed := range filter.AllowedPatterns {
matched, err := path.Match(allowed, toMatch)
if err == nil && matched {
return true
}
}
return len(filter.AllowedPatterns) == 0
}
return true
}
// IsLoginFromAddrAllowed returns true if the login is allowed from the specified remoteAddr.
// If AllowedIP is defined only the specified IP/Mask can login.
// If DeniedIP is defined the specified IP/Mask cannot login.
// If an IP is both allowed and denied then login will be denied
func (u *User) IsLoginFromAddrAllowed(remoteAddr string) bool {
if len(u.Filters.AllowedIP) == 0 && len(u.Filters.DeniedIP) == 0 {
return true
}
remoteIP := net.ParseIP(utils.GetIPFromRemoteAddress(remoteAddr))
// if remoteIP is invalid we allow login, this should never happen
if remoteIP == nil {
logger.Warn(logSender, "", "login allowed for invalid IP. remote address: %#v", remoteAddr)
return true
}
for _, IPMask := range u.Filters.DeniedIP {
_, IPNet, err := net.ParseCIDR(IPMask)
if err != nil {
return false
}
if IPNet.Contains(remoteIP) {
return false
}
}
for _, IPMask := range u.Filters.AllowedIP {
_, IPNet, err := net.ParseCIDR(IPMask)
if err != nil {
return false
}
if IPNet.Contains(remoteIP) {
return true
}
}
return len(u.Filters.AllowedIP) == 0
}
// GetPermissionsAsJSON returns the permissions as json byte array
func (u *User) GetPermissionsAsJSON() ([]byte, error) {
return json.Marshal(u.Permissions)
}
// GetPublicKeysAsJSON returns the public keys as json byte array
func (u *User) GetPublicKeysAsJSON() ([]byte, error) {
return json.Marshal(u.PublicKeys)
}
// GetFiltersAsJSON returns the filters as json byte array
func (u *User) GetFiltersAsJSON() ([]byte, error) {
return json.Marshal(u.Filters)
}
// GetFsConfigAsJSON returns the filesystem config as json byte array
func (u *User) GetFsConfigAsJSON() ([]byte, error) {
return json.Marshal(u.FsConfig)
}
// GetUID returns a validate uid, suitable for use with os.Chown
func (u *User) GetUID() int {
if u.UID <= 0 || u.UID > 65535 {
return -1
}
return u.UID
}
// GetGID returns a validate gid, suitable for use with os.Chown
func (u *User) GetGID() int {
if u.GID <= 0 || u.GID > 65535 {
return -1
}
return u.GID
}
// GetHomeDir returns the shortest path name equivalent to the user's home directory
func (u *User) GetHomeDir() string {
return filepath.Clean(u.HomeDir)
}
// HasQuotaRestrictions returns true if there is a quota restriction on number of files or size or both
func (u *User) HasQuotaRestrictions() bool {
return u.QuotaFiles > 0 || u.QuotaSize > 0
}
// GetQuotaSummary returns used quota and limits if defined
func (u *User) GetQuotaSummary() string {
var result string
result = "Files: " + strconv.Itoa(u.UsedQuotaFiles)
if u.QuotaFiles > 0 {
result += "/" + strconv.Itoa(u.QuotaFiles)
}
if u.UsedQuotaSize > 0 || u.QuotaSize > 0 {
result += ". Size: " + utils.ByteCountSI(u.UsedQuotaSize)
if u.QuotaSize > 0 {
result += "/" + utils.ByteCountSI(u.QuotaSize)
}
}
return result
}
// GetPermissionsAsString returns the user's permissions as comma separated string
func (u *User) GetPermissionsAsString() string {
result := ""
for dir, perms := range u.Permissions {
var dirPerms string
for _, p := range perms {
if len(dirPerms) > 0 {
dirPerms += ", "
}
dirPerms += p
}
dp := fmt.Sprintf("%#v: %#v", dir, dirPerms)
if dir == "/" {
if len(result) > 0 {
result = dp + ", " + result
} else {
result = dp
}
} else {
if len(result) > 0 {
result += ", "
}
result += dp
}
}
return result
}
// GetBandwidthAsString returns bandwidth limits if defines
func (u *User) GetBandwidthAsString() string {
result := "Download: "
if u.DownloadBandwidth > 0 {
result += utils.ByteCountSI(u.DownloadBandwidth*1000) + "/s."
} else {
result += "unlimited."
}
result += " Upload: "
if u.UploadBandwidth > 0 {
result += utils.ByteCountSI(u.UploadBandwidth*1000) + "/s."
} else {
result += "unlimited."
}
return result
}
// GetInfoString returns user's info as string.
// Storage provider, number of public keys, max sessions, uid,
// gid, denied and allowed IP/Mask are returned
func (u *User) GetInfoString() string {
var result string
if u.LastLogin > 0 {
t := utils.GetTimeFromMsecSinceEpoch(u.LastLogin)
result += fmt.Sprintf("Last login: %v ", t.Format("2006-01-02 15:04:05")) // YYYY-MM-DD HH:MM:SS
}
switch u.FsConfig.Provider {
case S3FilesystemProvider:
result += "Storage: S3 "
case GCSFilesystemProvider:
result += "Storage: GCS "
case AzureBlobFilesystemProvider:
result += "Storage: Azure "
case CryptedFilesystemProvider:
result += "Storage: Encrypted "
case SFTPFilesystemProvider:
result += "Storage: SFTP "
}
if len(u.PublicKeys) > 0 {
result += fmt.Sprintf("Public keys: %v ", len(u.PublicKeys))
}
if u.MaxSessions > 0 {
result += fmt.Sprintf("Max sessions: %v ", u.MaxSessions)
}
if u.UID > 0 {
result += fmt.Sprintf("UID: %v ", u.UID)
}
if u.GID > 0 {
result += fmt.Sprintf("GID: %v ", u.GID)
}
if len(u.Filters.DeniedIP) > 0 {
result += fmt.Sprintf("Denied IP/Mask: %v ", len(u.Filters.DeniedIP))
}
if len(u.Filters.AllowedIP) > 0 {
result += fmt.Sprintf("Allowed IP/Mask: %v ", len(u.Filters.AllowedIP))
}
return result
}
// GetExpirationDateAsString returns expiration date formatted as YYYY-MM-DD
func (u *User) GetExpirationDateAsString() string {
if u.ExpirationDate > 0 {
t := utils.GetTimeFromMsecSinceEpoch(u.ExpirationDate)
return t.Format("2006-01-02")
}
return ""
}
// GetAllowedIPAsString returns the allowed IP as comma separated string
func (u *User) GetAllowedIPAsString() string {
return strings.Join(u.Filters.AllowedIP, ",")
}
// GetDeniedIPAsString returns the denied IP as comma separated string
func (u *User) GetDeniedIPAsString() string {
return strings.Join(u.Filters.DeniedIP, ",")
}
// SetEmptySecretsIfNil sets the secrets to empty if nil
func (u *User) SetEmptySecretsIfNil() {
if u.FsConfig.S3Config.AccessSecret == nil {
u.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret()
}
if u.FsConfig.GCSConfig.Credentials == nil {
u.FsConfig.GCSConfig.Credentials = kms.NewEmptySecret()
}
if u.FsConfig.AzBlobConfig.AccountKey == nil {
u.FsConfig.AzBlobConfig.AccountKey = kms.NewEmptySecret()
}
if u.FsConfig.CryptConfig.Passphrase == nil {
u.FsConfig.CryptConfig.Passphrase = kms.NewEmptySecret()
}
if u.FsConfig.SFTPConfig.Password == nil {
u.FsConfig.SFTPConfig.Password = kms.NewEmptySecret()
}
if u.FsConfig.SFTPConfig.PrivateKey == nil {
u.FsConfig.SFTPConfig.PrivateKey = kms.NewEmptySecret()
}
}
func (u *User) getACopy() User {
u.SetEmptySecretsIfNil()
pubKeys := make([]string, len(u.PublicKeys))
copy(pubKeys, u.PublicKeys)
virtualFolders := make([]vfs.VirtualFolder, len(u.VirtualFolders))
copy(virtualFolders, u.VirtualFolders)
permissions := make(map[string][]string)
for k, v := range u.Permissions {
perms := make([]string, len(v))
copy(perms, v)
permissions[k] = perms
}
filters := UserFilters{}
filters.MaxUploadFileSize = u.Filters.MaxUploadFileSize
filters.AllowedIP = make([]string, len(u.Filters.AllowedIP))
copy(filters.AllowedIP, u.Filters.AllowedIP)
filters.DeniedIP = make([]string, len(u.Filters.DeniedIP))
copy(filters.DeniedIP, u.Filters.DeniedIP)
filters.DeniedLoginMethods = make([]string, len(u.Filters.DeniedLoginMethods))
copy(filters.DeniedLoginMethods, u.Filters.DeniedLoginMethods)
filters.FileExtensions = make([]ExtensionsFilter, len(u.Filters.FileExtensions))
copy(filters.FileExtensions, u.Filters.FileExtensions)
filters.FilePatterns = make([]PatternsFilter, len(u.Filters.FilePatterns))
copy(filters.FilePatterns, u.Filters.FilePatterns)
filters.DeniedProtocols = make([]string, len(u.Filters.DeniedProtocols))
copy(filters.DeniedProtocols, u.Filters.DeniedProtocols)
fsConfig := Filesystem{
Provider: u.FsConfig.Provider,
S3Config: vfs.S3FsConfig{
Bucket: u.FsConfig.S3Config.Bucket,
Region: u.FsConfig.S3Config.Region,
AccessKey: u.FsConfig.S3Config.AccessKey,
AccessSecret: u.FsConfig.S3Config.AccessSecret.Clone(),
Endpoint: u.FsConfig.S3Config.Endpoint,
StorageClass: u.FsConfig.S3Config.StorageClass,
KeyPrefix: u.FsConfig.S3Config.KeyPrefix,
UploadPartSize: u.FsConfig.S3Config.UploadPartSize,
UploadConcurrency: u.FsConfig.S3Config.UploadConcurrency,
},
GCSConfig: vfs.GCSFsConfig{
Bucket: u.FsConfig.GCSConfig.Bucket,
CredentialFile: u.FsConfig.GCSConfig.CredentialFile,
Credentials: u.FsConfig.GCSConfig.Credentials.Clone(),
AutomaticCredentials: u.FsConfig.GCSConfig.AutomaticCredentials,
StorageClass: u.FsConfig.GCSConfig.StorageClass,
KeyPrefix: u.FsConfig.GCSConfig.KeyPrefix,
},
AzBlobConfig: vfs.AzBlobFsConfig{
Container: u.FsConfig.AzBlobConfig.Container,
AccountName: u.FsConfig.AzBlobConfig.AccountName,
AccountKey: u.FsConfig.AzBlobConfig.AccountKey.Clone(),
Endpoint: u.FsConfig.AzBlobConfig.Endpoint,
SASURL: u.FsConfig.AzBlobConfig.SASURL,
KeyPrefix: u.FsConfig.AzBlobConfig.KeyPrefix,
UploadPartSize: u.FsConfig.AzBlobConfig.UploadPartSize,
UploadConcurrency: u.FsConfig.AzBlobConfig.UploadConcurrency,
UseEmulator: u.FsConfig.AzBlobConfig.UseEmulator,
AccessTier: u.FsConfig.AzBlobConfig.AccessTier,
},
CryptConfig: vfs.CryptFsConfig{
Passphrase: u.FsConfig.CryptConfig.Passphrase.Clone(),
},
SFTPConfig: vfs.SFTPFsConfig{
Endpoint: u.FsConfig.SFTPConfig.Endpoint,
Username: u.FsConfig.SFTPConfig.Username,
Password: u.FsConfig.SFTPConfig.Password.Clone(),
PrivateKey: u.FsConfig.SFTPConfig.PrivateKey.Clone(),
Prefix: u.FsConfig.SFTPConfig.Prefix,
},
}
if len(u.FsConfig.SFTPConfig.Fingerprints) > 0 {
fsConfig.SFTPConfig.Fingerprints = make([]string, len(u.FsConfig.SFTPConfig.Fingerprints))
copy(fsConfig.SFTPConfig.Fingerprints, u.FsConfig.SFTPConfig.Fingerprints)
}
return User{
ID: u.ID,
Username: u.Username,
Password: u.Password,
PublicKeys: pubKeys,
HomeDir: u.HomeDir,
VirtualFolders: virtualFolders,
UID: u.UID,
GID: u.GID,
MaxSessions: u.MaxSessions,
QuotaSize: u.QuotaSize,
QuotaFiles: u.QuotaFiles,
Permissions: permissions,
UsedQuotaSize: u.UsedQuotaSize,
UsedQuotaFiles: u.UsedQuotaFiles,
LastQuotaUpdate: u.LastQuotaUpdate,
UploadBandwidth: u.UploadBandwidth,
DownloadBandwidth: u.DownloadBandwidth,
Status: u.Status,
ExpirationDate: u.ExpirationDate,
LastLogin: u.LastLogin,
Filters: filters,
FsConfig: fsConfig,
AdditionalInfo: u.AdditionalInfo,
}
}
func (u *User) getNotificationFieldsAsSlice(action string) []string {
return []string{action, u.Username,
strconv.FormatInt(u.ID, 10),
strconv.FormatInt(int64(u.Status), 10),
strconv.FormatInt(u.ExpirationDate, 10),
u.HomeDir,
strconv.FormatInt(int64(u.UID), 10),
strconv.FormatInt(int64(u.GID), 10),
}
}
func (u *User) getGCSCredentialsFilePath() string {
return filepath.Join(credentialsDirPath, fmt.Sprintf("%v_gcs_credentials.json", u.Username))
}

View File

@@ -4,12 +4,14 @@ SFTPGo provides an official Docker image, it is available on both [Docker Hub](h
## Supported tags and respective Dockerfile links
- [v2.0.0, v2.0, v2, latest](https://github.com/drakkan/sftpgo/blob/v2.0.0/Dockerfile.full)
- [v2.0.0-alpine, v2.0-alpine, v2-alpine, alpine](https://github.com/drakkan/sftpgo/blob/v2.0.0/Dockerfile.full.alpine)
- [v2.0.0-slim, v2.0-slim, v2-slim, slim](https://github.com/drakkan/sftpgo/blob/v2.0.0/Dockerfile)
- [v2.0.0-alpine-slim, v2.0-alpine-slim, v2-alpine-slim, alpine-slim](https://github.com/drakkan/sftpgo/blob/v2.0.0/Dockerfile.alpine)
- [edge](../Dockerfile.full)
- [edge-alpine](../Dockerfile.full.alpine)
- [v2.5.2, v2.5, v2, latest](https://github.com/drakkan/sftpgo/blob/v2.5.2/Dockerfile)
- [v2.5.2-plugins, v2.5-plugins, v2-plugins, plugins](https://github.com/drakkan/sftpgo/blob/v2.5.2/Dockerfile)
- [v2.5.2-alpine, v2.5-alpine, v2-alpine, alpine](https://github.com/drakkan/sftpgo/blob/v2.5.2/Dockerfile.alpine)
- [v2.5.2-slim, v2.5-slim, v2-slim, slim](https://github.com/drakkan/sftpgo/blob/v2.5.2/Dockerfile)
- [v2.5.2-alpine-slim, v2.5-alpine-slim, v2-alpine-slim, alpine-slim](https://github.com/drakkan/sftpgo/blob/v2.5.2/Dockerfile.alpine)
- [edge](../Dockerfile)
- [edge-plugins](../Dockerfile)
- [edge-alpine](../Dockerfile.alpine)
- [edge-slim](../Dockerfile)
- [edge-alpine-slim](../Dockerfile.alpine)
@@ -20,15 +22,59 @@ SFTPGo provides an official Docker image, it is available on both [Docker Hub](h
Starting a SFTPGo instance is simple:
```shell
docker run --name some-sftpgo -p 127.0.0.1:8080:8080 -p 2022:2022 -d "drakkan/sftpgo:tag"
docker run --name some-sftpgo -p 8080:8080 -p 2022:2022 -d "drakkan/sftpgo:tag"
```
... where `some-sftpgo` is the name you want to assign to your container, and `tag` is the tag specifying the SFTPGo version you want. See the list above for relevant tags.
Now visit [http://localhost:8080/](http://localhost:8080/) and create a new SFTPGo user. The SFTP service is available on port 2022.
Now visit [http://localhost:8080/web/admin](http://localhost:8080/web/admin), replacing `localhost` with the appropriate IP address if SFTPGo is not reachable on localhost, create the first admin and a new SFTPGo user. The SFTP service is available on port 2022.
If you don't want to persist any files, for example for testing purposes, you can run an SFTPGo instance like this:
```shell
docker run --rm --name some-sftpgo -p 8080:8080 -p 2022:2022 -d "drakkan/sftpgo:tag"
```
If you prefer GitHub Container Registry to Docker Hub replace `drakkan/sftpgo:tag` with `ghcr.io/drakkan/sftpgo:tag`.
### Enable FTP service
FTP is disabled by default, you can enable the FTP service by starting the SFTPGo instance in this way:
```shell
docker run --name some-sftpgo \
-p 8080:8080 \
-p 2022:2022 \
-p 2121:2121 \
-p 50000-50100:50000-50100 \
-e SFTPGO_FTPD__BINDINGS__0__PORT=2121 \
-e SFTPGO_FTPD__BINDINGS__0__FORCE_PASSIVE_IP=<your external ip here> \
-d "drakkan/sftpgo:tag"
```
The FTP service is now available on port 2121 and SFTP on port 2022.
You can change the passive ports range (`50000-50100` by default) by setting the environment variables `SFTPGO_FTPD__PASSIVE_PORT_RANGE__START` and `SFTPGO_FTPD__PASSIVE_PORT_RANGE__END`.
It is recommended that you provide a certificate and key file to enable FTP over TLS. You should prefer SFTP to FTP even if you configure TLS, please don't blindly enable the old FTP protocol.
### Enable WebDAV service
WebDAV is disabled by default, you can enable the WebDAV service by starting the SFTPGo instance in this way:
```shell
docker run --name some-sftpgo \
-p 8080:8080 \
-p 2022:2022 \
-p 10080:10080 \
-e SFTPGO_WEBDAVD__BINDINGS__0__PORT=10080 \
-d "drakkan/sftpgo:tag"
```
The WebDAV service is now available on port 10080 and SFTP on port 2022.
It is recommended that you provide a certificate and key file to enable WebDAV over https.
### Container shell access and viewing SFTPGo logs
The docker exec command allows you to run commands inside a Docker container. The following command line will give you a shell inside your `sftpgo` container:
@@ -43,39 +89,59 @@ The logs are available through Docker's container log:
docker logs some-sftpgo
```
### Container graceful shutdown
```shell
docker run --name some-sftpgo \
-p 2022:2022 \
-e SFTPGO_GRACE_TIME=32 \
-d "drakkan/sftpgo:tag"
```
Setting the `SFTPGO_GRACE_TIME` environment variable to a non zero value when creating or running a container will enable a graceful shutdown period in seconds that will allow existing connections to hopefully complete before being forcibly closed when the time has passed.
While the SFTPGo container is in graceful shutdown mode waiting for the last connection(s) to finish, no new connections will be allowed.
If no connections are active or `SFTPGO_GRACE_TIME=0` (default value if unset) the container will shutdown immediately.
:warning: The default docker grace time is 10 seconds, so if your SFTPGO_GRACE_TIME is larger than the docker grace time, then any `docker stop some-sftpgo` command will terminate your container once the docker grace time has passed. To ensure that the full SFTPGO_GRACE_TIME can be used, you can send a SIGINT or SIGTERM signal. Those signals can be sent using one of these commands: `docker kill --signal=SIGINT some-sftpgo` or `docker kill --signal=SIGTERM some-sftpgo`.
Alternatively you can increase the default docker grace time to a value larger than your SFTPGO_GRACE_TIME. The default docker grace time can either be specified at creation/run time using `--stop-timeout <value>` or you can simply add `--time <value>` to the docker stop command like in this 60 seconds example `docker stop --time 60 some-sftpgo`.
### Where to Store Data
Important note: There are several ways to store data used by applications that run in Docker containers. We encourage users of the SFTPGo images to familiarize themselves with the options available, including:
- Let Docker manage the storage for SFTPGo data by [writing them to disk on the host system using its own internal volume management](https://docs.docker.com/engine/tutorials/dockervolumes/#adding-a-data-volume). This is the default and is easy and fairly transparent to the user. The downside is that the files may be hard to locate for tools and applications that run directly on the host system, i.e. outside containers.
- Create a data directory on the host system (outside the container) and [mount this to a directory visible from inside the container]((https://docs.docker.com/engine/tutorials/dockervolumes/#mount-a-host-directory-as-a-data-volume)). This places the SFTPGo files in a known location on the host system, and makes it easy for tools and applications on the host system to access the files. The downside is that the user needs to make sure that the directory exists, and that e.g. directory permissions and other security mechanisms on the host system are set up correctly. The SFTPGo image runs using `1000` as UID/GID by default.
- Create a data directory on the host system (outside the container) and [mount this to a directory visible from inside the container](https://docs.docker.com/engine/tutorials/dockervolumes/#mount-a-host-directory-as-a-data-volume). This places the SFTPGo files in a known location on the host system, and makes it easy for tools and applications on the host system to access the files. The downside is that the user needs to make sure that the directory exists, and that e.g. directory permissions and other security mechanisms on the host system are set up correctly. The SFTPGo image runs using `1000` as UID/GID by default.
The Docker documentation is a good starting point for understanding the different storage options and variations, and there are multiple blogs and forum postings that discuss and give advice in this area. We will simply show the basic procedure here for the latter option above:
1. Create a data directory on a suitable volume on your host system, e.g. `/my/own/sftpgodata`.
2. Create a home directory for the sftpgo container user on your host system e.g. `/my/own/sftpgohome`.
1. Create a data directory on a suitable volume on your host system, e.g. `/my/own/sftpgodata`. The user with ID `1000` must be able to write to this directory. Please note that you don't need an actual user with ID `1000` on your host system: `chown -R 1000:1000 /my/own/sftpgodata` is enough even if there is no user/group with UID/GID `1000`.
2. Create a home directory for the sftpgo container user on your host system e.g. `/my/own/sftpgohome`. As with the data directory above, make sure that the user with ID `1000` can write to this directory: `chown -R 1000:1000 /my/own/sftpgohome`
3. Start your SFTPGo container like this:
```shell
docker run --name some-sftpgo \
-p 127.0.0.1:8080:8090 \
-p 8080:8090 \
-p 2022:2022 \
--mount type=bind,source=/my/own/sftpgodata,target=/srv/sftpgo \
--mount type=bind,source=/my/own/sftpgohome,target=/var/lib/sftpgo \
-e SFTPGO_HTTPD__BIND_PORT=8090 \
-e SFTPGO_HTTPD__BINDINGS__0__PORT=8090 \
-d "drakkan/sftpgo:tag"
```
As you can see SFTPGo uses two volumes:
As you can see SFTPGo uses two main volumes:
- `/srv/sftpgo` to handle persistent data. The default home directory for SFTP/FTP/WebDAV users is `/srv/sftpgo/data/<username>`. Backups are stored in `/srv/sftpgo/backups`
- `/var/lib/sftpgo` is the home directory for the sftpgo system user defined inside the container. This is the container working directory too, host keys will be created here when using the default configuration.
If you want to get fine grained control, you can also mount `/srv/sftpgo/data` and `/srv/sftpgo/backups` as separate volumes instead of mounting `/srv/sftpgo`.
### Configuration
The runtime configuration can be customized via environment variables that you can set passing the `-e` option to the `docker run` command or inside the `environment` section if you are using [docker stack deploy](https://docs.docker.com/engine/reference/commandline/stack_deploy/) or [docker-compose](https://github.com/docker/compose).
Please take a look [here](../docs/full-configuration.md#environment-variables) to learn how to configure SFTPGo via environment variables.
Please take a look [here](../docs/full-configuration.md) to learn how to configure SFTPGo via environment variables.
Alternately you can mount your custom configuration file to `/var/lib/sftpgo` or `/var/lib/sftpgo/.config/sftpgo`.
@@ -83,7 +149,7 @@ Alternately you can mount your custom configuration file to `/var/lib/sftpgo` or
Initial data can be loaded in the following ways:
- via the `--loaddata-from` flag or the `SFTPGO_LOADDATA_FROM` environment variable
- via the `--loaddata-from` flag or the `SFTPGO_LOADDATA_FROM` environment variable. This flag is supported for both the `serve` command (load initial data and start the service) and the `initprovider` command (initialize the provider, load initial data and exit)
- by providing a dump file to the memory provider
Please take a look [here](../docs/full-configuration.md) for more details.
@@ -104,7 +170,7 @@ With the above directory permissions, you can start a SFTPGo instance like this:
```shell
docker run --name some-sftpgo \
--user 1100:1100 \
-p 127.0.0.1:8080:8080 \
-p 8080:8080 \
-p 2022:2022 \
--mount type=bind,source="${PWD}/data",target=/srv/sftpgo \
--mount type=bind,source="${PWD}/config",target=/var/lib/sftpgo \
@@ -122,7 +188,7 @@ USER 1100:1100
## Image Variants
The `sftpgo` images comes in many flavors, each designed for a specific use case. The `edge` and `edge-alpine`tags are updated after each new commit.
The `sftpgo` images comes in many flavors, each designed for a specific use case. The `edge`, `edge-slim`, `edge-alpine`, and `edge-alpine-slim` tags are updated after each new commit.
### `sftpgo:<version>`
@@ -136,8 +202,14 @@ This variant is highly recommended when final image size being as small as possi
### `sftpgo:<suite>-slim`
These tags provide a slimmer image that does not include the optional `git` and `rsync` dependencies.
These tags provide a slimmer image that does not include `jq` and the optional `git` and `rsync` dependencies.
### `sftpgo:<suite>-plugins`
These tags provide the standard image with the addition of all "official" plugins installed in `/usr/local/bin`.
## Helm Chart
An helm chart is [available](https://artifacthub.io/packages/helm/sagikazarmark/sftpgo). You can find the source code [here](https://github.com/sagikazarmark/helm-charts/tree/master/charts/sftpgo).
This chart is not maintained by the SFTPGo project and any issues with it should be raised to the upstream repo.

View File

@@ -1,8 +0,0 @@
FROM debian:latest
LABEL maintainer="nicola.murino@gmail.com"
RUN apt-get update && apt-get install -y curl python3-requests python3-pygments
RUN curl https://raw.githubusercontent.com/drakkan/sftpgo/master/examples/rest-api-cli/sftpgo_api_cli --output /usr/bin/sftpgo_api_cli
ENTRYPOINT ["python3", "/usr/bin/sftpgo_api_cli" ]
CMD []

View File

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -e
ARCH=`uname -m`
case ${ARCH} in
"x86_64")
SUFFIX=amd64
;;
"aarch64")
SUFFIX=arm64
;;
*)
SUFFIX=ppc64le
;;
esac
echo "download plugins for arch ${SUFFIX}"
for PLUGIN in geoipfilter kms pubsub eventstore eventsearch metadata
do
echo "download plugin from https://github.com/sftpgo/sftpgo-plugin-${PLUGIN}/releases/latest/download/sftpgo-plugin-${PLUGIN}-linux-${SUFFIX}"
curl -L "https://github.com/sftpgo/sftpgo-plugin-${PLUGIN}/releases/latest/download/sftpgo-plugin-${PLUGIN}-linux-${SUFFIX}" --output "/usr/local/bin/sftpgo-plugin-${PLUGIN}"
chmod 755 "/usr/local/bin/sftpgo-plugin-${PLUGIN}"
done

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
SFTPGO_PUID=${SFTPGO_PUID:-1000}
SFTPGO_PGID=${SFTPGO_PGID:-1000}
if [ "$1" = 'sftpgo' ]; then
if [ "$(id -u)" = '0' ]; then
for DIR in "/etc/sftpgo" "/var/lib/sftpgo" "/srv/sftpgo"
do
DIR_UID=$(stat -c %u ${DIR})
DIR_GID=$(stat -c %g ${DIR})
if [ ${DIR_UID} != ${SFTPGO_PUID} ] || [ ${DIR_GID} != ${SFTPGO_PGID} ]; then
echo '{"level":"info","time":"'`date +%Y-%m-%dT%H:%M:%S.000`'","sender":"entrypoint","message":"change owner for \"'${DIR}'\" UID: '${SFTPGO_PUID}' GID: '${SFTPGO_PGID}'"}'
if [ ${DIR} = "/etc/sftpgo" ]; then
chown -R ${SFTPGO_PUID}:${SFTPGO_PGID} ${DIR}
else
chown ${SFTPGO_PUID}:${SFTPGO_PGID} ${DIR}
fi
fi
done
echo '{"level":"info","time":"'`date +%Y-%m-%dT%H:%M:%S.000`'","sender":"entrypoint","message":"run as UID: '${SFTPGO_PUID}' GID: '${SFTPGO_PGID}'"}'
exec su-exec ${SFTPGO_PUID}:${SFTPGO_PGID} "$@"
fi
exec "$@"
fi
exec "$@"

View File

@@ -1,32 +0,0 @@
#!/usr/bin/env bash
SFTPGO_PUID=${SFTPGO_PUID:-1000}
SFTPGO_PGID=${SFTPGO_PGID:-1000}
if [ "$1" = 'sftpgo' ]; then
if [ "$(id -u)" = '0' ]; then
getent passwd ${SFTPGO_PUID} > /dev/null
HAS_PUID=$?
getent group ${SFTPGO_PGID} > /dev/null
HAS_PGID=$?
if [ ${HAS_PUID} -ne 0 ] || [ ${HAS_PGID} -ne 0 ]; then
echo '{"level":"info","time":"'`date +%Y-%m-%dT%H:%M:%S.%3N`'","sender":"entrypoint","message":"prepare to run as UID: '${SFTPGO_PUID}' GID: '${SFTPGO_PGID}'"}'
if [ ${HAS_PGID} -ne 0 ]; then
echo '{"level":"info","time":"'`date +%Y-%m-%dT%H:%M:%S.%3N`'","sender":"entrypoint","message":"set GID to: '${SFTPGO_PGID}'"}'
groupmod -g ${SFTPGO_PGID} sftpgo
fi
if [ ${HAS_PUID} -ne 0 ]; then
echo '{"level":"info","time":"'`date +%Y-%m-%dT%H:%M:%S.%3N`'","sender":"entrypoint","message":"set UID to: '${SFTPGO_PUID}'"}'
usermod -u ${SFTPGO_PUID} sftpgo
fi
chown -R ${SFTPGO_PUID}:${SFTPGO_PGID} /etc/sftpgo
chown ${SFTPGO_PUID}:${SFTPGO_PGID} /var/lib/sftpgo /srv/sftpgo
fi
echo '{"level":"info","time":"'`date +%Y-%m-%dT%H:%M:%S.%3N`'","sender":"entrypoint","message":"run as UID: '${SFTPGO_PUID}' GID: '${SFTPGO_PGID}'"}'
exec gosu ${SFTPGO_PUID}:${SFTPGO_PGID} "$@"
fi
exec "$@"
fi
exec "$@"

View File

@@ -1,50 +0,0 @@
FROM golang:alpine as builder
RUN apk add --no-cache git gcc g++ ca-certificates \
&& go get -v -d github.com/drakkan/sftpgo
WORKDIR /go/src/github.com/drakkan/sftpgo
ARG TAG
ARG FEATURES
# Use --build-arg TAG=LATEST for latest tag. Use e.g. --build-arg TAG=v1.0.0 for a specific tag/commit. Otherwise HEAD (master) is built.
RUN git checkout $(if [ "${TAG}" = LATEST ]; then echo `git rev-list --tags --max-count=1`; elif [ -n "${TAG}" ]; then echo "${TAG}"; else echo HEAD; fi)
RUN go build $(if [ -n "${FEATURES}" ]; then echo "-tags ${FEATURES}"; fi) -ldflags "-s -w -X github.com/drakkan/sftpgo/version.commit=`git describe --always --dirty` -X github.com/drakkan/sftpgo/version.date=`date -u +%FT%TZ`" -v -o /go/bin/sftpgo
FROM alpine:latest
RUN apk add --no-cache ca-certificates su-exec \
&& mkdir -p /data /etc/sftpgo /srv/sftpgo/config /srv/sftpgo/web /srv/sftpgo/backups
# git and rsync are optional, uncomment the next line to add support for them if needed.
#RUN apk add --no-cache git rsync
COPY --from=builder /go/bin/sftpgo /bin/
COPY --from=builder /go/src/github.com/drakkan/sftpgo/sftpgo.json /etc/sftpgo/sftpgo.json
COPY --from=builder /go/src/github.com/drakkan/sftpgo/templates /srv/sftpgo/web/templates
COPY --from=builder /go/src/github.com/drakkan/sftpgo/static /srv/sftpgo/web/static
COPY docker-entrypoint.sh /bin/entrypoint.sh
RUN chmod +x /bin/entrypoint.sh
VOLUME [ "/data", "/srv/sftpgo/config", "/srv/sftpgo/backups" ]
EXPOSE 2022 8080
# uncomment the following settings to enable FTP support
#ENV SFTPGO_FTPD__BIND_PORT=2121
#ENV SFTPGO_FTPD__FORCE_PASSIVE_IP=<your FTP visibile IP here>
#EXPOSE 2121
# we need to expose the passive ports range too
#EXPOSE 50000-50100
# it is a good idea to provide certificates to enable FTPS too
#ENV SFTPGO_FTPD__CERTIFICATE_FILE=/srv/sftpgo/config/mycert.crt
#ENV SFTPGO_FTPD__CERTIFICATE_KEY_FILE=/srv/sftpgo/config/mycert.key
# uncomment the following setting to enable WebDAV support
#ENV SFTPGO_WEBDAVD__BIND_PORT=8090
# it is a good idea to provide certificates to enable WebDAV over HTTPS
#ENV SFTPGO_WEBDAVD__CERTIFICATE_FILE=${CONFIG_DIR}/mycert.crt
#ENV SFTPGO_WEBDAVD__CERTIFICATE_KEY_FILE=${CONFIG_DIR}/mycert.key
ENTRYPOINT ["/bin/entrypoint.sh"]
CMD ["serve"]

View File

@@ -1,61 +0,0 @@
# SFTPGo with Docker and Alpine
:warning: The recommended way to run SFTPGo on Docker is to use the official [images](https://hub.docker.com/r/drakkan/sftpgo). The documentation here is now obsolete.
This DockerFile is made to build image to host multiple instances of SFTPGo started with different users.
## Example
> 1003 is a custom uid:gid for this instance of SFTPGo
```bash
# Prereq on docker host
sudo groupadd -g 1003 sftpgrp && \
sudo useradd -u 1003 -g 1003 sftpuser -d /home/sftpuser/ && \
sudo -u sftpuser mkdir /home/sftpuser/{conf,data} && \
curl https://raw.githubusercontent.com/drakkan/sftpgo/master/sftpgo.json -o /home/sftpuser/conf/sftpgo.json
# Edit sftpgo.json as you need
# Get and build SFTPGo image.
# Add --build-arg TAG=LATEST to build the latest tag or e.g. TAG=v1.0.0 for a specific tag/commit.
# Add --build-arg FEATURES=<build features comma separated> to specify the features to build.
git clone https://github.com/drakkan/sftpgo.git && \
cd sftpgo && \
sudo docker build -t sftpgo docker/sftpgo/alpine/
# Initialize the configured provider. For PostgreSQL and MySQL providers you need to create the configured database and the "initprovider" command will create the required tables.
sudo docker run --name sftpgo \
-e PUID=1003 \
-e GUID=1003 \
-v /home/sftpuser/conf/:/srv/sftpgo/config \
sftpgo initprovider -c /srv/sftpgo/config
# Start the image
sudo docker rm sftpgo && sudo docker run --name sftpgo \
-e SFTPGO_LOG_FILE_PATH= \
-e SFTPGO_CONFIG_DIR=/srv/sftpgo/config \
-e SFTPGO_HTTPD__TEMPLATES_PATH=/srv/sftpgo/web/templates \
-e SFTPGO_HTTPD__STATIC_FILES_PATH=/srv/sftpgo/web/static \
-e SFTPGO_HTTPD__BACKUPS_PATH=/srv/sftpgo/backups \
-p 8080:8080 \
-p 2022:2022 \
-e PUID=1003 \
-e GUID=1003 \
-v /home/sftpuser/conf/:/srv/sftpgo/config \
-v /home/sftpuser/data:/data \
-v /home/sftpuser/backups:/srv/sftpgo/backups \
sftpgo
```
If you want to enable FTP/S you also need the publish the FTP port and the FTP passive port range, defined in your `Dockerfile`, by adding, for example, the following options to the `docker run` command `-p 2121:2121 -p 50000-50100:50000-50100`. The same goes for WebDAV, you need to publish the configured port.
The script `entrypoint.sh` makes sure to correct the permissions of directories and start the process with the right user.
Several images can be run with different parameters.
## Custom systemd script
An example of systemd script is present [here](sftpgo.service), with `Environment` parameter to set `PUID` and `GUID`
`WorkingDirectory` parameter must be exist with one file in this directory like `sftpgo-${PUID}.env` corresponding to the variable file for SFTPGo instance.

View File

@@ -1,7 +0,0 @@
#!/bin/sh
set -eu
chown -R "${PUID}:${GUID}" /data /etc/sftpgo /srv/sftpgo/config /srv/sftpgo/backups \
&& exec su-exec "${PUID}:${GUID}" \
/bin/sftpgo "$@"

View File

@@ -1,35 +0,0 @@
[Unit]
Description=SFTPGo server
After=docker.service
[Service]
User=root
Group=root
WorkingDirectory=/etc/sftpgo
Environment=PUID=1003
Environment=GUID=1003
EnvironmentFile=-/etc/sysconfig/sftpgo.env
ExecStartPre=-docker kill sftpgo
ExecStartPre=-docker rm sftpgo
ExecStart=docker run --name sftpgo \
--env-file sftpgo-${PUID}.env \
-e PUID=${PUID} \
-e GUID=${GUID} \
-e SFTPGO_LOG_FILE_PATH= \
-e SFTPGO_CONFIG_DIR=/srv/sftpgo/config \
-e SFTPGO_HTTPD__TEMPLATES_PATH=/srv/sftpgo/web/templates \
-e SFTPGO_HTTPD__STATIC_FILES_PATH=/srv/sftpgo/web/static \
-e SFTPGO_HTTPD__BACKUPS_PATH=/srv/sftpgo/backups \
-p 8080:8080 \
-p 2022:2022 \
-v /home/sftpuser/conf/:/srv/sftpgo/config \
-v /home/sftpuser/data:/data \
-v /home/sftpuser/backups:/srv/sftpgo/backups \
sftpgo
ExecStop=docker stop sftpgo
SyslogIdentifier=sftpgo
Restart=always
RestartSec=10s
[Install]
WantedBy=multi-user.target

View File

@@ -1,93 +0,0 @@
# we use a multi stage build to have a separate build and run env
FROM golang:latest as buildenv
LABEL maintainer="nicola.murino@gmail.com"
RUN go get -v -d github.com/drakkan/sftpgo
WORKDIR /go/src/github.com/drakkan/sftpgo
ARG TAG
ARG FEATURES
# Use --build-arg TAG=LATEST for latest tag. Use e.g. --build-arg TAG=v1.0.0 for a specific tag/commit. Otherwise HEAD (master) is built.
RUN git checkout $(if [ "${TAG}" = LATEST ]; then echo `git rev-list --tags --max-count=1`; elif [ -n "${TAG}" ]; then echo "${TAG}"; else echo HEAD; fi)
RUN go build $(if [ -n "${FEATURES}" ]; then echo "-tags ${FEATURES}"; fi) -ldflags "-s -w -X github.com/drakkan/sftpgo/version.commit=`git describe --always --dirty` -X github.com/drakkan/sftpgo/version.date=`date -u +%FT%TZ`" -v -o sftpgo
# now define the run environment
FROM debian:latest
# ca-certificates is needed for Cloud Storage Support and for HTTPS/FTPS.
RUN apt-get update && apt-get install -y ca-certificates && apt-get clean
# git and rsync are optional, uncomment the next line to add support for them if needed.
#RUN apt-get update && apt-get install -y git rsync && apt-get clean
ARG BASE_DIR=/app
ARG DATA_REL_DIR=data
ARG CONFIG_REL_DIR=config
ARG BACKUP_REL_DIR=backups
ARG USERNAME=sftpgo
ARG GROUPNAME=sftpgo
ARG UID=515
ARG GID=515
ARG WEB_REL_PATH=web
# HOME_DIR for sftpgo itself
ENV HOME_DIR=${BASE_DIR}/${USERNAME}
# DATA_DIR, this is a volume that you can use hold user's home dirs
ENV DATA_DIR=${BASE_DIR}/${DATA_REL_DIR}
# CONFIG_DIR, this is a volume to persist the daemon private keys, configuration file ecc..
ENV CONFIG_DIR=${BASE_DIR}/${CONFIG_REL_DIR}
# BACKUPS_DIR, this is a volume to store backups done using "dumpdata" REST API
ENV BACKUPS_DIR=${BASE_DIR}/${BACKUP_REL_DIR}
ENV WEB_DIR=${BASE_DIR}/${WEB_REL_PATH}
RUN mkdir -p ${DATA_DIR} ${CONFIG_DIR} ${WEB_DIR} ${BACKUPS_DIR}
RUN groupadd --system -g ${GID} ${GROUPNAME}
RUN useradd --system --create-home --no-log-init --home-dir ${HOME_DIR} --comment "SFTPGo user" --shell /usr/sbin/nologin --gid ${GID} --uid ${UID} ${USERNAME}
WORKDIR ${HOME_DIR}
RUN mkdir -p bin .config/sftpgo
ENV PATH ${HOME_DIR}/bin:$PATH
COPY --from=buildenv /go/src/github.com/drakkan/sftpgo/sftpgo bin/sftpgo
# default config file to use if no config file is found inside the CONFIG_DIR volume.
# You can override each configuration options via env vars too
COPY --from=buildenv /go/src/github.com/drakkan/sftpgo/sftpgo.json .config/sftpgo/
COPY --from=buildenv /go/src/github.com/drakkan/sftpgo/templates ${WEB_DIR}/templates
COPY --from=buildenv /go/src/github.com/drakkan/sftpgo/static ${WEB_DIR}/static
RUN chown -R ${UID}:${GID} ${DATA_DIR} ${BACKUPS_DIR}
# run as non root user
USER ${USERNAME}
EXPOSE 2022 8080
# the defined volumes must have write access for the UID and GID defined above
VOLUME [ "$DATA_DIR", "$CONFIG_DIR", "$BACKUPS_DIR" ]
# override some default configuration options using env vars
ENV SFTPGO_CONFIG_DIR=${CONFIG_DIR}
# setting SFTPGO_LOG_FILE_PATH to an empty string will log to stdout
ENV SFTPGO_LOG_FILE_PATH=""
ENV SFTPGO_HTTPD__BIND_ADDRESS=""
ENV SFTPGO_HTTPD__TEMPLATES_PATH=${WEB_DIR}/templates
ENV SFTPGO_HTTPD__STATIC_FILES_PATH=${WEB_DIR}/static
ENV SFTPGO_DATA_PROVIDER__USERS_BASE_DIR=${DATA_DIR}
ENV SFTPGO_HTTPD__BACKUPS_PATH=${BACKUPS_DIR}
# uncomment the following settings to enable FTP support
#ENV SFTPGO_FTPD__BIND_PORT=2121
#ENV SFTPGO_FTPD__FORCE_PASSIVE_IP=<your FTP visibile IP here>
#EXPOSE 2121
# we need to expose the passive ports range too
#EXPOSE 50000-50100
# it is a good idea to provide certificates to enable FTPS too
#ENV SFTPGO_FTPD__CERTIFICATE_FILE=${CONFIG_DIR}/mycert.crt
#ENV SFTPGO_FTPD__CERTIFICATE_KEY_FILE=${CONFIG_DIR}/mycert.key
# uncomment the following setting to enable WebDAV support
#ENV SFTPGO_WEBDAVD__BIND_PORT=8090
# it is a good idea to provide certificates to enable WebDAV over HTTPS
#ENV SFTPGO_WEBDAVD__CERTIFICATE_FILE=${CONFIG_DIR}/mycert.crt
#ENV SFTPGO_WEBDAVD__CERTIFICATE_KEY_FILE=${CONFIG_DIR}/mycert.key
ENTRYPOINT ["sftpgo"]
CMD ["serve"]

View File

@@ -1,59 +0,0 @@
# Dockerfile based on Debian stable
:warning: The recommended way to run SFTPGo on Docker is to use the official [images](https://hub.docker.com/r/drakkan/sftpgo). The documentation here is now obsolete.
Please read the comments inside the `Dockerfile` to learn how to customize things for your setup.
You can build the container image using `docker build`, for example:
```bash
docker build -t="drakkan/sftpgo" .
```
This will build master of github.com/drakkan/sftpgo.
To build the latest tag you can add `--build-arg TAG=LATEST` and to build a specific tag/commit you can use for example `TAG=v1.0.0`, like this:
```bash
docker build -t="drakkan/sftpgo" --build-arg TAG=v1.0.0 .
```
To specify the features to build you can add `--build-arg FEATURES=<build features comma separated>`. For example you can disable SQLite and S3 support like this:
```bash
docker build -t="drakkan/sftpgo" --build-arg FEATURES=nosqlite,nos3 .
```
Please take a look at the [build from source](./../../../docs/build-from-source.md) documentation for the complete list of the features that can be disabled.
Now create the required folders on the host system, for example:
```bash
sudo mkdir -p /srv/sftpgo/data /srv/sftpgo/config /srv/sftpgo/backups
```
and give write access to them to the UID/GID defined inside the `Dockerfile`. You can choose to create a new user, on the host system, with a matching UID/GID pair, or simply do something like this:
```bash
sudo chown -R <UID>:<GID> /srv/sftpgo/data /srv/sftpgo/config /srv/sftpgo/backups
```
Download the default configuration file and edit it as you need:
```bash
sudo curl https://raw.githubusercontent.com/drakkan/sftpgo/master/sftpgo.json -o /srv/sftpgo/config/sftpgo.json
```
Initialize the configured provider. For PostgreSQL and MySQL providers you need to create the configured database and the `initprovider` command will create the required tables:
```bash
docker run --name sftpgo --mount type=bind,source=/srv/sftpgo/config,target=/app/config drakkan/sftpgo initprovider -c /app/config
```
and finally you can run the image using something like this:
```bash
docker rm sftpgo && docker run --name sftpgo -p 8080:8080 -p 2022:2022 --mount type=bind,source=/srv/sftpgo/data,target=/app/data --mount type=bind,source=/srv/sftpgo/config,target=/app/config --mount type=bind,source=/srv/sftpgo/backups,target=/app/backups drakkan/sftpgo
```
If you want to enable FTP/S you also need the publish the FTP port and the FTP passive port range, defined in your `Dockerfile`, by adding, for example, the following options to the `docker run` command `-p 2121:2121 -p 50000-50100:50000-50100`. The same goes for WebDAV, you need to publish the configured port.

View File

@@ -1,19 +1,22 @@
# Account's configuration properties
Please take a look at the [OpenAPI schema](../httpd/schema/openapi.yaml) for the exact definitions of user and folder fields.
If you need an example you can export a dump using the REST API CLI client or by invoking the `dumpdata` endpoint directly, for example:
Please take a look at the [OpenAPI schema](../openapi/openapi.yaml) for the exact definitions of user, folder and admin fields.
If you need an example you can export a dump using the Web Admin or by invoking the `dumpdata` endpoint directly, you need to obtain an access token first, for example:
```shell
curl "http://127.0.0.1:8080/api/v1/dumpdata?output_file=dump.json&indent=1"
$ curl "http://admin:password@127.0.0.1:8080/api/v2/token"
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiQVBJIl0sImV4cCI6MTYxMzMzNTI2MSwianRpIjoiYzBrb2gxZmNkcnBjaHNzMGZwZmciLCJuYmYiOjE2MTMzMzQ2MzEsInBlcm1pc3Npb25zIjpbIioiXSwic3ViIjoiYUJ0SHUwMHNBUmxzZ29yeEtLQ1pZZWVqSTRKVTlXbThHSGNiVWtWVmc1TT0iLCJ1c2VybmFtZSI6ImFkbWluIn0.WiyqvUF-92zCr--y4Q_sxn-tPnISFzGZd_exsG-K7ME","expires_at":"2021-02-14T20:41:01Z"}
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiQVBJIl0sImV4cCI6MTYxMzMzNTI2MSwianRpIjoiYzBrb2gxZmNkcnBjaHNzMGZwZmciLCJuYmYiOjE2MTMzMzQ2MzEsInBlcm1pc3Npb25zIjpbIioiXSwic3ViIjoiYUJ0SHUwMHNBUmxzZ29yeEtLQ1pZZWVqSTRKVTlXbThHSGNiVWtWVmc1TT0iLCJ1c2VybmFtZSI6ImFkbWluIn0.WiyqvUF-92zCr--y4Q_sxn-tPnISFzGZd_exsG-K7ME" "http://127.0.0.1:8080/api/v2/dumpdata?output-data=1"
```
the dump is a JSON with users and folder.
the dump is a JSON with all SFTPGo data including users, folders, admins.
These properties are stored inside the configured data provider.
SFTPGo supports checking passwords stored with bcrypt, pbkdf2, md5crypt and sha512crypt too. For pbkdf2 the supported format is `$<algo>$<iterations>$<salt>$<hashed pwd base64 encoded>`, where algo is `pbkdf2-sha1` or `pbkdf2-sha256` or `pbkdf2-sha512` or `$pbkdf2-b64salt-sha256$`. For example the pbkdf2-sha256 of the word password using 150000 iterations and E86a9YMX3zC7 as salt must be stored as `$pbkdf2-sha256$150000$E86a9YMX3zC7$R5J62hsSq+pYw00hLLPKBbcGXmq7fj5+/M0IFoYtZbo=`. In pbkdf2 variant with b64salt the salt is base64 encoded. For bcrypt the format must be the one supported by golang's crypto/bcrypt package, for example the password secret with cost 14 must be stored as `$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK`. For md5crypt and sha512crypt we support the format used in `/etc/shadow` with the `$1$` and `$6$` prefix, this is useful if you are migrating from Unix system user accounts. We support Apache md5crypt (`$apr1$` prefix) too. Using the REST API you can send a password hashed as bcrypt, pbkdf2, md5crypt or sha512crypt and it will be stored as is.
SFTPGo supports checking passwords stored with argon2id, bcrypt, pbkdf2, md5cryptm sha256crypt and sha512crypt too. For pbkdf2 the supported format is `$<algo>$<iterations>$<salt>$<hashed pwd base64 encoded>`, where algo is `pbkdf2-sha1` or `pbkdf2-sha256` or `pbkdf2-sha512` or `$pbkdf2-b64salt-sha256$`. For example the pbkdf2-sha256 of the word password using 150000 iterations and E86a9YMX3zC7 as salt must be stored as `$pbkdf2-sha256$150000$E86a9YMX3zC7$R5J62hsSq+pYw00hLLPKBbcGXmq7fj5+/M0IFoYtZbo=`. In pbkdf2 variant with b64salt the salt is base64 encoded. For bcrypt the format must be the one supported by golang's crypto/bcrypt package, for example the password secret with cost 14 must be stored as `$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK`. For md5crypt, sha256crypt and sha512crypt we support the format used in `/etc/shadow` with the `$1$`, `$5$` and `$6$` prefix, this is useful if you are migrating from Unix system user accounts. We support Apache md5crypt (`$apr1$` prefix) too. Using the REST API you can send a password hashed as argon2id, bcrypt, pbkdf2, md5crypt, sha256crypt or sha512crypt and it will be stored as is.
If you want to use your existing accounts, you have these options:
- you can import your users inside SFTPGo. Take a look at [convert users](.../examples/convertusers) script, it can convert and import users from Linux system users and Pure-FTPd/ProFTPD virtual users
- you can import your users inside SFTPGo. Take a look at [convert users](../examples/convertusers) script, it can convert and import users from Linux system users and Pure-FTPd/ProFTPD virtual users
- you can use an external authentication program

View File

@@ -17,4 +17,4 @@ For multipart uploads you can customize the parts size and the upload concurrenc
The configured container must exist.
This backend is very similar to the [S3](./s3.md) backend, and it has the same limitations.
This backend is very similar to the [S3](./s3.md) backend, and it has the same limitations. As with S3 `chtime` will fail with the default configuration, you can install the [metadata plugin](https://github.com/sftpgo/sftpgo-plugin-metadata) to make it work and thus be able to preserve/change file modification times.

View File

@@ -13,9 +13,8 @@ The following build tags are available:
- `nosqlite`, disable SQLite data provider, default enabled
- `noportable`, disable portable mode, default enabled
- `nometrics`, disable Prometheus metrics, default enabled
- `novaultkms`, disable Vault transit secret engine, default enabled
- `noawskms`, disable AWS KMS, default enabled
- `nogcpkms`, disable GCP KMS, default enabled
- `bundle`, embed static files and templates. Before building with this tag enabled you have to copy `openapi`, `static` and `templates` dirs to `internal/bundle` directory. Default disabled
- `unixcrypt`, enable linking to `libcrypt`, default disabled, requires `CGO`
If no build tag is specified the build will include the default features.
@@ -26,18 +25,18 @@ The compiler is a build time only dependency. It is not required at runtime.
Version info, such as git commit and build date, can be embedded setting the following string variables at build time:
- `github.com/drakkan/sftpgo/version.commit`
- `github.com/drakkan/sftpgo/version.date`
- `github.com/drakkan/sftpgo/v2/internal/version.commit`
- `github.com/drakkan/sftpgo/v2/internal/version.date`
For example, you can build using the following command:
```bash
go build -tags nogcs,nos3,nosqlite -ldflags "-s -w -X github.com/drakkan/sftpgo/version.commit=`git describe --always --dirty` -X github.com/drakkan/sftpgo/version.date=`date -u +%FT%TZ`" -o sftpgo
go build -tags nogcs,nos3,nosqlite -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
```
You should get a version that includes git commit, build date and available features like this one:
```bash
$ ./sftpgo -v
SFTPGo 0.9.6-dev-b30614e-dirty-2020-06-19T11:04:56Z +metrics -gcs -s3 +bolt +mysql +pgsql -sqlite +portable
SFTPGo 2.3.1-dev-c8158e1-2022-07-24T17:25:45Z +metrics +azblob +gcs +s3 +bolt +mysql +pgsql +sqlite +portable
```

View File

@@ -16,12 +16,14 @@ If the hook defines an external program it can read the following environment va
- `SFTPGO_AUTHD_USERNAME`
- `SFTPGO_AUTHD_PASSWORD`
- `SFTPGO_AUTHD_IP`
- `SFTPGO_AUTHD_PROTOCOL`, possible values are `SSH`, `FTP`, `DAV`
- `SFTPGO_AUTHD_PROTOCOL`, possible values are `SSH`, `FTP`, `DAV`, `HTTP`
Previous global environment variables aren't cleared when the script is called. The content of these variables is _not_ quoted. They may contain special characters. They are under the control of a possibly malicious remote user.
Global environment variables are cleared, for security reasons, when the script is called. You can set additional environment variables in the "command" configuration section.
The program must write, on its standard output, the expected JSON serialized response described above.
Any output of the program on its standard error will be recorded in the SFTPGo logs with sender `check_password_hook` and level `warn`.
If the hook is an HTTP URL then it will be invoked as HTTP POST. The request body will contain a JSON serialized struct with the following fields:
- `username`
@@ -42,4 +44,6 @@ You can also restrict the hook scope using the `check_password_scope` configurat
You can combine the scopes. For example, 6 means FTP and WebDAV.
You can disable the hook on a per-user basis.
An example check password program allowing 2FA using password + one time token can be found inside the source tree [checkpwd](../examples/OTP/authy/checkpwd) directory.

View File

@@ -1,76 +1,129 @@
# Custom Actions
The `actions` struct inside the "common" configuration section allows to configure the actions for file operations and SSH commands.
SFTPGo can notify filesystem and provider events using custom actions. A custom action can be an external program or an HTTP URL.
## Filesystem events
The `actions` struct inside the `common` configuration section allows to configure the actions for file operations and SSH commands.
The `hook` can be defined as the absolute path of your program or an HTTP URL.
The `upload` condition includes both uploads to new files and overwrite of existing files. If an upload is aborted for quota limits SFTPGo tries to remove the partial file, so if the notification reports a zero size file and a quota exceeded error the file has been deleted. The `ssh_cmd` condition will be triggered after a command is successfully executed via SSH. `scp` will trigger the `download` and `upload` conditions and not `ssh_cmd`.
The following `actions` are supported:
- `download`
- `first-download`
- `pre-download`
- `upload`
- `first-upload`
- `pre-upload`
- `delete`
- `pre-delete`
- `rename`
- `mkdir`
- `rmdir`
- `ssh_cmd`
- `copy`
The `upload` condition includes both uploads to new files and overwrite of existing ones. If an upload is aborted for quota limits SFTPGo tries to remove the partial file, so if the notification reports a zero size file and a quota exceeded error the file has been deleted. The `ssh_cmd` condition will be triggered after a command is successfully executed via SSH. `scp` will trigger the `download` and `upload` conditions and not `ssh_cmd`. The `first-download` and `first-upload` action are executed only if no error occour and they don't exclude the `download` and `upload` notifications, so you will get both the `first-upload` and `upload` notification after the first successful upload and the same for the first successful download.
For cloud backends directories are virtual, they are created implicitly when you upload a file and are implicitly removed when the last file within a directory is removed. The `mkdir` and `rmdir` notifications are sent only when a directory is explicitly created or removed.
The notification will indicate if an error is detected and so, for example, a partial file is uploaded.
The `pre-delete` action, if defined, will be called just before files deletion. If the external command completes with a zero exit status or the HTTP notification response code is `200` then SFTPGo will assume that the file was already deleted/moved and so it will not try to remove the file and it will not execute the hook defined for the `delete` action.
If the `hook` defines a path to an external program, then this program is invoked with the following arguments:
The `pre-delete`, `pre-download` and `pre-upload` actions, will be called before deleting, downloading and uploading files. If the external command completes with a zero exit status or the HTTP notification response code is `200`, SFTPGo will allow the operation, otherwise the client will get a permission denied error.
- `action`, string, possible values are: `download`, `upload`, `pre-delete`,`delete`, `rename`, `ssh_cmd`
- `username`
- `path` is the full filesystem path, can be empty for some ssh commands
- `target_path`, non-empty for `rename` action and for `sftpgo-copy` SSH command
- `ssh_cmd`, non-empty for `ssh_cmd` action
If the `hook` defines a path to an external program, then this program can read the following environment variables:
The external program can also read the following environment variables:
- `SFTPGO_ACTION`
- `SFTPGO_ACTION`, supported action
- `SFTPGO_ACTION_USERNAME`
- `SFTPGO_ACTION_PATH`
- `SFTPGO_ACTION_TARGET`, non-empty for `rename` `SFTPGO_ACTION`
- `SFTPGO_ACTION_PATH`, is the full filesystem path, can be empty for some ssh commands
- `SFTPGO_ACTION_TARGET`, full filesystem path, non-empty for `rename` `SFTPGO_ACTION` and for some SSH commands
- `SFTPGO_ACTION_VIRTUAL_PATH`, virtual path, seen by SFTPGo users
- `SFTPGO_ACTION_VIRTUAL_TARGET`, virtual target path, seen by SFTPGo users
- `SFTPGO_ACTION_SSH_CMD`, non-empty for `ssh_cmd` `SFTPGO_ACTION`
- `SFTPGO_ACTION_FILE_SIZE`, non-empty for `upload`, `download` and `delete` `SFTPGO_ACTION`
- `SFTPGO_ACTION_FS_PROVIDER`, `0` for local filesystem, `1` for S3 backend, `2` for Google Cloud Storage (GCS) backend, `3` for Azure Blob Storage backend
- `SFTPGO_ACTION_FILE_SIZE`, non-zero for `pre-upload`, `upload`, `download`, `delete`, and `copy` actions if the file size is greater than `0`
- `SFTPGO_ACTION_ELAPSED`, elapsed time as milliseconds
- `SFTPGO_ACTION_FS_PROVIDER`, `0` for local filesystem, `1` for S3 backend, `2` for Google Cloud Storage (GCS) backend, `3` for Azure Blob Storage backend, `4` for local encrypted backend, `5` for SFTP backend
- `SFTPGO_ACTION_BUCKET`, non-empty for S3, GCS and Azure backends
- `SFTPGO_ACTION_ENDPOINT`, non-empty for S3 and Azure backend if configured. For Azure this is the SAS URL, if configured otherwise the endpoint
- `SFTPGO_ACTION_STATUS`, integer. 0 means a generic error occurred. 1 means no error, 2 means quota exceeded error
- `SFTPGO_ACTION_PROTOCOL`, string. Possible values are `SSH`, `SFTP`, `SCP`, `FTP`, `DAV`
- `SFTPGO_ACTION_ENDPOINT`, non-empty for S3, SFTP and Azure backend if configured
- `SFTPGO_ACTION_STATUS`, integer. Status for `upload`, `download` and `ssh_cmd` actions. 1 means no error, 2 means a generic error occurred, 3 means quota exceeded error
- `SFTPGO_ACTION_PROTOCOL`, string. Possible values are `SSH`, `SFTP`, `SCP`, `FTP`, `DAV`, `HTTP`, `HTTPShare`, `OIDC`, `DataRetention`, `EventAction`
- `SFTPGO_ACTION_IP`, the action was executed from this IP address
- `SFTPGO_ACTION_SESSION_ID`, string. Unique protocol session identifier. For stateless protocols such as HTTP the session id will change for each request
- `SFTPGO_ACTION_OPEN_FLAGS`, integer. File open flags, can be non-zero for `pre-upload` action. If `SFTPGO_ACTION_FILE_SIZE` is greater than zero and `SFTPGO_ACTION_OPEN_FLAGS&512 == 0` the target file will not be truncated
- `SFTPGO_ACTION_ROLE`, string. Role of the user who executed the action
- `SFTPGO_ACTION_TIMESTAMP`, int64. Event timestamp as nanoseconds since epoch
Previous global environment variables aren't cleared when the script is called.
Global environment variables are cleared, for security reasons, when the script is called. You can set additional environment variables in the "command" configuration section.
The program must finish within 30 seconds.
If the `hook` defines an HTTP URL then this URL will be invoked as HTTP POST. The request body will contain a JSON serialized struct with the following fields:
- `action`
- `username`
- `path`
- `target_path`, not null for `rename` action
- `ssh_cmd`, not null for `ssh_cmd` action
- `file_size`, not null for `upload`, `download`, `delete` actions
- `fs_provider`, `0` for local filesystem, `1` for S3 backend, `2` for Google Cloud Storage (GCS) backend, `3` for Azure Blob Storage backend
- `bucket`, not null for S3, GCS and Azure backends
- `endpoint`, not null for S3 and Azure backend if configured. For Azure this is the SAS URL, if configured otherwise the endpoint
- `status`, integer. 0 means a generic error occurred. 1 means no error, 2 means quota exceeded error
- `protocol`, string. Possible values are `SSH`, `FTP`, `DAV`
- `action`, string
- `username`, string
- `path`, string
- `target_path`, string, included for `rename` action and `sftpgo-copy` SSH command
- `virtual_path`, string, virtual path, seen by SFTPGo users
- `virtual_target_path`, string, virtual target path, seen by SFTPGo users
- `ssh_cmd`, string, included for `ssh_cmd` action
- `file_size`, int64, included for `pre-upload`, `upload`, `download`, `delete` and `copy` actions if the file size is greater than `0`
- `elapsed`, int64, elapsed size as milliseconds
- `fs_provider`, integer, `0` for local filesystem, `1` for S3 backend, `2` for Google Cloud Storage (GCS) backend, `3` for Azure Blob Storage backend, `4` for local encrypted backend, `5` for SFTP backend, `6` for HTTPFs backend
- `bucket`, string, included for S3, GCS and Azure backends
- `endpoint`, string, included for S3, SFTP and Azure backend if configured
- `status`, integer. Status for `upload`, `download` and `ssh_cmd` actions. 1 means no error, 2 means a generic error occurred, 3 means quota exceeded error
- `protocol`, string. Possible values are `SSH`, `SFTP`, `SCP`, `FTP`, `DAV`, `HTTP`, `HTTPShare`, `OIDC`, `DataRetention`, `EventAction`
- `ip`, string. The action was executed from this IP address
- `session_id`, string. Unique protocol session identifier. For stateless protocols such as HTTP the session id will change for each request
- `open_flags`, integer. File open flags, can be non-zero for `pre-upload` action. If `file_size` is greater than zero and `file_size&512 == 0` the target file will not be truncated
- `role`, string. Included if the user who executed the action has a role
- `timestamp`, int64. Event timestamp as nanoseconds since epoch
The HTTP request will use the global configuration for HTTP clients.
The HTTP hook will use the global configuration for HTTP clients and will respect the retry configurations.
The `actions` struct inside the "data_provider" configuration section allows you to configure actions on user add, update, delete.
The `pre-*` actions are always executed synchronously while the other ones are asynchronous. You can specify the actions to run synchronously via the `execute_sync` configuration key. Executing an action synchronously means that SFTPGo will not return a result code to the client (which is waiting for it) until your hook have completed its execution. If your hook takes a long time to complete this could cause a timeout on the client side, which wouldn't receive the server response in a timely manner and eventually drop the connection.
If you add the `upload` action to the `execute_sync` configuration key, SFTPGo will try to delete the uploaded file and return an error to the client if the hook fails. A hook is considered failed if the external command completes with a non-zero exit status or the HTTP notification response code is other than `200` (or the HTTP endpoint cannot be reached or times out).
After a hook failure, the uploaded size is removed from the quota if SFTPGo is able to remove the file.
## Provider events
The `actions` struct inside the `data_provider` configuration section allows you to configure actions on data provider objects add, update, delete.
The supported object types are:
- `user`
- `folder`
- `group`
- `admin`
- `api_key`
- `share`
- `event_action`
- `event_rule`
Actions will not be fired for internal updates, such as the last login or the user quota fields, or after external authentication.
If the `hook` defines a path to an external program, then this program is invoked with the following arguments:
If the `hook` defines a path to an external program, then this program can read the following environment variables:
- `action`, string, possible values are: `add`, `update`, `delete`
- `username`
- `ID`
- `status`
- `expiration_date`
- `home_dir`
- `uid`
- `gid`
- `SFTPGO_PROVIDER_ACTION`, supported values are `add`, `update`, `delete`
- `SFTPGO_PROVIDER_OBJECT_TYPE`, affected object type
- `SFTPGO_PROVIDER_OBJECT_NAME`, unique identifier for the affected object, for example username or key id
- `SFTPGO_PROVIDER_USERNAME`, the admin username that executed the action. There are two special usernames: `__self__` identifies a user/admin that updates itself and `__system__` identifies an action that does not have an explicit executor associated with it, for example users/admins can be added/updated by loading them from initial data
- `SFTPGO_PROVIDER_IP`, the action was executed from this IP address
- `SFTPGO_PROVIDER_ROLE`, the action was executed by an admin with this role
- `SFTPGO_PROVIDER_TIMESTAMP`, event timestamp as nanoseconds since epoch
- `SFTPGO_PROVIDER_OBJECT`, object serialized as JSON with sensitive fields removed
The external program can also read the following environment variables:
- `SFTPGO_USER_ACTION`
- `SFTPGO_USER`, user serialized as JSON with sensitive fields removed
Previous global environment variables aren't cleared when the script is called.
Global environment variables are cleared, for security reasons, when the script is called. You can set additional environment variables in the "command" configuration section.
The program must finish within 15 seconds.
If the `hook` defines an HTTP URL then this URL will be invoked as HTTP POST. The action is added to the query string, for example `<hook>?action=update`, and the user is sent serialized as JSON inside the POST body with sensitive fields removed.
If the `hook` defines an HTTP URL then this URL will be invoked as HTTP POST. The action, username, ip, object_type and object_name and timestamp and role are added to the query string, for example `<hook>?action=update&username=admin&ip=127.0.0.1&object_type=user&object_name=user1&timestamp=1633860803249`, and the full object is sent serialized as JSON inside the POST body with sensitive fields removed. The role is added only if not empty.
The HTTP request will use the global configuration for HTTP clients.
The HTTP hook will use the global configuration for HTTP clients and will respect the retry configurations.
The structure for SFTPGo objects can be found within the [OpenAPI schema](../openapi/openapi.yaml).
## Pub/Sub services
You can forward SFTPGo events to several publish/subscribe systems using the [sftpgo-plugin-pubsub](https://github.com/sftpgo/sftpgo-plugin-pubsub). The notifiers SFTPGo plugins are not suitable for interactive actions such as `pre-*` events. Their scope is to simply forward events to external services. A custom hook is a better choice if you need to react to `pre-*` events.
## Database services
You can store SFTPGo events in database systems using the [sftpgo-plugin-eventstore](https://github.com/sftpgo/sftpgo-plugin-eventstore) and you can search the stored events using the [sftpgo-plugin-eventsearch](https://github.com/sftpgo/sftpgo-plugin-eventsearch).

View File

@@ -1,6 +1,8 @@
# Data At Rest Encryption (DARE)
SFTPGo supports data at-rest encryption via its `cryptfs` virtual file system, in this mode SFTPGo transparently encrypts and decrypts data (to/from the disk) on-the-fly during uploads and/or downloads, making sure that the files at-rest on the server-side are always encrypted.
SFTPGo supports data at-rest encryption via its `cryptfs` virtual file system, in this mode SFTPGo transparently encrypts and decrypts data (to/from the local disk) on-the-fly during uploads and/or downloads, making sure that the files at-rest on the server-side are always encrypted.
Data At Rest Encryption is supported for local filesystem, for cloud storage backends you can use their server side encryption feature.
So, because of the way it works, as described here above, when you set up an encrypted filesystem for a user you need to make sure it points to an empty path/directory (that has no files in it). Otherwise, it would try to decrypt existing files that are not encrypted in the first place and fail.
@@ -12,8 +14,7 @@ The passphrase is stored encrypted itself according to your [KMS configuration](
The encrypted filesystem has some limitations compared to the local, unencrypted, one:
- Upload resume is not supported.
- Resuming uploads is not supported.
- Opening a file for both reading and writing at the same time is not supported and so clients that require advanced filesystem-like features such as `sshfs` are not supported too.
- Truncate is not supported.
- System commands such as `git` or `rsync` are not supported: they will store data unencrypted.
- Virtual folders are not implemented for now, if you are interested in this feature, please consider submitting a well written pull request (fully covered by test cases) or sponsoring this development. We could add a filesystem configuration to each virtual folder so we can mount encrypted or cloud backends as subfolders for local filesystems and vice versa.

View File

@@ -0,0 +1,32 @@
# Data retention hook
This hook runs after a data retention check completes if you specify `Hook` between notifications methods when you start the check.
The `data_retention_hook` can be defined as the absolute path of your program or an HTTP URL.
If the hook defines an external program it can read the following environment variable:
- `SFTPGO_DATA_RETENTION_RESULT`, it contains the data retention check result JSON serialized.
Global environment variables are cleared, for security reasons, when the script is called. You can set additional environment variables in the "command" configuration section.
The program must finish within 20 seconds.
If the hook defines an HTTP URL then this URL will be invoked as HTTP POST and the POST body contains the data retention check result JSON serialized.
The HTTP hook will use the global configuration for HTTP clients and will respect the retry configurations.
Here is the schema for the data retention check result:
- `username`, string
- `status`, int. 1 means success, 0 error
- `start_time`, int64. Start time as UNIX timestamp in milliseconds
- `total_deleted_files`, int. Total number of files deleted
- `total_deleted_size`, int64. Total size deleted in bytes
- `elapsed`, int64. Elapsed time in milliseconds
- `details`, list of struct with details for each checked path, each struct contains the following fields:
- `path`, string
- `retention`, int. Retention time in hours
- `deleted_files`, int. Number of files deleted
- `deleted_size`, int64. Size deleted in bytes
- `info`, string. Informative, non fatal, message if any. For example it can indicates that the check was skipped because the user doesn't have the required permissions on this path
- `error`, string. Error message if any

View File

@@ -2,12 +2,16 @@
The built-in `defender` allows you to configure an auto-blocking policy for SFTPGo and thus helps to prevent DoS (Denial of Service) and brute force password guessing.
If enabled it will protect SFTP, FTP and WebDAV services and it will automatically block hosts (IP addresses) that continually fail to log in or attempt to connect.
If enabled it will protect SFTP, HTTP (WebClient and user API), FTP and WebDAV services and it will automatically block hosts (IP addresses) that continually fail to log in or attempt to connect.
You can configure a score for each event type:
You can configure a score for the following events:
- `score_valid`, defines the score for valid login attempts, eg. user accounts that exist. Default `1`.
- `score_invalid`, defines the score for invalid login attempts, eg. non-existent user accounts or client disconnected for inactivity without authentication attempts. Default `2`.
- `score_invalid`, defines the score for invalid login attempts, eg. non-existent user accounts. Default `2`.
- `score_no_auth`, defines the score for clients disconnected without any authentication attempt. Default `0`.
- `score_limit_exceeded`, defines the score for hosts that exceeded the configured rate limits or the configured max connections per host. Default `3`.
You can set the score to `0` to not penalize some events.
And then you can configure:
@@ -15,7 +19,9 @@ And then you can configure:
- `threshold`, defines the threshold value before banning a host.
- `ban_time`, defines the time to ban a client, as minutes
So a host is banned, for `ban_time` minutes, if it has exceeded the defined threshold during the last observation time minutes.
So a host is banned, for `ban_time` minutes, if the sum of the scores has exceeded the defined threshold during the last observation time minutes.
By defining the scores, each type of event can be weighted. Let's see an example: if `score_invalid` is 3 and `threshold` is 8, a host will be banned after 3 login attempts with an non-existent user within the configured `observation_time`.
A banned IP has no score, it makes no sense to accumulate host events in memory for an already banned IP address.
@@ -23,41 +29,17 @@ If an already banned client tries to log in again, its ban time will be incremen
The `ban_time_increment` is calculated as percentage of `ban_time`, so if `ban_time` is 30 minutes and `ban_time_increment` is 50 the host will be banned for additionally 15 minutes. You can also specify values greater than 100 for `ban_time_increment` if you want to increase the penalty for already banned hosts.
The `defender` will keep in memory both the host scores and the banned hosts, you can limit the memory usage using the `entries_soft_limit` and `entries_hard_limit` configuration keys.
SFTPGo can store host scores and banned hosts in memory or within the configured data provider according to the `driver` set in the `defender` configuration section. The available drivers are `memory` and `provider`.
The `provider` driver is useful if you want to share the defender data across multiple SFTPGo instances and it requires a shared or distributed data provider: `MySQL`, `PostgreSQL` and `CockroachDB` are supported.
If you set the `provider` driver, the defender implementation may do many database queries (at least one query every time a new client connects to check if it is banned), if you have a single SFTPGo instance the `memory` driver is recommended.
The REST API allows:
For the `memory` driver, you can limit the memory usage using the `entries_soft_limit` and `entries_hard_limit` configuration keys.
- to retrieve the score for an IP address
- to retrieve the ban time for an IP address
- to unban an IP address
The `provider` driver will periodically clean up expired hosts and events.
We don't return the whole list of the banned IP addresses or all stored scores because we store them as a hash map and iterating over all the keys of a hash map is not a fast operation and will slow down the recordings of new events.
Using the REST API you can:
The `defender` can also load a permanent block list and/or a safe list of ip addresses/networks from a file:
- list hosts within the defender's lists
- remove hosts from the defender's lists
- `safelist_file`, defines the path to a file containing a list of ip addresses and/or networks to never ban.
- `blocklist_file`, defines the path to a file containing a list of ip addresses and/or networks to always ban.
These list must be stored as JSON conforming to the following schema:
- `addresses`, list of strings. Each string must be a valid IPv4/IPv6 address.
- `networks`, list of strings. Each string must be a valid IPv4/IPv6 CIDR address.
Here is a small example:
```json
{
"addresses":[
"192.0.2.1",
"2001:db8::68"
],
"networks":[
"192.0.2.0/24",
"2001:db8:1234::/48"
]
}
```
These list will be loaded in memory for faster lookups. The REST API queries "live" data and not these lists.
The `defender` is optimized for fast and time constant lookups however as it keeps all the lists and the entries in memory you should carefully measure the memory requirements for your use case.
The `defender` can also check permanent block and safe lists of IP addresses/networks. You can define these lists using the WebAdmin UI or the REST API. In multi-nodes setups, the list entries propagation between nodes may take some minutes.

View File

@@ -6,15 +6,17 @@ To enable dynamic user modification, you must set the absolute path of your prog
The external program can read the following environment variables to get info about the user trying to login:
- `SFTPGO_LOGIND_USER`, it contains the user trying to login serialized as JSON. A JSON serialized user id equal to zero means the user does not exist inside SFTPGo
- `SFTPGO_LOGIND_METHOD`, possible values are: `password`, `publickey` and `keyboard-interactive`
- `SFTPGO_LOGIND_METHOD`, possible values are: `password`, `publickey`, `keyboard-interactive`, `TLSCertificate`, `IDP` (external identity provider) or empty if the hook is executed after receiving the FTP `USER` command
- `SFTPGO_LOGIND_IP`, ip address of the user trying to login
- `SFTPGO_LOGIND_PROTOCOL`, possible values are `SSH`, `FTP`, `DAV`
- `SFTPGO_LOGIND_PROTOCOL`, possible values are `SSH`, `FTP`, `DAV`, `HTTP`, `OIDC` (OpenID Connect)
The program must write, on its standard output:
- an empty string (or no response at all) if the user should not be created/updated
- or the SFTPGo user, JSON serialized, if you want to create or update the given user
Any output of the program on its standard error will be recorded in the SFTPGo logs with sender `pre_login_hook` and level `warn`.
If the hook is an HTTP URL then it will be invoked as HTTP POST. The login method, the used protocol and the ip address of the user trying to login are added to the query string, for example `<http_url>?login_method=password&ip=1.2.3.4&protocol=SSH`.
The request body will contain the user trying to login serialized as JSON. If no modification is needed the HTTP response code must be 204, otherwise the response code must be 200 and the response body a valid SFTPGo user serialized as JSON.
@@ -35,6 +37,12 @@ If an error happens while executing the hook then login will be denied.
"Dynamic user creation or modification" and "External Authentication" are mutually exclusive, they are quite similar, the difference is that "External Authentication" returns an already authenticated user while using "Dynamic users modification" you simply create or update a user. The authentication will be checked inside SFTPGo.
In other words while using "External Authentication" the external program receives the credentials of the user trying to login (for example the cleartext password) and it needs to validate them. While using "Dynamic users modification" the pre-login program receives the user stored inside the dataprovider (it includes the hashed password if any) and it can modify it, after the modification SFTPGo will check the credentials of the user trying to login.
For SFTPGo users (not admins) authenticating using an external identity provider such as OpenID Connect, the pre-login hook will be executed after a successful authentication against the external IDP so that you can create/update the SFTPGo user matching the one authenticated against the identity provider. In this case where the pre-login hook is executed even if an external authentication hook is defined.
If you enable FTP and allow both encrypted and plain text sessions, the pre-login hook is executed after receiving the FTP `USER` command. If you return an SFTPGo user with `ftp_security` set to `1` and the FTP session is not encrypted, it will be terminated. In this case where the pre-login hook is executed even if an external authentication hook is defined.
You can disable the hook on a per-user basis.
Let's see a very basic example. Our sample program will grant access to the existing user `test_user` only in the time range 10:00-18:00. Other users will not be modified since the program will terminate with no output.
```shell
@@ -53,3 +61,5 @@ fi
```
Please note that this is a demo program and it might not work in all cases. For example, the username should be obtained by parsing the JSON serialized user and not by searching the username inside the JSON as shown here.
The structure for SFTPGo users can be found within the [OpenAPI schema](../openapi/openapi.yaml).

84
docs/eventmanager.md Normal file
View File

@@ -0,0 +1,84 @@
# Event Manager
The Event Manager allows an administrator to configure HTTP notifications, commands execution, email notifications and carry out certain server operations based on server events or schedules.
The following actions are supported:
- `HTTP notification`. You can notify an HTTP/S endpoing via GET, POST, PUT methods. You can define custom headers, query parameters and a body for POST and PUT request. Placeholders are supported for username, body, header and query parameter values.
- `Command execution`. You can launch custom commands passing parameters via environment variables. Placeholders are supported for environment variable values.
- `Email notification`. Placeholders are supported in subject and body. The email will be sent as plain text. For this action to work you have to configure an SMTP server in the SFTPGo configuration file.
- `Backup`. A backup will be saved in the configured backup directory. The backup will contain the week day and the hour in the file name.
- `User quota reset`. The quota used by users will be updated based on current usage.
- `Folder quota reset`. The quota used by virtual folders will be updated based on current usage.
- `Transfer quota reset`. The transfer quota values will be reset to `0`.
- `Data retention check`. You can define per-folder retention policies.
- `Metadata check`. A metadata check requires a metadata plugin such as [this one](https://github.com/sftpgo/sftpgo-plugin-metadata) and removes the metadata associated to missing items (for example objects deleted outside SFTPGo). A metadata check does nothing is no metadata plugin is installed or external metadata are not supported for a filesystem.
- `Password expiration check`. You can send an email notification to users whose password is about to expire.
- `User expiration check`. You can receive notifications with expired users.
- `Identity Provider account check`. You can create/update accounts for users/admins logging in using an Identity Provider.
- `Filesystem`. For these actions, the required permissions are automatically granted. This is the same as executing the actions from an SFTP client and the same restrictions applies. Supported actions:
- `Rename`. You can rename one or more files or directories.
- `Delete`. You can delete one or more files and directories.
- `Create directories`. You can create one or more directories including sub-directories.
- `Path exists`. Check if the specified path exists.
- `Copy`. You can copy one or more files or directories.
- `Compress paths`. You can compress (currently as zip) ore or more files and directories.
The following placeholders are supported:
- `{{Name}}`. Username, folder name or admin username for provider events.
- `{{Event}}`. Event name, for example `upload`, `download` for filesystem events or `add`, `update` for provider events.
- `{{Status}}`. Status for `upload`, `download` and `ssh_cmd` events. 1 means no error, 2 means a generic error occurred, 3 means quota exceeded error.
- `{{StatusString}}`. Status as string. Possible values "OK", "KO".
- `{{ErrorString}}`. Error details. Replaced with an empty string if no errors occur.
- `{{VirtualPath}}`. Path seen by SFTPGo users, for example `/adir/afile.txt`.
- `{{VirtualDirPath}}`. Parent directory for VirtualPath, for example if VirtualPath is "/adir/afile.txt", VirtualDirPath is "/adir".
- `{{FsPath}}`. Full filesystem path, for example `/user/homedir/adir/afile.txt` or `C:/data/user/homedir/adir/afile.txt` on Windows.
- `{{ObjectName}}`. File/directory name, for example `afile.txt` or provider object name.
- `{{ObjectType}}`. Object type for provider events: `user`, `group`, `admin`, etc.
- `{{VirtualTargetPath}}`. Virtual target path for renames.
- `{{VirtualTargetDirPath}}`. Parent directory for VirtualTargetPath.
- `{{TargetName}}`. Target object name for renames.
- `{{FsTargetPath}}`. Full filesystem target path for renames.
- `{{FileSize}}`. File size.
- `{{Elapsed}}`. Elapsed time as milliseconds for filesystem events.
- `{{Protocol}}`. Used protocol, for example `SFTP`, `FTP`.
- `{{IP}}`. Client IP address.
- `{{Role}}`. User or admin role.
- `{{Timestamp}}`. Event timestamp as nanoseconds since epoch.
- `{{Email}}`. For filesystem events, this is the email associated with the user performing the action. For the provider events, this is the email associated with the affected user or admin. Blank in all other cases.
- `{{ObjectData}}`. Provider object data serialized as JSON with sensitive fields removed.
- `{{RetentionReports}}`. Data retention reports as zip compressed CSV files. Supported as email attachment, file path for multipart HTTP request and as single parameter for HTTP requests body. Data retention reports contain details on the number of files deleted and the total size deleted for each folder.
- `{{IDPField<fieldname>}}`. Identity Provider custom fields containing a string.
Event rules are based on the premise that an event occours. To each rule you can associate one or more actions.
The following trigger events are supported:
- `Filesystem events`, for example `upload`, `download` etc.
- `Provider events`, for example `add`, `update`, `delete` user or other resources.
- `Schedules`. The scheduler uses UTC time.
- `IP Blocked`, this event can be generated if you enable the [defender](./defender.md).
- `Certificate`, this event is generated when a certificate is renewed using the built-in ACME protocol. Both successful and failed renewals are notified.
- `On demand`, this trigger is generated manually using the WebAdmin or the REST API.
- `Identity Provider login`, this trigger is generated when a user/admin logs in using an external Identity Provider.
You can further restrict a rule by specifying additional conditions that must be met before the rules actions are taken. For example you can react to uploads only if they are performed by a particular user or using a specified protocol.
Actions such as user quota reset, transfer quota reset, data retention check, folder quota reset and filesystem events are executed for all matching users if the trigger is a schedule or for the affected user if the trigger is a provider event or a filesystem action.
Actions are executed in a sequential order except for sync actions that are executed before the others. For each action associated to a rule you can define the following settings:
- `Stop on failure`, the next action will not be executed if the current one fails.
- `Failure action`, this action will be executed only if at least another one fails. :warning: Please note that a failure action isn't executed if the event fails, for example if a download fails the main action is executed. The failure action is executed only if one of the non-failure actions associated to a rule fails.
- `Execute sync`, for upload events, you can execute the action(s) synchronously. Executing an action synchronously means that SFTPGo will not return a result code to the client (which is waiting for it) until your action have completed its execution. If your acion takes a long time to complete this could cause a timeout on the client side, which wouldn't receive the server response in a timely manner and eventually drop the connection. For pre-* events at least a sync action is required. If pre-delete,pre-upload, pre-download sync action(s) completes successfully, SFTPGo will allow the operation, otherwise the client will get a permission denied error.
If you are running multiple SFTPGo instances connected to the same data provider, you can choose whether to allow simultaneous execution for scheduled actions.
Some actions are not supported for some triggers, rules containing incompatible actions are skipped at runtime:
- `Filesystem events`, folder quota reset cannot be executed, we don't have a direct way to get the affected folder.
- `Provider events`, user quota reset, transfer quota reset, data retention check and filesystem actions can be executed only if a user is updated. They will be executed for the affected user. Folder quota reset can be executed only for folders. Filesystem actions are not executed for `delete` user events because the actions is executed after the user deletion.
- `IP Blocked`, user quota reset, folder quota reset, transfer quota reset, data retention check and filesystem actions cannot be executed, we only have an IP.
- `Certificate`, user quota reset, folder quota reset, transfer quota reset, data retention check and filesystem actions cannot be executed.
- `Email with attachments` are supported for filesystem events and provider events if a user is added/updated. We need a user to get the files to attach.
- `HTTP multipart requests with files as attachments` are supported for filesystem events and provider events if a user is added/updated. We need a user to get the files to attach.

View File

@@ -5,37 +5,56 @@ To enable external authentication, you must set the absolute path of your authen
The external program can read the following environment variables to get info about the user trying to authenticate:
- `SFTPGO_AUTHD_USERNAME`
- `SFTPGO_AUTHD_USER`, STPGo user serialized as JSON, empty if the user does not exist within the data provider
- `SFTPGO_AUTHD_IP`
- `SFTPGO_AUTHD_PROTOCOL`, possible values are `SSH`, `FTP`, `DAV`
- `SFTPGO_AUTHD_PROTOCOL`, possible values are `SSH`, `FTP`, `DAV`, `HTTP`
- `SFTPGO_AUTHD_PASSWORD`, not empty for password authentication
- `SFTPGO_AUTHD_PUBLIC_KEY`, not empty for public key authentication
- `SFTPGO_AUTHD_KEYBOARD_INTERACTIVE`, not empty for keyboard interactive authentication
- `SFTPGO_AUTHD_TLS_CERT`, TLS client certificate PEM encoded. Not empty for TLS certificate authentication
Previous global environment variables aren't cleared when the script is called. The content of these variables is _not_ quoted. They may contain special characters. They are under the control of a possibly malicious remote user.
The program must write, on its standard output, a valid SFTPGo user serialized as JSON if the authentication succeeds or a user with an empty username if the authentication fails.
Global environment variables are cleared, for security reasons, when the script is called. You can set additional environment variables in the "command" configuration section.
The program can inspect the SFTPGo user, if it exists, using the `SFTPGO_AUTHD_USER` environment variable.
The program must write, on its standard output:
- a valid SFTPGo user serialized as JSON if the authentication succeeds. The user will be added/updated within the defined data provider
- an empty string, or no response at all, if authentication succeeds and the existing SFTPGo user does not need to be updated. This means that the credentials already stored in SFTPGo must match those used for the current authentication.
- a user with an empty username if the authentication fails
Any output of the program on its standard error will be recorded in the SFTPGo logs with sender `external_auth_hook` and level `warn`.
If the hook is an HTTP URL then it will be invoked as HTTP POST. The request body will contain a JSON serialized struct with the following fields:
- `username`
- `ip`
- `protocol`, possible values are `SSH`, `FTP`, `DAV`
- `user`, STPGo user, omitted if the user does not exist within the data provider
- `protocol`, possible values are `SSH`, `FTP`, `DAV`, `HTTP`
- `password`, not empty for password authentication
- `public_key`, not empty for public key authentication
- `keyboard_interactive`, not empty for keyboard interactive authentication
- `tls_cert`, TLS client certificate PEM encoded. Not empty for TLS certificate authentication
If authentication succeeds the HTTP response code must be 200 and the response body a valid SFTPGo user serialized as JSON. If the authentication fails the HTTP response code must be != 200 or the response body must be empty.
If authentication succeeds the HTTP response code must be 200 and the response body can be:
If the authentication succeeds, the user will be automatically added/updated inside the defined data provider. Actions defined for users added/updated will not be executed in this case and an already logged in user with the same username will not be disconnected, you have to handle these things yourself.
- a valid SFTPGo user serialized as JSON. The user will be added/updated within the defined data provider
- empty, the existing SFTPGo user does not need to be updated. Please note that in versions 2.0.x and earlier an empty response was interpreted as an authentication error
If the authentication fails the HTTP response code must be != 200 or the returned SFTPGo user must have an empty username.
If the hook returns a user who is only allowed to authenticate using public key + password (multi step authentication), your hook will be invoked for each authentication step, so it must validate the public key and password separately. SFTPGo will take care that the client uses the allowed sequence.
Actions defined for users added/updated will not be executed in this case and an already logged in user with the same username will not be disconnected.
The program hook must finish within 30 seconds, the HTTP hook timeout will use the global configuration for HTTP clients.
This method is slower than built-in authentication, but it's very flexible as anyone can easily write his own authentication hooks.
You can also restrict the authentication scope for the hook using the `external_auth_scope` configuration key:
- `0` means all supported authentication scopes. The external hook will be used for password, public key and keyboard interactive authentication
- `0` means all supported authentication scopes. The external hook will be used for password, public key, keyboard interactive and TLS certificate authentication
- `1` means passwords only
- `2` means public keys only
- `4` means keyboard interactive only
- `8` means TLS certificate only
You can combine the scopes. For example, 3 means password and public key, 5 means password and keyboard interactive, and so on.
@@ -51,6 +70,12 @@ else
fi
```
The structure for SFTPGo users can be found within the [OpenAPI schema](../openapi/openapi.yaml).
You can instruct SFTPGo to cache the external user by setting an `external_auth_cache_time` in user object returned by your hook. The `external_auth_cache_time` defines the cache time in seconds.
You can disable the hook on a per-user basis so that you can mix external and internal users.
An example authentication program allowing to authenticate against an LDAP server can be found inside the source tree [ldapauth](../examples/ldapauth) directory.
An example server, to use as HTTP authentication hook, allowing to authenticate against an LDAP server can be found inside the source tree [ldapauthserver](../examples/ldapauthserver) directory.

View File

@@ -1,6 +1,6 @@
# Configuring SFTPGo
## Command line options
<details><summary><font size=5> Command line options</font></summary>
The SFTPGo executable can be used this way:
@@ -9,23 +9,31 @@ Usage:
sftpgo [command]
Available Commands:
gen A collection of useful generators
help Help about any command
initprovider Initializes and/or updates the configured data provider
portable Serve a single directory
serve Start the SFTP Server
acme Obtain TLS certificates from ACME-based CAs like Let's Encrypt
gen A collection of useful generators
help Help about any command
initprovider Initialize and/or updates the configured data provider
ping Issues an health check to SFTPGo
portable Serve a single directory/account
resetprovider Reset the configured provider, any data will be lost
resetpwd Reset the password for the specified administrator
revertprovider Revert the configured data provider to a previous version
serve Start the SFTPGo service
smtptest Test the SMTP configuration
startsubsys Use sftpgo as SFTP file transfer subsystem
Flags:
-h, --help help for sftpgo
-v, --version
Use "sftpgo [command] --help" for more information about a command
Use "sftpgo [command] --help" for more information about a command.
```
The `serve` command supports the following flags:
- `--config-dir` string. Location of the config dir. This directory is used as the base for files with a relative path, eg. the private keys for the SFTP server or the SQLite database if you use SQLite as data provider. The configuration file, if not explicitly set, is looked for in this dir. We support reading from JSON, TOML, YAML, HCL, envfile and Java properties config files. The default config file name is `sftpgo` and therefore `sftpgo.json`, `sftpgo.yaml` and so on are searched. The default value is the working directory (".") or the value of `SFTPGO_CONFIG_DIR` environment variable.
- `--config-file` string. This flag explicitly defines the path, name and extension of the config file. If must be an absolute path or a path relative to the configuration directory. The specified file name must have a supported extension (JSON, YAML, TOML, HCL or Java properties). The default value is empty or the value of `SFTPGO_CONFIG_FILE` environment variable.
- `--grace-time`, integer. Graceful shutdown is an option to initiate a shutdown without abrupt cancellation of the currently ongoing client-initiated transfer sessions. This grace time defines the number of seconds allowed for existing transfers to get completed before shutting down. 0 means disabled. The default value is `0` or the value of `SFTPGO_GRACE_TIME` environment variable. A graceful shutdown is triggered by an interrupt signal or by a service `stop` request on Windows, if a grace time is configured.
- `--loaddata-from` string. Load users and folders from this file. The file must be specified as absolute path and it must contain a backup obtained using the `dumpdata` REST API or compatible content. The default value is empty or the value of `SFTPGO_LOADDATA_FROM` environment variable.
- `--loaddata-clean` boolean. Determine if the loaddata-from file should be removed after a successful load. Default `false` or the value of `SFTPGO_LOADDATA_CLEAN` environment variable (1 or `true`, 0 or `false`).
- `--loaddata-mode`, integer. Restore mode for data to load. 0 means new users are added, existing users are updated. 1 means new users are added, existing users are not modified. Default 1 or the value of `SFTPGO_LOADDATA_MODE` environment variable.
@@ -35,8 +43,8 @@ The `serve` command supports the following flags:
- `--log-max-age` int. Maximum number of days to retain old log files. Default 28 or the value of `SFTPGO_LOG_MAX_AGE` environment variable. It is unused if `log-file-path` is empty.
- `--log-max-backups` int. Maximum number of old log files to retain. Default 5 or the value of `SFTPGO_LOG_MAX_BACKUPS` environment variable. It is unused if `log-file-path` is empty.
- `--log-max-size` int. Maximum size in megabytes of the log file before it gets rotated. Default 10 or the value of `SFTPGO_LOG_MAX_SIZE` environment variable. It is unused if `log-file-path` is empty.
- `--log-verbose` boolean. Enable verbose logs. Default `true` or the value of `SFTPGO_LOG_VERBOSE` environment variable (1 or `true`, 0 or `false`).
- `--profiler` boolean. Enable the built-in profiler. The profiler will be accessible via HTTP/HTTPS using the base URL "/debug/pprof/". Default `false` or the value of `SFTPGO_PROFILER` environment variable (1 or `true`, 0 or `false`).
- `--log-level` string. Set the log level. Supported values: `debug`, `info`, `warn`, `error`. Default `debug` or the value of `SFTPGO_LOG_LEVEL` environment variable.
- `--log-utc-time` boolean. Enable UTC time for logging. Default `false` or the value of `SFTPGO_LOG_UTC_TIME` environment variable (1 or `true`, 0 or `false`)
Log file can be rotated on demand sending a `SIGUSR1` signal on Unix based systems and using the command `sftpgo service rotatelogs` on Windows.
@@ -44,100 +52,163 @@ If you don't configure any private host key, the daemon will use `id_rsa`, `id_e
The `gen` command allows to generate completion scripts for your shell and man pages.
## Configuration file
</details>
<details><summary><font size=5> Configuration file</font></summary>
The configuration file contains the following sections:
<details><summary><font size=4>Common</font></summary>
- **"common"**, configuration parameters shared among all the supported protocols
- `idle_timeout`, integer. Time in minutes after which an idle client will be disconnected. 0 means disabled. Default: 15
- `upload_mode` integer. 0 means standard: the files are uploaded directly to the requested path. 1 means atomic: files are uploaded to a temporary path and renamed to the requested path when the client ends the upload. Atomic mode avoids problems such as a web server that serves partial files when the files are being uploaded. In atomic mode, if there is an upload error, the temporary file is deleted and so the requested upload path will not contain a partial file. 2 means atomic with resume support: same as atomic but if there is an upload error, the temporary file is renamed to the requested path and not deleted. This way, a client can reconnect and resume the upload.
- `upload_mode` integer. 0 means standard: the files are uploaded directly to the requested path. 1 means atomic: files are uploaded to a temporary path and renamed to the requested path when the client ends the upload. Atomic mode avoids problems such as a web server that serves partial files when the files are being uploaded. In atomic mode, if there is an upload error, the temporary file is deleted and so the requested upload path will not contain a partial file. 2 means atomic with resume support: same as atomic but if there is an upload error, the temporary file is renamed to the requested path and not deleted. This way, a client can reconnect and resume the upload. Ignored for cloud-based storage backends (uploads are always atomic and resume is not supported for these backends) and for SFTP backend if buffering is enabled. Default: 0
- `actions`, struct. It contains the command to execute and/or the HTTP URL to notify and the trigger conditions. See [Custom Actions](./custom-actions.md) for more details
- `execute_on`, list of strings. Valid values are `download`, `upload`, `pre-delete`, `delete`, `rename`, `ssh_cmd`. Leave empty to disable actions.
- `execute_on`, list of strings. Valid values are `pre-download`, `download`, `first-download`, `pre-upload`, `upload`, `first-upload`, `pre-delete`, `delete`, `rename`, `mkdir`, `rmdir`, `ssh_cmd`, `copy`. Leave empty to disable actions.
- `execute_sync`, list of strings. Actions, defined in the `execute_on` list above, to be performed synchronously. The `pre-*` actions are always executed synchronously while the other ones are asynchronous. Executing an action synchronously means that SFTPGo will not return a result code to the client (which is waiting for it) until your hook have completed its execution. Leave empty to execute only the defined `pre-*` hook synchronously
- `hook`, string. Absolute path to the command to execute or HTTP URL to notify.
- `setstat_mode`, integer. 0 means "normal mode": requests for changing permissions, owner/group and access/modification times are executed. 1 means "ignore mode": requests for changing permissions, owner/group and access/modification times are silently ignored. 2 means "ignore mode for cloud based filesystems": requests for changing permissions, owner/group and access/modification times are silently ignored for cloud filesystems and executed for local filesystem.
- `proxy_protocol`, integer. Support for [HAProxy PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt). If you are running SFTPGo behind a proxy server such as HAProxy, AWS ELB or NGNIX, you can enable the proxy protocol. It provides a convenient way to safely transport connection information such as a client's address across multiple layers of NAT or TCP proxies to get the real client IP address instead of the proxy IP. Both protocol versions 1 and 2 are supported. If the proxy protocol is enabled in SFTPGo then you have to enable the protocol in your proxy configuration too. For example, for HAProxy, add `send-proxy` or `send-proxy-v2` to each server configuration line. The following modes are supported:
- `setstat_mode`, integer. 0 means "normal mode": requests for changing permissions, owner/group and access/modification times are executed. 1 means "ignore mode": requests for changing permissions, owner/group and access/modification times are silently ignored. 2 means "ignore mode if not supported": requests for changing permissions and owner/group are silently ignored for cloud filesystems and executed for local/SFTP filesystem. Requests for changing modification times are always executed for local/SFTP filesystems and are executed for cloud based filesystems if the target is a file and there is a metadata plugin available. A metadata plugin can be found [here](https://github.com/sftpgo/sftpgo-plugin-metadata).
- `rename_mode`, integer. By default (`0`), renaming of non-empty directories is not allowed for cloud storage providers (S3, GCS, Azure Blob). Set to `1` to enable recursive renames for these providers, they may be slow, there is no atomic rename API like for local filesystem, so SFTPGo will recursively list the directory contents and do a rename for each entry (partial renaming and incorrect disk quota updates are possible in error cases). Default `0`.
- `temp_path`, string. Defines the path for temporary files such as those used for atomic uploads or file pipes. If you set this option you must make sure that the defined path exists, is accessible for writing by the user running SFTPGo, and is on the same filesystem as the users home directories otherwise the renaming for atomic uploads will become a copy and therefore may take a long time. The temporary files are not namespaced. The default is generally fine. Leave empty for the default.
- `proxy_protocol`, integer. Support for [HAProxy PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt). If you are running SFTPGo behind a proxy server such as HAProxy, AWS ELB or NGINX, you can enable the proxy protocol. It provides a convenient way to safely transport connection information such as a client's address across multiple layers of NAT or TCP proxies to get the real client IP address instead of the proxy IP. Both protocol versions 1 and 2 are supported. If the proxy protocol is enabled in SFTPGo then you have to enable the protocol in your proxy configuration too. For example, for HAProxy, add `send-proxy` or `send-proxy-v2` to each server configuration line. The PROXY protocol is supported for SSH/SFTP and FTP/S. The following modes are supported:
- 0, disabled
- 1, enabled. Proxy header will be used and requests without proxy header will be accepted
- 2, required. Proxy header will be used and requests without proxy header will be rejected
- `proxy_allowed`, List of IP addresses and IP ranges allowed to send the proxy header:
- 1, enabled. If the upstream IP is not allowed to send a proxy header the header be ignored. Using this mode does not mean that we can accept connections with and without the proxy header. We always try to read the proxy header and we ignore it if the upstream IP is not allowed to send a proxy header. Set `proxy_skipped` if you want to allow some IP/networks to connect without sending a proxy header
- 2, required. If the upstream IP is not allowed to send a proxy header the connection will be rejected (unless the upstream IP is listed in `proxy_skipped`)
- `proxy_allowed`, list of IP addresses and IP ranges allowed to send the proxy header:
- If `proxy_protocol` is set to 1 and we receive a proxy header from an IP that is not in the list then the connection will be accepted and the header will be ignored
- If `proxy_protocol` is set to 2 and we receive a proxy header from an IP that is not in the list then the connection will be rejected
- `post_connect_hook`, string. Absolute path to the command to execute or HTTP URL to notify. See [Post connect hook](./post-connect-hook.md) for more details. Leave empty to disable
- `max_total_connections`, integer. Maximum number of concurrent client connections. 0 means unlimited
- `proxy_skipped`, list of IP address and IP ranges for which not to read the proxy header
- `startup_hook`, string. Absolute path to an external program or an HTTP URL to invoke as soon as SFTPGo starts. If you define an HTTP URL it will be invoked using a `GET` request. Please note that SFTPGo services may not yet be available when this hook is run. Leave empty do disable
- `post_connect_hook`, string. Absolute path to the command to execute or HTTP URL to notify. See [Post-connect hook](./post-connect-hook.md) for more details. Leave empty to disable
- `post_disconnect_hook`, string. Absolute path to the command to execute or HTTP URL to notify. See [Post-disconnect hook](./post-disconnect-hook.md) for more details. Leave empty to disable
- `data_retention_hook`, string. Absolute path to the command to execute or HTTP URL to notify. See [Data retention hook](./data-retention-hook.md) for more details. Leave empty to disable
- `max_total_connections`, integer. Maximum number of concurrent client connections. 0 means unlimited. Default: `0`.
- `max_per_host_connections`, integer. Maximum number of concurrent client connections from the same host (IP). If the defender is enabled, exceeding this limit will generate `score_limit_exceeded` events and thus hosts that repeatedly exceed the max allowed connections can be automatically blocked. 0 means unlimited. Default: `20`.
- `allowlist_status`, integer. Set to `1` to enable the allow list. The allow list can be populated using the WebAdmin or the REST API. If enabled, only the listed IPs/networks can access the configured services, all other client connections will be dropped before they even try to authenticate. Ensure to populate your allow list before enabling this setting. In multi-nodes setups, the list entries propagation between nodes may take some minutes. Default: `0`.
- `allow_self_connections`, integer. Allow users on this instance to use other users/virtual folders on this instance as storage backend. Enable this setting if you know what you are doing. Set to `1` to enable. Default: `0`.
- `defender`, struct containing the defender configuration. See [Defender](./defender.md) for more details.
- `enabled`, boolean. Default `false`.
- `ban_time`, integer. Ban time in minutes.
- `ban_time_increment`, integer. Ban time increment, as a percentage, if a banned host tries to connect again.
- `threshold`, integer. Threshold value for banning a client.
- `score_invalid`, integer. Score for invalid login attempts, eg. non-existent user accounts or client disconnected for inactivity without authentication attempts.
- `score_valid`, integer. Score for valid login attempts, eg. user accounts that exist.
- `observation_time`, integer. Defines the time window, in minutes, for tracking client errors. A host is banned if it has exceeded the defined threshold during the last observation time minutes.
- `driver`, string. Supported drivers are `memory` and `provider`. The `provider` driver will use the configured data provider to store defender events and it is supported for `MySQL`, `PostgreSQL` and `CockroachDB` data providers. Using the `provider` driver you can share the defender events among multiple SFTPGO instances. For a single instance the `memory` driver will be much faster. Default: `memory`.
- `ban_time`, integer. Ban time in minutes. Default: `30`.
- `ban_time_increment`, integer. Ban time increment, as a percentage, if a banned host tries to connect again. Default: `50`.
- `threshold`, integer. Threshold value for banning a client. Default: `15`.
- `score_invalid`, integer. Score for invalid login attempts, eg. non-existent user accounts. Default: `2`.
- `score_valid`, integer. Score for valid login attempts, eg. user accounts that exist. Default: `1`.
- `score_limit_exceeded`, integer. Score for hosts that exceeded the configured rate limits or the maximum, per-host, allowed connections. Default: `3`.
- `score_no_auth`, defines the score for clients disconnected without any authentication attempt. Default: `0`.
- `observation_time`, integer. Defines the time window, in minutes, for tracking client errors. A host is banned if it has exceeded the defined threshold during the last observation time minutes. Default: `30`.
- `entries_soft_limit`, integer. Ignored for `provider` driver. Default: `100`.
- `entries_hard_limit`, integer. The number of banned IPs and host scores kept in memory will vary between the soft and hard limit for `memory` driver. If you use the `provider` driver, this setting will limit the number of entries to return when you ask for the entire host list from the defender. Default: `150`.
- `rate_limiters`, list of structs containing the rate limiters configuration. Take a look [here](./rate-limiting.md) for more details. Each struct has the following fields:
- `average`, integer. Average defines the maximum rate allowed. 0 means disabled. Default: 0
- `period`, integer. Period defines the period as milliseconds. The rate is actually defined by dividing average by period Default: 1000 (1 second).
- `burst`, integer. Burst defines the maximum number of requests allowed to go through in the same arbitrarily small period of time. Default: 1
- `type`, integer. 1 means a global rate limiter, independent from the source host. 2 means a per-ip rate limiter. Default: 2
- `protocols`, list of strings. Available protocols are `SSH`, `FTP`, `DAV`, `HTTP`. By default all supported protocols are enabled
- `generate_defender_events`, boolean. If `true`, the defender is enabled, and this is not a global rate limiter, a new defender event will be generated each time the configured limit is exceeded. Default `false`
- `entries_soft_limit`, integer.
- `entries_hard_limit`, integer. The number of banned IPs and host scores kept in memory will vary between the soft and hard limit.
- `safelist_file`, string. Path to a file containing a list of ip addresses and/or networks to never ban.
- `blocklist_file`, string. Path to a file containing a list of ip addresses and/or networks to always ban. The lists can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows. An host that is already banned will not be automatically unbanned if you put it inside the safe list, you have to unban it using the REST API.
- `entries_hard_limit`, integer. The number of per-ip rate limiters kept in memory will vary between the soft and hard limit
</details>
<details><summary><font size=4>ACME</font></summary>
- **"acme"**, Automatic Certificate Management Environment (ACME) protocol configuration. To obtain the certificates the first time you have to configure the ACME protocol and execute the `sftpgo acme run` command. The SFTPGo service will take care of the automatic renewal of certificates for the configured domains.
- `domains`, list of domains for which to obtain certificates. If a single certificate is to be valid for multiple domains specify the names separated by commas or spaces, for example: `example.com,www.example.com` or `example.com www.example.com`. An empty list means that ACME protocol is disabled. Default: empty.
- `email`, string. Email used for registration and recovery contact. Default: empty.
- `key_type`, string. Key type to use for private keys. Supported values: `2048` (RSA 2048), `3072` (RSA 3072), `4096` (RSA 4096), `8192` (RSA 8192), `P256` (EC 256), `P384` (EC 384). Default: `4096`
- `certs_path`, string. Directory, absolute or relative to the configuration directory, to use for storing certificates and related data.
- `ca_endpoint`, string. Default: `https://acme-v02.api.letsencrypt.org/directory`.
- `renew_days`, integer. The number of days left on a certificate to renew it. Default: `30`.
- `http01_challenge`, configuration for `HTTP-01` challenge type, the following fields are supported:
- `port`, integer. This challenge is expected to run on port `80`. If you set a port other than `80` you have to proxy the path `/.well-known/acme-challenge` from the port `80` to the configured port. Default: `80`.
- `proxy_header`, string. Validate against this HTTP header when solving HTTP based challenges behind a reverse proxy. Empty means `Host`. Default: empty.
- `webroot`, string. Set the absolute path to the webroot folder to use for HTTP based challenges to write directly in a file in `.well-known/acme-challenge`. Setting a `webroot` disables the built-in server (the `port` setting is ignored) and expects the given directory to be publicly served, on port `80`, with access to `.well-known/acme-challenge`. If `webroot` is empty and `port` is `0` the `HTTP-01` challenge is disabled. Default: empty.
- `tls_alpn01_challenge`, configuration for `TLS-ALPN-01` challenge type, the following fields are supported:
- `port`, integer. This challenge is expected to run on port `443`. `0` means `TLS-ALPN-01` is disabled. Default: `0`.
</details>
<details><summary><font size=4>SFTP Server</font></summary>
- **"sftpd"**, the configuration for the SFTP server
- `bindings`, list of structs. Each struct has the following fields:
- `port`, integer. The port used for serving SFTP requests. 0 means disabled. Default: 2022
- `address`, string. Leave blank to listen on all available network interfaces. Default: ""
- `apply_proxy_config`, boolean. If enabled the common proxy configuration, if any, will be applied. Default `true`
- `bind_port`, integer. Deprecated, please use `bindings`
- `bind_address`, string. Deprecated, please use `bindings`
- `idle_timeout`, integer. Deprecated, please use the same key in `common` section.
- `max_auth_tries` integer. Maximum number of authentication attempts permitted per connection. If set to a negative number, the number of attempts is unlimited. If set to zero, the number of attempts is limited to 6.
- `banner`, string. Identification string used by the server. Leave empty to use the default banner. Default `SFTPGo_<version>`, for example `SSH-2.0-SFTPGo_0.9.5`
- `upload_mode` integer. Deprecated, please use the same key in `common` section.
- `actions`, struct. Deprecated, please use the same key in `common` section.
- `keys`, struct array. Deprecated, please use `host_keys`.
- `private_key`, path to the private key file. It can be a path relative to the config dir or an absolute one.
- `host_keys`, list of strings. It contains the daemon's private host keys. Each host key can be defined as a path relative to the configuration directory or an absolute one. If empty, the daemon will search or try to generate `id_rsa`, `id_ecdsa` and `id_ed25519` keys inside the configuration directory. If you configure absolute paths to files named `id_rsa`, `id_ecdsa` and/or `id_ed25519` then SFTPGo will try to generate these keys using the default settings.
- `kex_algorithms`, list of strings. Available KEX (Key Exchange) algorithms in preference order. Leave empty to use default values. The supported values can be found here: [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/common.go#L46 "Supported kex algos")
- `ciphers`, list of strings. Allowed ciphers. Leave empty to use default values. The supported values can be found here: [crypto/ssh](https://github.com/golang/crypto/blob/master/ssh/common.go#L28 "Supported ciphers")
- `macs`, list of strings. Available MAC (message authentication code) algorithms in preference order. Leave empty to use default values. The supported values can be found here: [crypto/ssh](https://github.com/golang/crypto/blob/master/ssh/common.go#L84 "Supported MACs")
- `host_certificates`, list of strings. Public host certificates. Each certificate can be defined as a path relative to the configuration directory or an absolute one. Certificate's public key must match a private host key otherwise it will be silently ignored. Default: empty.
- `host_key_algorithms`, list of strings. Public key algorithms that the server will accept for host key authentication. The supported values are: `rsa-sha2-512-cert-v01@openssh.com`, `rsa-sha2-256-cert-v01@openssh.com`, `ssh-rsa-cert-v01@openssh.com`, `ssh-dss-cert-v01@openssh.com`, `ecdsa-sha2-nistp256-cert-v01@openssh.com`, `ecdsa-sha2-nistp384-cert-v01@openssh.com`, `ecdsa-sha2-nistp521-cert-v01@openssh.com`, `ssh-ed25519-cert-v01@openssh.com`, `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, `rsa-sha2-512`, `rsa-sha2-256`, `ssh-rsa`, `ssh-dss`, `ssh-ed25519`. Default values: `rsa-sha2-512-cert-v01@openssh.com`, `rsa-sha2-256-cert-v01@openssh.com`, `ecdsa-sha2-nistp256-cert-v01@openssh.com`, `ecdsa-sha2-nistp384-cert-v01@openssh.com`, `ecdsa-sha2-nistp521-cert-v01@openssh.com`, `ssh-ed25519-cert-v01@openssh.com`, `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, `rsa-sha2-512`, `rsa-sha2-256`, `ssh-ed25519`.
- `moduli`, list of strings. Diffie-Hellman moduli files. Each moduli file can be defined as a path relative to the configuration directory or an absolute one. If set and valid, `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1` KEX algorithms will be available, `diffie-hellman-group-exchange-sha256` will be enabled by default if you don't explicitly set KEXs. Invalid moduli file will be silently ignored. Default: empty.
- `kex_algorithms`, list of strings. Available KEX (Key Exchange) algorithms in preference order. Leave empty to use default values. The supported values are: `curve25519-sha256`, `curve25519-sha256@libssh.org`, `ecdh-sha2-nistp256`, `ecdh-sha2-nistp384`, `ecdh-sha2-nistp521`, `diffie-hellman-group14-sha256`, `diffie-hellman-group16-sha512`, `diffie-hellman-group18-sha512`, `diffie-hellman-group14-sha1`, `diffie-hellman-group1-sha1`. Default values: `curve25519-sha256`, `curve25519-sha256@libssh.org`, `ecdh-sha2-nistp256`, `ecdh-sha2-nistp384`, `ecdh-sha2-nistp521`, `diffie-hellman-group14-sha256`. SHA512 based KEXs are disabled by default because they are slow. If you set one or more moduli files, `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1` will be available.
- `ciphers`, list of strings. Allowed ciphers in preference order. Leave empty to use default values. The supported values are: `aes128-gcm@openssh.com`, `aes256-gcm@openssh.com`, `chacha20-poly1305@openssh.com`, `aes128-ctr`, `aes192-ctr`, `aes256-ctr`, `aes128-cbc`, `aes192-cbc`, `aes256-cbc`, `3des-cbc`, `arcfour256`, `arcfour128`, `arcfour`. Default values: `aes128-gcm@openssh.com`, `aes256-gcm@openssh.com`, `chacha20-poly1305@openssh.com`, `aes128-ctr`, `aes192-ctr`, `aes256-ctr`. Please note that the ciphers disabled by default are insecure, you should expect that an active attacker can recover plaintext if you enable them.
- `macs`, list of strings. Available MAC (message authentication code) algorithms in preference order. Leave empty to use default values. The supported values are: `hmac-sha2-256-etm@openssh.com`, `hmac-sha2-256`, `hmac-sha2-512-etm@openssh.com`, `hmac-sha2-512`, `hmac-sha1`, `hmac-sha1-96`. Default values: `hmac-sha2-256-etm@openssh.com`, `hmac-sha2-256`.
- `trusted_user_ca_keys`, list of public keys paths of certificate authorities that are trusted to sign user certificates for authentication. The paths can be absolute or relative to the configuration directory.
- `revoked_user_certs_file`, path to a file containing the revoked user certificates. The path can be absolute or relative to the configuration directory. It must contain a JSON list with the public key fingerprints of the revoked certificates. Example content: `["SHA256:bsBRHC/xgiqBJdSuvSTNpJNLTISP/G356jNMCRYC5Es","SHA256:119+8cL/HH+NLMawRsJx6CzPF1I3xC+jpM60bQHXGE8"]`. The revocation list can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows. Default: "".
- `login_banner_file`, path to the login banner file. The contents of the specified file, if any, are sent to the remote user before authentication is allowed. It can be a path relative to the config dir or an absolute one. Leave empty to disable login banner.
- `setstat_mode`, integer. Deprecated, please use the same key in `common` section.
- `enabled_ssh_commands`, list of enabled SSH commands. `*` enables all supported commands. More information can be found [here](./ssh-commands.md).
- `keyboard_interactive_authentication`, boolean. This setting specifies whether keyboard interactive authentication is allowed. If no keyboard interactive hook or auth plugin is defined the default is to prompt for the user password and then the one time authentication code, if defined. Default: `true`.
- `keyboard_interactive_auth_hook`, string. Absolute path to an external program or an HTTP URL to invoke for keyboard interactive authentication. See [Keyboard Interactive Authentication](./keyboard-interactive.md) for more details.
- `password_authentication`, boolean. Set to false to disable password authentication. This setting will disable multi-step authentication method using public key + password too. It is useful for public key only configurations if you need to manage old clients that will not attempt to authenticate with public keys if the password login method is advertised. Default: true.
- `proxy_protocol`, integer. Deprecated, please use the same key in `common` section.
- `proxy_allowed`, list of strings. Deprecated, please use the same key in `common` section.
- `password_authentication`, boolean. Set to false to disable password authentication. This setting will disable multi-step authentication method using public key + password too. It is useful for public key only configurations if you need to manage old clients that will not attempt to authenticate with public keys if the password login method is advertised. Default: `true`.
- `folder_prefix`, string. Virtual root folder prefix to include in all file operations (ex: `/files`). The virtual paths used for per-directory permissions, file patterns etc. must not include the folder prefix. The prefix is only applied to SFTP requests (in SFTP server mode), SCP and other SSH commands will be automatically disabled if you configure a prefix. The prefix is ignored while running as OpenSSH's SFTP subsystem. This setting can help some specific migrations from SFTP servers based on OpenSSH and it is not recommended for general usage. Default: blank.
</details>
<details><summary><font size=4>FTP Server</font></summary>
- **"ftpd"**, the configuration for the FTP server
- `bindings`, list of structs. Each struct has the following fields:
- `port`, integer. The port used for serving FTP requests. 0 means disabled. Default: 0.
- `address`, string. Leave blank to listen on all available network interfaces. Default: "".
- `apply_proxy_config`, boolean. If enabled the common proxy configuration, if any, will be applied. Default `true`.
- `apply_proxy_config`, boolean. If enabled the common proxy configuration, if any, will be applied. Please note that we expect the proxy header on control and data connections. Default `true`.
- `tls_mode`, integer. 0 means accept both cleartext and encrypted sessions. 1 means TLS is required for both control and data connection. 2 means implicit TLS. Do not enable this blindly, please check that a proper TLS config is in place if you set `tls_mode` is different from 0.
- `force_passive_ip`, ip address. External IP address to expose for passive connections. Leavy empty to autodetect. Defaut: "".
- `client_auth_type`, integer. Set to `1` to require client certificate authentication in addition to FTP authentication. You need to define at least a certificate authority for this to work. Default: 0.
- `bind_port`, integer. Deprecated, please use `bindings`
- `bind_address`, string. Deprecated, please use `bindings`
- `certificate_file`, string. Binding specific TLS certificate. This can be an absolute path or a path relative to the config dir.
- `certificate_key_file`, string. Binding specific private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If not set the global ones will be used, if any.
- `min_tls_version`, integer. Defines the minimum version of TLS to be enabled. `12` means TLS 1.2 (and therefore TLS 1.2 and TLS 1.3 will be enabled),`13` means TLS 1.3. Default: `12`.
- `force_passive_ip`, ip address. External IP address for passive connections. Leave empty to autodetect. If not empty, it must be a valid IPv4 address. Default: "".
- `passive_ip_overrides`, list of struct that allows to return a different passive ip based on the client IP address. Each struct has the following fields:
- `networks`, list of strings. Each string must define a network in CIDR notation, for example 192.168.1.0/24.
- `ip`, string. Passive IP to return if the client IP address belongs to the defined networks. Empty means autodetect.
- `passive_host`, string. Hostname for passive connections. This hostname will be resolved each time a passive connection is requested and this can, depending on the DNS configuration, take a noticeable amount of time. Enable this setting only if you have a dynamic IP address. Default: "".
- `client_auth_type`, integer. Set to `1` to require a client certificate and verify it. Set to `2` to request a client certificate during the TLS handshake and verify it if given, in this mode the client is allowed not to send a certificate. At least one certification authority must be defined in order to verify client certificates. If no certification authority is defined, this setting is ignored. Default: 0.
- `tls_cipher_suites`, list of strings. List of supported cipher suites for TLS version 1.2. If empty, a default list of secure cipher suites is used, with a preference order based on hardware performance. Note that TLS 1.3 ciphersuites are not configurable. The supported ciphersuites names are defined [here](https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52). Any invalid name will be silently ignored. The order matters, the ciphers listed first will be the preferred ones. Default: empty.
- `passive_connections_security`, integer. Defines the security checks for passive data connections. Set to `0` to require matching peer IP addresses of control and data connection. Set to `1` to disable any checks. Please note that if you run the FTP service behind a proxy you must enable the proxy protocol for control and data connections. Default: `0`.
- `active_connections_security`, integer. Defines the security checks for active data connections. The supported values are the same as described for `passive_connections_security`. Please note that disabling the security checks you will make the FTP service vulnerable to bounce attacks on active data connections, so change the default value only if you are on a trusted/internal network. Default: `0`.
- `debug`, boolean. If enabled any FTP command will be logged. This will generate a lot of logs. Enable only if you are investigating a client compatibility issue or something similar. You shouldn't leave this setting enabled for production servers. Default `false`.
- `banner`, string. Greeting banner displayed when a connection first comes in. Leave empty to use the default banner. Default `SFTPGo <version> ready`, for example `SFTPGo 1.0.0-dev ready`.
- `banner_file`, path to the banner file. The contents of the specified file, if any, are displayed when someone connects to the server. It can be a path relative to the config dir or an absolute one. If set, it overrides the banner string provided by the `banner` option. Leave empty to disable.
- `active_transfers_port_non_20`, boolean. Do not impose the port 20 for active data transfers. Enabling this option allows to run SFTPGo with less privilege. Default: false.
- `force_passive_ip`, ip address. Deprecated, please use `bindings`
- `active_transfers_port_non_20`, boolean. Do not impose the port 20 for active data transfers. Enabling this option allows to run SFTPGo with less privilege. Default: `true`.
- `passive_port_range`, struct containing the key `start` and `end`. Port Range for data connections. Random if not specified. Default range is 50000-50100.
- `disable_active_mode`, boolean. Set to `true` to disable active FTP, default `false`.
- `enable_site`, boolean. Set to true to enable the FTP SITE command. We support `chmod` and `symlink` if SITE support is enabled. Default `false`
- `hash_support`, integer. Set to `1` to enable FTP commands that allow to calculate the hash value of files. These FTP commands will be enabled: `HASH`, `XCRC`, `MD5/XMD5`, `XSHA/XSHA1`, `XSHA256`, `XSHA512`. Please keep in mind that to calculate the hash we need to read the whole file, for remote backends this means downloading the file, for the encrypted backend this means decrypting the file. Default `0`.
- `combine_support`, integer. Set to 1 to enable support for the non standard `COMB` FTP command. Combine is only supported for local filesystem, for cloud backends it has no advantage as it will download the partial files and will upload the combined one. Cloud backends natively support multipart uploads. Default `0`.
- `certificate_file`, string. Certificate for FTPS. This can be an absolute path or a path relative to the config dir.
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. A certificate and the private key are required to enable explicit and implicit TLS. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. A certificate and the private key are required to enable explicit and implicit TLS. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows. The certificates are also polled for changes every 8 hours.
- `ca_certificates`, list of strings. Set of root certificate authorities to be used to verify client certificates.
- `ca_revocation_lists`, list of strings. Set a revocation lists, one for each root CA, to be used to check if a client certificate has been revoked. The revocation lists can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
- `tls_mode`, integer. Deprecated, please use `bindings`
</details>
<details><summary><font size=4>WebDAV Server</font></summary>
- **"webdavd"**, the configuration for the WebDAV server, more info [here](./webdav.md)
- `bindings`, list of structs. Each struct has the following fields:
- `port`, integer. The port used for serving WebDAV requests. 0 means disabled. Default: 0.
- `address`, string. Leave blank to listen on all available network interfaces. Default: "".
- `enable_https`, boolean. Set to `true` and provide both a certificate and a key file to enable HTTPS connection for this binding. Default `false`.
- `client_auth_type`, integer. Set to `1` to require client certificate authentication in addition to basic authentication. You need to define at least a certificate authority for this to work. Default: 0.
- `bind_port`, integer. Deprecated, please use `bindings`.
- `bind_address`, string. Deprecated, please use `bindings`.
- `certificate_file`, string. Binding specific TLS certificate. This can be an absolute path or a path relative to the config dir.
- `certificate_key_file`, string. Binding specific private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If not set the global ones will be used, if any.
- `min_tls_version`, integer. Defines the minimum version of TLS to be enabled. `12` means TLS 1.2 (and therefore TLS 1.2 and TLS 1.3 will be enabled),`13` means TLS 1.3. Default: `12`.
- `client_auth_type`, integer. Set to `1` to require a client certificate and verify it. Set to `2` to request a client certificate during the TLS handshake and verify it if given, in this mode the client is allowed not to send a certificate. At least one certification authority must be defined in order to verify client certificates. If no certification authority is defined, this setting is ignored. Default: 0.
- `tls_cipher_suites`, list of strings. List of supported cipher suites for TLS version 1.2. If empty, a default list of secure cipher suites is used, with a preference order based on hardware performance. Note that TLS 1.3 ciphersuites are not configurable. The supported ciphersuites names are defined [here](https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52). Any invalid name will be silently ignored. The order matters, the ciphers listed first will be the preferred ones. Default: empty.
- `prefix`, string. Prefix for WebDAV resources, if empty WebDAV resources will be available at the `/` URI. If defined it must be an absolute URI, for example `/dav`. Default: "".
- `proxy_allowed`, list of IP addresses and IP ranges allowed to set client IP proxy header such as `X-Forwarded-For`. Any client IP proxy headers, if set on requests from a connection address not in this list, will be silently ignored. Default: empty.
- `client_ip_proxy_header`, string. Defines the allowed client IP proxy header such as `X-Forwarded-For`, `X-Real-IP` etc. Default: empty
- `client_ip_header_depth`, integer. Some client IP headers such as `X-Forwarded-For` can contain multiple IP address, this setting define the position to trust starting from the right. For example if we have: `10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1` and the depth is `0`, SFTPGo will use `13.0.0.1` as client IP, if depth is `1`, `12.0.0.1` will be used and so on. Default: `0`.
- `disable_www_auth_header`, boolean. Set to `true` to not add the WWW-Authenticate header after an authentication failure, only the `401` status code will be sent. Default: `false`.
- `certificate_file`, string. Certificate for WebDAV over HTTPS. This can be an absolute path or a path relative to the config dir.
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. A certificate and a private key are required to enable HTTPS connections. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
- `ca_certificates`, list of strings. Set of root certificate authorities to be used to verify client certificates.
- `ca_revocation_lists`, list of strings. Set a revocation lists, one for each root CA, to be used to check if a client certificate has been revoked. The revocation lists can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
- `ca_revocation_lists`, list of strings. Set a revocation lists, one for each root CA, to be used to check if a client certificate has been revoked. The revocation lists can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows. The certificates are also polled for changes every 8 hours.
- `cors` struct containing CORS configuration. SFTPGo uses [Go CORS handler](https://github.com/rs/cors), please refer to upstream documentation for fields meaning and their default values.
- `enabled`, boolean, set to true to enable CORS.
- `allowed_origins`, list of strings.
@@ -146,77 +217,277 @@ The configuration file contains the following sections:
- `exposed_headers`, list of strings.
- `allow_credentials` boolean.
- `max_age`, integer.
- `cache` struct containing cache configuration for the authenticated users.
- `enabled`, boolean, set to true to enable user caching. Default: true.
- `expiration_time`, integer. Expiration time, in minutes, for the cached users. 0 means unlimited. Default: 0.
- `max_size`, integer. Maximum number of users to cache. 0 means unlimited. Default: 50.
- `options_passthrough`, boolean.
- `options_success_status`, integer.
- `allow_private_network`, boolean.
- `cache` struct containing cache configurations.
- `users`, cache configuration for the authenticated users.
- `expiration_time`, integer. Expiration time, in minutes, for the cached users. 0 means unlimited. Default: 0.
- `max_size`, integer. Maximum number of users to cache. 0 means unlimited. Default: 50.
- `mime_types`, cache configuration for mime types.
- `enabled`, boolean, set to true to enable mime types caching. Default: `true`.
- `max_size`, integer. Maximum number of mime types to cache. 0 means no cache. Default: 1000.
- `custom_mappings`, additional mime types mapping. This is a platform independet way to add few additional mappings. You can set a limited number of mappings here, if you want to add a large list use the method provided by the OS of your choice. List of struct, each struct has the following fields:
- `ext`, string, file extension including the dot, for example `.json`
- `mime`, string, mime type, for example `application/json`
</details>
<details><summary><font size=4>Data Provider</font></summary>
- **"data_provider"**, the configuration for the data provider
- `driver`, string. Supported drivers are `sqlite`, `mysql`, `postgresql`, `bolt`, `memory`
- `driver`, string. Supported drivers are `sqlite`, `mysql`, `postgresql`, `cockroachdb`, `bolt`, `memory`
- `name`, string. Database name. For driver `sqlite` this can be the database name relative to the config dir or the absolute path to the SQLite database. For driver `memory` this is the (optional) path relative to the config dir or the absolute path to the provider dump, obtained using the `dumpdata` REST API, to load. This dump will be loaded at startup and can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows. The `memory` provider will not modify the provided file so quota usage and last login will not be persisted. If you plan to use a SQLite database over a `cifs` network share (this is not recommended in general) you must use the `nobrl` mount option otherwise you will get the `database is locked` error. Some users reported that the `bolt` provider works fine over `cifs` shares.
- `host`, string. Database host. Leave empty for drivers `sqlite`, `bolt` and `memory`
- `host`, string. Database host. For `postgresql` and `cockroachdb` drivers you can specify multiple hosts separated by commas. Leave empty for drivers `sqlite`, `bolt` and `memory`
- `port`, integer. Database port. Leave empty for drivers `sqlite`, `bolt` and `memory`
- `username`, string. Database user. Leave empty for drivers `sqlite`, `bolt` and `memory`
- `password`, string. Database password. Leave empty for drivers `sqlite`, `bolt` and `memory`
- `sslmode`, integer. Used for drivers `mysql` and `postgresql`. 0 disable SSL/TLS connections, 1 require ssl, 2 set ssl mode to `verify-ca` for driver `postgresql` and `skip-verify` for driver `mysql`, 3 set ssl mode to `verify-full` for driver `postgresql` and `preferred` for driver `mysql`
- `connectionstring`, string. Provide a custom database connection string. If not empty, this connection string will be used instead of building one using the previous parameters. Leave empty for drivers `bolt` and `memory`
- `sslmode`, integer. Used for drivers `mysql` and `postgresql`. 0 disable TLS connections, 1 require TLS, 2 set TLS mode to `verify-ca` for driver `postgresql` and `skip-verify` for driver `mysql`, 3 set TLS mode to `verify-full` for driver `postgresql` and `preferred` for driver `mysql`
- `root_cert`, string. Path to the root certificate authority used to verify that the server certificate was signed by a trusted CA
- `disable_sni`, boolean. Allows to opt out Server Name Indication (SNI) for TLS connections. Default: `false`
- `target_session_attrs`, string. This is a `postgresql` and `cockroachdb` specific option. It determines whether the session must have certain properties to be acceptable. It's typically used in combination with multiple host names to select the first acceptable alternative among several hosts. Supported values: `any`, `read-write`, `read-only`, `primary`, `standby`, `prefer-standby`. If empty, `any` is assumed. If you explicitly set `any` the connections will be randomly distributed among the specified hosts
- `client_cert`, string. Path to the client certificate for two-way TLS authentication
- `client_key`,string. Path to the client key for two-way TLS authentication
- `connection_string`, string. Provide a custom database connection string. If not empty, this connection string will be used instead of building one using the previous parameters. Leave empty for drivers `bolt` and `memory`
- `sql_tables_prefix`, string. Prefix for SQL tables
- `track_quota`, integer. Set the preferred mode to track users quota between the following choices:
- 0, disable quota tracking. REST API to scan users home directories/virtual folders and update quota will do nothing
- 1, quota is updated each time a user uploads or deletes a file, even if the user has no quota restrictions
- 2, quota is updated each time a user uploads or deletes a file, but only for users with quota restrictions and for virtual folders. With this configuration, the `quota scan` and `folder_quota_scan` REST API can still be used to periodically update space usage for users without quota restrictions and for folders
- `delayed_quota_update`, integer. This configuration parameter defines the number of seconds to accumulate quota updates. If there are a lot of close uploads, accumulating quota updates can save you many queries to the data provider. If you want to track quotas, a scheduled quota update is recommended in any case, the stored quota may be incorrect for several reasons, such as an unexpected shutdown while uploading files, temporary provider failures, files copied outside of SFTPGo, and so on. You could use the [quotascan example](../examples/quotascan) as a starting point. 0 means immediate quota update.
- `pool_size`, integer. Sets the maximum number of open connections for `mysql` and `postgresql` driver. Default 0 (unlimited)
- `users_base_dir`, string. Users default base directory. If no home dir is defined while adding a new user, and this value is a valid absolute path, then the user home dir will be automatically defined as the path obtained joining the base dir and the username
- `actions`, struct. It contains the command to execute and/or the HTTP URL to notify and the trigger conditions. See [Custom Actions](./custom-actions.md) for more details
- `execute_on`, list of strings. Valid values are `add`, `update`, `delete`. `update` action will not be fired for internal updates such as the last login or the user quota fields.
- `execute_for`, list of strings. Defines the provider objects that trigger the action. Valid values are `user`, `folder`, `group`, `admin`, `api_key`, `share`, `event_action`, `event_rule`.
- `hook`, string. Absolute path to the command to execute or HTTP URL to notify.
- `external_auth_program`, string. Deprecated, please use `external_auth_hook`.
- `external_auth_hook`, string. Absolute path to an external program or an HTTP URL to invoke for users authentication. See [External Authentication](./external-auth.md) for more details. Leave empty to disable.
- `external_auth_scope`, integer. 0 means all supported authentication scopes (passwords, public keys and keyboard interactive). 1 means passwords only. 2 means public keys only. 4 means key keyboard interactive only. The flags can be combined, for example 6 means public keys and keyboard interactive
- `external_auth_scope`, integer. 0 means all supported authentication scopes (passwords, public keys and keyboard interactive). 1 means passwords only. 2 means public keys only. 4 means key keyboard interactive only. 8 means TLS certificate. The flags can be combined, for example 6 means public keys and keyboard interactive
- `credentials_path`, string. It defines the directory for storing user provided credential files such as Google Cloud Storage credentials. This can be an absolute path or a path relative to the config dir
- `prefer_database_credentials`, boolean. When true, users' Google Cloud Storage credentials will be written to the data provider instead of disk, though pre-existing credentials on disk will be used as a fallback. When false, they will be written to the directory specified by `credentials_path`.
- `pre_login_program`, string. Deprecated, please use `pre_login_hook`.
- `pre_login_hook`, string. Absolute path to an external program or an HTTP URL to invoke to modify user details just before the login. See [Dynamic user modification](./dynamic-user-mod.md) for more details. Leave empty to disable.
- `post_login_hook`, string. Absolute path to an external program or an HTTP URL to invoke to notify a successful or failed login. See [Post-login hook](./post-login-hook.md) for more details. Leave empty to disable.
- `post_login_scope`, defines the scope for the post-login hook. 0 means notify both failed and successful logins. 1 means notify failed logins. 2 means notify successful logins.
- `check_password_hook`, string. Absolute path to an external program or an HTTP URL to invoke to check the user provided password. See [Check password hook](./check-password-hook.md) for more details. Leave empty to disable.
- `check_password_scope`, defines the scope for the check password hook. 0 means all protocols, 1 means SSH, 2 means FTP, 4 means WebDAV. You can combine the scopes, for example 6 means FTP and WebDAV.
- `password_hashing`, struct. It contains the configuration parameters to be used to generate the password hash. SFTPGo can verify passwords in several formats and uses the `argon2id` algorithm to hash passwords in plain-text before storing them inside the data provider. These options allow you to customize how the hash is generated.
- `argon2_options` struct containing the options for argon2id hashing algorithm. The `memory` and `iterations` parameters control the computational cost of hashing the password. The higher these figures are, the greater the cost of generating the hash and the longer the runtime. It also follows that the greater the cost will be for any attacker trying to guess the password. If the code is running on a machine with multiple cores, then you can decrease the runtime without reducing the cost by increasing the `parallelism` parameter. This controls the number of threads that the work is spread across.
- `password_hashing`, struct. It contains the configuration parameters to be used to generate the password hash. SFTPGo can verify passwords in several formats and uses, by default, the `bcrypt` algorithm to hash passwords in plain-text before storing them inside the data provider. These options allow you to customize how the hash is generated.
- `argon2_options`, struct containing the options for argon2id hashing algorithm. The `memory` and `iterations` parameters control the computational cost of hashing the password. The higher these figures are, the greater the cost of generating the hash and the longer the runtime. It also follows that the greater the cost will be for any attacker trying to guess the password. If the code is running on a machine with multiple cores, then you can decrease the runtime without reducing the cost by increasing the `parallelism` parameter. This controls the number of threads that the work is spread across.
- `memory`, unsigned integer. The amount of memory used by the algorithm (in kibibytes). Default: 65536.
- `iterations`, unsigned integer. The number of iterations over the memory. Default: 1.
- `parallelism`. unsigned 8 bit integer. The number of threads (or lanes) used by the algorithm. Default: 2.
- `bcrypt_options`, struct containing the options for bcrypt hashing algorithm
- `cost`, integer between 4 and 31. Default: 10
- `algo`, string. Algorithm to use for hashing passwords. Available algorithms: `argon2id`, `bcrypt`. For bcrypt hashing we use the `$2a$` prefix. Default: `bcrypt`
- `password_validation` struct. It defines the password validation rules for admins and protocol users.
- `admins`, struct. It defines the password validation rules for SFTPGo admins.
- `min_entropy`, float. Defines the minimum password entropy. Take a looke [here](https://github.com/wagslane/go-password-validator#what-entropy-value-should-i-use) for more details. `0` means disabled, any password will be accepted. Default: `0`.
- `users`, struct. It defines the password validation rules for SFTPGo protocol users.
- `min_entropy`, float. This value is used as fallback if no more specific password strength is set at user/group level. Default: `0`.
- `password_caching`, boolean. Verifying argon2id passwords has a high memory and computational cost, verifying bcrypt passwords has a high computational cost, by enabling, in memory, password caching you reduce these costs. Default: `true`
- `update_mode`, integer. Defines how the database will be initialized/updated. 0 means automatically. 1 means manually using the initprovider sub-command.
- **"httpd"**, the configuration for the HTTP server used to serve REST API and to expose the built-in web interface
- `create_default_admin`, boolean. Before you can use SFTPGo you need to create an admin account. If you open the admin web UI, a setup screen will guide you in creating the first admin account. You can automatically create the first admin account by enabling this setting and setting the environment variables `SFTPGO_DEFAULT_ADMIN_USERNAME` and `SFTPGO_DEFAULT_ADMIN_PASSWORD`. You can also create the first admin by loading initial data. This setting has no effect if an admin account is already found within the data provider. Default `false`.
- `naming_rules`, integer. Naming rules for usernames, folder, group, role and object names in general. `0` means no rules. `1` means you can use any UTF-8 character. The names are used in URIs for REST API and Web admin. If not set only unreserved URI characters are allowed: ALPHA / DIGIT / "-" / "." / "_" / "~". `2` means names are converted to lowercase before saving/matching and so case insensitive matching is possible. `4` means trimming trailing and leading white spaces before saving/matching, the WebAdmin needs this setting to work properly. Rules can be combined, for example `3` means both converting to lowercase and allowing any UTF-8 character. Enabling these options for existing installations could be backward incompatible, some users could be unable to login, for example existing users with mixed cases in their usernames. You have to ensure that all existing users respect the defined rules. Default: `5`.
- `is_shared`, integer. If the data provider is shared across multiple SFTPGo instances, set this parameter to `1`. `MySQL`, `PostgreSQL` and `CockroachDB` can be shared, this setting is ignored for other data providers. For shared data providers, active transfers are persisted in the database and thus quota checks between ongoing transfers will work cross multiple instances. Password reset requests and OIDC tokens/states are also persisted in the database if the provider is shared. For shared data providers, scheduled event actions are only executed on a single SFTPGo instance by default, you can override this behavior on a per-action basis. The database table `shared_sessions` is used only to store temporary sessions. In performance critical installations, you might consider using a database-specific optimization, for example you might use an `UNLOGGED` table for PostgreSQL. This optimization in only required in very limited use cases. Default: `0`.
- `node`, struct. Node-specific configurations to allow inter-node communications. If your provider is shared across multiple nodes, the nodes can exchange information to present a uniform view for node-specific data. The current implementation allows to obtain active connections from all nodes. Nodes connect to each other using the REST API.
- `host`, string. IP address or hostname that other nodes can use to connect to this node via REST API. Empty means inter-node communications disabled. Default: empty.
- `port`, integer. The port that other nodes can use to connect to this node via REST API. Default: `0`
- `proto`, string. Supported values `http` or `https`. For `https` the configurations for http clients is used, so you can, for example, enable mutual TLS authentication. Default: `http`
- `backups_path`, string. Path to the backup directory. This can be an absolute path or a path relative to the config dir. We don't allow backups in arbitrary paths for security reasons.
</details>
<details><summary><font size=4>HTTP Server</font></summary>
- **"httpd"**, the configuration for the HTTP server used to serve REST API and the built-in web interfaces
- `bindings`, list of structs. Each struct has the following fields:
- `port`, integer. The port used for serving HTTP requests. Default: 8080.
- `address`, string. Leave blank to listen on all available network interfaces. On *NIX you can specify an absolute path to listen on a Unix-domain socket Default: "127.0.0.1".
- `enable_web_admin`, boolean. Set to `false` to disable the built-in web admin for this binding. You also need to define `templates_path` and `static_files_path` to enable the built-in web admin interface. Default `true`.
- `address`, string. Leave blank to listen on all available network interfaces. On *NIX you can specify an absolute path to listen on a Unix-domain socket Default: blank.
- `enable_web_admin`, boolean. Set to `false` to disable the built-in web admin for this binding. You also need to define `templates_path` and `static_files_path` to use the built-in web admin interface. Default `true`.
- `enable_web_client`, boolean. Set to `false` to disable the built-in web client for this binding. You also need to define `templates_path` and `static_files_path` to use the built-in web client interface. Default `true`.
- `enable_rest_api`, boolean. Set to `false` to disable REST API. Default `true`.
- `enabled_login_methods`, integer. Defines the login methods available for the WebAdmin and WebClient UIs. `0` means any configured method: username/password login form and OIDC, if enabled. `1` means OIDC for the WebAdmin UI. `2` means OIDC for the WebClient UI. `4` means login form for the WebAdmin UI. `8` means login form for the WebClient UI. You can combine the values. For example `3` means that you can only login using OIDC on both WebClient and WebAdmin UI. Default: `0`.
- `enable_https`, boolean. Set to `true` and provide both a certificate and a key file to enable HTTPS connection for this binding. Default `false`.
- `certificate_file`, string. Binding specific TLS certificate. This can be an absolute path or a path relative to the config dir.
- `certificate_key_file`, string. Binding specific private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If not set the global ones will be used, if any.
- `min_tls_version`, integer. Defines the minimum version of TLS to be enabled. `12` means TLS 1.2 (and therefore TLS 1.2 and TLS 1.3 will be enabled),`13` means TLS 1.3. Default: `12`.
- `client_auth_type`, integer. Set to `1` to require client certificate authentication in addition to JWT/Web authentication. You need to define at least a certificate authority for this to work. Default: 0.
- `bind_port`, integer. Deprecated, please use `bindings`.
- `bind_address`, string. Deprecated, please use `bindings`. Leave blank to listen on all available network interfaces. On \*NIX you can specify an absolute path to listen on a Unix-domain socket. Default: "127.0.0.1"
- `tls_cipher_suites`, list of strings. List of supported cipher suites for TLS version 1.2. If empty, a default list of secure cipher suites is used, with a preference order based on hardware performance. Note that TLS 1.3 ciphersuites are not configurable. The supported ciphersuites names are defined [here](https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52). Any invalid name will be silently ignored. The order matters, the ciphers listed first will be the preferred ones. Default: empty.
- `proxy_allowed`, list of IP addresses and IP ranges allowed to set client IP proxy header such as `X-Forwarded-For`, `X-Real-IP` and any other headers defined in the `security` section. Any of the indicated headers, if set on requests from a connection address not in this list, will be silently ignored. Default: empty.
- `client_ip_proxy_header`, string. Defines the allowed client IP proxy header such as `X-Forwarded-For`, `X-Real-IP` etc. Default: empty
- `client_ip_header_depth`, integer. Some client IP headers such as `X-Forwarded-For` can contain multiple IP address, this setting define the position to trust starting from the right. For example if we have: `10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1` and the depth is `0`, SFTPGo will use `13.0.0.1` as client IP, if depth is `1`, `12.0.0.1` will be used and so on. Default: `0`.
- `hide_login_url`, integer. If both web admin and web client are enabled each login page will show a link to the other one. This setting allows to hide this link. 0 means that the login links are displayed on both admin and client login page. This is the default. 1 means that the login link to the web client login page is hidden on admin login page. 2 means that the login link to the web admin login page is hidden on client login page. The flags can be combined, for example 3 will disable both login links.
- `render_openapi`, boolean. Set to `false` to disable serving of the OpenAPI schema and renderer. Default `true`.
- `web_client_integrations`, list of struct. The SFTPGo web client allows to send the files with the specified extensions to the configured URL using the [postMessage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage). This way you can integrate your own file viewer or editor. Take a look at the commentented example [here](../examples/webclient-integrations/test.html) to understand how to use this feature. Each struct has the following fields:
- `file_extensions`, list of strings. File extensions must be specified with the leading dot, for example `.pdf`.
- `url`, string. URL to open for the configured file extensions. The url will open in a new tab.
- `oidc`, struct. Defines the OpenID connect configuration. OpenID integration allows you to map your identity provider users to SFTPGo users and so you can login to SFTPGo Web Client and Web Admin user interfaces using your identity provider. The following fields are supported:
- `config_url`, string. Identifier for the service. If defined, SFTPGo will add `/.well-known/openid-configuration` to this url and attempt to retrieve the provider configuration on startup. SFTPGo will refuse to start if it fails to connect to the specified URL. Default: blank.
- `client_id`, string. Defines the application's ID. Default: blank.
- `client_secret`, string. Defines the application's secret. Default: blank.
- `redirect_base_url`, string. Defines the base URL to redirect to after OpenID authentication. The suffix `/web/oidc/redirect` will be added to this base URL, adding also the `web_root` if configured. Default: blank.
- `username_field`, string. Defines the ID token claims field to map to the SFTPGo username. Default: blank.
- `scopes`, list of strings. Request the OAuth provider to provide the scope information from an authenticated users. The `openid` scope is mandatory. Default: `"openid", "profile", "email"`.
- `role_field`, string. Defines the optional ID token claims field to map to a SFTPGo role. If the defined ID token claims field is set to `admin` the authenticated user is mapped to an SFTPGo admin. You don't need to specify this field if you want to use OpenID only for the Web Client UI. If the field is inside a nested structure, you can use the dot notation to traverse the structures. Default: blank.
- `implicit_roles`, boolean. If set, the `role_field` is ignored and the SFTPGo role is assumed based on the login link used. Default: `false`.
- `custom_fields`, list of strings. Custom token claims fields to pass to the pre-login hook. Default: empty.
- `insecure_skip_signature_check`, boolean. This setting causes SFTPGo to skip JWT signature validation. It's intended for special cases where providers, such as Azure, use the `none` algorithm. Skipping the signature validation can cause security issues. Default: `false`.
- `debug`, boolean. If set, the received id tokens will be logged at debug level. Default: `false`.
- `security`, struct. Defines security headers to add to HTTP responses and allows to restrict allowed hosts. The following parameters are supported:
- `enabled`, boolean. Set to `true` to enable security configurations. Default: `false`.
- `allowed_hosts`, list of strings. Fully qualified domain names that are allowed. An empty list allows any and all host names. Default: empty.
- `allowed_hosts_are_regex`, boolean. Determines if the provided allowed hosts contains valid regular expressions. Default: `false`.
- `hosts_proxy_headers`, list of string. Defines a set of header keys that may hold a proxied hostname value for the request, for example `X-Forwarded-Host`. Default: empty.
- `https_redirect`, boolean. Set to `true` to redirect HTTP requests to HTTPS. If you redirect from port `80` and you get your TLS certificates using the built-in ACME protocol and the `HTTP-01` challenge type, you need to use the webroot method and set the ACME web root to a path writable by SFTPGo in order to renew your certificates. Default: `false`.
- `https_host`, string. Defines the host name that is used to redirect HTTP requests to HTTPS. Default is blank, which indicates to use the same host. For example, if `https_redirect` is enabled and `https_host` is blank, a request for `http://127.0.0.1/web/client/login` will be redirected to `https://127.0.0.1/web/client/login`, if `https_host` is set to `www.example.com` the same request will be redirected to `https://www.example.com/web/client/login`.
- `https_proxy_headers`, list of struct, each struct contains the fields `key` and `value`. Defines a a list of header keys with associated values that would indicate a valid https request. For example `key` could be `X-Forwarded-Proto` and `value` `https`. Default: empty.
- `sts_seconds`, integer. Defines the max-age of the `Strict-Transport-Security` header. This header will be included for `https` responses or for HTTP request if the request includes a defined HTTPS proxy header. Default: `0`, which would NOT include the header.
- `sts_include_subdomains`, boolean. Set to `true`, the `includeSubdomains` will be appended to the `Strict-Transport-Security` header. Default: `false`.
- `sts_preload`, boolean. Set to true, the `preload` flag will be appended to the `Strict-Transport-Security` header. Default: `false`.
- `content_type_nosniff`, boolean. Set to `true` to add the `X-Content-Type-Options` header with the value `nosniff`. Default: `false`.
- `content_security_policy`, string. Allows to set the `Content-Security-Policy` header value. Default: blank.
- `permissions_policy`, string. Allows to set the `Permissions-Policy` header value. Default: blank.
- `cross_origin_opener_policy`, string. Allows to set the `Cross-Origin-Opener-Policy` header value. Default: blank.
- `expect_ct_header`, string. Allows to set the `Expect-CT` header value. Default: blank.
- `branding`, struct. Defines the supported customizations to suit your brand. It contains the `web_admin` and `web_client` structs that define customizations for the WebAdmin and the WebClient UIs. Each customization struct contains the following fields:
- `name`, string. Defines the UI name
- `short_name`, string. Defines the short name to show next to the logo image and on the login page
- `favicon_path`, string. Path to the favicon relative to `static_files_path`. For example, if you create a directory named `branding` inside the static dir and put the `favicon.ico` file in it, you must set `/branding/favicon.ico` as path.
- `logo_path`, string. Path to your logo relative to `static_files_path`. The preferred image size is 256x256 pixel
- `login_image_path`, string. Path to a custom image to show on the login screen relative to `static_files_path`. The preferred image size is 900x900 pixel
- `disclaimer_name`, string. Name for your optional disclaimer
- `disclaimer_path`, string. Path to the HTML page with the disclaimer relative to `static_files_path`
- `default_css`, string. Optional path to a custom CSS file, relative to `static_files_path`, which replaces the SB Admin2 default CSS
- `extra_css`, list of strings. Defines the paths, relative to `static_files_path`, to additional CSS files
- `templates_path`, string. Path to the HTML web templates. This can be an absolute path or a path relative to the config dir
- `static_files_path`, string. Path to the static files for the web interface. This can be an absolute path or a path relative to the config dir. If both `templates_path` and `static_files_path` are empty the built-in web interface will be disabled
- `backups_path`, string. Path to the backup directory. This can be an absolute path or a path relative to the config dir. We don't allow backups in arbitrary paths for security reasons
- `openapi_path`, string. Path to the directory that contains the OpenAPI schema and the default renderer. This can be an absolute path or a path relative to the config dir. If empty the OpenAPI schema and the renderer will not be served regardless of the `render_openapi` directive
- `web_root`, string. Defines a base URL for the web admin and client interfaces. If empty web admin and client resources will be available at the root ("/") URI. If defined it must be an absolute URI or it will be ignored
- `certificate_file`, string. Certificate for HTTPS. This can be an absolute path or a path relative to the config dir.
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If both the certificate and the private key are provided, the server will expect HTTPS connections. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If both the certificate and the private key are provided, you can enable HTTPS for the configured bindings. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows. The certificates are also polled for changes every 8 hours.
- `ca_certificates`, list of strings. Set of root certificate authorities to be used to verify client certificates.
- `ca_revocation_lists`, list of strings. Set a revocation lists, one for each root CA, to be used to check if a client certificate has been revoked. The revocation lists can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
- `signing_passphrase`, string. Passphrase to use to derive the signing key for JWT and CSRF tokens. If empty a random signing key will be generated each time SFTPGo starts. If you set a signing passphrase you should consider rotating it periodically for added security.
- `token_validation`, integer. Define how to validate JWT tokens, cookies and CSRF tokens. By default all the available security checks are enabled. Set to 1 to disable the requirement that a token must be used by the same IP for which it was issued. Default: `0`.
- `max_upload_file_size`, integer. Defines the maximum request body size, in bytes, for Web Client/API HTTP upload requests. `0` means no limit. Default: `0`.
- `cors` struct containing CORS configuration. SFTPGo uses [Go CORS handler](https://github.com/rs/cors), please refer to upstream documentation for fields meaning and their default values.
- `enabled`, boolean, set to `true` to enable CORS.
- `allowed_origins`, list of strings.
- `allowed_methods`, list of strings.
- `allowed_headers`, list of strings.
- `exposed_headers`, list of strings.
- `allow_credentials` boolean.
- `max_age`, integer.
- `options_passthrough`, boolean.
- `options_success_status`, integer.
- `allow_private_network`, boolean.
- `setup` struct containing configurations for the initial setup screen
- `installation_code`, string. If set, this installation code will be required when creating the first admin account. Please note that even if set using an environment variable this field is read at SFTPGo startup and not at runtime. This is not a license key or similar, the purpose here is to prevent anyone who can access to the initial setup screen from creating an admin user. Default: blank.
- `installation_code_hint`, string. Description for the installation code input field. Default: `Installation code`.
- `hide_support_link`, boolean. If set, the link to the [sponsors section](../README.md#sponsors) will not appear on the setup screen page. Default: `false`.
</details>
<details><summary><font size=4>Telemetry</font></summary>
- **"telemetry"**, the configuration for the telemetry server, more details [below](#telemetry-server)
- `bind_port`, integer. The port used for serving HTTP requests. Set to 0 to disable HTTP server. Default: 10000
- `bind_address`, string. Leave blank to listen on all available network interfaces. On \*NIX you can specify an absolute path to listen on a Unix-domain socket. Default: "127.0.0.1"
- `bind_port`, integer. The port used for serving HTTP requests. Set to 0 to disable HTTP server. Default: 0
- `bind_address`, string. Leave blank to listen on all available network interfaces. On \*NIX you can specify an absolute path to listen on a Unix-domain socket. Default: `127.0.0.1`
- `enable_profiler`, boolean. Enable the built-in profiler. Default `false`
- `auth_user_file`, string. Path to a file used to store usernames and passwords for basic authentication. This can be an absolute path or a path relative to the config dir. We support HTTP basic authentication, and the file format must conform to the one generated using the Apache `htpasswd` tool. The supported password formats are bcrypt (`$2y$` prefix) and md5 crypt (`$apr1$` prefix). If empty, HTTP authentication is disabled. Authentication will be always disabled for the `/healthz` endpoint.
- `certificate_file`, string. Certificate for HTTPS. This can be an absolute path or a path relative to the config dir.
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If both the certificate and the private key are provided, the server will expect HTTPS connections. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
- **"http"**, the configuration for HTTP clients. HTTP clients are used for executing hooks such as the ones used for custom actions, external authentication and pre-login user modifications
- `timeout`, integer. Timeout specifies a time limit, in seconds, for requests.
- `min_tls_version`, integer. Defines the minimum version of TLS to be enabled. `12` means TLS 1.2 (and therefore TLS 1.2 and TLS 1.3 will be enabled),`13` means TLS 1.3. Default: `12`.
- `tls_cipher_suites`, list of strings. List of supported cipher suites for TLS version 1.2. If empty, a default list of secure cipher suites is used, with a preference order based on hardware performance. Note that TLS 1.3 ciphersuites are not configurable. The supported ciphersuites names are defined [here](https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52). Any invalid name will be silently ignored. The order matters, the ciphers listed first will be the preferred ones. Default: empty.
</details>
<details><summary><font size=4>HTTP clients</font></summary>
- **"http"**, the configuration for HTTP clients. HTTP clients are used for executing hooks. Some hooks use a retryable HTTP client, for these hooks you can configure the time between retries and the number of retries. Please check the hook specific documentation to understand which hooks use a retryable HTTP client.
- `timeout`, float. Timeout specifies a time limit, in seconds, for requests. For requests with retries this is the timeout for a single request
- `retry_wait_min`, integer. Defines the minimum waiting time between attempts in seconds.
- `retry_wait_max`, integer. Defines the maximum waiting time between attempts in seconds. The backoff algorithm will perform exponential backoff based on the attempt number and limited by the provided minimum and maximum durations.
- `retry_max`, integer. Defines the maximum number of retries if the first request fails.
- `ca_certificates`, list of strings. List of paths to extra CA certificates to trust. The paths can be absolute or relative to the config dir. Adding trusted CA certificates is a convenient way to use self-signed certificates without defeating the purpose of using TLS.
- `certificates`, list of certificate for mutual TLS. Each certificate is a struct with the following fields:
- `cert`, string. Path to the certificate file. The path can be absolute or relative to the config dir.
- `key`, string. Path to the key file. The path can be absolute or relative to the config dir.
- `skip_tls_verify`, boolean. if enabled the HTTP client accepts any TLS certificate presented by the server and any host name in that certificate. In this mode, TLS is susceptible to man-in-the-middle attacks. This should be used only for testing.
- `headers`, list of structs. You can define a list of http headers to add to each hook. Each struct has the following fields:
- `key`, string
- `value`, string. The header is silently ignored if `key` or `value` are empty
- `url`, string, optional. If not empty, the header will be added only if the request URL starts with the one specified here
</details>
<details><summary><font size=4>Commands</font></summary>
- **command**, configuration for external commands such as program based hooks
- `timeout`, integer. Timeout specifies a time limit, in seconds, to execute external commands. Valid range: `1-300`. Default: `30`
- `env`, list of strings. Environment variables to pass to all the external commands. Global environment variables are cleared, for security reasons, you have to explicitly set any environment variable such as `PATH` etc. if you need them. Each entry is of the form `key=value`. Do not use environment variables prefixed with `SFTPGO_` to avoid conflicts with environment variables that SFTPGo hooks can set. Default: empty
- `commands`, list of structs. Allow to customize configuration per-command. Each struct has the following fields:
- `path`, string. Define the command path as defined in the hook configuration
- `timeout`, integer. This value overrides the global timeout if set
- `env`, list of strings. These values are added to the environment variables defined for all commands, if any. Default: empty
- `args`, list of strings. Arguments to pass to the command identified by `path`. Default: empty
- `hook`, string. If not empty this configuration only apply to the specified hook name. Supported hook names: `fs_actions`, `provider_actions`, `startup`, `post_connect`, `post_disconnect`, `data_retention`, `check_password`, `pre_login`, `post_login`, `external_auth`, `keyboard_interactive`. Default: empty
</details>
<details><summary><font size=4>KMS</font></summary>
- **kms**, configuration for the Key Management Service, more details can be found [here](./kms.md)
- `secrets`
- `url`
- `master_key_path`
- `url`, string. Defines the URI to the KMS service. Default: blank.
- `master_key`, string. Defines the master encryption key as string. If not empty, it takes precedence over `master_key_path`. Default: blank.
- `master_key_path`, string. Defines the absolute path to a file containing the master encryption key. Default: blank.
</details>
<details><summary><font size=4>MFA</font></summary>
- **mfa**, multi-factor authentication settings
- `totp`, list of struct that define settings for time-based one time passwords (RFC 6238). Each struct has the following fields:
- `name`, string. Unique configuration name. This name should not be changed if there are users or admins using the configuration. The name is not visible to the authentication apps. Default: `Default`.
- `issuer`, string. Name of the issuing Organization/Company. Default: `SFTPGo`.
- `algo`, string. Algorithm to use for HMAC. The supported algorithms are: `sha1`, `sha256`, `sha512`. Currently Google Authenticator app on iPhone seems to only support `sha1`, please check the compatibility with your target apps/device before setting a different algorithm. You can also define multiple configurations, for example one that uses `sha256` or `sha512` and another one that uses `sha1` and instruct your users to use the appropriate configuration for their devices/apps. The algorithm should not be changed if there are users or admins using the configuration. Default: `sha1`.
</details>
<details><summary><font size=4>SMTP</font></summary>
- **smtp**, SMTP configuration enables SFTPGo email sending capabilities
- `host`, string. Location of SMTP email server. Leave empty to disable email sending capabilities. Default: blank.
- `port`, integer. Port of SMTP email server.
- `from`, string. From address, for example `SFTPGo <sftpgo@example.com>`. Many SMTP servers reject emails without a `From` header so, if not set, SFTPGo will try to use the username as fallback, this may or may not be appropriate. Default: blank
- `user`, string. SMTP username. Default: blank
- `password`, string. SMTP password. Leaving both username and password empty the SMTP authentication will be disabled. Default: blank
- `auth_type`, integer. 0 means `Plain`, 1 means `Login`, 2 means `CRAM-MD5`, 3 means `XOAUTH2`. Default: `0`.
- `encryption`, integer. 0 means no encryption, 1 means `TLS`, 2 means `STARTTLS`. Default: `0`.
- `domain`, string. Domain to use for `HELO` command, if empty `localhost` will be used. Default: blank.
- `templates_path`, string. Path to the email templates. This can be an absolute path or a path relative to the config dir. Templates are searched within a subdirectory named "email" in the specified path. You can customize the email templates by simply specifying an alternate path and putting your custom templates there.
- `debug`, integer. Set to `1` to enable SMTP debug. Default: `0`.
- `oauth2`, struct containing OAuth2 related configurations:
- `provider`, integer, 0 means `Google`, 1 means `Microsoft`. Default: `0`.
- `tenant`, string. Azure Active Directory tenant for the Microsoft provider. Typical values are `common`, `organizations`, `consumers` or tenant identifier. If empty `common` is used. Default: blank.
- `client_id`, string. Default: blank.
- `client_secret`, string. Default: blank.
- `refresh_token`, string. Default: blank.
</details>
<details><summary><font size=4>Plugins</font></summary>
- **plugins**, list of external plugins. :warning: Please note that the plugin system is experimental, the configuration parameters and interfaces may change in a backward incompatible way in future. Each plugin is configured using a struct with the following fields:
- `type`, string. Defines the plugin type. Supported types: `notifier`, `kms`, `auth`, `metadata`.
- `notifier_options`, struct. Defines the options for notifier plugins.
- `fs_events`, list of strings. Defines the filesystem events that will be notified to this plugin.
- `provider_events`, list of strings. Defines the provider events that will be notified to this plugin.
- `provider_objects`, list if strings. Defines the provider objects that will be notified to this plugin.
- `log_events`, list of integers. Defines the log events that will be notified to this plugin. `1` means "Login failed", `2` means "Login with non-existent user", `3` means "No login tried", `4` means "Algorithm negotiation failed".
- `retry_max_time`, integer. Defines the maximum number of seconds an event can be late. SFTPGo adds a timestamp to each event and add to an internal queue any events that a the plugin fails to handle (the plugin returns an error or it is not running). If a plugin fails to handle an event that is too late, based on this configuration, it will be discarded. SFTPGo will try to resend queued events every 30 seconds. 0 means no retry.
- `retry_queue_max_size`, integer. Defines the maximum number of events that the internal queue can hold. Once the queue is full, the events that cannot be sent to the plugin will be discarded. 0 means no limit.
- `kms_options`, struct. Defines the options for kms plugins.
- `scheme`, string. KMS scheme. Supported schemes are: `awskms`, `gcpkms`, `hashivault`, `azurekeyvault`.
- `encrypted_status`, string. Encrypted status for a KMS secret. Supported statuses are: `AWS`, `GCP`, `VaultTransit`, `AzureKeyVault`.
- `auth_options`, struct. Defines the options for auth plugins.
- `scope`, integer. 1 means passwords only. 2 means public keys only. 4 means key keyboard interactive only. 8 means TLS certificate. The flags can be combined, for example 6 means public keys and keyboard interactive. The scope must be explicit, `0` is not a valid option.
- `cmd`, string. Path to the plugin executable.
- `args`, list of strings. Optional arguments to pass to the plugin executable.
- `sha256sum`, string. SHA256 checksum for the plugin executable. If not empty it will be used to verify the integrity of the executable.
- `auto_mtls`, boolean. If enabled the client and the server automatically negotiate mutual TLS for transport authentication. This ensures that only the original client will be allowed to connect to the server, and all other connections will be rejected. The client will also refuse to connect to any server that isn't the original instance started by the client.
</details>
A full example showing the default config (in JSON format) can be found [here](../sftpgo.json).
@@ -246,7 +517,9 @@ then SFTPGo will try to create `id_rsa`, `id_ecdsa` and `id_ed25519`, if they ar
The configuration can be read from JSON, TOML, YAML, HCL, envfile and Java properties config files. If your `config-file` flag is set to `sftpgo` (default value), you need to create a configuration file called `sftpgo.json` or `sftpgo.yaml` and so on inside `config-dir`.
## Environment variables
</details>
<details><summary><font size=5> Environment variables</font></summary>
You can also override all the available configuration options using environment variables. SFTPGo will check for environment variables with a name matching the key uppercased and prefixed with the `SFTPGO_`. You need to use `__` to traverse a struct.
@@ -255,9 +528,75 @@ Let's see some examples:
- To set the `port` for the first sftpd binding, you need to define the env var `SFTPGO_SFTPD__BINDINGS__0__PORT`
- To set the `execute_on` actions, you need to define the env var `SFTPGO_COMMON__ACTIONS__EXECUTE_ON`. For example `SFTPGO_COMMON__ACTIONS__EXECUTE_ON=upload,download`
On some hardware you can get faster SFTP performance by replacing the Go `crypto/sha256` implementation with [sha256-simd](https://github.com/minio/sha256-simd).
The performances of SHA256 is relevant for clients using AES CTR ciphers and `hmac-sha2-256` as Message Authentication Code (MAC).
Up to 2.0.x versions SFTPGo automatically used `sha256-simd` but over the time the standard Go implementation improved a lot and now is faster than `sha256-simd` on some CPUs.
You can select `sha256-simd` setting the environment variable `SFTPGO_MINIO_SHA256_SIMD` to `1`.
`sha256-simd` is particularly useful if you have an Intel CPU with SHA extensions or an ARM CPU with Cryptography Extensions.
The configuration file can change between different versions and merging your custom settings with the default configuration file, after updating SFTPGo, may be time-consuming. For this reason we suggest to set your custom settings using environment variables. This eliminates the need to merge your changes with the default configuration file after each update, you have to just check that your custom configuration keys still exists.
Setting configuration options from environment variables is natural in Docker/Kubernetes.
If you install SFTPGo on Linux using the official deb/rpm packages you can set your custom environment variables in the file `/etc/sftpgo/sftpgo.env` (create this file if it does not exist, it is defined as `EnvironmentFile` in the SFTPGo systemd unit).
SFTPGo also reads files inside the `env.d` directory relative to config dir and then exports the valid variables into environment variables if they are not already set. With this method you can override any configuration options, set environment variables for SFTPGo plugins but you cannot set command flags because these files are read after that SFTPGo starts and the config dir must already be set.
Of course you can also set environment variables with the method provided by the operating system of your choice.
</details>
<details><summary><font size=5>Binding to privileged ports</font></summary>
On Linux, if you want to use Internet domain privileged ports (port numbers less than 1024) instead of running the SFTPGo service as root user you can set the `cap_net_bind_service` capability on the `sftpgo` binary. To set the capability you can use the following command:
```shell
$ sudo setcap cap_net_bind_service=+ep /usr/bin/sftpgo
# Check that the capability is added
$ getcap /usr/bin/sftpgo
/usr/bin/sftpgo cap_net_bind_service=ep
```
Now you can use privileged ports such as 21, 22, 443 etc.. without running the SFTPGo service as root user. You have to set the `cap_net_bind_service` capability each time you update the `sftpgo` binary.
The "official" deb/rpm packages attempt to set the `cap_net_bind_service` capability in their `postinstall` scripts.
An alternative method is to use `iptables`, for example you run the SFTP service on port `2022` and redirect traffic from port `22` to port `2022`:
```shell
sudo iptables -t nat -A PREROUTING -d <ip> -p tcp --dport 22 -m addrtype --dst-type LOCAL -j DNAT --to-destination <ip>:2022
sudo iptables -t nat -A OUTPUT -d <ip> -p tcp --dport 22 -m addrtype --dst-type LOCAL -j DNAT --to-destination <ip>:2022
```
</details>
<details><summary><font size=5>Supported Password Hashing Algorithms</font></summary>
SFTPGo can verify passwords in several formats and uses, by default, the `bcrypt` algorithm to hash passwords in plain-text before storing them inside the data provider. Each hashing algorithm is identified by a prefix.
Supported hash algorithms:
- bcrypt, prefix `$2a$`
- argon2id, prefix `$argon2id$`
- PBKDF2 sha1, prefix `$pbkdf2-sha1$`
- PBKDF2 sha256, prefix `$pbkdf2-sha256$`
- PBKDF2 sha512, prefix `$pbkdf2-sha512$`
- PBKDF2 sha256 with base64 salt, prefix `$pbkdf2-b64salt-sha256$`
- MD5 crypt, prefix `$1$`
- MD5 crypt APR1, prefix `$apr1$`
- SHA256 crypt, prefix `$5$`
- SHA512 crypt, prefix `$6$`
- MD5 digest, prefix `{MD5}`
- SHA256 digest, prefix `{SHA256}`
- SHA512 digest, prefix `{SHA512}`
If you set a password with one of these prefixes it will not be hashed.
When users log in, if their passwords are stored with anything other than the preferred algorithm, SFTPGo will automatically upgrade the algorithm to the preferred one.
</details>
## Telemetry Server
The telemetry server exposes the following endpoints:
The telemetry server publishes the following endpoints:
- `/healthz`, health information (for health checks)
- `/metrics`, Prometheus metrics

View File

@@ -8,4 +8,4 @@ You can optionally specify a [storage class](https://cloud.google.com/storage/do
The configured bucket must exist.
This backend is very similar to the [S3](./s3.md) backend, and it has the same limitations.
This backend is very similar to the [S3](./s3.md) backend, and it has the same limitations. As with S3 `chtime` will fail with the default configuration, you can install the [metadata plugin](https://github.com/sftpgo/sftpgo-plugin-metadata) to make it work and thus be able to preserve/change file modification times.

45
docs/groups.md Normal file
View File

@@ -0,0 +1,45 @@
# Groups
Using groups simplifies the administration of multiple accounts by letting you assign settings once to a group, instead of multiple times to each individual user.
SFTPGo supports the following types of groups:
- primary groups
- secondary groups
- membership groups
A user can be a member of a primary group and many secondary and membership groups. Depending on the group type, the settings are inherited differently.
:warning: SFTPGo groups are completely unrelated to system groups. Therefore, it is not necessary to add Linux/Windows groups to use SFTPGo groups.
The following settings are inherited from the primary group:
- home dir, if set for the group will replace the one defined for the user. The `%username%` placeholder is replaced with the username
- filesystem config, if the provider set for the group is different from the "local provider" will replace the one defined for the user. The `%username%` placeholder is replaced with the username within the defined "prefix", for any vfs, and the "username" for the SFTP filesystem config
- max sessions, quota size/files, upload/download bandwidth, upload/download/total data transfer, max upload size, external auth cache time, ftp_security, default share expiration, password expiration, password strength: if they are set to `0` for the user they are replaced with the value set for the group, if different from `0`. The password strength defined at group level is only enforce when users change their password
- expires_in, if defined and the user does not have an expiration date set, defines the expiration of the account in number of days from the creation date
- TLS username, check password hook disabled, pre-login hook disabled, external auth hook disabled, filesystem checks disabled, allow API key authentication, anonymous user: if they are not set for the user they are replaced with the value set for the group
- starting directory, if the user does not have a starting directory set, the value set for the group is used, if any. The `%username%` placeholder is replaced with the username
The following settings are inherited from the primary and secondary groups:
- virtual folders, file patterns, permissions: they are added to the user configuration if the user does not already have a setting for the configured path. The `/` path is ignored for secondary groups. The `%username%` placeholder is replaced with the username within the virtual path, the defined "prefix", for any vfs, and the "username" for the SFTP and HTTP filesystem config
- per-source bandwidth limits
- per-source data transfer limits
- allowed/denied IPs
- denied login methods and protocols
- two factor auth protocols
- web client/REST API permissions
The settings from the primary group are always merged first. no setting is inherited from "membership" groups.
The final settings are a combination of the user settings and the group ones.
For example you can define the following groups:
- "group1", it has a virtual directory to mount on `/vdir1`
- "group2", it has a virtual directory to mount on `/vdir2`
- "group3", it has a virtual directory to mount on `/vdir3`
If you define users with a virtual directory to mount on `/vdir` and make them member of all the above groups, they will have virtual directories mounted on `/vdir`, `/vdir1`, `/vdir2`, `/vdir3`. If users already have a virtual directory to mount on `/vdir1`, the group's one will be ignored.
Please note that if the same virtual path is set in more than one secondary group the behavior is undefined. For example if a user is a member of two secondary groups and each secondary group defines a virtual folder to mount on the `/vdir2` path, the virtual folder mounted on `/vdir2` may change with every login.

View File

@@ -2,5 +2,11 @@
Here we collect step-to-step tutorials. SFTPGo users are encouraged to contribute!
- [Getting Started](./getting-started.md)
- [Securing SFTPGo with a free Let's Encrypt TLS Certificate](./lets-encrypt-certificate.md)
- [Two-factor Authentication](./two-factor-authentication.md)
- [Event Manager](./eventmanager.md)
- [SFTPGo as OpenSSH's SFTP subsystem](./openssh-sftp-subsystem.md)
- [SFTPGo with PostgreSQL data provider and S3 backend](./postgresql-s3.md)
- [Expose Web Admin and REST API over HTTPS and password protected](./rest-api-https-auth.md)
- [SFTPGo on Windows with Active Directory Integration + Caddy Static File Server](https://www.youtube.com/watch?v=M5UcJI8t4AI)
- [File Traefik: Serve files securely via SFTP, HTTPS, and WebDAV with SFTPGo proxied behind Traefik](https://thad.getterman.org/articles/file-traefik/)

130
docs/howto/eventmanager.md Normal file
View File

@@ -0,0 +1,130 @@
# Event Manager
The Event Manager allows an administrator to configure HTTP notifications, commands execution, email notifications and carry out certain server operations based on server events or schedules. More details [here](../eventmanager.md).
Let's see some common use cases.
- [Preliminary Note](#preliminary-note)
- [Daily backups](#daily-backups)
- [Automatically create a folder structure](#automatically-create-a-folder-structure)
- [Upload notifications](#upload-notifications)
- [Recycle Bin](#recycle-bin)
## Preliminary Note
We will use email actions in the following paragraphs, so let's assume you have a working SMTP configuration.
You can adapt the following snippet to configure an SMTP server using environment variables.
```shell
SFTPGO_SMTP__HOST="your smtp server host"
SFTPGO_SMTP__FROM="SFTPGo <sftpgo@example.com>"
SFTPGO_SMTP__USER=sftpgo@example.com
SFTPGO_SMTP__PASSWORD="your password"
SFTPGO_SMTP__AUTH_TYPE=1 # change based on what your server supports
SFTPGO_SMTP__ENCRYPTION=2 # change based on what your server supports
```
SFTPGo supports several placeholders for event actions. You can see all supported placeholders by clicking on the "info" icon at the top right of the add/update action page.
## Daily backups
You can schedule SFTPGo data backups (users, folders, groups, admins etc.) on a regular basis, such as daily.
From the WebAdmin expand the `Event Manager` section, select `Event actions` and add a new action.
Create an action named `backup` and set the type to `Backup`.
![Backup action](./img/backup-action.png)
Create another action named `backup notification`, set the type to `Email` and fill the recipient/s.
As email subject set `Backup {{StatusString}}`. The `{{StatusString}}` placeholder will be expanded to `OK` or `KO`.
As email body set `Backup done {{ErrorString}}`. The error string will be empty if no errors occur.
![Backup notification action](./img/backup-notification-action.png)
Now select `Event rules` and create a rule named `Daily backup`, select `Schedule` as trigger and schedule a backup at midnight UTC time.
![Daily backup schedule](./img/daily-backup-schedule.png)
As actions select `backup` and `backup notification`.
![Daily backup actions](./img/daily-backup-actions.png)
Done! SFTPGo will make a new backup every day and you will receive an email with the status of the backup. The backup will be saved on the server side in the configured backup directory. The backup files will have names like this `backup_<week day>_<hour>.json`.
## Automatically create a folder structure
Suppose you want to automatically create the folders `in` and `out` when you create new users.
From the WebAdmin expand the `Event Manager` section, select `Event actions` and add a new action.
Create an action named `create dirs`, with the settings you can see in the following screen.
![Create dirs action](./img/create-dirs-action.png)
Create another action named `create dirs failure notification`, set the type to `Email` and fill the recipient/s.
As email subject set `Unable to create dirs for user {{ObjectName}}`.
As email body set `Error: {{ErrorString}}`.
![Create dirs notification](./img/create-dirs-failure-notification.png)
Now select `Event rules` and create a rule named `Create dirs for users`, select `Provider event` as trigger, `add` as provider event and `user` as object filters.
![Create dirs rule](./img/create-dirs-rule.png)
As actions select `create dirs` and `create dirs failure notification`, check `Is failure action` for the notification action.
This way you will only be notified by email if an error occurs.
![Create dirs rule actions](./img/create-dirs-rule-actions.png)
Done! Create a new user and check that the defined directories are automatically created.
## Upload notifications
Let's see how you can receive an email notification after each upload and, optionally, the uploaded file as well.
From the WebAdmin expand the `Event Manager` section, select `Event actions` and add a new action.
Create an action named `upload notification` with the settings you can see in the following screen.
![Upload notification action](./img/upload-notification.png)
You can optionally add the uploaded file as an attachment but note that SFTPGo allows you to attach a maximum of 10MB. Then the action will fail for files bigger than 10MB.
Now select `Event rules` and create a rule named `Upload rule`, select `Filesystem evens` as trigger and `upload` as filesystem event.
You can also filters events based on protocol, user and group name, filepath shell-like patterns, file size. We omit these additional filters for simplicity.
![Upload rule](./img/upload-rule.png)
As actions, select `upload notification`.
Done! Try uploading a new file and you will receive the configured email notification.
## Recycle Bin
Let's see how we can configure a Recycle Bin style function where files are not deleted strait away, but instead moved to a separate folder.
To easily apply the Recycle Bin to multiple users we will create a virtual folder and a group, this way all users who belong to the group will have a Recycle Bin.
Create a virtual folder named `recycle` with the settings you can see in the following screen.
![Recycle folder](./img/recycle-folder.png)
Create a group named `recycle` with the settings you can see in the following screen.
![Recycle group](./img/recycle-group.png)
Make your users members of the `recycle` group.
From the WebAdmin expand the `Event Manager` section, select `Event actions` and add a new action.
Create an action named `move to recycle` with the settings you can see in the following screen.
![Recycle move action](./img/recycle-move-action.png)
Now select `Event rules` and create a rule named `Recycle rule`, select `Filesystem events` as trigger, `pre-delete` as filesystem event and exclude the `/recycle` path.
![Recycle rule](./img/recycle-rule.png)
![Recycle rule exclude path](./img/recycle-rule-path.png)
As actions, select `move to recycle` and set `Execute sync`.
Done! Try deleting a file, it will be moved to the Recycle Bin.
You can also add a scheduled event rule to automatically delete files older than a configurable time from the `/recycle` folder.

View File

@@ -0,0 +1,657 @@
# Getting Started
SFTPGo allows to securely share your files over SFTP and optionally FTP/S and WebDAV too.
Several storage backends are supported and they are configurable per user, so you can serve a local directory for a user and an S3 bucket (or part of it) for another one.
SFTPGo also supports virtual folders, a virtual folder can use any of the supported storage backends. So you can have, for example, a user with the S3 backend mapping a GCS bucket (or part of it) on a specified path and an encrypted local filesystem on another one.
Virtual folders can be private or shared among multiple users, for shared virtual folders you can define different quota limits for each user.
In this tutorial we explore the main features and concepts using the built-in web admin interface. Advanced users can also use the SFTPGo [REST API](https://sftpgo.stoplight.io/docs/sftpgo/openapi.yaml)
- [Installation](#installation)
- [Initial configuration](#initial-configuration)
- [Creating users](#creating-users)
- [Creating users with a Cloud Storage backend](#creating-users-with-a-cloud-storage-backend)
- [Creating users with a local encrypted backend (Data At Rest Encryption)](#creating-users-with-a-local-encrypted-backend-data-at-rest-encryption)
- [Virtual permissions](#virtual-permissions)
- [Virtual folders](#virtual-folders)
- [Groups](#groups)
- [Usage example](#usage-example)
- [Simplify user page](#simplify-user-page)
- [Configuration parameters](#configuration-parameters)
- [Use PostgreSQL data provider](#use-postgresql-data-provider)
- [Use MySQL/MariaDB data provider](#use-mysqlmariadb-data-provider)
- [Use CockroachDB data provider](#use-cockroachdb-data-provider)
- [Enable FTP service](#enable-ftp-service)
- [Enable WebDAV service](#enable-webdav-service)
## Installation
You can easily install SFTPGo in several ways, more details [here](../../README.md#installation).
In this guide, we assume that SFTPGo is already installed and running using the default configuration.
## Initial configuration
Before you can use SFTPGo you need to create an admin account, so open [http://127.0.0.1:8080/web/admin](http://127.0.0.1:8080/web) in your web browser, replacing `127.0.0.1` with the appropriate IP address if SFTPGo is not running on localhost.
![Setup](./img/setup.png)
After creating the admin account you will be automatically logged in.
![Users list](./img/initial-screen.png)
The web admin is now available at the following URL:
[http://127.0.0.1:8080/web/admin](http://127.0.0.1:8080/web/admin)
From the `Status` page you see the active services.
![Status](./img/status.png)
The default configuration enables the SFTP service on port `2022` and uses an embedded data provider (`SQLite` or `bolt` based on the target OS and architecture).
## Creating users
Let's create our first local user:
- from the `Users` page click the `+` icon to open the `Add user page`
- the only required fields are the `Username` and a `Password` or a `Public key`
- if you are on Windows or you installed SFTPGo manually and no `users_base_dir` is defined in your configuration file you also have to set a `Home Dir`. It must be an absolute path, for example `/srv/sftpgo/data/username` on Linux or `C:\sftpgo\data\username` on Windows. SFTPGo will try to automatically create the home directory, if missing, when the user logs in. Each user can only access files and folders inside its home directory.
- click `Submit`
![Add user](./img/add-user.png)
:warning: Please note that, on Linux, SFTPGo runs using a dedicated system user and group called `sftpgo`, for added security. If you want to be able to use directories outside the `/srv/sftpgo` path you need to set the appropriate system level permissions. For example if you define `/home/username/test` as home dir you have to create this directory yourself, if it doesn't exist, and set the appropriate system-level permissions:
```shell
sudo mkdir /home/username/test
sudo chown sftpgo:sftpgo /home/username/test
```
You also need to make sure that the `sftpgo` system user has at least the read permission for any parent directory, so in the example above `/home/username` and `/home` must not have `0700` permissions.
Now test the new user, we use the `sftp` CLI here, you can use any SFTP client.
```shell
$ sftp -P 2022 nicola@127.0.0.1
nicola@127.0.0.1's password:
Connected to 127.0.0.1.
sftp> ls
sftp> put file.txt
Uploading file.txt to /file.txt
file.txt 100% 4034 3.9MB/s 00:00
sftp> ls
file.txt
sftp> mkdir adir
sftp> cd adir/
sftp> put file.txt
Uploading file.txt to /adir/file.txt
file.txt 100% 4034 4.0MB/s 00:00
sftp> ls
file.txt
sftp> get file.txt
Fetching /adir/file.txt to file.txt
/adir/file.txt 100% 4034 1.9MB/s 00:00
```
It worked! We can upload/download files and create directories.
Each user can browse and download their files, share files with external users, change their credentials and configure two-factor authentication using the WebClient interface available at the following URL:
[http://127.0.0.1:8080/web/client](http://127.0.0.1:8080/web/client)
![WebClient files](./img/web-client-files.png)
![WebClient two-factor authentication](./img/web-client-two-factor-auth.png)
### Creating users with a Cloud Storage backend
The procedure is similar to the one described for local users, you have only specify the Cloud Storage backend and its credentials.
The screenshot below shows an example configuration for an S3 backend.
![S3 user](./img/s3-user-1.png)
![S3 user](./img/s3-user-2.png)
The screenshot below shows an example configuration for an Azure Blob Storage backend.
![Azure Blob user](./img/az-user-1.png)
![Azure Blob user](./img/az-user-2.png)
The screenshot below shows an example configuration for a Google Cloud Storage backend.
![Google Cloud user](./img/gcs-user.png)
The screenshot below shows an example configuration for an SFTP server as storage backend.
![User using another SFTP server as storage backend](./img/sftp-user.png)
Setting a `Key Prefix` you restrict the user to a specific "sub-folder" in the bucket, so that the same bucket can be shared among different users.
### Creating users with a local encrypted backend (Data At Rest Encryption)
The procedure is similar to the one described for local users, you have only specify the encryption passphrase.
The screenshot below shows an example configuration.
![User with cryptfs backend](./img/local-encrypted.png)
You can find more details about Data At Rest Encryption [here](../dare.md).
## Virtual permissions
SFTPGo supports per directory virtual permissions. For each user you have to specify global permissions and then override them on a per-directory basis.
Take a look at the following screens.
![Permissions](./img/virtual-permissions.png)
This user has full access as default (`*`), can only list and download from `/read-only` path and has no permissions at all for the `/subdir` path.
Let's test it. We use the `sftp` CLI here, you can use any SFTP client.
```shell
$ sftp -P 2022 nicola@127.0.0.1
Connected to 127.0.0.1.
sftp> ls
adir file.txt read-only subdir
sftp> put file.txt
Uploading file.txt to /file.txt
file.txt 100% 4034 19.4MB/s 00:00
sftp> rm file.txt
Removing /file.txt
sftp> ls
adir read-only subdir
sftp> cd read-only/
sftp> ls
file.txt
sftp> put file1.txt
Uploading file1.txt to /read-only/file1.txt
remote open("/read-only/file1.txt"): Permission denied
sftp> get file.txt
Fetching /read-only/file.txt to file.txt
/read-only/file.txt 100% 4034 2.2MB/s 00:00
sftp> cd ..
sftp> ls
adir read-only subdir
sftp> cd /subdir
sftp> ls
remote readdir("/subdir"): Permission denied
```
as you can see it worked as expected.
## Virtual folders
A virtual folder is a mapping between a SFTPGo virtual path and a filesystem path outside the user home directory or on a different storage provider.
Therefore, there is no need to create virtual folders for the users home directory or for directories within the users home directory.
From the web admin interface click `Folders` and then the `+` icon.
![Add folder](./img/add-folder.png)
To create a local folder you need to specify a `Name` and an `Absolute path`. For other backends you have to specify the backend type and its credentials, this is the same procedure already detailed for creating users with cloud backends.
Suppose we created two virtual folders name `localfolder` and `minio` as you can see in the following screen.
![Folders](./img/folders.png)
- `localfolder` uses the local filesystem as storage backend
- `minio` uses MinIO (S3 compatible) as storage backend
Now, click `Users`, on the left menu, select a user and click the `Edit` icon, to update the user and associate the virtual folders.
Virtual folders must be referenced using their unique name and you can map them on a configurable virtual path. Take a look at the following screenshot.
![Virtual Folders](./img/virtual-folders.png)
We mapped the folder named `localfolder` on the path `/vdirlocal` (this must be an absolute UNIX path on Windows too) and the folder named `minio` on the path `/vdirminio`. For `localfolder` the quota usage is included within the user quota, while for the `minio` folder we defined separate quota limits: at most 2 files and at most 100MB, whichever is reached first.
The folder `minio` can be shared with other users and we can define different quota limits on a per-user basis. The folder `localfolder` is considered private since we have included its quota limits within those of the user, if we share them with other users we will break quota calculation.
Let's test these virtual folders. We use the `sftp` CLI here, you can use any SFTP client.
```shell
$ sftp -P 2022 nicola@127.0.0.1
nicola@127.0.0.1's password:
Connected to 127.0.0.1.
sftp> ls
adir read-only subdir vdirlocal vdirminio
sftp> cd vdirlocal
sftp> put file.txt
Uploading file.txt to /vdirlocal/file.txt
file.txt 100% 4034 17.3MB/s 00:00
sftp> ls
file.txt
sftp> cd ..
sftp> cd vdirminio/
sftp> put file.txt
Uploading file.txt to /vdirminio/file.txt
file.txt 100% 4034 4.8MB/s 00:00
sftp> ls
file.txt
sftp> put file.txt file1.txt
Uploading file.txt to /vdirminio/file1.txt
file.txt 100% 4034 2.8MB/s 00:00
sftp> put file.txt file2.txt
Uploading file.txt to /vdirminio/file2.txt
remote open("/vdirminio/file2.txt"): Failure
sftp> quit
```
The last upload failed since we exceeded the number of files quota limit.
## Groups
Using groups simplifies the administration of multiple SFTPGo users: you can assign settings once to a group, instead of multiple times to each individual user.
SFTPGo supports the following types of groups:
- primary groups
- secondary groups
- membership groups
A user can be a member of a primary group and many secondary and membership groups. Depending on the group type, the settings are inherited differently, more details [here](../groups.md).
:warning: SFTPGo groups are completely unrelated to system groups. Therefore, it is not necessary to add Linux/Windows groups to use SFTPGo groups.
### Usage example
Suppose you have the following requirements:
- each user must be restricted to a local home directory containing the username as last element of the path, for example `/srv/sftpgo/data/<username>`
- for each user, the maximum upload size for a single file must be limited to 1GB
- each user must have an S3 virtual folder available in the path `/s3<username>` and each user can only access a specified "prefix" of the S3 bucket. It must not be able to access other users' files
- each user must have an S3 virtual folder available in the path `/shared`. This is a folder shared with other users
- a group of users can only download and list contents in the `/shared` path while another group of users have full access
We can easily meet these requirements by defining two groups.
From the SFTPGo WebAdmin UI, click on `Folders` and then on the `+` icon.
Create a folder named `S3private`.
Set the storage to `AWS S3 (Compatible)` and fill the required parameters:
- bucket name
- region
- credentials: access key and access secret
![S3Private folder](./img/s3-private-folder.png)
The important part is the `Key Prefix`, set it to `users/%username%/`
![S3Private Key Prefix](./img/s3-key-prefix.png)
The placeholder `%username%` will be replaced with the associated username.
Create another folder named `S3shared` with the same settings as `S3private` but this time set the `Key Prefix` to `shared/`.
The `Key Prefix` has no placeholder, so the folder will operate on a static path that won't change based on the associated user.
Now click on `Groups` and then on the `+` icon and add a group named `Primary`.
Set the `Home Dir` to `/srv/sftpgo/data/%username%`.
![Add group](./img/add-group.png)
As before, the placeholder `%username%` will be replaced with the associated username.
Add the two virtual folders to this group and set the `Max file upload size` to 1GB.
![Primary group settings](./img/primary-group-settings.png)
Add a new group and name it `SharedReadOnly`, in the ACLs section set the permission on the `/shared` path so that read only access is granted.
![Read-only share](./img/read-only-share.png)
The group setup is now complete. We can now create our users and set the primary group to `Primary`.
For the users who need read-only access to the `/shared` path we also have to set `SharedReadOnly` as a secondary group.
You can now login with any SFTP client like FileZilla, WinSCP etc. and verify that the requirements are met.
### Simplify user page
The add/update user page has many configuration options and can be intimidating for some administrators. We can hide most of the settings and automatically add groups to newly created users. This way the hidden settings are inherited from the automatically assigned groups and therefore administrators can add new users simply by setting the username and credentials.
Click on `Admins` and then on the `+` icon and add an admin named `simply`.
In the `Groups for users` section set `Primary` as primary group and `SharedReadOnly` as `seconday` group.
In the `User page preferences` section hide all the sections.
![Simplified admin](./img/simplified-admin.png)
Log in using the newly created administrator and try to add a new user. The user page is simplified as you can see in the following screen.
![Simplified user add](./img/add-user-simplified.png)
## Configuration parameters
Until now we used the default configuration, to change the global service parameters you have to edit the configuration file, or set appropriate environment variables, and restart SFTPGo to apply the changes.
A full explanation of all configuration methods can be found [here](./../full-configuration.md), we explore some common use cases. Please keep in mind that SFTPGo can also be configured via environment variables, this is very convenient if you are using Docker.
The default configuration file is `sftpgo.json` and it can be found within the `/etc/sftpgo` directory if you installed from Linux distro packages. On Windows the configuration file can be found within the `{commonappdata}\SFTPGo` directory where `{commonappdata}` is typically `C:\ProgramData`. SFTPGo also supports reading from TOML and YAML configuration files.
The configuration file can change between different versions and merging your custom settings with the default configuration file, after updating SFTPGo, may be time-consuming. For this reason we suggest to set your custom settings using environment variables.
If you install SFTPGo on Linux using the official deb/rpm packages you can set your custom environment variables in the file `/etc/sftpgo/sftpgo.env`.
SFTPGo also reads files inside the `env.d` directory relative to config dir (`/etc/sftpgo/env.d` on Linux and `{commonappdata}\SFTPGo\env.d` on Windows) and then exports the valid variables into environment variables if they are not already set.
Of course you can also set environment variables with the method provided by the operating system of your choice.
The following snippets assume your are running SFTPGo on Linux but they can be easily adapted for other operating systems.
### Use PostgreSQL data provider
Create a PostgreSQL database named `sftpgo` and a PostgreSQL user with the correct permissions, for example using the `psql` CLI.
```shell
sudo -i -u postgres psql
CREATE DATABASE "sftpgo" WITH ENCODING='UTF8' CONNECTION LIMIT=-1;
create user "sftpgo" with encrypted password 'your password here';
grant all privileges on database "sftpgo" to "sftpgo";
\q
```
You can open the SFTPGo configuration file, search for the `data_provider` section and change it as follow.
```json
"data_provider": {
"driver": "postgresql",
"name": "sftpgo",
"host": "127.0.0.1",
"port": 5432,
"username": "sftpgo",
"password": "your password here",
...
}
```
Alternatively (recommended), you can use environment variables by creating the file `/etc/sftpgo/env.d/postgresql.env` with the following content.
```shell
SFTPGO_DATA_PROVIDER__DRIVER=postgresql
SFTPGO_DATA_PROVIDER__NAME=sftpgo
SFTPGO_DATA_PROVIDER__HOST=127.0.0.1
SFTPGO_DATA_PROVIDER__PORT=5432
SFTPGO_DATA_PROVIDER__USERNAME=sftpgo
SFTPGO_DATA_PROVIDER__PASSWORD=your password here
```
Confirm that the database connection works by initializing the data provider.
```shell
$ sudo su - sftpgo -s /bin/bash -c 'sftpgo initprovider -c /etc/sftpgo'
2021-05-19T22:21:54.000 INF Initializing provider: "postgresql" config file: "/etc/sftpgo/sftpgo.json"
2021-05-19T22:21:54.000 INF updating database schema version: 8 -> 9
2021-05-19T22:21:54.000 INF Data provider successfully initialized/updated
```
Ensure that SFTPGo starts after the database service.
```shell
sudo systemctl edit sftpgo.service
```
And override the unit definition with the following snippet.
```shell
[Unit]
After=postgresql.service
```
Restart SFTPGo to apply the changes.
### Use MySQL/MariaDB data provider
Create a MySQL database named `sftpgo` and a MySQL user with the correct permissions, for example using the `mysql` CLI.
```shell
$ mysql -u root
MariaDB [(none)]> CREATE DATABASE sftpgo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Query OK, 1 row affected (0.000 sec)
MariaDB [(none)]> grant all privileges on sftpgo.* to sftpgo@localhost identified by 'your password here';
Query OK, 0 rows affected (0.027 sec)
MariaDB [(none)]> quit
Bye
```
You can open the SFTPGo configuration file, search for the `data_provider` section and change it as follow.
```json
"data_provider": {
"driver": "mysql",
"name": "sftpgo",
"host": "127.0.0.1",
"port": 3306,
"username": "sftpgo",
"password": "your password here",
...
}
```
Alternatively (recommended), you can use environment variables by creating the file `/etc/sftpgo/env.d/mysql.env` with the following content.
```shell
SFTPGO_DATA_PROVIDER__DRIVER=mysql
SFTPGO_DATA_PROVIDER__NAME=sftpgo
SFTPGO_DATA_PROVIDER__HOST=127.0.0.1
SFTPGO_DATA_PROVIDER__PORT=3306
SFTPGO_DATA_PROVIDER__USERNAME=sftpgo
SFTPGO_DATA_PROVIDER__PASSWORD=your password here
```
Confirm that the database connection works by initializing the data provider.
```shell
$ sudo su - sftpgo -s /bin/bash -c 'sftpgo initprovider -c /etc/sftpgo'
2021-05-19T22:29:30.000 INF Initializing provider: "mysql" config file: "/etc/sftpgo/sftpgo.json"
2021-05-19T22:29:30.000 INF updating database schema version: 8 -> 9
2021-05-19T22:29:30.000 INF Data provider successfully initialized/updated
```
Ensure that SFTPGo starts after the database service.
```shell
sudo systemctl edit sftpgo.service
```
And override the unit definition with the following snippet.
```shell
[Unit]
After=mariadb.service
```
Restart SFTPGo to apply the changes.
### Use CockroachDB data provider
We suppose you have installed CockroachDB this way:
```shell
sudo su
export CRDB_VERSION=22.1.8 # set the latest available version here
wget -qO- https://binaries.cockroachdb.com/cockroach-v${CRDB_VERSION}.linux-amd64.tgz | tar xvz
cp -i cockroach-v${CRDB_VERSION}.linux-amd64/cockroach /usr/local/bin/
mkdir -p /usr/local/lib/cockroach
cp -i cockroach-v${CRDB_VERSION}.linux-amd64/lib/libgeos.so /usr/local/lib/cockroach/
cp -i cockroach-v${CRDB_VERSION}.linux-amd64/lib/libgeos_c.so /usr/local/lib/cockroach/
mkdir /var/lib/cockroach
chown sftpgo:sftpgo /var/lib/cockroach
mkdir -p /etc/cockroach/{certs,ca}
chmod 700 /etc/cockroach/ca
/usr/local/bin/cockroach cert create-ca --certs-dir=/etc/cockroach/certs --ca-key=/etc/cockroach/ca/ca.key
/usr/local/bin/cockroach cert create-node localhost $(hostname) --certs-dir=/etc/cockroach/certs --ca-key=/etc/cockroach/ca/ca.key
/usr/local/bin/cockroach cert create-client root --certs-dir=/etc/cockroach/certs --ca-key=/etc/cockroach/ca/ca.key
chown -R sftpgo:sftpgo /etc/cockroach/certs
exit
```
and you are running it using a systemd unit like this one:
```shell
[Unit]
Description=Cockroach Database single node
Requires=network.target
[Service]
Type=notify
WorkingDirectory=/var/lib/cockroach
ExecStart=/usr/local/bin/cockroach start-single-node --certs-dir=/etc/cockroach/certs --http-addr 127.0.0.1:8888 --listen-addr 127.0.0.1:26257 --cache=.25 --max-sql-memory=.25 --store=path=/var/lib/cockroach
TimeoutStopSec=60
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
User=sftpgo
[Install]
WantedBy=default.target
```
Create a CockroachDB database named `sftpgo`.
```shell
$ sudo /usr/local/bin/cockroach sql --certs-dir=/etc/cockroach/certs -e 'create database "sftpgo"'
CREATE DATABASE
Time: 13ms
```
You can open the SFTPGo configuration file, search for the `data_provider` section and change it as follow.
```json
"data_provider": {
"driver": "cockroachdb",
"name": "sftpgo",
"host": "localhost",
"port": 26257,
"username": "root",
"password": "",
"sslmode": 3,
"root_cert": "/etc/cockroach/certs/ca.crt",
"client_cert": "/etc/cockroach/certs/client.root.crt",
"client_key": "/etc/cockroach/certs/client.root.key",
...
}
```
Alternatively (recommended), you can use environment variables by creating the file `/etc/sftpgo/env.d/cockroachdb.env` with the following content.
```shell
SFTPGO_DATA_PROVIDER__DRIVER=cockroachdb
SFTPGO_DATA_PROVIDER__NAME=sftpgo
SFTPGO_DATA_PROVIDER__HOST=localhost
SFTPGO_DATA_PROVIDER__PORT=26257
SFTPGO_DATA_PROVIDER__USERNAME=root
SFTPGO_DATA_PROVIDER__SSLMODE=3
SFTPGO_DATA_PROVIDER__ROOT_CERT="/etc/cockroach/certs/ca.crt"
SFTPGO_DATA_PROVIDER__CLIENT_CERT="/etc/cockroach/certs/client.root.crt"
SFTPGO_DATA_PROVIDER__CLIENT_KEY="/etc/cockroach/certs/client.root.key"
```
Confirm that the database connection works by initializing the data provider.
```shell
$ sudo su - sftpgo -s /bin/bash -c 'sftpgo initprovider -c /etc/sftpgo'
2022-06-02T14:54:04.510 INF Initializing provider: "cockroachdb" config file: "/etc/sftpgo/sftpgo.json"
2022-06-02T14:54:04.554 INF creating initial database schema, version 15
2022-06-02T14:54:04.698 INF updating database schema version: 15 -> 16
2022-06-02T14:54:07.093 INF updating database schema version: 16 -> 17
2022-06-02T14:54:07.672 INF updating database schema version: 17 -> 18
2022-06-02T14:54:07.699 INF updating database schema version: 18 -> 19
2022-06-02T14:54:07.721 INF Data provider successfully initialized/updated
```
Ensure that SFTPGo starts after the database service.
```shell
sudo systemctl edit sftpgo.service
```
And override the unit definition with the following snippet.
```shell
[Unit]
After=cockroachdb.service
```
Restart SFTPGo to apply the changes.
### Enable FTP service
You can set the configuration options to enable the FTP service by opening the SFTPGo configuration file, looking for the `ftpd` section and editing it as follows.
```json
"ftpd": {
"bindings": [
{
"port": 2121,
"address": "",
"apply_proxy_config": true,
"tls_mode": 0,
"certificate_file": "",
"certificate_key_file": "",
"min_tls_version": 12,
"force_passive_ip": "",
"passive_ip_overrides": [],
"client_auth_type": 0,
"tls_cipher_suites": [],
"passive_connections_security": 0,
"active_connections_security": 0,
"debug": false
}
],
"banner": "",
"banner_file": "",
"active_transfers_port_non_20": true,
"passive_port_range": {
"start": 50000,
"end": 50100
},
...
}
```
Alternatively (recommended), you can use environment variables by creating the file `/etc/sftpgo/env.d/ftpd.env` with the following content.
```shell
SFTPGO_FTPD__BINDINGS__0__PORT=2121
```
Restart SFTPGo to apply the changes. The FTP service is now available on port `2121`.
You can also configure the passive ports range (`50000-50100` by default), these ports must be reachable for passive FTP to work. If your FTP server is on the private network side of a NAT configuration you have to set `force_passive_ip` to your external IP address. You may also need to open the passive port range on your firewall.
It is recommended that you provide a certificate and key file to allow FTP over TLS. You should prefer SFTP to FTP even if you configure TLS, please don't blindly enable the old FTP protocol.
### Enable WebDAV service
You can set the configuration options to enable the FTP service by opening the SFTPGo configuration file, looking for the `webdavd` section and editing it as follows.
```json
"webdavd": {
"bindings": [
{
"port": 10080,
"address": "",
"enable_https": false,
"certificate_file": "",
"certificate_key_file": "",
"min_tls_version": 12,
"client_auth_type": 0,
"tls_cipher_suites": [],
"prefix": "",
"proxy_allowed": [],
"client_ip_proxy_header": "",
"client_ip_header_depth": 0,
"disable_www_auth_header": false
}
],
...
}
```
Alternatively (recommended), you can use environment variables by creating the file `/etc/sftpgo/env.d/webdavd.env` with the following content.
```shell
SFTPGO_WEBDAVD__BINDINGS__0__PORT=10080
```
Restart SFTPGo to apply the changes. The WebDAV service is now available on port `10080`. It is recommended that you provide a certificate and key file to allow WebDAV over https.

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
docs/howto/img/add-user.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Some files were not shown because too many files have changed in this diff Show More