This commit is contained in:
2024-02-25 08:30:34 +08:00
commit 4947f39e74
273 changed files with 45396 additions and 0 deletions

323
middleware/auth.go Normal file
View File

@ -0,0 +1,323 @@
package middleware
import (
"bytes"
"context"
"crypto/md5"
"fmt"
"io/ioutil"
"net/http"
"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"
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/gin-gonic/gin"
)
const (
CallbackFailedStatusCode = http.StatusUnauthorized
)
// SignRequired 验证请求签名
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)
default:
err = auth.CheckURI(authInstance, c.Request.URL)
}
if err != nil {
c.JSON(200, serializer.Err(serializer.CodeCredentialInvalid, err.Error(), err))
c.Abort()
return
}
c.Next()
}
}
// 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)
}
}
c.Next()
}
}
// AuthRequired 需要登录
func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
if user, _ := c.Get("user"); user != nil {
if _, ok := user.(*model.User); ok {
c.Next()
return
}
}
c.JSON(200, serializer.CheckLogin())
c.Abort()
}
}
// PhoneRequired 需要绑定手机
// TODO 有bug
func PhoneRequired() gin.HandlerFunc {
return func(c *gin.Context) {
if model.IsTrueVal(model.GetSettingByName("phone_required")) &&
model.IsTrueVal(model.GetSettingByName("phone_enabled")) {
user, _ := c.Get("user")
if user.(*model.User).Phone != "" {
// TODO 忽略管理员
c.Next()
return
}
}
c.Next()
}
}
// 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 {
c.Writer.Header()["WWW-Authenticate"] = []string{`Basic realm="cloudreve"`}
c.Status(http.StatusUnauthorized)
c.Abort()
return
}
expectedUser, err := model.GetActiveUserByEmail(username)
if err != nil {
c.Status(http.StatusUnauthorized)
c.Abort()
return
}
// 密码正确?
webdav, err := model.GetWebdavByPassword(password, expectedUser.ID)
if err != nil {
c.Status(http.StatusUnauthorized)
c.Abort()
return
}
// 用户组已启用WebDAV
if !expectedUser.Group.WebDAVEnabled {
c.Status(http.StatusForbidden)
c.Abort()
return
}
// 用户组已启用WebDAV代理
if !expectedUser.Group.OptionsSerialized.WebDAVProxy {
webdav.UseProxy = false
}
c.Set("user", &expectedUser)
c.Set("webdav", webdav)
c.Next()
}
}
// 对上传会话进行验证
func UseUploadSession(policyType string) gin.HandlerFunc {
return func(c *gin.Context) {
// 验证key并查找用户
resp := uploadCallbackCheck(c, policyType)
if resp.Code != 0 {
c.JSON(CallbackFailedStatusCode, resp)
c.Abort()
return
}
c.Next()
}
}
// uploadCallbackCheck 对上传回调请求的 callback key 进行验证,如果成功则返回上传用户
func uploadCallbackCheck(c *gin.Context, policyType string) serializer.Response {
// 验证 Callback Key
sessionID := c.Param("sessionID")
if sessionID == "" {
return serializer.ParamErr("Session ID cannot be empty", nil)
}
callbackSessionRaw, exist := cache.Get(filesystem.UploadSessionCachePrefix + sessionID)
if !exist {
return serializer.Err(serializer.CodeUploadSessionExpired, "上传会话不存在或已过期", nil)
}
callbackSession := callbackSessionRaw.(serializer.UploadSession)
c.Set(filesystem.UploadSessionCtx, &callbackSession)
if callbackSession.Policy.Type != policyType {
return serializer.Err(serializer.CodePolicyNotAllowed, "", nil)
}
// 清理回调会话
_ = cache.Deletes([]string{sessionID}, filesystem.UploadSessionCachePrefix)
// 查找用户
user, err := model.GetActiveUserByID(callbackSession.UID)
if err != nil {
return serializer.Err(serializer.CodeUserNotFound, "", err)
}
c.Set(filesystem.UserCtx, &user)
return serializer.Response{}
}
// 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))
c.Abort()
return
}
c.Next()
}
}
// 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)
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
}
c.Next()
}
}
// 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))
c.Abort()
return
}
c.Next()
}
}

127
middleware/captcha.go Normal file
View File

