mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-13 08:36:38 +08:00
enforce pure cloud 302 playback
This commit is contained in:
@@ -313,19 +313,15 @@ func Test115QRFlow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCloudDrive2WebDAVListAndResolve(t *testing.T) {
|
func TestCloudDrive2WebDAVListAndResolve(t *testing.T) {
|
||||||
var gotAuth, gotDepth string
|
var gotAuth, gotDepth, gotRange string
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
gotAuth = r.Header.Get("Authorization")
|
switch {
|
||||||
gotDepth = r.Header.Get("Depth")
|
case r.Method == "PROPFIND" && r.URL.Path == "/dav":
|
||||||
if r.Method != "PROPFIND" {
|
gotAuth = r.Header.Get("Authorization")
|
||||||
t.Fatalf("unexpected method %s", r.Method)
|
gotDepth = r.Header.Get("Depth")
|
||||||
}
|
w.Header().Set("Content-Type", "application/xml")
|
||||||
if r.URL.Path != "/dav" {
|
w.WriteHeader(http.StatusMultiStatus)
|
||||||
t.Fatalf("unexpected path %s", r.URL.Path)
|
_, _ = w.Write([]byte(`<?xml version="1.0" encoding="utf-8"?>
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/xml")
|
|
||||||
w.WriteHeader(http.StatusMultiStatus)
|
|
||||||
_, _ = w.Write([]byte(`<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<d:multistatus xmlns:d="DAV:">
|
<d:multistatus xmlns:d="DAV:">
|
||||||
<d:response>
|
<d:response>
|
||||||
<d:href>/dav/</d:href>
|
<d:href>/dav/</d:href>
|
||||||
@@ -340,6 +336,13 @@ func TestCloudDrive2WebDAVListAndResolve(t *testing.T) {
|
|||||||
<d:propstat><d:prop><d:displayname>Movie.mkv</d:displayname><d:getcontentlength>789</d:getcontentlength><d:resourcetype/></d:prop></d:propstat>
|
<d:propstat><d:prop><d:displayname>Movie.mkv</d:displayname><d:getcontentlength>789</d:getcontentlength><d:resourcetype/></d:prop></d:propstat>
|
||||||
</d:response>
|
</d:response>
|
||||||
</d:multistatus>`))
|
</d:multistatus>`))
|
||||||
|
case r.Method == http.MethodGet && r.URL.Path == "/dav/123/Movie.mkv":
|
||||||
|
gotAuth = r.Header.Get("Authorization")
|
||||||
|
gotRange = r.Header.Get("Range")
|
||||||
|
http.Redirect(w, r, "https://cdn.example.test/123/Movie.mkv?sign=1", http.StatusFound)
|
||||||
|
default:
|
||||||
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
@@ -370,14 +373,46 @@ func TestCloudDrive2WebDAVListAndResolve(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("resolve: %v", err)
|
t.Fatalf("resolve: %v", err)
|
||||||
}
|
}
|
||||||
if link.URL != srv.URL+"/dav/123/Movie.mkv" {
|
if link.URL != "https://cdn.example.test/123/Movie.mkv?sign=1" {
|
||||||
t.Fatalf("bad url: %s", link.URL)
|
t.Fatalf("bad url: %s", link.URL)
|
||||||
}
|
}
|
||||||
if !link.Proxy {
|
if link.Proxy || len(link.Headers) != 0 {
|
||||||
t.Fatalf("clouddrive2 should default to proxy mode")
|
t.Fatalf("clouddrive2 video should resolve to pure 302 link: %#v", link)
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(link.Headers["Authorization"], "Basic ") {
|
if gotRange != "bytes=0-0" {
|
||||||
t.Fatalf("resolve must carry basic auth: %#v", link.Headers)
|
t.Fatalf("resolve should probe with a tiny range, got %q", gotRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloudDrive2ResolveRejectsWebDAVProxyFallbackWithoutRedirect(t *testing.T) {
|
||||||
|
var getSeen bool
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
case r.Method == "PROPFIND" && r.URL.Path == "/dav":
|
||||||
|
w.Header().Set("Content-Type", "application/xml")
|
||||||
|
w.WriteHeader(http.StatusMultiStatus)
|
||||||
|
_, _ = w.Write([]byte(`<?xml version="1.0" encoding="utf-8"?><d:multistatus xmlns:d="DAV:"><d:response><d:href>/dav/</d:href><d:propstat><d:prop><d:resourcetype><d:collection/></d:resourcetype></d:prop></d:propstat></d:response></d:multistatus>`))
|
||||||
|
case r.Method == http.MethodGet && r.URL.Path == "/dav/123/Movie.mkv":
|
||||||
|
getSeen = true
|
||||||
|
w.Header().Set("Content-Range", "bytes 0-0/10")
|
||||||
|
w.WriteHeader(http.StatusPartialContent)
|
||||||
|
_, _ = w.Write([]byte("x"))
|
||||||
|
default:
|
||||||
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
p, err := New(TypeCloudDrive2, map[string]any{"url": srv.URL + "/dav", "username": "u", "password": "p"}, srv.Client())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = p.Resolve(context.Background(), "/123/Movie.mkv")
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "without CDN Location") || !strings.Contains(err.Error(), "refusing WebDAV/proxy fallback") {
|
||||||
|
t.Fatalf("resolve error = %v, want pure 302 refusal", err)
|
||||||
|
}
|
||||||
|
if !getSeen {
|
||||||
|
t.Fatal("expected CloudDrive2 WebDAV direct-link probe")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,15 +468,9 @@ func TestOpenListWebDAVListAndResolve(t *testing.T) {
|
|||||||
if len(entries) != 1 || entries[0].ID != "/Cloud/Movie.mkv" || entries[0].Size != 1024 {
|
if len(entries) != 1 || entries[0].ID != "/Cloud/Movie.mkv" || entries[0].Size != 1024 {
|
||||||
t.Fatalf("entries = %#v", entries)
|
t.Fatalf("entries = %#v", entries)
|
||||||
}
|
}
|
||||||
link, err := p.Resolve(context.Background(), entries[0].ID)
|
_, err = p.Resolve(context.Background(), entries[0].ID)
|
||||||
if err != nil {
|
if err == nil || !strings.Contains(err.Error(), "pure 302 playback requires OpenList raw_url") {
|
||||||
t.Fatalf("resolve: %v", err)
|
t.Fatalf("openlist video resolve should require raw_url instead of WebDAV proxy fallback, err=%v", err)
|
||||||
}
|
|
||||||
if link.URL != srv.URL+"/dav/Cloud/Movie.mkv" {
|
|
||||||
t.Fatalf("bad url: %s", link.URL)
|
|
||||||
}
|
|
||||||
if !link.Proxy {
|
|
||||||
t.Fatalf("openlist should default to proxy mode")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,6 +509,41 @@ func TestOpenListResolveUsesAPIRawURLFor302Playback(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOpenListResolveCollapsesHostedRawURLRedirectToCDN(t *testing.T) {
|
||||||
|
var probeSeen bool
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/api/fs/get":
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"code":200,"data":{"raw_url":"/d/Cloud/Movie.mkv?sign=1"}}`))
|
||||||
|
case "/d/Cloud/Movie.mkv":
|
||||||
|
probeSeen = true
|
||||||
|
if r.Header.Get("Range") != "bytes=0-0" {
|
||||||
|
t.Fatalf("probe Range = %q", r.Header.Get("Range"))
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, "https://cdn.example.test/movie.mkv?sign=cdn", http.StatusFound)
|
||||||
|
default:
|
||||||
|
t.Fatalf("unexpected path %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
p, err := New(TypeOpenList, map[string]any{"server": srv.URL, "token": "alist-token"}, srv.Client())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
link, err := p.Resolve(context.Background(), "/Cloud/Movie.mkv")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolve: %v", err)
|
||||||
|
}
|
||||||
|
if !probeSeen {
|
||||||
|
t.Fatal("expected OpenList-hosted raw_url probe")
|
||||||
|
}
|
||||||
|
if link.URL != "https://cdn.example.test/movie.mkv?sign=cdn" || link.Proxy || len(link.Headers) != 0 {
|
||||||
|
t.Fatalf("link = %#v, want collapsed CDN 302 playback", link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestOpenListResolveLogsInWithUsernamePasswordForAPIRawURL(t *testing.T) {
|
func TestOpenListResolveLogsInWithUsernamePasswordForAPIRawURL(t *testing.T) {
|
||||||
var loginSeen bool
|
var loginSeen bool
|
||||||
var gotAuth string
|
var gotAuth string
|
||||||
@@ -524,7 +588,7 @@ func TestOpenListResolveLogsInWithUsernamePasswordForAPIRawURL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOpenListResolveFallsBackToProxyWhenAPIRawURLNeedsHeaders(t *testing.T) {
|
func TestOpenListResolveRejectsProxyWhenAPIRawURLNeedsHeaders(t *testing.T) {
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path != "/api/fs/get" {
|
if r.URL.Path != "/api/fs/get" {
|
||||||
t.Fatalf("unexpected path %s", r.URL.Path)
|
t.Fatalf("unexpected path %s", r.URL.Path)
|
||||||
@@ -538,15 +602,69 @@ func TestOpenListResolveFallsBackToProxyWhenAPIRawURLNeedsHeaders(t *testing.T)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
link, err := p.Resolve(context.Background(), "/Cloud/Movie.mkv")
|
_, err = p.Resolve(context.Background(), "/Cloud/Movie.mkv")
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "refusing WebDAV/proxy fallback") || !strings.Contains(err.Error(), "Cookie") {
|
||||||
|
t.Fatalf("resolve error = %v, want pure 302 refusal with header names", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenListResolveRejectsHostedRawURLWithoutCDNRedirect(t *testing.T) {
|
||||||
|
var probeSeen bool
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/api/fs/get":
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"code":200,"data":{"raw_url":"/d/Cloud/Movie.mkv?sign=1"}}`))
|
||||||
|
case "/d/Cloud/Movie.mkv":
|
||||||
|
probeSeen = true
|
||||||
|
w.Header().Set("Content-Range", "bytes 0-0/10")
|
||||||
|
w.WriteHeader(http.StatusPartialContent)
|
||||||
|
_, _ = w.Write([]byte("x"))
|
||||||
|
default:
|
||||||
|
t.Fatalf("unexpected path %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
p, err := New(TypeOpenList, map[string]any{"server": srv.URL, "token": "alist-token"}, srv.Client())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("resolve: %v", err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if link.URL != srv.URL+"/dav/Cloud/Movie.mkv" {
|
_, err = p.Resolve(context.Background(), "/Cloud/Movie.mkv")
|
||||||
t.Fatalf("url = %q", link.URL)
|
if err == nil || !strings.Contains(err.Error(), "OpenList-hosted raw_url") || !strings.Contains(err.Error(), "no CDN Location") {
|
||||||
|
t.Fatalf("resolve error = %v, want hosted raw_url refusal", err)
|
||||||
}
|
}
|
||||||
if !link.Proxy || link.Headers["Cookie"] != "sid=abc" {
|
if !probeSeen {
|
||||||
t.Fatalf("link should keep proxy mode with required headers: %#v", link)
|
t.Fatal("expected OpenList-hosted raw_url probe")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenListResolveDoesNotFallbackToWebDAVWhenAPIRawURLFails(t *testing.T) {
|
||||||
|
var davSeen bool
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/api/fs/get":
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"code":500,"message":"driver cannot provide raw_url"}`))
|
||||||
|
case "/dav/Cloud/Movie.mkv":
|
||||||
|
davSeen = true
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
default:
|
||||||
|
t.Fatalf("unexpected path %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
p, err := New(TypeOpenList, map[string]any{"server": srv.URL, "token": "alist-token"}, srv.Client())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = p.Resolve(context.Background(), "/Cloud/Movie.mkv")
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "pure 302 playback requires OpenList raw_url") {
|
||||||
|
t.Fatalf("resolve error = %v, want raw_url requirement", err)
|
||||||
|
}
|
||||||
|
if davSeen {
|
||||||
|
t.Fatal("openlist video resolve fell back to WebDAV after raw_url failure")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -212,10 +213,22 @@ func (p *cloudDrive2Provider) Resolve(ctx context.Context, fileRef string) (*Dir
|
|||||||
if ref == "/" {
|
if ref == "/" {
|
||||||
return nil, fmt.Errorf("%s: file reference required", p.name)
|
return nil, fmt.Errorf("%s: file reference required", p.name)
|
||||||
}
|
}
|
||||||
if p.typ == TypeOpenList && p.apiBase != nil && isOpenListDirectPlaybackCandidate(ref) {
|
if p.typ == TypeOpenList && isCloudVideoPlaybackCandidate(ref) {
|
||||||
if link, err := p.resolveOpenListAPIDirect(ctx, ref); err == nil && link != nil {
|
if p.apiBase == nil {
|
||||||
return link, nil
|
return nil, fmt.Errorf("%s: pure 302 playback requires an OpenList API server address; configure server/api_url so /api/fs/get can return raw_url", p.name)
|
||||||
}
|
}
|
||||||
|
link, err := p.resolveOpenListAPIDirect(ctx, ref)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: pure 302 playback requires OpenList raw_url for %s: %w", p.name, ref, err)
|
||||||
|
}
|
||||||
|
return link, nil
|
||||||
|
}
|
||||||
|
if p.typ == TypeCloudDrive2 && isCloudVideoPlaybackCandidate(ref) {
|
||||||
|
link, err := p.resolveCloudDAVRedirectDirect(ctx, ref)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: pure 302 playback requires CloudDrive2/WebDAV to return a CDN Location for %s: %w", p.name, ref, err)
|
||||||
|
}
|
||||||
|
return link, nil
|
||||||
}
|
}
|
||||||
headers := map[string]string{
|
headers := map[string]string{
|
||||||
"User-Agent": p.ua,
|
"User-Agent": p.ua,
|
||||||
@@ -272,11 +285,108 @@ func (p *cloudDrive2Provider) resolveOpenListAPIDirect(ctx context.Context, file
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
headers := normalizeOpenListPlaybackHeaders(decoded.Data.Header)
|
headers := normalizeOpenListPlaybackHeaders(decoded.Data.Header)
|
||||||
proxy := p.proxy && len(headers) > 0
|
if len(headers) > 0 {
|
||||||
if !proxy {
|
return nil, fmt.Errorf("%s: api get %s returned raw_url that requires headers (%s); refusing WebDAV/proxy fallback for pure 302 playback", p.name, fileRef, strings.Join(sortedHeaderNames(headers), ","))
|
||||||
headers = nil
|
|
||||||
}
|
}
|
||||||
return &DirectLink{URL: resolved, Headers: headers, Proxy: proxy}, nil
|
resolved, err = p.resolveOpenListCDNRedirect(ctx, fileRef, resolved)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &DirectLink{URL: resolved, Headers: nil, Proxy: false}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *cloudDrive2Provider) resolveOpenListCDNRedirect(ctx context.Context, fileRef, rawURL string) (string, error) {
|
||||||
|
if p.apiBase == nil || !sameURLHost(rawURL, p.apiBase) {
|
||||||
|
return rawURL, nil
|
||||||
|
}
|
||||||
|
location, status, err := p.firstHTTPRedirectLocation(ctx, rawURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("%s: probe raw_url %s failed: %w", p.name, fileRef, err)
|
||||||
|
}
|
||||||
|
if location != "" {
|
||||||
|
return location, nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("%s: api get %s returned an OpenList-hosted raw_url with http %d and no CDN Location; refusing OpenList/WebDAV proxy fallback for pure 302 playback", p.name, fileRef, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *cloudDrive2Provider) resolveCloudDAVRedirectDirect(ctx context.Context, fileRef string) (*DirectLink, error) {
|
||||||
|
target := p.urlFor(fileRef)
|
||||||
|
headers := map[string]string{
|
||||||
|
"User-Agent": p.ua,
|
||||||
|
}
|
||||||
|
if p.token != "" {
|
||||||
|
headers["Authorization"] = p.token
|
||||||
|
} else if p.username != "" {
|
||||||
|
headers["Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(p.username+":"+p.password))
|
||||||
|
}
|
||||||
|
location, status, err := p.firstHTTPRedirectLocation(ctx, target, headers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, decorateDAVTransportError(p.name, target, err)
|
||||||
|
}
|
||||||
|
if location == "" {
|
||||||
|
return nil, fmt.Errorf("%s: WebDAV %s returned http %d without CDN Location; refusing WebDAV/proxy fallback for pure 302 playback", p.name, fileRef, status)
|
||||||
|
}
|
||||||
|
return &DirectLink{URL: location, Headers: nil, Proxy: false}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *cloudDrive2Provider) firstHTTPRedirectLocation(ctx context.Context, target string, headers map[string]string) (string, int, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "*/*")
|
||||||
|
req.Header.Set("Accept-Encoding", "identity")
|
||||||
|
req.Header.Set("Range", "bytes=0-0")
|
||||||
|
if strings.TrimSpace(p.ua) != "" {
|
||||||
|
req.Header.Set("User-Agent", p.ua)
|
||||||
|
}
|
||||||
|
for key, value := range headers {
|
||||||
|
key = strings.TrimSpace(key)
|
||||||
|
if key != "" && strings.TrimSpace(value) != "" {
|
||||||
|
req.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client := p.client
|
||||||
|
if client == nil {
|
||||||
|
client = http.DefaultClient
|
||||||
|
}
|
||||||
|
noFollow := *client
|
||||||
|
noFollow.CheckRedirect = func(*http.Request, []*http.Request) error {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
}
|
||||||
|
resp, err := noFollow.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
status := resp.StatusCode
|
||||||
|
if status >= 300 && status < 400 {
|
||||||
|
rawLocation := strings.TrimSpace(resp.Header.Get("Location"))
|
||||||
|
if rawLocation == "" {
|
||||||
|
return "", status, fmt.Errorf("%s: upstream returned redirect http %d without Location", p.name, status)
|
||||||
|
}
|
||||||
|
location, err := resolveHTTPRedirectLocation(target, rawLocation)
|
||||||
|
if err != nil {
|
||||||
|
return "", status, err
|
||||||
|
}
|
||||||
|
return location, status, nil
|
||||||
|
}
|
||||||
|
return "", status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortedHeaderNames(headers map[string]string) []string {
|
||||||
|
if len(headers) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]string, 0, len(headers))
|
||||||
|
for key := range headers {
|
||||||
|
key = strings.TrimSpace(key)
|
||||||
|
if key != "" {
|
||||||
|
out = append(out, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(out)
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *cloudDrive2Provider) hasOpenListAPICredentials() bool {
|
func (p *cloudDrive2Provider) hasOpenListAPICredentials() bool {
|
||||||
@@ -359,6 +469,49 @@ func (p *cloudDrive2Provider) resolveOpenListPlaybackURL(raw string) (string, er
|
|||||||
return base.ResolveReference(u).String(), nil
|
return base.ResolveReference(u).String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sameURLHost(raw string, base *url.URL) bool {
|
||||||
|
if base == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
u, err := url.Parse(strings.TrimSpace(raw))
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !u.IsAbs() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return strings.EqualFold(u.Host, base.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveHTTPRedirectLocation(baseURL, rawLocation string) (string, error) {
|
||||||
|
rawLocation = strings.TrimSpace(rawLocation)
|
||||||
|
if rawLocation == "" {
|
||||||
|
return "", fmt.Errorf("empty redirect Location")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(rawLocation, "//") {
|
||||||
|
base, err := url.Parse(baseURL)
|
||||||
|
if err != nil || base.Scheme == "" {
|
||||||
|
return "", fmt.Errorf("protocol-relative redirect Location without base scheme")
|
||||||
|
}
|
||||||
|
rawLocation = base.Scheme + ":" + rawLocation
|
||||||
|
}
|
||||||
|
location, err := url.Parse(rawLocation)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("invalid redirect Location: %w", err)
|
||||||
|
}
|
||||||
|
if location.IsAbs() {
|
||||||
|
if location.Scheme != "http" && location.Scheme != "https" {
|
||||||
|
return "", fmt.Errorf("unsupported redirect Location scheme %q", location.Scheme)
|
||||||
|
}
|
||||||
|
return location.String(), nil
|
||||||
|
}
|
||||||
|
base, err := url.Parse(baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("invalid redirect base URL: %w", err)
|
||||||
|
}
|
||||||
|
return base.ResolveReference(location).String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeOpenListPlaybackHeaders(raw json.RawMessage) map[string]string {
|
func normalizeOpenListPlaybackHeaders(raw json.RawMessage) map[string]string {
|
||||||
if len(raw) == 0 || string(raw) == "null" {
|
if len(raw) == 0 || string(raw) == "null" {
|
||||||
return nil
|
return nil
|
||||||
@@ -396,7 +549,7 @@ func normalizeOpenListPlaybackHeaders(raw json.RawMessage) map[string]string {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func isOpenListDirectPlaybackCandidate(fileRef string) bool {
|
func isCloudVideoPlaybackCandidate(fileRef string) bool {
|
||||||
switch strings.ToLower(path.Ext(strings.TrimSpace(fileRef))) {
|
switch strings.ToLower(path.Ext(strings.TrimSpace(fileRef))) {
|
||||||
case ".mkv", ".mp4", ".m4v", ".avi", ".mov", ".webm", ".ts", ".rmvb", ".rm", ".3gp", ".mpg", ".mpeg":
|
case ".mkv", ".mp4", ".m4v", ".avi", ".mov", ".webm", ".ts", ".rmvb", ".rm", ".3gp", ".mpg", ".mpeg":
|
||||||
return true
|
return true
|
||||||
|
|||||||
Reference in New Issue
Block a user