Skip to content

Commit e4c902d

Browse files
KirCuteSuyunmengj2rong4cn
authored
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 <[email protected]> Co-authored-by: j2rong4cn <[email protected]>
1 parent 5d8bd25 commit e4c902d

File tree

28 files changed

+1698
-94
lines changed

28 files changed

+1698
-94
lines changed

drivers/all.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import (
4848
_ "github.com/OpenListTeam/OpenList/v4/drivers/onedrive_app"
4949
_ "github.com/OpenListTeam/OpenList/v4/drivers/onedrive_sharelink"
5050
_ "github.com/OpenListTeam/OpenList/v4/drivers/openlist"
51+
_ "github.com/OpenListTeam/OpenList/v4/drivers/openlist_share"
5152
_ "github.com/OpenListTeam/OpenList/v4/drivers/pikpak"
5253
_ "github.com/OpenListTeam/OpenList/v4/drivers/pikpak_share"
5354
_ "github.com/OpenListTeam/OpenList/v4/drivers/quark_open"

drivers/openlist_share/driver.go

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package openlist_share
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"net/url"
8+
stdpath "path"
9+
"strings"
10+
11+
"github.com/OpenListTeam/OpenList/v4/internal/driver"
12+
"github.com/OpenListTeam/OpenList/v4/internal/errs"
13+
"github.com/OpenListTeam/OpenList/v4/internal/model"
14+
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
15+
"github.com/OpenListTeam/OpenList/v4/server/common"
16+
"github.com/go-resty/resty/v2"
17+
)
18+
19+
type OpenListShare struct {
20+
model.Storage
21+
Addition
22+
serverArchivePreview bool
23+
}
24+
25+
func (d *OpenListShare) Config() driver.Config {
26+
return config
27+
}
28+
29+
func (d *OpenListShare) GetAddition() driver.Additional {
30+
return &d.Addition
31+
}
32+
33+
func (d *OpenListShare) Init(ctx context.Context) error {
34+
d.Addition.Address = strings.TrimSuffix(d.Addition.Address, "/")
35+
var settings common.Resp[map[string]string]
36+
_, _, err := d.request("/public/settings", http.MethodGet, func(req *resty.Request) {
37+
req.SetResult(&settings)
38+
})
39+
if err != nil {
40+
return err
41+
}
42+
d.serverArchivePreview = settings.Data["share_archive_preview"] == "true"
43+
return nil
44+
}
45+
46+
func (d *OpenListShare) Drop(ctx context.Context) error {
47+
return nil
48+
}
49+
50+
func (d *OpenListShare) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
51+
var resp common.Resp[FsListResp]
52+
_, _, err := d.request("/fs/list", http.MethodPost, func(req *resty.Request) {
53+
req.SetResult(&resp).SetBody(ListReq{
54+
PageReq: model.PageReq{
55+
Page: 1,
56+
PerPage: 0,
57+
},
58+
Path: stdpath.Join(fmt.Sprintf("/@s/%s", d.ShareId), dir.GetPath()),
59+
Password: d.Pwd,
60+
Refresh: false,
61+
})
62+
})
63+
if err != nil {
64+
return nil, err
65+
}
66+
var files []model.Obj
67+
for _, f := range resp.Data.Content {
68+
file := model.ObjThumb{
69+
Object: model.Object{
70+
Name: f.Name,
71+
Modified: f.Modified,
72+
Ctime: f.Created,
73+
Size: f.Size,
74+
IsFolder: f.IsDir,
75+
HashInfo: utils.FromString(f.HashInfo),
76+
},
77+
Thumbnail: model.Thumbnail{Thumbnail: f.Thumb},
78+
}
79+
files = append(files, &file)
80+
}
81+
return files, nil
82+
}
83+
84+
func (d *OpenListShare) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
85+
path := utils.FixAndCleanPath(stdpath.Join(d.ShareId, file.GetPath()))
86+
u := fmt.Sprintf("%s/sd%s?pwd=%s", d.Address, path, d.Pwd)
87+
return &model.Link{URL: u}, nil
88+
}
89+
90+
func (d *OpenListShare) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
91+
if !d.serverArchivePreview || !d.ForwardArchiveReq {
92+
return nil, errs.NotImplement
93+
}
94+
var resp common.Resp[ArchiveMetaResp]
95+
_, code, err := d.request("/fs/archive/meta", http.MethodPost, func(req *resty.Request) {
96+
req.SetResult(&resp).SetBody(ArchiveMetaReq{
97+
ArchivePass: args.Password,
98+
Path: stdpath.Join(fmt.Sprintf("/@s/%s", d.ShareId), obj.GetPath()),
99+
Password: d.Pwd,
100+
Refresh: false,
101+
})
102+
})
103+
if code == 202 {
104+
return nil, errs.WrongArchivePassword
105+
}
106+
if err != nil {
107+
return nil, err
108+
}
109+
var tree []model.ObjTree
110+
if resp.Data.Content != nil {
111+
tree = make([]model.ObjTree, 0, len(resp.Data.Content))
112+
for _, content := range resp.Data.Content {
113+
tree = append(tree, &content)
114+
}
115+
}
116+
return &model.ArchiveMetaInfo{
117+
Comment: resp.Data.Comment,
118+
Encrypted: resp.Data.Encrypted,
119+
Tree: tree,
120+
}, nil
121+
}
122+
123+
func (d *OpenListShare) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {
124+
if !d.serverArchivePreview || !d.ForwardArchiveReq {
125+
return nil, errs.NotImplement
126+
}
127+
var resp common.Resp[ArchiveListResp]
128+
_, code, err := d.request("/fs/archive/list", http.MethodPost, func(req *resty.Request) {
129+
req.SetResult(&resp).SetBody(ArchiveListReq{
130+
ArchiveMetaReq: ArchiveMetaReq{
131+
ArchivePass: args.Password,
132+
Path: stdpath.Join(fmt.Sprintf("/@s/%s", d.ShareId), obj.GetPath()),
133+
Password: d.Pwd,
134+
Refresh: false,
135+
},
136+
PageReq: model.PageReq{
137+
Page: 1,
138+
PerPage: 0,
139+
},
140+
InnerPath: args.InnerPath,
141+
})
142+
})
143+
if code == 202 {
144+
return nil, errs.WrongArchivePassword
145+
}
146+
if err != nil {
147+
return nil, err
148+
}
149+
var files []model.Obj
150+
for _, f := range resp.Data.Content {
151+
file := model.ObjThumb{
152+
Object: model.Object{
153+
Name: f.Name,
154+
Modified: f.Modified,
155+
Ctime: f.Created,
156+
Size: f.Size,
157+
IsFolder: f.IsDir,
158+
HashInfo: utils.FromString(f.HashInfo),
159+
},
160+
Thumbnail: model.Thumbnail{Thumbnail: f.Thumb},
161+
}
162+
files = append(files, &file)
163+
}
164+
return files, nil
165+
}
166+
167+
func (d *OpenListShare) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {
168+
if !d.serverArchivePreview || !d.ForwardArchiveReq {
169+
return nil, errs.NotSupport
170+
}
171+
path := utils.FixAndCleanPath(stdpath.Join(d.ShareId, obj.GetPath()))
172+
u := fmt.Sprintf("%s/sad%s?pwd=%s&inner=%s&pass=%s",
173+
d.Address,
174+
path,
175+
d.Pwd,
176+
utils.EncodePath(args.InnerPath, true),
177+
url.QueryEscape(args.Password))
178+
return &model.Link{URL: u}, nil
179+
}
180+
181+
var _ driver.Driver = (*OpenListShare)(nil)

