mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-02 12:26:36 +08:00
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:
@@ -90,7 +90,7 @@ func cloudPlayHandler(svc *service.Container) gin.HandlerFunc {
|
|||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "ref required"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "ref required"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
link, err := svc.StorageCfg.CloudResolve(c.Request.Context(), typ, ref)
|
link, err := svc.StorageCfg.CloudResolve(c.Request.Context(), typ, ref, c.Request.UserAgent())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package cloud
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -77,11 +79,6 @@ func Test115ListAndResolve(t *testing.T) {
|
|||||||
w.Write([]byte(`{"state":true,"data":[
|
w.Write([]byte(`{"state":true,"data":[
|
||||||
{"cid":"100","n":"Movies","s":0},
|
{"cid":"100","n":"Movies","s":0},
|
||||||
{"fid":"200","n":"Inception.mkv","s":456,"pc":"pick200"}]}`))
|
{"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:
|
default:
|
||||||
t.Errorf("unexpected path %s", r.URL.Path)
|
t.Errorf("unexpected path %s", r.URL.Path)
|
||||||
}
|
}
|
||||||
@@ -92,6 +89,20 @@ func Test115ListAndResolve(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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(), "")
|
entries, err := p.List(context.Background(), "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("list: %v", err)
|
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) {
|
func Test115QRFlow(t *testing.T) {
|
||||||
// status sequence: waiting → scanned → confirmed
|
// status sequence: waiting → scanned → confirmed
|
||||||
calls := 0
|
calls := 0
|
||||||
|
|||||||
@@ -20,11 +20,20 @@ type pan115Provider struct {
|
|||||||
cookie string
|
cookie string
|
||||||
ua string
|
ua string
|
||||||
webBase string // https://webapi.115.com (override in tests)
|
webBase string // https://webapi.115.com (override in tests)
|
||||||
|
proBase string // https://proapi.115.com (override in tests)
|
||||||
client *http.Client
|
client *http.Client
|
||||||
proxy bool
|
proxy bool
|
||||||
|
|
||||||
|
// downURLPayload fetches and decrypts the app/chrome/downurl response for a
|
||||||
|
// pickcode, returning the raw JSON payload (map of file id -> info). It is a
|
||||||
|
// seam so tests can bypass the live 115 crypto/transport.
|
||||||
|
downURLPayload func(ctx context.Context, pickcode string) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pan115WebBase = "https://webapi.115.com"
|
const (
|
||||||
|
pan115WebBase = "https://webapi.115.com"
|
||||||
|
pan115ProBase = "https://proapi.115.com"
|
||||||
|
)
|
||||||
|
|
||||||
func new115(cfg map[string]any, client *http.Client) *pan115Provider {
|
func new115(cfg map[string]any, client *http.Client) *pan115Provider {
|
||||||
web := str(cfg["base"])
|
web := str(cfg["base"])
|
||||||
@@ -41,13 +50,20 @@ func new115(cfg map[string]any, client *http.Client) *pan115Provider {
|
|||||||
if _, ok := cfg["force_proxy"]; ok && boolish(cfg["force_proxy"]) {
|
if _, ok := cfg["force_proxy"]; ok && boolish(cfg["force_proxy"]) {
|
||||||
proxy = true
|
proxy = true
|
||||||
}
|
}
|
||||||
return &pan115Provider{
|
pro := str(cfg["pro_base"])
|
||||||
|
if pro == "" {
|
||||||
|
pro = pan115ProBase
|
||||||
|
}
|
||||||
|
p := &pan115Provider{
|
||||||
cookie: str(cfg["cookie"]),
|
cookie: str(cfg["cookie"]),
|
||||||
ua: ua,
|
ua: ua,
|
||||||
webBase: strings.TrimRight(web, "/"),
|
webBase: strings.TrimRight(web, "/"),
|
||||||
|
proBase: strings.TrimRight(pro, "/"),
|
||||||
client: client,
|
client: client,
|
||||||
proxy: proxy,
|
proxy: proxy,
|
||||||
}
|
}
|
||||||
|
p.downURLPayload = p.fetchDownURLPayload
|
||||||
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pan115Provider) Type() string { return Type115 }
|
func (p *pan115Provider) Type() string { return Type115 }
|
||||||
@@ -126,39 +142,90 @@ func (p *pan115Provider) List(ctx context.Context, dirID string) ([]FileEntry, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Resolve accepts a pickcode (preferred) and returns the CDN download URL.
|
// Resolve accepts a pickcode (preferred) and returns the CDN download URL.
|
||||||
|
//
|
||||||
|
// 115 deprecated the plain web /files/download endpoint (it no longer returns
|
||||||
|
// file_url for ordinary cookies). We use the current app/chrome/downurl
|
||||||
|
// endpoint, which takes an m115-encrypted body and returns an m115-encrypted
|
||||||
|
// payload mapping the file id to a short-lived, OSS-signed CDN URL suitable for
|
||||||
|
// a 302 redirect (the same approach Alist's 115 driver uses).
|
||||||
func (p *pan115Provider) Resolve(ctx context.Context, pickcode string) (*DirectLink, error) {
|
func (p *pan115Provider) Resolve(ctx context.Context, pickcode string) (*DirectLink, error) {
|
||||||
if pickcode == "" {
|
if pickcode == "" {
|
||||||
return nil, fmt.Errorf("115: empty pickcode")
|
return nil, fmt.Errorf("115: empty pickcode")
|
||||||
}
|
}
|
||||||
u := fmt.Sprintf("%s/files/download?pickcode=%s&_=%d", p.webBase, url.QueryEscape(pickcode), nowUnix())
|
raw, err := p.downURLPayload(ctx, pickcode)
|
||||||
resp, err := p.get(ctx, u)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var payload map[string]struct {
|
||||||
|
FileName string `json:"file_name"`
|
||||||
|
FileSize json.Number `json:"file_size"`
|
||||||
|
URL struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
} `json:"url"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||||
|
return nil, fmt.Errorf("115: decode downurl: %w", err)
|
||||||
|
}
|
||||||
|
for _, info := range payload {
|
||||||
|
if info.URL.URL == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return &DirectLink{
|
||||||
|
URL: info.URL.URL,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"User-Agent": p.ua,
|
||||||
|
"Cookie": p.cookie,
|
||||||
|
},
|
||||||
|
Proxy: p.proxy,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("115: download failed: no url")
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchDownURLPayload performs the live encrypted app/chrome/downurl request and
|
||||||
|
// returns the decrypted JSON payload.
|
||||||
|
func (p *pan115Provider) fetchDownURLPayload(ctx context.Context, pickcode string) ([]byte, error) {
|
||||||
|
key := m115GenerateKey()
|
||||||
|
params, err := json.Marshal(map[string]string{"pickcode": pickcode})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
form := url.Values{}
|
||||||
|
form.Set("data", m115Encode(params, key))
|
||||||
|
u := fmt.Sprintf("%s/app/chrome/downurl?t=%d", p.proBase, nowUnix())
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, strings.NewReader(form.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.Header.Set("Cookie", p.cookie)
|
||||||
|
req.Header.Set("User-Agent", p.ua)
|
||||||
|
req.Header.Set("Accept", "application/json, text/plain, */*")
|
||||||
|
resp, err := p.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
var r struct {
|
var r struct {
|
||||||
State bool `json:"state"`
|
State bool `json:"state"`
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
FileURL string `json:"file_url"`
|
Data string `json:"data"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
|
||||||
return nil, fmt.Errorf("115: decode download: %w", err)
|
return nil, fmt.Errorf("115: decode downurl: %w", err)
|
||||||
}
|
}
|
||||||
if !r.State || r.FileURL == "" {
|
if !r.State || r.Data == "" {
|
||||||
msg := r.Error
|
msg := r.Error
|
||||||
if msg == "" {
|
if msg == "" {
|
||||||
msg = "no file_url"
|
msg = "no data"
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("115: download failed: %s", msg)
|
return nil, fmt.Errorf("115: download failed: %s", msg)
|
||||||
}
|
}
|
||||||
return &DirectLink{
|
out, err := m115Decode(r.Data, key)
|
||||||
URL: r.FileURL,
|
if err != nil {
|
||||||
Headers: map[string]string{
|
return nil, fmt.Errorf("115: decrypt downurl: %w", err)
|
||||||
"User-Agent": p.ua,
|
}
|
||||||
"Cookie": p.cookie,
|
return out, nil
|
||||||
},
|
|
||||||
Proxy: p.proxy,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// nowUnix is a seam for deterministic tests.
|
// nowUnix is a seam for deterministic tests.
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
package cloud
|
||||||
|
|
||||||
|
// 115 网盘的 app/chrome/downurl 下载接口要求请求体使用 115 自有的 "m115" 加密协议
|
||||||
|
// (RSA + XOR 混淆),返回的直链也以同样方式加密。普通 web cookie 调用旧的
|
||||||
|
// /files/download 接口已不再返回 file_url,必须改用该加密接口。
|
||||||
|
//
|
||||||
|
// 下面的实现移植自 MIT 许可的 github.com/SheltonZhu/115driver
|
||||||
|
// (pkg/crypto/m115),alist 等项目同样采用此实现。仅做最小改动:函数加 m115
|
||||||
|
// 前缀以归入本包命名空间。
|
||||||
|
//
|
||||||
|
// Copyright (c) 115driver authors. MIT License.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
)
|
||||||
|
|
||||||
|
// m115Key is the random 16-byte session key generated per request.
|
||||||
|
type m115Key [16]byte
|
||||||
|
|
||||||
|
func m115GenerateKey() m115Key {
|
||||||
|
key := m115Key{}
|
||||||
|
_, _ = io.ReadFull(rand.Reader, key[:])
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
// m115Encode encrypts request input for the downurl endpoint.
|
||||||
|
func m115Encode(input []byte, key m115Key) string {
|
||||||
|
buf := make([]byte, 16+len(input))
|
||||||
|
copy(buf, key[:])
|
||||||
|
copy(buf[16:], input)
|
||||||
|
m115XORTransform(buf[16:], m115XORDeriveKey(key[:], 4))
|
||||||
|
m115ReverseBytes(buf[16:])
|
||||||
|
m115XORTransform(buf[16:], m115XORClientKey)
|
||||||
|
return base64.StdEncoding.EncodeToString(m115RSAEncrypt(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
// m115Decode decrypts the base64 response payload using the request key.
|
||||||
|
func m115Decode(input string, key m115Key) ([]byte, error) {
|
||||||
|
data, err := base64.StdEncoding.DecodeString(input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data = m115RSADecrypt(data)
|
||||||
|
output := make([]byte, len(data)-16)
|
||||||
|
copy(output, data[16:])
|
||||||
|
m115XORTransform(output, m115XORDeriveKey(data[:16], 12))
|
||||||
|
m115ReverseBytes(output)
|
||||||
|
m115XORTransform(output, m115XORDeriveKey(key[:], 4))
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func m115ReverseBytes(data []byte) {
|
||||||
|
for i, j := 0, len(data)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
data[i], data[j] = data[j], data[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- RSA layer ---
|
||||||
|
|
||||||
|
var (
|
||||||
|
m115N, _ = big.NewInt(0).SetString(
|
||||||
|
"8686980c0f5a24c4b9d43020cd2c22703ff3f450756529058b1cf88f09b86021"+
|
||||||
|
"36477198a6e2683149659bd122c33592fdb5ad47944ad1ea4d36c6b172aad633"+
|
||||||
|
"8c3bb6ac6227502d010993ac967d1aef00f0c8e038de2e4d3bc2ec368af2e9f1"+
|
||||||
|
"0a6f1eda4f7262f136420c07c331b871bf139f74f3010e3c4fe57df3afb71683", 16)
|
||||||
|
m115E, _ = big.NewInt(0).SetString("10001", 16)
|
||||||
|
|
||||||
|
m115KeyLength = m115N.BitLen() / 8
|
||||||
|
)
|
||||||
|
|
||||||
|
func m115RSAEncrypt(input []byte) []byte {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
for remainSize := len(input); remainSize > 0; {
|
||||||
|
sliceSize := m115KeyLength - 11
|
||||||
|
if sliceSize > remainSize {
|
||||||
|
sliceSize = remainSize
|
||||||
|
}
|
||||||
|
m115RSAEncryptSlice(input[:sliceSize], buf)
|
||||||
|
input = input[sliceSize:]
|
||||||
|
remainSize -= sliceSize
|
||||||
|
}
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func m115RSAEncryptSlice(input []byte, w io.Writer) {
|
||||||
|
padSize := m115KeyLength - len(input) - 3
|
||||||
|
padData := make([]byte, padSize)
|
||||||
|
_, _ = rand.Read(padData)
|
||||||
|
buf := make([]byte, m115KeyLength)
|
||||||
|
buf[0], buf[1] = 0, 2
|
||||||
|
for i, b := range padData {
|
||||||
|
buf[2+i] = b%0xff + 0x01
|
||||||
|
}
|
||||||
|
buf[padSize+2] = 0
|
||||||
|
copy(buf[padSize+3:], input)
|
||||||
|
msg := big.NewInt(0).SetBytes(buf)
|
||||||
|
ret := big.NewInt(0).Exp(msg, m115E, m115N).Bytes()
|
||||||
|
if fillSize := m115KeyLength - len(ret); fillSize > 0 {
|
||||||
|
zeros := make([]byte, fillSize)
|
||||||
|
_, _ = w.Write(zeros)
|
||||||
|
}
|
||||||
|
_, _ = w.Write(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func m115RSADecrypt(input []byte) []byte {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
for remainSize := len(input); remainSize > 0; {
|
||||||
|
sliceSize := m115KeyLength
|
||||||
|
if sliceSize > remainSize {
|
||||||
|
sliceSize = remainSize
|
||||||
|
}
|
||||||
|
m115RSADecryptSlice(input[:sliceSize], buf)
|
||||||
|
input = input[sliceSize:]
|
||||||
|
remainSize -= sliceSize
|
||||||
|
}
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func m115RSADecryptSlice(input []byte, w io.Writer) {
|
||||||
|
msg := big.NewInt(0).SetBytes(input)
|
||||||
|
ret := big.NewInt(0).Exp(msg, m115E, m115N).Bytes()
|
||||||
|
for i, b := range ret {
|
||||||
|
if b == 0 && i != 0 {
|
||||||
|
_, _ = w.Write(ret[i+1:])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- XOR layer ---
|
||||||
|
|
||||||
|
var (
|
||||||
|
m115XORKeySeed = []byte{
|
||||||
|
0xf0, 0xe5, 0x69, 0xae, 0xbf, 0xdc, 0xbf, 0x8a,
|
||||||
|
0x1a, 0x45, 0xe8, 0xbe, 0x7d, 0xa6, 0x73, 0xb8,
|
||||||
|
0xde, 0x8f, 0xe7, 0xc4, 0x45, 0xda, 0x86, 0xc4,
|
||||||
|
0x9b, 0x64, 0x8b, 0x14, 0x6a, 0xb4, 0xf1, 0xaa,
|
||||||
|
0x38, 0x01, 0x35, 0x9e, 0x26, 0x69, 0x2c, 0x86,
|
||||||
|
0x00, 0x6b, 0x4f, 0xa5, 0x36, 0x34, 0x62, 0xa6,
|
||||||
|
0x2a, 0x96, 0x68, 0x18, 0xf2, 0x4a, 0xfd, 0xbd,
|
||||||
|
0x6b, 0x97, 0x8f, 0x4d, 0x8f, 0x89, 0x13, 0xb7,
|
||||||
|
0x6c, 0x8e, 0x93, 0xed, 0x0e, 0x0d, 0x48, 0x3e,
|
||||||
|
0xd7, 0x2f, 0x88, 0xd8, 0xfe, 0xfe, 0x7e, 0x86,
|
||||||
|
0x50, 0x95, 0x4f, 0xd1, 0xeb, 0x83, 0x26, 0x34,
|
||||||
|
0xdb, 0x66, 0x7b, 0x9c, 0x7e, 0x9d, 0x7a, 0x81,
|
||||||
|
0x32, 0xea, 0xb6, 0x33, 0xde, 0x3a, 0xa9, 0x59,
|
||||||
|
0x34, 0x66, 0x3b, 0xaa, 0xba, 0x81, 0x60, 0x48,
|
||||||
|
0xb9, 0xd5, 0x81, 0x9c, 0xf8, 0x6c, 0x84, 0x77,
|
||||||
|
0xff, 0x54, 0x78, 0x26, 0x5f, 0xbe, 0xe8, 0x1e,
|
||||||
|
0x36, 0x9f, 0x34, 0x80, 0x5c, 0x45, 0x2c, 0x9b,
|
||||||
|
0x76, 0xd5, 0x1b, 0x8f, 0xcc, 0xc3, 0xb8, 0xf5,
|
||||||
|
}
|
||||||
|
|
||||||
|
m115XORClientKey = []byte{
|
||||||
|
0x78, 0x06, 0xad, 0x4c, 0x33, 0x86, 0x5d, 0x18,
|
||||||
|
0x4c, 0x01, 0x3f, 0x46,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func m115XORDeriveKey(seed []byte, size int) []byte {
|
||||||
|
key := make([]byte, size)
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
key[i] = (seed[i] + m115XORKeySeed[size*i]) & 0xff
|
||||||
|
key[i] ^= m115XORKeySeed[size*(size-i-1)]
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func m115XORTransform(data []byte, key []byte) {
|
||||||
|
dataSize, keySize := len(data), len(key)
|
||||||
|
mod := dataSize % 4
|
||||||
|
if mod > 0 {
|
||||||
|
for i := 0; i < mod; i++ {
|
||||||
|
data[i] ^= key[i%keySize]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := mod; i < dataSize; i++ {
|
||||||
|
data[i] ^= key[(i-mod)%keySize]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -221,14 +221,46 @@ func (s *StorageConfigService) CloudList(ctx context.Context, typ, dirID string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CloudResolve resolves a cloud file reference to a direct link.
|
// CloudResolve resolves a cloud file reference to a direct link.
|
||||||
func (s *StorageConfigService) CloudResolve(ctx context.Context, typ, fileRef string) (*cloud.DirectLink, error) {
|
//
|
||||||
p, err := s.CloudProvider(ctx, typ)
|
// clientUA is the User-Agent of the playback client that will follow the 302
|
||||||
|
// redirect. 115/夸克 CDN links are bound to the UA used to request them, so we
|
||||||
|
// resolve with the client's own UA — that way the pure 302 the host issues
|
||||||
|
// points at a link the client can fetch directly (true offload). When clientUA
|
||||||
|
// is empty the provider's default UA is used.
|
||||||
|
func (s *StorageConfigService) CloudResolve(ctx context.Context, typ, fileRef, clientUA string) (*cloud.DirectLink, error) {
|
||||||
|
p, err := s.cloudProviderWithUA(ctx, typ, clientUA)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return p.Resolve(ctx, fileRef)
|
return p.Resolve(ctx, fileRef)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cloudProviderWithUA builds a provider, overriding the request UA when a
|
||||||
|
// non-empty clientUA is supplied.
|
||||||
|
func (s *StorageConfigService) cloudProviderWithUA(ctx context.Context, typ, clientUA string) (cloud.Provider, error) {
|
||||||
|
if !cloud.IsCloudType(typ) {
|
||||||
|
return nil, fmt.Errorf("not a cloud provider: %q", typ)
|
||||||
|
}
|
||||||
|
view, err := s.Get(ctx, typ)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if view == nil {
|
||||||
|
return nil, fmt.Errorf("%s storage not configured", typ)
|
||||||
|
}
|
||||||
|
cfg := view.Config
|
||||||
|
if strings.TrimSpace(clientUA) != "" {
|
||||||
|
// Copy so we never mutate the cached view config.
|
||||||
|
cp := make(map[string]any, len(cfg)+1)
|
||||||
|
for k, v := range cfg {
|
||||||
|
cp[k] = v
|
||||||
|
}
|
||||||
|
cp["ua"] = clientUA
|
||||||
|
cfg = cp
|
||||||
|
}
|
||||||
|
return cloud.New(typ, cfg, s.client)
|
||||||
|
}
|
||||||
|
|
||||||
// cloudLibraryName maps a provider type to a friendly Chinese library name.
|
// cloudLibraryName maps a provider type to a friendly Chinese library name.
|
||||||
func cloudLibraryName(typ string) string {
|
func cloudLibraryName(typ string) string {
|
||||||
switch typ {
|
switch typ {
|
||||||
|
|||||||
Reference in New Issue
Block a user