mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 11:36:36 +08:00
split pan115 qr login helpers
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user