| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648 |
- // 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 (
- "bytes"
- "encoding/json"
- "fmt"
- "image/png"
- "net/url"
- "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.InsecureKeyAlgoDSA}
- supportedKexAlgos = []string{
- ssh.KeyExchangeDH16SHA512, ssh.InsecureKeyExchangeDH14SHA1, ssh.InsecureKeyExchangeDH1SHA1,
- ssh.InsecureKeyExchangeDHGEXSHA1,
- }
- supportedCiphers = []string{
- ssh.InsecureCipherAES128CBC, ssh.InsecureCipherAES192CBC, ssh.InsecureCipherAES256CBC,
- ssh.InsecureCipherTripleDESCBC,
- }
- supportedMACs = []string{
- ssh.HMACSHA512ETM, ssh.HMACSHA512,
- ssh.InsecureHMACSHA1, ssh.InsecureHMACSHA196,
- }
- )
- // SFTPDConfigs defines configurations for SFTPD
- type SFTPDConfigs struct {
- HostKeyAlgos []string `json:"host_key_algos,omitempty"`
- PublicKeyAlgos []string `json:"public_key_algos,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.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
- }
- 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" || algo == ssh.KeyExchangeDHGEXSHA256 {
- 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)
- 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,
- 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.NewI18nError(
- util.NewValidationError("smtp oauth2: client id is required"),
- util.I18nErrorSMTPClientIDRequired,
- )
- }
- if c.ClientSecret == nil {
- return util.NewI18nError(
- util.NewValidationError("smtp oauth2: client secret is required"),
- util.I18nErrorSMTPClientSecretRequired,
- )
- }
- if c.RefreshToken == nil {
- return util.NewI18nError(
- util.NewValidationError("smtp oauth2: refresh token is required"),
- util.I18nErrorSMTPRefreshTokenRequired,
- )
- }
- 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.NewI18nError(
- util.NewValidationError("smtp: from address and user cannot both be empty"),
- util.I18nErrorSMTPRequiredFields,
- )
- }
- 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) prepareForRendering() {
- if c.Password != nil {
- c.Password.Hide()
- if c.Password.IsEmpty() {
- c.Password = nil
- }
- }
- if c.OAuth2.ClientSecret != nil {
- c.OAuth2.ClientSecret.Hide()
- if c.OAuth2.ClientSecret.IsEmpty() {
- c.OAuth2.ClientSecret = nil
- }
- }
- if c.OAuth2.RefreshToken != nil {
- c.OAuth2.RefreshToken.Hide()
- if c.OAuth2.RefreshToken.IsEmpty() {
- c.OAuth2.RefreshToken = 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.NewI18nError(
- util.NewValidationError(fmt.Sprintf("acme: invalid email %q", c.Email)),
- util.I18nErrorInvalidEmail,
- )
- }
- 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,
- }
- }
- // BrandingConfig defines the branding configuration
- type BrandingConfig struct {
- Name string `json:"name"`
- ShortName string `json:"short_name"`
- Logo []byte `json:"logo"`
- Favicon []byte `json:"favicon"`
- DisclaimerName string `json:"disclaimer_name"`
- DisclaimerURL string `json:"disclaimer_url"`
- }
- func (c *BrandingConfig) isEmpty() bool {
- if c.Name != "" {
- return false
- }
- if c.ShortName != "" {
- return false
- }
- if len(c.Logo) > 0 {
- return false
- }
- if len(c.Favicon) > 0 {
- return false
- }
- if c.DisclaimerName != "" && c.DisclaimerURL != "" {
- return false
- }
- return true
- }
- func (*BrandingConfig) validatePNG(b []byte, maxWidth, maxHeight int) error {
- if len(b) == 0 {
- return nil
- }
- // DecodeConfig is more efficient, but I'm not sure if this would lead to
- // accepting invalid images in some edge cases and performance does not
- // matter here.
- img, err := png.Decode(bytes.NewBuffer(b))
- if err != nil {
- return util.NewI18nError(
- util.NewValidationError("invalid PNG image"),
- util.I18nErrorInvalidPNG,
- )
- }
- bounds := img.Bounds()
- if bounds.Dx() > maxWidth || bounds.Dy() > maxHeight {
- return util.NewI18nError(
- util.NewValidationError("invalid PNG image size"),
- util.I18nErrorInvalidPNGSize,
- )
- }
- return nil
- }
- func (c *BrandingConfig) validateDisclaimerURL() error {
- if c.DisclaimerURL == "" {
- return nil
- }
- u, err := url.Parse(c.DisclaimerURL)
- if err != nil {
- return util.NewI18nError(
- util.NewValidationError("invalid disclaimer URL"),
- util.I18nErrorInvalidDisclaimerURL,
- )
- }
- if u.Scheme != "http" && u.Scheme != "https" {
- return util.NewI18nError(
- util.NewValidationError("invalid disclaimer URL scheme"),
- util.I18nErrorInvalidDisclaimerURL,
- )
- }
- return nil
- }
- func (c *BrandingConfig) validate() error {
- if err := c.validateDisclaimerURL(); err != nil {
- return err
- }
- if err := c.validatePNG(c.Logo, 512, 512); err != nil {
- return err
- }
- return c.validatePNG(c.Favicon, 256, 256)
- }
- func (c *BrandingConfig) getACopy() BrandingConfig {
- logo := make([]byte, len(c.Logo))
- copy(logo, c.Logo)
- favicon := make([]byte, len(c.Favicon))
- copy(favicon, c.Favicon)
- return BrandingConfig{
- Name: c.Name,
- ShortName: c.ShortName,
- Logo: logo,
- Favicon: favicon,
- DisclaimerName: c.DisclaimerName,
- DisclaimerURL: c.DisclaimerURL,
- }
- }
- // BrandingConfigs defines the branding configuration for WebAdmin and WebClient UI
- type BrandingConfigs struct {
- WebAdmin BrandingConfig
- WebClient BrandingConfig
- }
- func (c *BrandingConfigs) isEmpty() bool {
- return c.WebAdmin.isEmpty() && c.WebClient.isEmpty()
- }
- func (c *BrandingConfigs) validate() error {
- if err := c.WebAdmin.validate(); err != nil {
- return err
- }
- return c.WebClient.validate()
- }
- func (c *BrandingConfigs) getACopy() *BrandingConfigs {
- return &BrandingConfigs{
- WebAdmin: c.WebAdmin.getACopy(),
- WebClient: c.WebClient.getACopy(),
- }
- }
- // 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"`
- Branding *BrandingConfigs `json:"branding,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
- }
- }
- if c.Branding != nil {
- if err := c.Branding.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.Branding != nil && c.Branding.isEmpty() {
- c.Branding = nil
- }
- if c.SMTP != nil {
- c.SMTP.prepareForRendering()
- }
- }
- // 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{}
- }
- if c.Branding == nil {
- c.Branding = &BrandingConfigs{}
- }
- }
- // 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()
- }
- if c.Branding != nil {
- result.Branding = c.Branding.getACopy()
- }
- result.UpdatedAt = c.UpdatedAt
- return result
- }
|