chore: 初始化 Cloudreve 源码仓库
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"net/http"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
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 http.MethodPut, http.MethodPost, http.MethodPatch:
|
||||
err = auth.CheckRequest(c, authInstance, c.Request)
|
||||
default:
|
||||
err = auth.CheckURI(c, authInstance, c.Request.URL)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCredentialInvalid, err.Error(), err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// CurrentUser 获取登录用户
|
||||
func CurrentUser() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// 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 u := inventory.UserFromContext(c); u != nil && !inventory.IsAnonymousUser(u) {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCheckLogin, "Login required", nil))
|
||||
c.Abort()
|
||||
}
|
||||
}
|
||||
|
||||
// WebDAVAuth 验证WebDAV登录及权限
|
||||
func WebDAVAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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?
|
||||
group, err := expectedUser.Edges.GroupOrErr()
|
||||
if err != nil {
|
||||
l.Debug("WebDAVAuth: user group not found: %s", err)
|
||||
c.Status(http.StatusInternalServerError)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 检查是否只读
|
||||
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 types.PolicyType) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 验证key并查找用户
|
||||
err := uploadCallbackCheck(c, policyType)
|
||||
if err != nil {
|
||||
c.JSON(CallbackFailedStatusCode, serializer.Err(c, err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// uploadCallbackCheck 对上传回调请求的 callback key 进行验证,如果成功则返回上传用户
|
||||
func uploadCallbackCheck(c *gin.Context, policyType types.PolicyType) error {
|
||||
// 验证 Callback Key
|
||||
sessionID := c.Param("sessionID")
|
||||
if sessionID == "" {
|
||||
return serializer.NewError(serializer.CodeParamErr, "Session ID cannot be empty", nil)
|
||||
}
|
||||
|
||||
dep := dependency.FromContext(c)
|
||||
callbackSessionRaw, exist := dep.KV().Get(manager.UploadSessionCachePrefix + sessionID)
|
||||
if !exist {
|
||||
return serializer.NewError(serializer.CodeUploadSessionExpired, "Upload session does not exist or expired", nil)
|
||||
}
|
||||
|
||||
callbackSession := callbackSessionRaw.(fs.UploadSession)
|
||||
if subtle.ConstantTimeCompare(
|
||||
[]byte(callbackSession.CallbackSecret),
|
||||
[]byte(c.Param("key")),
|
||||
) != 1 {
|
||||
return serializer.NewError(serializer.CodeCredentialInvalid, "Invalid callback secret", nil)
|
||||
}
|
||||
|
||||
c.Set(manager.UploadSessionCtx, &callbackSession)
|
||||
if callbackSession.Policy.Type != string(policyType) {
|
||||
return serializer.NewError(serializer.CodePolicyNotAllowed, "", nil)
|
||||
}
|
||||
|
||||
if err := SetUserCtx(c, callbackSession.UID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoteCallbackAuth 远程回调签名验证
|
||||
func RemoteCallbackAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 验证签名
|
||||
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
|
||||
}
|
||||
|
||||
c.Next()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// OSSCallbackAuth 阿里云OSS回调签名验证
|
||||
func OSSCallbackAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
dep := dependency.FromContext(c)
|
||||
err := oss.VerifyCallbackSignature(c.Request, dep.KV(), dep.RequestClient(
|
||||
request.WithContext(c),
|
||||
request.WithLogger(logging.FromContext(c)),
|
||||
))
|
||||
if err != nil {
|
||||
dep.Logger().Debug("Failed to verify callback request: %s", err)
|
||||
c.JSON(401, serializer.GeneralUploadCallbackFailed{Error: "Failed to verify callback request."})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// IsAdmin 必须为管理员用户组
|
||||
func IsAdmin() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
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
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequiredScopes checks if the JWT token has the required scopes.
|
||||
// If the token has scopes (hasScopes is true), it verifies the token has all required scopes.
|
||||
// Write scopes implicitly include Read scopes for the same resource (e.g., "File.Write" includes "File.Read").
|
||||
// If hasScopes is false (e.g., session-based auth), the check is skipped.
|
||||
func RequiredScopes(requiredScopes ...string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if err := auth.CheckScope(c, requiredScopes...); err != nil {
|
||||
c.JSON(200, serializer.Err(c, err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/recaptcha"
|
||||
request2 "github.com/cloudreve/Cloudreve/v4/pkg/request"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mojocn/base64Captcha"
|
||||
)
|
||||
|
||||
type req struct {
|
||||
Captcha string `json:"captcha"`
|
||||
Ticket string `json:"ticket"`
|
||||
Randstr string `json:"randstr"`
|
||||
}
|
||||
|
||||
const (
|
||||
captchaNotMatch = "CAPTCHA not match."
|
||||
captchaRefresh = "Verification failed, please refresh the page and retry."
|
||||
|
||||
tcCaptchaEndpoint = "captcha.tencentcloudapi.com"
|
||||
turnstileEndpoint = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
|
||||
)
|
||||
|
||||
// CaptchaIDCtx defines keys for captcha ID
|
||||
type (
|
||||
CaptchaIDCtx struct{}
|
||||
turnstileResponse struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
capResponse struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
)
|
||||
|
||||
// CaptchaRequired 验证请求签名
|
||||
func CaptchaRequired(enabled func(c *gin.Context) bool) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if enabled(c) {
|
||||
dep := dependency.FromContext(c)
|
||||
settings := dep.SettingProvider()
|
||||
l := logging.FromContext(c)
|
||||
|
||||
var service req
|
||||
bodyCopy := new(bytes.Buffer)
|
||||
_, err := io.Copy(bodyCopy, c.Request.Body)
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, captchaNotMatch, err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
bodyData := bodyCopy.Bytes()
|
||||
err = json.Unmarshal(bodyData, &service)
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, captchaNotMatch, err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader(bodyData))
|
||||
switch settings.CaptchaType(c) {
|
||||
case setting.CaptchaNormal, setting.CaptchaTcaptcha:
|
||||
if service.Ticket == "" || !base64Captcha.VerifyCaptcha(service.Ticket, service.Captcha) {
|
||||
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, captchaNotMatch, err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
break
|
||||
case setting.CaptchaReCaptcha:
|
||||
captchaSetting := settings.ReCaptcha(c)
|
||||
reCAPTCHA, err := recaptcha.NewReCAPTCHA(captchaSetting.Secret, recaptcha.V2, 10*time.Second)
|
||||
if err != nil {
|
||||
l.Warning("reCAPTCHA verification failed, %s", err)
|
||||
c.Abort()
|
||||
break
|
||||
}
|
||||
|
||||
err = reCAPTCHA.Verify(service.Captcha)
|
||||
if err != nil {
|
||||
l.Warning("reCAPTCHA verification failed, %s", err)
|
||||
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, captchaRefresh, err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
break
|
||||
case setting.CaptchaTurnstile:
|
||||
captchaSetting := settings.TurnstileCaptcha(c)
|
||||
r := dep.RequestClient(
|
||||
request2.WithContext(c),
|
||||
request2.WithLogger(logging.FromContext(c)),
|
||||
request2.WithHeader(http.Header{"Content-Type": []string{"application/x-www-form-urlencoded"}}),
|
||||
)
|
||||
formData := url.Values{}
|
||||
formData.Set("secret", captchaSetting.Secret)
|
||||
formData.Set("response", service.Ticket)
|
||||
res, err := r.Request("POST", turnstileEndpoint, strings.NewReader(formData.Encode())).
|
||||
CheckHTTPResponse(http.StatusOK).
|
||||
GetResponse()
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
var trunstileRes turnstileResponse
|
||||
err = json.Unmarshal([]byte(res), &trunstileRes)
|
||||
if err != nil {
|
||||
l.Warning("Turnstile verification failed, %s", err)
|
||||
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if !trunstileRes.Success {
|
||||
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
break
|
||||
case setting.CaptchaCap:
|
||||
captchaSetting := settings.CapCaptcha(c)
|
||||
if captchaSetting.InstanceURL == "" || captchaSetting.SiteKey == "" || captchaSetting.SecretKey == "" {
|
||||
l.Warning("Cap verification failed: missing configuration")
|
||||
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha configuration error", nil))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
r := dep.RequestClient(
|
||||
request2.WithContext(c),
|
||||
request2.WithLogger(logging.FromContext(c)),
|
||||
request2.WithHeader(http.Header{"Content-Type": []string{"application/json"}}),
|
||||
)
|
||||
|
||||
// Cap 2.0 API format: /{siteKey}/siteverify
|
||||
capEndpoint := strings.TrimSuffix(captchaSetting.InstanceURL, "/") + "/" + captchaSetting.SiteKey + "/siteverify"
|
||||
requestBody := map[string]string{
|
||||
"secret": captchaSetting.SecretKey,
|
||||
"response": service.Ticket,
|
||||
}
|
||||
requestData, err := json.Marshal(requestBody)
|
||||
if err != nil {
|
||||
l.Warning("Cap verification failed: %s", err)
|
||||
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
res, err := r.Request("POST", capEndpoint, strings.NewReader(string(requestData))).
|
||||
CheckHTTPResponse(http.StatusOK).
|
||||
GetResponse()
|
||||
if err != nil {
|
||||
l.Warning("Cap verification failed: %s", err)
|
||||
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
var capRes capResponse
|
||||
err = json.Unmarshal([]byte(res), &capRes)
|
||||
if err != nil {
|
||||
l.Warning("Cap verification failed: %s", err)
|
||||
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if !capRes.Success {
|
||||
l.Warning("Cap verification failed: validation returned false")
|
||||
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", nil))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/downloader"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/request"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/routers/controllers"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SlaveNodeSettingGetter interface {
|
||||
// GetNodeSetting returns the node settings and its hash
|
||||
GetNodeSetting() (*types.NodeSetting, string)
|
||||
}
|
||||
|
||||
var downloaderPool = sync.Map{}
|
||||
|
||||
// PrepareSlaveDownloader creates or resume a downloader based on input node settings
|
||||
func PrepareSlaveDownloader(dep dependency.Dep, ctxKey interface{}) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
nodeSettings, hash := controllers.ParametersFromContext[SlaveNodeSettingGetter](c, ctxKey).GetNodeSetting()
|
||||
|
||||
// try to get downloader from pool
|
||||
if d, ok := downloaderPool.Load(hash); ok {
|
||||
c.Set(downloader.DownloaderCtxKey, d)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// create a new downloader
|
||||
d, err := cluster.NewDownloader(c, dep.RequestClient(request.WithContext(c), request.WithLogger(dep.Logger())), dep.SettingProvider(), nodeSettings)
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.ParamErr(c, "Failed to create downloader", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// save downloader to pool
|
||||
downloaderPool.Store(hash, d)
|
||||
c.Set(downloader.DownloaderCtxKey, d)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func SlaveRPCSignRequired() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
nodeId := cluster.NodeIdFromContext(c)
|
||||
if nodeId == 0 {
|
||||
c.JSON(200, serializer.ParamErr(c, "Unknown node ID", nil))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
np, err := dependency.FromContext(c).NodePool(c)
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.NewError(serializer.CodeInternalSetting, "Failed to get node pool", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
slaveNode, err := np.Get(c, types.NodeCapabilityNone, nodeId)
|
||||
if slaveNode == nil || slaveNode.IsMaster() {
|
||||
c.JSON(200, serializer.ParamErr(c, "Unknown node ID", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
SignRequired(slaveNode.AuthInstance())(c)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/application/constants"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/auth/requestinfo"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/request"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
// HashID 将给定对象的HashID转换为真实ID
|
||||
func HashID(IDType int) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
dep := dependency.FromContext(c)
|
||||
if c.Param("id") != "" {
|
||||
id, err := dep.HashIDEncoder().Decode(c.Param("id"), IDType)
|
||||
if err == nil {
|
||||
util.WithValue(c, hashid.ObjectIDCtx{}, id)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
c.JSON(200, serializer.ParamErr(c, "Failed to parse object ID", err))
|
||||
c.Abort()
|
||||
return
|
||||
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// IsFunctionEnabled 当功能未开启时阻止访问
|
||||
func IsFunctionEnabled(check func(c *gin.Context) bool) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !check(c) {
|
||||
c.JSON(200, serializer.ErrWithDetails(c, 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(dep dependency.Dep) gin.HandlerFunc {
|
||||
settings := dep.SettingProvider()
|
||||
return func(c *gin.Context) {
|
||||
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", settings.PublicResourceMaxAge(c)))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// MobileRequestOnly
|
||||
func MobileRequestOnly() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
dep := dependency.FromContext(c)
|
||||
if c.GetHeader(constants.CrHeaderPrefix+"ios") == "" {
|
||||
c.Redirect(http.StatusMovedPermanently, dep.SettingProvider().SiteURL(c).String())
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// InitializeHandling is added at the beginning of handler chain, it did following setups:
|
||||
// 1. Inject dependency manager into request context
|
||||
// 2. Generate and inject correlation ID for diagnostic.
|
||||
func InitializeHandling(dep dependency.Dep) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
clientIp := c.ClientIP()
|
||||
if idx := strings.Index(clientIp, ","); idx > 0 {
|
||||
clientIp = clientIp[:idx]
|
||||
}
|
||||
|
||||
reqInfo := &requestinfo.RequestInfo{
|
||||
IP: clientIp,
|
||||
Host: c.Request.Host,
|
||||
UserAgent: c.Request.UserAgent(),
|
||||
ClientID: c.GetHeader(request.ClientIDHeader),
|
||||
}
|
||||
cid := uuid.FromStringOrNil(c.GetHeader(request.CorrelationHeader))
|
||||
if cid == uuid.Nil {
|
||||
cid = uuid.Must(uuid.NewV4())
|
||||
}
|
||||
|
||||
l := dep.Logger().CopyWithPrefix(fmt.Sprintf("[Cid: %s]", cid))
|
||||
ctx := dep.ForkWithLogger(c.Request.Context(), l)
|
||||
ctx = context.WithValue(ctx, logging.CorrelationIDCtx{}, cid)
|
||||
ctx = context.WithValue(ctx, requestinfo.RequestInfoCtx{}, reqInfo)
|
||||
ctx = context.WithValue(ctx, logging.LoggerCtx{}, l)
|
||||
if id := c.Param("nodeId"); id != "" {
|
||||
ctx = context.WithValue(ctx, cluster.SlaveNodeIDCtx{}, id)
|
||||
} else {
|
||||
ctx = context.WithValue(ctx, cluster.SlaveNodeIDCtx{}, c.GetHeader(request.SlaveNodeIDHeader))
|
||||
}
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// InitializeHandlingSlave retrieves coll correlation ID and other metadata from request header
|
||||
func InitializeHandlingSlave() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := context.WithValue(c.Request.Context(), cluster.MasterSiteIDCtx{}, c.GetHeader(request.SiteIDHeader))
|
||||
ctx = context.WithValue(ctx, cluster.MasterSiteUrlCtx{}, c.GetHeader(request.SiteURLHeader))
|
||||
ctx = context.WithValue(ctx, cluster.MasterSiteVersionCtx{}, c.GetHeader(request.SiteVersionHeader))
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// Logging logs incoming request info
|
||||
func Logging() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Start timer
|
||||
start := time.Now()
|
||||
path := c.Request.URL.Path
|
||||
raw := c.Request.URL.RawQuery
|
||||
|
||||
// Process request
|
||||
c.Next()
|
||||
|
||||
if raw != "" {
|
||||
path = path + "?" + raw
|
||||
}
|
||||
|
||||
l := logging.FromContext(c)
|
||||
logging.Request(l, true, c.Writer.Status(), c.Request.Method, c.ClientIP(), path,
|
||||
c.Errors.ByType(gin.ErrorTypePrivate).String(), start)
|
||||
}
|
||||
}
|
||||
|
||||
func ContentCORS() gin.HandlerFunc {
|
||||
return cors.New(cors.Config{
|
||||
AllowOrigins: []string{"*"},
|
||||
AllowMethods: []string{"GET", "HEAD", "OPTIONS"},
|
||||
AllowHeaders: []string{"Range", "If-Range", "Authorization", "Content-Type"},
|
||||
ExposeHeaders: []string{"Content-Range", "Accept-Ranges", "Content-Length", "Content-Disposition", "Content-Disposition", "ETag"},
|
||||
AllowCredentials: false,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/cloudreve/Cloudreve/v4/routers/controllers"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
// UrisService is a wrapper for service supports batch file operations
|
||||
type UrisService interface {
|
||||
GetUris() []string
|
||||
}
|
||||
|
||||
// ValidateBatchFileCount validates if the batch file count is within the limit
|
||||
func ValidateBatchFileCount(dep dependency.Dep, ctxKey interface{}) gin.HandlerFunc {
|
||||
settings := dep.SettingProvider()
|
||||
return func(c *gin.Context) {
|
||||
uris := controllers.ParametersFromContext[UrisService](c, ctxKey)
|
||||
limit := settings.MaxBatchedFile(c)
|
||||
if len((uris).GetUris()) > limit {
|
||||
c.JSON(200, serializer.ErrWithDetails(
|
||||
c,
|
||||
serializer.CodeTooManyUris,
|
||||
fmt.Sprintf("Maximum allowed batch size: %d", limit),
|
||||
nil,
|
||||
))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// ContextHint parses the context hint header and set it to context
|
||||
func ContextHint() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if c.GetHeader(dbfs.ContextHintHeader) != "" {
|
||||
util.WithValue(c, dbfs.ContextHintCtxKey{}, uuid.FromStringOrNil(c.GetHeader(dbfs.ContextHintHeader)))
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FrontendFileHandler 前端静态文件处理
|
||||
func FrontendFileHandler(dep dependency.Dep) gin.HandlerFunc {
|
||||
fs := dep.ServerStaticFS()
|
||||
l := dep.Logger()
|
||||
|
||||
ignoreFunc := func(c *gin.Context) {
|
||||
c.Next()
|
||||
}
|
||||
|
||||
if fs == nil {
|
||||
return ignoreFunc
|
||||
}
|
||||
|
||||
// 读取index.html
|
||||
file, err := fs.Open("/index.html")
|
||||
if err != nil {
|
||||
l.Warning("Static file \"index.html\" does not exist, it might affect the display of the homepage.")
|
||||
return ignoreFunc
|
||||
}
|
||||
|
||||
fileContentBytes, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
l.Warning("Cannot read static file \"index.html\", it might affect the display of the homepage.")
|
||||
return ignoreFunc
|
||||
}
|
||||
fileContent := string(fileContentBytes)
|
||||
|
||||
fileServer := http.FileServer(fs)
|
||||
return func(c *gin.Context) {
|
||||
path := c.Request.URL.Path
|
||||
|
||||
// Skipping routers handled by backend
|
||||
if strings.HasPrefix(path, "/api") ||
|
||||
strings.HasPrefix(path, "/dav") ||
|
||||
strings.HasPrefix(path, "/f/") ||
|
||||
strings.HasPrefix(path, "/s/") ||
|
||||
path == "/manifest.json" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 不存在的路径和index.html均返回index.html
|
||||
if (path == "/index.html") || (path == "/") || !fs.Exists("/", path) {
|
||||
// 读取、替换站点设置
|
||||
settingClient := dep.SettingProvider()
|
||||
siteBasic := settingClient.SiteBasic(c)
|
||||
pwaOpts := settingClient.PWA(c)
|
||||
theme := settingClient.Theme(c)
|
||||
finalHTML := util.Replace(map[string]string{
|
||||
"{siteName}": siteBasic.Name,
|
||||
"{siteDes}": siteBasic.Description,
|
||||
"{siteScript}": siteBasic.Script,
|
||||
"{pwa_small_icon}": pwaOpts.SmallIcon,
|
||||
"{pwa_medium_icon}": pwaOpts.MediumIcon,
|
||||
"var(--defaultThemeColor)": theme.DefaultTheme,
|
||||
}, fileContent)
|
||||
|
||||
c.Header("Content-Type", "text/html")
|
||||
c.Header("Cache-Control", "public, no-cache")
|
||||
c.String(200, finalHTML)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if path == "/sw.js" || strings.HasPrefix(path, "/locales/") {
|
||||
c.Header("Cache-Control", "public, no-cache")
|
||||
} else if strings.HasPrefix(path, "/assets/") {
|
||||
c.Header("Cache-Control", "public, max-age=31536000")
|
||||
}
|
||||
|
||||
// 存在的静态文件
|
||||
fileServer.ServeHTTP(c.Writer, c.Request)
|
||||
c.Abort()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/cloudreve/Cloudreve/v4/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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/sessionstore"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/conf"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Store session存储
|
||||
var Store sessions.Store
|
||||
|
||||
const SessionName = "cloudreve-session"
|
||||
|
||||
// Session 初始化session
|
||||
func Session(dep dependency.Dep) gin.HandlerFunc {
|
||||
Store = sessionstore.NewStore(dep.KV(), []byte(dep.ConfigProvider().System().SessionSecret))
|
||||
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(SessionName, 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.ErrDeprecated(serializer.CodeNoPermissionErr, "Invalid origin", nil))
|
||||
c.Abort()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/application/constants"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/cloudreve/Cloudreve/v4/service/explorer"
|
||||
"github.com/cloudreve/Cloudreve/v4/service/share"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
ogStatusInvalidLink = "Invalid Link"
|
||||
)
|
||||
|
||||
type ogData struct {
|
||||
SiteName string
|
||||
Title string
|
||||
Description string
|
||||
ImageURL string
|
||||
ShareURL string
|
||||
RedirectURL string
|
||||
}
|
||||
|
||||
const ogHTMLTemplate = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta property="og:title" content="{{.Title}}">
|
||||
<meta property="og:description" content="{{.Description}}">
|
||||
<meta property="og:image" content="{{.ImageURL}}">
|
||||
<meta property="og:url" content="{{.ShareURL}}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:site_name" content="{{.SiteName}}">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="{{.Title}}">
|
||||
<meta name="twitter:description" content="{{.Description}}">
|
||||
<meta name="twitter:image" content="{{.ImageURL}}">
|
||||
<title>{{.Title}} - {{.SiteName}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>window.location.href = "{{.RedirectURL}}";</script>
|
||||
<noscript><a href="{{.RedirectURL}}">{{.Title}}</a></noscript>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
var ogTemplate = template.Must(template.New("og").Parse(ogHTMLTemplate))
|
||||
|
||||
var socialMediaBots = []string{
|
||||
"facebookexternalhit",
|
||||
"facebookcatalog",
|
||||
"facebot",
|
||||
"twitterbot",
|
||||
"linkedinbot",
|
||||
"discordbot",
|
||||
"telegrambot",
|
||||
"slackbot",
|
||||
"whatsapp",
|
||||
}
|
||||
|
||||
func isSocialMediaBot(ua string) bool {
|
||||
ua = strings.ToLower(ua)
|
||||
for _, bot := range socialMediaBots {
|
||||
if strings.Contains(ua, bot) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SharePreview 为社交媒体爬虫渲染OG预览页面
|
||||
func SharePreview(dep dependency.Dep) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !isSocialMediaBot(c.GetHeader("User-Agent")) {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
id, password := extractShareParams(c)
|
||||
if id == "" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
html := renderShareOGPage(c, dep, id, password)
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
c.Header("Cache-Control", "public, no-cache")
|
||||
c.String(200, html)
|
||||
c.Abort()
|
||||
}
|
||||
}
|
||||
|
||||
func extractShareParams(c *gin.Context) (id, password string) {
|
||||
urlPath := c.Request.URL.Path
|
||||
|
||||
if strings.HasPrefix(urlPath, "/s/") {
|
||||
parts := strings.Split(strings.TrimPrefix(urlPath, "/s/"), "/")
|
||||
if len(parts) >= 1 && parts[0] != "" {
|
||||
id = parts[0]
|
||||
if len(parts) >= 2 {
|
||||
password = parts[1]
|
||||
}
|
||||
}
|
||||
} else if urlPath == "/home" || urlPath == "/home/" {
|
||||
rawPath := c.Query("path")
|
||||
uri, err := fs.NewUriFromString(rawPath)
|
||||
if err != nil || uri.FileSystem() != constants.FileSystemShare {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
return uri.ID(""), uri.Password()
|
||||
}
|
||||
|
||||
return id, password
|
||||
}
|
||||
|
||||
func renderShareOGPage(c *gin.Context, dep dependency.Dep, id, password string) string {
|
||||
settings := dep.SettingProvider()
|
||||
siteBasic := settings.SiteBasic(c)
|
||||
pwa := settings.PWA(c)
|
||||
base := settings.SiteURL(c)
|
||||
|
||||
data := &ogData{
|
||||
SiteName: siteBasic.Name,
|
||||
Title: siteBasic.Name,
|
||||
Description: siteBasic.Description,
|
||||
ShareURL: routes.MasterShareUrl(base, id, password).String(),
|
||||
RedirectURL: routes.MasterShareLongUrl(id, password).String(),
|
||||
}
|
||||
|
||||
if pwa.LargeIcon != "" {
|
||||
data.ImageURL = resolveURL(base, pwa.LargeIcon)
|
||||
} else if pwa.MediumIcon != "" {
|
||||
data.ImageURL = resolveURL(base, pwa.MediumIcon)
|
||||
}
|
||||
|
||||
shareID, err := dep.HashIDEncoder().Decode(id, hashid.ShareID)
|
||||
if err != nil {
|
||||
data.Description = ogStatusInvalidLink
|
||||
return renderOGHTML(data)
|
||||
}
|
||||
|
||||
shareInfo, err := loadShareForOG(c, shareID, password)
|
||||
if err != nil {
|
||||
var appErr serializer.AppError
|
||||
if errors.As(err, &appErr) {
|
||||
data.Description = appErr.Msg
|
||||
} else {
|
||||
data.Description = ogStatusInvalidLink
|
||||
}
|
||||
return renderOGHTML(data)
|
||||
}
|
||||
|
||||
data.Title = shareInfo.Name
|
||||
if shareInfo.SourceType != nil && *shareInfo.SourceType == types.FileTypeFolder {
|
||||
data.Description = "Folder"
|
||||
} else if shareInfo.Unlocked {
|
||||
data.Description = formatFileSize(shareInfo.Size)
|
||||
thumbnail, err := loadShareThumbnail(c, id, password, shareInfo)
|
||||
if err == nil {
|
||||
data.ImageURL = thumbnail
|
||||
}
|
||||
}
|
||||
|
||||
data.Description += " · " + shareInfo.Owner.Nickname
|
||||
return renderOGHTML(data)
|
||||
}
|
||||
|
||||
func loadShareThumbnail(c *gin.Context, shareID, password string, shareInfo *explorer.Share) (string, error) {
|
||||
shareUri, err := fs.NewUriFromString(fs.NewShareUri(shareID, password))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to construct share uri: %w", err)
|
||||
}
|
||||
|
||||
subService := &explorer.FileThumbService{
|
||||
Uri: shareUri.Join(shareInfo.Name).String(),
|
||||
}
|
||||
|
||||
if err := SetUserCtx(c, 0); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
res, err := subService.Get(c)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return res.Url, nil
|
||||
}
|
||||
|
||||
func loadShareForOG(c *gin.Context, shareID int, password string) (*explorer.Share, error) {
|
||||
subService := &share.ShareInfoService{
|
||||
Password: password,
|
||||
CountViews: false,
|
||||
}
|
||||
|
||||
if err := SetUserCtx(c, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
util.WithValue(c, hashid.ObjectIDCtx{}, shareID)
|
||||
return subService.Get(c)
|
||||
}
|
||||
|
||||
func renderOGHTML(data *ogData) string {
|
||||
var buf bytes.Buffer
|
||||
if err := ogTemplate.Execute(&buf, data); err != nil {
|
||||
return ""
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func resolveURL(base *url.URL, path string) string {
|
||||
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
|
||||
return path
|
||||
}
|
||||
return base.ResolveReference(&url.URL{Path: path}).String()
|
||||
}
|
||||
|
||||
func formatFileSize(size int64) string {
|
||||
const (
|
||||
KB = 1024
|
||||
MB = 1024 * KB
|
||||
GB = 1024 * MB
|
||||
TB = 1024 * GB
|
||||
)
|
||||
|
||||
switch {
|
||||
case size >= TB:
|
||||
return fmt.Sprintf("%.2f TB", float64(size)/TB)
|
||||
case size >= GB:
|
||||
return fmt.Sprintf("%.2f GB", float64(size)/GB)
|
||||
case size >= MB:
|
||||
return fmt.Sprintf("%.2f MB", float64(size)/MB)
|
||||
case size >= KB:
|
||||
return fmt.Sprintf("%.2f KB", float64(size)/KB)
|
||||
default:
|
||||
return fmt.Sprintf("%d B", size)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/wopi"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// WopiWriteAccess validates if write access is obtained.
|
||||
func WopiWriteAccess() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session := c.MustGet(wopi.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 ViewerSessionValidation() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
dep := dependency.FromContext(c)
|
||||
store := dep.KV()
|
||||
settings := dep.SettingProvider()
|
||||
|
||||
accessToken := c.Query(wopi.AccessTokenQuery)
|
||||
sessionID, _, ok := strings.Cut(accessToken, ".")
|
||||
if !ok || sessionID == "" {
|
||||
c.Status(http.StatusForbidden)
|
||||
c.Header(wopi.ServerErrorHeader, "malformed access token")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
sessionRaw, exist := store.Get(manager.ViewerSessionCachePrefix + sessionID)
|
||||
if !exist {
|
||||
c.Status(http.StatusForbidden)
|
||||
c.Header(wopi.ServerErrorHeader, "invalid access token")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
session := sessionRaw.(manager.ViewerSessionCache)
|
||||
if subtle.ConstantTimeCompare([]byte(accessToken), []byte(session.Token)) != 1 {
|
||||
c.Status(http.StatusForbidden)
|
||||
c.Header(wopi.ServerErrorHeader, "invalid access token")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if err := SetUserCtx(c, session.UserID); err != nil {
|
||||
c.Status(http.StatusInternalServerError)
|
||||
c.Header(wopi.ServerErrorHeader, "user not found")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
fileId := hashid.FromContext(c)
|
||||
if fileId != session.FileID {
|
||||
c.Status(http.StatusForbidden)
|
||||
c.Header(wopi.ServerErrorHeader, "invalid file")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the viewer is still available
|
||||
viewers := settings.FileViewers(c)
|
||||
var v *types.Viewer
|
||||
for _, group := range viewers {
|
||||
for _, viewer := range group.Viewers {
|
||||
if viewer.ID == session.ViewerID && !viewer.Disabled {
|
||||
v = &viewer
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if v != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if v == nil {
|
||||
c.Status(http.StatusInternalServerError)
|
||||
c.Header(wopi.ServerErrorHeader, "viewer not found")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
util.WithValue(c, manager.ViewerCtx{}, v)
|
||||
util.WithValue(c, manager.ViewerSessionCacheCtx{}, &session)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user