@ -0,0 +1,127 @@
package middleware
import (
"bytes"
"encoding/json"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/recaptcha"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/gin-gonic/gin"
"github.com/mojocn/base64Captcha"
captcha "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/captcha/v20190722"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
"io"
"io/ioutil"
"strconv"
"time"
)
type req struct {
CaptchaCode string `json:"captchaCode"`
Ticket string `json:"ticket"`
Randstr string `json:"randstr"`
}
const (
captchaNotMatch = "CAPTCHA not match."
captchaRefresh = "Verification failed, please refresh the page and retry."
)
// CaptchaRequired 验证请求签名
func CaptchaRequired(configName string) gin.HandlerFunc {
return func(c *gin.Context) {
// 相关设定
options := model.GetSettingByNames(configName,
"captcha_type",
"captcha_ReCaptchaSecret",
"captcha_TCaptcha_SecretId",
"captcha_TCaptcha_SecretKey",
"captcha_TCaptcha_CaptchaAppId",
"captcha_TCaptcha_AppSecretKey")
// 检查验证码
isCaptchaRequired := model.IsTrueVal(options[configName])
if isCaptchaRequired {
var service req
bodyCopy := new(bytes.Buffer)
_, err := io.Copy(bodyCopy, c.Request.Body)
if err != nil {
c.JSON(200, serializer.Err(serializer.CodeCaptchaError, captchaNotMatch, err))
c.Abort()
return
}
bodyData := bodyCopy.Bytes()
err = json.Unmarshal(bodyData, &service)
if err != nil {
c.JSON(200, serializer.Err(serializer.CodeCaptchaError, captchaNotMatch, err))
c.Abort()
return
}
c.Request.Body = ioutil.NopCloser(bytes.NewReader(bodyData))
switch options["captcha_type"] {
case "normal":
captchaID := util.GetSession(c, "captchaID")
util.DeleteSession(c, "captchaID")
if captchaID == nil || !base64Captcha.VerifyCaptcha(captchaID.(string), service.CaptchaCode) {
c.JSON(200, serializer.Err(serializer.CodeCaptchaError, captchaNotMatch, err))
c.Abort()
return
}
break
case "recaptcha":
reCAPTCHA, err := recaptcha.NewReCAPTCHA(options["captcha_ReCaptchaSecret"], recaptcha.V2, 10*time.Second)
if err != nil {
util.Log().Warning("reCAPTCHA verification failed, %s", err)
c.Abort()
break
}
err = reCAPTCHA.Verify(service.CaptchaCode)
if err != nil {
util.Log().Warning("reCAPTCHA verification failed, %s", err)
c.JSON(200, serializer.Err(serializer.CodeCaptchaRefreshNeeded, captchaRefresh, nil))
c.Abort()
return
}
break
case "tcaptcha":
credential := common.NewCredential(
options["captcha_TCaptcha_SecretId"],
options["captcha_TCaptcha_SecretKey"],
)
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "captcha.tencentcloudapi.com"
client, _ := captcha.NewClient(credential, "", cpf)
request := captcha.NewDescribeCaptchaResultRequest()
request.CaptchaType = common.Uint64Ptr(9)
appid, _ := strconv.Atoi(options["captcha_TCaptcha_CaptchaAppId"])
request.CaptchaAppId = common.Uint64Ptr(uint64(appid))
request.AppSecretKey = common.StringPtr(options["captcha_TCaptcha_AppSecretKey"])
request.Ticket = common.StringPtr(service.Ticket)
request.Randstr = common.StringPtr(service.Randstr)
request.UserIp = common.StringPtr(c.ClientIP())
response, err := client.DescribeCaptchaResult(request)
if err != nil {
util.Log().Warning("TCaptcha verification failed, %s", err)
c.Abort()
break
}
if *response.Response.CaptchaCode != int64(1) {
c.JSON(200, serializer.Err(serializer.CodeCaptchaRefreshNeeded, captchaRefresh, nil))
c.Abort()
return
}
break
}
}
c.Next()
}
}

62
middleware/cluster.go Normal file
View File

