From c2292682632740dc0568fd594f18849a470b8556 Mon Sep 17 00:00:00 2001 From: ShukeBta <272197458+ShukeBta@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:58:52 +0800 Subject: [PATCH] split pan115 qr login helpers --- internal/service/cloud/pan115.go | 138 -------------------------- internal/service/cloud/pan115_qr.go | 147 ++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 138 deletions(-) create mode 100644 internal/service/cloud/pan115_qr.go diff --git a/internal/service/cloud/pan115.go b/internal/service/cloud/pan115.go index 95833fa..b92194f 100644 --- a/internal/service/cloud/pan115.go +++ b/internal/service/cloud/pan115.go @@ -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 -} diff --git a/internal/service/cloud/pan115_qr.go b/internal/service/cloud/pan115_qr.go new file mode 100644 index 0000000..8dc4ebc --- /dev/null +++ b/internal/service/cloud/pan115_qr.go @@ -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 +}