init
This commit is contained in:
323
middleware/auth.go
Normal file
323
middleware/auth.go
Normal 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
127
middleware/captcha.go
Normal 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
62
middleware/cluster.go
Normal 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
77
middleware/common.go
Normal 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
30
middleware/file.go
Normal 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
84
middleware/frontend.go
Normal 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
24
middleware/mock.go
Normal 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
68
middleware/session.go
Normal 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
139
middleware/share.go
Normal 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
70
middleware/wopi.go
Normal 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()
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user