auth.go 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. package pia
  2. import (
  3. "bytes"
  4. "context"
  5. "fmt"
  6. "mime/multipart"
  7. "net/http"
  8. "strings"
  9. "time"
  10. )
  11. type AuthClient struct {
  12. Endpoint string
  13. HTTPClient *http.Client
  14. MaxBody int64
  15. UserAgent string
  16. Now func() time.Time
  17. }
  18. func NewAuthClient(endpoint string) *AuthClient {
  19. return &AuthClient{
  20. Endpoint: endpoint,
  21. MaxBody: DefaultMaxResponseBody,
  22. UserAgent: DefaultUserAgent,
  23. Now: time.Now,
  24. HTTPClient: &http.Client{Timeout: DefaultRequestTimeout, CheckRedirect: noRedirect},
  25. }
  26. }
  27. func (c *AuthClient) Authenticate(ctx context.Context, username string, password []byte) (Token, error) {
  28. username = strings.TrimSpace(username)
  29. if !validSecret([]byte(username), 1, 256) || !validSecret(password, 1, 1024) {
  30. return Token{}, NewError(CodeInvalidCredentials, "Enter a valid PIA username and password.")
  31. }
  32. var body bytes.Buffer
  33. writer := multipart.NewWriter(&body)
  34. if err := writer.WriteField("username", username); err != nil {
  35. return Token{}, WrapError(CodeAuthenticationUnavailable, "Could not prepare the authentication request.", err)
  36. }
  37. if err := writer.WriteField("password", string(password)); err != nil {
  38. return Token{}, WrapError(CodeAuthenticationUnavailable, "Could not prepare the authentication request.", err)
  39. }
  40. if err := writer.Close(); err != nil {
  41. return Token{}, WrapError(CodeAuthenticationUnavailable, "Could not prepare the authentication request.", err)
  42. }
  43. request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.Endpoint, &body)
  44. if err != nil {
  45. return Token{}, WrapError(CodeAuthenticationUnavailable, "The authentication endpoint is invalid.", err)
  46. }
  47. request.Header.Set("Content-Type", writer.FormDataContentType())
  48. request.Header.Set("Accept", "application/json")
  49. request.Header.Set("User-Agent", c.UserAgent)
  50. response, err := c.HTTPClient.Do(request)
  51. if err != nil {
  52. return Token{}, classifyNetworkError(ctx, CodeAuthenticationUnavailable, "PIA authentication could not be reached.", err)
  53. }
  54. defer response.Body.Close()
  55. if response.StatusCode == http.StatusUnauthorized || response.StatusCode == http.StatusForbidden {
  56. return Token{}, NewError(CodeInvalidCredentials, "The PIA username or password was rejected.")
  57. }
  58. if response.StatusCode != http.StatusOK {
  59. return Token{}, NewError(CodeAuthenticationUnavailable, fmt.Sprintf("PIA authentication returned HTTP %d.", response.StatusCode))
  60. }
  61. if !expectedContentType(response.Header.Get("Content-Type"), "application/json") {
  62. return Token{}, NewError(CodeAuthenticationUnavailable, "PIA authentication returned an unexpected content type.")
  63. }
  64. raw, err := readLimitedBody(response.Body, c.MaxBody)
  65. if err != nil {
  66. return Token{}, WrapError(CodeAuthenticationUnavailable, "PIA authentication returned an invalid response.", err)
  67. }
  68. var payload struct {
  69. Token string `json:"token"`
  70. }
  71. if err := decodeSingleJSON(raw, &payload); err != nil || !validSecret([]byte(payload.Token), 16, 4096) {
  72. return Token{}, NewError(CodeAuthenticationUnavailable, "PIA authentication returned an invalid token response.")
  73. }
  74. now := time.Now
  75. if c.Now != nil {
  76. now = c.Now
  77. }
  78. return Token{Value: []byte(payload.Token), ExpiresAt: now().Add(DefaultTokenTTL)}, nil
  79. }