client.go 6.7 KB

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