Initial commit
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Assisted-by: GLM 5.3
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||||
|
||||
// Package preview signs the shareable links that show an unpublished
|
||||
// post on the public API. The signature is an HMAC over the slug and an
|
||||
// expiry stamp, keyed with the admin session key, so a link can be
|
||||
// handed to a reviewer without granting them anything else.
|
||||
package preview
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TTL is how long a preview link stays valid.
|
||||
const TTL = 7 * 24 * time.Hour
|
||||
|
||||
// Token returns the preview token for slug, or "" when no session key
|
||||
// is configured: without a key the signature could not be verified, so
|
||||
// no link is offered at all.
|
||||
func Token(slug, sessionKey string, now time.Time) string {
|
||||
if slug == "" || sessionKey == "" {
|
||||
return ""
|
||||
}
|
||||
expiry := now.Add(TTL).Unix()
|
||||
return sign(slug, expiry, sessionKey) + "-" + strconv.FormatInt(expiry, 10)
|
||||
}
|
||||
|
||||
// Valid reports whether token authorises a preview of slug.
|
||||
func Valid(token, slug, sessionKey string, now time.Time) bool {
|
||||
if token == "" || slug == "" || sessionKey == "" {
|
||||
return false
|
||||
}
|
||||
mac, stamp, found := strings.CutLast(token, "-")
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
expiry, err := strconv.ParseInt(stamp, 10, 64)
|
||||
if err != nil || now.Unix() > expiry {
|
||||
return false
|
||||
}
|
||||
return hmac.Equal([]byte(mac), []byte(sign(slug, expiry, sessionKey)))
|
||||
}
|
||||
|
||||
func sign(slug string, expiry int64, sessionKey string) string {
|
||||
mac := hmac.New(sha256.New, []byte(sessionKey))
|
||||
mac.Write([]byte(slug))
|
||||
mac.Write([]byte("-"))
|
||||
mac.Write([]byte(strconv.FormatInt(expiry, 10)))
|
||||
return hex.EncodeToString(mac.Sum(nil))[:32]
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||||
|
||||
package preview
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTokenRoundTrip(t *testing.T) {
|
||||
now := time.Now()
|
||||
secret := strings.Repeat("k", 64)
|
||||
token := Token("hello", secret, now)
|
||||
if token == "" {
|
||||
t.Fatal("Token returned nothing for a configured key")
|
||||
}
|
||||
if !Valid(token, "hello", secret, now) {
|
||||
t.Fatal("a fresh token was rejected")
|
||||
}
|
||||
if Valid(token, "hello", secret, now.Add(TTL+time.Minute)) {
|
||||
t.Fatal("an expired token was accepted")
|
||||
}
|
||||
// The link carries no start time: a reviewer whose clock sits a little
|
||||
// behind the server's must still be able to open it.
|
||||
if !Valid(token, "hello", secret, now.Add(-time.Hour)) {
|
||||
t.Fatal("a token was rejected shortly before its mint time")
|
||||
}
|
||||
if Valid(token, "other", secret, now) {
|
||||
t.Fatal("a token for another slug was accepted")
|
||||
}
|
||||
if Valid(token, "hello", strings.Repeat("j", 64), now) {
|
||||
t.Fatal("a token was accepted under another key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenRequiresAKeyAndSlug(t *testing.T) {
|
||||
now := time.Now()
|
||||
if Token("", "secret", now) != "" {
|
||||
t.Fatal("a token was minted for an empty slug")
|
||||
}
|
||||
if Token("hello", "", now) != "" {
|
||||
t.Fatal("a token was minted without a session key")
|
||||
}
|
||||
if Valid("", "hello", "secret", now) {
|
||||
t.Fatal("an empty token was accepted")
|
||||
}
|
||||
if Valid("garbage", "hello", "secret", now) {
|
||||
t.Fatal("a malformed token was accepted")
|
||||
}
|
||||
if Valid("x-notanumber", "hello", "secret", now) {
|
||||
t.Fatal("a token with an unparsable stamp was accepted")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user