arblant
firefox-vpn-client / core / internal / guardian / usage.go
usage.go68 lines · 2.0 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
package guardian

import (
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"
)

// ProxyUsage is the quota state Guardian reports in X-Quota-* headers.
type ProxyUsage struct {
	Unlimited bool
	Limit     int64
	Remaining int64
	Reset     time.Time
}

// Exhausted reports whether no quota is left for the current period.
func (u *ProxyUsage) Exhausted() bool {
	return u != nil && !u.Unlimited && u.Remaining <= 0
}

// parseProxyUsage parses the X-Quota-* headers like Firefox's ProxyUsage:
// X-Quota-Unlimited: true needs nothing else; otherwise limit, remaining,
// and reset must all be present and consistent.
func parseProxyUsage(h http.Header) (*ProxyUsage, error) {
	if strings.EqualFold(strings.TrimSpace(h.Get("X-Quota-Unlimited")), "true") {
		return &ProxyUsage{Unlimited: true}, nil
	}
	rawLimit := strings.TrimSpace(h.Get("X-Quota-Limit"))
	rawRemaining := strings.TrimSpace(h.Get("X-Quota-Remaining"))
	rawReset := strings.TrimSpace(h.Get("X-Quota-Reset"))
	if rawLimit == "" || rawRemaining == "" || rawReset == "" {
		return nil, fmt.Errorf("quota headers missing")
	}
	limit, err := strconv.ParseInt(rawLimit, 10, 64)
	if err != nil || limit < 0 {
		return nil, fmt.Errorf("invalid X-Quota-Limit")
	}
	remaining, err := strconv.ParseInt(rawRemaining, 10, 64)
	if err != nil || remaining < 0 {
		return nil, fmt.Errorf("invalid X-Quota-Remaining")
	}
	if remaining > limit {
		return nil, fmt.Errorf("X-Quota-Remaining exceeds X-Quota-Limit")
	}
	reset, err := parseQuotaReset(rawReset)
	if err != nil {
		return nil, err
	}
	return &ProxyUsage{Limit: limit, Remaining: remaining, Reset: reset}, nil
}

func parseQuotaReset(value string) (time.Time, error) {
	if t, err := time.Parse(time.RFC3339Nano, value); err == nil {
		return t, nil
	}
	if seconds, err := strconv.ParseInt(value, 10, 64); err == nil && seconds > 0 {
		return time.Unix(seconds, 0), nil
	}
	return time.Time{}, fmt.Errorf("invalid X-Quota-Reset")
}

// ParseProxyUsage parses Guardian's X-Quota-* response headers.
func ParseProxyUsage(h http.Header) (*ProxyUsage, error) {
	return parseProxyUsage(h)
}