mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2025-09-19 04:06:18 +08:00

提供一种类似大多数网盘的文件分享操作,这种分享方式可以通过强制 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>
552 lines
15 KiB
Go
552 lines
15 KiB
Go
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
|
|
}
|