@ -0,0 +1,62 @@
package middleware
import (
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/gin-gonic/gin"
"strconv"
)
// MasterMetadata 解析主机节点发来请求的包含主机节点信息的元数据
func MasterMetadata() gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("MasterSiteID", c.GetHeader(auth.CrHeaderPrefix+"Site-Id"))
c.Set("MasterSiteURL", c.GetHeader(auth.CrHeaderPrefix+"Site-Url"))
c.Set("MasterVersion", c.GetHeader(auth.CrHeaderPrefix+"Cloudreve-Version"))
c.Next()
}
}
// UseSlaveAria2Instance 从机用于获取对应主机节点的Aria2实例
func UseSlaveAria2Instance(clusterController cluster.Controller) gin.HandlerFunc {
return func(c *gin.Context) {
if siteID, exist := c.Get("MasterSiteID"); exist {
// 获取对应主机节点的从机Aria2实例
caller, err := clusterController.GetAria2Instance(siteID.(string))
if err != nil {
c.JSON(200, serializer.Err(serializer.CodeNotSet, "Failed to get Aria2 instance", err))
c.Abort()
return
}
c.Set("MasterAria2Instance", caller)
c.Next()
return
}
c.JSON(200, serializer.ParamErr("Unknown master node ID", nil))
c.Abort()
}
}
func SlaveRPCSignRequired(nodePool cluster.Pool) gin.HandlerFunc {
return func(c *gin.Context) {
nodeID, err := strconv.ParseUint(c.GetHeader(auth.CrHeaderPrefix+"Node-Id"), 10, 64)
if err != nil {
c.JSON(200, serializer.ParamErr("Unknown master node ID", err))
c.Abort()
return
}
slaveNode := nodePool.GetNodeByID(uint(nodeID))
if slaveNode == nil {
c.JSON(200, serializer.ParamErr("Unknown master node ID", err))
c.Abort()
return
}
SignRequired(slaveNode.MasterAuthInstance())(c)
}
}

77
middleware/common.go Normal file
View File

@ -0,0 +1,77 @@
package middleware
import (
"fmt"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/gin-gonic/gin"
"net/http"
)
// HashID 将给定对象的HashID转换为真实ID
func HashID(IDType int) gin.HandlerFunc {
return func(c *gin.Context) {
if c.Param("id") != "" {
id, err := hashid.DecodeHashID(c.Param("id"), IDType)
if err == nil {
c.Set("object_id", id)
c.Next()
return
}
c.JSON(200, serializer.ParamErr("Failed to parse object ID", nil))
c.Abort()
return
}
c.Next()
}
}
// IsFunctionEnabled 当功能未开启时阻止访问
func IsFunctionEnabled(key string) gin.HandlerFunc {
return func(c *gin.Context) {
if !model.IsTrueVal(model.GetSettingByName(key)) {
c.JSON(200, serializer.Err(serializer.CodeFeatureNotEnabled, "This feature is not enabled", nil))
c.Abort()
return
}
c.Next()
}
}
// CacheControl 屏蔽客户端缓存
func CacheControl() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Cache-Control", "private, no-cache")
}
}
func Sandbox() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Content-Security-Policy", "sandbox")
}
}
// StaticResourceCache 使用静态资源缓存策略
func StaticResourceCache() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", model.GetIntSetting("public_resource_maxage", 86400)))
}
}
// MobileRequestOnly
func MobileRequestOnly() gin.HandlerFunc {
return func(c *gin.Context) {
if c.GetHeader(auth.CrHeaderPrefix+"ios") == "" {
c.Redirect(http.StatusMovedPermanently, model.GetSiteURL().String())
c.Abort()
return
}
c.Next()
}
}

30
middleware/file.go Normal file
View File

@ -0,0 +1,30 @@
package middleware
import (
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/gin-gonic/gin"
)
// ValidateSourceLink validates if the perm source link is a valid redirect link
func ValidateSourceLink() gin.HandlerFunc {
return func(c *gin.Context) {
linkID, ok := c.Get("object_id")
if !ok {
c.JSON(200, serializer.Err(serializer.CodeFileNotFound, "", nil))
c.Abort()
return
}
sourceLink, err := model.GetSourceLinkByID(linkID)
if err != nil || sourceLink.File.ID == 0 || sourceLink.File.Name != c.Param("name") {
c.JSON(200, serializer.Err(serializer.CodeFileNotFound, "", nil))
c.Abort()
return
}
sourceLink.Downloaded()
c.Set("source_link", sourceLink)
c.Next()
}
}

84
middleware/frontend.go Normal file
View File

