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_certificates`, list of strings. Public host certificates. Each certificate can be defined as a path relative to the configuration directory or an absolute one. Certificate's public key must match a private host key otherwise it will be silently ignored. Default: empty.
- `host_key_algorithms`, list of strings. Public key algorithms that the server will accept for host key authentication. The supported values are: `rsa-sha2-512-cert-v01@openssh.com`, `rsa-sha2-256-cert-v01@openssh.com`, `ssh-rsa-cert-v01@openssh.com`, `ssh-dss-cert-v01@openssh.com`, `ecdsa-sha2-nistp256-cert-v01@openssh.com`, `ecdsa-sha2-nistp384-cert-v01@openssh.com`, `ecdsa-sha2-nistp521-cert-v01@openssh.com`, `ssh-ed25519-cert-v01@openssh.com`, `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, `rsa-sha2-512`, `rsa-sha2-256`, `ssh-rsa`, `ssh-dss`, `ssh-ed25519`. Default values: `rsa-sha2-512-cert-v01@openssh.com`, `rsa-sha2-256-cert-v01@openssh.com`, `ecdsa-sha2-nistp256-cert-v01@openssh.com`, `ecdsa-sha2-nistp384-cert-v01@openssh.com`, `ecdsa-sha2-nistp521-cert-v01@openssh.com`, `ssh-ed25519-cert-v01@openssh.com`, `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, `rsa-sha2-512`, `rsa-sha2-256`, `ssh-ed25519`.
- `moduli`, list of strings. Diffie-Hellman moduli files. Each moduli file can be defined as a path relative to the configuration directory or an absolute one. If set, `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.
- `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`.

View file

