🏡 index : ironmagma/quintodrome.git

package deezer

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"sync"
	"time"

	"github.com/lestrrat-go/jwx/v3/jwt"
	"github.com/navidrome/navidrome/log"
)

type jwtToken struct {
	token     string
	expiresAt time.Time
	mu        sync.RWMutex
}

func (j *jwtToken) get() (string, bool) {
	j.mu.RLock()
	defer j.mu.RUnlock()
	if time.Now().Before(j.expiresAt) {
		return j.token, true
	}
	return "", false
}

func (j *jwtToken) set(token string, expiresIn time.Duration) {
	j.mu.Lock()
	defer j.mu.Unlock()
	j.token = token
	j.expiresAt = time.Now().Add(expiresIn)
}

func (c *client) getJWT(ctx context.Context) (string, error) {
	// Check if we have a valid cached token
	if token, valid := c.jwt.get(); valid {
		return token, nil
	}

	// Fetch a new anonymous token
	req, err := http.NewRequestWithContext(ctx, "GET", authBaseURL+"/login/anonymous?jo=p&rto=c", nil)
	if err != nil {
		return "", err
	}
	req.Header.Set("Accept", "application/json")

	resp, err := c.httpDoer.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		return "", fmt.Errorf("deezer: failed to get JWT token: %s", resp.Status)
	}

	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	type authResponse struct {
		JWT string `json:"jwt"` //nolint:gosec
	}

	var result authResponse
	if err := json.Unmarshal(data, &result); err != nil {
		return "", fmt.Errorf("deezer: failed to parse auth response: %w", err)
	}

	if result.JWT == "" {
		return "", errors.New("deezer: no JWT token in response")
	}

	// Parse JWT to get actual expiration time
	token, err := jwt.ParseString(result.JWT, jwt.WithVerify(false), jwt.WithValidate(false))
	if err != nil {
		return "", fmt.Errorf("deezer: failed to parse JWT token: %w", err)
	}

	// Calculate TTL with a 1-minute buffer for clock skew and network delays
	expiresAt, ok := token.Expiration()
	if !ok || expiresAt.IsZero() {
		return "", errors.New("deezer: JWT token has no expiration time")
	}

	ttl := time.Until(expiresAt) - 1*time.Minute
	if ttl <= 0 {
		return "", errors.New("deezer: JWT token already expired or expires too soon")
	}

	c.jwt.set(result.JWT, ttl)
	log.Trace(ctx, "Fetched new Deezer JWT token", "expiresAt", expiresAt, "ttl", ttl)

	return result.JWT, nil
}