- Lien /s/<jeton> : jeton = id aléatoire 128 bits + HMAC-SHA256 tronqué par la
clé du serveur (SHARE_SECRET, sinon DATA_DIR/.share-secret générée en 0600).
Valide si la signature est correcte, le lien présent dans .shares.json et non
expiré (1, 7, 30 jours ou sans expiration).
- API : GET/POST/DELETE /api/shares, POST /api/shares/revoke-all (renouvelle la
clé), GET /api/public/{jeton} hors authentification (nom, titre, logo, scène).
- Auth Basic : /s/, /api/public/, /healthz et les fichiers statiques du build
sont publics ; le reste de l'API et l'index restent protégés. Le serveur
statique ne liste plus les répertoires. X-Robots-Tag: noindex sur les liens.
- Les liens suivent renommages et déplacements, disparaissent à la suppression.
- Interface : dialogue Partager (barre du haut, menu de l'explorateur), section
Liens de partage dans Paramètres, vue publique Excalidraw en lecture seule
avec téléchargement et bascule clair/sombre.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H6J7vKwXx7uopxKhWfFJpm
108 lines
3.3 KiB
Go
108 lines
3.3 KiB
Go
package storage
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestShares(t *testing.T) {
|
|
s := newStore(t)
|
|
s.Write("d/a.excalidraw", []byte(doc), "", true)
|
|
|
|
if _, err := s.CreateShare("d/absent.excalidraw", 0); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("schéma absent : %v", err)
|
|
}
|
|
if _, err := s.CreateShare("d", 0); !errors.Is(err, ErrInvalidPath) {
|
|
t.Fatalf("dossier : %v", err)
|
|
}
|
|
sh, err := s.CreateShare("d/a.excalidraw", 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if fi, _ := os.Stat(filepath.Join(s.root, secretFile)); fi == nil || fi.Mode().Perm() != 0o600 {
|
|
t.Fatalf("clé : %v", fi)
|
|
}
|
|
got, data, err := s.ReadShared(sh.Token)
|
|
if err != nil || got.Path != "d/a.excalidraw" || string(data) != doc {
|
|
t.Fatalf("ReadShared = %+v, %s, %v", got, data, err)
|
|
}
|
|
|
|
// Jetons altérés ou mal formés.
|
|
bad := []string{"", "x", sh.ID, sh.ID + ".", sh.Token[:len(sh.Token)-1] + "A", "A" + sh.Token[1:]}
|
|
for _, tok := range bad {
|
|
if tok == sh.Token {
|
|
continue
|
|
}
|
|
if _, err := s.ResolveShare(tok); !errors.Is(err, ErrNotFound) {
|
|
t.Errorf("jeton %q accepté : %v", tok, err)
|
|
}
|
|
}
|
|
|
|
// La clé survit au redémarrage ; le lien suit le schéma déplacé.
|
|
s2, _ := New(s.root)
|
|
s2.Move("d", "e")
|
|
if got, err := s2.ResolveShare(sh.Token); err != nil || got.Path != "e/a.excalidraw" {
|
|
t.Fatalf("après déplacement : %+v, %v", got, err)
|
|
}
|
|
if l, _ := s2.Shares("e/a.excalidraw"); len(l) != 1 || l[0].Token != sh.Token {
|
|
t.Fatalf("Shares = %+v", l)
|
|
}
|
|
|
|
// Révocation unitaire.
|
|
if err := s2.RevokeShare(sh.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := s2.ResolveShare(sh.Token); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("lien révoqué accepté : %v", err)
|
|
}
|
|
|
|
// Expiration : le lien expiré est refusé puis purgé de la liste.
|
|
exp, _ := s2.CreateShare("e/a.excalidraw", time.Millisecond)
|
|
time.Sleep(5 * time.Millisecond)
|
|
if _, err := s2.ResolveShare(exp.Token); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("lien expiré accepté : %v", err)
|
|
}
|
|
if l, _ := s2.Shares(""); len(l) != 0 {
|
|
t.Fatalf("lien expiré listé : %+v", l)
|
|
}
|
|
|
|
// Tout révoquer change la clé : un lien recréé avec un ancien id ne passe plus.
|
|
keep, _ := s2.CreateShare("e/a.excalidraw", 0)
|
|
if err := s2.RevokeAllShares(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := s2.ResolveShare(keep.Token); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("après révocation globale : %v", err)
|
|
}
|
|
|
|
// Suppression du schéma : ses liens disparaissent.
|
|
del, _ := s2.CreateShare("e/a.excalidraw", 0)
|
|
s2.Delete("e", true)
|
|
if _, err := s2.ResolveShare(del.Token); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("schéma supprimé : %v", err)
|
|
}
|
|
}
|
|
|
|
func TestShareFixedSecret(t *testing.T) {
|
|
s := newStore(t)
|
|
s.SetShareSecret("une-clé-imposée-assez-longue")
|
|
s.Write("a.excalidraw", []byte(doc), "", true)
|
|
sh, _ := s.CreateShare("a.excalidraw", 0)
|
|
if _, err := os.Stat(filepath.Join(s.root, secretFile)); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("clé écrite sur disque malgré SHARE_SECRET : %v", err)
|
|
}
|
|
s2, _ := New(s.root)
|
|
s2.SetShareSecret("une-clé-imposée-assez-longue")
|
|
if _, err := s2.ResolveShare(sh.Token); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s3, _ := New(s.root)
|
|
s3.SetShareSecret("une-autre-clé-assez-longue")
|
|
if _, err := s3.ResolveShare(sh.Token); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("autre clé acceptée : %v", err)
|
|
}
|
|
}
|