WebAdmin: add configs section

Setting configurations is an experimental feature and is not currently
supported in the REST API

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2023-02-19 19:03:45 +01:00
parent 14961a573f
commit a3fff56da5
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
39 changed files with 2193 additions and 382 deletions

View file

@ -136,7 +136,7 @@ The configuration file contains the following sections:
- `host_keys`, list of strings. It contains the daemon's private host keys. Each host key can be defined as a path relative to the configuration directory or an absolute one. If empty, the daemon will search or try to generate `id_rsa`, `id_ecdsa` and `id_ed25519` keys inside the configuration directory. If you configure absolute paths to files named `id_rsa`, `id_ecdsa` and/or `id_ed25519` then SFTPGo will try to generate these keys using the default settings. - `host_keys`, list of strings. It contains the daemon's private host keys. Each host key can be defined as a path relative to the configuration directory or an absolute one. If empty, the daemon will search or try to generate `id_rsa`, `id_ecdsa` and `id_ed25519` keys inside the configuration directory. If you configure absolute paths to files named `id_rsa`, `id_ecdsa` and/or `id_ed25519` then SFTPGo will try to generate these keys using the default settings.
- `host_certificates`, list of strings. Public host certificates. Each certificate can be defined as a path relative to the configuration directory or an absolute one. Certificate's public key must match a private host key otherwise it will be silently ignored. Default: empty. - `host_certificates`, list of strings. Public host certificates. Each certificate can be defined as a path relative to the configuration directory or an absolute one. Certificate's public key must match a private host key otherwise it will be silently ignored. Default: empty.
- `host_key_algorithms`, list of strings. Public key algorithms that the server will accept for host key authentication. The supported values are: `rsa-sha2-512-cert-v01@openssh.com`, `rsa-sha2-256-cert-v01@openssh.com`, `ssh-rsa-cert-v01@openssh.com`, `ssh-dss-cert-v01@openssh.com`, `ecdsa-sha2-nistp256-cert-v01@openssh.com`, `ecdsa-sha2-nistp384-cert-v01@openssh.com`, `ecdsa-sha2-nistp521-cert-v01@openssh.com`, `ssh-ed25519-cert-v01@openssh.com`, `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, `rsa-sha2-512`, `rsa-sha2-256`, `ssh-rsa`, `ssh-dss`, `ssh-ed25519`. Default values: `rsa-sha2-512-cert-v01@openssh.com`, `rsa-sha2-256-cert-v01@openssh.com`, `ecdsa-sha2-nistp256-cert-v01@openssh.com`, `ecdsa-sha2-nistp384-cert-v01@openssh.com`, `ecdsa-sha2-nistp521-cert-v01@openssh.com`, `ssh-ed25519-cert-v01@openssh.com`, `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, `rsa-sha2-512`, `rsa-sha2-256`, `ssh-ed25519`. - `host_key_algorithms`, list of strings. Public key algorithms that the server will accept for host key authentication. The supported values are: `rsa-sha2-512-cert-v01@openssh.com`, `rsa-sha2-256-cert-v01@openssh.com`, `ssh-rsa-cert-v01@openssh.com`, `ssh-dss-cert-v01@openssh.com`, `ecdsa-sha2-nistp256-cert-v01@openssh.com`, `ecdsa-sha2-nistp384-cert-v01@openssh.com`, `ecdsa-sha2-nistp521-cert-v01@openssh.com`, `ssh-ed25519-cert-v01@openssh.com`, `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, `rsa-sha2-512`, `rsa-sha2-256`, `ssh-rsa`, `ssh-dss`, `ssh-ed25519`. Default values: `rsa-sha2-512-cert-v01@openssh.com`, `rsa-sha2-256-cert-v01@openssh.com`, `ecdsa-sha2-nistp256-cert-v01@openssh.com`, `ecdsa-sha2-nistp384-cert-v01@openssh.com`, `ecdsa-sha2-nistp521-cert-v01@openssh.com`, `ssh-ed25519-cert-v01@openssh.com`, `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, `rsa-sha2-512`, `rsa-sha2-256`, `ssh-ed25519`.
- `moduli`, list of strings. Diffie-Hellman moduli files. Each moduli file can be defined as a path relative to the configuration directory or an absolute one. If set, `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1` KEX algorithms will be available, `diffie-hellman-group-exchange-sha256` will be enabled by default if you don't explicitly set KEXs. Default: empty. - `moduli`, list of strings. Diffie-Hellman moduli files. Each moduli file can be defined as a path relative to the configuration directory or an absolute one. If set and valid, `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1` KEX algorithms will be available, `diffie-hellman-group-exchange-sha256` will be enabled by default if you don't explicitly set KEXs. Invalid moduli file will be silently ignored. Default: empty.
- `kex_algorithms`, list of strings. Available KEX (Key Exchange) algorithms in preference order. Leave empty to use default values. The supported values are: `curve25519-sha256`, `curve25519-sha256@libssh.org`, `ecdh-sha2-nistp256`, `ecdh-sha2-nistp384`, `ecdh-sha2-nistp521`, `diffie-hellman-group14-sha256`, `diffie-hellman-group16-sha512`, `diffie-hellman-group18-sha512`, `diffie-hellman-group14-sha1`, `diffie-hellman-group1-sha1`. Default values: `curve25519-sha256`, `curve25519-sha256@libssh.org`, `ecdh-sha2-nistp256`, `ecdh-sha2-nistp384`, `ecdh-sha2-nistp521`, `diffie-hellman-group14-sha256`. SHA512 based KEXs are disabled by default because they are slow. If you set one or more moduli files, `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1` will be available. - `kex_algorithms`, list of strings. Available KEX (Key Exchange) algorithms in preference order. Leave empty to use default values. The supported values are: `curve25519-sha256`, `curve25519-sha256@libssh.org`, `ecdh-sha2-nistp256`, `ecdh-sha2-nistp384`, `ecdh-sha2-nistp521`, `diffie-hellman-group14-sha256`, `diffie-hellman-group16-sha512`, `diffie-hellman-group18-sha512`, `diffie-hellman-group14-sha1`, `diffie-hellman-group1-sha1`. Default values: `curve25519-sha256`, `curve25519-sha256@libssh.org`, `ecdh-sha2-nistp256`, `ecdh-sha2-nistp384`, `ecdh-sha2-nistp521`, `diffie-hellman-group14-sha256`. SHA512 based KEXs are disabled by default because they are slow. If you set one or more moduli files, `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1` will be available.
- `ciphers`, list of strings. Allowed ciphers in preference order. Leave empty to use default values. The supported values are: `aes128-gcm@openssh.com`, `aes256-gcm@openssh.com`, `chacha20-poly1305@openssh.com`, `aes128-ctr`, `aes192-ctr`, `aes256-ctr`, `aes128-cbc`, `aes192-cbc`, `aes256-cbc`, `3des-cbc`, `arcfour256`, `arcfour128`, `arcfour`. Default values: `aes128-gcm@openssh.com`, `aes256-gcm@openssh.com`, `chacha20-poly1305@openssh.com`, `aes128-ctr`, `aes192-ctr`, `aes256-ctr`. Please note that the ciphers disabled by default are insecure, you should expect that an active attacker can recover plaintext if you enable them. - `ciphers`, list of strings. Allowed ciphers in preference order. Leave empty to use default values. The supported values are: `aes128-gcm@openssh.com`, `aes256-gcm@openssh.com`, `chacha20-poly1305@openssh.com`, `aes128-ctr`, `aes192-ctr`, `aes256-ctr`, `aes128-cbc`, `aes192-cbc`, `aes256-cbc`, `3des-cbc`, `arcfour256`, `arcfour128`, `arcfour`. Default values: `aes128-gcm@openssh.com`, `aes256-gcm@openssh.com`, `chacha20-poly1305@openssh.com`, `aes128-ctr`, `aes192-ctr`, `aes256-ctr`. Please note that the ciphers disabled by default are insecure, you should expect that an active attacker can recover plaintext if you enable them.
- `macs`, list of strings. Available MAC (message authentication code) algorithms in preference order. Leave empty to use default values. The supported values are: `hmac-sha2-256-etm@openssh.com`, `hmac-sha2-256`, `hmac-sha2-512-etm@openssh.com`, `hmac-sha2-512`, `hmac-sha1`, `hmac-sha1-96`. Default values: `hmac-sha2-256-etm@openssh.com`, `hmac-sha2-256`. - `macs`, list of strings. Available MAC (message authentication code) algorithms in preference order. Leave empty to use default values. The supported values are: `hmac-sha2-256-etm@openssh.com`, `hmac-sha2-256`, `hmac-sha2-512-etm@openssh.com`, `hmac-sha2-512`, `hmac-sha1`, `hmac-sha1-96`. Default values: `hmac-sha2-256-etm@openssh.com`, `hmac-sha2-256`.

View file

