feat: add HLS playlist rewriting to proxy stream
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user