From 67bb1d01c87aad6cc3d21adf9c20a352adc72685 Mon Sep 17 00:00:00 2001 From: yvan Date: Fri, 18 Sep 2026 11:14:56 +0800 Subject: [PATCH] =?UTF-8?q?iOS=20=E6=92=AD=E6=94=BE=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=EF=BC=9A=E5=AA=92=E4=BD=93=E8=B5=84=E4=BA=A7=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=97=A0=20cookie=20=E6=8B=89=E6=B5=81?= =?UTF-8?q?=EF=BC=88=E4=BC=9A=E8=AF=9D=20ID=20=E5=8D=B3=E5=87=AD=E8=AF=81?= =?UTF-8?q?=EF=BC=89+=20=E6=8B=92=E7=BB=9D=E8=AF=8A=E6=96=AD=E6=97=A5?= =?UTF-8?q?=E5=BF=97=EF=BC=9B=E6=96=B0=E5=A2=9E=20caddy-djk=20HTTPS=20?= =?UTF-8?q?=E5=8F=8D=E4=BB=A3=E9=83=A8=E7=BD=B2=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 ++++ deploy/caddy-djk/Caddyfile | 14 ++++++++++++++ deploy/caddy-djk/Dockerfile | 6 ++++++ internal/app/playback_gateway.go | 8 +++++++- internal/app/ui_playback.go | 11 +++++++++++ internal/app/ui_playback_native.go | 8 +++++++- internal/app/ui_viewer.go | 12 ++++++++++++ 7 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 deploy/caddy-djk/Caddyfile create mode 100644 deploy/caddy-djk/Dockerfile diff --git a/.gitignore b/.gitignore index dab7297..1304b65 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ __pycache__/ *.log *.zip *.tar +*.tar.gz +_*.py +deploy/caddy-djk/deploy_nas.py +deploy/caddy-djk/Caddyfile.nas diff --git a/deploy/caddy-djk/Caddyfile b/deploy/caddy-djk/Caddyfile new file mode 100644 index 0000000..2bd4643 --- /dev/null +++ b/deploy/caddy-djk/Caddyfile @@ -0,0 +1,14 @@ +{ + email yvan@xyvan.cn +} + +djk2.xyvan.cn:31513 { + tls { + dns alidns { + access_key_id {$ALI_ID} + access_key_secret {$ALI_SECRET} + } + resolvers 223.5.5.5 + } + reverse_proxy 192.168.31.32:8998 +} diff --git a/deploy/caddy-djk/Dockerfile b/deploy/caddy-djk/Dockerfile new file mode 100644 index 0000000..81ddee6 --- /dev/null +++ b/deploy/caddy-djk/Dockerfile @@ -0,0 +1,6 @@ +FROM caddy:2-builder AS builder +ENV GOPROXY=https://goproxy.cn,direct +RUN xcaddy build --with github.com/caddy-dns/alidns + +FROM caddy:2 +COPY --from=builder /usr/bin/caddy /usr/bin/caddy diff --git a/internal/app/playback_gateway.go b/internal/app/playback_gateway.go index 9ca0a37..e5733c4 100644 --- a/internal/app/playback_gateway.go +++ b/internal/app/playback_gateway.go @@ -259,10 +259,16 @@ func (app *UIApp) handlePlaybackMediaAsset(writer http.ResponseWriter, request * return } run, err := strconv.ParseUint(parts[1], 10, 64) + viewer := contextViewer(request.Context()) app.playbackMu.Lock() session := app.playbacks[parts[0]] - if err != nil || !viewerOwnsPlayback(request.Context(), session) || session.run != run || session.media == nil { + denied := err != nil || session == nil || session.run != run || session.media == nil + if !denied && viewer != nil { + denied = !viewerOwnsPlayback(request.Context(), session) + } + if denied { app.playbackMu.Unlock() + app.recordPlaybackAssetDenial(request, "session-unavailable") writer.WriteHeader(http.StatusGone) return } diff --git a/internal/app/ui_playback.go b/internal/app/ui_playback.go index 89cc479..8848078 100644 --- a/internal/app/ui_playback.go +++ b/internal/app/ui_playback.go @@ -94,6 +94,17 @@ func (app *UIApp) registerPlaybackRoutes(mux *http.ServeMux) { mux.HandleFunc("/api/ui/playback/progress", app.handlePlaybackProgress) } +// recordPlaybackAssetDenial 记录媒体资产请求被拒的情况,便于诊断 +// iOS/WebKit 原生媒体管线的拉流失败(区分会话失效与观看者不匹配)。 +func (app *UIApp) recordPlaybackAssetDenial(request *http.Request, reason string) { + agent := request.UserAgent() + if len(agent) > 120 { + agent = agent[:120] + } + app.downloader.recordDiagnostic(diagnosticEvent{Level: "warn", Event: "playback.asset.denied", + HTTPStatus: http.StatusGone, Message: reason + " ua=" + agent}) +} + func playbackRequestAllowed(writer http.ResponseWriter, request *http.Request, method string) bool { writer.Header().Set("Cache-Control", "private, no-store, no-transform") writer.Header().Set("X-Accel-Buffering", "no") diff --git a/internal/app/ui_playback_native.go b/internal/app/ui_playback_native.go index ad06015..d535975 100644 --- a/internal/app/ui_playback_native.go +++ b/internal/app/ui_playback_native.go @@ -95,10 +95,16 @@ func (app *UIApp) handlePlaybackNativeAsset(writer http.ResponseWriter, request writeJSON(writer, http.StatusBadRequest, map[string]string{"error": "播放请求编号无效"}) return } + viewer := contextViewer(request.Context()) app.playbackMu.Lock() session := app.playbacks[query.Get("session")] - if !viewerOwnsPlayback(request.Context(), session) || session.run != run || session.native == nil { + denied := session == nil || session.run != run || session.native == nil + if !denied && viewer != nil { + denied = !viewerOwnsPlayback(request.Context(), session) + } + if denied { app.playbackMu.Unlock() + app.recordPlaybackAssetDenial(request, "stream-unavailable") writeJSON(writer, http.StatusGone, map[string]string{"error": "此播放流已结束,请重新选择分集"}) return } diff --git a/internal/app/ui_viewer.go b/internal/app/ui_viewer.go index c2706ae..a3e505c 100644 --- a/internal/app/ui_viewer.go +++ b/internal/app/ui_viewer.go @@ -207,6 +207,12 @@ func viewerOwnsPlayback(ctx context.Context, session *playbackSession) bool { return viewer != nil && session != nil && session.viewer == viewer && playbackSourceAllowed(ctx, session) } +// playbackCapabilityPath 判断是否为无需 viewer cookie 的媒体资产路径: +// 这些 URL 中已包含不可猜测的播放会话 ID,可作为独立能力凭证。 +func playbackCapabilityPath(path string) bool { + return strings.HasPrefix(path, "/api/ui/playback/media/") || path == "/api/ui/playback/hls/index.m3u8" || path == "/api/ui/playback/hls/segment.ts" +} + func writeViewerError(writer http.ResponseWriter, status int, code, message string) { writer.Header().Set("Cache-Control", "private, no-store") writer.Header().Add("Vary", "Cookie") @@ -234,6 +240,12 @@ func (app *UIApp) withBrowserViewer(next http.Handler) http.Handler { } identity := manager.identity(request) if account.ID == "" && (identity == "" || accountCookie) { + if playbackCapabilityPath(path) { + // AVPlayer 等原生媒体管线可能不携带 cookie;播放会话 ID 本身即能力凭证, + // 由媒体资产接口自行校验会话与运行编号。 + next.ServeHTTP(writer, request) + return + } writeViewerError(writer, http.StatusUnauthorized, "viewer_required", "请刷新页面,恢复此浏览器的独立观看记录") return }