@ -0,0 +1,84 @@
package middleware
import (
"io/ioutil"
"net/http"
"strings"
"github.com/cloudreve/Cloudreve/v3/bootstrap"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/gin-gonic/gin"
)
// FrontendFileHandler 前端静态文件处理
func FrontendFileHandler() gin.HandlerFunc {
ignoreFunc := func(c *gin.Context) {
c.Next()
}
if bootstrap.StaticFS == nil {
return ignoreFunc
}
// 读取index.html
file, err := bootstrap.StaticFS.Open("/index.html")
if err != nil {
util.Log().Warning("Static file \"index.html\" does not exist, it might affect the display of the homepage.")
return ignoreFunc
}
fileContentBytes, err := ioutil.ReadAll(file)
if err != nil {
util.Log().Warning("Cannot read static file \"index.html\", it might affect the display of the homepage.")
return ignoreFunc
}
fileContent := string(fileContentBytes)
fileServer := http.FileServer(bootstrap.StaticFS)
return func(c *gin.Context) {
path := c.Request.URL.Path
// API 跳过
if strings.HasPrefix(path, "/api") ||
strings.HasPrefix(path, "/custom") ||
strings.HasPrefix(path, "/dav") ||
strings.HasPrefix(path, "/f") ||
path == "/manifest.json" {
c.Next()
return
}
// 不存在的路径和index.html均返回index.html
if (path == "/index.html") || (path == "/") || !bootstrap.StaticFS.Exists("/", path) {
// 读取、替换站点设置
options := model.GetSettingByNames(
"siteName", // 站点名称
"siteKeywords", // 关键词
"siteDes", // 描述
"siteScript", // 自定义代码
"pwa_small_icon", // 图标
)
finalHTML := util.Replace(map[string]string{
"{siteName}": options["siteName"],
"{siteKeywords}": options["siteKeywords"],
"{siteDes}": options["siteDes"],
"{siteScript}": options["siteScript"],
"{pwa_small_icon}": options["pwa_small_icon"],
}, fileContent)
c.Header("Content-Type", "text/html")
c.String(200, finalHTML)
c.Abort()
return
}
if path == "/service-worker.js" {
c.Header("Cache-Control", "public, no-cache")
}
// 存在的静态文件
fileServer.ServeHTTP(c.Writer, c.Request)
c.Abort()
}
}

24
middleware/mock.go Normal file
View File

@ -0,0 +1,24 @@
package middleware
import (
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/gin-gonic/gin"
)
// SessionMock 测试时模拟Session
var SessionMock = make(map[string]interface{})
// ContextMock 测试时模拟Context
var ContextMock = make(map[string]interface{})
// MockHelper 单元测试助手中间件
func MockHelper() gin.HandlerFunc {
return func(c *gin.Context) {
// 将SessionMock写入会话
util.SetSession(c, SessionMock)
for key, value := range ContextMock {
c.Set(key, value)
}
c.Next()
}
}

68
middleware/session.go Normal file
View File

