Compare commits

..

1 Commits

Author SHA1 Message Date
renovate[bot] 7e2d71282f chore(deps): pin dependencies 2026-10-01 14:00:56 +00:00
9 changed files with 92 additions and 483 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
### Default image is base. You can add other support by modifying BASE_IMAGE_TAG. The following parameters are supported: base (default), aria2, ffmpeg, aio
ARG BASE_IMAGE_TAG=base
ARG BASE_IMAGE_TAG=base@sha256:042e7139b7daf131b15bb582c2e9f1ce0f8bf2b3e9ac04a5368bf3088c2d73c2
FROM alpine:edge AS builder
LABEL stage=go-builder
+1 -1
View File
@@ -11,4 +11,4 @@ services:
- UMASK=022
- TZ=Asia/Shanghai
container_name: openlist
image: 'openlistteam/openlist:latest'
image: 'openlistteam/openlist:latest@sha256:c555c6e1c8af2aead38ed12ec761ac077fdf046d19cf033414be8e056aec6b64'
+2 -7
View File
@@ -7,16 +7,11 @@ import (
type Addition struct {
LoginType string `json:"login_type" type:"select" options:"password,qrcode" default:"password" required:"true"`
Username string `json:"username" help:"Not needed when an access token or refresh token is provided"`
Password string `json:"password" help:"Not needed when an access token or refresh token is provided"`
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
VCode string `json:"validate_code"`
SmsCode string `json:"sms_code" help:"SMS code for the second device verification, fill it in and save again when login asks for it"`
AccessToken string `json:"access_token" required:"false"`
RefreshToken string `json:"refresh_token" help:"To switch accounts, please clear this field"`
DeviceID string `json:"device_id" help:"DEVICEID cookie issued after the second device verification, keep it to avoid verifying again"`
ClientSn string `json:"client_sn" help:"Device serial number captured from the official client, leave it empty if you do not have one"`
JgOpenId string `json:"jg_open_id" help:"Optional push id reported by the official client"`
UserFinger string `json:"user_finger" help:"Device fingerprint sent with login requests, generated and kept automatically when empty"`
driver.RootID
OrderBy string `json:"order_by" type:"select" options:"filename,filesize,lastOpTime" default:"filename"`
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
-62
View File
@@ -72,8 +72,6 @@ type BaseLoginParam struct {
// 请求头参数
Lt string
ReqId string
// logbox页面地址,作为后续请求的Referer,缺失会被判定为陌生设备
Referer string
// 表单参数
ParamId string
@@ -99,20 +97,10 @@ type LoginParam struct {
// rsa密钥
jRsaKey string
// 加密字段的前缀,服务端下发(如 {NRP})
rsaPrefix string
// 设备二次校验时服务端返回的加密手机号
SecondAuthMobile string
BaseLoginParam
}
// encryptSecret 用登陆时拿到的公钥加密敏感值,格式与userName/epd一致
func (p *LoginParam) encryptSecret(value string) string {
return p.rsaPrefix + RsaEncrypt(p.jRsaKey, value)
}
// 登陆加密相关
type EncryptConfResp struct {
Result int `json:"result"`
@@ -128,35 +116,6 @@ type LoginResp struct {
Msg string `json:"msg"`
Result int `json:"result"`
ToUrl string `json:"toUrl"`
// 设备二次校验时返回的加密手机号
Mobile string `json:"mobile"`
}
// 登陆页配置,新版登陆页的paramId由该接口下发
// 该接口的result可能是数字也可能是字符串
type AppConfResp struct {
Result any `json:"result"`
Msg string `json:"msg"`
Data struct {
ParamId string `json:"paramId"`
AccountType string `json:"accountType"`
ReturnUrl string `json:"returnUrl"`
MailSuffix string `json:"mailSuffix"`
} `json:"data"`
}
func (r *AppConfResp) Succeeded() bool {
switch v := r.Result.(type) {
case nil:
return true
case string:
return v == "0" || v == ""
case float64:
return v == 0
case int:
return v == 0
}
return false
}
// 刷新session返回
@@ -190,27 +149,6 @@ type AppSessionResp struct {
RefreshToken string `json:"refreshToken"`
}
// 刷新token返回,失败时以HTTP 200返回result/msg,需要单独判断
type RefreshTokenResp struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
ExpiresIn int `json:"expiresIn"`
Result int `json:"result"`
Msg string `json:"msg"`
}
func (r *RefreshTokenResp) HasError() bool {
return r.Result != 0 || r.AccessToken == ""
}
func (r *RefreshTokenResp) Error() string {
if r.Msg != "" {
return fmt.Sprintf("refresh token failed, result: %d, msg: %s", r.Result, r.Msg)
}
return fmt.Sprintf("refresh token failed, result: %d", r.Result)
}
// 家庭云账户
type FamilyInfoListResp struct {
FamilyInfoResp []FamilyInfoResp `json:"familyInfoResp"`
+77 -355
View File
@@ -30,7 +30,6 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/errgroup"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/v4/pkg/utils/random"
"github.com/skip2/go-qrcode"
"github.com/avast/retry-go"
@@ -42,13 +41,9 @@ import (
const (
ACCOUNT_TYPE = "02"
// 官方 PC 端(cloud.189.cn 网页/客户端)使用的 appId,
// 登录、生成二维码、换取 session 必须全程使用同一个 appId
APP_ID = "9317140619"
CLIENT_TYPE = "10020"
// 扫码状态轮询使用的 clientType,与密码登录的 10020 不同
QR_CLIENT_TYPE = "1"
VERSION = "7.2.4.0"
APP_ID = "8025431004"
CLIENT_TYPE = "10020"
VERSION = "6.2"
WEB_URL = "https://cloud.189.cn"
AUTH_URL = "https://open.e.189.cn"
@@ -62,18 +57,8 @@ const (
CHANNEL_ID = "web_cloud.189.cn"
// 服务端通过短信二次校验后下发的设备标识,复用它可以避免再次触发校验
DEVICE_ID_COOKIE = "DEVICEID"
// 扫码登录本地轮询参数,超时后把二维码交回前端,避免请求被反向代理掐断
QRCODE_POLL_INTERVAL = 2 * time.Second
QRCODE_POLL_TIMEOUT = 20 * time.Second
// Error codes
UserInvalidOpenTokenError = "UserInvalidOpenToken"
// 密码登录返回该结果表示需要设备二次校验
SecondDeviceAuthResult = -133
)
func (y *Cloud189PC) SignatureHeader(url, method, params string, isFamily bool) map[string]string {
@@ -303,72 +288,9 @@ func (y *Cloud189PC) login() error {
if y.LoginType == "qrcode" {
return y.loginByQRCode()
}
if y.Username == "" || y.Password == "" {
return errors.New("please fill in the username and password, or provide an access token / refresh token")
}
return y.loginByPassword()
}
// 设备指纹,为空时生成并保存,服务端以此识别是否为同一台设备
func (y *Cloud189PC) getUserFinger() string {
if y.Addition.UserFinger == "" {
y.Addition.UserFinger = fmt.Sprint(random.Rand.Int63n(9e9) + 1e9)
op.MustSaveDriverStorage(y)
}
return y.Addition.UserFinger
}
// 换取会话时携带的设备参数,与官方PC客户端保持一致
// clientSn/jgOpenId 只在用户从官方客户端抓到并填写后才发送,避免上报一个服务端不认识的设备号
func (y *Cloud189PC) deviceParams() map[string]string {
params := map[string]string{"returnType": "JSON"}
if y.Addition.ClientSn != "" {
params["clientSn"] = y.Addition.ClientSn
}
if y.Addition.JgOpenId != "" {
params["jgOpenId"] = y.Addition.JgOpenId
}
return params
}
// logbox接口的公共请求头,缺少user-finger和Referer会被判定为陌生设备
func (y *Cloud189PC) loginHeaders(param BaseLoginParam) map[string]string {
return map[string]string{
"REQID": param.ReqId,
"lt": param.Lt,
"user-finger": y.getUserFinger(),
"Referer": IF(param.Referer != "", param.Referer, AUTH_URL),
}
}
// 把已保存的设备标识写入cookie,避免重复触发设备二次校验
func (y *Cloud189PC) applyDeviceID(jar http.CookieJar) {
if y.Addition.DeviceID == "" {
return
}
authUrl, err := url.Parse(AUTH_URL)
if err != nil {
return
}
jar.SetCookies(authUrl, []*http.Cookie{{
Name: DEVICE_ID_COOKIE,
Value: y.Addition.DeviceID,
Domain: "e.189.cn",
Path: "/",
}})
}
// 保存服务端下发的设备标识,下次登陆复用即可跳过设备二次校验
func (y *Cloud189PC) saveDeviceID(res *resty.Response) {
for _, cookie := range res.Cookies() {
if cookie.Name == DEVICE_ID_COOKIE && cookie.Value != "" && cookie.Value != y.Addition.DeviceID {
y.Addition.DeviceID = cookie.Value
op.MustSaveDriverStorage(y)
return
}
}
}
func (y *Cloud189PC) loginByPassword() (err error) {
// 初始化登陆所需参数
if y.loginParam == nil {
@@ -377,16 +299,9 @@ func (y *Cloud189PC) loginByPassword() (err error) {
return err
}
}
// 设备二次校验必须复用同一套登陆参数,此时不能销毁也不能重新初始化
keepLoginParam := false
defer func() {
// 销毁验证码
y.VCode = ""
if keepLoginParam {
y.Status = err.Error()
op.MustSaveDriverStorage(y)
return
}
// 销毁登陆参数
y.loginParam = nil
// 遇到错误,重新加载登陆参数(刷新验证码)
@@ -404,18 +319,17 @@ func (y *Cloud189PC) loginByPassword() (err error) {
param := y.loginParam
var loginresp LoginResp
res, err := y.client.R().
_, err = y.client.R().
ForceContentType("application/json;charset=UTF-8").SetResult(&loginresp).
SetHeaders(y.loginHeaders(param.BaseLoginParam)).
SetHeaders(map[string]string{
"REQID": param.ReqId,
"lt": param.Lt,
}).
SetFormData(map[string]string{
"version": "v2.0",
"apToken": "",
"appKey": APP_ID,
"pageKey": "normal",
"accountType": ACCOUNT_TYPE,
"userName": param.RsaUsername,
"password": param.RsaPassword,
"epd": param.RsaPassword,
"validateCode": y.VCode,
"captchaToken": param.CaptchaToken,
"returnUrl": RETURN_URL,
@@ -431,106 +345,17 @@ func (y *Cloud189PC) loginByPassword() (err error) {
if err != nil {
return err
}
y.saveDeviceID(res)
// 设备二次校验:服务端要求短信验证,保留登陆参数并引导填写短信验证码
if loginresp.Result == SecondDeviceAuthResult {
err = y.secondDeviceAuth(loginresp.Mobile)
// 校验未完成时保留登陆参数,等待用户回填短信验证码
keepLoginParam = err != nil && y.loginParam != nil
return err
}
if loginresp.ToUrl == "" {
return fmt.Errorf("login failed,No toUrl obtained, msg: %s", loginresp.Msg)
}
return y.getSessionByRedirectURL(loginresp.ToUrl)
}
// 设备二次校验:先发短信,用户回填验证码后再提交
func (y *Cloud189PC) secondDeviceAuth(mobile string) error {
param := y.loginParam
if mobile != "" {
param.SecondAuthMobile = mobile
}
if param.SecondAuthMobile == "" {
return errors.New("second device verification is required, but no mobile was returned")
}
// 已填写短信验证码,直接提交校验
if y.SmsCode != "" {
smsCode := y.SmsCode
y.SmsCode = ""
op.MustSaveDriverStorage(y)
return y.submitSecondDeviceAuth(smsCode)
}
var smsResp LoginResp
_, err := y.client.R().
ForceContentType("application/json;charset=UTF-8").SetResult(&smsResp).
SetHeaders(y.loginHeaders(param.BaseLoginParam)).
SetFormData(map[string]string{
"mobile": param.SecondAuthMobile,
"appKey": APP_ID,
}).
Post(AUTH_URL + "/api/logbox/oauth2/sendSmsCodeForSecondAuth.do")
if err != nil {
return err
}
if smsResp.Result != 0 {
return fmt.Errorf("failed to send the verification SMS: %s", smsResp.Msg)
}
// 保留登陆参数,等待用户回填短信验证码后重新保存
return errors.New("second device verification is required, an SMS code has been sent, please fill it into `sms_code` and save again")
}
// 提交短信验证码完成设备二次校验
// 注意:该接口没有独立的短信码字段,短信码要加密后放在epd里(登陆时epd装的是密码)
func (y *Cloud189PC) submitSecondDeviceAuth(smsCode string) error {
param := y.loginParam
var authResp LoginResp
res, err := y.client.R().
ForceContentType("application/json;charset=UTF-8").SetResult(&authResp).
SetHeaders(y.loginHeaders(param.BaseLoginParam)).
SetFormData(map[string]string{
"mobile": param.SecondAuthMobile,
"appKey": APP_ID,
"userName": param.RsaUsername,
"epd": param.encryptSecret(smsCode),
"accountType": ACCOUNT_TYPE,
"returnUrl": RETURN_URL,
"isOauth2": "false",
"cb_SaveName": "1",
"state": "",
"paramId": param.ParamId,
}).
Post(AUTH_URL + "/api/logbox/oauth2/submitForSecondAuth.do")
if err != nil {
return err
}
// 校验通过后服务端会下发DEVICEID,保存下来以后就不会再触发二次校验
y.saveDeviceID(res)
if authResp.Result != 0 {
return fmt.Errorf("second device verification failed: %s", authResp.Msg)
}
if authResp.ToUrl == "" {
return fmt.Errorf("second device verification failed, no toUrl obtained, msg: %s", authResp.Msg)
}
return y.getSessionByRedirectURL(authResp.ToUrl)
}
// 用登陆结果的跳转地址换取会话
func (y *Cloud189PC) getSessionByRedirectURL(redirectURL string) error {
// 获取Session
var erron RespErr
var tokenInfo AppSessionResp
_, err := y.client.R().
_, err = y.client.R().
SetResult(&tokenInfo).SetError(&erron).
SetQueryParams(clientSuffix()).
SetQueryParams(y.deviceParams()).
SetQueryParam("redirectURL", redirectURL).
SetHeader("X-Request-ID", uuid.NewString()).
SetQueryParam("redirectURL", loginresp.ToUrl).
Post(API_URL + "/getSessionForPC.action")
if err != nil {
return err
@@ -540,13 +365,14 @@ func (y *Cloud189PC) getSessionByRedirectURL(redirectURL string) error {
return &erron
}
if tokenInfo.ResCode != 0 {
return errors.New(tokenInfo.ResMessage)
err = fmt.Errorf(tokenInfo.ResMessage)
return err
}
y.Addition.AccessToken = tokenInfo.AccessToken
y.Addition.RefreshToken = tokenInfo.RefreshToken
y.tokenInfo = &tokenInfo
op.MustSaveDriverStorage(y)
return nil
return err
}
func (y *Cloud189PC) loginByQRCode() error {
@@ -557,74 +383,66 @@ func (y *Cloud189PC) loginByQRCode() error {
}
}
// 本地轮询,扫码确认后自动继续,不需要用户反复保存
deadline := time.Now().Add(QRCODE_POLL_TIMEOUT)
lastStatus := -106
for {
state, err := y.checkQRCodeState()
if err != nil {
return fmt.Errorf("failed to check QR code state: %w", err)
}
lastStatus = state.Status
switch state.Status {
case 0: // 登录成功
y.qrcodeParam = nil
return y.getSessionByRedirectURL(state.RedirectUrl)
case -106, -11002: // -106 等待扫描,-11002 已扫描等待确认
case -11001: // 二维码过期
y.qrcodeParam = nil
return errors.New("QR code expired, please try again")
default: // 其他错误
y.qrcodeParam = nil
return fmt.Errorf("QR code login failed with status %d: %s", state.Status, state.Msg)
}
if time.Now().Add(QRCODE_POLL_INTERVAL).After(deadline) {
break
}
time.Sleep(QRCODE_POLL_INTERVAL)
var state struct {
Status int `json:"status"`
RedirectUrl string `json:"redirectUrl"`
Msg string `json:"msg"`
}
// 轮询超时,把二维码交回前端等待下一次保存
if lastStatus == -11002 {
return y.genQRCode("QR code has been scanned, please confirm the login on your phone and save again")
}
return y.genQRCode("QR code has not been scanned yet, please scan and save again")
}
type qrCodeState struct {
Status int `json:"status"`
RedirectUrl string `json:"redirectUrl"`
Msg string `json:"msg"`
}
// 查询扫码状态,参数需与官方PC端一致,否则服务端不会返回授权结果
func (y *Cloud189PC) checkQRCodeState() (*qrCodeState, error) {
now := time.Now()
var state qrCodeState
_, err := y.client.R().
SetHeaders(y.loginHeaders(y.qrcodeParam.BaseLoginParam)).
SetHeaders(map[string]string{
"Referer": AUTH_URL,
"Reqid": y.qrcodeParam.ReqId,
"lt": y.qrcodeParam.Lt,
}).
SetFormData(map[string]string{
"appId": APP_ID,
"clientType": QR_CLIENT_TYPE,
"returnUrl": RETURN_URL,
"paramId": y.qrcodeParam.ParamId,
"uuid": y.qrcodeParam.UUID,
"encryuuid": y.qrcodeParam.EncryUUID,
"cb_SaveName": "3",
"isOauth2": "false",
"state": "",
"date": formatDate(now),
"timeStamp": fmt.Sprint(now.UTC().UnixNano() / 1e6),
"appId": APP_ID,
"clientType": CLIENT_TYPE,
"returnUrl": RETURN_URL,
"paramId": y.qrcodeParam.ParamId,
"uuid": y.qrcodeParam.UUID,
"encryuuid": y.qrcodeParam.EncryUUID,
"date": formatDate(now),
"timeStamp": fmt.Sprint(now.UTC().UnixNano() / 1e6),
}).
ForceContentType("application/json;charset=UTF-8").
SetResult(&state).
Post(AUTH_URL + "/api/logbox/oauth2/qrcodeLoginState.do")
if err != nil {
return nil, err
return fmt.Errorf("failed to check QR code state: %w", err)
}
switch state.Status {
case 0: // 登录成功
var tokenInfo AppSessionResp
_, err = y.client.R().
SetResult(&tokenInfo).
SetQueryParams(clientSuffix()).
SetQueryParam("redirectURL", state.RedirectUrl).
Post(API_URL + "/getSessionForPC.action")
if err != nil {
return err
}
if tokenInfo.ResCode != 0 {
return fmt.Errorf(tokenInfo.ResMessage)
}
y.Addition.AccessToken = tokenInfo.AccessToken
y.Addition.RefreshToken = tokenInfo.RefreshToken
y.tokenInfo = &tokenInfo
op.MustSaveDriverStorage(y)
return nil
case -11001: // 二维码过期
y.qrcodeParam = nil
return errors.New("QR code expired, please try again")
case -106: // 等待扫描
return y.genQRCode("QR code has not been scanned yet, please scan and save again")
case -11002: // 等待确认
return y.genQRCode("QR code has been scanned, please confirm the login on your phone and save again")
default: // 其他错误
y.qrcodeParam = nil
return fmt.Errorf("QR code login failed with status %d: %s", state.Status, state.Msg)
}
return &state, nil
}
func (y *Cloud189PC) genQRCode(text string) error {
@@ -650,9 +468,8 @@ func (y *Cloud189PC) genQRCode(text string) error {
}
func (y *Cloud189PC) initBaseParams() (*BaseLoginParam, error) {
// 清除cookie,并带上已保存的设备标识
// 清除cookie
jar, _ := cookiejar.New(nil)
y.applyDeviceID(jar)
y.client.SetCookieJar(jar)
res, err := y.client.R().
@@ -667,98 +484,14 @@ func (y *Cloud189PC) initBaseParams() (*BaseLoginParam, error) {
return nil, err
}
// 当前登陆页把lt/reqId放在跳转地址上,老页面则写在页内变量里,两种都要支持
param, err := parseBaseParamFromRedirect(res.RawResponse.Request.URL)
if err != nil {
param, err = parseBaseParamFromPage(res.String())
if err != nil {
return nil, err
}
return param, nil
}
// 跳转地址上没有paramId,需要再问一次appConf.do
var appConf AppConfResp
_, err = y.client.R().
SetHeaders(y.loginHeaders(*param)).
ForceContentType("application/json;charset=UTF-8").
SetResult(&appConf).
SetFormData(map[string]string{
"version": "2.0",
"appKey": APP_ID,
}).
Post(AUTH_URL + "/api/logbox/oauth2/appConf.do")
if err != nil {
return nil, err
}
if !appConf.Succeeded() || appConf.Data.ParamId == "" {
return nil, fmt.Errorf("failed to get the login paramId: %s", appConf.Msg)
}
param.ParamId = appConf.Data.ParamId
return param, nil
}
// parseBaseParamFromRedirect 从logbox跳转地址提取登陆参数,并以该地址作为后续请求的Referer
func parseBaseParamFromRedirect(finalUrl *url.URL) (*BaseLoginParam, error) {
if finalUrl == nil {
return nil, errors.New("no login page redirect")
}
query := finalUrl.Query()
lt, reqId := query.Get("lt"), query.Get("reqId")
if lt == "" || reqId == "" {
return nil, errors.New("no lt/reqId in the login page redirect")
}
return &BaseLoginParam{
Lt: lt,
ReqId: reqId,
Referer: finalUrl.String(),
CaptchaToken: regexp.MustCompile(`'captchaToken' value='(.+?)'`).FindStringSubmatch(res.String())[1],
Lt: regexp.MustCompile(`lt = "(.+?)"`).FindStringSubmatch(res.String())[1],
ParamId: regexp.MustCompile(`paramId = "(.+?)"`).FindStringSubmatch(res.String())[1],
ReqId: regexp.MustCompile(`reqId = "(.+?)"`).FindStringSubmatch(res.String())[1],
}, nil
}
// parseBaseParamFromPage 兼容把参数写在页内变量里的老登陆页
func parseBaseParamFromPage(body string) (*BaseLoginParam, error) {
lt, err := matchLoginParam(body, `lt = "(.+?)"`, "lt")
if err != nil {
return nil, err
}
reqId, err := matchLoginParam(body, `reqId = "(.+?)"`, "reqId")
if err != nil {
return nil, err
}
paramId, err := matchLoginParam(body, `paramId = "(.+?)"`, "paramId")
if err != nil {
return nil, err
}
// 老页面才有内嵌的图形验证码token
captchaToken, _ := matchLoginParam(body, `'captchaToken' value='(.+?)'`, "captchaToken")
param := &BaseLoginParam{
CaptchaToken: captchaToken,
Lt: lt,
ParamId: paramId,
ReqId: reqId,
}
encryptUrl, _ := matchLoginParam(body, `encryptUrl = "(.+?)"`, "encryptUrl")
param.Referer = AUTH_URL + "/api/logbox/separate/web/index.html?" + strings.Join([]string{
"appId=" + url.QueryEscape(APP_ID),
"lt=" + url.QueryEscape(param.Lt),
"reqId=" + url.QueryEscape(param.ReqId),
}, "&")
if encryptUrl != "" {
param.Referer += "&encryptUrl=" + url.QueryEscape(encryptUrl)
}
return param, nil
}
// matchLoginParam 从登陆页面提取参数,缺失时返回可读的错误而不是panic
func matchLoginParam(body, pattern, name string) (string, error) {
matches := regexp.MustCompile(pattern).FindStringSubmatch(body)
if len(matches) < 2 {
return "", fmt.Errorf("failed to get %s from the login page", name)
}
return matches[1], nil
}
/* 初始化登陆需要的参数
* 如果遇到验证码返回错误
*/
@@ -783,13 +516,12 @@ func (y *Cloud189PC) initLoginParam() error {
}
y.loginParam.jRsaKey = fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", encryptConf.Data.PubKey)
y.loginParam.rsaPrefix = encryptConf.Data.Pre
y.loginParam.RsaUsername = y.loginParam.encryptSecret(y.Username)
y.loginParam.RsaPassword = y.loginParam.encryptSecret(y.Password)
y.loginParam.RsaUsername = encryptConf.Data.Pre + RsaEncrypt(y.loginParam.jRsaKey, y.Username)
y.loginParam.RsaPassword = encryptConf.Data.Pre + RsaEncrypt(y.loginParam.jRsaKey, y.Password)
// 判断是否需要验证码
resp, err := y.client.R().
SetHeaders(y.loginHeaders(y.loginParam.BaseLoginParam)).
SetHeader("REQID", y.loginParam.ReqId).
SetFormData(map[string]string{
"appKey": APP_ID,
"accountType": ACCOUNT_TYPE,
@@ -844,7 +576,6 @@ func (y *Cloud189PC) initQRCodeParam() (err error) {
var qrcodeParam QRLoginParam
_, err = y.client.R().
SetHeaders(y.loginHeaders(*baseParam)).
SetFormData(map[string]string{"appId": APP_ID}).
ForceContentType("application/json;charset=UTF-8").
SetResult(&qrcodeParam).
@@ -852,9 +583,6 @@ func (y *Cloud189PC) initQRCodeParam() (err error) {
if err != nil {
return err
}
if qrcodeParam.UUID == "" {
return errors.New("failed to get the QR code uuid")
}
qrcodeParam.BaseLoginParam = *baseParam
y.qrcodeParam = &qrcodeParam
@@ -875,7 +603,6 @@ func (y *Cloud189PC) refreshSessionWithRetry(retryCount int) (err error) {
_, err = y.client.R().
SetResult(&userSessionResp).SetError(&erron).
SetQueryParams(clientSuffix()).
SetQueryParams(y.deviceParams()).
SetQueryParams(map[string]string{
"appId": APP_ID,
"accessToken": y.tokenInfo.AccessToken,
@@ -916,11 +643,12 @@ func (y *Cloud189PC) refreshTokenWithRetry(retryCount int) (err error) {
return errors.New("refresh token failed after maximum retries")
}
// 该接口刷新失败时以HTTP 200返回 result/msg,SetError不会触发,必须解析响应体判断
var tokenInfo RefreshTokenResp
var erron RespErr
var tokenInfo AppSessionResp
_, err = y.client.R().
SetResult(&tokenInfo).
ForceContentType("application/json;charset=UTF-8").
SetError(&erron).
SetFormData(map[string]string{
"clientId": APP_ID,
"refreshToken": y.tokenInfo.RefreshToken,
@@ -933,8 +661,7 @@ func (y *Cloud189PC) refreshTokenWithRetry(retryCount int) (err error) {
}
// 如果刷新失败,返回错误给上层处理
if tokenInfo.HasError() {
refreshErr := tokenInfo.Error()
if erron.HasError() {
if y.Addition.RefreshToken != "" {
y.Addition.RefreshToken = ""
op.MustSaveDriverStorage(y)
@@ -942,11 +669,7 @@ func (y *Cloud189PC) refreshTokenWithRetry(retryCount int) (err error) {
// 根据登录类型决定下一步行为
if y.LoginType == "qrcode" {
return fmt.Errorf("QR code session has expired, please re-scan the code to log in: %s", refreshErr)
}
// 没有账号密码时无法回退到完整登录,直接把刷新失败的原因返回
if y.Username == "" || y.Password == "" {
return errors.New(refreshErr)
return errors.New("QR code session has expired, please re-scan the code to log in")
}
// 密码登录模式下,尝试回退到完整登录
return y.login()
@@ -954,8 +677,7 @@ func (y *Cloud189PC) refreshTokenWithRetry(retryCount int) (err error) {
y.Addition.AccessToken = tokenInfo.AccessToken
y.Addition.RefreshToken = tokenInfo.RefreshToken
y.tokenInfo.AccessToken = tokenInfo.AccessToken
y.tokenInfo.RefreshToken = tokenInfo.RefreshToken
y.tokenInfo = &tokenInfo
op.MustSaveDriverStorage(y)
return y.refreshSessionWithRetry(retryCount + 1)
}
+8 -43
View File
@@ -315,36 +315,6 @@ var findDownPageParamReg = regexp.MustCompile(`<iframe.*?src="(.+?)"`)
// 获取文件ID
var findFileIDReg = regexp.MustCompile(`'/ajax(?:file|m)\.php\?file=(\d+)'`)
// 2026-10 改版:文件页将下载参数移入 /fn? 内页,接口变为 apifile 绝对地址并携带签名
var (
fnDomainReg = regexp.MustCompile(`var\s+domain[12]\s*=\s*'([^']*(?:ajaxfile|ajaxm)\.php\?file=(\d+)[^']*)'`)
fnSignReg = regexp.MustCompile(`var\s+wp_sign\s*=\s*'([^']*)'`)
fnAjaxDataReg = regexp.MustCompile(`var\s+ajaxdata\s*=\s*'([^']*)'`)
)
// parseFnPage 从改版后的 /fn? 内页提取下载接口地址与签名表单
// 对应页面 JS:POST domain1 {'action':'downprocess','websignkey':ajaxdata,'signs':ajaxdata,'sign':wp_sign,'websign':'2','kd':kdns,'ves':1}
func parseFnPage(pageData string) (string, map[string]string, error) {
matches := fnDomainReg.FindStringSubmatch(pageData)
if len(matches) < 3 {
return "", nil, fmt.Errorf("not find fn ajax url")
}
sign := fnSignReg.FindStringSubmatch(pageData)
ajaxdata := fnAjaxDataReg.FindStringSubmatch(pageData)
if len(sign) < 2 || len(ajaxdata) < 2 {
return "", nil, fmt.Errorf("not find fn sign")
}
return matches[1], map[string]string{
"action": "downprocess",
"websignkey": ajaxdata[1],
"signs": ajaxdata[1],
"sign": sign[1],
"websign": "2",
"kd": "1",
"ves": "1",
}, nil
}
// 获取分享链接主界面
func (d *LanZou) getShareUrlHtml(shareID string) (string, error) {
var vs string
@@ -467,23 +437,18 @@ func (d *LanZou) getFilesByShareUrl(shareID, pwd string, sharePageData string) (
return nil, err
}
nextPageData := RemoveNotes(string(data))
param, err = htmlJsonToMap(nextPageData)
if err != nil {
return nil, err
}
var resp FileShareInfoAndUrlResp[int]
matches := findFileIDReg.FindStringSubmatch(nextPageData)
if len(matches) >= 2 {
// 旧版结构:相对路径 /ajaxm.php?file=N
param, err = htmlJsonToMap(nextPageData)
if err != nil {
return nil, err
}
ajaxUrl := d.ShareUrl + matches[0][1:len(matches[0])-1]
_, err = d.post(ajaxUrl, func(req *resty.Request) { req.SetFormData(param) }, &resp)
} else if fnUrl, fnForm, ferr := parseFnPage(nextPageData); ferr == nil {
// 2026-10 改版结构:/fn? 内页携带 apifile 绝对地址与签名参数
_, err = d.post(fnUrl, func(req *resty.Request) { req.SetFormData(fnForm) }, &resp)
} else {
if len(matches) < 2 {
return nil, fmt.Errorf("not find file id")
}
ajaxUrl := d.ShareUrl + matches[0][1:len(matches[0])-1]
var resp FileShareInfoAndUrlResp[int]
_, err = d.post(ajaxUrl, func(req *resty.Request) { req.SetFormData(param) }, &resp)
if err != nil {
return nil, err
}
+1 -1
View File
@@ -10,7 +10,7 @@ require (
github.com/KarpelesLab/reflink v1.0.2
github.com/KirCute/zip v1.0.1
github.com/OpenListTeam/go-cache v0.1.0
github.com/OpenListTeam/gofakes3 v0.8.2
github.com/OpenListTeam/gofakes3 v0.8.2-0.20260911142347-cd3c030a83b4
github.com/OpenListTeam/sftpd-openlist v1.0.1
github.com/OpenListTeam/tache v0.2.2
github.com/OpenListTeam/times v0.1.0
-2
View File
@@ -53,8 +53,6 @@ github.com/OpenListTeam/go-cache v0.1.0 h1:eV2+FCP+rt+E4OCJqLUW7wGccWZNJMV0NNkh+
github.com/OpenListTeam/go-cache v0.1.0/go.mod h1:AHWjKhNK3LE4rorVdKyEALDHoeMnP8SjiNyfVlB+Pz4=
github.com/OpenListTeam/gofakes3 v0.8.2-0.20260911142347-cd3c030a83b4 h1:Zy7/qg6aCS0OF/FPIoJh9/d0IgcIxpWRvn79ACm2R/Y=
github.com/OpenListTeam/gofakes3 v0.8.2-0.20260911142347-cd3c030a83b4/go.mod h1:mS9Ywbo6aId6BrRzeYjOIOpK0QDVnMoKOIb0hpaQZ3U=
github.com/OpenListTeam/gofakes3 v0.8.2 h1:iR4B8WH0qWqWkzVTNSj2TgWw6kovTh2bV8TGMOFSnVI=
github.com/OpenListTeam/gofakes3 v0.8.2/go.mod h1:mS9Ywbo6aId6BrRzeYjOIOpK0QDVnMoKOIb0hpaQZ3U=
github.com/OpenListTeam/gsync v0.1.0 h1:ywzGybOvA3lW8K1BUjKZ2IUlT2FSlzPO4DOazfYXjcs=
github.com/OpenListTeam/gsync v0.1.0/go.mod h1:h/Rvv9aX/6CdW/7B8di3xK3xNV8dUg45Fehrd/ksZ9s=
github.com/OpenListTeam/reflink v0.0.0-20260701021214-78760eaeafef h1:67uGHancMF/abMrnkc8abVUWQiG73Wk5d8CKt3RzkFo=
+2 -11
View File
@@ -159,18 +159,9 @@ func s3RequestAuthorized(r *http.Request, authPairs map[string]string) bool {
if len(authPairs) == 0 {
return true
}
// Verify against the keys this server was configured with. V4SignVerify and
// V2SignVerify read the signature package's process-wide key store, which
// gofakes3 never writes (keys are kept per instance), so they always
// returned InvalidAccessKeyId and the 302/307 direct-transfer redirects
// never ran. Same V4-then-V2 order the auth middleware uses.
lookup := func(accessKey string) (string, bool) {
secret, ok := authPairs[accessKey]
return secret, ok
}
result := signature.V4SignVerifyWithLookup(r, lookup)
result := signature.V4SignVerify(r)
if result == signature.ErrUnsupportAlgorithm {
result = signature.V2SignVerifyWithLookup(r, lookup)
result = signature.V2SignVerify(r)
}
return result == signature.ErrNone
}