Init V4 community edition (#2265)
* Init V4 community edition * Init V4 community edition
This commit is contained in:
@@ -1,24 +1,21 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/oss"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/upyun"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/mq"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/qiniu/go-sdk/v7/auth/qbox"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/oss"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/request"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -31,14 +28,14 @@ func SignRequired(authInstance auth.Auth) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var err error
|
||||
switch c.Request.Method {
|
||||
case "PUT", "POST", "PATCH":
|
||||
err = auth.CheckRequest(authInstance, c.Request)
|
||||
case http.MethodPut, http.MethodPost, http.MethodPatch:
|
||||
err = auth.CheckRequest(c, authInstance, c.Request)
|
||||
default:
|
||||
err = auth.CheckURI(authInstance, c.Request.URL)
|
||||
err = auth.CheckURI(c, authInstance, c.Request.URL)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.Err(serializer.CodeCredentialInvalid, err.Error(), err))
|
||||
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCredentialInvalid, err.Error(), err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
@@ -50,29 +47,55 @@ func SignRequired(authInstance auth.Auth) gin.HandlerFunc {
|
||||
// CurrentUser 获取登录用户
|
||||
func CurrentUser() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
uid := session.Get("user_id")
|
||||
if uid != nil {
|
||||
user, err := model.GetActiveUserByID(uid)
|
||||
if err == nil {
|
||||
c.Set("user", &user)
|
||||
}
|
||||
dep := dependency.FromContext(c)
|
||||
shouldContinue, err := dep.TokenAuth().VerifyAndRetrieveUser(c)
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.Err(c, err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if shouldContinue {
|
||||
// TODO: Logto handler
|
||||
}
|
||||
|
||||
uid := inventory.UserIDFromContext(c)
|
||||
if err := SetUserCtx(c, uid); err != nil {
|
||||
c.JSON(200, serializer.Err(c, err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// AuthRequired 需要登录
|
||||
func AuthRequired() gin.HandlerFunc {
|
||||
// SetUserCtx set the current login user via uid
|
||||
func SetUserCtx(c *gin.Context, uid int) error {
|
||||
dep := dependency.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
loginUser, err := userClient.GetLoginUserByID(c, uid)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "failed to get login user", err)
|
||||
}
|
||||
|
||||
SetUserCtxByUser(c, loginUser)
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetUserCtxByUser(c *gin.Context, user *ent.User) {
|
||||
util.WithValue(c, inventory.UserCtx{}, user)
|
||||
}
|
||||
|
||||
// LoginRequired 需要登录
|
||||
func LoginRequired() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if user, _ := c.Get("user"); user != nil {
|
||||
if _, ok := user.(*model.User); ok {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
if u := inventory.UserFromContext(c); u != nil && !inventory.IsAnonymousUser(u) {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, serializer.CheckLogin())
|
||||
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCheckLogin, "Login required", nil))
|
||||
c.Abort()
|
||||
}
|
||||
}
|
||||
@@ -80,60 +103,84 @@ func AuthRequired() gin.HandlerFunc {
|
||||
// WebDAVAuth 验证WebDAV登录及权限
|
||||
func WebDAVAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// OPTIONS 请求不需要鉴权,否则Windows10下无法保存文档
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
username, password, ok := c.Request.BasicAuth()
|
||||
if !ok {
|
||||
// OPTIONS 请求不需要鉴权
|
||||
if c.Request.Method == http.MethodOptions {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
c.Writer.Header()["WWW-Authenticate"] = []string{`Basic realm="cloudreve"`}
|
||||
c.Status(http.StatusUnauthorized)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
expectedUser, err := model.GetActiveUserByEmail(username)
|
||||
dep := dependency.FromContext(c)
|
||||
l := dep.Logger()
|
||||
userClient := dep.UserClient()
|
||||
expectedUser, err := userClient.GetActiveByDavAccount(c, username, password)
|
||||
if err != nil {
|
||||
if username == "" {
|
||||
if u, err := userClient.GetByEmail(c, username); err == nil {
|
||||
// Try login with known user but incorrect password, record audit log
|
||||
SetUserCtxByUser(c, u)
|
||||
}
|
||||
}
|
||||
|
||||
l.Debug("WebDAVAuth: failed to get user %q with provided credential: %s", username, err)
|
||||
c.Status(http.StatusUnauthorized)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 密码正确?
|
||||
webdav, err := model.GetWebdavByPassword(password, expectedUser.ID)
|
||||
if err != nil {
|
||||
// Validate dav account
|
||||
accounts, err := expectedUser.Edges.DavAccountsOrErr()
|
||||
if err != nil || len(accounts) == 0 {
|
||||
l.Debug("WebDAVAuth: failed to get user dav accounts %q with provided credential: %s", username, err)
|
||||
c.Status(http.StatusUnauthorized)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 用户组已启用WebDAV?
|
||||
if !expectedUser.Group.WebDAVEnabled {
|
||||
c.Status(http.StatusForbidden)
|
||||
group, err := expectedUser.Edges.GroupOrErr()
|
||||
if err != nil {
|
||||
l.Debug("WebDAVAuth: user group not found: %s", err)
|
||||
c.Status(http.StatusInternalServerError)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 用户组已启用WebDAV代理?
|
||||
if !expectedUser.Group.OptionsSerialized.WebDAVProxy {
|
||||
webdav.UseProxy = false
|
||||
if !group.Permissions.Enabled(int(types.GroupPermissionWebDAV)) {
|
||||
c.Status(http.StatusForbidden)
|
||||
l.Debug("WebDAVAuth: user %q does not have WebDAV permission.", expectedUser.Email)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("user", &expectedUser)
|
||||
c.Set("webdav", webdav)
|
||||
// 检查是否只读
|
||||
if expectedUser.Edges.DavAccounts[0].Options.Enabled(int(types.DavAccountReadOnly)) {
|
||||
switch c.Request.Method {
|
||||
case http.MethodDelete, http.MethodPut, "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK":
|
||||
c.Status(http.StatusForbidden)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
SetUserCtxByUser(c, expectedUser)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// 对上传会话进行验证
|
||||
func UseUploadSession(policyType string) gin.HandlerFunc {
|
||||
func UseUploadSession(policyType types.PolicyType) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 验证key并查找用户
|
||||
resp := uploadCallbackCheck(c, policyType)
|
||||
if resp.Code != 0 {
|
||||
c.JSON(CallbackFailedStatusCode, resp)
|
||||
err := uploadCallbackCheck(c, policyType)
|
||||
if err != nil {
|
||||
c.JSON(CallbackFailedStatusCode, serializer.Err(c, err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
@@ -143,44 +190,46 @@ func UseUploadSession(policyType string) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
// uploadCallbackCheck 对上传回调请求的 callback key 进行验证,如果成功则返回上传用户
|
||||
func uploadCallbackCheck(c *gin.Context, policyType string) serializer.Response {
|
||||
func uploadCallbackCheck(c *gin.Context, policyType types.PolicyType) error {
|
||||
// 验证 Callback Key
|
||||
sessionID := c.Param("sessionID")
|
||||
if sessionID == "" {
|
||||
return serializer.ParamErr("Session ID cannot be empty", nil)
|
||||
return serializer.NewError(serializer.CodeParamErr, "Session ID cannot be empty", nil)
|
||||
}
|
||||
|
||||
callbackSessionRaw, exist := cache.Get(filesystem.UploadSessionCachePrefix + sessionID)
|
||||
dep := dependency.FromContext(c)
|
||||
callbackSessionRaw, exist := dep.KV().Get(manager.UploadSessionCachePrefix + sessionID)
|
||||
if !exist {
|
||||
return serializer.Err(serializer.CodeUploadSessionExpired, "上传会话不存在或已过期", nil)
|
||||
return serializer.NewError(serializer.CodeUploadSessionExpired, "Upload session does not exist or expired", nil)
|
||||
}
|
||||
|
||||
callbackSession := callbackSessionRaw.(serializer.UploadSession)
|
||||
c.Set(filesystem.UploadSessionCtx, &callbackSession)
|
||||
if callbackSession.Policy.Type != policyType {
|
||||
return serializer.Err(serializer.CodePolicyNotAllowed, "", nil)
|
||||
callbackSession := callbackSessionRaw.(fs.UploadSession)
|
||||
c.Set(manager.UploadSessionCtx, &callbackSession)
|
||||
if callbackSession.Policy.Type != string(policyType) {
|
||||
return serializer.NewError(serializer.CodePolicyNotAllowed, "", nil)
|
||||
}
|
||||
|
||||
// 清理回调会话
|
||||
_ = cache.Deletes([]string{sessionID}, filesystem.UploadSessionCachePrefix)
|
||||
|
||||
// 查找用户
|
||||
user, err := model.GetActiveUserByID(callbackSession.UID)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeUserNotFound, "", err)
|
||||
if err := SetUserCtx(c, callbackSession.UID); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Set(filesystem.UserCtx, &user)
|
||||
return serializer.Response{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoteCallbackAuth 远程回调签名验证
|
||||
func RemoteCallbackAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 验证签名
|
||||
session := c.MustGet(filesystem.UploadSessionCtx).(*serializer.UploadSession)
|
||||
authInstance := auth.HMACAuth{SecretKey: []byte(session.Policy.SecretKey)}
|
||||
if err := auth.CheckRequest(authInstance, c.Request); err != nil {
|
||||
c.JSON(CallbackFailedStatusCode, serializer.Err(serializer.CodeCredentialInvalid, err.Error(), err))
|
||||
session := c.MustGet(manager.UploadSessionCtx).(*fs.UploadSession)
|
||||
if session.Policy.Edges.Node == nil {
|
||||
c.JSON(CallbackFailedStatusCode, serializer.ErrWithDetails(c, serializer.CodeCredentialInvalid, "Node not found", nil))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
authInstance := auth.HMACAuth{SecretKey: []byte(session.Policy.Edges.Node.SlaveKey)}
|
||||
if err := auth.CheckRequest(c, authInstance, c.Request); err != nil {
|
||||
c.JSON(CallbackFailedStatusCode, serializer.ErrWithDetails(c, serializer.CodeCredentialInvalid, err.Error(), err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
@@ -190,37 +239,16 @@ func RemoteCallbackAuth() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// QiniuCallbackAuth 七牛回调签名验证
|
||||
func QiniuCallbackAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session := c.MustGet(filesystem.UploadSessionCtx).(*serializer.UploadSession)
|
||||
|
||||
// 验证回调是否来自qiniu
|
||||
mac := qbox.NewMac(session.Policy.AccessKey, session.Policy.SecretKey)
|
||||
ok, err := mac.VerifyCallback(c.Request)
|
||||
if err != nil {
|
||||
util.Log().Debug("Failed to verify callback request: %s", err)
|
||||
c.JSON(401, serializer.GeneralUploadCallbackFailed{Error: "Failed to verify callback request."})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if !ok {
|
||||
c.JSON(401, serializer.GeneralUploadCallbackFailed{Error: "Invalid signature."})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// OSSCallbackAuth 阿里云OSS回调签名验证
|
||||
func OSSCallbackAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
err := oss.VerifyCallbackSignature(c.Request)
|
||||
dep := dependency.FromContext(c)
|
||||
err := oss.VerifyCallbackSignature(c.Request, dep.KV(), dep.RequestClient(
|
||||
request.WithContext(c),
|
||||
request.WithLogger(logging.FromContext(c)),
|
||||
))
|
||||
if err != nil {
|
||||
util.Log().Debug("Failed to verify callback request: %s", err)
|
||||
dep.Logger().Debug("Failed to verify callback request: %s", err)
|
||||
c.JSON(401, serializer.GeneralUploadCallbackFailed{Error: "Failed to verify callback request."})
|
||||
c.Abort()
|
||||
return
|
||||
@@ -230,71 +258,12 @@ func OSSCallbackAuth() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// UpyunCallbackAuth 又拍云回调签名验证
|
||||
func UpyunCallbackAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session := c.MustGet(filesystem.UploadSessionCtx).(*serializer.UploadSession)
|
||||
|
||||
// 获取请求正文
|
||||
body, err := ioutil.ReadAll(c.Request.Body)
|
||||
c.Request.Body.Close()
|
||||
if err != nil {
|
||||
c.JSON(401, serializer.GeneralUploadCallbackFailed{Error: err.Error()})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Request.Body = ioutil.NopCloser(bytes.NewReader(body))
|
||||
|
||||
// 准备验证Upyun回调签名
|
||||
handler := upyun.Driver{Policy: &session.Policy}
|
||||
contentMD5 := c.Request.Header.Get("Content-Md5")
|
||||
date := c.Request.Header.Get("Date")
|
||||
actualSignature := c.Request.Header.Get("Authorization")
|
||||
|
||||
// 计算正文MD5
|
||||
actualContentMD5 := fmt.Sprintf("%x", md5.Sum(body))
|
||||
if actualContentMD5 != contentMD5 {
|
||||
c.JSON(401, serializer.GeneralUploadCallbackFailed{Error: "MD5 mismatch."})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 计算理论签名
|
||||
signature := handler.Sign(context.Background(), []string{
|
||||
"POST",
|
||||
c.Request.URL.Path,
|
||||
date,
|
||||
contentMD5,
|
||||
})
|
||||
|
||||
// 对比签名
|
||||
if signature != actualSignature {
|
||||
c.JSON(401, serializer.GeneralUploadCallbackFailed{Error: "Signature not match"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// OneDriveCallbackAuth OneDrive回调签名验证
|
||||
func OneDriveCallbackAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 发送回调结束信号
|
||||
mq.GlobalMQ.Publish(c.Param("sessionID"), mq.Message{})
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// IsAdmin 必须为管理员用户组
|
||||
func IsAdmin() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
user, _ := c.Get("user")
|
||||
if user.(*model.User).Group.ID != 1 && user.(*model.User).ID != 1 {
|
||||
c.JSON(200, serializer.Err(serializer.CodeNoPermissionErr, "", nil))
|
||||
user := inventory.UserFromContext(c)
|
||||
if !user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionIsAdmin)) {
|
||||
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeNoPermissionErr, "", nil))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user