@@ -28,6 +28,15 @@ import (
2828
2929// do others that not defined in Driver interface
3030
31+ const (
32+ CodeLoginRequired = http .StatusUnauthorized
33+ CodeCredentialInvalid = 40020 // Failed to issue token
34+ )
35+
36+ var (
37+ ErrorIssueToken = errors .New ("failed to issue token" )
38+ )
39+
3140func (d * CloudreveV4 ) getUA () string {
3241 if d .CustomUA != "" {
3342 return d .CustomUA
@@ -39,6 +48,23 @@ func (d *CloudreveV4) request(method string, path string, callback base.ReqCallb
3948 if d .ref != nil {
4049 return d .ref .request (method , path , callback , out )
4150 }
51+
52+ // ensure token
53+ if d .isTokenExpired () {
54+ err := d .refreshToken ()
55+ if err != nil {
56+ return err
57+ }
58+ }
59+
60+ return d ._request (method , path , callback , out )
61+ }
62+
63+ func (d * CloudreveV4 ) _request (method string , path string , callback base.ReqCallback , out any ) error {
64+ if d .ref != nil {
65+ return d .ref ._request (method , path , callback , out )
66+ }
67+
4268 u := d .Address + "/api/v4" + path
4369 req := base .RestyClient .R ()
4470 req .SetHeaders (map [string ]string {
@@ -65,15 +91,17 @@ func (d *CloudreveV4) request(method string, path string, callback base.ReqCallb
6591 }
6692
6793 if r .Code != 0 {
68- if r .Code == 401 && d .RefreshToken != "" && path != "/session/token/refresh" {
69- // try to refresh token
70- err = d .refreshToken ()
94+ if r .Code == CodeLoginRequired && d .canLogin () && path != "/session/token/refresh" {
95+ err = d .login ()
7196 if err != nil {
7297 return err
7398 }
7499 return d .request (method , path , callback , out )
75100 }
76- return errors .New (r .Msg )
101+ if r .Code == CodeCredentialInvalid {
102+ return ErrorIssueToken
103+ }
104+ return fmt .Errorf ("%d: %s" , r .Code , r .Msg )
77105 }
78106
79107 if out != nil && r .Data != nil {
@@ -91,14 +119,18 @@ func (d *CloudreveV4) request(method string, path string, callback base.ReqCallb
91119 return nil
92120}
93121
122+ func (d * CloudreveV4 ) canLogin () bool {
123+ return d .Username != "" && d .Password != ""
124+ }
125+
94126func (d * CloudreveV4 ) login () error {
95127 var siteConfig SiteLoginConfigResp
96- err := d .request (http .MethodGet , "/site/config/login" , nil , & siteConfig )
128+ err := d ._request (http .MethodGet , "/site/config/login" , nil , & siteConfig )
97129 if err != nil {
98130 return err
99131 }
100132 var prepareLogin PrepareLoginResp
101- err = d .request (http .MethodGet , "/session/prepare?email=" + d .Addition .Username , nil , & prepareLogin )
133+ err = d ._request (http .MethodGet , "/session/prepare?email=" + d .Addition .Username , nil , & prepareLogin )
102134 if err != nil {
103135 return err
104136 }
@@ -128,15 +160,15 @@ func (d *CloudreveV4) doLogin(needCaptcha bool) error {
128160 }
129161 if needCaptcha {
130162 var config BasicConfigResp
131- err = d .request (http .MethodGet , "/site/config/basic" , nil , & config )
163+ err = d ._request (http .MethodGet , "/site/config/basic" , nil , & config )
132164 if err != nil {
133165 return err
134166 }
135167 if config .CaptchaType != "normal" {
136168 return fmt .Errorf ("captcha type %s not support" , config .CaptchaType )
137169 }
138170 var captcha CaptchaResp
139- err = d .request (http .MethodGet , "/site/captcha" , nil , & captcha )
171+ err = d ._request (http .MethodGet , "/site/captcha" , nil , & captcha )
140172 if err != nil {
141173 return err
142174 }
@@ -162,41 +194,150 @@ func (d *CloudreveV4) doLogin(needCaptcha bool) error {
162194 loginBody ["captcha" ] = captchaCode
163195 }
164196 var token TokenResponse
165- err = d .request (http .MethodPost , "/session/token" , func (req * resty.Request ) {
197+ err = d ._request (http .MethodPost , "/session/token" , func (req * resty.Request ) {
166198 req .SetBody (loginBody )
167199 }, & token )
168200 if err != nil {
169201 return err
170202 }
171203 d .AccessToken , d .RefreshToken = token .Token .AccessToken , token .Token .RefreshToken
204+ d .AccessExpires , d .RefreshExpires = token .Token .AccessExpires , token .Token .RefreshExpires
172205 op .MustSaveDriverStorage (d )
173206 return nil
174207}
175208
176209func (d * CloudreveV4 ) refreshToken () error {
210+ // if no refresh token, try to login if possible
177211 if d .RefreshToken == "" {
178- if d .Username != "" {
212+ if d .canLogin () {
179213 err := d .login ()
180214 if err != nil {
181215 return fmt .Errorf ("cannot login to get refresh token, error: %s" , err )
182216 }
183217 }
184218 return nil
185219 }
220+
221+ // parse jwt to check if refresh token is valid
222+ var jwt RefreshJWT
223+ err := d .parseJWT (d .RefreshToken , & jwt )
224+ if err != nil {
225+ // if refresh token is invalid, try to login if possible
226+ if d .canLogin () {
227+ return d .login ()
228+ }
229+ d .GetStorage ().SetStatus (fmt .Sprintf ("Invalid RefreshToken: %s" , err .Error ()))
230+ op .MustSaveDriverStorage (d )
231+ return fmt .Errorf ("invalid refresh token: %w" , err )
232+ }
233+
234+ // do refresh token
186235 var token Token
187- err : = d .request (http .MethodPost , "/session/token/refresh" , func (req * resty.Request ) {
236+ err = d ._request (http .MethodPost , "/session/token/refresh" , func (req * resty.Request ) {
188237 req .SetBody (base.Json {
189238 "refresh_token" : d .RefreshToken ,
190239 })
191240 }, & token )
192241 if err != nil {
242+ if errors .Is (err , ErrorIssueToken ) {
243+ if d .canLogin () {
244+ // try to login again
245+ return d .login ()
246+ }
247+ d .GetStorage ().SetStatus ("This session is no longer valid" )
248+ op .MustSaveDriverStorage (d )
249+ return ErrorIssueToken
250+ }
193251 return err
194252 }
195253 d .AccessToken , d .RefreshToken = token .AccessToken , token .RefreshToken
254+ d .AccessExpires , d .RefreshExpires = token .AccessExpires , token .RefreshExpires
196255 op .MustSaveDriverStorage (d )
197256 return nil
198257}
199258
259+ func (d * CloudreveV4 ) parseJWT (token string , jwt any ) error {
260+ split := strings .Split (token , "." )
261+ if len (split ) != 3 {
262+ return fmt .Errorf ("invalid token length: %d, ensure the token is a valid JWT" , len (split ))
263+ }
264+ data , err := base64 .RawURLEncoding .DecodeString (split [1 ])
265+ if err != nil {
266+ return fmt .Errorf ("invalid token encoding: %w, ensure the token is a valid JWT" , err )
267+ }
268+ err = json .Unmarshal (data , & jwt )
269+ if err != nil {
270+ return fmt .Errorf ("invalid token content: %w, ensure the token is a valid JWT" , err )
271+ }
272+ return nil
273+ }
274+
275+ // check if token is expired
276+ // https://github.com/cloudreve/frontend/blob/ddfacc1c31c49be03beb71de4cc114c8811038d6/src/session/index.ts#L177-L200
277+ func (d * CloudreveV4 ) isTokenExpired () bool {
278+ if d .RefreshToken == "" {
279+ // login again if username and password is set
280+ if d .canLogin () {
281+ return true
282+ }
283+ // no refresh token, cannot refresh
284+ return false
285+ }
286+ if d .AccessToken == "" {
287+ return true
288+ }
289+ var (
290+ err error
291+ expires time.Time
292+ )
293+ // check if token is expired
294+ if d .AccessExpires != "" {
295+ // use expires field if possible to prevent timezone issue
296+ // only available after login or refresh token
297+ // 2025-08-28T02:43:07.645109985+08:00
298+ expires , err = time .Parse (time .RFC3339Nano , d .AccessExpires )
299+ if err != nil {
300+ return false
301+ }
302+ } else {
303+ // fallback to parse jwt
304+ // if failed, disable the storage
305+ var jwt AccessJWT
306+ err = d .parseJWT (d .AccessToken , & jwt )
307+ if err != nil {
308+ d .GetStorage ().SetStatus (fmt .Sprintf ("Invalid AccessToken: %s" , err .Error ()))
309+ op .MustSaveDriverStorage (d )
310+ return false
311+ }
312+ // may be have timezone issue
313+ expires = time .Unix (jwt .Exp , 0 )
314+ }
315+ // add a 10 minutes safe margin
316+ ddl := time .Now ().Add (10 * time .Minute )
317+ if expires .Before (ddl ) {
318+ // current access token expired, check if refresh token is expired
319+ // warning: cannot parse refresh token from jwt, because the exp field is not standard
320+ if d .RefreshExpires != "" {
321+ refreshExpires , err := time .Parse (time .RFC3339Nano , d .RefreshExpires )
322+ if err != nil {
323+ return false
324+ }
325+ if refreshExpires .Before (time .Now ()) {
326+ // This session is no longer valid
327+ if d .canLogin () {
328+ // try to login again
329+ return true
330+ }
331+ d .GetStorage ().SetStatus ("This session is no longer valid" )
332+ op .MustSaveDriverStorage (d )
333+ return false
334+ }
335+ }
336+ return true
337+ }
338+ return false
339+ }
340+
200341func (d * CloudreveV4 ) upLocal (ctx context.Context , file model.FileStreamer , u FileUploadResp , up driver.UpdateProgress ) error {
201342 var finish int64 = 0
202343 var chunk int = 0
0 commit comments