diff --git a/docs/full-configuration.md b/docs/full-configuration.md index c93e2eb3..2cf04f00 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -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`. diff --git a/docs/howto/lets-encrypt-certificate.md b/docs/howto/lets-encrypt-certificate.md index e3f76150..e0bb3520 100644 --- a/docs/howto/lets-encrypt-certificate.md +++ b/docs/howto/lets-encrypt-certificate.md @@ -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 diff --git a/go.mod b/go.mod index 31ff1889..d70f5900 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index dc9bc0ab..63dc7977 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/cmd/smtptest.go b/internal/cmd/smtptest.go index 6f0290a7..2943929b 100644 --- a/internal/cmd/smtptest.go +++ b/internal/cmd/smtptest.go @@ -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.") }, } ) diff --git a/internal/cmd/startsubsys.go b/internal/cmd/startsubsys.go index 1f1a21a6..2ccfaab3 100644 --- a/internal/cmd/startsubsys.go +++ b/internal/cmd/startsubsys.go @@ -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 diff --git a/internal/common/common.go b/internal/common/common.go index 37046ac8..f324e162 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -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) diff --git a/internal/common/common_test.go b/internal/common/common_test.go index 3bd2f6ab..041d8210 100644 --- a/internal/common/common_test.go +++ b/internal/common/common_test.go @@ -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() diff --git a/internal/dataprovider/actions.go b/internal/dataprovider/actions.go index f9eb9e93..2777b53e 100644 --- a/internal/dataprovider/actions.go +++ b/internal/dataprovider/actions.go @@ -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 } diff --git a/internal/dataprovider/bolt.go b/internal/dataprovider/bolt.go index d1c61b35..4d375a33 100644 --- a/internal/dataprovider/bolt.go +++ b/internal/dataprovider/bolt.go @@ -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,9 +3180,11 @@ func (p *BoltProvider) revertDatabase(targetVersion int) error { } } } - err = tx.DeleteBucket(rolesBucket) - if err != nil && !errors.Is(err, bolt.ErrBucketNotFound) { - return err + for _, b := range [][]byte{rolesBucket, configsBucket} { + err = tx.DeleteBucket(b) + if err != nil && !errors.Is(err, bolt.ErrBucketNotFound) { + return err + } } return nil }) diff --git a/internal/dataprovider/configs.go b/internal/dataprovider/configs.go new file mode 100644 index 00000000..25d93f50 --- /dev/null +++ b/internal/dataprovider/configs.go @@ -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 . + +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 +} diff --git a/internal/dataprovider/dataprovider.go b/internal/dataprovider/dataprovider.go index 67465fe2..fabb3a60 100644 --- a/internal/dataprovider/dataprovider.go +++ b/internal/dataprovider/dataprovider.go @@ -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 } diff --git a/internal/dataprovider/memory.go b/internal/dataprovider/memory.go index 3fc50d39..8788d2c0 100644 --- a/internal/dataprovider/memory.go +++ b/internal/dataprovider/memory.go @@ -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) diff --git a/internal/dataprovider/mysql.go b/internal/dataprovider/mysql.go index e304cd85..2e3bcaa9 100644 --- a/internal/dataprovider/mysql.go +++ b/internal/dataprovider/mysql.go @@ -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) +} diff --git a/internal/dataprovider/pgsql.go b/internal/dataprovider/pgsql.go index 9d666a07..c4122dce 100644 --- a/internal/dataprovider/pgsql.go +++ b/internal/dataprovider/pgsql.go @@ -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) +} diff --git a/internal/dataprovider/sqlcommon.go b/internal/dataprovider/sqlcommon.go index 732bfce1..549abb80 100644 --- a/internal/dataprovider/sqlcommon.go +++ b/internal/dataprovider/sqlcommon.go @@ -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) diff --git a/internal/dataprovider/sqlite.go b/internal/dataprovider/sqlite.go index 06d2827c..092af313 100644 --- a/internal/dataprovider/sqlite.go +++ b/internal/dataprovider/sqlite.go @@ -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() diff --git a/internal/dataprovider/sqlqueries.go b/internal/dataprovider/sqlqueries.go index 3aea828c..18c4ce52 100644 --- a/internal/dataprovider/sqlqueries.go +++ b/internal/dataprovider/sqlqueries.go @@ -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]) diff --git a/internal/httpd/api_configs.go b/internal/httpd/api_configs.go new file mode 100644 index 00000000..ba164da4 --- /dev/null +++ b/internal/httpd/api_configs.go @@ -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 . + +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) +} diff --git a/internal/httpd/api_maintenance.go b/internal/httpd/api_maintenance.go index c847515f..23e432fd 100644 --- a/internal/httpd/api_maintenance.go +++ b/internal/httpd/api_maintenance.go @@ -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, diff --git a/internal/httpd/httpd.go b/internal/httpd/httpd.go index 50ed8a38..0ffa2fe2 100644 --- a/internal/httpd/httpd.go +++ b/internal/httpd/httpd.go @@ -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) } diff --git a/internal/httpd/httpd_test.go b/internal/httpd/httpd_test.go index 386f4909..022c5006 100644 --- a/internal/httpd/httpd_test.go +++ b/internal/httpd/httpd_test.go @@ -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 ") + 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 ", 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) diff --git a/internal/httpd/internal_test.go b/internal/httpd/internal_test.go index b43150e1..e19f932f 100644 --- a/internal/httpd/internal_test.go +++ b/internal/httpd/internal_test.go @@ -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) diff --git a/internal/httpd/server.go b/internal/httpd/server.go index b7b36e63..0a2b0e91 100644 --- a/internal/httpd/server.go +++ b/internal/httpd/server.go @@ -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) }) } } diff --git a/internal/httpd/webadmin.go b/internal/httpd/webadmin.go index 988656f4..d863a1cb 100644 --- a/internal/httpd/webadmin.go +++ b/internal/httpd/webadmin.go @@ -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" @@ -126,60 +129,63 @@ var ( ) type basePage struct { - Title string - CurrentURL string - UsersURL string - UserURL string - UserTemplateURL string - AdminsURL string - AdminURL string - QuotaScanURL string - ConnectionsURL string - GroupsURL string - GroupURL string - FoldersURL string - FolderURL string - FolderTemplateURL string - DefenderURL string - IPListsURL string - IPListURL string - EventsURL string - LogoutURL string - ProfileURL string - ChangePwdURL string - MFAURL string - EventRulesURL string - EventRuleURL string - EventActionsURL string - EventActionURL string - RolesURL string - RoleURL string - FolderQuotaScanURL string - StatusURL string - MaintenanceURL string - StaticURL string - UsersTitle string - AdminsTitle string - ConnectionsTitle string - FoldersTitle string - GroupsTitle string - EventRulesTitle string - EventActionsTitle string - RolesTitle string - StatusTitle string - MaintenanceTitle string - DefenderTitle string - IPListsTitle string - EventsTitle string - Version string - CSRFToken string - IsEventManagerPage bool - IsIPManagerPage bool - HasDefender bool - HasSearcher bool - HasExternalLogin bool - LoggedAdmin *dataprovider.Admin - Branding UIBranding + Title string + CurrentURL string + UsersURL string + UserURL string + UserTemplateURL string + AdminsURL string + AdminURL string + QuotaScanURL string + ConnectionsURL string + GroupsURL string + GroupURL string + FoldersURL string + FolderURL string + FolderTemplateURL string + DefenderURL string + IPListsURL string + IPListURL string + EventsURL string + ConfigsURL string + LogoutURL string + ProfileURL string + ChangePwdURL string + MFAURL string + EventRulesURL string + EventRuleURL string + EventActionsURL string + EventActionURL string + RolesURL string + RoleURL string + FolderQuotaScanURL string + StatusURL string + MaintenanceURL string + StaticURL string + UsersTitle string + AdminsTitle string + ConnectionsTitle string + FoldersTitle string + GroupsTitle string + EventRulesTitle string + EventActionsTitle string + RolesTitle string + StatusTitle string + MaintenanceTitle string + DefenderTitle string + IPListsTitle string + EventsTitle string + ConfigsTitle string + Version string + CSRFToken string + IsEventManagerPage bool + IsIPManagerPage bool + IsServerManagerPage bool + HasDefender bool + HasSearcher bool + HasExternalLogin bool + LoggedAdmin *dataprovider.Admin + Branding UIBranding } type usersPage struct { @@ -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,66 +679,74 @@ 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 != "" { csrfToken = createCSRFToken(util.GetIPFromRemoteAddress(r.RemoteAddr)) } return basePage{ - Title: title, - CurrentURL: currentURL, - UsersURL: webUsersPath, - UserURL: webUserPath, - UserTemplateURL: webTemplateUser, - AdminsURL: webAdminsPath, - AdminURL: webAdminPath, - GroupsURL: webGroupsPath, - GroupURL: webGroupPath, - FoldersURL: webFoldersPath, - FolderURL: webFolderPath, - FolderTemplateURL: webTemplateFolder, - DefenderURL: webDefenderPath, - IPListsURL: webIPListsPath, - IPListURL: webIPListPath, - EventsURL: webEventsPath, - LogoutURL: webLogoutPath, - ProfileURL: webAdminProfilePath, - ChangePwdURL: webChangeAdminPwdPath, - MFAURL: webAdminMFAPath, - EventRulesURL: webAdminEventRulesPath, - EventRuleURL: webAdminEventRulePath, - EventActionsURL: webAdminEventActionsPath, - EventActionURL: webAdminEventActionPath, - RolesURL: webAdminRolesPath, - RoleURL: webAdminRolePath, - QuotaScanURL: webQuotaScanPath, - ConnectionsURL: webConnectionsPath, - StatusURL: webStatusPath, - FolderQuotaScanURL: webScanVFolderPath, - MaintenanceURL: webMaintenancePath, - StaticURL: webStaticFilesPath, - UsersTitle: pageUsersTitle, - AdminsTitle: pageAdminsTitle, - ConnectionsTitle: pageConnectionsTitle, - FoldersTitle: pageFoldersTitle, - GroupsTitle: pageGroupsTitle, - EventRulesTitle: pageEventRulesTitle, - EventActionsTitle: pageEventActionsTitle, - RolesTitle: pageRolesTitle, - StatusTitle: pageStatusTitle, - MaintenanceTitle: pageMaintenanceTitle, - DefenderTitle: pageDefenderTitle, - IPListsTitle: pageIPListsTitle, - EventsTitle: pageEventsTitle, - Version: version.GetAsString(), - LoggedAdmin: getAdminFromToken(r), - IsEventManagerPage: isEventManagerResource(currentURL), - IsIPManagerPage: isIPListsResource(currentURL), - HasDefender: common.Config.DefenderConfig.Enabled, - HasSearcher: plugin.Handler.HasSearcher(), - HasExternalLogin: isLoggedInWithOIDC(r), - CSRFToken: csrfToken, - Branding: s.binding.Branding.WebAdmin, + Title: title, + CurrentURL: currentURL, + UsersURL: webUsersPath, + UserURL: webUserPath, + UserTemplateURL: webTemplateUser, + AdminsURL: webAdminsPath, + AdminURL: webAdminPath, + GroupsURL: webGroupsPath, + GroupURL: webGroupPath, + FoldersURL: webFoldersPath, + FolderURL: webFolderPath, + FolderTemplateURL: webTemplateFolder, + DefenderURL: webDefenderPath, + IPListsURL: webIPListsPath, + IPListURL: webIPListPath, + EventsURL: webEventsPath, + ConfigsURL: webConfigsPath, + LogoutURL: webLogoutPath, + ProfileURL: webAdminProfilePath, + ChangePwdURL: webChangeAdminPwdPath, + MFAURL: webAdminMFAPath, + EventRulesURL: webAdminEventRulesPath, + EventRuleURL: webAdminEventRulePath, + EventActionsURL: webAdminEventActionsPath, + EventActionURL: webAdminEventActionPath, + RolesURL: webAdminRolesPath, + RoleURL: webAdminRolePath, + QuotaScanURL: webQuotaScanPath, + ConnectionsURL: webConnectionsPath, + StatusURL: webStatusPath, + FolderQuotaScanURL: webScanVFolderPath, + MaintenanceURL: webMaintenancePath, + StaticURL: webStaticFilesPath, + UsersTitle: pageUsersTitle, + AdminsTitle: pageAdminsTitle, + ConnectionsTitle: pageConnectionsTitle, + FoldersTitle: pageFoldersTitle, + GroupsTitle: pageGroupsTitle, + EventRulesTitle: pageEventRulesTitle, + EventActionsTitle: pageEventActionsTitle, + RolesTitle: pageRolesTitle, + StatusTitle: pageStatusTitle, + MaintenanceTitle: pageMaintenanceTitle, + 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), + CSRFToken: csrfToken, + Branding: s.binding.Branding.WebAdmin, } } @@ -867,6 +896,24 @@ func (s *httpdServer) renderMaintenancePage(w http.ResponseWriter, r *http.Reque renderAdminTemplate(w, templateMaintenance, data) } +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") +} diff --git a/internal/service/service.go b/internal/service/service.go index 16588821..dbeeec22 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -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) } diff --git a/internal/sftpd/internal_test.go b/internal/sftpd/internal_test.go index ab7b48fb..f0a5733a 100644 --- a/internal/sftpd/internal_test.go +++ b/internal/sftpd/internal_test.go @@ -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) diff --git a/internal/sftpd/server.go b/internal/sftpd/server.go index 49e35b1c..0f64b370 100644 --- a/internal/sftpd/server.go +++ b/internal/sftpd/server.go @@ -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())) } } diff --git a/internal/sftpd/sftpd.go b/internal/sftpd/sftpd.go index 15977d83..b6b9e109 100644 --- a/internal/sftpd/sftpd.go +++ b/internal/sftpd/sftpd.go @@ -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 diff --git a/internal/sftpd/sftpd_test.go b/internal/sftpd/sftpd_test.go index 63737943..5fe6a61b 100644 --- a/internal/sftpd/sftpd_test.go +++ b/internal/sftpd/sftpd_test.go @@ -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) { diff --git a/internal/smtp/smtp.go b/internal/smtp/smtp.go index 25d4db6a..e3943c67 100644 --- a/internal/smtp/smtp.go +++ b/internal/smtp/smtp.go @@ -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) - } - - c, err := mail.NewClient(config.Host, config.getMailClientOptions()...) - if err != nil { - return fmt.Errorf("unable to create mail client: %w", err) - } - ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second) - defer cancelFn() - - return c.DialAndSendWithContext(ctx, m) + return config.sendEmail(to, subject, body, contentType, attachments...) +} + +// 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 { + logger.Error(logSender, "", "unable to load config from provider: %v", err) + return fmt.Errorf("smtp: unable to load config from provider: %w", err) + } + 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 } diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 72a9759d..d0e8bb25 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -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: diff --git a/templates/webadmin/base.html b/templates/webadmin/base.html index be75904d..75e74719 100644 --- a/templates/webadmin/base.html +++ b/templates/webadmin/base.html @@ -125,7 +125,7 @@ along with this program. If not, see .
{{ if .LoggedAdmin.HasPermission "manage_ip_lists"}} - {{.IPListsTitle}} + {{.IPListsTitle}} {{end}} {{ if and .HasDefender (.LoggedAdmin.HasPermission "view_defender")}} {{.DefenderTitle}} @@ -151,27 +151,29 @@ along with this program. If not, see . {{end}} - {{ if and .HasSearcher (.LoggedAdmin.HasPermission "view_events")}} - - {{end}} - - {{ if .LoggedAdmin.HasPermission "manage_system"}} - - {{end}} - - {{ if .LoggedAdmin.HasPermission "view_status"}} - {{end}} diff --git a/templates/webadmin/configs.html b/templates/webadmin/configs.html new file mode 100644 index 00000000..a62797db --- /dev/null +++ b/templates/webadmin/configs.html @@ -0,0 +1,327 @@ + +{{template "base" .}} + +{{define "title"}}{{.Title}}{{end}} + +{{define "extra_css"}} + +{{end}} + +{{define "page_body"}} +
+
+
{{.Title}}
+
+
+ {{if .Error}} + + {{end}} +
+ +
+
+
+

+ +

+
+ +
+
+
+
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.
+
+ +
+ +
+ +
+
+ +
+ +
+ + + Comma separated moduli file paths. Invalid/missing paths are silently ignored. Moduli are required to enable Diffie-Helmann Group Exchange KEX algos + +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+
+
+
+

+ +

+
+ +
+
+
+
Set the SMTP configuration replacing the one defined using env vars or config file if any.
+
+ +
+ +
+ + + If empty the configuration is disabled + +
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ + + HELO domain. Leave blank to use the server hostname + +
+
+ +
+
+
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+
+
+
+{{end}} +{{define "dialog"}} + + + +{{end}} +{{define "extra_js"}} + + +{{end}} diff --git a/templates/webadmin/status.html b/templates/webadmin/status.html index 0a28057d..638b1202 100644 --- a/templates/webadmin/status.html +++ b/templates/webadmin/status.html @@ -47,6 +47,15 @@ along with this program. If not, see . Fingerprint: "{{.Fingerprint}}"
{{end}} +
+ Host Key algorithms: "{{.Status.SSH.GetHostKeyAlgosAsString}}" +

+ MAC algorithms: "{{.Status.SSH.GetMACsAsString}}" +

+ KEX algorithms: "{{.Status.SSH.GetKEXsAsString}}" +

+ Ciphers: "{{.Status.SSH.GetCiphersAsString}}" +
{{end}}

diff --git a/tests/eventsearcher/go.mod b/tests/eventsearcher/go.mod index c803b553..61973a55 100644 --- a/tests/eventsearcher/go.mod +++ b/tests/eventsearcher/go.mod @@ -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 ) diff --git a/tests/eventsearcher/go.sum b/tests/eventsearcher/go.sum index de445216..091505f1 100644 --- a/tests/eventsearcher/go.sum +++ b/tests/eventsearcher/go.sum @@ -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= diff --git a/tests/ipfilter/go.mod b/tests/ipfilter/go.mod index c2d4b30c..3bbbb855 100644 --- a/tests/ipfilter/go.mod +++ b/tests/ipfilter/go.mod @@ -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 ) diff --git a/tests/ipfilter/go.sum b/tests/ipfilter/go.sum index ea977501..18c80f0c 100644 --- a/tests/ipfilter/go.sum +++ b/tests/ipfilter/go.sum @@ -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=