177 lines
5.3 KiB
Go
177 lines
5.3 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|