@ -19,12 +19,12 @@ In this tutorial we'll focus on `HTTP-01` challenge type and make the following
## Overview
- [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)
- [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 WebDAV service](#Enable-HTTPS-for-WebDAV-service)
- [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)
- [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)
- [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 explicit FTP over TLS](#enable-explicit-ftp-over-tls)
## 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.
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.
```bash

30
go.mod
View file

@ -9,13 +9,13 @@ require (
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
github.com/aws/aws-sdk-go-v2 v1.17.4
github.com/aws/aws-sdk-go-v2/config v1.18.12
github.com/aws/aws-sdk-go-v2/credentials v1.13.12
github.com/aws/aws-sdk-go-v2/config v1.18.13
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/s3/manager v1.11.51
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.14.2
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.2
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.18.3
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.53
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.14.3
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.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/bmatcuk/doublestar/v4 v4.6.0
github.com/cockroachdb/cockroach-go/v2 v2.2.20
@ -68,33 +68,33 @@ require (
go.uber.org/automaxprocs v1.5.1
gocloud.dev v0.28.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/sys v0.5.0
golang.org/x/term v0.5.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
)
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/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/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/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/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/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/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/ssooidc v1.14.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.2 // indirect
github.com/aws/smithy-go v1.13.5 // indirect
github.com/beorn7/perks 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/protobuf v1.5.2 // 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/hashicorp/go-cleanhttp v0.5.2 // 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/xerrors v0.0.0-20220907171357-04be3eba64a2 // 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/protobuf v1.28.1 // 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.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
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.109.0/go.mod h1:2sYycXt75t/CSB5R9M2wPU1tJmire7AQZTPtITcGBVE=
cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
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.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw=
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.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.10.0 h1:fpP/gByFs6US1ma53v7VxhvbJpO2Aapng6wabJ99MuI=
cloud.google.com/go/iam v0.10.0/go.mod h1:nXAECrMt2qHpF6RZUZseteD6QyanL68reN4OXPw0UWM=
cloud.google.com/go/iam v0.12.0 h1:DRtTY29b75ciH6Ov1PHb4/iat2CLCvrOm40Q0a6DFpE=
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.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A=
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/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.12 h1:fKs/I4wccmfrNRO9rdrbMO1NgLxct6H9rNMiPdBxHWw=
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 h1:v0xlYqbO6/EVlM8tUn2QEOA7btQxcgidEq2JRDBPTho=
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.12 h1:Cb+HhuEnV19zHRaYYVglwvdHGMJWbdsyP4oHhw04xws=
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 h1:zw1KAc1kl00NYd3ofVmFrb09qnYlSQMeh+fmlQRAihI=
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.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/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.51/go.mod h1:7Grl2gV+dx9SWrUIgwwlUvU40t7+lOSbx34XwfmsTkY=
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.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.28 h1:r+XwaCLpIvCKjBIYy/HVZujQS9tsz5ohHG3ZIe0wKoE=
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/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.19 h1:FGvpyTg2LKEmMrLlpjOgkoNp9XF5CGeyAyo33LdqZW8=
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 h1:YIvKIfPXQVp0EhXUV644kmQo6cQPPSRmC44A1HSoJeg=
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.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA=
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/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/marketplacemetering v1.14.2 h1:7vuSkPqVqwBwSV0OJD71qqWOEFr3Hh1K0e2yOQ/JWwQ=
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 h1:7SguEzgmyCr6bgJ4+GLk1QWGJ+tpN8q26oNpWcQg1jw=
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.30.2 h1:5EQWIFO+Hc8E2hFcXQJ1vm6ufl/PMt/6RVRDZRju2vM=
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 h1:PVieHTwugdlHedlxLpYLQsOZAq736RScuEb/m4zhzc4=
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.18.3 h1:Zod/h9QcDvbrrG3jjTUp4lctRb6Qg2nj7ARC/xMsUc4=
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 h1:0P9VF9miVGT40WSZSuMzHwkwTVIltpDrTrvswMLjbx0=
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/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/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.1/go.mod h1:IgV8l3sj22nQDd5qcAGY0WenwCzCphqdbFOpfktZPrI=
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.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.14.1 h1:0bLhH6DRAqox+g0LatcjGKjjhU6Eudyys6HB6DJVPj8=
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 h1:f1lmlce7r13CX1BPyPqt9oh/H+uqOWc9367lDoGGwNQ=
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.18.3 h1:s49mSnsBZEXjfGBkRfmK+nPqzT7Lt3+t2SmAKNyHblw=
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.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.2 h1:jUqbmxlR+gGPQq/uvQviKpS1bSQecfs2t7o6F14sk9s=
github.com/googleapis/enterprise-certificate-proxy v0.2.2/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
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.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
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.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.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
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-20190226205417-e64efc72b421/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.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.109.0 h1:sW9hgHyX497PP5//NUM7nqfV8D0iDfBApqq7sOh1XR8=
google.golang.org/api v0.109.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=
google.golang.org/api v0.110.0 h1:l+rh0KYUooe9JGbGVx71tbFo4SMbMTXK3I3ia2QSEeU=
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.2.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-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-20230209215440-0dfe4f8abfcc h1:ijGwO+0vL2hJt5gaygqP2j6PfflOBrRot0IczKbmtio=
google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44 h1:EfLuoKW5WfkgVdDy7dTK8qSbH37AX5mj/MFh+bGPz14=
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 v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=

View file

@ -21,6 +21,7 @@ import (
"github.com/spf13/cobra"
"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/smtp"
"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)
err := config.LoadConfig(configDir, configFile)
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)
}
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)
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)
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()
if dataProviderConf.Driver == dataprovider.SQLiteDataProviderName || dataProviderConf.Driver == dataprovider.BoltDataProviderName {
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)
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()
// idle connection are managed externally
commonConfig.IdleTimeout = 0

View file

@ -39,6 +39,7 @@ import (
"github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/metric"
"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/vfs"
)
@ -170,7 +171,7 @@ func Initialize(c Configuration, isShared int) error {
Config.ProxyAllowed = util.RemoveDuplicates(Config.ProxyAllowed, true)
Config.idleLoginTimeout = 2 * time.Minute
Config.idleTimeoutAsDuration = time.Duration(Config.IdleTimeout) * time.Minute
startPeriodicChecks(periodicTimeoutCheckInterval)
startPeriodicChecks(periodicTimeoutCheckInterval, isShared)
Config.defender = nil
Config.allowList = nil
Config.rateLimitersList = nil
@ -382,12 +383,17 @@ func AddDefenderEvent(ip, protocol string, event HostEvent) {
Config.defender.AddEvent(ip, protocol, event)
}
func startPeriodicChecks(duration time.Duration) {
func startPeriodicChecks(duration time.Duration, isShared int) {
startEventScheduler()
spec := fmt.Sprintf("@every %s", duration)
_, err := eventScheduler.AddFunc(spec, Connections.checkTransfers)
util.PanicOnError(err)
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 {
ratio := idleTimeoutCheckInterval / periodicTimeoutCheckInterval
spec = fmt.Sprintf("@every %s", duration*ratio)

View file

@ -722,7 +722,7 @@ func TestIdleConnections(t *testing.T) {
assert.Len(t, Connections.sshConnections, 2)
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 {
Connections.RLock()
@ -734,7 +734,7 @@ func TestIdleConnections(t *testing.T) {
c.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())
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 {
Connections.RLock()

View file

@ -52,6 +52,7 @@ const (
actionObjectEventRule = "event_rule"
actionObjectRole = "role"
actionObjectIPListEntry = "ip_list_entry"
actionObjectConfigs = "configs"
)
var (
@ -90,14 +91,14 @@ func executeAction(operation, executor, ip, objectType, objectName, role string,
dataAsJSON, err := object.RenderAsJSON(operation != operationDelete)
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
}
if strings.HasPrefix(config.Actions.Hook, "http") {
var url *url.URL
url, err := url.Parse(config.Actions.Hook)
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)
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 {
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)
return err
}

View file

@ -37,7 +37,7 @@ import (
)
const (
boltDatabaseVersion = 27
boltDatabaseVersion = 28
)
var (
@ -51,10 +51,12 @@ var (
rulesBucket = []byte("events_rules")
rolesBucket = []byte("roles")
ipListsBucket = []byte("ip_lists")
configsBucket = []byte("configs")
dbVersionBucket = []byte("db_version")
dbVersionKey = []byte("version")
configsKey = []byte("configs")
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
@ -2977,6 +2979,39 @@ func (p *BoltProvider) getListEntriesForIP(ip string, listType IPListType) ([]IP
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 {
return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, err := p.getUsersBucket(tx)
@ -3061,9 +3096,9 @@ func (p *BoltProvider) migrateDatabase() error {
providerLog(logger.LevelError, "%v", err)
logger.ErrorToConsole("%v", err)
return err
case version == 23, version == 24, version == 25, version == 26:
logger.InfoToConsole("updating database schema version: %d -> 27", version)
providerLog(logger.LevelInfo, "updating database schema version: %d -> 27", version)
case version == 23, version == 24, version == 25, version == 26, version == 27:
logger.InfoToConsole("updating database schema version: %d -> 28", version)
providerLog(logger.LevelInfo, "updating database schema version: %d -> 28", version)
err := p.dbHandle.Update(func(tx *bolt.Tx) error {
rules, err := p.dumpEventRules()
if err != nil {
@ -3095,7 +3130,7 @@ func (p *BoltProvider) migrateDatabase() error {
if err != nil {
return err
}
return updateBoltDatabaseVersion(p.dbHandle, 27)
return updateBoltDatabaseVersion(p.dbHandle, 28)
default:
if version > boltDatabaseVersion {
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)
if err != nil {
return err
@ -3117,7 +3152,7 @@ func (p *BoltProvider) revertDatabase(targetVersion int) error {
return errors.New("current version match target version, nothing to do")
}
switch dbVersion.Version {
case 24, 25, 26:
case 24, 25, 26, 27, 28:
logger.InfoToConsole("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 {
@ -3145,10 +3180,12 @@ func (p *BoltProvider) revertDatabase(targetVersion int) error {
}
}
}
err = tx.DeleteBucket(rolesBucket)
for _, b := range [][]byte{rolesBucket, configsBucket} {
err = tx.DeleteBucket(b)
if err != nil && !errors.Is(err, bolt.ErrBucketNotFound) {
return err
}
}
return nil
})
if err != 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
sqlTableRoles string
sqlTableIPLists string
sqlTableConfigs string
sqlTableSchemaVersion string
argon2Params *argon2id.Params
lastLoginMinDelay = 10 * time.Minute
@ -225,6 +226,7 @@ func initSQLTables() {
sqlTableNodes = "nodes"
sqlTableRoles = "roles"
sqlTableIPLists = "ip_lists"
sqlTableConfigs = "configurations"
sqlTableSchemaVersion = "schema_version"
}
@ -666,6 +668,7 @@ type BackupData struct {
EventRules []EventRule `json:"event_rules"`
Roles []Role `json:"roles"`
IPLists []IPListEntry `json:"ip_lists"`
Configs *Configs `json:"configs"`
Version int `json:"version"`
}
@ -817,6 +820,8 @@ type Provider interface {
dumpIPListEntries() ([]IPListEntry, error)
countIPListEntries(listType IPListType) (int64, error)
getListEntriesForIP(ip string, listType IPListType) ([]IPListEntry, error)
getConfigs() (Configs, error)
setConfigs(configs *Configs) error
checkAvailability() error
close() error
reloadConfig() error
@ -997,17 +1002,18 @@ func validateSQLTablesPrefix() error {
sqlTableNodes = config.SQLTablesPrefix + sqlTableNodes
sqlTableRoles = config.SQLTablesPrefix + sqlTableRoles
sqlTableIPLists = config.SQLTablesPrefix + sqlTableIPLists
sqlTableConfigs = config.SQLTablesPrefix + sqlTableConfigs
sqlTableSchemaVersion = config.SQLTablesPrefix + sqlTableSchemaVersion
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 "+
"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"+
"ip lists %q",
"ip lists %q configs %q",
sqlTableUsers, sqlTableFolders, sqlTableUsersFoldersMapping, sqlTableAdmins, sqlTableAPIKeys,
sqlTableShares, sqlTableDefenderHosts, sqlTableDefenderEvents, sqlTableActiveTransfers, sqlTableGroups,
sqlTableUsersGroupsMapping, sqlTableAdminsGroupsMapping, sqlTableGroupsFoldersMapping, sqlTableSharedSessions,
sqlTableSchemaVersion, sqlTableEventsActions, sqlTableEventsRules, sqlTableRulesActionsMapping,
sqlTableTasks, sqlTableNodes, sqlTableRoles, sqlTableIPLists)
sqlTableTasks, sqlTableNodes, sqlTableRoles, sqlTableIPLists, sqlTableConfigs)
}
return nil
}
@ -1518,6 +1524,25 @@ func GetUsedVirtualFolderQuota(name string) (int, int64, error) {
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
func AddShare(share *Share, executor, ipAddress, role string) error {
err := provider.addShare(share)
@ -2306,6 +2331,10 @@ func DumpData() (BackupData, error) {
if err != nil {
return data, err
}
configs, err := provider.getConfigs()
if err != nil {
return data, err
}
data.Users = users
data.Groups = groups
data.Folders = folders
@ -2316,6 +2345,7 @@ func DumpData() (BackupData, error) {
data.EventRules = rules
data.Roles = roles
data.IPLists = ipLists
data.Configs = &configs
data.Version = DumpVersion
return data, err
}

View file

@ -80,6 +80,8 @@ type memoryProviderHandle struct {
ipListEntries map[string]IPListEntry
// slice with ordered IP list entries
ipListEntriesKeys []string
// configurations
configs Configs
}
// MemoryProvider defines the auth provider for a memory store
@ -118,6 +120,7 @@ func initializeMemoryProvider(basePath string) {
roleNames: []string{},
ipListEntries: map[string]IPListEntry{},
ipListEntriesKeys: []string{},
configs: Configs{},
configFile: configFile,
},
}
@ -2797,6 +2800,28 @@ func (p *MemoryProvider) getListEntriesForIP(ip string, listType IPListType) ([]
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 {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
@ -2929,6 +2954,7 @@ func (p *MemoryProvider) clear() {
p.dbHandle.roleNames = []string{}
p.dbHandle.ipListEntries = map[string]IPListEntry{}
p.dbHandle.ipListEntriesKeys = []string{}
p.dbHandle.configs = Configs{}
}
func (p *MemoryProvider) reloadConfig() error {
@ -2968,7 +2994,11 @@ func (p *MemoryProvider) reloadConfig() error {
func (p *MemoryProvider) restoreDump(dump *BackupData) error {
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
}
@ -3130,7 +3160,14 @@ func (p *MemoryProvider) restoreAdmins(dump *BackupData) error {
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 {
entry := dump.IPLists[idx]
_, err := p.ipListEntryExists(entry.IPOrNet, entry.Type)

View file

@ -59,6 +59,7 @@ const (
"DROP TABLE IF EXISTS `{{nodes}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{roles}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{ip_lists}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{configs}}` 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);" +
"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_first_last_idx` ON `{{ip_lists}}` (`first`, `last`);"
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
@ -745,6 +749,14 @@ func (p *MySQLProvider) getListEntriesForIP(ip string, listType IPListType) ([]I
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 {
return sqlCommonSetFirstDownloadTimestamp(username, p.dbHandle)
}
@ -800,6 +812,8 @@ func (p *MySQLProvider) migrateDatabase() error { //nolint:dupl
return updateMySQLDatabaseFromV25(p.dbHandle)
case version == 26:
return updateMySQLDatabaseFromV26(p.dbHandle)
case version == 27:
return updateMySQLDatabaseFromV27(p.dbHandle)
default:
if version > sqlDatabaseVersion {
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)
case 27:
return downgradeMySQLDatabaseFromV27(p.dbHandle)
case 28:
return downgradeMySQLDatabaseFromV28(p.dbHandle)
default:
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 {
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 {
@ -890,6 +913,13 @@ func downgradeMySQLDatabaseFromV27(dbHandle *sql.DB) error {
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 {
logger.InfoToConsole("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)
}
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 {
logger.InfoToConsole("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)
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 "{{roles}}" CASCADE;
DROP TABLE IF EXISTS "{{ip_lists}}" CASCADE;
DROP TABLE IF EXISTS "{{configs}}" CASCADE;
DROP TABLE IF EXISTS "{{schema_version}}" CASCADE;
`
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");
`
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
@ -718,6 +723,14 @@ func (p *PGSQLProvider) getListEntriesForIP(ip string, listType IPListType) ([]I
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 {
return sqlCommonSetFirstDownloadTimestamp(username, p.dbHandle)
}
@ -773,6 +786,8 @@ func (p *PGSQLProvider) migrateDatabase() error { //nolint:dupl
return updatePgSQLDatabaseFromV25(p.dbHandle)
case version == 26:
return updatePgSQLDatabaseFromV26(p.dbHandle)
case version == 27:
return updatePgSQLDatabaseFromV27(p.dbHandle)
default:
if version > sqlDatabaseVersion {
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)
case 27:
return downgradePgSQLDatabaseFromV27(p.dbHandle)
case 28:
return downgradePgSQLDatabaseFromV28(p.dbHandle)
default:
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 {
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 {
@ -863,6 +887,13 @@ func downgradePgSQLDatabaseFromV27(dbHandle *sql.DB) error {
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 {
logger.InfoToConsole("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)
}
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 {
logger.InfoToConsole("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)
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 (
sqlDatabaseVersion = 27
sqlDatabaseVersion = 28
defaultSQLQueryTimeout = 10 * 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, "{{roles}}", sqlTableRoles)
sql = strings.ReplaceAll(sql, "{{ip_lists}}", sqlTableIPLists)
sql = strings.ReplaceAll(sql, "{{configs}}", sqlTableConfigs)
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
return sql
}
@ -3825,6 +3826,43 @@ func sqlCommonCleanupNodes(dbHandle *sql.DB) error {
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) {
var result schemaVersion
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 "{{roles}}";
DROP TABLE IF EXISTS "{{ip_lists}}";
DROP TABLE IF EXISTS "{{configs}}";
DROP TABLE IF EXISTS "{{schema_version}}";
`
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");
`
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
@ -674,6 +679,14 @@ func (p *SQLiteProvider) getListEntriesForIP(ip string, listType IPListType) ([]
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 {
return sqlCommonSetFirstDownloadTimestamp(username, p.dbHandle)
}
@ -728,6 +741,8 @@ func (p *SQLiteProvider) migrateDatabase() error { //nolint:dupl
return updateSQLiteDatabaseFromV25(p.dbHandle)
case version == 26:
return updateSQLiteDatabaseFromV26(p.dbHandle)
case version == 27:
return updateSQLiteDatabaseFromV27(p.dbHandle)
default:
if version > sqlDatabaseVersion {
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)
case 27:
return downgradeSQLiteDatabaseFromV27(p.dbHandle)
case 28:
return downgradeSQLiteDatabaseFromV28(p.dbHandle)
default:
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 {
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 {
@ -818,6 +842,13 @@ func downgradeSQLiteDatabaseFromV27(dbHandle *sql.DB) error {
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 {
logger.InfoToConsole("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)
}
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 {
logger.InfoToConsole("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)
}
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 {
ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)
defer cancel()

View file

@ -274,6 +274,14 @@ func getRemoveSoftDeletedIPListEntryQuery() string {
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 {
return fmt.Sprintf(`SELECT %s FROM %s WHERE name = %s`, selectRoleFields, sqlTableRoles,
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))
}
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 {
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 {
return err
}
logger.Debug(logSender, "", "backup restored, users: %d, folders: %d, admins: %d",
len(dump.Users), len(dump.Folders), len(dump.Admins))
logger.Debug(logSender, "", "backup restored")
return nil
}
@ -420,6 +422,26 @@ func RestoreAdmins(admins []dataprovider.Admin, inputFile string, mode int, exec
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
func RestoreIPListEntries(entries []dataprovider.IPListEntry, inputFile string, mode int, executor, ipAddress,
executorRole string,

View file

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

View file

@ -54,6 +54,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/ssh"
"golang.org/x/net/html"
"github.com/drakkan/sftpgo/v2/internal/common"
@ -167,6 +168,7 @@ const (
webAdminRolesPath = "/web/admin/roles"
webAdminRolePath = "/web/admin/role"
webEventsPath = "/web/admin/events"
webConfigsPath = "/web/admin/configs"
webBasePathClient = "/web/client"
webClientLoginPath = "/web/client/login"
webClientFilesPath = "/web/client/files"
@ -1288,6 +1290,46 @@ func TestGroupSettingsOverride(t *testing.T) {
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) {
entry := dataprovider.IPListEntry{
IPOrNet: "::ffff:12.34.56.78",
@ -6782,6 +6824,7 @@ func TestProviderErrors(t *testing.T) {
backupData := dataprovider.BackupData{
Version: dataprovider.DumpVersion,
}
backupData.Configs = &dataprovider.Configs{}
backupData.Users = append(backupData.Users, user)
backupContent, err := json.Marshal(backupData)
assert.NoError(t, err)
@ -6790,6 +6833,13 @@ func TestProviderErrors(t *testing.T) {
assert.NoError(t, err)
_, _, err = httpdtest.Loaddata(backupFilePath, "", "", http.StatusInternalServerError)
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())})
backupContent, err = json.Marshal(backupData)
assert.NoError(t, err)
@ -7478,6 +7528,8 @@ func TestLoaddataFromPostBody(t *testing.T) {
}
func TestLoaddata(t *testing.T) {
err := dataprovider.UpdateConfigs(nil, "", "", "")
assert.NoError(t, err)
mappedPath := filepath.Join(os.TempDir(), "restored_folder")
folderName := filepath.Base(mappedPath)
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{
Version: 14,
}
backupData.Configs = &configs
backupData.Users = append(backupData.Users, user)
backupData.Roles = append(backupData.Roles, role)
backupData.Groups = append(backupData.Groups, group)
@ -7621,6 +7684,15 @@ func TestLoaddata(t *testing.T) {
// update from backup
_, _, err = httpdtest.Loaddata(backupFilePath, "2", "", http.StatusOK)
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)
assert.NoError(t, err)
assert.Len(t, user.VirtualFolders, 1)
@ -7751,11 +7823,20 @@ func TestLoaddata(t *testing.T) {
assert.NoError(t, err)
err = os.Remove(backupFilePath)
assert.NoError(t, err)
err = dataprovider.UpdateConfigs(nil, "", "", "")
assert.NoError(t, err)
}
func TestLoaddataMode(t *testing.T) {
err := dataprovider.UpdateConfigs(nil, "", "", "")
assert.NoError(t, err)
mappedPath := filepath.Join(os.TempDir(), "restored_fold")
folderName := filepath.Base(mappedPath)
configs := dataprovider.Configs{
SFTPD: &dataprovider.SFTPDConfigs{
Moduli: []string{"/moduli"},
},
}
role := getTestRole()
role.ID = 1
role.Name = "test_role_load"
@ -7834,6 +7915,7 @@ func TestLoaddataMode(t *testing.T) {
backupData := dataprovider.BackupData{
Version: dataprovider.DumpVersion,
}
backupData.Configs = &configs
backupData.Users = append(backupData.Users, user)
backupData.Groups = append(backupData.Groups, group)
backupData.Admins = append(backupData.Admins, admin)
@ -7859,10 +7941,13 @@ func TestLoaddataMode(t *testing.T) {
backupData.IPLists = append(backupData.IPLists, ipListEntry)
backupContent, _ := json.Marshal(backupData)
backupFilePath := filepath.Join(backupsPath, "backup.json")
err := os.WriteFile(backupFilePath, backupContent, os.ModePerm)
err = os.WriteFile(backupFilePath, backupContent, os.ModePerm)
assert.NoError(t, err)
_, _, err = httpdtest.Loaddata(backupFilePath, "0", "0", http.StatusOK)
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)
assert.NoError(t, err)
assert.Equal(t, mappedPath+"1", folder.MappedPath)
@ -7934,6 +8019,10 @@ func TestLoaddataMode(t *testing.T) {
entry, _, err = httpdtest.UpdateIPListEntry(entry, http.StatusOK)
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{
{
MappedPath: mappedPath,
@ -7942,6 +8031,9 @@ func TestLoaddataMode(t *testing.T) {
}
_, _, err = httpdtest.Loaddata(backupFilePath, "0", "1", http.StatusOK)
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)
assert.NoError(t, err)
assert.NotEqual(t, oldGroupDesc, group.Description)
@ -7999,6 +8091,9 @@ func TestLoaddataMode(t *testing.T) {
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
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
_, err = httpdtest.RemoveGroup(group, http.StatusBadRequest)
assert.NoError(t, err)
@ -8022,6 +8117,8 @@ func TestLoaddataMode(t *testing.T) {
assert.NoError(t, err)
err = os.Remove(backupFilePath)
assert.NoError(t, err)
err = dataprovider.UpdateConfigs(nil, "", "", "")
assert.NoError(t, err)
}
func TestRateLimiter(t *testing.T) {
@ -8992,6 +9089,84 @@ func TestChangeAdminPwdInvalidJsonMock(t *testing.T) {
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) {
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
@ -12072,6 +12247,149 @@ func TestMaxSessions(t *testing.T) {
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) {
user1 := getTestUser()
user2 := getTestUser()
@ -22949,10 +23267,35 @@ func TestProviderClosedMock(t *testing.T) {
assert.NoError(t, err)
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)
req.Header.Set("X-CSRF-TOKEN", csrfToken)
rr := executeRequest(req)
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)
setJWTCookieForReq(req, token)
rr = executeRequest(req)

View file

@ -616,6 +616,11 @@ func TestInvalidToken(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, rr.Code)
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()
addAdmin(rr, req)
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), compressor.Handler, s.refreshCookie).
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)
router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Post(webIPListPath+"/{type}",
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)
router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Post(webIPListPath+"/{type}/{ipornet}",
s.handleWebUpdateIPListEntryPost)
router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), verifyCSRFHeader).
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/dataprovider"
"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/plugin"
"github.com/drakkan/sftpgo/v2/internal/smtp"
@ -94,6 +95,7 @@ const (
templateDefender = "defender.html"
templateIPLists = "iplists.html"
templateIPList = "iplist.html"
templateConfigs = "configs.html"
templateProfile = "profile.html"
templateChangePwd = "changepassword.html"
templateMaintenance = "maintenance.html"
@ -114,6 +116,7 @@ const (
pageDefenderTitle = "Auto Blocklist"
pageIPListsTitle = "IP Lists"
pageEventsTitle = "Logs"
pageConfigsTitle = "Configurations"
pageForgotPwdTitle = "SFTPGo Admin - Forgot password"
pageResetPwdTitle = "SFTPGo Admin - Reset password"
pageSetupTitle = "Create first admin user"
@ -144,6 +147,7 @@ type basePage struct {
IPListsURL string
IPListURL string
EventsURL string
ConfigsURL string
LogoutURL string
ProfileURL string
ChangePwdURL string
@ -171,10 +175,12 @@ type basePage struct {
DefenderTitle string
IPListsTitle string
EventsTitle string
ConfigsTitle string
Version string
CSRFToken string
IsEventManagerPage bool
IsIPManagerPage bool
IsServerManagerPage bool
HasDefender bool
HasSearcher bool
HasExternalLogin bool
@ -383,6 +389,14 @@ type eventsPage struct {
ProviderEventsSearchURL string
}
type configsPage struct {
basePage
Configs dataprovider.Configs
ConfigSection int
RedactedSecret string
Error string
}
type messagePage struct {
basePage
Error string
@ -554,6 +568,11 @@ func loadAdminTemplates(templatesPath string) {
filepath.Join(templatesPath, templateAdminDir, templateBase),
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{
"ListFSProviders": func() []sdk.FilesystemProvider {
@ -595,6 +614,7 @@ func loadAdminTemplates(templatesPath string) {
rolesTmpl := util.LoadTemplate(nil, rolesPaths...)
roleTmpl := util.LoadTemplate(nil, rolePaths...)
eventsTmpl := util.LoadTemplate(nil, eventsPaths...)
configsTmpl := util.LoadTemplate(nil, configsPaths...)
adminTemplates[templateUsers] = usersTmpl
adminTemplates[templateUser] = userTmpl
@ -627,6 +647,7 @@ func loadAdminTemplates(templatesPath string) {
adminTemplates[templateRoles] = rolesTmpl
adminTemplates[templateRole] = roleTmpl
adminTemplates[templateEvents] = eventsTmpl
adminTemplates[templateConfigs] = configsTmpl
}
func isEventManagerResource(currentURL string) bool {
@ -658,6 +679,11 @@ func isIPListsResource(currentURL string) bool {
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 {
var csrfToken string
if currentURL != "" {
@ -680,6 +706,7 @@ func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request)
IPListsURL: webIPListsPath,
IPListURL: webIPListPath,
EventsURL: webEventsPath,
ConfigsURL: webConfigsPath,
LogoutURL: webLogoutPath,
ProfileURL: webAdminProfilePath,
ChangePwdURL: webChangeAdminPwdPath,
@ -709,10 +736,12 @@ func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request)
DefenderTitle: pageDefenderTitle,
IPListsTitle: pageIPListsTitle,
EventsTitle: pageEventsTitle,
ConfigsTitle: pageConfigsTitle,
Version: version.GetAsString(),
LoggedAdmin: getAdminFromToken(r),
IsEventManagerPage: isEventManagerResource(currentURL),
IsIPManagerPage: isIPListsResource(currentURL),
IsServerManagerPage: isServerManagerResource(currentURL),
HasDefender: common.Config.DefenderConfig.Enabled,
HasSearcher: plugin.Handler.HasSearcher(),
HasExternalLogin: isLoggedInWithOIDC(r),
@ -867,6 +896,24 @@ func (s *httpdServer) renderMaintenancePage(w http.ResponseWriter, r *http.Reque
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) {
data := setupPage{
basePage: s.getBasePageData(pageSetupTitle, webAdminSetupPath, r),
@ -2218,11 +2265,11 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven
}
var emailAttachments []string
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
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{
HTTPConfig: dataprovider.EventActionHTTPConfig{
@ -2244,7 +2291,7 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven
EnvVars: getKeyValsFromPostFields(r, "cmd_env_key", "cmd_env_val"),
},
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"),
Body: r.Form.Get("email_body"),
Attachments: emailAttachments,
@ -2255,13 +2302,13 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven
FsConfig: dataprovider.EventActionFilesystemConfig{
Type: fsActionType,
Renames: getKeyValsFromPostFields(r, "fs_rename_source", "fs_rename_target"),
Deletes: strings.Split(strings.ReplaceAll(r.Form.Get("fs_delete_paths"), " ", ""), ","),
MkDirs: strings.Split(strings.ReplaceAll(r.Form.Get("fs_mkdir_paths"), " ", ""), ","),
Exist: strings.Split(strings.ReplaceAll(r.Form.Get("fs_exist_paths"), " ", ""), ","),
Deletes: getSliceFromDelimitedValues(r.Form.Get("fs_delete_paths"), ","),
MkDirs: getSliceFromDelimitedValues(r.Form.Get("fs_mkdir_paths"), ","),
Exist: getSliceFromDelimitedValues(r.Form.Get("fs_exist_paths"), ","),
Copy: getKeyValsFromPostFields(r, "fs_copy_source", "fs_copy_target"),
Compress: dataprovider.EventActionFsCompress{
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{
@ -2487,6 +2534,41 @@ func getIPListEntryFromPostFields(r *http.Request, listType dataprovider.IPListT
}, 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) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
if !smtp.IsEnabled() {
@ -3921,3 +4003,70 @@ func (s *httpdServer) handleWebUpdateIPListEntryPost(w http.ResponseWriter, r *h
}
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)
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 {
logger.Error(logSender, "", "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)
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())
if err != nil {
logger.Error(logSender, "", "%v", err)
@ -350,7 +350,11 @@ func (s *Service) LoadInitialData() 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 {
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))
}
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) {
c := Configuration{
KexAlgorithms: supportedKexAlgos,
@ -1888,6 +1926,12 @@ func TestSupportedSecurityOptions(t *testing.T) {
assert.Equal(t, supportedCiphers, serverConfig.Ciphers)
assert.Equal(t, supportedMACs, serverConfig.MACs)
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) {
@ -1895,20 +1939,22 @@ func TestLoadModuli(t *testing.T) {
dhGEXSha256 := "diffie-hellman-group-exchange-sha256"
c := Configuration{}
c.Moduli = []string{".", "missing file"}
err := c.loadModuli(configDir)
assert.Error(t, err)
c.loadModuli(configDir)
assert.NotContains(t, supportedKexAlgos, dhGEXSha1)
assert.NotContains(t, supportedKexAlgos, dhGEXSha256)
assert.NotContains(t, preferredKexAlgos, dhGEXSha1)
assert.NotContains(t, preferredKexAlgos, dhGEXSha256)
assert.Len(t, supportedKexAlgos, 10)
moduli := []byte("20220414072358 2 6 100 2047 5 F19C2D09AD49978F8A0C1B84168A4011A26F9CD516815934764A319FDC5975FA514AAF11B747D8CA6B3919532BEFB68FA118079473895674F3770F71FBB742F176883841EB3DE679BEF53C6AFE437A662F228B03C1E34B5A0D3909F608CEAA16C1F8131DE11E67878EFD918A89205E5E4DE323054010CA4711F25D466BB7727A016DD3F9F53BDBCE093055A4F2497ADEFB5A2500F9C5C3B0BCD88C6489F4C1CBC7CFB67BA6EABA0195794E4188CE9060F431041AD52FB9BAC4DF7FA536F585FBE67746CD57BFAD67567E9706C24D95C49BE95B759657C6BB5151E2AEA32F4CD557C40298A5C402101520EE8AAB8DFEED6FFC11AAF8036D6345923CFB5D1B922F")
moduliFile := filepath.Join(os.TempDir(), "moduli")
err = os.WriteFile(moduliFile, moduli, 0600)
err := os.WriteFile(moduliFile, moduli, 0600)
assert.NoError(t, err)
c.Moduli = []string{moduliFile}
err = c.loadModuli(configDir)
assert.NoError(t, err)
c.loadModuli(configDir)
assert.Contains(t, supportedKexAlgos, dhGEXSha1)
assert.Contains(t, supportedKexAlgos, dhGEXSha256)
assert.NotContains(t, preferredKexAlgos, dhGEXSha1)
assert.Contains(t, preferredKexAlgos, dhGEXSha256)
assert.Len(t, supportedKexAlgos, 12)
err = os.Remove(moduliFile)
assert.NoError(t, err)

View file

@ -47,6 +47,8 @@ const (
defaultPrivateECDSAKeyName = "id_ecdsa"
defaultPrivateEd25519KeyName = "id_ed25519"
sourceAddressCriticalOption = "source-address"
kexDHGroupExchangeSHA1 = "diffie-hellman-group-exchange-sha1"
kexDHGroupExchangeSHA256 = "diffie-hellman-group-exchange-sha256"
)
var (
@ -75,6 +77,11 @@ var (
"diffie-hellman-group18-sha512", "diffie-hellman-group14-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{
"aes128-gcm@openssh.com", "aes256-gcm@openssh.com",
"chacha20-poly1305@openssh.com",
@ -83,11 +90,19 @@ var (
"3des-cbc",
"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{
"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256",
"hmac-sha2-512-etm@openssh.com", "hmac-sha2-512",
"hmac-sha1", "hmac-sha1-96",
}
preferredMACs = []string{
"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256",
}
revokedCertManager = revokedCertificates{
certs: map[string]bool{},
@ -145,7 +160,7 @@ type Configuration struct {
HostKeyAlgorithms []string `json:"host_key_algorithms" mapstructure:"host_key_algorithms"`
// Diffie-Hellman moduli files.
// Each moduli file can be defined as a path relative to the configuration directory or an absolute one.
// If set, "diffie-hellman-group-exchange-sha256" and "diffie-hellman-group-exchange-sha1" KEX algorithms
// 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
Moduli []string `json:"moduli" mapstructure:"moduli"`
@ -272,7 +287,7 @@ func (c *Configuration) getServerConfig() *ssh.ServerConfig {
}
return nextMethods
},
ServerVersion: fmt.Sprintf("SSH-2.0-%v", c.Banner),
ServerVersion: fmt.Sprintf("SSH-2.0-%s", c.Banner),
}
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.
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()
if !c.ShouldBind() {
@ -324,9 +376,7 @@ func (c *Configuration) Initialize(configDir string) error {
return err
}
if err := c.loadModuli(configDir); err != nil {
return err
}
c.loadModuli(configDir)
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 {
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
for {
@ -416,37 +466,52 @@ func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig)
}
for _, hostKeyAlgo := range c.HostKeyAlgorithms {
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
serviceStatus.HostKeyAlgos = c.HostKeyAlgorithms
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)
for _, kex := range c.KexAlgorithms {
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
serviceStatus.KexAlgorithms = c.KexAlgorithms
} else {
serviceStatus.KexAlgorithms = preferredKexAlgos
}
if len(c.Ciphers) > 0 {
c.Ciphers = util.RemoveDuplicates(c.Ciphers, true)
for _, cipher := range c.Ciphers {
if !util.Contains(supportedCiphers, cipher) {
return fmt.Errorf("unsupported cipher %#v", cipher)
return fmt.Errorf("unsupported cipher %q", cipher)
}
}
serverConfig.Ciphers = c.Ciphers
serviceStatus.Ciphers = c.Ciphers
} else {
serviceStatus.Ciphers = preferredCiphers
}
if len(c.MACs) > 0 {
c.MACs = util.RemoveDuplicates(c.MACs, true)
for _, mac := range c.MACs {
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
serviceStatus.MACs = c.MACs
} else {
serviceStatus.MACs = preferredMACs
}
return nil
}
@ -875,9 +940,11 @@ func (c *Configuration) checkHostKeyAutoGeneration(configDir string) error {
return nil
}
func (c *Configuration) loadModuli(configDir string) error {
supportedKexAlgos = util.Remove(supportedKexAlgos, "diffie-hellman-group-exchange-sha1")
supportedKexAlgos = util.Remove(supportedKexAlgos, "diffie-hellman-group-exchange-sha256")
func (c *Configuration) loadModuli(configDir string) {
supportedKexAlgos = util.Remove(supportedKexAlgos, kexDHGroupExchangeSHA1)
supportedKexAlgos = util.Remove(supportedKexAlgos, kexDHGroupExchangeSHA256)
preferredKexAlgos = util.Remove(preferredKexAlgos, kexDHGroupExchangeSHA256)
c.Moduli = util.RemoveDuplicates(c.Moduli, false)
for _, m := range c.Moduli {
m = strings.TrimSpace(m)
if !util.IsFileInputValid(m) {
@ -890,12 +957,19 @@ func (c *Configuration) loadModuli(configDir string) error {
}
logger.Info(logSender, "", "loading moduli file %q", m)
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.
@ -934,7 +1008,7 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
Fingerprint: ssh.FingerprintSHA256(private.PublicKey()),
}
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)
// 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)
if err == nil {
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()))
}
}

View file

@ -57,6 +57,10 @@ type ServiceStatus struct {
SSHCommands []string `json:"ssh_commands"`
HostKeys []HostKey `json:"host_keys"`
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
@ -69,6 +73,26 @@ func (s *ServiceStatus) GetSupportedAuthsAsString() string {
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
func GetStatus() ServiceStatus {
return serviceStatus

View file

@ -219,6 +219,12 @@ func TestMain(m *testing.M) {
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)
if err != nil {
logger.WarnToConsole("error initializing common: %v", err)
@ -401,12 +407,6 @@ func TestInitialization(t *testing.T) {
assert.True(t, sftpdConf.Bindings[0].HasProxy())
err = sftpdConf.Initialize(configDir)
assert.Error(t, err)
sftpdConf.Moduli = []string{"missing moduli file"}
err = sftpdConf.Initialize(configDir)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "unable to open moduli file")
}
sftpdConf.Moduli = nil
sftpdConf.HostKeys = []string{"missing key"}
err = sftpdConf.Initialize(configDir)
assert.Error(t, err)
@ -429,11 +429,13 @@ func TestInitialization(t *testing.T) {
if assert.Error(t, err) {
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)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "unsupported key-exchange algorithm")
}
sftpdConf.KexAlgorithms = nil
sftpdConf.HostKeyAlgorithms = []string{"not a host key algo"}
err = sftpdConf.Initialize(configDir)
if assert.Error(t, err) {
@ -495,6 +497,17 @@ func TestInitialization(t *testing.T) {
assert.NoError(t, err)
err = sftpdConf.Initialize(configDir)
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) {
@ -562,6 +575,10 @@ func TestBasicSFTPHandling(t *testing.T) {
assert.NotEmpty(t, sshCommands)
sshAuths := status.GetSupportedAuthsAsString()
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) {

View file

@ -22,10 +22,12 @@ import (
"fmt"
"html/template"
"path/filepath"
"sync"
"time"
"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/util"
)
@ -47,16 +49,93 @@ const (
templateEmailDir = "email"
templatePasswordReset = "reset-password.html"
templatePasswordExpiration = "password-expiration.html"
dialTimeout = 10 * time.Second
)
var (
config *Config
config = &activeConfig{}
initialConfig *Config
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
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
@ -89,34 +168,65 @@ type Config struct {
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
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 == "" {
logger.Debug(logSender, "", "configuration disabled, email capabilities will not be available")
return nil
return loadConfigFromProvider()
}
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 {
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 {
return fmt.Errorf("smtp: invalid encryption %v", c.Encryption)
return fmt.Errorf("smtp: invalid encryption %d", c.Encryption)
}
if c.From == "" && c.User == "" {
return fmt.Errorf(`smtp: from address and user cannot both be empty`)
}
templatesPath := util.FindSharedDataPath(c.TemplatesPath, configDir)
if templatesPath == "" {
return fmt.Errorf("smtp: invalid templates path %#v", templatesPath)
}
loadTemplates(filepath.Join(templatesPath, templateEmailDir))
config = c
initialConfig = c
config.Set(nil)
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)
return nil
c.Host, c.Port, c.User, c.AuthType, c.Encryption, c.Domain)
return loadConfigFromProvider()
}
func (c *Config) getMailClientOptions() []mail.Option {
@ -130,14 +240,14 @@ func (c *Config) getMailClientOptions() []mail.Option {
default:
options = append(options, mail.WithTLSPolicy(mail.NoTLS))
}
if config.User != "" {
options = append(options, mail.WithUsername(config.User))
if c.User != "" {
options = append(options, mail.WithUsername(c.User))
}
if config.Password != "" {
options = append(options, mail.WithPassword(config.Password))
if c.Password != "" {
options = append(options, mail.WithPassword(c.Password))
}
if config.User != "" || config.Password != "" {
switch config.AuthType {
if c.User != "" || c.Password != "" {
switch c.AuthType {
case 1:
options = append(options, mail.WithSMTPAuth(mail.SMTPAuthLogin))
case 2:
@ -146,14 +256,63 @@ func (c *Config) getMailClientOptions() []mail.Option {
options = append(options, mail.WithSMTPAuth(mail.SMTPAuthPlain))
}
}
if config.Domain != "" {
options = append(options, mail.WithHELO(config.Domain))
if c.Domain != "" {
options = append(options, mail.WithHELO(c.Domain))
}
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) {
logger.Debug(logSender, "", "loading templates from %#v", templatesPath)
logger.Debug(logSender, "", "loading templates from %q", templatesPath)
passwordResetPath := filepath.Join(templatesPath, templatePasswordReset)
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.
func SendEmail(to []string, subject, body string, contentType EmailContentType, attachments ...*mail.File) error {
if !IsEnabled() {
return errors.New("smtp: not configured")
}
m := mail.NewMsg()
var from string
if config.From != "" {
from = config.From
} else {
from = config.User
}
if err := m.From(from); err != nil {
return fmt.Errorf("invalid from address: %w", err)
}
if err := m.To(to...); err != nil {
return err
}
m.Subject(subject)
m.SetDate()
m.SetMessageID()
m.SetAttachements(attachments)
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)
return config.sendEmail(to, subject, body, contentType, attachments...)
}
c, err := mail.NewClient(config.Host, config.getMailClientOptions()...)
// ReloadProviderConf reloads the configuration from the provider
// and apply it if different from the active one
func ReloadProviderConf() {
loadConfigFromProvider() //nolint:errcheck
}
func loadConfigFromProvider() error {
configs, err := dataprovider.GetConfigs()
if err != nil {
return fmt.Errorf("unable to create mail client: %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)
}
ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second)
defer cancelFn()
return c.DialAndSendWithContext(ctx, m)
configs.SetNilsToEmpty()
if err := configs.SMTP.Password.TryDecrypt(); err != nil {
logger.Error(logSender, "", "unable to decrypt password: %v", err)
return fmt.Errorf("smtp: unable to decrypt password: %w", err)
}
config.Set(configs.SMTP)
return nil
}

View file

@ -6263,6 +6263,22 @@ components:
type: array
items:
$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:
type: object
properties:

View file

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

View file

@ -10,17 +10,16 @@ require (
require (
github.com/fatih/color v1.14.1 // 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/yamux v0.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // 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/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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
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/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/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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-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-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 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.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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/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/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
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.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.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-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-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-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/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/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
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-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/genproto v0.0.0-20230216225411-c8e22ba71e44 h1:EfLuoKW5WfkgVdDy7dTK8qSbH37AX5mj/MFh+bGPz14=
google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA=
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/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/mitchellh/go-testing-interface v1.14.1 // 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/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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
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/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.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/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
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-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 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.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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/go.mod h1:B1lPGb05WtvvrX5IuhHrSjWdRT867qBaoxlS2Q9+1bA=
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/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
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.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.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-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-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-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/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/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-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 h1:jmIfw8+gSvXcZSgaFAGyInDXeWzUhvYH57G/5GKMn70=
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/genproto v0.0.0-20230216225411-c8e22ba71e44 h1:EfLuoKW5WfkgVdDy7dTK8qSbH37AX5mj/MFh+bGPz14=
google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA=
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/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=