feat(policy): add Google Drive Oauth client

This commit is contained in:
Aaron Liu
2023-05-24 14:39:54 +08:00
parent 4c18e5acd1
commit 37926e3133
22 changed files with 524 additions and 79 deletions

View File

@@ -0,0 +1,72 @@
package googledrive
import (
"errors"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
"github.com/cloudreve/Cloudreve/v3/pkg/request"
)
// Client Google Drive client
type Client struct {
Endpoints *Endpoints
Policy *model.Policy
Credential *Credential
ClientID string
ClientSecret string
Redirect string
Request request.Client
ClusterController cluster.Controller
}
// Endpoints OneDrive客户端相关设置
type Endpoints struct {
UserConsentEndpoint string // OAuth认证的基URL
TokenEndpoint string // OAuth token 基URL
EndpointURL string // 接口请求的基URL
}
const (
TokenCachePrefix = "googledrive_"
oauthEndpoint = "https://oauth2.googleapis.com/token"
userConsentBase = "https://accounts.google.com/o/oauth2/auth"
v3DriveEndpoint = "https://www.googleapis.com/drive/v3"
)
var (
// Defualt required scopes
RequiredScope = []string{
"https://www.googleapis.com/auth/drive",
"openid",
"profile",
"https://www.googleapis.com/auth/userinfo.profile",
}
// ErrInvalidRefreshToken 上传策略无有效的RefreshToken
ErrInvalidRefreshToken = errors.New("no valid refresh token in this policy")
)
// NewClient 根据存储策略获取新的client
func NewClient(policy *model.Policy) (*Client, error) {
client := &Client{
Endpoints: &Endpoints{
TokenEndpoint: oauthEndpoint,
UserConsentEndpoint: userConsentBase,
EndpointURL: v3DriveEndpoint,
},
Credential: &Credential{
RefreshToken: policy.AccessKey,
},
Policy: policy,
ClientID: policy.BucketName,
ClientSecret: policy.SecretKey,
Redirect: policy.OptionsSerialized.OauthRedirect,
Request: request.NewClient(),
ClusterController: cluster.DefaultController,
}
return client, nil
}

View File

@@ -0,0 +1,70 @@
package googledrive
import (
"context"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
"github.com/cloudreve/Cloudreve/v3/pkg/request"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"net/url"
)
// Driver Google Drive 适配器
type Driver struct {
Policy *model.Policy
HTTPClient request.Client
}
// http://localhost:3000/api/v3/callback/googledrive/auth?code=4/0AVHEtk4AaNbo5YoCrMSGgoJfZfe6SgEOVmA7XtalZl8BMtdsAIRWqxt6jO4NKJCxGVxyQA&scope=profile%20openid%20https://www.googleapis.com/auth/userinfo.profile%20https://www.googleapis.com/auth/drive&authuser=0&prompt=consent
// https://accounts.google.com/o/oauth2/v2/auth?client_id=89866991293-5uja7qsbl8btuak3hb41h3o8u9jhlckg.apps.googleusercontent.com&response_type=code&redirect_uri=http://localhost:3000/api/v3/callback/googledrive/auth&scope=openid+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive&access_type=offline&prompt=consent
//https://accounts.google.com/o/oauth2/auth?client_id=202264815644.apps.googleusercontent.com&response_type=code&redirect_uri=http%3A%2F%2F127.0.0.1%3A53682%2F&scope=openid+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fphotoslibrary&access_type=offline&prompt=consent&state=MjAyMjY0ODE1NjQ0LmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tOjpYNFozY2E4eGZXRGIxVm9vLUY5YTdaeEo6Omh0dHA6Ly8xMjcuMC4wLjE6NTM2ODIv
// NewDriver 从存储策略初始化新的Driver实例
func NewDriver(policy *model.Policy) (driver.Handler, error) {
return &Driver{
Policy: policy,
HTTPClient: request.NewClient(),
}, nil
}
func (d *Driver) Put(ctx context.Context, file fsctx.FileHeader) error {
//TODO implement me
panic("implement me")
}
func (d *Driver) Delete(ctx context.Context, files []string) ([]string, error) {
//TODO implement me
panic("implement me")
}
func (d *Driver) Get(ctx context.Context, path string) (response.RSCloser, error) {
//TODO implement me
panic("implement me")
}
func (d *Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) {
//TODO implement me
panic("implement me")
}
func (d *Driver) Source(ctx context.Context, path string, url url.URL, ttl int64, isDownload bool, speed int) (string, error) {
//TODO implement me
panic("implement me")
}
func (d *Driver) Token(ctx context.Context, ttl int64, uploadSession *serializer.UploadSession, file fsctx.FileHeader) (*serializer.UploadCredential, error) {
//TODO implement me
panic("implement me")
}
func (d *Driver) CancelToken(ctx context.Context, uploadSession *serializer.UploadSession) error {
//TODO implement me
panic("implement me")
}
func (d *Driver) List(ctx context.Context, path string, recursive bool) ([]response.Object, error) {
//TODO implement me
panic("implement me")
}

View File

