Line data Source code
1 : package api
2 :
3 : import (
4 : "bytes"
5 : "encoding/json"
6 : "fmt"
7 : "io"
8 : "net/http"
9 : "net/url"
10 : "sync"
11 : )
12 :
13 : // The HTTP client to request Sylvia-IoT APIs. With this client, you do not need to handle 401
14 : // refresh token flow.
15 : type Client struct {
16 : // The underlying HTTP client instance.
17 : client http.Client
18 : // `sylvia-iot-auth` base path.
19 : authBase string
20 : // `sylvia-iot-coremgr` base path.
21 : coremgrBase string
22 : // Client ID.
23 : clientID string
24 : // Client secret.
25 : clientSecret string
26 : // The access token.
27 : accessToken string
28 : // Mutex for access token.
29 : mutex sync.Mutex
30 : }
31 :
32 : // Options of the HTTP client [`Client`] that contains OAuth2 information.
33 : type ClientOptions struct {
34 : // `sylvia-iot-auth` base path with scheme. For example `http://localhost:1080/auth`
35 : AuthBase string
36 : // `sylvia-iot-coremgr` base path with scheme. For example `http://localhost:1080/coremgr`
37 : CoremgrBase string
38 : // Client ID.
39 : ClientID string
40 : // Client secret.
41 : ClientSecret string
42 : }
43 :
44 : // The OAuth2 error response.
45 : type Oauth2Error struct {
46 : // Error code.
47 : ErrorCode string `json:"error"`
48 : // Detail message.
49 : ErrorMessage string `json:"error_message,omitempty"`
50 : }
51 :
52 : // The Sylvia-IoT API error response.
53 : type ApiError struct {
54 : // Error code.
55 : Code string `json:"code"`
56 : // Detail message.
57 : Message string `json:"message,omitempty"`
58 : }
59 :
60 : // Response from OAuth2 token API.
61 : type oauth2TokenRes struct {
62 : AccessToken string `json:"access_token"`
63 : }
64 :
65 : var _ error = (*Oauth2Error)(nil)
66 : var _ error = (*ApiError)(nil)
67 :
68 : // Create an instance.
69 11 : func NewClient(opts ClientOptions) (*Client, error) {
70 11 : return &Client{
71 11 : client: http.Client{},
72 11 : authBase: opts.AuthBase,
73 11 : coremgrBase: opts.CoremgrBase,
74 11 : clientID: opts.ClientID,
75 11 : clientSecret: opts.ClientSecret,
76 11 : }, nil
77 11 : }
78 :
79 : // Execute a Sylvia-IoT API request.
80 : // - `apiPath` is the relative path (of the coremgr base) the API with query string.
81 : // For example: `/api/v1/user/list?contains=word`, the client will do a request with
82 : // `http://coremgr-host/coremgr/api/v1/user/list?contains=word` URL.
83 : // - `body` MUST be JSON format.
84 : func (c *Client) Request(
85 : method string,
86 : apiPath string,
87 : body []byte,
88 10 : ) (statusCode int, resBody []byte, err error) {
89 10 : url := c.coremgrBase + apiPath
90 21 : for retry := 1; ; retry-- {
91 11 : token := c.accessToken
92 21 : if token == "" {
93 10 : token, err = c.authToken()
94 12 : if err != nil {
95 2 : return 0, nil, err
96 2 : }
97 : }
98 9 : var bodyReader io.Reader
99 11 : if body != nil {
100 2 : bodyReader = bytes.NewReader(body)
101 2 : }
102 9 : req, err := http.NewRequest(method, url, bodyReader)
103 9 : if err != nil {
104 0 : return 0, nil, err
105 0 : }
106 9 : req.Header.Add("Authorization", "Bearer "+token)
107 9 : req.Header.Add("Content-Type", "application/json")
108 9 : res, err := c.client.Do(req)
109 11 : if err != nil {
110 2 : return 0, nil, err
111 2 : }
112 7 : defer res.Body.Close()
113 7 : body, err := io.ReadAll(res.Body)
114 7 : if err != nil {
115 0 : return 0, nil, err
116 0 : }
117 13 : if res.StatusCode != http.StatusUnauthorized || retry <= 0 {
118 6 : return res.StatusCode, body, nil
119 6 : }
120 1 : c.mutex.Lock()
121 1 : c.accessToken = ""
122 1 : c.mutex.Unlock()
123 : }
124 : }
125 :
126 0 : func (e *Oauth2Error) Error() string {
127 0 : return fmt.Sprintf("error: %s, error_message: %s", e.ErrorCode, e.ErrorMessage)
128 0 : }
129 :
130 0 : func (e *ApiError) Error() string {
131 0 : return fmt.Sprintf("code: %s, message: %s", e.Code, e.Message)
132 0 : }
133 :
134 10 : func (c *Client) authToken() (string, error) {
135 10 : body := url.Values{"grant_type": []string{"client_credentials"}}
136 10 : bodyReader := bytes.NewReader([]byte(body.Encode()))
137 10 : req, err := http.NewRequest(http.MethodPost, c.authBase+"/oauth2/token", bodyReader)
138 10 : if err != nil {
139 0 : return "", err
140 0 : }
141 10 : req.SetBasicAuth(url.QueryEscape(c.clientID), url.QueryEscape(c.clientSecret))
142 10 : req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
143 10 : res, err := c.client.Do(req)
144 12 : if err != nil {
145 2 : return "", err
146 2 : }
147 8 : defer res.Body.Close()
148 8 : bodyBytes, err := io.ReadAll(res.Body)
149 8 : if err != nil {
150 0 : return "", err
151 0 : }
152 8 : var tokenRes oauth2TokenRes
153 8 : if err = json.Unmarshal(bodyBytes, &tokenRes); err != nil {
154 0 : return "", err
155 0 : }
156 8 : c.mutex.Lock()
157 8 : c.accessToken = tokenRes.AccessToken
158 8 : c.mutex.Unlock()
159 8 :
160 8 : return tokenRes.AccessToken, nil
161 : }
|