client.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. //go:build ee
  2. // +build ee
  3. package billing
  4. import (
  5. "crypto/hmac"
  6. "crypto/sha256"
  7. "encoding/hex"
  8. "encoding/json"
  9. "fmt"
  10. "io/ioutil"
  11. "net/http"
  12. "net/url"
  13. "strings"
  14. "time"
  15. "github.com/porter-dev/porter/api/types"
  16. cemodels "github.com/porter-dev/porter/internal/models"
  17. )
  18. // Client contains an API client for the internal billing engine
  19. type Client struct {
  20. apiKey string
  21. serverURL string
  22. httpClient *http.Client
  23. }
  24. // NewClient creates a new billing API client
  25. func NewClient(serverURL, apiKey string) (*Client, error) {
  26. httpClient := &http.Client{
  27. Timeout: time.Minute,
  28. }
  29. client := &Client{apiKey, serverURL, httpClient}
  30. return client, nil
  31. }
  32. func (c *Client) CreateTeam(user *cemodels.User, proj *cemodels.Project) (string, error) {
  33. // call the internal billing endpoint to create a new customer in the database
  34. reqData := &CreateCustomerRequest{
  35. Email: user.Email,
  36. UserID: user.ID,
  37. ProjectID: proj.ID,
  38. ProjectName: proj.Name,
  39. }
  40. err := c.postRequest("/api/v1/private/customer", reqData, nil)
  41. if err != nil {
  42. return "", err
  43. }
  44. return fmt.Sprintf("%d-%d", proj.ID, user.ID), nil
  45. }
  46. func (c *Client) DeleteTeam(user *cemodels.User, proj *cemodels.Project) error {
  47. // call delete customer
  48. reqData := &DeleteCustomerRequest{
  49. UserID: user.ID,
  50. ProjectID: proj.ID,
  51. }
  52. return c.deleteRequest("/api/v1/private/customer", reqData, nil)
  53. }
  54. // VerifySignature verifies a webhook signature based on hmac protocol
  55. func (c *Client) VerifySignature(signature string, body []byte) bool {
  56. if len(signature) != 71 || !strings.HasPrefix(signature, "sha256=") {
  57. return false
  58. }
  59. actual := make([]byte, 32)
  60. _, err := hex.Decode(actual, []byte(signature[7:]))
  61. if err != nil {
  62. return false
  63. }
  64. computed := hmac.New(sha256.New, []byte(c.apiKey))
  65. _, err = computed.Write(body)
  66. if err != nil {
  67. return false
  68. }
  69. return hmac.Equal(computed.Sum(nil), actual)
  70. }
  71. func (c *Client) postRequest(path string, data interface{}, dst interface{}) error {
  72. return c.writeRequest("POST", path, data, dst)
  73. }
  74. func (c *Client) putRequest(path string, data interface{}, dst interface{}) error {
  75. return c.writeRequest("PUT", path, data, dst)
  76. }
  77. func (c *Client) deleteRequest(path string, data interface{}, dst interface{}) error {
  78. return c.writeRequest("DELETE", path, data, dst)
  79. }
  80. func (c *Client) getRequest(path string, dst interface{}, query ...map[string]string) error {
  81. reqURL, err := url.Parse(c.serverURL)
  82. if err != nil {
  83. return nil
  84. }
  85. reqURL.Path = path
  86. q := reqURL.Query()
  87. for _, queryGroup := range query {
  88. for key, val := range queryGroup {
  89. q.Add(key, val)
  90. }
  91. }
  92. reqURL.RawQuery = q.Encode()
  93. req, err := http.NewRequest(
  94. "GET",
  95. reqURL.String(),
  96. nil,
  97. )
  98. if err != nil {
  99. return err
  100. }
  101. req.Header.Set("Content-Type", "application/json; charset=utf-8")
  102. req.Header.Set("Accept", "application/json; charset=utf-8")
  103. req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
  104. res, err := c.httpClient.Do(req)
  105. if err != nil {
  106. return err
  107. }
  108. defer res.Body.Close()
  109. if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
  110. resBytes, err := ioutil.ReadAll(res.Body)
  111. if err != nil {
  112. return fmt.Errorf("request failed with status code %d, but could not read body (%s)\n", res.StatusCode, err.Error())
  113. }
  114. return fmt.Errorf("request failed with status code %d: %s\n", res.StatusCode, string(resBytes))
  115. }
  116. if dst != nil {
  117. return json.NewDecoder(res.Body).Decode(dst)
  118. }
  119. return nil
  120. }
  121. func (c *Client) writeRequest(method, path string, data interface{}, dst interface{}) error {
  122. reqURL, err := url.Parse(c.serverURL)
  123. if err != nil {
  124. return nil
  125. }
  126. reqURL.Path = path
  127. var strData []byte
  128. if data != nil {
  129. strData, err = json.Marshal(data)
  130. if err != nil {
  131. return err
  132. }
  133. }
  134. req, err := http.NewRequest(
  135. method,
  136. reqURL.String(),
  137. strings.NewReader(string(strData)),
  138. )
  139. if err != nil {
  140. return err
  141. }
  142. req.Header.Set("Content-Type", "application/json; charset=utf-8")
  143. req.Header.Set("Accept", "application/json; charset=utf-8")
  144. req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
  145. res, err := c.httpClient.Do(req)
  146. if err != nil {
  147. return err
  148. }
  149. defer res.Body.Close()
  150. if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
  151. resBytes, err := ioutil.ReadAll(res.Body)
  152. if err != nil {
  153. return fmt.Errorf("request failed with status code %d, but could not read body (%s)\n", res.StatusCode, err.Error())
  154. }
  155. return fmt.Errorf("request failed with status code %d: %s\n", res.StatusCode, string(resBytes))
  156. }
  157. if dst != nil {
  158. return json.NewDecoder(res.Body).Decode(dst)
  159. }
  160. return nil
  161. }
  162. const (
  163. FeatureSlugCPU string = "cpu"
  164. FeatureSlugMemory string = "memory"
  165. FeatureSlugClusters string = "clusters"
  166. FeatureSlugUsers string = "users"
  167. )
  168. func (c *Client) ParseProjectUsageFromWebhook(payload []byte) (*cemodels.ProjectUsage, *types.FeatureFlags, error) {
  169. usageData := &APIWebhookRequest{}
  170. err := json.Unmarshal(payload, usageData)
  171. if err != nil {
  172. return nil, nil, err
  173. }
  174. return &cemodels.ProjectUsage{
  175. ProjectID: usageData.ProjectID,
  176. ResourceCPU: usageData.CPU,
  177. ResourceMemory: usageData.Memory * 1000,
  178. Clusters: usageData.Clusters,
  179. Users: usageData.Users,
  180. }, &types.FeatureFlags{
  181. PreviewEnvironmentsEnabled: usageData.PreviewEnvironmentsEnabled,
  182. ManagedInfraEnabled: usageData.ManagedInfraEnabled,
  183. StacksEnabled: usageData.StacksEnabled,
  184. ManagedDatabasesEnabled: usageData.ManagedDatabasesEnabled,
  185. }, nil
  186. }