portainer/portainer

View on GitHub
api/http/client/client.go

Summary

Maintainability
A
0 mins
Test Coverage
package client

import (
    "crypto/tls"
    "errors"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "strings"
    "time"

    portainer "github.com/portainer/portainer/api"

    "github.com/rs/zerolog/log"
    "github.com/segmentio/encoding/json"
)

var errInvalidResponseStatus = errors.New("invalid response status (expecting 200)")

const defaultHTTPTimeout = 5

// HTTPClient represents a client to send HTTP requests.
type HTTPClient struct {
    *http.Client
}

// NewHTTPClient is used to build a new HTTPClient.
func NewHTTPClient() *HTTPClient {
    return &HTTPClient{
        &http.Client{
            Timeout: time.Second * time.Duration(defaultHTTPTimeout),
        },
    }
}

// AzureAuthenticationResponse represents an Azure API authentication response.
type AzureAuthenticationResponse struct {
    AccessToken string `json:"access_token"`
    ExpiresOn   string `json:"expires_on"`
}

// ExecuteAzureAuthenticationRequest is used to execute an authentication request
// against the Azure API. It re-uses the same http.Client.
func (client *HTTPClient) ExecuteAzureAuthenticationRequest(credentials *portainer.AzureCredentials) (*AzureAuthenticationResponse, error) {
    loginURL := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/token", credentials.TenantID)
    params := url.Values{
        "grant_type":    {"client_credentials"},
        "client_id":     {credentials.ApplicationID},
        "client_secret": {credentials.AuthenticationKey},
        "resource":      {"https://management.azure.com/"},
    }

    resp, err := client.PostForm(loginURL, params)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, errors.New("invalid Azure credentials")
    }

    var token AzureAuthenticationResponse
    err = json.NewDecoder(resp.Body).Decode(&token)
    if err != nil {
        return nil, err
    }

    return &token, nil
}

// Get executes a simple HTTP GET to the specified URL and returns
// the content of the response body. Timeout can be specified via the timeout parameter,
// will default to defaultHTTPTimeout if set to 0.
func Get(url string, timeout int) ([]byte, error) {
    if timeout == 0 {
        timeout = defaultHTTPTimeout
    }

    client := &http.Client{
        Timeout: time.Second * time.Duration(timeout),
    }

    response, err := client.Get(url)
    if err != nil {
        return nil, err
    }
    defer response.Body.Close()

    if response.StatusCode != http.StatusOK {
        log.Error().Int("status_code", response.StatusCode).Msg("unexpected status code")

        return nil, errInvalidResponseStatus
    }

    body, err := io.ReadAll(response.Body)
    if err != nil {
        return nil, err
    }

    return body, nil
}

// ExecutePingOperation will send a SystemPing operation HTTP request to a Docker environment(endpoint)
// using the specified host and optional TLS configuration.
// It uses a new Http.Client for each operation.
func ExecutePingOperation(host string, tlsConfig *tls.Config) (bool, error) {
    transport := &http.Transport{}

    scheme := "http"
    if tlsConfig != nil {
        transport.TLSClientConfig = tlsConfig
        scheme = "https"
    }

    client := &http.Client{
        Timeout:   time.Second * 3,
        Transport: transport,
    }

    target := strings.Replace(host, "tcp://", scheme+"://", 1)
    return pingOperation(client, target)
}

func pingOperation(client *http.Client, target string) (bool, error) {
    pingOperationURL := target + "/_ping"

    resp, err := client.Get(pingOperationURL)
    if err != nil {
        return false, err
    }

    io.Copy(io.Discard, resp.Body)
    resp.Body.Close()

    agentOnDockerEnvironment := false
    if resp.Header.Get(portainer.PortainerAgentHeader) != "" {
        agentOnDockerEnvironment = true
    }

    return agentOnDockerEnvironment, nil
}