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