mirror of
				https://github.com/cloudreve/cloudreve.git
				synced 2025-10-31 16:49:03 +08:00 
			
		
		
		
	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
	 AaronLiu
					AaronLiu