Feat: OneDrive OAuth / refresh token

pull/247/head
HFO4 5 years ago
parent fa3b51096a
commit 807aa5ac18

@ -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_")
}

@ -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
}

@ -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
}

@ -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
}

@ -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{}
}

@ -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 删除一个或多个文件,

@ -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
}

Loading…
Cancel
Save