@@ -0,0 +1,174 @@
package googledrive
import (
"context"
"encoding/json"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/oauth"
"github.com/cloudreve/Cloudreve/v3/pkg/request"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// Credential 获取token时返回的凭证
type Credential struct {
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"`
}
// 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},
"access_type": {"offline"},
"prompt": {"consent"},
}
u, _ := url.Parse(client.Endpoints.UserConsentEndpoint)
u.RawQuery = query.Encode()
return u.String()
}
// ObtainToken 通过code或refresh_token兑换token
func (client *Client) ObtainToken(ctx context.Context, code, refreshToken string) (*Credential, error) {
body := url.Values{
"client_id": {client.ClientID},
"redirect_uri": {client.Redirect},
"client_secret": {client.ClientSecret},
}
if code != "" {
body.Add("grant_type", "authorization_code")
body.Add("code", code)
} else {
body.Add("grant_type", "refresh_token")
body.Add("refresh_token", refreshToken)
}
strBody := body.Encode()
res := client.Request.Request(
"POST",
client.Endpoints.TokenEndpoint,
io.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, isSlave bool) error {
if isSlave {
return client.fetchCredentialFromMaster(ctx)
}
oauth.GlobalMutex.Lock(client.Policy.ID)
defer oauth.GlobalMutex.Unlock(client.Policy.ID)
// 如果已存在凭证
if client.Credential != nil && client.Credential.AccessToken != "" {
// 检查已有凭证是否过期
if client.Credential.ExpiresIn > time.Now().Unix() {
// 未过期,不要更新
return nil
}
}
// 尝试从缓存中获取凭证
if cacheCredential, ok := cache.Get(TokenCachePrefix + 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("Failed to refresh credential for policy %q, please login your Google account again.", client.Policy.Name)
return ErrInvalidRefreshToken
}
credential, err := client.ObtainToken(ctx, "", client.Credential.RefreshToken)
if err != nil {
return err
}
// 更新有效期为绝对时间戳
expires := credential.ExpiresIn - 60
credential.ExpiresIn = time.Now().Add(time.Duration(expires) * time.Second).Unix()
// refresh token for Google Drive does not expire in production
credential.RefreshToken = client.Credential.RefreshToken
client.Credential = credential
// 更新缓存
cache.Set(TokenCachePrefix+client.ClientID, *credential, int(expires))
return nil
}
func (client *Client) AccessToken() string {
return client.Credential.AccessToken
}
// UpdateCredential 更新凭证,并检查有效期
func (client *Client) fetchCredentialFromMaster(ctx context.Context) error {
res, err := client.ClusterController.GetPolicyOauthToken(client.Policy.MasterID, client.Policy.ID)
if err != nil {
return err
}
client.Credential = &Credential{AccessToken: res}
return nil
}

View File

@@ -58,7 +58,7 @@ func NewClient(policy *model.Policy) (*Client, error) {
Policy: policy,
ClientID: policy.BucketName,
ClientSecret: policy.SecretKey,
Redirect: policy.OptionsSerialized.OdRedirect,
Redirect: policy.OptionsSerialized.OauthRedirect,
Request: request.NewClient(),
ClusterController: cluster.DefaultController,
}

View File

@@ -1,25 +0,0 @@
package onedrive
import "sync"
// CredentialLock 针对存储策略凭证的锁
type CredentialLock interface {
Lock(uint)
Unlock(uint)
}
var GlobalMutex = mutexMap{}
type mutexMap struct {
locks sync.Map
}
func (m *mutexMap) Lock(id uint) {
lock, _ := m.locks.LoadOrStore(id, &sync.Mutex{})
lock.(*sync.Mutex).Lock()
}
func (m *mutexMap) Unlock(id uint) {
lock, _ := m.locks.LoadOrStore(id, &sync.Mutex{})
lock.(*sync.Mutex).Unlock()
}

View File

@@ -10,6 +10,7 @@ import (
"time"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/oauth"
"github.com/cloudreve/Cloudreve/v3/pkg/request"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
)
@@ -128,8 +129,8 @@ func (client *Client) UpdateCredential(ctx context.Context, isSlave bool) error
return client.fetchCredentialFromMaster(ctx)
}
GlobalMutex.Lock(client.Policy.ID)
defer GlobalMutex.Unlock(client.Policy.ID)
oauth.GlobalMutex.Lock(client.Policy.ID)
defer oauth.GlobalMutex.Unlock(client.Policy.ID)
// 如果已存在凭证
if client.Credential != nil && client.Credential.AccessToken != "" {
@@ -175,9 +176,13 @@ func (client *Client) UpdateCredential(ctx context.Context, isSlave bool) error
return nil
}
func (client *Client) AccessToken() string {
return client.Credential.AccessToken
}
// UpdateCredential 更新凭证,并检查有效期
func (client *Client) fetchCredentialFromMaster(ctx context.Context) error {
res, err := client.ClusterController.GetOneDriveToken(client.Policy.MasterID, client.Policy.ID)
res, err := client.ClusterController.GetPolicyOauthToken(client.Policy.MasterID, client.Policy.ID)
if err != nil {
return err
}

View File

@@ -368,7 +368,7 @@ func TestClient_UpdateCredential(t *testing.T) {
// slave failed
{
mockController := &controllermock.SlaveControllerMock{}
mockController.On("GetOneDriveToken", testMock.Anything, testMock.Anything).Return("", errors.New("error"))
mockController.On("GetPolicyOauthToken", testMock.Anything, testMock.Anything).Return("", errors.New("error"))
client.ClusterController = mockController
err := client.UpdateCredential(context.Background(), true)
asserts.Error(err)
@@ -377,7 +377,7 @@ func TestClient_UpdateCredential(t *testing.T) {
// slave success
{
mockController := &controllermock.SlaveControllerMock{}
mockController.On("GetOneDriveToken", testMock.Anything, testMock.Anything).Return("AccessToken3", nil)
mockController.On("GetPolicyOauthToken", testMock.Anything, testMock.Anything).Return("AccessToken3", nil)
client.ClusterController = mockController
err := client.UpdateCredential(context.Background(), true)
asserts.NoError(err)