2020-04-26 21:29:09 +00:00
|
|
|
package httpclient
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/tls"
|
|
|
|
"crypto/x509"
|
2021-02-13 13:41:37 +00:00
|
|
|
"fmt"
|
2021-05-25 06:36:01 +00:00
|
|
|
"io"
|
2020-04-26 21:29:09 +00:00
|
|
|
"net/http"
|
2021-02-25 20:53:04 +00:00
|
|
|
"os"
|
2020-04-26 21:29:09 +00:00
|
|
|
"path/filepath"
|
2021-05-25 06:36:01 +00:00
|
|
|
"strings"
|
2020-04-26 21:29:09 +00:00
|
|
|
"time"
|
|
|
|
|
2021-02-12 20:42:49 +00:00
|
|
|
"github.com/hashicorp/go-retryablehttp"
|
|
|
|
|
2021-06-26 05:31:41 +00:00
|
|
|
"github.com/drakkan/sftpgo/v2/logger"
|
2021-07-11 13:26:51 +00:00
|
|
|
"github.com/drakkan/sftpgo/v2/util"
|
2020-04-26 21:29:09 +00:00
|
|
|
)
|
|
|
|
|
2021-02-13 13:41:37 +00:00
|
|
|
// TLSKeyPair defines the paths for a TLS key pair
|
|
|
|
type TLSKeyPair struct {
|
|
|
|
Cert string `json:"cert" mapstructure:"cert"`
|
|
|
|
Key string `json:"key" mapstructure:"key"`
|
|
|
|
}
|
|
|
|
|
2021-05-25 06:36:01 +00:00
|
|
|
// Header defines an HTTP header.
|
|
|
|
// If the URL is not empty, the header is added only if the
|
|
|
|
// requested URL starts with the one specified
|
|
|
|
type Header struct {
|
|
|
|
Key string `json:"key" mapstructure:"key"`
|
|
|
|
Value string `json:"value" mapstructure:"value"`
|
|
|
|
URL string `json:"url" mapstructure:"url"`
|
|
|
|
}
|
|
|
|
|
2020-04-26 21:29:09 +00:00
|
|
|
// Config defines the configuration for HTTP clients.
|
|
|
|
// HTTP clients are used for executing hooks such as the ones used for
|
|
|
|
// custom actions, external authentication and pre-login user modifications
|
|
|
|
type Config struct {
|
2021-02-12 20:42:49 +00:00
|
|
|
// Timeout specifies a time limit, in seconds, for a request
|
2021-05-16 10:50:06 +00:00
|
|
|
Timeout float64 `json:"timeout" mapstructure:"timeout"`
|
2021-02-12 20:42:49 +00:00
|
|
|
// RetryWaitMin defines the minimum waiting time between attempts in seconds
|
|
|
|
RetryWaitMin int `json:"retry_wait_min" mapstructure:"retry_wait_min"`
|
|
|
|
// RetryWaitMax defines the minimum waiting time between attempts in seconds
|
|
|
|
RetryWaitMax int `json:"retry_wait_max" mapstructure:"retry_wait_max"`
|
|
|
|
// RetryMax defines the maximum number of attempts
|
|
|
|
RetryMax int `json:"retry_max" mapstructure:"retry_max"`
|
2020-04-26 21:29:09 +00:00
|
|
|
// CACertificates defines extra CA certificates to trust.
|
|
|
|
// The paths can be absolute or relative to the config dir.
|
|
|
|
// Adding trusted CA certificates is a convenient way to use self-signed
|
|
|
|
// certificates without defeating the purpose of using TLS
|
2020-05-03 09:37:50 +00:00
|
|
|
CACertificates []string `json:"ca_certificates" mapstructure:"ca_certificates"`
|
2021-02-13 13:41:37 +00:00
|
|
|
// Certificates defines the certificates to use for mutual TLS
|
|
|
|
Certificates []TLSKeyPair `json:"certificates" mapstructure:"certificates"`
|
2020-05-03 09:37:50 +00:00
|
|
|
// if enabled the HTTP client accepts any TLS certificate presented by
|
|
|
|
// the server and any host name in that certificate.
|
|
|
|
// In this mode, TLS is susceptible to man-in-the-middle attacks.
|
|
|
|
// This should be used only for testing.
|
2021-05-25 06:36:01 +00:00
|
|
|
SkipTLSVerify bool `json:"skip_tls_verify" mapstructure:"skip_tls_verify"`
|
|
|
|
// Headers defines a list of http headers to add to each request
|
|
|
|
Headers []Header `json:"headers" mapstructure:"headers"`
|
2020-04-26 21:29:09 +00:00
|
|
|
customTransport *http.Transport
|
2021-02-12 20:42:49 +00:00
|
|
|
tlsConfig *tls.Config
|
2020-04-26 21:29:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const logSender = "httpclient"
|
|
|
|
|
|
|
|
var httpConfig Config
|
|
|
|
|
|
|
|
// Initialize configures HTTP clients
|
2021-02-13 13:41:37 +00:00
|
|
|
func (c *Config) Initialize(configDir string) error {
|
|
|
|
rootCAs, err := c.loadCACerts(configDir)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-04-26 21:29:09 +00:00
|
|
|
customTransport := http.DefaultTransport.(*http.Transport).Clone()
|
|
|
|
if customTransport.TLSClientConfig != nil {
|
|
|
|
customTransport.TLSClientConfig.RootCAs = rootCAs
|
|
|
|
} else {
|
|
|
|
customTransport.TLSClientConfig = &tls.Config{
|
2021-02-13 13:41:37 +00:00
|
|
|
RootCAs: rootCAs,
|
|
|
|
NextProtos: []string{"h2", "http/1.1"},
|
2020-04-26 21:29:09 +00:00
|
|
|
}
|
|
|
|
}
|
2020-05-03 09:37:50 +00:00
|
|
|
customTransport.TLSClientConfig.InsecureSkipVerify = c.SkipTLSVerify
|
2021-02-13 13:41:37 +00:00
|
|
|
c.customTransport = customTransport
|
|
|
|
c.tlsConfig = customTransport.TLSClientConfig
|
|
|
|
|
|
|
|
err = c.loadCertificates(configDir)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-05-25 06:36:01 +00:00
|
|
|
var headers []Header
|
|
|
|
for _, h := range c.Headers {
|
|
|
|
if h.Key != "" && h.Value != "" {
|
|
|
|
headers = append(headers, h)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
c.Headers = headers
|
2021-02-13 13:41:37 +00:00
|
|
|
httpConfig = *c
|
|
|
|
return nil
|
2020-04-26 21:29:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// loadCACerts returns system cert pools and try to add the configured
|
|
|
|
// CA certificates to it
|
2021-02-13 13:41:37 +00:00
|
|
|
func (c *Config) loadCACerts(configDir string) (*x509.CertPool, error) {
|
2021-03-10 20:45:48 +00:00
|
|
|
if len(c.CACertificates) == 0 {
|
|
|
|
return nil, nil
|
|
|
|
}
|
2020-04-26 21:29:09 +00:00
|
|
|
rootCAs, err := x509.SystemCertPool()
|
|
|
|
if err != nil {
|
|
|
|
rootCAs = x509.NewCertPool()
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, ca := range c.CACertificates {
|
2021-07-11 13:26:51 +00:00
|
|
|
if !util.IsFileInputValid(ca) {
|
2021-02-13 13:41:37 +00:00
|
|
|
return nil, fmt.Errorf("unable to load invalid CA certificate: %#v", ca)
|
2020-04-26 21:29:09 +00:00
|
|
|
}
|
|
|
|
if !filepath.IsAbs(ca) {
|
|
|
|
ca = filepath.Join(configDir, ca)
|
|
|
|
}
|
2021-02-25 20:53:04 +00:00
|
|
|
certs, err := os.ReadFile(ca)
|
2020-04-26 21:29:09 +00:00
|
|
|
if err != nil {
|
2021-02-13 13:41:37 +00:00
|
|
|
return nil, fmt.Errorf("unable to load CA certificate: %v", err)
|
2020-04-26 21:29:09 +00:00
|
|
|
}
|
|
|
|
if rootCAs.AppendCertsFromPEM(certs) {
|
|
|
|
logger.Debug(logSender, "", "CA certificate %#v added to the trusted certificates", ca)
|
|
|
|
} else {
|
2021-02-13 13:41:37 +00:00
|
|
|
return nil, fmt.Errorf("unable to add CA certificate %#v to the trusted cetificates", ca)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return rootCAs, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Config) loadCertificates(configDir string) error {
|
|
|
|
if len(c.Certificates) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, keyPair := range c.Certificates {
|
|
|
|
cert := keyPair.Cert
|
|
|
|
key := keyPair.Key
|
2021-07-11 13:26:51 +00:00
|
|
|
if !util.IsFileInputValid(cert) {
|
2021-02-13 13:41:37 +00:00
|
|
|
return fmt.Errorf("unable to load invalid certificate: %#v", cert)
|
|
|
|
}
|
2021-07-11 13:26:51 +00:00
|
|
|
if !util.IsFileInputValid(key) {
|
2021-02-13 13:41:37 +00:00
|
|
|
return fmt.Errorf("unable to load invalid key: %#v", key)
|
|
|
|
}
|
|
|
|
if !filepath.IsAbs(cert) {
|
|
|
|
cert = filepath.Join(configDir, cert)
|
|
|
|
}
|
|
|
|
if !filepath.IsAbs(key) {
|
|
|
|
key = filepath.Join(configDir, key)
|
|
|
|
}
|
|
|
|
tlsCert, err := tls.LoadX509KeyPair(cert, key)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to load key pair %#v, %#v: %v", cert, key, err)
|
2020-04-26 21:29:09 +00:00
|
|
|
}
|
2021-02-13 13:41:37 +00:00
|
|
|
logger.Debug(logSender, "", "client certificate %#v and key %#v successfully loaded", cert, key)
|
|
|
|
c.tlsConfig.Certificates = append(c.tlsConfig.Certificates, tlsCert)
|
2020-04-26 21:29:09 +00:00
|
|
|
}
|
2021-02-13 13:41:37 +00:00
|
|
|
return nil
|
2020-04-26 21:29:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetHTTPClient returns an HTTP client with the configured parameters
|
|
|
|
func GetHTTPClient() *http.Client {
|
|
|
|
return &http.Client{
|
2021-05-16 10:50:06 +00:00
|
|
|
Timeout: time.Duration(httpConfig.Timeout * float64(time.Second)),
|
2020-04-26 21:29:09 +00:00
|
|
|
Transport: httpConfig.customTransport,
|
|
|
|
}
|
|
|
|
}
|
2021-02-12 20:42:49 +00:00
|
|
|
|
|
|
|
// GetRetraybleHTTPClient returns an HTTP client that retry a request on error.
|
|
|
|
// It uses the configured retry parameters
|
|
|
|
func GetRetraybleHTTPClient() *retryablehttp.Client {
|
|
|
|
client := retryablehttp.NewClient()
|
2021-05-16 10:50:06 +00:00
|
|
|
client.HTTPClient.Timeout = time.Duration(httpConfig.Timeout * float64(time.Second))
|
2021-02-12 20:42:49 +00:00
|
|
|
client.HTTPClient.Transport.(*http.Transport).TLSClientConfig = httpConfig.tlsConfig
|
|
|
|
client.Logger = &logger.LeveledLogger{Sender: "RetryableHTTPClient"}
|
|
|
|
client.RetryWaitMin = time.Duration(httpConfig.RetryWaitMin) * time.Second
|
|
|
|
client.RetryWaitMax = time.Duration(httpConfig.RetryWaitMax) * time.Second
|
|
|
|
client.RetryMax = httpConfig.RetryMax
|
|
|
|
|
|
|
|
return client
|
|
|
|
}
|
2021-05-25 06:36:01 +00:00
|
|
|
|
|
|
|
// Get issues a GET to the specified URL
|
|
|
|
func Get(url string) (*http.Response, error) {
|
|
|
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
addHeaders(req, url)
|
|
|
|
return GetHTTPClient().Do(req)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Post issues a POST to the specified URL
|
|
|
|
func Post(url string, contentType string, body io.Reader) (*http.Response, error) {
|
|
|
|
req, err := http.NewRequest(http.MethodPost, url, body)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
req.Header.Set("Content-Type", contentType)
|
|
|
|
addHeaders(req, url)
|
|
|
|
return GetHTTPClient().Do(req)
|
|
|
|
}
|
|
|
|
|
|
|
|
// RetryableGet issues a GET to the specified URL using the retryable client
|
|
|
|
func RetryableGet(url string) (*http.Response, error) {
|
|
|
|
req, err := retryablehttp.NewRequest(http.MethodGet, url, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
addHeadersToRetryableReq(req, url)
|
|
|
|
return GetRetraybleHTTPClient().Do(req)
|
|
|
|
}
|
|
|
|
|
|
|
|
// RetryablePost issues a POST to the specified URL using the retryable client
|
|
|
|
func RetryablePost(url string, contentType string, body io.Reader) (*http.Response, error) {
|
|
|
|
req, err := retryablehttp.NewRequest(http.MethodPost, url, body)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
req.Header.Set("Content-Type", contentType)
|
|
|
|
addHeadersToRetryableReq(req, url)
|
|
|
|
return GetRetraybleHTTPClient().Do(req)
|
|
|
|
}
|
|
|
|
|
|
|
|
func addHeaders(req *http.Request, url string) {
|
|
|
|
for idx := range httpConfig.Headers {
|
|
|
|
h := &httpConfig.Headers[idx]
|
|
|
|
if h.URL == "" || strings.HasPrefix(url, h.URL) {
|
|
|
|
req.Header.Set(h.Key, h.Value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func addHeadersToRetryableReq(req *retryablehttp.Request, url string) {
|
|
|
|
for idx := range httpConfig.Headers {
|
|
|
|
h := &httpConfig.Headers[idx]
|
|
|
|
if h.URL == "" || strings.HasPrefix(url, h.URL) {
|
|
|
|
req.Header.Set(h.Key, h.Value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|