package client import ( "crypto/tls" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strings" "time" portainer "github.com/portainer/portainer/api" "github.com/rs/zerolog/log" ) 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 }