mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2025-09-19 12:16:24 +08:00
feat(share): support more secure file sharing (#991)
提供一种类似大多数网盘的文件分享操作,这种分享方式可以通过强制 Web 代理隐藏文件源路径,可以设置分享码、最大访问数和过期时间,并且不需要启用 guest 用户。 在全局设置中可以调整: - 是否强制 Web 代理 - 是否允许预览 - 是否允许预览压缩文件 - 分享文件后,点击“复制链接”按钮复制的内容 前端部分:OpenListTeam/OpenList-Frontend#156 文档部分:OpenListTeam/OpenList-Docs#130 Close #183 Close #526 Close #860 Close #892 Close #1079 * feat(share): support more secure file sharing * feat(share): add archive preview * fix(share): fix some bugs * feat(openlist_share): add openlist share driver * fix(share): lack unwrap when get virtual path * fix: use unwrapPath instead of path for virtual file name comparison * fix(share): change request method of /api/share/list from GET to Any * fix(share): path traversal vulnerability in sharing path check * 修复分享alias驱动的文件 没开代理时无法获取URL * fix(sharing): update error message for sharing root link extraction --------- Co-authored-by: Suyunmeng <69945917+Suyunmeng@users.noreply.github.com> Co-authored-by: j2rong4cn <j2rong@qq.com>
This commit is contained in:
@ -3,9 +3,9 @@ package handles
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
stdpath "path"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/task"
|
||||
"strings"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/archive/tool"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/conf"
|
||||
@ -15,6 +15,7 @@ import (
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/op"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/setting"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/sign"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/task"
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||
"github.com/OpenListTeam/OpenList/v4/server/common"
|
||||
"github.com/gin-gonic/gin"
|
||||
@ -71,13 +72,26 @@ func toContentResp(objs []model.ObjTree) []ArchiveContentResp {
|
||||
return ret
|
||||
}
|
||||
|
||||
func FsArchiveMeta(c *gin.Context) {
|
||||
func FsArchiveMetaSplit(c *gin.Context) {
|
||||
var req ArchiveMetaReq
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(req.Path, "/@s") {
|
||||
req.Path = strings.TrimPrefix(req.Path, "/@s")
|
||||
SharingArchiveMeta(c, &req)
|
||||
return
|
||||
}
|
||||
user := c.Request.Context().Value(conf.UserKey).(*model.User)
|
||||
if user.IsGuest() && user.Disabled {
|
||||
common.ErrorStrResp(c, "Guest user is disabled, login please", 401)
|
||||
return
|
||||
}
|
||||
FsArchiveMeta(c, &req, user)
|
||||
}
|
||||
|
||||
func FsArchiveMeta(c *gin.Context, req *ArchiveMetaReq, user *model.User) {
|
||||
if !user.CanReadArchives() {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
@ -142,19 +156,27 @@ type ArchiveListReq struct {
|
||||
InnerPath string `json:"inner_path" form:"inner_path"`
|
||||
}
|
||||
|
||||
type ArchiveListResp struct {
|
||||
Content []ObjResp `json:"content"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
func FsArchiveList(c *gin.Context) {
|
||||
func FsArchiveListSplit(c *gin.Context) {
|
||||
var req ArchiveListReq
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
req.Validate()
|
||||
if strings.HasPrefix(req.Path, "/@s") {
|
||||
req.Path = strings.TrimPrefix(req.Path, "/@s")
|
||||
SharingArchiveList(c, &req)
|
||||
return
|
||||
}
|
||||
user := c.Request.Context().Value(conf.UserKey).(*model.User)
|
||||
if user.IsGuest() && user.Disabled {
|
||||
common.ErrorStrResp(c, "Guest user is disabled, login please", 401)
|
||||
return
|
||||
}
|
||||
FsArchiveList(c, &req, user)
|
||||
}
|
||||
|
||||
func FsArchiveList(c *gin.Context, req *ArchiveListReq, user *model.User) {
|
||||
if !user.CanReadArchives() {
|
||||
common.ErrorResp(c, errs.PermissionDenied, 403)
|
||||
return
|
||||
@ -201,7 +223,7 @@ func FsArchiveList(c *gin.Context) {
|
||||
ret, _ := utils.SliceConvert(objs, func(src model.Obj) (ObjResp, error) {
|
||||
return toObjsRespWithoutSignAndThumb(src), nil
|
||||
})
|
||||
common.SuccessResp(c, ArchiveListResp{
|
||||
common.SuccessResp(c, common.PageResp{
|
||||
Content: ret,
|
||||
Total: int64(total),
|
||||
})
|
||||
@ -358,6 +380,24 @@ func ArchiveProxy(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func proxyInternalExtract(c *gin.Context, rc io.ReadCloser, size int64, fileName string) {
|
||||
defer func() {
|
||||
if err := rc.Close(); err != nil {
|
||||
log.Errorf("failed to close file streamer, %v", err)
|
||||
}
|
||||
}()
|
||||
headers := map[string]string{
|
||||
"Referrer-Policy": "no-referrer",
|
||||
"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate",
|
||||
}
|
||||
headers["Content-Disposition"] = utils.GenerateContentDisposition(fileName)
|
||||
contentType := c.Request.Header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
contentType = utils.GetMimeType(fileName)
|
||||
}
|
||||
c.DataFromReader(200, size, contentType, rc, headers)
|
||||
}
|
||||
|
||||
func ArchiveInternalExtract(c *gin.Context) {
|
||||
archiveRawPath := c.Request.Context().Value(conf.PathKey).(string)
|
||||
innerPath := utils.FixAndCleanPath(c.Query("inner"))
|
||||
@ -376,22 +416,8 @@ func ArchiveInternalExtract(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := rc.Close(); err != nil {
|
||||
log.Errorf("failed to close file streamer, %v", err)
|
||||
}
|
||||
}()
|
||||
headers := map[string]string{
|
||||
"Referrer-Policy": "no-referrer",
|
||||
"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate",
|
||||
}
|
||||
fileName := stdpath.Base(innerPath)
|
||||
headers["Content-Disposition"] = utils.GenerateContentDisposition(fileName)
|
||||
contentType := c.Request.Header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
contentType = utils.GetMimeType(fileName)
|
||||
}
|
||||
c.DataFromReader(200, size, contentType, rc, headers)
|
||||
proxyInternalExtract(c, rc, size, fileName)
|
||||
}
|
||||
|
||||
func ArchiveExtensions(c *gin.Context) {
|
||||
|
@ -56,14 +56,27 @@ type FsListResp struct {
|
||||
Provider string `json:"provider"`
|
||||
}
|
||||
|
||||
func FsList(c *gin.Context) {
|
||||
func FsListSplit(c *gin.Context) {
|
||||
var req ListReq
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
req.Validate()
|
||||
if strings.HasPrefix(req.Path, "/@s") {
|
||||
req.Path = strings.TrimPrefix(req.Path, "/@s")
|
||||
SharingList(c, &req)
|
||||
return
|
||||
}
|
||||
user := c.Request.Context().Value(conf.UserKey).(*model.User)
|
||||
if user.IsGuest() && user.Disabled {
|
||||
common.ErrorStrResp(c, "Guest user is disabled, login please", 401)
|
||||
return
|
||||
}
|
||||
FsList(c, &req, user)
|
||||
}
|
||||
|
||||
func FsList(c *gin.Context, req *ListReq, user *model.User) {
|
||||
reqPath, err := user.JoinPath(req.Path)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 403)
|
||||
@ -243,13 +256,26 @@ type FsGetResp struct {
|
||||
Related []ObjResp `json:"related"`
|
||||
}
|
||||
|
||||
func FsGet(c *gin.Context) {
|
||||
func FsGetSplit(c *gin.Context) {
|
||||
var req FsGetReq
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(req.Path, "/@s") {
|
||||
req.Path = strings.TrimPrefix(req.Path, "/@s")
|
||||
SharingGet(c, &req)
|
||||
return
|
||||
}
|
||||
user := c.Request.Context().Value(conf.UserKey).(*model.User)
|
||||
if user.IsGuest() && user.Disabled {
|
||||
common.ErrorStrResp(c, "Guest user is disabled, login please", 401)
|
||||
return
|
||||
}
|
||||
FsGet(c, &req, user)
|
||||
}
|
||||
|
||||
func FsGet(c *gin.Context, req *FsGetReq, user *model.User) {
|
||||
reqPath, err := user.JoinPath(req.Path)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 403)
|
||||
|
551
server/handles/sharing.go
Normal file
551
server/handles/sharing.go
Normal file
@ -0,0 +1,551 @@
|
||||
package handles
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
stdpath "path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/conf"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/errs"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/op"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/setting"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/sharing"
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||
"github.com/OpenListTeam/OpenList/v4/server/common"
|
||||
"github.com/OpenListTeam/go-cache"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func SharingGet(c *gin.Context, req *FsGetReq) {
|
||||
sid, path, _ := strings.Cut(strings.TrimPrefix(req.Path, "/"), "/")
|
||||
if sid == "" {
|
||||
common.ErrorStrResp(c, "invalid share id", 400)
|
||||
return
|
||||
}
|
||||
s, obj, err := sharing.Get(c.Request.Context(), sid, path, model.SharingListArgs{
|
||||
Refresh: false,
|
||||
Pwd: req.Password,
|
||||
})
|
||||
if dealError(c, err) {
|
||||
return
|
||||
}
|
||||
_ = countAccess(c.ClientIP(), s)
|
||||
fakePath := fmt.Sprintf("/%s/%s", sid, path)
|
||||
url := ""
|
||||
if !obj.IsDir() {
|
||||
url = fmt.Sprintf("%s/sd%s", common.GetApiUrl(c), utils.EncodePath(fakePath, true))
|
||||
if s.Pwd != "" {
|
||||
url += "?pwd=" + s.Pwd
|
||||
}
|
||||
}
|
||||
thumb, _ := model.GetThumb(obj)
|
||||
common.SuccessResp(c, FsGetResp{
|
||||
ObjResp: ObjResp{
|
||||
Id: "",
|
||||
Path: fakePath,
|
||||
Name: obj.GetName(),
|
||||
Size: obj.GetSize(),
|
||||
IsDir: obj.IsDir(),
|
||||
Modified: obj.ModTime(),
|
||||
Created: obj.CreateTime(),
|
||||
HashInfoStr: obj.GetHash().String(),
|
||||
HashInfo: obj.GetHash().Export(),
|
||||
Sign: "",
|
||||
Type: utils.GetFileType(obj.GetName()),
|
||||
Thumb: thumb,
|
||||
},
|
||||
RawURL: url,
|
||||
Readme: s.Readme,
|
||||
Header: s.Header,
|
||||
Provider: "unknown",
|
||||
Related: nil,
|
||||
})
|
||||
}
|
||||
|
||||
func SharingList(c *gin.Context, req *ListReq) {
|
||||
sid, path, _ := strings.Cut(strings.TrimPrefix(req.Path, "/"), "/")
|
||||
if sid == "" {
|
||||
common.ErrorStrResp(c, "invalid share id", 400)
|
||||
return
|
||||
}
|
||||
s, objs, err := sharing.List(c.Request.Context(), sid, path, model.SharingListArgs{
|
||||
Refresh: req.Refresh,
|
||||
Pwd: req.Password,
|
||||
})
|
||||
if dealError(c, err) {
|
||||
return
|
||||
}
|
||||
_ = countAccess(c.ClientIP(), s)
|
||||
fakePath := fmt.Sprintf("/%s/%s", sid, path)
|
||||
total, objs := pagination(objs, &req.PageReq)
|
||||
common.SuccessResp(c, FsListResp{
|
||||
Content: utils.MustSliceConvert(objs, func(obj model.Obj) ObjResp {
|
||||
thumb, _ := model.GetThumb(obj)
|
||||
return ObjResp{
|
||||
Id: "",
|
||||
Path: stdpath.Join(fakePath, obj.GetName()),
|
||||
Name: obj.GetName(),
|
||||
Size: obj.GetSize(),
|
||||
IsDir: obj.IsDir(),
|
||||
Modified: obj.ModTime(),
|
||||
Created: obj.CreateTime(),
|
||||
HashInfoStr: obj.GetHash().String(),
|
||||
HashInfo: obj.GetHash().Export(),
|
||||
Sign: "",
|
||||
Thumb: thumb,
|
||||
Type: utils.GetObjType(obj.GetName(), obj.IsDir()),
|
||||
}
|
||||
}),
|
||||
Total: int64(total),
|
||||
Readme: s.Readme,
|
||||
Header: s.Header,
|
||||
Write: false,
|
||||
Provider: "unknown",
|
||||
})
|
||||
}
|
||||
|
||||
func SharingArchiveMeta(c *gin.Context, req *ArchiveMetaReq) {
|
||||
if !setting.GetBool(conf.ShareArchivePreview) {
|
||||
common.ErrorStrResp(c, "sharing archives previewing is not allowed", 403)
|
||||
return
|
||||
}
|
||||
sid, path, _ := strings.Cut(strings.TrimPrefix(req.Path, "/"), "/")
|
||||
if sid == "" {
|
||||
common.ErrorStrResp(c, "invalid share id", 400)
|
||||
return
|
||||
}
|
||||
archiveArgs := model.ArchiveArgs{
|
||||
LinkArgs: model.LinkArgs{
|
||||
Header: c.Request.Header,
|
||||
Type: c.Query("type"),
|
||||
},
|
||||
Password: req.ArchivePass,
|
||||
}
|
||||
s, ret, err := sharing.ArchiveMeta(c.Request.Context(), sid, path, model.SharingArchiveMetaArgs{
|
||||
ArchiveMetaArgs: model.ArchiveMetaArgs{
|
||||
ArchiveArgs: archiveArgs,
|
||||
Refresh: req.Refresh,
|
||||
},
|
||||
Pwd: req.Password,
|
||||
})
|
||||
if dealError(c, err) {
|
||||
return
|
||||
}
|
||||
_ = countAccess(c.ClientIP(), s)
|
||||
fakePath := fmt.Sprintf("/%s/%s", sid, path)
|
||||
url := fmt.Sprintf("%s/sad%s", common.GetApiUrl(c), utils.EncodePath(fakePath, true))
|
||||
if s.Pwd != "" {
|
||||
url += "?pwd=" + s.Pwd
|
||||
}
|
||||
common.SuccessResp(c, ArchiveMetaResp{
|
||||
Comment: ret.GetComment(),
|
||||
IsEncrypted: ret.IsEncrypted(),
|
||||
Content: toContentResp(ret.GetTree()),
|
||||
Sort: ret.Sort,
|
||||
RawURL: url,
|
||||
Sign: "",
|
||||
})
|
||||
}
|
||||
|
||||
func SharingArchiveList(c *gin.Context, req *ArchiveListReq) {
|
||||
if !setting.GetBool(conf.ShareArchivePreview) {
|
||||
common.ErrorStrResp(c, "sharing archives previewing is not allowed", 403)
|
||||
return
|
||||
}
|
||||
sid, path, _ := strings.Cut(strings.TrimPrefix(req.Path, "/"), "/")
|
||||
if sid == "" {
|
||||
common.ErrorStrResp(c, "invalid share id", 400)
|
||||
return
|
||||
}
|
||||
innerArgs := model.ArchiveInnerArgs{
|
||||
ArchiveArgs: model.ArchiveArgs{
|
||||
LinkArgs: model.LinkArgs{
|
||||
Header: c.Request.Header,
|
||||
Type: c.Query("type"),
|
||||
},
|
||||
Password: req.ArchivePass,
|
||||
},
|
||||
InnerPath: utils.FixAndCleanPath(req.InnerPath),
|
||||
}
|
||||
s, objs, err := sharing.ArchiveList(c.Request.Context(), sid, path, model.SharingArchiveListArgs{
|
||||
ArchiveListArgs: model.ArchiveListArgs{
|
||||
ArchiveInnerArgs: innerArgs,
|
||||
Refresh: req.Refresh,
|
||||
},
|
||||
Pwd: req.Password,
|
||||
})
|
||||
if dealError(c, err) {
|
||||
return
|
||||
}
|
||||
_ = countAccess(c.ClientIP(), s)
|
||||
total, objs := pagination(objs, &req.PageReq)
|
||||
ret, _ := utils.SliceConvert(objs, func(src model.Obj) (ObjResp, error) {
|
||||
return toObjsRespWithoutSignAndThumb(src), nil
|
||||
})
|
||||
common.SuccessResp(c, common.PageResp{
|
||||
Content: ret,
|
||||
Total: int64(total),
|
||||
})
|
||||
}
|
||||
|
||||
func SharingDown(c *gin.Context) {
|
||||
sid := c.Request.Context().Value(conf.SharingIDKey).(string)
|
||||
path := c.Request.Context().Value(conf.PathKey).(string)
|
||||
pwd := c.Query("pwd")
|
||||
s, err := op.GetSharingById(sid)
|
||||
if err == nil {
|
||||
if !s.Valid() {
|
||||
err = errs.InvalidSharing
|
||||
} else if !s.Verify(pwd) {
|
||||
err = errs.WrongShareCode
|
||||
} else if len(s.Files) != 1 && path == "/" {
|
||||
err = errors.New("cannot get sharing root link")
|
||||
}
|
||||
}
|
||||
if dealError(c, err) {
|
||||
return
|
||||
}
|
||||
unwrapPath, err := op.GetSharingUnwrapPath(s, path)
|
||||
if err != nil {
|
||||
common.ErrorStrResp(c, "failed get sharing unwrap path", 500)
|
||||
return
|
||||
}
|
||||
storage, actualPath, err := op.GetStorageAndActualPath(unwrapPath)
|
||||
if dealError(c, err) {
|
||||
return
|
||||
}
|
||||
if setting.GetBool(conf.ShareForceProxy) || common.ShouldProxy(storage, stdpath.Base(actualPath)) {
|
||||
link, obj, err := op.Link(c.Request.Context(), storage, actualPath, model.LinkArgs{
|
||||
Header: c.Request.Header,
|
||||
Type: c.Query("type"),
|
||||
})
|
||||
if err != nil {
|
||||
common.ErrorResp(c, errors.WithMessage(err, "failed get sharing link"), 500)
|
||||
return
|
||||
}
|
||||
_ = countAccess(c.ClientIP(), s)
|
||||
proxy(c, link, obj, storage.GetStorage().ProxyRange)
|
||||
} else {
|
||||
link, _, err := op.Link(c.Request.Context(), storage, actualPath, model.LinkArgs{
|
||||
IP: c.ClientIP(),
|
||||
Header: c.Request.Header,
|
||||
Type: c.Query("type"),
|
||||
Redirect: true,
|
||||
})
|
||||
if err != nil {
|
||||
common.ErrorResp(c, errors.WithMessage(err, "failed get sharing link"), 500)
|
||||
return
|
||||
}
|
||||
_ = countAccess(c.ClientIP(), s)
|
||||
redirect(c, link)
|
||||
}
|
||||
}
|
||||
|
||||
func SharingArchiveExtract(c *gin.Context) {
|
||||
if !setting.GetBool(conf.ShareArchivePreview) {
|
||||
common.ErrorStrResp(c, "sharing archives previewing is not allowed", 403)
|
||||
return
|
||||
}
|
||||
sid := c.Request.Context().Value(conf.SharingIDKey).(string)
|
||||
path := c.Request.Context().Value(conf.PathKey).(string)
|
||||
pwd := c.Query("pwd")
|
||||
innerPath := utils.FixAndCleanPath(c.Query("inner"))
|
||||
archivePass := c.Query("pass")
|
||||
s, err := op.GetSharingById(sid)
|
||||
if err == nil {
|
||||
if !s.Valid() {
|
||||
err = errs.InvalidSharing
|
||||
} else if !s.Verify(pwd) {
|
||||
err = errs.WrongShareCode
|
||||
} else if len(s.Files) != 1 && path == "/" {
|
||||
err = errors.New("cannot extract sharing root")
|
||||
}
|
||||
}
|
||||
if dealError(c, err) {
|
||||
return
|
||||
}
|
||||
unwrapPath, err := op.GetSharingUnwrapPath(s, path)
|
||||
if err != nil {
|
||||
common.ErrorStrResp(c, "failed get sharing unwrap path", 500)
|
||||
return
|
||||
}
|
||||
storage, actualPath, err := op.GetStorageAndActualPath(unwrapPath)
|
||||
if dealError(c, err) {
|
||||
return
|
||||
}
|
||||
args := model.ArchiveInnerArgs{
|
||||
ArchiveArgs: model.ArchiveArgs{
|
||||
LinkArgs: model.LinkArgs{
|
||||
Header: c.Request.Header,
|
||||
Type: c.Query("type"),
|
||||
},
|
||||
Password: archivePass,
|
||||
},
|
||||
InnerPath: innerPath,
|
||||
}
|
||||
if _, ok := storage.(driver.ArchiveReader); ok {
|
||||
if setting.GetBool(conf.ShareForceProxy) || common.ShouldProxy(storage, stdpath.Base(actualPath)) {
|
||||
link, obj, err := op.DriverExtract(c.Request.Context(), storage, actualPath, args)
|
||||
if dealError(c, err) {
|
||||
return
|
||||
}
|
||||
proxy(c, link, obj, storage.GetStorage().ProxyRange)
|
||||
} else {
|
||||
args.Redirect = true
|
||||
link, _, err := op.DriverExtract(c.Request.Context(), storage, actualPath, args)
|
||||
if dealError(c, err) {
|
||||
return
|
||||
}
|
||||
redirect(c, link)
|
||||
}
|
||||
} else {
|
||||
rc, size, err := op.InternalExtract(c.Request.Context(), storage, actualPath, args)
|
||||
if dealError(c, err) {
|
||||
return
|
||||
}
|
||||
fileName := stdpath.Base(innerPath)
|
||||
proxyInternalExtract(c, rc, size, fileName)
|
||||
}
|
||||
}
|
||||
|
||||
func dealError(c *gin.Context, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
} else if errors.Is(err, errs.SharingNotFound) {
|
||||
common.ErrorStrResp(c, "the share does not exist", 500)
|
||||
} else if errors.Is(err, errs.InvalidSharing) {
|
||||
common.ErrorStrResp(c, "the share has expired or is no longer valid", 500)
|
||||
} else if errors.Is(err, errs.WrongShareCode) {
|
||||
common.ErrorResp(c, err, 403)
|
||||
} else if errors.Is(err, errs.WrongArchivePassword) {
|
||||
common.ErrorResp(c, err, 202)
|
||||
} else {
|
||||
common.ErrorResp(c, err, 500)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type SharingResp struct {
|
||||
*model.Sharing
|
||||
CreatorName string `json:"creator"`
|
||||
CreatorRole int `json:"creator_role"`
|
||||
}
|
||||
|
||||
func GetSharing(c *gin.Context) {
|
||||
sid := c.Query("id")
|
||||
user := c.Request.Context().Value(conf.UserKey).(*model.User)
|
||||
s, err := op.GetSharingById(sid)
|
||||
if err != nil || (!user.IsAdmin() && s.Creator.ID != user.ID) {
|
||||
common.ErrorStrResp(c, "sharing not found", 404)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c, SharingResp{
|
||||
Sharing: s,
|
||||
CreatorName: s.Creator.Username,
|
||||
CreatorRole: s.Creator.Role,
|
||||
})
|
||||
}
|
||||
|
||||
func ListSharings(c *gin.Context) {
|
||||
var req model.PageReq
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
req.Validate()
|
||||
user := c.Request.Context().Value(conf.UserKey).(*model.User)
|
||||
var sharings []model.Sharing
|
||||
var total int64
|
||||
var err error
|
||||
if user.IsAdmin() {
|
||||
sharings, total, err = op.GetSharings(req.Page, req.PerPage)
|
||||
} else {
|
||||
sharings, total, err = op.GetSharingsByCreatorId(user.ID, req.Page, req.PerPage)
|
||||
}
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c, common.PageResp{
|
||||
Content: utils.MustSliceConvert(sharings, func(s model.Sharing) SharingResp {
|
||||
return SharingResp{
|
||||
Sharing: &s,
|
||||
CreatorName: s.Creator.Username,
|
||||
CreatorRole: s.Creator.Role,
|
||||
}
|
||||
}),
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
type CreateSharingReq struct {
|
||||
Files []string `json:"files"`
|
||||
Expires *time.Time `json:"expires"`
|
||||
Pwd string `json:"pwd"`
|
||||
MaxAccessed int `json:"max_accessed"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Remark string `json:"remark"`
|
||||
Readme string `json:"readme"`
|
||||
Header string `json:"header"`
|
||||
model.Sort
|
||||
}
|
||||
|
||||
type UpdateSharingReq struct {
|
||||
ID string `json:"id"`
|
||||
Accessed int `json:"accessed"`
|
||||
CreateSharingReq
|
||||
}
|
||||
|
||||
func UpdateSharing(c *gin.Context) {
|
||||
var req UpdateSharingReq
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if len(req.Files) == 0 || (len(req.Files) == 1 && req.Files[0] == "") {
|
||||
common.ErrorStrResp(c, "must add at least 1 object", 400)
|
||||
return
|
||||
}
|
||||
user := c.Request.Context().Value(conf.UserKey).(*model.User)
|
||||
if !user.CanShare() {
|
||||
common.ErrorStrResp(c, "permission denied", 403)
|
||||
return
|
||||
}
|
||||
for i, s := range req.Files {
|
||||
s = utils.FixAndCleanPath(s)
|
||||
req.Files[i] = s
|
||||
if !user.IsAdmin() && !strings.HasPrefix(s, user.BasePath) {
|
||||
common.ErrorStrResp(c, fmt.Sprintf("permission denied to share path [%s]", s), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
s, err := op.GetSharingById(req.ID)
|
||||
if err != nil || (!user.IsAdmin() && s.CreatorId != user.ID) {
|
||||
common.ErrorStrResp(c, "sharing not found", 404)
|
||||
return
|
||||
}
|
||||
s.Files = req.Files
|
||||
s.Expires = req.Expires
|
||||
s.Pwd = req.Pwd
|
||||
s.Accessed = req.Accessed
|
||||
s.MaxAccessed = req.MaxAccessed
|
||||
s.Disabled = req.Disabled
|
||||
s.Sort = req.Sort
|
||||
s.Header = req.Header
|
||||
s.Readme = req.Readme
|
||||
s.Remark = req.Remark
|
||||
if err = op.UpdateSharing(s); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
} else {
|
||||
common.SuccessResp(c, SharingResp{
|
||||
Sharing: s,
|
||||
CreatorName: s.Creator.Username,
|
||||
CreatorRole: s.Creator.Role,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func CreateSharing(c *gin.Context) {
|
||||
var req CreateSharingReq
|
||||
var err error
|
||||
if err = c.ShouldBind(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if len(req.Files) == 0 || (len(req.Files) == 1 && req.Files[0] == "") {
|
||||
common.ErrorStrResp(c, "must add at least 1 object", 400)
|
||||
return
|
||||
}
|
||||
user := c.Request.Context().Value(conf.UserKey).(*model.User)
|
||||
if !user.CanShare() {
|
||||
common.ErrorStrResp(c, "permission denied", 403)
|
||||
return
|
||||
}
|
||||
for i, s := range req.Files {
|
||||
s = utils.FixAndCleanPath(s)
|
||||
req.Files[i] = s
|
||||
if !user.IsAdmin() && !strings.HasPrefix(s, user.BasePath) {
|
||||
common.ErrorStrResp(c, fmt.Sprintf("permission denied to share path [%s]", s), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
s := &model.Sharing{
|
||||
SharingDB: &model.SharingDB{
|
||||
Expires: req.Expires,
|
||||
Pwd: req.Pwd,
|
||||
Accessed: 0,
|
||||
MaxAccessed: req.MaxAccessed,
|
||||
Disabled: req.Disabled,
|
||||
Sort: req.Sort,
|
||||
Remark: req.Remark,
|
||||
Readme: req.Readme,
|
||||
Header: req.Header,
|
||||
},
|
||||
Files: req.Files,
|
||||
Creator: user,
|
||||
}
|
||||
var id string
|
||||
if id, err = op.CreateSharing(s); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
} else {
|
||||
s.ID = id
|
||||
common.SuccessResp(c, SharingResp{
|
||||
Sharing: s,
|
||||
CreatorName: s.Creator.Username,
|
||||
CreatorRole: s.Creator.Role,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteSharing(c *gin.Context) {
|
||||
sid := c.Query("id")
|
||||
user := c.Request.Context().Value(conf.UserKey).(*model.User)
|
||||
s, err := op.GetSharingById(sid)
|
||||
if err != nil || (!user.IsAdmin() && s.CreatorId != user.ID) {
|
||||
common.ErrorResp(c, err, 404)
|
||||
return
|
||||
}
|
||||
if err = op.DeleteSharing(sid); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
} else {
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
}
|
||||
|
||||
func SetEnableSharing(disable bool) func(ctx *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
sid := c.Query("id")
|
||||
user := c.Request.Context().Value(conf.UserKey).(*model.User)
|
||||
s, err := op.GetSharingById(sid)
|
||||
if err != nil || (!user.IsAdmin() && s.CreatorId != user.ID) {
|
||||
common.ErrorStrResp(c, "sharing not found", 404)
|
||||
return
|
||||
}
|
||||
s.Disabled = disable
|
||||
if err = op.UpdateSharing(s, true); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
} else {
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
AccessCache = cache.NewMemCache[interface{}]()
|
||||
AccessCountDelay = 30 * time.Minute
|
||||
)
|
||||
|
||||
func countAccess(ip string, s *model.Sharing) error {
|
||||
key := fmt.Sprintf("%s:%s", s.ID, ip)
|
||||
_, ok := AccessCache.Get(key)
|
||||
if !ok {
|
||||
AccessCache.Set(key, struct{}{}, cache.WithEx[interface{}](AccessCountDelay))
|
||||
s.Accessed += 1
|
||||
return op.UpdateSharing(s, true)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -14,63 +14,65 @@ import (
|
||||
|
||||
// Auth is a middleware that checks if the user is logged in.
|
||||
// if token is empty, set user to guest
|
||||
func Auth(c *gin.Context) {
|
||||
token := c.GetHeader("Authorization")
|
||||
if subtle.ConstantTimeCompare([]byte(token), []byte(setting.GetStr(conf.Token))) == 1 {
|
||||
admin, err := op.GetAdmin()
|
||||
func Auth(allowDisabledGuest bool) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
token := c.GetHeader("Authorization")
|
||||
if subtle.ConstantTimeCompare([]byte(token), []byte(setting.GetStr(conf.Token))) == 1 {
|
||||
admin, err := op.GetAdmin()
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
common.GinWithValue(c, conf.UserKey, admin)
|
||||
log.Debugf("use admin token: %+v", admin)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
if token == "" {
|
||||
guest, err := op.GetGuest()
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if !allowDisabledGuest && guest.Disabled {
|
||||
common.ErrorStrResp(c, "Guest user is disabled, login please", 401)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
common.GinWithValue(c, conf.UserKey, guest)
|
||||
log.Debugf("use empty token: %+v", guest)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
userClaims, err := common.ParseToken(token)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
common.ErrorResp(c, err, 401)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
common.GinWithValue(c, conf.UserKey, admin)
|
||||
log.Debugf("use admin token: %+v", admin)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
if token == "" {
|
||||
guest, err := op.GetGuest()
|
||||
user, err := op.GetUserByName(userClaims.Username)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
common.ErrorResp(c, err, 401)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if guest.Disabled {
|
||||
common.ErrorStrResp(c, "Guest user is disabled, login please", 401)
|
||||
// validate password timestamp
|
||||
if userClaims.PwdTS != user.PwdTS {
|
||||
common.ErrorStrResp(c, "Password has been changed, login please", 401)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
common.GinWithValue(c, conf.UserKey, guest)
|
||||
log.Debugf("use empty token: %+v", guest)
|
||||
if user.Disabled {
|
||||
common.ErrorStrResp(c, "Current user is disabled, replace please", 401)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
common.GinWithValue(c, conf.UserKey, user)
|
||||
log.Debugf("use login token: %+v", user)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
userClaims, err := common.ParseToken(token)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 401)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
user, err := op.GetUserByName(userClaims.Username)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 401)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
// validate password timestamp
|
||||
if userClaims.PwdTS != user.PwdTS {
|
||||
common.ErrorStrResp(c, "Password has been changed, login please", 401)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if user.Disabled {
|
||||
common.ErrorStrResp(c, "Current user is disabled, replace please", 401)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
common.GinWithValue(c, conf.UserKey, user)
|
||||
log.Debugf("use login token: %+v", user)
|
||||
c.Next()
|
||||
}
|
||||
|
||||
func Authn(c *gin.Context) {
|
||||
|
@ -15,10 +15,15 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func PathParse(c *gin.Context) {
|
||||
rawPath := parsePath(c.Param("path"))
|
||||
common.GinWithValue(c, conf.PathKey, rawPath)
|
||||
c.Next()
|
||||
}
|
||||
|
||||
func Down(verifyFunc func(string, string) error) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
rawPath := parsePath(c.Param("path"))
|
||||
common.GinWithValue(c, conf.PathKey, rawPath)
|
||||
rawPath := c.Request.Context().Value(conf.PathKey).(string)
|
||||
meta, err := op.GetNearestMeta(rawPath)
|
||||
if err != nil {
|
||||
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
||||
|
18
server/middlewares/sharing.go
Normal file
18
server/middlewares/sharing.go
Normal file
@ -0,0 +1,18 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/conf"
|
||||
"github.com/OpenListTeam/OpenList/v4/server/common"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SharingIdParse(c *gin.Context) {
|
||||
sid := c.Param("sid")
|
||||
common.GinWithValue(c, conf.SharingIDKey, sid)
|
||||
c.Next()
|
||||
}
|
||||
|
||||
func EmptyPathParse(c *gin.Context) {
|
||||
common.GinWithValue(c, conf.PathKey, "/")
|
||||
c.Next()
|
||||
}
|
@ -44,20 +44,29 @@ func Init(e *gin.Engine) {
|
||||
|
||||
downloadLimiter := middlewares.DownloadRateLimiter(stream.ClientDownloadLimit)
|
||||
signCheck := middlewares.Down(sign.Verify)
|
||||
g.GET("/d/*path", signCheck, downloadLimiter, handles.Down)
|
||||
g.GET("/p/*path", signCheck, downloadLimiter, handles.Proxy)
|
||||
g.HEAD("/d/*path", signCheck, handles.Down)
|
||||
g.HEAD("/p/*path", signCheck, handles.Proxy)
|
||||
g.GET("/d/*path", middlewares.PathParse, signCheck, downloadLimiter, handles.Down)
|
||||
g.GET("/p/*path", middlewares.PathParse, signCheck, downloadLimiter, handles.Proxy)
|
||||
g.HEAD("/d/*path", middlewares.PathParse, signCheck, handles.Down)
|
||||
g.HEAD("/p/*path", middlewares.PathParse, signCheck, handles.Proxy)
|
||||
archiveSignCheck := middlewares.Down(sign.VerifyArchive)
|
||||
g.GET("/ad/*path", archiveSignCheck, downloadLimiter, handles.ArchiveDown)
|
||||
g.GET("/ap/*path", archiveSignCheck, downloadLimiter, handles.ArchiveProxy)
|
||||
g.GET("/ae/*path", archiveSignCheck, downloadLimiter, handles.ArchiveInternalExtract)
|
||||
g.HEAD("/ad/*path", archiveSignCheck, handles.ArchiveDown)
|
||||
g.HEAD("/ap/*path", archiveSignCheck, handles.ArchiveProxy)
|
||||
g.HEAD("/ae/*path", archiveSignCheck, handles.ArchiveInternalExtract)
|
||||
g.GET("/ad/*path", middlewares.PathParse, archiveSignCheck, downloadLimiter, handles.ArchiveDown)
|
||||
g.GET("/ap/*path", middlewares.PathParse, archiveSignCheck, downloadLimiter, handles.ArchiveProxy)
|
||||
g.GET("/ae/*path", middlewares.PathParse, archiveSignCheck, downloadLimiter, handles.ArchiveInternalExtract)
|
||||
g.HEAD("/ad/*path", middlewares.PathParse, archiveSignCheck, handles.ArchiveDown)
|
||||
g.HEAD("/ap/*path", middlewares.PathParse, archiveSignCheck, handles.ArchiveProxy)
|
||||
g.HEAD("/ae/*path", middlewares.PathParse, archiveSignCheck, handles.ArchiveInternalExtract)
|
||||
|
||||
g.GET("/sd/:sid", middlewares.EmptyPathParse, middlewares.SharingIdParse, downloadLimiter, handles.SharingDown)
|
||||
g.GET("/sd/:sid/*path", middlewares.PathParse, middlewares.SharingIdParse, downloadLimiter, handles.SharingDown)
|
||||
g.HEAD("/sd/:sid", middlewares.EmptyPathParse, middlewares.SharingIdParse, handles.SharingDown)
|
||||
g.HEAD("/sd/:sid/*path", middlewares.PathParse, middlewares.SharingIdParse, handles.SharingDown)
|
||||
g.GET("/sad/:sid", middlewares.EmptyPathParse, middlewares.SharingIdParse, downloadLimiter, handles.SharingArchiveExtract)
|
||||
g.GET("/sad/:sid/*path", middlewares.PathParse, middlewares.SharingIdParse, downloadLimiter, handles.SharingArchiveExtract)
|
||||
g.HEAD("/sad/:sid", middlewares.EmptyPathParse, middlewares.SharingIdParse, handles.SharingArchiveExtract)
|
||||
g.HEAD("/sad/:sid/*path", middlewares.PathParse, middlewares.SharingIdParse, handles.SharingArchiveExtract)
|
||||
|
||||
api := g.Group("/api")
|
||||
auth := api.Group("", middlewares.Auth)
|
||||
auth := api.Group("", middlewares.Auth(false))
|
||||
webauthn := api.Group("/authn", middlewares.Authn)
|
||||
|
||||
api.POST("/auth/login", handles.Login)
|
||||
@ -93,7 +102,9 @@ func Init(e *gin.Engine) {
|
||||
public.Any("/archive_extensions", handles.ArchiveExtensions)
|
||||
|
||||
_fs(auth.Group("/fs"))
|
||||
fsAndShare(api.Group("/fs", middlewares.Auth(true)))
|
||||
_task(auth.Group("/task", middlewares.AuthNotGuest))
|
||||
_sharing(auth.Group("/share", middlewares.AuthNotGuest))
|
||||
admin(auth.Group("/admin", middlewares.AuthAdmin))
|
||||
if flags.Debug || flags.Dev {
|
||||
debug(g.Group("/debug"))
|
||||
@ -169,10 +180,16 @@ func admin(g *gin.RouterGroup) {
|
||||
index.GET("/progress", middlewares.SearchIndex, handles.GetProgress)
|
||||
}
|
||||
|
||||
func fsAndShare(g *gin.RouterGroup) {
|
||||
g.Any("/list", handles.FsListSplit)
|
||||
g.Any("/get", handles.FsGetSplit)
|
||||
a := g.Group("/archive")
|
||||
a.Any("/meta", handles.FsArchiveMetaSplit)
|
||||
a.Any("/list", handles.FsArchiveListSplit)
|
||||
}
|
||||
|
||||
func _fs(g *gin.RouterGroup) {
|
||||
g.Any("/list", handles.FsList)
|
||||
g.Any("/search", middlewares.SearchIndex, handles.Search)
|
||||
g.Any("/get", handles.FsGet)
|
||||
g.Any("/other", handles.FsOther)
|
||||
g.Any("/dirs", handles.FsDirs)
|
||||
g.POST("/mkdir", handles.FsMkdir)
|
||||
@ -192,16 +209,23 @@ func _fs(g *gin.RouterGroup) {
|
||||
// g.POST("/add_qbit", handles.AddQbittorrent)
|
||||
// g.POST("/add_transmission", handles.SetTransmission)
|
||||
g.POST("/add_offline_download", handles.AddOfflineDownload)
|
||||
a := g.Group("/archive")
|
||||
a.Any("/meta", handles.FsArchiveMeta)
|
||||
a.Any("/list", handles.FsArchiveList)
|
||||
a.POST("/decompress", handles.FsArchiveDecompress)
|
||||
g.POST("/archive/decompress", handles.FsArchiveDecompress)
|
||||
}
|
||||
|
||||
func _task(g *gin.RouterGroup) {
|
||||
handles.SetupTaskRoute(g)
|
||||
}
|
||||
|
||||
func _sharing(g *gin.RouterGroup) {
|
||||
g.Any("/list", handles.ListSharings)
|
||||
g.GET("/get", handles.GetSharing)
|
||||
g.POST("/create", handles.CreateSharing)
|
||||
g.POST("/update", handles.UpdateSharing)
|
||||
g.POST("/delete", handles.DeleteSharing)
|
||||
g.POST("/enable", handles.SetEnableSharing(false))
|
||||
g.POST("/disable", handles.SetEnableSharing(true))
|
||||
}
|
||||
|
||||
func Cors(r *gin.Engine) {
|
||||
config := cors.DefaultConfig()
|
||||
// config.AllowAllOrigins = true
|
||||
|
Reference in New Issue
Block a user