| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- package main
- // 真实挂载测试:需要 /dev/fuse、fusermount3,以及 /etc/fuse.conf 中的 user_allow_other
- import (
- "bufio"
- "encoding/json"
- "errors"
- "net"
- "os"
- "path/filepath"
- "sort"
- "testing"
- "time"
- )
- func needFuse(t *testing.T) {
- if _, err := os.Stat("/dev/fuse"); err != nil {
- t.Skip("没有 /dev/fuse")
- }
- }
- func names(t *testing.T, dir string) []string {
- ents, err := os.ReadDir(dir)
- if err != nil {
- t.Fatalf("读取 %s:%v", dir, err)
- }
- var out []string
- for _, e := range ents {
- out = append(out, e.Name())
- }
- sort.Strings(out)
- return out
- }
- func TestMountedFilesystem(t *testing.T) {
- needFuse(t)
- base, roots := tempRoots(t)
- src := filepath.Join(roots[0], "data", "docs")
- outside := filepath.Join(base, "outside")
- os.MkdirAll(outside, 0o755)
- os.WriteFile(filepath.Join(outside, "b.txt"), []byte("机密"), 0o600)
- os.WriteFile(filepath.Join(src, "a.txt"), []byte("资料内容"), 0o600)
- os.WriteFile(filepath.Join(src, ".env"), []byte("SECRET=1"), 0o600)
- os.MkdirAll(filepath.Join(src, "sub", ".git"), 0o700)
- os.WriteFile(filepath.Join(src, "sub", ".git", "config"), []byte("x"), 0o600)
- os.WriteFile(filepath.Join(src, "sub", "b.txt"), []byte("子目录"), 0o600)
- os.Symlink("a.txt", filepath.Join(src, "link"))
- mntDir := filepath.Join(base, "kbmnt")
- os.MkdirAll(mntDir, 0o755)
- m := newManager(realSystem{}, roots, mntDir, filepath.Join(base, "state.json"))
- defer m.unmountAll()
- st, err := m.sync([]mountSpec{{ID: "kb_t1", Kind: "local", Local: &localSource{Path: src}}}, true)
- if err != nil || len(st) != 1 || st[0].State != "ok" {
- t.Fatalf("挂载:%+v %v", st, err)
- }
- mp := filepath.Join(mntDir, "kb_t1")
- // 隐藏文件不出现、按路径也访问不到
- if got := names(t, mp); len(got) != 3 || got[0] != "a.txt" || got[1] != "link" || got[2] != "sub" {
- t.Errorf("目录列表:%v", got)
- }
- if got := names(t, filepath.Join(mp, "sub")); len(got) != 1 {
- t.Errorf("子目录列表:%v", got)
- }
- for _, p := range []string{".env", "sub/.git", "sub/.git/config"} {
- if _, err := os.Stat(filepath.Join(mp, p)); !errors.Is(err, os.ErrNotExist) {
- t.Errorf("%s 应该不存在:%v", p, err)
- }
- }
- // 读取、权限、符号链接
- if b, err := os.ReadFile(filepath.Join(mp, "a.txt")); err != nil || string(b) != "资料内容" {
- t.Errorf("读取:%q %v", b, err)
- }
- if b, err := os.ReadFile(filepath.Join(mp, "link")); err != nil || string(b) != "资料内容" {
- t.Errorf("相对符号链接:%q %v", b, err)
- }
- fi, _ := os.Stat(filepath.Join(mp, "a.txt"))
- if fi.Mode().Perm() != 0o444 {
- t.Errorf("文件权限应为 0444:%v", fi.Mode())
- }
- di, _ := os.Stat(filepath.Join(mp, "sub"))
- if di.Mode().Perm() != 0o555 {
- t.Errorf("目录权限应为 0555:%v", di.Mode())
- }
- // 只读
- if err := os.WriteFile(filepath.Join(mp, "new.txt"), []byte("x"), 0o644); err == nil {
- t.Error("应该不能写入")
- }
- if f, err := os.OpenFile(filepath.Join(mp, "a.txt"), os.O_WRONLY, 0); err == nil {
- f.Close()
- t.Error("应该不能以写方式打开")
- }
- // 访问过程中目录被替换为指向外部的符号链接:不会读到外部内容
- if _, err := os.Stat(filepath.Join(mp, "sub")); err != nil {
- t.Fatal(err)
- }
- os.Rename(filepath.Join(src, "sub"), filepath.Join(src, "sub.bak"))
- os.Symlink(outside, filepath.Join(src, "sub"))
- if b, err := os.ReadFile(filepath.Join(mp, "sub", "b.txt")); err == nil {
- t.Errorf("不应读到源目录以外的内容:%q", b)
- }
- os.Remove(filepath.Join(src, "sub"))
- os.Rename(filepath.Join(src, "sub.bak"), filepath.Join(src, "sub"))
- // 不长期持有源目录的句柄(否则设备无法卸载)
- fds, _ := os.ReadDir("/proc/self/fd")
- for _, fd := range fds {
- if target, _ := os.Readlink(filepath.Join("/proc/self/fd", fd.Name())); target != "" && within(target, src) {
- t.Errorf("仍打开着源目录中的 %s", target)
- }
- }
- // 原路径上换成了另一个目录(例如设备拔出后露出挂载点下面的目录):重新检查之前不提供其中的内容
- os.Rename(src, src+".orig")
- os.MkdirAll(src, 0o755)
- os.WriteFile(filepath.Join(src, "other.txt"), []byte("挂载点下面的文件"), 0o644)
- time.Sleep(1100 * time.Millisecond)
- if b, err := os.ReadFile(filepath.Join(mp, "other.txt")); err == nil {
- t.Errorf("不应提供另一个目录的内容:%q", b)
- }
- os.RemoveAll(src)
- os.Rename(src+".orig", src)
- m.refreshAll()
- time.Sleep(1100 * time.Millisecond)
- // 源目录不存在:状态变为 path_missing,读取返回错误;恢复后自动可用
- os.Rename(src, src+".moved")
- m.refreshAll()
- if s := m.status()[0]; s.State != "path_missing" {
- t.Errorf("源目录移走后:%+v", s)
- }
- if _, err := os.ReadDir(mp); err == nil {
- t.Error("不可用时读取应该出错")
- }
- if _, err := os.Stat(mp); err != nil {
- t.Errorf("不可用时挂载点根目录照常存在:%v", err)
- }
- os.Rename(src+".moved", src)
- m.refreshAll()
- time.Sleep(1100 * time.Millisecond) // 等内核缓存过期
- if s := m.status()[0]; s.State != "ok" {
- t.Errorf("恢复后:%+v", s)
- }
- if got := names(t, mp); len(got) != 3 {
- t.Errorf("恢复后目录列表:%v", got)
- }
- // state.json 已保存;同步空列表时卸载并删除挂载点
- if b, err := os.ReadFile(filepath.Join(base, "state.json")); err != nil || !contains(string(b), "kb_t1") {
- t.Errorf("state.json:%s %v", b, err)
- }
- if _, err := m.sync(nil, true); err != nil {
- t.Fatal(err)
- }
- if _, err := os.Stat(mp); !errors.Is(err, os.ErrNotExist) {
- t.Errorf("卸载后挂载点应删除:%v", err)
- }
- }
- func TestSyncRejectsBadIDs(t *testing.T) {
- m := newManager(realSystem{}, allowedRoots, t.TempDir(), filepath.Join(t.TempDir(), "s.json"))
- for _, id := range []string{"../etc", "kb_A", "x", "kb_a/b"} {
- if _, err := m.sync([]mountSpec{{ID: id, Kind: "local", Local: &localSource{Path: "/mnt/x"}}}, false); err == nil {
- t.Errorf("%q 应该被拒绝", id)
- }
- }
- }
- func TestServerToken(t *testing.T) {
- _, roots := tempRoots(t)
- s := &server{token: "secret", mgr: newManager(realSystem{}, roots, t.TempDir(), filepath.Join(t.TempDir(), "s.json")), version: "t"}
- call := func(req map[string]any) map[string]any {
- a, b := net.Pipe()
- go s.serveConn(b)
- defer a.Close()
- line, _ := json.Marshal(req)
- a.Write(append(line, '\n'))
- resp, err := bufio.NewReader(a).ReadBytes('\n')
- if err != nil {
- t.Fatal(err)
- }
- var out map[string]any
- json.Unmarshal(resp, &out)
- return out
- }
- if r := call(map[string]any{"token": "wrong", "op": "ping"}); r["ok"] != false || r["error"] != "密钥不正确" {
- t.Errorf("错误的密钥:%v", r)
- }
- if r := call(map[string]any{"op": "status"}); r["ok"] != false {
- t.Errorf("没有密钥:%v", r)
- }
- if r := call(map[string]any{"token": "secret", "op": "ping"}); r["ok"] != true {
- t.Errorf("正确的密钥:%v", r)
- }
- if r := call(map[string]any{"token": "secret", "op": "check", "path": "/etc"}); r["ok"] != false || r["state"] != "not_allowed" {
- t.Errorf("检查范围外的目录:%v", r)
- }
- }
|