httpclient.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. // Copyright (C) 2019-2022 Nicola Murino
  2. //
  3. // This program is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU Affero General Public License as published
  5. // by the Free Software Foundation, version 3.
  6. //
  7. // This program is distributed in the hope that it will be useful,
  8. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. // GNU Affero General Public License for more details.
  11. //
  12. // You should have received a copy of the GNU Affero General Public License
  13. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  14. package httpclient
  15. import (
  16. "crypto/tls"
  17. "crypto/x509"
  18. "fmt"
  19. "io"
  20. "net/http"
  21. "os"
  22. "path/filepath"
  23. "strings"
  24. "time"
  25. "github.com/hashicorp/go-retryablehttp"
  26. "github.com/drakkan/sftpgo/v2/internal/logger"
  27. "github.com/drakkan/sftpgo/v2/internal/util"
  28. )
  29. // TLSKeyPair defines the paths for a TLS key pair
  30. type TLSKeyPair struct {
  31. Cert string `json:"cert" mapstructure:"cert"`
  32. Key string `json:"key" mapstructure:"key"`
  33. }
  34. // Header defines an HTTP header.
  35. // If the URL is not empty, the header is added only if the
  36. // requested URL starts with the one specified
  37. type Header struct {
  38. Key string `json:"key" mapstructure:"key"`
  39. Value string `json:"value" mapstructure:"value"`
  40. URL string `json:"url" mapstructure:"url"`
  41. }
  42. // Config defines the configuration for HTTP clients.
  43. // HTTP clients are used for executing hooks such as the ones used for
  44. // custom actions, external authentication and pre-login user modifications
  45. type Config struct {
  46. // Timeout specifies a time limit, in seconds, for a request
  47. Timeout float64 `json:"timeout" mapstructure:"timeout"`
  48. // RetryWaitMin defines the minimum waiting time between attempts in seconds
  49. RetryWaitMin int `json:"retry_wait_min" mapstructure:"retry_wait_min"`
  50. // RetryWaitMax defines the minimum waiting time between attempts in seconds
  51. RetryWaitMax int `json:"retry_wait_max" mapstructure:"retry_wait_max"`
  52. // RetryMax defines the maximum number of attempts
  53. RetryMax int `json:"retry_max" mapstructure:"retry_max"`
  54. // CACertificates defines extra CA certificates to trust.
  55. // The paths can be absolute or relative to the config dir.
  56. // Adding trusted CA certificates is a convenient way to use self-signed
  57. // certificates without defeating the purpose of using TLS
  58. CACertificates []string `json:"ca_certificates" mapstructure:"ca_certificates"`
  59. // Certificates defines the certificates to use for mutual TLS
  60. Certificates []TLSKeyPair `json:"certificates" mapstructure:"certificates"`
  61. // if enabled the HTTP client accepts any TLS certificate presented by
  62. // the server and any host name in that certificate.
  63. // In this mode, TLS is susceptible to man-in-the-middle attacks.
  64. // This should be used only for testing.
  65. SkipTLSVerify bool `json:"skip_tls_verify" mapstructure:"skip_tls_verify"`
  66. // Headers defines a list of http headers to add to each request
  67. Headers []Header `json:"headers" mapstructure:"headers"`
  68. customTransport *http.Transport
  69. tlsConfig *tls.Config
  70. }
  71. const logSender = "httpclient"
  72. var httpConfig Config
  73. // Initialize configures HTTP clients
  74. func (c *Config) Initialize(configDir string) error {
  75. rootCAs, err := c.loadCACerts(configDir)
  76. if err != nil {
  77. return err
  78. }
  79. customTransport := http.DefaultTransport.(*http.Transport).Clone()
  80. if customTransport.TLSClientConfig != nil {
  81. customTransport.TLSClientConfig.RootCAs = rootCAs
  82. } else {
  83. customTransport.TLSClientConfig = &tls.Config{
  84. RootCAs: rootCAs,
  85. NextProtos: []string{"h2", "http/1.1"},
  86. }
  87. }
  88. customTransport.TLSClientConfig.InsecureSkipVerify = c.SkipTLSVerify
  89. c.customTransport = customTransport
  90. c.tlsConfig = customTransport.TLSClientConfig
  91. err = c.loadCertificates(configDir)
  92. if err != nil {
  93. return err
  94. }
  95. var headers []Header
  96. for _, h := range c.Headers {
  97. if h.Key != "" && h.Value != "" {
  98. headers = append(headers, h)
  99. }
  100. }
  101. c.Headers = headers
  102. httpConfig = *c
  103. return nil
  104. }
  105. // loadCACerts returns system cert pools and try to add the configured
  106. // CA certificates to it
  107. func (c *Config) loadCACerts(configDir string) (*x509.CertPool, error) {
  108. if len(c.CACertificates) == 0 {
  109. return nil, nil
  110. }
  111. rootCAs, err := x509.SystemCertPool()
  112. if err != nil {
  113. rootCAs = x509.NewCertPool()
  114. }
  115. for _, ca := range c.CACertificates {
  116. if !util.IsFileInputValid(ca) {
  117. return nil, fmt.Errorf("unable to load invalid CA certificate: %#v", ca)
  118. }
  119. if !filepath.IsAbs(ca) {
  120. ca = filepath.Join(configDir, ca)
  121. }
  122. certs, err := os.ReadFile(ca)
  123. if err != nil {
  124. return nil, fmt.Errorf("unable to load CA certificate: %v", err)
  125. }
  126. if rootCAs.AppendCertsFromPEM(certs) {
  127. logger.Debug(logSender, "", "CA certificate %#v added to the trusted certificates", ca)
  128. } else {
  129. return nil, fmt.Errorf("unable to add CA certificate %#v to the trusted cetificates", ca)
  130. }
  131. }
  132. return rootCAs, nil
  133. }
  134. func (c *Config) loadCertificates(configDir string) error {
  135. if len(c.Certificates) == 0 {
  136. return nil
  137. }
  138. for _, keyPair := range c.Certificates {
  139. cert := keyPair.Cert
  140. key := keyPair.Key
  141. if !util.IsFileInputValid(cert) {
  142. return fmt.Errorf("unable to load invalid certificate: %#v", cert)
  143. }
  144. if !util.IsFileInputValid(key) {
  145. return fmt.Errorf("unable to load invalid key: %#v", key)
  146. }
  147. if !filepath.IsAbs(cert) {
  148. cert = filepath.Join(configDir, cert)
  149. }
  150. if !filepath.IsAbs(key) {
  151. key = filepath.Join(configDir, key)
  152. }
  153. tlsCert, err := tls.LoadX509KeyPair(cert, key)
  154. if err != nil {
  155. return fmt.Errorf("unable to load key pair %#v, %#v: %v", cert, key, err)
  156. }
  157. logger.Debug(logSender, "", "client certificate %#v and key %#v successfully loaded", cert, key)
  158. c.tlsConfig.Certificates = append(c.tlsConfig.Certificates, tlsCert)
  159. }
  160. return nil
  161. }
  162. // GetHTTPClient returns an HTTP client with the configured parameters
  163. func GetHTTPClient() *http.Client {
  164. return &http.Client{
  165. Timeout: time.Duration(httpConfig.Timeout * float64(time.Second)),
  166. Transport: httpConfig.customTransport,
  167. }
  168. }
  169. // GetRetraybleHTTPClient returns an HTTP client that retry a request on error.
  170. // It uses the configured retry parameters
  171. func GetRetraybleHTTPClient() *retryablehttp.Client {
  172. client := retryablehttp.NewClient()
  173. client.HTTPClient.Timeout = time.Duration(httpConfig.Timeout * float64(time.Second))
  174. client.HTTPClient.Transport.(*http.Transport).TLSClientConfig = httpConfig.tlsConfig
  175. client.Logger = &logger.LeveledLogger{Sender: "RetryableHTTPClient"}
  176. client.RetryWaitMin = time.Duration(httpConfig.RetryWaitMin) * time.Second
  177. client.RetryWaitMax = time.Duration(httpConfig.RetryWaitMax) * time.Second
  178. client.RetryMax = httpConfig.RetryMax
  179. return client
  180. }
  181. // Get issues a GET to the specified URL
  182. func Get(url string) (*http.Response, error) {
  183. req, err := http.NewRequest(http.MethodGet, url, nil)
  184. if err != nil {
  185. return nil, err
  186. }
  187. addHeaders(req, url)
  188. client := GetHTTPClient()
  189. defer client.CloseIdleConnections()
  190. return client.Do(req)
  191. }
  192. // Post issues a POST to the specified URL
  193. func Post(url string, contentType string, body io.Reader) (*http.Response, error) {
  194. req, err := http.NewRequest(http.MethodPost, url, body)
  195. if err != nil {
  196. return nil, err
  197. }
  198. req.Header.Set("Content-Type", contentType)
  199. addHeaders(req, url)
  200. client := GetHTTPClient()
  201. defer client.CloseIdleConnections()
  202. return client.Do(req)
  203. }
  204. // RetryableGet issues a GET to the specified URL using the retryable client
  205. func RetryableGet(url string) (*http.Response, error) {
  206. req, err := retryablehttp.NewRequest(http.MethodGet, url, nil)
  207. if err != nil {
  208. return nil, err
  209. }
  210. addHeadersToRetryableReq(req, url)
  211. client := GetRetraybleHTTPClient()
  212. defer client.HTTPClient.CloseIdleConnections()
  213. return client.Do(req)
  214. }
  215. // RetryablePost issues a POST to the specified URL using the retryable client
  216. func RetryablePost(url string, contentType string, body io.Reader) (*http.Response, error) {
  217. req, err := retryablehttp.NewRequest(http.MethodPost, url, body)
  218. if err != nil {
  219. return nil, err
  220. }
  221. req.Header.Set("Content-Type", contentType)
  222. addHeadersToRetryableReq(req, url)
  223. client := GetRetraybleHTTPClient()
  224. defer client.HTTPClient.CloseIdleConnections()
  225. return client.Do(req)
  226. }
  227. func addHeaders(req *http.Request, url string) {
  228. for idx := range httpConfig.Headers {
  229. h := &httpConfig.Headers[idx]
  230. if h.URL == "" || strings.HasPrefix(url, h.URL) {
  231. req.Header.Set(h.Key, h.Value)
  232. }
  233. }
  234. }
  235. func addHeadersToRetryableReq(req *retryablehttp.Request, url string) {
  236. for idx := range httpConfig.Headers {
  237. h := &httpConfig.Headers[idx]
  238. if h.URL == "" || strings.HasPrefix(url, h.URL) {
  239. req.Header.Set(h.Key, h.Value)
  240. }
  241. }
  242. }