LCOV - code coverage report
Current view: top level - api - http.go (source / functions) Hit Total Coverage
Test: sdk.lcov Lines: 60 76 78.9 %
Date: 2024-12-20 13:58:22 Functions: 0 0 -

          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             : }

Generated by: LCOV version 1.14