-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathresin.go
297 lines (270 loc) · 7.28 KB
/
resin.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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
package resingo
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"time"
jwt "github.com/dgrijalva/jwt-go"
)
//AuthType is the authentication type that is used to authenticate with a
//resin.io api.
type AuthType int
// supported authentication types
const (
Credentials AuthType = iota
AuthToken
)
const (
apiEndpoint = "https://api.resin.io"
)
//APIVersion is the version of resin API
type APIVersion int
// supported resin API versions
const (
VersionOne APIVersion = iota
VersionTwo
VersionThree
)
func (v APIVersion) String() string {
switch v {
case VersionOne:
return "v1"
case VersionTwo:
return "v2"
case VersionThree:
return "v3"
}
return ""
}
//ErrUnkownAuthType error returned when the type of authentication is not
//supported.
var ErrUnkownAuthType = errors.New("resingo: unknown authentication type")
//ErrMissingCredentials error returned when either username or password is
//missing
var ErrMissingCredentials = errors.New("resingo: missing credentials( username or password)")
//ErrBadToken error returned when the resin session token is bad.
var ErrBadToken = errors.New("resingo: bad session token")
//HTTPClient is an interface for a http clinet that is used to communicate with
//the resin API
type HTTPClient interface {
Do(*http.Request) (*http.Response, error)
Post(url string, bodyTyp string, body io.Reader) (*http.Response, error)
}
//Context holds information necessary to make a call to the resin API
type Context struct {
Client HTTPClient
Config *Config
}
//Config is the configuration object for the Client
type Config struct {
AuthToken string
Username string
Password string
APIKey string
tokenClain *TokenClain
ResinEndpoint string
ResinVersion APIVersion
}
//TokenClain are the values that are encoded into a session token from resin.io.
//
// It embeds jst.StandardClaims, so as to help with Verification of expired
// data. Resin doens't do claim verification :(.
type TokenClain struct {
Username string `json:"username"`
UserID int64 `json:"id"`
Email string `json:"email"`
jwt.StandardClaims
}
// formats a proper url forthe API call. The format is
// /<base_url>/<api_version>/<api_endpoin. The endpoint can be en empty string.
//
//This assumes that the endpoint doesn't start with /
// TODO: handle endpoint that starts with / and base url that ends with /
func apiURL(base string, version APIVersion, endpoint string) string {
return fmt.Sprintf("%s/%s/%s", base, version, endpoint)
}
//APIEndpoint returns a url that points to the given endpoint. This adds the
//resin.io api host and version.
func (c *Config) APIEndpoint(endpoint string) string {
return apiURL(c.ResinEndpoint, c.ResinVersion, endpoint)
}
//IsValidToken return true if the token tok is a valid resin session token.
//
// This method ecodes the token. A token that can't be doced is bad token. Any
// token that has expired is also a bad token.
func (c *Config) IsValidToken(tok string) bool {
return ValidToken(tok)
}
//ValidToken return true if tok is avalid token
func ValidToken(tok string) bool {
tk, err := ParseToken(tok)
if err != nil {
return false
}
return tk.StandardClaims.ExpiresAt > time.Now().Unix()
}
//UserID returns the user id.
func (c *Config) UserID() int64 {
return c.tokenClain.UserID
}
func authHeader(token string) http.Header {
h := make(http.Header)
h.Add("Authorization", "Bearer "+token)
return h
}
//SaveToken saves token, to the current Configuration object.
func (c *Config) SaveToken(tok string) error {
tk, err := ParseToken(tok)
if err != nil {
return err
}
c.tokenClain = tk
c.AuthToken = tok
return nil
}
//ParseToken parses the given token and extracts the claims emcode into it. This
//function uses JWT method to parse the token, with verification of claims
//turned off.
func ParseToken(tok string) (*TokenClain, error) {
p := jwt.Parser{
SkipClaimsValidation: true,
}
tk, _ := p.ParseWithClaims(tok, &TokenClain{}, func(token *jwt.Token) (interface{}, error) {
return nil, nil
})
claims, ok := tk.Claims.(*TokenClain)
if ok {
return claims, nil
}
return nil, ErrBadToken
}
//Authenticate authenticates the client and returns the Auth token. See Login if
//you want to save the token in the client. This function does not save the
//authentication token and user detals.
func Authenticate(ctx *Context, typ AuthType, authToken ...string) (string, error) {
loginURL := apiEndpoint + "/login_"
switch typ {
case Credentials:
// Absence of either username or password result in missing creadentials
// error.
if ctx.Config.Username == "" || ctx.Config.Password == "" {
return "", ErrMissingCredentials
}
form := url.Values{}
form.Add("username", ctx.Config.Username)
form.Add("password", ctx.Config.Password)
res, err := ctx.Client.Post(loginURL,
"application/x-www-form-urlencoded",
strings.NewReader(form.Encode()))
if err != nil {
return "", err
}
defer func() {
_ = res.Body.Close()
}()
data, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
return string(data), nil
case AuthToken:
if len(authToken) > 0 {
tk := authToken[0]
if ctx.Config.IsValidToken(tk) {
return tk, nil
}
return "", ErrBadToken
}
return "", errors.New("resingo: Failed to authenticate missing authToken")
}
return "", ErrUnkownAuthType
}
//Login authenticates the contextand stores the session token. This function
//checks the validity of the session token before saving it.
//
// The call to ctx.IsLoged() should return true if the returned error is nil.
func Login(ctx *Context, authTyp AuthType, authToken ...string) error {
tok, err := Authenticate(ctx, authTyp, authToken...)
if err != nil {
return err
}
if ctx.Config.IsValidToken(tok) {
return ctx.Config.SaveToken(tok)
}
return errors.New("resingo: Failed to login")
}
//Encode encode properly the request params for use with resin API.
//
// Encode tartegts the filter param, which for some reasom(based on OData) is
// supposed to be $filter and not filter. The value specified by the eq param
// key is combined with the value from the fileter key to produce the $filter
// value string.
//
// Any other url params are encoded by the default encoder from
// url.Values.Encoder.
//TODO: check a better way to encode OData url params.
func Encode(q url.Values) string {
if q == nil {
return ""
}
var buf bytes.Buffer
var keys []string
for k := range q {
keys = append(keys, k)
}
for _, k := range keys {
switch k {
case "filter":
if buf.Len() != 0 {
_, _ = buf.WriteRune('&')
}
v := q.Get("filter")
_, _ = buf.WriteString("$filter=" + v)
for _, fk := range keys {
switch fk {
case "eq":
fv := "%20" + fk + "%20" + quote(q.Get(fk))
_, _ = buf.WriteString(fv)
q.Del(fk)
}
}
q.Del(k)
case "expand":
if buf.Len() != 0 {
_, _ = buf.WriteRune('&')
}
v := q.Get("expand")
_, _ = buf.WriteString("$expand=" + v)
q.Del(k)
}
}
e := q.Encode()
if e != "" {
if buf.Len() != 0 {
_, _ = buf.WriteRune('&')
}
_, _ = buf.WriteString(e)
}
return buf.String()
}
func quote(v string) string {
ok, _ := strconv.ParseBool(v)
if ok {
return v
}
_, err := strconv.Atoi(v)
if err == nil {
return v
}
_, err = strconv.ParseFloat(v, 64)
if err == nil {
return v
}
return "'" + v + "'"
}