client.go raw

   1  package gotransip
   2  
   3  import (
   4  	"errors"
   5  	"fmt"
   6  	"io"
   7  	"net/http"
   8  	"os"
   9  
  10  	"github.com/transip/gotransip/v6/authenticator"
  11  	"github.com/transip/gotransip/v6/jwt"
  12  	"github.com/transip/gotransip/v6/repository"
  13  	"github.com/transip/gotransip/v6/rest"
  14  )
  15  
  16  // client manages communication with the TransIP API
  17  // In most cases there should be only one, shared, client.
  18  type client struct {
  19  	// client configuration file, allows you to:
  20  	// - setting a custom useragent
  21  	// - enable test mode
  22  	// - use the demo token
  23  	// - enable debugging
  24  	config ClientConfiguration
  25  	// authenticator wraps all authentication logic
  26  	// - checking if the token is not expired yet
  27  	// - creating an authentication request
  28  	// - requesting and setting a new token
  29  	authenticator *authenticator.Authenticator
  30  }
  31  
  32  // httpBodyLimit provides a maximum byte limit around the http body reader.
  33  // If a request somehow ends up having a huge response body you load all of that data into memory.
  34  // We do not expect to hit this extreme high number even when serving things like PDFs
  35  const httpBodyLimit = 1024 * 1024 * 4
  36  
  37  // NewClient creates a new API client.
  38  // optionally you could put a custom http.client in the configuration struct
  39  // to allow for advanced features such as caching.
  40  func NewClient(config ClientConfiguration) (repository.Client, error) {
  41  	return newClient(config)
  42  }
  43  
  44  // newClient method is used internally for testing,
  45  // the NewClient method is exported as it follows the repository.Client interface
  46  // which is so that we don't have to bind to this specific implementation
  47  func newClient(config ClientConfiguration) (*client, error) {
  48  	if config.HTTPClient == nil {
  49  		config.HTTPClient = http.DefaultClient
  50  	}
  51  	var privateKeyBody []byte
  52  	var token jwt.Token
  53  
  54  	// check account name
  55  	if len(config.AccountName) == 0 && len(config.Token) == 0 {
  56  		return &client{}, errors.New("AccountName is required")
  57  	}
  58  
  59  	// if a private key path is specified and a private key reader is not we
  60  	// fill the private key reader with a opened file on the given PrivateKeyPath
  61  	if len(config.PrivateKeyPath) > 0 && config.PrivateKeyReader == nil {
  62  		privateKeyFile, err := os.Open(config.PrivateKeyPath)
  63  		config.PrivateKeyReader = privateKeyFile
  64  
  65  		if err != nil {
  66  			return &client{}, fmt.Errorf("error while opening private key file: %w", err)
  67  		}
  68  	}
  69  
  70  	// check if token or private key is set
  71  	if len(config.Token) == 0 && config.PrivateKeyReader == nil {
  72  		return &client{}, errors.New("PrivateKeyReader, token or PrivateKeyReader is required")
  73  	}
  74  
  75  	if config.PrivateKeyReader != nil {
  76  		var err error
  77  		privateKeyBody, err = io.ReadAll(config.PrivateKeyReader)
  78  
  79  		if err != nil {
  80  			return &client{}, fmt.Errorf("error while reading private key: %w", err)
  81  		}
  82  	}
  83  
  84  	if len(config.Token) > 0 {
  85  		var err error
  86  		token, err = jwt.New(config.Token)
  87  
  88  		if err != nil {
  89  			return &client{}, err
  90  		}
  91  	}
  92  
  93  	// default to APIMode read/write
  94  	if len(config.Mode) == 0 {
  95  		config.Mode = APIModeReadWrite
  96  	}
  97  
  98  	// set defaultBasePath by default
  99  	if len(config.URL) == 0 {
 100  		config.URL = defaultBasePath
 101  	}
 102  
 103  	return &client{
 104  		authenticator: &authenticator.Authenticator{
 105  			Login:           config.AccountName,
 106  			PrivateKeyBody:  privateKeyBody,
 107  			Token:           token,
 108  			HTTPClient:      config.HTTPClient,
 109  			TokenCache:      config.TokenCache,
 110  			BasePath:        config.URL,
 111  			ReadOnly:        config.Mode == APIModeReadOnly,
 112  			TokenExpiration: config.TokenExpiration,
 113  			Whitelisted:     config.TokenWhitelisted,
 114  		},
 115  		config: config,
 116  	}, nil
 117  }
 118  
 119  // This method is used by all rest client methods, thus: 'get','post','put','delete'
 120  // It uses the authenticator to get a token, either statically provided by the user or requested from the authentication server
 121  // Then decodes the json response to a supplied interface
 122  func (c *client) call(method rest.Method, request rest.Request, result any) (rest.Response, error) {
 123  	token, err := c.authenticator.GetToken()
 124  	if err != nil {
 125  		return rest.Response{}, fmt.Errorf("could not get token from authenticator: %w", err)
 126  	}
 127  
 128  	// if test mode is enabled we always want to change rest requests to add a HTTP test=1 query string
 129  	// to a HTTP request
 130  	if c.config.TestMode {
 131  		request.TestMode = true
 132  	}
 133  
 134  	httpRequest, err := request.GetHTTPRequest(c.config.URL, method.Method)
 135  	if err != nil {
 136  		return rest.Response{}, fmt.Errorf("error during request creation: %w", err)
 137  	}
 138  
 139  	httpRequest.Header.Add("Authorization", token.GetAuthenticationHeaderValue())
 140  	httpRequest.Header.Set("User-Agent", userAgent)
 141  	client := c.config.HTTPClient
 142  	httpResponse, err := client.Do(httpRequest)
 143  	if err != nil {
 144  		return rest.Response{}, fmt.Errorf("request error: %w", err)
 145  	}
 146  
 147  	defer httpResponse.Body.Close()
 148  
 149  	bodyReader := io.LimitReader(httpResponse.Body, httpBodyLimit)
 150  
 151  	// read entire httpResponse body
 152  	b, err := io.ReadAll(bodyReader)
 153  	if err != nil {
 154  		return rest.Response{}, fmt.Errorf("error reading http response body: %w", err)
 155  	}
 156  
 157  	contentLocation := httpResponse.Header.Get("Content-Location")
 158  
 159  	restResponse := rest.Response{
 160  		Body:            b,
 161  		StatusCode:      httpResponse.StatusCode,
 162  		Method:          method,
 163  		ContentLocation: contentLocation,
 164  	}
 165  
 166  	err = restResponse.ParseResponse(result)
 167  
 168  	return restResponse, err
 169  }
 170  
 171  // ChangeBasePath changes base path to allow switching to mocks
 172  func (c *client) ChangeBasePath(path string) {
 173  	c.config.URL = path
 174  }
 175  
 176  // Allow modification of underlying config for alternate implementations and testing
 177  // Caution: modifying the configuration while live can cause data races and potentially unwanted behavior
 178  func (c *client) GetConfig() ClientConfiguration {
 179  	return c.config
 180  }
 181  
 182  // Allow modification of underlying config for alternate implementations and testing
 183  // Caution: modifying the configuration while live can cause data races and potentially unwanted behavior
 184  func (c *client) GetAuthenticator() *authenticator.Authenticator {
 185  	return c.authenticator
 186  }
 187  
 188  // This method will create and execute a http Get request
 189  func (c *client) Get(request rest.Request, responseObject interface{}) error {
 190  	_, err := c.call(rest.GetMethod, request, responseObject)
 191  	return err
 192  }
 193  
 194  // This method will create and execute a http Post request
 195  // It expects no response, that is why it does not ask for a responseObject
 196  func (c *client) Post(request rest.Request) error {
 197  	var response any
 198  	_, err := c.call(rest.PostMethod, request, &response)
 199  	return err
 200  }
 201  
 202  // This method will create and execute a http Post request
 203  // It expects a response
 204  func (c *client) PostWithResponse(request rest.Request) (rest.Response, error) {
 205  	var response any
 206  	return c.call(rest.PostMethod, request, &response)
 207  }
 208  
 209  // This method will create and execute a http Put request
 210  // It expects no response, that is why it does not ask for a responseObject
 211  func (c *client) Put(request rest.Request) error {
 212  	var response any
 213  	_, err := c.call(rest.PutMethod, request, &response)
 214  	return err
 215  }
 216  
 217  // This method will create and execute a http Put request
 218  // It expects a response
 219  func (c *client) PutWithResponse(request rest.Request) (rest.Response, error) {
 220  	var response any
 221  	return c.call(rest.PutMethod, request, &response)
 222  }
 223  
 224  // This method will create and execute a http Delete request
 225  // It expects no response, that is why it does not ask for a responseObject
 226  func (c *client) Delete(request rest.Request) error {
 227  	var response any
 228  	_, err := c.call(rest.DeleteMethod, request, &response)
 229  	return err
 230  }
 231  
 232  // This method will create and execute a http Patch request
 233  // It expects no response, that is why it does not ask for a responseObject
 234  func (c *client) Patch(request rest.Request) error {
 235  	var response any
 236  	_, err := c.call(rest.PatchMethod, request, &response)
 237  	return err
 238  }
 239  
 240  // This method will create and execute a http Patch request
 241  // It expects a response
 242  func (c *client) PatchWithResponse(request rest.Request) (rest.Response, error) {
 243  	var response any
 244  	return c.call(rest.PatchMethod, request, &response)
 245  }
 246