drivers/openlist_share/meta.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package openlist_share
2+
3+
import (
4+
"github.com/OpenListTeam/OpenList/v4/internal/driver"
5+
"github.com/OpenListTeam/OpenList/v4/internal/op"
6+
)
7+
8+
type Addition struct {
9+
driver.RootPath
10+
Address string `json:"url" required:"true"`
11+
ShareId string `json:"sid" required:"true"`
12+
Pwd string `json:"pwd"`
13+
ForwardArchiveReq bool `json:"forward_archive_requests" default:"true"`
14+
}
15+
16+
var config = driver.Config{
17+
Name: "OpenListShare",
18+
LocalSort: true,
19+
NoUpload: true,
20+
DefaultRoot: "/",
21+
}
22+
23+
func init() {
24+
op.RegisterDriver(func() driver.Driver {
25+
return &OpenListShare{}
26+
})
27+
}

drivers/openlist_share/types.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package openlist_share
2+
3+
import (
4+
"time"
5+
6+
"github.com/OpenListTeam/OpenList/v4/internal/model"
7+
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
8+
)
9+
10+
type ListReq struct {
11+
model.PageReq
12+
Path string `json:"path" form:"path"`
13+
Password string `json:"password" form:"password"`
14+
Refresh bool `json:"refresh"`
15+
}
16+
17+
type ObjResp struct {
18+
Name string `json:"name"`
19+
Size int64 `json:"size"`
20+
IsDir bool `json:"is_dir"`
21+
Modified time.Time `json:"modified"`
22+
Created time.Time `json:"created"`
23+
Sign string `json:"sign"`
24+
Thumb string `json:"thumb"`
25+
Type int `json:"type"`
26+
HashInfo string `json:"hashinfo"`
27+
}
28+
29+
type FsListResp struct {
30+
Content []ObjResp `json:"content"`
31+
Total int64 `json:"total"`
32+
Readme string `json:"readme"`
33+
Write bool `json:"write"`
34+
Provider string `json:"provider"`
35+
}
36+
37+
type ArchiveMetaReq struct {
38+
ArchivePass string `json:"archive_pass"`
39+
Password string `json:"password"`
40+
Path string `json:"path"`
41+
Refresh bool `json:"refresh"`
42+
}
43+
44+
type TreeResp struct {
45+
ObjResp
46+
Children []TreeResp `json:"children"`
47+
hashCache *utils.HashInfo
48+
}
49+
50+
func (t *TreeResp) GetSize() int64 {
51+
return t.Size
52+
}
53+
54+
func (t *TreeResp) GetName() string {
55+
return t.Name
56+
}
57+
58+
func (t *TreeResp) ModTime() time.Time {
59+
return t.Modified
60+
}
61+
62+
func (t *TreeResp) CreateTime() time.Time {
63+
return t.Created
64+
}
65+
66+
func (t *TreeResp) IsDir() bool {
67+
return t.ObjResp.IsDir
68+
}
69+
70+
func (t *TreeResp) GetHash() utils.HashInfo {
71+
return utils.FromString(t.HashInfo)
72+
}
73+
74+
func (t *TreeResp) GetID() string {
75+
return ""
76+
}
77+
78+
func (t *TreeResp) GetPath() string {
79+
return ""
80+
}
81+
82+
func (t *TreeResp) GetChildren() []model.ObjTree {
83+
ret := make([]model.ObjTree, 0, len(t.Children))
84+
for _, child := range t.Children {
85+
ret = append(ret, &child)
86+
}
87+
return ret
88+
}
89+
90+
func (t *TreeResp) Thumb() string {
91+
return t.ObjResp.Thumb
92+
}
93+
94+
type ArchiveMetaResp struct {
95+
Comment string `json:"comment"`
96+
Encrypted bool `json:"encrypted"`
97+
Content []TreeResp `json:"content"`
98+
RawURL string `json:"raw_url"`
99+
Sign string `json:"sign"`
100+
}
101+
102+
type ArchiveListReq struct {
103+
model.PageReq
104+
ArchiveMetaReq
105+
InnerPath string `json:"inner_path"`
106+
}
107+
108+
type ArchiveListResp struct {
109+
Content []ObjResp `json:"content"`
110+
Total int64 `json:"total"`
111+
}

