mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 14:27:27 +00:00
Re-model the web file host as a file server (not a website host): - Drop the misnamed /site area. A member's public files are now their /me/public subfolder (unix ~/public), served anonymously at ~<name>/public. The rest of /me stays private; only ~name/public is ever exposed. Bare /~name redirects to /~name/public/. - The root / is now a directory of ALL members, each linked to their BBS site (https://<bbs-host>/~name via WebConfig.SiteBase) AND their public files here (~name/public). No longer hides empty members. - Sites/homepages stay on the BBS — files.<host> only links to them. - Usage gauge is just /me again (which includes /me/public). setup.sh + docs updated; tests cover ~name/public browse/download, the bare-~name redirect, empty-member empty-listing, /public-only exposure, and traversal confinement. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
87 lines
2.2 KiB
Go
87 lines
2.2 KiB
Go
package files
|
|
|
|
import (
|
|
"io"
|
|
"net"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/pkg/sftp"
|
|
)
|
|
|
|
// startE2E wires a real sftp client to the request server for one member, over
|
|
// an in-memory pipe (no SSH transport).
|
|
func startE2E(t *testing.T) *sftp.Client {
|
|
t.Helper()
|
|
svc, _, u := newTestService(t)
|
|
sess, err := svc.newSession(u)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
srvConn, cliConn := net.Pipe()
|
|
handlers := sftp.Handlers{FileGet: sess, FilePut: sess, FileCmd: sess, FileList: sess}
|
|
server := sftp.NewRequestServer(srvConn, handlers)
|
|
go func() { _ = server.Serve() }()
|
|
client, err := sftp.NewClientPipe(cliConn, cliConn)
|
|
if err != nil {
|
|
t.Fatalf("NewClientPipe: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = client.Close(); _ = server.Close() })
|
|
return client
|
|
}
|
|
|
|
func TestE2E_RootListing(t *testing.T) {
|
|
client := startE2E(t)
|
|
infos, err := client.ReadDir("/")
|
|
if err != nil {
|
|
t.Fatalf("ReadDir /: %v", err)
|
|
}
|
|
var names []string
|
|
for _, fi := range infos {
|
|
names = append(names, fi.Name())
|
|
}
|
|
sort.Strings(names)
|
|
if len(names) != 2 || names[0] != "me" || names[1] != "public" {
|
|
t.Errorf("root listing = %v, want [me public]", names)
|
|
}
|
|
}
|
|
|
|
func TestE2E_UploadDownloadPrivate(t *testing.T) {
|
|
client := startE2E(t)
|
|
f, err := client.Create("/me/hello.txt")
|
|
if err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
if _, err := f.Write([]byte("hello bbs")); err != nil {
|
|
t.Fatalf("Write: %v", err)
|
|
}
|
|
if err := f.Close(); err != nil {
|
|
t.Fatalf("Close: %v", err)
|
|
}
|
|
|
|
rf, err := client.Open("/me/hello.txt")
|
|
if err != nil {
|
|
t.Fatalf("Open: %v", err)
|
|
}
|
|
defer rf.Close()
|
|
got, err := io.ReadAll(rf)
|
|
if err != nil {
|
|
t.Fatalf("ReadAll: %v", err)
|
|
}
|
|
if string(got) != "hello bbs" {
|
|
t.Errorf("read = %q, want %q", got, "hello bbs")
|
|
}
|
|
}
|
|
|
|
func TestE2E_TraversalDenied(t *testing.T) {
|
|
client := startE2E(t)
|
|
// A normalizing client may collapse "/me/../.." to "/" before sending, so
|
|
// the guarantee we assert is the negative one: no escape ever yields a
|
|
// handle onto a system path.
|
|
if _, err := client.Open("/me/../../../../etc/passwd"); err == nil {
|
|
t.Error("opening an escaping path should fail")
|
|
}
|
|
if _, err := client.Open("/etc/passwd"); err == nil {
|
|
t.Error("opening an unknown area should fail")
|
|
}
|
|
}
|