302 lines
9.9 KiB
Go
302 lines
9.9 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// fakeStore stands for the object store behind signed links: a PUT keeps the body under its path, a GET answers it.
|
|
// objectlink (mesh-sdk go/objectlink) is the real client of both.
|
|
type fakeStore struct {
|
|
mu sync.Mutex
|
|
objects map[string][]byte
|
|
puts []string
|
|
srv *httptest.Server
|
|
}
|
|
|
|
func newFakeStore(t *testing.T) *fakeStore {
|
|
t.Helper()
|
|
s := &fakeStore{objects: map[string][]byte{}}
|
|
s.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
switch r.Method {
|
|
case http.MethodPut:
|
|
b, _ := io.ReadAll(r.Body)
|
|
s.objects[r.URL.Path] = b
|
|
s.puts = append(s.puts, r.URL.Path)
|
|
case http.MethodGet:
|
|
b, ok := s.objects[r.URL.Path]
|
|
if !ok {
|
|
w.WriteHeader(404)
|
|
return
|
|
}
|
|
w.Write(b)
|
|
}
|
|
}))
|
|
t.Cleanup(s.srv.Close)
|
|
return s
|
|
}
|
|
|
|
// linkVerb is the fake object-store.link: it records each ask and answers a link to the fake store.
|
|
type linkVerb struct {
|
|
store *fakeStore
|
|
asks []string
|
|
err error
|
|
}
|
|
|
|
func (l *linkVerb) ask(bucket, key, method string, seconds int) (string, error) {
|
|
l.asks = append(l.asks, method+" "+bucket+"/"+key)
|
|
if l.err != nil {
|
|
return "", l.err
|
|
}
|
|
return l.store.srv.URL + "/" + bucket + "/" + key + "?sig=1", nil
|
|
}
|
|
|
|
func transferRig(t *testing.T, places ...string) (*rig, *fakeStore, *linkVerb) {
|
|
t.Helper()
|
|
g := testFiles(t)
|
|
g.Refuser.Places = places
|
|
st := newFakeStore(t)
|
|
lv := &linkVerb{store: st}
|
|
g.Link = lv.ask
|
|
return g, st, lv
|
|
}
|
|
|
|
func sum(b []byte) string { h := sha256.Sum256(b); return hex.EncodeToString(h[:]) }
|
|
|
|
// 1597/AC1: the store receives exactly the file's bytes and the answer is {key: SHA-256, size, sha256}.
|
|
func TestUploadSendsTheFileUnderItsHash(t *testing.T) {
|
|
g, st, lv := transferRig(t)
|
|
body := bytes.Repeat([]byte("a document\n"), 1000)
|
|
write(t, g.path("home/ops/doc.pdf"), string(body))
|
|
ans, err := g.Upload(g.path("home/ops/doc.pdf"), "mesh-shanks-brother", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := sum(body)
|
|
if ans.Key != want || ans.SHA256 != want || ans.Size != int64(len(body)) {
|
|
t.Fatalf("answer %+v", ans)
|
|
}
|
|
if !bytes.Equal(st.objects["/mesh-shanks-brother/"+want], body) {
|
|
t.Fatal("the store did not receive exactly the file's bytes")
|
|
}
|
|
if len(lv.asks) != 1 || lv.asks[0] != "put mesh-shanks-brother/"+want {
|
|
t.Fatalf("link asks %v", lv.asks)
|
|
}
|
|
}
|
|
|
|
// 1597/AC2: a key given is the key it is stored under.
|
|
func TestUploadStoresUnderTheKeyGiven(t *testing.T) {
|
|
g, st, _ := transferRig(t)
|
|
write(t, g.path("a.txt"), "hello")
|
|
ans, err := g.Upload(g.path("a.txt"), "b", "scans/a.txt")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ans.Key != "scans/a.txt" || string(st.objects["/b/scans/a.txt"]) != "hello" || ans.SHA256 != sum([]byte("hello")) {
|
|
t.Fatalf("answer %+v, store %v", ans, st.puts)
|
|
}
|
|
}
|
|
|
|
// 1597/AC3: places a read refuses are refused by name, and no link is asked and nothing reaches the store.
|
|
func TestUploadRefusesWhatAReadRefuses(t *testing.T) {
|
|
dir := t.TempDir()
|
|
// A vault directory and a module's own-secret directory are the controller's secret places (MESH_SECRET_PLACES).
|
|
places := []string{filepath.Join(dir, "var/lib/vault"), filepath.Join(dir, "var/lib/brother")}
|
|
g, st, lv := transferRig(t, places...)
|
|
secret := map[string]string{
|
|
"vault": filepath.Join(dir, "var/lib/vault/box"),
|
|
"own-secret": filepath.Join(dir, "var/lib/brother/admin-password.secret"),
|
|
"key file": filepath.Join(dir, "srv/tls/server.key"),
|
|
"ssh key": filepath.Join(dir, "home/ops/.ssh/id_ed25519"),
|
|
"known profile": filepath.Join(dir, "home/ops/.mozilla/firefox/logins.json"),
|
|
}
|
|
for name, p := range secret {
|
|
write(t, p, "SECRET")
|
|
if _, err := g.Upload(p, "b", ""); err == nil || !strings.Contains(err.Error(), "refused by name") {
|
|
t.Errorf("%s: not refused by name: %v", name, err)
|
|
}
|
|
}
|
|
link := filepath.Join(dir, "home/ops/innocent.txt")
|
|
if err := os.Symlink(secret["ssh key"], link); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := g.Upload(link, "b", ""); err == nil || !strings.Contains(err.Error(), "leads to") {
|
|
t.Errorf("a link to a secret: %v", err)
|
|
}
|
|
if len(lv.asks) != 0 || len(st.puts) != 0 {
|
|
t.Fatalf("a link was asked %v or the store was reached %v", lv.asks, st.puts)
|
|
}
|
|
}
|
|
|
|
// 1597/AC4: a file over 100 MiB is refused before a link is asked.
|
|
func TestUploadRefusesOverOneHundredMiB(t *testing.T) {
|
|
g, st, lv := transferRig(t)
|
|
p := g.path("big.bin")
|
|
f, err := os.Create(p)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := f.Truncate(100<<20 + 1); err != nil { // a sparse file: no bytes are written
|
|
t.Fatal(err)
|
|
}
|
|
f.Close()
|
|
if _, err := g.Upload(p, "b", ""); err == nil || !strings.Contains(err.Error(), "over the") {
|
|
t.Fatalf("not refused: %v", err)
|
|
}
|
|
if len(lv.asks) != 0 || len(st.puts) != 0 {
|
|
t.Fatal("a link was asked for a file over the limit")
|
|
}
|
|
}
|
|
|
|
// 1597/AC5: download writes the object to the path; a module-owned path is refused without an override; a hash that
|
|
// does not match leaves no file.
|
|
func TestDownloadWritesTheObject(t *testing.T) {
|
|
g, st, lv := transferRig(t)
|
|
st.objects["/b/k.txt"] = []byte("from the store")
|
|
ans, err := g.Download(by, "b", "k.txt", g.path("out/k.txt"), "")
|
|
if err == nil {
|
|
t.Fatal("the folder does not exist, yet download succeeded")
|
|
}
|
|
if err := os.MkdirAll(g.path("out"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ans, err = g.Download(by, "b", "k.txt", g.path("out/k.txt"), "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if mustRead(t, g.path("out/k.txt")) != "from the store" || ans.SHA256 != sum([]byte("from the store")) || ans.Size != 14 {
|
|
t.Fatalf("answer %+v", ans)
|
|
}
|
|
if len(lv.asks) != 1 || lv.asks[0] != "get b/k.txt" {
|
|
t.Fatalf("asks %v", lv.asks)
|
|
}
|
|
if lines := actLines(t, g); len(lines) != 1 || lines[0]["verb"] != "download" {
|
|
t.Fatalf("act log %v", lines)
|
|
}
|
|
}
|
|
|
|
func TestDownloadRefusesAModuleOwnedPathWithoutOverride(t *testing.T) {
|
|
g, st, lv := transferRig(t)
|
|
st.objects["/b/k"] = []byte("x")
|
|
owned := g.path("etc/brother.conf")
|
|
write(t, owned, "old")
|
|
g.Owned[owned] = "brother (settings)"
|
|
_, err := g.Download(by, "b", "k", owned, "")
|
|
if err == nil || !strings.Contains(err.Error(), "is owned by brother") {
|
|
t.Fatalf("not refused: %v", err)
|
|
}
|
|
if mustRead(t, owned) != "old" || len(lv.asks) != 0 {
|
|
t.Fatal("the owned file changed or a link was asked")
|
|
}
|
|
if _, err := g.Download(Act{Caller: by.Caller, Reason: "a test", Override: "because"}, "b", "k", owned, ""); err != nil {
|
|
t.Fatalf("with an override: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDownloadWithAWrongHashLeavesNoFile(t *testing.T) {
|
|
g, st, _ := transferRig(t)
|
|
st.objects["/b/"+sum([]byte("expected"))] = []byte("something else") // the key says a hash the bytes do not have
|
|
dir := g.path("out")
|
|
os.MkdirAll(dir, 0o755)
|
|
write(t, filepath.Join(dir, "doc"), "old content")
|
|
_, err := g.Download(by, "b", sum([]byte("expected")), filepath.Join(dir, "doc"), "")
|
|
if err == nil || !strings.Contains(err.Error(), "hashes to") {
|
|
t.Fatalf("not refused: %v", err)
|
|
}
|
|
if mustRead(t, filepath.Join(dir, "doc")) != "old content" {
|
|
t.Fatal("the old file was replaced")
|
|
}
|
|
entries, _ := os.ReadDir(dir)
|
|
if len(entries) != 1 {
|
|
t.Fatalf("a temporary file is left: %v", entries)
|
|
}
|
|
// An explicit sha256 is judged too, for a key that is not a hash.
|
|
st.objects["/b/plain"] = []byte("abc")
|
|
if _, err := g.Download(by, "b", "plain", filepath.Join(dir, "plain"), sum([]byte("other"))); err == nil {
|
|
t.Fatal("a wrong sha256 was accepted")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "plain")); err == nil {
|
|
t.Fatal("a file was left for a wrong sha256")
|
|
}
|
|
}
|
|
|
|
// 1597/AC6: upload then download keeps the SHA-256 across the round trip.
|
|
func TestUploadThenDownloadKeepsTheHash(t *testing.T) {
|
|
g, _, _ := transferRig(t)
|
|
body := bytes.Repeat([]byte{0, 1, 2, 3, 250}, 50000)
|
|
write(t, g.path("in.bin"), string(body))
|
|
up, err := g.Upload(g.path("in.bin"), "b", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
down, err := g.Download(by, "b", up.Key, g.path("out.bin"), "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if up.SHA256 != down.SHA256 || sum([]byte(mustRead(t, g.path("out.bin")))) != up.SHA256 {
|
|
t.Fatalf("up %+v down %+v", up, down)
|
|
}
|
|
}
|
|
|
|
// 1597/AC1: the verbs are listed on the seat with their inputs.
|
|
func TestTheSeatListsUploadAndDownload(t *testing.T) {
|
|
g, _, _ := transferRig(t)
|
|
names := map[string]bool{}
|
|
for _, tool := range tools(g.Files) {
|
|
names[tool.Name] = true
|
|
}
|
|
if !names[Seat+".upload"] || !names[Seat+".download"] {
|
|
t.Fatalf("tools %v", names)
|
|
}
|
|
}
|
|
|
|
// 1597/AC9: a transfer slower than its bound is refused naming it, and no object or file is left.
|
|
func TestASlowTransferIsRefusedNamingTheBoundAndKeepsNothing(t *testing.T) {
|
|
g := testFiles(t)
|
|
g.Bound = 150 * time.Millisecond
|
|
release := make(chan struct{})
|
|
slow := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodGet {
|
|
w.Write([]byte("half of the document"))
|
|
w.(http.Flusher).Flush()
|
|
}
|
|
select { // a store that does not finish: nothing is kept of a PUT whose body it never took whole
|
|
case <-release:
|
|
case <-r.Context().Done():
|
|
}
|
|
}))
|
|
t.Cleanup(func() { close(release); slow.Close() })
|
|
g.Link = func(bucket, key, method string, seconds int) (string, error) {
|
|
return slow.URL + "/" + bucket + "/" + key, nil
|
|
}
|
|
write(t, g.path("in.bin"), "a document")
|
|
if _, err := g.Upload(g.path("in.bin"), "b", "k"); err == nil || !strings.Contains(err.Error(), "did not finish within 150ms") ||
|
|
!strings.Contains(err.Error(), "nothing is kept") {
|
|
t.Fatalf("upload error = %v", err)
|
|
}
|
|
if err := os.MkdirAll(g.path("out"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := g.Download(by, "b", "k", g.path("out/doc"), ""); err == nil || !strings.Contains(err.Error(), "did not finish within 150ms") {
|
|
t.Fatalf("download error = %v", err)
|
|
}
|
|
if e, _ := os.ReadDir(g.path("out")); len(e) != 0 {
|
|
t.Errorf("a file is left: %v", e)
|
|
}
|
|
if TransferTime != 25*time.Second {
|
|
t.Errorf("the bound is %s, want 25s", TransferTime)
|
|
}
|
|
}
|