fix(115): resolve direct link via app/chrome/downurl + bind CDN link to client UA

115 deprecated the plain web /files/download endpoint (ordinary cookies no
longer get file_url), breaking 302 playback with 'no file_url'. Switch
Resolve() to the current proapi.115.com/app/chrome/downurl endpoint, which
uses 115's m115 (RSA+XOR) request/response encryption (vendored from the
MIT-licensed SheltonZhu/115driver).

115 CDN links are bound to the User-Agent used to request them, so resolve
with the playback client's own UA (plumbed from the play handler) — the host
can then issue a pure 302 the client fetches directly, preserving true offload.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
soldosluka857
2026-05-30 12:06:27 +00:00
committed by Shuke
parent 74271081f4
commit 3fa6932c99
5 changed files with 355 additions and 26 deletions
+51 -5
View File
@@ -2,8 +2,10 @@ package cloud
import (
"context"
"encoding/base64"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
@@ -77,11 +79,6 @@ func Test115ListAndResolve(t *testing.T) {
w.Write([]byte(`{"state":true,"data":[
{"cid":"100","n":"Movies","s":0},
{"fid":"200","n":"Inception.mkv","s":456,"pc":"pick200"}]}`))
case "/files/download":
if r.URL.Query().Get("pickcode") != "pick200" {
t.Errorf("bad pickcode %q", r.URL.Query().Get("pickcode"))
}
w.Write([]byte(`{"state":true,"file_url":"https://cdn.115/x.mkv?t=1"}`))
default:
t.Errorf("unexpected path %s", r.URL.Path)
}
@@ -92,6 +89,20 @@ func Test115ListAndResolve(t *testing.T) {
if err != nil {
t.Fatal(err)
}
// The downurl endpoint is m115-encrypted end-to-end (the server side
// requires 115's private key), so stub the decrypted payload via the seam
// and assert the pickcode→URL extraction. The live crypto/transport path is
// exercised by integration testing against the real 115 API.
p115, ok := p.(*pan115Provider)
if !ok {
t.Fatalf("expected *pan115Provider, got %T", p)
}
p115.downURLPayload = func(ctx context.Context, pickcode string) ([]byte, error) {
if pickcode != "pick200" {
t.Errorf("bad pickcode %q", pickcode)
}
return []byte(`{"200":{"file_name":"Inception.mkv","file_size":"456","url":{"url":"https://cdn.115/x.mkv?t=1"}}}`), nil
}
entries, err := p.List(context.Background(), "")
if err != nil {
t.Fatalf("list: %v", err)
@@ -117,6 +128,41 @@ func Test115ListAndResolve(t *testing.T) {
}
}
// Test115DownURLEndpointAndError exercises the live fetchDownURLPayload path:
// it must POST an m115-encrypted `data` body to /app/chrome/downurl?t=... and
// surface 115's error when state=false (no decryption needed for that branch).
func Test115DownURLEndpointAndError(t *testing.T) {
var gotData, gotT string
pro := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/app/chrome/downurl" {
t.Errorf("unexpected path %s", r.URL.Path)
}
gotT = r.URL.Query().Get("t")
_ = r.ParseForm()
gotData = r.PostFormValue("data")
w.Write([]byte(`{"state":false,"error":"not exist"}`))
}))
defer pro.Close()
p, err := New(Type115, map[string]any{"cookie": "UID=1", "pro_base": pro.URL}, pro.Client())
if err != nil {
t.Fatal(err)
}
_, err = p.Resolve(context.Background(), "pickX")
if err == nil || !strings.Contains(err.Error(), "not exist") {
t.Fatalf("want upstream error surfaced, got %v", err)
}
if gotT == "" {
t.Errorf("missing t query param")
}
if gotData == "" {
t.Errorf("missing encrypted data body")
}
if _, derr := base64.StdEncoding.DecodeString(gotData); derr != nil {
t.Errorf("data body is not base64: %v", derr)
}
}
func Test115QRFlow(t *testing.T) {
// status sequence: waiting → scanned → confirmed
calls := 0