Compare commits

...

21 commits
main ... v2.4.2

Author SHA1 Message Date
Nicola Murino
b989cdabe5
set version to 2.4.2
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-25 15:06:12 +01:00
Nicola Murino
9e7e89d69e
backports from main
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-24 18:36:25 +01:00
Nicola Murino
f64056b820
update nfpm to 2.22.1
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-20 15:40:44 +01:00
Nicola Murino
7e6d944cb5
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-20 13:49:04 +01:00
Nicola Murino
a66d207291
fix SeaweedFS rename compatibility
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-20 13:07:43 +01:00
Nicola Murino
0a8edcd811
backports from main
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-19 13:07:03 +01:00
Nicola Murino
0fa08ddbaa
set version to 2.4.1
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-12 18:41:49 +01:00
Nicola Murino
3d4c35522a
initprovider: fix loading users with MFA config
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-11 19:48:27 +01:00
Nicola Murino
f400e67daa
fix restore users with MFA config
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-11 18:12:56 +01:00
Nicola Murino
4e10275fd1
clarify that the PROXY protocol is supported for SFTP/FTP
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-07 09:17:12 +01:00
Nicola Murino
7bd71474ef
plugins: fix hash check
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-05 10:23:41 +01:00
Nicola Murino
0ac2120532
WebUI: try harder to prevent browsers from auto-filling in password fields
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-04 19:20:57 +01:00
Nicola Murino
9e5287cfb4
webdav: always open files for reading in lazy mode
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-03 08:53:19 +01:00
Nicola Murino
450ab6b252
shared providers: allow to immediately re-add soft-deleted event rules
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-01 17:40:39 +01:00
Nicola Murino
51d900558a
WebDAV: make test cases more robust
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-01 14:30:00 +01:00
Nicola Murino
a71690ff2a
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 13:17:49 +01:00
Nicola Murino
f390eab1de
sftpfs: reuse connections
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-01 13:17:26 +01:00
Nicola Murino
571e088fdd
improve some docs
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-01 13:17:19 +01:00
Nicola Murino
6714085d58
eventmanager: add placeholder to get the parent directory
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-01 13:17:11 +01:00
Nicola Murino
0389605d65
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-11-01 13:17:07 +01:00
Nicola Murino
b8ef94ece7
CI: use 2.4.x branch
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-11-01 11:01:21 +01:00
85 changed files with 1882 additions and 738 deletions

View file

@ -2,7 +2,7 @@ name: CI
on:
push:
branches: [main]
branches: [2.4.x]
pull_request:
jobs:

View file

@ -5,7 +5,7 @@ on:
# - cron: '0 4 * * *' # everyday at 4:00 AM UTC
push:
branches:
- main
- 2.4.x
tags:
- v*
pull_request:

View file

@ -5,7 +5,7 @@ on:
tags: 'v*'
env:
GO_VERSION: 1.19.2
GO_VERSION: 1.19.3
jobs:
prepare-sources-with-deps:

1
CODEOWNERS Normal file
View file

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

View file

@ -28,6 +28,8 @@ ARG DOWNLOAD_PLUGINS=false
RUN if [ "${DOWNLOAD_PLUGINS}" = "true" ]; then apt-get update && apt-get install --no-install-recommends -y curl && ./docker/scripts/download-plugins.sh; fi
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
@ -45,6 +47,7 @@ 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/openapi /usr/share/sftpgo/openapi

View file

@ -25,6 +25,7 @@ RUN set -xe && \
export COMMIT_SHA=${COMMIT_SHA:-$(git describe --always --abbrev=8 --dirty)} && \
go build $(if [ -n "${FEATURES}" ]; then echo "-tags ${FEATURES}"; fi) -trimpath -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${COMMIT_SHA} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -v -o sftpgo
RUN apk add --update --no-cache openssh-client-common
FROM alpine:3.16
@ -35,16 +36,13 @@ RUN apk add --update --no-cache ca-certificates tzdata mailcap
RUN if [ "${INSTALL_OPTIONAL_PACKAGES}" = "true" ]; then apk add --update --no-cache jq git rsync; fi
# 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 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

View file

