@@ -0,0 +1,176 @@
|
||||
package hevcpull
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSignAndVerifyURL(t *testing.T) {
|
||||
now := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC)
|
||||
signed, err := SignURL(
|
||||
"https://app.example.com/api/app/vid/transcode/m3u8/laosiji/m3m/demo.m3u8",
|
||||
"test-pull-secret-strong-32-bytes!!",
|
||||
now.Add(480*time.Hour),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("SignURL failed: %v", err)
|
||||
}
|
||||
parsed, err := url.Parse(signed)
|
||||
if err != nil {
|
||||
t.Fatalf("parse signed URL: %v", err)
|
||||
}
|
||||
if err = VerifyURL(parsed, "test-pull-secret-strong-32-bytes!!", now); err != nil {
|
||||
t.Fatalf("VerifyURL failed: %v", err)
|
||||
}
|
||||
|
||||
parsed.Path += ".tampered"
|
||||
if err = VerifyURL(parsed, "test-pull-secret-strong-32-bytes!!", now); !errors.Is(err, ErrInvalidSignature) {
|
||||
t.Fatalf("tampered path returned %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyURLRejectsExpiredOrExtraQuery(t *testing.T) {
|
||||
now := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC)
|
||||
signed, err := SignURL(
|
||||
"https://app.example.com/api/app/vid/transcode/m3u8/source.m3u8",
|
||||
"test-pull-secret-strong-32-bytes!!",
|
||||
now.Add(time.Minute),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("SignURL failed: %v", err)
|
||||
}
|
||||
parsed, _ := url.Parse(signed)
|
||||
if err = VerifyURL(parsed, "test-pull-secret-strong-32-bytes!!", now.Add(time.Minute)); !errors.Is(err, ErrExpired) {
|
||||
t.Fatalf("expired URL returned %v", err)
|
||||
}
|
||||
|
||||
parsed, _ = url.Parse(signed)
|
||||
query := parsed.Query()
|
||||
query.Set("c", "unbound-cdn")
|
||||
parsed.RawQuery = query.Encode()
|
||||
if err = VerifyURL(parsed, "test-pull-secret-strong-32-bytes!!", now); !errors.Is(err, ErrInvalidSignature) {
|
||||
t.Fatalf("extra query returned %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignURLRejectsWeakSecret(t *testing.T) {
|
||||
_, err := SignURL("https://app.example.com/source.m3u8", "short", time.Now().Add(time.Hour))
|
||||
if !errors.Is(err, ErrInvalidSecret) {
|
||||
t.Fatalf("weak secret returned %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactSignedPullURL(t *testing.T) {
|
||||
const signature = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
raw := "https://app.example/source.m3u8?hevc_exp=1&hevc_sig=" + signature
|
||||
redacted := RedactURL(raw)
|
||||
if redacted == raw || RedactText(redacted) != redacted {
|
||||
t.Fatalf("URL was not redacted: %s", redacted)
|
||||
}
|
||||
if got := RedactText("file_url=" + url.QueryEscape(raw)); got == "file_url="+url.QueryEscape(raw) {
|
||||
t.Fatalf("escaped nested URL was not redacted: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeSource(t *testing.T) {
|
||||
tests := []struct {
|
||||
source string
|
||||
want string
|
||||
}{
|
||||
{source: " /laosiji/m3m/demo.m3u8 ", want: "laosiji/m3m/demo.m3u8"},
|
||||
{source: "sp/movie/index.m3u8", want: "sp/movie/index.m3u8"},
|
||||
{source: "sp/movie/../index.m3u8"},
|
||||
{source: `sp\movie\index.m3u8`},
|
||||
{source: `sp/movie%5Cindex.m3u8`},
|
||||
{source: `sp/movie%0Aindex.m3u8`},
|
||||
{source: "https://cdn.example.com/index.m3u8"},
|
||||
{source: "sp/movie/index.m3u8?token=x"},
|
||||
{source: "sp/movie/index.mp4"},
|
||||
{source: "sp/movie/index.M3U8"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got, err := NormalizeSource(tt.source)
|
||||
if tt.want == "" {
|
||||
if err == nil {
|
||||
t.Errorf("NormalizeSource(%q) = %q, want error", tt.source, got)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil || got != tt.want {
|
||||
t.Errorf("NormalizeSource(%q) = %q, %v; want %q", tt.source, got, err, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveChildSource(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
parent string
|
||||
child string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "relative child",
|
||||
parent: "sp/movie/master.m3u8",
|
||||
child: "720/index.m3u8",
|
||||
want: "sp/movie/720/index.m3u8",
|
||||
},
|
||||
{
|
||||
name: "parent traversal",
|
||||
parent: "sp/movie/master.m3u8",
|
||||
child: "../audio/index.m3u8",
|
||||
want: "sp/audio/index.m3u8",
|
||||
},
|
||||
{
|
||||
name: "root child remains on default SP origin",
|
||||
parent: "sp/movie/master.m3u8",
|
||||
child: "/shared/index.m3u8",
|
||||
want: "shared/index.m3u8",
|
||||
},
|
||||
{
|
||||
name: "explicit PMS root namespace is preserved",
|
||||
parent: "pms/movie/master.m3u8",
|
||||
child: "/pms/shared/index.m3u8",
|
||||
want: "pms/shared/index.m3u8",
|
||||
},
|
||||
{
|
||||
name: "explicit laosiji root namespace is preserved",
|
||||
parent: "laosiji/m3m/movie/master.m3u8",
|
||||
child: "/laosiji/shared/index.m3u8",
|
||||
want: "laosiji/shared/index.m3u8",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ResolveChildSource(tt.parent, tt.child)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveChildSource error: %v", err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("ResolveChildSource = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
parent string
|
||||
child string
|
||||
}{
|
||||
{parent: "sp/movie/master.m3u8", child: "https://cdn.example.com/index.m3u8"},
|
||||
{parent: "sp/movie/master.m3u8", child: "index.m3u8?token=secret"},
|
||||
{parent: "sp/movie/master.m3u8", child: "index.ts"},
|
||||
{parent: "sp/movie/master.m3u8", child: `..\index.m3u8`},
|
||||
{parent: "sp/movie/master.m3u8", child: "../../outside/index.m3u8"},
|
||||
{parent: "sp/movie/master.m3u8", child: "../../../sp/outside/index.m3u8"},
|
||||
{parent: "sp/movie/master.m3u8", child: "/pms/outside/index.m3u8"},
|
||||
{parent: "pms/movie/master.m3u8", child: "/shared/index.m3u8"},
|
||||
{parent: "pms/movie/master.m3u8", child: "/sp/outside/index.m3u8"},
|
||||
{parent: "laosiji/movie/master.m3u8", child: "/shared/index.m3u8"},
|
||||
} {
|
||||
if _, err := ResolveChildSource(tt.parent, tt.child); err == nil {
|
||||
t.Fatalf("unsafe child accepted: parent=%q child=%q", tt.parent, tt.child)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user