mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-21 23:20:24 +00:00
WebAdmin: add configs section
Setting configurations is an experimental feature and is not currently supported in the REST API Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
14961a573f
commit
a3fff56da5
39 changed files with 2193 additions and 382 deletions
|
@ -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`.
|
||||
|
|
|
@ -19,12 +19,12 @@ In this tutorial we'll focus on `HTTP-01` challenge type and make the following
|
|||
|
||||
## Overview
|
||||
|
||||
- [Obtaining a certificate using the Lego CLI tool](#Obtaining-a-certificate-using-the-Lego-CLI-tool)
|
||||
- [Automatic certificate renewal using the Lego CLI tool](#Automatic-certificate-renewal-using-the-Lego-CLI-tool)
|
||||
- [Obtaining a certificate using the ACME protocol built into SFTPGo](#Obtaining-a-certificate-using-the-ACME-protocol-built-into-SFTPGo)
|
||||
- [Enable HTTPS for SFTPGo Web UI and REST API](#Enable-HTTPS-for-SFTPGo-Web-UI-and-REST-API)
|
||||
- [Enable HTTPS for WebDAV service](#Enable-HTTPS-for-WebDAV-service)
|
||||
- [Enable explicit FTP over TLS](#Enable-explicit-FTP-over-TLS)
|
||||
- [Obtaining a certificate using the Lego CLI tool](#obtaining-a-certificate-using-the-lego-cli-tool)
|
||||
- [Automatic certificate renewal using the Lego CLI tool](#automatic-certificate-renewal-using-the-lego-cli-tool)
|
||||
- [Obtaining a certificate using the ACME protocol built into SFTPGo](#obtaining-a-certificate-using-the-acme-protocol-built-into-sftpgo)
|
||||
- [Enable HTTPS for SFTPGo Web UI and REST API](#enable-https-for-sftpgo-web-ui-and-rest-api)
|
||||
- [Enable HTTPS for WebDAV service](#enable-https-for-webdav-service)
|
||||
- [Enable explicit FTP over TLS](#enable-explicit-ftp-over-tls)
|
||||
|
||||
## Obtaining a certificate using the Lego CLI tool
|
||||
|
||||
|
@ -149,6 +149,8 @@ SFTPGO_ACME__HTTP01_CHALLENGE__WEBROOT="/var/www/sftpgo.com"
|
|||
Make sure that the `sftpgo` user can write to the `/var/www/sftpgo.com` directory or pre-create the `/var/www/sftpgo.com/.well-known/acme-challenge` directory with the appropriate permissions.
|
||||
This directory must be publicly served by your web server.
|
||||
|
||||
:warning: in this example we assume you have an existing HTTP server. If not, you can leave the web root blank and SFTPGo will resolve the HTTP01 challenge by itself.
|
||||
|
||||
Register your account and obtain certificates by running the following command.
|
||||
|
||||
```bash
|
||||
|
|
30
go.mod
30
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
|
||||
|
|
59
go.sum
59
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=
|
||||
|
|
|
@ -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.")
|
||||
},
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
boltDatabaseVersion = 27
|
||||
boltDatabaseVersion = 28
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -51,10 +51,12 @@ var (
|
|||
rulesBucket = []byte("events_rules")
|
||||
rolesBucket = []byte("roles")
|
||||
ipListsBucket = []byte("ip_lists")
|
||||
configsBucket = []byte("configs")
|
||||
dbVersionBucket = []byte("db_version")
|
||||
dbVersionKey = []byte("version")
|
||||
configsKey = []byte("configs")
|
||||
boltBuckets = [][]byte{usersBucket, groupsBucket, foldersBucket, adminsBucket, apiKeysBucket,
|
||||
sharesBucket, actionsBucket, rulesBucket, rolesBucket, ipListsBucket, dbVersionBucket}
|
||||
sharesBucket, actionsBucket, rulesBucket, rolesBucket, ipListsBucket, configsBucket, dbVersionBucket}
|
||||
)
|
||||
|
||||
// BoltProvider defines the auth provider for bolt key/value store
|
||||
|
@ -2977,6 +2979,39 @@ func (p *BoltProvider) getListEntriesForIP(ip string, listType IPListType) ([]IP
|
|||
return entries, err
|
||||
}
|
||||
|
||||
func (p *BoltProvider) getConfigs() (Configs, error) {
|
||||
var configs Configs
|
||||
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket(configsBucket)
|
||||
if bucket == nil {
|
||||
return fmt.Errorf("unable to find configs bucket")
|
||||
}
|
||||
data := bucket.Get(configsKey)
|
||||
if data != nil {
|
||||
return json.Unmarshal(data, &configs)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return configs, err
|
||||
}
|
||||
|
||||
func (p *BoltProvider) setConfigs(configs *Configs) error {
|
||||
if err := configs.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket(configsBucket)
|
||||
if bucket == nil {
|
||||
return fmt.Errorf("unable to find configs bucket")
|
||||
}
|
||||
buf, err := json.Marshal(configs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bucket.Put(configsKey, buf)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *BoltProvider) setFirstDownloadTimestamp(username string) error {
|
||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, err := p.getUsersBucket(tx)
|
||||
|
@ -3061,9 +3096,9 @@ func (p *BoltProvider) migrateDatabase() error {
|
|||
providerLog(logger.LevelError, "%v", err)
|
||||
logger.ErrorToConsole("%v", err)
|
||||
return err
|
||||
case version == 23, version == 24, version == 25, version == 26:
|
||||
logger.InfoToConsole("updating database schema version: %d -> 27", version)
|
||||
providerLog(logger.LevelInfo, "updating database schema version: %d -> 27", version)
|
||||
case version == 23, version == 24, version == 25, version == 26, version == 27:
|
||||
logger.InfoToConsole("updating database schema version: %d -> 28", version)
|
||||
providerLog(logger.LevelInfo, "updating database schema version: %d -> 28", version)
|
||||
err := p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
rules, err := p.dumpEventRules()
|
||||
if err != nil {
|
||||
|
@ -3095,7 +3130,7 @@ func (p *BoltProvider) migrateDatabase() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return updateBoltDatabaseVersion(p.dbHandle, 27)
|
||||
return updateBoltDatabaseVersion(p.dbHandle, 28)
|
||||
default:
|
||||
if version > boltDatabaseVersion {
|
||||
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
|
||||
|
@ -3108,7 +3143,7 @@ func (p *BoltProvider) migrateDatabase() error {
|
|||
}
|
||||
}
|
||||
|
||||
func (p *BoltProvider) revertDatabase(targetVersion int) error {
|
||||
func (p *BoltProvider) revertDatabase(targetVersion int) error { //nolint:gocyclo
|
||||
dbVersion, err := getBoltDatabaseVersion(p.dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -3117,7 +3152,7 @@ func (p *BoltProvider) revertDatabase(targetVersion int) error {
|
|||
return errors.New("current version match target version, nothing to do")
|
||||
}
|
||||
switch dbVersion.Version {
|
||||
case 24, 25, 26:
|
||||
case 24, 25, 26, 27, 28:
|
||||
logger.InfoToConsole("downgrading database schema version: %d -> 23", dbVersion.Version)
|
||||
providerLog(logger.LevelInfo, "downgrading database schema version: %d -> 23", dbVersion.Version)
|
||||
err := p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
|
@ -3145,10 +3180,12 @@ func (p *BoltProvider) revertDatabase(targetVersion int) error {
|
|||
}
|
||||
}
|
||||
}
|
||||
err = tx.DeleteBucket(rolesBucket)
|
||||
for _, b := range [][]byte{rolesBucket, configsBucket} {
|
||||
err = tx.DeleteBucket(b)
|
||||
if err != nil && !errors.Is(err, bolt.ErrBucketNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
|
|
299
internal/dataprovider/configs.go
Normal file
299
internal/dataprovider/configs.go
Normal file
|
@ -0,0 +1,299 @@
|
|||
// Copyright (C) 2019-2023 Nicola Murino
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, version 3.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package dataprovider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/kms"
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
)
|
||||
|
||||
// Supported values for host keys, KEXs, ciphers, MACs
|
||||
var (
|
||||
supportedHostKeyAlgos = []string{ssh.KeyAlgoRSA, ssh.CertAlgoRSAv01}
|
||||
supportedKexAlgos = []string{
|
||||
"diffie-hellman-group16-sha512", "diffie-hellman-group18-sha512",
|
||||
"diffie-hellman-group14-sha1", "diffie-hellman-group1-sha1",
|
||||
"diffie-hellman-group-exchange-sha256", "diffie-hellman-group-exchange-sha1",
|
||||
}
|
||||
supportedCiphers = []string{
|
||||
"aes128-cbc", "aes192-cbc", "aes256-cbc",
|
||||
"3des-cbc",
|
||||
}
|
||||
supportedMACs = []string{
|
||||
"hmac-sha2-512-etm@openssh.com", "hmac-sha2-512",
|
||||
"hmac-sha1", "hmac-sha1-96",
|
||||
}
|
||||
)
|
||||
|
||||
// SFTPDConfigs defines configurations for SFTPD
|
||||
type SFTPDConfigs struct {
|
||||
HostKeyAlgos []string `json:"host_key_algos,omitempty"`
|
||||
Moduli []string `json:"moduli,omitempty"`
|
||||
KexAlgorithms []string `json:"kex_algorithms,omitempty"`
|
||||
Ciphers []string `json:"ciphers,omitempty"`
|
||||
MACs []string `json:"macs,omitempty"`
|
||||
}
|
||||
|
||||
func (c *SFTPDConfigs) isEmpty() bool {
|
||||
if len(c.HostKeyAlgos) > 0 {
|
||||
return false
|
||||
}
|
||||
if len(c.Moduli) > 0 {
|
||||
return false
|
||||
}
|
||||
if len(c.KexAlgorithms) > 0 {
|
||||
return false
|
||||
}
|
||||
if len(c.Ciphers) > 0 {
|
||||
return false
|
||||
}
|
||||
if len(c.MACs) > 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// GetSupportedHostKeyAlgos returns the supported legacy host key algos
|
||||
func (*SFTPDConfigs) GetSupportedHostKeyAlgos() []string {
|
||||
return supportedHostKeyAlgos
|
||||
}
|
||||
|
||||
// GetSupportedKEXAlgos returns the supported KEX algos
|
||||
func (*SFTPDConfigs) GetSupportedKEXAlgos() []string {
|
||||
return supportedKexAlgos
|
||||
}
|
||||
|
||||
// GetSupportedCiphers returns the supported ciphers
|
||||
func (*SFTPDConfigs) GetSupportedCiphers() []string {
|
||||
return supportedCiphers
|
||||
}
|
||||
|
||||
// GetSupportedMACs returns the supported MACs algos
|
||||
func (*SFTPDConfigs) GetSupportedMACs() []string {
|
||||
return supportedMACs
|
||||
}
|
||||
|
||||
// GetModuliAsString returns moduli files as comma separated string
|
||||
func (c *SFTPDConfigs) GetModuliAsString() string {
|
||||
return strings.Join(c.Moduli, ",")
|
||||
}
|
||||
|
||||
func (c *SFTPDConfigs) validate() error {
|
||||
for _, algo := range c.HostKeyAlgos {
|
||||
if !util.Contains(supportedHostKeyAlgos, algo) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported host key algorithm %q", algo))
|
||||
}
|
||||
}
|
||||
for _, algo := range c.KexAlgorithms {
|
||||
if !util.Contains(supportedKexAlgos, algo) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported KEX algorithm %q", algo))
|
||||
}
|
||||
}
|
||||
for _, cipher := range c.Ciphers {
|
||||
if !util.Contains(supportedCiphers, cipher) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported cipher %q", cipher))
|
||||
}
|
||||
}
|
||||
for _, mac := range c.MACs {
|
||||
if !util.Contains(supportedMACs, mac) {
|
||||
return util.NewValidationError(fmt.Sprintf("unsupported MAC algorithm %q", mac))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *SFTPDConfigs) getACopy() *SFTPDConfigs {
|
||||
hostKeys := make([]string, len(c.HostKeyAlgos))
|
||||
copy(hostKeys, c.HostKeyAlgos)
|
||||
moduli := make([]string, len(c.Moduli))
|
||||
copy(moduli, c.Moduli)
|
||||
kexs := make([]string, len(c.KexAlgorithms))
|
||||
copy(kexs, c.KexAlgorithms)
|
||||
ciphers := make([]string, len(c.Ciphers))
|
||||
copy(ciphers, c.Ciphers)
|
||||
macs := make([]string, len(c.MACs))
|
||||
copy(macs, c.MACs)
|
||||
|
||||
return &SFTPDConfigs{
|
||||
HostKeyAlgos: hostKeys,
|
||||
Moduli: moduli,
|
||||
KexAlgorithms: kexs,
|
||||
Ciphers: ciphers,
|
||||
MACs: macs,
|
||||
}
|
||||
}
|
||||
|
||||
// SMTPConfigs defines configuration for SMTP
|
||||
type SMTPConfigs struct {
|
||||
Host string `json:"host,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
From string `json:"from,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
Password *kms.Secret `json:"password,omitempty"`
|
||||
AuthType int `json:"auth_type,omitempty"`
|
||||
Encryption int `json:"encryption,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
}
|
||||
|
||||
func (c *SMTPConfigs) isEmpty() bool {
|
||||
return c.Host == ""
|
||||
}
|
||||
|
||||
func (c *SMTPConfigs) validatePassword() error {
|
||||
if c.Password != nil {
|
||||
if c.Password.IsRedacted() {
|
||||
return util.NewValidationError("cannot save a redacted smtp password")
|
||||
}
|
||||
if c.Password.IsEncrypted() && !c.Password.IsValid() {
|
||||
return util.NewValidationError("invalid encrypted smtp password")
|
||||
}
|
||||
if !c.Password.IsEmpty() && !c.Password.IsValidInput() {
|
||||
return util.NewValidationError("invalid smtp password")
|
||||
}
|
||||
if c.Password.IsPlain() {
|
||||
c.Password.SetAdditionalData("smtp")
|
||||
if err := c.Password.Encrypt(); err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not encrypt smtp password: %v", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *SMTPConfigs) validate() error {
|
||||
if c.isEmpty() {
|
||||
return nil
|
||||
}
|
||||
if c.Port <= 0 || c.Port > 65535 {
|
||||
return util.NewValidationError(fmt.Sprintf("smtp: invalid port %d", c.Port))
|
||||
}
|
||||
if err := c.validatePassword(); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.User == "" && c.From == "" {
|
||||
return util.NewValidationError("smtp: from address and user cannot both be empty")
|
||||
}
|
||||
if c.AuthType < 0 || c.AuthType > 2 {
|
||||
return util.NewValidationError(fmt.Sprintf("smtp: invalid auth type %d", c.AuthType))
|
||||
}
|
||||
if c.Encryption < 0 || c.Encryption > 2 {
|
||||
return util.NewValidationError(fmt.Sprintf("smtp: invalid encryption %d", c.Encryption))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *SMTPConfigs) getACopy() *SMTPConfigs {
|
||||
var password *kms.Secret
|
||||
if c.Password != nil {
|
||||
password = c.Password.Clone()
|
||||
}
|
||||
return &SMTPConfigs{
|
||||
Host: c.Host,
|
||||
Port: c.Port,
|
||||
From: c.From,
|
||||
User: c.User,
|
||||
Password: password,
|
||||
AuthType: c.AuthType,
|
||||
Encryption: c.Encryption,
|
||||
Domain: c.Domain,
|
||||
}
|
||||
}
|
||||
|
||||
// Configs allows to set configuration keys disabled by default without
|
||||
// modifying the config file or setting env vars
|
||||
type Configs struct {
|
||||
SFTPD *SFTPDConfigs `json:"sftpd,omitempty"`
|
||||
SMTP *SMTPConfigs `json:"smtp,omitempty"`
|
||||
UpdatedAt int64 `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Configs) validate() error {
|
||||
if c.SFTPD != nil {
|
||||
if err := c.SFTPD.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if c.SMTP != nil {
|
||||
if err := c.SMTP.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrepareForRendering prepares configs for rendering.
|
||||
// It hides confidential data and set to nil the empty structs/secrets
|
||||
// so they are not serialized
|
||||
func (c *Configs) PrepareForRendering() {
|
||||
if c.SFTPD != nil && c.SFTPD.isEmpty() {
|
||||
c.SFTPD = nil
|
||||
}
|
||||
if c.SMTP != nil && c.SMTP.isEmpty() {
|
||||
c.SMTP = nil
|
||||
}
|
||||
if c.SMTP != nil && c.SMTP.Password != nil {
|
||||
c.SMTP.Password.Hide()
|
||||
if c.SMTP.Password.IsEmpty() {
|
||||
c.SMTP.Password = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetNilsToEmpty sets nil fields to empty
|
||||
func (c *Configs) SetNilsToEmpty() {
|
||||
if c.SFTPD == nil {
|
||||
c.SFTPD = &SFTPDConfigs{}
|
||||
}
|
||||
if c.SMTP == nil {
|
||||
c.SMTP = &SMTPConfigs{}
|
||||
}
|
||||
if c.SMTP.Password == nil {
|
||||
c.SMTP.Password = kms.NewEmptySecret()
|
||||
}
|
||||
}
|
||||
|
||||
// RenderAsJSON implements the renderer interface used within plugins
|
||||
func (c *Configs) RenderAsJSON(reload bool) ([]byte, error) {
|
||||
if reload {
|
||||
config, err := provider.getConfigs()
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "unable to reload config overrides before rendering as json: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
config.PrepareForRendering()
|
||||
return json.Marshal(config)
|
||||
}
|
||||
c.PrepareForRendering()
|
||||
return json.Marshal(c)
|
||||
}
|
||||
|
||||
func (c *Configs) getACopy() Configs {
|
||||
var result Configs
|
||||
if c.SFTPD != nil {
|
||||
result.SFTPD = c.SFTPD.getACopy()
|
||||
}
|
||||
if c.SMTP != nil {
|
||||
result.SMTP = c.SMTP.getACopy()
|
||||
}
|
||||
result.UpdatedAt = c.UpdatedAt
|
||||
return result
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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])
|
||||
|
|
57
internal/httpd/api_configs.go
Normal file
57
internal/httpd/api_configs.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
// Copyright (C) 2019-2023 Nicola Murino
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, version 3.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package httpd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/internal/smtp"
|
||||
)
|
||||
|
||||
type smtpTestRequest struct {
|
||||
smtp.Config
|
||||
Recipient string `json:"recipient"`
|
||||
}
|
||||
|
||||
func testSMTPConfig(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
|
||||
var req smtpTestRequest
|
||||
err := render.DecodeJSON(r.Body, &req)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.Password == redactedSecret {
|
||||
configs, err := dataprovider.GetConfigs()
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
configs.SetNilsToEmpty()
|
||||
if err := configs.SMTP.Password.TryDecrypt(); err == nil {
|
||||
req.Password = configs.SMTP.Password.GetPayload()
|
||||
}
|
||||
}
|
||||
if err := req.SendEmail([]string{req.Recipient}, "SFTPGo - Testing Email Settings",
|
||||
"It appears your SFTPGo email is setup correctly!", smtp.EmailContentTypeTextPlain); err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
sendAPIResponse(w, r, nil, "SMTP connection OK", http.StatusOK)
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/net/html"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/common"
|
||||
|
@ -167,6 +168,7 @@ const (
|
|||
webAdminRolesPath = "/web/admin/roles"
|
||||
webAdminRolePath = "/web/admin/role"
|
||||
webEventsPath = "/web/admin/events"
|
||||
webConfigsPath = "/web/admin/configs"
|
||||
webBasePathClient = "/web/client"
|
||||
webClientLoginPath = "/web/client/login"
|
||||
webClientFilesPath = "/web/client/files"
|
||||
|
@ -1288,6 +1290,46 @@ func TestGroupSettingsOverride(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestConfigs(t *testing.T) {
|
||||
err := dataprovider.UpdateConfigs(nil, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
configs, err := dataprovider.GetConfigs()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), configs.UpdatedAt)
|
||||
assert.Nil(t, configs.SFTPD)
|
||||
assert.Nil(t, configs.SMTP)
|
||||
configs = dataprovider.Configs{
|
||||
SFTPD: &dataprovider.SFTPDConfigs{},
|
||||
SMTP: &dataprovider.SMTPConfigs{},
|
||||
}
|
||||
err = dataprovider.UpdateConfigs(&configs, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
configs, err = dataprovider.GetConfigs()
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, configs.UpdatedAt, int64(0))
|
||||
|
||||
configs = dataprovider.Configs{
|
||||
SFTPD: &dataprovider.SFTPDConfigs{
|
||||
Ciphers: []string{"unknown"},
|
||||
},
|
||||
SMTP: &dataprovider.SMTPConfigs{},
|
||||
}
|
||||
err = dataprovider.UpdateConfigs(&configs, "", "", "")
|
||||
assert.ErrorIs(t, err, util.ErrValidation)
|
||||
configs = dataprovider.Configs{
|
||||
SFTPD: &dataprovider.SFTPDConfigs{},
|
||||
SMTP: &dataprovider.SMTPConfigs{
|
||||
Host: "smtp.example.com",
|
||||
Port: -1,
|
||||
},
|
||||
}
|
||||
err = dataprovider.UpdateConfigs(&configs, "", "", "")
|
||||
assert.ErrorIs(t, err, util.ErrValidation)
|
||||
|
||||
err = dataprovider.UpdateConfigs(nil, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestBasicIPListEntriesHandling(t *testing.T) {
|
||||
entry := dataprovider.IPListEntry{
|
||||
IPOrNet: "::ffff:12.34.56.78",
|
||||
|
@ -6782,6 +6824,7 @@ func TestProviderErrors(t *testing.T) {
|
|||
backupData := dataprovider.BackupData{
|
||||
Version: dataprovider.DumpVersion,
|
||||
}
|
||||
backupData.Configs = &dataprovider.Configs{}
|
||||
backupData.Users = append(backupData.Users, user)
|
||||
backupContent, err := json.Marshal(backupData)
|
||||
assert.NoError(t, err)
|
||||
|
@ -6790,6 +6833,13 @@ func TestProviderErrors(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
_, _, err = httpdtest.Loaddata(backupFilePath, "", "", http.StatusInternalServerError)
|
||||
assert.NoError(t, err)
|
||||
backupData.Configs = nil
|
||||
backupContent, err = json.Marshal(backupData)
|
||||
assert.NoError(t, err)
|
||||
err = os.WriteFile(backupFilePath, backupContent, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
_, _, err = httpdtest.Loaddata(backupFilePath, "", "", http.StatusInternalServerError)
|
||||
assert.NoError(t, err)
|
||||
backupData.Folders = append(backupData.Folders, vfs.BaseVirtualFolder{Name: "testFolder", MappedPath: filepath.Clean(os.TempDir())})
|
||||
backupContent, err = json.Marshal(backupData)
|
||||
assert.NoError(t, err)
|
||||
|
@ -7478,6 +7528,8 @@ func TestLoaddataFromPostBody(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLoaddata(t *testing.T) {
|
||||
err := dataprovider.UpdateConfigs(nil, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
mappedPath := filepath.Join(os.TempDir(), "restored_folder")
|
||||
folderName := filepath.Base(mappedPath)
|
||||
folderDesc := "restored folder desc"
|
||||
|
@ -7567,9 +7619,20 @@ func TestLoaddata(t *testing.T) {
|
|||
},
|
||||
},
|
||||
}
|
||||
configs := dataprovider.Configs{
|
||||
SFTPD: &dataprovider.SFTPDConfigs{
|
||||
HostKeyAlgos: []string{ssh.KeyAlgoRSA, ssh.CertAlgoRSAv01},
|
||||
},
|
||||
SMTP: &dataprovider.SMTPConfigs{
|
||||
Host: "mail.example.com",
|
||||
Port: 587,
|
||||
From: "from@example.net",
|
||||
},
|
||||
}
|
||||
backupData := dataprovider.BackupData{
|
||||
Version: 14,
|
||||
}
|
||||
backupData.Configs = &configs
|
||||
backupData.Users = append(backupData.Users, user)
|
||||
backupData.Roles = append(backupData.Roles, role)
|
||||
backupData.Groups = append(backupData.Groups, group)
|
||||
|
@ -7621,6 +7684,15 @@ func TestLoaddata(t *testing.T) {
|
|||
// update from backup
|
||||
_, _, err = httpdtest.Loaddata(backupFilePath, "2", "", http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
configsGet, err := dataprovider.GetConfigs()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, configs.SMTP, configsGet.SMTP)
|
||||
assert.Equal(t, configs.SFTPD.HostKeyAlgos, configsGet.SFTPD.HostKeyAlgos)
|
||||
assert.Len(t, configsGet.SFTPD.Moduli, 0)
|
||||
assert.Len(t, configsGet.SFTPD.KexAlgorithms, 0)
|
||||
assert.Len(t, configsGet.SFTPD.Ciphers, 0)
|
||||
assert.Len(t, configsGet.SFTPD.MACs, 0)
|
||||
assert.Greater(t, configsGet.UpdatedAt, int64(0))
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, user.VirtualFolders, 1)
|
||||
|
@ -7751,11 +7823,20 @@ func TestLoaddata(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = os.Remove(backupFilePath)
|
||||
assert.NoError(t, err)
|
||||
err = dataprovider.UpdateConfigs(nil, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLoaddataMode(t *testing.T) {
|
||||
err := dataprovider.UpdateConfigs(nil, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
mappedPath := filepath.Join(os.TempDir(), "restored_fold")
|
||||
folderName := filepath.Base(mappedPath)
|
||||
configs := dataprovider.Configs{
|
||||
SFTPD: &dataprovider.SFTPDConfigs{
|
||||
Moduli: []string{"/moduli"},
|
||||
},
|
||||
}
|
||||
role := getTestRole()
|
||||
role.ID = 1
|
||||
role.Name = "test_role_load"
|
||||
|
@ -7834,6 +7915,7 @@ func TestLoaddataMode(t *testing.T) {
|
|||
backupData := dataprovider.BackupData{
|
||||
Version: dataprovider.DumpVersion,
|
||||
}
|
||||
backupData.Configs = &configs
|
||||
backupData.Users = append(backupData.Users, user)
|
||||
backupData.Groups = append(backupData.Groups, group)
|
||||
backupData.Admins = append(backupData.Admins, admin)
|
||||
|
@ -7859,10 +7941,13 @@ func TestLoaddataMode(t *testing.T) {
|
|||
backupData.IPLists = append(backupData.IPLists, ipListEntry)
|
||||
backupContent, _ := json.Marshal(backupData)
|
||||
backupFilePath := filepath.Join(backupsPath, "backup.json")
|
||||
err := os.WriteFile(backupFilePath, backupContent, os.ModePerm)
|
||||
err = os.WriteFile(backupFilePath, backupContent, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
_, _, err = httpdtest.Loaddata(backupFilePath, "0", "0", http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
configs, err = dataprovider.GetConfigs()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, configs.SFTPD.Moduli, 1)
|
||||
folder, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, mappedPath+"1", folder.MappedPath)
|
||||
|
@ -7934,6 +8019,10 @@ func TestLoaddataMode(t *testing.T) {
|
|||
entry, _, err = httpdtest.UpdateIPListEntry(entry, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
||||
configs.SFTPD.Moduli = append(configs.SFTPD.Moduli, "/moduli_new")
|
||||
err = dataprovider.UpdateConfigs(&configs, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
backupData.Configs = &configs
|
||||
backupData.Folders = []vfs.BaseVirtualFolder{
|
||||
{
|
||||
MappedPath: mappedPath,
|
||||
|
@ -7942,6 +8031,9 @@ func TestLoaddataMode(t *testing.T) {
|
|||
}
|
||||
_, _, err = httpdtest.Loaddata(backupFilePath, "0", "1", http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
configs, err = dataprovider.GetConfigs()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, configs.SFTPD.Moduli, 2)
|
||||
group, _, err = httpdtest.GetGroupByName(group.Name, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, oldGroupDesc, group.Description)
|
||||
|
@ -7999,6 +8091,9 @@ func TestLoaddataMode(t *testing.T) {
|
|||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, oldUploadBandwidth, user.UploadBandwidth)
|
||||
configs, err = dataprovider.GetConfigs()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, configs.SFTPD.Moduli, 1)
|
||||
// the group is referenced
|
||||
_, err = httpdtest.RemoveGroup(group, http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
|
@ -8022,6 +8117,8 @@ func TestLoaddataMode(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = os.Remove(backupFilePath)
|
||||
assert.NoError(t, err)
|
||||
err = dataprovider.UpdateConfigs(nil, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRateLimiter(t *testing.T) {
|
||||
|
@ -8992,6 +9089,84 @@ func TestChangeAdminPwdInvalidJsonMock(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
}
|
||||
|
||||
func TestSMTPConfig(t *testing.T) {
|
||||
smtpCfg := smtp.Config{
|
||||
Host: "127.0.0.1",
|
||||
Port: 3525,
|
||||
From: "notification@example.com",
|
||||
TemplatesPath: "templates",
|
||||
}
|
||||
err := smtpCfg.Initialize(configDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
smtpTestURL := path.Join(webConfigsPath, "smtp", "test")
|
||||
tokenHeader := "X-CSRF-TOKEN"
|
||||
webToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
csrfToken, err := getCSRFToken(httpBaseURL + webClientLoginPath)
|
||||
assert.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodPost, smtpTestURL, bytes.NewBuffer([]byte("{")))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
|
||||
req.Header.Set(tokenHeader, csrfToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
|
||||
testReq := make(map[string]any)
|
||||
testReq["host"] = smtpCfg.Host
|
||||
testReq["port"] = 3525
|
||||
testReq["from"] = "from@example.com"
|
||||
asJSON, err := json.Marshal(testReq)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, smtpTestURL, bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set(tokenHeader, csrfToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusInternalServerError, rr)
|
||||
|
||||
testReq["recipient"] = "example@example.com"
|
||||
asJSON, err = json.Marshal(testReq)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, smtpTestURL, bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set(tokenHeader, csrfToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
configs := dataprovider.Configs{
|
||||
SMTP: &dataprovider.SMTPConfigs{
|
||||
Host: "127.0.0.1",
|
||||
Port: 3535,
|
||||
User: "user@example.com",
|
||||
Password: kms.NewPlainSecret(defaultPassword),
|
||||
},
|
||||
}
|
||||
err = dataprovider.UpdateConfigs(&configs, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
testReq["password"] = redactedSecret
|
||||
asJSON, err = json.Marshal(testReq)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, smtpTestURL, bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set(tokenHeader, csrfToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusInternalServerError, rr)
|
||||
assert.Contains(t, rr.Body.String(), "server does not support SMTP AUTH")
|
||||
|
||||
err = dataprovider.UpdateConfigs(nil, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
smtpCfg = smtp.Config{}
|
||||
err = smtpCfg.Initialize(configDir)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMFAPermission(t *testing.T) {
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
@ -12072,6 +12247,149 @@ func TestMaxSessions(t *testing.T) {
|
|||
assert.Len(t, common.Connections.GetStats(""), 0)
|
||||
}
|
||||
|
||||
func TestWebConfigsMock(t *testing.T) {
|
||||
err := dataprovider.UpdateConfigs(nil, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
webToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
csrfToken, err := getCSRFToken(httpBaseURL + webClientLoginPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, webConfigsPath, nil)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
form := make(url.Values)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
// parse form error
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath+"?p=p%C3%AO%GH", bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
// save SFTP configs
|
||||
form.Set("sftp_host_key_algos", ssh.KeyAlgoRSA)
|
||||
form.Add("sftp_host_key_algos", ssh.CertAlgoDSAv01)
|
||||
form.Set("sftp_moduli", "path 1 , path 2")
|
||||
form.Set("form_action", "sftp_submit")
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), ssh.CertAlgoDSAv01) // invalid algo
|
||||
form.Set("sftp_host_key_algos", ssh.KeyAlgoRSA)
|
||||
form.Add("sftp_host_key_algos", ssh.CertAlgoRSAv01)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Configurations updated")
|
||||
// check SFTP configs
|
||||
configs, err := dataprovider.GetConfigs()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, configs.SFTPD.HostKeyAlgos, 2)
|
||||
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA)
|
||||
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.CertAlgoRSAv01)
|
||||
assert.Len(t, configs.SFTPD.Moduli, 2)
|
||||
assert.Contains(t, configs.SFTPD.Moduli, "path 1")
|
||||
assert.Contains(t, configs.SFTPD.Moduli, "path 2")
|
||||
// invalid form action
|
||||
form.Set("form_action", "")
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
assert.Contains(t, rr.Body.String(), "unsupported form action")
|
||||
// test SMTP configs
|
||||
form.Set("form_action", "smtp_submit")
|
||||
form.Set("smtp_host", "mail.example.net")
|
||||
form.Set("smtp_from", "Example <info@example.net>")
|
||||
form.Set("smtp_username", defaultUsername)
|
||||
form.Set("smtp_password", defaultPassword)
|
||||
form.Set("smtp_domain", "localdomain")
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Validation error") // port is not passed so 0
|
||||
// set valid parameters
|
||||
form.Set("smtp_port", "465")
|
||||
form.Set("smtp_auth", "1")
|
||||
form.Set("smtp_encryption", "2")
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Configurations updated")
|
||||
// check
|
||||
configs, err = dataprovider.GetConfigs()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, configs.SFTPD.HostKeyAlgos, 2)
|
||||
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA)
|
||||
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.CertAlgoRSAv01)
|
||||
assert.Len(t, configs.SFTPD.Moduli, 2)
|
||||
assert.Equal(t, "mail.example.net", configs.SMTP.Host)
|
||||
assert.Equal(t, 465, configs.SMTP.Port)
|
||||
assert.Equal(t, "Example <info@example.net>", configs.SMTP.From)
|
||||
assert.Equal(t, defaultUsername, configs.SMTP.User)
|
||||
err = configs.SMTP.Password.Decrypt()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, defaultPassword, configs.SMTP.Password.GetPayload())
|
||||
assert.Equal(t, 1, configs.SMTP.AuthType)
|
||||
assert.Equal(t, 2, configs.SMTP.Encryption)
|
||||
assert.Equal(t, "localdomain", configs.SMTP.Domain)
|
||||
// set a redacted password, the current password must be preserved
|
||||
form.Set("smtp_password", redactedSecret)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Configurations updated")
|
||||
updatedConfigs, err := dataprovider.GetConfigs()
|
||||
assert.NoError(t, err)
|
||||
encryptedPayload := updatedConfigs.SMTP.Password.GetPayload()
|
||||
secretKey := updatedConfigs.SMTP.Password.GetKey()
|
||||
err = updatedConfigs.SMTP.Password.Decrypt()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, configs.SFTPD, updatedConfigs.SFTPD)
|
||||
assert.Equal(t, configs.SMTP, updatedConfigs.SMTP)
|
||||
// now set an undecryptable password
|
||||
updatedConfigs.SMTP.Password = kms.NewSecret(sdkkms.SecretStatusSecretBox, encryptedPayload, secretKey, "")
|
||||
err = dataprovider.UpdateConfigs(&updatedConfigs, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Configurations updated")
|
||||
|
||||
err = dataprovider.UpdateConfigs(nil, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSFTPLoopError(t *testing.T) {
|
||||
user1 := getTestUser()
|
||||
user2 := getTestUser()
|
||||
|
@ -22949,10 +23267,35 @@ func TestProviderClosedMock(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
dataprovider.Close()
|
||||
req, _ := http.NewRequest(http.MethodGet, webFoldersPath, nil)
|
||||
|
||||
testReq := make(map[string]any)
|
||||
testReq["password"] = redactedSecret
|
||||
asJSON, err := json.Marshal(testReq)
|
||||
assert.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodPost, path.Join(webConfigsPath, "smtp", "test"), bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("X-CSRF-TOKEN", csrfToken)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusInternalServerError, rr)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, webConfigsPath, nil)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusInternalServerError, rr)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, nil)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusInternalServerError, rr)
|
||||
|
||||
req, _ = http.NewRequest(http.MethodGet, webFoldersPath, nil)
|
||||
setJWTCookieForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusInternalServerError, rr)
|
||||
req, _ = http.NewRequest(http.MethodGet, webUsersPath, nil)
|
||||
setJWTCookieForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import (
|
|||
"github.com/drakkan/sftpgo/v2/internal/common"
|
||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/internal/kms"
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
"github.com/drakkan/sftpgo/v2/internal/mfa"
|
||||
"github.com/drakkan/sftpgo/v2/internal/plugin"
|
||||
"github.com/drakkan/sftpgo/v2/internal/smtp"
|
||||
|
@ -94,6 +95,7 @@ const (
|
|||
templateDefender = "defender.html"
|
||||
templateIPLists = "iplists.html"
|
||||
templateIPList = "iplist.html"
|
||||
templateConfigs = "configs.html"
|
||||
templateProfile = "profile.html"
|
||||
templateChangePwd = "changepassword.html"
|
||||
templateMaintenance = "maintenance.html"
|
||||
|
@ -114,6 +116,7 @@ const (
|
|||
pageDefenderTitle = "Auto Blocklist"
|
||||
pageIPListsTitle = "IP Lists"
|
||||
pageEventsTitle = "Logs"
|
||||
pageConfigsTitle = "Configurations"
|
||||
pageForgotPwdTitle = "SFTPGo Admin - Forgot password"
|
||||
pageResetPwdTitle = "SFTPGo Admin - Reset password"
|
||||
pageSetupTitle = "Create first admin user"
|
||||
|
@ -144,6 +147,7 @@ type basePage struct {
|
|||
IPListsURL string
|
||||
IPListURL string
|
||||
EventsURL string
|
||||
ConfigsURL string
|
||||
LogoutURL string
|
||||
ProfileURL string
|
||||
ChangePwdURL string
|
||||
|
@ -171,10 +175,12 @@ type basePage struct {
|
|||
DefenderTitle string
|
||||
IPListsTitle string
|
||||
EventsTitle string
|
||||
ConfigsTitle string
|
||||
Version string
|
||||
CSRFToken string
|
||||
IsEventManagerPage bool
|
||||
IsIPManagerPage bool
|
||||
IsServerManagerPage bool
|
||||
HasDefender bool
|
||||
HasSearcher bool
|
||||
HasExternalLogin bool
|
||||
|
@ -383,6 +389,14 @@ type eventsPage struct {
|
|||
ProviderEventsSearchURL string
|
||||
}
|
||||
|
||||
type configsPage struct {
|
||||
basePage
|
||||
Configs dataprovider.Configs
|
||||
ConfigSection int
|
||||
RedactedSecret string
|
||||
Error string
|
||||
}
|
||||
|
||||
type messagePage struct {
|
||||
basePage
|
||||
Error string
|
||||
|
@ -554,6 +568,11 @@ func loadAdminTemplates(templatesPath string) {
|
|||
filepath.Join(templatesPath, templateAdminDir, templateBase),
|
||||
filepath.Join(templatesPath, templateAdminDir, templateEvents),
|
||||
}
|
||||
configsPaths := []string{
|
||||
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
|
||||
filepath.Join(templatesPath, templateAdminDir, templateBase),
|
||||
filepath.Join(templatesPath, templateAdminDir, templateConfigs),
|
||||
}
|
||||
|
||||
fsBaseTpl := template.New("fsBaseTemplate").Funcs(template.FuncMap{
|
||||
"ListFSProviders": func() []sdk.FilesystemProvider {
|
||||
|
@ -595,6 +614,7 @@ func loadAdminTemplates(templatesPath string) {
|
|||
rolesTmpl := util.LoadTemplate(nil, rolesPaths...)
|
||||
roleTmpl := util.LoadTemplate(nil, rolePaths...)
|
||||
eventsTmpl := util.LoadTemplate(nil, eventsPaths...)
|
||||
configsTmpl := util.LoadTemplate(nil, configsPaths...)
|
||||
|
||||
adminTemplates[templateUsers] = usersTmpl
|
||||
adminTemplates[templateUser] = userTmpl
|
||||
|
@ -627,6 +647,7 @@ func loadAdminTemplates(templatesPath string) {
|
|||
adminTemplates[templateRoles] = rolesTmpl
|
||||
adminTemplates[templateRole] = roleTmpl
|
||||
adminTemplates[templateEvents] = eventsTmpl
|
||||
adminTemplates[templateConfigs] = configsTmpl
|
||||
}
|
||||
|
||||
func isEventManagerResource(currentURL string) bool {
|
||||
|
@ -658,6 +679,11 @@ func isIPListsResource(currentURL string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func isServerManagerResource(currentURL string) bool {
|
||||
return currentURL == webEventsPath || currentURL == webStatusPath || currentURL == webMaintenancePath ||
|
||||
currentURL == webConfigsPath
|
||||
}
|
||||
|
||||
func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request) basePage {
|
||||
var csrfToken string
|
||||
if currentURL != "" {
|
||||
|
@ -680,6 +706,7 @@ func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request)
|
|||
IPListsURL: webIPListsPath,
|
||||
IPListURL: webIPListPath,
|
||||
EventsURL: webEventsPath,
|
||||
ConfigsURL: webConfigsPath,
|
||||
LogoutURL: webLogoutPath,
|
||||
ProfileURL: webAdminProfilePath,
|
||||
ChangePwdURL: webChangeAdminPwdPath,
|
||||
|
@ -709,10 +736,12 @@ func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request)
|
|||
DefenderTitle: pageDefenderTitle,
|
||||
IPListsTitle: pageIPListsTitle,
|
||||
EventsTitle: pageEventsTitle,
|
||||
ConfigsTitle: pageConfigsTitle,
|
||||
Version: version.GetAsString(),
|
||||
LoggedAdmin: getAdminFromToken(r),
|
||||
IsEventManagerPage: isEventManagerResource(currentURL),
|
||||
IsIPManagerPage: isIPListsResource(currentURL),
|
||||
IsServerManagerPage: isServerManagerResource(currentURL),
|
||||
HasDefender: common.Config.DefenderConfig.Enabled,
|
||||
HasSearcher: plugin.Handler.HasSearcher(),
|
||||
HasExternalLogin: isLoggedInWithOIDC(r),
|
||||
|
@ -867,6 +896,24 @@ func (s *httpdServer) renderMaintenancePage(w http.ResponseWriter, r *http.Reque
|
|||
renderAdminTemplate(w, templateMaintenance, data)
|
||||
}
|
||||
|
||||
func (s *httpdServer) renderConfigsPage(w http.ResponseWriter, r *http.Request, configs dataprovider.Configs,
|
||||
error string, section int,
|
||||
) {
|
||||
configs.SetNilsToEmpty()
|
||||
if configs.SMTP.Port == 0 {
|
||||
configs.SMTP.Port = 587
|
||||
}
|
||||
data := configsPage{
|
||||
basePage: s.getBasePageData(pageConfigsTitle, webConfigsPath, r),
|
||||
Configs: configs,
|
||||
ConfigSection: section,
|
||||
RedactedSecret: redactedSecret,
|
||||
Error: error,
|
||||
}
|
||||
|
||||
renderAdminTemplate(w, templateConfigs, data)
|
||||
}
|
||||
|
||||
func (s *httpdServer) renderAdminSetupPage(w http.ResponseWriter, r *http.Request, username, error string) {
|
||||
data := setupPage{
|
||||
basePage: s.getBasePageData(pageSetupTitle, webAdminSetupPath, r),
|
||||
|
@ -2218,11 +2265,11 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven
|
|||
}
|
||||
var emailAttachments []string
|
||||
if r.Form.Get("email_attachments") != "" {
|
||||
emailAttachments = strings.Split(strings.ReplaceAll(r.Form.Get("email_attachments"), " ", ""), ",")
|
||||
emailAttachments = getSliceFromDelimitedValues(r.Form.Get("email_attachments"), ",")
|
||||
}
|
||||
var cmdArgs []string
|
||||
if r.Form.Get("cmd_arguments") != "" {
|
||||
cmdArgs = strings.Split(strings.ReplaceAll(r.Form.Get("cmd_arguments"), " ", ""), ",")
|
||||
cmdArgs = getSliceFromDelimitedValues(r.Form.Get("cmd_arguments"), ",")
|
||||
}
|
||||
options := dataprovider.BaseEventActionOptions{
|
||||
HTTPConfig: dataprovider.EventActionHTTPConfig{
|
||||
|
@ -2244,7 +2291,7 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven
|
|||
EnvVars: getKeyValsFromPostFields(r, "cmd_env_key", "cmd_env_val"),
|
||||
},
|
||||
EmailConfig: dataprovider.EventActionEmailConfig{
|
||||
Recipients: strings.Split(strings.ReplaceAll(r.Form.Get("email_recipients"), " ", ""), ","),
|
||||
Recipients: getSliceFromDelimitedValues(r.Form.Get("email_recipients"), ","),
|
||||
Subject: r.Form.Get("email_subject"),
|
||||
Body: r.Form.Get("email_body"),
|
||||
Attachments: emailAttachments,
|
||||
|
@ -2255,13 +2302,13 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven
|
|||
FsConfig: dataprovider.EventActionFilesystemConfig{
|
||||
Type: fsActionType,
|
||||
Renames: getKeyValsFromPostFields(r, "fs_rename_source", "fs_rename_target"),
|
||||
Deletes: strings.Split(strings.ReplaceAll(r.Form.Get("fs_delete_paths"), " ", ""), ","),
|
||||
MkDirs: strings.Split(strings.ReplaceAll(r.Form.Get("fs_mkdir_paths"), " ", ""), ","),
|
||||
Exist: strings.Split(strings.ReplaceAll(r.Form.Get("fs_exist_paths"), " ", ""), ","),
|
||||
Deletes: getSliceFromDelimitedValues(r.Form.Get("fs_delete_paths"), ","),
|
||||
MkDirs: getSliceFromDelimitedValues(r.Form.Get("fs_mkdir_paths"), ","),
|
||||
Exist: getSliceFromDelimitedValues(r.Form.Get("fs_exist_paths"), ","),
|
||||
Copy: getKeyValsFromPostFields(r, "fs_copy_source", "fs_copy_target"),
|
||||
Compress: dataprovider.EventActionFsCompress{
|
||||
Name: r.Form.Get("fs_compress_name"),
|
||||
Paths: strings.Split(strings.ReplaceAll(r.Form.Get("fs_compress_paths"), " ", ""), ","),
|
||||
Paths: getSliceFromDelimitedValues(r.Form.Get("fs_compress_paths"), ","),
|
||||
},
|
||||
},
|
||||
PwdExpirationConfig: dataprovider.EventActionPasswordExpiration{
|
||||
|
@ -2487,6 +2534,41 @@ func getIPListEntryFromPostFields(r *http.Request, listType dataprovider.IPListT
|
|||
}, nil
|
||||
}
|
||||
|
||||
func getSFTPConfigsFromPostFields(r *http.Request) *dataprovider.SFTPDConfigs {
|
||||
return &dataprovider.SFTPDConfigs{
|
||||
HostKeyAlgos: r.Form["sftp_host_key_algos"],
|
||||
Moduli: getSliceFromDelimitedValues(r.Form.Get("sftp_moduli"), ","),
|
||||
KexAlgorithms: r.Form["sftp_kex_algos"],
|
||||
Ciphers: r.Form["sftp_ciphers"],
|
||||
MACs: r.Form["sftp_macs"],
|
||||
}
|
||||
}
|
||||
|
||||
func getSMTPConfigsFromPostFields(r *http.Request) *dataprovider.SMTPConfigs {
|
||||
port, err := strconv.Atoi(r.Form.Get("smtp_port"))
|
||||
if err != nil {
|
||||
port = 0
|
||||
}
|
||||
authType, err := strconv.Atoi(r.Form.Get("smtp_auth"))
|
||||
if err != nil {
|
||||
authType = 0
|
||||
}
|
||||
encryption, err := strconv.Atoi(r.Form.Get("smtp_encryption"))
|
||||
if err != nil {
|
||||
encryption = 0
|
||||
}
|
||||
return &dataprovider.SMTPConfigs{
|
||||
Host: r.Form.Get("smtp_host"),
|
||||
Port: port,
|
||||
From: r.Form.Get("smtp_from"),
|
||||
User: r.Form.Get("smtp_username"),
|
||||
Password: getSecretFromFormField(r, "smtp_password"),
|
||||
AuthType: authType,
|
||||
Encryption: encryption,
|
||||
Domain: r.Form.Get("smtp_domain"),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebAdminForgotPwd(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
if !smtp.IsEnabled() {
|
||||
|
@ -3921,3 +4003,70 @@ func (s *httpdServer) handleWebUpdateIPListEntryPost(w http.ResponseWriter, r *h
|
|||
}
|
||||
http.Redirect(w, r, webIPListsPath, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebConfigs(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
configs, err := dataprovider.GetConfigs()
|
||||
if err != nil {
|
||||
s.renderInternalServerErrorPage(w, r, err)
|
||||
return
|
||||
}
|
||||
s.renderConfigsPage(w, r, configs, "", 0)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebConfigsPost(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
claims, err := getTokenClaims(r)
|
||||
if err != nil || claims.Username == "" {
|
||||
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
|
||||
return
|
||||
}
|
||||
configs, err := dataprovider.GetConfigs()
|
||||
if err != nil {
|
||||
s.renderInternalServerErrorPage(w, r, err)
|
||||
return
|
||||
}
|
||||
err = r.ParseForm()
|
||||
if err != nil {
|
||||
s.renderBadRequestPage(w, r, err)
|
||||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
s.renderForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
var configSection int
|
||||
switch r.Form.Get("form_action") {
|
||||
case "sftp_submit":
|
||||
configSection = 1
|
||||
sftpConfigs := getSFTPConfigsFromPostFields(r)
|
||||
configs.SFTPD = sftpConfigs
|
||||
case "smtp_submit":
|
||||
configSection = 2
|
||||
smtpConfigs := getSMTPConfigsFromPostFields(r)
|
||||
if smtpConfigs.Password.IsNotPlainAndNotEmpty() {
|
||||
smtpConfigs.Password = configs.SMTP.Password
|
||||
}
|
||||
configs.SMTP = smtpConfigs
|
||||
default:
|
||||
s.renderBadRequestPage(w, r, errors.New("unsupported form action"))
|
||||
return
|
||||
}
|
||||
|
||||
err = dataprovider.UpdateConfigs(&configs, claims.Username, ipAddr, claims.Role)
|
||||
if err != nil {
|
||||
s.renderConfigsPage(w, r, configs, err.Error(), configSection)
|
||||
return
|
||||
}
|
||||
if configSection == 2 {
|
||||
err := configs.SMTP.Password.TryDecrypt()
|
||||
if err == nil {
|
||||
smtp.Activate(configs.SMTP)
|
||||
} else {
|
||||
logger.Error(logSender, "", "unable to decrypt SMTP password, cannot activate configuration")
|
||||
}
|
||||
}
|
||||
s.renderMessagePage(w, r, "Configurations updated", "", http.StatusOK, nil,
|
||||
"Configurations has been successfully updated")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -151,27 +151,29 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
</li>
|
||||
{{end}}
|
||||
|
||||
{{ if and .HasSearcher (.LoggedAdmin.HasPermission "view_events")}}
|
||||
<li class="nav-item {{if eq .CurrentURL .EventsURL}}active{{end}}">
|
||||
<a class="nav-link" href="{{.EventsURL}}">
|
||||
<i class="fas fa-clipboard-list"></i>
|
||||
<span>{{.EventsTitle}}</span></a>
|
||||
</li>
|
||||
{{end}}
|
||||
|
||||
{{ if or (.LoggedAdmin.HasPermission "manage_system") (.LoggedAdmin.HasPermission "view_status") (and .HasSearcher (.LoggedAdmin.HasPermission "view_events"))}}
|
||||
<li class="nav-item {{if .IsServerManagerPage}}active{{end}}">
|
||||
<a class="nav-link {{if not .IsServerManagerPage}}collapsed{{end}}" href="#" data-toggle="collapse" data-target="#collapseServerManager"
|
||||
aria-expanded="true" aria-controls="collapseServerManager">
|
||||
<i class="fas fa-tools"></i>
|
||||
<span>Server Manager</span>
|
||||
</a>
|
||||
<div id="collapseServerManager" class="collapse {{if .IsServerManagerPage}}show{{end}}" aria-labelledby="headingServerManager" data-parent="#accordionSidebar">
|
||||
<div class="bg-white py-2 collapse-inner rounded">
|
||||
{{ if .LoggedAdmin.HasPermission "manage_system"}}
|
||||
<li class="nav-item {{if eq .CurrentURL .MaintenanceURL}}active{{end}}">
|
||||
<a class="nav-link" href="{{.MaintenanceURL}}">
|
||||
<i class="fas fa-wrench"></i>
|
||||
<span>{{.MaintenanceTitle}}</span></a>
|
||||
</li>
|
||||
<a class="collapse-item {{if eq .CurrentURL .ConfigsURL}}active{{end}}" href="{{.ConfigsURL}}">{{.ConfigsTitle}}</a>
|
||||
{{end}}
|
||||
{{ if and .HasSearcher (.LoggedAdmin.HasPermission "view_events")}}
|
||||
<a class="collapse-item {{if eq .CurrentURL .EventsURL}}active{{end}}" href="{{.EventsURL}}">{{.EventsTitle}}</a>
|
||||
{{end}}
|
||||
{{ if .LoggedAdmin.HasPermission "manage_system"}}
|
||||
<a class="collapse-item {{if eq .CurrentURL .MaintenanceURL}}active{{end}}" href="{{.MaintenanceURL}}">{{.MaintenanceTitle}}</a>
|
||||
{{end}}
|
||||
|
||||
{{ if .LoggedAdmin.HasPermission "view_status"}}
|
||||
<li class="nav-item {{if eq .CurrentURL .StatusURL}}active{{end}}">
|
||||
<a class="nav-link" href="{{.StatusURL}}">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<span>{{.StatusTitle}}</span></a>
|
||||
<a class="collapse-item {{if eq .CurrentURL .StatusURL}}active{{end}}" href="{{.StatusURL}}">{{.StatusTitle}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{end}}
|
||||
|
||||
|
|
327
templates/webadmin/configs.html
Normal file
327
templates/webadmin/configs.html
Normal file
|
@ -0,0 +1,327 @@
|
|||
<!--
|
||||
Copyright (C) 2019-2023 Nicola Murino
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, version 3.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}{{.Title}}{{end}}
|
||||
|
||||
{{define "extra_css"}}
|
||||
<link href="{{.StaticURL}}/vendor/bootstrap-select/css/bootstrap-select.min.css" rel="stylesheet">
|
||||
{{end}}
|
||||
|
||||
{{define "page_body"}}
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">{{.Title}}</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{if .Error}}
|
||||
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
||||
{{.Error}}
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
{{end}}
|
||||
<form id="configs_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
|
||||
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
|
||||
<div class="accordion" id="accordionConfigs">
|
||||
<div class="card">
|
||||
<div class="card-header" id="headingSFTPD">
|
||||
<h2 class="mb-0">
|
||||
<button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse"
|
||||
data-target="#collapseSFTPD" aria-expanded="true" aria-controls="collapseSFTPD">
|
||||
<h6 class="m-0 font-weight-bold text-primary">SFTP</h6>
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div id="collapseSFTPD" class="collapse {{if eq .ConfigSection 1}}show{{end}}" aria-labelledby="headingSFTPD" data-parent="#accordionConfigs">
|
||||
<div class="card-body">
|
||||
<div id="configs-sftp-info" class="card mb-3 border-left-info">
|
||||
<div class="card-body">Here you can enable algorithms disabled by default. You don't need to set values already defined using env vars or config file. A service restart is required to apply changes.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idHostKeyAlgos" class="col-sm-2 col-form-label">Host Key Algos</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control selectpicker" id="idHostKeyAlgos" name="sftp_host_key_algos" multiple>
|
||||
{{range $val := .Configs.SFTPD.GetSupportedHostKeyAlgos}}
|
||||
<option value="{{$val}}" {{range $algo :=$.Configs.SFTPD.HostKeyAlgos }}{{if eq $algo $val}}selected{{end}}{{end}}>{{$val}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idModuli" class="col-sm-2 col-form-label">Moduli</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="idModuli" name="sftp_moduli" rows="2" placeholder=""
|
||||
aria-describedby="moduliHelpBlock">{{.Configs.SFTPD.GetModuliAsString}}</textarea>
|
||||
<small id="moduliHelpBlock" class="form-text text-muted">
|
||||
Comma separated moduli file paths. Invalid/missing paths are silently ignored. Moduli are required to enable Diffie-Helmann Group Exchange KEX algos
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idKEXAlgos" class="col-sm-2 col-form-label">KEX Algos</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control selectpicker" id="idKEXAlgos" name="sftp_kex_algos" multiple>
|
||||
{{range $val := .Configs.SFTPD.GetSupportedKEXAlgos}}
|
||||
<option value="{{$val}}" {{range $algo :=$.Configs.SFTPD.KexAlgorithms }}{{if eq $algo $val}}selected{{end}}{{end}}>{{$val}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idCiphers" class="col-sm-2 col-form-label">Ciphers</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control selectpicker" id="idCiphers" name="sftp_ciphers" multiple>
|
||||
{{range $val := .Configs.SFTPD.GetSupportedCiphers}}
|
||||
<option value="{{$val}}" {{range $algo :=$.Configs.SFTPD.Ciphers }}{{if eq $algo $val}}selected{{end}}{{end}}>{{$val}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idMACAlgos" class="col-sm-2 col-form-label">MAC Algos</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control selectpicker" id="idMACAlgos" name="sftp_macs" multiple>
|
||||
{{range $val := .Configs.SFTPD.GetSupportedMACs}}
|
||||
<option value="{{$val}}" {{range $algo :=$.Configs.SFTPD.MACs }}{{if eq $algo $val}}selected{{end}}{{end}}>{{$val}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 text-right px-0">
|
||||
<button type="submit" class="btn btn-primary mt-3 ml-3 px-5" name="form_action" value="sftp_submit">Submit</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header" id="headingSMTP">
|
||||
<h2 class="mb-0">
|
||||
<button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse"
|
||||
data-target="#collapseSMTP" aria-expanded="true" aria-controls="collapseSMTP">
|
||||
<h6 class="m-0 font-weight-bold text-primary">SMTP</h6>
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div id="collapseSMTP" class="collapse {{if eq .ConfigSection 2}}show{{end}}" aria-labelledby="headingSMTP" data-parent="#accordionConfigs">
|
||||
<div class="card-body">
|
||||
<div id="configs-smtp-info" class="card mb-3 border-left-info">
|
||||
<div class="card-body">Set the SMTP configuration replacing the one defined using env vars or config file if any.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idSMTPHost" class="col-sm-2 col-form-label">Server name</label>
|
||||
<div class="col-sm-5">
|
||||
<input type="text" class="form-control" id="idSMTPHost" name="smtp_host" placeholder=""
|
||||
value="{{.Configs.SMTP.Host}}" maxlength="512" spellcheck="false" aria-describedby="smtpHostHelpBlock">
|
||||
<small id="smtpHostHelpBlock" class="form-text text-muted">
|
||||
If empty the configuration is disabled
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-sm-1"></div>
|
||||
<label for="idSMTPPort" class="col-sm-2 col-form-label">Port</label>
|
||||
<div class="col-sm-2">
|
||||
<input type="number" min="0" max="65535" class="form-control" id="idSMTPPort" name="smtp_port" placeholder=""
|
||||
value="{{.Configs.SMTP.Port}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idSMTPUsername" class="col-sm-2 col-form-label">Username</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idSMTPUsername" name="smtp_username" placeholder=""
|
||||
value="{{.Configs.SMTP.User}}" maxlength="255" spellcheck="false">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idSMTPPassword" class="col-sm-2 col-form-label">Password</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="idSMTPPassword" name="smtp_password" placeholder="" autocomplete="new-password" spellcheck="false"
|
||||
value="{{if .Configs.SMTP.Password.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.Configs.SMTP.Password.GetPayload}}{{end}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idSMTPAuth" class="col-sm-2 col-form-label">Auth</label>
|
||||
<div class="col-sm-3">
|
||||
<select class="form-control selectpicker" id="idSMTPAuth" name="smtp_auth">
|
||||
<option value="0" {{if eq .Configs.SMTP.AuthType 0}}selected{{end}}>Plain</option>
|
||||
<option value="1" {{if eq .Configs.SMTP.AuthType 1}}selected{{end}}>Login</option>
|
||||
<option value="2" {{if eq .Configs.SMTP.AuthType 2}}selected{{end}}>CRAM-MD5</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-2"></div>
|
||||
<label for="idSMTPEncryption" class="col-sm-2 col-form-label">Encryption</label>
|
||||
<div class="col-sm-3">
|
||||
<select class="form-control selectpicker" id="idSMTPEncryption" name="smtp_encryption">
|
||||
<option value="0" {{if eq .Configs.SMTP.Encryption 0}}selected{{end}}>None</option>
|
||||
<option value="1" {{if eq .Configs.SMTP.Encryption 1}}selected{{end}}>TLS</option>
|
||||
<option value="2" {{if eq .Configs.SMTP.Encryption 2}}selected{{end}}>STARTTLS</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idSMTPFrom" class="col-sm-2 col-form-label">From</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idSMTPFrom" name="smtp_from" placeholder=""
|
||||
value="{{.Configs.SMTP.From}}" maxlength="512" spellcheck="false">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idSMTPDomain" class="col-sm-2 col-form-label">Domain</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idSMTPDomain" name="smtp_domain" placeholder=""
|
||||
value="{{.Configs.SMTP.Domain}}" aria-describedby="smtpDomainHelpBlock">
|
||||
<small id="smtpDomainHelpBlock" class="form-text text-muted">
|
||||
HELO domain. Leave blank to use the server hostname
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-12">
|
||||
<div class="input-group">
|
||||
<input type="email" class="form-control float-right" id="idSMTPRecipient" placeholder="Test email recipient" aria-label="Test email recipient">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-secondary px-5" onclick="testSMTP(event);">Test</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 text-right px-0">
|
||||
<button type="submit" class="btn btn-primary mt-3 px-5" name="form_action" value="smtp_submit">Submit</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "dialog"}}
|
||||
<div class="modal fade" id="spinnerModal" tabindex="-1" role="dialog" data-keyboard="false" data-backdrop="static">
|
||||
<div class="modal-dialog modal-dialog-centered justify-content-center" role="document">
|
||||
<span style="color: #333333;" class="fa fa-spinner fa-spin fa-3x"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="smtpTestResultModal" tabindex="-1" role="dialog" aria-labelledby="smtpTestResultModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="smtpTestResultModal">
|
||||
SMTP test result
|
||||
</h5>
|
||||
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="smtpSuccessMsg" class="card mb-4 border-left-success" style="display: none;">
|
||||
<div id="successTxt" class="card-body">No errors were reported while sending the test email. Please check your inbox to make sure.</div>
|
||||
</div>
|
||||
<div id="smtpErrorMsg" class="card mb-4 border-left-warning" style="display: none;">
|
||||
<div id="smtpErrorTxt" class="card-body text-form-error"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" type="button" data-dismiss="modal">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "extra_js"}}
|
||||
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
var spinnerDone = false;
|
||||
|
||||
function testSMTP(event){
|
||||
event.preventDefault();
|
||||
let recipient = $('#idSMTPRecipient').val();
|
||||
if (!recipient){
|
||||
$('#smtpErrorTxt').text('Set a recipient to send the test mail to.');
|
||||
$('#smtpErrorMsg').show();
|
||||
$('#smtpTestResultModal').modal('show');
|
||||
return;
|
||||
}
|
||||
$('#smtpSuccessMsg').hide();
|
||||
$('#smtpErrorMsg').hide();
|
||||
$('#spinnerModal').modal('show');
|
||||
|
||||
$.ajax({
|
||||
url: "{{.ConfigsURL}}/smtp/test",
|
||||
type: 'POST',
|
||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||
data: JSON.stringify({"host": $('#idSMTPHost').val(),"port": parseInt($('#idSMTPPort').val()),"from": $('#idSMTPFrom').val(),"user": $('#idSMTPUsername').val(),"password": $('#idSMTPPassword').val(),"auth_type": parseInt($('#idSMTPAuth').val()),"encryption": parseInt($('#idSMTPEncryption').val()), "domain": $('#idSMTPDomain').val(),"recipient": recipient}),
|
||||
dataType: 'json',
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
timeout: 15000,
|
||||
success: function (result) {
|
||||
$('#spinnerModal').modal('hide');
|
||||
spinnerDone = true;
|
||||
$('#smtpSuccessMsg').show();
|
||||
$('#smtpTestResultModal').modal('show');
|
||||
},
|
||||
error: function ($xhr, textStatus, errorThrown) {
|
||||
$('#spinnerModal').modal('hide');
|
||||
spinnerDone = true;
|
||||
let txt = "SMTP connection failed";
|
||||
if ($xhr) {
|
||||
let json = $xhr.responseJSON;
|
||||
if (json) {
|
||||
if (json.message){
|
||||
txt += ": " + json.message;
|
||||
} else {
|
||||
txt += ": " + json.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
$('#smtpErrorTxt').text(txt);
|
||||
$('#smtpErrorMsg').show();
|
||||
$('#smtpTestResultModal').modal('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$('#spinnerModal').on('shown.bs.modal', function () {
|
||||
if (spinnerDone){
|
||||
$('#spinnerModal').modal('hide');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
|
@ -47,6 +47,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
Fingerprint: "{{.Fingerprint}}"
|
||||
<br>
|
||||
{{end}}
|
||||
<br>
|
||||
Host Key algorithms: "{{.Status.SSH.GetHostKeyAlgosAsString}}"
|
||||
<br><br>
|
||||
MAC algorithms: "{{.Status.SSH.GetMACsAsString}}"
|
||||
<br><br>
|
||||
KEX algorithms: "{{.Status.SSH.GetKEXsAsString}}"
|
||||
<br><br>
|
||||
Ciphers: "{{.Status.SSH.GetCiphersAsString}}"
|
||||
<br>
|
||||
{{end}}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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=
|
||||
|
|
Loading…
Reference in a new issue