split pan115 qr login helpers

This commit is contained in:
ShukeBta
2026-06-27 11:58:52 +08:00
parent c9cc4a6ee9
commit c229268263
2 changed files with 147 additions and 138 deletions
-138
View File
@@ -235,141 +235,3 @@ func (p *pan115Provider) fetchDownURLPayload(ctx context.Context, pickcode strin
// nowUnix is a seam for deterministic tests.
var nowUnix = func() int64 { return timeNow().Unix() }
// ─── QR-code login ───────────────────────────────────────────────────────────
// QRSession is the handle returned by QRStart; the client renders QRImageURL
// and polls QRPoll until it returns a cookie.
type QRSession struct {
UID string `json:"uid"`
Time int64 `json:"time"`
Sign string `json:"sign"`
QRImageURL string `json:"qr_image_url"`
}
// QR login hosts (overridable for tests).
var (
qr115APIBase = "https://qrcodeapi.115.com"
qr115PassportBase = "https://passportapi.115.com"
)
// QRStart obtains a 115 QR-code login token + image URL.
func QRStart(ctx context.Context, client *http.Client) (*QRSession, error) {
if client == nil {
client = http.DefaultClient
}
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, qr115APIBase+"/api/1.0/web/1.0/token/", nil)
req.Header.Set("User-Agent", defaultUA)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var r struct {
State int `json:"state"`
Data struct {
UID string `json:"uid"`
Time int64 `json:"time"`
Sign string `json:"sign"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
return nil, fmt.Errorf("115 qr: decode token: %w", err)
}
if r.State != 1 || r.Data.UID == "" {
return nil, fmt.Errorf("115 qr: token request failed")
}
return &QRSession{
UID: r.Data.UID,
Time: r.Data.Time,
Sign: r.Data.Sign,
QRImageURL: qr115APIBase + "/api/1.0/web/1.0/qrcode?uid=" + url.QueryEscape(r.Data.UID),
}, nil
}
// QRStatus is the poll result.
type QRStatus struct {
// State is one of: "waiting" (not scanned), "scanned" (scanned, awaiting
// confirmation), "confirmed" (login approved; Cookie populated),
// "expired" (token expired/cancelled).
State string `json:"state"`
Cookie string `json:"cookie,omitempty"`
}
// QRPoll checks the QR session status; on confirmation it exchanges the token
// for a session cookie via the passport API.
func QRPoll(ctx context.Context, client *http.Client, sess *QRSession) (*QRStatus, error) {
if client == nil {
client = http.DefaultClient
}
if sess == nil || sess.UID == "" {
return nil, fmt.Errorf("115 qr: nil session")
}
q := url.Values{}
q.Set("uid", sess.UID)
q.Set("time", strconv.FormatInt(sess.Time, 10))
q.Set("sign", sess.Sign)
q.Set("_", strconv.FormatInt(timeNow().UnixMilli(), 10))
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, qr115APIBase+"/get/status/?"+q.Encode(), nil)
req.Header.Set("User-Agent", defaultUA)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var r struct {
State int `json:"state"`
Data struct {
Status int `json:"status"` // 0 waiting, 1 scanned, 2 confirmed, -1/-2 expired
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
return nil, fmt.Errorf("115 qr: decode status: %w", err)
}
switch r.Data.Status {
case 1:
return &QRStatus{State: "scanned"}, nil
case 2:
cookie, err := qr115Exchange(ctx, client, sess.UID)
if err != nil {
return nil, err
}
return &QRStatus{State: "confirmed", Cookie: cookie}, nil
case 0:
return &QRStatus{State: "waiting"}, nil
default:
return &QRStatus{State: "expired"}, nil
}
}
// qr115Exchange swaps an approved uid for a session cookie.
func qr115Exchange(ctx context.Context, client *http.Client, uid string) (string, error) {
form := url.Values{}
form.Set("account", uid)
form.Set("app", "web")
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, qr115PassportBase+"/app/1.0/web/1.0/login/qrcode/", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("User-Agent", defaultUA)
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var r struct {
State int `json:"state"`
Data struct {
Cookie map[string]string `json:"cookie"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
return "", fmt.Errorf("115 qr: decode login: %w", err)
}
if r.State != 1 || len(r.Data.Cookie) == 0 {
return "", fmt.Errorf("115 qr: login exchange failed")
}
parts := make([]string, 0, len(r.Data.Cookie))
for k, v := range r.Data.Cookie {
parts = append(parts, k+"="+v)
}
return strings.Join(parts, "; "), nil
}
+147
View File
@@ -0,0 +1,147 @@
package cloud
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
)
// QRSession is the handle returned by QRStart; the client renders QRImageURL
// and polls QRPoll until it returns a cookie.
type QRSession struct {
UID string `json:"uid"`
Time int64 `json:"time"`
Sign string `json:"sign"`
QRImageURL string `json:"qr_image_url"`
}
// QR login hosts (overridable for tests).
var (
qr115APIBase = "https://qrcodeapi.115.com"
qr115PassportBase = "https://passportapi.115.com"
)
// QRStart obtains a 115 QR-code login token + image URL.
func QRStart(ctx context.Context, client *http.Client) (*QRSession, error) {
if client == nil {
client = http.DefaultClient
}
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, qr115APIBase+"/api/1.0/web/1.0/token/", nil)
req.Header.Set("User-Agent", defaultUA)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var r struct {
State int `json:"state"`
Data struct {
UID string `json:"uid"`
Time int64 `json:"time"`
Sign string `json:"sign"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
return nil, fmt.Errorf("115 qr: decode token: %w", err)
}
if r.State != 1 || r.Data.UID == "" {
return nil, fmt.Errorf("115 qr: token request failed")
}
return &QRSession{
UID: r.Data.UID,
Time: r.Data.Time,
Sign: r.Data.Sign,
QRImageURL: qr115APIBase + "/api/1.0/web/1.0/qrcode?uid=" + url.QueryEscape(r.Data.UID),
}, nil
}
// QRStatus is the poll result.
type QRStatus struct {
// State is one of: "waiting" (not scanned), "scanned" (scanned, awaiting
// confirmation), "confirmed" (login approved; Cookie populated),
// "expired" (token expired/cancelled).
State string `json:"state"`
Cookie string `json:"cookie,omitempty"`
}
// QRPoll checks the QR session status; on confirmation it exchanges the token
// for a session cookie via the passport API.
func QRPoll(ctx context.Context, client *http.Client, sess *QRSession) (*QRStatus, error) {
if client == nil {
client = http.DefaultClient
}
if sess == nil || sess.UID == "" {
return nil, fmt.Errorf("115 qr: nil session")
}
q := url.Values{}
q.Set("uid", sess.UID)
q.Set("time", strconv.FormatInt(sess.Time, 10))
q.Set("sign", sess.Sign)
q.Set("_", strconv.FormatInt(timeNow().UnixMilli(), 10))
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, qr115APIBase+"/get/status/?"+q.Encode(), nil)
req.Header.Set("User-Agent", defaultUA)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var r struct {
State int `json:"state"`
Data struct {
Status int `json:"status"` // 0 waiting, 1 scanned, 2 confirmed, -1/-2 expired
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
return nil, fmt.Errorf("115 qr: decode status: %w", err)
}
switch r.Data.Status {
case 1:
return &QRStatus{State: "scanned"}, nil
case 2:
cookie, err := qr115Exchange(ctx, client, sess.UID)
if err != nil {
return nil, err
}
return &QRStatus{State: "confirmed", Cookie: cookie}, nil
case 0:
return &QRStatus{State: "waiting"}, nil
default:
return &QRStatus{State: "expired"}, nil
}
}
// qr115Exchange swaps an approved uid for a session cookie.
func qr115Exchange(ctx context.Context, client *http.Client, uid string) (string, error) {
form := url.Values{}
form.Set("account", uid)
form.Set("app", "web")
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, qr115PassportBase+"/app/1.0/web/1.0/login/qrcode/", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("User-Agent", defaultUA)
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var r struct {
State int `json:"state"`
Data struct {
Cookie map[string]string `json:"cookie"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
return "", fmt.Errorf("115 qr: decode login: %w", err)
}
if r.State != 1 || len(r.Data.Cookie) == 0 {
return "", fmt.Errorf("115 qr: login exchange failed")
}
parts := make([]string, 0, len(r.Data.Cookie))
for k, v := range r.Data.Cookie {
parts = append(parts, k+"="+v)
}
return strings.Join(parts, "; "), nil
}