mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-03 20:26:36 +08:00
fix(emby): 网盘直链绑定 UA,第三方播放器保留同源一跳
115 直链与换取时的 User-Agent 逐字符绑定,换错一律 403。服务端换链用的是邮件客户端 API 层的 UA,而真正拉流的是播放器组件,RodelPlayer 2.2610 两层 UA 不一致(API 层带系统后缀)后会拿到 403 直链,表现为播放失败。非浏览器客户端改为 302 到同源 strm 端点,由播放器自己换链;浏览器仍走服务端换链一跳。
This commit is contained in:
@@ -145,6 +145,43 @@ func TestServeFileFallsBackWhenDirectResolveUnavailable(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 客户端 API 层与播放器层 UA 不一致时(原生播放器普遍如此)不能由服务端换链后
|
||||
// 直接 302 到 CDN:115 直链绑定换取时的 UA,换错就是 403。此时必须回退到同源的
|
||||
// strm 端点跳转,让播放器用自己的 UA 换链。
|
||||
func TestServeFileWithClientSideResolveKeepsSTRMEndpoint(t *testing.T) {
|
||||
repos := directRedirectTestRepo(t)
|
||||
seedCloudSTRMMedia(t, repos, "cloud-client-side", "/api/strm/play/cloud115/video.mkv?acct=a1&pickcode=pc1")
|
||||
direct := "https://cdnfhnfile.115cdn.net/637b/Movie.mkv?t=1&k=sig"
|
||||
resolveCalls := 0
|
||||
svc := NewStreamService(&config.Config{}, zap.NewNop(), repos, nil).
|
||||
SetStrmPlayTargetResolver(func(_ context.Context, raw, userAgent string) (*StrmPlayResult, error) {
|
||||
resolveCalls++
|
||||
return &StrmPlayResult{
|
||||
RedirectURL: direct,
|
||||
Link: &cloud.DirectLink{URL: direct, Headers: map[string]string{"User-Agent": userAgent}},
|
||||
}, nil
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "http://nas.local:18080/emby/Videos/cloud-client-side/stream?api_key=jwt123", nil)
|
||||
req.Header.Set("User-Agent", "RodelPlayer/2.2610.2.0 (Windows NT 10.0.26200; x64)")
|
||||
req = req.WithContext(WithClientSideSTRMResolve(req.Context()))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
if err := svc.ServeFileWithCloudMode(w, req, "cloud-client-side", CloudPlaybackModeRedirectProxy); err != nil {
|
||||
t.Fatalf("ServeFileWithCloudMode: %v", err)
|
||||
}
|
||||
if resolveCalls != 0 {
|
||||
t.Fatalf("resolver called %d times, want 0", resolveCalls)
|
||||
}
|
||||
loc := w.Header().Get("Location")
|
||||
if !strings.Contains(loc, "/api/strm/play/cloud115/video.mkv") {
|
||||
t.Fatalf("Location = %q, want the same-origin strm endpoint", loc)
|
||||
}
|
||||
if strings.Contains(loc, "cdnfhnfile") {
|
||||
t.Fatalf("Location = %q, must not point straight at the CDN", loc)
|
||||
}
|
||||
}
|
||||
|
||||
// 没有注入解析器(测试/精简部署)时保持原有跳转,不受本次优化影响。
|
||||
func TestServeFileKeepsSTRMEndpointWithoutResolver(t *testing.T) {
|
||||
repos := directRedirectTestRepo(t)
|
||||
|
||||
@@ -86,6 +86,36 @@ func setCloudRedirectNoStore(w http.ResponseWriter) {
|
||||
// 由 /api/strm/play 再去换链,最坏情况只是回到改动前的行为。
|
||||
const directPlayResolveTimeout = 10 * time.Second
|
||||
|
||||
// clientSideSTRMResolveKey 标记「本次播放由客户端自己换取 strm 直链」。
|
||||
type clientSideSTRMResolveKey struct{}
|
||||
|
||||
// WithClientSideSTRMResolve 让本次播放跳过「服务端换链后直接 302 到 CDN」的短路,
|
||||
// 改为 302 到同源的 strm 播放端点,由客户端自己的请求去换取直链。
|
||||
//
|
||||
// 为什么需要它:115 等网盘的直链与「换取直链时提交的 User-Agent」逐个字符严格
|
||||
// 绑定,换错一个字符 CDN 一律 403。服务端短路换链用的是当前请求的 UA,而真正
|
||||
// 拉流的是客户端的播放器组件;当客户端的 API 层与播放器层 UA 不一致时
|
||||
// (例如小幻影视 / RodelPlayer:API 请求带 "(Windows NT 10.0.26200; x64)" 后缀、
|
||||
// 播放器拉流仍是裸 UA),客户端会拿到与自身 UA 不匹配的直链,CDN 返回 403,
|
||||
// 表现为「播放失败」。交给客户端自己请求同源端点换链,换链用的就是播放器真实 UA。
|
||||
//
|
||||
// 浏览器(含 WebView / Electron)的 XHR 与 <video> 拉流必然是同一个 UA,
|
||||
// 不需要走这条路径。
|
||||
func WithClientSideSTRMResolve(ctx context.Context) context.Context {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
return context.WithValue(ctx, clientSideSTRMResolveKey{}, true)
|
||||
}
|
||||
|
||||
func clientSideSTRMResolve(ctx context.Context) bool {
|
||||
if ctx == nil {
|
||||
return false
|
||||
}
|
||||
enabled, _ := ctx.Value(clientSideSTRMResolveKey{}).(bool)
|
||||
return enabled
|
||||
}
|
||||
|
||||
// resolveDirectPlayTargetURL 尝试在服务端把 strm 目标解析成客户端可直接拉取的
|
||||
// 最终直链,供调用方直接 302。
|
||||
//
|
||||
@@ -96,6 +126,11 @@ func (s *StreamService) resolveDirectPlayTargetURL(r *http.Request, raw string)
|
||||
if s == nil || s.strmResolve == nil || r == nil {
|
||||
return "", false
|
||||
}
|
||||
// 调用方声明该客户端的拉流 UA 与本次请求 UA 不保证一致(见
|
||||
// WithClientSideSTRMResolve):服务端换出来的直链会绑定错误的 UA,必须放弃短路。
|
||||
if clientSideSTRMResolve(r.Context()) {
|
||||
return "", false
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), directPlayResolveTimeout)
|
||||
defer cancel()
|
||||
// 必须带播放器 UA:115 直链绑定换取时的 UA,且按 UA 分键缓存,换错会拿到
|
||||
|
||||
Reference in New Issue
Block a user