1package imapserver
2
3import (
4 "encoding/base64"
5 "errors"
6 "fmt"
7 "io"
8 "net"
9 "os"
10 "path/filepath"
11 "testing"
12 "time"
13
14 "github.com/mjl-/mox/imapclient"
15 "github.com/mjl-/mox/mlog"
16 "github.com/mjl-/mox/mox-"
17 "github.com/mjl-/mox/store"
18)
19
20// Fuzz the server. For each fuzz string, we set up servers in various connection states, and write the string as command.
21func FuzzServer(f *testing.F) {
22 seed := []string{
23 fmt.Sprintf("authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000testtest"))),
24 "*",
25 "capability",
26 "noop",
27 "logout",
28 "select inbox",
29 "examine inbox",
30 "unselect",
31 "close",
32 "expunge",
33 "subscribe inbox",
34 "unsubscribe inbox",
35 `lsub "" "*"`,
36 `list "" ""`,
37 `namespace`,
38 "enable utf8=accept",
39 "create inbox",
40 "create tmpbox",
41 "rename tmpbox ntmpbox",
42 "delete ntmpbox",
43 "status inbox (uidnext messages uidvalidity deleted size unseen recent)",
44 "append inbox (\\seen) {2+}\r\nhi",
45 "fetch 1 all",
46 "fetch 1 body",
47 "fetch 1 (bodystructure)",
48 `store 1 flags (\seen \answered)`,
49 `store 1 +flags ($junk)`,
50 `store 1 -flags ($junk)`,
51 "noop",
52 "copy 1Trash",
53 "copy 1 Trash",
54 "move 1 Trash",
55 "search 1 all",
56 }
57 for _, cmd := range seed {
58 const tag = "x "
59 f.Add(tag + cmd)
60 }
61
62 log := mlog.New("imapserver", nil)
63 mox.ConfigStaticPath = filepath.FromSlash("../testdata/imapserverfuzz/mox.conf")
64 mox.MustLoadConfig(true, false)
65 dataDir := mox.ConfigDirPath(mox.Conf.Static.DataDir)
66 os.RemoveAll(dataDir)
67 acc, err := store.OpenAccount(log, "mjl", false)
68 if err != nil {
69 f.Fatalf("open account: %v", err)
70 }
71 defer func() {
72 acc.Close()
73 acc.CheckClosed()
74 }()
75 err = acc.SetPassword(log, password0)
76 if err != nil {
77 f.Fatalf("set password: %v", err)
78 }
79 defer store.Switchboard()()
80
81 comm := store.RegisterComm(acc)
82 defer comm.Unregister()
83
84 var cid int64 = 1
85
86 var fl *os.File
87 if false {
88 fl, err = os.OpenFile("fuzz.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
89 if err != nil {
90 f.Fatalf("fuzz log")
91 }
92 defer fl.Close()
93 }
94 flog := func(err error, msg string) {
95 if fl != nil && err != nil {
96 fmt.Fprintf(fl, "%s: %v\n", msg, err)
97 }
98 }
99
100 f.Fuzz(func(t *testing.T, s string) {
101 run := func(cmds []string) {
102 limitersInit() // Reset rate limiters.
103 serverConn, clientConn := net.Pipe()
104 defer serverConn.Close()
105
106 go func() {
107 defer func() {
108 x := recover()
109 // Protocol can become botched, when fuzzer sends literals.
110 if x == nil {
111 return
112 }
113 err, ok := x.(error)
114 if !ok || (!errors.Is(err, os.ErrDeadlineExceeded) && !errors.Is(err, io.EOF)) {
115 panic(x)
116 }
117 }()
118
119 defer clientConn.Close()
120
121 err := clientConn.SetDeadline(time.Now().Add(time.Second))
122 flog(err, "set client deadline")
123 client, _ := imapclient.New(mox.Cid(), clientConn, true)
124
125 for _, cmd := range cmds {
126 client.Commandf("", "%s", cmd)
127 client.Response()
128 }
129 client.Commandf("", "%s", s)
130 client.Response()
131 }()
132
133 err = serverConn.SetDeadline(time.Now().Add(time.Second))
134 flog(err, "set server deadline")
135 serve("test", cid, nil, serverConn, false, true, false, "")
136 cid++
137 }
138
139 // Each command brings the connection state one step further. We try the fuzzing
140 // input for each state.
141 run([]string{})
142 run([]string{`login mjl@mox.example "` + password0 + `"`})
143 run([]string{`login mjl@mox.example "` + password0 + `"`, "select inbox"})
144 xappend := fmt.Sprintf("append inbox () {%d+}\r\n%s", len(exampleMsg), exampleMsg)
145 run([]string{`login mjl@mox.example "` + password0 + `"`, "select inbox", xappend})
146 })
147}
148