](https://www.aledade.com/)
-
-[
](https://www.jumptrading.com/)
-
-[
](https://wpengine.com/)
+```bash
+go build -i -ldflags "-s -w -X github.com/drakkan/sftpgo/utils.commit=`git describe --tags --always --dirty` -X github.com/drakkan/sftpgo/utils.date=`date -u +%FT%TZ`" -o sftpgo
+```
-#### Silver sponsors
+and you will get a version that includes git commit and build date like this one:
-[
](https://idcs.ip-paris.fr/)
+```bash
+sftpgo -v
+SFTPGo version: 0.9.0-dev-90607d4-dirty-2019-08-08T19:28:36Z
+```
-#### Bronze sponsors
+For Linux, a systemd sample [service](https://github.com/drakkan/sftpgo/tree/master/init/sftpgo.service "systemd service") can be found inside the source tree.
-[
](https://www.7digital.com/)
-
-[
](https://servinga.com/)
-
-[
](https://www.reui.io/)
+Alternately you can use distro packages:
-## Documentation
+- Arch Linux PKGBUILD is available on [AUR](https://aur.archlinux.org/packages/sftpgo-git/ "SFTPGo")
-You can explore all supported features and configuration options at [docs.sftpgo.com](https://docs.sftpgo.com/latest/).
+## Configuration
-**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/).
+The `sftpgo` executable can be used this way:
-## Support
+```bash
+Usage:
+ sftpgo [command]
-- **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).
+Available Commands:
+ help Help about any command
+ serve Start the SFTP Server
-SFTPGo Enterprise is available as:
+Flags:
+ -h, --help help for sftpgo
+ -v, --version
+```
-- 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)
+The `serve` subcommand supports the following flags:
-## Internationalization
+- `--config-dir` string. Location of the config dir. This directory should contain the `sftpgo` configuration file and is used as the base for files with a relative path (eg. the private keys for the SFTP server, the SQLite or bblot database if you use SQLite or bbolt as data provider). The default value is "." or the value of `SFTPGO_CONFIG_DIR` environment variable.
+- `--config-file` string. Name of the configuration file. It must be the name of a file stored in config-dir not the absolute path to the configuration file. The specified file name must have no extension we automatically load JSON, YAML, TOML, HCL and Java properties. The default value is "sftpgo" (and therefore `sftpgo.json`, `sftpgo.yaml` and so on are searched) or the value of `SFTPGO_CONFIG_FILE` environment variable.
+- `--log-compress` boolean. Determine if the rotated log files should be compressed using gzip. Default `false` or the value of `SFTPGO_LOG_COMPRESS` environment variable (1 or `true`, 0 or `false`).
+- `--log-file-path` string. Location for the log file, default "sftpgo.log" or the value of `SFTPGO_LOG_FILE_PATH` environment variable.
+- `--log-max-age` int. Maximum number of days to retain old log files. Default 28 or the value of `SFTPGO_LOG_MAX_AGE` environment variable.
+- `--log-max-backups` int. Maximum number of old log files to retain. Default 5 or the value of `SFTPGO_LOG_MAX_BACKUPS` environment variable.
+- `--log-max-size` int. Maximum size in megabytes of the log file before it gets rotated. Default 10 or the value of `SFTPGO_LOG_MAX_SIZE` environment variable.
+- `--log-verbose` boolean. Enable verbose logs. Default `true` or the value of `SFTPGO_LOG_VERBOSE` environment variable (1 or `true`, 0 or `false`).
-The translations are available via [Crowdin](https://crowdin.com/project/sftpgo), who have granted us an open source license.
+If you don't configure any private host keys, the daemon will use `id_rsa` in the configuration directory. If that file doesn't exist, the daemon will attempt to autogenerate it (if the user that executes SFTPGo has write access to the config-dir). The server supports any private key format supported by [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/keys.go#L32).
-Before translating please take a look at our contribution [guidelines](https://docs.sftpgo.com/latest/web-interfaces/#internationalization).
+Before starting `sftpgo` a dataprovider must be configured.
-## Release Cadence
+Sample SQL scripts to create the required database structure can be found inside the source tree [sql](https://github.com/drakkan/sftpgo/tree/master/sql "sql") directory. The SQL scripts filename's is, by convention, the date as `YYYYMMDD` and the suffix `.sql`. You need to apply all the SQL scripts for your database ordered by name, for example `20190706.sql` must be applied before `20190728.sql` and so on.
-SFTPGo follows a feature-driven release cycle.
+The `sftpgo` configuration file contains the following sections:
-- 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.
+- **"sftpd"**, the configuration for the SFTP server
+ - `bind_port`, integer. The port used for serving SFTP requests. Default: 2022
+ - `bind_address`, string. Leave blank to listen on all available network interfaces. Default: ""
+ - `idle_timeout`, integer. Time in minutes after which an idle client will be disconnected. Default: 15
+ - `max_auth_tries` integer. Maximum number of authentication attempts permitted per connection. If set to a negative number, the number of attempts are unlimited. If set to zero, the number of attempts are limited to 6.
+ - `umask`, string. Umask for the new files and directories. This setting has no effect on Windows. Default: "0022"
+ - `banner`, string. Identification string used by the server. Default "SFTPGo"
+ - `upload_mode` integer. 0 means standard, the files are uploaded directly to the requested path. 1 means atomic: files are uploaded to a temporary path and renamed to the requested path when the client ends the upload. Atomic mode avoids problems such as a web server that serves partial files when the files are being uploaded
+ - `actions`, struct. It contains the command to execute and/or the HTTP URL to notify and the trigger conditions
+ - `execute_on`, list of strings. Valid values are `download`, `upload`, `delete`, `rename`. On folder deletion a `delete` notification will be sent for each deleted file. Leave empty to disable actions.
+ - `command`, string. Absolute path to the command to execute. Leave empty to disable. The command is invoked with the following arguments:
+ - `action`, any valid `execute_on` string
+ - `username`, user who did the action
+ - `path` to the affected file. For `rename` action this is the old file name
+ - `target_path`, non empty for `rename` action, this is the new file name
+ - `http_notification_url`, a valid URL. An HTTP GET request will be executed to this URL. Leave empty to disable. The query string will contain the following parameters that have the same meaning of the command's arguments:
+ - `action`
+ - `username`
+ - `path`
+ - `target_path`, added for `rename` action only
+ - `keys`, struct array. It contains the daemon's private keys. If empty or missing the daemon will search or try to generate `id_rsa` in the configuration directory.
+ - `private_key`, path to the private key file. It can be a path relative to the config dir or an absolute one.
+ - `enable_scp`, boolean. Default disabled. Set to `true` to enable SCP support. SCP is an experimental feature, we have our own SCP implementation since we can't rely on `scp` system command to proper handle permissions, quota and user's home dir restrictions. The SCP protocol is quite simple but there is no official docs about it, so we need more testing and feedbacks before enabling it by default. We may not handle some borderline cases or have sneaky bugs. Please do accurate tests yourself before enabling SCP and let us known if something does not work as expected for your use cases. SCP between two remote hosts is supported using the `-3` scp option.
+- **"data_provider"**, the configuration for the data provider
+ - `driver`, string. Supported drivers are `sqlite`, `mysql`, `postgresql`, `bolt`
+ - `name`, string. Database name. For driver `sqlite` this can be the database name relative to the config dir or the absolute path to the SQLite database.
+ - `host`, string. Database host. Leave empty for driver `sqlite` and `bolt`
+ - `port`, integer. Database port. Leave empty for driver `sqlite` and `bolt`
+ - `username`, string. Database user. Leave empty for driver `sqlite` and `bolt`
+ - `password`, string. Database password. Leave empty for driver `sqlite` and `bolt`
+ - `sslmode`, integer. Used for drivers `mysql` and `postgresql`. 0 disable SSL/TLS connections, 1 require ssl, 2 set ssl mode to `verify-ca` for driver `postgresql` and `skip-verify` for driver `mysql`, 3 set ssl mode to `verify-full` for driver `postgresql` and `preferred` for driver `mysql`
+ - `connectionstring`, string. Provide a custom database connection string. If not empty this connection string will be used instead of build one using the previous parameters. Leave empty for driver `bolt`
+ - `users_table`, string. Database table for SFTP users
+ - `manage_users`, integer. Set to 0 to disable users management, 1 to enable
+ - `track_quota`, integer. Set the preferred way to track users quota between the following choices:
+ - 0, disable quota tracking. REST API to scan user dir and update quota will do nothing
+ - 1, quota is updated each time a user upload or delete a file even if the user has no quota restrictions
+ - 2, quota is updated each time a user upload or delete a file but only for users with quota restrictions. With this configuration the "quota scan" REST API can still be used to periodically update space usage for users without quota restrictions
+- **"httpd"**, the configuration for the HTTP server used to serve REST API
+ - `bind_port`, integer. The port used for serving HTTP requests. Set to 0 to disable HTTP server. Default: 8080
+ - `bind_address`, string. Leave blank to listen on all available network interfaces. Default: "127.0.0.1"
+
+Here is a full example showing the default config in JSON format:
+
+```json
+{
+ "sftpd": {
+ "bind_port": 2022,
+ "bind_address": "",
+ "idle_timeout": 15,
+ "max_auth_tries": 0,
+ "umask": "0022",
+ "banner": "SFTPGo",
+ "actions": {
+ "execute_on": [],
+ "command": "",
+ "http_notification_url": ""
+ },
+ "keys": [],
+ "enable_scp": false
+ },
+ "data_provider": {
+ "driver": "sqlite",
+ "name": "sftpgo.db",
+ "host": "",
+ "port": 5432,
+ "username": "",
+ "password": "",
+ "sslmode": 0,
+ "connection_string": "",
+ "users_table": "users",
+ "manage_users": 1,
+ "track_quota": 2
+ },
+ "httpd": {
+ "bind_port": 8080,
+ "bind_address": "127.0.0.1"
+ }
+}
+```
+
+If you want to use a private key that use an algorithm different from RSA or more than one private key then replace the empty `keys` array with something like this:
+
+```json
+"keys": [
+ {
+ "private_key": "id_rsa"
+ },
+ {
+ "private_key": "id_ecdsa"
+ }
+]
+```
+
+The configuration can be read from JSON, TOML, YAML, HCL, envfile and Java properties config files, if your `config-file` flag is set to `sftpgo` (default value) you need to create a configuration file called `sftpgo.json` or `sftpgo.yaml` and so on inside `config-dir`.
+
+You can also configure all the available options using environment variables, sftpgo will check for environment variables with a name matching the key uppercased and prefixed with the `SFTPGO_`. You need to use `__` to traverse a struct.
+
+Let's see some examples:
+
+- To set sftpd `bind_port` you need to define the env var `SFTPGO_SFTPD__BIND_PORT`
+- To set the `execute_on` actions you need to define the env var `SFTPGO_SFTPD__ACTIONS__EXECUTE_ON` for example `SFTPGO_SFTPD__ACTIONS__EXECUTE_ON=upload,download`
+
+To start the SFTP Server with the default values for the command line flags simply use:
+
+```bash
+sftpgo serve
+```
+
+## Account's configuration properties
+
+For each account the following properties can be configured:
+
+- `username`
+- `password` used for password authentication. For users created using SFTPGo REST API if the password has no known hashing algo prefix it will be stored using argon2id. SFTPGo supports checking passwords stored with bcrypt and pbkdf2 too. For pbkdf2 the supported format is `${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"
-}
-```
diff --git a/examples/quotascan/README.md b/examples/quotascan/README.md
deleted file mode 100644
index 0830a308..00000000
--- a/examples/quotascan/README.md
+++ /dev/null
@@ -1,23 +0,0 @@
-# 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.
diff --git a/examples/quotascan/scanuserquota b/examples/quotascan/scanuserquota
deleted file mode 100755
index 7648bdd5..00000000
--- a/examples/quotascan/scanuserquota
+++ /dev/null
@@ -1,119 +0,0 @@
-#!/usr/bin/env python
-
-from datetime import datetime
-import sys
-import time
-
-import pytz
-import requests
-
-try:
- import urllib.parse as urlparse
-except ImportError:
- import urlparse
-
-# change base_url to point to your SFTPGo installation
-base_url = "http://127.0.0.1:8080"
-# set to False if you want to skip TLS certificate validation
-verify_tls_cert = True
-# set the credentials for a valid admin here
-admin_user = "admin"
-admin_password = "password"
-
-
-# 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()
diff --git a/go.mod b/go.mod
index d2fafb83..f3438371 100644
--- a/go.mod
+++ b/go.mod
@@ -1,185 +1,27 @@
-module github.com/drakkan/sftpgo/v2
+module github.com/drakkan/sftpgo
-go 1.25.0
+go 1.12
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.3
- github.com/aws/aws-sdk-go-v2/config v1.32.11
- github.com/aws/aws-sdk-go-v2/credentials v1.19.11
- github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4
- github.com/aws/aws-sdk-go-v2/service/sts v1.41.8
- github.com/bmatcuk/doublestar/v4 v4.10.0
- github.com/cockroachdb/cockroach-go/v2 v2.4.3
- github.com/coreos/go-oidc/v3 v3.17.0
- github.com/drakkan/webdav v0.0.0-20241026165615-b8b8f74ae71b
- github.com/eikenb/pipeat v0.0.0-20251030185646-385cd3c3e07b
- github.com/fclairamb/ftpserverlib v0.30.0
- github.com/go-acme/lego/v4 v4.32.0
- github.com/go-chi/chi/v5 v5.2.5
- github.com/go-chi/render v1.0.3
- github.com/go-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.34
- 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.271.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.6 // indirect
- github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect
- github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect
- github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect
- github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect
- github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 // indirect
- github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect
- github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect
- github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect
- github.com/aws/smithy-go v1.24.2 // indirect
- github.com/beorn7/perks v1.0.1 // indirect
- github.com/boombuler/barcode v1.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.18.0 // indirect
- github.com/hashicorp/errwrap v1.1.0 // indirect
- github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
- github.com/hashicorp/go-multierror v1.1.1 // indirect
- github.com/hashicorp/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.0 // indirect
- github.com/spf13/cast v1.10.0 // indirect
- github.com/spf13/pflag v1.0.10 // indirect
- github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
- github.com/tklauser/go-sysconf v0.3.16 // indirect
- github.com/tklauser/numcpus v0.11.0 // indirect
- github.com/yusufpapurcu/wmi v1.2.4 // indirect
- go.opentelemetry.io/auto/sdk v1.2.1 // indirect
- go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect
- go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
- go.opentelemetry.io/otel v1.42.0 // indirect
- go.opentelemetry.io/otel/metric v1.42.0 // indirect
- go.opentelemetry.io/otel/sdk v1.42.0 // indirect
- go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect
- go.opentelemetry.io/otel/trace v1.42.0 // indirect
- go.yaml.in/yaml/v2 v2.4.4 // indirect
- go.yaml.in/yaml/v3 v3.0.4 // indirect
- golang.org/x/mod v0.34.0 // indirect
- golang.org/x/sync v0.20.0 // indirect
- golang.org/x/text v0.35.0 // indirect
- golang.org/x/tools v0.43.0 // indirect
- golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
- google.golang.org/genproto v0.0.0-20260311181403-84a4fc48630c // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
- google.golang.org/grpc v1.79.2 // indirect
- google.golang.org/protobuf v1.36.11 // indirect
- gopkg.in/yaml.v3 v3.0.1 // indirect
-)
-
-replace (
- github.com/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
+ github.com/alexedwards/argon2id v0.0.0-20190612080829-01a59b2b8802
+ github.com/go-chi/chi v4.0.2+incompatible
+ github.com/go-chi/render v1.0.1
+ github.com/go-sql-driver/mysql v1.4.1
+ github.com/lib/pq v1.2.0
+ github.com/magiconair/properties v1.8.1 // indirect
+ github.com/mattn/go-sqlite3 v1.11.0
+ github.com/pelletier/go-toml v1.4.0 // indirect
+ github.com/pkg/sftp v1.10.1
+ github.com/rs/xid v1.2.1
+ github.com/rs/zerolog v1.15.0
+ github.com/spf13/afero v1.2.2 // indirect
+ github.com/spf13/cobra v0.0.5
+ github.com/spf13/jwalterweatherman v1.1.0 // indirect
+ github.com/spf13/viper v1.4.0
+ go.etcd.io/bbolt v1.3.3
+ golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472
+ golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 // indirect
+ golang.org/x/sys v0.0.0-20190830142957-1e83adbbebd0 // indirect
+ google.golang.org/appengine v1.6.2 // indirect
+ gopkg.in/natefinch/lumberjack.v2 v2.0.0
)
diff --git a/go.sum b/go.sum
index d81c977d..512b8d6c 100644
--- a/go.sum
+++ b/go.sum
@@ -1,477 +1,211 @@
-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.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA=
-github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 h1:N4lRUXZpZ1KVEUn6hxtco/1d2lgYhNn1fHkkl8WhlyQ=
-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
-github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs=
-github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo=
-github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc=
-github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 h1:qi3e/dmpdONhj1RyIZdi6DKKpDXS5Lb8ftr3p7cyHJc=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20/go.mod h1:V1K+TeJVD5JOk3D9e5tsX2KUdL7BlB+FV6cBhdobN8c=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 h1:BYf7XNsJMzl4mObARUBUib+j2tf0U//JAAtTnYqvqCw=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11/go.mod h1:aEUS4WrNk/+FxkBZZa7tVgp4pGH+kFGW40Y8rCPqt5g=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 h1:JnQeStZvPHFHeyky/7LbMlyQjUa+jIBj36OlWm0pzIk=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19/go.mod h1:HGyasyHvYdFQeJhvDHfH7HXkHh57htcJGKDZ+7z+I24=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4 h1:4ExZyubQ6LQQVuF2Qp9OsfEvsTdAWh5Gfwf6PgIdLdk=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4/go.mod h1:NF3JcMGOiARAss1ld3WGORCw71+4ExDD2cbbdKS5PpA=
-github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row=
-github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU=
-github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias=
-github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8=
-github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo=
-github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI=
-github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
-github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
-github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
-github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/bmatcuk/doublestar/v4 v4.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=
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alexedwards/argon2id v0.0.0-20190612080829-01a59b2b8802 h1:RwMM1q/QSKYIGbHfOkf843hE8sSUJtf1dMwFPtEDmm0=
+github.com/alexedwards/argon2id v0.0.0-20190612080829-01a59b2b8802/go.mod h1:4dsm7ufQm1Gwl8S2ss57u+2J7KlxIL2QUmFGlGtWogY=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/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.32.0 h1:z7Ss7aa1noabhKj+DBzhNCO2SM96xhE3b0ucVW3x8Tc=
-github.com/go-acme/lego/v4 v4.32.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI=
-github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
-github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
-github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
-github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
-github.com/go-jose/go-jose/v4 v4.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.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI=
-github.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE=
-github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
-github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
-github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
-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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs=
+github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
+github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
+github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
+github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/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.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
-github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
-github.com/mhale/smtpd v0.8.3 h1:8j8YNXajksoSLZja3HdwvYVZPuJSqAxFsib3adzRRt8=
-github.com/mhale/smtpd v0.8.3/go.mod h1:MQl+y2hwIEQCXtNhe5+55n0GZOjSmeqORDIXbqUL3x4=
-github.com/miekg/dns v1.1.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/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
+github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
+github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
+github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg=
+github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/sftp v1.10.1 h1:VasscCm72135zRysgrJDKsntdmPN+OuU3+nnHYA9wyc=
+github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/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.0 h1:t4GNqvPZ84Vjtpboo/kT3pIkbaK3vc+JIlD/Wz1zSFY=
-github.com/shoenig/go-m1cpu v0.2.0/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w=
-github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=
-github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
-github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
-github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
-github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
-github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
-github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
-github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
-github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
-github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
-github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
-github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
-github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
+github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
+github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY=
+github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
+github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
+github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
+github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
+github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
+github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
+github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
+github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
+github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
+github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/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=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
+go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk=
+go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
+go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-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/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0=
+golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472 h1:Gv7RPwsi3eZ2Fgewe3CBsuOebPwO27PoXzRpJPsvSSM=
+golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM=
+golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
-golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-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/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190830142957-1e83adbbebd0 h1:7z820YPX9pxWR59qM7BE5+fglp4D/mKqAwCvGt11b+8=
+golang.org/x/sys v0.0.0-20190830142957-1e83adbbebd0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/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/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/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.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY=
-google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q=
-google.golang.org/genproto v0.0.0-20260311181403-84a4fc48630c h1:ZhFDeBMmFc/4g8/GwxnJ4rzB3O4GwQVNr+8Mh7Y5z4g=
-google.golang.org/genproto v0.0.0-20260311181403-84a4fc48630c/go.mod h1:hf4r/rBuzaTkLUWRO03771Xvcs6P5hwdQK3UUEJjqo0=
-google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c h1:OyQPd6I3pN/9gDxz6L13kYGJgqkpdrAohJRBeXyxlgI=
-google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c/go.mod h1:X2gu9Qwng7Nn009s/r3RUxqkzQNqOrAy79bluY7ojIg=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
-google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
-google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
-google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
-google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.6.2 h1:j8RI1yW0SkI+paT6uGwMlrMI/6zwYA6/CFil8rxOzGI=
+google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
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/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/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=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/img/7digital.png b/img/7digital.png
deleted file mode 100644
index c6c1c540..00000000
Binary files a/img/7digital.png and /dev/null differ
diff --git a/img/Aledade_logo.png b/img/Aledade_logo.png
deleted file mode 100644
index 1639a321..00000000
Binary files a/img/Aledade_logo.png and /dev/null differ
diff --git a/img/IDCS.png b/img/IDCS.png
deleted file mode 100644
index 0a890e89..00000000
Binary files a/img/IDCS.png and /dev/null differ
diff --git a/img/jumptrading.png b/img/jumptrading.png
deleted file mode 100644
index 88161521..00000000
Binary files a/img/jumptrading.png and /dev/null differ
diff --git a/img/logo.png b/img/logo.png
deleted file mode 100644
index 81b1f94b..00000000
Binary files a/img/logo.png and /dev/null differ
diff --git a/img/reui.png b/img/reui.png
deleted file mode 100644
index 20973803..00000000
Binary files a/img/reui.png and /dev/null differ
diff --git a/img/servinga.png b/img/servinga.png
deleted file mode 100644
index c5ccb638..00000000
Binary files a/img/servinga.png and /dev/null differ
diff --git a/img/wpengine.png b/img/wpengine.png
deleted file mode 100644
index 0ca2381f..00000000
Binary files a/img/wpengine.png and /dev/null differ
diff --git a/init/com.github.drakkan.sftpgo.plist b/init/com.github.drakkan.sftpgo.plist
deleted file mode 100644
index 58b83f8d..00000000
--- a/init/com.github.drakkan.sftpgo.plist
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-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)) -} diff --git a/internal/common/ratelimiter.go b/internal/common/ratelimiter.go deleted file mode 100644 index 85d88092..00000000 --- a/internal/common/ratelimiter.go +++ /dev/null @@ -1,245 +0,0 @@ -// Copyright (C) 2019 Nicola Murino -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, version 3. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, 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.
\ No newline at end of file diff --git a/templates/email/reset-password.html b/templates/email/reset-password.html deleted file mode 100644 index 79a458fc..00000000 --- a/templates/email/reset-password.html +++ /dev/null @@ -1,4 +0,0 @@ -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.
diff --git a/templates/webadmin/admin.html b/templates/webadmin/admin.html deleted file mode 100644 index a96f2e68..00000000 --- a/templates/webadmin/admin.html +++ /dev/null @@ -1,313 +0,0 @@ - -{{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 | -- |
|---|
| IP/Network | -Protocols | -Mode | -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 | -- |
|---|