mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-22 07:30:25 +00:00
configs: add ACME section
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
fcf9a8c673
commit
8805d85377
23 changed files with 908 additions and 139 deletions
|
@ -179,7 +179,7 @@ The configuration file contains the following sections:
|
||||||
- `hash_support`, integer. Set to `1` to enable FTP commands that allow to calculate the hash value of files. These FTP commands will be enabled: `HASH`, `XCRC`, `MD5/XMD5`, `XSHA/XSHA1`, `XSHA256`, `XSHA512`. Please keep in mind that to calculate the hash we need to read the whole file, for remote backends this means downloading the file, for the encrypted backend this means decrypting the file. Default `0`.
|
- `hash_support`, integer. Set to `1` to enable FTP commands that allow to calculate the hash value of files. These FTP commands will be enabled: `HASH`, `XCRC`, `MD5/XMD5`, `XSHA/XSHA1`, `XSHA256`, `XSHA512`. Please keep in mind that to calculate the hash we need to read the whole file, for remote backends this means downloading the file, for the encrypted backend this means decrypting the file. Default `0`.
|
||||||
- `combine_support`, integer. Set to 1 to enable support for the non standard `COMB` FTP command. Combine is only supported for local filesystem, for cloud backends it has no advantage as it will download the partial files and will upload the combined one. Cloud backends natively support multipart uploads. Default `0`.
|
- `combine_support`, integer. Set to 1 to enable support for the non standard `COMB` FTP command. Combine is only supported for local filesystem, for cloud backends it has no advantage as it will download the partial files and will upload the combined one. Cloud backends natively support multipart uploads. Default `0`.
|
||||||
- `certificate_file`, string. Certificate for FTPS. This can be an absolute path or a path relative to the config dir.
|
- `certificate_file`, string. Certificate for FTPS. This can be an absolute path or a path relative to the config dir.
|
||||||
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. A certificate and the private key are required to enable explicit and implicit TLS. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows. If the integrated ACME protocol is disabled and therefore the certificates are not automatically renewed and reloaded, the certificates are polled for changes every 8 hours.
|
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. A certificate and the private key are required to enable explicit and implicit TLS. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows. The certificates are also polled for changes every 8 hours.
|
||||||
- `ca_certificates`, list of strings. Set of root certificate authorities to be used to verify client certificates.
|
- `ca_certificates`, list of strings. Set of root certificate authorities to be used to verify client certificates.
|
||||||
- `ca_revocation_lists`, list of strings. Set a revocation lists, one for each root CA, to be used to check if a client certificate has been revoked. The revocation lists can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
|
- `ca_revocation_lists`, list of strings. Set a revocation lists, one for each root CA, to be used to check if a client certificate has been revoked. The revocation lists can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
|
||||||
|
|
||||||
|
@ -204,7 +204,7 @@ The configuration file contains the following sections:
|
||||||
- `certificate_file`, string. Certificate for WebDAV over HTTPS. This can be an absolute path or a path relative to the config dir.
|
- `certificate_file`, string. Certificate for WebDAV over HTTPS. This can be an absolute path or a path relative to the config dir.
|
||||||
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. A certificate and a private key are required to enable HTTPS connections. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
|
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. A certificate and a private key are required to enable HTTPS connections. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
|
||||||
- `ca_certificates`, list of strings. Set of root certificate authorities to be used to verify client certificates.
|
- `ca_certificates`, list of strings. Set of root certificate authorities to be used to verify client certificates.
|
||||||
- `ca_revocation_lists`, list of strings. Set a revocation lists, one for each root CA, to be used to check if a client certificate has been revoked. The revocation lists can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows. If the integrated ACME protocol is disabled and therefore the certificates are not automatically renewed and reloaded, the certificates are polled for changes every 8 hours.
|
- `ca_revocation_lists`, list of strings. Set a revocation lists, one for each root CA, to be used to check if a client certificate has been revoked. The revocation lists can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows. The certificates are also polled for changes every 8 hours.
|
||||||
- `cors` struct containing CORS configuration. SFTPGo uses [Go CORS handler](https://github.com/rs/cors), please refer to upstream documentation for fields meaning and their default values.
|
- `cors` struct containing CORS configuration. SFTPGo uses [Go CORS handler](https://github.com/rs/cors), please refer to upstream documentation for fields meaning and their default values.
|
||||||
- `enabled`, boolean, set to true to enable CORS.
|
- `enabled`, boolean, set to true to enable CORS.
|
||||||
- `allowed_origins`, list of strings.
|
- `allowed_origins`, list of strings.
|
||||||
|
@ -356,7 +356,7 @@ The configuration file contains the following sections:
|
||||||
- `openapi_path`, string. Path to the directory that contains the OpenAPI schema and the default renderer. This can be an absolute path or a path relative to the config dir. If empty the OpenAPI schema and the renderer will not be served regardless of the `render_openapi` directive
|
- `openapi_path`, string. Path to the directory that contains the OpenAPI schema and the default renderer. This can be an absolute path or a path relative to the config dir. If empty the OpenAPI schema and the renderer will not be served regardless of the `render_openapi` directive
|
||||||
- `web_root`, string. Defines a base URL for the web admin and client interfaces. If empty web admin and client resources will be available at the root ("/") URI. If defined it must be an absolute URI or it will be ignored
|
- `web_root`, string. Defines a base URL for the web admin and client interfaces. If empty web admin and client resources will be available at the root ("/") URI. If defined it must be an absolute URI or it will be ignored
|
||||||
- `certificate_file`, string. Certificate for HTTPS. This can be an absolute path or a path relative to the config dir.
|
- `certificate_file`, string. Certificate for HTTPS. This can be an absolute path or a path relative to the config dir.
|
||||||
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If both the certificate and the private key are provided, you can enable HTTPS for the configured bindings. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows. If the integrated ACME protocol is disabled and therefore the certificates are not automatically renewed and reloaded, the certificates are polled for changes every 8 hours.
|
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If both the certificate and the private key are provided, you can enable HTTPS for the configured bindings. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows. The certificates are also polled for changes every 8 hours.
|
||||||
- `ca_certificates`, list of strings. Set of root certificate authorities to be used to verify client certificates.
|
- `ca_certificates`, list of strings. Set of root certificate authorities to be used to verify client certificates.
|
||||||
- `ca_revocation_lists`, list of strings. Set a revocation lists, one for each root CA, to be used to check if a client certificate has been revoked. The revocation lists can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
|
- `ca_revocation_lists`, list of strings. Set a revocation lists, one for each root CA, to be used to check if a client certificate has been revoked. The revocation lists can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
|
||||||
- `signing_passphrase`, string. Passphrase to use to derive the signing key for JWT and CSRF tokens. If empty a random signing key will be generated each time SFTPGo starts. If you set a signing passphrase you should consider rotating it periodically for added security.
|
- `signing_passphrase`, string. Passphrase to use to derive the signing key for JWT and CSRF tokens. If empty a random signing key will be generated each time SFTPGo starts. If you set a signing passphrase you should consider rotating it periodically for added security.
|
||||||
|
|
|
@ -8,6 +8,7 @@ The logs can be divided into the following categories:
|
||||||
- `sender` string. This is generally the package name that emits the log
|
- `sender` string. This is generally the package name that emits the log
|
||||||
- `time` string. Date/time with millisecond precision
|
- `time` string. Date/time with millisecond precision
|
||||||
- `level` string
|
- `level` string
|
||||||
|
- `connection_id`, string, optional
|
||||||
- `message` string
|
- `message` string
|
||||||
- **"transfer logs"**, SFTP/SCP transfer logs:
|
- **"transfer logs"**, SFTP/SCP transfer logs:
|
||||||
- `sender` string. `Upload` or `Download`
|
- `sender` string. `Upload` or `Download`
|
||||||
|
@ -20,7 +21,7 @@ The logs can be divided into the following categories:
|
||||||
- `username`, string
|
- `username`, string
|
||||||
- `file_path` string
|
- `file_path` string
|
||||||
- `connection_id` string. Unique connection identifier
|
- `connection_id` string. Unique connection identifier
|
||||||
- `protocol` string. `SFTP`, `SCP`, `SSH`, `FTP`, `HTTP`, `DAV`, `DataRetention`, `EventAction`
|
- `protocol` string. `SFTP`, `SCP`, `SSH`, `FTP`, `HTTP`, `HTTPShare`, `DAV`, `DataRetention`, `EventAction`
|
||||||
- `ftp_mode`, string. `active` or `passive`. Included only for `FTP` protocol
|
- `ftp_mode`, string. `active` or `passive`. Included only for `FTP` protocol
|
||||||
- **"command logs"**, SFTP/SCP command logs:
|
- **"command logs"**, SFTP/SCP command logs:
|
||||||
- `sender` string. `Rename`, `Rmdir`, `Mkdir`, `Symlink`, `Remove`, `Chmod`, `Chown`, `Chtimes`, `Truncate`, `Copy`, `SSHCommand`
|
- `sender` string. `Rename`, `Rmdir`, `Mkdir`, `Symlink`, `Remove`, `Chmod`, `Chown`, `Chtimes`, `Truncate`, `Copy`, `SSHCommand`
|
||||||
|
@ -43,10 +44,12 @@ The logs can be divided into the following categories:
|
||||||
- **"http logs"**, REST API logs:
|
- **"http logs"**, REST API logs:
|
||||||
- `sender` string. `httpd`
|
- `sender` string. `httpd`
|
||||||
- `level` string
|
- `level` string
|
||||||
|
- `time` string. Date/time with millisecond precision
|
||||||
- `local_addr` string. IP/port of the local address the connection arrived on. For example `127.0.0.1:1234`
|
- `local_addr` string. IP/port of the local address the connection arrived on. For example `127.0.0.1:1234`
|
||||||
- `remote_addr` string. IP and, optionally, port of the remote client. For example `127.0.0.1:1234` or `127.0.0.1`
|
- `remote_addr` string. IP and, optionally, port of the remote client. For example `127.0.0.1:1234` or `127.0.0.1`
|
||||||
- `proto` string, for example `HTTP/1.1`
|
- `proto` string, for example `HTTP/1.1`
|
||||||
- `method` string. HTTP method (`GET`, `POST`, `PUT`, `DELETE` etc.)
|
- `method` string. HTTP method (`GET`, `POST`, `PUT`, `DELETE` etc.)
|
||||||
|
- `request_id` string. Omitted in telemetry logs
|
||||||
- `user_agent` string
|
- `user_agent` string
|
||||||
- `uri` string. Full uri
|
- `uri` string. Full uri
|
||||||
- `resp_status` integer. HTTP response status code
|
- `resp_status` integer. HTTP response status code
|
||||||
|
@ -56,6 +59,7 @@ The logs can be divided into the following categories:
|
||||||
- **"connection failed logs"**, logs for failed attempts to initialize a connection. A connection can fail for an authentication error or other errors such as a client abort or a timeout if the login does not happen in two minutes
|
- **"connection failed logs"**, logs for failed attempts to initialize a connection. A connection can fail for an authentication error or other errors such as a client abort or a timeout if the login does not happen in two minutes
|
||||||
- `sender` string. `connection_failed`
|
- `sender` string. `connection_failed`
|
||||||
- `level` string
|
- `level` string
|
||||||
|
- `time` string. Date/time with millisecond precision
|
||||||
- `username`, string. Can be empty if the connection is closed before an authentication attempt
|
- `username`, string. Can be empty if the connection is closed before an authentication attempt
|
||||||
- `client_ip` string.
|
- `client_ip` string.
|
||||||
- `protocol` string. Possible values are `SSH`, `FTP`, `DAV`
|
- `protocol` string. Possible values are `SSH`, `FTP`, `DAV`
|
||||||
|
|
24
go.mod
24
go.mod
|
@ -9,14 +9,14 @@ require (
|
||||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
||||||
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
||||||
github.com/aws/aws-sdk-go-v2 v1.17.5
|
github.com/aws/aws-sdk-go-v2 v1.17.5
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.18.14
|
github.com/aws/aws-sdk-go-v2/config v1.18.15
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.14
|
github.com/aws/aws-sdk-go-v2/credentials v1.13.15
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.54
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.55
|
||||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.14.4
|
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.14.5
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.4
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.5
|
||||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.18.5
|
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.18.6
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.18.4
|
github.com/aws/aws-sdk-go-v2/service/sts v1.18.5
|
||||||
github.com/bmatcuk/doublestar/v4 v4.6.0
|
github.com/bmatcuk/doublestar/v4 v4.6.0
|
||||||
github.com/cockroachdb/cockroach-go/v2 v2.2.20
|
github.com/cockroachdb/cockroach-go/v2 v2.2.20
|
||||||
github.com/coreos/go-oidc/v3 v3.5.0
|
github.com/coreos/go-oidc/v3 v3.5.0
|
||||||
|
@ -54,7 +54,7 @@ require (
|
||||||
github.com/rs/zerolog v1.29.0
|
github.com/rs/zerolog v1.29.0
|
||||||
github.com/sftpgo/sdk v0.1.3-0.20230213182959-2d89540f8810
|
github.com/sftpgo/sdk v0.1.3-0.20230213182959-2d89540f8810
|
||||||
github.com/shirou/gopsutil/v3 v3.23.1
|
github.com/shirou/gopsutil/v3 v3.23.1
|
||||||
github.com/spf13/afero v1.9.3
|
github.com/spf13/afero v1.9.4
|
||||||
github.com/spf13/cobra v1.6.1
|
github.com/spf13/cobra v1.6.1
|
||||||
github.com/spf13/viper v1.15.0
|
github.com/spf13/viper v1.15.0
|
||||||
github.com/stretchr/testify v1.8.1
|
github.com/stretchr/testify v1.8.1
|
||||||
|
@ -93,8 +93,8 @@ require (
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.24 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.24 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.23 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.23 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.3 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sso v1.12.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.3 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4 // indirect
|
||||||
github.com/aws/smithy-go v1.13.5 // indirect
|
github.com/aws/smithy-go v1.13.5 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/boombuler/barcode v1.0.1 // indirect
|
github.com/boombuler/barcode v1.0.1 // indirect
|
||||||
|
@ -157,7 +157,7 @@ require (
|
||||||
golang.org/x/tools v0.6.0 // indirect
|
golang.org/x/tools v0.6.0 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 // indirect
|
google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec // indirect
|
||||||
google.golang.org/grpc v1.53.0 // indirect
|
google.golang.org/grpc v1.53.0 // indirect
|
||||||
google.golang.org/protobuf v1.28.1 // indirect
|
google.golang.org/protobuf v1.28.1 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
@ -166,6 +166,6 @@ require (
|
||||||
|
|
||||||
replace (
|
replace (
|
||||||
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
|
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
|
||||||
github.com/robfig/cron/v3 => github.com/drakkan/cron/v3 v3.0.0-20230221091953-db24a8bf7e83
|
github.com/robfig/cron/v3 => github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0
|
||||||
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20230209112458-e15d12511558
|
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20230209112458-e15d12511558
|
||||||
)
|
)
|
||||||
|
|
48
go.sum
48
go.sum
|
@ -539,17 +539,17 @@ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.9/go.mod h1:vCmV1q1VK
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.18.3/go.mod h1:BYdrbeCse3ZnOD5+2/VE/nATOK8fEUpBtmPMdKSyhMU=
|
github.com/aws/aws-sdk-go-v2/config v1.18.3/go.mod h1:BYdrbeCse3ZnOD5+2/VE/nATOK8fEUpBtmPMdKSyhMU=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.18.14 h1:rI47jCe0EzuJlAO5ptREe3LIBAyP5c7gR3wjyYVjuOM=
|
github.com/aws/aws-sdk-go-v2/config v1.18.15 h1:509yMO0pJUGUugBP2H9FOFyV+7Mz7sRR+snfDN5W4NY=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.18.14/go.mod h1:0pI6JQBHKwd0JnwAZS3VCapLKMO++UL2BOkWwyyzTnA=
|
github.com/aws/aws-sdk-go-v2/config v1.18.15/go.mod h1:vS0tddZqpE8cD9CyW0/kITHF5Bq2QasW9Y1DFHD//O0=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.3/go.mod h1:/rOMmqYBcFfNbRPU0iN9IgGqD5+V2yp3iWNmIlz0wI4=
|
github.com/aws/aws-sdk-go-v2/credentials v1.13.3/go.mod h1:/rOMmqYBcFfNbRPU0iN9IgGqD5+V2yp3iWNmIlz0wI4=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.14 h1:jE34fUepssrhmYpvPpdbd+d39PHpuignDpNPNJguP60=
|
github.com/aws/aws-sdk-go-v2/credentials v1.13.15 h1:0rZQIi6deJFjOEgHI9HI2eZcLPPEGQPictX66oRFLL8=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.14/go.mod h1:85ckagDuzdIOnZRwws1eLKnymJs3ZM1QwVC1XcuNGOY=
|
github.com/aws/aws-sdk-go-v2/credentials v1.13.15/go.mod h1:vRMLMD3/rXU+o6j2MW5YefrGMBmdTvkLLGqFwMLBHQc=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19/go.mod h1:VihW95zQpeKQWVPGkwT+2+WJNQV8UXFfMTWdU6VErL8=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19/go.mod h1:VihW95zQpeKQWVPGkwT+2+WJNQV8UXFfMTWdU6VErL8=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23 h1:Kbiv9PGnQfG/imNI4L/heyUXvzKmcWSBeDvkrQz5pFc=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23 h1:Kbiv9PGnQfG/imNI4L/heyUXvzKmcWSBeDvkrQz5pFc=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23/go.mod h1:mOtmAg65GT1HIL/HT/PynwPbS+UG0BgCZ6vhkPqnxWo=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23/go.mod h1:mOtmAg65GT1HIL/HT/PynwPbS+UG0BgCZ6vhkPqnxWo=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.42/go.mod h1:LHOsygMiW/14CkFxdXxvzKyMh3jbk/QfZVaDtCbLkl8=
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.42/go.mod h1:LHOsygMiW/14CkFxdXxvzKyMh3jbk/QfZVaDtCbLkl8=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.54 h1:u4Cyifho7bnp6NeTCS8zAuxqzycHla4PSJvwXlU8ELI=
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.55 h1:ClZKHmu2QIRQCEQ2Y2upfu4JPO0pG69Ce5eiq3PS2V4=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.54/go.mod h1:a8gjZYNkBoxPMaA4mIoBT1M+4rOAcJUgFeaxVopMS+k=
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.55/go.mod h1:L/h5B6I7reig2QJXCGY0e0NVx4hYCcjETmsfR02hFng=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25/go.mod h1:Zb29PYkf42vVYQY6pvSyJCJcFHlPIiY+YKdPtwnvMkY=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25/go.mod h1:Zb29PYkf42vVYQY6pvSyJCJcFHlPIiY+YKdPtwnvMkY=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29 h1:9/aKwwus0TQxppPXFmf010DFrE+ssSbzroLVYINA+xE=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29 h1:9/aKwwus0TQxppPXFmf010DFrE+ssSbzroLVYINA+xE=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29/go.mod h1:Dip3sIGv485+xerzVv24emnjX5Sg88utCL8fwGmCeWg=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29/go.mod h1:Dip3sIGv485+xerzVv24emnjX5Sg88utCL8fwGmCeWg=
|
||||||
|
@ -575,26 +575,26 @@ 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.23 h1:qc+RW0WWZ2KApMnsu/EVCPqLTyIH55uc7YQq7mq4XqE=
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.23 h1:qc+RW0WWZ2KApMnsu/EVCPqLTyIH55uc7YQq7mq4XqE=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.23/go.mod h1:FJhZWVWBCcgAF8jbep7pxQ1QUsjzTwa9tvEXGw2TDRo=
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.23/go.mod h1:FJhZWVWBCcgAF8jbep7pxQ1QUsjzTwa9tvEXGw2TDRo=
|
||||||
github.com/aws/aws-sdk-go-v2/service/kms v1.19.0/go.mod h1:kZodDPTQjSH/qM6/OvyTfM5mms5JHB/EKYp5dhn/vI4=
|
github.com/aws/aws-sdk-go-v2/service/kms v1.19.0/go.mod h1:kZodDPTQjSH/qM6/OvyTfM5mms5JHB/EKYp5dhn/vI4=
|
||||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.14.4 h1:gYNfHRTtnKPH7yeYqG7SF5hMnWhJy3EAP0QMekYo0K4=
|
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.14.5 h1:L5uD73sZtrTDxn/WTv0LEL00NHCDZmbMUItKHrSdHFs=
|
||||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.14.4/go.mod h1:Nlw/9tgFims+/X+xwFLy/EG6E+NYkZKFXDtLmKJNDA0=
|
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.14.5/go.mod h1:Nlw/9tgFims+/X+xwFLy/EG6E+NYkZKFXDtLmKJNDA0=
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.4/go.mod h1:/NHbqPRiwxSPVOB2Xr+StDEH+GWV/64WwnUjv4KYzV0=
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.4/go.mod h1:/NHbqPRiwxSPVOB2Xr+StDEH+GWV/64WwnUjv4KYzV0=
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.4 h1:0eeEl2lyZkZPhPCt9ggIr3PbCbvae3vfggTkeqJ4O98=
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.5 h1:kFfb+NMap4R7nDvBYyABa/nw7KFMtAfygD1Hyoxh4uE=
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.4/go.mod h1:Dze3kNt4T+Dgb8YCfuIFSBLmE6hadKNxqfdF0Xmqz1I=
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.5/go.mod h1:Dze3kNt4T+Dgb8YCfuIFSBLmE6hadKNxqfdF0Xmqz1I=
|
||||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.8/go.mod h1:k6CPuxyzO247nYEM1baEwHH1kRtosRCvgahAepaaShw=
|
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.8/go.mod h1:k6CPuxyzO247nYEM1baEwHH1kRtosRCvgahAepaaShw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.18.5 h1:8J8gcY1Nepvta2YpZJO7deIOmFAZngHWh8+ULVsfklk=
|
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.18.6 h1:VjvQw/1Qf/rhDSl+NNOeybSpdPRjBfH60//5vzveVsY=
|
||||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.18.5/go.mod h1:CJcdJtrO6ulXfI8l2DotKWmJShhXHCEcd9Wibyx3kC0=
|
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.18.6/go.mod h1:CJcdJtrO6ulXfI8l2DotKWmJShhXHCEcd9Wibyx3kC0=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sns v1.18.6/go.mod h1:2cPUjR63iE9MPMPJtSyzYmsTFCNrN/Xi9j0v9BL5OU0=
|
github.com/aws/aws-sdk-go-v2/service/sns v1.18.6/go.mod h1:2cPUjR63iE9MPMPJtSyzYmsTFCNrN/Xi9j0v9BL5OU0=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.19.15/go.mod h1:DKX/7/ZiAzHO6p6AhArnGdrV4r+d461weby8KeVtvC4=
|
github.com/aws/aws-sdk-go-v2/service/sqs v1.19.15/go.mod h1:DKX/7/ZiAzHO6p6AhArnGdrV4r+d461weby8KeVtvC4=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.33.1/go.mod h1:rEsqsZrOp9YvSGPOrcL3pR9+i/QJaWRkAYbuxMa7yCU=
|
github.com/aws/aws-sdk-go-v2/service/ssm v1.33.1/go.mod h1:rEsqsZrOp9YvSGPOrcL3pR9+i/QJaWRkAYbuxMa7yCU=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.25/go.mod h1:IARHuzTXmj1C0KS35vboR0FeJ89OkEy1M9mWbK2ifCI=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.11.25/go.mod h1:IARHuzTXmj1C0KS35vboR0FeJ89OkEy1M9mWbK2ifCI=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.3 h1:bUeZTWfF1vBdZnoNnnq70rB/CzdZD7NR2Jg2Ax+rvjA=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.12.4 h1:qJdM48OOLl1FBSzI7ZrA1ZfLwOyCYqkXV5lko1hYDBw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.3/go.mod h1:jtLIhd+V+lft6ktxpItycqHqiVXrPIRjWIsFIlzMriw=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.12.4/go.mod h1:jtLIhd+V+lft6ktxpItycqHqiVXrPIRjWIsFIlzMriw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8/go.mod h1:er2JHN+kBY6FcMfcBBKNGCT3CarImmdFzishsqBmSRI=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8/go.mod h1:er2JHN+kBY6FcMfcBBKNGCT3CarImmdFzishsqBmSRI=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.3 h1:G/+7NUi+q+H0LG3v32jfV4OkaQIcpI92g0owbXKk6NY=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4 h1:YRkWXQveFb0tFC0TLktmmhGsOcCgLwvq88MC2al47AA=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.3/go.mod h1:zVwRrfdSmbRZWkUkWjOItY7SOalnFnq/Yg2LVPqDjwc=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4/go.mod h1:zVwRrfdSmbRZWkUkWjOItY7SOalnFnq/Yg2LVPqDjwc=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.17.5/go.mod h1:bXcN3koeVYiJcdDU89n3kCYILob7Y34AeLopUbZgLT4=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.17.5/go.mod h1:bXcN3koeVYiJcdDU89n3kCYILob7Y34AeLopUbZgLT4=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.18.4 h1:j0USUNbl9c/8tBJ8setEbwxc7wva0WyoeAaFRiyTUT8=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.18.5 h1:L1600eLr0YvTT7gNh3Ni24yGI7NSHkq9Gp62vijPRCs=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.18.4/go.mod h1:1mKZHLLpDMHTNSYPJ7qrcnCQdHCWsNQaT0xRvq2u80s=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.18.5/go.mod h1:1mKZHLLpDMHTNSYPJ7qrcnCQdHCWsNQaT0xRvq2u80s=
|
||||||
github.com/aws/smithy-go v1.13.4/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
github.com/aws/smithy-go v1.13.4/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||||
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
|
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
|
||||||
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||||
|
@ -847,8 +847,8 @@ github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
|
||||||
github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
|
github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
|
||||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
||||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||||
github.com/drakkan/cron/v3 v3.0.0-20230221091953-db24a8bf7e83 h1:PqD3xF2E/kKjrJct7giLNLK0EuvEfuVl2GqNnlQ9QLk=
|
github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0 h1:EW9gIJRmt9lzk66Fhh4S8VEtURA6QHZqGeSRE9Nb2/U=
|
||||||
github.com/drakkan/cron/v3 v3.0.0-20230221091953-db24a8bf7e83/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/drakkan/crypto v0.0.0-20230209112458-e15d12511558 h1:M4nv9gf47uCKouIe/FR3RzBPZdx+vquMR1mIRSEa2uw=
|
github.com/drakkan/crypto v0.0.0-20230209112458-e15d12511558 h1:M4nv9gf47uCKouIe/FR3RzBPZdx+vquMR1mIRSEa2uw=
|
||||||
github.com/drakkan/crypto v0.0.0-20230209112458-e15d12511558/go.mod h1:20JIOkADKNe0e6yKflVpVinG/uP19j94rhQlU7Ea/hQ=
|
github.com/drakkan/crypto v0.0.0-20230209112458-e15d12511558/go.mod h1:20JIOkADKNe0e6yKflVpVinG/uP19j94rhQlU7Ea/hQ=
|
||||||
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA=
|
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA=
|
||||||
|
@ -1833,8 +1833,8 @@ github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd
|
||||||
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
|
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
|
||||||
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||||
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
||||||
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
|
github.com/spf13/afero v1.9.4 h1:Sd43wM1IWz/s1aVXdOBkjJvuP8UdyqioeE4AmM0QsBs=
|
||||||
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
github.com/spf13/afero v1.9.4/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
||||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
||||||
|
@ -2715,8 +2715,8 @@ google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZV
|
||||||
google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
|
google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
|
||||||
google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
|
google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
|
||||||
google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
|
google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
|
||||||
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 h1:muK+gVBJBfFb4SejshDBlN2/UgxCCOKH9Y34ljqEGOc=
|
google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec h1:6rwgChOSUfpzJF2/KnLgo+gMaxGpujStSkPWrbhXArU=
|
||||||
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
|
google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
|
||||||
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
|
|
@ -46,6 +46,7 @@ import (
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/v2/internal/common"
|
"github.com/drakkan/sftpgo/v2/internal/common"
|
||||||
|
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||||
"github.com/drakkan/sftpgo/v2/internal/ftpd"
|
"github.com/drakkan/sftpgo/v2/internal/ftpd"
|
||||||
"github.com/drakkan/sftpgo/v2/internal/httpd"
|
"github.com/drakkan/sftpgo/v2/internal/httpd"
|
||||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||||
|
@ -61,11 +62,23 @@ const (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
config *Configuration
|
config *Configuration
|
||||||
|
initialConfig Configuration
|
||||||
scheduler *cron.Cron
|
scheduler *cron.Cron
|
||||||
logMode int
|
logMode int
|
||||||
|
supportedKeyTypes = []string{
|
||||||
|
string(certcrypto.EC256),
|
||||||
|
string(certcrypto.EC384),
|
||||||
|
string(certcrypto.RSA2048),
|
||||||
|
string(certcrypto.RSA4096),
|
||||||
|
string(certcrypto.RSA8192),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetCertificates tries to obtain the certificates for the configured domains
|
func init() {
|
||||||
|
httpd.SetCertificatesGetter(getCertificatesForConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCertificates tries to obtain the certificates using the global configuration
|
||||||
func GetCertificates() error {
|
func GetCertificates() error {
|
||||||
if config == nil {
|
if config == nil {
|
||||||
return errors.New("acme is disabled")
|
return errors.New("acme is disabled")
|
||||||
|
@ -73,6 +86,78 @@ func GetCertificates() error {
|
||||||
return config.getCertificates()
|
return config.getCertificates()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getCertificatesForConfig tries to obtain the certificates using the provided
|
||||||
|
// configuration override. This is a NOOP if we already have certificates
|
||||||
|
func getCertificatesForConfig(c *dataprovider.ACMEConfigs, configDir string) error {
|
||||||
|
if c.Domain == "" {
|
||||||
|
acmeLog(logger.LevelDebug, "no domain configured, nothing to do")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
config := mergeConfig(getConfiguration(), c)
|
||||||
|
if err := config.Initialize(configDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hasCerts, err := config.hasCertificates(c.Domain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to check if we already have certificates for domain %q: %w", c.Domain, err)
|
||||||
|
}
|
||||||
|
if hasCerts {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return config.getCertificates()
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeConfig(config Configuration, c *dataprovider.ACMEConfigs) Configuration {
|
||||||
|
config.Domains = []string{c.Domain}
|
||||||
|
config.Email = c.Email
|
||||||
|
config.HTTP01Challenge.Port = c.HTTP01Challenge.Port
|
||||||
|
config.TLSALPN01Challenge.Port = 0
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
// getConfiguration returns the configuration set using config file and env vars
|
||||||
|
func getConfiguration() Configuration {
|
||||||
|
return initialConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadProviderConf(c Configuration) (Configuration, error) {
|
||||||
|
configs, err := dataprovider.GetConfigs()
|
||||||
|
if err != nil {
|
||||||
|
return c, fmt.Errorf("unable to load config from provider: %w", err)
|
||||||
|
}
|
||||||
|
configs.SetNilsToEmpty()
|
||||||
|
if configs.ACME.Domain == "" {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
return mergeConfig(c, configs.ACME), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize validates and set the configuration
|
||||||
|
func Initialize(c Configuration, configDir string, checkRenew bool) error {
|
||||||
|
config = nil
|
||||||
|
initialConfig = c
|
||||||
|
c, err := loadProviderConf(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
util.CertsBasePath = ""
|
||||||
|
setLogMode(checkRenew)
|
||||||
|
|
||||||
|
if err := c.Initialize(configDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(c.Domains) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
util.CertsBasePath = c.CertsPath
|
||||||
|
acmeLog(logger.LevelInfo, "configured domains: %+v, certs base path %q", c.Domains, c.CertsPath)
|
||||||
|
config = &c
|
||||||
|
if checkRenew {
|
||||||
|
return startScheduler()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// HTTP01Challenge defines the configuration for HTTP-01 challenge type
|
// HTTP01Challenge defines the configuration for HTTP-01 challenge type
|
||||||
type HTTP01Challenge struct {
|
type HTTP01Challenge struct {
|
||||||
Port int `json:"port" mapstructure:"port"`
|
Port int `json:"port" mapstructure:"port"`
|
||||||
|
@ -141,72 +226,52 @@ type Configuration struct {
|
||||||
tempDir string
|
tempDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize validates and set the configuration
|
// Initialize validates and initialize the configuration
|
||||||
func (c *Configuration) Initialize(configDir string, checkRenew bool) error {
|
func (c *Configuration) Initialize(configDir string) error {
|
||||||
common.SetCertAutoReloadMode(true)
|
|
||||||
config = nil
|
|
||||||
setLogMode(checkRenew)
|
|
||||||
c.checkDomains()
|
c.checkDomains()
|
||||||
if len(c.Domains) == 0 {
|
if len(c.Domains) == 0 {
|
||||||
acmeLog(logger.LevelInfo, "no domains configured, acme disabled")
|
acmeLog(logger.LevelInfo, "no domains configured, acme disabled")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if c.Email == "" || !util.IsEmailValid(c.Email) {
|
if c.Email == "" || !util.IsEmailValid(c.Email) {
|
||||||
return fmt.Errorf("invalid email address %#v", c.Email)
|
return fmt.Errorf("invalid email address %q", c.Email)
|
||||||
}
|
}
|
||||||
if c.RenewDays < 1 {
|
if c.RenewDays < 1 {
|
||||||
return fmt.Errorf("invalid number of days remaining before renewal: %d", c.RenewDays)
|
return fmt.Errorf("invalid number of days remaining before renewal: %d", c.RenewDays)
|
||||||
}
|
}
|
||||||
supportedKeyTypes := []string{
|
|
||||||
string(certcrypto.EC256),
|
|
||||||
string(certcrypto.EC384),
|
|
||||||
string(certcrypto.RSA2048),
|
|
||||||
string(certcrypto.RSA4096),
|
|
||||||
string(certcrypto.RSA8192),
|
|
||||||
}
|
|
||||||
if !util.Contains(supportedKeyTypes, c.KeyType) {
|
if !util.Contains(supportedKeyTypes, c.KeyType) {
|
||||||
return fmt.Errorf("invalid key type %#v", c.KeyType)
|
return fmt.Errorf("invalid key type %q", c.KeyType)
|
||||||
}
|
}
|
||||||
caURL, err := url.Parse(c.CAEndpoint)
|
caURL, err := url.Parse(c.CAEndpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid CA endopoint: %w", err)
|
return fmt.Errorf("invalid CA endopoint: %w", err)
|
||||||
}
|
}
|
||||||
if !util.IsFileInputValid(c.CertsPath) {
|
if !util.IsFileInputValid(c.CertsPath) {
|
||||||
return fmt.Errorf("invalid certs path %#v", c.CertsPath)
|
return fmt.Errorf("invalid certs path %q", c.CertsPath)
|
||||||
}
|
}
|
||||||
if !filepath.IsAbs(c.CertsPath) {
|
if !filepath.IsAbs(c.CertsPath) {
|
||||||
c.CertsPath = filepath.Join(configDir, c.CertsPath)
|
c.CertsPath = filepath.Join(configDir, c.CertsPath)
|
||||||
}
|
}
|
||||||
err = os.MkdirAll(c.CertsPath, 0700)
|
err = os.MkdirAll(c.CertsPath, 0700)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create certs path %#v: %w", c.CertsPath, err)
|
return fmt.Errorf("unable to create certs path %q: %w", c.CertsPath, err)
|
||||||
}
|
}
|
||||||
c.tempDir = filepath.Join(c.CertsPath, "temp")
|
c.tempDir = filepath.Join(c.CertsPath, "temp")
|
||||||
err = os.MkdirAll(c.CertsPath, 0700)
|
err = os.MkdirAll(c.CertsPath, 0700)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create certs temp path %#v: %w", c.tempDir, err)
|
return fmt.Errorf("unable to create certs temp path %q: %w", c.tempDir, err)
|
||||||
}
|
}
|
||||||
serverPath := strings.NewReplacer(":", "_", "/", string(os.PathSeparator)).Replace(caURL.Host)
|
serverPath := strings.NewReplacer(":", "_", "/", string(os.PathSeparator)).Replace(caURL.Host)
|
||||||
accountPath := filepath.Join(c.CertsPath, serverPath)
|
accountPath := filepath.Join(c.CertsPath, serverPath)
|
||||||
err = os.MkdirAll(accountPath, 0700)
|
err = os.MkdirAll(accountPath, 0700)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create account path %#v: %w", accountPath, err)
|
return fmt.Errorf("unable to create account path %q: %w", accountPath, err)
|
||||||
}
|
}
|
||||||
c.accountConfigPath = filepath.Join(accountPath, c.Email+".json")
|
c.accountConfigPath = filepath.Join(accountPath, c.Email+".json")
|
||||||
c.accountKeyPath = filepath.Join(accountPath, c.Email+".key")
|
c.accountKeyPath = filepath.Join(accountPath, c.Email+".key")
|
||||||
c.lockPath = filepath.Join(c.CertsPath, "lock")
|
c.lockPath = filepath.Join(c.CertsPath, "lock")
|
||||||
|
|
||||||
if err = c.validateChallenges(); err != nil {
|
return c.validateChallenges()
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
acmeLog(logger.LevelInfo, "configured domains: %+v", c.Domains)
|
|
||||||
common.SetCertAutoReloadMode(false)
|
|
||||||
config = c
|
|
||||||
if checkRenew {
|
|
||||||
return startScheduler()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Configuration) validateChallenges() error {
|
func (c *Configuration) validateChallenges() error {
|
||||||
|
@ -240,10 +305,10 @@ func (c *Configuration) setLockTime() error {
|
||||||
lockTime := fmt.Sprintf("%v", util.GetTimeAsMsSinceEpoch(time.Now()))
|
lockTime := fmt.Sprintf("%v", util.GetTimeAsMsSinceEpoch(time.Now()))
|
||||||
err := os.WriteFile(c.lockPath, []byte(lockTime), 0600)
|
err := os.WriteFile(c.lockPath, []byte(lockTime), 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
acmeLog(logger.LevelError, "unable to save lock time to %#v: %v", c.lockPath, err)
|
acmeLog(logger.LevelError, "unable to save lock time to %q: %v", c.lockPath, err)
|
||||||
return fmt.Errorf("unable to save lock time: %w", err)
|
return fmt.Errorf("unable to save lock time: %w", err)
|
||||||
}
|
}
|
||||||
acmeLog(logger.LevelDebug, "lock time saved: %#v", lockTime)
|
acmeLog(logger.LevelDebug, "lock time saved: %q", lockTime)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,10 +316,10 @@ func (c *Configuration) getLockTime() (time.Time, error) {
|
||||||
content, err := os.ReadFile(c.lockPath)
|
content, err := os.ReadFile(c.lockPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
acmeLog(logger.LevelDebug, "lock file %#v not found", c.lockPath)
|
acmeLog(logger.LevelDebug, "lock file %q not found", c.lockPath)
|
||||||
return time.Time{}, nil
|
return time.Time{}, nil
|
||||||
}
|
}
|
||||||
acmeLog(logger.LevelError, "unable to read lock file %#v: %v", c.lockPath, err)
|
acmeLog(logger.LevelError, "unable to read lock file %q: %v", c.lockPath, err)
|
||||||
return time.Time{}, err
|
return time.Time{}, err
|
||||||
}
|
}
|
||||||
msec, err := strconv.ParseInt(strings.TrimSpace(string(content)), 10, 64)
|
msec, err := strconv.ParseInt(strings.TrimSpace(string(content)), 10, 64)
|
||||||
|
@ -272,7 +337,7 @@ func (c *Configuration) saveAccount(account *account) error {
|
||||||
}
|
}
|
||||||
err = os.WriteFile(c.accountConfigPath, jsonBytes, 0600)
|
err = os.WriteFile(c.accountConfigPath, jsonBytes, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
acmeLog(logger.LevelError, "unable to save account to file %#v: %v", c.accountConfigPath, err)
|
acmeLog(logger.LevelError, "unable to save account to file %q: %v", c.accountConfigPath, err)
|
||||||
return fmt.Errorf("unable to save account: %w", err)
|
return fmt.Errorf("unable to save account: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -287,7 +352,7 @@ func (c *Configuration) getAccount(privateKey crypto.PrivateKey) (account, error
|
||||||
var account account
|
var account account
|
||||||
fileBytes, err := os.ReadFile(c.accountConfigPath)
|
fileBytes, err := os.ReadFile(c.accountConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
acmeLog(logger.LevelError, "unable to read account from file %#v: %v", c.accountConfigPath, err)
|
acmeLog(logger.LevelError, "unable to read account from file %q: %v", c.accountConfigPath, err)
|
||||||
return account, fmt.Errorf("unable to read account from file: %w", err)
|
return account, fmt.Errorf("unable to read account from file: %w", err)
|
||||||
}
|
}
|
||||||
err = json.Unmarshal(fileBytes, &account)
|
err = json.Unmarshal(fileBytes, &account)
|
||||||
|
@ -316,7 +381,7 @@ func (c *Configuration) getAccount(privateKey crypto.PrivateKey) (account, error
|
||||||
func (c *Configuration) loadPrivateKey() (crypto.PrivateKey, error) {
|
func (c *Configuration) loadPrivateKey() (crypto.PrivateKey, error) {
|
||||||
keyBytes, err := os.ReadFile(c.accountKeyPath)
|
keyBytes, err := os.ReadFile(c.accountKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
acmeLog(logger.LevelError, "unable to read account key from file %#v: %v", c.accountKeyPath, err)
|
acmeLog(logger.LevelError, "unable to read account key from file %q: %v", c.accountKeyPath, err)
|
||||||
return nil, fmt.Errorf("unable to read account key: %w", err)
|
return nil, fmt.Errorf("unable to read account key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -329,10 +394,10 @@ func (c *Configuration) loadPrivateKey() (crypto.PrivateKey, error) {
|
||||||
case "EC PRIVATE KEY":
|
case "EC PRIVATE KEY":
|
||||||
privateKey, err = x509.ParseECPrivateKey(keyBlock.Bytes)
|
privateKey, err = x509.ParseECPrivateKey(keyBlock.Bytes)
|
||||||
default:
|
default:
|
||||||
err = fmt.Errorf("unknown private key type %#v", keyBlock.Type)
|
err = fmt.Errorf("unknown private key type %q", keyBlock.Type)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
acmeLog(logger.LevelError, "unable to parse private key from file %#v: %v", c.accountKeyPath, err)
|
acmeLog(logger.LevelError, "unable to parse private key from file %q: %v", c.accountKeyPath, err)
|
||||||
return privateKey, fmt.Errorf("unable to parse private key: %w", err)
|
return privateKey, fmt.Errorf("unable to parse private key: %w", err)
|
||||||
}
|
}
|
||||||
return privateKey, nil
|
return privateKey, nil
|
||||||
|
@ -346,7 +411,7 @@ func (c *Configuration) generatePrivateKey() (crypto.PrivateKey, error) {
|
||||||
}
|
}
|
||||||
certOut, err := os.Create(c.accountKeyPath)
|
certOut, err := os.Create(c.accountKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
acmeLog(logger.LevelError, "unable to save private key to file %#v: %v", c.accountKeyPath, err)
|
acmeLog(logger.LevelError, "unable to save private key to file %q: %v", c.accountKeyPath, err)
|
||||||
return nil, fmt.Errorf("unable to save private key: %w", err)
|
return nil, fmt.Errorf("unable to save private key: %w", err)
|
||||||
}
|
}
|
||||||
defer certOut.Close()
|
defer certOut.Close()
|
||||||
|
@ -365,25 +430,25 @@ func (c *Configuration) generatePrivateKey() (crypto.PrivateKey, error) {
|
||||||
func (c *Configuration) getPrivateKey() (crypto.PrivateKey, error) {
|
func (c *Configuration) getPrivateKey() (crypto.PrivateKey, error) {
|
||||||
_, err := os.Stat(c.accountKeyPath)
|
_, err := os.Stat(c.accountKeyPath)
|
||||||
if err != nil && os.IsNotExist(err) {
|
if err != nil && os.IsNotExist(err) {
|
||||||
acmeLog(logger.LevelDebug, "private key file %#v does not exist, generating new private key", c.accountKeyPath)
|
acmeLog(logger.LevelDebug, "private key file %q does not exist, generating new private key", c.accountKeyPath)
|
||||||
return c.generatePrivateKey()
|
return c.generatePrivateKey()
|
||||||
}
|
}
|
||||||
acmeLog(logger.LevelDebug, "loading private key from file %#v, stat error: %v", c.accountKeyPath, err)
|
acmeLog(logger.LevelDebug, "loading private key from file %q, stat error: %v", c.accountKeyPath, err)
|
||||||
return c.loadPrivateKey()
|
return c.loadPrivateKey()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Configuration) loadCertificatesForDomain(domain string) ([]*x509.Certificate, error) {
|
func (c *Configuration) loadCertificatesForDomain(domain string) ([]*x509.Certificate, error) {
|
||||||
domain = sanitizedDomain(domain)
|
domain = util.SanitizeDomain(domain)
|
||||||
acmeLog(logger.LevelDebug, "loading certificates for domain %#v", domain)
|
acmeLog(logger.LevelDebug, "loading certificates for domain %q", domain)
|
||||||
content, err := os.ReadFile(filepath.Join(c.CertsPath, domain+".crt"))
|
content, err := os.ReadFile(filepath.Join(c.CertsPath, domain+".crt"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
acmeLog(logger.LevelError, "unable to load certificates for domain %#v: %v", domain, err)
|
acmeLog(logger.LevelError, "unable to load certificates for domain %q: %v", domain, err)
|
||||||
return nil, fmt.Errorf("unable to load certificates for domain %#v: %w", domain, err)
|
return nil, fmt.Errorf("unable to load certificates for domain %q: %w", domain, err)
|
||||||
}
|
}
|
||||||
certs, err := certcrypto.ParsePEMBundle(content)
|
certs, err := certcrypto.ParsePEMBundle(content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
acmeLog(logger.LevelError, "unable to parse certificates for domain %#v: %v", domain, err)
|
acmeLog(logger.LevelError, "unable to parse certificates for domain %q: %v", domain, err)
|
||||||
return certs, fmt.Errorf("unable to parse certificates for domain %#v: %w", domain, err)
|
return certs, fmt.Errorf("unable to parse certificates for domain %q: %w", domain, err)
|
||||||
}
|
}
|
||||||
return certs, nil
|
return certs, nil
|
||||||
}
|
}
|
||||||
|
@ -395,7 +460,7 @@ func (c *Configuration) needRenewal(x509Cert *x509.Certificate, domain string) b
|
||||||
}
|
}
|
||||||
notAfter := int(time.Until(x509Cert.NotAfter).Hours() / 24.0)
|
notAfter := int(time.Until(x509Cert.NotAfter).Hours() / 24.0)
|
||||||
if notAfter > c.RenewDays {
|
if notAfter > c.RenewDays {
|
||||||
acmeLog(logger.LevelDebug, "the certificate for domain %#v expires in %d days, no renewal", domain, notAfter)
|
acmeLog(logger.LevelDebug, "the certificate for domain %q expires in %d days, no renewal", domain, notAfter)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
@ -430,10 +495,10 @@ func (c *Configuration) setupChalleges(client *lego.Client) error {
|
||||||
client.Challenge.Remove(challenge.DNS01)
|
client.Challenge.Remove(challenge.DNS01)
|
||||||
if c.HTTP01Challenge.isEnabled() {
|
if c.HTTP01Challenge.isEnabled() {
|
||||||
if c.HTTP01Challenge.WebRoot != "" {
|
if c.HTTP01Challenge.WebRoot != "" {
|
||||||
acmeLog(logger.LevelDebug, "configuring HTTP-01 web root challenge, path %#v", c.HTTP01Challenge.WebRoot)
|
acmeLog(logger.LevelDebug, "configuring HTTP-01 web root challenge, path %q", c.HTTP01Challenge.WebRoot)
|
||||||
providerServer, err := webroot.NewHTTPProvider(c.HTTP01Challenge.WebRoot)
|
providerServer, err := webroot.NewHTTPProvider(c.HTTP01Challenge.WebRoot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
acmeLog(logger.LevelError, "unable to create HTTP-01 web root challenge provider from path %#v: %v",
|
acmeLog(logger.LevelError, "unable to create HTTP-01 web root challenge provider from path %q: %v",
|
||||||
c.HTTP01Challenge.WebRoot, err)
|
c.HTTP01Challenge.WebRoot, err)
|
||||||
return fmt.Errorf("unable to create HTTP-01 web root challenge provider: %w", err)
|
return fmt.Errorf("unable to create HTTP-01 web root challenge provider: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -490,6 +555,18 @@ func (c *Configuration) tryRecoverRegistration(privateKey crypto.PrivateKey) (*r
|
||||||
return client.Registration.ResolveAccountByKey()
|
return client.Registration.ResolveAccountByKey()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Configuration) getCrtPath(domain string) string {
|
||||||
|
return filepath.Join(c.CertsPath, domain+".crt")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Configuration) getKeyPath(domain string) string {
|
||||||
|
return filepath.Join(c.CertsPath, domain+".key")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Configuration) getResourcePath(domain string) string {
|
||||||
|
return filepath.Join(c.CertsPath, domain+".json")
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Configuration) obtainAndSaveCertificate(client *lego.Client, domain string) error {
|
func (c *Configuration) obtainAndSaveCertificate(client *lego.Client, domain string) error {
|
||||||
domains := getDomains(domain)
|
domains := getDomains(domain)
|
||||||
acmeLog(logger.LevelInfo, "requesting certificates for domains %+v", domains)
|
acmeLog(logger.LevelInfo, "requesting certificates for domains %+v", domains)
|
||||||
|
@ -505,15 +582,15 @@ func (c *Configuration) obtainAndSaveCertificate(client *lego.Client, domain str
|
||||||
acmeLog(logger.LevelError, "unable to obtain certificates for domains %+v: %v", domains, err)
|
acmeLog(logger.LevelError, "unable to obtain certificates for domains %+v: %v", domains, err)
|
||||||
return fmt.Errorf("unable to obtain certificates: %w", err)
|
return fmt.Errorf("unable to obtain certificates: %w", err)
|
||||||
}
|
}
|
||||||
domain = sanitizedDomain(domain)
|
domain = util.SanitizeDomain(domain)
|
||||||
err = os.WriteFile(filepath.Join(c.CertsPath, domain+".crt"), cert.Certificate, 0600)
|
err = os.WriteFile(c.getCrtPath(domain), cert.Certificate, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
acmeLog(logger.LevelError, "unable to save certificate for domain %v: %v", domain, err)
|
acmeLog(logger.LevelError, "unable to save certificate for domain %s: %v", domain, err)
|
||||||
return fmt.Errorf("unable to save certificate: %w", err)
|
return fmt.Errorf("unable to save certificate: %w", err)
|
||||||
}
|
}
|
||||||
err = os.WriteFile(filepath.Join(c.CertsPath, domain+".key"), cert.PrivateKey, 0600)
|
err = os.WriteFile(c.getKeyPath(domain), cert.PrivateKey, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
acmeLog(logger.LevelError, "unable to save private key for domain %v: %v", domain, err)
|
acmeLog(logger.LevelError, "unable to save private key for domain %s: %v", domain, err)
|
||||||
return fmt.Errorf("unable to save private key: %w", err)
|
return fmt.Errorf("unable to save private key: %w", err)
|
||||||
}
|
}
|
||||||
jsonBytes, err := json.MarshalIndent(cert, "", "\t")
|
jsonBytes, err := json.MarshalIndent(cert, "", "\t")
|
||||||
|
@ -521,7 +598,7 @@ func (c *Configuration) obtainAndSaveCertificate(client *lego.Client, domain str
|
||||||
acmeLog(logger.LevelError, "unable to marshal certificate resources for domain %v: %v", domain, err)
|
acmeLog(logger.LevelError, "unable to marshal certificate resources for domain %v: %v", domain, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = os.WriteFile(filepath.Join(c.CertsPath, domain+".json"), jsonBytes, 0600)
|
err = os.WriteFile(c.getResourcePath(domain), jsonBytes, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
acmeLog(logger.LevelError, "unable to save certificate resources for domain %v: %v", domain, err)
|
acmeLog(logger.LevelError, "unable to save certificate resources for domain %v: %v", domain, err)
|
||||||
return fmt.Errorf("unable to save certificate resources: %w", err)
|
return fmt.Errorf("unable to save certificate resources: %w", err)
|
||||||
|
@ -531,6 +608,25 @@ func (c *Configuration) obtainAndSaveCertificate(client *lego.Client, domain str
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hasCertificates returns true if certificates for the specified domain has already been issued
|
||||||
|
func (c *Configuration) hasCertificates(domain string) (bool, error) {
|
||||||
|
domain = util.SanitizeDomain(domain)
|
||||||
|
if _, err := os.Stat(c.getCrtPath(domain)); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(c.getKeyPath(domain)); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCertificates tries to obtain the certificates for the configured domains
|
||||||
func (c *Configuration) getCertificates() error {
|
func (c *Configuration) getCertificates() error {
|
||||||
account, client, err := c.setup()
|
account, client, err := c.setup()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -664,11 +760,7 @@ func getDomains(domain string) []string {
|
||||||
domains = append(domains, d)
|
domains = append(domains, d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return domains
|
return util.RemoveDuplicates(domains, false)
|
||||||
}
|
|
||||||
|
|
||||||
func sanitizedDomain(domain string) string {
|
|
||||||
return strings.NewReplacer(":", "_", "*", "_", ",", "_", " ", "_").Replace(domain)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopScheduler() {
|
func stopScheduler() {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/v2/internal/acme"
|
"github.com/drakkan/sftpgo/v2/internal/acme"
|
||||||
"github.com/drakkan/sftpgo/v2/internal/config"
|
"github.com/drakkan/sftpgo/v2/internal/config"
|
||||||
|
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||||
)
|
)
|
||||||
|
@ -49,10 +50,29 @@ renewed by the SFTPGo service
|
||||||
logger.ErrorToConsole("Unable to initialize ACME, config load error: %v", err)
|
logger.ErrorToConsole("Unable to initialize ACME, config load error: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
kmsConfig := config.GetKMSConfig()
|
||||||
|
err = kmsConfig.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorToConsole("unable to initialize KMS: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
mfaConfig := config.GetMFAConfig()
|
||||||
|
err = mfaConfig.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorToConsole("Unable to initialize MFA: %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)
|
||||||
|
}
|
||||||
acmeConfig := config.GetACMEConfig()
|
acmeConfig := config.GetACMEConfig()
|
||||||
err = acmeConfig.Initialize(configDir, false)
|
err = acme.Initialize(acmeConfig, configDir, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorToConsole("Unable to initialize ACME configuration: %v", err)
|
logger.ErrorToConsole("Unable to initialize ACME configuration: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if err = acme.GetCertificates(); err != nil {
|
if err = acme.GetCertificates(); err != nil {
|
||||||
logger.ErrorToConsole("Cannot get certificates: %v", err)
|
logger.ErrorToConsole("Cannot get certificates: %v", err)
|
||||||
|
|
|
@ -120,7 +120,6 @@ func TestMain(m *testing.M) {
|
||||||
logger.WarnToConsole("error initializing common: %v", err)
|
logger.WarnToConsole("error initializing common: %v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
common.SetCertAutoReloadMode(true)
|
|
||||||
|
|
||||||
httpConfig := config.GetHTTPConfig()
|
httpConfig := config.GetHTTPConfig()
|
||||||
httpConfig.Timeout = 5
|
httpConfig.Timeout = 5
|
||||||
|
|
|
@ -38,17 +38,9 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
certAutoReload bool
|
|
||||||
pemCRLPrefix = []byte("-----BEGIN X509 CRL")
|
pemCRLPrefix = []byte("-----BEGIN X509 CRL")
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetCertAutoReloadMode sets if the certificate must be monitored for changes and
|
|
||||||
// automatically reloaded
|
|
||||||
func SetCertAutoReloadMode(val bool) {
|
|
||||||
certAutoReload = val
|
|
||||||
logger.Debug(logSender, "", "is certificate monitoring enabled? %t", certAutoReload)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TLSKeyPair defines the paths and the unique identifier for a TLS key pair
|
// TLSKeyPair defines the paths and the unique identifier for a TLS key pair
|
||||||
type TLSKeyPair struct {
|
type TLSKeyPair struct {
|
||||||
Cert string
|
Cert string
|
||||||
|
@ -302,11 +294,11 @@ func NewCertManager(keyPairs []TLSKeyPair, configDir, logSender string) (*CertMa
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if certAutoReload {
|
|
||||||
randSecs := rand.Intn(59)
|
randSecs := rand.Intn(59)
|
||||||
manager.monitor()
|
manager.monitor()
|
||||||
_, err := eventScheduler.AddFunc(fmt.Sprintf("@every 8h0m%ds", randSecs), manager.monitor)
|
if eventScheduler != nil {
|
||||||
util.PanicOnError(err)
|
logger.Debug(manager.logSender, "", "starting certificates monitoring tasks")
|
||||||
|
_, err = eventScheduler.AddFunc(fmt.Sprintf("@every 8h0m%ds", randSecs), manager.monitor)
|
||||||
}
|
}
|
||||||
return manager, nil
|
return manager, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -218,11 +218,72 @@ func (c *SMTPConfigs) getACopy() *SMTPConfigs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ACMEHTTP01Challenge defines the configuration for HTTP-01 challenge type
|
||||||
|
type ACMEHTTP01Challenge struct {
|
||||||
|
Port int `json:"port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ACMEConfigs defines ACME related configuration
|
||||||
|
type ACMEConfigs struct {
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
HTTP01Challenge ACMEHTTP01Challenge `json:"http01_challenge"`
|
||||||
|
// apply the certificate for the specified protocols:
|
||||||
|
//
|
||||||
|
// 1 means HTTP
|
||||||
|
// 2 means FTP
|
||||||
|
// 4 means WebDAV
|
||||||
|
//
|
||||||
|
// Protocols can be combined
|
||||||
|
Protocols int `json:"protocols"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ACMEConfigs) isEmpty() bool {
|
||||||
|
return c.Domain == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ACMEConfigs) validate() error {
|
||||||
|
if c.Domain == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if c.Email == "" && !util.IsEmailValid(c.Email) {
|
||||||
|
return util.NewValidationError(fmt.Sprintf("acme: invalid email %q", c.Email))
|
||||||
|
}
|
||||||
|
if c.HTTP01Challenge.Port <= 0 || c.HTTP01Challenge.Port > 65535 {
|
||||||
|
return util.NewValidationError(fmt.Sprintf("acme: invalid HTTP-01 challenge port %d", c.HTTP01Challenge.Port))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasProtocol returns true if the ACME certificate must be used for the specified protocol
|
||||||
|
func (c *ACMEConfigs) HasProtocol(protocol string) bool {
|
||||||
|
switch protocol {
|
||||||
|
case protocolHTTP:
|
||||||
|
return c.Protocols&1 != 0
|
||||||
|
case protocolFTP:
|
||||||
|
return c.Protocols&2 != 0
|
||||||
|
case protocolWebDAV:
|
||||||
|
return c.Protocols&4 != 0
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ACMEConfigs) getACopy() *ACMEConfigs {
|
||||||
|
return &ACMEConfigs{
|
||||||
|
Email: c.Email,
|
||||||
|
Domain: c.Domain,
|
||||||
|
HTTP01Challenge: ACMEHTTP01Challenge{Port: c.HTTP01Challenge.Port},
|
||||||
|
Protocols: c.Protocols,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Configs allows to set configuration keys disabled by default without
|
// Configs allows to set configuration keys disabled by default without
|
||||||
// modifying the config file or setting env vars
|
// modifying the config file or setting env vars
|
||||||
type Configs struct {
|
type Configs struct {
|
||||||
SFTPD *SFTPDConfigs `json:"sftpd,omitempty"`
|
SFTPD *SFTPDConfigs `json:"sftpd,omitempty"`
|
||||||
SMTP *SMTPConfigs `json:"smtp,omitempty"`
|
SMTP *SMTPConfigs `json:"smtp,omitempty"`
|
||||||
|
ACME *ACMEConfigs `json:"acme,omitempty"`
|
||||||
UpdatedAt int64 `json:"updated_at,omitempty"`
|
UpdatedAt int64 `json:"updated_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,6 +298,11 @@ func (c *Configs) validate() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if c.ACME != nil {
|
||||||
|
if err := c.ACME.validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -250,6 +316,9 @@ func (c *Configs) PrepareForRendering() {
|
||||||
if c.SMTP != nil && c.SMTP.isEmpty() {
|
if c.SMTP != nil && c.SMTP.isEmpty() {
|
||||||
c.SMTP = nil
|
c.SMTP = nil
|
||||||
}
|
}
|
||||||
|
if c.ACME != nil && c.ACME.isEmpty() {
|
||||||
|
c.ACME = nil
|
||||||
|
}
|
||||||
if c.SMTP != nil && c.SMTP.Password != nil {
|
if c.SMTP != nil && c.SMTP.Password != nil {
|
||||||
c.SMTP.Password.Hide()
|
c.SMTP.Password.Hide()
|
||||||
if c.SMTP.Password.IsEmpty() {
|
if c.SMTP.Password.IsEmpty() {
|
||||||
|
@ -269,6 +338,9 @@ func (c *Configs) SetNilsToEmpty() {
|
||||||
if c.SMTP.Password == nil {
|
if c.SMTP.Password == nil {
|
||||||
c.SMTP.Password = kms.NewEmptySecret()
|
c.SMTP.Password = kms.NewEmptySecret()
|
||||||
}
|
}
|
||||||
|
if c.ACME == nil {
|
||||||
|
c.ACME = &ACMEConfigs{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderAsJSON implements the renderer interface used within plugins
|
// RenderAsJSON implements the renderer interface used within plugins
|
||||||
|
@ -294,6 +366,9 @@ func (c *Configs) getACopy() Configs {
|
||||||
if c.SMTP != nil {
|
if c.SMTP != nil {
|
||||||
result.SMTP = c.SMTP.getACopy()
|
result.SMTP = c.SMTP.getACopy()
|
||||||
}
|
}
|
||||||
|
if c.ACME != nil {
|
||||||
|
result.ACME = c.ACME.getACopy()
|
||||||
|
}
|
||||||
result.UpdatedAt = c.UpdatedAt
|
result.UpdatedAt = c.UpdatedAt
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,12 +19,14 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
ftpserver "github.com/fclairamb/ftpserverlib"
|
ftpserver "github.com/fclairamb/ftpserverlib"
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/v2/internal/common"
|
"github.com/drakkan/sftpgo/v2/internal/common"
|
||||||
|
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||||
)
|
)
|
||||||
|
@ -267,6 +269,7 @@ type Configuration struct {
|
||||||
CombineSupport int `json:"combine_support" mapstructure:"combine_support"`
|
CombineSupport int `json:"combine_support" mapstructure:"combine_support"`
|
||||||
// Port Range for data connections. Random if not specified
|
// Port Range for data connections. Random if not specified
|
||||||
PassivePortRange PortRange `json:"passive_port_range" mapstructure:"passive_port_range"`
|
PassivePortRange PortRange `json:"passive_port_range" mapstructure:"passive_port_range"`
|
||||||
|
acmeDomain string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldBind returns true if there is at least a valid binding
|
// ShouldBind returns true if there is at least a valid binding
|
||||||
|
@ -294,8 +297,13 @@ func (c *Configuration) getKeyPairs(configDir string) []common.TLSKeyPair {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
certificateFile := getConfigPath(c.CertificateFile, configDir)
|
var certificateFile, certificateKeyFile string
|
||||||
certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)
|
if c.acmeDomain != "" {
|
||||||
|
certificateFile, certificateKeyFile = util.GetACMECertificateKeyPair(c.acmeDomain)
|
||||||
|
} else {
|
||||||
|
certificateFile = getConfigPath(c.CertificateFile, configDir)
|
||||||
|
certificateKeyFile = getConfigPath(c.CertificateKeyFile, configDir)
|
||||||
|
}
|
||||||
if certificateFile != "" && certificateKeyFile != "" {
|
if certificateFile != "" && certificateKeyFile != "" {
|
||||||
keyPairs = append(keyPairs, common.TLSKeyPair{
|
keyPairs = append(keyPairs, common.TLSKeyPair{
|
||||||
Cert: certificateFile,
|
Cert: certificateFile,
|
||||||
|
@ -306,8 +314,37 @@ func (c *Configuration) getKeyPairs(configDir string) []common.TLSKeyPair {
|
||||||
return keyPairs
|
return keyPairs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 configs.ACME.Domain == "" || !configs.ACME.HasProtocol(common.ProtocolFTP) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
crt, key := util.GetACMECertificateKeyPair(configs.ACME.Domain)
|
||||||
|
if crt != "" && key != "" {
|
||||||
|
if _, err := os.Stat(crt); err != nil {
|
||||||
|
logger.Error(logSender, "", "unable to load acme cert file %q: %v", crt, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(key); err != nil {
|
||||||
|
logger.Error(logSender, "", "unable to load acme key file %q: %v", key, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c.acmeDomain = configs.ACME.Domain
|
||||||
|
logger.Info(logSender, "", "acme domain set to %q", c.acmeDomain)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize configures and starts the FTP server
|
// Initialize configures and starts the FTP server
|
||||||
func (c *Configuration) Initialize(configDir string) error {
|
func (c *Configuration) Initialize(configDir string) error {
|
||||||
|
if err := c.loadFromProvider(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
logger.Info(logSender, "", "initializing FTP server with config %+v", *c)
|
logger.Info(logSender, "", "initializing FTP server with config %+v", *c)
|
||||||
if !c.ShouldBind() {
|
if !c.ShouldBind() {
|
||||||
return common.ErrNoBinding
|
return common.ErrNoBinding
|
||||||
|
|
|
@ -503,6 +503,18 @@ func TestInitializationFailure(t *testing.T) {
|
||||||
ftpdConf.Bindings[1].ForcePassiveIP = ""
|
ftpdConf.Bindings[1].ForcePassiveIP = ""
|
||||||
err = ftpdConf.Initialize(configDir)
|
err = ftpdConf.Initialize(configDir)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
|
err = dataprovider.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = ftpdConf.Initialize(configDir)
|
||||||
|
if assert.Error(t, err) {
|
||||||
|
assert.Contains(t, err.Error(), "unable to load config from provider")
|
||||||
|
}
|
||||||
|
err = config.LoadConfig(configDir, "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
providerConf := config.GetProviderConf()
|
||||||
|
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBasicFTPHandling(t *testing.T) {
|
func TestBasicFTPHandling(t *testing.T) {
|
||||||
|
|
|
@ -36,6 +36,7 @@ import (
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/v2/internal/common"
|
"github.com/drakkan/sftpgo/v2/internal/common"
|
||||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||||
|
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||||
"github.com/drakkan/sftpgo/v2/internal/vfs"
|
"github.com/drakkan/sftpgo/v2/internal/vfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1059,3 +1060,70 @@ func TestRelativePath(t *testing.T) {
|
||||||
rel = getPathRelativeTo("/dir3", "dir3")
|
rel = getPathRelativeTo("/dir3", "dir3")
|
||||||
assert.Equal(t, "dir3", rel)
|
assert.Equal(t, "dir3", rel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigsFromProvider(t *testing.T) {
|
||||||
|
err := dataprovider.UpdateConfigs(nil, "", "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
c := Configuration{}
|
||||||
|
err = c.loadFromProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, c.acmeDomain)
|
||||||
|
configs := dataprovider.Configs{
|
||||||
|
ACME: &dataprovider.ACMEConfigs{
|
||||||
|
Domain: "domain.com",
|
||||||
|
Email: "info@domain.com",
|
||||||
|
HTTP01Challenge: dataprovider.ACMEHTTP01Challenge{Port: 80},
|
||||||
|
Protocols: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = dataprovider.UpdateConfigs(&configs, "", "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
util.CertsBasePath = ""
|
||||||
|
// crt and key empty
|
||||||
|
err = c.loadFromProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, c.acmeDomain)
|
||||||
|
util.CertsBasePath = filepath.Clean(os.TempDir())
|
||||||
|
// crt not found
|
||||||
|
err = c.loadFromProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, c.acmeDomain)
|
||||||
|
keyPairs := c.getKeyPairs(configDir)
|
||||||
|
assert.Len(t, keyPairs, 0)
|
||||||
|
crtPath := filepath.Join(util.CertsBasePath, util.SanitizeDomain(configs.ACME.Domain)+".crt")
|
||||||
|
err = os.WriteFile(crtPath, nil, 0666)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// key not found
|
||||||
|
err = c.loadFromProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, c.acmeDomain)
|
||||||
|
keyPairs = c.getKeyPairs(configDir)
|
||||||
|
assert.Len(t, keyPairs, 0)
|
||||||
|
keyPath := filepath.Join(util.CertsBasePath, util.SanitizeDomain(configs.ACME.Domain)+".key")
|
||||||
|
err = os.WriteFile(keyPath, nil, 0666)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// acme cert used
|
||||||
|
err = c.loadFromProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, configs.ACME.Domain, c.acmeDomain)
|
||||||
|
keyPairs = c.getKeyPairs(configDir)
|
||||||
|
assert.Len(t, keyPairs, 1)
|
||||||
|
// protocols does not match
|
||||||
|
configs.ACME.Protocols = 5
|
||||||
|
err = dataprovider.UpdateConfigs(&configs, "", "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
c.acmeDomain = ""
|
||||||
|
err = c.loadFromProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, c.acmeDomain)
|
||||||
|
keyPairs = c.getKeyPairs(configDir)
|
||||||
|
assert.Len(t, keyPairs, 0)
|
||||||
|
|
||||||
|
err = os.Remove(crtPath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.Remove(keyPath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
util.CertsBasePath = ""
|
||||||
|
err = dataprovider.UpdateConfigs(nil, "", "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
@ -275,6 +276,8 @@ var (
|
||||||
installationCode string
|
installationCode string
|
||||||
installationCodeHint string
|
installationCodeHint string
|
||||||
fnInstallationCodeResolver FnInstallationCodeResolver
|
fnInstallationCodeResolver FnInstallationCodeResolver
|
||||||
|
configurationDir string
|
||||||
|
fnGetCetificates FnGetCertificates
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -286,6 +289,9 @@ func init() {
|
||||||
// If the installation code cannot be resolved the provided default must be returned
|
// If the installation code cannot be resolved the provided default must be returned
|
||||||
type FnInstallationCodeResolver func(defaultInstallationCode string) string
|
type FnInstallationCodeResolver func(defaultInstallationCode string) string
|
||||||
|
|
||||||
|
// FnGetCertificates defines the method to call to get TLS certificates
|
||||||
|
type FnGetCertificates func(*dataprovider.ACMEConfigs, string) error
|
||||||
|
|
||||||
// HTTPSProxyHeader defines an HTTPS proxy header as key/value.
|
// HTTPSProxyHeader defines an HTTPS proxy header as key/value.
|
||||||
// For example Key could be "X-Forwarded-Proto" and Value "https"
|
// For example Key could be "X-Forwarded-Proto" and Value "https"
|
||||||
type HTTPSProxyHeader struct {
|
type HTTPSProxyHeader struct {
|
||||||
|
@ -740,6 +746,7 @@ type Conf struct {
|
||||||
Setup SetupConfig `json:"setup" mapstructure:"setup"`
|
Setup SetupConfig `json:"setup" mapstructure:"setup"`
|
||||||
// If enabled, the link to the sponsors section will not appear on the setup screen page
|
// If enabled, the link to the sponsors section will not appear on the setup screen page
|
||||||
HideSupportLink bool `json:"hide_support_link" mapstructure:"hide_support_link"`
|
HideSupportLink bool `json:"hide_support_link" mapstructure:"hide_support_link"`
|
||||||
|
acmeDomain string
|
||||||
}
|
}
|
||||||
|
|
||||||
type apiResponse struct {
|
type apiResponse struct {
|
||||||
|
@ -820,8 +827,13 @@ func (c *Conf) getKeyPairs(configDir string) []common.TLSKeyPair {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
certificateFile := getConfigPath(c.CertificateFile, configDir)
|
var certificateFile, certificateKeyFile string
|
||||||
certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)
|
if c.acmeDomain != "" {
|
||||||
|
certificateFile, certificateKeyFile = util.GetACMECertificateKeyPair(c.acmeDomain)
|
||||||
|
} else {
|
||||||
|
certificateFile = getConfigPath(c.CertificateFile, configDir)
|
||||||
|
certificateKeyFile = getConfigPath(c.CertificateKeyFile, configDir)
|
||||||
|
}
|
||||||
if certificateFile != "" && certificateKeyFile != "" {
|
if certificateFile != "" && certificateKeyFile != "" {
|
||||||
keyPairs = append(keyPairs, common.TLSKeyPair{
|
keyPairs = append(keyPairs, common.TLSKeyPair{
|
||||||
Cert: certificateFile,
|
Cert: certificateFile,
|
||||||
|
@ -840,9 +852,42 @@ func (c *Conf) setTokenValidationMode() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Conf) loadFromProvider() error {
|
||||||
|
configs, err := dataprovider.GetConfigs()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to load config from provider: %w", err)
|
||||||
|
}
|
||||||
|
configs.SetNilsToEmpty()
|
||||||
|
if configs.ACME.Domain == "" || !configs.ACME.HasProtocol(common.ProtocolHTTP) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
crt, key := util.GetACMECertificateKeyPair(configs.ACME.Domain)
|
||||||
|
if crt != "" && key != "" {
|
||||||
|
if _, err := os.Stat(crt); err != nil {
|
||||||
|
logger.Error(logSender, "", "unable to load acme cert file %q: %v", crt, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(key); err != nil {
|
||||||
|
logger.Error(logSender, "", "unable to load acme key file %q: %v", key, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for idx := range c.Bindings {
|
||||||
|
c.Bindings[idx].EnableHTTPS = true
|
||||||
|
}
|
||||||
|
c.acmeDomain = configs.ACME.Domain
|
||||||
|
logger.Info(logSender, "", "acme domain set to %q", c.acmeDomain)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize configures and starts the HTTP server
|
// Initialize configures and starts the HTTP server
|
||||||
func (c *Conf) Initialize(configDir string, isShared int) error {
|
func (c *Conf) Initialize(configDir string, isShared int) error {
|
||||||
|
if err := c.loadFromProvider(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
logger.Info(logSender, "", "initializing HTTP server with config %+v", c.getRedacted())
|
logger.Info(logSender, "", "initializing HTTP server with config %+v", c.getRedacted())
|
||||||
|
configurationDir = configDir
|
||||||
resetCodesMgr = newResetCodeManager(isShared)
|
resetCodesMgr = newResetCodeManager(isShared)
|
||||||
oidcMgr = newOIDCManager(isShared)
|
oidcMgr = newOIDCManager(isShared)
|
||||||
staticFilesPath := util.FindSharedDataPath(c.StaticFilesPath, configDir)
|
staticFilesPath := util.FindSharedDataPath(c.StaticFilesPath, configDir)
|
||||||
|
@ -1144,3 +1189,15 @@ func resolveInstallationCode() string {
|
||||||
}
|
}
|
||||||
return installationCode
|
return installationCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetCertificatesGetter sets the function to call to get TLS certificates
|
||||||
|
func SetCertificatesGetter(fn FnGetCertificates) {
|
||||||
|
fnGetCetificates = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTLSCertificates(c *dataprovider.ACMEConfigs) error {
|
||||||
|
if fnGetCetificates == nil {
|
||||||
|
return errors.New("unable to get TLS certificates, callback not defined")
|
||||||
|
}
|
||||||
|
return fnGetCetificates(c, configurationDir)
|
||||||
|
}
|
||||||
|
|
|
@ -568,6 +568,17 @@ func TestInitialization(t *testing.T) {
|
||||||
if assert.Error(t, err) {
|
if assert.Error(t, err) {
|
||||||
assert.Contains(t, err.Error(), "no login method available for WebClient UI")
|
assert.Contains(t, err.Error(), "no login method available for WebClient UI")
|
||||||
}
|
}
|
||||||
|
err = dataprovider.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = httpdConf.Initialize(configDir, isShared)
|
||||||
|
if assert.Error(t, err) {
|
||||||
|
assert.Contains(t, err.Error(), "unable to load config from provider")
|
||||||
|
}
|
||||||
|
err = config.LoadConfig(configDir, "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
providerConf := config.GetProviderConf()
|
||||||
|
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBasicUserHandling(t *testing.T) {
|
func TestBasicUserHandling(t *testing.T) {
|
||||||
|
@ -12322,15 +12333,16 @@ func TestWebConfigsMock(t *testing.T) {
|
||||||
form.Set("smtp_username", defaultUsername)
|
form.Set("smtp_username", defaultUsername)
|
||||||
form.Set("smtp_password", defaultPassword)
|
form.Set("smtp_password", defaultPassword)
|
||||||
form.Set("smtp_domain", "localdomain")
|
form.Set("smtp_domain", "localdomain")
|
||||||
|
form.Set("smtp_auth", "100")
|
||||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
setJWTCookieForReq(req, webToken)
|
setJWTCookieForReq(req, webToken)
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
rr = executeRequest(req)
|
rr = executeRequest(req)
|
||||||
checkResponseCode(t, http.StatusOK, rr)
|
checkResponseCode(t, http.StatusOK, rr)
|
||||||
assert.Contains(t, rr.Body.String(), "Validation error") // port is not passed so 0
|
assert.Contains(t, rr.Body.String(), "Validation error") // invalid smtp_auth
|
||||||
// set valid parameters
|
// set valid parameters
|
||||||
form.Set("smtp_port", "465")
|
form.Set("smtp_port", "a") // converted to 587
|
||||||
form.Set("smtp_auth", "1")
|
form.Set("smtp_auth", "1")
|
||||||
form.Set("smtp_encryption", "2")
|
form.Set("smtp_encryption", "2")
|
||||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||||
|
@ -12348,7 +12360,7 @@ func TestWebConfigsMock(t *testing.T) {
|
||||||
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.CertAlgoRSAv01)
|
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.CertAlgoRSAv01)
|
||||||
assert.Len(t, configs.SFTPD.Moduli, 2)
|
assert.Len(t, configs.SFTPD.Moduli, 2)
|
||||||
assert.Equal(t, "mail.example.net", configs.SMTP.Host)
|
assert.Equal(t, "mail.example.net", configs.SMTP.Host)
|
||||||
assert.Equal(t, 465, configs.SMTP.Port)
|
assert.Equal(t, 587, configs.SMTP.Port)
|
||||||
assert.Equal(t, "Example <info@example.net>", configs.SMTP.From)
|
assert.Equal(t, "Example <info@example.net>", configs.SMTP.From)
|
||||||
assert.Equal(t, defaultUsername, configs.SMTP.User)
|
assert.Equal(t, defaultUsername, configs.SMTP.User)
|
||||||
err = configs.SMTP.Password.Decrypt()
|
err = configs.SMTP.Password.Decrypt()
|
||||||
|
@ -12359,6 +12371,8 @@ func TestWebConfigsMock(t *testing.T) {
|
||||||
assert.Equal(t, "localdomain", configs.SMTP.Domain)
|
assert.Equal(t, "localdomain", configs.SMTP.Domain)
|
||||||
// set a redacted password, the current password must be preserved
|
// set a redacted password, the current password must be preserved
|
||||||
form.Set("smtp_password", redactedSecret)
|
form.Set("smtp_password", redactedSecret)
|
||||||
|
form.Set("smtp_auth", "")
|
||||||
|
configs.SMTP.AuthType = 0 // empty will be converted to 0
|
||||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
setJWTCookieForReq(req, webToken)
|
setJWTCookieForReq(req, webToken)
|
||||||
|
@ -12385,6 +12399,76 @@ func TestWebConfigsMock(t *testing.T) {
|
||||||
rr = executeRequest(req)
|
rr = executeRequest(req)
|
||||||
checkResponseCode(t, http.StatusOK, rr)
|
checkResponseCode(t, http.StatusOK, rr)
|
||||||
assert.Contains(t, rr.Body.String(), "Configurations updated")
|
assert.Contains(t, rr.Body.String(), "Configurations updated")
|
||||||
|
// test ACME configs, set a fake callback to avoid Let's encrypt calls
|
||||||
|
httpd.SetCertificatesGetter(func(a *dataprovider.ACMEConfigs, s string) error { return nil })
|
||||||
|
form.Set("form_action", "acme_submit")
|
||||||
|
form.Set("acme_port", "") // on error will be set to 80
|
||||||
|
form.Set("acme_protocols", "1")
|
||||||
|
form.Add("acme_protocols", "2")
|
||||||
|
form.Add("acme_protocols", "3")
|
||||||
|
form.Set("acme_domain", "example.com")
|
||||||
|
// no email set, validation will fail
|
||||||
|
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")
|
||||||
|
form.Set("acme_domain", "")
|
||||||
|
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, 80, configs.ACME.HTTP01Challenge.Port)
|
||||||
|
assert.Equal(t, 7, configs.ACME.Protocols)
|
||||||
|
assert.Empty(t, configs.ACME.Domain)
|
||||||
|
assert.Empty(t, configs.ACME.Email)
|
||||||
|
assert.True(t, configs.ACME.HasProtocol(common.ProtocolFTP))
|
||||||
|
assert.True(t, configs.ACME.HasProtocol(common.ProtocolWebDAV))
|
||||||
|
assert.True(t, configs.ACME.HasProtocol(common.ProtocolHTTP))
|
||||||
|
|
||||||
|
form.Set("acme_port", "402")
|
||||||
|
form.Set("acme_protocols", "1")
|
||||||
|
form.Add("acme_protocols", "1000")
|
||||||
|
form.Set("acme_domain", "acme.example.com")
|
||||||
|
form.Set("acme_email", "email@example.com")
|
||||||
|
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")
|
||||||
|
configs, err = dataprovider.GetConfigs()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, configs.SFTPD.HostKeyAlgos, 2)
|
||||||
|
assert.Equal(t, 402, configs.ACME.HTTP01Challenge.Port)
|
||||||
|
assert.Equal(t, 1, configs.ACME.Protocols)
|
||||||
|
assert.Equal(t, "acme.example.com", configs.ACME.Domain)
|
||||||
|
assert.Equal(t, "email@example.com", configs.ACME.Email)
|
||||||
|
assert.False(t, configs.ACME.HasProtocol(common.ProtocolFTP))
|
||||||
|
assert.False(t, configs.ACME.HasProtocol(common.ProtocolWebDAV))
|
||||||
|
assert.True(t, configs.ACME.HasProtocol(common.ProtocolHTTP))
|
||||||
|
// updates will fail, the get certificate fn will return error with nil callback
|
||||||
|
httpd.SetCertificatesGetter(nil)
|
||||||
|
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(), "unable to get TLS certificates")
|
||||||
|
|
||||||
err = dataprovider.UpdateConfigs(nil, "", "", "")
|
err = dataprovider.UpdateConfigs(nil, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
|
@ -3055,6 +3055,80 @@ func TestEventsCSV(t *testing.T) {
|
||||||
assert.Equal(t, "Quota exceeded", data[5])
|
assert.Equal(t, "Quota exceeded", data[5])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigsFromProvider(t *testing.T) {
|
||||||
|
err := dataprovider.UpdateConfigs(nil, "", "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
c := Conf{
|
||||||
|
Bindings: []Binding{
|
||||||
|
{
|
||||||
|
Port: 1234,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = c.loadFromProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, c.acmeDomain)
|
||||||
|
configs := dataprovider.Configs{
|
||||||
|
ACME: &dataprovider.ACMEConfigs{
|
||||||
|
Domain: "domain.com",
|
||||||
|
Email: "info@domain.com",
|
||||||
|
HTTP01Challenge: dataprovider.ACMEHTTP01Challenge{Port: 80},
|
||||||
|
Protocols: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = dataprovider.UpdateConfigs(&configs, "", "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
util.CertsBasePath = ""
|
||||||
|
// crt and key empty
|
||||||
|
err = c.loadFromProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, c.acmeDomain)
|
||||||
|
util.CertsBasePath = filepath.Clean(os.TempDir())
|
||||||
|
// crt not found
|
||||||
|
err = c.loadFromProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, c.acmeDomain)
|
||||||
|
keyPairs := c.getKeyPairs(configDir)
|
||||||
|
assert.Len(t, keyPairs, 0)
|
||||||
|
crtPath := filepath.Join(util.CertsBasePath, util.SanitizeDomain(configs.ACME.Domain)+".crt")
|
||||||
|
err = os.WriteFile(crtPath, nil, 0666)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// key not found
|
||||||
|
err = c.loadFromProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, c.acmeDomain)
|
||||||
|
keyPairs = c.getKeyPairs(configDir)
|
||||||
|
assert.Len(t, keyPairs, 0)
|
||||||
|
keyPath := filepath.Join(util.CertsBasePath, util.SanitizeDomain(configs.ACME.Domain)+".key")
|
||||||
|
err = os.WriteFile(keyPath, nil, 0666)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// acme cert used
|
||||||
|
err = c.loadFromProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, configs.ACME.Domain, c.acmeDomain)
|
||||||
|
keyPairs = c.getKeyPairs(configDir)
|
||||||
|
assert.Len(t, keyPairs, 1)
|
||||||
|
assert.True(t, c.Bindings[0].EnableHTTPS)
|
||||||
|
// protocols does not match
|
||||||
|
configs.ACME.Protocols = 6
|
||||||
|
err = dataprovider.UpdateConfigs(&configs, "", "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
c.acmeDomain = ""
|
||||||
|
err = c.loadFromProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, c.acmeDomain)
|
||||||
|
keyPairs = c.getKeyPairs(configDir)
|
||||||
|
assert.Len(t, keyPairs, 0)
|
||||||
|
|
||||||
|
err = os.Remove(crtPath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.Remove(keyPath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
util.CertsBasePath = ""
|
||||||
|
err = dataprovider.UpdateConfigs(nil, "", "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func isSharedProviderSupported() bool {
|
func isSharedProviderSupported() bool {
|
||||||
// SQLite shares the implementation with other SQL-based provider but it makes no sense
|
// SQLite shares the implementation with other SQL-based provider but it makes no sense
|
||||||
// to use it outside test cases
|
// to use it outside test cases
|
||||||
|
|
|
@ -903,6 +903,9 @@ func (s *httpdServer) renderConfigsPage(w http.ResponseWriter, r *http.Request,
|
||||||
if configs.SMTP.Port == 0 {
|
if configs.SMTP.Port == 0 {
|
||||||
configs.SMTP.Port = 587
|
configs.SMTP.Port = 587
|
||||||
}
|
}
|
||||||
|
if configs.ACME.HTTP01Challenge.Port == 0 {
|
||||||
|
configs.ACME.HTTP01Challenge.Port = 80
|
||||||
|
}
|
||||||
data := configsPage{
|
data := configsPage{
|
||||||
basePage: s.getBasePageData(pageConfigsTitle, webConfigsPath, r),
|
basePage: s.getBasePageData(pageConfigsTitle, webConfigsPath, r),
|
||||||
Configs: configs,
|
Configs: configs,
|
||||||
|
@ -2544,10 +2547,35 @@ func getSFTPConfigsFromPostFields(r *http.Request) *dataprovider.SFTPDConfigs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getACMEConfigsFromPostFields(r *http.Request) *dataprovider.ACMEConfigs {
|
||||||
|
port, err := strconv.Atoi(r.Form.Get("acme_port"))
|
||||||
|
if err != nil {
|
||||||
|
port = 80
|
||||||
|
}
|
||||||
|
var protocols int
|
||||||
|
for _, val := range r.Form["acme_protocols"] {
|
||||||
|
switch val {
|
||||||
|
case "1":
|
||||||
|
protocols++
|
||||||
|
case "2":
|
||||||
|
protocols += 2
|
||||||
|
case "3":
|
||||||
|
protocols += 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &dataprovider.ACMEConfigs{
|
||||||
|
Domain: strings.TrimSpace(r.Form.Get("acme_domain")),
|
||||||
|
Email: strings.TrimSpace(r.Form.Get("acme_email")),
|
||||||
|
HTTP01Challenge: dataprovider.ACMEHTTP01Challenge{Port: port},
|
||||||
|
Protocols: protocols,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getSMTPConfigsFromPostFields(r *http.Request) *dataprovider.SMTPConfigs {
|
func getSMTPConfigsFromPostFields(r *http.Request) *dataprovider.SMTPConfigs {
|
||||||
port, err := strconv.Atoi(r.Form.Get("smtp_port"))
|
port, err := strconv.Atoi(r.Form.Get("smtp_port"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
port = 0
|
port = 587
|
||||||
}
|
}
|
||||||
authType, err := strconv.Atoi(r.Form.Get("smtp_auth"))
|
authType, err := strconv.Atoi(r.Form.Get("smtp_auth"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -4042,8 +4070,16 @@ func (s *httpdServer) handleWebConfigsPost(w http.ResponseWriter, r *http.Reques
|
||||||
configSection = 1
|
configSection = 1
|
||||||
sftpConfigs := getSFTPConfigsFromPostFields(r)
|
sftpConfigs := getSFTPConfigsFromPostFields(r)
|
||||||
configs.SFTPD = sftpConfigs
|
configs.SFTPD = sftpConfigs
|
||||||
case "smtp_submit":
|
case "acme_submit":
|
||||||
configSection = 2
|
configSection = 2
|
||||||
|
acmeConfigs := getACMEConfigsFromPostFields(r)
|
||||||
|
configs.ACME = acmeConfigs
|
||||||
|
if err := getTLSCertificates(acmeConfigs); err != nil {
|
||||||
|
s.renderConfigsPage(w, r, configs, err.Error(), configSection)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "smtp_submit":
|
||||||
|
configSection = 3
|
||||||
smtpConfigs := getSMTPConfigsFromPostFields(r)
|
smtpConfigs := getSMTPConfigsFromPostFields(r)
|
||||||
if smtpConfigs.Password.IsNotPlainAndNotEmpty() {
|
if smtpConfigs.Password.IsNotPlainAndNotEmpty() {
|
||||||
smtpConfigs.Password = configs.SMTP.Password
|
smtpConfigs.Password = configs.SMTP.Password
|
||||||
|
@ -4059,7 +4095,7 @@ func (s *httpdServer) handleWebConfigsPost(w http.ResponseWriter, r *http.Reques
|
||||||
s.renderConfigsPage(w, r, configs, err.Error(), configSection)
|
s.renderConfigsPage(w, r, configs, err.Error(), configSection)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if configSection == 2 {
|
if configSection == 3 {
|
||||||
err := configs.SMTP.Password.TryDecrypt()
|
err := configs.SMTP.Password.TryDecrypt()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
smtp.Activate(configs.SMTP)
|
smtp.Activate(configs.SMTP)
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
|
||||||
|
"github.com/drakkan/sftpgo/v2/internal/acme"
|
||||||
"github.com/drakkan/sftpgo/v2/internal/common"
|
"github.com/drakkan/sftpgo/v2/internal/common"
|
||||||
"github.com/drakkan/sftpgo/v2/internal/config"
|
"github.com/drakkan/sftpgo/v2/internal/config"
|
||||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||||
|
@ -169,7 +170,7 @@ func (s *Service) initializeServices(disableAWSInstallationCode bool) error {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
acmeConfig := config.GetACMEConfig()
|
acmeConfig := config.GetACMEConfig()
|
||||||
err = acmeConfig.Initialize(s.ConfigDir, true)
|
err = acme.Initialize(acmeConfig, s.ConfigDir, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(logSender, "", "error initializing ACME configuration: %v", err)
|
logger.Error(logSender, "", "error initializing ACME configuration: %v", err)
|
||||||
logger.ErrorToConsole("error initializing ACME configuration: %v", err)
|
logger.ErrorToConsole("error initializing ACME configuration: %v", err)
|
||||||
|
|
|
@ -30,6 +30,7 @@ func initializeRouter(enableProfiler bool) {
|
||||||
router = chi.NewRouter()
|
router = chi.NewRouter()
|
||||||
|
|
||||||
router.Use(middleware.GetHead)
|
router.Use(middleware.GetHead)
|
||||||
|
router.Use(logger.NewStructuredLogger(logger.GetLogger()))
|
||||||
router.Use(middleware.Recoverer)
|
router.Use(middleware.Recoverer)
|
||||||
|
|
||||||
router.Group(func(r chi.Router) {
|
router.Group(func(r chi.Router) {
|
||||||
|
|
|
@ -61,6 +61,9 @@ var (
|
||||||
emailRegex = regexp.MustCompile("^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$")
|
emailRegex = regexp.MustCompile("^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$")
|
||||||
// this can be set at build time
|
// this can be set at build time
|
||||||
additionalSharedDataSearchPath = ""
|
additionalSharedDataSearchPath = ""
|
||||||
|
// CertsBasePath defines base path for certificates obtained using the built-in ACME protocol.
|
||||||
|
// It is empty is ACME support is disabled
|
||||||
|
CertsBasePath string
|
||||||
)
|
)
|
||||||
|
|
||||||
// IEC Sizes.
|
// IEC Sizes.
|
||||||
|
@ -754,6 +757,11 @@ func IsEmailValid(email string) bool {
|
||||||
return emailRegex.MatchString(email)
|
return emailRegex.MatchString(email)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SanitizeDomain return the specified domain name in a form suitable to save as file
|
||||||
|
func SanitizeDomain(domain string) string {
|
||||||
|
return strings.NewReplacer(":", "_", "*", "_", ",", "_", " ", "_").Replace(domain)
|
||||||
|
}
|
||||||
|
|
||||||
// PanicOnError calls panic if err is not nil
|
// PanicOnError calls panic if err is not nil
|
||||||
func PanicOnError(err error) {
|
func PanicOnError(err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -777,6 +785,15 @@ func GetAbsolutePath(name string) (string, error) {
|
||||||
return filepath.Join(curDir, name), nil
|
return filepath.Join(curDir, name), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetACMECertificateKeyPair returns the path to the ACME TLS crt and key for the specified domain
|
||||||
|
func GetACMECertificateKeyPair(domain string) (string, string) {
|
||||||
|
if CertsBasePath == "" {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
domain = SanitizeDomain(domain)
|
||||||
|
return filepath.Join(CertsBasePath, domain+".crt"), filepath.Join(CertsBasePath, domain+".key")
|
||||||
|
}
|
||||||
|
|
||||||
// GetLastIPForPrefix returns the last IP for the given prefix
|
// GetLastIPForPrefix returns the last IP for the given prefix
|
||||||
// https://github.com/go4org/netipx/blob/8449b0a6169f5140fb0340cb4fc0de4c9b281ef6/netipx.go#L173
|
// https://github.com/go4org/netipx/blob/8449b0a6169f5140fb0340cb4fc0de4c9b281ef6/netipx.go#L173
|
||||||
func GetLastIPForPrefix(p netip.Prefix) netip.Addr {
|
func GetLastIPForPrefix(p netip.Prefix) netip.Addr {
|
||||||
|
|
|
@ -1584,3 +1584,78 @@ func TestMisc(t *testing.T) {
|
||||||
|
|
||||||
certMgr = oldCertMgr
|
certMgr = oldCertMgr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigsFromProvider(t *testing.T) {
|
||||||
|
configDir := "."
|
||||||
|
err := dataprovider.UpdateConfigs(nil, "", "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
c := Configuration{
|
||||||
|
Bindings: []Binding{
|
||||||
|
{
|
||||||
|
Port: 1234,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = c.loadFromProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, c.acmeDomain)
|
||||||
|
configs := dataprovider.Configs{
|
||||||
|
ACME: &dataprovider.ACMEConfigs{
|
||||||
|
Domain: "domain.com",
|
||||||
|
Email: "info@domain.com",
|
||||||
|
HTTP01Challenge: dataprovider.ACMEHTTP01Challenge{Port: 80},
|
||||||
|
Protocols: 7,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = dataprovider.UpdateConfigs(&configs, "", "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
util.CertsBasePath = ""
|
||||||
|
// crt and key empty
|
||||||
|
err = c.loadFromProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, c.acmeDomain)
|
||||||
|
util.CertsBasePath = filepath.Clean(os.TempDir())
|
||||||
|
// crt not found
|
||||||
|
err = c.loadFromProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, c.acmeDomain)
|
||||||
|
keyPairs := c.getKeyPairs(configDir)
|
||||||
|
assert.Len(t, keyPairs, 0)
|
||||||
|
crtPath := filepath.Join(util.CertsBasePath, util.SanitizeDomain(configs.ACME.Domain)+".crt")
|
||||||
|
err = os.WriteFile(crtPath, nil, 0666)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// key not found
|
||||||
|
err = c.loadFromProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, c.acmeDomain)
|
||||||
|
keyPairs = c.getKeyPairs(configDir)
|
||||||
|
assert.Len(t, keyPairs, 0)
|
||||||
|
keyPath := filepath.Join(util.CertsBasePath, util.SanitizeDomain(configs.ACME.Domain)+".key")
|
||||||
|
err = os.WriteFile(keyPath, nil, 0666)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// acme cert used
|
||||||
|
err = c.loadFromProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, configs.ACME.Domain, c.acmeDomain)
|
||||||
|
keyPairs = c.getKeyPairs(configDir)
|
||||||
|
assert.Len(t, keyPairs, 1)
|
||||||
|
assert.True(t, c.Bindings[0].EnableHTTPS)
|
||||||
|
// protocols does not match
|
||||||
|
configs.ACME.Protocols = 3
|
||||||
|
err = dataprovider.UpdateConfigs(&configs, "", "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
c.acmeDomain = ""
|
||||||
|
err = c.loadFromProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, c.acmeDomain)
|
||||||
|
keyPairs = c.getKeyPairs(configDir)
|
||||||
|
assert.Len(t, keyPairs, 0)
|
||||||
|
|
||||||
|
err = os.Remove(crtPath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.Remove(keyPath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
util.CertsBasePath = ""
|
||||||
|
err = dataprovider.UpdateConfigs(nil, "", "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ package webdavd
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
|
@ -182,6 +183,7 @@ type Configuration struct {
|
||||||
Cors CorsConfig `json:"cors" mapstructure:"cors"`
|
Cors CorsConfig `json:"cors" mapstructure:"cors"`
|
||||||
// Cache configuration
|
// Cache configuration
|
||||||
Cache Cache `json:"cache" mapstructure:"cache"`
|
Cache Cache `json:"cache" mapstructure:"cache"`
|
||||||
|
acmeDomain string
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStatus returns the server status
|
// GetStatus returns the server status
|
||||||
|
@ -214,8 +216,13 @@ func (c *Configuration) getKeyPairs(configDir string) []common.TLSKeyPair {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
certificateFile := getConfigPath(c.CertificateFile, configDir)
|
var certificateFile, certificateKeyFile string
|
||||||
certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)
|
if c.acmeDomain != "" {
|
||||||
|
certificateFile, certificateKeyFile = util.GetACMECertificateKeyPair(c.acmeDomain)
|
||||||
|
} else {
|
||||||
|
certificateFile = getConfigPath(c.CertificateFile, configDir)
|
||||||
|
certificateKeyFile = getConfigPath(c.CertificateKeyFile, configDir)
|
||||||
|
}
|
||||||
if certificateFile != "" && certificateKeyFile != "" {
|
if certificateFile != "" && certificateKeyFile != "" {
|
||||||
keyPairs = append(keyPairs, common.TLSKeyPair{
|
keyPairs = append(keyPairs, common.TLSKeyPair{
|
||||||
Cert: certificateFile,
|
Cert: certificateFile,
|
||||||
|
@ -226,8 +233,40 @@ func (c *Configuration) getKeyPairs(configDir string) []common.TLSKeyPair {
|
||||||
return keyPairs
|
return keyPairs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 configs.ACME.Domain == "" || !configs.ACME.HasProtocol(common.ProtocolWebDAV) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
crt, key := util.GetACMECertificateKeyPair(configs.ACME.Domain)
|
||||||
|
if crt != "" && key != "" {
|
||||||
|
if _, err := os.Stat(crt); err != nil {
|
||||||
|
logger.Error(logSender, "", "unable to load acme cert file %q: %v", crt, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(key); err != nil {
|
||||||
|
logger.Error(logSender, "", "unable to load acme key file %q: %v", key, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for idx := range c.Bindings {
|
||||||
|
c.Bindings[idx].EnableHTTPS = true
|
||||||
|
}
|
||||||
|
c.acmeDomain = configs.ACME.Domain
|
||||||
|
logger.Info(logSender, "", "acme domain set to %q", c.acmeDomain)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize configures and starts the WebDAV server
|
// Initialize configures and starts the WebDAV server
|
||||||
func (c *Configuration) Initialize(configDir string) error {
|
func (c *Configuration) Initialize(configDir string) error {
|
||||||
|
if err := c.loadFromProvider(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
logger.Info(logSender, "", "initializing WebDAV server with config %+v", *c)
|
logger.Info(logSender, "", "initializing WebDAV server with config %+v", *c)
|
||||||
mimeTypeCache = mimeCache{
|
mimeTypeCache = mimeCache{
|
||||||
maxSize: c.Cache.MimeTypes.MaxSize,
|
maxSize: c.Cache.MimeTypes.MaxSize,
|
||||||
|
|
|
@ -510,6 +510,17 @@ func TestInitialization(t *testing.T) {
|
||||||
cfg.Bindings[0].ProxyAllowed = nil
|
cfg.Bindings[0].ProxyAllowed = nil
|
||||||
err = cfg.Initialize(configDir)
|
err = cfg.Initialize(configDir)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
err = dataprovider.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = cfg.Initialize(configDir)
|
||||||
|
if assert.Error(t, err) {
|
||||||
|
assert.Contains(t, err.Error(), "unable to load config from provider")
|
||||||
|
}
|
||||||
|
err = config.LoadConfig(configDir, "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
providerConf := config.GetProviderConf()
|
||||||
|
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBasicHandling(t *testing.T) {
|
func TestBasicHandling(t *testing.T) {
|
||||||
|
|
|
@ -116,6 +116,77 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header" id="headingACME">
|
||||||
|
<h2 class="mb-0">
|
||||||
|
<button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse"
|
||||||
|
data-target="#collapseACME" aria-expanded="true" aria-controls="collapseACME">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">ACME</h6>
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="collapseACME" class="collapse {{if eq .ConfigSection 2}}show{{end}}" aria-labelledby="headingACME" data-parent="#accordionConfigs">
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="configs-acme-info" class="card mb-3 border-left-info">
|
||||||
|
<div class="card-body">From this section you can request free TLS certificates for your SFTPGo services using the ACME protocol and the HTTP-01 challenge type. You must create a DNS entry under a custom domain that you own which resolves to your SFTPGo public IP address and the port 80 must be publicly reachable. You can set the configuration options for the most common use cases and single node setups here, for advanced configurations refer to the SFTPGo docs. A service restart is required to apply changes</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="idACMEDomain" class="col-sm-2 col-form-label">Domain</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" id="idACMEDomain" name="acme_domain" placeholder=""
|
||||||
|
value="{{.Configs.ACME.Domain}}" aria-describedby="acmeDomainHelpBlock">
|
||||||
|
<small id="acmeDomainHelpBlock" class="form-text text-muted">
|
||||||
|
Multiple domains can be specified comma or space separated. They will be included in the same certificate
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="idACMEEmail" class="col-sm-2 col-form-label">Email</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" id="idACMEEmail" name="acme_email" placeholder=""
|
||||||
|
value="{{.Configs.ACME.Email}}" spellcheck="false" aria-describedby="acmeEmailHelpBlock">
|
||||||
|
<small id="acmeEmailHelpBlock" class="form-text text-muted">
|
||||||
|
Email used for registration and recovery contact
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="idACMEPort" class="col-sm-2 col-form-label">Port</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="number" min="1" max="65535" class="form-control" id="idACMEPort" name="acme_port" placeholder=""
|
||||||
|
value="{{.Configs.ACME.HTTP01Challenge.Port}}" aria-describedby="acmePortHelpBlock">
|
||||||
|
<small id="acmePortHelpBlock" class="form-text text-muted">
|
||||||
|
If different from 80 you have to configure a reverse proxy
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="idACMEProtocols" class="col-sm-2 col-form-label">Protocols</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<select class="form-control selectpicker" id="idACMEProtocols" name="acme_protocols" aria-describedby="acmeProtocolsHelpBlock" multiple>
|
||||||
|
<option value="1" {{if .Configs.ACME.HasProtocol "HTTP"}}selected{{end}}>HTTP</option>
|
||||||
|
<option value="2" {{if .Configs.ACME.HasProtocol "FTP"}}selected{{end}}>FTP</option>
|
||||||
|
<option value="3" {{if .Configs.ACME.HasProtocol "DAV"}}selected{{end}}>DAV</option>
|
||||||
|
</select>
|
||||||
|
<small id="acmePortHelpBlock" class="form-text text-muted">
|
||||||
|
Use the obtained certificates for the specified protocols
|
||||||
|
</small>
|
||||||
|
</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="acme_submit" onclick="showSpinner();">Submit</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header" id="headingSMTP">
|
<div class="card-header" id="headingSMTP">
|
||||||
<h2 class="mb-0">
|
<h2 class="mb-0">
|
||||||
|
@ -126,7 +197,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="collapseSMTP" class="collapse {{if eq .ConfigSection 2}}show{{end}}" aria-labelledby="headingSMTP" data-parent="#accordionConfigs">
|
<div id="collapseSMTP" class="collapse {{if eq .ConfigSection 3}}show{{end}}" aria-labelledby="headingSMTP" data-parent="#accordionConfigs">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div id="configs-smtp-info" class="card mb-3 border-left-info">
|
<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 class="card-body">Set the SMTP configuration replacing the one defined using env vars or config file if any.</div>
|
||||||
|
@ -144,7 +215,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
<div class="col-sm-1"></div>
|
<div class="col-sm-1"></div>
|
||||||
<label for="idSMTPPort" class="col-sm-2 col-form-label">Port</label>
|
<label for="idSMTPPort" class="col-sm-2 col-form-label">Port</label>
|
||||||
<div class="col-sm-2">
|
<div class="col-sm-2">
|
||||||
<input type="number" min="0" max="65535" class="form-control" id="idSMTPPort" name="smtp_port" placeholder=""
|
<input type="number" min="1" max="65535" class="form-control" id="idSMTPPort" name="smtp_port" placeholder=""
|
||||||
value="{{.Configs.SMTP.Port}}">
|
value="{{.Configs.SMTP.Port}}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -268,6 +339,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var spinnerDone = false;
|
var spinnerDone = false;
|
||||||
|
|
||||||
|
function showSpinner(){
|
||||||
|
$('#spinnerModal').modal('show');
|
||||||
|
}
|
||||||
|
|
||||||
function testSMTP(event){
|
function testSMTP(event){
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let recipient = $('#idSMTPRecipient').val();
|
let recipient = $('#idSMTPRecipient').val();
|
||||||
|
@ -279,7 +354,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
}
|
}
|
||||||
$('#smtpSuccessMsg').hide();
|
$('#smtpSuccessMsg').hide();
|
||||||
$('#smtpErrorMsg').hide();
|
$('#smtpErrorMsg').hide();
|
||||||
$('#spinnerModal').modal('show');
|
showSpinner();
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "{{.ConfigsURL}}/smtp/test",
|
url: "{{.ConfigsURL}}/smtp/test",
|
||||||
|
|
Loading…
Reference in a new issue