httpclient.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. package httpclient
  2. import (
  3. "crypto/tls"
  4. "crypto/x509"
  5. "fmt"
  6. "net/http"
  7. "os"
  8. "path/filepath"
  9. "time"
  10. "github.com/hashicorp/go-retryablehttp"
  11. "github.com/drakkan/sftpgo/logger"
  12. "github.com/drakkan/sftpgo/utils"
  13. )
  14. // TLSKeyPair defines the paths for a TLS key pair
  15. type TLSKeyPair struct {
  16. Cert string `json:"cert" mapstructure:"cert"`
  17. Key string `json:"key" mapstructure:"key"`
  18. }
  19. // Config defines the configuration for HTTP clients.
  20. // HTTP clients are used for executing hooks such as the ones used for
  21. // custom actions, external authentication and pre-login user modifications
  22. type Config struct {
  23. // Timeout specifies a time limit, in seconds, for a request
  24. Timeout int64 `json:"timeout" mapstructure:"timeout"`
  25. // RetryWaitMin defines the minimum waiting time between attempts in seconds
  26. RetryWaitMin int `json:"retry_wait_min" mapstructure:"retry_wait_min"`
  27. // RetryWaitMax defines the minimum waiting time between attempts in seconds
  28. RetryWaitMax int `json:"retry_wait_max" mapstructure:"retry_wait_max"`
  29. // RetryMax defines the maximum number of attempts
  30. RetryMax int `json:"retry_max" mapstructure:"retry_max"`
  31. // CACertificates defines extra CA certificates to trust.
  32. // The paths can be absolute or relative to the config dir.
  33. // Adding trusted CA certificates is a convenient way to use self-signed
  34. // certificates without defeating the purpose of using TLS
  35. CACertificates []string `json:"ca_certificates" mapstructure:"ca_certificates"`
  36. // Certificates defines the certificates to use for mutual TLS
  37. Certificates []TLSKeyPair `json:"certificates" mapstructure:"certificates"`
  38. // if enabled the HTTP client accepts any TLS certificate presented by
  39. // the server and any host name in that certificate.
  40. // In this mode, TLS is susceptible to man-in-the-middle attacks.
  41. // This should be used only for testing.
  42. SkipTLSVerify bool `json:"skip_tls_verify" mapstructure:"skip_tls_verify"`
  43. customTransport *http.Transport
  44. tlsConfig *tls.Config
  45. }
  46. const logSender = "httpclient"
  47. var httpConfig Config
  48. // Initialize configures HTTP clients
  49. func (c *Config) Initialize(configDir string) error {
  50. rootCAs, err := c.loadCACerts(configDir)
  51. if err != nil {
  52. return err
  53. }
  54. customTransport := http.DefaultTransport.(*http.Transport).Clone()
  55. if customTransport.TLSClientConfig != nil {
  56. customTransport.TLSClientConfig.RootCAs = rootCAs
  57. } else {
  58. customTransport.TLSClientConfig = &tls.Config{
  59. RootCAs: rootCAs,
  60. NextProtos: []string{"h2", "http/1.1"},
  61. }
  62. }
  63. customTransport.TLSClientConfig.InsecureSkipVerify = c.SkipTLSVerify
  64. c.customTransport = customTransport
  65. c.tlsConfig = customTransport.TLSClientConfig
  66. err = c.loadCertificates(configDir)
  67. if err != nil {
  68. return err
  69. }
  70. httpConfig = *c
  71. return nil
  72. }
  73. // loadCACerts returns system cert pools and try to add the configured
  74. // CA certificates to it
  75. func (c *Config) loadCACerts(configDir string) (*x509.CertPool, error) {
  76. if len(c.CACertificates) == 0 {
  77. return nil, nil
  78. }
  79. rootCAs, err := x509.SystemCertPool()
  80. if err != nil {
  81. rootCAs = x509.NewCertPool()
  82. }
  83. for _, ca := range c.CACertificates {
  84. if !utils.IsFileInputValid(ca) {
  85. return nil, fmt.Errorf("unable to load invalid CA certificate: %#v", ca)
  86. }
  87. if !filepath.IsAbs(ca) {
  88. ca = filepath.Join(configDir, ca)
  89. }
  90. certs, err := os.ReadFile(ca)
  91. if err != nil {
  92. return nil, fmt.Errorf("unable to load CA certificate: %v", err)
  93. }
  94. if rootCAs.AppendCertsFromPEM(certs) {
  95. logger.Debug(logSender, "", "CA certificate %#v added to the trusted certificates", ca)
  96. } else {
  97. return nil, fmt.Errorf("unable to add CA certificate %#v to the trusted cetificates", ca)
  98. }
  99. }
  100. return rootCAs, nil
  101. }
  102. func (c *Config) loadCertificates(configDir string) error {
  103. if len(c.Certificates) == 0 {
  104. return nil
  105. }
  106. for _, keyPair := range c.Certificates {
  107. cert := keyPair.Cert
  108. key := keyPair.Key
  109. if !utils.IsFileInputValid(cert) {
  110. return fmt.Errorf("unable to load invalid certificate: %#v", cert)
  111. }
  112. if !utils.IsFileInputValid(key) {
  113. return fmt.Errorf("unable to load invalid key: %#v", key)
  114. }
  115. if !filepath.IsAbs(cert) {
  116. cert = filepath.Join(configDir, cert)
  117. }
  118. if !filepath.IsAbs(key) {
  119. key = filepath.Join(configDir, key)
  120. }
  121. tlsCert, err := tls.LoadX509KeyPair(cert, key)
  122. if err != nil {
  123. return fmt.Errorf("unable to load key pair %#v, %#v: %v", cert, key, err)
  124. }
  125. logger.Debug(logSender, "", "client certificate %#v and key %#v successfully loaded", cert, key)
  126. c.tlsConfig.Certificates = append(c.tlsConfig.Certificates, tlsCert)
  127. }
  128. return nil
  129. }
  130. // GetHTTPClient returns an HTTP client with the configured parameters
  131. func GetHTTPClient() *http.Client {
  132. return &http.Client{
  133. Timeout: time.Duration(httpConfig.Timeout) * time.Second,
  134. Transport: httpConfig.customTransport,
  135. }
  136. }
  137. // GetRetraybleHTTPClient returns an HTTP client that retry a request on error.
  138. // It uses the configured retry parameters
  139. func GetRetraybleHTTPClient() *retryablehttp.Client {
  140. client := retryablehttp.NewClient()
  141. client.HTTPClient.Timeout = time.Duration(httpConfig.Timeout) * time.Second
  142. client.HTTPClient.Transport.(*http.Transport).TLSClientConfig = httpConfig.tlsConfig
  143. client.Logger = &logger.LeveledLogger{Sender: "RetryableHTTPClient"}
  144. client.RetryWaitMin = time.Duration(httpConfig.RetryWaitMin) * time.Second
  145. client.RetryWaitMax = time.Duration(httpConfig.RetryWaitMax) * time.Second
  146. client.RetryMax = httpConfig.RetryMax
  147. return client
  148. }