From 0ff2aaec601eba55b28b161465af4d651f9e7487 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sun, 22 Feb 2026 01:30:06 +0000 Subject: [PATCH] [ci skip] Add native HLS playback for VIPLeague/DaddyLive streams (v1.3.1) - Add HLS proxy (hlsproxy) for rewriting m3u8 playlists and proxying segments with correct Referer/Origin headers (uses ?domain= param) - Add playerconfig service for detecting stream types (VIPLeague, DaddyLive, HLS) and extracting auth params from ksohls pages - Add VIPLeague URL resolution: extract slug from URL path, match against DaddyLive 24/7 channel index with token-based scoring - Replace Clappr with direct HLS.js player for better compatibility - Add CryptoJS CDN for DaddyLive auth module support - Disable CrowdSec on f1-stream ingress to prevent false positives - Bump image to v1.3.1 --- .../files/internal/hlsproxy/hlsproxy.go | 209 +++++++ .../internal/playerconfig/playerconfig.go | 512 ++++++++++++++++++ .../f1-stream/files/internal/proxy/proxy.go | 29 +- .../f1-stream/files/internal/server/server.go | 47 +- modules/kubernetes/f1-stream/files/main.go | 4 +- .../f1-stream/files/static/css/custom.css | 9 + .../f1-stream/files/static/index.html | 3 + .../f1-stream/files/static/js/player.js | 216 ++++++++ .../f1-stream/files/static/js/streams.js | 58 +- modules/kubernetes/f1-stream/main.tf | 13 +- 10 files changed, 1049 insertions(+), 51 deletions(-) create mode 100644 modules/kubernetes/f1-stream/files/internal/hlsproxy/hlsproxy.go create mode 100644 modules/kubernetes/f1-stream/files/internal/playerconfig/playerconfig.go create mode 100644 modules/kubernetes/f1-stream/files/static/js/player.js diff --git a/modules/kubernetes/f1-stream/files/internal/hlsproxy/hlsproxy.go b/modules/kubernetes/f1-stream/files/internal/hlsproxy/hlsproxy.go new file mode 100644 index 00000000..24410af7 --- /dev/null +++ b/modules/kubernetes/f1-stream/files/internal/hlsproxy/hlsproxy.go @@ -0,0 +1,209 @@ +package hlsproxy + +import ( + "bufio" + "encoding/base64" + "io" + "log" + "net/http" + "net/url" + "strings" +) + +// NewHandler returns an http.Handler for /hls/{base64url_encoded_full_url}. +// It proxies HLS playlists and segments, rewriting m3u8 URLs to route +// through the proxy and forwarding X-Hls-Forward-* headers upstream. +func NewHandler() http.Handler { + client := &http.Client{ + Timeout: 30_000_000_000, // 30s + CheckRedirect: func(req *http.Request, via []*http.Request) error { + if len(via) >= 5 { + return http.ErrUseLastResponse + } + return nil + }, + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodOptions { + setCORS(w) + w.WriteHeader(http.StatusNoContent) + return + } + + // Parse: /hls/{base64url_encoded_full_url} + trimmed := strings.TrimPrefix(r.URL.Path, "/hls/") + if trimmed == "" || trimmed == r.URL.Path { + http.Error(w, "bad hls proxy URL", http.StatusBadRequest) + return + } + + // Decode the full upstream URL from base64url + upstreamURL, err := base64.RawURLEncoding.DecodeString(trimmed) + if err != nil { + http.Error(w, "invalid base64url", http.StatusBadRequest) + return + } + target := string(upstreamURL) + + parsed, err := url.Parse(target) + if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") { + http.Error(w, "invalid upstream URL", http.StatusBadRequest) + return + } + + log.Printf("hlsproxy: %s -> %s", r.URL.Path, target) + + upReq, err := http.NewRequestWithContext(r.Context(), http.MethodGet, target, nil) + if err != nil { + http.Error(w, "failed to create request", http.StatusInternalServerError) + return + } + + // Set Referer and Origin. If the URL has a ?domain= param (CDN segments), + // use that domain as the origin so the CDN accepts the request. + refererOrigin := parsed.Scheme + "://" + parsed.Host + if domainParam := parsed.Query().Get("domain"); domainParam != "" { + refererOrigin = "https://" + domainParam + } + upReq.Header.Set("Referer", refererOrigin+"/") + upReq.Header.Set("Origin", refererOrigin) + upReq.Header.Set("User-Agent", r.Header.Get("User-Agent")) + + // Forward X-Hls-Forward-* headers (strip prefix) + for key, vals := range r.Header { + if strings.HasPrefix(key, "X-Hls-Forward-") { + realKey := strings.TrimPrefix(key, "X-Hls-Forward-") + for _, v := range vals { + upReq.Header.Set(realKey, v) + } + } + } + + resp, err := client.Do(upReq) + if err != nil { + log.Printf("hlsproxy: upstream fetch failed: %v", err) + http.Error(w, "upstream fetch failed", http.StatusBadGateway) + return + } + defer resp.Body.Close() + + log.Printf("hlsproxy: %s <- %d (%s)", truncPath(r.URL.Path, 60), resp.StatusCode, resp.Header.Get("Content-Type")) + + setCORS(w) + + ct := resp.Header.Get("Content-Type") + isM3U8 := strings.Contains(ct, "mpegurl") || + strings.Contains(ct, "x-mpegURL") || + strings.HasSuffix(parsed.Path, ".m3u8") + + if isM3U8 { + w.Header().Set("Content-Type", "application/vnd.apple.mpegurl") + w.WriteHeader(resp.StatusCode) + rewriteM3U8(w, resp.Body, target) + return + } + + // Stream segment or other content directly + for key, vals := range resp.Header { + lk := strings.ToLower(key) + if lk == "content-type" || lk == "content-length" || lk == "cache-control" || lk == "accept-ranges" { + for _, v := range vals { + w.Header().Add(key, v) + } + } + } + w.WriteHeader(resp.StatusCode) + io.Copy(w, resp.Body) + }) +} + +// rewriteM3U8 reads an m3u8 playlist from r, rewrites segment/playlist URLs +// to route through /hls/{b64}, and writes the result to w. +func rewriteM3U8(w io.Writer, r io.Reader, playlistURL string) { + base, err := url.Parse(playlistURL) + if err != nil { + io.Copy(w, r) + return + } + + scanner := bufio.NewScanner(r) + scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) + + for scanner.Scan() { + line := scanner.Text() + + if strings.HasPrefix(line, "#") { + // Rewrite URI="..." in directives like #EXT-X-KEY, #EXT-X-MAP + rewritten := rewriteURIAttribute(line, base) + w.Write([]byte(rewritten)) + w.Write([]byte("\n")) + continue + } + + // Non-comment, non-empty lines are URLs + trimmed := strings.TrimSpace(line) + if trimmed == "" { + w.Write([]byte("\n")) + continue + } + + resolved := resolveURL(base, trimmed) + encoded := encodeHLSURL(resolved) + w.Write([]byte(encoded)) + w.Write([]byte("\n")) + } +} + +// rewriteURIAttribute rewrites URI="..." attributes in HLS directives. +func rewriteURIAttribute(line string, base *url.URL) string { + // Look for URI="..." (case insensitive) + uriIdx := strings.Index(strings.ToUpper(line), "URI=\"") + if uriIdx == -1 { + return line + } + + // Find the actual position (preserving original case) + prefix := line[:uriIdx+5] // everything up to and including URI=" + rest := line[uriIdx+5:] + endQuote := strings.Index(rest, "\"") + if endQuote == -1 { + return line + } + + uri := rest[:endQuote] + suffix := rest[endQuote:] // closing quote and anything after + + resolved := resolveURL(base, uri) + encoded := encodeHLSURL(resolved) + + return prefix + encoded + suffix +} + +// resolveURL resolves a potentially relative URL against a base URL. +func resolveURL(base *url.URL, ref string) string { + refURL, err := url.Parse(ref) + if err != nil { + return ref + } + return base.ResolveReference(refURL).String() +} + +// encodeHLSURL encodes a full URL into /hls/{base64url} format. +func encodeHLSURL(fullURL string) string { + encoded := base64.RawURLEncoding.EncodeToString([]byte(fullURL)) + return "/hls/" + encoded +} + +func setCORS(w http.ResponseWriter) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "*") +} + +func truncPath(s string, n int) string { + if len(s) <= n { + return s + } + return s[:n] + "..." +} diff --git a/modules/kubernetes/f1-stream/files/internal/playerconfig/playerconfig.go b/modules/kubernetes/f1-stream/files/internal/playerconfig/playerconfig.go new file mode 100644 index 00000000..3ebbb549 --- /dev/null +++ b/modules/kubernetes/f1-stream/files/internal/playerconfig/playerconfig.go @@ -0,0 +1,512 @@ +package playerconfig + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "regexp" + "strings" + "sync" + "time" +) + +// PlayerConfig is returned by the /api/streams/{id}/player-config endpoint. +type PlayerConfig struct { + Type string `json:"type"` + HLSURL string `json:"hls_url,omitempty"` + AuthToken string `json:"auth_token,omitempty"` + ChannelKey string `json:"channel_key,omitempty"` + ChannelSalt string `json:"channel_salt,omitempty"` + Timestamp string `json:"timestamp,omitempty"` + AuthModURL string `json:"auth_mod_url,omitempty"` + ServerKey string `json:"server_key,omitempty"` + Error string `json:"error,omitempty"` +} + +type cacheEntry struct { + config *PlayerConfig + expiresAt time.Time +} + +// Service handles stream type detection and DaddyLive config extraction. +type Service struct { + client *http.Client + mu sync.RWMutex + cache map[string]*cacheEntry +} + +// New creates a new playerconfig Service. +func New() *Service { + return &Service{ + client: &http.Client{ + Timeout: 15 * time.Second, + }, + cache: make(map[string]*cacheEntry), + } +} + +// DetectStreamType returns "hls", "daddylive", "vipleague", or "proxy" based on the URL. +func DetectStreamType(rawURL string) string { + lower := strings.ToLower(rawURL) + + if strings.HasSuffix(strings.SplitN(lower, "?", 2)[0], ".m3u8") { + return "hls" + } + + daddyPatterns := []string{"dlhd.link", "dlhd.sx", "dlhd.dad", "daddylive", "ksohls.ru"} + for _, p := range daddyPatterns { + if strings.Contains(lower, p) { + return "daddylive" + } + } + + vipPatterns := []string{"vipleague.io", "vipleague.im", "vipleague.cc", "casthill.net"} + for _, p := range vipPatterns { + if strings.Contains(lower, p) { + return "vipleague" + } + } + + return "proxy" +} + +// GetConfig returns a PlayerConfig for the given stream URL. +func (s *Service) GetConfig(ctx context.Context, rawURL string) *PlayerConfig { + streamType := DetectStreamType(rawURL) + + switch streamType { + case "hls": + encoded := base64.RawURLEncoding.EncodeToString([]byte(rawURL)) + return &PlayerConfig{ + Type: "hls", + HLSURL: "/hls/" + encoded, + } + + case "daddylive": + return s.getDaddyLiveConfig(ctx, rawURL) + + case "vipleague": + return s.getVIPLeagueConfig(ctx, rawURL) + + default: + return &PlayerConfig{Type: "proxy"} + } +} + +// Channel ID extraction patterns +var channelIDPatterns = []*regexp.Regexp{ + regexp.MustCompile(`stream-(\d+)\.php`), + regexp.MustCompile(`[?&]id=(\d+)`), + regexp.MustCompile(`/(\d+)\.php`), +} + +// Page content extraction patterns +var ( + iframeRe = regexp.MustCompile(`]*src=["'](https?://[^"']*ksohls\.ru[^"']*)["']`) + authTokenRe = regexp.MustCompile(`authToken\s*[:=]\s*['"]([^'"]+)['"]`) + channelKeyRe = regexp.MustCompile(`channelKey\s*[:=]\s*['"]([^'"]+)['"]`) + channelSaltRe = regexp.MustCompile(`channelSalt\s*[:=]\s*['"]([^'"]+)['"]`) + timestampRe = regexp.MustCompile(`timestamp\s*[:=]\s*['"]?(\d+)['"]?`) + authModRe = regexp.MustCompile(`]*src=["'](https?://[^"']*aiaged\.fun[^"']*obfuscated[^"']*)["']`) +) + +func (s *Service) getDaddyLiveConfig(ctx context.Context, rawURL string) *PlayerConfig { + // Check cache + s.mu.RLock() + if entry, ok := s.cache[rawURL]; ok && time.Now().Before(entry.expiresAt) { + s.mu.RUnlock() + return entry.config + } + s.mu.RUnlock() + + config := s.fetchDaddyLiveConfig(ctx, rawURL) + + // Cache the result (even errors, to avoid hammering) + s.mu.Lock() + s.cache[rawURL] = &cacheEntry{ + config: config, + expiresAt: time.Now().Add(1 * time.Hour), + } + s.mu.Unlock() + + return config +} + +func (s *Service) fetchDaddyLiveConfig(ctx context.Context, rawURL string) *PlayerConfig { + // Step 1: Extract channel ID from URL + channelID := "" + for _, re := range channelIDPatterns { + if m := re.FindStringSubmatch(rawURL); len(m) > 1 { + channelID = m[1] + break + } + } + if channelID == "" { + return &PlayerConfig{Type: "proxy", Error: "could not extract channel ID"} + } + + log.Printf("playerconfig: DaddyLive channel=%s from %s", channelID, rawURL) + return s.fetchDaddyLiveConfigByID(ctx, channelID) +} + +func (s *Service) fetchDaddyLiveConfigByID(ctx context.Context, channelID string) *PlayerConfig { + // Step 2: Fetch the cast page to find the ksohls iframe + castURL := fmt.Sprintf("https://dlhd.link/cast/stream-%s.php", channelID) + castBody, err := s.fetchPage(ctx, castURL, "https://dlhd.link/") + if err != nil { + log.Printf("playerconfig: failed to fetch cast page: %v", err) + return &PlayerConfig{Type: "proxy", Error: "failed to fetch cast page"} + } + + // Step 3: Extract ksohls iframe URL + iframeMatch := iframeRe.FindStringSubmatch(castBody) + if iframeMatch == nil { + log.Printf("playerconfig: no ksohls iframe found in cast page") + return &PlayerConfig{Type: "proxy", Error: "no ksohls iframe found"} + } + ksohURL := iframeMatch[1] + + // Step 4: Fetch the ksohls page + referer := fmt.Sprintf("https://dlhd.link/stream/stream-%s.php", channelID) + ksohBody, err := s.fetchPage(ctx, ksohURL, referer) + if err != nil { + log.Printf("playerconfig: failed to fetch ksohls page: %v", err) + return &PlayerConfig{Type: "proxy", Error: "failed to fetch ksohls page"} + } + + // Step 5: Extract auth params from ksohls page + config := &PlayerConfig{Type: "daddylive"} + + if m := authTokenRe.FindStringSubmatch(ksohBody); len(m) > 1 { + config.AuthToken = m[1] + } + if m := channelKeyRe.FindStringSubmatch(ksohBody); len(m) > 1 { + config.ChannelKey = m[1] + } + if m := channelSaltRe.FindStringSubmatch(ksohBody); len(m) > 1 { + config.ChannelSalt = m[1] + } + if m := timestampRe.FindStringSubmatch(ksohBody); len(m) > 1 { + config.Timestamp = m[1] + } + if m := authModRe.FindStringSubmatch(ksohBody); len(m) > 1 { + config.AuthModURL = m[1] + } + + if config.ChannelKey == "" { + log.Printf("playerconfig: no channelKey found in ksohls page") + return &PlayerConfig{Type: "proxy", Error: "no channelKey found"} + } + + // Step 6: Server lookup + lookupURL := fmt.Sprintf("https://chevy.soyspace.cyou/server_lookup?channel_id=%s", config.ChannelKey) + lookupBody, err := s.fetchPage(ctx, lookupURL, "") + if err != nil { + log.Printf("playerconfig: server lookup failed: %v", err) + return &PlayerConfig{Type: "proxy", Error: "server lookup failed"} + } + + var lookupResp struct { + ServerKey string `json:"server_key"` + } + if err := json.Unmarshal([]byte(lookupBody), &lookupResp); err != nil || lookupResp.ServerKey == "" { + log.Printf("playerconfig: failed to parse server lookup: %v body=%s", err, lookupBody) + return &PlayerConfig{Type: "proxy", Error: "server lookup parse failed"} + } + config.ServerKey = lookupResp.ServerKey + + // Step 7: Build m3u8 URL + m3u8URL := fmt.Sprintf("https://chevy.soyspace.cyou/proxy/%s/%s/mono.m3u8", + config.ServerKey, config.ChannelKey) + encoded := base64.RawURLEncoding.EncodeToString([]byte(m3u8URL)) + config.HLSURL = "/hls/" + encoded + + log.Printf("playerconfig: DaddyLive config ready channel=%s server=%s", config.ChannelKey, config.ServerKey) + return config +} + +// VIPLeague/casthill resolution + +var zmidRe = regexp.MustCompile(`(?:const|var|let)\s+zmid\s*=\s*["']([^"']+)["']`) +var casthillVRe = regexp.MustCompile(`[?&]v=([^&]+)`) + +func (s *Service) getVIPLeagueConfig(ctx context.Context, rawURL string) *PlayerConfig { + // Check cache using normalized URL + s.mu.RLock() + if entry, ok := s.cache[rawURL]; ok && time.Now().Before(entry.expiresAt) { + s.mu.RUnlock() + return entry.config + } + s.mu.RUnlock() + + config := s.fetchVIPLeagueConfig(ctx, rawURL) + + s.mu.Lock() + s.cache[rawURL] = &cacheEntry{ + config: config, + expiresAt: time.Now().Add(1 * time.Hour), + } + s.mu.Unlock() + + return config +} + +func (s *Service) fetchVIPLeagueConfig(ctx context.Context, rawURL string) *PlayerConfig { + lower := strings.ToLower(rawURL) + + var zmid string + + if strings.Contains(lower, "casthill.net") { + // Extract zmid from casthill URL query param ?v=... + if m := casthillVRe.FindStringSubmatch(rawURL); len(m) > 1 { + zmid = m[1] + } + } + + if zmid == "" { + // Try to fetch VIPLeague page and extract zmid from JavaScript + body, err := s.fetchPage(ctx, rawURL, "") + if err != nil { + log.Printf("playerconfig: failed to fetch VIPLeague page: %v, trying URL-based extraction", err) + } else { + if m := zmidRe.FindStringSubmatch(body); len(m) > 1 { + zmid = m[1] + } + } + } + + if zmid == "" { + // Fallback: extract slug from URL path and use it directly for channel matching + // e.g. /f-1/sky-sports-f1-streaming → "sky sports f1" + zmid = extractSlugFromURL(rawURL) + if zmid != "" { + log.Printf("playerconfig: extracted slug %q from URL path", zmid) + } + } + + if zmid == "" { + log.Printf("playerconfig: no zmid found for VIPLeague URL %s", rawURL) + return &PlayerConfig{Type: "proxy", Error: "no zmid found in VIPLeague page"} + } + + log.Printf("playerconfig: VIPLeague zmid=%q from %s", zmid, rawURL) + + channelID, err := s.resolveChannelID(ctx, zmid) + if err != nil { + log.Printf("playerconfig: failed to resolve zmid %q: %v", zmid, err) + return &PlayerConfig{Type: "proxy", Error: fmt.Sprintf("failed to resolve zmid: %v", err)} + } + + log.Printf("playerconfig: resolved zmid=%q to DaddyLive channel=%s", zmid, channelID) + return s.fetchDaddyLiveConfigByID(ctx, channelID) +} + +// extractSlugFromURL extracts a channel-matching slug from a VIPLeague URL path. +// e.g. "https://vipleague.io/f-1/sky-sports-f1-streaming" → "sky sports f1" +// Strips common suffixes like "-streaming", "-live-stream", "-live", etc. +func extractSlugFromURL(rawURL string) string { + // Get the last path segment + path := rawURL + if idx := strings.Index(path, "?"); idx != -1 { + path = path[:idx] + } + path = strings.TrimRight(path, "/") + lastSlash := strings.LastIndex(path, "/") + if lastSlash == -1 { + return "" + } + slug := path[lastSlash+1:] + + // Strip common suffixes + for _, suffix := range []string{"-streaming", "-live-stream", "-stream", "-live", "-online", "-free"} { + slug = strings.TrimSuffix(slug, suffix) + } + + // Replace hyphens with spaces for matching against channel names + slug = strings.ReplaceAll(slug, "-", " ") + slug = strings.TrimSpace(slug) + + if slug == "" || len(slug) < 3 { + return "" + } + return slug +} + +var channelLinkRe = regexp.MustCompile(`]*href=["'][^"']*watch\.php\?id=(\d+)["'][^>]*data-title=["']([^"']+)["']`) +var channelLinkRe2 = regexp.MustCompile(`]*data-title=["']([^"']+)["'][^>]*href=["'][^"']*watch\.php\?id=(\d+)["']`) + +func (s *Service) resolveChannelID(ctx context.Context, zmid string) (string, error) { + channels, err := s.getChannelIndex(ctx) + if err != nil { + return "", err + } + + zmidLower := strings.ToLower(zmid) + + // Build tokens: if zmid contains spaces, split on spaces; otherwise use tokenizer + var tokens []string + if strings.Contains(zmidLower, " ") { + for _, word := range strings.Fields(zmidLower) { + if len(word) >= 2 { + tokens = append(tokens, word) + } + } + } else { + tokens = tokenize(zmidLower) + } + + bestID := "" + bestScore := 0 + bestNameLen := 0 + + for id, name := range channels { + score := 0 + for _, tok := range tokens { + if strings.Contains(name, tok) { + score++ + } + } + // Tiebreaker: prefer shorter names (more specific match) and + // English/UK channels which tend to have shorter names + if score > bestScore || (score == bestScore && score > 0 && len(name) < bestNameLen) { + bestScore = score + bestID = id + bestNameLen = len(name) + } + } + + if bestID == "" || bestScore == 0 { + return "", fmt.Errorf("no channel matched zmid %q (tried %d channels)", zmid, len(channels)) + } + + log.Printf("playerconfig: zmid=%q matched channel %s (%s) with score %d/%d", + zmid, bestID, channels[bestID], bestScore, len(tokens)) + return bestID, nil +} + +func (s *Service) getChannelIndex(ctx context.Context) (map[string]string, error) { + const cacheKey = "__channel_index__" + + s.mu.RLock() + if entry, ok := s.cache[cacheKey]; ok && time.Now().Before(entry.expiresAt) { + s.mu.RUnlock() + // Decode from the Error field (ab)used as storage + var idx map[string]string + if err := json.Unmarshal([]byte(entry.config.Error), &idx); err == nil { + return idx, nil + } + } + s.mu.RUnlock() + + body, err := s.fetchPage(ctx, "https://dlhd.link/24-7-channels.php", "https://dlhd.link/") + if err != nil { + return nil, fmt.Errorf("failed to fetch channel index: %w", err) + } + + channels := make(map[string]string) + + // Try both attribute orderings + for _, m := range channelLinkRe.FindAllStringSubmatch(body, -1) { + channels[m[1]] = strings.ToLower(strings.TrimSpace(m[2])) + } + for _, m := range channelLinkRe2.FindAllStringSubmatch(body, -1) { + channels[m[2]] = strings.ToLower(strings.TrimSpace(m[1])) + } + + if len(channels) == 0 { + return nil, fmt.Errorf("no channels found in 24/7 page (%d bytes)", len(body)) + } + + log.Printf("playerconfig: loaded %d channels from DaddyLive 24/7 page", len(channels)) + + // Cache as JSON in a fake PlayerConfig entry + encoded, _ := json.Marshal(channels) + s.mu.Lock() + s.cache[cacheKey] = &cacheEntry{ + config: &PlayerConfig{Error: string(encoded)}, + expiresAt: time.Now().Add(6 * time.Hour), + } + s.mu.Unlock() + + return channels, nil +} + +// tokenize splits a zmid slug into meaningful tokens. +// e.g. "skyf1" -> ["sky", "f1"], "daznf1" -> ["dazn", "f1"] +func tokenize(zmid string) []string { + // Common known prefixes/suffixes in sports streaming slugs + knownTokens := []string{ + "sky", "sports", "f1", "dazn", "espn", "fox", "bein", "bt", + "star", "nbc", "cbs", "tnt", "abc", "tsn", "supersport", + "canal", "rtl", "viaplay", "premier", "main", "event", + "arena", "action", "cricket", "football", "tennis", "golf", + "racing", "news", "extra", "max", "hd", "uhd", + } + + var tokens []string + remaining := zmid + + for len(remaining) > 0 { + matched := false + for _, tok := range knownTokens { + if strings.HasPrefix(remaining, tok) { + tokens = append(tokens, tok) + remaining = remaining[len(tok):] + matched = true + break + } + } + if !matched { + // Try numeric suffix (like channel numbers) + i := 0 + for i < len(remaining) && remaining[i] >= '0' && remaining[i] <= '9' { + i++ + } + if i > 0 { + tokens = append(tokens, remaining[:i]) + remaining = remaining[i:] + } else { + // Skip single character and try again + remaining = remaining[1:] + } + } + } + + // If tokenization produced nothing useful, use the whole zmid as a single token + if len(tokens) == 0 { + tokens = []string{zmid} + } + + return tokens +} + +func (s *Service) fetchPage(ctx context.Context, pageURL, referer string) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, pageURL, nil) + if err != nil { + return "", err + } + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") + if referer != "" { + req.Header.Set("Referer", referer) + } + + resp, err := s.client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("status %d from %s", resp.StatusCode, pageURL) + } + + body, err := io.ReadAll(io.LimitReader(resp.Body, 2*1024*1024)) // 2MB max + if err != nil { + return "", err + } + return string(body), nil +} diff --git a/modules/kubernetes/f1-stream/files/internal/proxy/proxy.go b/modules/kubernetes/f1-stream/files/internal/proxy/proxy.go index d3785e0d..2796c929 100644 --- a/modules/kubernetes/f1-stream/files/internal/proxy/proxy.go +++ b/modules/kubernetes/f1-stream/files/internal/proxy/proxy.go @@ -85,7 +85,7 @@ var _ce=document.createElement.bind(document); document.createElement=function(t){ var el=_ce(t); var tag=t.toLowerCase(); -if(tag==='script'||tag==='iframe'||tag==='img'||tag==='link'||tag==='source'||tag==='video'||tag==='audio'){ +if(tag==='script'||tag==='img'||tag==='link'||tag==='source'||tag==='video'||tag==='audio'){ var _ss=Object.getOwnPropertyDescriptor(HTMLElement.prototype,'src')||Object.getOwnPropertyDescriptor(el.__proto__,'src'); if(_ss&&_ss.set){Object.defineProperty(el,'src',{get:function(){return _ss.get?_ss.get.call(this):'';},set:function(v){_ss.set.call(this,rw(v));},configurable:true});} } @@ -316,9 +316,6 @@ var rootRelativeAttrRe = regexp.MustCompile(`((?:src|href|action|poster|data)\s* // Matches url("/...") or url('/...') or url(/...) in inline styles — but NOT url("//...") var rootRelativeCSSRe = regexp.MustCompile(`(url\(\s*["']?)/([^/"')[^"')]*)(["']?\s*\))`) -// crossOriginIframeSrcRe matches