@@ -3,8 +3,10 @@ package pikpak
33import (
44 "bytes"
55 "context"
6+ "crypto/md5"
67 "crypto/sha1"
78 "encoding/hex"
9+ "encoding/json"
810 "errors"
911 "fmt"
1012 "io"
@@ -14,10 +16,13 @@ import (
1416 "os"
1517 "strconv"
1618 "strings"
19+ "sync"
1720 "time"
1821
1922 "github.com/rclone/rclone/backend/pikpak/api"
2023 "github.com/rclone/rclone/fs"
24+ "github.com/rclone/rclone/fs/config/configmap"
25+ "github.com/rclone/rclone/fs/fserrors"
2126 "github.com/rclone/rclone/lib/rest"
2227)
2328
@@ -262,15 +267,20 @@ func (f *Fs) getGcid(ctx context.Context, src fs.ObjectInfo) (gcid string, err e
262267 if err != nil {
263268 return
264269 }
270+ if src .Size () == 0 {
271+ // If src is zero-length, the API will return
272+ // Error "cid and file_size is required" (400)
273+ // In this case, we can simply return cid == gcid
274+ return cid , nil
275+ }
265276
266277 params := url.Values {}
267278 params .Set ("cid" , cid )
268279 params .Set ("file_size" , strconv .FormatInt (src .Size (), 10 ))
269280 opts := rest.Opts {
270- Method : "GET" ,
271- Path : "/drive/v1/resource/cid" ,
272- Parameters : params ,
273- ExtraHeaders : map [string ]string {"x-device-id" : f .deviceID },
281+ Method : "GET" ,
282+ Path : "/drive/v1/resource/cid" ,
283+ Parameters : params ,
274284 }
275285
276286 info := struct {
@@ -408,6 +418,8 @@ func calcCid(ctx context.Context, src fs.ObjectInfo) (cid string, err error) {
408418 return
409419}
410420
421+ // ------------------------------------------------------------ authorization
422+
411423// randomly generates device id used for request header 'x-device-id'
412424//
413425// original javascript implementation
@@ -428,3 +440,206 @@ func genDeviceID() string {
428440 }
429441 return string (base )
430442}
443+
444+ var md5Salt = []string {
445+ "C9qPpZLN8ucRTaTiUMWYS9cQvWOE" ,
446+ "+r6CQVxjzJV6LCV" ,
447+ "F" ,
448+ "pFJRC" ,
449+ "9WXYIDGrwTCz2OiVlgZa90qpECPD6olt" ,
450+ "/750aCr4lm/Sly/c" ,
451+ "RB+DT/gZCrbV" ,
452+ "" ,
453+ "CyLsf7hdkIRxRm215hl" ,
454+ "7xHvLi2tOYP0Y92b" ,
455+ "ZGTXXxu8E/MIWaEDB+Sm/" ,
456+ "1UI3" ,
457+ "E7fP5Pfijd+7K+t6Tg/NhuLq0eEUVChpJSkrKxpO" ,
458+ "ihtqpG6FMt65+Xk+tWUH2" ,
459+ "NhXXU9rg4XXdzo7u5o" ,
460+ }
461+
462+ func md5Sum (text string ) string {
463+ hash := md5 .Sum ([]byte (text ))
464+ return hex .EncodeToString (hash [:])
465+ }
466+
467+ func calcCaptchaSign (deviceID string ) (timestamp , sign string ) {
468+ timestamp = fmt .Sprint (time .Now ().UnixMilli ())
469+ str := fmt .Sprint (clientID , clientVersion , packageName , deviceID , timestamp )
470+ for _ , salt := range md5Salt {
471+ str = md5Sum (str + salt )
472+ }
473+ sign = "1." + str
474+ return
475+ }
476+
477+ func newCaptchaTokenRequest (action , oldToken string , opt * Options ) (req * api.CaptchaTokenRequest ) {
478+ req = & api.CaptchaTokenRequest {
479+ Action : action ,
480+ CaptchaToken : oldToken , // can be empty initially
481+ ClientID : clientID ,
482+ DeviceID : opt .DeviceID ,
483+ Meta : new (api.CaptchaTokenMeta ),
484+ }
485+ switch action {
486+ case "POST:/v1/auth/signin" :
487+ req .Meta .UserName = opt .Username
488+ default :
489+ timestamp , captchaSign := calcCaptchaSign (opt .DeviceID )
490+ req .Meta .CaptchaSign = captchaSign
491+ req .Meta .Timestamp = timestamp
492+ req .Meta .ClientVersion = clientVersion
493+ req .Meta .PackageName = packageName
494+ req .Meta .UserID = opt .UserID
495+ }
496+ return
497+ }
498+
499+ // CaptchaTokenSource stores updated captcha tokens in the config file
500+ type CaptchaTokenSource struct {
501+ mu sync.Mutex
502+ m configmap.Mapper
503+ opt * Options
504+ token * api.CaptchaToken
505+ ctx context.Context
506+ rst * pikpakClient
507+ }
508+
509+ // initialize CaptchaTokenSource from rclone.conf if possible
510+ func newCaptchaTokenSource (ctx context.Context , opt * Options , m configmap.Mapper ) * CaptchaTokenSource {
511+ token := new (api.CaptchaToken )
512+ tokenString , ok := m .Get ("captcha_token" )
513+ if ! ok || tokenString == "" {
514+ fs .Debugf (nil , "failed to read captcha token out of config file" )
515+ } else {
516+ if err := json .Unmarshal ([]byte (tokenString ), token ); err != nil {
517+ fs .Debugf (nil , "failed to parse captcha token out of config file: %v" , err )
518+ }
519+ }
520+ return & CaptchaTokenSource {
521+ m : m ,
522+ opt : opt ,
523+ token : token ,
524+ ctx : ctx ,
525+ rst : newPikpakClient (getClient (ctx , opt ), opt ),
526+ }
527+ }
528+
529+ // requestToken retrieves captcha token from API
530+ func (cts * CaptchaTokenSource ) requestToken (ctx context.Context , req * api.CaptchaTokenRequest ) (err error ) {
531+ opts := rest.Opts {
532+ Method : "POST" ,
533+ RootURL : "https://user.mypikpak.com/v1/shield/captcha/init" ,
534+ }
535+ var info * api.CaptchaToken
536+ _ , err = cts .rst .CallJSON (ctx , & opts , & req , & info )
537+ if err == nil && info .ExpiresIn != 0 {
538+ // populate to Expiry
539+ info .Expiry = time .Now ().Add (time .Duration (info .ExpiresIn ) * time .Second )
540+ cts .token = info // update with a new one
541+ }
542+ return
543+ }
544+
545+ func (cts * CaptchaTokenSource ) refreshToken (opts * rest.Opts ) (string , error ) {
546+ oldToken := ""
547+ if cts .token != nil {
548+ oldToken = cts .token .CaptchaToken
549+ }
550+ action := "GET:/drive/v1/about"
551+ if opts .RootURL == "" && opts .Path != "" {
552+ action = fmt .Sprintf ("%s:%s" , opts .Method , opts .Path )
553+ } else if u , err := url .Parse (opts .RootURL ); err == nil {
554+ action = fmt .Sprintf ("%s:%s" , opts .Method , u .Path )
555+ }
556+ req := newCaptchaTokenRequest (action , oldToken , cts .opt )
557+ if err := cts .requestToken (cts .ctx , req ); err != nil {
558+ return "" , fmt .Errorf ("failed to retrieve captcha token from api: %w" , err )
559+ }
560+
561+ // put it into rclone.conf
562+ tokenBytes , err := json .Marshal (cts .token )
563+ if err != nil {
564+ return "" , fmt .Errorf ("failed to marshal captcha token: %w" , err )
565+ }
566+ cts .m .Set ("captcha_token" , string (tokenBytes ))
567+ return cts .token .CaptchaToken , nil
568+ }
569+
570+ // Invalidate resets existing captcha token for a forced refresh
571+ func (cts * CaptchaTokenSource ) Invalidate () {
572+ cts .mu .Lock ()
573+ cts .token .CaptchaToken = ""
574+ cts .mu .Unlock ()
575+ }
576+
577+ // Token returns a valid captcha token
578+ func (cts * CaptchaTokenSource ) Token (opts * rest.Opts ) (string , error ) {
579+ cts .mu .Lock ()
580+ defer cts .mu .Unlock ()
581+ if cts .token .Valid () {
582+ return cts .token .CaptchaToken , nil
583+ }
584+ return cts .refreshToken (opts )
585+ }
586+
587+ // pikpakClient wraps rest.Client with a handle of captcha token
588+ type pikpakClient struct {
589+ opt * Options
590+ client * rest.Client
591+ captcha * CaptchaTokenSource
592+ }
593+
594+ // newPikpakClient takes an (oauth) http.Client and makes a new api instance for pikpak with
595+ // * error handler
596+ // * root url
597+ // * default headers
598+ func newPikpakClient (c * http.Client , opt * Options ) * pikpakClient {
599+ client := rest .NewClient (c ).SetErrorHandler (errorHandler ).SetRoot (rootURL )
600+ for key , val := range map [string ]string {
601+ "Referer" : "https://mypikpak.com/" ,
602+ "x-client-id" : clientID ,
603+ "x-client-version" : clientVersion ,
604+ "x-device-id" : opt .DeviceID ,
605+ // "x-device-model": "firefox%2F129.0",
606+ // "x-device-name": "PC-Firefox",
607+ // "x-device-sign": fmt.Sprintf("wdi10.%sxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", opt.DeviceID),
608+ // "x-net-work-type": "NONE",
609+ // "x-os-version": "Win32",
610+ // "x-platform-version": "1",
611+ // "x-protocol-version": "301",
612+ // "x-provider-name": "NONE",
613+ // "x-sdk-version": "8.0.3",
614+ } {
615+ client .SetHeader (key , val )
616+ }
617+ return & pikpakClient {
618+ client : client ,
619+ opt : opt ,
620+ }
621+ }
622+
623+ // This should be called right after pikpakClient initialized
624+ func (c * pikpakClient ) SetCaptchaTokener (ctx context.Context , m configmap.Mapper ) * pikpakClient {
625+ c .captcha = newCaptchaTokenSource (ctx , c .opt , m )
626+ return c
627+ }
628+
629+ func (c * pikpakClient ) CallJSON (ctx context.Context , opts * rest.Opts , request interface {}, response interface {}) (resp * http.Response , err error ) {
630+ if c .captcha != nil {
631+ token , err := c .captcha .Token (opts )
632+ if err != nil || token == "" {
633+ return nil , fserrors .FatalError (fmt .Errorf ("couldn't get captcha token: %v" , err ))
634+ }
635+ if opts .ExtraHeaders == nil {
636+ opts .ExtraHeaders = make (map [string ]string )
637+ }
638+ opts .ExtraHeaders ["x-captcha-token" ] = token
639+ }
640+ return c .client .CallJSON (ctx , opts , request , response )
641+ }
642+
643+ func (c * pikpakClient ) Call (ctx context.Context , opts * rest.Opts ) (resp * http.Response , err error ) {
644+ return c .client .Call (ctx , opts )
645+ }
0 commit comments