@ -19,12 +19,12 @@ In this tutorial we'll focus on `HTTP-01` challenge type and make the following
## Overview ## Overview
- [Obtaining a certificate using the Lego CLI tool](#Obtaining-a-certificate-using-the-Lego-CLI-tool) - [Obtaining a certificate using the Lego CLI tool](#obtaining-a-certificate-using-the-lego-cli-tool)
- [Automatic certificate renewal using the Lego CLI tool](#Automatic-certificate-renewal-using-the-Lego-CLI-tool) - [Automatic certificate renewal using the Lego CLI tool](#automatic-certificate-renewal-using-the-lego-cli-tool)
- [Obtaining a certificate using the ACME protocol built into SFTPGo](#Obtaining-a-certificate-using-the-ACME-protocol-built-into-SFTPGo) - [Obtaining a certificate using the ACME protocol built into SFTPGo](#obtaining-a-certificate-using-the-acme-protocol-built-into-sftpgo)
- [Enable HTTPS for SFTPGo Web UI and REST API](#Enable-HTTPS-for-SFTPGo-Web-UI-and-REST-API) - [Enable HTTPS for SFTPGo Web UI and REST API](#enable-https-for-sftpgo-web-ui-and-rest-api)
- [Enable HTTPS for WebDAV service](#Enable-HTTPS-for-WebDAV-service) - [Enable HTTPS for WebDAV service](#enable-https-for-webdav-service)
- [Enable explicit FTP over TLS](#Enable-explicit-FTP-over-TLS) - [Enable explicit FTP over TLS](#enable-explicit-ftp-over-tls)
## Obtaining a certificate using the Lego CLI tool ## Obtaining a certificate using the Lego CLI tool
@ -149,6 +149,8 @@ SFTPGO_ACME__HTTP01_CHALLENGE__WEBROOT="/var/www/sftpgo.com"
Make sure that the `sftpgo` user can write to the `/var/www/sftpgo.com` directory or pre-create the `/var/www/sftpgo.com/.well-known/acme-challenge` directory with the appropriate permissions. Make sure that the `sftpgo` user can write to the `/var/www/sftpgo.com` directory or pre-create the `/var/www/sftpgo.com/.well-known/acme-challenge` directory with the appropriate permissions.
This directory must be publicly served by your web server. This directory must be publicly served by your web server.
:warning: in this example we assume you have an existing HTTP server. If not, you can leave the web root blank and SFTPGo will resolve the HTTP01 challenge by itself.
Register your account and obtain certificates by running the following command. Register your account and obtain certificates by running the following command.
```bash ```bash

30
go.mod
View file

@ -9,13 +9,13 @@ require (
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387 github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
github.com/aws/aws-sdk-go-v2 v1.17.4 github.com/aws/aws-sdk-go-v2 v1.17.4
github.com/aws/aws-sdk-go-v2/config v1.18.12 github.com/aws/aws-sdk-go-v2/config v1.18.13
github.com/aws/aws-sdk-go-v2/credentials v1.13.12 github.com/aws/aws-sdk-go-v2/credentials v1.13.13
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.22 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.22
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.51 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.53
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.14.2 github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.14.3
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.2 github.com/aws/aws-sdk-go-v2/service/s3 v1.30.3
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.18.3 github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.18.4
github.com/aws/aws-sdk-go-v2/service/sts v1.18.3 github.com/aws/aws-sdk-go-v2/service/sts v1.18.3
github.com/bmatcuk/doublestar/v4 v4.6.0 github.com/bmatcuk/doublestar/v4 v4.6.0
github.com/cockroachdb/cockroach-go/v2 v2.2.20 github.com/cockroachdb/cockroach-go/v2 v2.2.20
@ -68,33 +68,33 @@ require (
go.uber.org/automaxprocs v1.5.1 go.uber.org/automaxprocs v1.5.1
gocloud.dev v0.28.0 gocloud.dev v0.28.0
golang.org/x/crypto v0.6.0 golang.org/x/crypto v0.6.0
golang.org/x/net v0.6.0 golang.org/x/net v0.7.0
golang.org/x/oauth2 v0.5.0 golang.org/x/oauth2 v0.5.0
golang.org/x/sys v0.5.0 golang.org/x/sys v0.5.0
golang.org/x/term v0.5.0 golang.org/x/term v0.5.0
golang.org/x/time v0.3.0 golang.org/x/time v0.3.0
google.golang.org/api v0.109.0 google.golang.org/api v0.110.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
) )
require ( require (
cloud.google.com/go v0.109.0 // indirect cloud.google.com/go v0.110.0 // indirect
cloud.google.com/go/compute v1.18.0 // indirect cloud.google.com/go/compute v1.18.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v0.10.0 // indirect cloud.google.com/go/iam v0.12.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2 // indirect
github.com/ajg/form v1.5.1 // indirect github.com/ajg/form v1.5.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.29 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.3.29 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.19 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.20 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.23 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.23 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.22 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.22 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.22 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.22 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.1 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.12.2 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.1 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.2 // indirect
github.com/aws/smithy-go v1.13.5 // indirect github.com/aws/smithy-go v1.13.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1 // indirect github.com/boombuler/barcode v1.0.1 // indirect
@ -112,7 +112,7 @@ require (
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-cmp v0.5.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.2 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/googleapis/gax-go/v2 v2.7.0 // indirect github.com/googleapis/gax-go/v2 v2.7.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
@ -157,7 +157,7 @@ require (
golang.org/x/tools v0.6.0 // indirect golang.org/x/tools v0.6.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc // indirect google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44 // indirect
google.golang.org/grpc v1.53.0 // indirect google.golang.org/grpc v1.53.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect

59
go.sum
View file

@ -37,8 +37,8 @@ cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34h
cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA=
cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=
cloud.google.com/go v0.109.0 h1:38CZoKGlCnPZjGdyj0ZfpoGae0/wgNfy5F0byyxg0Gk= cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
cloud.google.com/go v0.109.0/go.mod h1:2sYycXt75t/CSB5R9M2wPU1tJmire7AQZTPtITcGBVE= cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY=
cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4=
cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw=
cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o=
@ -205,8 +205,8 @@ cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp
cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc=
cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc=
cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg=
cloud.google.com/go/iam v0.10.0 h1:fpP/gByFs6US1ma53v7VxhvbJpO2Aapng6wabJ99MuI= cloud.google.com/go/iam v0.12.0 h1:DRtTY29b75ciH6Ov1PHb4/iat2CLCvrOm40Q0a6DFpE=
cloud.google.com/go/iam v0.10.0/go.mod h1:nXAECrMt2qHpF6RZUZseteD6QyanL68reN4OXPw0UWM= cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY=
cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc=
cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A=
cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM=
@ -539,17 +539,17 @@ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.9/go.mod h1:vCmV1q1VK
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
github.com/aws/aws-sdk-go-v2/config v1.18.3/go.mod h1:BYdrbeCse3ZnOD5+2/VE/nATOK8fEUpBtmPMdKSyhMU= github.com/aws/aws-sdk-go-v2/config v1.18.3/go.mod h1:BYdrbeCse3ZnOD5+2/VE/nATOK8fEUpBtmPMdKSyhMU=
github.com/aws/aws-sdk-go-v2/config v1.18.12 h1:fKs/I4wccmfrNRO9rdrbMO1NgLxct6H9rNMiPdBxHWw= github.com/aws/aws-sdk-go-v2/config v1.18.13 h1:v0xlYqbO6/EVlM8tUn2QEOA7btQxcgidEq2JRDBPTho=
github.com/aws/aws-sdk-go-v2/config v1.18.12/go.mod h1:J36fOhj1LQBr+O4hJCiT8FwVvieeoSGOtPuvhKlsNu8= github.com/aws/aws-sdk-go-v2/config v1.18.13/go.mod h1:r39wGSZB7wPDW1i54JyQXUpc5KsWjh5z/3S5D9eCqDg=
github.com/aws/aws-sdk-go-v2/credentials v1.13.3/go.mod h1:/rOMmqYBcFfNbRPU0iN9IgGqD5+V2yp3iWNmIlz0wI4= github.com/aws/aws-sdk-go-v2/credentials v1.13.3/go.mod h1:/rOMmqYBcFfNbRPU0iN9IgGqD5+V2yp3iWNmIlz0wI4=
github.com/aws/aws-sdk-go-v2/credentials v1.13.12 h1:Cb+HhuEnV19zHRaYYVglwvdHGMJWbdsyP4oHhw04xws= github.com/aws/aws-sdk-go-v2/credentials v1.13.13 h1:zw1KAc1kl00NYd3ofVmFrb09qnYlSQMeh+fmlQRAihI=
github.com/aws/aws-sdk-go-v2/credentials v1.13.12/go.mod h1:37HG2MBroXK3jXfxVGtbM2J48ra2+Ltu+tmwr/jO0KA= github.com/aws/aws-sdk-go-v2/credentials v1.13.13/go.mod h1:DW9nbIIF9MrIja0cBQrUpeWYQMSlNmP8fevLUyF9W38=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19/go.mod h1:VihW95zQpeKQWVPGkwT+2+WJNQV8UXFfMTWdU6VErL8= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19/go.mod h1:VihW95zQpeKQWVPGkwT+2+WJNQV8UXFfMTWdU6VErL8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.22 h1:3aMfcTmoXtTZnaT86QlVaYh+BRMbvrrmZwIQ5jWqCZQ= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.22 h1:3aMfcTmoXtTZnaT86QlVaYh+BRMbvrrmZwIQ5jWqCZQ=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.22/go.mod h1:YGSIJyQ6D6FjKMQh16hVFSIUD54L4F7zTGePqYMYYJU= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.22/go.mod h1:YGSIJyQ6D6FjKMQh16hVFSIUD54L4F7zTGePqYMYYJU=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.42/go.mod h1:LHOsygMiW/14CkFxdXxvzKyMh3jbk/QfZVaDtCbLkl8= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.42/go.mod h1:LHOsygMiW/14CkFxdXxvzKyMh3jbk/QfZVaDtCbLkl8=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.51 h1:iTFYCAdKzSAjGnVIUe88Hxvix0uaBqr0Rv7qJEOX5hE= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.53 h1:h1MmqGtYgkf49DhG2BSjGukpm8c+BJ9CL+bBbdFGzlk=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.51/go.mod h1:7Grl2gV+dx9SWrUIgwwlUvU40t7+lOSbx34XwfmsTkY= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.53/go.mod h1:mlWLxwKZNeEwE+3Pko07lSr1NvHZwUtdzmo9AiGn7QU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25/go.mod h1:Zb29PYkf42vVYQY6pvSyJCJcFHlPIiY+YKdPtwnvMkY= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25/go.mod h1:Zb29PYkf42vVYQY6pvSyJCJcFHlPIiY+YKdPtwnvMkY=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28 h1:r+XwaCLpIvCKjBIYy/HVZujQS9tsz5ohHG3ZIe0wKoE= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28 h1:r+XwaCLpIvCKjBIYy/HVZujQS9tsz5ohHG3ZIe0wKoE=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28/go.mod h1:3lwChorpIM/BhImY/hy+Z6jekmN92cXGPI1QJasVPYY= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28/go.mod h1:3lwChorpIM/BhImY/hy+Z6jekmN92cXGPI1QJasVPYY=
@ -560,8 +560,8 @@ github.com/aws/aws-sdk-go-v2/internal/ini v1.3.26/go.mod h1:Y2OJ+P+MC1u1VKnavT+P
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.29 h1:J4xhFd6zHhdF9jPP0FQJ6WknzBboGMBNjKOv4iTuw4A= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.29 h1:J4xhFd6zHhdF9jPP0FQJ6WknzBboGMBNjKOv4iTuw4A=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.29/go.mod h1:TwuqRBGzxjQJIwH16/fOZodwXt2Zxa9/cwJC5ke4j7s= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.29/go.mod h1:TwuqRBGzxjQJIwH16/fOZodwXt2Zxa9/cwJC5ke4j7s=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.16/go.mod h1:XH+3h395e3WVdd6T2Z3mPxuI+x/HVtdqVOREkTiyubs= github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.16/go.mod h1:XH+3h395e3WVdd6T2Z3mPxuI+x/HVtdqVOREkTiyubs=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.19 h1:FGvpyTg2LKEmMrLlpjOgkoNp9XF5CGeyAyo33LdqZW8= github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.20 h1:YIvKIfPXQVp0EhXUV644kmQo6cQPPSRmC44A1HSoJeg=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.19/go.mod h1:8W88sW3PjamQpKFUQvHWWKay6ARsNvZnzU7+a4apubw= github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.20/go.mod h1:8W88sW3PjamQpKFUQvHWWKay6ARsNvZnzU7+a4apubw=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.10/go.mod h1:9cBNUHI2aW4ho0A5T87O294iPDuuUOSIEDjnd1Lq/z0= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.10/go.mod h1:9cBNUHI2aW4ho0A5T87O294iPDuuUOSIEDjnd1Lq/z0=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=
@ -575,23 +575,23 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.19/go.mod h1:BmQWRV
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.22 h1:ISLJ2BKXe4zzyZ7mp5ewKECiw0U7KpLgS3S6OxY9Cm0= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.22 h1:ISLJ2BKXe4zzyZ7mp5ewKECiw0U7KpLgS3S6OxY9Cm0=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.22/go.mod h1:QFVbqK54XArazLvn2wvWMRBi/jGrWii46qbr5DyPGjc= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.22/go.mod h1:QFVbqK54XArazLvn2wvWMRBi/jGrWii46qbr5DyPGjc=
github.com/aws/aws-sdk-go-v2/service/kms v1.19.0/go.mod h1:kZodDPTQjSH/qM6/OvyTfM5mms5JHB/EKYp5dhn/vI4= github.com/aws/aws-sdk-go-v2/service/kms v1.19.0/go.mod h1:kZodDPTQjSH/qM6/OvyTfM5mms5JHB/EKYp5dhn/vI4=
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.14.2 h1:7vuSkPqVqwBwSV0OJD71qqWOEFr3Hh1K0e2yOQ/JWwQ= github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.14.3 h1:7SguEzgmyCr6bgJ4+GLk1QWGJ+tpN8q26oNpWcQg1jw=
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.14.2/go.mod h1:vrZVsmrC7QRNBK/W8nplI0tfJDvMl6DZAUT/pkFJiws= github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.14.3/go.mod h1:vrZVsmrC7QRNBK/W8nplI0tfJDvMl6DZAUT/pkFJiws=
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.4/go.mod h1:/NHbqPRiwxSPVOB2Xr+StDEH+GWV/64WwnUjv4KYzV0= github.com/aws/aws-sdk-go-v2/service/s3 v1.29.4/go.mod h1:/NHbqPRiwxSPVOB2Xr+StDEH+GWV/64WwnUjv4KYzV0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.2 h1:5EQWIFO+Hc8E2hFcXQJ1vm6ufl/PMt/6RVRDZRju2vM= github.com/aws/aws-sdk-go-v2/service/s3 v1.30.3 h1:PVieHTwugdlHedlxLpYLQsOZAq736RScuEb/m4zhzc4=
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.2/go.mod h1:SXDHd6fI2RhqB7vmAzyYQCTQnpZrIprVJvYxpzW3JAM= github.com/aws/aws-sdk-go-v2/service/s3 v1.30.3/go.mod h1:XN3YcdmnWYZ3Hrnojvo5p2mc/wfF973nkq3ClXPDMHk=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.8/go.mod h1:k6CPuxyzO247nYEM1baEwHH1kRtosRCvgahAepaaShw= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.8/go.mod h1:k6CPuxyzO247nYEM1baEwHH1kRtosRCvgahAepaaShw=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.18.3 h1:Zod/h9QcDvbrrG3jjTUp4lctRb6Qg2nj7ARC/xMsUc4= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.18.4 h1:0P9VF9miVGT40WSZSuMzHwkwTVIltpDrTrvswMLjbx0=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.18.3/go.mod h1:hqPcyOuLU6yWIbLy3qMnQnmidgKuIEwqIlW6+chYnog= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.18.4/go.mod h1:hqPcyOuLU6yWIbLy3qMnQnmidgKuIEwqIlW6+chYnog=
github.com/aws/aws-sdk-go-v2/service/sns v1.18.6/go.mod h1:2cPUjR63iE9MPMPJtSyzYmsTFCNrN/Xi9j0v9BL5OU0= github.com/aws/aws-sdk-go-v2/service/sns v1.18.6/go.mod h1:2cPUjR63iE9MPMPJtSyzYmsTFCNrN/Xi9j0v9BL5OU0=
github.com/aws/aws-sdk-go-v2/service/sqs v1.19.15/go.mod h1:DKX/7/ZiAzHO6p6AhArnGdrV4r+d461weby8KeVtvC4= github.com/aws/aws-sdk-go-v2/service/sqs v1.19.15/go.mod h1:DKX/7/ZiAzHO6p6AhArnGdrV4r+d461weby8KeVtvC4=
github.com/aws/aws-sdk-go-v2/service/ssm v1.33.1/go.mod h1:rEsqsZrOp9YvSGPOrcL3pR9+i/QJaWRkAYbuxMa7yCU= github.com/aws/aws-sdk-go-v2/service/ssm v1.33.1/go.mod h1:rEsqsZrOp9YvSGPOrcL3pR9+i/QJaWRkAYbuxMa7yCU=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.25/go.mod h1:IARHuzTXmj1C0KS35vboR0FeJ89OkEy1M9mWbK2ifCI= github.com/aws/aws-sdk-go-v2/service/sso v1.11.25/go.mod h1:IARHuzTXmj1C0KS35vboR0FeJ89OkEy1M9mWbK2ifCI=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.1 h1:lQKN/LNa3qqu2cDOQZybP7oL4nMGGiFqob0jZJaR8/4= github.com/aws/aws-sdk-go-v2/service/sso v1.12.2 h1:EN102fWY7hI5u/2FPheTrwwMHkSXfl49RYkeEnJsrCU=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.1/go.mod h1:IgV8l3sj22nQDd5qcAGY0WenwCzCphqdbFOpfktZPrI= github.com/aws/aws-sdk-go-v2/service/sso v1.12.2/go.mod h1:IgV8l3sj22nQDd5qcAGY0WenwCzCphqdbFOpfktZPrI=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8/go.mod h1:er2JHN+kBY6FcMfcBBKNGCT3CarImmdFzishsqBmSRI= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8/go.mod h1:er2JHN+kBY6FcMfcBBKNGCT3CarImmdFzishsqBmSRI=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.1 h1:0bLhH6DRAqox+g0LatcjGKjjhU6Eudyys6HB6DJVPj8= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.2 h1:f1lmlce7r13CX1BPyPqt9oh/H+uqOWc9367lDoGGwNQ=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.1/go.mod h1:O1YSOg3aekZibh2SngvCRRG+cRHKKlYgxf/JBF/Kr/k= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.2/go.mod h1:O1YSOg3aekZibh2SngvCRRG+cRHKKlYgxf/JBF/Kr/k=
github.com/aws/aws-sdk-go-v2/service/sts v1.17.5/go.mod h1:bXcN3koeVYiJcdDU89n3kCYILob7Y34AeLopUbZgLT4= github.com/aws/aws-sdk-go-v2/service/sts v1.17.5/go.mod h1:bXcN3koeVYiJcdDU89n3kCYILob7Y34AeLopUbZgLT4=
github.com/aws/aws-sdk-go-v2/service/sts v1.18.3 h1:s49mSnsBZEXjfGBkRfmK+nPqzT7Lt3+t2SmAKNyHblw= github.com/aws/aws-sdk-go-v2/service/sts v1.18.3 h1:s49mSnsBZEXjfGBkRfmK+nPqzT7Lt3+t2SmAKNyHblw=
github.com/aws/aws-sdk-go-v2/service/sts v1.18.3/go.mod h1:b+psTJn33Q4qGoDaM7ZiOVVG8uVjGI6HaZ8WBHdgDgU= github.com/aws/aws-sdk-go-v2/service/sts v1.18.3/go.mod h1:b+psTJn33Q4qGoDaM7ZiOVVG8uVjGI6HaZ8WBHdgDgU=
@ -1172,8 +1172,8 @@ github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1
github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
github.com/googleapis/enterprise-certificate-proxy v0.2.2 h1:jUqbmxlR+gGPQq/uvQviKpS1bSQecfs2t7o6F14sk9s= github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
github.com/googleapis/enterprise-certificate-proxy v0.2.2/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
@ -2180,8 +2180,9 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -2578,8 +2579,8 @@ google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91
google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70=
google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo=
google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0=
google.golang.org/api v0.109.0 h1:sW9hgHyX497PP5//NUM7nqfV8D0iDfBApqq7sOh1XR8= google.golang.org/api v0.110.0 h1:l+rh0KYUooe9JGbGVx71tbFo4SMbMTXK3I3ia2QSEeU=
google.golang.org/api v0.109.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -2713,8 +2714,8 @@ google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZV
google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc h1:ijGwO+0vL2hJt5gaygqP2j6PfflOBrRot0IczKbmtio= google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44 h1:EfLuoKW5WfkgVdDy7dTK8qSbH37AX5mj/MFh+bGPz14=
google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA=
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=

View file

@ -21,6 +21,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/drakkan/sftpgo/v2/internal/config" "github.com/drakkan/sftpgo/v2/internal/config"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/logger" "github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/smtp" "github.com/drakkan/sftpgo/v2/internal/smtp"
"github.com/drakkan/sftpgo/v2/internal/util" "github.com/drakkan/sftpgo/v2/internal/util"
@ -39,7 +40,13 @@ If the SMTP configuration is correct you should receive this email.`,
configDir = util.CleanDirInput(configDir) configDir = util.CleanDirInput(configDir)
err := config.LoadConfig(configDir, configFile) err := config.LoadConfig(configDir, configFile)
if err != nil { if err != nil {
logger.WarnToConsole("Unable to load configuration: %v", err) logger.ErrorToConsole("Unable to load configuration: %v", err)
os.Exit(1)
}
providerConf := config.GetProviderConf()
err = dataprovider.Initialize(providerConf, configDir, false)
if err != nil {
logger.ErrorToConsole("error initializing data provider: %v", err)
os.Exit(1) os.Exit(1)
} }
smtpConfig := config.GetSMTPConfig() smtpConfig := config.GetSMTPConfig()
@ -54,7 +61,7 @@ If the SMTP configuration is correct you should receive this email.`,
logger.WarnToConsole("Error sending email: %v", err) logger.WarnToConsole("Error sending email: %v", err)
os.Exit(1) os.Exit(1)
} }
logger.InfoToConsole("No errors were reported while sending an email. Please check your inbox to make sure.") logger.InfoToConsole("No errors were reported while sending the test email. Please check your inbox to make sure.")
}, },
} }
) )

View file

@ -96,16 +96,6 @@ Command-line flags should be specified in the Subsystem declaration.
logger.Error(logSender, "", "unable to initialize MFA: %v", err) logger.Error(logSender, "", "unable to initialize MFA: %v", err)
os.Exit(1) os.Exit(1)
} }
if err := plugin.Initialize(config.GetPluginsConfig(), logLevel); err != nil {
logger.Error(logSender, connectionID, "unable to initialize plugin system: %v", err)
os.Exit(1)
}
smtpConfig := config.GetSMTPConfig()
err = smtpConfig.Initialize(configDir)
if err != nil {
logger.Error(logSender, connectionID, "unable to initialize SMTP configuration: %v", err)
os.Exit(1)
}
dataProviderConf := config.GetProviderConf() dataProviderConf := config.GetProviderConf()
if dataProviderConf.Driver == dataprovider.SQLiteDataProviderName || dataProviderConf.Driver == dataprovider.BoltDataProviderName { if dataProviderConf.Driver == dataprovider.SQLiteDataProviderName || dataProviderConf.Driver == dataprovider.BoltDataProviderName {
logger.Debug(logSender, connectionID, "data provider %q not supported in subsystem mode, using %q provider", logger.Debug(logSender, connectionID, "data provider %q not supported in subsystem mode, using %q provider",
@ -119,6 +109,16 @@ Command-line flags should be specified in the Subsystem declaration.
logger.Error(logSender, connectionID, "unable to initialize the data provider: %v", err) logger.Error(logSender, connectionID, "unable to initialize the data provider: %v", err)
os.Exit(1) os.Exit(1)
} }
if err := plugin.Initialize(config.GetPluginsConfig(), logLevel); err != nil {
logger.Error(logSender, connectionID, "unable to initialize plugin system: %v", err)
os.Exit(1)
}
smtpConfig := config.GetSMTPConfig()
err = smtpConfig.Initialize(configDir)
if err != nil {
logger.Error(logSender, connectionID, "unable to initialize SMTP configuration: %v", err)
os.Exit(1)
}
commonConfig := config.GetCommonConfig() commonConfig := config.GetCommonConfig()
// idle connection are managed externally // idle connection are managed externally
commonConfig.IdleTimeout = 0 commonConfig.IdleTimeout = 0

View file

@ -39,6 +39,7 @@ import (
"github.com/drakkan/sftpgo/v2/internal/logger" "github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/metric" "github.com/drakkan/sftpgo/v2/internal/metric"
"github.com/drakkan/sftpgo/v2/internal/plugin" "github.com/drakkan/sftpgo/v2/internal/plugin"
"github.com/drakkan/sftpgo/v2/internal/smtp"
"github.com/drakkan/sftpgo/v2/internal/util" "github.com/drakkan/sftpgo/v2/internal/util"
"github.com/drakkan/sftpgo/v2/internal/vfs" "github.com/drakkan/sftpgo/v2/internal/vfs"
) )
@ -170,7 +171,7 @@ func Initialize(c Configuration, isShared int) error {
Config.ProxyAllowed = util.RemoveDuplicates(Config.ProxyAllowed, true) Config.ProxyAllowed = util.RemoveDuplicates(Config.ProxyAllowed, true)
Config.idleLoginTimeout = 2 * time.Minute Config.idleLoginTimeout = 2 * time.Minute
Config.idleTimeoutAsDuration = time.Duration(Config.IdleTimeout) * time.Minute Config.idleTimeoutAsDuration = time.Duration(Config.IdleTimeout) * time.Minute
startPeriodicChecks(periodicTimeoutCheckInterval) startPeriodicChecks(periodicTimeoutCheckInterval, isShared)
Config.defender = nil Config.defender = nil
Config.allowList = nil Config.allowList = nil
Config.rateLimitersList = nil Config.rateLimitersList = nil
@ -382,12 +383,17 @@ func AddDefenderEvent(ip, protocol string, event HostEvent) {
Config.defender.AddEvent(ip, protocol, event) Config.defender.AddEvent(ip, protocol, event)
} }
func startPeriodicChecks(duration time.Duration) { func startPeriodicChecks(duration time.Duration, isShared int) {
startEventScheduler() startEventScheduler()
spec := fmt.Sprintf("@every %s", duration) spec := fmt.Sprintf("@every %s", duration)
_, err := eventScheduler.AddFunc(spec, Connections.checkTransfers) _, err := eventScheduler.AddFunc(spec, Connections.checkTransfers)
util.PanicOnError(err) util.PanicOnError(err)
logger.Info(logSender, "", "scheduled overquota transfers check, schedule %q", spec) logger.Info(logSender, "", "scheduled overquota transfers check, schedule %q", spec)
if isShared == 1 {
logger.Info(logSender, "", "add reload configs task")
_, err := eventScheduler.AddFunc("@every 10m", smtp.ReloadProviderConf)
util.PanicOnError(err)
}
if Config.IdleTimeout > 0 { if Config.IdleTimeout > 0 {
ratio := idleTimeoutCheckInterval / periodicTimeoutCheckInterval ratio := idleTimeoutCheckInterval / periodicTimeoutCheckInterval
spec = fmt.Sprintf("@every %s", duration*ratio) spec = fmt.Sprintf("@every %s", duration*ratio)

View file

@ -722,7 +722,7 @@ func TestIdleConnections(t *testing.T) {
assert.Len(t, Connections.sshConnections, 2) assert.Len(t, Connections.sshConnections, 2)
Connections.RUnlock() Connections.RUnlock()
startPeriodicChecks(100 * time.Millisecond) startPeriodicChecks(100*time.Millisecond, 0)
assert.Eventually(t, func() bool { return Connections.GetActiveSessions(username) == 1 }, 2*time.Second, 200*time.Millisecond) assert.Eventually(t, func() bool { return Connections.GetActiveSessions(username) == 1 }, 2*time.Second, 200*time.Millisecond)
assert.Eventually(t, func() bool { assert.Eventually(t, func() bool {
Connections.RLock() Connections.RLock()
@ -734,7 +734,7 @@ func TestIdleConnections(t *testing.T) {
c.lastActivity.Store(time.Now().Add(-24 * time.Hour).UnixNano()) c.lastActivity.Store(time.Now().Add(-24 * time.Hour).UnixNano())
cFTP.lastActivity.Store(time.Now().Add(-24 * time.Hour).UnixNano()) cFTP.lastActivity.Store(time.Now().Add(-24 * time.Hour).UnixNano())
sshConn2.lastActivity.Store(c.lastActivity.Load()) sshConn2.lastActivity.Store(c.lastActivity.Load())
startPeriodicChecks(100 * time.Millisecond) startPeriodicChecks(100*time.Millisecond, 1)
assert.Eventually(t, func() bool { return len(Connections.GetStats("")) == 0 }, 2*time.Second, 200*time.Millisecond) assert.Eventually(t, func() bool { return len(Connections.GetStats("")) == 0 }, 2*time.Second, 200*time.Millisecond)
assert.Eventually(t, func() bool { assert.Eventually(t, func() bool {
Connections.RLock() Connections.RLock()

View file

@ -52,6 +52,7 @@ const (
actionObjectEventRule = "event_rule" actionObjectEventRule = "event_rule"
actionObjectRole = "role" actionObjectRole = "role"
actionObjectIPListEntry = "ip_list_entry" actionObjectIPListEntry = "ip_list_entry"
actionObjectConfigs = "configs"
) )
var ( var (
@ -90,14 +91,14 @@ func executeAction(operation, executor, ip, objectType, objectName, role string,
dataAsJSON, err := object.RenderAsJSON(operation != operationDelete) dataAsJSON, err := object.RenderAsJSON(operation != operationDelete)
if err != nil { if err != nil {
providerLog(logger.LevelError, "unable to serialize user as JSON for operation %#v: %v", operation, err) providerLog(logger.LevelError, "unable to serialize user as JSON for operation %q: %v", operation, err)
return return
} }
if strings.HasPrefix(config.Actions.Hook, "http") { if strings.HasPrefix(config.Actions.Hook, "http") {
var url *url.URL var url *url.URL
url, err := url.Parse(config.Actions.Hook) url, err := url.Parse(config.Actions.Hook)
if err != nil { if err != nil {
providerLog(logger.LevelError, "Invalid http_notification_url %#v for operation %#v: %v", providerLog(logger.LevelError, "Invalid http_notification_url %q for operation %q: %v",
config.Actions.Hook, operation, err) config.Actions.Hook, operation, err)
return return
} }
@ -129,7 +130,7 @@ func executeAction(operation, executor, ip, objectType, objectName, role string,
func executeNotificationCommand(operation, executor, ip, objectType, objectName, role string, objectAsJSON []byte) error { func executeNotificationCommand(operation, executor, ip, objectType, objectName, role string, objectAsJSON []byte) error {
if !filepath.IsAbs(config.Actions.Hook) { if !filepath.IsAbs(config.Actions.Hook) {
err := fmt.Errorf("invalid notification command %#v", config.Actions.Hook) err := fmt.Errorf("invalid notification command %q", config.Actions.Hook)
logger.Warn(logSender, "", "unable to execute notification command: %v", err) logger.Warn(logSender, "", "unable to execute notification command: %v", err)
return err return err
} }

View file

@ -37,7 +37,7 @@ import (
) )
const ( const (
boltDatabaseVersion = 27 boltDatabaseVersion = 28
) )
var ( var (
@ -51,10 +51,12 @@ var (
rulesBucket = []byte("events_rules") rulesBucket = []byte("events_rules")
rolesBucket = []byte("roles") rolesBucket = []byte("roles")
ipListsBucket = []byte("ip_lists") ipListsBucket = []byte("ip_lists")
configsBucket = []byte("configs")
dbVersionBucket = []byte("db_version") dbVersionBucket = []byte("db_version")
dbVersionKey = []byte("version") dbVersionKey = []byte("version")
configsKey = []byte("configs")
boltBuckets = [][]byte{usersBucket, groupsBucket, foldersBucket, adminsBucket, apiKeysBucket, boltBuckets = [][]byte{usersBucket, groupsBucket, foldersBucket, adminsBucket, apiKeysBucket,
sharesBucket, actionsBucket, rulesBucket, rolesBucket, ipListsBucket, dbVersionBucket} sharesBucket, actionsBucket, rulesBucket, rolesBucket, ipListsBucket, configsBucket, dbVersionBucket}
) )
// BoltProvider defines the auth provider for bolt key/value store // BoltProvider defines the auth provider for bolt key/value store
@ -2977,6 +2979,39 @@ func (p *BoltProvider) getListEntriesForIP(ip string, listType IPListType) ([]IP
return entries, err return entries, err
} }
func (p *BoltProvider) getConfigs() (Configs, error) {
var configs Configs
err := p.dbHandle.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket(configsBucket)
if bucket == nil {
return fmt.Errorf("unable to find configs bucket")
}
data := bucket.Get(configsKey)
if data != nil {
return json.Unmarshal(data, &configs)
}
return nil
})
return configs, err
}
func (p *BoltProvider) setConfigs(configs *Configs) error {
if err := configs.validate(); err != nil {
return err
}
return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket(configsBucket)
if bucket == nil {
return fmt.Errorf("unable to find configs bucket")
}
buf, err := json.Marshal(configs)
if err != nil {
return err
}
return bucket.Put(configsKey, buf)
})
}
func (p *BoltProvider) setFirstDownloadTimestamp(username string) error { func (p *BoltProvider) setFirstDownloadTimestamp(username string) error {
return p.dbHandle.Update(func(tx *bolt.Tx) error { return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, err := p.getUsersBucket(tx) bucket, err := p.getUsersBucket(tx)
@ -3061,9 +3096,9 @@ func (p *BoltProvider) migrateDatabase() error {
providerLog(logger.LevelError, "%v", err) providerLog(logger.LevelError, "%v", err)
logger.ErrorToConsole("%v", err) logger.ErrorToConsole("%v", err)
return err return err
case version == 23, version == 24, version == 25, version == 26: case version == 23, version == 24, version == 25, version == 26, version == 27:
logger.InfoToConsole("updating database schema version: %d -> 27", version) logger.InfoToConsole("updating database schema version: %d -> 28", version)
providerLog(logger.LevelInfo, "updating database schema version: %d -> 27", version) providerLog(logger.LevelInfo, "updating database schema version: %d -> 28", version)
err := p.dbHandle.Update(func(tx *bolt.Tx) error { err := p.dbHandle.Update(func(tx *bolt.Tx) error {
rules, err := p.dumpEventRules() rules, err := p.dumpEventRules()
if err != nil { if err != nil {
@ -3095,7 +3130,7 @@ func (p *BoltProvider) migrateDatabase() error {
if err != nil { if err != nil {
return err return err
} }
return updateBoltDatabaseVersion(p.dbHandle, 27) return updateBoltDatabaseVersion(p.dbHandle, 28)
default: default:
if version > boltDatabaseVersion { if version > boltDatabaseVersion {
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version, providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
@ -3108,7 +3143,7 @@ func (p *BoltProvider) migrateDatabase() error {
} }
} }
func (p *BoltProvider) revertDatabase(targetVersion int) error { func (p *BoltProvider) revertDatabase(targetVersion int) error { //nolint:gocyclo
dbVersion, err := getBoltDatabaseVersion(p.dbHandle) dbVersion, err := getBoltDatabaseVersion(p.dbHandle)
if err != nil { if err != nil {
return err return err
@ -3117,7 +3152,7 @@ func (p *BoltProvider) revertDatabase(targetVersion int) error {
return errors.New("current version match target version, nothing to do") return errors.New("current version match target version, nothing to do")
} }
switch dbVersion.Version { switch dbVersion.Version {
case 24, 25, 26: case 24, 25, 26, 27, 28:
logger.InfoToConsole("downgrading database schema version: %d -> 23", dbVersion.Version) logger.InfoToConsole("downgrading database schema version: %d -> 23", dbVersion.Version)
providerLog(logger.LevelInfo, "downgrading database schema version: %d -> 23", dbVersion.Version) providerLog(logger.LevelInfo, "downgrading database schema version: %d -> 23", dbVersion.Version)
err := p.dbHandle.Update(func(tx *bolt.Tx) error { err := p.dbHandle.Update(func(tx *bolt.Tx) error {
@ -3145,9 +3180,11 @@ func (p *BoltProvider) revertDatabase(targetVersion int) error {
} }
} }
} }
err = tx.DeleteBucket(rolesBucket) for _, b := range [][]byte{rolesBucket, configsBucket} {
if err != nil && !errors.Is(err, bolt.ErrBucketNotFound) { err = tx.DeleteBucket(b)
return err if err != nil && !errors.Is(err, bolt.ErrBucketNotFound) {
return err
}
} }
return nil return nil
}) })

View file

@ -0,0 +1,299 @@
// Copyright (C) 2019-2023 Nicola Murino
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package dataprovider
import (
"encoding/json"
"fmt"
"strings"
"golang.org/x/crypto/ssh"
"github.com/drakkan/sftpgo/v2/internal/kms"
"github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/util"
)
// Supported values for host keys, KEXs, ciphers, MACs
var (
supportedHostKeyAlgos = []string{ssh.KeyAlgoRSA, ssh.CertAlgoRSAv01}
supportedKexAlgos = []string{
"diffie-hellman-group16-sha512", "diffie-hellman-group18-sha512",
"diffie-hellman-group14-sha1", "diffie-hellman-group1-sha1",
"diffie-hellman-group-exchange-sha256", "diffie-hellman-group-exchange-sha1",
}
supportedCiphers = []string{
"aes128-cbc", "aes192-cbc", "aes256-cbc",
"3des-cbc",
}
supportedMACs = []string{
"hmac-sha2-512-etm@openssh.com", "hmac-sha2-512",
"hmac-sha1", "hmac-sha1-96",
}
)
// SFTPDConfigs defines configurations for SFTPD
type SFTPDConfigs struct {
HostKeyAlgos []string `json:"host_key_algos,omitempty"`
Moduli []string `json:"moduli,omitempty"`
KexAlgorithms []string `json:"kex_algorithms,omitempty"`
Ciphers []string `json:"ciphers,omitempty"`
MACs []string `json:"macs,omitempty"`
}
func (c *SFTPDConfigs) isEmpty() bool {
if len(c.HostKeyAlgos) > 0 {
return false
}
if len(c.Moduli) > 0 {
return false
}
if len(c.KexAlgorithms) > 0 {
return false
}
if len(c.Ciphers) > 0 {
return false
}
if len(c.MACs) > 0 {
return false
}
return true
}
// GetSupportedHostKeyAlgos returns the supported legacy host key algos
func (*SFTPDConfigs) GetSupportedHostKeyAlgos() []string {
return supportedHostKeyAlgos
}
// GetSupportedKEXAlgos returns the supported KEX algos
func (*SFTPDConfigs) GetSupportedKEXAlgos() []string {
return supportedKexAlgos
}
// GetSupportedCiphers returns the supported ciphers
func (*SFTPDConfigs) GetSupportedCiphers() []string {
return supportedCiphers
}
// GetSupportedMACs returns the supported MACs algos
func (*SFTPDConfigs) GetSupportedMACs() []string {
return supportedMACs
}
// GetModuliAsString returns moduli files as comma separated string
func (c *SFTPDConfigs) GetModuliAsString() string {
return strings.Join(c.Moduli, ",")
}
func (c *SFTPDConfigs) validate() error {
for _, algo := range c.HostKeyAlgos {
if !util.Contains(supportedHostKeyAlgos, algo) {
return util.NewValidationError(fmt.Sprintf("unsupported host key algorithm %q", algo))
}
}
for _, algo := range c.KexAlgorithms {
if !util.Contains(supportedKexAlgos, algo) {
return util.NewValidationError(fmt.Sprintf("unsupported KEX algorithm %q", algo))
}
}
for _, cipher := range c.Ciphers {
if !util.Contains(supportedCiphers, cipher) {
return util.NewValidationError(fmt.Sprintf("unsupported cipher %q", cipher))
}
}
for _, mac := range c.MACs {
if !util.Contains(supportedMACs, mac) {
return util.NewValidationError(fmt.Sprintf("unsupported MAC algorithm %q", mac))
}
}
return nil
}
func (c *SFTPDConfigs) getACopy() *SFTPDConfigs {
hostKeys := make([]string, len(c.HostKeyAlgos))
copy(hostKeys, c.HostKeyAlgos)
moduli := make([]string, len(c.Moduli))
copy(moduli, c.Moduli)
kexs := make([]string, len(c.KexAlgorithms))
copy(kexs, c.KexAlgorithms)
ciphers := make([]string, len(c.Ciphers))
copy(ciphers, c.Ciphers)
macs := make([]string, len(c.MACs))
copy(macs, c.MACs)
return &SFTPDConfigs{
HostKeyAlgos: hostKeys,
Moduli: moduli,
KexAlgorithms: kexs,
Ciphers: ciphers,
MACs: macs,
}
}
// SMTPConfigs defines configuration for SMTP
type SMTPConfigs struct {
Host string `json:"host,omitempty"`
Port int `json:"port,omitempty"`
From string `json:"from,omitempty"`
User string `json:"user,omitempty"`
Password *kms.Secret `json:"password,omitempty"`
AuthType int `json:"auth_type,omitempty"`
Encryption int `json:"encryption,omitempty"`
Domain string `json:"domain,omitempty"`
}
func (c *SMTPConfigs) isEmpty() bool {
return c.Host == ""
}
func (c *SMTPConfigs) validatePassword() error {
if c.Password != nil {
if c.Password.IsRedacted() {
return util.NewValidationError("cannot save a redacted smtp password")
}
if c.Password.IsEncrypted() && !c.Password.IsValid() {
return util.NewValidationError("invalid encrypted smtp password")
}
if !c.Password.IsEmpty() && !c.Password.IsValidInput() {
return util.NewValidationError("invalid smtp password")
}
if c.Password.IsPlain() {
c.Password.SetAdditionalData("smtp")
if err := c.Password.Encrypt(); err != nil {
return util.NewValidationError(fmt.Sprintf("could not encrypt smtp password: %v", err))
}
}
}
return nil
}
func (c *SMTPConfigs) validate() error {
if c.isEmpty() {
return nil
}
if c.Port <= 0 || c.Port > 65535 {
return util.NewValidationError(fmt.Sprintf("smtp: invalid port %d", c.Port))
}
if err := c.validatePassword(); err != nil {
return err
}
if c.User == "" && c.From == "" {
return util.NewValidationError("smtp: from address and user cannot both be empty")
}
if c.AuthType < 0 || c.AuthType > 2 {
return util.NewValidationError(fmt.Sprintf("smtp: invalid auth type %d", c.AuthType))
}
if c.Encryption < 0 || c.Encryption > 2 {
return util.NewValidationError(fmt.Sprintf("smtp: invalid encryption %d", c.Encryption))
}
return nil
}
func (c *SMTPConfigs) getACopy() *SMTPConfigs {
var password *kms.Secret
if c.Password != nil {
password = c.Password.Clone()
}
return &SMTPConfigs{
Host: c.Host,
Port: c.Port,
From: c.From,
User: c.User,
Password: password,
AuthType: c.AuthType,
Encryption: c.Encryption,
Domain: c.Domain,
}
}
// Configs allows to set configuration keys disabled by default without
// modifying the config file or setting env vars
type Configs struct {
SFTPD *SFTPDConfigs `json:"sftpd,omitempty"`
SMTP *SMTPConfigs `json:"smtp,omitempty"`
UpdatedAt int64 `json:"updated_at,omitempty"`
}
func (c *Configs) validate() error {
if c.SFTPD != nil {
if err := c.SFTPD.validate(); err != nil {
return err
}
}
if c.SMTP != nil {
if err := c.SMTP.validate(); err != nil {
return err
}
}
return nil
}
// PrepareForRendering prepares configs for rendering.
// It hides confidential data and set to nil the empty structs/secrets
// so they are not serialized
func (c *Configs) PrepareForRendering() {
if c.SFTPD != nil && c.SFTPD.isEmpty() {
c.SFTPD = nil
}
if c.SMTP != nil && c.SMTP.isEmpty() {
c.SMTP = nil
}
if c.SMTP != nil && c.SMTP.Password != nil {
c.SMTP.Password.Hide()
if c.SMTP.Password.IsEmpty() {
c.SMTP.Password = nil
}
}
}
// SetNilsToEmpty sets nil fields to empty
func (c *Configs) SetNilsToEmpty() {
if c.SFTPD == nil {
c.SFTPD = &SFTPDConfigs{}
}
if c.SMTP == nil {
c.SMTP = &SMTPConfigs{}
}
if c.SMTP.Password == nil {
c.SMTP.Password = kms.NewEmptySecret()
}
}
// RenderAsJSON implements the renderer interface used within plugins
func (c *Configs) RenderAsJSON(reload bool) ([]byte, error) {
if reload {
config, err := provider.getConfigs()
if err != nil {
providerLog(logger.LevelError, "unable to reload config overrides before rendering as json: %v", err)
return nil, err
}
config.PrepareForRendering()
return json.Marshal(config)
}
c.PrepareForRendering()
return json.Marshal(c)
}
func (c *Configs) getACopy() Configs {
var result Configs
if c.SFTPD != nil {
result.SFTPD = c.SFTPD.getACopy()
}
if c.SMTP != nil {
result.SMTP = c.SMTP.getACopy()
}
result.UpdatedAt = c.UpdatedAt
return result
}

View file

@ -192,6 +192,7 @@ var (
sqlTableNodes string sqlTableNodes string
sqlTableRoles string sqlTableRoles string
sqlTableIPLists string sqlTableIPLists string
sqlTableConfigs string
sqlTableSchemaVersion string sqlTableSchemaVersion string
argon2Params *argon2id.Params argon2Params *argon2id.Params
lastLoginMinDelay = 10 * time.Minute lastLoginMinDelay = 10 * time.Minute
@ -225,6 +226,7 @@ func initSQLTables() {
sqlTableNodes = "nodes" sqlTableNodes = "nodes"
sqlTableRoles = "roles" sqlTableRoles = "roles"
sqlTableIPLists = "ip_lists" sqlTableIPLists = "ip_lists"
sqlTableConfigs = "configurations"
sqlTableSchemaVersion = "schema_version" sqlTableSchemaVersion = "schema_version"
} }
@ -666,6 +668,7 @@ type BackupData struct {
EventRules []EventRule `json:"event_rules"` EventRules []EventRule `json:"event_rules"`
Roles []Role `json:"roles"` Roles []Role `json:"roles"`
IPLists []IPListEntry `json:"ip_lists"` IPLists []IPListEntry `json:"ip_lists"`
Configs *Configs `json:"configs"`
Version int `json:"version"` Version int `json:"version"`
} }
@ -817,6 +820,8 @@ type Provider interface {
dumpIPListEntries() ([]IPListEntry, error) dumpIPListEntries() ([]IPListEntry, error)
countIPListEntries(listType IPListType) (int64, error) countIPListEntries(listType IPListType) (int64, error)
getListEntriesForIP(ip string, listType IPListType) ([]IPListEntry, error) getListEntriesForIP(ip string, listType IPListType) ([]IPListEntry, error)
getConfigs() (Configs, error)
setConfigs(configs *Configs) error
checkAvailability() error checkAvailability() error
close() error close() error
reloadConfig() error reloadConfig() error
@ -997,17 +1002,18 @@ func validateSQLTablesPrefix() error {
sqlTableNodes = config.SQLTablesPrefix + sqlTableNodes sqlTableNodes = config.SQLTablesPrefix + sqlTableNodes
sqlTableRoles = config.SQLTablesPrefix + sqlTableRoles sqlTableRoles = config.SQLTablesPrefix + sqlTableRoles
sqlTableIPLists = config.SQLTablesPrefix + sqlTableIPLists sqlTableIPLists = config.SQLTablesPrefix + sqlTableIPLists
sqlTableConfigs = config.SQLTablesPrefix + sqlTableConfigs
sqlTableSchemaVersion = config.SQLTablesPrefix + sqlTableSchemaVersion sqlTableSchemaVersion = config.SQLTablesPrefix + sqlTableSchemaVersion
providerLog(logger.LevelDebug, "sql table for users %q, folders %q users folders mapping %q admins %q "+ providerLog(logger.LevelDebug, "sql table for users %q, folders %q users folders mapping %q admins %q "+
"api keys %q shares %q defender hosts %q defender events %q transfers %q groups %q "+ "api keys %q shares %q defender hosts %q defender events %q transfers %q groups %q "+
"users groups mapping %q admins groups mapping %q groups folders mapping %q shared sessions %q "+ "users groups mapping %q admins groups mapping %q groups folders mapping %q shared sessions %q "+
"schema version %q events actions %q events rules %q rules actions mapping %q tasks %q nodes %q roles %q"+ "schema version %q events actions %q events rules %q rules actions mapping %q tasks %q nodes %q roles %q"+
"ip lists %q", "ip lists %q configs %q",
sqlTableUsers, sqlTableFolders, sqlTableUsersFoldersMapping, sqlTableAdmins, sqlTableAPIKeys, sqlTableUsers, sqlTableFolders, sqlTableUsersFoldersMapping, sqlTableAdmins, sqlTableAPIKeys,
sqlTableShares, sqlTableDefenderHosts, sqlTableDefenderEvents, sqlTableActiveTransfers, sqlTableGroups, sqlTableShares, sqlTableDefenderHosts, sqlTableDefenderEvents, sqlTableActiveTransfers, sqlTableGroups,
sqlTableUsersGroupsMapping, sqlTableAdminsGroupsMapping, sqlTableGroupsFoldersMapping, sqlTableSharedSessions, sqlTableUsersGroupsMapping, sqlTableAdminsGroupsMapping, sqlTableGroupsFoldersMapping, sqlTableSharedSessions,
sqlTableSchemaVersion, sqlTableEventsActions, sqlTableEventsRules, sqlTableRulesActionsMapping, sqlTableSchemaVersion, sqlTableEventsActions, sqlTableEventsRules, sqlTableRulesActionsMapping,
sqlTableTasks, sqlTableNodes, sqlTableRoles, sqlTableIPLists) sqlTableTasks, sqlTableNodes, sqlTableRoles, sqlTableIPLists, sqlTableConfigs)
} }
return nil return nil
} }
@ -1518,6 +1524,25 @@ func GetUsedVirtualFolderQuota(name string) (int, int64, error) {
return files + delayedFiles, size + delayedSize, err return files + delayedFiles, size + delayedSize, err
} }
// GetConfigs returns the configurations
func GetConfigs() (Configs, error) {
return provider.getConfigs()
}
// UpdateConfigs updates configurations
func UpdateConfigs(configs *Configs, executor, ipAddress, role string) error {
if configs == nil {
configs = &Configs{}
} else {
configs.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
}
err := provider.setConfigs(configs)
if err == nil {
executeAction(operationUpdate, executor, ipAddress, actionObjectConfigs, "configs", role, configs)
}
return err
}
// AddShare adds a new share // AddShare adds a new share
func AddShare(share *Share, executor, ipAddress, role string) error { func AddShare(share *Share, executor, ipAddress, role string) error {
err := provider.addShare(share) err := provider.addShare(share)
@ -2306,6 +2331,10 @@ func DumpData() (BackupData, error) {
if err != nil { if err != nil {
return data, err return data, err
} }
configs, err := provider.getConfigs()
if err != nil {
return data, err
}
data.Users = users data.Users = users
data.Groups = groups data.Groups = groups
data.Folders = folders data.Folders = folders
@ -2316,6 +2345,7 @@ func DumpData() (BackupData, error) {
data.EventRules = rules data.EventRules = rules
data.Roles = roles data.Roles = roles
data.IPLists = ipLists data.IPLists = ipLists
data.Configs = &configs
data.Version = DumpVersion data.Version = DumpVersion
return data, err return data, err
} }

View file

@ -80,6 +80,8 @@ type memoryProviderHandle struct {
ipListEntries map[string]IPListEntry ipListEntries map[string]IPListEntry
// slice with ordered IP list entries // slice with ordered IP list entries
ipListEntriesKeys []string ipListEntriesKeys []string
// configurations
configs Configs
} }
// MemoryProvider defines the auth provider for a memory store // MemoryProvider defines the auth provider for a memory store
@ -118,6 +120,7 @@ func initializeMemoryProvider(basePath string) {
roleNames: []string{}, roleNames: []string{},
ipListEntries: map[string]IPListEntry{}, ipListEntries: map[string]IPListEntry{},
ipListEntriesKeys: []string{}, ipListEntriesKeys: []string{},
configs: Configs{},
configFile: configFile, configFile: configFile,
}, },
} }
@ -2797,6 +2800,28 @@ func (p *MemoryProvider) getListEntriesForIP(ip string, listType IPListType) ([]
return entries, nil return entries, nil
} }
func (p *MemoryProvider) getConfigs() (Configs, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return Configs{}, errMemoryProviderClosed
}
return p.dbHandle.configs.getACopy(), nil
}
func (p *MemoryProvider) setConfigs(configs *Configs) error {
if err := configs.validate(); err != nil {
return err
}
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
p.dbHandle.configs = configs.getACopy()
return nil
}
func (p *MemoryProvider) setFirstDownloadTimestamp(username string) error { func (p *MemoryProvider) setFirstDownloadTimestamp(username string) error {
p.dbHandle.Lock() p.dbHandle.Lock()
defer p.dbHandle.Unlock() defer p.dbHandle.Unlock()
@ -2929,6 +2954,7 @@ func (p *MemoryProvider) clear() {
p.dbHandle.roleNames = []string{} p.dbHandle.roleNames = []string{}
p.dbHandle.ipListEntries = map[string]IPListEntry{} p.dbHandle.ipListEntries = map[string]IPListEntry{}
p.dbHandle.ipListEntriesKeys = []string{} p.dbHandle.ipListEntriesKeys = []string{}
p.dbHandle.configs = Configs{}
} }
func (p *MemoryProvider) reloadConfig() error { func (p *MemoryProvider) reloadConfig() error {
@ -2968,7 +2994,11 @@ func (p *MemoryProvider) reloadConfig() error {
func (p *MemoryProvider) restoreDump(dump *BackupData) error { func (p *MemoryProvider) restoreDump(dump *BackupData) error {
p.clear() p.clear()
if err := p.restoreIPListEntries(*dump); err != nil { if err := p.restoreConfigs(dump); err != nil {
return err
}
if err := p.restoreIPListEntries(dump); err != nil {
return err return err
} }
@ -3130,7 +3160,14 @@ func (p *MemoryProvider) restoreAdmins(dump *BackupData) error {
return nil return nil
} }
func (p *MemoryProvider) restoreIPListEntries(dump BackupData) error { func (p *MemoryProvider) restoreConfigs(dump *BackupData) error {
if dump.Configs != nil && dump.Configs.UpdatedAt > 0 {
return UpdateConfigs(dump.Configs, ActionExecutorSystem, "", "")
}
return nil
}
func (p *MemoryProvider) restoreIPListEntries(dump *BackupData) error {
for idx := range dump.IPLists { for idx := range dump.IPLists {
entry := dump.IPLists[idx] entry := dump.IPLists[idx]
_, err := p.ipListEntryExists(entry.IPOrNet, entry.Type) _, err := p.ipListEntryExists(entry.IPOrNet, entry.Type)

View file

@ -59,6 +59,7 @@ const (
"DROP TABLE IF EXISTS `{{nodes}}` CASCADE;" + "DROP TABLE IF EXISTS `{{nodes}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{roles}}` CASCADE;" + "DROP TABLE IF EXISTS `{{roles}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{ip_lists}}` CASCADE;" + "DROP TABLE IF EXISTS `{{ip_lists}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{configs}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{schema_version}}` CASCADE;" "DROP TABLE IF EXISTS `{{schema_version}}` CASCADE;"
mysqlInitialSQL = "CREATE TABLE `{{schema_version}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `version` integer NOT NULL);" + mysqlInitialSQL = "CREATE TABLE `{{schema_version}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `version` integer NOT NULL);" +
"CREATE TABLE `{{admins}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `username` varchar(255) NOT NULL UNIQUE, " + "CREATE TABLE `{{admins}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `username` varchar(255) NOT NULL UNIQUE, " +
@ -202,6 +203,9 @@ const (
"CREATE INDEX `{{prefix}}ip_lists_deleted_at_idx` ON `{{ip_lists}}` (`deleted_at`);" + "CREATE INDEX `{{prefix}}ip_lists_deleted_at_idx` ON `{{ip_lists}}` (`deleted_at`);" +
"CREATE INDEX `{{prefix}}ip_lists_first_last_idx` ON `{{ip_lists}}` (`first`, `last`);" "CREATE INDEX `{{prefix}}ip_lists_first_last_idx` ON `{{ip_lists}}` (`first`, `last`);"
mysqlV27DownSQL = "DROP TABLE `{{ip_lists}}` CASCADE;" mysqlV27DownSQL = "DROP TABLE `{{ip_lists}}` CASCADE;"
mysqlV28SQL = "CREATE TABLE `{{configs}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `configs` longtext NOT NULL);" +
"INSERT INTO {{configs}} (configs) VALUES ('{}');"
mysqlV28DownSQL = "DROP TABLE `{{configs}}` CASCADE;"
) )
// MySQLProvider defines the auth provider for MySQL/MariaDB database // MySQLProvider defines the auth provider for MySQL/MariaDB database
@ -745,6 +749,14 @@ func (p *MySQLProvider) getListEntriesForIP(ip string, listType IPListType) ([]I
return sqlCommonGetListEntriesForIP(ip, listType, p.dbHandle) return sqlCommonGetListEntriesForIP(ip, listType, p.dbHandle)
} }
func (p *MySQLProvider) getConfigs() (Configs, error) {
return sqlCommonGetConfigs(p.dbHandle)
}
func (p *MySQLProvider) setConfigs(configs *Configs) error {
return sqlCommonSetConfigs(configs, p.dbHandle)
}
func (p *MySQLProvider) setFirstDownloadTimestamp(username string) error { func (p *MySQLProvider) setFirstDownloadTimestamp(username string) error {
return sqlCommonSetFirstDownloadTimestamp(username, p.dbHandle) return sqlCommonSetFirstDownloadTimestamp(username, p.dbHandle)
} }
@ -800,6 +812,8 @@ func (p *MySQLProvider) migrateDatabase() error { //nolint:dupl
return updateMySQLDatabaseFromV25(p.dbHandle) return updateMySQLDatabaseFromV25(p.dbHandle)
case version == 26: case version == 26:
return updateMySQLDatabaseFromV26(p.dbHandle) return updateMySQLDatabaseFromV26(p.dbHandle)
case version == 27:
return updateMySQLDatabaseFromV27(p.dbHandle)
default: default:
if version > sqlDatabaseVersion { if version > sqlDatabaseVersion {
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version, providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
@ -830,6 +844,8 @@ func (p *MySQLProvider) revertDatabase(targetVersion int) error {
return downgradeMySQLDatabaseFromV26(p.dbHandle) return downgradeMySQLDatabaseFromV26(p.dbHandle)
case 27: case 27:
return downgradeMySQLDatabaseFromV27(p.dbHandle) return downgradeMySQLDatabaseFromV27(p.dbHandle)
case 28:
return downgradeMySQLDatabaseFromV28(p.dbHandle)
default: default:
return fmt.Errorf("database schema version not handled: %d", dbVersion.Version) return fmt.Errorf("database schema version not handled: %d", dbVersion.Version)
} }
@ -862,7 +878,14 @@ func updateMySQLDatabaseFromV25(dbHandle *sql.DB) error {
} }
func updateMySQLDatabaseFromV26(dbHandle *sql.DB) error { func updateMySQLDatabaseFromV26(dbHandle *sql.DB) error {
return updateMySQLDatabaseFrom26To27(dbHandle) if err := updateMySQLDatabaseFrom26To27(dbHandle); err != nil {
return err
}
return updateMySQLDatabaseFromV27(dbHandle)
}
func updateMySQLDatabaseFromV27(dbHandle *sql.DB) error {
return updateMySQLDatabaseFrom27To28(dbHandle)
} }
func downgradeMySQLDatabaseFromV24(dbHandle *sql.DB) error { func downgradeMySQLDatabaseFromV24(dbHandle *sql.DB) error {
@ -890,6 +913,13 @@ func downgradeMySQLDatabaseFromV27(dbHandle *sql.DB) error {
return downgradeMySQLDatabaseFromV26(dbHandle) return downgradeMySQLDatabaseFromV26(dbHandle)
} }
func downgradeMySQLDatabaseFromV28(dbHandle *sql.DB) error {
if err := downgradeMySQLDatabaseFrom28To27(dbHandle); err != nil {
return err
}
return downgradeMySQLDatabaseFromV27(dbHandle)
}
func updateMySQLDatabaseFrom23To24(dbHandle *sql.DB) error { func updateMySQLDatabaseFrom23To24(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database schema version: 23 -> 24") logger.InfoToConsole("updating database schema version: 23 -> 24")
providerLog(logger.LevelInfo, "updating database schema version: 23 -> 24") providerLog(logger.LevelInfo, "updating database schema version: 23 -> 24")
@ -922,6 +952,13 @@ func updateMySQLDatabaseFrom26To27(dbHandle *sql.DB) error {
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 27, true) return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 27, true)
} }
func updateMySQLDatabaseFrom27To28(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database schema version: 27 -> 28")
providerLog(logger.LevelInfo, "updating database schema version: 27 -> 28")
sql := strings.ReplaceAll(mysqlV28SQL, "{{configs}}", sqlTableConfigs)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 28, true)
}
func downgradeMySQLDatabaseFrom24To23(dbHandle *sql.DB) error { func downgradeMySQLDatabaseFrom24To23(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database schema version: 24 -> 23") logger.InfoToConsole("downgrading database schema version: 24 -> 23")
providerLog(logger.LevelInfo, "downgrading database schema version: 24 -> 23") providerLog(logger.LevelInfo, "downgrading database schema version: 24 -> 23")
@ -952,3 +989,10 @@ func downgradeMySQLDatabaseFrom27To26(dbHandle *sql.DB) error {
sql := strings.ReplaceAll(mysqlV27DownSQL, "{{ip_lists}}", sqlTableIPLists) sql := strings.ReplaceAll(mysqlV27DownSQL, "{{ip_lists}}", sqlTableIPLists)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 26, false) return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 26, false)
} }
func downgradeMySQLDatabaseFrom28To27(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database schema version: 28 -> 27")
providerLog(logger.LevelInfo, "downgrading database schema version: 28 -> 27")
sql := strings.ReplaceAll(mysqlV28DownSQL, "{{configs}}", sqlTableConfigs)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 27, false)
}

View file

@ -57,6 +57,7 @@ DROP TABLE IF EXISTS "{{tasks}}" CASCADE;
DROP TABLE IF EXISTS "{{nodes}}" CASCADE; DROP TABLE IF EXISTS "{{nodes}}" CASCADE;
DROP TABLE IF EXISTS "{{roles}}" CASCADE; DROP TABLE IF EXISTS "{{roles}}" CASCADE;
DROP TABLE IF EXISTS "{{ip_lists}}" CASCADE; DROP TABLE IF EXISTS "{{ip_lists}}" CASCADE;
DROP TABLE IF EXISTS "{{configs}}" CASCADE;
DROP TABLE IF EXISTS "{{schema_version}}" CASCADE; DROP TABLE IF EXISTS "{{schema_version}}" CASCADE;
` `
pgsqlInitial = `CREATE TABLE "{{schema_version}}" ("id" serial NOT NULL PRIMARY KEY, "version" integer NOT NULL); pgsqlInitial = `CREATE TABLE "{{schema_version}}" ("id" serial NOT NULL PRIMARY KEY, "version" integer NOT NULL);
@ -216,6 +217,10 @@ CREATE INDEX "{{prefix}}ip_lists_deleted_at_idx" ON "{{ip_lists}}" ("deleted_at"
CREATE INDEX "{{prefix}}ip_lists_first_last_idx" ON "{{ip_lists}}" ("first", "last"); CREATE INDEX "{{prefix}}ip_lists_first_last_idx" ON "{{ip_lists}}" ("first", "last");
` `
pgsqlV27DownSQL = `DROP TABLE "{{ip_lists}}" CASCADE;` pgsqlV27DownSQL = `DROP TABLE "{{ip_lists}}" CASCADE;`
pgsqlV28SQL = `CREATE TABLE "{{configs}}" ("id" serial NOT NULL PRIMARY KEY, "configs" text NOT NULL);
INSERT INTO {{configs}} (configs) VALUES ('{}');
`
pgsqlV28DownSQL = `DROP TABLE "{{configs}}" CASCADE;`
) )
// PGSQLProvider defines the auth provider for PostgreSQL database // PGSQLProvider defines the auth provider for PostgreSQL database
@ -718,6 +723,14 @@ func (p *PGSQLProvider) getListEntriesForIP(ip string, listType IPListType) ([]I
return sqlCommonGetListEntriesForIP(ip, listType, p.dbHandle) return sqlCommonGetListEntriesForIP(ip, listType, p.dbHandle)
} }
func (p *PGSQLProvider) getConfigs() (Configs, error) {
return sqlCommonGetConfigs(p.dbHandle)
}
func (p *PGSQLProvider) setConfigs(configs *Configs) error {
return sqlCommonSetConfigs(configs, p.dbHandle)
}
func (p *PGSQLProvider) setFirstDownloadTimestamp(username string) error { func (p *PGSQLProvider) setFirstDownloadTimestamp(username string) error {
return sqlCommonSetFirstDownloadTimestamp(username, p.dbHandle) return sqlCommonSetFirstDownloadTimestamp(username, p.dbHandle)
} }
@ -773,6 +786,8 @@ func (p *PGSQLProvider) migrateDatabase() error { //nolint:dupl
return updatePgSQLDatabaseFromV25(p.dbHandle) return updatePgSQLDatabaseFromV25(p.dbHandle)
case version == 26: case version == 26:
return updatePgSQLDatabaseFromV26(p.dbHandle) return updatePgSQLDatabaseFromV26(p.dbHandle)
case version == 27:
return updatePgSQLDatabaseFromV27(p.dbHandle)
default: default:
if version > sqlDatabaseVersion { if version > sqlDatabaseVersion {
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version, providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
@ -803,6 +818,8 @@ func (p *PGSQLProvider) revertDatabase(targetVersion int) error {
return downgradePgSQLDatabaseFromV26(p.dbHandle) return downgradePgSQLDatabaseFromV26(p.dbHandle)
case 27: case 27:
return downgradePgSQLDatabaseFromV27(p.dbHandle) return downgradePgSQLDatabaseFromV27(p.dbHandle)
case 28:
return downgradePgSQLDatabaseFromV28(p.dbHandle)
default: default:
return fmt.Errorf("database schema version not handled: %d", dbVersion.Version) return fmt.Errorf("database schema version not handled: %d", dbVersion.Version)
} }
@ -835,7 +852,14 @@ func updatePgSQLDatabaseFromV25(dbHandle *sql.DB) error {
} }
func updatePgSQLDatabaseFromV26(dbHandle *sql.DB) error { func updatePgSQLDatabaseFromV26(dbHandle *sql.DB) error {
return updatePgSQLDatabaseFrom26To27(dbHandle) if err := updatePgSQLDatabaseFrom26To27(dbHandle); err != nil {
return err
}
return updatePgSQLDatabaseFromV27(dbHandle)
}
func updatePgSQLDatabaseFromV27(dbHandle *sql.DB) error {
return updatePgSQLDatabaseFrom27To28(dbHandle)
} }
func downgradePgSQLDatabaseFromV24(dbHandle *sql.DB) error { func downgradePgSQLDatabaseFromV24(dbHandle *sql.DB) error {
@ -863,6 +887,13 @@ func downgradePgSQLDatabaseFromV27(dbHandle *sql.DB) error {
return downgradePgSQLDatabaseFromV26(dbHandle) return downgradePgSQLDatabaseFromV26(dbHandle)
} }
func downgradePgSQLDatabaseFromV28(dbHandle *sql.DB) error {
if err := downgradePgSQLDatabaseFrom28To27(dbHandle); err != nil {
return err
}
return downgradePgSQLDatabaseFromV27(dbHandle)
}
func updatePgSQLDatabaseFrom23To24(dbHandle *sql.DB) error { func updatePgSQLDatabaseFrom23To24(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database schema version: 23 -> 24") logger.InfoToConsole("updating database schema version: 23 -> 24")
providerLog(logger.LevelInfo, "updating database schema version: 23 -> 24") providerLog(logger.LevelInfo, "updating database schema version: 23 -> 24")
@ -907,6 +938,13 @@ func updatePgSQLDatabaseFrom26To27(dbHandle *sql.DB) error {
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 27, true) return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 27, true)
} }
func updatePgSQLDatabaseFrom27To28(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database schema version: 27 -> 28")
providerLog(logger.LevelInfo, "updating database schema version: 27 -> 28")
sql := strings.ReplaceAll(pgsqlV28SQL, "{{configs}}", sqlTableConfigs)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 28, true)
}
func downgradePgSQLDatabaseFrom24To23(dbHandle *sql.DB) error { func downgradePgSQLDatabaseFrom24To23(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database schema version: 24 -> 23") logger.InfoToConsole("downgrading database schema version: 24 -> 23")
providerLog(logger.LevelInfo, "downgrading database schema version: 24 -> 23") providerLog(logger.LevelInfo, "downgrading database schema version: 24 -> 23")
@ -937,3 +975,10 @@ func downgradePgSQLDatabaseFrom27To26(dbHandle *sql.DB) error {
sql := strings.ReplaceAll(pgsqlV27DownSQL, "{{ip_lists}}", sqlTableIPLists) sql := strings.ReplaceAll(pgsqlV27DownSQL, "{{ip_lists}}", sqlTableIPLists)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 26, false) return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 26, false)
} }
func downgradePgSQLDatabaseFrom28To27(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database schema version: 28 -> 27")
providerLog(logger.LevelInfo, "downgrading database schema version: 28 -> 27")
sql := strings.ReplaceAll(pgsqlV28DownSQL, "{{configs}}", sqlTableConfigs)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 27, false)
}

View file

@ -35,7 +35,7 @@ import (
) )
const ( const (
sqlDatabaseVersion = 27 sqlDatabaseVersion = 28
defaultSQLQueryTimeout = 10 * time.Second defaultSQLQueryTimeout = 10 * time.Second
longSQLQueryTimeout = 60 * time.Second longSQLQueryTimeout = 60 * time.Second
) )
@ -81,6 +81,7 @@ func sqlReplaceAll(sql string) string {
sql = strings.ReplaceAll(sql, "{{nodes}}", sqlTableNodes) sql = strings.ReplaceAll(sql, "{{nodes}}", sqlTableNodes)
sql = strings.ReplaceAll(sql, "{{roles}}", sqlTableRoles) sql = strings.ReplaceAll(sql, "{{roles}}", sqlTableRoles)
sql = strings.ReplaceAll(sql, "{{ip_lists}}", sqlTableIPLists) sql = strings.ReplaceAll(sql, "{{ip_lists}}", sqlTableIPLists)
sql = strings.ReplaceAll(sql, "{{configs}}", sqlTableConfigs)
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix) sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
return sql return sql
} }
@ -3825,6 +3826,43 @@ func sqlCommonCleanupNodes(dbHandle *sql.DB) error {
return err return err
} }
func sqlCommonGetConfigs(dbHandle sqlQuerier) (Configs, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
var result Configs
var configs []byte
q := getConfigsQuery()
err := dbHandle.QueryRowContext(ctx, q).Scan(&configs)
if err != nil {
return result, err
}
err = json.Unmarshal(configs, &result)
return result, err
}
func sqlCommonSetConfigs(configs *Configs, dbHandle *sql.DB) error {
if err := configs.validate(); err != nil {
return err
}
asJSON, err := json.Marshal(configs)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getUpdateConfigsQuery()
res, err := dbHandle.ExecContext(ctx, q, asJSON)
if err != nil {
return err
}
if config.Driver == MySQLDataProviderName {
return nil
}
return sqlCommonRequireRowAffected(res)
}
func sqlCommonGetDatabaseVersion(dbHandle sqlQuerier, showInitWarn bool) (schemaVersion, error) { func sqlCommonGetDatabaseVersion(dbHandle sqlQuerier, showInitWarn bool) (schemaVersion, error) {
var result schemaVersion var result schemaVersion
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)

View file

@ -58,6 +58,7 @@ DROP TABLE IF EXISTS "{{events_actions}}";
DROP TABLE IF EXISTS "{{tasks}}"; DROP TABLE IF EXISTS "{{tasks}}";
DROP TABLE IF EXISTS "{{roles}}"; DROP TABLE IF EXISTS "{{roles}}";
DROP TABLE IF EXISTS "{{ip_lists}}"; DROP TABLE IF EXISTS "{{ip_lists}}";
DROP TABLE IF EXISTS "{{configs}}";
DROP TABLE IF EXISTS "{{schema_version}}"; DROP TABLE IF EXISTS "{{schema_version}}";
` `
sqliteInitialSQL = `CREATE TABLE "{{schema_version}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "version" integer NOT NULL); sqliteInitialSQL = `CREATE TABLE "{{schema_version}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "version" integer NOT NULL);
@ -193,6 +194,10 @@ CREATE INDEX "{{prefix}}ip_lists_ip_deleted_at_idx" ON "{{ip_lists}}" ("deleted_
CREATE INDEX "{{prefix}}ip_lists_first_last_idx" ON "{{ip_lists}}" ("first", "last"); CREATE INDEX "{{prefix}}ip_lists_first_last_idx" ON "{{ip_lists}}" ("first", "last");
` `
sqliteV27DownSQL = `DROP TABLE "{{ip_lists}}";` sqliteV27DownSQL = `DROP TABLE "{{ip_lists}}";`
sqliteV28SQL = `CREATE TABLE "{{configs}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "configs" text NOT NULL);
INSERT INTO {{configs}} (configs) VALUES ('{}');
`
sqliteV28DownSQL = `DROP TABLE "{{configs}}";`
) )
// SQLiteProvider defines the auth provider for SQLite database // SQLiteProvider defines the auth provider for SQLite database
@ -674,6 +679,14 @@ func (p *SQLiteProvider) getListEntriesForIP(ip string, listType IPListType) ([]
return sqlCommonGetListEntriesForIP(ip, listType, p.dbHandle) return sqlCommonGetListEntriesForIP(ip, listType, p.dbHandle)
} }
func (p *SQLiteProvider) getConfigs() (Configs, error) {
return sqlCommonGetConfigs(p.dbHandle)
}
func (p *SQLiteProvider) setConfigs(configs *Configs) error {
return sqlCommonSetConfigs(configs, p.dbHandle)
}
func (p *SQLiteProvider) setFirstDownloadTimestamp(username string) error { func (p *SQLiteProvider) setFirstDownloadTimestamp(username string) error {
return sqlCommonSetFirstDownloadTimestamp(username, p.dbHandle) return sqlCommonSetFirstDownloadTimestamp(username, p.dbHandle)
} }
@ -728,6 +741,8 @@ func (p *SQLiteProvider) migrateDatabase() error { //nolint:dupl
return updateSQLiteDatabaseFromV25(p.dbHandle) return updateSQLiteDatabaseFromV25(p.dbHandle)
case version == 26: case version == 26:
return updateSQLiteDatabaseFromV26(p.dbHandle) return updateSQLiteDatabaseFromV26(p.dbHandle)
case version == 27:
return updateSQLiteDatabaseFromV27(p.dbHandle)
default: default:
if version > sqlDatabaseVersion { if version > sqlDatabaseVersion {
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version, providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
@ -758,6 +773,8 @@ func (p *SQLiteProvider) revertDatabase(targetVersion int) error {
return downgradeSQLiteDatabaseFromV26(p.dbHandle) return downgradeSQLiteDatabaseFromV26(p.dbHandle)
case 27: case 27:
return downgradeSQLiteDatabaseFromV27(p.dbHandle) return downgradeSQLiteDatabaseFromV27(p.dbHandle)
case 28:
return downgradeSQLiteDatabaseFromV28(p.dbHandle)
default: default:
return fmt.Errorf("database schema version not handled: %d", dbVersion.Version) return fmt.Errorf("database schema version not handled: %d", dbVersion.Version)
} }
@ -790,7 +807,14 @@ func updateSQLiteDatabaseFromV25(dbHandle *sql.DB) error {
} }
func updateSQLiteDatabaseFromV26(dbHandle *sql.DB) error { func updateSQLiteDatabaseFromV26(dbHandle *sql.DB) error {
return updateSQLiteDatabaseFrom26To27(dbHandle) if err := updateSQLiteDatabaseFrom26To27(dbHandle); err != nil {
return err
}
return updateSQLiteDatabaseFromV27(dbHandle)
}
func updateSQLiteDatabaseFromV27(dbHandle *sql.DB) error {
return updateSQLiteDatabaseFrom27To28(dbHandle)
} }
func downgradeSQLiteDatabaseFromV24(dbHandle *sql.DB) error { func downgradeSQLiteDatabaseFromV24(dbHandle *sql.DB) error {
@ -818,6 +842,13 @@ func downgradeSQLiteDatabaseFromV27(dbHandle *sql.DB) error {
return downgradeSQLiteDatabaseFromV26(dbHandle) return downgradeSQLiteDatabaseFromV26(dbHandle)
} }
func downgradeSQLiteDatabaseFromV28(dbHandle *sql.DB) error {
if err := downgradeSQLiteDatabaseFrom28To27(dbHandle); err != nil {
return err
}
return downgradeSQLiteDatabaseFromV27(dbHandle)
}
func updateSQLiteDatabaseFrom23To24(dbHandle *sql.DB) error { func updateSQLiteDatabaseFrom23To24(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database schema version: 23 -> 24") logger.InfoToConsole("updating database schema version: 23 -> 24")
providerLog(logger.LevelInfo, "updating database schema version: 23 -> 24") providerLog(logger.LevelInfo, "updating database schema version: 23 -> 24")
@ -850,6 +881,13 @@ func updateSQLiteDatabaseFrom26To27(dbHandle *sql.DB) error {
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 27, true) return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 27, true)
} }
func updateSQLiteDatabaseFrom27To28(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database schema version: 27 -> 28")
providerLog(logger.LevelInfo, "updating database schema version: 27 -> 28")
sql := strings.ReplaceAll(sqliteV28SQL, "{{configs}}", sqlTableConfigs)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 28, true)
}
func downgradeSQLiteDatabaseFrom24To23(dbHandle *sql.DB) error { func downgradeSQLiteDatabaseFrom24To23(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database schema version: 24 -> 23") logger.InfoToConsole("downgrading database schema version: 24 -> 23")
providerLog(logger.LevelInfo, "downgrading database schema version: 24 -> 23") providerLog(logger.LevelInfo, "downgrading database schema version: 24 -> 23")
@ -881,6 +919,13 @@ func downgradeSQLiteDatabaseFrom27To26(dbHandle *sql.DB) error {
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 26, false) return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 26, false)
} }
func downgradeSQLiteDatabaseFrom28To27(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database schema version: 28 -> 27")
providerLog(logger.LevelInfo, "downgrading database schema version: 28 -> 27")
sql := strings.ReplaceAll(sqliteV28DownSQL, "{{configs}}", sqlTableConfigs)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 27, false)
}
/*func setPragmaFK(dbHandle *sql.DB, value string) error { /*func setPragmaFK(dbHandle *sql.DB, value string) error {
ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout) ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)
defer cancel() defer cancel()

View file

@ -274,6 +274,14 @@ func getRemoveSoftDeletedIPListEntryQuery() string {
sqlTableIPLists, sqlPlaceholders[0], sqlPlaceholders[1]) sqlTableIPLists, sqlPlaceholders[0], sqlPlaceholders[1])
} }
func getConfigsQuery() string {
return fmt.Sprintf(`SELECT configs FROM %s LIMIT 1`, sqlTableConfigs)
}
func getUpdateConfigsQuery() string {
return fmt.Sprintf(`UPDATE %s SET configs = %s`, sqlTableConfigs, sqlPlaceholders[0])
}
func getRoleByNameQuery() string { func getRoleByNameQuery() string {
return fmt.Sprintf(`SELECT %s FROM %s WHERE name = %s`, selectRoleFields, sqlTableRoles, return fmt.Sprintf(`SELECT %s FROM %s WHERE name = %s`, selectRoleFields, sqlTableRoles,
sqlPlaceholders[0]) sqlPlaceholders[0])

View file

@ -0,0 +1,57 @@
// Copyright (C) 2019-2023 Nicola Murino
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package httpd
import (
"net/http"
"github.com/go-chi/render"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/smtp"
)
type smtpTestRequest struct {
smtp.Config
Recipient string `json:"recipient"`
}
func testSMTPConfig(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
var req smtpTestRequest
err := render.DecodeJSON(r.Body, &req)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
if req.Password == redactedSecret {
configs, err := dataprovider.GetConfigs()
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
return
}
configs.SetNilsToEmpty()
if err := configs.SMTP.Password.TryDecrypt(); err == nil {
req.Password = configs.SMTP.Password.GetPayload()
}
}
if err := req.SendEmail([]string{req.Recipient}, "SFTPGo - Testing Email Settings",
"It appears your SFTPGo email is setup correctly!", smtp.EmailContentTypeTextPlain); err != nil {
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
return
}
sendAPIResponse(w, r, nil, "SMTP connection OK", http.StatusOK)
}

View file

@ -183,6 +183,10 @@ func restoreBackup(content []byte, inputFile string, scanQuota, mode int, execut
return util.NewValidationError(fmt.Sprintf("unable to parse backup content: %v", err)) return util.NewValidationError(fmt.Sprintf("unable to parse backup content: %v", err))
} }
if err = RestoreConfigs(dump.Configs, inputFile, mode, executor, ipAddress, role); err != nil {
return err
}
if err = RestoreIPListEntries(dump.IPLists, inputFile, mode, executor, ipAddress, role); err != nil { if err = RestoreIPListEntries(dump.IPLists, inputFile, mode, executor, ipAddress, role); err != nil {
return err return err
} }
@ -222,9 +226,7 @@ func restoreBackup(content []byte, inputFile string, scanQuota, mode int, execut
if err = RestoreEventRules(dump.EventRules, inputFile, mode, executor, ipAddress, role, dump.Version); err != nil { if err = RestoreEventRules(dump.EventRules, inputFile, mode, executor, ipAddress, role, dump.Version); err != nil {
return err return err
} }
logger.Debug(logSender, "", "backup restored")
logger.Debug(logSender, "", "backup restored, users: %d, folders: %d, admins: %d",
len(dump.Users), len(dump.Folders), len(dump.Admins))
return nil return nil
} }
@ -420,6 +422,26 @@ func RestoreAdmins(admins []dataprovider.Admin, inputFile string, mode int, exec
return nil return nil
} }
// RestoreConfigs restores the specified provider configs
func RestoreConfigs(configs *dataprovider.Configs, inputFile string, mode int, executor, ipAddress,
executorRole string,
) error {
if configs == nil {
return nil
}
c, err := dataprovider.GetConfigs()
if err != nil {
return fmt.Errorf("unable to restore configs, error loading existing from db: %w", err)
}
if c.UpdatedAt > 0 {
if mode == 1 {
logger.Debug(logSender, "", "loaddata mode 1, existing configs not updated")
return nil
}
}
return dataprovider.UpdateConfigs(configs, executor, ipAddress, executorRole)
}
// RestoreIPListEntries restores the specified IP list entries // RestoreIPListEntries restores the specified IP list entries
func RestoreIPListEntries(entries []dataprovider.IPListEntry, inputFile string, mode int, executor, ipAddress, func RestoreIPListEntries(entries []dataprovider.IPListEntry, inputFile string, mode int, executor, ipAddress,
executorRole string, executorRole string,

View file

@ -146,6 +146,7 @@ const (
webEventsPathDefault = "/web/admin/events" webEventsPathDefault = "/web/admin/events"
webEventsFsSearchPathDefault = "/web/admin/events/fs" webEventsFsSearchPathDefault = "/web/admin/events/fs"
webEventsProviderSearchPathDefault = "/web/admin/events/provider" webEventsProviderSearchPathDefault = "/web/admin/events/provider"
webConfigsPathDefault = "/web/admin/configs"
webClientLoginPathDefault = "/web/client/login" webClientLoginPathDefault = "/web/client/login"
webClientOIDCLoginPathDefault = "/web/client/oidclogin" webClientOIDCLoginPathDefault = "/web/client/oidclogin"
webClientTwoFactorPathDefault = "/web/client/twofactor" webClientTwoFactorPathDefault = "/web/client/twofactor"
@ -239,6 +240,7 @@ var (
webEventsPath string webEventsPath string
webEventsFsSearchPath string webEventsFsSearchPath string
webEventsProviderSearchPath string webEventsProviderSearchPath string
webConfigsPath string
webDefenderHostsPath string webDefenderHostsPath string
webClientLoginPath string webClientLoginPath string
webClientOIDCLoginPath string webClientOIDCLoginPath string
@ -1069,6 +1071,7 @@ func updateWebAdminURLs(baseURL string) {
webEventsPath = path.Join(baseURL, webEventsPathDefault) webEventsPath = path.Join(baseURL, webEventsPathDefault)
webEventsFsSearchPath = path.Join(baseURL, webEventsFsSearchPathDefault) webEventsFsSearchPath = path.Join(baseURL, webEventsFsSearchPathDefault)
webEventsProviderSearchPath = path.Join(baseURL, webEventsProviderSearchPathDefault) webEventsProviderSearchPath = path.Join(baseURL, webEventsProviderSearchPathDefault)
webConfigsPath = path.Join(baseURL, webConfigsPathDefault)
webStaticFilesPath = path.Join(baseURL, webStaticFilesPathDefault) webStaticFilesPath = path.Join(baseURL, webStaticFilesPathDefault)
webOpenAPIPath = path.Join(baseURL, webOpenAPIPathDefault) webOpenAPIPath = path.Join(baseURL, webOpenAPIPathDefault)
} }

View file

@ -54,6 +54,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/ssh"
"golang.org/x/net/html" "golang.org/x/net/html"
"github.com/drakkan/sftpgo/v2/internal/common" "github.com/drakkan/sftpgo/v2/internal/common"
@ -167,6 +168,7 @@ const (
webAdminRolesPath = "/web/admin/roles" webAdminRolesPath = "/web/admin/roles"
webAdminRolePath = "/web/admin/role" webAdminRolePath = "/web/admin/role"
webEventsPath = "/web/admin/events" webEventsPath = "/web/admin/events"
webConfigsPath = "/web/admin/configs"
webBasePathClient = "/web/client" webBasePathClient = "/web/client"
webClientLoginPath = "/web/client/login" webClientLoginPath = "/web/client/login"
webClientFilesPath = "/web/client/files" webClientFilesPath = "/web/client/files"
@ -1288,6 +1290,46 @@ func TestGroupSettingsOverride(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestConfigs(t *testing.T) {
err := dataprovider.UpdateConfigs(nil, "", "", "")
assert.NoError(t, err)
configs, err := dataprovider.GetConfigs()
assert.NoError(t, err)
assert.Equal(t, int64(0), configs.UpdatedAt)
assert.Nil(t, configs.SFTPD)
assert.Nil(t, configs.SMTP)
configs = dataprovider.Configs{
SFTPD: &dataprovider.SFTPDConfigs{},
SMTP: &dataprovider.SMTPConfigs{},
}
err = dataprovider.UpdateConfigs(&configs, "", "", "")
assert.NoError(t, err)
configs, err = dataprovider.GetConfigs()
assert.NoError(t, err)
assert.Greater(t, configs.UpdatedAt, int64(0))
configs = dataprovider.Configs{
SFTPD: &dataprovider.SFTPDConfigs{
Ciphers: []string{"unknown"},
},
SMTP: &dataprovider.SMTPConfigs{},
}
err = dataprovider.UpdateConfigs(&configs, "", "", "")
assert.ErrorIs(t, err, util.ErrValidation)
configs = dataprovider.Configs{
SFTPD: &dataprovider.SFTPDConfigs{},
SMTP: &dataprovider.SMTPConfigs{
Host: "smtp.example.com",
Port: -1,
},
}
err = dataprovider.UpdateConfigs(&configs, "", "", "")
assert.ErrorIs(t, err, util.ErrValidation)
err = dataprovider.UpdateConfigs(nil, "", "", "")
assert.NoError(t, err)
}
func TestBasicIPListEntriesHandling(t *testing.T) { func TestBasicIPListEntriesHandling(t *testing.T) {
entry := dataprovider.IPListEntry{ entry := dataprovider.IPListEntry{
IPOrNet: "::ffff:12.34.56.78", IPOrNet: "::ffff:12.34.56.78",
@ -6782,6 +6824,7 @@ func TestProviderErrors(t *testing.T) {
backupData := dataprovider.BackupData{ backupData := dataprovider.BackupData{
Version: dataprovider.DumpVersion, Version: dataprovider.DumpVersion,
} }
backupData.Configs = &dataprovider.Configs{}
backupData.Users = append(backupData.Users, user) backupData.Users = append(backupData.Users, user)
backupContent, err := json.Marshal(backupData) backupContent, err := json.Marshal(backupData)
assert.NoError(t, err) assert.NoError(t, err)
@ -6790,6 +6833,13 @@ func TestProviderErrors(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
_, _, err = httpdtest.Loaddata(backupFilePath, "", "", http.StatusInternalServerError) _, _, err = httpdtest.Loaddata(backupFilePath, "", "", http.StatusInternalServerError)
assert.NoError(t, err) assert.NoError(t, err)
backupData.Configs = nil
backupContent, err = json.Marshal(backupData)
assert.NoError(t, err)
err = os.WriteFile(backupFilePath, backupContent, os.ModePerm)
assert.NoError(t, err)
_, _, err = httpdtest.Loaddata(backupFilePath, "", "", http.StatusInternalServerError)
assert.NoError(t, err)
backupData.Folders = append(backupData.Folders, vfs.BaseVirtualFolder{Name: "testFolder", MappedPath: filepath.Clean(os.TempDir())}) backupData.Folders = append(backupData.Folders, vfs.BaseVirtualFolder{Name: "testFolder", MappedPath: filepath.Clean(os.TempDir())})
backupContent, err = json.Marshal(backupData) backupContent, err = json.Marshal(backupData)
assert.NoError(t, err) assert.NoError(t, err)
@ -7478,6 +7528,8 @@ func TestLoaddataFromPostBody(t *testing.T) {
} }
func TestLoaddata(t *testing.T) { func TestLoaddata(t *testing.T) {
err := dataprovider.UpdateConfigs(nil, "", "", "")
assert.NoError(t, err)
mappedPath := filepath.Join(os.TempDir(), "restored_folder") mappedPath := filepath.Join(os.TempDir(), "restored_folder")
folderName := filepath.Base(mappedPath) folderName := filepath.Base(mappedPath)
folderDesc := "restored folder desc" folderDesc := "restored folder desc"
@ -7567,9 +7619,20 @@ func TestLoaddata(t *testing.T) {
}, },
}, },
} }
configs := dataprovider.Configs{
SFTPD: &dataprovider.SFTPDConfigs{
HostKeyAlgos: []string{ssh.KeyAlgoRSA, ssh.CertAlgoRSAv01},
},
SMTP: &dataprovider.SMTPConfigs{
Host: "mail.example.com",
Port: 587,
From: "from@example.net",
},
}
backupData := dataprovider.BackupData{ backupData := dataprovider.BackupData{
Version: 14, Version: 14,
} }
backupData.Configs = &configs
backupData.Users = append(backupData.Users, user) backupData.Users = append(backupData.Users, user)
backupData.Roles = append(backupData.Roles, role) backupData.Roles = append(backupData.Roles, role)
backupData.Groups = append(backupData.Groups, group) backupData.Groups = append(backupData.Groups, group)
@ -7621,6 +7684,15 @@ func TestLoaddata(t *testing.T) {
// update from backup // update from backup
_, _, err = httpdtest.Loaddata(backupFilePath, "2", "", http.StatusOK) _, _, err = httpdtest.Loaddata(backupFilePath, "2", "", http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
configsGet, err := dataprovider.GetConfigs()
assert.NoError(t, err)
assert.Equal(t, configs.SMTP, configsGet.SMTP)
assert.Equal(t, configs.SFTPD.HostKeyAlgos, configsGet.SFTPD.HostKeyAlgos)
assert.Len(t, configsGet.SFTPD.Moduli, 0)
assert.Len(t, configsGet.SFTPD.KexAlgorithms, 0)
assert.Len(t, configsGet.SFTPD.Ciphers, 0)
assert.Len(t, configsGet.SFTPD.MACs, 0)
assert.Greater(t, configsGet.UpdatedAt, int64(0))
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, user.VirtualFolders, 1) assert.Len(t, user.VirtualFolders, 1)
@ -7751,11 +7823,20 @@ func TestLoaddata(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
err = os.Remove(backupFilePath) err = os.Remove(backupFilePath)
assert.NoError(t, err) assert.NoError(t, err)
err = dataprovider.UpdateConfigs(nil, "", "", "")
assert.NoError(t, err)
} }
func TestLoaddataMode(t *testing.T) { func TestLoaddataMode(t *testing.T) {
err := dataprovider.UpdateConfigs(nil, "", "", "")
assert.NoError(t, err)
mappedPath := filepath.Join(os.TempDir(), "restored_fold") mappedPath := filepath.Join(os.TempDir(), "restored_fold")
folderName := filepath.Base(mappedPath) folderName := filepath.Base(mappedPath)
configs := dataprovider.Configs{
SFTPD: &dataprovider.SFTPDConfigs{
Moduli: []string{"/moduli"},
},
}
role := getTestRole() role := getTestRole()
role.ID = 1 role.ID = 1
role.Name = "test_role_load" role.Name = "test_role_load"
@ -7834,6 +7915,7 @@ func TestLoaddataMode(t *testing.T) {
backupData := dataprovider.BackupData{ backupData := dataprovider.BackupData{
Version: dataprovider.DumpVersion, Version: dataprovider.DumpVersion,
} }
backupData.Configs = &configs
backupData.Users = append(backupData.Users, user) backupData.Users = append(backupData.Users, user)
backupData.Groups = append(backupData.Groups, group) backupData.Groups = append(backupData.Groups, group)
backupData.Admins = append(backupData.Admins, admin) backupData.Admins = append(backupData.Admins, admin)
@ -7859,10 +7941,13 @@ func TestLoaddataMode(t *testing.T) {
backupData.IPLists = append(backupData.IPLists, ipListEntry) backupData.IPLists = append(backupData.IPLists, ipListEntry)
backupContent, _ := json.Marshal(backupData) backupContent, _ := json.Marshal(backupData)
backupFilePath := filepath.Join(backupsPath, "backup.json") backupFilePath := filepath.Join(backupsPath, "backup.json")
err := os.WriteFile(backupFilePath, backupContent, os.ModePerm) err = os.WriteFile(backupFilePath, backupContent, os.ModePerm)
assert.NoError(t, err) assert.NoError(t, err)
_, _, err = httpdtest.Loaddata(backupFilePath, "0", "0", http.StatusOK) _, _, err = httpdtest.Loaddata(backupFilePath, "0", "0", http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
configs, err = dataprovider.GetConfigs()
assert.NoError(t, err)
assert.Len(t, configs.SFTPD.Moduli, 1)
folder, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK) folder, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, mappedPath+"1", folder.MappedPath) assert.Equal(t, mappedPath+"1", folder.MappedPath)
@ -7934,6 +8019,10 @@ func TestLoaddataMode(t *testing.T) {
entry, _, err = httpdtest.UpdateIPListEntry(entry, http.StatusOK) entry, _, err = httpdtest.UpdateIPListEntry(entry, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
configs.SFTPD.Moduli = append(configs.SFTPD.Moduli, "/moduli_new")
err = dataprovider.UpdateConfigs(&configs, "", "", "")
assert.NoError(t, err)
backupData.Configs = &configs
backupData.Folders = []vfs.BaseVirtualFolder{ backupData.Folders = []vfs.BaseVirtualFolder{
{ {
MappedPath: mappedPath, MappedPath: mappedPath,
@ -7942,6 +8031,9 @@ func TestLoaddataMode(t *testing.T) {
} }
_, _, err = httpdtest.Loaddata(backupFilePath, "0", "1", http.StatusOK) _, _, err = httpdtest.Loaddata(backupFilePath, "0", "1", http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
configs, err = dataprovider.GetConfigs()
assert.NoError(t, err)
assert.Len(t, configs.SFTPD.Moduli, 2)
group, _, err = httpdtest.GetGroupByName(group.Name, http.StatusOK) group, _, err = httpdtest.GetGroupByName(group.Name, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotEqual(t, oldGroupDesc, group.Description) assert.NotEqual(t, oldGroupDesc, group.Description)
@ -7999,6 +8091,9 @@ func TestLoaddataMode(t *testing.T) {
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, oldUploadBandwidth, user.UploadBandwidth) assert.Equal(t, oldUploadBandwidth, user.UploadBandwidth)
configs, err = dataprovider.GetConfigs()
assert.NoError(t, err)
assert.Len(t, configs.SFTPD.Moduli, 1)
// the group is referenced // the group is referenced
_, err = httpdtest.RemoveGroup(group, http.StatusBadRequest) _, err = httpdtest.RemoveGroup(group, http.StatusBadRequest)
assert.NoError(t, err) assert.NoError(t, err)
@ -8022,6 +8117,8 @@ func TestLoaddataMode(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
err = os.Remove(backupFilePath) err = os.Remove(backupFilePath)
assert.NoError(t, err) assert.NoError(t, err)
err = dataprovider.UpdateConfigs(nil, "", "", "")
assert.NoError(t, err)
} }
func TestRateLimiter(t *testing.T) { func TestRateLimiter(t *testing.T) {
@ -8992,6 +9089,84 @@ func TestChangeAdminPwdInvalidJsonMock(t *testing.T) {
checkResponseCode(t, http.StatusBadRequest, rr) checkResponseCode(t, http.StatusBadRequest, rr)
} }
func TestSMTPConfig(t *testing.T) {
smtpCfg := smtp.Config{
Host: "127.0.0.1",
Port: 3525,
From: "notification@example.com",
TemplatesPath: "templates",
}
err := smtpCfg.Initialize(configDir)
require.NoError(t, err)
smtpTestURL := path.Join(webConfigsPath, "smtp", "test")
tokenHeader := "X-CSRF-TOKEN"
webToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err)
csrfToken, err := getCSRFToken(httpBaseURL + webClientLoginPath)
assert.NoError(t, err)
req, err := http.NewRequest(http.MethodPost, smtpTestURL, bytes.NewBuffer([]byte("{")))
assert.NoError(t, err)
setJWTCookieForReq(req, webToken)
rr := executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr)
req.Header.Set(tokenHeader, csrfToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
testReq := make(map[string]any)
testReq["host"] = smtpCfg.Host
testReq["port"] = 3525
testReq["from"] = "from@example.com"
asJSON, err := json.Marshal(testReq)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPost, smtpTestURL, bytes.NewBuffer(asJSON))
assert.NoError(t, err)
setJWTCookieForReq(req, webToken)
req.Header.Set(tokenHeader, csrfToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusInternalServerError, rr)
testReq["recipient"] = "example@example.com"
asJSON, err = json.Marshal(testReq)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPost, smtpTestURL, bytes.NewBuffer(asJSON))
assert.NoError(t, err)
setJWTCookieForReq(req, webToken)
req.Header.Set(tokenHeader, csrfToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
configs := dataprovider.Configs{
SMTP: &dataprovider.SMTPConfigs{
Host: "127.0.0.1",
Port: 3535,
User: "user@example.com",
Password: kms.NewPlainSecret(defaultPassword),
},
}
err = dataprovider.UpdateConfigs(&configs, "", "", "")
assert.NoError(t, err)
testReq["password"] = redactedSecret
asJSON, err = json.Marshal(testReq)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPost, smtpTestURL, bytes.NewBuffer(asJSON))
assert.NoError(t, err)
setJWTCookieForReq(req, webToken)
req.Header.Set(tokenHeader, csrfToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusInternalServerError, rr)
assert.Contains(t, rr.Body.String(), "server does not support SMTP AUTH")
err = dataprovider.UpdateConfigs(nil, "", "", "")
assert.NoError(t, err)
smtpCfg = smtp.Config{}
err = smtpCfg.Initialize(configDir)
require.NoError(t, err)
}
func TestMFAPermission(t *testing.T) { func TestMFAPermission(t *testing.T) {
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err) assert.NoError(t, err)
@ -12072,6 +12247,149 @@ func TestMaxSessions(t *testing.T) {
assert.Len(t, common.Connections.GetStats(""), 0) assert.Len(t, common.Connections.GetStats(""), 0)
} }
func TestWebConfigsMock(t *testing.T) {
err := dataprovider.UpdateConfigs(nil, "", "", "")
assert.NoError(t, err)
webToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err)
csrfToken, err := getCSRFToken(httpBaseURL + webClientLoginPath)
assert.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, webConfigsPath, nil)
assert.NoError(t, err)
setJWTCookieForReq(req, webToken)
rr := executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
form := make(url.Values)
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
assert.NoError(t, err)
setJWTCookieForReq(req, webToken)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr)
// parse form error
form.Set(csrfFormToken, csrfToken)
req, err = http.NewRequest(http.MethodPost, webConfigsPath+"?p=p%C3%AO%GH", bytes.NewBuffer([]byte(form.Encode())))
assert.NoError(t, err)
setJWTCookieForReq(req, webToken)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
// save SFTP configs
form.Set("sftp_host_key_algos", ssh.KeyAlgoRSA)
form.Add("sftp_host_key_algos", ssh.CertAlgoDSAv01)
form.Set("sftp_moduli", "path 1 , path 2")
form.Set("form_action", "sftp_submit")
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
assert.NoError(t, err)
setJWTCookieForReq(req, webToken)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), ssh.CertAlgoDSAv01) // invalid algo
form.Set("sftp_host_key_algos", ssh.KeyAlgoRSA)
form.Add("sftp_host_key_algos", ssh.CertAlgoRSAv01)
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
assert.NoError(t, err)
setJWTCookieForReq(req, webToken)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Configurations updated")
// check SFTP configs
configs, err := dataprovider.GetConfigs()
assert.NoError(t, err)
assert.Len(t, configs.SFTPD.HostKeyAlgos, 2)
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA)
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.CertAlgoRSAv01)
assert.Len(t, configs.SFTPD.Moduli, 2)
assert.Contains(t, configs.SFTPD.Moduli, "path 1")
assert.Contains(t, configs.SFTPD.Moduli, "path 2")
// invalid form action
form.Set("form_action", "")
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
assert.NoError(t, err)
setJWTCookieForReq(req, webToken)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "unsupported form action")
// test SMTP configs
form.Set("form_action", "smtp_submit")
form.Set("smtp_host", "mail.example.net")
form.Set("smtp_from", "Example <info@example.net>")
form.Set("smtp_username", defaultUsername)
form.Set("smtp_password", defaultPassword)
form.Set("smtp_domain", "localdomain")
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
assert.NoError(t, err)
setJWTCookieForReq(req, webToken)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Validation error") // port is not passed so 0
// set valid parameters
form.Set("smtp_port", "465")
form.Set("smtp_auth", "1")
form.Set("smtp_encryption", "2")
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
assert.NoError(t, err)
setJWTCookieForReq(req, webToken)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Configurations updated")
// check
configs, err = dataprovider.GetConfigs()
assert.NoError(t, err)
assert.Len(t, configs.SFTPD.HostKeyAlgos, 2)
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA)
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.CertAlgoRSAv01)
assert.Len(t, configs.SFTPD.Moduli, 2)
assert.Equal(t, "mail.example.net", configs.SMTP.Host)
assert.Equal(t, 465, configs.SMTP.Port)
assert.Equal(t, "Example <info@example.net>", configs.SMTP.From)
assert.Equal(t, defaultUsername, configs.SMTP.User)
err = configs.SMTP.Password.Decrypt()
assert.NoError(t, err)
assert.Equal(t, defaultPassword, configs.SMTP.Password.GetPayload())
assert.Equal(t, 1, configs.SMTP.AuthType)
assert.Equal(t, 2, configs.SMTP.Encryption)
assert.Equal(t, "localdomain", configs.SMTP.Domain)
// set a redacted password, the current password must be preserved
form.Set("smtp_password", redactedSecret)
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
assert.NoError(t, err)
setJWTCookieForReq(req, webToken)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Configurations updated")
updatedConfigs, err := dataprovider.GetConfigs()
assert.NoError(t, err)
encryptedPayload := updatedConfigs.SMTP.Password.GetPayload()
secretKey := updatedConfigs.SMTP.Password.GetKey()
err = updatedConfigs.SMTP.Password.Decrypt()
assert.NoError(t, err)
assert.Equal(t, configs.SFTPD, updatedConfigs.SFTPD)
assert.Equal(t, configs.SMTP, updatedConfigs.SMTP)
// now set an undecryptable password
updatedConfigs.SMTP.Password = kms.NewSecret(sdkkms.SecretStatusSecretBox, encryptedPayload, secretKey, "")
err = dataprovider.UpdateConfigs(&updatedConfigs, "", "", "")
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
assert.NoError(t, err)
setJWTCookieForReq(req, webToken)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Configurations updated")
err = dataprovider.UpdateConfigs(nil, "", "", "")
assert.NoError(t, err)
}
func TestSFTPLoopError(t *testing.T) { func TestSFTPLoopError(t *testing.T) {
user1 := getTestUser() user1 := getTestUser()
user2 := getTestUser() user2 := getTestUser()
@ -22949,10 +23267,35 @@ func TestProviderClosedMock(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
dataprovider.Close() dataprovider.Close()
req, _ := http.NewRequest(http.MethodGet, webFoldersPath, nil)
testReq := make(map[string]any)
testReq["password"] = redactedSecret
asJSON, err := json.Marshal(testReq)
assert.NoError(t, err)
req, err := http.NewRequest(http.MethodPost, path.Join(webConfigsPath, "smtp", "test"), bytes.NewBuffer(asJSON))
assert.NoError(t, err)
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
req.Header.Set("X-CSRF-TOKEN", csrfToken)
rr := executeRequest(req) rr := executeRequest(req)
checkResponseCode(t, http.StatusInternalServerError, rr) checkResponseCode(t, http.StatusInternalServerError, rr)
req, err = http.NewRequest(http.MethodGet, webConfigsPath, nil)
assert.NoError(t, err)
setJWTCookieForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusInternalServerError, rr)
req, err = http.NewRequest(http.MethodPost, webConfigsPath, nil)
assert.NoError(t, err)
setJWTCookieForReq(req, token)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req)
checkResponseCode(t, http.StatusInternalServerError, rr)
req, _ = http.NewRequest(http.MethodGet, webFoldersPath, nil)
setJWTCookieForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusInternalServerError, rr)
req, _ = http.NewRequest(http.MethodGet, webUsersPath, nil) req, _ = http.NewRequest(http.MethodGet, webUsersPath, nil)
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
rr = executeRequest(req) rr = executeRequest(req)

View file

@ -616,6 +616,11 @@ func TestInvalidToken(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, rr.Code) assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "invalid token claims") assert.Contains(t, rr.Body.String(), "invalid token claims")
rr = httptest.NewRecorder()
server.handleWebConfigsPost(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "invalid token claims")
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
addAdmin(rr, req) addAdmin(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code) assert.Equal(t, http.StatusBadRequest, rr.Code)

View file

@ -1687,16 +1687,20 @@ func (s *httpdServer) setupWebAdminRoutes() {
router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Get(webIPListsPath, s.handleWebIPListsPage) router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Get(webIPListsPath, s.handleWebIPListsPage)
router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), compressor.Handler, s.refreshCookie). router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), compressor.Handler, s.refreshCookie).
Get(webIPListsPath+"/{type}", getIPListEntries) Get(webIPListsPath+"/{type}", getIPListEntries)
router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Get(webIPListPath+"/{type}", router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), s.refreshCookie).Get(webIPListPath+"/{type}",
s.handleWebAddIPListEntryGet) s.handleWebAddIPListEntryGet)
router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Post(webIPListPath+"/{type}", router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Post(webIPListPath+"/{type}",
s.handleWebAddIPListEntryPost) s.handleWebAddIPListEntryPost)
router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Get(webIPListPath+"/{type}/{ipornet}", router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), s.refreshCookie).Get(webIPListPath+"/{type}/{ipornet}",
s.handleWebUpdateIPListEntryGet) s.handleWebUpdateIPListEntryGet)
router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Post(webIPListPath+"/{type}/{ipornet}", router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Post(webIPListPath+"/{type}/{ipornet}",
s.handleWebUpdateIPListEntryPost) s.handleWebUpdateIPListEntryPost)
router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), verifyCSRFHeader). router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), verifyCSRFHeader).
Delete(webIPListPath+"/{type}/{ipornet}", deleteIPListEntry) Delete(webIPListPath+"/{type}/{ipornet}", deleteIPListEntry)
router.With(s.checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie).Get(webConfigsPath, s.handleWebConfigs)
router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(webConfigsPath, s.handleWebConfigsPost)
router.With(s.checkPerm(dataprovider.PermAdminManageSystem), verifyCSRFHeader, s.refreshCookie).
Post(webConfigsPath+"/smtp/test", testSMTPConfig)
}) })
} }
} }

View file

@ -35,6 +35,7 @@ import (
"github.com/drakkan/sftpgo/v2/internal/common" "github.com/drakkan/sftpgo/v2/internal/common"
"github.com/drakkan/sftpgo/v2/internal/dataprovider" "github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/kms" "github.com/drakkan/sftpgo/v2/internal/kms"
"github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/mfa" "github.com/drakkan/sftpgo/v2/internal/mfa"
"github.com/drakkan/sftpgo/v2/internal/plugin" "github.com/drakkan/sftpgo/v2/internal/plugin"
"github.com/drakkan/sftpgo/v2/internal/smtp" "github.com/drakkan/sftpgo/v2/internal/smtp"
@ -94,6 +95,7 @@ const (
templateDefender = "defender.html" templateDefender = "defender.html"
templateIPLists = "iplists.html" templateIPLists = "iplists.html"
templateIPList = "iplist.html" templateIPList = "iplist.html"
templateConfigs = "configs.html"
templateProfile = "profile.html" templateProfile = "profile.html"
templateChangePwd = "changepassword.html" templateChangePwd = "changepassword.html"
templateMaintenance = "maintenance.html" templateMaintenance = "maintenance.html"
@ -114,6 +116,7 @@ const (
pageDefenderTitle = "Auto Blocklist" pageDefenderTitle = "Auto Blocklist"
pageIPListsTitle = "IP Lists" pageIPListsTitle = "IP Lists"
pageEventsTitle = "Logs" pageEventsTitle = "Logs"
pageConfigsTitle = "Configurations"
pageForgotPwdTitle = "SFTPGo Admin - Forgot password" pageForgotPwdTitle = "SFTPGo Admin - Forgot password"
pageResetPwdTitle = "SFTPGo Admin - Reset password" pageResetPwdTitle = "SFTPGo Admin - Reset password"
pageSetupTitle = "Create first admin user" pageSetupTitle = "Create first admin user"
@ -126,60 +129,63 @@ var (
) )
type basePage struct { type basePage struct {
Title string Title string
CurrentURL string CurrentURL string
UsersURL string UsersURL string
UserURL string UserURL string
UserTemplateURL string UserTemplateURL string
AdminsURL string AdminsURL string
AdminURL string AdminURL string
QuotaScanURL string QuotaScanURL string
ConnectionsURL string ConnectionsURL string
GroupsURL string GroupsURL string
GroupURL string GroupURL string
FoldersURL string FoldersURL string
FolderURL string FolderURL string
FolderTemplateURL string FolderTemplateURL string
DefenderURL string DefenderURL string
IPListsURL string IPListsURL string
IPListURL string IPListURL string
EventsURL string EventsURL string
LogoutURL string ConfigsURL string
ProfileURL string LogoutURL string
ChangePwdURL string ProfileURL string
MFAURL string ChangePwdURL string
EventRulesURL string MFAURL string
EventRuleURL string EventRulesURL string
EventActionsURL string EventRuleURL string
EventActionURL string EventActionsURL string
RolesURL string EventActionURL string
RoleURL string RolesURL string
FolderQuotaScanURL string RoleURL string
StatusURL string FolderQuotaScanURL string
MaintenanceURL string StatusURL string
StaticURL string MaintenanceURL string
UsersTitle string StaticURL string
AdminsTitle string UsersTitle string
ConnectionsTitle string AdminsTitle string
FoldersTitle string ConnectionsTitle string
GroupsTitle string FoldersTitle string
EventRulesTitle string GroupsTitle string
EventActionsTitle string EventRulesTitle string
RolesTitle string EventActionsTitle string
StatusTitle string RolesTitle string
MaintenanceTitle string StatusTitle string
DefenderTitle string MaintenanceTitle string
IPListsTitle string DefenderTitle string
EventsTitle string IPListsTitle string
Version string EventsTitle string
CSRFToken string ConfigsTitle string
IsEventManagerPage bool Version string
IsIPManagerPage bool CSRFToken string
HasDefender bool IsEventManagerPage bool
HasSearcher bool IsIPManagerPage bool
HasExternalLogin bool IsServerManagerPage bool
LoggedAdmin *dataprovider.Admin HasDefender bool
Branding UIBranding HasSearcher bool
HasExternalLogin bool
LoggedAdmin *dataprovider.Admin
Branding UIBranding
} }
type usersPage struct { type usersPage struct {
@ -383,6 +389,14 @@ type eventsPage struct {
ProviderEventsSearchURL string ProviderEventsSearchURL string
} }
type configsPage struct {
basePage
Configs dataprovider.Configs
ConfigSection int
RedactedSecret string
Error string
}
type messagePage struct { type messagePage struct {
basePage basePage
Error string Error string
@ -554,6 +568,11 @@ func loadAdminTemplates(templatesPath string) {
filepath.Join(templatesPath, templateAdminDir, templateBase), filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateEvents), filepath.Join(templatesPath, templateAdminDir, templateEvents),
} }
configsPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateConfigs),
}
fsBaseTpl := template.New("fsBaseTemplate").Funcs(template.FuncMap{ fsBaseTpl := template.New("fsBaseTemplate").Funcs(template.FuncMap{
"ListFSProviders": func() []sdk.FilesystemProvider { "ListFSProviders": func() []sdk.FilesystemProvider {
@ -595,6 +614,7 @@ func loadAdminTemplates(templatesPath string) {
rolesTmpl := util.LoadTemplate(nil, rolesPaths...) rolesTmpl := util.LoadTemplate(nil, rolesPaths...)
roleTmpl := util.LoadTemplate(nil, rolePaths...) roleTmpl := util.LoadTemplate(nil, rolePaths...)
eventsTmpl := util.LoadTemplate(nil, eventsPaths...) eventsTmpl := util.LoadTemplate(nil, eventsPaths...)
configsTmpl := util.LoadTemplate(nil, configsPaths...)
adminTemplates[templateUsers] = usersTmpl adminTemplates[templateUsers] = usersTmpl
adminTemplates[templateUser] = userTmpl adminTemplates[templateUser] = userTmpl
@ -627,6 +647,7 @@ func loadAdminTemplates(templatesPath string) {
adminTemplates[templateRoles] = rolesTmpl adminTemplates[templateRoles] = rolesTmpl
adminTemplates[templateRole] = roleTmpl adminTemplates[templateRole] = roleTmpl
adminTemplates[templateEvents] = eventsTmpl adminTemplates[templateEvents] = eventsTmpl
adminTemplates[templateConfigs] = configsTmpl
} }
func isEventManagerResource(currentURL string) bool { func isEventManagerResource(currentURL string) bool {
@ -658,66 +679,74 @@ func isIPListsResource(currentURL string) bool {
return false return false
} }
func isServerManagerResource(currentURL string) bool {
return currentURL == webEventsPath || currentURL == webStatusPath || currentURL == webMaintenancePath ||
currentURL == webConfigsPath
}
func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request) basePage { func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request) basePage {
var csrfToken string var csrfToken string
if currentURL != "" { if currentURL != "" {
csrfToken = createCSRFToken(util.GetIPFromRemoteAddress(r.RemoteAddr)) csrfToken = createCSRFToken(util.GetIPFromRemoteAddress(r.RemoteAddr))
} }
return basePage{ return basePage{
Title: title, Title: title,
CurrentURL: currentURL, CurrentURL: currentURL,
UsersURL: webUsersPath, UsersURL: webUsersPath,
UserURL: webUserPath, UserURL: webUserPath,
UserTemplateURL: webTemplateUser, UserTemplateURL: webTemplateUser,
AdminsURL: webAdminsPath, AdminsURL: webAdminsPath,
AdminURL: webAdminPath, AdminURL: webAdminPath,
GroupsURL: webGroupsPath, GroupsURL: webGroupsPath,
GroupURL: webGroupPath, GroupURL: webGroupPath,
FoldersURL: webFoldersPath, FoldersURL: webFoldersPath,
FolderURL: webFolderPath, FolderURL: webFolderPath,
FolderTemplateURL: webTemplateFolder, FolderTemplateURL: webTemplateFolder,
DefenderURL: webDefenderPath, DefenderURL: webDefenderPath,
IPListsURL: webIPListsPath, IPListsURL: webIPListsPath,
IPListURL: webIPListPath, IPListURL: webIPListPath,
EventsURL: webEventsPath, EventsURL: webEventsPath,
LogoutURL: webLogoutPath, ConfigsURL: webConfigsPath,
ProfileURL: webAdminProfilePath, LogoutURL: webLogoutPath,
ChangePwdURL: webChangeAdminPwdPath, ProfileURL: webAdminProfilePath,
MFAURL: webAdminMFAPath, ChangePwdURL: webChangeAdminPwdPath,
EventRulesURL: webAdminEventRulesPath, MFAURL: webAdminMFAPath,
EventRuleURL: webAdminEventRulePath, EventRulesURL: webAdminEventRulesPath,
EventActionsURL: webAdminEventActionsPath, EventRuleURL: webAdminEventRulePath,
EventActionURL: webAdminEventActionPath, EventActionsURL: webAdminEventActionsPath,
RolesURL: webAdminRolesPath, EventActionURL: webAdminEventActionPath,
RoleURL: webAdminRolePath, RolesURL: webAdminRolesPath,
QuotaScanURL: webQuotaScanPath, RoleURL: webAdminRolePath,
ConnectionsURL: webConnectionsPath, QuotaScanURL: webQuotaScanPath,
StatusURL: webStatusPath, ConnectionsURL: webConnectionsPath,
FolderQuotaScanURL: webScanVFolderPath, StatusURL: webStatusPath,
MaintenanceURL: webMaintenancePath, FolderQuotaScanURL: webScanVFolderPath,
StaticURL: webStaticFilesPath, MaintenanceURL: webMaintenancePath,
UsersTitle: pageUsersTitle, StaticURL: webStaticFilesPath,
AdminsTitle: pageAdminsTitle, UsersTitle: pageUsersTitle,
ConnectionsTitle: pageConnectionsTitle, AdminsTitle: pageAdminsTitle,
FoldersTitle: pageFoldersTitle, ConnectionsTitle: pageConnectionsTitle,
GroupsTitle: pageGroupsTitle, FoldersTitle: pageFoldersTitle,
EventRulesTitle: pageEventRulesTitle, GroupsTitle: pageGroupsTitle,
EventActionsTitle: pageEventActionsTitle, EventRulesTitle: pageEventRulesTitle,
RolesTitle: pageRolesTitle, EventActionsTitle: pageEventActionsTitle,
StatusTitle: pageStatusTitle, RolesTitle: pageRolesTitle,
MaintenanceTitle: pageMaintenanceTitle, StatusTitle: pageStatusTitle,
DefenderTitle: pageDefenderTitle, MaintenanceTitle: pageMaintenanceTitle,
IPListsTitle: pageIPListsTitle, DefenderTitle: pageDefenderTitle,
EventsTitle: pageEventsTitle, IPListsTitle: pageIPListsTitle,
Version: version.GetAsString(), EventsTitle: pageEventsTitle,
LoggedAdmin: getAdminFromToken(r), ConfigsTitle: pageConfigsTitle,
IsEventManagerPage: isEventManagerResource(currentURL), Version: version.GetAsString(),
IsIPManagerPage: isIPListsResource(currentURL), LoggedAdmin: getAdminFromToken(r),
HasDefender: common.Config.DefenderConfig.Enabled, IsEventManagerPage: isEventManagerResource(currentURL),
HasSearcher: plugin.Handler.HasSearcher(), IsIPManagerPage: isIPListsResource(currentURL),
HasExternalLogin: isLoggedInWithOIDC(r), IsServerManagerPage: isServerManagerResource(currentURL),
CSRFToken: csrfToken, HasDefender: common.Config.DefenderConfig.Enabled,
Branding: s.binding.Branding.WebAdmin, HasSearcher: plugin.Handler.HasSearcher(),
HasExternalLogin: isLoggedInWithOIDC(r),
CSRFToken: csrfToken,
Branding: s.binding.Branding.WebAdmin,
} }
} }
@ -867,6 +896,24 @@ func (s *httpdServer) renderMaintenancePage(w http.ResponseWriter, r *http.Reque
renderAdminTemplate(w, templateMaintenance, data) renderAdminTemplate(w, templateMaintenance, data)
} }
func (s *httpdServer) renderConfigsPage(w http.ResponseWriter, r *http.Request, configs dataprovider.Configs,
error string, section int,
) {
configs.SetNilsToEmpty()
if configs.SMTP.Port == 0 {
configs.SMTP.Port = 587
}
data := configsPage{
basePage: s.getBasePageData(pageConfigsTitle, webConfigsPath, r),
Configs: configs,
ConfigSection: section,
RedactedSecret: redactedSecret,
Error: error,
}
renderAdminTemplate(w, templateConfigs, data)
}
func (s *httpdServer) renderAdminSetupPage(w http.ResponseWriter, r *http.Request, username, error string) { func (s *httpdServer) renderAdminSetupPage(w http.ResponseWriter, r *http.Request, username, error string) {
data := setupPage{ data := setupPage{
basePage: s.getBasePageData(pageSetupTitle, webAdminSetupPath, r), basePage: s.getBasePageData(pageSetupTitle, webAdminSetupPath, r),
@ -2218,11 +2265,11 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven
} }
var emailAttachments []string var emailAttachments []string
if r.Form.Get("email_attachments") != "" { if r.Form.Get("email_attachments") != "" {
emailAttachments = strings.Split(strings.ReplaceAll(r.Form.Get("email_attachments"), " ", ""), ",") emailAttachments = getSliceFromDelimitedValues(r.Form.Get("email_attachments"), ",")
} }
var cmdArgs []string var cmdArgs []string
if r.Form.Get("cmd_arguments") != "" { if r.Form.Get("cmd_arguments") != "" {
cmdArgs = strings.Split(strings.ReplaceAll(r.Form.Get("cmd_arguments"), " ", ""), ",") cmdArgs = getSliceFromDelimitedValues(r.Form.Get("cmd_arguments"), ",")
} }
options := dataprovider.BaseEventActionOptions{ options := dataprovider.BaseEventActionOptions{
HTTPConfig: dataprovider.EventActionHTTPConfig{ HTTPConfig: dataprovider.EventActionHTTPConfig{
@ -2244,7 +2291,7 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven
EnvVars: getKeyValsFromPostFields(r, "cmd_env_key", "cmd_env_val"), EnvVars: getKeyValsFromPostFields(r, "cmd_env_key", "cmd_env_val"),
}, },
EmailConfig: dataprovider.EventActionEmailConfig{ EmailConfig: dataprovider.EventActionEmailConfig{
Recipients: strings.Split(strings.ReplaceAll(r.Form.Get("email_recipients"), " ", ""), ","), Recipients: getSliceFromDelimitedValues(r.Form.Get("email_recipients"), ","),
Subject: r.Form.Get("email_subject"), Subject: r.Form.Get("email_subject"),
Body: r.Form.Get("email_body"), Body: r.Form.Get("email_body"),
Attachments: emailAttachments, Attachments: emailAttachments,
@ -2255,13 +2302,13 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven
FsConfig: dataprovider.EventActionFilesystemConfig{ FsConfig: dataprovider.EventActionFilesystemConfig{
Type: fsActionType, Type: fsActionType,
Renames: getKeyValsFromPostFields(r, "fs_rename_source", "fs_rename_target"), Renames: getKeyValsFromPostFields(r, "fs_rename_source", "fs_rename_target"),
Deletes: strings.Split(strings.ReplaceAll(r.Form.Get("fs_delete_paths"), " ", ""), ","), Deletes: getSliceFromDelimitedValues(r.Form.Get("fs_delete_paths"), ","),
MkDirs: strings.Split(strings.ReplaceAll(r.Form.Get("fs_mkdir_paths"), " ", ""), ","), MkDirs: getSliceFromDelimitedValues(r.Form.Get("fs_mkdir_paths"), ","),
Exist: strings.Split(strings.ReplaceAll(r.Form.Get("fs_exist_paths"), " ", ""), ","), Exist: getSliceFromDelimitedValues(r.Form.Get("fs_exist_paths"), ","),
Copy: getKeyValsFromPostFields(r, "fs_copy_source", "fs_copy_target"), Copy: getKeyValsFromPostFields(r, "fs_copy_source", "fs_copy_target"),
Compress: dataprovider.EventActionFsCompress{ Compress: dataprovider.EventActionFsCompress{
Name: r.Form.Get("fs_compress_name"), Name: r.Form.Get("fs_compress_name"),
Paths: strings.Split(strings.ReplaceAll(r.Form.Get("fs_compress_paths"), " ", ""), ","), Paths: getSliceFromDelimitedValues(r.Form.Get("fs_compress_paths"), ","),
}, },
}, },
PwdExpirationConfig: dataprovider.EventActionPasswordExpiration{ PwdExpirationConfig: dataprovider.EventActionPasswordExpiration{
@ -2487,6 +2534,41 @@ func getIPListEntryFromPostFields(r *http.Request, listType dataprovider.IPListT
}, nil }, nil
} }
func getSFTPConfigsFromPostFields(r *http.Request) *dataprovider.SFTPDConfigs {
return &dataprovider.SFTPDConfigs{
HostKeyAlgos: r.Form["sftp_host_key_algos"],
Moduli: getSliceFromDelimitedValues(r.Form.Get("sftp_moduli"), ","),
KexAlgorithms: r.Form["sftp_kex_algos"],
Ciphers: r.Form["sftp_ciphers"],
MACs: r.Form["sftp_macs"],
}
}
func getSMTPConfigsFromPostFields(r *http.Request) *dataprovider.SMTPConfigs {
port, err := strconv.Atoi(r.Form.Get("smtp_port"))
if err != nil {
port = 0
}
authType, err := strconv.Atoi(r.Form.Get("smtp_auth"))
if err != nil {
authType = 0
}
encryption, err := strconv.Atoi(r.Form.Get("smtp_encryption"))
if err != nil {
encryption = 0
}
return &dataprovider.SMTPConfigs{
Host: r.Form.Get("smtp_host"),
Port: port,
From: r.Form.Get("smtp_from"),
User: r.Form.Get("smtp_username"),
Password: getSecretFromFormField(r, "smtp_password"),
AuthType: authType,
Encryption: encryption,
Domain: r.Form.Get("smtp_domain"),
}
}
func (s *httpdServer) handleWebAdminForgotPwd(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) handleWebAdminForgotPwd(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
if !smtp.IsEnabled() { if !smtp.IsEnabled() {
@ -3921,3 +4003,70 @@ func (s *httpdServer) handleWebUpdateIPListEntryPost(w http.ResponseWriter, r *h
} }
http.Redirect(w, r, webIPListsPath, http.StatusSeeOther) http.Redirect(w, r, webIPListsPath, http.StatusSeeOther)
} }
func (s *httpdServer) handleWebConfigs(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
configs, err := dataprovider.GetConfigs()
if err != nil {
s.renderInternalServerErrorPage(w, r, err)
return
}
s.renderConfigsPage(w, r, configs, "", 0)
}
func (s *httpdServer) handleWebConfigsPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
return
}
configs, err := dataprovider.GetConfigs()
if err != nil {
s.renderInternalServerErrorPage(w, r, err)
return
}
err = r.ParseForm()
if err != nil {
s.renderBadRequestPage(w, r, err)
return
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
s.renderForbiddenPage(w, r, err.Error())
return
}
var configSection int
switch r.Form.Get("form_action") {
case "sftp_submit":
configSection = 1
sftpConfigs := getSFTPConfigsFromPostFields(r)
configs.SFTPD = sftpConfigs
case "smtp_submit":
configSection = 2
smtpConfigs := getSMTPConfigsFromPostFields(r)
if smtpConfigs.Password.IsNotPlainAndNotEmpty() {
smtpConfigs.Password = configs.SMTP.Password
}
configs.SMTP = smtpConfigs
default:
s.renderBadRequestPage(w, r, errors.New("unsupported form action"))
return
}
err = dataprovider.UpdateConfigs(&configs, claims.Username, ipAddr, claims.Role)
if err != nil {
s.renderConfigsPage(w, r, configs, err.Error(), configSection)
return
}
if configSection == 2 {
err := configs.SMTP.Password.TryDecrypt()
if err == nil {
smtp.Activate(configs.SMTP)
} else {
logger.Error(logSender, "", "unable to decrypt SMTP password, cannot activate configuration")
}
}
s.renderMessagePage(w, r, "Configurations updated", "", http.StatusOK, nil,
"Configurations has been successfully updated")
}

View file

@ -135,6 +135,12 @@ func (s *Service) initializeServices(disableAWSInstallationCode bool) error {
logger.ErrorToConsole("unable to initialize MFA: %v", err) logger.ErrorToConsole("unable to initialize MFA: %v", err)
return err return err
} }
err = dataprovider.Initialize(providerConf, s.ConfigDir, s.PortableMode == 0)
if err != nil {
logger.Error(logSender, "", "error initializing data provider: %v", err)
logger.ErrorToConsole("error initializing data provider: %v", err)
return err
}
if err := plugin.Initialize(config.GetPluginsConfig(), s.LogLevel); err != nil { if err := plugin.Initialize(config.GetPluginsConfig(), s.LogLevel); err != nil {
logger.Error(logSender, "", "unable to initialize plugin system: %v", err) logger.Error(logSender, "", "unable to initialize plugin system: %v", err)
logger.ErrorToConsole("unable to initialize plugin system: %v", err) logger.ErrorToConsole("unable to initialize plugin system: %v", err)
@ -147,12 +153,6 @@ func (s *Service) initializeServices(disableAWSInstallationCode bool) error {
logger.ErrorToConsole("unable to initialize SMTP configuration: %v", err) logger.ErrorToConsole("unable to initialize SMTP configuration: %v", err)
return err return err
} }
err = dataprovider.Initialize(providerConf, s.ConfigDir, s.PortableMode == 0)
if err != nil {
logger.Error(logSender, "", "error initializing data provider: %v", err)
logger.ErrorToConsole("error initializing data provider: %v", err)
return err
}
err = common.Initialize(config.GetCommonConfig(), providerConf.GetShared()) err = common.Initialize(config.GetCommonConfig(), providerConf.GetShared())
if err != nil { if err != nil {
logger.Error(logSender, "", "%v", err) logger.Error(logSender, "", "%v", err)
@ -350,7 +350,11 @@ func (s *Service) LoadInitialData() error {
} }
func (s *Service) restoreDump(dump *dataprovider.BackupData) error { func (s *Service) restoreDump(dump *dataprovider.BackupData) error {
err := httpd.RestoreIPListEntries(dump.IPLists, s.LoadDataFrom, s.LoadDataMode, dataprovider.ActionExecutorSystem, "", "") err := httpd.RestoreConfigs(dump.Configs, s.LoadDataFrom, s.LoadDataMode, dataprovider.ActionExecutorSystem, "", "")
if err != nil {
return fmt.Errorf("unable to restore configs from file %q: %v", s.LoadDataFrom, err)
}
err = httpd.RestoreIPListEntries(dump.IPLists, s.LoadDataFrom, s.LoadDataMode, dataprovider.ActionExecutorSystem, "", "")
if err != nil { if err != nil {
return fmt.Errorf("unable to restore IP list entries from file %q: %v", s.LoadDataFrom, err) return fmt.Errorf("unable to restore IP list entries from file %q: %v", s.LoadDataFrom, err)
} }

View file

@ -1862,6 +1862,44 @@ func TestConnectionStatusStruct(t *testing.T) {
assert.NotEqual(t, 0, len(connInfo)) assert.NotEqual(t, 0, len(connInfo))
} }
func TestConfigsFromProvider(t *testing.T) {
err := dataprovider.UpdateConfigs(nil, "", "", "")
assert.NoError(t, err)
c := Configuration{}
err = c.loadFromProvider()
assert.NoError(t, err)
assert.Len(t, c.HostKeyAlgorithms, 0)
assert.Len(t, c.Moduli, 0)
assert.Len(t, c.KexAlgorithms, 0)
assert.Len(t, c.Ciphers, 0)
assert.Len(t, c.MACs, 0)
configs := dataprovider.Configs{
SFTPD: &dataprovider.SFTPDConfigs{
HostKeyAlgos: []string{ssh.KeyAlgoRSA},
Moduli: []string{"/etc/ssh/moduli"},
KexAlgorithms: []string{kexDHGroupExchangeSHA256},
Ciphers: []string{"aes128-cbc", "aes192-cbc", "aes256-cbc"},
MACs: []string{"hmac-sha2-512-etm@openssh.com"},
},
}
err = dataprovider.UpdateConfigs(&configs, "", "", "")
assert.NoError(t, err)
err = c.loadFromProvider()
assert.NoError(t, err)
expectedHostKeyAlgos := append(preferredHostKeyAlgos, configs.SFTPD.HostKeyAlgos...)
expectedKEXs := append(preferredKexAlgos, configs.SFTPD.KexAlgorithms...)
expectedCiphers := append(preferredCiphers, configs.SFTPD.Ciphers...)
expectedMACs := append(preferredMACs, configs.SFTPD.MACs...)
assert.Equal(t, expectedHostKeyAlgos, c.HostKeyAlgorithms)
assert.Equal(t, expectedKEXs, c.KexAlgorithms)
assert.Equal(t, expectedCiphers, c.Ciphers)
assert.Equal(t, expectedMACs, c.MACs)
assert.Equal(t, configs.SFTPD.Moduli, c.Moduli)
err = dataprovider.UpdateConfigs(nil, "", "", "")
assert.NoError(t, err)
}
func TestSupportedSecurityOptions(t *testing.T) { func TestSupportedSecurityOptions(t *testing.T) {
c := Configuration{ c := Configuration{
KexAlgorithms: supportedKexAlgos, KexAlgorithms: supportedKexAlgos,
@ -1888,6 +1926,12 @@ func TestSupportedSecurityOptions(t *testing.T) {
assert.Equal(t, supportedCiphers, serverConfig.Ciphers) assert.Equal(t, supportedCiphers, serverConfig.Ciphers)
assert.Equal(t, supportedMACs, serverConfig.MACs) assert.Equal(t, supportedMACs, serverConfig.MACs)
assert.Equal(t, supportedKexAlgos, serverConfig.KeyExchanges) assert.Equal(t, supportedKexAlgos, serverConfig.KeyExchanges)
c.KexAlgorithms = append(preferredKexAlgos, kexDHGroupExchangeSHA256) // removed because no moduli is provided
err = c.configureSecurityOptions(serverConfig)
assert.NoError(t, err)
assert.Equal(t, supportedCiphers, serverConfig.Ciphers)
assert.Equal(t, supportedMACs, serverConfig.MACs)
assert.Equal(t, preferredKexAlgos, serverConfig.KeyExchanges)
} }
func TestLoadModuli(t *testing.T) { func TestLoadModuli(t *testing.T) {
@ -1895,20 +1939,22 @@ func TestLoadModuli(t *testing.T) {
dhGEXSha256 := "diffie-hellman-group-exchange-sha256" dhGEXSha256 := "diffie-hellman-group-exchange-sha256"
c := Configuration{} c := Configuration{}
c.Moduli = []string{".", "missing file"} c.Moduli = []string{".", "missing file"}
err := c.loadModuli(configDir) c.loadModuli(configDir)
assert.Error(t, err)
assert.NotContains(t, supportedKexAlgos, dhGEXSha1) assert.NotContains(t, supportedKexAlgos, dhGEXSha1)
assert.NotContains(t, supportedKexAlgos, dhGEXSha256) assert.NotContains(t, supportedKexAlgos, dhGEXSha256)
assert.NotContains(t, preferredKexAlgos, dhGEXSha1)
assert.NotContains(t, preferredKexAlgos, dhGEXSha256)
assert.Len(t, supportedKexAlgos, 10) assert.Len(t, supportedKexAlgos, 10)
moduli := []byte("20220414072358 2 6 100 2047 5 F19C2D09AD49978F8A0C1B84168A4011A26F9CD516815934764A319FDC5975FA514AAF11B747D8CA6B3919532BEFB68FA118079473895674F3770F71FBB742F176883841EB3DE679BEF53C6AFE437A662F228B03C1E34B5A0D3909F608CEAA16C1F8131DE11E67878EFD918A89205E5E4DE323054010CA4711F25D466BB7727A016DD3F9F53BDBCE093055A4F2497ADEFB5A2500F9C5C3B0BCD88C6489F4C1CBC7CFB67BA6EABA0195794E4188CE9060F431041AD52FB9BAC4DF7FA536F585FBE67746CD57BFAD67567E9706C24D95C49BE95B759657C6BB5151E2AEA32F4CD557C40298A5C402101520EE8AAB8DFEED6FFC11AAF8036D6345923CFB5D1B922F") moduli := []byte("20220414072358 2 6 100 2047 5 F19C2D09AD49978F8A0C1B84168A4011A26F9CD516815934764A319FDC5975FA514AAF11B747D8CA6B3919532BEFB68FA118079473895674F3770F71FBB742F176883841EB3DE679BEF53C6AFE437A662F228B03C1E34B5A0D3909F608CEAA16C1F8131DE11E67878EFD918A89205E5E4DE323054010CA4711F25D466BB7727A016DD3F9F53BDBCE093055A4F2497ADEFB5A2500F9C5C3B0BCD88C6489F4C1CBC7CFB67BA6EABA0195794E4188CE9060F431041AD52FB9BAC4DF7FA536F585FBE67746CD57BFAD67567E9706C24D95C49BE95B759657C6BB5151E2AEA32F4CD557C40298A5C402101520EE8AAB8DFEED6FFC11AAF8036D6345923CFB5D1B922F")
moduliFile := filepath.Join(os.TempDir(), "moduli") moduliFile := filepath.Join(os.TempDir(), "moduli")
err = os.WriteFile(moduliFile, moduli, 0600) err := os.WriteFile(moduliFile, moduli, 0600)
assert.NoError(t, err) assert.NoError(t, err)
c.Moduli = []string{moduliFile} c.Moduli = []string{moduliFile}
err = c.loadModuli(configDir) c.loadModuli(configDir)
assert.NoError(t, err)
assert.Contains(t, supportedKexAlgos, dhGEXSha1) assert.Contains(t, supportedKexAlgos, dhGEXSha1)
assert.Contains(t, supportedKexAlgos, dhGEXSha256) assert.Contains(t, supportedKexAlgos, dhGEXSha256)
assert.NotContains(t, preferredKexAlgos, dhGEXSha1)
assert.Contains(t, preferredKexAlgos, dhGEXSha256)
assert.Len(t, supportedKexAlgos, 12) assert.Len(t, supportedKexAlgos, 12)
err = os.Remove(moduliFile) err = os.Remove(moduliFile)
assert.NoError(t, err) assert.NoError(t, err)

View file

@ -47,6 +47,8 @@ const (
defaultPrivateECDSAKeyName = "id_ecdsa" defaultPrivateECDSAKeyName = "id_ecdsa"
defaultPrivateEd25519KeyName = "id_ed25519" defaultPrivateEd25519KeyName = "id_ed25519"
sourceAddressCriticalOption = "source-address" sourceAddressCriticalOption = "source-address"
kexDHGroupExchangeSHA1 = "diffie-hellman-group-exchange-sha1"
kexDHGroupExchangeSHA256 = "diffie-hellman-group-exchange-sha256"
) )
var ( var (
@ -75,6 +77,11 @@ var (
"diffie-hellman-group18-sha512", "diffie-hellman-group14-sha1", "diffie-hellman-group18-sha512", "diffie-hellman-group14-sha1",
"diffie-hellman-group1-sha1", "diffie-hellman-group1-sha1",
} }
preferredKexAlgos = []string{
"curve25519-sha256", "curve25519-sha256@libssh.org",
"ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521",
"diffie-hellman-group14-sha256",
}
supportedCiphers = []string{ supportedCiphers = []string{
"aes128-gcm@openssh.com", "aes256-gcm@openssh.com", "aes128-gcm@openssh.com", "aes256-gcm@openssh.com",
"chacha20-poly1305@openssh.com", "chacha20-poly1305@openssh.com",
@ -83,11 +90,19 @@ var (
"3des-cbc", "3des-cbc",
"arcfour", "arcfour128", "arcfour256", "arcfour", "arcfour128", "arcfour256",
} }
preferredCiphers = []string{
"aes128-gcm@openssh.com", "aes256-gcm@openssh.com",
"chacha20-poly1305@openssh.com",
"aes128-ctr", "aes192-ctr", "aes256-ctr",
}
supportedMACs = []string{ supportedMACs = []string{
"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256", "hmac-sha2-256-etm@openssh.com", "hmac-sha2-256",
"hmac-sha2-512-etm@openssh.com", "hmac-sha2-512", "hmac-sha2-512-etm@openssh.com", "hmac-sha2-512",
"hmac-sha1", "hmac-sha1-96", "hmac-sha1", "hmac-sha1-96",
} }
preferredMACs = []string{
"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256",
}
revokedCertManager = revokedCertificates{ revokedCertManager = revokedCertificates{
certs: map[string]bool{}, certs: map[string]bool{},
@ -145,7 +160,7 @@ type Configuration struct {
HostKeyAlgorithms []string `json:"host_key_algorithms" mapstructure:"host_key_algorithms"` HostKeyAlgorithms []string `json:"host_key_algorithms" mapstructure:"host_key_algorithms"`
// Diffie-Hellman moduli files. // Diffie-Hellman moduli files.
// Each moduli file can be defined as a path relative to the configuration directory or an absolute one. // Each moduli file can be defined as a path relative to the configuration directory or an absolute one.
// If set, "diffie-hellman-group-exchange-sha256" and "diffie-hellman-group-exchange-sha1" KEX algorithms // If set and valid, "diffie-hellman-group-exchange-sha256" and "diffie-hellman-group-exchange-sha1" KEX algorithms
// will be available, `diffie-hellman-group-exchange-sha256` will be enabled by default if you // will be available, `diffie-hellman-group-exchange-sha256` will be enabled by default if you
// don't explicitly set KEXs // don't explicitly set KEXs
Moduli []string `json:"moduli" mapstructure:"moduli"` Moduli []string `json:"moduli" mapstructure:"moduli"`
@ -272,7 +287,7 @@ func (c *Configuration) getServerConfig() *ssh.ServerConfig {
} }
return nextMethods return nextMethods
}, },
ServerVersion: fmt.Sprintf("SSH-2.0-%v", c.Banner), ServerVersion: fmt.Sprintf("SSH-2.0-%s", c.Banner),
} }
if c.PasswordAuthentication { if c.PasswordAuthentication {
@ -306,9 +321,46 @@ func (c *Configuration) updateSupportedAuthentications() {
} }
} }
func (c *Configuration) loadFromProvider() error {
configs, err := dataprovider.GetConfigs()
if err != nil {
return fmt.Errorf("unable to load config from provider: %w", err)
}
configs.SetNilsToEmpty()
if len(configs.SFTPD.HostKeyAlgos) > 0 {
if len(c.HostKeyAlgorithms) == 0 {
c.HostKeyAlgorithms = preferredHostKeyAlgos
}
c.HostKeyAlgorithms = append(c.HostKeyAlgorithms, configs.SFTPD.HostKeyAlgos...)
}
c.Moduli = append(c.Moduli, configs.SFTPD.Moduli...)
if len(configs.SFTPD.KexAlgorithms) > 0 {
if len(c.KexAlgorithms) == 0 {
c.KexAlgorithms = preferredKexAlgos
}
c.KexAlgorithms = append(c.KexAlgorithms, configs.SFTPD.KexAlgorithms...)
}
if len(configs.SFTPD.Ciphers) > 0 {
if len(c.Ciphers) == 0 {
c.Ciphers = preferredCiphers
}
c.Ciphers = append(c.Ciphers, configs.SFTPD.Ciphers...)
}
if len(configs.SFTPD.MACs) > 0 {
if len(c.MACs) == 0 {
c.MACs = preferredMACs
}
c.MACs = append(c.MACs, configs.SFTPD.MACs...)
}
return nil
}
// Initialize the SFTP server and add a persistent listener to handle inbound SFTP connections. // Initialize the SFTP server and add a persistent listener to handle inbound SFTP connections.
func (c *Configuration) Initialize(configDir string) error { func (c *Configuration) Initialize(configDir string) error {
serviceStatus.Authentications = nil if err := c.loadFromProvider(); err != nil {
return fmt.Errorf("unable to load configs from provider: %w", err)
}
serviceStatus = ServiceStatus{}
serverConfig := c.getServerConfig() serverConfig := c.getServerConfig()
if !c.ShouldBind() { if !c.ShouldBind() {
@ -324,9 +376,7 @@ func (c *Configuration) Initialize(configDir string) error {
return err return err
} }
if err := c.loadModuli(configDir); err != nil { c.loadModuli(configDir)
return err
}
sftp.SetSFTPExtensions(sftpExtensions...) //nolint:errcheck // we configure valid SFTP Extensions so we cannot get an error sftp.SetSFTPExtensions(sftpExtensions...) //nolint:errcheck // we configure valid SFTP Extensions so we cannot get an error
@ -379,7 +429,7 @@ func (c *Configuration) Initialize(configDir string) error {
} }
func (c *Configuration) serve(listener net.Listener, serverConfig *ssh.ServerConfig) error { func (c *Configuration) serve(listener net.Listener, serverConfig *ssh.ServerConfig) error {
logger.Info(logSender, "", "server listener registered, address: %v", listener.Addr().String()) logger.Info(logSender, "", "server listener registered, address: %s", listener.Addr().String())
var tempDelay time.Duration // how long to sleep on accept failure var tempDelay time.Duration // how long to sleep on accept failure
for { for {
@ -416,37 +466,52 @@ func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig)
} }
for _, hostKeyAlgo := range c.HostKeyAlgorithms { for _, hostKeyAlgo := range c.HostKeyAlgorithms {
if !util.Contains(supportedHostKeyAlgos, hostKeyAlgo) { if !util.Contains(supportedHostKeyAlgos, hostKeyAlgo) {
return fmt.Errorf("unsupported host key algorithm %#v", hostKeyAlgo) return fmt.Errorf("unsupported host key algorithm %q", hostKeyAlgo)
} }
} }
serverConfig.HostKeyAlgorithms = c.HostKeyAlgorithms serverConfig.HostKeyAlgorithms = c.HostKeyAlgorithms
serviceStatus.HostKeyAlgos = c.HostKeyAlgorithms
if len(c.KexAlgorithms) > 0 { if len(c.KexAlgorithms) > 0 {
hasDHGroupKEX := util.Contains(supportedKexAlgos, kexDHGroupExchangeSHA256)
if !hasDHGroupKEX {
c.KexAlgorithms = util.Remove(c.KexAlgorithms, kexDHGroupExchangeSHA1)
c.KexAlgorithms = util.Remove(c.KexAlgorithms, kexDHGroupExchangeSHA256)
}
c.KexAlgorithms = util.RemoveDuplicates(c.KexAlgorithms, true) c.KexAlgorithms = util.RemoveDuplicates(c.KexAlgorithms, true)
for _, kex := range c.KexAlgorithms { for _, kex := range c.KexAlgorithms {
if !util.Contains(supportedKexAlgos, kex) { if !util.Contains(supportedKexAlgos, kex) {
return fmt.Errorf("unsupported key-exchange algorithm %#v", kex) return fmt.Errorf("unsupported key-exchange algorithm %q", kex)
} }
} }
serverConfig.KeyExchanges = c.KexAlgorithms serverConfig.KeyExchanges = c.KexAlgorithms
serviceStatus.KexAlgorithms = c.KexAlgorithms
} else {
serviceStatus.KexAlgorithms = preferredKexAlgos
} }
if len(c.Ciphers) > 0 { if len(c.Ciphers) > 0 {
c.Ciphers = util.RemoveDuplicates(c.Ciphers, true) c.Ciphers = util.RemoveDuplicates(c.Ciphers, true)
for _, cipher := range c.Ciphers { for _, cipher := range c.Ciphers {
if !util.Contains(supportedCiphers, cipher) { if !util.Contains(supportedCiphers, cipher) {
return fmt.Errorf("unsupported cipher %#v", cipher) return fmt.Errorf("unsupported cipher %q", cipher)
} }
} }
serverConfig.Ciphers = c.Ciphers serverConfig.Ciphers = c.Ciphers
serviceStatus.Ciphers = c.Ciphers
} else {
serviceStatus.Ciphers = preferredCiphers
} }
if len(c.MACs) > 0 { if len(c.MACs) > 0 {
c.MACs = util.RemoveDuplicates(c.MACs, true) c.MACs = util.RemoveDuplicates(c.MACs, true)
for _, mac := range c.MACs { for _, mac := range c.MACs {
if !util.Contains(supportedMACs, mac) { if !util.Contains(supportedMACs, mac) {
return fmt.Errorf("unsupported MAC algorithm %#v", mac) return fmt.Errorf("unsupported MAC algorithm %q", mac)
} }
} }
serverConfig.MACs = c.MACs serverConfig.MACs = c.MACs
serviceStatus.MACs = c.MACs
} else {
serviceStatus.MACs = preferredMACs
} }
return nil return nil
} }
@ -875,9 +940,11 @@ func (c *Configuration) checkHostKeyAutoGeneration(configDir string) error {
return nil return nil
} }
func (c *Configuration) loadModuli(configDir string) error { func (c *Configuration) loadModuli(configDir string) {
supportedKexAlgos = util.Remove(supportedKexAlgos, "diffie-hellman-group-exchange-sha1") supportedKexAlgos = util.Remove(supportedKexAlgos, kexDHGroupExchangeSHA1)
supportedKexAlgos = util.Remove(supportedKexAlgos, "diffie-hellman-group-exchange-sha256") supportedKexAlgos = util.Remove(supportedKexAlgos, kexDHGroupExchangeSHA256)
preferredKexAlgos = util.Remove(preferredKexAlgos, kexDHGroupExchangeSHA256)
c.Moduli = util.RemoveDuplicates(c.Moduli, false)
for _, m := range c.Moduli { for _, m := range c.Moduli {
m = strings.TrimSpace(m) m = strings.TrimSpace(m)
if !util.IsFileInputValid(m) { if !util.IsFileInputValid(m) {
@ -890,12 +957,19 @@ func (c *Configuration) loadModuli(configDir string) error {
} }
logger.Info(logSender, "", "loading moduli file %q", m) logger.Info(logSender, "", "loading moduli file %q", m)
if err := ssh.ParseModuli(m); err != nil { if err := ssh.ParseModuli(m); err != nil {
return err logger.Warn(logSender, "", "ignoring moduli file %q, error: %v", m, err)
continue
}
if !util.Contains(supportedKexAlgos, kexDHGroupExchangeSHA1) {
supportedKexAlgos = append(supportedKexAlgos, kexDHGroupExchangeSHA1)
}
if !util.Contains(supportedKexAlgos, kexDHGroupExchangeSHA256) {
supportedKexAlgos = append(supportedKexAlgos, kexDHGroupExchangeSHA256)
}
if !util.Contains(preferredKexAlgos, kexDHGroupExchangeSHA256) {
preferredKexAlgos = append(preferredKexAlgos, kexDHGroupExchangeSHA256)
} }
supportedKexAlgos = append(supportedKexAlgos, "diffie-hellman-group-exchange-sha1",
"diffie-hellman-group-exchange-sha256")
} }
return nil
} }
// If no host keys are defined we try to use or generate the default ones. // If no host keys are defined we try to use or generate the default ones.
@ -934,7 +1008,7 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
Fingerprint: ssh.FingerprintSHA256(private.PublicKey()), Fingerprint: ssh.FingerprintSHA256(private.PublicKey()),
} }
serviceStatus.HostKeys = append(serviceStatus.HostKeys, k) serviceStatus.HostKeys = append(serviceStatus.HostKeys, k)
logger.Info(logSender, "", "Host key %#v loaded, type %#v, fingerprint %#v", hostKey, logger.Info(logSender, "", "Host key %q loaded, type %q, fingerprint %q", hostKey,
private.PublicKey().Type(), k.Fingerprint) private.PublicKey().Type(), k.Fingerprint)
// Add private key to the server configuration. // Add private key to the server configuration.
@ -943,7 +1017,7 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
signer, err := ssh.NewCertSigner(cert, private) signer, err := ssh.NewCertSigner(cert, private)
if err == nil { if err == nil {
serverConfig.AddHostKey(signer) serverConfig.AddHostKey(signer)
logger.Info(logSender, "", "Host certificate loaded for host key %#v, fingerprint %#v", logger.Info(logSender, "", "Host certificate loaded for host key %q, fingerprint %q",
hostKey, ssh.FingerprintSHA256(signer.PublicKey())) hostKey, ssh.FingerprintSHA256(signer.PublicKey()))
} }
} }

View file

@ -57,6 +57,10 @@ type ServiceStatus struct {
SSHCommands []string `json:"ssh_commands"` SSHCommands []string `json:"ssh_commands"`
HostKeys []HostKey `json:"host_keys"` HostKeys []HostKey `json:"host_keys"`
Authentications []string `json:"authentications"` Authentications []string `json:"authentications"`
HostKeyAlgos []string `json:"host_key_algos"`
MACs []string `json:"macs"`
KexAlgorithms []string `json:"kex_algorithms"`
Ciphers []string `json:"ciphers"`
} }
// GetSSHCommandsAsString returns enabled SSH commands as comma separated string // GetSSHCommandsAsString returns enabled SSH commands as comma separated string
@ -69,6 +73,26 @@ func (s *ServiceStatus) GetSupportedAuthsAsString() string {
return strings.Join(s.Authentications, ", ") return strings.Join(s.Authentications, ", ")
} }
// GetHostKeyAlgosAsString returns the enabled host keys algorithms as comma separated string
func (s *ServiceStatus) GetHostKeyAlgosAsString() string {
return strings.Join(s.HostKeyAlgos, ", ")
}
// GetMACsAsString returns the enabled MAC algorithms as comma separated string
func (s *ServiceStatus) GetMACsAsString() string {
return strings.Join(s.MACs, ", ")
}
// GetKEXsAsString returns the enabled KEX algorithms as comma separated string
func (s *ServiceStatus) GetKEXsAsString() string {
return strings.Join(s.KexAlgorithms, ", ")
}
// GetCiphersAsString returns the enabled ciphers as comma separated string
func (s *ServiceStatus) GetCiphersAsString() string {
return strings.Join(s.Ciphers, ", ")
}
// GetStatus returns the server status // GetStatus returns the server status
func GetStatus() ServiceStatus { func GetStatus() ServiceStatus {
return serviceStatus return serviceStatus

View file

@ -219,6 +219,12 @@ func TestMain(m *testing.M) {
os.Exit(1) os.Exit(1)
} }
err = dataprovider.UpdateConfigs(nil, "", "", "")
if err != nil {
logger.ErrorToConsole("error resetting configs: %v", err)
os.Exit(1)
}
err = common.Initialize(commonConf, 0) err = common.Initialize(commonConf, 0)
if err != nil { if err != nil {
logger.WarnToConsole("error initializing common: %v", err) logger.WarnToConsole("error initializing common: %v", err)
@ -401,12 +407,6 @@ func TestInitialization(t *testing.T) {
assert.True(t, sftpdConf.Bindings[0].HasProxy()) assert.True(t, sftpdConf.Bindings[0].HasProxy())
err = sftpdConf.Initialize(configDir) err = sftpdConf.Initialize(configDir)
assert.Error(t, err) assert.Error(t, err)
sftpdConf.Moduli = []string{"missing moduli file"}
err = sftpdConf.Initialize(configDir)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "unable to open moduli file")
}
sftpdConf.Moduli = nil
sftpdConf.HostKeys = []string{"missing key"} sftpdConf.HostKeys = []string{"missing key"}
err = sftpdConf.Initialize(configDir) err = sftpdConf.Initialize(configDir)
assert.Error(t, err) assert.Error(t, err)
@ -429,11 +429,13 @@ func TestInitialization(t *testing.T) {
if assert.Error(t, err) { if assert.Error(t, err) {
assert.Contains(t, err.Error(), "unsupported MAC algorithm") assert.Contains(t, err.Error(), "unsupported MAC algorithm")
} }
sftpdConf.KexAlgorithms = []string{"not a KEX"} sftpdConf.MACs = nil
sftpdConf.KexAlgorithms = []string{"diffie-hellman-group-exchange-sha1", "not a KEX"}
err = sftpdConf.Initialize(configDir) err = sftpdConf.Initialize(configDir)
if assert.Error(t, err) { if assert.Error(t, err) {
assert.Contains(t, err.Error(), "unsupported key-exchange algorithm") assert.Contains(t, err.Error(), "unsupported key-exchange algorithm")
} }
sftpdConf.KexAlgorithms = nil
sftpdConf.HostKeyAlgorithms = []string{"not a host key algo"} sftpdConf.HostKeyAlgorithms = []string{"not a host key algo"}
err = sftpdConf.Initialize(configDir) err = sftpdConf.Initialize(configDir)
if assert.Error(t, err) { if assert.Error(t, err) {
@ -495,6 +497,17 @@ func TestInitialization(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
err = sftpdConf.Initialize(configDir) err = sftpdConf.Initialize(configDir)
assert.Error(t, err) assert.Error(t, err)
err = dataprovider.Close()
assert.NoError(t, err)
err = sftpdConf.Initialize(configDir)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "unable to load configs from provider")
}
err = config.LoadConfig(configDir, "")
assert.NoError(t, err)
providerConf := config.GetProviderConf()
err = dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
} }
func TestBasicSFTPHandling(t *testing.T) { func TestBasicSFTPHandling(t *testing.T) {
@ -562,6 +575,10 @@ func TestBasicSFTPHandling(t *testing.T) {
assert.NotEmpty(t, sshCommands) assert.NotEmpty(t, sshCommands)
sshAuths := status.GetSupportedAuthsAsString() sshAuths := status.GetSupportedAuthsAsString()
assert.NotEmpty(t, sshAuths) assert.NotEmpty(t, sshAuths)
assert.NotEmpty(t, status.GetHostKeyAlgosAsString())
assert.NotEmpty(t, status.GetMACsAsString())
assert.NotEmpty(t, status.GetKEXsAsString())
assert.NotEmpty(t, status.GetCiphersAsString())
} }
func TestBasicSFTPFsHandling(t *testing.T) { func TestBasicSFTPFsHandling(t *testing.T) {

View file

@ -22,10 +22,12 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"path/filepath" "path/filepath"
"sync"
"time" "time"
"github.com/wneessen/go-mail" "github.com/wneessen/go-mail"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/logger" "github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/util" "github.com/drakkan/sftpgo/v2/internal/util"
) )
@ -47,16 +49,93 @@ const (
templateEmailDir = "email" templateEmailDir = "email"
templatePasswordReset = "reset-password.html" templatePasswordReset = "reset-password.html"
templatePasswordExpiration = "password-expiration.html" templatePasswordExpiration = "password-expiration.html"
dialTimeout = 10 * time.Second
) )
var ( var (
config *Config config = &activeConfig{}
initialConfig *Config
emailTemplates = make(map[string]*template.Template) emailTemplates = make(map[string]*template.Template)
) )
type activeConfig struct {
sync.RWMutex
config *Config
}
func (c *activeConfig) isEnabled() bool {
c.RLock()
defer c.RUnlock()
return c.config != nil && c.config.Host != ""
}
func (c *activeConfig) Set(cfg *dataprovider.SMTPConfigs) {
var config *Config
if cfg != nil {
config = &Config{
Host: cfg.Host,
Port: cfg.Port,
From: cfg.From,
User: cfg.User,
Password: cfg.Password.GetPayload(),
AuthType: cfg.AuthType,
Encryption: cfg.Encryption,
Domain: cfg.Domain,
}
}
c.Lock()
defer c.Unlock()
if config != nil && config.Host != "" {
if c.config != nil && c.config.isEqual(config) {
return
}
c.config = config
logger.Info(logSender, "", "activated new config, server %s:%d", c.config.Host, c.config.Port)
} else {
logger.Debug(logSender, "", "activating initial config")
c.config = initialConfig
if c.config == nil || c.config.Host == "" {
logger.Debug(logSender, "", "configuration disabled, email capabilities will not be available")
}
}
}
func (c *activeConfig) getSMTPClientAndMsg(to []string, subject, body string, contentType EmailContentType,
attachments ...*mail.File,
) (*mail.Client, *mail.Msg, error) {
c.RLock()
defer c.RUnlock()
if c.config == nil || c.config.Host == "" {
return nil, nil, errors.New("smtp: not configured")
}
return c.config.getSMTPClientAndMsg(to, subject, body, contentType, attachments...)
}
func (c *activeConfig) sendEmail(to []string, subject, body string, contentType EmailContentType, attachments ...*mail.File) error {
client, msg, err := c.getSMTPClientAndMsg(to, subject, body, contentType, attachments...)
if err != nil {
return err
}
ctx, cancelFn := context.WithTimeout(context.Background(), dialTimeout)
defer cancelFn()
return client.DialAndSendWithContext(ctx, msg)
}
// IsEnabled returns true if an SMTP server is configured // IsEnabled returns true if an SMTP server is configured
func IsEnabled() bool { func IsEnabled() bool {
return config != nil return config.isEnabled()
}
// Activate sets the specified config as active
func Activate(c *dataprovider.SMTPConfigs) {
config.Set(c)
} }
// Config defines the SMTP configuration to use to send emails // Config defines the SMTP configuration to use to send emails
@ -89,34 +168,65 @@ type Config struct {
TemplatesPath string `json:"templates_path" mapstructure:"templates_path"` TemplatesPath string `json:"templates_path" mapstructure:"templates_path"`
} }
func (c *Config) isEqual(other *Config) bool {
if c.Host != other.Host {
return false
}
if c.Port != other.Port {
return false
}
if c.From != other.From {
return false
}
if c.User != other.User {
return false
}
if c.Password != other.Password {
return false
}
if c.AuthType != other.AuthType {
return false
}
if c.Encryption != other.Encryption {
return false
}
if c.Domain != other.Domain {
return false
}
return true
}
// Initialize initialized and validates the SMTP configuration // Initialize initialized and validates the SMTP configuration
func (c *Config) Initialize(configDir string) error { func (c *Config) Initialize(configDir string) error {
config = nil if c.TemplatesPath == "" {
logger.Debug(logSender, "", "templates path empty, using default")
c.TemplatesPath = "templates"
}
templatesPath := util.FindSharedDataPath(c.TemplatesPath, configDir)
if templatesPath == "" {
return fmt.Errorf("smtp: invalid templates path %q", templatesPath)
}
loadTemplates(filepath.Join(templatesPath, templateEmailDir))
if c.Host == "" { if c.Host == "" {
logger.Debug(logSender, "", "configuration disabled, email capabilities will not be available") return loadConfigFromProvider()
return nil
} }
if c.Port <= 0 || c.Port > 65535 { if c.Port <= 0 || c.Port > 65535 {
return fmt.Errorf("smtp: invalid port %v", c.Port) return fmt.Errorf("smtp: invalid port %d", c.Port)
} }
if c.AuthType < 0 || c.AuthType > 2 { if c.AuthType < 0 || c.AuthType > 2 {
return fmt.Errorf("smtp: invalid auth type %v", c.AuthType) return fmt.Errorf("smtp: invalid auth type %d", c.AuthType)
} }
if c.Encryption < 0 || c.Encryption > 2 { if c.Encryption < 0 || c.Encryption > 2 {
return fmt.Errorf("smtp: invalid encryption %v", c.Encryption) return fmt.Errorf("smtp: invalid encryption %d", c.Encryption)
} }
if c.From == "" && c.User == "" { if c.From == "" && c.User == "" {
return fmt.Errorf(`smtp: from address and user cannot both be empty`) return fmt.Errorf(`smtp: from address and user cannot both be empty`)
} }
templatesPath := util.FindSharedDataPath(c.TemplatesPath, configDir) initialConfig = c
if templatesPath == "" { config.Set(nil)
return fmt.Errorf("smtp: invalid templates path %#v", templatesPath)
}
loadTemplates(filepath.Join(templatesPath, templateEmailDir))
config = c
logger.Debug(logSender, "", "configuration successfully initialized, host: %q, port: %d, username: %q, auth: %d, encryption: %d, helo: %q", logger.Debug(logSender, "", "configuration successfully initialized, host: %q, port: %d, username: %q, auth: %d, encryption: %d, helo: %q",
config.Host, config.Port, config.User, config.AuthType, config.Encryption, config.Domain) c.Host, c.Port, c.User, c.AuthType, c.Encryption, c.Domain)
return nil return loadConfigFromProvider()
} }
func (c *Config) getMailClientOptions() []mail.Option { func (c *Config) getMailClientOptions() []mail.Option {
@ -130,14 +240,14 @@ func (c *Config) getMailClientOptions() []mail.Option {
default: default:
options = append(options, mail.WithTLSPolicy(mail.NoTLS)) options = append(options, mail.WithTLSPolicy(mail.NoTLS))
} }
if config.User != "" { if c.User != "" {
options = append(options, mail.WithUsername(config.User)) options = append(options, mail.WithUsername(c.User))
} }
if config.Password != "" { if c.Password != "" {
options = append(options, mail.WithPassword(config.Password)) options = append(options, mail.WithPassword(c.Password))
} }
if config.User != "" || config.Password != "" { if c.User != "" || c.Password != "" {
switch config.AuthType { switch c.AuthType {
case 1: case 1:
options = append(options, mail.WithSMTPAuth(mail.SMTPAuthLogin)) options = append(options, mail.WithSMTPAuth(mail.SMTPAuthLogin))
case 2: case 2:
@ -146,14 +256,63 @@ func (c *Config) getMailClientOptions() []mail.Option {
options = append(options, mail.WithSMTPAuth(mail.SMTPAuthPlain)) options = append(options, mail.WithSMTPAuth(mail.SMTPAuthPlain))
} }
} }
if config.Domain != "" { if c.Domain != "" {
options = append(options, mail.WithHELO(config.Domain)) options = append(options, mail.WithHELO(c.Domain))
} }
return options return options
} }
func (c *Config) getSMTPClientAndMsg(to []string, subject, body string, contentType EmailContentType,
attachments ...*mail.File) (*mail.Client, *mail.Msg, error) {
msg := mail.NewMsg()
var from string
if c.From != "" {
from = c.From
} else {
from = c.User
}
if err := msg.From(from); err != nil {
return nil, nil, fmt.Errorf("invalid from address: %w", err)
}
if err := msg.To(to...); err != nil {
return nil, nil, err
}
msg.Subject(subject)
msg.SetDate()
msg.SetMessageID()
msg.SetAttachements(attachments)
switch contentType {
case EmailContentTypeTextPlain:
msg.SetBodyString(mail.TypeTextPlain, body)
case EmailContentTypeTextHTML:
msg.SetBodyString(mail.TypeTextHTML, body)
default:
return nil, nil, fmt.Errorf("smtp: unsupported body content type %v", contentType)
}
client, err := mail.NewClient(c.Host, c.getMailClientOptions()...)
if err != nil {
return nil, nil, fmt.Errorf("unable to create mail client: %w", err)
}
return client, msg, nil
}
// SendEmail tries to send an email using the specified parameters
func (c *Config) SendEmail(to []string, subject, body string, contentType EmailContentType, attachments ...*mail.File) error {
client, msg, err := c.getSMTPClientAndMsg(to, subject, body, contentType, attachments...)
if err != nil {
return err
}
ctx, cancelFn := context.WithTimeout(context.Background(), dialTimeout)
defer cancelFn()
return client.DialAndSendWithContext(ctx, msg)
}
func loadTemplates(templatesPath string) { func loadTemplates(templatesPath string) {
logger.Debug(logSender, "", "loading templates from %#v", templatesPath) logger.Debug(logSender, "", "loading templates from %q", templatesPath)
passwordResetPath := filepath.Join(templatesPath, templatePasswordReset) passwordResetPath := filepath.Join(templatesPath, templatePasswordReset)
pwdResetTmpl := util.LoadTemplate(nil, passwordResetPath) pwdResetTmpl := util.LoadTemplate(nil, passwordResetPath)
@ -182,43 +341,26 @@ func RenderPasswordExpirationTemplate(buf *bytes.Buffer, data any) error {
// SendEmail tries to send an email using the specified parameters. // SendEmail tries to send an email using the specified parameters.
func SendEmail(to []string, subject, body string, contentType EmailContentType, attachments ...*mail.File) error { func SendEmail(to []string, subject, body string, contentType EmailContentType, attachments ...*mail.File) error {
if !IsEnabled() { return config.sendEmail(to, subject, body, contentType, attachments...)
return errors.New("smtp: not configured") }
}
m := mail.NewMsg() // ReloadProviderConf reloads the configuration from the provider
// and apply it if different from the active one
var from string func ReloadProviderConf() {
if config.From != "" { loadConfigFromProvider() //nolint:errcheck
from = config.From }
} else {
from = config.User func loadConfigFromProvider() error {
} configs, err := dataprovider.GetConfigs()
if err := m.From(from); err != nil { if err != nil {
return fmt.Errorf("invalid from address: %w", err) logger.Error(logSender, "", "unable to load config from provider: %v", err)
} return fmt.Errorf("smtp: unable to load config from provider: %w", err)
if err := m.To(to...); err != nil { }
return err configs.SetNilsToEmpty()
} if err := configs.SMTP.Password.TryDecrypt(); err != nil {
m.Subject(subject) logger.Error(logSender, "", "unable to decrypt password: %v", err)
m.SetDate() return fmt.Errorf("smtp: unable to decrypt password: %w", err)
m.SetMessageID() }
m.SetAttachements(attachments) config.Set(configs.SMTP)
return nil
switch contentType {
case EmailContentTypeTextPlain:
m.SetBodyString(mail.TypeTextPlain, body)
case EmailContentTypeTextHTML:
m.SetBodyString(mail.TypeTextHTML, body)
default:
return fmt.Errorf("smtp: unsupported body content type %v", contentType)
}
c, err := mail.NewClient(config.Host, config.getMailClientOptions()...)
if err != nil {
return fmt.Errorf("unable to create mail client: %w", err)
}
ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second)
defer cancelFn()
return c.DialAndSendWithContext(ctx, m)
} }

View file

@ -6263,6 +6263,22 @@ components:
type: array type: array
items: items:
$ref: '#/components/schemas/SSHAuthentications' $ref: '#/components/schemas/SSHAuthentications'
host_key_algos:
type: array
items:
type: string
macs:
type: array
items:
type: string
kex_algorithms:
type: array
items:
type: string
ciphers:
type: array
items:
type: string
FTPPassivePortRange: FTPPassivePortRange:
type: object type: object
properties: properties:

View file

@ -125,7 +125,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div id="collapseIPManager" class="collapse {{if .IsIPManagerPage}}show{{end}}" aria-labelledby="headingIPManager" data-parent="#accordionSidebar"> <div id="collapseIPManager" class="collapse {{if .IsIPManagerPage}}show{{end}}" aria-labelledby="headingIPManager" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded"> <div class="bg-white py-2 collapse-inner rounded">
{{ if .LoggedAdmin.HasPermission "manage_ip_lists"}} {{ if .LoggedAdmin.HasPermission "manage_ip_lists"}}
<a class="collapse-item {{if eq .CurrentURL .IPListsURL}}active{{end}}" href="{{.IPListsURL}}">{{.IPListsTitle}}</a> <a class="collapse-item {{if eq .CurrentURL .IPListsURL}}active{{end}}" href="{{.IPListsURL}}">{{.IPListsTitle}}</a>
{{end}} {{end}}
{{ if and .HasDefender (.LoggedAdmin.HasPermission "view_defender")}} {{ if and .HasDefender (.LoggedAdmin.HasPermission "view_defender")}}
<a class="collapse-item {{if eq .CurrentURL .DefenderURL}}active{{end}}" href="{{.DefenderURL}}">{{.DefenderTitle}}</a> <a class="collapse-item {{if eq .CurrentURL .DefenderURL}}active{{end}}" href="{{.DefenderURL}}">{{.DefenderTitle}}</a>
@ -151,27 +151,29 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</li> </li>
{{end}} {{end}}
{{ if and .HasSearcher (.LoggedAdmin.HasPermission "view_events")}} {{ if or (.LoggedAdmin.HasPermission "manage_system") (.LoggedAdmin.HasPermission "view_status") (and .HasSearcher (.LoggedAdmin.HasPermission "view_events"))}}
<li class="nav-item {{if eq .CurrentURL .EventsURL}}active{{end}}"> <li class="nav-item {{if .IsServerManagerPage}}active{{end}}">
<a class="nav-link" href="{{.EventsURL}}"> <a class="nav-link {{if not .IsServerManagerPage}}collapsed{{end}}" href="#" data-toggle="collapse" data-target="#collapseServerManager"
<i class="fas fa-clipboard-list"></i> aria-expanded="true" aria-controls="collapseServerManager">
<span>{{.EventsTitle}}</span></a> <i class="fas fa-tools"></i>
</li> <span>Server Manager</span>
{{end}} </a>
<div id="collapseServerManager" class="collapse {{if .IsServerManagerPage}}show{{end}}" aria-labelledby="headingServerManager" data-parent="#accordionSidebar">
{{ if .LoggedAdmin.HasPermission "manage_system"}} <div class="bg-white py-2 collapse-inner rounded">
<li class="nav-item {{if eq .CurrentURL .MaintenanceURL}}active{{end}}"> {{ if .LoggedAdmin.HasPermission "manage_system"}}
<a class="nav-link" href="{{.MaintenanceURL}}"> <a class="collapse-item {{if eq .CurrentURL .ConfigsURL}}active{{end}}" href="{{.ConfigsURL}}">{{.ConfigsTitle}}</a>
<i class="fas fa-wrench"></i> {{end}}
<span>{{.MaintenanceTitle}}</span></a> {{ if and .HasSearcher (.LoggedAdmin.HasPermission "view_events")}}
</li> <a class="collapse-item {{if eq .CurrentURL .EventsURL}}active{{end}}" href="{{.EventsURL}}">{{.EventsTitle}}</a>
{{end}} {{end}}
{{ if .LoggedAdmin.HasPermission "manage_system"}}
{{ if .LoggedAdmin.HasPermission "view_status"}} <a class="collapse-item {{if eq .CurrentURL .MaintenanceURL}}active{{end}}" href="{{.MaintenanceURL}}">{{.MaintenanceTitle}}</a>
<li class="nav-item {{if eq .CurrentURL .StatusURL}}active{{end}}"> {{end}}
<a class="nav-link" href="{{.StatusURL}}"> {{ if .LoggedAdmin.HasPermission "view_status"}}
<i class="fas fa-info-circle"></i> <a class="collapse-item {{if eq .CurrentURL .StatusURL}}active{{end}}" href="{{.StatusURL}}">{{.StatusTitle}}</a>
<span>{{.StatusTitle}}</span></a> {{end}}
</div>
</div>
</li> </li>
{{end}} {{end}}

View file

@ -0,0 +1,327 @@
<!--
Copyright (C) 2019-2023 Nicola Murino
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
{{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{define "extra_css"}}
<link href="{{.StaticURL}}/vendor/bootstrap-select/css/bootstrap-select.min.css" rel="stylesheet">
{{end}}
{{define "page_body"}}
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">{{.Title}}</h6>
</div>
<div class="card-body">
{{if .Error}}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
{{.Error}}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{{end}}
<form id="configs_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<div class="accordion" id="accordionConfigs">
<div class="card">
<div class="card-header" id="headingSFTPD">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse"
data-target="#collapseSFTPD" aria-expanded="true" aria-controls="collapseSFTPD">
<h6 class="m-0 font-weight-bold text-primary">SFTP</h6>
</button>
</h2>
</div>
<div id="collapseSFTPD" class="collapse {{if eq .ConfigSection 1}}show{{end}}" aria-labelledby="headingSFTPD" data-parent="#accordionConfigs">
<div class="card-body">
<div id="configs-sftp-info" class="card mb-3 border-left-info">
<div class="card-body">Here you can enable algorithms disabled by default. You don't need to set values already defined using env vars or config file. A service restart is required to apply changes.</div>
</div>
<div class="form-group row">
<label for="idHostKeyAlgos" class="col-sm-2 col-form-label">Host Key Algos</label>
<div class="col-sm-10">
<select class="form-control selectpicker" id="idHostKeyAlgos" name="sftp_host_key_algos" multiple>
{{range $val := .Configs.SFTPD.GetSupportedHostKeyAlgos}}
<option value="{{$val}}" {{range $algo :=$.Configs.SFTPD.HostKeyAlgos }}{{if eq $algo $val}}selected{{end}}{{end}}>{{$val}}</option>
{{end}}
</select>
</div>
</div>
<div class="form-group row">
<label for="idModuli" class="col-sm-2 col-form-label">Moduli</label>
<div class="col-sm-10">
<textarea class="form-control" id="idModuli" name="sftp_moduli" rows="2" placeholder=""
aria-describedby="moduliHelpBlock">{{.Configs.SFTPD.GetModuliAsString}}</textarea>
<small id="moduliHelpBlock" class="form-text text-muted">
Comma separated moduli file paths. Invalid/missing paths are silently ignored. Moduli are required to enable Diffie-Helmann Group Exchange KEX algos
</small>
</div>
</div>
<div class="form-group row">
<label for="idKEXAlgos" class="col-sm-2 col-form-label">KEX Algos</label>
<div class="col-sm-10">
<select class="form-control selectpicker" id="idKEXAlgos" name="sftp_kex_algos" multiple>
{{range $val := .Configs.SFTPD.GetSupportedKEXAlgos}}
<option value="{{$val}}" {{range $algo :=$.Configs.SFTPD.KexAlgorithms }}{{if eq $algo $val}}selected{{end}}{{end}}>{{$val}}</option>
{{end}}
</select>
</div>
</div>
<div class="form-group row">
<label for="idCiphers" class="col-sm-2 col-form-label">Ciphers</label>
<div class="col-sm-10">
<select class="form-control selectpicker" id="idCiphers" name="sftp_ciphers" multiple>
{{range $val := .Configs.SFTPD.GetSupportedCiphers}}
<option value="{{$val}}" {{range $algo :=$.Configs.SFTPD.Ciphers }}{{if eq $algo $val}}selected{{end}}{{end}}>{{$val}}</option>
{{end}}
</select>
</div>
</div>
<div class="form-group row">
<label for="idMACAlgos" class="col-sm-2 col-form-label">MAC Algos</label>
<div class="col-sm-10">
<select class="form-control selectpicker" id="idMACAlgos" name="sftp_macs" multiple>
{{range $val := .Configs.SFTPD.GetSupportedMACs}}
<option value="{{$val}}" {{range $algo :=$.Configs.SFTPD.MACs }}{{if eq $algo $val}}selected{{end}}{{end}}>{{$val}}</option>
{{end}}
</select>
</div>
</div>
<div class="col-sm-12 text-right px-0">
<button type="submit" class="btn btn-primary mt-3 ml-3 px-5" name="form_action" value="sftp_submit">Submit</button>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="headingSMTP">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse"
data-target="#collapseSMTP" aria-expanded="true" aria-controls="collapseSMTP">
<h6 class="m-0 font-weight-bold text-primary">SMTP</h6>
</button>
</h2>
</div>
<div id="collapseSMTP" class="collapse {{if eq .ConfigSection 2}}show{{end}}" aria-labelledby="headingSMTP" data-parent="#accordionConfigs">
<div class="card-body">
<div id="configs-smtp-info" class="card mb-3 border-left-info">
<div class="card-body">Set the SMTP configuration replacing the one defined using env vars or config file if any.</div>
</div>
<div class="form-group row">
<label for="idSMTPHost" class="col-sm-2 col-form-label">Server name</label>
<div class="col-sm-5">
<input type="text" class="form-control" id="idSMTPHost" name="smtp_host" placeholder=""
value="{{.Configs.SMTP.Host}}" maxlength="512" spellcheck="false" aria-describedby="smtpHostHelpBlock">
<small id="smtpHostHelpBlock" class="form-text text-muted">
If empty the configuration is disabled
</small>
</div>
<div class="col-sm-1"></div>
<label for="idSMTPPort" class="col-sm-2 col-form-label">Port</label>
<div class="col-sm-2">
<input type="number" min="0" max="65535" class="form-control" id="idSMTPPort" name="smtp_port" placeholder=""
value="{{.Configs.SMTP.Port}}">
</div>
</div>
<div class="form-group row">
<label for="idSMTPUsername" class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idSMTPUsername" name="smtp_username" placeholder=""
value="{{.Configs.SMTP.User}}" maxlength="255" spellcheck="false">
</div>
</div>
<div class="form-group row">
<label for="idSMTPPassword" class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idSMTPPassword" name="smtp_password" placeholder="" autocomplete="new-password" spellcheck="false"
value="{{if .Configs.SMTP.Password.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.Configs.SMTP.Password.GetPayload}}{{end}}">
</div>
</div>
<div class="form-group row">
<label for="idSMTPAuth" class="col-sm-2 col-form-label">Auth</label>
<div class="col-sm-3">
<select class="form-control selectpicker" id="idSMTPAuth" name="smtp_auth">
<option value="0" {{if eq .Configs.SMTP.AuthType 0}}selected{{end}}>Plain</option>
<option value="1" {{if eq .Configs.SMTP.AuthType 1}}selected{{end}}>Login</option>
<option value="2" {{if eq .Configs.SMTP.AuthType 2}}selected{{end}}>CRAM-MD5</option>
</select>
</div>
<div class="col-sm-2"></div>
<label for="idSMTPEncryption" class="col-sm-2 col-form-label">Encryption</label>
<div class="col-sm-3">
<select class="form-control selectpicker" id="idSMTPEncryption" name="smtp_encryption">
<option value="0" {{if eq .Configs.SMTP.Encryption 0}}selected{{end}}>None</option>
<option value="1" {{if eq .Configs.SMTP.Encryption 1}}selected{{end}}>TLS</option>
<option value="2" {{if eq .Configs.SMTP.Encryption 2}}selected{{end}}>STARTTLS</option>
</select>
</div>
</div>
<div class="form-group row">
<label for="idSMTPFrom" class="col-sm-2 col-form-label">From</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idSMTPFrom" name="smtp_from" placeholder=""
value="{{.Configs.SMTP.From}}" maxlength="512" spellcheck="false">
</div>
</div>
<div class="form-group row">
<label for="idSMTPDomain" class="col-sm-2 col-form-label">Domain</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idSMTPDomain" name="smtp_domain" placeholder=""
value="{{.Configs.SMTP.Domain}}" aria-describedby="smtpDomainHelpBlock">
<small id="smtpDomainHelpBlock" class="form-text text-muted">
HELO domain. Leave blank to use the server hostname
</small>
</div>
</div>
<div class="form-group row">
<div class="col-sm-12">
<div class="input-group">
<input type="email" class="form-control float-right" id="idSMTPRecipient" placeholder="Test email recipient" aria-label="Test email recipient">
<div class="input-group-append">
<button class="btn btn-secondary px-5" onclick="testSMTP(event);">Test</button>
</div>
</div>
</div>
</div>
<div class="col-sm-12 text-right px-0">
<button type="submit" class="btn btn-primary mt-3 px-5" name="form_action" value="smtp_submit">Submit</button>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
{{end}}
{{define "dialog"}}
<div class="modal fade" id="spinnerModal" tabindex="-1" role="dialog" data-keyboard="false" data-backdrop="static">
<div class="modal-dialog modal-dialog-centered justify-content-center" role="document">
<span style="color: #333333;" class="fa fa-spinner fa-spin fa-3x"></span>
</div>
</div>
<div class="modal fade" id="smtpTestResultModal" tabindex="-1" role="dialog" aria-labelledby="smtpTestResultModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="smtpTestResultModal">
SMTP test result
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div id="smtpSuccessMsg" class="card mb-4 border-left-success" style="display: none;">
<div id="successTxt" class="card-body">No errors were reported while sending the test email. Please check your inbox to make sure.</div>
</div>
<div id="smtpErrorMsg" class="card mb-4 border-left-warning" style="display: none;">
<div id="smtpErrorTxt" class="card-body text-form-error"></div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" type="button" data-dismiss="modal">
OK
</button>
</div>
</div>
</div>
</div>
{{end}}
{{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
<script type="text/javascript">
var spinnerDone = false;
function testSMTP(event){
event.preventDefault();
let recipient = $('#idSMTPRecipient').val();
if (!recipient){
$('#smtpErrorTxt').text('Set a recipient to send the test mail to.');
$('#smtpErrorMsg').show();
$('#smtpTestResultModal').modal('show');
return;
}
$('#smtpSuccessMsg').hide();
$('#smtpErrorMsg').hide();
$('#spinnerModal').modal('show');
$.ajax({
url: "{{.ConfigsURL}}/smtp/test",
type: 'POST',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"host": $('#idSMTPHost').val(),"port": parseInt($('#idSMTPPort').val()),"from": $('#idSMTPFrom').val(),"user": $('#idSMTPUsername').val(),"password": $('#idSMTPPassword').val(),"auth_type": parseInt($('#idSMTPAuth').val()),"encryption": parseInt($('#idSMTPEncryption').val()), "domain": $('#idSMTPDomain').val(),"recipient": recipient}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
timeout: 15000,
success: function (result) {
$('#spinnerModal').modal('hide');
spinnerDone = true;
$('#smtpSuccessMsg').show();
$('#smtpTestResultModal').modal('show');
},
error: function ($xhr, textStatus, errorThrown) {
$('#spinnerModal').modal('hide');
spinnerDone = true;
let txt = "SMTP connection failed";
if ($xhr) {
let json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
}
}
}
$('#smtpErrorTxt').text(txt);
$('#smtpErrorMsg').show();
$('#smtpTestResultModal').modal('show');
}
});
}
$(document).ready(function () {
$('#spinnerModal').on('shown.bs.modal', function () {
if (spinnerDone){
$('#spinnerModal').modal('hide');
}
});
});
</script>
{{end}}

View file

@ -47,6 +47,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
Fingerprint: "{{.Fingerprint}}" Fingerprint: "{{.Fingerprint}}"
<br> <br>
{{end}} {{end}}
<br>
Host Key algorithms: "{{.Status.SSH.GetHostKeyAlgosAsString}}"
<br><br>
MAC algorithms: "{{.Status.SSH.GetMACsAsString}}"
<br><br>
KEX algorithms: "{{.Status.SSH.GetKEXsAsString}}"
<br><br>
Ciphers: "{{.Status.SSH.GetCiphersAsString}}"
<br>
{{end}} {{end}}
</p> </p>
</div> </div>

View file

@ -10,17 +10,16 @@ require (
require ( require (
github.com/fatih/color v1.14.1 // indirect github.com/fatih/color v1.14.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/hashicorp/go-hclog v1.4.0 // indirect github.com/hashicorp/go-hclog v1.4.0 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/oklog/run v1.1.0 // indirect github.com/oklog/run v1.1.0 // indirect
golang.org/x/net v0.6.0 // indirect golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect golang.org/x/text v0.7.0 // indirect
google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc // indirect google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44 // indirect
google.golang.org/grpc v1.53.0 // indirect google.golang.org/grpc v1.53.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.28.1 // indirect
) )

View file

@ -1,7 +1,6 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 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.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
@ -10,7 +9,6 @@ github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/go-hclog v1.4.0 h1:ctuWFGrhFha8BnnzxqeRGidlEcQkDyL5u8J8t5eA11I= github.com/hashicorp/go-hclog v1.4.0 h1:ctuWFGrhFha8BnnzxqeRGidlEcQkDyL5u8J8t5eA11I=
github.com/hashicorp/go-hclog v1.4.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-hclog v1.4.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-plugin v1.4.8 h1:CHGwpxYDOttQOY7HOWgETU9dyVjOXzniXDqJcYJE1zM= github.com/hashicorp/go-plugin v1.4.8 h1:CHGwpxYDOttQOY7HOWgETU9dyVjOXzniXDqJcYJE1zM=
@ -24,7 +22,6 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 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.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@ -34,38 +31,26 @@ github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sftpgo/sdk v0.1.3-0.20221208080405-e682ae869318 h1:oDr2it5L9nh13+P3BzyIqx89gN9Kfrsdk4cw42HaIuQ=
github.com/sftpgo/sdk v0.1.3-0.20221208080405-e682ae869318/go.mod h1:3GpW3Qy8IHH6kex0ny+Y6ayeYb9OJxz8Pxh3IZgAs2E=
github.com/sftpgo/sdk v0.1.3-0.20230212154322-556375985d8c h1:SiWQZe99SZ/O4QSIsxzL91NgwFJNoo4IJ31cazUrYh4= github.com/sftpgo/sdk v0.1.3-0.20230212154322-556375985d8c h1:SiWQZe99SZ/O4QSIsxzL91NgwFJNoo4IJ31cazUrYh4=
github.com/sftpgo/sdk v0.1.3-0.20230212154322-556375985d8c/go.mod h1:B1lPGb05WtvvrX5IuhHrSjWdRT867qBaoxlS2Q9+1bA= github.com/sftpgo/sdk v0.1.3-0.20230212154322-556375985d8c/go.mod h1:B1lPGb05WtvvrX5IuhHrSjWdRT867qBaoxlS2Q9+1bA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/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-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/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-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-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 h1:jmIfw8+gSvXcZSgaFAGyInDXeWzUhvYH57G/5GKMn70= google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44 h1:EfLuoKW5WfkgVdDy7dTK8qSbH37AX5mj/MFh+bGPz14=
google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA=
google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc h1:ijGwO+0vL2hJt5gaygqP2j6PfflOBrRot0IczKbmtio=
google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
google.golang.org/grpc v1.51.0 h1:E1eGv1FTqoLIdnBCZufiSHgKjlqG6fKFf6pPWtMTh8U=
google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww=
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=

View file

@ -16,10 +16,10 @@ require (
github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/oklog/run v1.1.0 // indirect github.com/oklog/run v1.1.0 // indirect
golang.org/x/net v0.6.0 // indirect golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect golang.org/x/text v0.7.0 // indirect
google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc // indirect google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44 // indirect
google.golang.org/grpc v1.53.0 // indirect google.golang.org/grpc v1.53.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.28.1 // indirect
) )

View file

@ -1,7 +1,6 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 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.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
@ -9,7 +8,7 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/hashicorp/go-hclog v1.4.0 h1:ctuWFGrhFha8BnnzxqeRGidlEcQkDyL5u8J8t5eA11I= github.com/hashicorp/go-hclog v1.4.0 h1:ctuWFGrhFha8BnnzxqeRGidlEcQkDyL5u8J8t5eA11I=
github.com/hashicorp/go-hclog v1.4.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-hclog v1.4.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-plugin v1.4.8 h1:CHGwpxYDOttQOY7HOWgETU9dyVjOXzniXDqJcYJE1zM= github.com/hashicorp/go-plugin v1.4.8 h1:CHGwpxYDOttQOY7HOWgETU9dyVjOXzniXDqJcYJE1zM=
@ -23,7 +22,6 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 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.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@ -33,41 +31,26 @@ github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sftpgo/sdk v0.1.3-0.20221208080405-e682ae869318 h1:oDr2it5L9nh13+P3BzyIqx89gN9Kfrsdk4cw42HaIuQ=
github.com/sftpgo/sdk v0.1.3-0.20221208080405-e682ae869318/go.mod h1:3GpW3Qy8IHH6kex0ny+Y6ayeYb9OJxz8Pxh3IZgAs2E=
github.com/sftpgo/sdk v0.1.3-0.20230212154322-556375985d8c h1:SiWQZe99SZ/O4QSIsxzL91NgwFJNoo4IJ31cazUrYh4=
github.com/sftpgo/sdk v0.1.3-0.20230212154322-556375985d8c/go.mod h1:B1lPGb05WtvvrX5IuhHrSjWdRT867qBaoxlS2Q9+1bA=
github.com/sftpgo/sdk v0.1.3-0.20230213120720-de3129520736 h1:QFzoqYPIxuqDOe2NJfYI7J71bZrsfC0Aejc0ChblkcA= github.com/sftpgo/sdk v0.1.3-0.20230213120720-de3129520736 h1:QFzoqYPIxuqDOe2NJfYI7J71bZrsfC0Aejc0ChblkcA=
github.com/sftpgo/sdk v0.1.3-0.20230213120720-de3129520736/go.mod h1:B1lPGb05WtvvrX5IuhHrSjWdRT867qBaoxlS2Q9+1bA= github.com/sftpgo/sdk v0.1.3-0.20230213120720-de3129520736/go.mod h1:B1lPGb05WtvvrX5IuhHrSjWdRT867qBaoxlS2Q9+1bA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/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-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/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-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-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44 h1:EfLuoKW5WfkgVdDy7dTK8qSbH37AX5mj/MFh+bGPz14=
google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 h1:jmIfw8+gSvXcZSgaFAGyInDXeWzUhvYH57G/5GKMn70= google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA=
google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc h1:ijGwO+0vL2hJt5gaygqP2j6PfflOBrRot0IczKbmtio=
google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
google.golang.org/grpc v1.51.0 h1:E1eGv1FTqoLIdnBCZufiSHgKjlqG6fKFf6pPWtMTh8U=
google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww=
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=