/
client.go
132 lines (110 loc) · 3.3 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
package peplink
import (
"context"
"fmt"
"log/slog"
"strconv"
"time"
"github.com/go-resty/resty/v2"
)
// Client for the https://www.peplink.com/ic2-api-doc
type Client struct {
httpClient *resty.Client
log *slog.Logger
}
// NewClient creates a new Peplink Client and authenticates against the API
// Runs token update process in the background
func NewClient(ctx context.Context, opts ...Option) (*Client, error) {
options := &options{
timeout: 10 * time.Second,
httpBasicEndpoint: "http://127.0.0.1:8080",
snmpAddress: "127.0.0.1:161",
snmpCommunity: "public",
}
for _, o := range opts {
o(options)
}
rest := resty.New().
SetBaseURL(options.httpBasicEndpoint).
SetHeader("Content-Type", "application/json").
SetHeader("Accept", "application/json").
SetTimeout(options.timeout)
c := &Client{
httpClient: rest,
log: slog.Default(),
}
ttl, err := c.authenticate(context.Background(), options.httpClientID, options.httpClientSecret)
if err != nil {
c.log.Error("Failed to authenticate", "error", err)
return nil, fmt.Errorf("failed to authenticate: %w", err)
}
go func() {
time.Sleep(ttl - 10*time.Minute)
err := c.watchToken(ctx, options.httpClientID, options.httpClientSecret)
if err != nil {
c.log.Error("Failed to watch token", "error", err)
}
}()
return c, nil
}
func (c *Client) watchToken(ctx context.Context, clientID, clientSecret string) error {
c.log.Info("Peplink token refresh goroutine started")
defer c.log.Info("Peplink token refresh goroutine stopped")
ttl, err := c.authenticate(ctx, clientID, clientSecret)
if err != nil {
return fmt.Errorf("failed to update token: %w", err)
}
if ttl < 10*time.Minute {
return fmt.Errorf("token TTL is too short: %s", fmt.Sprint(ttl))
}
for {
select {
case <-ctx.Done():
return nil
case <-time.After(ttl - 10*time.Minute):
ttl, err = c.authenticate(ctx, clientID, clientSecret)
if err != nil {
return fmt.Errorf("failed to update token: %w", err)
}
}
}
}
func (c *Client) authenticate(ctx context.Context, clientID, clientSecret string) (time.Duration, error) {
type tokenRequest struct {
ClientID string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
Scope string `json:"scope"`
}
type tokenResponse struct {
Stat string `json:"stat"`
Response struct {
// The access token string for API call
AccessToken string `json:"accessToken"`
// Expiration time in seconds
ExpiresIn string `json:"expiresIn"`
} `json:"response"`
}
resp := &tokenResponse{}
rr, err := c.httpClient.NewRequest().SetBody(tokenRequest{
ClientID: clientID,
ClientSecret: clientSecret,
Scope: "api",
}).
SetResult(resp).
SetContext(ctx).
Post("/api/auth.token.grant")
if err != nil {
return 0, fmt.Errorf("failed to authenticate: %w", err)
}
if resp.Stat != "ok" {
return 0, fmt.Errorf("failed to authenticate: stat='%s' body='%s'", resp.Stat, rr.Body())
}
ttlInt, err := strconv.Atoi(resp.Response.ExpiresIn)
if err != nil {
return 0, fmt.Errorf("unexpeted 'ExpiresIn': %w", err)
}
ttl := time.Duration(ttlInt) * time.Second
c.log.Info("Authenticated against Peplink API", "status", rr.Status(), "TTL", fmt.Sprint(ttl))
c.httpClient.SetPathParam("accessToken", resp.Response.AccessToken)
return ttl, nil
}