mirror of
https://github.com/photoprism/photoprism.git
synced 2026-07-29 21:12:04 +00:00
A configured PHOTOPRISM_DOWNLOAD_TOKEN was delivered to every client, so download requests could not be attributed to the session that made them. Unlike preview tokens, a download token is a query parameter on a one-shot transfer rather than part of the URL path, so it is not part of the cache key and a per-session value costs no cache sharing. There is accordingly no reason to trade the session binding for it. Sessions now always receive a signed, session-bound token. The configured value is still accepted, so permanent URLs keep working, and it is delivered only to callers with no session to sign for. No value is generated when the option is blank either, so an instance that was never configured for permanent URLs has none to accept: Config.DownloadToken returns an empty string rather than a random one, and tokens.DownloadStatic is no longer needed. A session without its own preview token now receives no download token, in both ClientSession and AddTokenHeaders, since the download token is the higher-value credential — it also authorizes originals, and a coarse one is accepted for previews. Corrects the download-token and download-token-maxage usage strings and the startup notice, which all described the previous delivery policy, and notes on the frontend download helper that its anchor path cannot observe an expired token.
63 lines
1.9 KiB
Go
63 lines
1.9 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/photoprism/photoprism/internal/auth/tokens"
|
|
"github.com/photoprism/photoprism/internal/config"
|
|
"github.com/photoprism/photoprism/internal/entity"
|
|
"github.com/photoprism/photoprism/internal/photoprism/get"
|
|
"github.com/photoprism/photoprism/pkg/rnd"
|
|
)
|
|
|
|
func TestAddTokenHeaders(t *testing.T) {
|
|
sess := &entity.Session{
|
|
ID: rnd.SessionID("add-token-headers-test"),
|
|
PreviewToken: "prev123",
|
|
}
|
|
|
|
t.Run("SignedDownloadToken", func(t *testing.T) {
|
|
conf := get.Config()
|
|
conf.SetAuthMode(config.AuthModePasswd)
|
|
defer conf.SetAuthMode(config.AuthModePublic)
|
|
|
|
// Pin the delivery policy to signed (not public, no configured static token).
|
|
origPublic, origCoarse := tokens.PublicMode, tokens.CoarseDownload
|
|
tokens.PublicMode, tokens.CoarseDownload = false, ""
|
|
defer func() { tokens.PublicMode, tokens.CoarseDownload = origPublic, origCoarse }()
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
AddTokenHeaders(c, sess)
|
|
|
|
assert.Equal(t, "prev123", w.Header().Get("X-Preview-Token"))
|
|
|
|
// The download token is the signed, session-bound compact value (<expires>.<sid>.<token>),
|
|
// delivered as the bare "?t=" value.
|
|
downloadToken := w.Header().Get("X-Download-Token")
|
|
parts := strings.SplitN(downloadToken, ".", 3)
|
|
assert.Len(t, parts, 3)
|
|
expires, err := strconv.ParseInt(parts[0], 10, 64)
|
|
assert.NoError(t, err)
|
|
gotID, ok := tokens.VerifyDownload(expires, parts[1], parts[2])
|
|
assert.True(t, ok)
|
|
assert.Equal(t, sess.ID, gotID)
|
|
})
|
|
t.Run("PublicModeEmitsNothing", func(t *testing.T) {
|
|
conf := get.Config()
|
|
conf.SetAuthMode(config.AuthModePublic)
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
AddTokenHeaders(c, sess)
|
|
|
|
assert.Empty(t, w.Header().Get("X-Preview-Token"))
|
|
assert.Empty(t, w.Header().Get("X-Download-Token"))
|
|
})
|
|
}
|