Client for the pixeldrain API. Used by pixeldrain itself for tranferring data between the web UI and API server. And for rendering JSON responses
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

194 lines
4.9 KiB

  1. package pixelapi
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "io/ioutil"
  8. "net"
  9. "net/http"
  10. "net/url"
  11. "strings"
  12. "time"
  13. )
  14. // PixelAPI is the Pixeldrain API client
  15. type PixelAPI struct {
  16. client *http.Client
  17. apiEndpoint string
  18. key string
  19. realIP string
  20. }
  21. // New creates a new Pixeldrain API client to query the Pixeldrain API with
  22. func New(apiEndpoint string) (api PixelAPI) {
  23. api.client = &http.Client{Timeout: time.Minute * 5}
  24. api.apiEndpoint = apiEndpoint
  25. // Pixeldrain uses unix domain sockets on its servers to minimize latency
  26. // between the web interface daemon and API daemon. Golang does not
  27. // understand that it needs to dial a unix socket on this case so we create
  28. // a custom HTTP transport which uses the unix socket instead of TCP
  29. if strings.HasPrefix(apiEndpoint, "http://unix:") {
  30. // Get the socket path from the API endpoint
  31. var sockPath = strings.TrimPrefix(apiEndpoint, "http://unix:")
  32. // Fake the dialer to use a unix socket instead of TCP
  33. api.client.Transport = &http.Transport{
  34. DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
  35. return net.Dial("unix", sockPath)
  36. },
  37. }
  38. // Fake a domain name to stop Go's HTTP client from complaining about
  39. // the domain name. This string will be completely ignored during
  40. // requests
  41. api.apiEndpoint = "http://api.sock"
  42. } else {
  43. api.client.Transport = http.DefaultTransport
  44. }
  45. return api
  46. }
  47. // Login logs a user into the pixeldrain API. The original PixelAPI does not get
  48. // logged in, only the returned PixelAPI
  49. func (p PixelAPI) Login(apiKey string) PixelAPI {
  50. p.key = apiKey
  51. return p
  52. }
  53. // RealIP sets the real IP address to use when making API requests
  54. func (p PixelAPI) RealIP(ip string) PixelAPI {
  55. p.realIP = ip
  56. return p
  57. }
  58. // Standard response types
  59. // Error is an error returned by the pixeldrain API. If the request failed
  60. // before it could reach the API the error will be on a different type
  61. type Error struct {
  62. Status int `json:"-"` // One of the http.Status types
  63. Success bool `json:"success"`
  64. StatusCode string `json:"value"`
  65. Message string `json:"message"`
  66. // In case of the multiple_errors code this array will be populated with
  67. // more errors
  68. Errors []Error `json:"errors,omitempty"`
  69. // Metadata regarding the error
  70. Extra map[string]interface{} `json:"extra,omitempty"`
  71. }
  72. func (e Error) Error() string { return e.StatusCode }
  73. // ErrIsServerError returns true if the error is a server-side error
  74. func ErrIsServerError(err error) bool {
  75. if apierr, ok := err.(Error); ok && apierr.Status >= 500 {
  76. return true
  77. }
  78. return false
  79. }
  80. // ErrIsClientError returns true if the error is a client-side error
  81. func ErrIsClientError(err error) bool {
  82. if apierr, ok := err.(Error); ok && apierr.Status >= 400 && apierr.Status < 500 {
  83. return true
  84. }
  85. return false
  86. }
  87. func (p *PixelAPI) do(r *http.Request) (*http.Response, error) {
  88. if p.key != "" {
  89. r.SetBasicAuth("", p.key)
  90. }
  91. if p.realIP != "" {
  92. r.Header.Set("X-Real-IP", p.realIP)
  93. }
  94. return p.client.Do(r)
  95. }
  96. func (p *PixelAPI) getString(path string) (string, error) {
  97. req, err := http.NewRequest("GET", p.apiEndpoint+"/"+path, nil)
  98. if err != nil {
  99. return "", err
  100. }
  101. resp, err := p.do(req)
  102. if err != nil {
  103. return "", err
  104. }
  105. defer resp.Body.Close()
  106. bodyBytes, err := ioutil.ReadAll(resp.Body)
  107. return string(bodyBytes), err
  108. }
  109. func (p *PixelAPI) getRaw(path string) (io.ReadCloser, error) {
  110. req, err := http.NewRequest("GET", p.apiEndpoint+"/"+path, nil)
  111. if err != nil {
  112. return nil, err
  113. }
  114. resp, err := p.do(req)
  115. if err != nil {
  116. return nil, err
  117. }
  118. return resp.Body, err
  119. }
  120. func (p *PixelAPI) jsonRequest(method, path string, target interface{}) error {
  121. req, err := http.NewRequest(method, p.apiEndpoint+"/"+path, nil)
  122. if err != nil {
  123. return err
  124. }
  125. resp, err := p.do(req)
  126. if err != nil {
  127. return err
  128. }
  129. defer resp.Body.Close()
  130. return parseJSONResponse(resp, target)
  131. }
  132. func (p *PixelAPI) form(method, url string, vals url.Values, target interface{}) error {
  133. req, err := http.NewRequest(method, p.apiEndpoint+"/"+url, strings.NewReader(vals.Encode()))
  134. if err != nil {
  135. return fmt.Errorf("prepare request failed: %w", err)
  136. }
  137. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  138. resp, err := p.do(req)
  139. if err != nil {
  140. return fmt.Errorf("do request failed: %w", err)
  141. }
  142. defer resp.Body.Close()
  143. return parseJSONResponse(resp, target)
  144. }
  145. func parseJSONResponse(resp *http.Response, target interface{}) (err error) {
  146. // Test for client side and server side errors
  147. if resp.StatusCode >= 400 {
  148. errResp := Error{Status: resp.StatusCode}
  149. if err = json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
  150. return fmt.Errorf("failed to decode json error: %w", err)
  151. }
  152. return errResp
  153. }
  154. if target == nil {
  155. return nil
  156. }
  157. if err = json.NewDecoder(resp.Body).Decode(target); err != nil {
  158. return fmt.Errorf("failed to decode json response: %w", err)
  159. }
  160. return nil
  161. }