Include note filenames in new share links
This commit is contained in:
1 parent
ac81544e03
commit
e66fee9dac
4 files changed
+54
-6
No files matched your search
@@ -3,11 +3,13 @@ package api
|
|||||||
import (
|
import (
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -350,6 +352,52 @@ func TestShareAccess(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestShareFilenameToken(t *testing.T) {
|
||||||
|
f := newFixture(t)
|
||||||
|
name := "计划 #1? 50% +;\""
|
||||||
|
p := "目录/" + name + ".md"
|
||||||
|
f.write(p, "正文")
|
||||||
|
f.write("目录/"+name+".assets/图.png", "PNG")
|
||||||
|
body, err := json.Marshal(map[string]string{"path": p, "password": "访问密码"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
sh := decode[shareView](t, f.do(f.admin, "POST", "/api/v1/shares", string(body), nil))
|
||||||
|
suffix, ok := strings.CutPrefix(sh.Token, name+"-")
|
||||||
|
random, err := base64.RawURLEncoding.DecodeString(suffix)
|
||||||
|
if !ok || err != nil || len(random) != 16 {
|
||||||
|
t.Fatal("分享应包含完整文件名和 128 位随机字符,不包含目录")
|
||||||
|
}
|
||||||
|
other := decode[shareView](t, f.do(f.admin, "POST", "/api/v1/shares", string(body), nil))
|
||||||
|
if other.Token == sh.Token {
|
||||||
|
t.Fatal("同一笔记的分享链接不能重复")
|
||||||
|
}
|
||||||
|
base := "/api/v1/public/shares/" + url.PathEscape(sh.Token)
|
||||||
|
guest := newClient()
|
||||||
|
if code := f.status(guest, "GET", base, "", nil); code != 401 {
|
||||||
|
t.Fatalf("未解锁应拒绝访问,得到 %d", code)
|
||||||
|
}
|
||||||
|
if code := f.status(guest, "POST", base+"/unlock", `{"password":"访问密码"}`, nil); code != 200 {
|
||||||
|
t.Fatalf("解锁失败:%d", code)
|
||||||
|
}
|
||||||
|
if code := f.status(guest, "GET", base, "", nil); code != 200 {
|
||||||
|
t.Fatalf("带保留字符的链接应可读取正文,得到 %d", code)
|
||||||
|
}
|
||||||
|
asset := base + "/assets/" + url.PathEscape(name+".assets") + "/图.png"
|
||||||
|
if code := f.status(guest, "GET", asset, "", nil); code != 200 {
|
||||||
|
t.Fatalf("解锁凭据应覆盖图片路径,得到 %d", code)
|
||||||
|
}
|
||||||
|
if code := f.status(guest, "GET", "/api/v1/public/shares/"+url.PathEscape(other.Token), "", nil); code != 401 {
|
||||||
|
t.Fatalf("凭据不能解锁另一分享,得到 %d", code)
|
||||||
|
}
|
||||||
|
if code := f.status(f.admin, "DELETE", "/api/v1/shares/"+url.PathEscape(sh.Token), "", nil); code != 200 {
|
||||||
|
t.Fatalf("撤销带保留字符的链接失败:%d", code)
|
||||||
|
}
|
||||||
|
if code := f.status(guest, "GET", base, "", nil); code != 404 {
|
||||||
|
t.Fatalf("撤销后应返回 404,得到 %d", code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestShareWithoutPasswordAndExpiry(t *testing.T) {
|
func TestShareWithoutPasswordAndExpiry(t *testing.T) {
|
||||||
f := newFixture(t)
|
f := newFixture(t)
|
||||||
f.write("a.md", "公开")
|
f.write("a.md", "公开")
|
||||||
|
|||||||
@@ -1083,7 +1083,7 @@
|
|||||||
},
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "新分享,访问地址为 /s/{token}",
|
"description": "新分享,token 为文件名(不含 .md)加连字符和随机字符,访问地址为 /s/{URL 编码后的 token};文件移动或改名不改变已生成的链接",
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func (s *Server) createShare(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
sh := store.Share{
|
sh := store.Share{
|
||||||
Token: auth.NewShareToken(),
|
Token: strings.TrimSuffix(path.Base(p), vault.NoteExt) + "-" + auth.NewShareToken(),
|
||||||
Path: p,
|
Path: p,
|
||||||
PasswordHash: auth.HashSharePassword(body.Password),
|
PasswordHash: auth.HashSharePassword(body.Password),
|
||||||
CreatedAt: now.UnixMilli(),
|
CreatedAt: now.UnixMilli(),
|
||||||
@@ -212,9 +212,9 @@ func (s *Server) unlockShare(w http.ResponseWriter, r *http.Request) {
|
|||||||
ttl = time.Until(time.UnixMilli(sh.ExpiresAt))
|
ttl = time.Until(time.UnixMilli(sh.ExpiresAt))
|
||||||
}
|
}
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: shareCookieName,
|
Name: shareCookieName,
|
||||||
Value: value,
|
Value: value,
|
||||||
Path: "/api/v1/public/shares/" + sh.Token,
|
// 默认 Path 为 /unlock 所在目录,避免非 ASCII 文件名违反 Cookie Path 格式。
|
||||||
MaxAge: int(ttl / time.Second),
|
MaxAge: int(ttl / time.Second),
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Secure: r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https",
|
Secure: r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https",
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const busy = ref(false)
|
|||||||
const failed = ref(false)
|
const failed = ref(false)
|
||||||
const fmt = new Intl.DateTimeFormat('zh-CN', { dateStyle: 'short', timeStyle: 'short' })
|
const fmt = new Intl.DateTimeFormat('zh-CN', { dateStyle: 'short', timeStyle: 'short' })
|
||||||
|
|
||||||
const linkOf = (s: Share) => `${location.origin}/s/${s.token}`
|
const linkOf = (s: Share) => `${location.origin}/s/${encodeURIComponent(s.token)}`
|
||||||
const expired = (s: Share) => s.expiresAt !== 0 && s.expiresAt <= Date.now()
|
const expired = (s: Share) => s.expiresAt !== 0 && s.expiresAt <= Date.now()
|
||||||
|
|
||||||
function errorText(err: unknown) {
|
function errorText(err: unknown) {
|
||||||
|
|||||||
Reference in new issue
Block a user