From 807aa5ac187b2d8cac2deeebffd7b312f309f65b Mon Sep 17 00:00:00 2001 From: HFO4 <912394456@qq.com> Date: Sun, 19 Jan 2020 12:52:36 +0800 Subject: [PATCH] Feat: OneDrive OAuth / refresh token --- models/policy.go | 17 ++ pkg/filesystem/driver/onedrive/client.go | 59 +++++++ pkg/filesystem/driver/onedrive/handller.go | 68 ++++++++ pkg/filesystem/driver/onedrive/oauth.go | 192 +++++++++++++++++++++ pkg/filesystem/driver/onedrive/options.go | 36 ++++ pkg/filesystem/driver/upyun/handller.go | 57 +++++- pkg/filesystem/filesystem.go | 8 + 7 files changed, 435 insertions(+), 2 deletions(-) create mode 100644 pkg/filesystem/driver/onedrive/client.go create mode 100644 pkg/filesystem/driver/onedrive/handller.go create mode 100644 pkg/filesystem/driver/onedrive/oauth.go create mode 100644 pkg/filesystem/driver/onedrive/options.go diff --git a/models/policy.go b/models/policy.go index 9ce9d31..3941223 100644 --- a/models/policy.go +++ b/models/policy.go @@ -43,6 +43,9 @@ type PolicyOption struct { FileType []string `json:"file_type"` // MimeType MimeType string `json:"mimetype"` + + // OdRedirect Onedrive重定向地址 + OdRedirect string `json:"od_redirect,omitempty"` } func init() { @@ -190,3 +193,17 @@ func (policy *Policy) GetUploadURL() string { } return server.ResolveReference(controller).String() } + +// UpdateAccessKey 更新 AccessKey +// TODO 测试 +func (policy *Policy) UpdateAccessKey(key string) error { + policy.AccessKey = key + err := DB.Save(policy).Error + policy.ClearCache() + return err +} + +// ClearCache 清空policy缓存 +func (policy *Policy) ClearCache() { + cache.Deletes([]string{strconv.FormatUint(uint64(policy.ID), 10)}, "policy_") +} diff --git a/pkg/filesystem/driver/onedrive/client.go b/pkg/filesystem/driver/onedrive/client.go new file mode 100644 index 0000000..61a7558 --- /dev/null +++ b/pkg/filesystem/driver/onedrive/client.go @@ -0,0 +1,59 @@ +package onedrive + +import ( + "errors" + model "github.com/HFO4/cloudreve/models" + "github.com/HFO4/cloudreve/pkg/request" +) + +var ( + // ErrAuthEndpoint 无法解析授权端点地址 + ErrAuthEndpoint = errors.New("无法解析授权端点地址") + ErrInvalidRefreshToken = errors.New("上传策略无有效的RefreshToken") +) + +// Client OneDrive客户端 +type Client struct { + Endpoints *Endpoints + Policy *model.Policy + Credential *Credential + + ClientID string + ClientSecret string + Redirect string + + Request request.Client +} + +// Endpoints OneDrive客户端相关设置 +type Endpoints struct { + OAuthURL string // OAuth认证的基URL + OAuthEndpoints *oauthEndpoint + EndpointURL string // 接口请求的基URL +} + +// NewClient 根据存储策略获取新的client +func NewClient(policy *model.Policy) (*Client, error) { + client := &Client{ + Endpoints: &Endpoints{ + OAuthURL: policy.BaseURL, + EndpointURL: policy.Server, + }, + Credential: &Credential{ + RefreshToken: policy.AccessKey, + }, + Policy: policy, + ClientID: policy.BucketName, + ClientSecret: policy.SecretKey, + Redirect: policy.OptionsSerialized.OdRedirect, + Request: request.HTTPClient{}, + } + + oauthBase := client.getOAuthEndpoint() + if oauthBase == nil { + return nil, ErrAuthEndpoint + } + client.Endpoints.OAuthEndpoints = oauthBase + + return client, nil +} diff --git a/pkg/filesystem/driver/onedrive/handller.go b/pkg/filesystem/driver/onedrive/handller.go new file mode 100644 index 0000000..90a43a1 --- /dev/null +++ b/pkg/filesystem/driver/onedrive/handller.go @@ -0,0 +1,68 @@ +package onedrive + +import ( + "context" + "errors" + model "github.com/HFO4/cloudreve/models" + "github.com/HFO4/cloudreve/pkg/filesystem/response" + "github.com/HFO4/cloudreve/pkg/serializer" + "io" + "net/url" +) + +// Driver OneDrive 适配器 +type Driver struct { + Policy *model.Policy + Client *Client +} + +// Get 获取文件 +func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser, error) { + return nil, errors.New("未实现") +} + +// Put 将文件流保存到指定目录 +func (handler Driver) Put(ctx context.Context, file io.ReadCloser, dst string, size uint64) error { + return errors.New("未实现") +} + +// Delete 删除一个或多个文件, +// 返回未删除的文件,及遇到的最后一个错误 +func (handler Driver) Delete(ctx context.Context, files []string) ([]string, error) { + return []string{}, errors.New("未实现") +} + +// Thumb 获取文件缩略图 +func (handler Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) { + return nil, errors.New("未实现") +} + +// Source 获取外链URL +func (handler Driver) Source( + ctx context.Context, + path string, + baseURL url.URL, + ttl int64, + isDownload bool, + speed int, +) (string, error) { + return "", errors.New("未实现") +} + +// Token 获取上传策略和认证Token +func (handler Driver) Token(ctx context.Context, TTL int64, key string) (serializer.UploadCredential, error) { + err := handler.Client.UpdateCredential(ctx) + if err != nil { + return serializer.UploadCredential{}, err + } + return serializer.UploadCredential{ + Policy: handler.Client.Credential.AccessToken, + }, nil + //res,err := handler.Client.ObtainToken(ctx,WithCode("M2e92c4a9-de12-cdda-9cf4-e01f67272831")) + //if err != nil{ + // return serializer.UploadCredential{},err + //} + //return serializer.UploadCredential{ + // Policy:res.RefreshToken, + //}, nil +} diff --git a/pkg/filesystem/driver/onedrive/oauth.go b/pkg/filesystem/driver/onedrive/oauth.go new file mode 100644 index 0000000..96521c7 --- /dev/null +++ b/pkg/filesystem/driver/onedrive/oauth.go @@ -0,0 +1,192 @@ +package onedrive + +import ( + "context" + "encoding/gob" + "encoding/json" + "github.com/HFO4/cloudreve/pkg/cache" + "github.com/HFO4/cloudreve/pkg/request" + "github.com/HFO4/cloudreve/pkg/util" + "io/ioutil" + "net/http" + "net/url" + "strings" + "time" +) + +// oauthEndpoint OAuth接口地址 +type oauthEndpoint struct { + token url.URL + authorize url.URL +} + +// Credential 获取token时返回的凭证 +type Credential struct { + TokenType string `json:"token_type"` + ExpiresIn int64 `json:"expires_in"` + Scope string `json:"scope"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + UserID string `json:"user_id"` +} + +// OAuthError OAuth相关接口的错误响应 +type OAuthError struct { + ErrorType string `json:"error"` + ErrorDescription string `json:"error_description"` + CorrelationID string `json:"correlation_id"` +} + +func init() { + gob.Register(Credential{}) +} + +// Error 实现error接口 +func (err OAuthError) Error() string { + return err.ErrorDescription +} + +// OAuthURL 获取OAuth认证页面URL +func (client *Client) OAuthURL(ctx context.Context, scope []string) string { + query := url.Values{ + "client_id": {client.ClientID}, + "scope": {strings.Join(scope, " ")}, + "response_type": {"code"}, + "redirect_uri": {client.Redirect}, + } + client.Endpoints.OAuthEndpoints.authorize.RawQuery = query.Encode() + return client.Endpoints.OAuthEndpoints.authorize.String() +} + +// getOAuthEndpoint 根据指定的AuthURL获取详细的认证接口地址 +func (client *Client) getOAuthEndpoint() *oauthEndpoint { + base, err := url.Parse(client.Endpoints.OAuthURL) + if err != nil { + return nil + } + var ( + token *url.URL + authorize *url.URL + ) + switch base.Host { + case "login.live.com": + token, _ = url.Parse("https://login.live.com/oauth20_token.srf") + authorize, _ = url.Parse("https://login.live.com/oauth20_authorize.srf") + default: + token, _ = url.Parse("https://login.microsoftonline.com/common/oauth2/v2.0/token") + authorize, _ = url.Parse("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") + } + + return &oauthEndpoint{ + token: *token, + authorize: *authorize, + } +} + +// ObtainToken 通过code或refresh_token兑换token +func (client *Client) ObtainToken(ctx context.Context, opts ...Option) (*Credential, error) { + options := newDefaultOption() + for _, o := range opts { + o.apply(options) + } + + body := url.Values{ + "client_id": {client.ClientID}, + "redirect_uri": {client.Redirect}, + "client_secret": {client.ClientSecret}, + } + if options.code != "" { + body.Add("grant_type", "authorization_code") + body.Add("code", options.code) + } else { + body.Add("grant_type", "refresh_token") + body.Add("refresh_token", options.refreshToken) + } + strBody := body.Encode() + + res := client.Request.Request( + "POST", + client.Endpoints.OAuthEndpoints.token.String(), + ioutil.NopCloser(strings.NewReader(strBody)), + request.WithHeader(http.Header{ + "Content-Type": {"application/x-www-form-urlencoded"}}, + ), + request.WithContentLength(int64(len(strBody))), + ) + if res.Err != nil { + return nil, res.Err + } + + respBody, err := res.GetResponse() + if err != nil { + return nil, err + } + + var ( + errResp OAuthError + credential Credential + decodeErr error + ) + + if res.Response.StatusCode != 200 { + decodeErr = json.Unmarshal([]byte(respBody), &errResp) + } else { + decodeErr = json.Unmarshal([]byte(respBody), &credential) + } + if decodeErr != nil { + return nil, decodeErr + } + + if errResp.ErrorType != "" { + return nil, errResp + } + + return &credential, nil + +} + +// UpdateCredential 更新凭证,并检查有效期 +func (client *Client) UpdateCredential(ctx context.Context) error { + // 如果已存在凭证 + if client.Credential != nil && client.Credential.AccessToken != "" { + // 检查已有凭证是否过期 + if client.Credential.ExpiresIn > time.Now().Unix() { + // 未过期,不要更新 + return nil + } + } + + // 尝试从缓存中获取凭证 + if cacheCredential, ok := cache.Get("onedrive_" + client.ClientID); ok { + credential := cacheCredential.(Credential) + if credential.ExpiresIn > time.Now().Unix() { + client.Credential = &credential + return nil + } + } + + // 获取新的凭证 + if client.Credential == nil || client.Credential.RefreshToken == "" { + // 无有效的RefreshToken + util.Log().Error("上传策略[%s]凭证刷新失败,请重新授权OneDrive账号", client.Policy.Name) + return ErrInvalidRefreshToken + } + + credential, err := client.ObtainToken(ctx, WithRefreshToken(client.Credential.RefreshToken)) + if err != nil { + return err + } + + // 更新有效期为绝对时间戳 + expires := credential.ExpiresIn - 60 + credential.ExpiresIn = time.Now().Add(time.Duration(expires) * time.Second).Unix() + client.Credential = credential + + // 更新存储策略的 RefreshToken + client.Policy.UpdateAccessKey(credential.RefreshToken) + + // 更新缓存 + cache.Set("onedrive_"+client.ClientID, *credential, int(expires)) + + return nil +} diff --git a/pkg/filesystem/driver/onedrive/options.go b/pkg/filesystem/driver/onedrive/options.go new file mode 100644 index 0000000..e9e444c --- /dev/null +++ b/pkg/filesystem/driver/onedrive/options.go @@ -0,0 +1,36 @@ +package onedrive + +// Option 发送请求的额外设置 +type Option interface { + apply(*options) +} + +type options struct { + redirect string + code string + refreshToken string +} + +type optionFunc func(*options) + +// WithCode 设置接口Code +func WithCode(t string) Option { + return optionFunc(func(o *options) { + o.code = t + }) +} + +// WithRefreshToken 设置接口RefreshToken +func WithRefreshToken(t string) Option { + return optionFunc(func(o *options) { + o.refreshToken = t + }) +} + +func (f optionFunc) apply(o *options) { + f(o) +} + +func newDefaultOption() *options { + return &options{} +} diff --git a/pkg/filesystem/driver/upyun/handller.go b/pkg/filesystem/driver/upyun/handller.go index feed61e..7ccab2a 100644 --- a/pkg/filesystem/driver/upyun/handller.go +++ b/pkg/filesystem/driver/upyun/handller.go @@ -12,9 +12,11 @@ import ( model "github.com/HFO4/cloudreve/models" "github.com/HFO4/cloudreve/pkg/filesystem/fsctx" "github.com/HFO4/cloudreve/pkg/filesystem/response" + "github.com/HFO4/cloudreve/pkg/request" "github.com/HFO4/cloudreve/pkg/serializer" "github.com/upyun/go-sdk/upyun" "io" + "net/http" "net/url" "strconv" "strings" @@ -40,12 +42,63 @@ type Driver struct { // Get 获取文件 func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser, error) { - return nil, errors.New("未实现") + // 给文件名加上随机参数以强制拉取 + path = fmt.Sprintf("%s?v=%d", path, time.Now().UnixNano()) + + // 获取文件源地址 + downloadURL, err := handler.Source( + ctx, + path, + url.URL{}, + int64(model.GetIntSetting("preview_timeout", 60)), + false, + 0, + ) + if err != nil { + return nil, err + } + + // 获取文件数据流 + client := request.HTTPClient{} + resp, err := client.Request( + "GET", + downloadURL, + nil, + request.WithContext(ctx), + request.WithHeader( + http.Header{"Cache-Control": {"no-cache", "no-store", "must-revalidate"}}, + ), + ).CheckHTTPResponse(200).GetRSCloser() + if err != nil { + return nil, err + } + + resp.SetFirstFakeChunk() + + // 尝试自主获取文件大小 + if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok { + resp.SetContentLength(int64(file.Size)) + } + + return resp, nil + } // Put 将文件流保存到指定目录 func (handler Driver) Put(ctx context.Context, file io.ReadCloser, dst string, size uint64) error { - return errors.New("未实现") + defer file.Close() + + up := upyun.NewUpYun(&upyun.UpYunConfig{ + Bucket: handler.Policy.BucketName, + Operator: handler.Policy.AccessKey, + Password: handler.Policy.SecretKey, + }) + err := up.Put(&upyun.PutObjectConfig{ + Path: dst, + Reader: file, + }) + + return err } // Delete 删除一个或多个文件, diff --git a/pkg/filesystem/filesystem.go b/pkg/filesystem/filesystem.go index 0f52291..e780514 100644 --- a/pkg/filesystem/filesystem.go +++ b/pkg/filesystem/filesystem.go @@ -6,6 +6,7 @@ import ( "github.com/HFO4/cloudreve/pkg/auth" "github.com/HFO4/cloudreve/pkg/conf" "github.com/HFO4/cloudreve/pkg/filesystem/driver/local" + "github.com/HFO4/cloudreve/pkg/filesystem/driver/onedrive" "github.com/HFO4/cloudreve/pkg/filesystem/driver/oss" "github.com/HFO4/cloudreve/pkg/filesystem/driver/qiniu" "github.com/HFO4/cloudreve/pkg/filesystem/driver/remote" @@ -181,6 +182,13 @@ func (fs *FileSystem) DispatchHandler() error { Policy: currentPolicy, } return nil + case "onedrive": + client, err := onedrive.NewClient(currentPolicy) + fs.Handler = onedrive.Driver{ + Policy: currentPolicy, + Client: client, + } + return err default: return ErrUnknownPolicyType }