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