fs_test.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. package main
  2. // 真实挂载测试:需要 /dev/fuse、fusermount3,以及 /etc/fuse.conf 中的 user_allow_other
  3. import (
  4. "bufio"
  5. "encoding/json"
  6. "errors"
  7. "net"
  8. "os"
  9. "path/filepath"
  10. "sort"
  11. "testing"
  12. "time"
  13. )
  14. func needFuse(t *testing.T) {
  15. if _, err := os.Stat("/dev/fuse"); err != nil {
  16. t.Skip("没有 /dev/fuse")
  17. }
  18. }
  19. func names(t *testing.T, dir string) []string {
  20. ents, err := os.ReadDir(dir)
  21. if err != nil {
  22. t.Fatalf("读取 %s:%v", dir, err)
  23. }
  24. var out []string
  25. for _, e := range ents {
  26. out = append(out, e.Name())
  27. }
  28. sort.Strings(out)
  29. return out
  30. }
  31. func TestMountedFilesystem(t *testing.T) {
  32. needFuse(t)
  33. base, roots := tempRoots(t)
  34. src := filepath.Join(roots[0], "data", "docs")
  35. outside := filepath.Join(base, "outside")
  36. os.MkdirAll(outside, 0o755)
  37. os.WriteFile(filepath.Join(outside, "b.txt"), []byte("机密"), 0o600)
  38. os.WriteFile(filepath.Join(src, "a.txt"), []byte("资料内容"), 0o600)
  39. os.WriteFile(filepath.Join(src, ".env"), []byte("SECRET=1"), 0o600)
  40. os.MkdirAll(filepath.Join(src, "sub", ".git"), 0o700)
  41. os.WriteFile(filepath.Join(src, "sub", ".git", "config"), []byte("x"), 0o600)
  42. os.WriteFile(filepath.Join(src, "sub", "b.txt"), []byte("子目录"), 0o600)
  43. os.Symlink("a.txt", filepath.Join(src, "link"))
  44. mntDir := filepath.Join(base, "kbmnt")
  45. os.MkdirAll(mntDir, 0o755)
  46. m := newManager(realSystem{}, roots, mntDir, filepath.Join(base, "state.json"))
  47. defer m.unmountAll()
  48. st, err := m.sync([]mountSpec{{ID: "kb_t1", Kind: "local", Local: &localSource{Path: src}}}, true)
  49. if err != nil || len(st) != 1 || st[0].State != "ok" {
  50. t.Fatalf("挂载:%+v %v", st, err)
  51. }
  52. mp := filepath.Join(mntDir, "kb_t1")
  53. // 隐藏文件不出现、按路径也访问不到
  54. if got := names(t, mp); len(got) != 3 || got[0] != "a.txt" || got[1] != "link" || got[2] != "sub" {
  55. t.Errorf("目录列表:%v", got)
  56. }
  57. if got := names(t, filepath.Join(mp, "sub")); len(got) != 1 {
  58. t.Errorf("子目录列表:%v", got)
  59. }
  60. for _, p := range []string{".env", "sub/.git", "sub/.git/config"} {
  61. if _, err := os.Stat(filepath.Join(mp, p)); !errors.Is(err, os.ErrNotExist) {
  62. t.Errorf("%s 应该不存在:%v", p, err)
  63. }
  64. }
  65. // 读取、权限、符号链接
  66. if b, err := os.ReadFile(filepath.Join(mp, "a.txt")); err != nil || string(b) != "资料内容" {
  67. t.Errorf("读取:%q %v", b, err)
  68. }
  69. if b, err := os.ReadFile(filepath.Join(mp, "link")); err != nil || string(b) != "资料内容" {
  70. t.Errorf("相对符号链接:%q %v", b, err)
  71. }
  72. fi, _ := os.Stat(filepath.Join(mp, "a.txt"))
  73. if fi.Mode().Perm() != 0o444 {
  74. t.Errorf("文件权限应为 0444:%v", fi.Mode())
  75. }
  76. di, _ := os.Stat(filepath.Join(mp, "sub"))
  77. if di.Mode().Perm() != 0o555 {
  78. t.Errorf("目录权限应为 0555:%v", di.Mode())
  79. }
  80. // 只读
  81. if err := os.WriteFile(filepath.Join(mp, "new.txt"), []byte("x"), 0o644); err == nil {
  82. t.Error("应该不能写入")
  83. }
  84. if f, err := os.OpenFile(filepath.Join(mp, "a.txt"), os.O_WRONLY, 0); err == nil {
  85. f.Close()
  86. t.Error("应该不能以写方式打开")
  87. }
  88. // 访问过程中目录被替换为指向外部的符号链接:不会读到外部内容
  89. if _, err := os.Stat(filepath.Join(mp, "sub")); err != nil {
  90. t.Fatal(err)
  91. }
  92. os.Rename(filepath.Join(src, "sub"), filepath.Join(src, "sub.bak"))
  93. os.Symlink(outside, filepath.Join(src, "sub"))
  94. if b, err := os.ReadFile(filepath.Join(mp, "sub", "b.txt")); err == nil {
  95. t.Errorf("不应读到源目录以外的内容:%q", b)
  96. }
  97. os.Remove(filepath.Join(src, "sub"))
  98. os.Rename(filepath.Join(src, "sub.bak"), filepath.Join(src, "sub"))
  99. // 不长期持有源目录的句柄(否则设备无法卸载)
  100. fds, _ := os.ReadDir("/proc/self/fd")
  101. for _, fd := range fds {
  102. if target, _ := os.Readlink(filepath.Join("/proc/self/fd", fd.Name())); target != "" && within(target, src) {
  103. t.Errorf("仍打开着源目录中的 %s", target)
  104. }
  105. }
  106. // 原路径上换成了另一个目录(例如设备拔出后露出挂载点下面的目录):重新检查之前不提供其中的内容
  107. os.Rename(src, src+".orig")
  108. os.MkdirAll(src, 0o755)
  109. os.WriteFile(filepath.Join(src, "other.txt"), []byte("挂载点下面的文件"), 0o644)
  110. time.Sleep(1100 * time.Millisecond)
  111. if b, err := os.ReadFile(filepath.Join(mp, "other.txt")); err == nil {
  112. t.Errorf("不应提供另一个目录的内容:%q", b)
  113. }
  114. os.RemoveAll(src)
  115. os.Rename(src+".orig", src)
  116. m.refreshAll()
  117. time.Sleep(1100 * time.Millisecond)
  118. // 源目录不存在:状态变为 path_missing,读取返回错误;恢复后自动可用
  119. os.Rename(src, src+".moved")
  120. m.refreshAll()
  121. if s := m.status()[0]; s.State != "path_missing" {
  122. t.Errorf("源目录移走后:%+v", s)
  123. }
  124. if _, err := os.ReadDir(mp); err == nil {
  125. t.Error("不可用时读取应该出错")
  126. }
  127. if _, err := os.Stat(mp); err != nil {
  128. t.Errorf("不可用时挂载点根目录照常存在:%v", err)
  129. }
  130. os.Rename(src+".moved", src)
  131. m.refreshAll()
  132. time.Sleep(1100 * time.Millisecond) // 等内核缓存过期
  133. if s := m.status()[0]; s.State != "ok" {
  134. t.Errorf("恢复后:%+v", s)
  135. }
  136. if got := names(t, mp); len(got) != 3 {
  137. t.Errorf("恢复后目录列表:%v", got)
  138. }
  139. // state.json 已保存;同步空列表时卸载并删除挂载点
  140. if b, err := os.ReadFile(filepath.Join(base, "state.json")); err != nil || !contains(string(b), "kb_t1") {
  141. t.Errorf("state.json:%s %v", b, err)
  142. }
  143. if _, err := m.sync(nil, true); err != nil {
  144. t.Fatal(err)
  145. }
  146. if _, err := os.Stat(mp); !errors.Is(err, os.ErrNotExist) {
  147. t.Errorf("卸载后挂载点应删除:%v", err)
  148. }
  149. }
  150. func TestSyncRejectsBadIDs(t *testing.T) {
  151. m := newManager(realSystem{}, allowedRoots, t.TempDir(), filepath.Join(t.TempDir(), "s.json"))
  152. for _, id := range []string{"../etc", "kb_A", "x", "kb_a/b"} {
  153. if _, err := m.sync([]mountSpec{{ID: id, Kind: "local", Local: &localSource{Path: "/mnt/x"}}}, false); err == nil {
  154. t.Errorf("%q 应该被拒绝", id)
  155. }
  156. }
  157. }
  158. func TestServerToken(t *testing.T) {
  159. _, roots := tempRoots(t)
  160. s := &server{token: "secret", mgr: newManager(realSystem{}, roots, t.TempDir(), filepath.Join(t.TempDir(), "s.json")), version: "t"}
  161. call := func(req map[string]any) map[string]any {
  162. a, b := net.Pipe()
  163. go s.serveConn(b)
  164. defer a.Close()
  165. line, _ := json.Marshal(req)
  166. a.Write(append(line, '\n'))
  167. resp, err := bufio.NewReader(a).ReadBytes('\n')
  168. if err != nil {
  169. t.Fatal(err)
  170. }
  171. var out map[string]any
  172. json.Unmarshal(resp, &out)
  173. return out
  174. }
  175. if r := call(map[string]any{"token": "wrong", "op": "ping"}); r["ok"] != false || r["error"] != "密钥不正确" {
  176. t.Errorf("错误的密钥:%v", r)
  177. }
  178. if r := call(map[string]any{"op": "status"}); r["ok"] != false {
  179. t.Errorf("没有密钥:%v", r)
  180. }
  181. if r := call(map[string]any{"token": "secret", "op": "ping"}); r["ok"] != true {
  182. t.Errorf("正确的密钥:%v", r)
  183. }
  184. if r := call(map[string]any{"token": "secret", "op": "check", "path": "/etc"}); r["ok"] != false || r["state"] != "not_allowed" {
  185. t.Errorf("检查范围外的目录:%v", r)
  186. }
  187. }