diff --git a/internal/playback/handler/handler.go b/internal/playback/handler/handler.go index 7330e62..bbf7dd6 100644 --- a/internal/playback/handler/handler.go +++ b/internal/playback/handler/handler.go @@ -9,6 +9,7 @@ import ( "mal/internal/server" netutil "mal/pkg/net" "net/http" + "net/url" "strconv" "strings" "time" @@ -282,7 +283,7 @@ func (h *PlaybackHandler) HandleEpisodeThumbnails(c *gin.Context) { } func (h *PlaybackHandler) HandleProxyStream(c *gin.Context) { - targetURL, referer, ok := h.resolveProxyRequestTarget(c) + targetURL, referer, ok := h.resolveProxyRequestTarget(c, "stream") if !ok { return } @@ -306,11 +307,117 @@ func (h *PlaybackHandler) HandleProxyStream(c *gin.Context) { } defer func() { _ = resp.Body.Close() }() + if isHLSPlaylistResponse(targetURL, resp.Header) { + body, err := io.ReadAll(io.LimitReader(resp.Body, netutil.MiB2)) + if err != nil { + c.Status(http.StatusBadGateway) + return + } + rewritten, err := h.rewriteHLSPlaylist(string(body), targetURL, referer) + if err != nil { + c.Status(http.StatusBadGateway) + return + } + copyProxyHeaders(c.Writer.Header(), resp.Header) + c.Writer.Header().Del("Content-Length") + c.Data(resp.StatusCode, "application/vnd.apple.mpegurl", []byte(rewritten)) + return + } + copyProxyHeaders(c.Writer.Header(), resp.Header) c.Status(resp.StatusCode) _, _ = io.Copy(c.Writer, resp.Body) } +func isHLSPlaylistResponse(targetURL string, headers http.Header) bool { + contentType := strings.ToLower(headers.Get("Content-Type")) + if strings.Contains(contentType, "mpegurl") || strings.Contains(contentType, "x-mpegurl") { + return true + } + + parsed, err := url.Parse(targetURL) + if err != nil { + return strings.Contains(strings.ToLower(targetURL), ".m3u8") + } + return strings.Contains(strings.ToLower(parsed.Path), ".m3u8") +} + +func (h *PlaybackHandler) rewriteHLSPlaylist(body string, playlistURL string, referer string) (string, error) { + baseURL, err := url.Parse(playlistURL) + if err != nil { + return "", err + } + + lines := strings.SplitAfter(body, "\n") + var out strings.Builder + for _, line := range lines { + lineBody := strings.TrimSuffix(line, "\n") + newline := "" + if strings.HasSuffix(line, "\n") { + newline = "\n" + lineBody = strings.TrimSuffix(lineBody, "\r") + if strings.HasSuffix(line, "\r\n") { + newline = "\r\n" + } + } + + trimmed := strings.TrimSpace(lineBody) + rewritten := lineBody + if trimmed != "" { + if strings.HasPrefix(trimmed, "#") { + rewritten, err = h.rewriteHLSQuotedURIs(lineBody, baseURL, referer) + } else { + rewritten, err = h.proxyPlaylistURI(trimmed, baseURL, referer) + } + if err != nil { + return "", err + } + } + out.WriteString(rewritten) + out.WriteString(newline) + } + + return out.String(), nil +} + +func (h *PlaybackHandler) rewriteHLSQuotedURIs(line string, baseURL *url.URL, referer string) (string, error) { + const marker = `URI="` + var out strings.Builder + remaining := line + for { + idx := strings.Index(remaining, marker) + if idx < 0 { + out.WriteString(remaining) + return out.String(), nil + } + out.WriteString(remaining[:idx+len(marker)]) + remaining = remaining[idx+len(marker):] + end := strings.Index(remaining, `"`) + if end < 0 { + out.WriteString(remaining) + return out.String(), nil + } + proxied, err := h.proxyPlaylistURI(remaining[:end], baseURL, referer) + if err != nil { + return "", err + } + out.WriteString(proxied) + remaining = remaining[end:] + } +} + +func (h *PlaybackHandler) proxyPlaylistURI(rawURI string, baseURL *url.URL, referer string) (string, error) { + target, err := baseURL.Parse(rawURI) + if err != nil { + return "", err + } + token, err := h.svc.SignProxyToken(target.String(), referer, "stream") + if err != nil { + return "", err + } + return "/watch/proxy/stream?token=" + url.QueryEscape(token), nil +} + func copyProxyHeaders(dst http.Header, src http.Header) { // Skip hop-by-hop headers; see RFC 7230 section 6.1. // We intentionally preserve multi-value headers by copying the full slice. @@ -326,14 +433,14 @@ func copyProxyHeaders(dst http.Header, src http.Header) { } } -func (h *PlaybackHandler) resolveProxyRequestTarget(c *gin.Context) (string, string, bool) { +func (h *PlaybackHandler) resolveProxyRequestTarget(c *gin.Context, scope string) (string, string, bool) { token := c.Query("token") if token == "" { c.Status(http.StatusBadRequest) return "", "", false } - targetURL, referer, err := h.svc.ResolveProxyToken(token) + targetURL, referer, err := h.svc.ResolveProxyToken(token, scope) if err != nil { c.Status(http.StatusForbidden) return "", "", false @@ -357,7 +464,7 @@ func newProxyRequest(ctx context.Context, targetURL string, referer string) (*ht } func (h *PlaybackHandler) HandleProxySubtitle(c *gin.Context) { - targetURL, referer, ok := h.resolveProxyRequestTarget(c) + targetURL, referer, ok := h.resolveProxyRequestTarget(c, "subtitle") if !ok { return }