package triton import ( "bytes" "crypto/tls" "encoding/json" "fmt" "io" "log" "net" "net/http" "net/url" "os" "time" "github.com/hashicorp/errwrap" "github.com/hashicorp/go-retryablehttp" "github.com/joyent/triton-go/authentication" ) // Client represents a connection to the Triton API. type Client struct { client *retryablehttp.Client authorizer []authentication.Signer apiURL url.URL accountName string } // NewClient is used to construct a Client in order to make API // requests to the Triton API. // // At least one signer must be provided - example signers include // authentication.PrivateKeySigner and authentication.SSHAgentSigner. func NewClient(endpoint string, accountName string, signers ...authentication.Signer) (*Client, error) { defaultRetryWaitMin := 1 * time.Second defaultRetryWaitMax := 5 * time.Minute defaultRetryMax := 32 apiURL, err := url.Parse(endpoint) if err != nil { return nil, errwrap.Wrapf("invalid endpoint: {{err}}", err) } if accountName == "" { return nil, fmt.Errorf("account name can not be empty") } httpClient := &http.Client{ Transport: httpTransport(false), CheckRedirect: doNotFollowRedirects, } retryableClient := &retryablehttp.Client{ HTTPClient: httpClient, Logger: log.New(os.Stderr, "", log.LstdFlags), RetryWaitMin: defaultRetryWaitMin, RetryWaitMax: defaultRetryWaitMax, RetryMax: defaultRetryMax, CheckRetry: retryablehttp.DefaultRetryPolicy, } return &Client{ client: retryableClient, authorizer: signers, apiURL: *apiURL, accountName: accountName, }, nil } // InsecureSkipTLSVerify turns off TLS verification for the client connection. This // allows connection to an endpoint with a certificate which was signed by a non- // trusted CA, such as self-signed certificates. This can be useful when connecting // to temporary Triton installations such as Triton Cloud-On-A-Laptop. func (c *Client) InsecureSkipTLSVerify() { if c.client == nil { return } c.client.HTTPClient.Transport = httpTransport(true) } func httpTransport(insecureSkipTLSVerify bool) *http.Transport { return &http.Transport{ Proxy: http.ProxyFromEnvironment, Dial: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).Dial, TLSHandshakeTimeout: 10 * time.Second, DisableKeepAlives: true, MaxIdleConnsPerHost: -1, TLSClientConfig: &tls.Config{ InsecureSkipVerify: insecureSkipTLSVerify, }, } } func doNotFollowRedirects(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } func (c *Client) executeRequestURIParams(method, path string, body interface{}, query *url.Values) (io.ReadCloser, error) { var requestBody io.ReadSeeker if body != nil { marshaled, err := json.MarshalIndent(body, "", " ") if err != nil { return nil, err } requestBody = bytes.NewReader(marshaled) } endpoint := c.apiURL endpoint.Path = path if query != nil { endpoint.RawQuery = query.Encode() } req, err := retryablehttp.NewRequest(method, endpoint.String(), requestBody) if err != nil { return nil, errwrap.Wrapf("Error constructing HTTP request: {{err}}", err) } dateHeader := time.Now().UTC().Format(time.RFC1123) req.Header.Set("date", dateHeader) authHeader, err := c.authorizer[0].Sign(dateHeader) if err != nil { return nil, errwrap.Wrapf("Error signing HTTP request: {{err}}", err) } req.Header.Set("Authorization", authHeader) req.Header.Set("Accept", "application/json") req.Header.Set("Accept-Version", "8") req.Header.Set("User-Agent", "triton-go Client API") if body != nil { req.Header.Set("Content-Type", "application/json") } resp, err := c.client.Do(req) if err != nil { return nil, errwrap.Wrapf("Error executing HTTP request: {{err}}", err) } if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices { return resp.Body, nil } return nil, c.decodeError(resp.StatusCode, resp.Body) } func (c *Client) decodeError(statusCode int, body io.Reader) error { tritonError := &TritonError{ StatusCode: statusCode, } errorDecoder := json.NewDecoder(body) if err := errorDecoder.Decode(tritonError); err != nil { return errwrap.Wrapf("Error decoding error response: {{err}}", err) } return tritonError } func (c *Client) executeRequest(method, path string, body interface{}) (io.ReadCloser, error) { return c.executeRequestURIParams(method, path, body, nil) } func (c *Client) executeRequestRaw(method, path string, body interface{}) (*http.Response, error) { var requestBody io.ReadSeeker if body != nil { marshaled, err := json.MarshalIndent(body, "", " ") if err != nil { return nil, err } requestBody = bytes.NewReader(marshaled) } endpoint := c.apiURL endpoint.Path = path req, err := retryablehttp.NewRequest(method, endpoint.String(), requestBody) if err != nil { return nil, errwrap.Wrapf("Error constructing HTTP request: {{err}}", err) } dateHeader := time.Now().UTC().Format(time.RFC1123) req.Header.Set("date", dateHeader) authHeader, err := c.authorizer[0].Sign(dateHeader) if err != nil { return nil, errwrap.Wrapf("Error signing HTTP request: {{err}}", err) } req.Header.Set("Authorization", authHeader) req.Header.Set("Accept", "application/json") req.Header.Set("Accept-Version", "8") req.Header.Set("User-Agent", "triton-go c API") if body != nil { req.Header.Set("Content-Type", "application/json") } resp, err := c.client.Do(req) if err != nil { return nil, errwrap.Wrapf("Error executing HTTP request: {{err}}", err) } return resp, nil }