@ -0,0 +1,68 @@
package middleware
import (
"net/http"
"strings"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/sessionstore"
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
// Store session存储
var Store sessions.Store
// Session 初始化session
func Session(secret string) gin.HandlerFunc {
// Redis设置不为空且非测试模式时使用Redis
Store = sessionstore.NewStore(cache.Store, []byte(secret))
sameSiteMode := http.SameSiteDefaultMode
switch strings.ToLower(conf.CORSConfig.SameSite) {
case "default":
sameSiteMode = http.SameSiteDefaultMode
case "none":
sameSiteMode = http.SameSiteNoneMode
case "strict":
sameSiteMode = http.SameSiteStrictMode
case "lax":
sameSiteMode = http.SameSiteLaxMode
}
// Also set Secure: true if using SSL, you should though
Store.Options(sessions.Options{
HttpOnly: true,
MaxAge: 60 * 86400,
Path: "/",
SameSite: sameSiteMode,
Secure: conf.CORSConfig.Secure,
})
return sessions.Sessions("cloudreve-session", Store)
}
// CSRFInit 初始化CSRF标记
func CSRFInit() gin.HandlerFunc {
return func(c *gin.Context) {
util.SetSession(c, map[string]interface{}{"CSRF": true})
c.Next()
}
}
// CSRFCheck 检查CSRF标记
func CSRFCheck() gin.HandlerFunc {
return func(c *gin.Context) {
if check, ok := util.GetSession(c, "CSRF").(bool); ok && check {
c.Next()
return
}
c.JSON(200, serializer.Err(serializer.CodeNoPermissionErr, "Invalid origin", nil))
c.Abort()
}
}

139
middleware/share.go Normal file
View File

@ -0,0 +1,139 @@
package middleware
import (
"fmt"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/gin-gonic/gin"
)
// ShareOwner 检查当前登录用户是否为分享所有者
func ShareOwner() gin.HandlerFunc {
return func(c *gin.Context) {
var user *model.User
if userCtx, ok := c.Get("user"); ok {
user = userCtx.(*model.User)
} else {
c.JSON(200, serializer.Err(serializer.CodeCheckLogin, "", nil))
c.Abort()
return
}
if share, ok := c.Get("share"); ok {
if share.(*model.Share).Creator().ID != user.ID {
c.JSON(200, serializer.Err(serializer.CodeShareLinkNotFound, "", nil))
c.Abort()
return
}
}
c.Next()
}
}
// ShareAvailable 检查分享是否可用
func ShareAvailable() gin.HandlerFunc {
return func(c *gin.Context) {
var user *model.User
if userCtx, ok := c.Get("user"); ok {
user = userCtx.(*model.User)
} else {
user = model.NewAnonymousUser()
}
share := model.GetShareByHashID(c.Param("id"))
if share == nil || !share.IsAvailable() {
c.JSON(200, serializer.Err(serializer.CodeShareLinkNotFound, "", nil))
c.Abort()
return
}
c.Set("user", user)
c.Set("share", share)
c.Next()
}
}
// ShareCanPreview 检查分享是否可被预览
func ShareCanPreview() gin.HandlerFunc {
return func(c *gin.Context) {
if share, ok := c.Get("share"); ok {
if share.(*model.Share).PreviewEnabled {
c.Next()
return
}
c.JSON(200, serializer.Err(serializer.CodeDisabledSharePreview, "",
nil))
c.Abort()
return
}
c.Abort()
}
}
// CheckShareUnlocked 检查分享是否已解锁
func CheckShareUnlocked() gin.HandlerFunc {
return func(c *gin.Context) {
if shareCtx, ok := c.Get("share"); ok {
share := shareCtx.(*model.Share)
// 分享是否已解锁
if share.Password != "" {
sessionKey := fmt.Sprintf("share_unlock_%d", share.ID)
unlocked := util.GetSession(c, sessionKey) != nil
if !unlocked {
c.JSON(200, serializer.Err(serializer.CodeNoPermissionErr,
"", nil))
c.Abort()
return
}
}
c.Next()
return
}
c.Abort()
}
}
// BeforeShareDownload 分享被下载前的检查
func BeforeShareDownload() gin.HandlerFunc {
return func(c *gin.Context) {
if shareCtx, ok := c.Get("share"); ok {
if userCtx, ok := c.Get("user"); ok {
share := shareCtx.(*model.Share)
user := userCtx.(*model.User)
// 检查用户是否可以下载此分享的文件
err := share.CanBeDownloadBy(user)
if err != nil {
c.JSON(200, serializer.Err(serializer.CodeGroupNotAllowed, err.Error(),
nil))
c.Abort()
return
}
// 对积分、下载次数进行更新
err = share.DownloadBy(user, c)
if err != nil {
if err == model.ErrInsufficientCredit {
c.JSON(200, serializer.Err(serializer.CodeInsufficientCredit, err.Error(),
nil))
} else {
c.JSON(200, serializer.Err(serializer.CodeGroupNotAllowed, err.Error(),
nil))
}
c.Abort()
return
}
c.Next()
return
}
}
c.Abort()
}
}

70
middleware/wopi.go Normal file
View File

@ -0,0 +1,70 @@
package middleware
import (
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/wopi"
"github.com/gin-gonic/gin"
"net/http"
"strings"
)
const (
WopiSessionCtx = "wopi_session"
)
// WopiWriteAccess validates if write access is obtained.
func WopiWriteAccess() gin.HandlerFunc {
return func(c *gin.Context) {
session := c.MustGet(WopiSessionCtx).(*wopi.SessionCache)
if session.Action != wopi.ActionEdit {
c.Status(http.StatusNotFound)
c.Header(wopi.ServerErrorHeader, "read-only access")
c.Abort()
return
}
c.Next()
}
}
func WopiAccessValidation(w wopi.Client, store cache.Driver) gin.HandlerFunc {
return func(c *gin.Context) {
accessToken := strings.Split(c.Query(wopi.AccessTokenQuery), ".")
if len(accessToken) != 2 {
c.Status(http.StatusForbidden)
c.Header(wopi.ServerErrorHeader, "malformed access token")
c.Abort()
return
}
sessionRaw, exist := store.Get(wopi.SessionCachePrefix + accessToken[0])
if !exist {
c.Status(http.StatusForbidden)
c.Header(wopi.ServerErrorHeader, "invalid access token")
c.Abort()
return
}
session := sessionRaw.(wopi.SessionCache)
user, err := model.GetActiveUserByID(session.UserID)
if err != nil {
c.Status(http.StatusInternalServerError)
c.Header(wopi.ServerErrorHeader, "user not found")
c.Abort()
return
}
fileID := c.MustGet("object_id").(uint)
if fileID != session.FileID {
c.Status(http.StatusInternalServerError)
c.Header(wopi.ServerErrorHeader, "file not found")
c.Abort()
return
}
c.Set("user", &user)
c.Set(WopiSessionCtx, &session)
c.Next()
}
}