Initial commit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:57:10 +08:00
co-authored by Claude Opus 5
commit 8679200f41
1897 changed files with 257900 additions and 0 deletions
+269
View File
@@ -0,0 +1,269 @@
package hevcpull
import (
"91porn-server/common/log"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"net/url"
"path"
"regexp"
"strconv"
"strings"
"time"
)
const (
ExpiresParam = "hevc_exp"
SignatureParam = "hevc_sig"
minSecretBytes = 32
maxSignatureTTL = 480*time.Hour + 5*time.Minute
)
var (
ErrInvalidSecret = errors.New("HEVC pull secret must contain at least 32 bytes")
ErrInvalidSignature = errors.New("invalid HEVC pull signature")
ErrExpired = errors.New("expired HEVC pull signature")
signatureTextPattern = regexp.MustCompile(`(?i)(hevc_sig(?:=|%3D))[0-9a-f]{64}`)
)
// NormalizeSource accepts only the canonical relative m3u8 paths stored by
// the video service. Absolute URLs, query strings, fragments, backslashes,
// and dot traversal are rejected so the signed path and cloud task identity
// always refer to the same source.
func NormalizeSource(source string) (string, error) {
source = strings.TrimSpace(source)
if source == "" || strings.Contains(source, "\\") {
return "", errors.New("invalid HEVC pull source")
}
parsed, err := url.Parse(source)
if err != nil ||
parsed.IsAbs() ||
parsed.Host != "" ||
parsed.User != nil ||
parsed.RawQuery != "" ||
parsed.Fragment != "" {
return "", errors.New("invalid HEVC pull source")
}
sourcePath := strings.TrimLeft(parsed.Path, "/")
if sourcePath == "" || strings.Contains(sourcePath, "\\") {
return "", errors.New("invalid HEVC pull source")
}
for _, char := range sourcePath {
if char < 0x20 || char == 0x7f {
return "", errors.New("invalid HEVC pull source")
}
}
for _, segment := range strings.Split(sourcePath, "/") {
if segment == "." || segment == ".." {
return "", errors.New("invalid HEVC pull source traversal")
}
}
normalized := strings.TrimLeft(path.Clean("/"+sourcePath), "/")
if normalized == "" ||
normalized == "." ||
path.Ext(normalized) != ".m3u8" {
return "", errors.New("invalid HEVC pull source")
}
return normalized, nil
}
// ResolveChildSource resolves a child playlist URI from a signed master
// playlist against the parent source path. Only local m3u8 paths are accepted;
// absolute URLs and query-bearing variants are rejected.
func ResolveChildSource(parentSource, childURI string) (string, error) {
parentSource, err := NormalizeSource(parentSource)
if err != nil {
return "", err
}
childURI = strings.TrimSpace(childURI)
if childURI == "" || strings.Contains(childURI, "\\") {
return "", errors.New("invalid HEVC child playlist")
}
child, err := url.Parse(childURI)
if err != nil ||
child.IsAbs() ||
child.Host != "" ||
child.User != nil ||
child.RawQuery != "" ||
child.Fragment != "" ||
child.Path == "" {
return "", errors.New("invalid HEVC child playlist")
}
parentNamespace := knownSourceNamespace(parentSource)
var resolved string
if strings.HasPrefix(child.Path, "/") {
resolved = strings.TrimLeft(child.Path, "/")
childNamespace := knownSourceNamespace(resolved)
switch parentNamespace {
case "", "sp":
// A path without a reserved prefix still resolves to the default
// SP origin. Explicitly switching to another origin is forbidden.
if childNamespace != "" && childNamespace != "sp" {
return "", errors.New("HEVC child playlist changes source namespace")
}
default:
// For non-default origins the source namespace is encoded in the
// route path. A plain root-relative URI cannot preserve that origin,
// so require the playlist to name the same namespace explicitly.
if childNamespace != parentNamespace {
return "", errors.New("ambiguous HEVC root-relative child playlist")
}
}
} else {
if parentNamespace != "" && childEscapesSourceNamespace(parentSource, child.Path) {
return "", errors.New("HEVC child playlist escapes source namespace")
}
resolved = path.Join("/", path.Dir(parentSource), child.Path)
}
resolved, err = NormalizeSource(strings.TrimLeft(resolved, "/"))
if err != nil {
return "", err
}
if !strings.HasPrefix(child.Path, "/") &&
parentNamespace != "" &&
knownSourceNamespace(resolved) != parentNamespace {
return "", errors.New("HEVC child playlist escapes source namespace")
}
return resolved, nil
}
func childEscapesSourceNamespace(parentSource, childPath string) bool {
parentDir := strings.Trim(path.Dir(parentSource), "/")
depth := 0
if parentDir != "" && parentDir != "." {
depth = len(strings.Split(parentDir, "/"))
}
for _, segment := range strings.Split(childPath, "/") {
switch segment {
case "", ".":
continue
case "..":
if depth <= 1 {
return true
}
depth--
default:
depth++
}
}
return false
}
func knownSourceNamespace(source string) string {
first, _, _ := strings.Cut(strings.TrimLeft(source, "/"), "/")
switch first {
case "sp", "pms", "laosiji", "v1", "v2", "v3":
return first
default:
return ""
}
}
// SignURL signs an exact source path for the cloud transcoder. The returned
// URL contains only the expiry and signature query parameters.
func SignURL(rawURL, secret string, expiresAt time.Time) (string, error) {
secret = strings.TrimSpace(secret)
if len([]byte(secret)) < minSecretBytes {
return "", ErrInvalidSecret
}
parsed, err := url.Parse(strings.TrimSpace(rawURL))
if err != nil {
return "", err
}
if parsed.Scheme == "" || parsed.Host == "" || parsed.EscapedPath() == "" {
return "", errors.New("invalid HEVC pull URL")
}
if parsed.RawQuery != "" || parsed.Fragment != "" {
return "", errors.New("HEVC pull URL must not contain query or fragment")
}
expires := expiresAt.UTC().Unix()
if expires <= 0 {
return "", errors.New("invalid HEVC pull expiry")
}
query := parsed.Query()
query.Set(ExpiresParam, strconv.FormatInt(expires, 10))
query.Set(SignatureParam, signature(parsed.EscapedPath(), expires, secret))
parsed.RawQuery = query.Encode()
return parsed.String(), nil
}
// VerifyURL validates expiry, exact path binding, and the HMAC signature.
func VerifyURL(parsed *url.URL, secret string, now time.Time) error {
secret = strings.TrimSpace(secret)
if len([]byte(secret)) < minSecretBytes {
log.Error("VerifyURL fail 1")
return ErrInvalidSecret
}
if parsed == nil || parsed.EscapedPath() == "" {
log.Error("VerifyURL fail 2", log.Any("parsed", parsed))
return ErrInvalidSignature
}
query := parsed.Query()
if len(query) != 2 ||
len(query[ExpiresParam]) != 1 ||
len(query[SignatureParam]) != 1 {
log.Error("VerifyURL fail 3", log.Any("query", query))
return ErrInvalidSignature
}
expires, err := strconv.ParseInt(query.Get(ExpiresParam), 10, 64)
if err != nil {
log.Error("VerifyURL fail 4", log.E(err))
return ErrInvalidSignature
}
now = now.UTC()
expiresAt := time.Unix(expires, 0).UTC()
if !expiresAt.After(now) {
log.Error("VerifyURL fail 5", log.Any("expires", expires))
return ErrExpired
}
if expiresAt.After(now.Add(maxSignatureTTL)) {
log.Error("VerifyURL fail 6", log.Any("expires", expires))
return ErrInvalidSignature
}
provided, err := hex.DecodeString(query.Get(SignatureParam))
if err != nil {
log.Error("VerifyURL fail 7", log.E(err))
return ErrInvalidSignature
}
expected, err := hex.DecodeString(signature(parsed.EscapedPath(), expires, secret))
if err != nil || !hmac.Equal(provided, expected) {
log.Error("VerifyURL fail 8", log.Any("expires", expires))
return ErrInvalidSignature
}
return nil
}
// RedactText removes signed pull bearer values from logs and diagnostics,
// including when the nested URL has been query-escaped by another API.
func RedactText(text string) string {
return signatureTextPattern.ReplaceAllString(text, `${1}[REDACTED]`)
}
// RedactURL returns a diagnostic form of a signed pull URL that is safe to
// persist. It is not a usable pull URL.
func RedactURL(rawURL string) string {
parsed, err := url.Parse(strings.TrimSpace(rawURL))
if err != nil {
return RedactText(rawURL)
}
query := parsed.Query()
if query.Has(SignatureParam) {
query.Set(SignatureParam, "[REDACTED]")
parsed.RawQuery = query.Encode()
}
return parsed.String()
}
func signature(escapedPath string, expires int64, secret string) string {
canonical := fmt.Sprintf("%s\n%d", escapedPath, expires)
mac := hmac.New(sha256.New, []byte(secret))
_, _ = mac.Write([]byte(canonical))
return hex.EncodeToString(mac.Sum(nil))
}
+176
View File
@@ -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)
}
}
}