@ -28,7 +28,7 @@ RUN sed -i 's|"users_base_dir": "",|"users_base_dir": "/srv/sftpgo/data",|' sftp
sed -i 's|"backups"|"/srv/sftpgo/backups"|' sftpgo.json && \
sed -i 's|"sqlite"|"bolt"|' sftpgo.json
RUN apt-get update && apt-get install --no-install-recommends -y media-types && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install --no-install-recommends -y media-types openssh-server && rm -rf /var/lib/apt/lists/*
RUN mkdir /etc/sftpgo /var/lib/sftpgo /srv/sftpgo
@ -38,6 +38,7 @@ COPY --from=builder --chown=1000:1000 /etc/sftpgo /etc/sftpgo
COPY --from=builder --chown=1000:1000 /srv/sftpgo /srv/sftpgo
COPY --from=builder --chown=1000:1000 /var/lib/sftpgo /var/lib/sftpgo
COPY --from=builder --chown=1000:1000 /workspace/sftpgo.json /etc/sftpgo/sftpgo.json
COPY --from=builder --chown=1000:1000 /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

View file

@ -134,7 +134,7 @@ 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](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/prasselsrl1645470739547.sftpgo_linux)
- [Azure Marketplace](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/eliamarzia1667381463185.sftpgo_linux)
- [Elest.io](https://elest.io/open-source/sftpgo)
Purchasing from there will help keep SFTPGo a long-term sustainable project.

View file

@ -125,7 +125,7 @@ SFTPGo 基于 Linux 开发和创建。在每一次提交之后,代码会自动
</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/prasselsrl1645470739547.sftpgo_linux) 同样可用,在此付费可以帮助 SFTPGo 成为一个可持续发展的长期项目。
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>

View file

@ -4,12 +4,12 @@ SFTPGo provides an official Docker image, it is available on both [Docker Hub](h
## Supported tags and respective Dockerfile links
- [v2.4.0, v2.4, v2, latest](https://github.com/drakkan/sftpgo/blob/v2.4.0/Dockerfile)
- [v2.4.0-plugins, v2.4-plugins, v2-plugins, plugins](https://github.com/drakkan/sftpgo/blob/v2.4.0/Dockerfile)
- [v2.4.0-alpine, v2.4-alpine, v2-alpine, alpine](https://github.com/drakkan/sftpgo/blob/v2.4.0/Dockerfile.alpine)
- [v2.4.0-slim, v2.4-slim, v2-slim, slim](https://github.com/drakkan/sftpgo/blob/v2.4.0/Dockerfile)
- [v2.4.0-alpine-slim, v2.4-alpine-slim, v2-alpine-slim, alpine-slim](https://github.com/drakkan/sftpgo/blob/v2.4.0/Dockerfile.alpine)
- [v2.4.0-distroless-slim, v2.4-distroless-slim, v2-distroless-slim, distroless-slim](https://github.com/drakkan/sftpgo/blob/v2.4.0/Dockerfile.distroless)
- [v2.4.2, v2.4, v2, latest](https://github.com/drakkan/sftpgo/blob/v2.4.2/Dockerfile)
- [v2.4.2-plugins, v2.4-plugins, v2-plugins, plugins](https://github.com/drakkan/sftpgo/blob/v2.4.2/Dockerfile)
- [v2.4.2-alpine, v2.4-alpine, v2-alpine, alpine](https://github.com/drakkan/sftpgo/blob/v2.4.2/Dockerfile.alpine)
- [v2.4.2-slim, v2.4-slim, v2-slim, slim](https://github.com/drakkan/sftpgo/blob/v2.4.2/Dockerfile)
- [v2.4.2-alpine-slim, v2.4-alpine-slim, v2-alpine-slim, alpine-slim](https://github.com/drakkan/sftpgo/blob/v2.4.2/Dockerfile.alpine)
- [v2.4.2-distroless-slim, v2.4-distroless-slim, v2-distroless-slim, distroless-slim](https://github.com/drakkan/sftpgo/blob/v2.4.2/Dockerfile.distroless)
- [edge](../Dockerfile)
- [edge-plugins](../Dockerfile)
- [edge-alpine](../Dockerfile.alpine)

View file

@ -28,10 +28,13 @@ The following placeholders are supported:
- `{{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.
- `{{Protocol}}`. Used protocol, for example `SFTP`, `FTP`.

View file

@ -65,7 +65,7 @@ The configuration file contains the following sections:
- `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 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).
- `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 following modes are supported:
- `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. 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
- 2, required. If the upstream IP is not allowed to send a proxy header the connection will be rejected
@ -129,7 +129,8 @@ The configuration file contains the following sections:
- `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.
- `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`.
- `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.
- `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, `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. 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.

View file

@ -24,8 +24,7 @@ If you use WebDAV behind a reverse proxy ensure to preserve the `Host` header or
Know issues:
- removing a directory tree on Cloud Storage backends could generate a `not found` error when removing the last (virtual) directory. This happens if the client cycles the directories tree itself and removes files and directories one by one instead of issuing a single remove command
- the used [WebDAV library](https://pkg.go.dev/golang.org/x/net/webdav?tab=doc) asks to open a file to execute a `stat` and sometimes reads some bytes to find the content type. Stat calls are executed before and after a download too, so to be able to properly list a directory you need to grant both `list` and `download` permissions and to be able to upload files you need to gran both `list` and `upload` permissions
- the used [WebDAV library](https://pkg.go.dev/golang.org/x/net/webdav?tab=doc) not always returns a proper error code/message, most of the times it simply returns `Method not Allowed`. I'll try to improve the library error codes in the future
- to be able to properly list a directory you need to grant both `list` and `download` permissions and to be able to upload files you need to gran both `list` and `upload` permissions
- if a file or a directory cannot be accessed, for example due to OS permissions issues or because a mapped path for a virtual folder is a missing, it will be omitted from the directory listing. If there is a different error then the whole directory listing will fail. This behavior is different from SFTP/FTP where you will be able to see the problematic file/directory in the directory listing, you will only get an error if you try to access it
- if you use the native Windows client please check its usage and pay particular attention to the [registry settings](https://docs.microsoft.com/en-us/iis/publish/using-webdav/using-the-webdav-redirector#webdav-redirector-registry-settings). The default file size limit is 50MB and if you don't configure SFTPGo to use HTTPS you have to set `BasicAuthLevel` to `2`

View file

@ -1,5 +1,7 @@
# Data Backup
:warning: Since v2.4.0 you can use the [EventManager](../../docs/eventmanager.md) to schedule backups.
The `backup` example script shows how to use the SFTPGo REST API to backup your data.
The script is written in Python and has the following requirements:

View file

@ -1,5 +1,7 @@
# File retention policies
:warning: Since v2.4.0 you can use the [EventManager](../../docs/eventmanager.md) to schedule data retention checks.
The `checkretention` example script shows how to use the SFTPGo REST API to manage data retention.
:warning: Deleting files is an irreversible action, please make sure you fully understand what you are doing before using this feature, you may have users with overlapping home directories or virtual folders shared between multiple users, it is relatively easy to inadvertently delete files you need.

View file

@ -1,5 +1,7 @@
# Update user quota
:warning: Since v2.4.0 you can use the [EventManager](../../docs/eventmanager.md) to schedule quota scans.
The `scanuserquota` example script shows how to use the SFTPGo REST API to update the users' quota.
The stored quota may be incorrect for several reasons, such as an unexpected shutdown while uploading files, temporary provider failures, files copied outside of SFTPGo, and so on.

3
fail2ban/README.md Normal file
View file

@ -0,0 +1,3 @@
# Fail2ban
:warning: We recommend using the [built-in defender](../docs/defender.md) instead of Fail2ban.

93
go.mod
View file

@ -3,28 +3,29 @@ module github.com/drakkan/sftpgo/v2
go 1.19
require (
cloud.google.com/go/storage v1.27.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.4
cloud.google.com/go/storage v1.28.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.5.1
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
github.com/aws/aws-sdk-go-v2 v1.17.1
github.com/aws/aws-sdk-go-v2/config v1.17.10
github.com/aws/aws-sdk-go-v2/credentials v1.12.23
github.com/aws/aws-sdk-go-v2/config v1.18.3
github.com/aws/aws-sdk-go-v2/credentials v1.13.3
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.37
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.21
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.1
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.4
github.com/aws/aws-sdk-go-v2/service/sts v1.17.1
github.com/cockroachdb/cockroach-go/v2 v2.2.16
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.42
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.24
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.4
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.8
github.com/aws/aws-sdk-go-v2/service/sts v1.17.5
github.com/cockroachdb/cockroach-go/v2 v2.2.19
github.com/coreos/go-oidc/v3 v3.4.0
github.com/drakkan/webdav v0.0.0-20221101181759-17ed21f9337b
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
github.com/fclairamb/ftpserverlib v0.20.1-0.20221012093027-95be4ae0c9a6
github.com/fclairamb/go-log v0.4.1
github.com/go-acme/lego/v4 v4.9.0
github.com/go-chi/chi/v5 v5.0.8-0.20221018120124-e5529d9db4d3
github.com/go-chi/jwtauth/v5 v5.0.2
github.com/go-chi/jwtauth/v5 v5.1.0
github.com/go-chi/render v1.0.2
github.com/go-sql-driver/mysql v1.6.0
github.com/golang/mock v1.6.0
@ -32,55 +33,55 @@ require (
github.com/google/uuid v1.3.0
github.com/grandcat/zeroconf v1.0.0
github.com/hashicorp/go-hclog v1.3.1
github.com/hashicorp/go-plugin v1.4.5
github.com/hashicorp/go-plugin v1.4.6
github.com/hashicorp/go-retryablehttp v0.7.1
github.com/jackc/pgx/v5 v5.0.4
github.com/jackc/pgx/v5 v5.1.1
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
github.com/klauspost/compress v1.15.12
github.com/lestrrat-go/jwx v1.2.25
github.com/lestrrat-go/jwx/v2 v2.0.8
github.com/lithammer/shortuuid/v3 v3.0.7
github.com/mattn/go-sqlite3 v1.14.16
github.com/mhale/smtpd v0.8.0
github.com/minio/sio v0.3.0
github.com/otiai10/copy v1.7.0
github.com/otiai10/copy v1.9.0
github.com/pires/go-proxyproto v0.6.2
github.com/pkg/sftp v1.13.6-0.20221020054726-e4133ab7e9bd
github.com/pquerna/otp v1.3.0
github.com/prometheus/client_golang v1.13.0
github.com/prometheus/client_golang v1.14.0
github.com/robfig/cron/v3 v3.0.1
github.com/rs/cors v1.8.3-0.20220619195839-da52b0701de5
github.com/rs/xid v1.4.0
github.com/rs/zerolog v1.28.0
github.com/sftpgo/sdk v0.1.2
github.com/shirou/gopsutil/v3 v3.22.9
github.com/spf13/afero v1.9.2
github.com/shirou/gopsutil/v3 v3.22.10
github.com/spf13/afero v1.9.3
github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.13.0
github.com/spf13/viper v1.14.0
github.com/stretchr/testify v1.8.1
github.com/studio-b12/gowebdav v0.0.0-20221015232716-17255f2e7423
github.com/studio-b12/gowebdav v0.0.0-20221109171924-60ec5ad56012
github.com/subosito/gotenv v1.4.1
github.com/unrolled/secure v1.13.0
github.com/wagslane/go-password-validator v0.3.0
github.com/xhit/go-simple-mail/v2 v2.12.0
github.com/xhit/go-simple-mail/v2 v2.13.0
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
go.etcd.io/bbolt v1.3.6
go.uber.org/automaxprocs v1.5.1
gocloud.dev v0.27.0
golang.org/x/crypto v0.1.0
golang.org/x/net v0.1.0
golang.org/x/oauth2 v0.1.0
golang.org/x/sys v0.1.0
golang.org/x/time v0.1.0
google.golang.org/api v0.101.0
golang.org/x/crypto v0.3.0
golang.org/x/net v0.2.0
golang.org/x/oauth2 v0.2.0
golang.org/x/sys v0.2.0
golang.org/x/time v0.2.0
google.golang.org/api v0.103.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0
)
require (
cloud.google.com/go v0.105.0 // indirect
cloud.google.com/go v0.107.0 // indirect
cloud.google.com/go/compute v1.12.1 // indirect
cloud.google.com/go/compute/metadata v0.1.1 // indirect
cloud.google.com/go/iam v0.6.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1 // indirect
cloud.google.com/go/compute/metadata v0.2.1 // indirect
cloud.google.com/go/iam v0.7.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1 // indirect
github.com/ajg/form v1.5.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.9 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25 // indirect
@ -97,9 +98,9 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/coreos/go-systemd/v22 v22.4.0 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
@ -112,7 +113,7 @@ require (
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
github.com/googleapis/gax-go/v2 v2.6.0 // indirect
github.com/googleapis/gax-go/v2 v2.7.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
@ -120,11 +121,11 @@ require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.1.2 // indirect
github.com/klauspost/cpuid/v2 v2.2.1 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.4 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.0 // indirect
github.com/lib/pq v1.10.7 // indirect
@ -139,8 +140,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
github.com/prometheus/client_model v0.3.0 // indirect
@ -150,18 +150,18 @@ require (
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/tklauser/numcpus v0.5.0 // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.0 // indirect
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
go.opencensus.io v0.23.0 // indirect
golang.org/x/mod v0.6.0 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/mod v0.7.0 // indirect
golang.org/x/text v0.4.0 // indirect
golang.org/x/tools v0.2.0 // indirect
golang.org/x/tools v0.3.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71 // indirect
google.golang.org/grpc v1.50.1 // indirect
google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 // indirect
google.golang.org/grpc v1.51.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
@ -171,6 +171,5 @@ require (
replace (
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20221020054403-a265c1cba3cb
golang.org/x/net => github.com/drakkan/net v0.0.0-20221026175805-eaebd725b308
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20221117111000-a0321143587c
)

280
go.sum
View file

@ -36,8 +36,8 @@ cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w9
cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=
cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU=
cloud.google.com/go v0.103.0/go.mod h1:vwLx1nqLrzLX/fpwSMOXmFIqBOyHsvHbnAdbGSJ+mKk=
cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y=
cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
cloud.google.com/go v0.107.0 h1:qkj22L7bgkl6vIeZDlOY2po43Mx/TIa2Wsa7VR+PEww=
cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@ -52,18 +52,19 @@ cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLq
cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=
cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0=
cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
cloud.google.com/go/compute/metadata v0.1.1 h1:/sxEbyrm6cw+XOUw1YxBHlatV71z4vpnmO7z2IZ0h3I=
cloud.google.com/go/compute/metadata v0.1.1/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU=
cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48=
cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c=
cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
cloud.google.com/go/iam v0.6.0 h1:nsqQC88kT5Iwlm4MeNGTpfMWddp6NB/UOLFTH6m1QfQ=
cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc=
cloud.google.com/go/kms v1.4.0 h1:iElbfoE61VeLhnZcGOltqL8HIly8Nhbe5t6JlH9GXjo=
cloud.google.com/go/iam v0.7.0 h1:k4MuwOsS7zGJJ+QfZ5vBK8SgHBAvYN/23BWsiihJ1vs=
cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg=
cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA=
cloud.google.com/go/kms v1.6.0 h1:OWRZzrPmOZUzurjI2FBGtgY2mB1WaJkqhw6oIwSj0Yg=
cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs=
cloud.google.com/go/monitoring v1.1.0/go.mod h1:L81pzz7HKn14QCMaCs6NTQkdBnE87TElyanS95vIcl4=
cloud.google.com/go/monitoring v1.5.0/go.mod h1:/o9y8NYX5j91JjD/JvGLYbi86kL11OjyJXq2XziLJu4=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
@ -81,8 +82,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc=
cloud.google.com/go/storage v1.24.0/go.mod h1:3xrJEFMXBsQLgxwThyjuD3aYlroL0TMRec1ypGUQ0KE=
cloud.google.com/go/storage v1.27.0 h1:YOO045NZI9RKfCj1c5A/ZtuuENUc8OAW+gHdGnDgyMQ=
cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s=
cloud.google.com/go/storage v1.28.0 h1:DLrIZ6xkeZX6K70fU/boWx5INJumt6f+nwwWSHXzzGY=
cloud.google.com/go/storage v1.28.0/go.mod h1:qlgZML35PXA3zoEnIkiPLY4/TOkUleufRlu6qmcf7sI=
cloud.google.com/go/trace v1.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A=
cloud.google.com/go/trace v1.2.0/go.mod h1:Wc8y/uYyOhPy12KEnXG9XGrvfMz5F5SrYecQlbW1rwM=
code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8=
@ -100,15 +101,15 @@ github.com/Azure/azure-sdk-for-go v66.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.1/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.4 h1:pqrAR74b6EoR4kcxF7L7Wg2B8Jgil9UUZtMvxhEFqWo=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.4/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0 h1:sVW/AFBTGyJxDaMYlq0ct3jUXTtj12tQ6zE2GZUgVQw=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0/go.mod h1:+6sju8gk8FRmSajX3Oz4G5Gm7P+mbqE9FVaXXFYTkCM=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0 h1:QkAcEIAKbNL4KoFr4SathZPhDhF4mVwpBMFlYjyAqy8=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1 h1:XUNQ4mw+zJmaA2KXzP9JlQiecy1SI+Eog7xVkPiqIbg=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1 h1:Oj853U9kG+RLTCQXpjvOnrv0WaZHxgmZz1TlLywgOPY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.0.2/go.mod h1:LH9XQnMr2ZYxQdVdCrzLO9mxeDyrDFa6wbSI3x5zCZk=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.4.1/go.mod h1:eZ4g6GUvXiGulfIbbhh1Xr4XwUYaYaWMqzGD/284wCA=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.5.1 h1:BMTdr+ib5ljLa9MxTJK8x/Ds0MbBb4MfuW5BL0zMJnI=
@ -232,17 +233,17 @@ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.3/go.mod h1:gNsR5CaXK
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.9 h1:RKci2D7tMwpvGpDNZnGQw9wk6v7o/xSwFcUAuNPoB8k=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.9/go.mod h1:vCmV1q1VK8eoQJ5+aYE7PkK1K6v41qJ5pJdK3ggCDvg=
github.com/aws/aws-sdk-go-v2/config v1.15.15/go.mod h1:A1Lzyy/o21I5/s2FbyX5AevQfSVXpvvIDCoVFD0BC4E=
github.com/aws/aws-sdk-go-v2/config v1.17.10 h1:zBy5QQ/mkvHElM1rygHPAzuH+sl8nsdSaxSWj0+rpdE=
github.com/aws/aws-sdk-go-v2/config v1.17.10/go.mod h1:/4np+UiJJKpWHN7Q+LZvqXYgyjgeXm5+lLfDI6TPZao=
github.com/aws/aws-sdk-go-v2/config v1.18.3 h1:3kfBKcX3votFX84dm00U8RGA1sCCh3eRMOGzg5dCWfU=
github.com/aws/aws-sdk-go-v2/config v1.18.3/go.mod h1:BYdrbeCse3ZnOD5+2/VE/nATOK8fEUpBtmPMdKSyhMU=
github.com/aws/aws-sdk-go-v2/credentials v1.12.10/go.mod h1:g5eIM5XRs/OzIIK81QMBl+dAuDyoLN0VYaLP+tBqEOk=
github.com/aws/aws-sdk-go-v2/credentials v1.12.23 h1:LctvcJMIb8pxvk5hQhChpCu0WlU6oKQmcYb1HA4IZSA=
github.com/aws/aws-sdk-go-v2/credentials v1.12.23/go.mod h1:0awX9iRr/+UO7OwRQFpV1hNtXxOVuehpjVEzrIAYNcA=
github.com/aws/aws-sdk-go-v2/credentials v1.13.3 h1:ur+FHdp4NbVIv/49bUjBW+FE7e57HOo03ELodttmagk=
github.com/aws/aws-sdk-go-v2/credentials v1.13.3/go.mod h1:/rOMmqYBcFfNbRPU0iN9IgGqD5+V2yp3iWNmIlz0wI4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.9/go.mod h1:KDCCm4ONIdHtUloDcFvK2+vshZvx4Zmj7UMDfusuz5s=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19 h1:E3PXZSI3F2bzyj6XxUXdTIfvp425HHhwKsFvmzBwHgs=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19/go.mod h1:VihW95zQpeKQWVPGkwT+2+WJNQV8UXFfMTWdU6VErL8=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.21/go.mod h1:iIYPrQ2rYfZiB/iADYlhj9HHZ9TTi6PqKQPAqygohbE=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.37 h1:e1VtTBo+cLNjres0wTlMkmwCGGRjDEkkrz3frxxcaCs=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.37/go.mod h1:kdAV1UMnCkyG6tZJUC4mHbPoRjPA3dIK0L8mnsHERiM=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.42 h1:bxgBYvvBh+W1RnNYP4ROXEB8N+HSSucDszfE7Rb+kfU=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.42/go.mod h1:LHOsygMiW/14CkFxdXxvzKyMh3jbk/QfZVaDtCbLkl8=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.15/go.mod h1:pWrr2OoHlT7M/Pd2y4HV3gJyPb3qj5qMmnPkKSNPYK4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25 h1:nBO/RFxeq/IS5G9Of+ZrgucRciie2qpLy++3UGZ+q2E=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25/go.mod h1:Zb29PYkf42vVYQY6pvSyJCJcFHlPIiY+YKdPtwnvMkY=
@ -268,14 +269,14 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.9/go.mod h1:Rc5+wn2
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.19 h1:piDBAaWkaxkkVV3xJJbTehXCZRXYs49kvpi/LG6LR2o=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.19/go.mod h1:BmQWRVkLTmyNzYPFAZgon53qKLWBNSvonugD1MrSWUs=
github.com/aws/aws-sdk-go-v2/service/kms v1.18.1/go.mod h1:4PZMUkc9rXHWGVB5J9vKaZy3D7Nai79ORworQ3ASMiM=
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.21 h1:LpIut7TpOhp8RuTD72PBj8ksPy3+RelT3LPwGgQ8+Hg=
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.21/go.mod h1:mRGY+k3s1yt7yQA3AfzJhnr68OCs1xDfQfIABFUk+ek=
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.24 h1:DYr+X4xrRzcthq2OLJzsiS/uSJhZ/HHxXG0yUgGZceU=
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.24/go.mod h1:mRGY+k3s1yt7yQA3AfzJhnr68OCs1xDfQfIABFUk+ek=
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.2/go.mod h1:u+566cosFI+d+motIz3USXEh6sN8Nq4GrNXSg2RXVMo=
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.1 h1:/EMdFPW/Ppieh0WUtQf1+qCGNLdsq5UWUyevBQ6vMVc=
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.1/go.mod h1:/NHbqPRiwxSPVOB2Xr+StDEH+GWV/64WwnUjv4KYzV0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.4 h1:QgmmWifaYZZcpaw3y1+ccRlgH6jAvLm4K/MBGUc7cNM=
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.4/go.mod h1:/NHbqPRiwxSPVOB2Xr+StDEH+GWV/64WwnUjv4KYzV0=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.14/go.mod h1:xakbH8KMsQQKqzX87uyyzTHshc/0/Df8bsTneTS5pFU=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.4 h1:Hx79EGrkKNJya2iz2U5A7nyr7DjOu/TGTRefThfBZ1w=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.4/go.mod h1:k6CPuxyzO247nYEM1baEwHH1kRtosRCvgahAepaaShw=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.8 h1:Zw48FHykP40fKMxPmagkuzklpEuDPLhvUjKP8Ygrds0=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.8/go.mod h1:k6CPuxyzO247nYEM1baEwHH1kRtosRCvgahAepaaShw=
github.com/aws/aws-sdk-go-v2/service/sns v1.17.10/go.mod h1:uITsRNVMeCB3MkWpXxXw0eDz8pW4TYLzj+eyQtbhSxM=
github.com/aws/aws-sdk-go-v2/service/sqs v1.19.1/go.mod h1:A94o564Gj+Yn+7QO1eLFeI7UVv3riy/YBFOfICVqFvU=
github.com/aws/aws-sdk-go-v2/service/ssm v1.27.6/go.mod h1:fiFzQgj4xNOg4/wqmAiPvzgDMXPD+cUEplX/CYn+0j0=
@ -285,8 +286,8 @@ github.com/aws/aws-sdk-go-v2/service/sso v1.11.25/go.mod h1:IARHuzTXmj1C0KS35vbo
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8 h1:jcw6kKZrtNfBPJkaHrscDOZoe5gvi9wjudnxvozYFJo=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8/go.mod h1:er2JHN+kBY6FcMfcBBKNGCT3CarImmdFzishsqBmSRI=
github.com/aws/aws-sdk-go-v2/service/sts v1.16.10/go.mod h1:cftkHYN6tCDNfkSasAmclSfl4l7cySoay8vz7p/ce0E=
github.com/aws/aws-sdk-go-v2/service/sts v1.17.1 h1:KRAix/KHvjGODaHAMXnxRk9t0D+4IJVUuS/uwXxngXk=
github.com/aws/aws-sdk-go-v2/service/sts v1.17.1/go.mod h1:bXcN3koeVYiJcdDU89n3kCYILob7Y34AeLopUbZgLT4=
github.com/aws/aws-sdk-go-v2/service/sts v1.17.5 h1:60SJ4lhvn///8ygCzYy2l53bFW/Q15bVfyjyAWo6zuw=
github.com/aws/aws-sdk-go-v2/service/sts v1.17.5/go.mod h1:bXcN3koeVYiJcdDU89n3kCYILob7Y34AeLopUbZgLT4=
github.com/aws/smithy-go v1.12.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/aws/smithy-go v1.13.4 h1:/RN2z1txIJWeXeOkzX+Hk/4Uuvv7dWtCjbmVJcrskyk=
github.com/aws/smithy-go v1.13.4/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
@ -318,8 +319,9 @@ github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEe
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
@ -358,8 +360,8 @@ github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/cockroachdb/cockroach-go/v2 v2.2.16 h1:t9dmZuC9J2W8IDQDSIGXmP+fBuEJSsrGXxWQz4cYqBY=
github.com/cockroachdb/cockroach-go/v2 v2.2.16/go.mod h1:xZ2VHjUEb/cySv0scXBx7YsBnHtLHkR1+w/w73b5i3M=
github.com/cockroachdb/cockroach-go/v2 v2.2.19 h1:YIHyz17jZumBeXPuoZKq/0nrITsqDoDD8/KQt3/xiyc=
github.com/cockroachdb/cockroach-go/v2 v2.2.19/go.mod h1:mzlIDDBALQfEjv/7DU12fb2AfQ/MUYTlychcMpWp9QI=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo=
github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA=
@ -480,8 +482,8 @@ github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+
github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.4.0 h1:y9YHcjnjynCd/DVbg5j9L/33jQM3MxJlbj/zWskzfGU=
github.com/coreos/go-systemd/v22 v22.4.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
@ -501,7 +503,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
github.com/denisenkom/go-mssqldb v0.12.2/go.mod h1:lnIw1mZukFRZDJYQ0Pb833QS2IaC3l5HkEfra2LJ+sk=
@ -537,12 +538,12 @@ github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/drakkan/crypto v0.0.0-20221020054403-a265c1cba3cb h1:ex3x8ir969oV6bQ8fBYlaw1DXKGZCOM560bdeqeDJ/Y=
github.com/drakkan/crypto v0.0.0-20221020054403-a265c1cba3cb/go.mod h1:IBSs4ri4rdTqz2QKcpTKpwKMdM+WJ7atZeL9lCu2swQ=
github.com/drakkan/crypto v0.0.0-20221117111000-a0321143587c h1:3lJRXQcGw8fnheQ9ORGYE17JLuYzu6FkDuK8MZwqeRQ=
github.com/drakkan/crypto v0.0.0-20221117111000-a0321143587c/go.mod h1:z/3TjBrBT/Dch7NfsgFg7f07WiGYZVYQgnxGHacCvRc=
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA=
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
github.com/drakkan/net v0.0.0-20221026175805-eaebd725b308 h1:F7OUb3MgSa2SuY5mdmseihGXhVhxv1OCuKHrona2eh0=
github.com/drakkan/net v0.0.0-20221026175805-eaebd725b308/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
github.com/drakkan/webdav v0.0.0-20221101181759-17ed21f9337b h1:B9z7XyDoVxLO4yEvnXgdvZ+0Uw9NA1qdD4KTSGmKcoQ=
github.com/drakkan/webdav v0.0.0-20221101181759-17ed21f9337b/go.mod h1:8opebuqUyBXrvl7Vo/S1Zzl9U0G1X2Ceud440eVuhUE=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
@ -608,11 +609,10 @@ github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwv
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/go-acme/lego/v4 v4.9.0 h1:8Hjj44IqRS7cigshMyFQ+0pIZvwgkG/+9A0UnNh7G8A=
github.com/go-acme/lego/v4 v4.9.0/go.mod h1:g3JRUyWS3L/VObpp4bCxzJftKyf/Wba8QrSSnoiqjg4=
github.com/go-chi/chi/v5 v5.0.4/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.0.8-0.20221018120124-e5529d9db4d3 h1:qzwVVqrbdP93ZaSHy0yWQRYnig+t+j1OxnVtEs8SFuQ=
github.com/go-chi/chi/v5 v5.0.8-0.20221018120124-e5529d9db4d3/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/jwtauth/v5 v5.0.2 h1:CSKtr+b6Jnfy5T27sMaiBPxaVE/bjnjS3ramFQ0526w=
github.com/go-chi/jwtauth/v5 v5.0.2/go.mod h1:TeA7vmPe3uYThvHw8O8W13HOOpOd4MTgToxL41gZyjs=
github.com/go-chi/jwtauth/v5 v5.1.0 h1:wJyf2YZ/ohPvNJBwPOzZaQbyzwgMZZceE1m8FOzXLeA=
github.com/go-chi/jwtauth/v5 v5.1.0/go.mod h1:MA93hc1au3tAQwCKry+fI4LqJ5MIVN4XSsglOo+lSc8=
github.com/go-chi/render v1.0.2 h1:4ER/udB0+fMWB2Jlf15RV3F4A2FDuYi/9f+lFttR/Lg=
github.com/go-chi/render v1.0.2/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@ -712,8 +712,6 @@ github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY9
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/goccy/go-json v0.7.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
@ -863,8 +861,8 @@ github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0
github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
github.com/googleapis/gax-go/v2 v2.6.0 h1:SXk3ABtQYDT/OH8jAyvEOQ58mgawq5C4o/4/89qN2ZU=
github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=
github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ=
github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=
github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
@ -925,8 +923,8 @@ github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-plugin v1.4.5 h1:oTE/oQR4eghggRg8VY7PAz3dr++VwDNBGCcOfIvHpBo=
github.com/hashicorp/go-plugin v1.4.5/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
github.com/hashicorp/go-plugin v1.4.6 h1:MDV3UrKQBM3du3G7MApDGvOsMYy3JQJ4exhSoKBAeVA=
github.com/hashicorp/go-plugin v1.4.6/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
@ -1015,8 +1013,8 @@ github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQ
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.16.0/go.mod h1:N0A9sFdWzkw/Jy1lwoiB64F2+ugFZi987zRxcPez/wI=
github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ=
github.com/jackc/pgx/v5 v5.0.4 h1:r5O6y84qHX/z/HZV40JBdx2obsHz7/uRj5b+CcYEdeY=
github.com/jackc/pgx/v5 v5.0.4/go.mod h1:U0ynklHtgg43fue9Ly30w3OCSTDPlXjig9ghrNGaguQ=
github.com/jackc/pgx/v5 v5.1.1 h1:pZD79K1SYv8wc2HmCQA6VdmRQi7/OtCfv9bM3WAXUYA=
github.com/jackc/pgx/v5 v5.1.1/go.mod h1:Ptn7zmohNsWEsdxRawMzk3gaKma2obW+NWTnKa0S4nk=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
@ -1065,8 +1063,8 @@ github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47e
github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM=
github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.1.2 h1:XhdX4fqAJUA0yj+kUwMavO0hHrSPAecYdYf1ZmxHvak=
github.com/klauspost/cpuid/v2 v2.1.2/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/klauspost/cpuid/v2 v2.2.1 h1:U33DW0aiEj633gHYw3LoDNfkDiYnE5Q8M/TKJn2f2jI=
github.com/klauspost/cpuid/v2 v2.2.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -1089,21 +1087,16 @@ github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LE
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A=
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/codegen v1.0.1/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM=
github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx v1.2.6/go.mod h1:tJuGuAI3LC71IicTx82Mz1n3w9woAs2bYJZpkjJQ5aU=
github.com/lestrrat-go/jwx v1.2.25 h1:tAx93jN2SdPvFn08fHNAhqFJazn5mBBOB8Zli0g0otA=
github.com/lestrrat-go/jwx v1.2.25/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY=
github.com/lestrrat-go/jwx/v2 v2.0.8 h1:jCFT8oc0hEDVjgUgsBy1F9cbjsjAVZSXNi7JaU9HR/Q=
github.com/lestrrat-go/jwx/v2 v2.0.8/go.mod h1:zLxnyv9rTlEvOUHbc48FAfIL8iYu2hHvIRaTFGc8mT0=
github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@ -1307,13 +1300,13 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxS
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE=
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
github.com/otiai10/copy v1.9.0 h1:7KFNiCgZ91Ru4qW4CWPf/7jqtxLagGRmIxWldPP9VY4=
github.com/otiai10/copy v1.9.0/go.mod h1:hsfX19wcn0UWIHUQ3/4fHuehhk2UyArQ9dVFAn3FczI=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.3 h1:7JgpsBaN0uMkyju4tbYHu0mnM55hNKVYLsXmwr15NQI=
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/otiai10/mint v1.4.0 h1:umwcf7gbpEwf7WFzqmWwSv0CzbeMsae2u9ZvpP8j2q4=
github.com/otiai10/mint v1.4.0/go.mod h1:gifjb2MYOoULtKLqUAEILUG/9KONW6f7YsJ6vQLTlFI=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
@ -1324,8 +1317,8 @@ github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrap
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
@ -1370,8 +1363,8 @@ github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU=
github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ=
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
@ -1458,8 +1451,8 @@ github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
github.com/sftpgo/sdk v0.1.2 h1:j4V63RuVcYfJAOWV0zRUofa1PlQvKU2ujly0lB7quVA=
github.com/sftpgo/sdk v0.1.2/go.mod h1:PTp1TfXa+95wHw9yuZu7BA3vmzLqbRkz3gBmMNnwFQg=
github.com/shirou/gopsutil/v3 v3.22.9 h1:yibtJhIVEMcdw+tCTbOPiF1VcsuDeTE4utJ8Dm4c5eA=
github.com/shirou/gopsutil/v3 v3.22.9/go.mod h1:bBYl1kjgEJpWpxeHmLI+dVHWtyAwfcmSBLDsp2TNT8A=
github.com/shirou/gopsutil/v3 v3.22.10 h1:4KMHdfBRYXGF9skjDWiL4RA2N+E8dRdodU/bOZpPoVg=
github.com/shirou/gopsutil/v3 v3.22.10/go.mod h1:QNza6r4YQoydyCfo6rH0blGfKahgibh4dQmV5xdFkQk=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
@ -1486,8 +1479,8 @@ github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw=
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
@ -1509,8 +1502,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU=
github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw=
github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU=
github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As=
github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
@ -1535,8 +1528,8 @@ github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/studio-b12/gowebdav v0.0.0-20221015232716-17255f2e7423 h1:Wd8WDEEusB5+En4PiRWJp1cP59QLNsQun+mOTW8+s6s=
github.com/studio-b12/gowebdav v0.0.0-20221015232716-17255f2e7423/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/studio-b12/gowebdav v0.0.0-20221109171924-60ec5ad56012 h1:ZC+dlnsjxqrcB68nEFbIEfo4iXsog3Sg8FlXKytAjhY=
github.com/studio-b12/gowebdav v0.0.0-20221109171924-60ec5ad56012/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
@ -1546,11 +1539,12 @@ github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG
github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw=
github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk=
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ=
github.com/tklauser/numcpus v0.5.0 h1:ooe7gN0fg6myJ0EKoTAf5hebTZrH52px3New/D9iJ+A=
github.com/tklauser/numcpus v0.5.0/go.mod h1:OGzpTxpcIMNGYQdit2BYL1pvk/dSOaJWjKoflh+RQjo=
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@ -1586,8 +1580,8 @@ github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
github.com/xhit/go-simple-mail/v2 v2.12.0 h1:KweA6NO8Z6fZyeckMPNpvElU6QDIyBShlpce1sYUZgg=
github.com/xhit/go-simple-mail/v2 v2.12.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
github.com/xhit/go-simple-mail/v2 v2.13.0 h1:OANWU9jHZrVfBkNkvLf8Ww0fexwpQVF/v/5f96fFTLI=
github.com/xhit/go-simple-mail/v2 v2.13.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
@ -1634,8 +1628,9 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0/go.mod h1:vEhqr0m4eTc+DWxfsXoXue2GBgV2uUwVznkGIHW/e5w=
@ -1743,8 +1738,86 @@ golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220802222814-0bcc04d9c69b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -1771,8 +1844,8 @@ golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7Lm
golang.org/x/oauth2 v0.0.0-20220628200809-02e64fa58f26/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.1.0 h1:isLCZuhj4v+tYv7eskaN4v/TM+A1begWWgyVJDdl1+Y=
golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A=
golang.org/x/oauth2 v0.2.0 h1:GtQkldQ9m7yvzCL1V+LrYow3Khe0eJH0w7RbX/VbaIU=
golang.org/x/oauth2 v0.2.0/go.mod h1:Cwn6afJ8jrQwYMxQDTpISoXmXW9I6qF6vDeuuoX3Ibs=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -1862,6 +1935,7 @@ golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -1876,9 +1950,13 @@ golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -1923,18 +2001,20 @@ golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1958,8 +2038,8 @@ golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE=
golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -2027,7 +2107,6 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
@ -2035,7 +2114,6 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
@ -2047,8 +2125,8 @@ golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -2108,8 +2186,8 @@ google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6F
google.golang.org/api v0.86.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
google.golang.org/api v0.91.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
google.golang.org/api v0.101.0 h1:lJPPeEBIRxGpGLwnBTam1NPEM8Z2BmmXEd3z812pjwM=
google.golang.org/api v0.101.0/go.mod h1:CjxAAWWt3A3VrUE2IGDY2bgK5qhoG/OkyWVlYcP05MY=
google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ=
google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -2220,8 +2298,8 @@ google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljW
google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc=
google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71 h1:GEgb2jF5zxsFJpJfg9RoDDWm7tiwc/DDSTE2BtLUkXU=
google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s=
google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 h1:a2S6M0+660BgMNl++4JPlcAO/CjkqYItDEZwkoDQK7c=
google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@ -2263,8 +2341,8 @@ google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACu
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY=
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/grpc v1.51.0 h1:E1eGv1FTqoLIdnBCZufiSHgKjlqG6fKFf6pPWtMTh8U=
google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=

View file

@ -65,6 +65,12 @@ Please take a look at the usage below to customize the options.`,
logger.ErrorToConsole("Unable to initialize KMS: %v", err)
os.Exit(1)
}
mfaConfig := config.GetMFAConfig()
err = mfaConfig.Initialize()
if err != nil {
logger.ErrorToConsole("Unable to initialize MFA: %v", err)
os.Exit(1)
}
providerConf := config.GetProviderConf()
// ignore actions
providerConf.Actions.Hook = ""

View file

@ -309,7 +309,7 @@ func (c *BaseConnection) ListDir(virtualPath string) ([]os.FileInfo, error) {
// CheckParentDirs tries to create the specified directory and any missing parent dirs
func (c *BaseConnection) CheckParentDirs(virtualPath string) error {
fs, err := c.User.GetFilesystemForPath(virtualPath, "")
fs, err := c.User.GetFilesystemForPath(virtualPath, c.GetID())
if err != nil {
return err
}
@ -321,7 +321,7 @@ func (c *BaseConnection) CheckParentDirs(virtualPath string) error {
}
dirs := util.GetDirsForVirtualPath(virtualPath)
for idx := len(dirs) - 1; idx >= 0; idx-- {
fs, err = c.User.GetFilesystemForPath(dirs[idx], "")
fs, err = c.User.GetFilesystemForPath(dirs[idx], c.GetID())
if err != nil {
return err
}
@ -743,7 +743,7 @@ func (c *BaseConnection) doStatInternal(virtualPath string, mode int, checkFileP
if _, ok := vfolders[virtualPath]; ok {
return vfs.NewFileInfo(virtualPath, true, 0, time.Unix(0, 0), false), nil
}
if checkFilePatterns {
if checkFilePatterns && virtualPath != "/" {
ok, policy := c.User.IsFileAllowed(virtualPath)
if !ok && policy == sdk.DenyPolicyHide {
return nil, c.GetNotExistError()
@ -1509,6 +1509,7 @@ func (c *BaseConnection) GetGenericError(err error) error {
err == ErrQuotaExceeded || err == vfs.ErrStorageSizeUnavailable || err == ErrShuttingDown {
return err
}
c.Log(logger.LevelError, "generic error: %+v", err)
return ErrGenericFailure
}
}
@ -1536,7 +1537,7 @@ func (c *BaseConnection) GetFsAndResolvedPath(virtualPath string) (vfs.Fs, strin
// will not be listed
return nil, "", c.GetPermissionDeniedError()
}
return nil, "", err
return nil, "", c.GetGenericError(err)
}
if isShuttingDown.Load() {

View file

@ -29,6 +29,7 @@ import (
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"strings"
"sync"
@ -476,6 +477,21 @@ func (p *EventParams) AddError(err error) {
p.errors = append(p.errors, err.Error())
}
func (p *EventParams) setBackupParams(backupPath string) {
if p.sender != "" {
return
}
p.sender = dataprovider.ActionExecutorSystem
p.FsPath = backupPath
p.ObjectName = filepath.Base(backupPath)
p.VirtualPath = "/" + p.ObjectName
p.Timestamp = time.Now().UnixNano()
info, err := os.Stat(backupPath)
if err == nil {
p.FileSize = info.Size()
}
}
func (p *EventParams) getStatusString() string {
switch p.Status {
case 1:
@ -503,6 +519,18 @@ func (p *EventParams) getUsers() ([]dataprovider.User, error) {
}
func (p *EventParams) getUserFromSender() (dataprovider.User, error) {
if p.sender == dataprovider.ActionExecutorSystem {
return dataprovider.User{
BaseUser: sdk.BaseUser{
Status: 1,
Username: p.sender,
HomeDir: dataprovider.GetBackupsPath(),
Permissions: map[string][]string{
"/": {dataprovider.PermAny},
},
},
}, nil
}
user, err := dataprovider.UserExists(p.sender)
if err != nil {
eventManagerLog(logger.LevelError, "unable to get user %q: %+v", p.sender, err)
@ -585,6 +613,13 @@ func (p *EventParams) getStringReplacements(addObjectData bool) []string {
"{{Timestamp}}", fmt.Sprintf("%d", p.Timestamp),
"{{StatusString}}", p.getStatusString(),
}
if p.VirtualPath != "" {
replacements = append(replacements, "{{VirtualDirPath}}", path.Dir(p.VirtualPath))
}
if p.VirtualTargetPath != "" {
replacements = append(replacements, "{{VirtualTargetDirPath}}", path.Dir(p.VirtualTargetPath))
replacements = append(replacements, "{{TargetName}}", path.Base(p.VirtualTargetPath))
}
if len(p.errors) > 0 {
replacements = append(replacements, "{{ErrorString}}", strings.Join(p.errors, ", "))
} else {
@ -1903,7 +1938,11 @@ func executeRuleAction(action dataprovider.BaseEventAction, params *EventParams,
case dataprovider.ActionTypeEmail:
err = executeEmailRuleAction(action.Options.EmailConfig, params)
case dataprovider.ActionTypeBackup:
err = dataprovider.ExecuteBackup()
var backupPath string
backupPath, err = dataprovider.ExecuteBackup()
if err == nil {
params.setBackupParams(backupPath)
}
case dataprovider.ActionTypeUserQuotaReset:
err = executeUsersQuotaResetRuleAction(conditions, params)
case dataprovider.ActionTypeFolderQuotaReset:

View file

@ -951,6 +951,74 @@ func TestHiddenPatternFilter(t *testing.T) {
assert.NoError(t, err)
}
func TestHiddenRoot(t *testing.T) {
// only the "/ftp" directory is allowed and visibile in the "/" path
// within /ftp any file/directory is allowed and visibile
u := getTestUser()
u.Filters.FilePatterns = []sdk.PatternsFilter{
{
Path: "/",
AllowedPatterns: []string{"ftp"},
DenyPolicy: sdk.DenyPolicyHide,
},
{
Path: "/ftp",
AllowedPatterns: []string{"*"},
},
}
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
for i := 0; i < 10; i++ {
err = os.MkdirAll(filepath.Join(user.HomeDir, fmt.Sprintf("ftp%d", i)), os.ModePerm)
assert.NoError(t, err)
}
err = os.WriteFile(filepath.Join(user.HomeDir, testFileName), []byte(""), 0666)
assert.NoError(t, err)
err = os.WriteFile(filepath.Join(user.HomeDir, "ftp.txt"), []byte(""), 0666)
assert.NoError(t, err)
conn, client, err := getSftpClient(user)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
err = client.Mkdir("ftp")
assert.NoError(t, err)
entries, err := client.ReadDir("/")
assert.NoError(t, err)
if assert.Len(t, entries, 1) {
assert.Equal(t, "ftp", entries[0].Name())
}
_, err = client.Stat(".")
assert.NoError(t, err)
for _, name := range []string{testFileName, "ftp.txt"} {
_, err = client.Stat(name)
assert.ErrorIs(t, err, os.ErrNotExist)
}
for i := 0; i < 10; i++ {
_, err = client.Stat(fmt.Sprintf("ftp%d", i))
assert.ErrorIs(t, err, os.ErrNotExist)
}
err = writeSFTPFile(testFileName, 4096, client)
assert.ErrorIs(t, err, os.ErrPermission)
err = writeSFTPFile("ftp123", 4096, client)
assert.ErrorIs(t, err, os.ErrPermission)
err = client.Rename(testFileName, testFileName+"_rename")
assert.ErrorIs(t, err, os.ErrPermission)
err = writeSFTPFile(path.Join("/ftp", testFileName), 4096, client)
assert.NoError(t, err)
err = client.Mkdir("/ftp/dir")
assert.NoError(t, err)
err = client.Rename(path.Join("/ftp", testFileName), path.Join("/ftp/dir", testFileName))
assert.NoError(t, err)
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestFileNotAllowedErrors(t *testing.T) {
deniedDir := "/denied"
u := getTestUser()
@ -3353,6 +3421,10 @@ func TestEventRule(t *testing.T) {
}
a2 := dataprovider.BaseEventAction{
Name: "action2",
Type: dataprovider.ActionTypeBackup,
}
a3 := dataprovider.BaseEventAction{
Name: "action3",
Type: dataprovider.ActionTypeEmail,
Options: dataprovider.BaseEventActionOptions{
EmailConfig: dataprovider.EventActionEmailConfig{
@ -3362,8 +3434,8 @@ func TestEventRule(t *testing.T) {
},
},
}
a3 := dataprovider.BaseEventAction{
Name: "action3",
a4 := dataprovider.BaseEventAction{
Name: "action4",
Type: dataprovider.ActionTypeEmail,
Options: dataprovider.BaseEventActionOptions{
EmailConfig: dataprovider.EventActionEmailConfig{
@ -3379,6 +3451,9 @@ func TestEventRule(t *testing.T) {
assert.NoError(t, err)
action3, _, err := httpdtest.AddEventAction(a3, http.StatusCreated)
assert.NoError(t, err)
action4, _, err := httpdtest.AddEventAction(a4, http.StatusCreated)
assert.NoError(t, err)
r1 := dataprovider.EventRule{
Name: "test rule1",
Trigger: dataprovider.EventTriggerFsEvent,
@ -3417,6 +3492,12 @@ func TestEventRule(t *testing.T) {
Name: action3.Name,
},
Order: 3,
},
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action4.Name,
},
Order: 4,
Options: dataprovider.EventActionOptions{
IsFailureAction: true,
},
@ -3442,13 +3523,13 @@ func TestEventRule(t *testing.T) {
Actions: []dataprovider.EventAction{
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action2.Name,
Name: action3.Name,
},
Order: 1,
},
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action3.Name,
Name: action4.Name,
},
Order: 2,
Options: dataprovider.EventActionOptions{
@ -3469,7 +3550,7 @@ func TestEventRule(t *testing.T) {
Actions: []dataprovider.EventAction{
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action2.Name,
Name: action3.Name,
},
Order: 1,
},
@ -3645,6 +3726,8 @@ func TestEventRule(t *testing.T) {
assert.NoError(t, err)
_, err = httpdtest.RemoveEventAction(action3, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveEventAction(action4, http.StatusOK)
assert.NoError(t, err)
lastReceivedEmail.reset()
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
@ -3853,7 +3936,7 @@ func TestEventRuleFsActions(t *testing.T) {
Type: dataprovider.FilesystemActionRename,
Renames: []dataprovider.KeyValue{
{
Key: "/{{VirtualPath}}",
Key: "/{{VirtualDirPath}}/{{ObjectName}}",
Value: "/{{ObjectName}}_renamed",
},
},
@ -4237,6 +4320,89 @@ func TestEventFsActionsGroupFilters(t *testing.T) {
require.NoError(t, err)
}
func TestBackupAsAttachment(t *testing.T) {
smtpCfg := smtp.Config{
Host: "127.0.0.1",
Port: 2525,
From: "notification@example.com",
TemplatesPath: "templates",
}
err := smtpCfg.Initialize(configDir)
require.NoError(t, err)
a1 := dataprovider.BaseEventAction{
Name: "a1",
Type: dataprovider.ActionTypeBackup,
}
a2 := dataprovider.BaseEventAction{
Name: "a2",
Type: dataprovider.ActionTypeEmail,
Options: dataprovider.BaseEventActionOptions{
EmailConfig: dataprovider.EventActionEmailConfig{
Recipients: []string{"test@example.com"},
Subject: `"{{Event}} {{StatusString}}"`,
Body: "Domain: {{Name}}",
Attachments: []string{"/{{VirtualPath}}"},
},
},
}
action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)
assert.NoError(t, err)
action2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)
assert.NoError(t, err)
r1 := dataprovider.EventRule{
Name: "test rule certificate",
Trigger: dataprovider.EventTriggerCertificate,
Actions: []dataprovider.EventAction{
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action1.Name,
},
Order: 1,
},
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action2.Name,
},
Order: 2,
},
},
}
rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)
assert.NoError(t, err)
lastReceivedEmail.reset()
renewalEvent := "Certificate renewal"
common.HandleCertificateEvent(common.EventParams{
Name: "example.com",
Timestamp: time.Now().UnixNano(),
Status: 1,
Event: renewalEvent,
})
assert.Eventually(t, func() bool {
return lastReceivedEmail.get().From != ""
}, 3000*time.Millisecond, 100*time.Millisecond)
email := lastReceivedEmail.get()
assert.Len(t, email.To, 1)
assert.True(t, util.Contains(email.To, "test@example.com"))
assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, renewalEvent))
assert.Contains(t, email.Data, `Domain: example.com`)
assert.Contains(t, email.Data, "Content-Type: application/json")
_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)
assert.NoError(t, err)
smtpCfg = smtp.Config{}
err = smtpCfg.Initialize(configDir)
require.NoError(t, err)
}
func TestEventActionHTTPMultipart(t *testing.T) {
a1 := dataprovider.BaseEventAction{
Name: "action1",
@ -5196,6 +5362,86 @@ func TestEventRuleFirstUploadDownloadActions(t *testing.T) {
require.NoError(t, err)
}
func TestEventRuleRenameEvent(t *testing.T) {
smtpCfg := smtp.Config{
Host: "127.0.0.1",
Port: 2525,
From: "notify@example.com",
TemplatesPath: "templates",
}
err := smtpCfg.Initialize(configDir)
require.NoError(t, err)
a1 := dataprovider.BaseEventAction{
Name: "action1",
Type: dataprovider.ActionTypeEmail,
Options: dataprovider.BaseEventActionOptions{
EmailConfig: dataprovider.EventActionEmailConfig{
Recipients: []string{"test@example.com"},
Subject: `"{{Event}}" from "{{Name}}"`,
Body: `Fs path {{FsPath}}, Target path "{{VirtualTargetDirPath}}/{{TargetName}}", size: {{FileSize}}`,
},
},
}
action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)
assert.NoError(t, err)
r1 := dataprovider.EventRule{
Name: "test rename rule",
Trigger: dataprovider.EventTriggerFsEvent,
Conditions: dataprovider.EventConditions{
FsEvents: []string{"rename"},
},
Actions: []dataprovider.EventAction{
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action1.Name,
},
Order: 1,
},
},
}
rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)
assert.NoError(t, err)
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
conn, client, err := getSftpClient(user)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
testFileSize := int64(32768)
lastReceivedEmail.reset()
err = writeSFTPFileNoCheck(testFileName, testFileSize, client)
assert.NoError(t, err)
err = client.Mkdir("subdir")
assert.NoError(t, err)
err = client.Rename(testFileName, path.Join("/subdir", testFileName))
assert.NoError(t, err)
assert.Eventually(t, func() bool {
return lastReceivedEmail.get().From != ""
}, 1500*time.Millisecond, 100*time.Millisecond)
email := lastReceivedEmail.get()
assert.Len(t, email.To, 1)
assert.True(t, util.Contains(email.To, "test@example.com"))
assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "rename" from "%s"`, user.Username))
assert.Contains(t, email.Data, fmt.Sprintf("Target path %q", path.Join("/subdir", testFileName)))
}
_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
smtpCfg = smtp.Config{}
err = smtpCfg.Initialize(configDir)
require.NoError(t, err)
}
func TestEventRuleCertificate(t *testing.T) {
smtpCfg := smtp.Config{
Host: "127.0.0.1",
@ -6141,14 +6387,10 @@ func TestSFTPLoopError(t *testing.T) {
conn = common.NewBaseConnection("", common.ProtocolSFTP, "", "", user1)
_, _, err = conn.GetFsAndResolvedPath(user1.VirtualFolders[0].VirtualPath)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "SFTP loop")
}
assert.Error(t, err)
conn = common.NewBaseConnection("", common.ProtocolFTP, "", "", user1)
_, _, err = conn.GetFsAndResolvedPath(user1.VirtualFolders[0].VirtualPath)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "SFTP loop")
}
assert.Error(t, err)
_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
assert.NoError(t, err)

View file

@ -253,6 +253,7 @@ func Init() {
HostKeys: []string{},
HostCertificates: []string{},
HostKeyAlgorithms: []string{},
Moduli: []string{},
KexAlgorithms: []string{},
Ciphers: []string{},
MACs: []string{},
@ -1961,6 +1962,7 @@ func setViperDefaults() {
viper.SetDefault("sftpd.host_keys", globalConf.SFTPD.HostKeys)
viper.SetDefault("sftpd.host_certificates", globalConf.SFTPD.HostCertificates)
viper.SetDefault("sftpd.host_key_algorithms", globalConf.SFTPD.HostKeyAlgorithms)
viper.SetDefault("sftpd.moduli", globalConf.SFTPD.Moduli)
viper.SetDefault("sftpd.kex_algorithms", globalConf.SFTPD.KexAlgorithms)
viper.SetDefault("sftpd.ciphers", globalConf.SFTPD.Ciphers)
viper.SetDefault("sftpd.macs", globalConf.SFTPD.MACs)

View file

@ -15,14 +15,13 @@
package dataprovider
import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net"
"os"
"sort"
"strconv"
"strings"
"github.com/alexedwards/argon2id"
@ -71,9 +70,9 @@ const (
var (
validAdminPerms = []string{PermAdminAny, PermAdminAddUsers, PermAdminChangeUsers, PermAdminDeleteUsers,
PermAdminViewUsers, PermAdminManageGroups, PermAdminViewConnections, PermAdminCloseConnections,
PermAdminViewServerStatus, PermAdminManageAdmins, PermAdminManageAPIKeys, PermAdminQuotaScans,
PermAdminManageSystem, PermAdminManageDefender, PermAdminViewDefender, PermAdminRetentionChecks,
PermAdminMetadataChecks, PermAdminViewEvents}
PermAdminViewServerStatus, PermAdminManageAdmins, PermAdminManageEventRules, PermAdminManageAPIKeys,
PermAdminQuotaScans, PermAdminManageSystem, PermAdminManageDefender, PermAdminViewDefender,
PermAdminRetentionChecks, PermAdminMetadataChecks, PermAdminViewEvents}
)
// AdminTOTPConfig defines the time-based one time password configuration
@ -548,12 +547,9 @@ func (a *Admin) CanManageMFA() bool {
}
// GetSignature returns a signature for this admin.
// It could change after an update
// It will 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[:])
return strconv.FormatInt(a.UpdatedAt, 10)
}
func (a *Admin) getACopy() Admin {

View file

@ -18,7 +18,7 @@ import (
"sync"
"time"
"golang.org/x/net/webdav"
"github.com/drakkan/webdav"
"github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/util"

View file

@ -524,36 +524,36 @@ func (c *Config) requireCustomTLSForMySQL() bool {
return false
}
func (c *Config) doBackup() error {
func (c *Config) doBackup() (string, error) {
now := time.Now().UTC()
outputFile := filepath.Join(c.BackupsPath, fmt.Sprintf("backup_%s_%d.json", now.Weekday(), now.Hour()))
providerLog(logger.LevelDebug, "starting backup to file %q", outputFile)
err := os.MkdirAll(filepath.Dir(outputFile), 0700)
if err != nil {
providerLog(logger.LevelError, "unable to create backup dir %q: %v", outputFile, err)
return fmt.Errorf("unable to create backup dir: %w", err)
return outputFile, fmt.Errorf("unable to create backup dir: %w", err)
}
backup, err := DumpData()
if err != nil {
providerLog(logger.LevelError, "unable to execute backup: %v", err)
return fmt.Errorf("unable to dump backup data: %w", err)
return outputFile, fmt.Errorf("unable to dump backup data: %w", err)
}
dump, err := json.Marshal(backup)
if err != nil {
providerLog(logger.LevelError, "unable to marshal backup as JSON: %v", err)
return fmt.Errorf("unable to marshal backup data as JSON: %w", err)
return outputFile, fmt.Errorf("unable to marshal backup data as JSON: %w", err)
}
err = os.WriteFile(outputFile, dump, 0600)
if err != nil {
providerLog(logger.LevelError, "unable to save backup: %v", err)
return fmt.Errorf("unable to save backup: %w", err)
return outputFile, fmt.Errorf("unable to save backup: %w", err)
}
providerLog(logger.LevelDebug, "backup saved to %q", outputFile)
return nil
return outputFile, nil
}
// ExecuteBackup executes a backup
func ExecuteBackup() error {
func ExecuteBackup() (string, error) {
return config.doBackup()
}
@ -833,6 +833,12 @@ func Initialize(cnf Config, basePath string, checkAdmins bool) error {
if cnf.BackupsPath == "" {
return fmt.Errorf("required directory is invalid, backup path %#v", cnf.BackupsPath)
}
absoluteBackupPath, err := util.GetAbsolutePath(cnf.BackupsPath)
if err != nil {
return fmt.Errorf("unable to get absolute backup path: %w", err)
}
config.BackupsPath = absoluteBackupPath
providerLog(logger.LevelDebug, "absolute backup path %q", config.BackupsPath)
if err := initializeHashingAlgo(&cnf); err != nil {
return err
@ -1866,10 +1872,6 @@ func GetUserVariants(username string) (User, User, error) {
// AddUser adds a new SFTPGo user.
func AddUser(user *User, executor, ipAddress string) error {
user.Filters.RecoveryCodes = nil
user.Filters.TOTPConfig = UserTOTPConfig{
Enabled: false,
}
user.Username = config.convertName(user.Username)
err := provider.addUser(user)
if err == nil {

View file

@ -1354,6 +1354,12 @@ func (r *EventRule) hasUserAssociated(providerObjectType string) bool {
return providerObjectType == actionObjectUser
case EventTriggerFsEvent:
return true
default:
if len(r.Actions) > 0 {
// should we allow schedules where backup is not the first action?
// maybe we could pass the action index and check before that index
return r.Actions[0].Type == ActionTypeBackup
}
}
return false
}

View file

@ -2423,8 +2423,11 @@ func (p *MemoryProvider) getNextRuleID() int64 {
func (p *MemoryProvider) clear() {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
p.dbHandle.usernames = []string{}
p.dbHandle.users = make(map[string]User)
p.dbHandle.groupnames = []string{}
p.dbHandle.groups = map[string]Group{}
p.dbHandle.vfoldersNames = []string{}
p.dbHandle.vfolders = make(map[string]vfs.BaseVirtualFolder)
p.dbHandle.admins = make(map[string]Admin)
@ -2433,6 +2436,10 @@ func (p *MemoryProvider) clear() {
p.dbHandle.apiKeysIDs = []string{}
p.dbHandle.shares = make(map[string]Share)
p.dbHandle.sharesIDs = []string{}
p.dbHandle.actions = map[string]BaseEventAction{}
p.dbHandle.actionsNames = []string{}
p.dbHandle.rules = map[string]EventRule{}
p.dbHandle.rulesNames = []string{}
}
func (p *MemoryProvider) reloadConfig() error {

View file

@ -25,8 +25,8 @@ import (
"strings"
"time"
"github.com/lestrrat-go/jwx/jwa"
"github.com/lestrrat-go/jwx/jwt"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/rs/xid"
"github.com/drakkan/sftpgo/v2/internal/httpclient"
@ -128,12 +128,9 @@ func (n *Node) authenticate(token string) (string, error) {
if token == "" {
return "", ErrInvalidCredentials
}
t, err := jwt.Parse([]byte(token), jwt.WithVerify(jwa.HS256, []byte(n.Data.Key.GetPayload())))
t, err := jwt.Parse([]byte(token), jwt.WithKey(jwa.HS256, []byte(n.Data.Key.GetPayload())), jwt.WithValidate(true))
if err != nil {
return "", fmt.Errorf("unable to parse token: %v", err)
}
if err := jwt.Validate(t); err != nil {
return "", fmt.Errorf("unable to validate token: %v", err)
return "", fmt.Errorf("unable to parse and validate token: %v", err)
}
if admin, ok := t.Get("admin"); ok {
if val, ok := admin.(string); ok && val != "" {
@ -169,7 +166,7 @@ func (n *Node) generateAuthToken(username string) (string, error) {
t.Set(jwt.NotBeforeKey, now.Add(-30*time.Second)) //nolint:errcheck
t.Set(jwt.ExpirationKey, now.Add(1*time.Minute)) //nolint:errcheck
payload, err := jwt.Sign(t, jwa.HS256, []byte(n.Data.Key.GetPayload()))
payload, err := jwt.Sign(t, jwt.WithKey(jwa.HS256, []byte(n.Data.Key.GetPayload())))
if err != nil {
return "", fmt.Errorf("unable to sign authentication token: %w", err)
}

View file

@ -987,6 +987,12 @@ func sqlCommonAddUser(user *User, dbHandle *sql.DB) error {
defer cancel()
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
if config.IsShared == 1 {
_, err := tx.ExecContext(ctx, getRemoveSoftDeletedUserQuery(), user.Username)
if err != nil {
return err
}
}
q := getAddUserQuery()
_, err := tx.ExecContext(ctx, q, user.Username, user.Password, string(publicKeys), user.HomeDir, user.UID, user.GID,
user.MaxSessions, user.QuotaSize, user.QuotaFiles, string(permissions), user.UploadBandwidth,
@ -3127,6 +3133,12 @@ func sqlCommonAddEventRule(rule *EventRule, dbHandle *sql.DB) error {
defer cancel()
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
if config.IsShared == 1 {
_, err := tx.ExecContext(ctx, getRemoveSoftDeletedRuleQuery(), rule.Name)
if err != nil {
return err
}
}
q := getAddEventRuleQuery()
_, err := tx.ExecContext(ctx, q, rule.Name, rule.Description, util.GetTimeAsMsSinceEpoch(time.Now()),
util.GetTimeAsMsSinceEpoch(time.Now()), rule.Trigger, string(conditions))

View file

@ -527,6 +527,10 @@ func getDeleteUserQuery(softDelete bool) string {
return fmt.Sprintf(`DELETE FROM %s WHERE id = %s`, sqlTableUsers, sqlPlaceholders[0])
}
func getRemoveSoftDeletedUserQuery() string {
return fmt.Sprintf(`DELETE FROM %s WHERE username = %s AND deleted_at > 0`, sqlTableUsers, sqlPlaceholders[0])
}
func getFolderByNameQuery() string {
return fmt.Sprintf(`SELECT %s FROM %s WHERE name = %s`, selectFolderFields, sqlTableFolders, sqlPlaceholders[0])
}
@ -891,6 +895,10 @@ func getDeleteEventRuleQuery(softDelete bool) string {
return fmt.Sprintf(`DELETE FROM %s WHERE name = %s`, sqlTableEventsRules, sqlPlaceholders[0])
}
func getRemoveSoftDeletedRuleQuery() string {
return fmt.Sprintf(`DELETE FROM %s WHERE name = %s AND deleted_at > 0`, sqlTableEventsRules, sqlPlaceholders[0])
}
func getClearRuleActionMappingQuery() string {
return fmt.Sprintf(`DELETE FROM %s WHERE rule_id = (SELECT id FROM %s WHERE name = %s)`, sqlTableRulesActionsMapping,
sqlTableEventsRules, sqlPlaceholders[0])

View file

@ -15,8 +15,6 @@
package dataprovider
import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
@ -24,9 +22,11 @@ import (
"net"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/rs/xid"
"github.com/sftpgo/sdk"
"github.com/drakkan/sftpgo/v2/internal/kms"
@ -600,7 +600,7 @@ func (u *User) GetVirtualFolderForPath(virtualPath string) (vfs.VirtualFolder, e
// CheckMetadataConsistency checks the consistency between the metadata stored
// in the configured metadata plugin and the filesystem
func (u *User) CheckMetadataConsistency() error {
fs, err := u.getRootFs("")
fs, err := u.getRootFs(xid.New().String())
if err != nil {
return err
}
@ -621,7 +621,7 @@ func (u *User) CheckMetadataConsistency() error {
// ScanQuota scans the user home dir and virtual folders, included in its quota,
// and returns the number of files and their size
func (u *User) ScanQuota() (int, int64, error) {
fs, err := u.getRootFs("")
fs, err := u.getRootFs(xid.New().String())
if err != nil {
return 0, 0, err
}
@ -1131,12 +1131,9 @@ func (u *User) MustSetSecondFactorForProtocol(protocol string) bool {
}
// GetSignature returns a signature for this admin.
// It could change after an update
// It will change after an update
func (u *User) GetSignature() string {
data := []byte(fmt.Sprintf("%v_%v_%v", u.Username, u.Status, u.ExpirationDate))
data = append(data, []byte(u.Password)...)
signature := sha256.Sum256(data)
return base64.StdEncoding.EncodeToString(signature[:])
return strconv.FormatInt(u.UpdatedAt, 10)
}
// GetBandwidthForIP returns the upload and download bandwidth for the specified IP

View file

@ -81,6 +81,10 @@ func addUser(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
user.Filters.RecoveryCodes = nil
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
Enabled: false,
}
err = dataprovider.AddUser(&user, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))

View file

@ -21,7 +21,7 @@ import (
"time"
"github.com/go-chi/jwtauth/v5"
"github.com/lestrrat-go/jwx/jwt"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/rs/xid"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"

View file

@ -32,7 +32,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/jwtauth/v5"
"github.com/lestrrat-go/jwx/jwa"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/drakkan/sftpgo/v2/internal/common"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"

View file

@ -4516,7 +4516,7 @@ func TestUserSFTPFs(t *testing.T) {
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
user.FsConfig.Provider = sdk.SFTPFilesystemProvider
user.FsConfig.SFTPConfig.Endpoint = "127.0.0.1" // missing port
user.FsConfig.SFTPConfig.Endpoint = "[::1]:22:22" // invalid endpoint
user.FsConfig.SFTPConfig.Username = "sftp_user"
user.FsConfig.SFTPConfig.Password = kms.NewPlainSecret("sftp_pwd")
user.FsConfig.SFTPConfig.PrivateKey = kms.NewPlainSecret(sftpPrivateKey)
@ -4527,6 +4527,13 @@ func TestUserSFTPFs(t *testing.T) {
assert.NoError(t, err)
assert.Contains(t, string(resp), "invalid endpoint")
user.FsConfig.SFTPConfig.Endpoint = "127.0.0.1"
_, _, err = httpdtest.UpdateUser(user, http.StatusBadRequest, "")
assert.Error(t, err)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, "127.0.0.1:22", user.FsConfig.SFTPConfig.Endpoint)
user.FsConfig.SFTPConfig.Endpoint = "127.0.0.1:2022"
user.FsConfig.SFTPConfig.DisableCouncurrentReads = true
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")

View file

@ -40,8 +40,8 @@ import (
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/jwtauth/v5"
"github.com/klauspost/compress/zip"
"github.com/lestrrat-go/jwx/jwa"
"github.com/lestrrat-go/jwx/jwt"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/rs/xid"
"github.com/sftpgo/sdk"
"github.com/stretchr/testify/assert"

View file

@ -21,7 +21,7 @@ import (
"strings"
"github.com/go-chi/jwtauth/v5"
"github.com/lestrrat-go/jwx/jwt"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/rs/xid"
"github.com/sftpgo/sdk"

View file

@ -32,7 +32,7 @@ import (
"github.com/coreos/go-oidc/v3/oidc"
"github.com/go-chi/jwtauth/v5"
"github.com/lestrrat-go/jwx/jwa"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/rs/xid"
"github.com/sftpgo/sdk"
"github.com/stretchr/testify/assert"

View file

@ -31,7 +31,7 @@ import (
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/jwtauth/v5"
"github.com/go-chi/render"
"github.com/lestrrat-go/jwx/jwa"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/rs/cors"
"github.com/rs/xid"
"github.com/sftpgo/sdk"

View file

@ -1366,14 +1366,14 @@ func getSecretFromFormField(r *http.Request, field string) *kms.Secret {
func getS3Config(r *http.Request) (vfs.S3FsConfig, error) {
var err error
config := vfs.S3FsConfig{}
config.Bucket = r.Form.Get("s3_bucket")
config.Region = r.Form.Get("s3_region")
config.AccessKey = r.Form.Get("s3_access_key")
config.RoleARN = r.Form.Get("s3_role_arn")
config.Bucket = strings.TrimSpace(r.Form.Get("s3_bucket"))
config.Region = strings.TrimSpace(r.Form.Get("s3_region"))
config.AccessKey = strings.TrimSpace(r.Form.Get("s3_access_key"))
config.RoleARN = strings.TrimSpace(r.Form.Get("s3_role_arn"))
config.AccessSecret = getSecretFromFormField(r, "s3_access_secret")
config.Endpoint = r.Form.Get("s3_endpoint")
config.StorageClass = r.Form.Get("s3_storage_class")
config.ACL = r.Form.Get("s3_acl")
config.Endpoint = strings.TrimSpace(r.Form.Get("s3_endpoint"))
config.StorageClass = strings.TrimSpace(r.Form.Get("s3_storage_class"))
config.ACL = strings.TrimSpace(r.Form.Get("s3_acl"))
config.KeyPrefix = r.Form.Get("s3_key_prefix")
config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("s3_upload_part_size"), 10, 64)
if err != nil {
@ -1407,9 +1407,9 @@ func getGCSConfig(r *http.Request) (vfs.GCSFsConfig, error) {
var err error
config := vfs.GCSFsConfig{}
config.Bucket = r.Form.Get("gcs_bucket")
config.StorageClass = r.Form.Get("gcs_storage_class")
config.ACL = r.Form.Get("gcs_acl")
config.Bucket = strings.TrimSpace(r.Form.Get("gcs_bucket"))
config.StorageClass = strings.TrimSpace(r.Form.Get("gcs_storage_class"))
config.ACL = strings.TrimSpace(r.Form.Get("gcs_acl"))
config.KeyPrefix = r.Form.Get("gcs_key_prefix")
autoCredentials := r.Form.Get("gcs_auto_credentials")
if autoCredentials != "" {
@ -1440,7 +1440,7 @@ func getGCSConfig(r *http.Request) (vfs.GCSFsConfig, error) {
func getSFTPConfig(r *http.Request) (vfs.SFTPFsConfig, error) {
var err error
config := vfs.SFTPFsConfig{}
config.Endpoint = r.Form.Get("sftp_endpoint")
config.Endpoint = strings.TrimSpace(r.Form.Get("sftp_endpoint"))
config.Username = r.Form.Get("sftp_username")
config.Password = getSecretFromFormField(r, "sftp_password")
config.PrivateKey = getSecretFromFormField(r, "sftp_private_key")
@ -1463,7 +1463,7 @@ func getSFTPConfig(r *http.Request) (vfs.SFTPFsConfig, error) {
func getHTTPFsConfig(r *http.Request) vfs.HTTPFsConfig {
config := vfs.HTTPFsConfig{}
config.Endpoint = r.Form.Get("http_endpoint")
config.Endpoint = strings.TrimSpace(r.Form.Get("http_endpoint"))
config.Username = r.Form.Get("http_username")
config.SkipTLSVerify = r.Form.Get("http_skip_tls_verify") != ""
config.Password = getSecretFromFormField(r, "http_password")
@ -1479,13 +1479,13 @@ func getHTTPFsConfig(r *http.Request) vfs.HTTPFsConfig {
func getAzureConfig(r *http.Request) (vfs.AzBlobFsConfig, error) {
var err error
config := vfs.AzBlobFsConfig{}
config.Container = r.Form.Get("az_container")
config.AccountName = r.Form.Get("az_account_name")
config.Container = strings.TrimSpace(r.Form.Get("az_container"))
config.AccountName = strings.TrimSpace(r.Form.Get("az_account_name"))
config.AccountKey = getSecretFromFormField(r, "az_account_key")
config.SASURL = getSecretFromFormField(r, "az_sas_url")
config.Endpoint = r.Form.Get("az_endpoint")
config.Endpoint = strings.TrimSpace(r.Form.Get("az_endpoint"))
config.KeyPrefix = r.Form.Get("az_key_prefix")
config.AccessTier = r.Form.Get("az_access_tier")
config.AccessTier = strings.TrimSpace(r.Form.Get("az_access_tier"))
config.UseEmulator = r.Form.Get("az_use_emulator") != ""
config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("az_upload_part_size"), 10, 64)
if err != nil {
@ -2773,6 +2773,10 @@ func (s *httpdServer) handleWebAddUserPost(w http.ResponseWriter, r *http.Reques
Password: user.Password,
PublicKeys: user.PublicKeys,
})
user.Filters.RecoveryCodes = nil
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
Enabled: false,
}
err = dataprovider.AddUser(&user, claims.Username, ipAddr)
if err != nil {
s.renderUserPage(w, r, &user, userPageModeAdd, err.Error())

View file

@ -15,7 +15,6 @@
package plugin
import (
"crypto/sha256"
"errors"
"fmt"
"os/exec"
@ -113,10 +112,9 @@ func (p *authPlugin) initialize() error {
return fmt.Errorf("invalid options for auth plugin %#v: %v", p.config.Cmd, err)
}
var secureConfig *plugin.SecureConfig
if p.config.SHA256Sum != "" {
secureConfig.Checksum = []byte(p.config.SHA256Sum)
secureConfig.Hash = sha256.New()
secureConfig, err := p.config.getSecureConfig()
if err != nil {
return err
}
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: auth.Handshake,

View file

@ -15,7 +15,6 @@
package plugin
import (
"crypto/sha256"
"fmt"
"os/exec"
@ -54,10 +53,9 @@ func (p *ipFilterPlugin) cleanup() {
func (p *ipFilterPlugin) initialize() error {
logger.Debug(logSender, "", "create new IP filter plugin %#v", p.config.Cmd)
killProcess(p.config.Cmd)
var secureConfig *plugin.SecureConfig
if p.config.SHA256Sum != "" {
secureConfig.Checksum = []byte(p.config.SHA256Sum)
secureConfig.Hash = sha256.New()
secureConfig, err := p.config.getSecureConfig()
if err != nil {
return err
}
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: ipfilter.Handshake,

View file

@ -15,7 +15,6 @@
package plugin
import (
"crypto/sha256"
"fmt"
"os/exec"
"path/filepath"
@ -75,10 +74,9 @@ func (p *kmsPlugin) initialize() error {
if err := p.config.KMSOptions.validate(); err != nil {
return fmt.Errorf("invalid options for kms plugin %#v: %v", p.config.Cmd, err)
}
var secureConfig *plugin.SecureConfig
if p.config.SHA256Sum != "" {
secureConfig.Checksum = []byte(p.config.SHA256Sum)
secureConfig.Hash = sha256.New()
secureConfig, err := p.config.getSecureConfig()
if err != nil {
return err
}
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: kmsplugin.Handshake,

View file

@ -15,7 +15,6 @@
package plugin
import (
"crypto/sha256"
"fmt"
"os/exec"
@ -54,10 +53,9 @@ func (p *metadataPlugin) cleanup() {
func (p *metadataPlugin) initialize() error {
killProcess(p.config.Cmd)
logger.Debug(logSender, "", "create new metadata plugin %#v", p.config.Cmd)
var secureConfig *plugin.SecureConfig
if p.config.SHA256Sum != "" {
secureConfig.Checksum = []byte(p.config.SHA256Sum)
secureConfig.Hash = sha256.New()
secureConfig, err := p.config.getSecureConfig()
if err != nil {
return err
}
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: metadata.Handshake,

View file

@ -15,7 +15,6 @@
package plugin
import (
"crypto/sha256"
"fmt"
"os/exec"
"sync"
@ -138,10 +137,9 @@ func (p *notifierPlugin) initialize() error {
if !p.config.NotifierOptions.hasActions() {
return fmt.Errorf("no actions defined for the notifier plugin %#v", p.config.Cmd)
}
var secureConfig *plugin.SecureConfig
if p.config.SHA256Sum != "" {
secureConfig.Checksum = []byte(p.config.SHA256Sum)
secureConfig.Hash = sha256.New()
secureConfig, err := p.config.getSecureConfig()
if err != nil {
return err
}
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: notifier.Handshake,

View file

@ -16,7 +16,9 @@
package plugin
import (
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"errors"
"fmt"
"sync"
@ -24,6 +26,7 @@ import (
"time"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin"
"github.com/sftpgo/sdk/plugin/auth"
"github.com/sftpgo/sdk/plugin/eventsearcher"
"github.com/sftpgo/sdk/plugin/ipfilter"
@ -82,6 +85,20 @@ type Config struct {
kmsID int
}
func (c *Config) getSecureConfig() (*plugin.SecureConfig, error) {
if c.SHA256Sum != "" {
checksum, err := hex.DecodeString(c.SHA256Sum)
if err != nil {
return nil, fmt.Errorf("invalid sha256 hash %q: %w", c.SHA256Sum, err)
}
return &plugin.SecureConfig{
Checksum: checksum,
Hash: sha256.New(),
}, nil
}
return nil, nil
}
func (c *Config) newKMSPluginSecretProvider(base kms.BaseSecret, url, masterKey string) kms.SecretProvider {
return &kmsPluginSecretProvider{
BaseSecret: base,
@ -774,16 +791,17 @@ func setLogLevel(logLevel string) {
func startCheckTicker() {
logger.Debug(logSender, "", "start plugins checker")
checker := time.NewTicker(30 * time.Second)
go func() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-Handler.done:
logger.Debug(logSender, "", "handler done, stop plugins checker")
checker.Stop()
return
case <-checker.C:
case <-ticker.C:
Handler.checkCrashedPlugins()
}
}

View file

@ -15,7 +15,6 @@
package plugin
import (
"crypto/sha256"
"fmt"
"os/exec"
@ -54,10 +53,9 @@ func (p *searcherPlugin) cleanup() {
func (p *searcherPlugin) initialize() error {
killProcess(p.config.Cmd)
logger.Debug(logSender, "", "create new searcher plugin %#v", p.config.Cmd)
var secureConfig *plugin.SecureConfig
if p.config.SHA256Sum != "" {
secureConfig.Checksum = []byte(p.config.SHA256Sum)
secureConfig.Hash = sha256.New()
secureConfig, err := p.config.getSecureConfig()
if err != nil {
return err
}
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: eventsearcher.Handshake,

View file

@ -182,9 +182,6 @@ func (c *Connection) handleFilewrite(request *sftp.Request) (sftp.WriterAtReader
func (c *Connection) Filecmd(request *sftp.Request) error {
c.UpdateLastActivity()
c.Log(logger.LevelDebug, "new cmd, method: %v, sourcePath: %#v, targetPath: %#v", request.Method,
request.Filepath, request.Target)
switch request.Method {
case "Setstat":
return c.handleSFTPSetstat(request)

View file

@ -1926,6 +1926,30 @@ func TestSupportedSecurityOptions(t *testing.T) {
assert.Equal(t, supportedKexAlgos, serverConfig.KeyExchanges)
}
func TestLoadModuli(t *testing.T) {
dhGEXSha1 := "diffie-hellman-group-exchange-sha1"
dhGEXSha256 := "diffie-hellman-group-exchange-sha256"
c := Configuration{}
c.Moduli = []string{".", "missing file"}
err := c.loadModuli(configDir)
assert.Error(t, err)
assert.NotContains(t, supportedKexAlgos, dhGEXSha1)
assert.NotContains(t, supportedKexAlgos, dhGEXSha256)
assert.Len(t, supportedKexAlgos, 10)
moduli := []byte("20220414072358 2 6 100 2047 5 F19C2D09AD49978F8A0C1B84168A4011A26F9CD516815934764A319FDC5975FA514AAF11B747D8CA6B3919532BEFB68FA118079473895674F3770F71FBB742F176883841EB3DE679BEF53C6AFE437A662F228B03C1E34B5A0D3909F608CEAA16C1F8131DE11E67878EFD918A89205E5E4DE323054010CA4711F25D466BB7727A016DD3F9F53BDBCE093055A4F2497ADEFB5A2500F9C5C3B0BCD88C6489F4C1CBC7CFB67BA6EABA0195794E4188CE9060F431041AD52FB9BAC4DF7FA536F585FBE67746CD57BFAD67567E9706C24D95C49BE95B759657C6BB5151E2AEA32F4CD557C40298A5C402101520EE8AAB8DFEED6FFC11AAF8036D6345923CFB5D1B922F")
moduliFile := filepath.Join(os.TempDir(), "moduli")
err = os.WriteFile(moduliFile, moduli, 0600)
assert.NoError(t, err)
c.Moduli = []string{moduliFile}
err = c.loadModuli(configDir)
assert.NoError(t, err)
assert.Contains(t, supportedKexAlgos, dhGEXSha1)
assert.Contains(t, supportedKexAlgos, dhGEXSha256)
assert.Len(t, supportedKexAlgos, 12)
err = os.Remove(moduliFile)
assert.NoError(t, err)
}
func TestLoadHostKeys(t *testing.T) {
serverConfig := &ssh.ServerConfig{}
c := Configuration{}

View file

@ -141,6 +141,12 @@ type Configuration struct {
// HostKeyAlgorithms lists the public key algorithms that the server will accept for host
// key authentication.
HostKeyAlgorithms []string `json:"host_key_algorithms" mapstructure:"host_key_algorithms"`
// Diffie-Hellman moduli files.
// Each moduli file can be defined as a path relative to the configuration directory or an absolute one.
// If set, "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
Moduli []string `json:"moduli" mapstructure:"moduli"`
// KexAlgorithms specifies the available KEX (Key Exchange) algorithms in
// preference order.
KexAlgorithms []string `json:"kex_algorithms" mapstructure:"kex_algorithms"`
@ -294,6 +300,10 @@ func (c *Configuration) Initialize(configDir string) error {
return err
}
if err := c.loadModuli(configDir); err != nil {
return err
}
sftp.SetSFTPExtensions(sftpExtensions...) //nolint:errcheck // we configure valid SFTP Extensions so we cannot get an error
if err := c.configureSecurityOptions(serverConfig); err != nil {
@ -527,14 +537,12 @@ func (c *Configuration) AcceptInboundConnection(conn net.Conn, config *ssh.Serve
loginType := sconn.Permissions.Extensions["sftpgo_login_method"]
connectionID := hex.EncodeToString(sconn.SessionID())
defer user.CloseFs() //nolint:errcheck
if err = user.CheckFsRoot(connectionID); err != nil {
errClose := user.CloseFs()
logger.Warn(logSender, connectionID, "unable to check fs root: %v close fs error: %v", err, errClose)
logger.Warn(logSender, connectionID, "unable to check fs root: %v", err)
return
}
defer user.CloseFs() //nolint:errcheck
logger.Log(logger.LevelInfo, common.ProtocolSSH, connectionID,
"User %#v logged in with %#v, from ip %#v, client version %#v", user.Username, loginType,
ipAddr, string(sconn.ClientVersion()))
@ -842,6 +850,29 @@ func (c *Configuration) checkHostKeyAutoGeneration(configDir string) error {
return nil
}
func (c *Configuration) loadModuli(configDir string) error {
supportedKexAlgos = util.Remove(supportedKexAlgos, "diffie-hellman-group-exchange-sha1")
supportedKexAlgos = util.Remove(supportedKexAlgos, "diffie-hellman-group-exchange-sha256")
for _, m := range c.Moduli {
m = strings.TrimSpace(m)
if !util.IsFileInputValid(m) {
logger.Warn(logSender, "", "unable to load invalid moduli file %q", m)
logger.WarnToConsole("unable to load invalid host moduli file %q", m)
continue
}
if !filepath.IsAbs(m) {
m = filepath.Join(configDir, m)
}
logger.Info(logSender, "", "loading moduli file %q", m)
if err := ssh.ParseModuli(m); err != nil {
return err
}
supportedKexAlgos = append(supportedKexAlgos, "diffie-hellman-group-exchange-sha1",
"diffie-hellman-group-exchange-sha256")
}
return nil
}
// If no host keys are defined we try to use or generate the default ones.
func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh.ServerConfig) error {
if err := c.checkHostKeyAutoGeneration(configDir); err != nil {

View file

@ -182,7 +182,7 @@ func TestMain(m *testing.M) {
logFilePath = filepath.Join(configDir, "sftpgo_sftpd_test.log")
loginBannerFileName := "login_banner"
loginBannerFile := filepath.Join(configDir, loginBannerFileName)
logger.InitLogger(logFilePath, 5, 1, 28, false, false, zerolog.DebugLevel)
logger.InitLogger(logFilePath, 10, 1, 28, false, false, zerolog.DebugLevel)
err := os.WriteFile(loginBannerFile, []byte("simple login banner\n"), os.ModePerm)
if err != nil {
logger.ErrorToConsole("error creating login banner: %v", err)
@ -401,6 +401,12 @@ func TestInitialization(t *testing.T) {
assert.True(t, sftpdConf.Bindings[0].HasProxy())
err = sftpdConf.Initialize(configDir)
assert.Error(t, err)
sftpdConf.Moduli = []string{"missing moduli file"}
err = sftpdConf.Initialize(configDir)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "unable to open moduli file")
}
sftpdConf.Moduli = nil
sftpdConf.HostKeys = []string{"missing key"}
err = sftpdConf.Initialize(configDir)
assert.Error(t, err)
@ -5279,9 +5285,7 @@ func TestSFTPLoopVirtualFolders(t *testing.T) {
defer client.Close()
assert.NoError(t, checkBasicSFTP(client))
_, err = client.ReadDir("/vdir")
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "SFTP loop")
}
assert.Error(t, err)
}
// now make user2 a local account with an SFTP virtual folder to user1.
// So we have:
@ -5316,9 +5320,7 @@ func TestSFTPLoopVirtualFolders(t *testing.T) {
defer client.Close()
assert.NoError(t, checkBasicSFTP(client))
_, err = client.ReadDir("/vdir")
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "SFTP loop")
}
assert.Error(t, err)
}
_, err = httpdtest.RemoveUser(user1, http.StatusOK)
@ -10080,7 +10082,7 @@ func TestSCPNestedFolders(t *testing.T) {
// now change the password for the base user, so SFTP folder will not work
baseUser.Password = defaultPassword + "_mod"
_, _, err = httpdtest.UpdateUser(baseUser, http.StatusOK, "")
_, _, err = httpdtest.UpdateUser(baseUser, http.StatusOK, "1")
assert.NoError(t, err)
err = scpUpload(filepath.Join(baseDir, "vdir"), remoteRootPath, true, false)

View file

@ -181,6 +181,7 @@ func SendEmail(to []string, subject, body string, contentType EmailContentType,
}
email := mail.NewMSG()
email.AllowDuplicateAddress = true
if from != "" {
email.SetFrom(from)
} else {

View file

@ -125,6 +125,18 @@ func Contains[T comparable](elems []T, v T) bool {
return false
}
// Remove removes an element from a string slice and
// returns the modified slice
func Remove(elems []string, val string) []string {
for idx, v := range elems {
if v == val {
elems[idx] = elems[len(elems)-1]
return elems[:len(elems)-1]
}
}
return elems
}
// IsStringPrefixInSlice searches a string prefix in a slice and returns true
// if a matching prefix is found
func IsStringPrefixInSlice(obj string, list []string) bool {
@ -727,3 +739,19 @@ func PanicOnError(err error) {
panic(fmt.Errorf("unexpected error: %w", err))
}
}
// GetAbsolutePath returns an absolute path using the current dir as base
// if name defines a relative path
func GetAbsolutePath(name string) (string, error) {
if name == "" {
return name, errors.New("input path cannot be empty")
}
if filepath.IsAbs(name) {
return name, nil
}
curDir, err := os.Getwd()
if err != nil {
return name, err
}
return filepath.Join(curDir, name), nil
}

View file

@ -17,7 +17,7 @@ package version
import "strings"
const version = "2.4.0-dev"
const version = "2.4.2"
var (
commit = ""

View file

@ -55,6 +55,7 @@ import (
const (
azureDefaultEndpoint = "blob.core.windows.net"
azBlobFsName = "AzureBlobFs"
azFolderKey = "hdi_isfolder"
)
// AzureBlobFs is a Fs implementation for Azure Blob storage.
@ -182,6 +183,14 @@ func (fs *AzureBlobFs) Stat(name string) (os.FileInfo, error) {
if err == nil {
contentType := util.GetStringFromPointer(attrs.ContentType)
isDir := contentType == dirMimeType
if !isDir {
for k, v := range attrs.Metadata {
if strings.ToLower(k) == azFolderKey {
isDir = (v == "true")
break
}
}
}
metric.AZListObjectsCompleted(nil)
return updateFileInfoModTime(fs.getStorageID(), name, NewFileInfo(name, isDir,
util.GetIntFromPointer(attrs.ContentLength),
@ -217,7 +226,7 @@ func (fs *AzureBlobFs) Open(name string, offset int64) (File, *pipeat.PipeReader
go func() {
defer cancelFn()
blockBlob := fs.containerClient.NewBlockBlobClient(name)
blockBlob := fs.containerClient.NewBlockBlobClient(url.PathEscape(name))
err := fs.handleMultipartDownload(ctx, blockBlob, offset, w)
w.CloseWithError(err) //nolint:errcheck
fsLog(fs, logger.LevelDebug, "download completed, path: %#v size: %v, err: %+v", name, w.GetWrittenBytes(), err)
@ -238,8 +247,12 @@ func (fs *AzureBlobFs) Create(name string, flag int) (File, *PipeWriter, func(),
p := NewPipeWriter(w)
headers := blob.HTTPHeaders{}
var contentType string
var metadata map[string]string
if flag == -1 {
contentType = dirMimeType
metadata = map[string]string{
azFolderKey: "true",
}
} else {
contentType = mime.TypeByExtension(path.Ext(name))
}
@ -250,8 +263,8 @@ func (fs *AzureBlobFs) Create(name string, flag int) (File, *PipeWriter, func(),
go func() {
defer cancelFn()
blockBlob := fs.containerClient.NewBlockBlobClient(name)
err := fs.handleMultipartUpload(ctx, r, blockBlob, &headers)
blockBlob := fs.containerClient.NewBlockBlobClient(url.PathEscape(name))
err := fs.handleMultipartUpload(ctx, r, blockBlob, &headers, metadata)
r.CloseWithError(err) //nolint:errcheck
p.Done(err)
fsLog(fs, logger.LevelDebug, "upload completed, path: %#v, readed bytes: %v, err: %+v", name, r.GetReadedBytes(), err)
@ -282,43 +295,47 @@ func (fs *AzureBlobFs) Rename(source, target string) error {
if hasContents {
return fmt.Errorf("cannot rename non empty directory: %#v", source)
}
}
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxLongTimeout))
defer cancelFn()
srcBlob := fs.containerClient.NewBlockBlobClient(url.PathEscape(source))
dstBlob := fs.containerClient.NewBlockBlobClient(target)
resp, err := dstBlob.StartCopyFromURL(ctx, srcBlob.URL(), fs.getCopyOptions())
if err != nil {
metric.AZCopyObjectCompleted(err)
return err
}
copyStatus := blob.CopyStatusType(util.GetStringFromPointer((*string)(resp.CopyStatus)))
nErrors := 0
for copyStatus == blob.CopyStatusTypePending {
// Poll until the copy is complete.
time.Sleep(500 * time.Millisecond)
resp, err := dstBlob.GetProperties(ctx, &blob.GetPropertiesOptions{})
if err != nil {
// A GetProperties failure may be transient, so allow a couple
// of them before giving up.
nErrors++
if ctx.Err() != nil || nErrors == 3 {
metric.AZCopyObjectCompleted(err)
return err
}
} else {
copyStatus = blob.CopyStatusType(util.GetStringFromPointer((*string)(resp.CopyStatus)))
if err := fs.mkdirInternal(target); err != nil {
return err
}
}
if copyStatus != blob.CopyStatusTypeSuccess {
err := fmt.Errorf("copy failed with status: %s", copyStatus)
metric.AZCopyObjectCompleted(err)
return err
}
} else {
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxLongTimeout))
defer cancelFn()
metric.AZCopyObjectCompleted(nil)
fs.preserveModificationTime(source, target, fi)
srcBlob := fs.containerClient.NewBlockBlobClient(url.PathEscape(source))
dstBlob := fs.containerClient.NewBlockBlobClient(url.PathEscape(target))
resp, err := dstBlob.StartCopyFromURL(ctx, srcBlob.URL(), fs.getCopyOptions())
if err != nil {
metric.AZCopyObjectCompleted(err)
return err
}
copyStatus := blob.CopyStatusType(util.GetStringFromPointer((*string)(resp.CopyStatus)))
nErrors := 0
for copyStatus == blob.CopyStatusTypePending {
// Poll until the copy is complete.
time.Sleep(500 * time.Millisecond)
resp, err := dstBlob.GetProperties(ctx, &blob.GetPropertiesOptions{})
if err != nil {
// A GetProperties failure may be transient, so allow a couple
// of them before giving up.
nErrors++
if ctx.Err() != nil || nErrors == 3 {
metric.AZCopyObjectCompleted(err)
return err
}
} else {
copyStatus = blob.CopyStatusType(util.GetStringFromPointer((*string)(resp.CopyStatus)))
}
}
if copyStatus != blob.CopyStatusTypeSuccess {
err := fmt.Errorf("copy failed with status: %s", copyStatus)
metric.AZCopyObjectCompleted(err)
return err
}
metric.AZCopyObjectCompleted(nil)
fs.preserveModificationTime(source, target, fi)
}
return fs.Remove(source, fi.IsDir())
}
@ -337,11 +354,22 @@ func (fs *AzureBlobFs) Remove(name string, isDir bool) error {
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
defer cancelFn()
blobBlock := fs.containerClient.NewBlockBlobClient(name)
deletSnapshots := blob.DeleteSnapshotsOptionTypeInclude
blobBlock := fs.containerClient.NewBlockBlobClient(url.PathEscape(name))
var deletSnapshots blob.DeleteSnapshotsOptionType
if !isDir {
deletSnapshots = blob.DeleteSnapshotsOptionTypeInclude
}
_, err := blobBlock.Delete(ctx, &blob.DeleteOptions{
DeleteSnapshots: &deletSnapshots,
})
if err != nil && isDir {
if fs.isBadRequestError(err) {
deletSnapshots = blob.DeleteSnapshotsOptionTypeInclude
_, err = blobBlock.Delete(ctx, &blob.DeleteOptions{
DeleteSnapshots: &deletSnapshots,
})
}
}
metric.AZDeleteObjectCompleted(err)
if plugin.Handler.HasMetadater() && err == nil && !isDir {
if errMetadata := plugin.Handler.RemoveMetadata(fs.getStorageID(), ensureAbsPath(name)); errMetadata != nil {
@ -357,11 +385,7 @@ func (fs *AzureBlobFs) Mkdir(name string) error {
if !fs.IsNotExist(err) {
return err
}
_, w, _, err := fs.Create(name, -1)
if err != nil {
return err
}
return w.Close()
return fs.mkdirInternal(name)
}
// Symlink creates source as a symbolic link to target.
@ -424,8 +448,10 @@ func (fs *AzureBlobFs) ReadDir(dirname string) ([]os.FileInfo, error) {
prefixes := make(map[string]bool)
pager := fs.containerClient.NewListBlobsHierarchyPager("/", &container.ListBlobsHierarchyOptions{
Include: container.ListBlobsInclude{},
Prefix: &prefix,
Include: container.ListBlobsInclude{
//Metadata: true,
},
Prefix: &prefix,
})
for pager.More() {
@ -462,7 +488,7 @@ func (fs *AzureBlobFs) ReadDir(dirname string) ([]os.FileInfo, error) {
size = util.GetIntFromPointer(blobItem.Properties.ContentLength)
modTime = util.GetTimeFromPointer(blobItem.Properties.LastModified)
contentType := util.GetStringFromPointer(blobItem.Properties.ContentType)
isDir = (contentType == dirMimeType)
isDir = checkDirectoryMarkers(contentType, blobItem.Metadata)
if isDir {
// check if the dir is already included, it will be sent as blob prefix if it contains at least one item
if _, ok := prefixes[name]; ok {
@ -530,6 +556,17 @@ func (*AzureBlobFs) IsNotSupported(err error) bool {
return err == ErrVfsUnsupported
}
func (*AzureBlobFs) isBadRequestError(err error) bool {
if err == nil {
return false
}
var respErr *azcore.ResponseError
if errors.As(err, &respErr) {
return respErr.StatusCode == http.StatusBadRequest
}
return false
}
// CheckRootPath creates the specified local root directory if it does not exists
func (fs *AzureBlobFs) CheckRootPath(username string, uid int, gid int) bool {
// we need a local directory for temporary files
@ -544,6 +581,9 @@ func (fs *AzureBlobFs) ScanRootDirContents() (int, int64, error) {
size := int64(0)
pager := fs.containerClient.NewListBlobsFlatPager(&container.ListBlobsFlatOptions{
Include: container.ListBlobsInclude{
Metadata: true,
},
Prefix: &fs.config.KeyPrefix,
})
@ -559,13 +599,16 @@ func (fs *AzureBlobFs) ScanRootDirContents() (int, int64, error) {
for _, blobItem := range resp.ListBlobsFlatSegmentResponse.Segment.BlobItems {
if blobItem.Properties != nil {
contentType := util.GetStringFromPointer(blobItem.Properties.ContentType)
isDir := (contentType == dirMimeType)
isDir := checkDirectoryMarkers(contentType, blobItem.Metadata)
blobSize := util.GetIntFromPointer(blobItem.Properties.ContentLength)
if isDir && blobSize == 0 {
continue
}
numFiles++
size += blobSize
if numFiles%1000 == 0 {
fsLog(fs, logger.LevelDebug, "root dir scan in progress, files: %d, size: %d", numFiles, size)
}
}
}
}
@ -582,8 +625,10 @@ func (fs *AzureBlobFs) getFileNamesInPrefix(fsPrefix string) (map[string]bool, e
}
pager := fs.containerClient.NewListBlobsHierarchyPager("/", &container.ListBlobsHierarchyOptions{
Include: container.ListBlobsInclude{},
Prefix: &prefix,
Include: container.ListBlobsInclude{
//Metadata: true,
},
Prefix: &prefix,
})
for pager.More() {
@ -600,7 +645,7 @@ func (fs *AzureBlobFs) getFileNamesInPrefix(fsPrefix string) (map[string]bool, e
name = strings.TrimPrefix(name, prefix)
if blobItem.Properties != nil {
contentType := util.GetStringFromPointer(blobItem.Properties.ContentType)
isDir := (contentType == dirMimeType)
isDir := checkDirectoryMarkers(contentType, blobItem.Metadata)
if isDir {
continue
}
@ -657,7 +702,10 @@ func (fs *AzureBlobFs) GetRelativePath(name string) string {
func (fs *AzureBlobFs) Walk(root string, walkFn filepath.WalkFunc) error {
prefix := fs.getPrefix(root)
pager := fs.containerClient.NewListBlobsFlatPager(&container.ListBlobsFlatOptions{
Prefix: &fs.config.KeyPrefix,
Include: container.ListBlobsInclude{
Metadata: true,
},
Prefix: &prefix,
})
for pager.More() {
@ -679,7 +727,7 @@ func (fs *AzureBlobFs) Walk(root string, walkFn filepath.WalkFunc) error {
isDir := false
if blobItem.Properties != nil {
contentType := util.GetStringFromPointer(blobItem.Properties.ContentType)
isDir = (contentType == dirMimeType)
isDir = checkDirectoryMarkers(contentType, blobItem.Metadata)
blobSize = util.GetIntFromPointer(blobItem.Properties.ContentLength)
lastModified = util.GetTimeFromPointer(blobItem.Properties.LastModified)
}
@ -719,7 +767,7 @@ func (fs *AzureBlobFs) headObject(name string) (blob.GetPropertiesResponse, erro
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
defer cancelFn()
resp, err := fs.containerClient.NewBlockBlobClient(name).GetProperties(ctx, &blob.GetPropertiesOptions{})
resp, err := fs.containerClient.NewBlockBlobClient(url.PathEscape(name)).GetProperties(ctx, &blob.GetPropertiesOptions{})
metric.AZHeadObjectCompleted(err)
return resp, err
@ -792,6 +840,14 @@ func (fs *AzureBlobFs) setConfigDefaults() {
}
}
func (fs *AzureBlobFs) mkdirInternal(name string) error {
_, w, _, err := fs.Create(name, -1)
if err != nil {
return err
}
return w.Close()
}
func (fs *AzureBlobFs) hasContents(name string) (bool, error) {
result := false
prefix := fs.getPrefix(name)
@ -928,7 +984,7 @@ func (fs *AzureBlobFs) handleMultipartDownload(ctx context.Context, blockBlob *b
}
func (fs *AzureBlobFs) handleMultipartUpload(ctx context.Context, reader io.Reader,
blockBlob *blockblob.Client, httpHeaders *blob.HTTPHeaders,
blockBlob *blockblob.Client, httpHeaders *blob.HTTPHeaders, metadata map[string]string,
) error {
partSize := fs.config.UploadPartSize
guard := make(chan struct{}, fs.config.UploadConcurrency)
@ -1018,6 +1074,7 @@ func (fs *AzureBlobFs) handleMultipartUpload(ctx context.Context, reader io.Read
commitOptions := blockblob.CommitBlockListOptions{
HTTPHeaders: httpHeaders,
Metadata: metadata,
}
if fs.config.AccessTier != "" {
commitOptions.Tier = (*blob.AccessTier)(&fs.config.AccessTier)
@ -1080,6 +1137,18 @@ func (fs *AzureBlobFs) getStorageID() string {
return fmt.Sprintf("azblob://%v", fs.config.Container)
}
func checkDirectoryMarkers(contentType string, metadata map[string]*string) bool {
if contentType == dirMimeType {
return true
}
for k, v := range metadata {
if strings.ToLower(k) == azFolderKey {
return util.GetStringFromPointer(v) == "true"
}
}
return false
}
func getAzContainerClientOptions() *container.ClientOptions {
version := version.Get()
return &container.ClientOptions{

View file

@ -20,6 +20,7 @@ import (
"strconv"
"strings"
"github.com/rs/xid"
"github.com/sftpgo/sdk"
"github.com/drakkan/sftpgo/v2/internal/util"
@ -213,7 +214,7 @@ func (v *VirtualFolder) GetFilesystem(connectionID string, forbiddenSelfUsers []
// CheckMetadataConsistency checks the consistency between the metadata stored
// in the configured metadata plugin and the filesystem
func (v *VirtualFolder) CheckMetadataConsistency() error {
fs, err := v.GetFilesystem("", nil)
fs, err := v.GetFilesystem(xid.New().String(), nil)
if err != nil {
return err
}
@ -227,7 +228,7 @@ func (v *VirtualFolder) ScanQuota() (int, int64, error) {
if v.hasPathPlaceholder() {
return 0, 0, errors.New("cannot scan quota: this folder has a path placeholder")
}
fs, err := v.GetFilesystem("", nil)
fs, err := v.GetFilesystem(xid.New().String(), nil)
if err != nil {
return 0, 0, err
}

View file

@ -226,38 +226,32 @@ func (fs *GCSFs) Rename(source, target string) error {
if hasContents {
return fmt.Errorf("cannot rename non empty directory: %#v", source)
}
if !strings.HasSuffix(target, "/") {
target += "/"
if err := fs.mkdirInternal(target); err != nil {
return err
}
}
src := fs.svc.Bucket(fs.config.Bucket).Object(realSourceName)
dst := fs.svc.Bucket(fs.config.Bucket).Object(target)
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
defer cancelFn()
copier := dst.CopierFrom(src)
if fs.config.StorageClass != "" {
copier.StorageClass = fs.config.StorageClass
}
if fs.config.ACL != "" {
copier.PredefinedACL = fs.config.ACL
}
var contentType string
if fi.IsDir() {
contentType = dirMimeType
} else {
contentType = mime.TypeByExtension(path.Ext(source))
}
if contentType != "" {
copier.ContentType = contentType
}
_, err = copier.Run(ctx)
metric.GCSCopyObjectCompleted(err)
if err != nil {
return err
}
if plugin.Handler.HasMetadater() {
if !fi.IsDir() {
src := fs.svc.Bucket(fs.config.Bucket).Object(realSourceName)
dst := fs.svc.Bucket(fs.config.Bucket).Object(target)
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
defer cancelFn()
copier := dst.CopierFrom(src)
if fs.config.StorageClass != "" {
copier.StorageClass = fs.config.StorageClass
}
if fs.config.ACL != "" {
copier.PredefinedACL = fs.config.ACL
}
contentType := mime.TypeByExtension(path.Ext(source))
if contentType != "" {
copier.ContentType = contentType
}
_, err = copier.Run(ctx)
metric.GCSCopyObjectCompleted(err)
if err != nil {
return err
}
if plugin.Handler.HasMetadater() {
err = plugin.Handler.SetModificationTime(fs.getStorageID(), ensureAbsPath(target),
util.GetTimeAsMsSinceEpoch(fi.ModTime()))
if err != nil {
@ -306,14 +300,7 @@ func (fs *GCSFs) Mkdir(name string) error {
if !fs.IsNotExist(err) {
return err
}
if !strings.HasSuffix(name, "/") {
name += "/"
}
_, w, _, err := fs.Create(name, -1)
if err != nil {
return err
}
return w.Close()
return fs.mkdirInternal(name)
}
// Symlink creates source as a symbolic link to target.
@ -534,6 +521,9 @@ func (fs *GCSFs) ScanRootDirContents() (int, int64, error) {
}
numFiles++
size += attrs.Size
if numFiles%1000 == 0 {
fsLog(fs, logger.LevelDebug, "root dir scan in progress, files: %d, size: %d", numFiles, size)
}
}
objects = nil
@ -759,6 +749,17 @@ func (fs *GCSFs) getObjectStat(name string) (string, os.FileInfo, error) {
return name + "/", info, err
}
func (fs *GCSFs) mkdirInternal(name string) error {
if !strings.HasSuffix(name, "/") {
name += "/"
}
_, w, _, err := fs.Create(name, -1)
if err != nil {
return err
}
return w.Close()
}
func (fs *GCSFs) hasContents(name string) (bool, error) {
result := false
prefix := fs.getPrefix(name)

View file

@ -382,6 +382,9 @@ func (fs *OsFs) GetDirSize(dirname string) (int, int64, error) {
if info != nil && info.Mode().IsRegular() {
size += info.Size()
numFiles++
if numFiles%1000 == 0 {
fsLog(fs, logger.LevelDebug, "dirname %q scan in progress, files: %d, size: %d", dirname, numFiles, size)
}
}
return err
})

View file

@ -60,6 +60,10 @@ const (
s3fsName = "S3Fs"
)
var (
s3DirMimeTypes = []string{s3DirMimeType, "httpd/unix-directory"}
)
// S3Fs is a Fs implementation for AWS S3 compatible object storages
type S3Fs struct {
connectionID string
@ -159,8 +163,10 @@ func (fs *S3Fs) Stat(name string) (os.FileInfo, error) {
}
obj, err := fs.headObject(name)
if err == nil {
// a "dir" has a trailing "/" so we cannot have a directory here
return updateFileInfoModTime(fs.getStorageID(), name, NewFileInfo(name, false, obj.ContentLength,
// Some S3 providers (like SeaweedFS) remove the trailing '/' from object keys.
// So we check some common content types to detect if this is a "directory".
isDir := util.Contains(s3DirMimeTypes, util.GetStringFromPointer(obj.ContentType))
return updateFileInfoModTime(fs.getStorageID(), name, NewFileInfo(name, isDir, obj.ContentLength,
util.GetTimeFromPointer(obj.LastModified), false))
}
if !fs.IsNotExist(err) {
@ -289,63 +295,53 @@ func (fs *S3Fs) Rename(source, target string) error {
if err != nil {
return err
}
copySource := fs.Join(fs.config.Bucket, source)
if fi.IsDir() {
hasContents, err := fs.hasContents(source)
if err != nil {
return err
}
if hasContents {
return fmt.Errorf("cannot rename non empty directory: %#v", source)
return fmt.Errorf("cannot rename non empty directory: %q", source)
}
if !strings.HasSuffix(copySource, "/") {
copySource += "/"
if err := fs.mkdirInternal(target); err != nil {
return err
}
if !strings.HasSuffix(target, "/") {
target += "/"
}
}
var contentType string
if fi.IsDir() {
contentType = s3DirMimeType
} else {
contentType = mime.TypeByExtension(path.Ext(source))
}
copySource = pathEscape(copySource)
contentType := mime.TypeByExtension(path.Ext(source))
copySource := pathEscape(fs.Join(fs.config.Bucket, source))
if fi.Size() > 500*1024*1024 {
fsLog(fs, logger.LevelDebug, "renaming file %q with size %d using multipart copy",
source, fi.Size())
err = fs.doMultipartCopy(copySource, target, contentType, fi.Size())
} else {
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
defer cancelFn()
if fi.Size() > 500*1024*1024 {
fsLog(fs, logger.LevelDebug, "renaming file %q with size %d using multipart copy",
source, fi.Size())
err = fs.doMultipartCopy(copySource, target, contentType, fi.Size())
} else {
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
defer cancelFn()
_, err = fs.svc.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(fs.config.Bucket),
CopySource: aws.String(copySource),
Key: aws.String(target),
StorageClass: types.StorageClass(fs.config.StorageClass),
ACL: types.ObjectCannedACL(fs.config.ACL),
ContentType: util.NilIfEmpty(contentType),
})
}
if err != nil {
_, err = fs.svc.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(fs.config.Bucket),
CopySource: aws.String(copySource),
Key: aws.String(target),
StorageClass: types.StorageClass(fs.config.StorageClass),
ACL: types.ObjectCannedACL(fs.config.ACL),
ContentType: util.NilIfEmpty(contentType),
})
}
if err != nil {
metric.S3CopyObjectCompleted(err)
return err
}
waiter := s3.NewObjectExistsWaiter(fs.svc)
err = waiter.Wait(context.Background(), &s3.HeadObjectInput{
Bucket: aws.String(fs.config.Bucket),
Key: aws.String(target),
}, 10*time.Second)
metric.S3CopyObjectCompleted(err)
return err
}
waiter := s3.NewObjectExistsWaiter(fs.svc)
err = waiter.Wait(context.Background(), &s3.HeadObjectInput{
Bucket: aws.String(fs.config.Bucket),
Key: aws.String(target),
}, 10*time.Second)
metric.S3CopyObjectCompleted(err)
if err != nil {
return err
}
if plugin.Handler.HasMetadater() {
if !fi.IsDir() {
if err != nil {
return err
}
if plugin.Handler.HasMetadater() {
err = plugin.Handler.SetModificationTime(fs.getStorageID(), ensureAbsPath(target),
util.GetTimeAsMsSinceEpoch(fi.ModTime()))
if err != nil {
@ -393,14 +389,7 @@ func (fs *S3Fs) Mkdir(name string) error {
if !fs.IsNotExist(err) {
return err
}
if !strings.HasSuffix(name, "/") {
name += "/"
}
_, w, _, err := fs.Create(name, -1)
if err != nil {
return err
}
return w.Close()
return fs.mkdirInternal(name)
}
// Symlink creates source as a symbolic link to target.
@ -600,6 +589,9 @@ func (fs *S3Fs) ScanRootDirContents() (int, int64, error) {
}
numFiles++
size += fileObject.Size
if numFiles%1000 == 0 {
fsLog(fs, logger.LevelDebug, "root dir scan in progress, files: %d, size: %d", numFiles, size)
}
}
}
@ -775,6 +767,17 @@ func (fs *S3Fs) setConfigDefaults() {
}
}
func (fs *S3Fs) mkdirInternal(name string) error {
if !strings.HasSuffix(name, "/") {
name += "/"
}
_, w, _, err := fs.Create(name, -1)
if err != nil {
return err
}
return w.Close()
}
func (fs *S3Fs) hasContents(name string) (bool, error) {
prefix := fs.getPrefix(name)
paginator := s3.NewListObjectsV2Paginator(fs.svc, &s3.ListObjectsV2Input{

View file

@ -16,8 +16,10 @@ package vfs
import (
"bufio"
"bytes"
"errors"
"fmt"
"hash/fnv"
"io"
"io/fs"
"net"
@ -25,12 +27,15 @@ import (
"os"
"path"
"path/filepath"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/eikenb/pipeat"
"github.com/pkg/sftp"
"github.com/robfig/cron/v3"
"github.com/rs/xid"
"github.com/sftpgo/sdk"
"golang.org/x/crypto/ssh"
@ -43,11 +48,16 @@ import (
const (
// sftpFsName is the name for the SFTP Fs implementation
sftpFsName = "sftpfs"
sftpFsName = "sftpfs"
logSenderSFTPCache = "sftpCache"
maxSessionsPerConnection = 5
)
// ErrSFTPLoop defines the error to return if an SFTP loop is detected
var ErrSFTPLoop = errors.New("SFTP loop or nested local SFTP folders detected")
var (
// ErrSFTPLoop defines the error to return if an SFTP loop is detected
ErrSFTPLoop = errors.New("SFTP loop or nested local SFTP folders detected")
sftpConnsCache = newSFTPConnectionCache()
)
// SFTPFsConfig defines the configuration for SFTP based filesystem
type SFTPFsConfig struct {
@ -145,6 +155,9 @@ func (c *SFTPFsConfig) validate() error {
if c.Endpoint == "" {
return errors.New("endpoint cannot be empty")
}
if !strings.Contains(c.Endpoint, ":") {
c.Endpoint += ":22"
}
_, _, err := net.SplitHostPort(c.Endpoint)
if err != nil {
return fmt.Errorf("invalid endpoint: %v", err)
@ -220,17 +233,36 @@ func (c *SFTPFsConfig) ValidateAndEncryptCredentials(additionalData string) erro
return nil
}
// getUniqueID returns an hash of the settings used to connect to the SFTP server
func (c *SFTPFsConfig) getUniqueID(partition int) uint64 {
h := fnv.New64a()
var b bytes.Buffer
b.WriteString(c.Endpoint)
b.WriteString(c.Username)
b.WriteString(strings.Join(c.Fingerprints, ""))
b.WriteString(strconv.FormatBool(c.DisableCouncurrentReads))
b.WriteString(strconv.FormatInt(c.BufferSize, 10))
b.WriteString(c.Password.GetPayload())
b.WriteString(c.PrivateKey.GetPayload())
b.WriteString(c.KeyPassphrase.GetPayload())
if allowSelfConnections != 0 {
b.WriteString(strings.Join(c.forbiddenSelfUsernames, ""))
}
b.WriteString(strconv.Itoa(partition))
h.Write(b.Bytes())
return h.Sum64()
}
// SFTPFs is a Fs implementation for SFTP backends
type SFTPFs struct {
sync.Mutex
connectionID string
// if not empty this fs is mouted as virtual folder in the specified path
mountPath string
localTempDir string
config *SFTPFsConfig
sshClient *ssh.Client
sftpClient *sftp.Client
err chan error
conn *sftpConnection
}
// NewSFTPFs returns an SFTPFs object that allows to interact with an SFTP server
@ -266,15 +298,18 @@ func NewSFTPFs(connectionID, mountPath, localTempDir string, forbiddenSelfUserna
mountPath: getMountPath(mountPath),
localTempDir: localTempDir,
config: &config,
err: make(chan error, 1),
conn: sftpConnsCache.Get(&config, connectionID),
}
err := sftpFs.createConnection()
if err != nil {
sftpFs.Close() //nolint:errcheck
}
return sftpFs, err
}
// Name returns the name for the Fs implementation
func (fs *SFTPFs) Name() string {
return fmt.Sprintf("%v %#v", sftpFsName, fs.config.Endpoint)
return fmt.Sprintf(`%s %q@%q`, sftpFsName, fs.config.Username, fs.config.Endpoint)
}
// ConnectionID returns the connection ID associated to this Fs implementation
@ -284,26 +319,29 @@ func (fs *SFTPFs) ConnectionID() string {
// Stat returns a FileInfo describing the named file
func (fs *SFTPFs) Stat(name string) (os.FileInfo, error) {
if err := fs.checkConnection(); err != nil {
client, err := fs.conn.getClient()
if err != nil {
return nil, err
}
return fs.sftpClient.Stat(name)
return client.Stat(name)
}
// Lstat returns a FileInfo describing the named file
func (fs *SFTPFs) Lstat(name string) (os.FileInfo, error) {
if err := fs.checkConnection(); err != nil {
client, err := fs.conn.getClient()
if err != nil {
return nil, err
}
return fs.sftpClient.Lstat(name)
return client.Lstat(name)
}
// Open opens the named file for reading
func (fs *SFTPFs) Open(name string, offset int64) (File, *pipeat.PipeReaderAt, func(), error) {
if err := fs.checkConnection(); err != nil {
client, err := fs.conn.getClient()
if err != nil {
return nil, nil, nil, err
}
f, err := fs.sftpClient.Open(name)
f, err := client.Open(name)
if err != nil {
return nil, nil, nil, err
}
@ -337,21 +375,21 @@ func (fs *SFTPFs) Open(name string, offset int64) (File, *pipeat.PipeReaderAt, f
// Create creates or opens the named file for writing
func (fs *SFTPFs) Create(name string, flag int) (File, *PipeWriter, func(), error) {
err := fs.checkConnection()
client, err := fs.conn.getClient()
if err != nil {
return nil, nil, nil, err
}
if fs.config.BufferSize == 0 {
var f File
if flag == 0 {
f, err = fs.sftpClient.Create(name)
f, err = client.Create(name)
} else {
f, err = fs.sftpClient.OpenFile(name, flag)
f, err = client.OpenFile(name, flag)
}
return f, nil, nil, err
}
// buffering is enabled
f, err := fs.sftpClient.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC)
f, err := client.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC)
if err != nil {
return nil, nil, nil, err
}
@ -393,48 +431,53 @@ func (fs *SFTPFs) Rename(source, target string) error {
if source == target {
return nil
}
if err := fs.checkConnection(); err != nil {
client, err := fs.conn.getClient()
if err != nil {
return err
}
if _, ok := fs.sftpClient.HasExtension("posix-rename@openssh.com"); ok {
return fs.sftpClient.PosixRename(source, target)
if _, ok := client.HasExtension("posix-rename@openssh.com"); ok {
return client.PosixRename(source, target)
}
return fs.sftpClient.Rename(source, target)
return client.Rename(source, target)
}
// Remove removes the named file or (empty) directory.
func (fs *SFTPFs) Remove(name string, isDir bool) error {
if err := fs.checkConnection(); err != nil {
client, err := fs.conn.getClient()
if err != nil {
return err
}
if isDir {
return fs.sftpClient.RemoveDirectory(name)
return client.RemoveDirectory(name)
}
return fs.sftpClient.Remove(name)
return client.Remove(name)
}
// Mkdir creates a new directory with the specified name and default permissions
func (fs *SFTPFs) Mkdir(name string) error {
if err := fs.checkConnection(); err != nil {
client, err := fs.conn.getClient()
if err != nil {
return err
}
return fs.sftpClient.Mkdir(name)
return client.Mkdir(name)
}
// Symlink creates source as a symbolic link to target.
func (fs *SFTPFs) Symlink(source, target string) error {
if err := fs.checkConnection(); err != nil {
client, err := fs.conn.getClient()
if err != nil {
return err
}
return fs.sftpClient.Symlink(source, target)
return client.Symlink(source, target)
}
// Readlink returns the destination of the named symbolic link
func (fs *SFTPFs) Readlink(name string) (string, error) {
if err := fs.checkConnection(); err != nil {
client, err := fs.conn.getClient()
if err != nil {
return "", err
}
resolved, err := fs.sftpClient.ReadLink(name)
resolved, err := client.ReadLink(name)
if err != nil {
return resolved, err
}
@ -448,43 +491,48 @@ func (fs *SFTPFs) Readlink(name string) (string, error) {
// Chown changes the numeric uid and gid of the named file.
func (fs *SFTPFs) Chown(name string, uid int, gid int) error {
if err := fs.checkConnection(); err != nil {
client, err := fs.conn.getClient()
if err != nil {
return err
}
return fs.sftpClient.Chown(name, uid, gid)
return client.Chown(name, uid, gid)
}
// Chmod changes the mode of the named file to mode.
func (fs *SFTPFs) Chmod(name string, mode os.FileMode) error {
if err := fs.checkConnection(); err != nil {
client, err := fs.conn.getClient()
if err != nil {
return err
}
return fs.sftpClient.Chmod(name, mode)
return client.Chmod(name, mode)
}
// Chtimes changes the access and modification times of the named file.
func (fs *SFTPFs) Chtimes(name string, atime, mtime time.Time, isUploading bool) error {
if err := fs.checkConnection(); err != nil {
client, err := fs.conn.getClient()
if err != nil {
return err
}
return fs.sftpClient.Chtimes(name, atime, mtime)
return client.Chtimes(name, atime, mtime)
}
// Truncate changes the size of the named file.
func (fs *SFTPFs) Truncate(name string, size int64) error {
if err := fs.checkConnection(); err != nil {
client, err := fs.conn.getClient()
if err != nil {
return err
}
return fs.sftpClient.Truncate(name, size)
return client.Truncate(name, size)
}
// ReadDir reads the directory named by dirname and returns
// a list of directory entries.
func (fs *SFTPFs) ReadDir(dirname string) ([]os.FileInfo, error) {
if err := fs.checkConnection(); err != nil {
client, err := fs.conn.getClient()
if err != nil {
return nil, err
}
return fs.sftpClient.ReadDir(dirname)
return client.ReadDir(dirname)
}
// IsUploadResumeSupported returns true if resuming uploads is supported.
@ -528,11 +576,12 @@ func (fs *SFTPFs) CheckRootPath(username string, uid int, gid int) bool {
if fs.config.Prefix == "/" {
return true
}
if err := fs.checkConnection(); err != nil {
client, err := fs.conn.getClient()
if err != nil {
return false
}
if err := fs.sftpClient.MkdirAll(fs.config.Prefix); err != nil {
fsLog(fs, logger.LevelDebug, "error creating root directory %#v for user %#v: %v", fs.config.Prefix, username, err)
if err := client.MkdirAll(fs.config.Prefix); err != nil {
fsLog(fs, logger.LevelDebug, "error creating root directory %q for user %q: %v", fs.config.Prefix, username, err)
return false
}
return true
@ -581,10 +630,11 @@ func (fs *SFTPFs) GetRelativePath(name string) string {
// Walk walks the file tree rooted at root, calling walkFn for each file or
// directory in the tree, including root
func (fs *SFTPFs) Walk(root string, walkFn filepath.WalkFunc) error {
if err := fs.checkConnection(); err != nil {
client, err := fs.conn.getClient()
if err != nil {
return err
}
walker := fs.sftpClient.Walk(root)
walker := client.Walk(root)
for walker.Step() {
err := walker.Err()
if err != nil {
@ -620,9 +670,6 @@ func (fs *SFTPFs) ResolvePath(virtualPath string) (string, error) {
if fs.config.Prefix != "/" && fsPath != "/" {
// we need to check if this path is a symlink outside the given prefix
// or a file/dir inside a dir symlinked outside the prefix
if err := fs.checkConnection(); err != nil {
return "", err
}
var validatedPath string
var err error
validatedPath, err = fs.getRealPath(fsPath)
@ -657,10 +704,11 @@ func (fs *SFTPFs) ResolvePath(virtualPath string) (string, error) {
// RealPath implements the FsRealPather interface
func (fs *SFTPFs) RealPath(p string) (string, error) {
if err := fs.checkConnection(); err != nil {
client, err := fs.conn.getClient()
if err != nil {
return "", err
}
resolved, err := fs.sftpClient.RealPath(p)
resolved, err := client.RealPath(p)
if err != nil {
return "", err
}
@ -676,16 +724,20 @@ func (fs *SFTPFs) RealPath(p string) (string, error) {
// getRealPath returns the real remote path trying to resolve symbolic links if any
func (fs *SFTPFs) getRealPath(name string) (string, error) {
client, err := fs.conn.getClient()
if err != nil {
return "", err
}
linksWalked := 0
for {
info, err := fs.sftpClient.Lstat(name)
info, err := client.Lstat(name)
if err != nil {
return name, err
}
if info.Mode()&os.ModeSymlink == 0 {
return name, nil
}
resolvedLink, err := fs.sftpClient.ReadLink(name)
resolvedLink, err := client.ReadLink(name)
if err != nil {
return name, fmt.Errorf("unable to resolve link to %q: %w", name, err)
}
@ -723,12 +775,13 @@ func (fs *SFTPFs) isSubDir(name string) error {
func (fs *SFTPFs) GetDirSize(dirname string) (int, int64, error) {
numFiles := 0
size := int64(0)
if err := fs.checkConnection(); err != nil {
client, err := fs.conn.getClient()
if err != nil {
return numFiles, size, err
}
isDir, err := isDirectory(fs, dirname)
if err == nil && isDir {
walker := fs.sftpClient.Walk(dirname)
walker := client.Walk(dirname)
for walker.Step() {
err := walker.Err()
if err != nil {
@ -737,6 +790,9 @@ func (fs *SFTPFs) GetDirSize(dirname string) (int, int64, error) {
if walker.Stat().Mode().IsRegular() {
size += walker.Stat().Size()
numFiles++
if numFiles%1000 == 0 {
fsLog(fs, logger.LevelDebug, "dirname %q scan in progress, files: %d, size: %d", dirname, numFiles, size)
}
}
}
}
@ -745,10 +801,11 @@ func (fs *SFTPFs) GetDirSize(dirname string) (int, int64, error) {
// GetMimeType returns the content type
func (fs *SFTPFs) GetMimeType(name string) (string, error) {
if err := fs.checkConnection(); err != nil {
client, err := fs.conn.getClient()
if err != nil {
return "", err
}
f, err := fs.sftpClient.OpenFile(name, os.O_RDONLY)
f, err := client.OpenFile(name, os.O_RDONLY)
if err != nil {
return "", err
}
@ -766,31 +823,20 @@ func (fs *SFTPFs) GetMimeType(name string) (string, error) {
// GetAvailableDiskSize returns the available size for the specified path
func (fs *SFTPFs) GetAvailableDiskSize(dirName string) (*sftp.StatVFS, error) {
if err := fs.checkConnection(); err != nil {
client, err := fs.conn.getClient()
if err != nil {
return nil, err
}
if _, ok := fs.sftpClient.HasExtension("statvfs@openssh.com"); !ok {
if _, ok := client.HasExtension("statvfs@openssh.com"); !ok {
return nil, ErrStorageSizeUnavailable
}
return fs.sftpClient.StatVFS(dirName)
return client.StatVFS(dirName)
}
// Close the connection
func (fs *SFTPFs) Close() error {
fs.Lock()
defer fs.Unlock()
var sftpErr, sshErr error
if fs.sftpClient != nil {
sftpErr = fs.sftpClient.Close()
}
if fs.sshClient != nil {
sshErr = fs.sshClient.Close()
}
if sftpErr != nil {
return sftpErr
}
return sshErr
fs.conn.RemoveSession(fs.connectionID)
return nil
}
func (fs *SFTPFs) copy(dst io.Writer, src io.Reader) (written int64, err error) {
@ -825,64 +871,98 @@ func (fs *SFTPFs) copy(dst io.Writer, src io.Reader) (written int64, err error)
return written, err
}
func (fs *SFTPFs) checkConnection() error {
err := fs.closed()
if err == nil {
return nil
func (fs *SFTPFs) createConnection() error {
err := fs.conn.OpenConnection()
if err != nil {
fsLog(fs, logger.LevelError, "error opening connection: %v", err)
return err
}
return fs.createConnection()
return nil
}
func (fs *SFTPFs) createConnection() error {
fs.Lock()
defer fs.Unlock()
type sftpConnection struct {
config *SFTPFsConfig
logSender string
sshClient *ssh.Client
sftpClient *sftp.Client
mu sync.RWMutex
isConnected bool
sessions map[string]bool
lastActivity time.Time
}
var err error
func newSFTPConnection(config *SFTPFsConfig, sessionID string) *sftpConnection {
c := &sftpConnection{
config: config,
logSender: fmt.Sprintf(`%s "%s@%s"`, sftpFsName, config.Username, config.Endpoint),
isConnected: false,
sessions: map[string]bool{},
lastActivity: time.Now().UTC(),
}
c.sessions[sessionID] = true
return c
}
func (c *sftpConnection) OpenConnection() error {
c.mu.Lock()
defer c.mu.Unlock()
return c.openConnNoLock()
}
func (c *sftpConnection) openConnNoLock() error {
if c.isConnected {
logger.Debug(c.logSender, "", "reusing connection")
return nil
}
logger.Debug(c.logSender, "", "try to open a new connection")
clientConfig := &ssh.ClientConfig{
User: fs.config.Username,
User: c.config.Username,
HostKeyCallback: func(_ string, _ net.Addr, key ssh.PublicKey) error {
fp := ssh.FingerprintSHA256(key)
if util.Contains(sftpFingerprints, fp) {
if allowSelfConnections == 0 {
fsLog(fs, logger.LevelError, "SFTP self connections not allowed")
logger.Log(logger.LevelError, c.logSender, "", "SFTP self connections not allowed")
return ErrSFTPLoop
}
if util.Contains(fs.config.forbiddenSelfUsernames, fs.config.Username) {
fsLog(fs, logger.LevelError, "SFTP loop or nested local SFTP folders detected, mount path %q, username %q, forbidden usernames: %+v",
fs.mountPath, fs.config.Username, fs.config.forbiddenSelfUsernames)
if util.Contains(c.config.forbiddenSelfUsernames, c.config.Username) {
logger.Log(logger.LevelError, c.logSender, "",
"SFTP loop or nested local SFTP folders detected, username %q, forbidden usernames: %+v",
c.config.Username, c.config.forbiddenSelfUsernames)
return ErrSFTPLoop
}
}
if len(fs.config.Fingerprints) > 0 {
for _, provided := range fs.config.Fingerprints {
if len(c.config.Fingerprints) > 0 {
for _, provided := range c.config.Fingerprints {
if provided == fp {
return nil
}
}
return fmt.Errorf("invalid fingerprint %#v", fp)
return fmt.Errorf("invalid fingerprint %q", fp)
}
fsLog(fs, logger.LevelWarn, "login without host key validation, please provide at least a fingerprint!")
logger.Log(logger.LevelWarn, c.logSender, "", "login without host key validation, please provide at least a fingerprint!")
return nil
},
Timeout: 10 * time.Second,
ClientVersion: fmt.Sprintf("SSH-2.0-SFTPGo_%v", version.Get().Version),
}
if fs.config.PrivateKey.GetPayload() != "" {
if c.config.PrivateKey.GetPayload() != "" {
var signer ssh.Signer
if fs.config.KeyPassphrase.GetPayload() != "" {
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(fs.config.PrivateKey.GetPayload()),
[]byte(fs.config.KeyPassphrase.GetPayload()))
var err error
if c.config.KeyPassphrase.GetPayload() != "" {
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(c.config.PrivateKey.GetPayload()),
[]byte(c.config.KeyPassphrase.GetPayload()))
} else {
signer, err = ssh.ParsePrivateKey([]byte(fs.config.PrivateKey.GetPayload()))
signer, err = ssh.ParsePrivateKey([]byte(c.config.PrivateKey.GetPayload()))
}
if err != nil {
fs.err <- err
return fmt.Errorf("sftpfs: unable to parse the private key: %w", err)
}
clientConfig.Auth = append(clientConfig.Auth, ssh.PublicKeys(signer))
}
if fs.config.Password.GetPayload() != "" {
clientConfig.Auth = append(clientConfig.Auth, ssh.Password(fs.config.Password.GetPayload()))
if c.config.Password.GetPayload() != "" {
clientConfig.Auth = append(clientConfig.Auth, ssh.Password(c.config.Password.GetPayload()))
}
// add more ciphers, KEXs and MACs, they are negotiated according to the order
clientConfig.Ciphers = []string{"aes128-gcm@openssh.com", "aes256-gcm@openssh.com", "chacha20-poly1305@openssh.com",
@ -895,52 +975,225 @@ func (fs *SFTPFs) createConnection() error {
clientConfig.MACs = []string{"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256",
"hmac-sha2-512-etm@openssh.com", "hmac-sha2-512",
"hmac-sha1", "hmac-sha1-96"}
fs.sshClient, err = ssh.Dial("tcp", fs.config.Endpoint, clientConfig)
sshClient, err := ssh.Dial("tcp", c.config.Endpoint, clientConfig)
if err != nil {
fsLog(fs, logger.LevelError, "unable to connect: %v", err)
fs.err <- err
return err
return fmt.Errorf("sftpfs: unable to connect: %w", err)
}
fs.sftpClient, err = sftp.NewClient(fs.sshClient)
sftpClient, err := sftp.NewClient(sshClient, c.getClientOptions()...)
if err != nil {
fsLog(fs, logger.LevelError, "unable to create SFTP client: %v", err)
fs.sshClient.Close()
fs.err <- err
return err
sshClient.Close()
return fmt.Errorf("sftpfs: unable to create SFTP client: %w", err)
}
if fs.config.DisableCouncurrentReads {
fsLog(fs, logger.LevelDebug, "disabling concurrent reads")
opt := sftp.UseConcurrentReads(false)
opt(fs.sftpClient) //nolint:errcheck
}
if fs.config.BufferSize > 0 {
fsLog(fs, logger.LevelDebug, "enabling concurrent writes")
opt := sftp.UseConcurrentWrites(true)
opt(fs.sftpClient) //nolint:errcheck
}
go fs.wait()
c.sshClient = sshClient
c.sftpClient = sftpClient
c.isConnected = true
go c.Wait()
return nil
}
func (fs *SFTPFs) wait() {
func (c *sftpConnection) getClientOptions() []sftp.ClientOption {
var options []sftp.ClientOption
if c.config.DisableCouncurrentReads {
options = append(options, sftp.UseConcurrentReads(false))
logger.Debug(c.logSender, "", "disabling concurrent reads")
}
if c.config.BufferSize > 0 {
options = append(options, sftp.UseConcurrentWrites(true))
logger.Debug(c.logSender, "", "enabling concurrent writes")
}
return options
}
func (c *sftpConnection) getClient() (*sftp.Client, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.isConnected {
return c.sftpClient, nil
}
err := c.openConnNoLock()
return c.sftpClient, err
}
func (c *sftpConnection) Wait() {
done := make(chan struct{})
go func() {
var watchdogInProgress atomic.Bool
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if watchdogInProgress.Load() {
logger.Error(c.logSender, "", "watchdog still in progress, closing hanging connection")
c.sshClient.Close()
return
}
go func() {
watchdogInProgress.Store(true)
defer watchdogInProgress.Store(false)
_, err := c.sftpClient.Getwd()
if err != nil {
logger.Error(c.logSender, "", "watchdog error: %v", err)
}
}()
case <-done:
logger.Debug(c.logSender, "", "quitting watchdog")
return
}
}
}()
// we wait on the sftp client otherwise if the channel is closed but not the connection
// we don't detect the event.
fs.err <- fs.sftpClient.Wait()
fsLog(fs, logger.LevelDebug, "sftp channel closed")
err := c.sftpClient.Wait()
logger.Log(logger.LevelDebug, c.logSender, "", "sftp channel closed: %v", err)
close(done)
fs.Lock()
defer fs.Unlock()
c.mu.Lock()
defer c.mu.Unlock()
if fs.sshClient != nil {
fs.sshClient.Close()
c.isConnected = false
if c.sshClient != nil {
c.sshClient.Close()
}
}
func (fs *SFTPFs) closed() error {
select {
case err := <-fs.err:
return err
default:
return nil
func (c *sftpConnection) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
logger.Debug(c.logSender, "", "closing connection")
var sftpErr, sshErr error
if c.sftpClient != nil {
sftpErr = c.sftpClient.Close()
}
if c.sshClient != nil {
sshErr = c.sshClient.Close()
}
if sftpErr != nil {
return sftpErr
}
c.isConnected = false
return sshErr
}
func (c *sftpConnection) AddSession(sessionID string) {
c.mu.Lock()
defer c.mu.Unlock()
c.sessions[sessionID] = true
logger.Debug(c.logSender, "", "added session %s, active sessions: %d", sessionID, len(c.sessions))
}
func (c *sftpConnection) RemoveSession(sessionID string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.sessions, sessionID)
logger.Debug(c.logSender, "", "removed session %s, active sessions: %d", sessionID, len(c.sessions))
if len(c.sessions) == 0 {
c.lastActivity = time.Now().UTC()
}
}
func (c *sftpConnection) ActiveSessions() int {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.sessions)
}
func (c *sftpConnection) GetLastActivity() time.Time {
c.mu.RLock()
defer c.mu.RUnlock()
if len(c.sessions) > 0 {
return time.Now().UTC()
}
logger.Debug(c.logSender, "", "last activity %s", c.lastActivity)
return c.lastActivity
}
type sftpConnectionsCache struct {
scheduler *cron.Cron
sync.RWMutex
items map[uint64]*sftpConnection
}
func newSFTPConnectionCache() *sftpConnectionsCache {
c := &sftpConnectionsCache{
scheduler: cron.New(),
items: make(map[uint64]*sftpConnection),
}
_, err := c.scheduler.AddFunc("@every 1m", c.Cleanup)
util.PanicOnError(err)
c.scheduler.Start()
return c
}
func (c *sftpConnectionsCache) Get(config *SFTPFsConfig, sessionID string) *sftpConnection {
partition := 0
key := config.getUniqueID(partition)
c.Lock()
defer c.Unlock()
var oldKey uint64
for {
if val, ok := c.items[key]; ok {
activeSessions := val.ActiveSessions()
if activeSessions < maxSessionsPerConnection || key == oldKey {
logger.Debug(logSenderSFTPCache, "",
"reusing connection for session ID %q, key: %d, active sessions %d, active connections: %d",
sessionID, key, activeSessions+1, len(c.items))
val.AddSession(sessionID)
return val
}
partition++
oldKey = key
key = config.getUniqueID(partition)
logger.Debug(logSenderSFTPCache, "",
"connection full, generated new key for partition: %d, active sessions: %d, key: %d, old key: %d",
partition, activeSessions, oldKey, key)
} else {
conn := newSFTPConnection(config, sessionID)
c.items[key] = conn
logger.Debug(logSenderSFTPCache, "",
"adding new connection for session ID %q, partition: %d, key: %d, active connections: %d",
sessionID, partition, key, len(c.items))
return conn
}
}
}
func (c *sftpConnectionsCache) Remove(key uint64) {
c.Lock()
defer c.Unlock()
if conn, ok := c.items[key]; ok {
delete(c.items, key)
logger.Debug(logSenderSFTPCache, "", "removed connection with key %d, active connections: %d", key, len(c.items))
defer conn.Close()
}
}
func (c *sftpConnectionsCache) Cleanup() {
c.RLock()
for k, conn := range c.items {
if val := conn.GetLastActivity(); val.Before(time.Now().Add(-30 * time.Second)) {
logger.Debug(conn.logSender, "", "removing inactive connection, last activity %s", val)
defer func(key uint64) {
c.Remove(key)
}(k)
}
}
c.RUnlock()
}

View file

@ -26,8 +26,8 @@ import (
"sync/atomic"
"time"
"github.com/drakkan/webdav"
"github.com/eikenb/pipeat"
"golang.org/x/net/webdav"
"github.com/drakkan/sftpgo/v2/internal/common"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
@ -84,6 +84,9 @@ type webDavFileInfo struct {
// ContentType implements webdav.ContentTyper interface
func (fi *webDavFileInfo) ContentType(ctx context.Context) (string, error) {
extension := path.Ext(fi.virtualPath)
if extension == "" || extension == ".dat" {
return "application/octet-stream", nil
}
contentType := mime.TypeByExtension(extension)
if contentType != "" {
return contentType, nil
@ -105,20 +108,19 @@ func (f *webDavFile) Readdir(count int) ([]os.FileInfo, error) {
if !f.Connection.User.HasPerm(dataprovider.PermListItems, f.GetVirtualPath()) {
return nil, f.Connection.GetPermissionDeniedError()
}
fileInfos, err := f.Connection.ListDir(f.GetVirtualPath())
entries, err := f.Connection.ListDir(f.GetVirtualPath())
if err != nil {
return nil, err
}
result := make([]os.FileInfo, 0, len(fileInfos))
for _, fileInfo := range fileInfos {
result = append(result, &webDavFileInfo{
FileInfo: fileInfo,
for idx, info := range entries {
entries[idx] = &webDavFileInfo{
FileInfo: info,
Fs: f.Fs,
virtualPath: path.Join(f.GetVirtualPath(), fileInfo.Name()),
fsPath: f.Fs.Join(f.GetFsPath(), fileInfo.Name()),
})
virtualPath: path.Join(f.GetVirtualPath(), info.Name()),
fsPath: f.Fs.Join(f.GetFsPath(), info.Name()),
}
}
return result, nil
return entries, nil
}
// Stat the handle
@ -131,7 +133,7 @@ func (f *webDavFile) Stat() (os.FileInfo, error) {
f.Unlock()
if f.GetType() == common.TransferUpload && errUpload == nil {
info := &webDavFileInfo{
FileInfo: vfs.NewFileInfo(f.GetFsPath(), false, f.BytesReceived.Load(), time.Unix(0, 0), false),
FileInfo: vfs.NewFileInfo(f.GetFsPath(), false, f.BytesReceived.Load(), time.Now(), false),
Fs: f.Fs,
virtualPath: f.GetVirtualPath(),
fsPath: f.GetFsPath(),
@ -154,33 +156,38 @@ func (f *webDavFile) Stat() (os.FileInfo, error) {
return fi, nil
}
func (f *webDavFile) checkFirstRead() error {
if !f.Connection.User.HasPerm(dataprovider.PermDownload, path.Dir(f.GetVirtualPath())) {
return f.Connection.GetPermissionDeniedError()
}
transferQuota := f.BaseTransfer.GetTransferQuota()
if !transferQuota.HasDownloadSpace() {
f.Connection.Log(logger.LevelInfo, "denying file read due to quota limits")
return f.Connection.GetReadQuotaExceededError()
}
if ok, policy := f.Connection.User.IsFileAllowed(f.GetVirtualPath()); !ok {
f.Connection.Log(logger.LevelWarn, "reading file %#v is not allowed", f.GetVirtualPath())
return f.Connection.GetErrorForDeniedFile(policy)
}
err := common.ExecutePreAction(f.Connection, common.OperationPreDownload, f.GetFsPath(), f.GetVirtualPath(), 0, 0)
if err != nil {
f.Connection.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", f.GetVirtualPath(), err)
return f.Connection.GetPermissionDeniedError()
}
f.readTryed.Store(true)
return nil
}
// Read reads the contents to downloads.
func (f *webDavFile) Read(p []byte) (n int, err error) {
if f.AbortTransfer.Load() {
return 0, errTransferAborted
}
if !f.readTryed.Load() {
if !f.Connection.User.HasPerm(dataprovider.PermDownload, path.Dir(f.GetVirtualPath())) {
return 0, f.Connection.GetPermissionDeniedError()
if err := f.checkFirstRead(); err != nil {
return 0, err
}
transferQuota := f.BaseTransfer.GetTransferQuota()
if !transferQuota.HasDownloadSpace() {
f.Connection.Log(logger.LevelInfo, "denying file read due to quota limits")
return 0, f.Connection.GetReadQuotaExceededError()
}
if ok, policy := f.Connection.User.IsFileAllowed(f.GetVirtualPath()); !ok {
f.Connection.Log(logger.LevelWarn, "reading file %#v is not allowed", f.GetVirtualPath())
return 0, f.Connection.GetErrorForDeniedFile(policy)
}
err := common.ExecutePreAction(f.Connection, common.OperationPreDownload, f.GetFsPath(), f.GetVirtualPath(), 0, 0)
if err != nil {
f.Connection.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", f.GetVirtualPath(), err)
return 0, f.Connection.GetPermissionDeniedError()
}
f.readTryed.Store(true)
}
f.Connection.UpdateLastActivity()
// the file is read sequentially we don't need to check for concurrent reads and so
@ -190,10 +197,16 @@ func (f *webDavFile) Read(p []byte) (n int, err error) {
f.TransferError(common.ErrOpUnsupported)
return 0, common.ErrOpUnsupported
}
_, r, cancelFn, e := f.Fs.Open(f.GetFsPath(), 0)
file, r, cancelFn, e := f.Fs.Open(f.GetFsPath(), 0)
f.Lock()
if e == nil {
f.reader = r
if file != nil {
f.File = file
f.writer = f.File
f.reader = f.File
} else if r != nil {
f.reader = r
}
f.BaseTransfer.SetCancelFn(cancelFn)
}
f.ErrTransfer = e
@ -263,18 +276,41 @@ func (f *webDavFile) updateTransferQuotaOnSeek() {
}
}
func (f *webDavFile) checkFile() error {
if f.File == nil && vfs.IsLocalOrUnbufferedSFTPFs(f.Fs) {
file, _, _, err := f.Fs.Open(f.GetFsPath(), 0)
if err != nil {
f.Connection.Log(logger.LevelWarn, "could not open file %q for seeking: %v",
f.GetFsPath(), err)
f.TransferError(err)
return err
}
f.File = file
f.reader = file
f.writer = file
}
return nil
}
func (f *webDavFile) seekFile(offset int64, whence int) (int64, error) {
ret, err := f.File.Seek(offset, whence)
if err != nil {
f.TransferError(err)
}
return ret, err
}
// Seek sets the offset for the next Read or Write on the writer to offset,
// interpreted according to whence: 0 means relative to the origin of the file,
// 1 means relative to the current offset, and 2 means relative to the end.
// It returns the new offset and an error, if any.
func (f *webDavFile) Seek(offset int64, whence int) (int64, error) {
f.Connection.UpdateLastActivity()
if err := f.checkFile(); err != nil {
return 0, err
}
if f.File != nil {
ret, err := f.File.Seek(offset, whence)
if err != nil {
f.TransferError(err)
}
return ret, err
return f.seekFile(offset, whence)
}
if f.GetType() == common.TransferDownload {
readOffset := f.startOffset + f.BytesSent.Load()

View file

@ -21,8 +21,7 @@ import (
"path"
"strings"
"github.com/eikenb/pipeat"
"golang.org/x/net/webdav"
"github.com/drakkan/webdav"
"github.com/drakkan/sftpgo/v2/internal/common"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
@ -134,25 +133,13 @@ func (c *Connection) OpenFile(ctx context.Context, name string, flag int, perm o
}
func (c *Connection) getFile(fs vfs.Fs, fsPath, virtualPath string) (webdav.File, error) {
var err error
var file vfs.File
var r *pipeat.PipeReaderAt
var cancelFn func()
// for cloud fs we open the file when we receive the first read to avoid to download the first part of
// the file if it was opened only to do a stat or a readdir and so it is not a real download
if vfs.IsLocalOrUnbufferedSFTPFs(fs) {
file, r, cancelFn, err = fs.Open(fsPath, 0)
if err != nil {
c.Log(logger.LevelError, "could not open file %#v for reading: %+v", fsPath, err)
return nil, c.GetFsError(fs, err)
}
}
baseTransfer := common.NewBaseTransfer(file, c.BaseConnection, cancelFn, fsPath, fsPath, virtualPath,
// we open the file when we receive the first read so we only open the file if necessary
baseTransfer := common.NewBaseTransfer(nil, c.BaseConnection, cancelFn, fsPath, fsPath, virtualPath,
common.TransferDownload, 0, 0, 0, 0, false, fs, c.GetTransferQuota())
return newWebDavFile(baseTransfer, nil, r), nil
return newWebDavFile(baseTransfer, nil, nil), nil
}
func (c *Connection) putFile(fs vfs.Fs, fsPath, virtualPath string) (webdav.File, error) {

View file

@ -30,10 +30,10 @@ import (
"testing"
"time"
"github.com/drakkan/webdav"
"github.com/eikenb/pipeat"
"github.com/sftpgo/sdk"
"github.com/stretchr/testify/assert"
"golang.org/x/net/webdav"
"github.com/drakkan/sftpgo/v2/internal/common"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
@ -286,7 +286,10 @@ func (fs *MockOsFs) Name() string {
// Open returns nil
func (fs *MockOsFs) Open(name string, offset int64) (vfs.File, *pipeat.PipeReaderAt, func(), error) {
return nil, fs.reader, nil, nil
if fs.reader != nil {
return nil, fs.reader, nil, nil
}
return fs.Fs.Open(name, offset)
}
// IsUploadResumeSupported returns true if resuming uploads is supported
@ -314,14 +317,18 @@ func (fs *MockOsFs) Rename(source, target string) error {
// GetMimeType returns the content type
func (fs *MockOsFs) GetMimeType(name string) (string, error) {
if fs.err != nil {
return "", fs.err
}
return "application/custom-mime", nil
}
func newMockOsFs(atomicUpload bool, connectionID, rootDir string, reader *pipeat.PipeReaderAt) vfs.Fs {
func newMockOsFs(atomicUpload bool, connectionID, rootDir string, reader *pipeat.PipeReaderAt, err error) vfs.Fs {
return &MockOsFs{
Fs: vfs.NewOsFs(connectionID, rootDir, ""),
isAtomicUploadSupported: atomicUpload,
reader: reader,
err: err,
}
}
@ -552,38 +559,31 @@ func TestFileAccessErrors(t *testing.T) {
missingPath := "missing path"
fsMissingPath := filepath.Join(user.HomeDir, missingPath)
err := connection.RemoveAll(ctx, missingPath)
if assert.Error(t, err) {
assert.EqualError(t, err, os.ErrNotExist.Error())
}
_, err = connection.getFile(fs, fsMissingPath, missingPath)
if assert.Error(t, err) {
assert.EqualError(t, err, os.ErrNotExist.Error())
}
_, err = connection.getFile(fs, fsMissingPath, missingPath)
if assert.Error(t, err) {
assert.EqualError(t, err, os.ErrNotExist.Error())
}
assert.ErrorIs(t, err, os.ErrNotExist)
davFile, err := connection.getFile(fs, fsMissingPath, missingPath)
assert.NoError(t, err)
buf := make([]byte, 64)
_, err = davFile.Read(buf)
assert.ErrorIs(t, err, os.ErrNotExist)
err = davFile.Close()
assert.ErrorIs(t, err, os.ErrNotExist)
p := filepath.Join(user.HomeDir, "adir", missingPath)
_, err = connection.handleUploadToNewFile(fs, p, p, path.Join("adir", missingPath))
if assert.Error(t, err) {
assert.EqualError(t, err, os.ErrNotExist.Error())
}
assert.ErrorIs(t, err, os.ErrNotExist)
_, err = connection.handleUploadToExistingFile(fs, p, "_"+p, 0, path.Join("adir", missingPath))
if assert.Error(t, err) {
assert.ErrorIs(t, err, os.ErrNotExist)
}
fs = newMockOsFs(false, fs.ConnectionID(), user.HomeDir, nil)
fs = newMockOsFs(false, fs.ConnectionID(), user.HomeDir, nil, nil)
_, err = connection.handleUploadToExistingFile(fs, p, p, 0, path.Join("adir", missingPath))
if assert.Error(t, err) {
assert.ErrorIs(t, err, os.ErrNotExist)
}
assert.ErrorIs(t, err, os.ErrNotExist)
f, err := os.CreateTemp("", "temp")
assert.NoError(t, err)
err = f.Close()
assert.NoError(t, err)
davFile, err := connection.handleUploadToExistingFile(fs, f.Name(), f.Name(), 123, f.Name())
davFile, err = connection.handleUploadToExistingFile(fs, f.Name(), f.Name(), 123, f.Name())
if assert.NoError(t, err) {
transfer := davFile.(*webDavFile)
transfers := connection.GetTransfers()
@ -650,9 +650,9 @@ func TestContentType(t *testing.T) {
}
testFilePath := filepath.Join(user.HomeDir, testFile)
ctx := context.Background()
baseTransfer := common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile,
baseTransfer := common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile+".unknown",
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})
fs = newMockOsFs(false, fs.ConnectionID(), user.GetHomeDir(), nil)
fs = newMockOsFs(false, fs.ConnectionID(), user.GetHomeDir(), nil, nil)
err := os.WriteFile(testFilePath, []byte(""), os.ModePerm)
assert.NoError(t, err)
davFile := newWebDavFile(baseTransfer, nil, nil)
@ -668,6 +668,8 @@ func TestContentType(t *testing.T) {
err = davFile.Close()
assert.NoError(t, err)
baseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile+".unknown1",
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})
davFile = newWebDavFile(baseTransfer, nil, nil)
davFile.Fs = vfs.NewOsFs("id", user.HomeDir, "")
fi, err = davFile.Stat()
@ -679,9 +681,53 @@ func TestContentType(t *testing.T) {
err = davFile.Close()
assert.NoError(t, err)
fi.(*webDavFileInfo).fsPath = "missing"
_, err = fi.(*webDavFileInfo).ContentType(ctx)
assert.EqualError(t, err, webdav.ErrNotImplemented.Error())
baseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile,
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})
davFile = newWebDavFile(baseTransfer, nil, nil)
davFile.Fs = vfs.NewOsFs("id", user.HomeDir, "")
fi, err = davFile.Stat()
if assert.NoError(t, err) {
ctype, err := fi.(*webDavFileInfo).ContentType(ctx)
assert.NoError(t, err)
assert.Equal(t, "application/octet-stream", ctype)
}
err = davFile.Close()
assert.NoError(t, err)
for i := 0; i < 2; i++ {
// the second time the cache will be used
baseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile+".custom",
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})
davFile = newWebDavFile(baseTransfer, nil, nil)
davFile.Fs = vfs.NewOsFs("id", user.HomeDir, "")
fi, err = davFile.Stat()
if assert.NoError(t, err) {
ctype, err := fi.(*webDavFileInfo).ContentType(ctx)
assert.NoError(t, err)
assert.Equal(t, "text/plain; charset=utf-8", ctype)
}
err = davFile.Close()
assert.NoError(t, err)
}
baseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile+".unknown2",
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})
fs = newMockOsFs(false, fs.ConnectionID(), user.GetHomeDir(), nil, os.ErrInvalid)
davFile = newWebDavFile(baseTransfer, nil, nil)
davFile.Fs = fs
fi, err = davFile.Stat()
if assert.NoError(t, err) {
ctype, err := fi.(*webDavFileInfo).ContentType(ctx)
assert.EqualError(t, err, webdav.ErrNotImplemented.Error(), "unexpected content type %q", ctype)
}
cache := mimeCache{
maxSize: 10,
mimeTypes: map[string]string{},
}
cache.addMimeToCache("", "")
cache.RLock()
assert.Len(t, cache.mimeTypes, 0)
cache.RUnlock()
err = os.Remove(testFilePath)
assert.NoError(t, err)
@ -750,7 +796,7 @@ func TestTransferReadWriteErrors(t *testing.T) {
r, w, err = pipeat.Pipe()
assert.NoError(t, err)
mockFs := newMockOsFs(false, fs.ConnectionID(), user.HomeDir, r)
mockFs := newMockOsFs(false, fs.ConnectionID(), user.HomeDir, r, nil)
baseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile,
common.TransferDownload, 0, 0, 0, 0, false, mockFs, dataprovider.TransferQuota{})
davFile = newWebDavFile(baseTransfer, nil, nil)
@ -790,7 +836,7 @@ func TestTransferSeek(t *testing.T) {
}
user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny}
fs := vfs.NewOsFs("connID", user.HomeDir, "")
fs := newMockOsFs(true, "connID", user.HomeDir, nil, nil)
connection := &Connection{
BaseConnection: common.NewBaseConnection(fs.ConnectionID(), common.ProtocolWebDAV, "", "", user),
}
@ -831,6 +877,8 @@ func TestTransferSeek(t *testing.T) {
res, err := davFile.Seek(0, io.SeekStart)
assert.NoError(t, err)
assert.Equal(t, int64(0), res)
err = davFile.Close()
assert.NoError(t, err)
davFile.Connection.RemoveTransfer(davFile.BaseTransfer)
davFile = newWebDavFile(baseTransfer, nil, nil)
@ -838,7 +886,9 @@ func TestTransferSeek(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, int64(len(testFileContents)), res)
err = davFile.updateStatInfo()
assert.Nil(t, err)
assert.NoError(t, err)
err = davFile.Close()
assert.NoError(t, err)
baseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath+"1", testFilePath+"1", testFile,
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{AllowedTotalSize: 100})
@ -847,26 +897,42 @@ func TestTransferSeek(t *testing.T) {
assert.True(t, fs.IsNotExist(err))
davFile.Connection.RemoveTransfer(davFile.BaseTransfer)
fs = vfs.NewOsFs(fs.ConnectionID(), user.GetHomeDir(), "")
baseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath+"1", testFilePath+"1", testFile,
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{AllowedTotalSize: 100})
davFile = newWebDavFile(baseTransfer, nil, nil)
_, err = davFile.Seek(0, io.SeekEnd)
assert.True(t, fs.IsNotExist(err))
davFile.Connection.RemoveTransfer(davFile.BaseTransfer)
baseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile,
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{AllowedTotalSize: 100})
davFile = newWebDavFile(baseTransfer, nil, nil)
davFile.reader = f
davFile.Fs = newMockOsFs(true, fs.ConnectionID(), user.GetHomeDir(), nil)
r, _, err := pipeat.Pipe()
assert.NoError(t, err)
davFile.Fs = newMockOsFs(true, fs.ConnectionID(), user.GetHomeDir(), r, nil)
res, err = davFile.Seek(2, io.SeekStart)
assert.NoError(t, err)
assert.Equal(t, int64(2), res)
err = davFile.Close()
assert.NoError(t, err)
r, _, err = pipeat.Pipe()
assert.NoError(t, err)
davFile = newWebDavFile(baseTransfer, nil, nil)
davFile.Fs = newMockOsFs(true, fs.ConnectionID(), user.GetHomeDir(), nil)
davFile.Fs = newMockOsFs(true, fs.ConnectionID(), user.GetHomeDir(), r, nil)
res, err = davFile.Seek(2, io.SeekEnd)
assert.NoError(t, err)
assert.Equal(t, int64(5), res)
err = davFile.Close()
assert.NoError(t, err)
baseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath+"1", testFilePath+"1", testFile,
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{AllowedTotalSize: 100})
davFile = newWebDavFile(baseTransfer, nil, nil)
davFile.Fs = newMockOsFs(true, fs.ConnectionID(), user.GetHomeDir(), nil)
davFile.Fs = newMockOsFs(true, fs.ConnectionID(), user.GetHomeDir(), nil, nil)
res, err = davFile.Seek(2, io.SeekEnd)
assert.True(t, fs.IsNotExist(err))
assert.Equal(t, int64(0), res)

View file

@ -28,10 +28,10 @@ import (
"runtime/debug"
"time"
"github.com/drakkan/webdav"
"github.com/go-chi/chi/v5/middleware"
"github.com/rs/cors"
"github.com/rs/xid"
"golang.org/x/net/webdav"
"github.com/drakkan/sftpgo/v2/internal/common"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"

View file

@ -620,7 +620,8 @@ func TestBasicHandling(t *testing.T) {
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
assert.Len(t, common.Connections.GetStats(), 0)
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
1*time.Second, 100*time.Millisecond)
status := webdavd.GetStatus()
assert.True(t, status.IsActive)
}
@ -701,7 +702,8 @@ func TestBasicHandlingCryptFs(t *testing.T) {
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
assert.Len(t, common.Connections.GetStats(), 0)
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
1*time.Second, 100*time.Millisecond)
}
func TestLoginEmptyPassword(t *testing.T) {
@ -922,12 +924,13 @@ func TestPropPatch(t *testing.T) {
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
assert.Len(t, common.Connections.GetStats(), 0)
}
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
1*time.Second, 100*time.Millisecond)
}
func TestLoginInvalidPwd(t *testing.T) {
@ -1308,7 +1311,8 @@ func TestPreDownloadHook(t *testing.T) {
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
assert.Len(t, common.Connections.GetStats(), 0)
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
1*time.Second, 100*time.Millisecond)
common.Config.Actions.ExecuteOn = []string{common.OperationPreDownload}
common.Config.Actions.Hook = preDownloadPath
@ -1357,7 +1361,8 @@ func TestPreUploadHook(t *testing.T) {
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
assert.Len(t, common.Connections.GetStats(), 0)
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
1*time.Second, 100*time.Millisecond)
common.Config.Actions.ExecuteOn = oldExecuteOn
common.Config.Actions.Hook = oldHook
@ -1419,7 +1424,8 @@ func TestMaxConnections(t *testing.T) {
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
assert.Len(t, common.Connections.GetStats(), 0)
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
1*time.Second, 100*time.Millisecond)
common.Config.MaxTotalConnections = oldValue
}
@ -1450,7 +1456,8 @@ func TestMaxPerHostConnections(t *testing.T) {
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
assert.Len(t, common.Connections.GetStats(), 0)
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
1*time.Second, 100*time.Millisecond)
common.Config.MaxPerHostConnections = oldValue
}
@ -1475,7 +1482,8 @@ func TestMaxSessions(t *testing.T) {
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
assert.Len(t, common.Connections.GetStats(), 0)
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
1*time.Second, 100*time.Millisecond)
}
func TestLoginWithIPilters(t *testing.T) {
@ -1898,6 +1906,11 @@ func TestClientClose(t *testing.T) {
common.Connections.Close(stat.ConnectionID)
}
wg.Wait()
// for the sftp user a stat is done after the failed upload and
// this triggers a new connection
for _, stat := range common.Connections.GetStats() {
common.Connections.Close(stat.ConnectionID)
}
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
1*time.Second, 100*time.Millisecond)
@ -2104,6 +2117,19 @@ func TestBytesRangeRequests(t *testing.T) {
assert.Equal(t, "file", string(bodyBytes))
}
}
// seek on a missing file
remotePath = fmt.Sprintf("http://%v/%v", webDavServerAddr, testFileName+"_missing")
req, err = http.NewRequest(http.MethodGet, remotePath, nil)
if assert.NoError(t, err) {
httpClient := httpclient.GetHTTPClient()
req.SetBasicAuth(user.Username, defaultPassword)
req.Header.Set("Range", "bytes=5-")
resp, err := httpClient.Do(req)
if assert.NoError(t, err) {
defer resp.Body.Close()
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
}
}
err = os.Remove(testFilePath)
assert.NoError(t, err)
@ -2326,28 +2352,24 @@ func TestOsErrors(t *testing.T) {
info, err := client.Stat(vdir)
assert.NoError(t, err)
assert.True(t, info.IsDir())
// now remove the folder mapped to vdir. It should not appear in directory listing
// now remove the folder mapped to vdir. It still appear in directory listing
// virtual folders are automatically added
err = os.RemoveAll(mappedPath)
assert.NoError(t, err)
files, err = client.ReadDir(".")
assert.NoError(t, err)
assert.Len(t, files, 0)
assert.Len(t, files, 1)
err = createTestFile(filepath.Join(user.GetHomeDir(), testFileName), 32768)
assert.NoError(t, err)
files, err = client.ReadDir(".")
assert.NoError(t, err)
if assert.Len(t, files, 1) {
assert.Equal(t, testFileName, files[0].Name())
}
if runtime.GOOS != osWindows {
// if the file cannot be accessed it should not appear in directory listing
err = os.Chmod(filepath.Join(user.GetHomeDir(), testFileName), 0001)
assert.NoError(t, err)
files, err = client.ReadDir(".")
assert.NoError(t, err)
assert.Len(t, files, 0)
err = os.Chmod(filepath.Join(user.GetHomeDir(), testFileName), os.ModePerm)
assert.NoError(t, err)
if assert.Len(t, files, 2) {
var names []string
for _, info := range files {
names = append(names, info.Name())
}
assert.Contains(t, names, testFileName)
assert.Contains(t, names, "vdir")
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
@ -2803,9 +2825,18 @@ func TestSFTPLoopVirtualFolders(t *testing.T) {
contents, err := client.ReadDir("/")
assert.NoError(t, err)
if assert.Len(t, contents, 1) {
assert.Equal(t, testDir, contents[0].Name())
assert.True(t, contents[0].IsDir())
if assert.Len(t, contents, 2) {
expected := 0
for _, info := range contents {
switch info.Name() {
case testDir, "vdir":
assert.True(t, info.IsDir())
expected++
default:
t.Errorf("unexpected file/dir %q", info.Name())
}
}
assert.Equal(t, expected, 2)
}
_, err = httpdtest.RemoveUser(user1, http.StatusOK)
@ -2922,7 +2953,8 @@ func TestNestedVirtualFolders(t *testing.T) {
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
assert.Len(t, common.Connections.GetStats(), 0)
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
1*time.Second, 100*time.Millisecond)
}
func checkBasicFunc(client *gowebdav.Client) error {

View file

@ -27,7 +27,7 @@ info:
SFTPGo supports groups to simplify the administration of multiple accounts by letting you assign settings once to a group, instead of multiple times to each individual user.
The SFTPGo WebClient allows end users to change their credentials, browse and manage their files in the browser and setup two-factor authentication which works with Authy, Google Authenticator and other compatible apps.
From the WebClient each authorized user can also create HTTP/S links to externally share files and folders securely, by setting limits to the number of downloads/uploads, protecting the share with a password, limiting access by source IP address, setting an automatic expiration date.
version: 2.4.0-dev
version: 2.4.2
contact:
name: API support
url: 'https://github.com/drakkan/sftpgo'

View file

@ -1,6 +1,6 @@
#!/bin/bash
NFPM_VERSION=2.20.0
NFPM_VERSION=2.22.1
NFPM_ARCH=${NFPM_ARCH:-amd64}
if [ -z ${SFTPGO_VERSION} ]
then
@ -75,13 +75,13 @@ contents:
dst: "/lib/systemd/system/sftpgo.service"
- src: "${BASE_DIR}/templates/*"
dst: "/usr/share/sftpgo/templates/"
dst: "/usr/share/sftpgo/templates"
- src: "${BASE_DIR}/static/*"
dst: "/usr/share/sftpgo/static/"
dst: "/usr/share/sftpgo/static"
- src: "${BASE_DIR}/openapi/*"
dst: "/usr/share/sftpgo/openapi/"
dst: "/usr/share/sftpgo/openapi"
- src: "./sftpgo.json"
dst: "/etc/sftpgo/sftpgo.json"

View file

@ -84,6 +84,7 @@
"host_keys": [],
"host_certificates": [],
"host_key_algorithms": [],
"moduli": [],
"kex_algorithms": [],
"ciphers": [],
"macs": [],

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -72,7 +72,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
<div class="form-group">
<input type="password" class="form-control form-control-user-custom"
id="inputPassword" name="password" placeholder="New Password" required>
id="inputPassword" name="password" placeholder="New Password" autocomplete="new-password" required>
</div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary btn-user-custom btn-block">

View file

@ -73,7 +73,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="form-group row">
<label for="idPassword" class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idPassword" name="password" placeholder=""
<input type="password" class="form-control" id="idPassword" name="password" placeholder="" autocomplete="new-password"
{{if not .IsAdd}}aria-describedby="pwdHelpBlock" {{end}}>
{{if not .IsAdd}}
<small id="pwdHelpBlock" class="form-text text-muted">

View file

@ -73,11 +73,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
<div class="form-group">
<input type="password" class="form-control form-control-user-custom" id="inputPassword"
name="password" placeholder="Password" required>
name="password" placeholder="Password" autocomplete="new-password" required>
</div>
<div class="form-group">
<input type="password" class="form-control form-control-user-custom" id="inputConfirmPassword"
name="confirm_password" placeholder="Repeat password" required>
name="confirm_password" placeholder="Repeat password" autocomplete="new-password" required>
</div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary btn-user-custom btn-block">

View file

@ -33,21 +33,21 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="form-group row">
<label for="idCurrentPassword" class="col-sm-2 col-form-label">Current password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idCurrentPassword" name="current_password" required>
<input type="password" class="form-control" id="idCurrentPassword" name="current_password" autocomplete="new-password" required>
</div>
</div>
<div class="form-group row">
<label for="idNewPassword1" class="col-sm-2 col-form-label">New password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idNewPassword1" name="new_password1" required>
<input type="password" class="form-control" id="idNewPassword1" name="new_password1" autocomplete="new-password" required>
</div>
</div>
<div class="form-group row">
<label for="idNewPassword2" class="col-sm-2 col-form-label">Confirm password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idNewPassword2" name="new_password2" required>
<input type="password" class="form-control" id="idNewPassword2" name="new_password2" autocomplete="new-password" required>
</div>
</div>

View file

@ -97,7 +97,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="col-sm-2"></div>
<label for="idHTTPPassword" class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-3">
<input type="password" class="form-control" id="idHTTPPassword" name="http_password" placeholder=""
<input type="password" class="form-control" id="idHTTPPassword" name="http_password" placeholder="" autocomplete="new-password"
value="{{if .Action.Options.HTTPConfig.Password.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.Action.Options.HTTPConfig.Password.GetPayload}}{{end}}">
</div>
</div>
@ -669,6 +669,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<p>
<span class="shortcut"><b>{{`{{VirtualPath}}`}}</b></span> => Path seen by SFTPGo users, for example "/adir/afile.txt".
</p>
<p>
<span class="shortcut"><b>{{`{{VirtualDirPath}}`}}</b></span> => Parent directory for VirtualPath, for example if VirtualPath is "/adir/afile.txt", VirtualDirPath is "/adir".
</p>
<p>
<span class="shortcut"><b>{{`{{FsPath}}`}}</b></span> => Full filesystem path, for example "/user/homedir/adir/afile.txt" or "C:/data/user/homedir/adir/afile.txt" on Windows.
</p>
@ -681,6 +684,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<p>
<span class="shortcut"><b>{{`{{VirtualTargetPath}}`}}</b></span> => Virtual target path for renames.
</p>
<p>
<span class="shortcut"><b>{{`{{VirtualTargetDirPath}}`}}</b></span> => Parent directory for VirtualTargetPath.
</p>
<p>
<span class="shortcut"><b>{{`{{TargetName}}`}}</b></span> => Target object name for renames.
</p>
<p>
<span class="shortcut"><b>{{`{{FsTargetPath}}`}}</b></span> => Full filesystem target path for renames.
</p>

View file

@ -76,7 +76,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="col-sm-2"></div>
<label for="idS3AccessSecret" class="col-sm-2 col-form-label">Access Secret</label>
<div class="col-sm-3">
<input type="password" class="form-control" id="idS3AccessSecret" name="s3_access_secret" placeholder=""
<input type="password" class="form-control" id="idS3AccessSecret" name="s3_access_secret" placeholder="" autocomplete="new-password"
value="{{if .S3Config.AccessSecret.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.S3Config.AccessSecret.GetPayload}}{{end}}">
</div>
</div>
@ -285,7 +285,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="form-group row fsconfig fsconfig-azblobfs">
<label for="idAzAccountKey" class="col-sm-2 col-form-label">Account Key</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idAzAccountKey" name="az_account_key" placeholder=""
<input type="password" class="form-control" id="idAzAccountKey" name="az_account_key" placeholder="" autocomplete="new-password"
value="{{if .AzBlobConfig.AccountKey.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.AzBlobConfig.AccountKey.GetPayload}}{{end}}">
</div>
</div>
@ -294,7 +294,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<label for="idAzSASURL" class="col-sm-2 col-form-label">SAS URL</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idAzSASURL" name="az_sas_url" placeholder="" aria-describedby="AzSASURLHelpBlock"
value="{{if .AzBlobConfig.SASURL.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.AzBlobConfig.SASURL.GetPayload}}{{end}}">
autocomplete="new-password" value="{{if .AzBlobConfig.SASURL.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.AzBlobConfig.SASURL.GetPayload}}{{end}}">
<small id="AzSASURLHelpBlock" class="form-text text-muted">
Shared Access Signature URL can be used instead of account name/key
</small>
@ -389,7 +389,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<label for="idCryptPassphrase" class="col-sm-2 col-form-label">Passphrase</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idCryptPassphrase" name="crypt_passphrase"
placeholder="" aria-describedby="CryptPassphraseHelpBlock"
placeholder="" autocomplete="new-password" aria-describedby="CryptPassphraseHelpBlock"
value="{{if .CryptConfig.Passphrase.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.CryptConfig.Passphrase.GetPayload}}{{end}}">
<small id="CryptPassphraseHelpBlock" class="form-text text-muted">
Passphrase to derive the per-object encryption key
@ -426,7 +426,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="col-sm-2"></div>
<label for="idSFTPPassword" class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-3">
<input type="password" class="form-control" id="idSFTPPassword" name="sftp_password" placeholder=""
<input type="password" class="form-control" id="idSFTPPassword" name="sftp_password" placeholder="" autocomplete="new-password"
value="{{if .SFTPConfig.Password.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.SFTPConfig.Password.GetPayload}}{{end}}">
</div>
</div>
@ -442,7 +442,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="form-group row fsconfig fsconfig-sftpfs">
<label for="idSFTPPassphrase" class="col-sm-2 col-form-label">Key Passphrase</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idSFTPPassphrase" name="sftp_key_passphrase" placeholder=""
<input type="password" class="form-control" id="idSFTPPassphrase" name="sftp_key_passphrase" autocomplete="new-password" placeholder=""
value="{{if .SFTPConfig.KeyPassphrase.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.SFTPConfig.KeyPassphrase.GetPayload}}{{end}}"
aria-describedby="SFTPPassphraseHelpBlock">
<small id="SFTPPassphraseHelpBlock" class="form-text text-muted">
@ -509,7 +509,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="col-sm-2"></div>
<label for="idHTTPPassword" class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-3">
<input type="password" class="form-control" id="idHTTPPassword" name="http_password" placeholder=""
<input type="password" class="form-control" id="idHTTPPassword" name="http_password" autocomplete="new-password" placeholder=""
value="{{if .HTTPConfig.Password.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.HTTPConfig.Password.GetPayload}}{{end}}">
</div>
</div>
@ -517,7 +517,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="form-group row fsconfig fsconfig-httpfs">
<label for="idHTTPAPIKey" class="col-sm-2 col-form-label">API Key</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idHTTPAPIKey" name="http_api_key" placeholder=""
<input type="password" class="form-control" id="idHTTPAPIKey" name="http_api_key" autocomplete="new-password" placeholder=""
value="{{if .HTTPConfig.APIKey.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.HTTPConfig.APIKey.GetPayload}}{{end}}">
</div>
</div>

View file

@ -26,7 +26,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="card-body text-form-error">{{.Error}}</div>
</div>
{{end}}
<form id="login_form" action="{{.CurrentURL}}" method="POST" autocomplete="off"
<form id="login_form" action="{{.CurrentURL}}" method="POST"
class="user-custom">
{{if not .FormDisabled}}
<div class="form-group">
@ -35,7 +35,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
<div class="form-group">
<input type="password" class="form-control form-control-user-custom"
id="inputPassword" name="password" placeholder="Password" required>
id="inputPassword" name="password" placeholder="Password" autocomplete="current-password" required>
{{if .ForgotPwdURL}}
<div class="text-right">
<a class="small" href="{{.ForgotPwdURL}}">Forgot password?</a>

View file

@ -71,7 +71,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<input type="text" class="form-control" id="idTplUsername0" name="tpl_username" placeholder="Username" maxlength="255">
</div>
<div class="form-group col-md-3">
<input type="password" class="form-control" id="idTplPassword0" name="tpl_password" placeholder="Password" maxlength="255">
<input type="password" class="form-control" id="idTplPassword0" name="tpl_password" placeholder="Password" autocomplete="new-password">
</div>
<div class="form-group col-md-5">
<textarea class="form-control" id="idTplPublicKey0" name="tpl_public_keys" rows="5"
@ -108,7 +108,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="form-group row">
<label for="idPassword" class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idPassword" name="password" value="{{.User.Password}}" placeholder="">
<input type="password" class="form-control" id="idPassword" name="password" value="{{.User.Password}}" placeholder="" autocomplete="new-password">
</div>
</div>
@ -1057,6 +1057,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
showClear: false,
showClose: true,
showToday: false
},
widgetPositioning: {
horizontal: 'auto',
vertical: 'bottom'
}
});
@ -1120,7 +1124,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<input type="text" class="form-control" id="idTplUsername${index}" name="tpl_username" placeholder="Username" maxlength="255">
</div>
<div class="form-group col-md-3">
<input type="password" class="form-control" id="idTplPassword${index}" name="tpl_password" placeholder="Password" maxlength="255">
<input type="password" class="form-control" id="idTplPassword${index}" name="tpl_password" placeholder="Password" autocomplete="new-password">
</div>
<div class="form-group col-md-5">
<textarea class="form-control" id="idTplPublicKey${index}" name="tpl_public_keys" rows="5"

View file

@ -33,21 +33,21 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="form-group row">
<label for="idCurrentPassword" class="col-sm-2 col-form-label">Current password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idCurrentPassword" name="current_password" required>
<input type="password" class="form-control" id="idCurrentPassword" name="current_password" autocomplete="new-password" required>
</div>
</div>
<div class="form-group row">
<label for="idNewPassword1" class="col-sm-2 col-form-label">New password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idNewPassword1" name="new_password1" required>
<input type="password" class="form-control" id="idNewPassword1" name="new_password1" autocomplete="new-password" required>
</div>
</div>
<div class="form-group row">
<label for="idNewPassword2" class="col-sm-2 col-form-label">Confirm password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idNewPassword2" name="new_password2" required>
<input type="password" class="form-control" id="idNewPassword2" name="new_password2" autocomplete="new-password" required>
</div>
</div>

View file

@ -25,6 +25,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<link href="{{.StaticURL}}/vendor/datatables/dataTables.checkboxes.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/lightbox2/css/lightbox.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/video-js/video-js.min.css" rel="stylesheet" />
<link href="{{.StaticURL}}/vendor/filepond/filepond.min.css" rel="stylesheet" />
<style>
div.dataTables_wrapper span.selected-info,
div.dataTables_wrapper span.selected-item {
@ -48,7 +49,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="card-body text-form-error">{{.Error}}</div>
</div>
{{end}}
<div class="table-responsive">
<div id="tableContainer" class="table-responsive">
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0">
<thead>
<tr>
@ -110,7 +111,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
<form id="upload_files_form" action="{{.FilesURL}}?path={{.CurrentDir}}" method="POST" enctype="multipart/form-data">
<div class="modal-body">
<input type="file" class="form-control-file" id="files_name" name="filenames" required multiple>
<div id="uploadErrorMsg" class="card mb-4 border-left-warning" style="display: none;">
<div id="uploadErrorTxt" class="card-body text-form-error"></div>
</div>
<input type="file" id="files_name" name="filenames" required multiple>
</div>
<div class="modal-footer">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
@ -229,6 +233,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<script src="{{.StaticURL}}/vendor/codemirror/codemirror.js"></script>
<script src="{{.StaticURL}}/vendor/codemirror/meta.js"></script>
<script src="{{.StaticURL}}/vendor/video-js/video.min.js"></script>
<script src="{{.StaticURL}}/vendor/filepond/filepond.min.js"></script>
{{if .HasIntegrations}}
<script type="text/javascript">
var childReference = null;
@ -530,6 +535,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
return "far fa-file-code";
case "zip":
case "zipx":
case "7z":
case "rar":
case "tar":
case "gz":
@ -652,6 +658,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
});
}
const isDirectoryEntry = item => isEntry(item) && (getAsEntry(item) || {}).isDirectory;
const isEntry = item => 'webkitGetAsEntry' in item;
const getAsEntry = item => item.webkitGetAsEntry();
$(document).ready(function () {
player = videojs('video_player', {
controls: true,
@ -671,6 +681,72 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
$('#spinnerModal').modal('hide');
}
});
{{if .CanAddFiles}}
FilePond.create(document.getElementById("files_name"),{
allowMultiple: true,
name: 'filenames',
maxFiles: 30,
credits: false,
required: true,
onwarning: function(error){
if (error.code == 0){
$('#uploadErrorTxt').text('You can upload a maximum of 30 files');
$('#uploadErrorMsg').show();
setTimeout(function () {
$('#uploadErrorMsg').hide();
}, 10000);
}
},
beforeAddFile: (fileItem) => new Promise(resolve => {
let num = 0;
FilePond.find(document.getElementById("files_name")).getFiles().forEach(function(val){
if (val.filename == fileItem.filename){
num++;
}
});
resolve(num == 1);
})
});
$('#tableContainer').on("dragover", function(ev){
ev.preventDefault();
$('#tableContainer').css('opacity','0.5');
});
$('#tableContainer').on("dragend dragleave", function(ev){
ev.preventDefault();
$('#tableContainer').css('opacity','1');
});
$('#tableContainer').on("drop", function(ev){
ev.preventDefault();
$('#tableContainer').css('opacity','1');
let filesDropped = false;
if (ev.originalEvent.dataTransfer.items) {
[...ev.originalEvent.dataTransfer.items].forEach((item, i) => {
if (item.kind === 'file') {
// if this is a directory just open the upload dialog
if (!isDirectoryEntry(item)){
FilePond.find(document.getElementById("files_name")).addFile(item.getAsFile());
}
filesDropped = true;
}
});
} else {
[...ev.originalEvent.dataTransfer.files].forEach((file, i) => {
FilePond.find(document.getElementById("files_name")).addFile(file);
filesDropped = true;
});
}
if (filesDropped && !$('#uploadFilesModal').hasClass('show')){
$('#uploadFilesModal').modal('show');
}
});
{{end}}
$("#create_dir_form").submit(function (event) {
event.preventDefault();
$('#createDirModal').modal('hide');
@ -712,7 +788,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
keepAlive();
var keepAliveTimer = setInterval(keepAlive, 300000);
var files = $("#files_name")[0].files;
var files = FilePond.find(document.getElementById("files_name")).getFiles();
var has_errors = false;
var index = 0;
var success = 0;
@ -738,7 +814,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
var errorMessage = "Error uploading files";
let response;
try {
var f = files[index];
var f = files[index].file;
var uploadPath = '{{.FileURL}}?path={{.CurrentDir}}'+encodeURIComponent("/"+f.name);
var lastModified;
try {
@ -887,7 +963,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
name: 'addFiles',
titleAttr: "Upload files",
action: function (e, dt, node, config) {
document.getElementById("files_name").value = null;
//FilePond.find(document.getElementById("files_name")).removeFiles();
$('#uploadFilesModal').modal('show');
},
enabled: true

View file

@ -23,7 +23,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="card-body text-form-error">{{.Error}}</div>
</div>
{{end}}
<form id="login_form" action="{{.CurrentURL}}" method="POST" autocomplete="off"
<form id="login_form" action="{{.CurrentURL}}" method="POST"
class="user-custom">
{{if not .FormDisabled}}
<div class="form-group">
@ -32,7 +32,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
<div class="form-group">
<input type="password" class="form-control form-control-user-custom"
id="inputPassword" name="password" placeholder="Password" required>
id="inputPassword" name="password" placeholder="Password" autocomplete="current-password" required>
{{if .ForgotPwdURL}}
<div class="text-right">
<a class="small" href="{{.ForgotPwdURL}}">Forgot password?</a>

View file

@ -102,7 +102,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="form-group row">
<label for="idPassword" class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idPassword" name="password" placeholder=""
<input type="password" class="form-control" id="idPassword" name="password" autocomplete="new-password" placeholder=""
value="{{.Share.Password}}" aria-describedby="passwordHelpBlock">
<small id="passwordHelpBlock" class="form-text text-muted">
If set the share will be password-protected

View file

@ -23,6 +23,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/dataTables.checkboxes.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/filepond/filepond.min.css" rel="stylesheet" />
<style>
div.dataTables_wrapper span.selected-info,
div.dataTables_wrapper span.selected-item {
@ -45,7 +46,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div id="errorMsg" class="card mb-4 border-left-warning" style="display: none;">
<div id="errorTxt" class="card-body text-form-error"></div>
</div>
<div class="table-responsive">
<div id="tableContainer" class="table-responsive">
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0">
<thead>
<tr>
@ -76,7 +77,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
<form id="upload_files_form" action="{{.FilesURL}}?path={{.CurrentDir}}" method="POST" enctype="multipart/form-data">
<div class="modal-body">
<input type="file" class="form-control-file" id="files_name" name="filenames" required multiple>
<div id="uploadErrorMsg" class="card mb-4 border-left-warning" style="display: none;">
<div id="uploadErrorTxt" class="card-body text-form-error"></div>
</div>
<input type="file" id="files_name" name="filenames" required multiple>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
@ -102,6 +106,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.checkboxes.min.js"></script>
<script src="{{.StaticURL}}/vendor/filepond/filepond.min.js"></script>
<script type="text/javascript">
var spinnerDone = false;
@ -191,6 +196,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
return "far fa-file-code";
case "zip":
case "zipx":
case "7z":
case "rar":
case "tar":
case "gz":
@ -223,6 +229,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
return meta.split('_').slice(1).join('_');
}
const isDirectoryEntry = item => isEntry(item) && (getAsEntry(item) || {}).isDirectory;
const isEntry = item => 'webkitGetAsEntry' in item;
const getAsEntry = item => item.webkitGetAsEntry();
$(document).ready(function () {
$('#spinnerModal').on('shown.bs.modal', function () {
if (spinnerDone){
@ -230,9 +240,74 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
}
});
{{if gt .Scope 1}}
FilePond.create(document.getElementById("files_name"),{
allowMultiple: true,
name: 'filenames',
maxFiles: 30,
credits: false,
required: true,
onwarning: function(error){
if (error.code == 0){
$('#uploadErrorTxt').text('You can upload a maximum of 30 files');
$('#uploadErrorMsg').show();
setTimeout(function () {
$('#uploadErrorMsg').hide();
}, 10000);
}
},
beforeAddFile: (fileItem) => new Promise(resolve => {
let num = 0;
FilePond.find(document.getElementById("files_name")).getFiles().forEach(function(val){
if (val.filename == fileItem.filename){
num++;
}
});
resolve(num == 1);
})
});
$('#tableContainer').on("dragover", function(ev){
ev.preventDefault();
$('#tableContainer').css('opacity','0.5');
});
$('#tableContainer').on("dragend dragleave", function(ev){
ev.preventDefault();
$('#tableContainer').css('opacity','1');
});
$('#tableContainer').on("drop", function(ev){
ev.preventDefault();
$('#tableContainer').css('opacity','1');
let filesDropped = false;
if (ev.originalEvent.dataTransfer.items) {
[...ev.originalEvent.dataTransfer.items].forEach((item, i) => {
if (item.kind === 'file') {
// if this is a directory just open the upload dialog
if (!isDirectoryEntry(item)){
FilePond.find(document.getElementById("files_name")).addFile(item.getAsFile());
filesDropped = true;
}
}
});
} else {
[...ev.originalEvent.dataTransfer.files].forEach((file, i) => {
FilePond.find(document.getElementById("files_name")).addFile(file);
filesDropped = true;
});
}
if (filesDropped && !$('#uploadFilesModal').hasClass('show')){
$('#uploadFilesModal').modal('show');
}
});
{{end}}
$("#upload_files_form").submit(function (event){
event.preventDefault();
var files = $("#files_name")[0].files;
var files = FilePond.find(document.getElementById("files_name")).getFiles();
var has_errors = false;
var index = 0;
var success = 0;
@ -255,7 +330,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
var errorMessage = "Error uploading files";
let response;
try {
var f = files[index];
var f = files[index].file;
var uploadPath = '{{.UploadBaseURL}}'+fixedEncodeURIComponent("/"+escapeHTML(f.name));
var lastModified;
try {
@ -345,7 +420,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
name: 'addFiles',
titleAttr: "Upload files",
action: function (e, dt, node, config) {
document.getElementById("files_name").value = null;
//FilePond.find(document.getElementById("files_name")).removeFiles();
$('#uploadFilesModal').modal('show');
},
enabled: true