](https://www.aledade.com/)
[
](https://www.jumptrading.com/)
[
](https://wpengine.com/)
#### Silver sponsors
[
](https://idcs.ip-paris.fr/)
#### Bronze sponsors
[
](https://www.7digital.com/)
[
](https://servinga.com/)
[
](https://www.reui.io/)
## Documentation
You can explore all supported features and configuration options at [docs.sftpgo.com](https://docs.sftpgo.com/latest/).
**Note:** The link above refers to the **Community Edition**.
For details on **Enterprise Edition**, please refer to the [Enterprise Documentation](https://docs.sftpgo.com/enterprise/).
## Support
- **Community Support**: use [GitHub Discussions](https://github.com/drakkan/sftpgo/discussions) to ask questions, share feedback, and engage with other users.
- **Commercial Support**: If you require guaranteed SLAs, expert guidance, or the advanced features listed above, check out [SFTPGo Enterprise](https://sftpgo.com).
SFTPGo Enterprise is available as:
- On-premises: Full control on your infrastructure. More details: [sftpgo.com/on-premises](https://sftpgo.com/on-premises)
- Fully managed SaaS: We handle the infrastructure. More details: [sftpgo.com/saas](https://sftpgo.com/saas)
## Internationalization
The translations are available via [Crowdin](https://crowdin.com/project/sftpgo), who have granted us an open source license.
Before translating please take a look at our contribution [guidelines](https://docs.sftpgo.com/latest/web-interfaces/#internationalization).
## Release Cadence
SFTPGo follows a feature-driven release cycle.
- Enterprise Edition: Receives major new features first and follows a faster [release cadence](https://docs.sftpgo.com/enterprise/changelog/).
- Community Edition: Remains maintained, receiving bug fixes, security updates, and updates to core features.
## Acknowledgements
SFTPGo makes use of the third party libraries listed inside [go.mod](./go.mod).
We are very grateful to all the people who contributed with ideas and/or pull requests.
Thank you to [ysura](https://www.ysura.com/) for granting us stable access to a test AWS S3 account.
Thank you to [KeenThemes](https://keenthemes.com/) for granting us a custom license to use their amazing [themes](https://keenthemes.com/bootstrap-templates) for the SFTPGo WebAdmin and WebClient user interfaces, across both the Open Source and Open Core versions.
Thank you to [Crowdin](https://crowdin.com/) for granting us an Open Source License.
Thank you to [Incode](https://www.incode.it/) for helping us to improve the UI/UX.
## License
SFTPGo source code is licensed under the GNU AGPL-3.0-only with [additional terms](./NOTICE).
The [theme](https://keenthemes.com/bootstrap-templates) used in WebAdmin and WebClient user interfaces is proprietary, this means:
- KeenThemes HTML/CSS/JS components are allowed for use only within the SFTPGo product and restricted to be used in a resealable HTML template that can compete with KeenThemes products anyhow.
- The SFTPGo WebAdmin and WebClient user interfaces (HTML, CSS and JS components) based on this theme are allowed for use only within the SFTPGo product and therefore cannot be used in derivative works/products without an explicit grant from the [SFTPGo Team](mailto:support@sftpgo.com).
More information about [compliance](https://sftpgo.com/compliance.html).
**Note:** We do not provide legal advice. If you have questions about license compliance or whether your use case is permitted under the license terms, please consult your legal team.
## Copyright
Copyright (C) 2019 - 2026 Nicola Murino
================================================
FILE: SECURITY.md
================================================
# Security Policy
## Supported Versions
We actively maintain the latest stable release of SFTPGo. While we strive to keep the Open Source version secure and up-to-date, maintenance is performed on a best-effort basis by the community and contributors.
## Scope and Dependency Policy
Our security advisories focus on vulnerabilities found within the **SFTPGo codebase itself**.
To ensure the long-term sustainability of the project, we handle upstream dependencies (like the Go standard library, external packages, or Docker base images) as follows:
- Community Updates: For the Open Source version, vulnerabilities in upstream components (such as the Go standard library or third-party packages) are addressed during our **regular release cycles**. We generally do not provide immediate, out-of-band or ad-hoc releases to address dependency-only CVEs.
- Empowering Users: One of the strengths of SFTPGo being open-source is that you have full control. If your security scanners require an immediate fix, you can always rebuild the project using the latest patched Go toolchain or updated dependencies.
- Compatibility: We are committed to keeping SFTPGo compatible with the latest stable Go compiler. If an upstream fix breaks SFTPGo, fixing that becomes a priority for us.
- Professional Needs: We understand that some organizations have strict compliance requirements or internal SLAs that require guaranteed, immediate response times and out-of-band patches. For these cases, we offer [SFTPGo Enterprise](https://sftpgo.com/on-premises) to cover the additional maintenance and support overhead.
## Reporting a Vulnerability
To report (possible) security issues in SFTPGo, please either send a mail to the [SFTPGo Team](mailto:support@sftpgo.com) or use Github's [private reporting feature](https://github.com/drakkan/sftpgo/security/advisories/new).
================================================
FILE: crowdin.yml
================================================
project_id_env: CROWDIN_PROJECT_ID
api_token_env: CROWDIN_PERSONAL_TOKEN
files:
- source: /static/locales/en/translation.json
translation: /static/locales/%two_letters_code%/%original_file_name%
type: i18next_json
================================================
FILE: docker/scripts/download-plugins.sh
================================================
#!/usr/bin/env bash
set -euo pipefail
ARCH=$(uname -m)
case ${ARCH} in
x86_64)
SUFFIX=amd64
;;
aarch64)
SUFFIX=arm64
;;
*)
SUFFIX=ppc64le
;;
esac
echo "Downloading plugins for arch ${SUFFIX}"
PLUGINS=(geoipfilter kms pubsub eventstore eventsearch auth)
for PLUGIN in "${PLUGINS[@]}"; do
URL="https://github.com/sftpgo/sftpgo-plugin-${PLUGIN}/releases/latest/download/sftpgo-plugin-${PLUGIN}-linux-${SUFFIX}"
DEST="/usr/local/bin/sftpgo-plugin-${PLUGIN}"
echo "Downloading ${PLUGIN}..."
if curl --fail --silent --show-error -L "${URL}" --output "${DEST}"; then
chmod 755 "${DEST}"
else
echo "Error: Failed to download ${PLUGIN}" >&2
exit 1
fi
done
echo "All plugins downloaded successfully"
================================================
FILE: examples/OTP/authy/README.md
================================================
# Authy
These example show how-to integrate [Twilio Authy API](https://www.twilio.com/docs/authy/api) for One-Time-Password logins.
The examples assume that the user has the free [Authy app](https://authy.com/) installed and uses it to generate offline [TOTP](https://en.wikipedia.org/wiki/Time-based_One-time_Password_algorithm) codes (soft tokens).
You first need to [create an Authy Application in the Twilio Console](https://twilio.com/console/authy/applications?_ga=2.205553366.451688189.1597667213-1526360003.1597667213), then you can create a new Authy user and store a reference to the matching SFTPGo account.
Verify that your Authy application is successfully registered:
```bash
export AUTHY_API_KEY={http.error.status_code} {http.error.status_text}"
}
}
(add_hsts_headers) {
header {
# Enable HTTP Strict Transport Security (HSTS) to force clients to always
# connect via HTTPS (do not use if only testing)
Strict-Transport-Security "max-age=31536000; includeSubDomains"
# Enable cross-site filter (XSS) and tell browser to block detected attacks
X-XSS-Protection "1; mode=block"
# Prevent some browsers from MIME-sniffing a response away from the declared Content-Type
X-Content-Type-Options "nosniff"
# Disallow the site to be rendered within a frame (clickjacking protection)
X-Frame-Options "DENY"
# keep referrer data off of HTTP connections
Referrer-Policy no-referrer-when-downgrade
}
}
(add_logging_with_path) {
log {
output file "{args.0}" {
roll_size 100mb
roll_keep 5
roll_keep_for 720h
}
format json
#format console
#format single_field common_log
}
}
### Site Definitions:
public.example.com {
# Site Root:
root * F:\files\public
import add_logging_with_path "F:\caddy\logs\public_example_com_access.log"
import add_static_file_serving_features
import add_hsts_headers
}
### Reverse Proxy Definitions:
webdav.example.com {
reverse_proxy localhost:9000
import add_logging_with_path "F:\caddy\logs\webdav_example_com_access.log"
}
```
================================================
FILE: examples/quotascan/README.md
================================================
# Update user quota
:warning: Since v2.4.0 you can use the [EventManager](https://docs.sftpgo.com/latest/eventmanager/) to schedule quota scans.
The `scanuserquota` example script shows how to use the SFTPGo REST API to update the users' quota.
The stored quota may be incorrect for several reasons, such as an unexpected shutdown while uploading files, temporary provider failures, files copied outside of SFTPGo, and so on.
A quota scan updates the number of files and their total size for the specified user and the virtual folders, if any, included in his quota.
If you want to track quotas, a scheduled quota scan is recommended. You can use this example as a starting point.
The script is written in Python and has the following requirements:
- python3 or python2
- python [Requests](https://requests.readthedocs.io/en/master/) module
The provided example tries to connect to an SFTPGo instance running on `127.0.0.1:8080` using the following credentials:
- username: `admin`
- password: `password`
Please edit the script according to your needs.
================================================
FILE: examples/quotascan/scanuserquota
================================================
#!/usr/bin/env python
from datetime import datetime
import sys
import time
import pytz
import requests
try:
import urllib.parse as urlparse
except ImportError:
import urlparse
# change base_url to point to your SFTPGo installation
base_url = "http://127.0.0.1:8080"
# set to False if you want to skip TLS certificate validation
verify_tls_cert = True
# set the credentials for a valid admin here
admin_user = "admin"
admin_password = "password"
# set your update conditions here
def needQuotaUpdate(user):
if user["status"] == 0: # inactive user
return False
if user["quota_size"] == 0 and user["quota_files"] == 0: # no quota restrictions
return False
return True
class UpdateQuota:
def __init__(self):
self.limit = 100
self.offset = 0
self.access_token = ""
self.access_token_expiration = None
def printLog(self, message):
print("{} - {}".format(datetime.now(), message))
def checkAccessToken(self):
if self.access_token != "" and self.access_token_expiration:
expire_diff = self.access_token_expiration - datetime.now(tz=pytz.UTC)
# we don't use total_seconds to be python 2 compatible
seconds_to_expire = expire_diff.days * 86400 + expire_diff.seconds
if seconds_to_expire > 180:
return
auth = requests.auth.HTTPBasicAuth(admin_user, admin_password)
r = requests.get(urlparse.urljoin(base_url, "api/v2/token"), auth=auth, verify=verify_tls_cert, timeout=10)
if r.status_code != 200:
self.printLog("error getting access token: {}".format(r.text))
sys.exit(1)
self.access_token = r.json()["access_token"]
self.access_token_expiration = pytz.timezone("UTC").localize(datetime.strptime(r.json()["expires_at"],
"%Y-%m-%dT%H:%M:%SZ"))
def getAuthHeader(self):
self.checkAccessToken()
return {"Authorization": "Bearer " + self.access_token}
def waitForQuotaUpdate(self, username):
while True:
auth_header = self.getAuthHeader()
r = requests.get(urlparse.urljoin(base_url, "api/v2/quotas/users/scans"), headers=auth_header, verify=verify_tls_cert,
timeout=10)
if r.status_code != 200:
self.printLog("error getting quota scans while waiting for {}: {}".format(username, r.text))
sys.exit(1)
scanning = False
for scan in r.json():
if scan["username"] == username:
scanning = True
if not scanning:
break
self.printLog("waiting for the quota scan to complete for user {}".format(username))
time.sleep(2)
self.printLog("quota update for user {} finished".format(username))
def updateUserQuota(self, username):
self.printLog("starting quota update for user {}".format(username))
auth_header = self.getAuthHeader()
r = requests.post(urlparse.urljoin(base_url, "api/v2/quotas/users/" + username + "/scan"), headers=auth_header,
verify=verify_tls_cert, timeout=10)
if r.status_code != 202:
self.printLog("error starting quota scan for user {}: {}".format(username, r.text))
sys.exit(1)
self.waitForQuotaUpdate(username)
def updateUsersQuota(self):
while True:
self.printLog("get users, limit {} offset {}".format(self.limit, self.offset))
auth_header = self.getAuthHeader()
payload = {"limit":self.limit, "offset":self.offset}
r = requests.get(urlparse.urljoin(base_url, "api/v2/users"), headers=auth_header, params=payload,
verify=verify_tls_cert, timeout=10)
if r.status_code != 200:
self.printLog("error getting users: {}".format(r.text))
sys.exit(1)
users = r.json()
for user in users:
if needQuotaUpdate(user):
self.updateUserQuota(user["username"])
else:
self.printLog("user {} does not need a quota update".format(user["username"]))
self.offset += len(users)
if len(users) < self.limit:
break
if __name__ == '__main__':
q = UpdateQuota()
q.updateUsersQuota()
================================================
FILE: go.mod
================================================
module github.com/drakkan/sftpgo/v2
go 1.25.0
require (
cloud.google.com/go/storage v1.60.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
github.com/alexedwards/argon2id v1.0.0
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964
github.com/aws/aws-sdk-go-v2 v1.41.4
github.com/aws/aws-sdk-go-v2/config v1.32.12
github.com/aws/aws-sdk-go-v2/credentials v1.19.12
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9
github.com/bmatcuk/doublestar/v4 v4.10.0
github.com/cockroachdb/cockroach-go/v2 v2.4.3
github.com/coreos/go-oidc/v3 v3.17.0
github.com/drakkan/webdav v0.0.0-20241026165615-b8b8f74ae71b
github.com/eikenb/pipeat v0.0.0-20251030185646-385cd3c3e07b
github.com/fclairamb/ftpserverlib v0.30.0
github.com/go-acme/lego/v4 v4.33.0
github.com/go-chi/chi/v5 v5.2.5
github.com/go-chi/render v1.0.3
github.com/go-jose/go-jose/v4 v4.1.3
github.com/go-sql-driver/mysql v1.9.3
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.6.0
github.com/hashicorp/go-hclog v1.6.3
github.com/hashicorp/go-plugin v1.7.0
github.com/hashicorp/go-retryablehttp v0.7.8
github.com/jackc/pgx/v5 v5.8.0
github.com/jlaffaye/ftp v0.2.0
github.com/klauspost/compress v1.18.4
github.com/lithammer/shortuuid/v4 v4.2.0
github.com/mattn/go-sqlite3 v1.14.37
github.com/mhale/smtpd v0.8.3
github.com/minio/sio v0.4.3
github.com/otiai10/copy v1.14.1
github.com/pires/go-proxyproto v0.11.0
github.com/pkg/sftp v1.13.10
github.com/pquerna/otp v1.5.0
github.com/prometheus/client_golang v1.23.2
github.com/robfig/cron/v3 v3.0.1
github.com/rs/cors v1.11.1
github.com/rs/xid v1.6.0
github.com/rs/zerolog v1.34.0
github.com/sftpgo/sdk v0.1.9
github.com/shirou/gopsutil/v3 v3.24.5
github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/studio-b12/gowebdav v0.12.0
github.com/subosito/gotenv v1.6.0
github.com/unrolled/secure v1.17.0
github.com/wagslane/go-password-validator v0.3.0
github.com/wneessen/go-mail v0.7.2
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
go.etcd.io/bbolt v1.4.3
gocloud.dev v0.45.0
golang.org/x/crypto v0.49.0
golang.org/x/net v0.52.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sys v0.42.0
golang.org/x/term v0.41.0
golang.org/x/time v0.15.0
google.golang.org/api v0.272.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
require (
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/monitoring v1.24.3 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
github.com/ajg/form v1.7.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.19.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oklog/run v1.2.0 // indirect
github.com/otiai10/mint v1.6.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/shoenig/go-m1cpu v0.2.1 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
go.opentelemetry.io/otel v1.42.0 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.opentelemetry.io/otel/sdk v1.42.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect
go.opentelemetry.io/otel/trace v1.42.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/tools v0.43.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/genproto v0.0.0-20260319171110-e3a33c96fb44 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260319171110-e3a33c96fb44 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319171110-e3a33c96fb44 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace (
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20240430173938-7ba8270c8e7f
github.com/robfig/cron/v3 => github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0
)
================================================
FILE: go.sum
================================================
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU=
cloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58=
cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA=
cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak=
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
cloud.google.com/go/storage v1.60.0 h1:oBfZrSOCimggVNz9Y/bXY35uUcts7OViubeddTTVzQ8=
cloud.google.com/go/storage v1.60.0/go.mod h1:q+5196hXfejkctrnx+VYU8RKQr/L3c0cBIlrjmiAKE0=
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 h1:jWQK1GI+LeGGUKBADtcH2rRqPxYB1Ljwms5gFA2LqrM=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4/go.mod h1:8mwH4klAm9DUgR2EEHyEEAQlRDvLPyg5fQry3y+cDew=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0 h1:4iB+IesclUXdP0ICgAabvq2FYLXrJWKx1fJQ+GxSo3Y=
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI=
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/ajg/form v1.7.1 h1:OsnBDzTkrWdrxvEnO68I72ZVGJGNaMwPhoAm0V+llgc=
github.com/ajg/form v1.7.1/go.mod h1:HL757PzLyNkj5AIfptT6L+iGNeXTlnrr/oDePGc/y7Q=
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 h1:I9YN9WMo3SUh7p/4wKeNvD/IQla3U3SUa61U7ul+xM4=
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964/go.mod h1:eFiR01PwTcpbzXtdMces7zxg6utvFM5puiWHpWB8D/k=
github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k=
github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 h1:3kGOqnh1pPeddVa/E37XNTaWJ8W6vrbYV9lJEkCnhuY=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0=
github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g=
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 h1:SwGMTMLIlvDNyhMteQ6r8IJSBPlRdXX5d4idhIGbkXA=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21/go.mod h1:UUxgWxofmOdAMuqEsSppbDtGKLfR04HGsD0HXzvhI1k=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 h1:qtJZ70afD3ISKWnoX3xB0J2otEqu3LqicRcDBqsj0hQ=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12/go.mod h1:v2pNpJbRNl4vEUWEh5ytQok0zACAKfdmKS51Hotc3pQ=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 h1:siU1A6xjUZ2N8zjTHSXFhB9L/2OY8Dqs0xXiLjF30jA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1 h1:csi9NLpFZXb9fxY7rS1xVzgPRGMt7MSNWeQ6eo247kE=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1/go.mod h1:qXVal5H0ChqXP63t6jze5LmFalc7+ZE7wOdLtZ0LCP0=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
github.com/cockroachdb/cockroach-go/v2 v2.4.3 h1:LJO3K3jC5WXvMePRQSJE1NsIGoFGcEx1LW83W6RAlhw=
github.com/cockroachdb/cockroach-go/v2 v2.4.3/go.mod h1:9U179XbCx4qFWtNhc7BiWLPfuyMVQ7qdAhfrwLz1vH0=
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0 h1:EW9gIJRmt9lzk66Fhh4S8VEtURA6QHZqGeSRE9Nb2/U=
github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/drakkan/ftp v0.0.0-20240430173938-7ba8270c8e7f h1:S9JUlrOzjK58UKoLqqb40YLyVlt0bcIFtYrvnanV3zc=
github.com/drakkan/ftp v0.0.0-20240430173938-7ba8270c8e7f/go.mod h1:4p8lUl4vQ80L598CygL+3IFtm+3nggvvW/palOlViwE=
github.com/drakkan/webdav v0.0.0-20241026165615-b8b8f74ae71b h1:Y1tLiQ8fnxM5f3wiBjAXsHzHNwiY9BR+mXZA75nZwrs=
github.com/drakkan/webdav v0.0.0-20241026165615-b8b8f74ae71b/go.mod h1:zOVb1QDhwwqWn2L2qZ0U3swMSO4GTSNyIwXCGO/UGWE=
github.com/eikenb/pipeat v0.0.0-20251030185646-385cd3c3e07b h1:G2Mm3YhlyjkFrNnvu5E6LtNcPJtggXL1i5ekDV4hDD4=
github.com/eikenb/pipeat v0.0.0-20251030185646-385cd3c3e07b/go.mod h1:XccPiThW83W5pzeOCsJAylEUtWeH+3zQVwiO402FXXc=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fclairamb/ftpserverlib v0.30.0 h1:caB9sDn1Au//q0j2ev/icPn388qPuk4k1ajSvglDcMQ=
github.com/fclairamb/ftpserverlib v0.30.0/go.mod h1:QmogtltTOgkihyKza0GNo37Mu4AEzbJ+sH6W9Y0MBIQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-acme/lego/v4 v4.33.0 h1:2KrRKieG+VczT4zvVXKAY7Tp/S3XLvh/QImofBALRAM=
github.com/go-acme/lego/v4 v4.33.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE=
github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=
github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lithammer/shortuuid/v4 v4.2.0 h1:LMFOzVB3996a7b8aBuEXxqOBflbfPQAiVzkIcHO0h8c=
github.com/lithammer/shortuuid/v4 v4.2.0/go.mod h1:D5noHZ2oFw/YaKCfGy0YxyE7M0wMbezmMjPdhyEFe6Y=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mhale/smtpd v0.8.3 h1:8j8YNXajksoSLZja3HdwvYVZPuJSqAxFsib3adzRRt8=
github.com/mhale/smtpd v0.8.3/go.mod h1:MQl+y2hwIEQCXtNhe5+55n0GZOjSmeqORDIXbqUL3x4=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/minio/sio v0.4.3 h1:JqyID1XM86KwBZox5RAdLD4MLPIDoCY2cke2CXCJCkg=
github.com/minio/sio v0.4.3/go.mod h1:4ANoe4CCXqnt1FCiLM0+vlBUhhWZzVOhYCz0069KtFc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E=
github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk=
github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY=
github.com/sftpgo/sdk v0.1.9 h1:onBWfibCt34xHeKC2KFYPZ1DBqXGl9um/cAw+AVdgzY=
github.com/sftpgo/sdk v0.1.9/go.mod h1:ehimvlTP+XTEiE3t1CPwWx9n7+6A6OGvMGlZ7ouvKFk=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shoenig/go-m1cpu v0.2.1 h1:yqRB4fvOge2+FyRXFkXqsyMoqPazv14Yyy+iyccT2E4=
github.com/shoenig/go-m1cpu v0.2.1/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w=
github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=
github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/studio-b12/gowebdav v0.12.0 h1:kFRtQECt8jmVAvA6RHBz3geXUGJHUZA6/IKpOVUs5kM=
github.com/studio-b12/gowebdav v0.12.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU=
github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=
github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a h1:XfF01GyP+0eWCaVp0y6rNN+kFp7pt9Da4UUYrJ5XPWA=
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a/go.mod h1:aXb8yZQEWo1XHGMf1qQfnb83GR/EJ2EBlwtUgAaNBoE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ=
go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
gocloud.dev v0.45.0 h1:WknIK8IbRdmynDvara3Q7G6wQhmEiOGwpgJufbM39sY=
gocloud.dev v0.45.0/go.mod h1:0kXKmkCLG6d31N7NyLZWzt7jDSQura9zD/mWgiB6THI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA=
google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA=
google.golang.org/genproto v0.0.0-20260319171110-e3a33c96fb44 h1:5F2rCQQSavqKBEvXvkLt9Lc61ldIzythnt3+ucqJWG8=
google.golang.org/genproto v0.0.0-20260319171110-e3a33c96fb44/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
google.golang.org/genproto/googleapis/api v0.0.0-20260319171110-e3a33c96fb44 h1:3r0atjZtaSjEuUu8NNhKvvnFh8ad9PHWsN9VRotabFU=
google.golang.org/genproto/googleapis/api v0.0.0-20260319171110-e3a33c96fb44/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319171110-e3a33c96fb44 h1:sRy++txmErSjyVWlIgQB5nB+U75+Di+AH7eEZ002B/s=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319171110-e3a33c96fb44/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: init/com.github.drakkan.sftpgo.plist
================================================
Fs path {{.FsPath}}, Name: {{.Name}}, Target path "{{.VirtualTargetDirPath}}/{{.TargetName}}", size: {{.FileSize}}
`, }, }, } action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated) assert.NoError(t, err) r1 := dataprovider.EventRule{ Name: "test rename rule", Status: 1, 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) u := getTestUser() u.Username = "test & chars" user, _, err := httpdtest.AddUser(u, 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, slices.Contains(email.To, "test@example.com")) assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "rename" from "%s"`, user.Username)) assert.Contains(t, email.Data, "Content-Type: text/html") assert.Contains(t, email.Data, fmt.Sprintf("Target path %q", path.Join("/subdir", testFileName))) assert.Contains(t, email.Data, "Name: test & chars,") } _, 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, true) require.NoError(t, err) } func TestEventRuleIDPLogin(t *testing.T) { smtpCfg := smtp.Config{ Host: "127.0.0.1", Port: 2525, From: "notify@example.com", TemplatesPath: "templates", } err := smtpCfg.Initialize(configDir, true) require.NoError(t, err) lastReceivedEmail.reset() username := `test_'idp_'login` custom1 := `cust"oa"1` u := map[string]any{ "username": "{{.Name}}", "status": 1, "home_dir": filepath.Join(os.TempDir(), "{{.IDPFieldcustom1}}"), "permissions": map[string][]string{ "/": {dataprovider.PermAny}, }, } userTmpl, err := json.Marshal(u) require.NoError(t, err) a := map[string]any{ "username": "{{.Name}}", "status": 1, "permissions": []string{dataprovider.PermAdminAny}, } adminTmpl, err := json.Marshal(a) require.NoError(t, err) a1 := dataprovider.BaseEventAction{ Name: "a1", Type: dataprovider.ActionTypeIDPAccountCheck, Options: dataprovider.BaseEventActionOptions{ IDPConfig: dataprovider.EventActionIDPAccountCheck{ Mode: 1, // create if not exists TemplateUser: string(userTmpl), TemplateAdmin: string(adminTmpl), }, }, } action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated) assert.NoError(t, err) a2 := dataprovider.BaseEventAction{ Name: "a2", Type: dataprovider.ActionTypeEmail, Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"test@example.com"}, Subject: `"{{.Event}} {{.StatusString}}"`, Body: "{{.Name}} Custom field: {{.IDPFieldcustom1}}", }, }, } action2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated) assert.NoError(t, err) r1 := dataprovider.EventRule{ Name: "test rule IDP login", Status: 1, Trigger: dataprovider.EventTriggerIDPLogin, Conditions: dataprovider.EventConditions{ IDPLoginEvent: dataprovider.IDPLoginUser, }, Actions: []dataprovider.EventAction{ { BaseEventAction: dataprovider.BaseEventAction{ Name: action1.Name, // the rule is not sync and will be skipped }, Order: 1, }, { BaseEventAction: dataprovider.BaseEventAction{ Name: action2.Name, }, Order: 2, }, }, } rule1, resp, err := httpdtest.AddEventRule(r1, http.StatusCreated) assert.NoError(t, err, string(resp)) customFields := map[string]any{ "custom1": custom1, } user, admin, err := common.HandleIDPLoginEvent(common.EventParams{ Name: username, Event: common.IDPLoginUser, Status: 1, }, &customFields) assert.Nil(t, user) assert.Nil(t, admin) assert.NoError(t, err) rule1.Actions[0].Options.ExecuteSync = true rule1, resp, err = httpdtest.UpdateEventRule(rule1, http.StatusOK) assert.NoError(t, err, string(resp)) user, admin, err = common.HandleIDPLoginEvent(common.EventParams{ Name: username, Event: common.IDPLoginUser, Status: 1, }, &customFields) if assert.NotNil(t, user) { assert.Equal(t, filepath.Join(os.TempDir(), custom1), user.GetHomeDir()) _, err = httpdtest.RemoveUser(*user, http.StatusOK) assert.NoError(t, err) } assert.Nil(t, admin) assert.NoError(t, err) 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, slices.Contains(email.To, "test@example.com")) assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, common.IDPLoginUser)) assert.Contains(t, email.Data, username) assert.Contains(t, email.Data, custom1) user, admin, err = common.HandleIDPLoginEvent(common.EventParams{ Name: username, Event: common.IDPLoginAdmin, Status: 1, }, &customFields) assert.Nil(t, user) assert.Nil(t, admin) assert.NoError(t, err) rule1.Conditions.IDPLoginEvent = dataprovider.IDPLoginAny rule1.Actions = []dataprovider.EventAction{ { BaseEventAction: dataprovider.BaseEventAction{ Name: action1.Name, }, Options: dataprovider.EventActionOptions{ ExecuteSync: true, }, Order: 1, }, } rule1, _, err = httpdtest.UpdateEventRule(rule1, http.StatusOK) assert.NoError(t, err) r2 := dataprovider.EventRule{ Name: "test email on IDP login", Status: 1, Trigger: dataprovider.EventTriggerIDPLogin, Conditions: dataprovider.EventConditions{ IDPLoginEvent: dataprovider.IDPLoginAdmin, }, Actions: []dataprovider.EventAction{ { BaseEventAction: dataprovider.BaseEventAction{ Name: action2.Name, }, Order: 1, }, }, } rule2, resp, err := httpdtest.AddEventRule(r2, http.StatusCreated) assert.NoError(t, err, string(resp)) lastReceivedEmail.reset() user, admin, err = common.HandleIDPLoginEvent(common.EventParams{ Name: username, Event: common.IDPLoginAdmin, Status: 1, }, &customFields) assert.Nil(t, user) if assert.NotNil(t, admin) { assert.Equal(t, 1, admin.Status) } assert.NoError(t, err) 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, slices.Contains(email.To, "test@example.com")) assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, common.IDPLoginAdmin)) assert.Contains(t, email.Data, username) assert.Contains(t, email.Data, custom1) admin.Status = 0 _, _, err = httpdtest.UpdateAdmin(*admin, http.StatusOK) assert.NoError(t, err) user, admin, err = common.HandleIDPLoginEvent(common.EventParams{ Name: username, Event: common.IDPLoginAdmin, Status: 1, }, &customFields) assert.Nil(t, user) if assert.NotNil(t, admin) { assert.Equal(t, 0, admin.Status) } assert.NoError(t, err) action1.Options.IDPConfig.Mode = 0 action1, _, err = httpdtest.UpdateEventAction(action1, http.StatusOK) assert.NoError(t, err) user, admin, err = common.HandleIDPLoginEvent(common.EventParams{ Name: username, Event: common.IDPLoginAdmin, Status: 1, }, &customFields) assert.Nil(t, user) if assert.NotNil(t, admin) { assert.Equal(t, 1, admin.Status) } assert.NoError(t, err) _, err = httpdtest.RemoveAdmin(*admin, http.StatusOK) assert.NoError(t, err) r3 := dataprovider.EventRule{ Name: "test rule2 IDP login", Status: 1, Trigger: dataprovider.EventTriggerIDPLogin, Conditions: dataprovider.EventConditions{ IDPLoginEvent: dataprovider.IDPLoginAny, }, Actions: []dataprovider.EventAction{ { BaseEventAction: dataprovider.BaseEventAction{ Name: action1.Name, }, Order: 1, Options: dataprovider.EventActionOptions{ ExecuteSync: true, }, }, }, } rule3, resp, err := httpdtest.AddEventRule(r3, http.StatusCreated) assert.NoError(t, err, string(resp)) user, admin, err = common.HandleIDPLoginEvent(common.EventParams{ Name: username, Event: common.IDPLoginAdmin, Status: 1, }, &customFields) assert.Nil(t, user) assert.Nil(t, admin) if assert.Error(t, err) { assert.Contains(t, err.Error(), "more than one account check action rules matches") } _, err = httpdtest.RemoveEventRule(rule3, http.StatusOK) assert.NoError(t, err) action1.Options.IDPConfig.TemplateAdmin = `{}` action1, _, err = httpdtest.UpdateEventAction(action1, http.StatusOK) assert.NoError(t, err) _, _, err = common.HandleIDPLoginEvent(common.EventParams{ Name: username, Event: common.IDPLoginAdmin, Status: 1, }, &customFields) assert.ErrorIs(t, err, util.ErrValidation) _, err = httpdtest.RemoveEventRule(rule1, http.StatusOK) assert.NoError(t, err) user, admin, err = common.HandleIDPLoginEvent(common.EventParams{ Name: username, Event: common.IDPLoginAdmin, Status: 1, }, &customFields) assert.Nil(t, user) assert.Nil(t, admin) assert.NoError(t, err) _, err = httpdtest.RemoveEventRule(rule2, 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, true) require.NoError(t, err) } func TestEventRuleEmailField(t *testing.T) { smtpCfg := smtp.Config{ Host: "127.0.0.1", Port: 2525, From: "notify@example.com", TemplatesPath: "templates", } err := smtpCfg.Initialize(configDir, true) require.NoError(t, err) lastReceivedEmail.reset() a1 := dataprovider.BaseEventAction{ Name: "action1", Type: dataprovider.ActionTypeEmail, Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"{{.Email}}"}, Subject: `"{{.Event}}" from "{{.Name}}"`, Body: "Sample email body", }, }, } action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated) assert.NoError(t, err) a2 := dataprovider.BaseEventAction{ Name: "action2", Type: dataprovider.ActionTypeEmail, Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"failure@example.com"}, Subject: `"Failure`, Body: "{{.ErrorString}}", }, }, } action2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated) assert.NoError(t, err) r1 := dataprovider.EventRule{ Name: "r1", Status: 1, Trigger: dataprovider.EventTriggerFsEvent, Conditions: dataprovider.EventConditions{ FsEvents: []string{"mkdir"}, }, Actions: []dataprovider.EventAction{ { BaseEventAction: dataprovider.BaseEventAction{ Name: action1.Name, }, }, }, } r2 := dataprovider.EventRule{ Name: "test rule2", Status: 1, Trigger: dataprovider.EventTriggerProviderEvent, Conditions: dataprovider.EventConditions{ ProviderEvents: []string{"add"}, Options: dataprovider.ConditionOptions{ ProviderObjects: []string{"user"}, }, }, Actions: []dataprovider.EventAction{ { BaseEventAction: dataprovider.BaseEventAction{ Name: action1.Name, }, Order: 1, }, { BaseEventAction: dataprovider.BaseEventAction{ Name: action2.Name, }, Options: dataprovider.EventActionOptions{ IsFailureAction: true, }, }, }, } rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated) assert.NoError(t, err) rule2, _, err := httpdtest.AddEventRule(r2, http.StatusCreated) assert.NoError(t, err) u := getTestUser() u.Email = "user@example.com" user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) 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, slices.Contains(email.To, user.Email)) assert.Contains(t, email.Data, `Subject: "add" from "admin"`) // if we add a user without email the notification will fail lastReceivedEmail.reset() u1 := getTestUser() u1.Username += "_1" user1, _, err := httpdtest.AddUser(u1, http.StatusCreated) assert.NoError(t, err) 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, slices.Contains(email.To, "failure@example.com")) assert.Contains(t, email.Data, `no recipient addresses set`) conn, client, err := getSftpClient(user) if assert.NoError(t, err) { defer conn.Close() defer client.Close() lastReceivedEmail.reset() err = client.Mkdir(testFileName) assert.NoError(t, err) 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, slices.Contains(email.To, user.Email)) assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "mkdir" from "%s"`, user.Username)) } _, err = httpdtest.RemoveEventRule(rule1, http.StatusOK) assert.NoError(t, err) _, err = httpdtest.RemoveEventRule(rule2, 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) _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) _, err = httpdtest.RemoveUser(user1, http.StatusOK) assert.NoError(t, err) smtpCfg = smtp.Config{} err = smtpCfg.Initialize(configDir, true) require.NoError(t, err) } func TestEventRuleCertificate(t *testing.T) { smtpCfg := smtp.Config{ Host: "127.0.0.1", Port: 2525, From: "notify@example.com", TemplatesPath: "templates", } err := smtpCfg.Initialize(configDir, true) require.NoError(t, err) lastReceivedEmail.reset() a1 := dataprovider.BaseEventAction{ Name: "action1", Type: dataprovider.ActionTypeEmail, Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"test@example.com"}, Subject: `"{{.Event}} {{.StatusString}}"`, ContentType: 0, Body: "Domain: {{.Name}} Timestamp: {{.Timestamp}} {{.ErrorString}} Date time: {{.DateTime}}", }, }, } action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated) assert.NoError(t, err) a2 := dataprovider.BaseEventAction{ Name: "action2", Type: dataprovider.ActionTypeFolderQuotaReset, } action2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated) assert.NoError(t, err) r1 := dataprovider.EventRule{ Name: "test rule certificate", Status: 1, Trigger: dataprovider.EventTriggerCertificate, Actions: []dataprovider.EventAction{ { BaseEventAction: dataprovider.BaseEventAction{ Name: action1.Name, }, Order: 1, }, }, } rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated) assert.NoError(t, err) r2 := dataprovider.EventRule{ Name: "test rule 2", Status: 1, Trigger: dataprovider.EventTriggerCertificate, Actions: []dataprovider.EventAction{ { BaseEventAction: dataprovider.BaseEventAction{ Name: action1.Name, }, Order: 1, }, { BaseEventAction: dataprovider.BaseEventAction{ Name: action2.Name, }, Order: 2, }, }, } rule2, _, err := httpdtest.AddEventRule(r2, http.StatusCreated) assert.NoError(t, err) renewalEvent := "Certificate renewal" common.HandleCertificateEvent(common.EventParams{ Name: "example.com", Timestamp: time.Now(), 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, slices.Contains(email.To, "test@example.com")) assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, renewalEvent)) assert.Contains(t, email.Data, "Content-Type: text/plain") assert.Contains(t, email.Data, `Domain: example.com Timestamp`) lastReceivedEmail.reset() dateTime := time.Now() params := common.EventParams{ Name: "example.com", Timestamp: dateTime, Status: 2, Event: renewalEvent, } errRenew := errors.New("generic renew error") params.AddError(errRenew) common.HandleCertificateEvent(params) 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, slices.Contains(email.To, "test@example.com")) assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s KO"`, renewalEvent)) assert.Contains(t, email.Data, `Domain: example.com Timestamp`) assert.Contains(t, email.Data, dateTime.UTC().Format("2006-01-02T15:04:05.000")) assert.Contains(t, email.Data, errRenew.Error()) _, err = httpdtest.RemoveEventRule(rule1, http.StatusOK) assert.NoError(t, err) _, err = httpdtest.RemoveEventRule(rule2, 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) // ignored no more certificate rules common.HandleCertificateEvent(common.EventParams{ Name: "example.com", Timestamp: time.Now(), Status: 1, Event: renewalEvent, }) smtpCfg = smtp.Config{} err = smtpCfg.Initialize(configDir, true) require.NoError(t, err) } func TestEventRuleIPBlocked(t *testing.T) { oldConfig := config.GetCommonConfig() cfg := config.GetCommonConfig() cfg.DefenderConfig.Enabled = true cfg.DefenderConfig.Threshold = 3 cfg.DefenderConfig.ScoreLimitExceeded = 2 err := common.Initialize(cfg, 0) assert.NoError(t, err) smtpCfg := smtp.Config{ Host: "127.0.0.1", Port: 2525, From: "notification@example.com", TemplatesPath: "templates", } err = smtpCfg.Initialize(configDir, true) require.NoError(t, err) a1 := dataprovider.BaseEventAction{ Name: "action1", Type: dataprovider.ActionTypeEmail, Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"test3@example.com", "test4@example.com"}, Subject: `New "{{.Event}}"`, Body: "IP: {{.IP}} Timestamp: {{.Timestamp}}", }, }, } action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated) assert.NoError(t, err) a2 := dataprovider.BaseEventAction{ Name: "action2", Type: dataprovider.ActionTypeFolderQuotaReset, } action2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated) assert.NoError(t, err) r1 := dataprovider.EventRule{ Name: "test rule ip blocked", Status: 1, Trigger: dataprovider.EventTriggerIPBlocked, Actions: []dataprovider.EventAction{ { BaseEventAction: dataprovider.BaseEventAction{ Name: action1.Name, }, Order: 1, }, }, } rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated) assert.NoError(t, err) r2 := dataprovider.EventRule{ Name: "test rule 2", Status: 1, Trigger: dataprovider.EventTriggerIPBlocked, Actions: []dataprovider.EventAction{ { BaseEventAction: dataprovider.BaseEventAction{ Name: action1.Name, }, Order: 1, }, { BaseEventAction: dataprovider.BaseEventAction{ Name: action2.Name, }, Order: 2, }, }, } rule2, _, err := httpdtest.AddEventRule(r2, http.StatusCreated) assert.NoError(t, err) u := getTestUser() user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) lastReceivedEmail.reset() time.Sleep(300 * time.Millisecond) assert.Empty(t, lastReceivedEmail.get().From, lastReceivedEmail.get().Data) for i := 0; i < 3; i++ { user.Password = "wrong_pwd" _, _, err = getSftpClient(user) assert.Error(t, err) } // the client is now banned user.Password = defaultPassword _, _, err = getSftpClient(user) assert.Error(t, err) // check the email notification assert.Eventually(t, func() bool { return lastReceivedEmail.get().From != "" }, 3000*time.Millisecond, 100*time.Millisecond) email := lastReceivedEmail.get() assert.Len(t, email.To, 2) assert.True(t, slices.Contains(email.To, "test3@example.com")) assert.True(t, slices.Contains(email.To, "test4@example.com")) assert.Contains(t, email.Data, `Subject: New "IP Blocked"`) err = dataprovider.DeleteEventRule(rule1.Name, "", "", "") assert.NoError(t, err) err = dataprovider.DeleteEventRule(rule2.Name, "", "", "") assert.NoError(t, err) err = dataprovider.DeleteEventAction(action1.Name, "", "", "") assert.NoError(t, err) err = dataprovider.DeleteEventAction(action2.Name, "", "", "") assert.NoError(t, err) err = dataprovider.DeleteUser(user.Username, "", "", "") assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) smtpCfg = smtp.Config{} err = smtpCfg.Initialize(configDir, true) require.NoError(t, err) err = common.Initialize(oldConfig, 0) assert.NoError(t, err) } func TestEventRuleRotateLog(t *testing.T) { smtpCfg := smtp.Config{ Host: "127.0.0.1", Port: 2525, From: "notification@example.com", TemplatesPath: "templates", } err := smtpCfg.Initialize(configDir, true) require.NoError(t, err) user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) a1 := dataprovider.BaseEventAction{ Name: "a1", Type: dataprovider.ActionTypeRotateLogs, } action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated) assert.NoError(t, err) a2 := dataprovider.BaseEventAction{ Name: "a2", Type: dataprovider.ActionTypeEmail, Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"success@example.net"}, Subject: `OK`, Body: "OK action", }, }, } action2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated) assert.NoError(t, err) r1 := dataprovider.EventRule{ Name: "rule1", Status: 1, Trigger: dataprovider.EventTriggerFsEvent, Conditions: dataprovider.EventConditions{ FsEvents: []string{"mkdir"}, Options: dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: user.Username, }, }, }, }, Actions: []dataprovider.EventAction{ { BaseEventAction: dataprovider.BaseEventAction{ Name: action1.Name, }, Order: 1, }, { BaseEventAction: dataprovider.BaseEventAction{ Name: action2.Name, }, Order: 2, }, }, } rule1, resp, err := httpdtest.AddEventRule(r1, http.StatusCreated) assert.NoError(t, err, string(resp)) conn, client, err := getSftpClient(user) if assert.NoError(t, err) { defer conn.Close() defer client.Close() lastReceivedEmail.reset() err := client.Mkdir("just a test dir") assert.NoError(t, err) // just check that the action is executed 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.Contains(t, email.To, "success@example.net") } _, 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) _, 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, true) require.NoError(t, err) } func TestEventRuleInactivityCheck(t *testing.T) { smtpCfg := smtp.Config{ Host: "127.0.0.1", Port: 2525, From: "notification@example.com", TemplatesPath: "templates", } err := smtpCfg.Initialize(configDir, true) require.NoError(t, err) user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) a1 := dataprovider.BaseEventAction{ Name: "a1", Type: dataprovider.ActionTypeUserInactivityCheck, Options: dataprovider.BaseEventActionOptions{ UserInactivityConfig: dataprovider.EventActionUserInactivity{ DisableThreshold: 10, DeleteThreshold: 20, }, }, } action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated) assert.NoError(t, err) a2 := dataprovider.BaseEventAction{ Name: "a2", Type: dataprovider.ActionTypeEmail, Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"success@example.net"}, Subject: `OK`, Body: "OK action", }, }, } action2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated) assert.NoError(t, err) r1 := dataprovider.EventRule{ Name: "rule1", Status: 1, Trigger: dataprovider.EventTriggerFsEvent, Conditions: dataprovider.EventConditions{ FsEvents: []string{"mkdir"}, Options: dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: user.Username, }, }, }, }, Actions: []dataprovider.EventAction{ { BaseEventAction: dataprovider.BaseEventAction{ Name: action1.Name, }, Order: 1, }, { BaseEventAction: dataprovider.BaseEventAction{ Name: action2.Name, }, Order: 2, }, }, } rule1, resp, err := httpdtest.AddEventRule(r1, http.StatusCreated) assert.NoError(t, err, string(resp)) conn, client, err := getSftpClient(user) if assert.NoError(t, err) { defer conn.Close() defer client.Close() lastReceivedEmail.reset() err := client.Mkdir("just a test dir") assert.NoError(t, err) // just check that the action is executed 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.Contains(t, email.To, "success@example.net") } _, 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) _, 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, true) require.NoError(t, err) } func TestEventRulePasswordExpiration(t *testing.T) { smtpCfg := smtp.Config{ Host: "127.0.0.1", Port: 2525, From: "notification@example.com", TemplatesPath: "templates", } err := smtpCfg.Initialize(configDir, true) require.NoError(t, err) user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) a1 := dataprovider.BaseEventAction{ Name: "a1", Type: dataprovider.ActionTypeEmail, Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"failure@example.net"}, Subject: `Failure`, Body: "Failure action", }, }, } action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated) assert.NoError(t, err) a2 := dataprovider.BaseEventAction{ Name: "a2", Type: dataprovider.ActionTypePasswordExpirationCheck, Options: dataprovider.BaseEventActionOptions{ PwdExpirationConfig: dataprovider.EventActionPasswordExpiration{ Threshold: 10, }, }, } action2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated) assert.NoError(t, err) a3 := dataprovider.BaseEventAction{ Name: "a3", Type: dataprovider.ActionTypeEmail, Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"success@example.net"}, Subject: `OK`, Body: "OK action", }, }, } action3, _, err := httpdtest.AddEventAction(a3, http.StatusCreated) assert.NoError(t, err) r1 := dataprovider.EventRule{ Name: "rule1", Status: 1, Trigger: dataprovider.EventTriggerFsEvent, Conditions: dataprovider.EventConditions{ FsEvents: []string{"mkdir"}, }, Actions: []dataprovider.EventAction{ { BaseEventAction: dataprovider.BaseEventAction{ Name: action2.Name, }, Order: 1, }, { BaseEventAction: dataprovider.BaseEventAction{ Name: action3.Name, }, Order: 2, }, { BaseEventAction: dataprovider.BaseEventAction{ Name: action1.Name, }, Options: dataprovider.EventActionOptions{ IsFailureAction: true, }, }, }, } rule1, resp, err := httpdtest.AddEventRule(r1, http.StatusCreated) assert.NoError(t, err, string(resp)) dirName := "aTestDir" conn, client, err := getSftpClient(user) if assert.NoError(t, err) { defer conn.Close() defer client.Close() lastReceivedEmail.reset() err := client.Mkdir(dirName) assert.NoError(t, err) // the user has no password expiration, the check will be skipped and the ok action executed 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.Contains(t, email.To, "success@example.net") err = client.RemoveDirectory(dirName) assert.NoError(t, err) } user.Filters.PasswordExpiration = 20 _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) conn, client, err = getSftpClient(user) if assert.NoError(t, err) { defer conn.Close() defer client.Close() lastReceivedEmail.reset() err := client.Mkdir(dirName) assert.NoError(t, err) // the passowrd is not about to expire, the check will be skipped and the ok action executed 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.Contains(t, email.To, "success@example.net") err = client.RemoveDirectory(dirName) assert.NoError(t, err) } user.Filters.PasswordExpiration = 5 _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) conn, client, err = getSftpClient(user) if assert.NoError(t, err) { defer conn.Close() defer client.Close() lastReceivedEmail.reset() err := client.Mkdir(dirName) assert.NoError(t, err) // the passowrd is about to expire, the user has no email, the failure action will be executed 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.Contains(t, email.To, "failure@example.net") err = client.RemoveDirectory(dirName) assert.NoError(t, err) } // remove the success action rule1.Actions = []dataprovider.EventAction{ { BaseEventAction: dataprovider.BaseEventAction{ Name: action2.Name, }, Order: 1, }, { BaseEventAction: dataprovider.BaseEventAction{ Name: action1.Name, }, Options: dataprovider.EventActionOptions{ IsFailureAction: true, }, }, } _, _, err = httpdtest.UpdateEventRule(rule1, http.StatusOK) assert.NoError(t, err) user.Email = "user@example.net" user.Filters.AdditionalEmails = []string{"additional@example.net"} _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) conn, client, err = getSftpClient(user) if assert.NoError(t, err) { defer conn.Close() defer client.Close() lastReceivedEmail.reset() err := client.Mkdir(dirName) assert.NoError(t, err) // the passowrd expiration will be notified assert.Eventually(t, func() bool { return lastReceivedEmail.get().From != "" }, 1500*time.Millisecond, 100*time.Millisecond) email := lastReceivedEmail.get() assert.Len(t, email.To, 2) assert.Contains(t, email.To, user.Email) assert.Contains(t, email.To, user.Filters.AdditionalEmails[0]) assert.Contains(t, email.Data, "your SFTPGo password expires in 5 days") err = client.RemoveDirectory(dirName) assert.NoError(t, err) } _, 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) _, err = httpdtest.RemoveEventAction(action3, 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, true) require.NoError(t, err) } func TestSyncUploadAction(t *testing.T) { if runtime.GOOS == osWindows { t.Skip("this test is not available on Windows") } uploadScriptPath := filepath.Join(os.TempDir(), "upload.sh") common.Config.Actions.ExecuteOn = []string{"upload"} common.Config.Actions.ExecuteSync = []string{"upload"} common.Config.Actions.Hook = uploadScriptPath u := getTestUser() u.QuotaFiles = 1000 user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) movedFileName := "moved.dat" movedPath := filepath.Join(user.HomeDir, movedFileName) err = os.WriteFile(uploadScriptPath, getUploadScriptContent(movedPath, "", 0), 0755) assert.NoError(t, err) conn, client, err := getSftpClient(user) if assert.NoError(t, err) { defer conn.Close() defer client.Close() size := int64(32768) err = writeSFTPFileNoCheck(testFileName, size, client) assert.NoError(t, err) _, err = client.Stat(testFileName) assert.Error(t, err) info, err := client.Stat(movedFileName) if assert.NoError(t, err) { assert.Equal(t, size, info.Size()) } user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 1, user.UsedQuotaFiles) assert.Equal(t, size, user.UsedQuotaSize) // test some hook failure // the uploaded file is moved and the hook fails, it will be not removed from the quota err = os.WriteFile(uploadScriptPath, getUploadScriptContent(movedPath, "", 1), 0755) assert.NoError(t, err) err = writeSFTPFileNoCheck(testFileName+"_1", size, client) assert.Error(t, err) user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 2, user.UsedQuotaFiles) assert.Equal(t, size*2, user.UsedQuotaSize) // the uploaded file is not moved and the hook fails, the uploaded file will be deleted // and removed from the quota movedPath = filepath.Join(user.HomeDir, "missing dir", movedFileName) err = os.WriteFile(uploadScriptPath, getUploadScriptContent(movedPath, "", 1), 0755) assert.NoError(t, err) err = writeSFTPFileNoCheck(testFileName+"_2", size, client) assert.Error(t, err) user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 2, user.UsedQuotaFiles) assert.Equal(t, size*2, user.UsedQuotaSize) // overwrite an existing file _, err = client.Stat(movedFileName) assert.NoError(t, err) err = writeSFTPFileNoCheck(movedFileName, size, client) assert.Error(t, err) _, err = client.Stat(movedFileName) assert.Error(t, err) user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 1, user.UsedQuotaFiles) assert.Equal(t, size, user.UsedQuotaSize) } err = os.Remove(uploadScriptPath) assert.NoError(t, err) _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) common.Config.Actions.ExecuteOn = nil common.Config.Actions.ExecuteSync = nil common.Config.Actions.Hook = uploadScriptPath } func TestQuotaTrackDisabled(t *testing.T) { err := dataprovider.Close() assert.NoError(t, err) err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf := config.GetProviderConf() providerConf.TrackQuota = 0 err = dataprovider.Initialize(providerConf, configDir, true) 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() err = writeSFTPFile(testFileName, 32, client) assert.NoError(t, err) err = client.Rename(testFileName, testFileName+"1") assert.NoError(t, err) } _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) err = dataprovider.Close() assert.NoError(t, err) err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf = config.GetProviderConf() err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) } func TestGetQuotaError(t *testing.T) { if dataprovider.GetProviderStatus().Driver == "memory" { t.Skip("this test is not available with the memory provider") } u := getTestUser() u.TotalDataTransfer = 2000 mappedPath := filepath.Join(os.TempDir(), "vdir") folderName := filepath.Base(mappedPath) vdirPath := "/vpath" f := vfs.BaseVirtualFolder{ Name: folderName, MappedPath: mappedPath, } _, _, err := httpdtest.AddFolder(f, http.StatusCreated) assert.NoError(t, err) u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{ BaseVirtualFolder: vfs.BaseVirtualFolder{ Name: folderName, }, VirtualPath: vdirPath, QuotaSize: 0, QuotaFiles: 10, }) user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) conn, client, err := getSftpClient(user) if assert.NoError(t, err) { defer conn.Close() defer client.Close() err = writeSFTPFile(testFileName, 32, client) assert.NoError(t, err) err = dataprovider.Close() assert.NoError(t, err) err = client.Rename(testFileName, path.Join(vdirPath, testFileName)) assert.Error(t, err) err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf := config.GetProviderConf() err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) } _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) err = os.RemoveAll(mappedPath) assert.NoError(t, err) } func TestRetentionAPI(t *testing.T) { u := getTestUser() u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermOverwrite, dataprovider.PermDownload, dataprovider.PermCreateDirs, dataprovider.PermChtimes} user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) uploadPath := path.Join(testDir, testFileName) conn, client, err := getSftpClient(user) if assert.NoError(t, err) { defer conn.Close() defer client.Close() err = client.Mkdir(testDir) assert.NoError(t, err) err = writeSFTPFile(uploadPath, 32, client) assert.NoError(t, err) folderRetention := []dataprovider.FolderRetention{ { Path: "/", Retention: 24, DeleteEmptyDirs: true, }, } check := common.RetentionCheck{ Folders: folderRetention, } c := common.RetentionChecks.Add(check, &user) assert.NotNil(t, c) err = c.Start() assert.NoError(t, err) assert.Eventually(t, func() bool { return len(common.RetentionChecks.Get("")) == 0 }, 1000*time.Millisecond, 50*time.Millisecond) _, err = client.Stat(uploadPath) assert.NoError(t, err) err = client.Chtimes(uploadPath, time.Now().Add(-48*time.Hour), time.Now().Add(-48*time.Hour)) assert.NoError(t, err) err = c.Start() assert.NoError(t, err) assert.Eventually(t, func() bool { return len(common.RetentionChecks.Get("")) == 0 }, 1000*time.Millisecond, 50*time.Millisecond) _, err = client.Stat(uploadPath) assert.ErrorIs(t, err, os.ErrNotExist) _, err = client.Stat(testDir) assert.ErrorIs(t, err, os.ErrNotExist) err = client.Mkdir(testDir) assert.NoError(t, err) err = writeSFTPFile(uploadPath, 32, client) assert.NoError(t, err) check.Folders[0].DeleteEmptyDirs = false err = client.Chtimes(uploadPath, time.Now().Add(-48*time.Hour), time.Now().Add(-48*time.Hour)) assert.NoError(t, err) c = common.RetentionChecks.Add(check, &user) assert.NotNil(t, c) err = c.Start() assert.NoError(t, err) assert.Eventually(t, func() bool { return len(common.RetentionChecks.Get("")) == 0 }, 1000*time.Millisecond, 50*time.Millisecond) _, err = client.Stat(uploadPath) assert.ErrorIs(t, err, os.ErrNotExist) _, err = client.Stat(testDir) assert.NoError(t, err) err = writeSFTPFile(uploadPath, 32, client) assert.NoError(t, err) err = client.Chtimes(uploadPath, time.Now().Add(-48*time.Hour), time.Now().Add(-48*time.Hour)) assert.NoError(t, err) conn.Close() client.Close() } // remove delete permissions to the user, it will be automatically granted user.Permissions["/"+testDir] = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermCreateDirs, dataprovider.PermChtimes} user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) conn, client, err = getSftpClient(user) if assert.NoError(t, err) { innerUploadFilePath := path.Join("/"+testDir, testDir, testFileName) err = client.Mkdir(path.Join(testDir, testDir)) assert.NoError(t, err) err = writeSFTPFile(innerUploadFilePath, 32, client) assert.NoError(t, err) err = client.Chtimes(innerUploadFilePath, time.Now().Add(-48*time.Hour), time.Now().Add(-48*time.Hour)) assert.NoError(t, err) folderRetention := []dataprovider.FolderRetention{ { Path: "/missing", Retention: 24, }, { Path: "/" + testDir, Retention: 24, DeleteEmptyDirs: true, }, { Path: path.Dir(innerUploadFilePath), Retention: 0, }, } check := common.RetentionCheck{ Folders: folderRetention, } c := common.RetentionChecks.Add(check, &user) assert.NotNil(t, c) err = c.Start() assert.NoError(t, err) assert.Eventually(t, func() bool { return len(common.RetentionChecks.Get("")) == 0 }, 1000*time.Millisecond, 50*time.Millisecond) _, err = client.Stat(uploadPath) assert.ErrorIs(t, err, os.ErrNotExist) _, err = client.Stat(innerUploadFilePath) assert.NoError(t, err) folderRetention = []dataprovider.FolderRetention{ { Path: "/" + testDir, Retention: 24, DeleteEmptyDirs: true, }, } check = common.RetentionCheck{ Folders: folderRetention, } c = common.RetentionChecks.Add(check, &user) assert.NotNil(t, c) err = c.Start() assert.NoError(t, err) assert.Eventually(t, func() bool { return len(common.RetentionChecks.Get("")) == 0 }, 1000*time.Millisecond, 50*time.Millisecond) _, err = client.Stat(innerUploadFilePath) assert.ErrorIs(t, err, os.ErrNotExist) conn.Close() client.Close() } // finally test some errors removing files or folders if runtime.GOOS != osWindows { dirPath := filepath.Join(user.HomeDir, "adir", "sub") err := os.MkdirAll(dirPath, os.ModePerm) assert.NoError(t, err) filePath := filepath.Join(dirPath, "f.dat") err = os.WriteFile(filePath, nil, os.ModePerm) assert.NoError(t, err) err = os.Chtimes(filePath, time.Now().Add(-72*time.Hour), time.Now().Add(-72*time.Hour)) assert.NoError(t, err) err = os.Chmod(dirPath, 0001) assert.NoError(t, err) folderRetention := []dataprovider.FolderRetention{ { Path: "/adir", Retention: 24, DeleteEmptyDirs: true, }, } check := common.RetentionCheck{ Folders: folderRetention, } c := common.RetentionChecks.Add(check, &user) assert.NotNil(t, c) err = c.Start() assert.ErrorIs(t, err, os.ErrPermission) assert.Eventually(t, func() bool { return len(common.RetentionChecks.Get("")) == 0 }, 1000*time.Millisecond, 50*time.Millisecond) err = os.Chmod(dirPath, 0555) assert.NoError(t, err) c = common.RetentionChecks.Add(check, &user) assert.NotNil(t, c) err = c.Start() assert.ErrorIs(t, err, os.ErrPermission) assert.Eventually(t, func() bool { return len(common.RetentionChecks.Get("")) == 0 }, 1000*time.Millisecond, 50*time.Millisecond) err = os.Chmod(dirPath, os.ModePerm) assert.NoError(t, err) check = common.RetentionCheck{ Folders: folderRetention, } c = common.RetentionChecks.Add(check, &user) assert.NotNil(t, c) err = c.Start() assert.NoError(t, err) assert.Eventually(t, func() bool { return len(common.RetentionChecks.Get("")) == 0 }, 1000*time.Millisecond, 50*time.Millisecond) assert.NoDirExists(t, dirPath) } _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) assert.Eventually(t, func() bool { return common.Connections.GetClientConnections() == 0 }, 1*time.Second, 50*time.Millisecond) } func TestPerUserTransferLimits(t *testing.T) { oldMaxPerHostConns := common.Config.MaxPerHostConnections common.Config.MaxPerHostConnections = 2 u := getTestUser() u.UploadBandwidth = 32 user, _, err := httpdtest.AddUser(u, http.StatusCreated) if !assert.NoError(t, err) { printLatestLogs(20) } conn, client, err := getSftpClient(user) if assert.NoError(t, err) { defer conn.Close() defer client.Close() var wg sync.WaitGroup numErrors := 0 for i := 0; i <= 2; i++ { wg.Add(1) go func(counter int) { defer wg.Done() time.Sleep(20 * time.Millisecond) err := writeSFTPFile(fmt.Sprintf("%s_%d", testFileName, counter), 64*1024, client) if err != nil { numErrors++ } }(i) } wg.Wait() assert.Equal(t, 1, numErrors) } _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) common.Config.MaxPerHostConnections = oldMaxPerHostConns } func TestMaxSessionsSameConnection(t *testing.T) { u := getTestUser() u.UploadBandwidth = 32 u.MaxSessions = 2 user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) conn, client, err := getSftpClient(user) if assert.NoError(t, err) { defer conn.Close() defer client.Close() var wg sync.WaitGroup numErrors := 0 for i := 0; i <= 2; i++ { wg.Add(1) go func(counter int) { defer wg.Done() var err error if counter < 2 { err = writeSFTPFile(fmt.Sprintf("%s_%d", testFileName, counter), 64*1024, client) } else { // wait for the transfers to start time.Sleep(50 * time.Millisecond) _, _, err = getSftpClient(user) } if err != nil { numErrors++ } }(i) } wg.Wait() assert.Equal(t, 1, numErrors) } _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) } func TestRenameDir(t *testing.T) { u := getTestUser() testDir := "/dir-to-rename" u.Permissions[testDir] = []string{dataprovider.PermListItems, dataprovider.PermUpload} user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) conn, client, err := getSftpClient(user) if assert.NoError(t, err) { defer conn.Close() defer client.Close() err = client.Mkdir(testDir) assert.NoError(t, err) err = writeSFTPFile(path.Join(testDir, testFileName), 32, client) assert.NoError(t, err) err = client.Rename(testDir, testDir+"_rename") assert.ErrorIs(t, err, os.ErrPermission) } _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) } func TestBuiltinKeyboardInteractiveAuthentication(t *testing.T) { u := getTestUser() user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) authMethods := []ssh.AuthMethod{ ssh.KeyboardInteractive(func(_, _ string, _ []string, _ []bool) ([]string, error) { return []string{defaultPassword}, nil }), } conn, client, err := getCustomAuthSftpClient(user, authMethods) if assert.NoError(t, err) { defer conn.Close() defer client.Close() assert.NoError(t, checkBasicSFTP(client)) err = writeSFTPFile(testFileName, 4096, client) assert.NoError(t, err) } // add multi-factor authentication configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username) assert.NoError(t, err) user.Password = defaultPassword user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{ Enabled: true, ConfigName: configName, Secret: kms.NewPlainSecret(key.Secret()), Protocols: []string{common.ProtocolSSH}, } err = dataprovider.UpdateUser(&user, "", "", "") assert.NoError(t, err) passcode, err := generateTOTPPasscode(key.Secret(), otp.AlgorithmSHA1) assert.NoError(t, err) passwordAsked := false passcodeAsked := false authMethods = []ssh.AuthMethod{ ssh.KeyboardInteractive(func(_, _ string, questions []string, _ []bool) ([]string, error) { var answers []string if strings.HasPrefix(questions[0], "Password") { answers = append(answers, defaultPassword) passwordAsked = true } else { answers = append(answers, passcode) passcodeAsked = true } return answers, nil }), } conn, client, err = getCustomAuthSftpClient(user, authMethods) if assert.NoError(t, err) { defer conn.Close() defer client.Close() assert.NoError(t, checkBasicSFTP(client)) err = writeSFTPFile(testFileName, 4096, client) assert.NoError(t, err) } assert.True(t, passwordAsked) assert.True(t, passcodeAsked) _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) } func TestMultiStepBuiltinKeyboardAuth(t *testing.T) { u := getTestUser() u.PublicKeys = []string{testPubKey} u.Filters.DeniedLoginMethods = []string{ dataprovider.SSHLoginMethodPublicKey, dataprovider.LoginMethodPassword, dataprovider.SSHLoginMethodKeyboardInteractive, } user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) signer, err := ssh.ParsePrivateKey([]byte(testPrivateKey)) assert.NoError(t, err) // public key + password authMethods := []ssh.AuthMethod{ ssh.PublicKeys(signer), ssh.KeyboardInteractive(func(_, _ string, _ []string, _ []bool) ([]string, error) { return []string{defaultPassword}, nil }), } conn, client, err := getCustomAuthSftpClient(user, authMethods) if assert.NoError(t, err) { defer conn.Close() defer client.Close() assert.NoError(t, checkBasicSFTP(client)) err = writeSFTPFile(testFileName, 4096, client) assert.NoError(t, err) } // add multi-factor authentication configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username) assert.NoError(t, err) user.Password = defaultPassword user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{ Enabled: true, ConfigName: configName, Secret: kms.NewPlainSecret(key.Secret()), Protocols: []string{common.ProtocolSSH}, } err = dataprovider.UpdateUser(&user, "", "", "") assert.NoError(t, err) passcode, err := generateTOTPPasscode(key.Secret(), otp.AlgorithmSHA1) assert.NoError(t, err) // public key + passcode authMethods = []ssh.AuthMethod{ ssh.PublicKeys(signer), ssh.KeyboardInteractive(func(_, _ string, _ []string, _ []bool) ([]string, error) { return []string{passcode}, nil }), } conn, client, err = getCustomAuthSftpClient(user, authMethods) if assert.NoError(t, err) { defer conn.Close() defer client.Close() assert.NoError(t, checkBasicSFTP(client)) err = writeSFTPFile(testFileName, 4096, client) assert.NoError(t, err) } _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) } func TestRenameSymlink(t *testing.T) { u := getTestUser() testDir := "/dir-no-create-links" otherDir := "otherdir" u.Permissions[testDir] = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermCreateDirs} user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) conn, client, err := getSftpClient(user) if assert.NoError(t, err) { defer conn.Close() defer client.Close() err = client.Mkdir(otherDir) assert.NoError(t, err) err = client.Symlink(otherDir, otherDir+".link") assert.NoError(t, err) err = client.Rename(otherDir+".link", path.Join(testDir, "symlink")) assert.ErrorIs(t, err, os.ErrPermission) err = client.Rename(otherDir+".link", "allowed_link") assert.NoError(t, err) } _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) } func TestSplittedDeletePerms(t *testing.T) { u := getTestUser() u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermDeleteDirs, dataprovider.PermCreateDirs} user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) conn, client, err := getSftpClient(user) if assert.NoError(t, err) { defer conn.Close() defer client.Close() err = writeSFTPFile(testFileName, 4096, client) assert.NoError(t, err) err = client.Remove(testFileName) assert.Error(t, err) err = client.Mkdir(testDir) assert.NoError(t, err) err = client.RemoveDirectory(testDir) assert.NoError(t, err) } u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermDeleteFiles, dataprovider.PermCreateDirs, dataprovider.PermOverwrite} _, _, err = httpdtest.UpdateUser(u, http.StatusOK, "") assert.NoError(t, err) conn, client, err = getSftpClient(user) if assert.NoError(t, err) { defer conn.Close() defer client.Close() err = writeSFTPFile(testFileName, 4096, client) assert.NoError(t, err) err = client.Remove(testFileName) assert.NoError(t, err) err = client.Mkdir(testDir) assert.NoError(t, err) err = client.RemoveDirectory(testDir) assert.Error(t, err) } _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) } func TestSplittedRenamePerms(t *testing.T) { u := getTestUser() u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermRenameDirs, dataprovider.PermCreateDirs} user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) conn, client, err := getSftpClient(user) if assert.NoError(t, err) { defer conn.Close() defer client.Close() err = writeSFTPFile(testFileName, 4096, client) assert.NoError(t, err) err = client.Mkdir(testDir) assert.NoError(t, err) err = client.Rename(testFileName, testFileName+"_renamed") assert.Error(t, err) err = client.Rename(testDir, testDir+"_renamed") assert.NoError(t, err) } u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermRenameFiles, dataprovider.PermCreateDirs, dataprovider.PermOverwrite} _, _, err = httpdtest.UpdateUser(u, http.StatusOK, "") assert.NoError(t, err) conn, client, err = getSftpClient(user) if assert.NoError(t, err) { defer conn.Close() defer client.Close() err = writeSFTPFile(testFileName, 4096, client) assert.NoError(t, err) err = client.Mkdir(testDir) assert.NoError(t, err) err = client.Rename(testFileName, testFileName+"_renamed") assert.NoError(t, err) err = client.Rename(testDir, testDir+"_renamed") assert.Error(t, err) } _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) } func TestSFTPLoopError(t *testing.T) { smtpCfg := smtp.Config{ Host: "127.0.0.1", Port: 2525, From: "notification@example.com", TemplatesPath: "templates", } err := smtpCfg.Initialize(configDir, true) require.NoError(t, err) user1 := getTestUser() user2 := getTestUser() user1.Username += "1" user2.Username += "2" // user1 is a local account with a virtual SFTP folder to user2 // user2 has user1 as SFTP fs f := vfs.BaseVirtualFolder{ Name: "sftp", FsConfig: vfs.Filesystem{ Provider: sdk.SFTPFilesystemProvider, SFTPConfig: vfs.SFTPFsConfig{ BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{ Endpoint: sftpServerAddr, Username: user2.Username, }, Password: kms.NewPlainSecret(defaultPassword), }, }, } folder, _, err := httpdtest.AddFolder(f, http.StatusCreated) assert.NoError(t, err) user1.VirtualFolders = append(user1.VirtualFolders, vfs.VirtualFolder{ BaseVirtualFolder: vfs.BaseVirtualFolder{ Name: folder.Name, }, VirtualPath: "/vdir", }) user2.FsConfig.Provider = sdk.SFTPFilesystemProvider user2.FsConfig.SFTPConfig = vfs.SFTPFsConfig{ BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{ Endpoint: sftpServerAddr, Username: user1.Username, }, Password: kms.NewPlainSecret(defaultPassword), } user1, resp, err := httpdtest.AddUser(user1, http.StatusCreated) assert.NoError(t, err, string(resp)) user2, resp, err = httpdtest.AddUser(user2, http.StatusCreated) assert.NoError(t, err, string(resp)) a1 := dataprovider.BaseEventAction{ Name: "a1", Type: dataprovider.ActionTypeUserQuotaReset, } action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated) assert.NoError(t, err) a2 := dataprovider.BaseEventAction{ Name: "a2", Type: dataprovider.ActionTypeEmail, Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"failure@example.com"}, Subject: `Failed action"`, Body: "Test body", }, }, } action2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated) assert.NoError(t, err) r1 := dataprovider.EventRule{ Name: "rule1", Status: 1, Trigger: dataprovider.EventTriggerProviderEvent, Conditions: dataprovider.EventConditions{ ProviderEvents: []string{"update"}, }, Actions: []dataprovider.EventAction{ { BaseEventAction: dataprovider.BaseEventAction{ Name: action1.Name, }, Order: 1, }, { BaseEventAction: dataprovider.BaseEventAction{ Name: action2.Name, }, Order: 2, Options: dataprovider.EventActionOptions{ IsFailureAction: true, }, }, }, } rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated) assert.NoError(t, err) lastReceivedEmail.reset() _, _, err = httpdtest.UpdateUser(user2, http.StatusOK, "") assert.NoError(t, err) 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, slices.Contains(email.To, "failure@example.com")) assert.Contains(t, email.Data, `Subject: Failed action`) user1.VirtualFolders[0].FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword) user2.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword) conn := common.NewBaseConnection("", common.ProtocolWebDAV, "", "", user1) _, _, err = conn.GetFsAndResolvedPath(user1.VirtualFolders[0].VirtualPath) assert.ErrorIs(t, err, os.ErrPermission) conn = common.NewBaseConnection("", common.ProtocolSFTP, "", "", user1) _, _, err = conn.GetFsAndResolvedPath(user1.VirtualFolders[0].VirtualPath) assert.Error(t, err) conn = common.NewBaseConnection("", common.ProtocolFTP, "", "", user1) _, _, err = conn.GetFsAndResolvedPath(user1.VirtualFolders[0].VirtualPath) assert.Error(t, err) _, 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) _, err = httpdtest.RemoveUser(user1, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user1.GetHomeDir()) assert.NoError(t, err) _, err = httpdtest.RemoveUser(user2, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user2.GetHomeDir()) assert.NoError(t, err) _, err = httpdtest.RemoveFolder(folder, http.StatusOK) assert.NoError(t, err) smtpCfg = smtp.Config{} err = smtpCfg.Initialize(configDir, true) require.NoError(t, err) } func TestNonLocalCrossRename(t *testing.T) { baseUser, resp, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err, string(resp)) u := getTestUser() u.HomeDir += "_folders" u.Username += "_folders" mappedPathSFTP := filepath.Join(os.TempDir(), "sftp") folderNameSFTP := filepath.Base(mappedPathSFTP) vdirSFTPPath := "/vdir/sftp" u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{ BaseVirtualFolder: vfs.BaseVirtualFolder{ Name: folderNameSFTP, }, VirtualPath: vdirSFTPPath, }) mappedPathCrypt := filepath.Join(os.TempDir(), "crypt") folderNameCrypt := filepath.Base(mappedPathCrypt) vdirCryptPath := "/vdir/crypt" u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{ BaseVirtualFolder: vfs.BaseVirtualFolder{ Name: folderNameCrypt, }, VirtualPath: vdirCryptPath, }) f1 := vfs.BaseVirtualFolder{ Name: folderNameSFTP, FsConfig: vfs.Filesystem{ Provider: sdk.SFTPFilesystemProvider, SFTPConfig: vfs.SFTPFsConfig{ BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{ Endpoint: sftpServerAddr, Username: baseUser.Username, }, Password: kms.NewPlainSecret(defaultPassword), }, }, } _, _, err = httpdtest.AddFolder(f1, http.StatusCreated) assert.NoError(t, err) f2 := vfs.BaseVirtualFolder{ Name: folderNameCrypt, FsConfig: vfs.Filesystem{ Provider: sdk.CryptedFilesystemProvider, CryptConfig: vfs.CryptFsConfig{ Passphrase: kms.NewPlainSecret(defaultPassword), }, }, MappedPath: mappedPathCrypt, } _, _, err = httpdtest.AddFolder(f2, http.StatusCreated) assert.NoError(t, err) user, resp, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err, string(resp)) conn, client, err := getSftpClient(user) if assert.NoError(t, err) { defer conn.Close() defer client.Close() assert.NoError(t, checkBasicSFTP(client)) err = writeSFTPFile(testFileName, 4096, client) assert.NoError(t, err) err = writeSFTPFile(path.Join(vdirSFTPPath, testFileName), 8192, client) assert.NoError(t, err) err = writeSFTPFile(path.Join(vdirCryptPath, testFileName), 16384, client) assert.NoError(t, err) err = client.Rename(path.Join(vdirSFTPPath, testFileName), path.Join(vdirCryptPath, testFileName+".rename")) assert.ErrorIs(t, err, os.ErrPermission) err = client.Rename(path.Join(vdirCryptPath, testFileName), path.Join(vdirSFTPPath, testFileName+".rename")) assert.ErrorIs(t, err, os.ErrPermission) err = client.Rename(testFileName, path.Join(vdirCryptPath, testFileName+".rename")) assert.ErrorIs(t, err, os.ErrPermission) err = client.Rename(testFileName, path.Join(vdirSFTPPath, testFileName+".rename")) assert.ErrorIs(t, err, os.ErrPermission) err = client.Rename(path.Join(vdirSFTPPath, testFileName), testFileName+".rename") assert.ErrorIs(t, err, os.ErrPermission) err = client.Rename(path.Join(vdirCryptPath, testFileName), testFileName+".rename") assert.ErrorIs(t, err, os.ErrPermission) // rename on local fs or on the same folder must work err = client.Rename(testFileName, testFileName+".rename") assert.NoError(t, err) err = client.Rename(path.Join(vdirSFTPPath, testFileName), path.Join(vdirSFTPPath, testFileName+"_rename")) assert.NoError(t, err) err = client.Rename(path.Join(vdirCryptPath, testFileName), path.Join(vdirCryptPath, testFileName+"_rename")) assert.NoError(t, err) // renaming a virtual folder is not allowed err = client.Rename(vdirSFTPPath, vdirSFTPPath+"_rename") assert.ErrorIs(t, err, os.ErrPermission) err = client.Rename(vdirCryptPath, vdirCryptPath+"_rename") assert.ErrorIs(t, err, os.ErrPermission) err = client.Rename(vdirCryptPath, path.Join(vdirCryptPath, "rename")) assert.ErrorIs(t, err, os.ErrPermission) err = client.Mkdir(path.Join(vdirCryptPath, "subcryptdir")) assert.NoError(t, err) err = client.Rename(path.Join(vdirCryptPath, "subcryptdir"), vdirCryptPath) assert.ErrorIs(t, err, os.ErrPermission) // renaming root folder is not allowed err = client.Rename("/", "new_name") assert.ErrorIs(t, err, os.ErrPermission) // renaming a path to a virtual folder is not allowed err = client.Rename("/vdir", "new_vdir") if assert.Error(t, err) { assert.Contains(t, err.Error(), "SSH_FX_OP_UNSUPPORTED") } } _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderNameCrypt}, http.StatusOK) assert.NoError(t, err) _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderNameSFTP}, http.StatusOK) assert.NoError(t, err) _, err = httpdtest.RemoveUser(baseUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) err = os.RemoveAll(baseUser.GetHomeDir()) assert.NoError(t, err) err = os.RemoveAll(mappedPathCrypt) assert.NoError(t, err) err = os.RemoveAll(mappedPathSFTP) assert.NoError(t, err) } func TestNonLocalCrossRenameNonLocalBaseUser(t *testing.T) { baseUser, resp, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err, string(resp)) u := getTestSFTPUser() mappedPathLocal := filepath.Join(os.TempDir(), "local") folderNameLocal := filepath.Base(mappedPathLocal) vdirLocalPath := "/vdir/local" u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{ BaseVirtualFolder: vfs.BaseVirtualFolder{ Name: folderNameLocal, }, VirtualPath: vdirLocalPath, }) mappedPathCrypt := filepath.Join(os.TempDir(), "crypt") folderNameCrypt := filepath.Base(mappedPathCrypt) vdirCryptPath := "/vdir/crypt" u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{ BaseVirtualFolder: vfs.BaseVirtualFolder{ Name: folderNameCrypt, }, VirtualPath: vdirCryptPath, }) f1 := vfs.BaseVirtualFolder{ Name: folderNameLocal, MappedPath: mappedPathLocal, } _, _, err = httpdtest.AddFolder(f1, http.StatusCreated) assert.NoError(t, err) f2 := vfs.BaseVirtualFolder{ Name: folderNameCrypt, FsConfig: vfs.Filesystem{ Provider: sdk.CryptedFilesystemProvider, CryptConfig: vfs.CryptFsConfig{ Passphrase: kms.NewPlainSecret(defaultPassword), }, }, MappedPath: mappedPathCrypt, } _, _, err = httpdtest.AddFolder(f2, http.StatusCreated) assert.NoError(t, err) user, resp, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err, string(resp)) conn, client, err := getSftpClient(user) if assert.NoError(t, err) { defer conn.Close() defer client.Close() assert.NoError(t, checkBasicSFTP(client)) err = writeSFTPFile(testFileName, 4096, client) assert.NoError(t, err) err = writeSFTPFile(path.Join(vdirLocalPath, testFileName), 8192, client) assert.NoError(t, err) err = writeSFTPFile(path.Join(vdirCryptPath, testFileName), 16384, client) assert.NoError(t, err) err = client.Rename(path.Join(vdirLocalPath, testFileName), path.Join(vdirCryptPath, testFileName+".rename")) assert.ErrorIs(t, err, os.ErrPermission) err = client.Rename(path.Join(vdirCryptPath, testFileName), path.Join(vdirLocalPath, testFileName+".rename")) assert.ErrorIs(t, err, os.ErrPermission) err = client.Rename(testFileName, path.Join(vdirCryptPath, testFileName+".rename")) assert.ErrorIs(t, err, os.ErrPermission) err = client.Rename(testFileName, path.Join(vdirLocalPath, testFileName+".rename")) assert.ErrorIs(t, err, os.ErrPermission) err = client.Rename(path.Join(vdirLocalPath, testFileName), testFileName+".rename") assert.ErrorIs(t, err, os.ErrPermission) err = client.Rename(path.Join(vdirCryptPath, testFileName), testFileName+".rename") assert.ErrorIs(t, err, os.ErrPermission) // rename on local fs or on the same folder must work err = client.Rename(testFileName, testFileName+".rename") assert.NoError(t, err) err = client.Rename(path.Join(vdirLocalPath, testFileName), path.Join(vdirLocalPath, testFileName+"_rename")) assert.NoError(t, err) err = client.Rename(path.Join(vdirCryptPath, testFileName), path.Join(vdirCryptPath, testFileName+"_rename")) assert.NoError(t, err) // renaming a virtual folder is not allowed err = client.Rename(vdirLocalPath, vdirLocalPath+"_rename") assert.ErrorIs(t, err, os.ErrPermission) err = client.Rename(vdirCryptPath, vdirCryptPath+"_rename") assert.ErrorIs(t, err, os.ErrPermission) // renaming root folder is not allowed err = client.Rename("/", "new_name") assert.ErrorIs(t, err, os.ErrPermission) // renaming a path to a virtual folder is not allowed err = client.Rename("/vdir", "new_vdir") if assert.Error(t, err) { assert.Contains(t, err.Error(), "SSH_FX_OP_UNSUPPORTED") } } _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderNameCrypt}, http.StatusOK) assert.NoError(t, err) _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderNameLocal}, http.StatusOK) assert.NoError(t, err) _, err = httpdtest.RemoveUser(baseUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) err = os.RemoveAll(baseUser.GetHomeDir()) assert.NoError(t, err) err = os.RemoveAll(mappedPathCrypt) assert.NoError(t, err) err = os.RemoveAll(mappedPathLocal) assert.NoError(t, err) } func TestCopyAndRemoveSSHCommands(t *testing.T) { u := getTestUser() u.QuotaFiles = 1000 user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) conn, client, err := getSftpClient(user) if assert.NoError(t, err) { defer conn.Close() defer client.Close() fileSize := int64(32) err = writeSFTPFile(testFileName, fileSize, client) assert.NoError(t, err) testFileNameCopy := testFileName + "_copy" out, err := runSSHCommand(fmt.Sprintf("sftpgo-copy %s %s", testFileName, testFileNameCopy), user) assert.NoError(t, err, string(out)) // the resolved destination path match the source path out, err = runSSHCommand(fmt.Sprintf("sftpgo-copy %s %s", testFileName, path.Dir(testFileName)), user) assert.Error(t, err, string(out)) info, err := client.Stat(testFileNameCopy) if assert.NoError(t, err) { assert.Equal(t, fileSize, info.Size()) } testDir := "test dir" err = client.Mkdir(testDir) assert.NoError(t, err) out, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s '%s'`, testFileName, testDir), user) assert.NoError(t, err, string(out)) info, err = client.Stat(path.Join(testDir, testFileName)) if assert.NoError(t, err) { assert.Equal(t, fileSize, info.Size()) } user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 3*fileSize, user.UsedQuotaSize) assert.Equal(t, 3, user.UsedQuotaFiles) out, err = runSSHCommand(fmt.Sprintf("sftpgo-remove %s", testFileNameCopy), user) assert.NoError(t, err, string(out)) out, err = runSSHCommand(fmt.Sprintf(`sftpgo-remove '%s'`, testDir), user) assert.NoError(t, err, string(out)) user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, fileSize, user.UsedQuotaSize) assert.Equal(t, 1, user.UsedQuotaFiles) _, err = client.Stat(testFileNameCopy) assert.ErrorIs(t, err, os.ErrNotExist) // create a dir tree dir1 := "dir1" dir2 := "dir 2" err = client.MkdirAll(path.Join(dir1, dir2)) assert.NoError(t, err) toCreate := []string{ path.Join(dir1, testFileName), path.Join(dir1, dir2, testFileName), } for _, p := range toCreate { err = writeSFTPFile(p, fileSize, client) assert.NoError(t, err) } // create a symlink, copying a symlink is not supported err = client.Symlink(path.Join("/", dir1, testFileName), path.Join("/", dir1, testFileName+"_link")) assert.NoError(t, err) out, err = runSSHCommand(fmt.Sprintf("sftpgo-copy %s %s", path.Join("/", dir1, testFileName+"_link"), path.Join("/", testFileName+"_link")), user) assert.Error(t, err, string(out)) // copying a dir inside itself should fail out, err = runSSHCommand(fmt.Sprintf("sftpgo-copy %s %s", path.Join("/", dir1), path.Join("/", dir1, "sub")), user) assert.Error(t, err, string(out)) // copy source and dest must differ out, err = runSSHCommand(fmt.Sprintf("sftpgo-copy %s %s", path.Join("/", dir1), path.Join("/", dir1)), user) assert.Error(t, err, string(out)) // copy a missing file/dir should fail out, err = runSSHCommand(fmt.Sprintf("sftpgo-copy %s %s", path.Join("/", "missing_entry"), path.Join("/", dir1)), user) assert.Error(t, err, string(out)) // try to overwrite a file with a dir out, err = runSSHCommand(fmt.Sprintf("sftpgo-copy %s %s", path.Join("/", dir1), testFileName), user) assert.Error(t, err, string(out)) out, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s "%s"`, dir1, dir2), user) assert.NoError(t, err, string(out)) user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 5*fileSize, user.UsedQuotaSize) assert.Equal(t, 5, user.UsedQuotaFiles) // copy again, quota must remain unchanged out, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s/ "%s"`, dir1, dir2), user) assert.NoError(t, err, string(out)) _, err = client.Stat(dir2) assert.NoError(t, err) user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 5*fileSize, user.UsedQuotaSize) assert.Equal(t, 5, user.UsedQuotaFiles) // now copy inside target out, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s "%s"`, dir1, dir2), user) assert.NoError(t, err, string(out)) user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, 7*fileSize, user.UsedQuotaSize) assert.Equal(t, 7, user.UsedQuotaFiles) for _, p := range []string{dir1, dir2} { out, err = runSSHCommand(fmt.Sprintf(`sftpgo-remove "%s"`, p), user) assert.NoError(t, err, string(out)) _, err = client.Stat(p) assert.ErrorIs(t, err, os.ErrNotExist) } user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.Equal(t, fileSize, user.UsedQuotaSize) assert.Equal(t, 1, user.UsedQuotaFiles) // test quota errors user.QuotaFiles = 1 _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) // quota files exceeded out, err = runSSHCommand(fmt.Sprintf("sftpgo-copy %s %s", testFileName, testFileNameCopy), user) assert.Error(t, err, string(out)) user.QuotaFiles = 1000 user.QuotaSize = fileSize + 1 _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) // quota size exceeded after the copy out, err = runSSHCommand(fmt.Sprintf("sftpgo-copy %s %s", testFileName, testFileNameCopy), user) assert.Error(t, err, string(out)) user.QuotaSize = fileSize - 1 _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) // quota size exceeded out, err = runSSHCommand(fmt.Sprintf("sftpgo-copy %s %s", testFileName, testFileNameCopy), user) assert.Error(t, err, string(out)) } _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) } func TestCopyAndRemovePermissions(t *testing.T) { u := getTestUser() restrictedPath := "/dir/path" patternFilterPath := "/patterns" u.Filters.FilePatterns = []sdk.PatternsFilter{ { Path: patternFilterPath, DeniedPatterns: []string{"*.dat"}, }, } u.Permissions[restrictedPath] = []string{} user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) conn, client, err := getSftpClient(user) if assert.NoError(t, err) { defer conn.Close() defer client.Close() err = client.MkdirAll(restrictedPath) assert.NoError(t, err) err = client.MkdirAll(patternFilterPath) assert.NoError(t, err) err = writeSFTPFile(testFileName, 100, client) assert.NoError(t, err) // getting file writer will fail out, err := runSSHCommand(fmt.Sprintf(`sftpgo-copy %s %s`, testFileName, restrictedPath), user) assert.Error(t, err, string(out)) // file pattern not allowed out, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s %s`, testFileName, patternFilterPath), user) assert.Error(t, err, string(out)) testDir := path.Join("/", path.Base(restrictedPath)) err = client.Mkdir(testDir) assert.NoError(t, err) err = writeSFTPFile(path.Join(testDir, testFileName), 100, client) assert.NoError(t, err) // creating target dir will fail out, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s %s/`, testDir, restrictedPath), user) assert.Error(t, err, string(out)) // get dir contents will fail out, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s /`, restrictedPath), user) assert.Error(t, err, string(out)) // get dir contents will fail out, err = runSSHCommand(fmt.Sprintf(`sftpgo-remove %s`, restrictedPath), user) assert.Error(t, err, string(out)) // give list dir permissions and retry, now delete will fail user.Permissions[restrictedPath] = []string{dataprovider.PermListItems, dataprovider.PermUpload} user.Permissions[testDir] = []string{dataprovider.PermListItems} _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) // no copy permission out, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s %s`, testFileName, restrictedPath), user) assert.Error(t, err, string(out)) user.Permissions[restrictedPath] = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermCopy} user.Permissions[testDir] = []string{dataprovider.PermListItems, dataprovider.PermCopy} _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) out, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s %s`, testFileName, restrictedPath), user) assert.NoError(t, err, string(out)) // overwrite will fail, no permission out, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s %s`, testFileName, restrictedPath), user) assert.Error(t, err, string(out)) out, err = runSSHCommand(fmt.Sprintf(`sftpgo-remove %s`, restrictedPath), user) assert.Error(t, err, string(out)) // try to copy a file from testDir, we have only list permissions so getFileReader will fail out, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s %s`, path.Join(testDir, testFileName), testFileName+".copy"), user) assert.Error(t, err, string(out)) } _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) } func TestCrossFoldersCopy(t *testing.T) { baseUser, resp, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err, string(resp)) u := getTestUser() u.Username += "_1" u.HomeDir = filepath.Join(os.TempDir(), u.Username) u.QuotaFiles = 1000 mappedPath1 := filepath.Join(os.TempDir(), "mapped1") folderName1 := filepath.Base(mappedPath1) vpath1 := "/vdirs/vdir1" u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{ BaseVirtualFolder: vfs.BaseVirtualFolder{ Name: folderName1, }, VirtualPath: vpath1, QuotaSize: -1, QuotaFiles: -1, }) mappedPath2 := filepath.Join(os.TempDir(), "mapped1", "dir", "mapped2") folderName2 := filepath.Base(mappedPath2) vpath2 := "/vdirs/vdir2" u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{ BaseVirtualFolder: vfs.BaseVirtualFolder{ Name: folderName2, }, VirtualPath: vpath2, QuotaSize: -1, QuotaFiles: -1, }) mappedPath3 := filepath.Join(os.TempDir(), "mapped3") folderName3 := filepath.Base(mappedPath3) vpath3 := "/vdirs/vdir3" u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{ BaseVirtualFolder: vfs.BaseVirtualFolder{ Name: folderName3, }, VirtualPath: vpath3, QuotaSize: -1, QuotaFiles: -1, }) mappedPath4 := filepath.Join(os.TempDir(), "mapped4") folderName4 := filepath.Base(mappedPath4) vpath4 := "/vdirs/vdir4" u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{ BaseVirtualFolder: vfs.BaseVirtualFolder{ Name: folderName4, }, VirtualPath: vpath4, QuotaSize: -1, QuotaFiles: -1, }) f1 := vfs.BaseVirtualFolder{ Name: folderName1, MappedPath: mappedPath1, } _, _, err = httpdtest.AddFolder(f1, http.StatusCreated) assert.NoError(t, err) f2 := vfs.BaseVirtualFolder{ Name: folderName2, MappedPath: mappedPath2, } _, _, err = httpdtest.AddFolder(f2, http.StatusCreated) assert.NoError(t, err) f3 := vfs.BaseVirtualFolder{ Name: folderName3, MappedPath: mappedPath3, FsConfig: vfs.Filesystem{ Provider: sdk.CryptedFilesystemProvider, CryptConfig: vfs.CryptFsConfig{ Passphrase: kms.NewPlainSecret(defaultPassword), }, }, } _, _, err = httpdtest.AddFolder(f3, http.StatusCreated) assert.NoError(t, err) f4 := vfs.BaseVirtualFolder{ Name: folderName4, MappedPath: mappedPath4, FsConfig: vfs.Filesystem{ Provider: sdk.SFTPFilesystemProvider, SFTPConfig: vfs.SFTPFsConfig{ BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{ Endpoint: sftpServerAddr, Username: baseUser.Username, }, Password: kms.NewPlainSecret(defaultPassword), }, }, } _, _, err = httpdtest.AddFolder(f4, http.StatusCreated) assert.NoError(t, err) user, resp, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err, string(resp)) conn, client, err := getSftpClient(user) if assert.NoError(t, err) { defer conn.Close() defer client.Close() baseFileSize := int64(100) err = writeSFTPFile(path.Join(vpath1, testFileName), baseFileSize+1, client) assert.NoError(t, err) err = writeSFTPFile(path.Join(vpath2, testFileName), baseFileSize+2, client) assert.NoError(t, err) err = writeSFTPFile(path.Join(vpath3, testFileName), baseFileSize+3, client) assert.NoError(t, err) err = writeSFTPFile(path.Join(vpath4, testFileName), baseFileSize+4, client) assert.NoError(t, err) // cannot remove a directory with virtual folders inside out, err := runSSHCommand(fmt.Sprintf(`sftpgo-remove %s`, path.Dir(vpath1)), user) assert.Error(t, err, string(out)) // copy across virtual folders copyDir := "/copy" out, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s %s/`, path.Dir(vpath1), copyDir), user) assert.NoError(t, err, string(out)) // check the copy info, err := client.Stat(path.Join(copyDir, vpath1, testFileName)) if assert.NoError(t, err) { assert.Equal(t, baseFileSize+1, info.Size()) } info, err = client.Stat(path.Join(copyDir, vpath2, testFileName)) if assert.NoError(t, err) { assert.Equal(t, baseFileSize+2, info.Size()) } info, err = client.Stat(path.Join(copyDir, vpath3, testFileName)) if assert.NoError(t, err) { assert.Equal(t, baseFileSize+3, info.Size()) } info, err = client.Stat(path.Join(copyDir, vpath4, testFileName)) if assert.NoError(t, err) { assert.Equal(t, baseFileSize+4, info.Size()) } // nested fs paths out, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s %s`, vpath1, vpath2), user) assert.Error(t, err, string(out)) } _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) _, err = httpdtest.RemoveUser(baseUser, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(baseUser.GetHomeDir()) assert.NoError(t, err) for _, folderName := range []string{folderName1, folderName2, folderName3, folderName4} { _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(filepath.Join(os.TempDir(), folderName)) assert.NoError(t, err) } } func TestHTTPFs(t *testing.T) { u := getTestUserWithHTTPFs() user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) err = os.MkdirAll(user.GetHomeDir(), os.ModePerm) assert.NoError(t, err) conn := common.NewBaseConnection(xid.New().String(), common.ProtocolFTP, "", "", user) err = conn.CreateDir(httpFsWellKnowDir, false) assert.NoError(t, err) err = os.WriteFile(filepath.Join(os.TempDir(), "httpfs", defaultHTTPFsUsername, httpFsWellKnowDir, "file.txt"), []byte("data"), 0666) assert.NoError(t, err) err = conn.Copy(httpFsWellKnowDir, httpFsWellKnowDir+"_copy") assert.NoError(t, err) _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) } func TestProxyProtocol(t *testing.T) { resp, err := httpclient.Get(fmt.Sprintf("http://%v", httpProxyAddr)) if !assert.Error(t, err) { resp.Body.Close() } } func TestSetProtocol(t *testing.T) { conn := common.NewBaseConnection("id", "sshd_exec", "", "", dataprovider.User{BaseUser: sdk.BaseUser{HomeDir: os.TempDir()}}) conn.SetProtocol(common.ProtocolSCP) require.Equal(t, "SCP_id", conn.GetID()) } func TestGetFsError(t *testing.T) { u := getTestUser() u.FsConfig.Provider = sdk.GCSFilesystemProvider u.FsConfig.GCSConfig.Bucket = "test" u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("invalid JSON for credentials") conn := common.NewBaseConnection("", common.ProtocolFTP, "", "", u) _, _, err := conn.GetFsAndResolvedPath("/vpath") assert.Error(t, err) } func waitTCPListening(address string) { for { conn, err := net.Dial("tcp", address) if err != nil { logger.WarnToConsole("tcp server %v not listening: %v", address, err) time.Sleep(100 * time.Millisecond) continue } logger.InfoToConsole("tcp server %v now listening", address) conn.Close() break } } func checkBasicSFTP(client *sftp.Client) error { _, err := client.Getwd() if err != nil { return err } _, err = client.ReadDir(".") return err } func getCustomAuthSftpClient(user dataprovider.User, authMethods []ssh.AuthMethod) (*ssh.Client, *sftp.Client, error) { var sftpClient *sftp.Client config := &ssh.ClientConfig{ User: user.Username, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Auth: authMethods, Timeout: 5 * time.Second, } conn, err := ssh.Dial("tcp", sftpServerAddr, config) if err != nil { return conn, sftpClient, err } sftpClient, err = sftp.NewClient(conn) if err != nil { conn.Close() } return conn, sftpClient, err } func getSftpClient(user dataprovider.User) (*ssh.Client, *sftp.Client, error) { var sftpClient *sftp.Client config := &ssh.ClientConfig{ User: user.Username, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: 5 * time.Second, } if user.Password != "" { config.Auth = []ssh.AuthMethod{ssh.Password(user.Password)} } else { config.Auth = []ssh.AuthMethod{ssh.Password(defaultPassword)} } conn, err := ssh.Dial("tcp", sftpServerAddr, config) if err != nil { return conn, sftpClient, err } sftpClient, err = sftp.NewClient(conn) if err != nil { conn.Close() } return conn, sftpClient, err } func runSSHCommand(command string, user dataprovider.User) ([]byte, error) { var sshSession *ssh.Session var output []byte config := &ssh.ClientConfig{ User: user.Username, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: 5 * time.Second, } if user.Password != "" { config.Auth = []ssh.AuthMethod{ssh.Password(user.Password)} } else { config.Auth = []ssh.AuthMethod{ssh.Password(defaultPassword)} } conn, err := ssh.Dial("tcp", sftpServerAddr, config) if err != nil { return output, err } defer conn.Close() sshSession, err = conn.NewSession() if err != nil { return output, err } var stdout, stderr bytes.Buffer sshSession.Stdout = &stdout sshSession.Stderr = &stderr err = sshSession.Run(command) if err != nil { return nil, fmt.Errorf("failed to run command %v: %v", command, stderr.Bytes()) } return stdout.Bytes(), err } func getWebDavClient(user dataprovider.User) *gowebdav.Client { rootPath := fmt.Sprintf("http://localhost:%d/", webDavServerPort) pwd := defaultPassword if user.Password != "" { pwd = user.Password } client := gowebdav.NewClient(rootPath, user.Username, pwd) client.SetTimeout(10 * time.Second) return client } func getTestUser() dataprovider.User { user := dataprovider.User{ BaseUser: sdk.BaseUser{ Username: defaultUsername, Password: defaultPassword, HomeDir: filepath.Join(homeBasePath, defaultUsername), Status: 1, ExpirationDate: 0, }, } user.Permissions = make(map[string][]string) user.Permissions["/"] = allPerms return user } func getTestSFTPUser() dataprovider.User { u := getTestUser() u.Username = defaultSFTPUsername u.FsConfig.Provider = sdk.SFTPFilesystemProvider u.FsConfig.SFTPConfig.Endpoint = sftpServerAddr u.FsConfig.SFTPConfig.Username = defaultUsername u.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword) return u } func getCryptFsUser() dataprovider.User { u := getTestUser() u.Username += "_crypt" u.FsConfig.Provider = sdk.CryptedFilesystemProvider u.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(defaultPassword) return u } func getTestUserWithHTTPFs() dataprovider.User { u := getTestUser() u.FsConfig.Provider = sdk.HTTPFilesystemProvider u.FsConfig.HTTPConfig = vfs.HTTPFsConfig{ BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{ Endpoint: fmt.Sprintf("http://127.0.0.1:%d/api/v1", httpFsPort), Username: defaultHTTPFsUsername, }, } return u } func writeSFTPFile(name string, size int64, client *sftp.Client) error { err := writeSFTPFileNoCheck(name, size, client) if err != nil { return err } info, err := client.Stat(name) if err != nil { return err } if info.Size() != size { return fmt.Errorf("file size mismatch, wanted %v, actual %v", size, info.Size()) } return nil } func writeSFTPFileNoCheck(name string, size int64, client *sftp.Client) error { content := make([]byte, size) _, err := rand.Read(content) if err != nil { return err } f, err := client.Create(name) if err != nil { return err } _, err = io.Copy(f, bytes.NewBuffer(content)) if err != nil { f.Close() return err } return f.Close() } func getUploadScriptEnvContent(envVar string) []byte { content := []byte("#!/bin/sh\n\n") content = append(content, []byte(fmt.Sprintf("if [ -z \"$%s\" ]\n", envVar))...) content = append(content, []byte("then\n")...) content = append(content, []byte(" exit 1\n")...) content = append(content, []byte("else\n")...) content = append(content, []byte(" exit 0\n")...) content = append(content, []byte("fi\n")...) return content } func getUploadScriptContent(movedPath, logFilePath string, exitStatus int) []byte { content := []byte("#!/bin/sh\n\n") content = append(content, []byte("sleep 1\n")...) if logFilePath != "" { content = append(content, []byte(fmt.Sprintf("echo $@ > %v\n", logFilePath))...) } content = append(content, []byte(fmt.Sprintf("mv ${SFTPGO_ACTION_PATH} %v\n", movedPath))...) content = append(content, []byte(fmt.Sprintf("exit %d", exitStatus))...) return content } func getSaveProviderObjectScriptContent(outFilePath string, exitStatus int) []byte { content := []byte("#!/bin/sh\n\n") content = append(content, []byte(fmt.Sprintf("echo ${SFTPGO_OBJECT_DATA} > %v\n", outFilePath))...) content = append(content, []byte(fmt.Sprintf("exit %d", exitStatus))...) return content } func generateTOTPPasscode(secret string, algo otp.Algorithm) (string, error) { return totp.GenerateCodeCustom(secret, time.Now(), totp.ValidateOpts{ Period: 30, Skew: 1, Digits: otp.DigitsSix, Algorithm: algo, }) } func isDbDefenderSupported() bool { // SQLite shares the implementation with other SQL-based provider but it makes no sense // to use it outside test cases switch dataprovider.GetProviderStatus().Driver { case dataprovider.MySQLDataProviderName, dataprovider.PGSQLDataProviderName, dataprovider.CockroachDataProviderName, dataprovider.SQLiteDataProviderName: return true default: return false } } func getEncryptedFileSize(size int64) (int64, error) { encSize, err := sio.EncryptedSize(uint64(size)) return int64(encSize) + 33, err } func printLatestLogs(maxNumberOfLines int) { var lines []string f, err := os.Open(logFilePath) if err != nil { return } defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { lines = append(lines, scanner.Text()+"\r\n") for len(lines) > maxNumberOfLines { lines = lines[1:] } } if scanner.Err() != nil { logger.WarnToConsole("Unable to print latest logs: %v", scanner.Err()) return } for _, line := range lines { logger.DebugToConsole("%s", line) } } type receivedEmail struct { sync.RWMutex From string To []string Data string } func (e *receivedEmail) set(from string, to []string, data []byte) { e.Lock() defer e.Unlock() e.From = from e.To = to e.Data = strings.ReplaceAll(string(data), "=\r\n", "") } func (e *receivedEmail) reset() { e.Lock() defer e.Unlock() e.From = "" e.To = nil e.Data = "" } func (e *receivedEmail) get() receivedEmail { e.RLock() defer e.RUnlock() return receivedEmail{ From: e.From, To: e.To, Data: e.Data, } } func startHTTPFs() { go func() { readdirCallback := func(name string) []os.FileInfo { if name == httpFsWellKnowDir { return []os.FileInfo{vfs.NewFileInfo("ghost.txt", false, 0, time.Unix(0, 0), false)} } return nil } callbacks := &httpdtest.HTTPFsCallbacks{ Readdir: readdirCallback, } if err := httpdtest.StartTestHTTPFs(httpFsPort, callbacks); err != nil { logger.ErrorToConsole("could not start HTTPfs test server: %v", err) os.Exit(1) } }() waitTCPListening(fmt.Sprintf(":%d", httpFsPort)) } ================================================ FILE: internal/common/ratelimiter.go ================================================ // Copyright (C) 2019 Nicola Murino // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, version 3. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, seeHaving problems?
your SFTPGo password {{if le .Days 0}}has expired{{else}}expires in {{.Days}} {{if eq .Days 1}}day{{else}}days{{end}}{{end}}.
Please login to the WebClient and set a new password.
================================================ FILE: templates/email/reset-password.html ================================================ Hello there!Your SFTPGo email verification code is "{{.Code}}", this code is valid for 10 minutes.
Please enter this code in SFTPGo to confirm your email address.
================================================ FILE: templates/webadmin/admin.html ================================================ {{template "base" .}} {{- define "page_body"}}| Username | Status | Last login | Role | 2FA | Description |
|---|
| ID | Node | Username | Started | Remote address | Protocol | Last activity | Info |
|---|
| IP | Blocked until | Score |
|---|
| Name | Type | Rules |
|---|
| Name | Status | Trigger | Actions |
|---|
| Date and time | Action | Path | Username | Protocol | IP | Info |
|---|
| Date and time | Action | Object | Username | IP |
|---|
| Date and time | Event | Username | Protocol | IP | Info |
|---|
The following placeholders are supported
The generated folders can be saved or exported. Exported folders can be imported from the "Maintenance" section of this SFTPGo instance or another.
| Name | Storage | Disk quota | Associations | Description |
|---|
| Name | Members | Description |
|---|
Recovery codes are a set of one time use codes that can be used in place of the authentication code to login to the web UI. You can use them if you lose access to your phone to login to your account and disable or regenerate two-factor configuration.
To keep your account secure, don't share or distribute your recovery codes. We recommend saving them with a secure password manager.
If you generate new recovery codes, you automatically invalidate old ones.
| Name | Members | Description |
|---|
"{{.GetAddress}}"
{{- if .HasProxy}}{{- end}} {{- end}}
"{{.Path}}"
"{{.Fingerprint}}"
"{{.GetAlgosAsString}}"
"{{.Status.SSH.GetSSHCommandsAsString}}"
"{{.Status.SSH.GetSupportedAuthsAsString}}"
"{{.Status.SSH.GetPublicKeysAlgosAsString}}"
"{{.Status.SSH.GetMACsAsString}}"
"{{.Status.SSH.GetKEXsAsString}}"
"{{.Status.SSH.GetCiphersAsString}}"
"{{.GetAddress}}"
{{- if .HasProxy}}{{- end}}
{{- if .ForcePassiveIP}}
"{{.ForcePassiveIP}}"
{{- end}} {{- range .PassiveIPOverrides}}"{{.IP}} ({{.GetNetworksAsString}})"
{{- end}} {{- end}}"{{.Status.FTP.PassivePortRange.Start}}-{{.Status.FTP.PassivePortRange.End}}"
"{{.GetAddress}}"
{{if .EnableHTTPS}} HTTPS {{else}} HTTP {{end}}
{{- end}}{{if .Status.DataProvider.Error}} "{{.Status.DataProvider.Error}}"{{end}}
"{{.Status.DataProvider.Driver}}"
"{{.Status.RateLimiters.GetProtocolsAsString}}"
"{{.Name}}"
"{{.Issuer}}"
"{{.Algo}}"
The following placeholders are supported
Placeholders will be replaced in paths and credentials of the configured storage backend.
The generated users can be saved or exported. Exported users can be imported from the "Maintenance" section of this SFTPGo instance or another.
| Username | Status | Last login | Storage | Role | 2FA | Disk quota | Transfer quota | Groups |
|---|
|
|
Name | Size | Last Modified |
|---|
Recovery codes are a set of one time use codes that can be used in place of the authentication code to login to the web UI. You can use them if you lose access to your phone to login to your account and disable or regenerate two-factor configuration.
To keep your account secure, don't share or distribute your recovery codes. We recommend saving them with a secure password manager.
If you generate new recovery codes, you automatically invalidate old ones.
| Name | Scope | Info |
|---|