drivers/openlist_share/util.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package openlist_share
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/OpenListTeam/OpenList/v4/drivers/base"
7+
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
8+
)
9+
10+
func (d *OpenListShare) request(api, method string, callback base.ReqCallback) ([]byte, int, error) {
11+
url := d.Address + "/api" + api
12+
req := base.RestyClient.R()
13+
if callback != nil {
14+
callback(req)
15+
}
16+
res, err := req.Execute(method, url)
17+
if err != nil {
18+
code := 0
19+
if res != nil {
20+
code = res.StatusCode()
21+
}
22+
return nil, code, err
23+
}
24+
if res.StatusCode() >= 400 {
25+
return nil, res.StatusCode(), fmt.Errorf("request failed, status: %s", res.Status())
26+
}
27+
code := utils.Json.Get(res.Body(), "code").ToInt()
28+
if code != 200 {
29+
return nil, code, fmt.Errorf("request failed, code: %d, message: %s", code, utils.Json.Get(res.Body(), "message").ToString())
30+
}
31+
return res.Body(), 200, nil
32+
}

internal/bootstrap/data/setting.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ func InitialSettings() []model.SettingItem {
111111
{Key: conf.Favicon, Value: "https://res.oplist.org/logo/logo.svg", MigrationValue: "https://cdn.oplist.org/gh/OpenListTeam/Logo@main/logo.svg", Type: conf.TypeString, Group: model.STYLE},
112112
{Key: conf.MainColor, Value: "#1890ff", Type: conf.TypeString, Group: model.STYLE},
113113
{Key: "home_icon", Value: "🏠", Type: conf.TypeString, Group: model.STYLE},
114+
{Key: "share_icon", Value: "🎁", Type: conf.TypeString, Group: model.STYLE},
114115
{Key: "home_container", Value: "max_980px", Type: conf.TypeSelect, Options: "max_980px,hope_container", Group: model.STYLE},
115116
{Key: "settings_layout", Value: "list", Type: conf.TypeSelect, Options: "list,responsive", Group: model.STYLE},
116117
// preview settings
@@ -163,6 +164,10 @@ func InitialSettings() []model.SettingItem {
163164
{Key: conf.ForwardDirectLinkParams, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL},
164165
{Key: conf.IgnoreDirectLinkParams, Value: "sign,openlist_ts", Type: conf.TypeString, Group: model.GLOBAL},
165166
{Key: conf.WebauthnLoginEnabled, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PUBLIC},
167+
{Key: conf.SharePreview, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PUBLIC},
168+
{Key: conf.ShareArchivePreview, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PUBLIC},
169+
{Key: conf.ShareForceProxy, Value: "true", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PRIVATE},
170+
{Key: conf.ShareSummaryContent, Value: "@{{creator}} shared {{#each files}}{{#if @first}}\"{{filename this}}\"{{/if}}{{#if @last}}{{#unless (eq @index 0)}} and {{@index}} more files{{/unless}}{{/if}}{{/each}} from {{site_title}}: {{base_url}}/@s/{{id}}{{#if pwd}} , the share code is {{pwd}}{{/if}}{{#if expires}}, please access before {{dateLocaleString expires}}.{{/if}}", Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PUBLIC},
166171

167172
// single settings
168173
{Key: conf.Token, Value: token, Type: conf.TypeString, Group: model.SINGLE, Flag: model.PRIVATE},

internal/bootstrap/data/user.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ func initUser() {
3333
Role: model.ADMIN,
3434
BasePath: "/",
3535
Authn: "[]",
36-
// 0(can see hidden) - 7(can remove) & 12(can read archives) - 13(can decompress archives)
37-
Permission: 0x31FF,
36+
// 0(can see hidden) - 8(webdav read) & 12(can read archives) - 14(can share)
37+
Permission: 0x71FF,
3838
}
3939
if err := op.CreateUser(admin); err != nil {
4040
panic(err)

0 commit comments

Comments
 (0)