| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384 |
- package pia
- import (
- "bytes"
- "context"
- "fmt"
- "mime/multipart"
- "net/http"
- "strings"
- "time"
- )
- type AuthClient struct {
- Endpoint string
- HTTPClient *http.Client
- MaxBody int64
- UserAgent string
- Now func() time.Time
- }
- func NewAuthClient(endpoint string) *AuthClient {
- return &AuthClient{
- Endpoint: endpoint,
- MaxBody: DefaultMaxResponseBody,
- UserAgent: DefaultUserAgent,
- Now: time.Now,
- HTTPClient: &http.Client{Timeout: DefaultRequestTimeout, CheckRedirect: noRedirect},
- }
- }
- func (c *AuthClient) Authenticate(ctx context.Context, username string, password []byte) (Token, error) {
- username = strings.TrimSpace(username)
- if !validSecret([]byte(username), 1, 256) || !validSecret(password, 1, 1024) {
- return Token{}, NewError(CodeInvalidCredentials, "Enter a valid PIA username and password.")
- }
- var body bytes.Buffer
- writer := multipart.NewWriter(&body)
- if err := writer.WriteField("username", username); err != nil {
- return Token{}, WrapError(CodeAuthenticationUnavailable, "Could not prepare the authentication request.", err)
- }
- if err := writer.WriteField("password", string(password)); err != nil {
- return Token{}, WrapError(CodeAuthenticationUnavailable, "Could not prepare the authentication request.", err)
- }
- if err := writer.Close(); err != nil {
- return Token{}, WrapError(CodeAuthenticationUnavailable, "Could not prepare the authentication request.", err)
- }
- request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.Endpoint, &body)
- if err != nil {
- return Token{}, WrapError(CodeAuthenticationUnavailable, "The authentication endpoint is invalid.", err)
- }
- request.Header.Set("Content-Type", writer.FormDataContentType())
- request.Header.Set("Accept", "application/json")
- request.Header.Set("User-Agent", c.UserAgent)
- response, err := c.HTTPClient.Do(request)
- if err != nil {
- return Token{}, classifyNetworkError(ctx, CodeAuthenticationUnavailable, "PIA authentication could not be reached.", err)
- }
- defer response.Body.Close()
- if response.StatusCode == http.StatusUnauthorized || response.StatusCode == http.StatusForbidden {
- return Token{}, NewError(CodeInvalidCredentials, "The PIA username or password was rejected.")
- }
- if response.StatusCode != http.StatusOK {
- return Token{}, NewError(CodeAuthenticationUnavailable, fmt.Sprintf("PIA authentication returned HTTP %d.", response.StatusCode))
- }
- if !expectedContentType(response.Header.Get("Content-Type"), "application/json") {
- return Token{}, NewError(CodeAuthenticationUnavailable, "PIA authentication returned an unexpected content type.")
- }
- raw, err := readLimitedBody(response.Body, c.MaxBody)
- if err != nil {
- return Token{}, WrapError(CodeAuthenticationUnavailable, "PIA authentication returned an invalid response.", err)
- }
- var payload struct {
- Token string `json:"token"`
- }
- if err := decodeSingleJSON(raw, &payload); err != nil || !validSecret([]byte(payload.Token), 16, 4096) {
- return Token{}, NewError(CodeAuthenticationUnavailable, "PIA authentication returned an invalid token response.")
- }
- now := time.Now
- if c.Now != nil {
- now = c.Now
- }
- return Token{Value: []byte(payload.Token), ExpiresAt: now().Add(DefaultTokenTTL)}, nil
- }
|