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