github.com/haalcala/mattermost-server-change-repo/v5@v5.33.2/app/webhub_fuzz.go (about) 1 // +build gofuzz 2 3 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 4 // See LICENSE.txt for license information. 5 package app 6 7 import ( 8 "io/ioutil" 9 "math/rand" 10 "net" 11 "net/http" 12 "net/http/httptest" 13 "os" 14 "strconv" 15 "sync" 16 "testing" 17 "time" 18 19 "github.com/gorilla/websocket" 20 goi18n "github.com/mattermost/go-i18n/i18n" 21 22 "github.com/mattermost/mattermost-server/v5/model" 23 "github.com/mattermost/mattermost-server/v5/testlib" 24 ) 25 26 // This is a file used to fuzz test the web_hub code. 27 // It performs a high-level fuzzing of the web_hub by spawning a hub 28 // and creating connections to it with a fixed concurrency. 29 // 30 // During the fuzz test, we create the server just once, and we send 31 // the random byte slice through a channel and perform some actions depending 32 // on the random data. 33 // The actions are decided in the getActionData function which decides 34 // which user, team, channel should the message go to and some other stuff too. 35 // 36 // Since this requires help of the testing library, we have to duplicate some code 37 // over here because go-fuzz cannot take code from _test.go files. It won't affect 38 // the main build because it's behind a build tag. 39 // 40 // To run this: 41 // 1. go get -u github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build 42 // 2. mv app/helper_test.go app/helper.go 43 // (Also reduce the number of push notification workers to 1 to debug stack traces easily.) 44 // 3. go-fuzz-build github.com/mattermost/mattermost-server/v5/app 45 // 4. Generate a corpus dir. It's just a directory with files containing random data 46 // for go-fuzz to use as an initial seed. Use the generateInitialCorpus function for that. 47 // 5. go-fuzz -bin=app-fuzz.zip -workdir=./workdir 48 var mainHelper *testlib.MainHelper 49 50 func init() { 51 testing.Init() 52 var options = testlib.HelperOptions{ 53 EnableStore: true, 54 EnableResources: true, 55 } 56 57 mainHelper = testlib.NewMainHelperWithOptions(&options) 58 } 59 60 func dummyWebsocketHandler() http.HandlerFunc { 61 return func(w http.ResponseWriter, req *http.Request) { 62 upgrader := &websocket.Upgrader{ 63 ReadBufferSize: 1024, 64 WriteBufferSize: 1024, 65 } 66 conn, err := upgrader.Upgrade(w, req, nil) 67 for err == nil { 68 _, _, err = conn.ReadMessage() 69 } 70 if _, ok := err.(*websocket.CloseError); !ok { 71 panic(err) 72 } 73 } 74 } 75 76 func registerDummyWebConn(a *App, addr net.Addr, userID string) *WebConn { 77 session, appErr := a.CreateSession(&model.Session{ 78 UserId: userID, 79 }) 80 if appErr != nil { 81 panic(appErr) 82 } 83 84 d := websocket.Dialer{} 85 c, _, err := d.Dial("ws://"+addr.String()+"/ws", nil) 86 if err != nil { 87 panic(err) 88 } 89 90 wc := a.NewWebConn(c, *session, goi18n.IdentityTfunc(), "en") 91 a.HubRegister(wc) 92 go wc.Pump() 93 return wc 94 } 95 96 type actionData struct { 97 event string 98 createUserID string 99 selectChannelID string 100 selectTeamID string 101 invalidateConnUserID string 102 updateConnUserID string 103 attachment map[string]interface{} 104 } 105 106 func getActionData(data []byte, userIDs, teamIDs, channelIDs []string) *actionData { 107 // Some sample events 108 events := []string{ 109 model.WEBSOCKET_EVENT_CHANNEL_CREATED, 110 model.WEBSOCKET_EVENT_CHANNEL_DELETED, 111 model.WEBSOCKET_EVENT_USER_ADDED, 112 model.WEBSOCKET_EVENT_USER_UPDATED, 113 model.WEBSOCKET_EVENT_STATUS_CHANGE, 114 model.WEBSOCKET_EVENT_HELLO, 115 model.WEBSOCKET_AUTHENTICATION_CHALLENGE, 116 model.WEBSOCKET_EVENT_REACTION_ADDED, 117 model.WEBSOCKET_EVENT_REACTION_REMOVED, 118 model.WEBSOCKET_EVENT_RESPONSE, 119 } 120 // We need atleast 10 bytes to get all the data we need 121 if len(data) < 10 { 122 return nil 123 } 124 input := &actionData{} 125 // Assign userID, channelID, teamID randomly from respective byte indices 126 input.createUserID = userIDs[int(data[0])%len(userIDs)] 127 input.selectChannelID = channelIDs[int(data[1])%len(channelIDs)] 128 input.selectTeamID = teamIDs[int(data[2])%len(teamIDs)] 129 input.invalidateConnUserID = userIDs[int(data[3])%len(userIDs)] 130 input.updateConnUserID = userIDs[int(data[4])%len(userIDs)] 131 input.event = events[int(data[5])%len(events)] 132 data = data[6:] 133 input.attachment = make(map[string]interface{}) 134 for len(data) >= 4 { // 2 bytes key, 2 bytes value 135 k := data[:2] 136 v := data[2:4] 137 input.attachment[string(k)] = v 138 data = data[4:] 139 } 140 141 return input 142 } 143 144 var startServerOnce sync.Once 145 var dataChan chan []byte 146 var resChan = make(chan int, 4) // buffer of 4 to keep reading results. 147 148 func Fuzz(data []byte) int { 149 // We don't want to close anything down as the fuzzer will keep on running forever. 150 startServerOnce.Do(func() { 151 t := &testing.T{} 152 th := Setup(t).InitBasic() 153 154 s := httptest.NewServer(dummyWebsocketHandler()) 155 156 th.App.HubStart() 157 158 u1 := th.CreateUser() 159 u2 := th.CreateUser() 160 u3 := th.CreateUser() 161 162 t1 := th.CreateTeam() 163 t2 := th.CreateTeam() 164 165 ch1 := th.CreateDmChannel(u1) 166 ch2 := th.CreateChannel(t1) 167 ch3 := th.CreateChannel(t2) 168 169 th.LinkUserToTeam(u1, t1) 170 th.LinkUserToTeam(u1, t2) 171 th.LinkUserToTeam(u2, t1) 172 th.LinkUserToTeam(u2, t2) 173 th.LinkUserToTeam(u3, t1) 174 th.LinkUserToTeam(u3, t2) 175 176 th.AddUserToChannel(u1, ch2) 177 th.AddUserToChannel(u2, ch2) 178 th.AddUserToChannel(u3, ch2) 179 th.AddUserToChannel(u1, ch3) 180 th.AddUserToChannel(u2, ch3) 181 th.AddUserToChannel(u3, ch3) 182 183 sema := make(chan struct{}, 4) // A counting semaphore with concurrency of 4. 184 dataChan = make(chan []byte) 185 186 go func() { 187 for { 188 // get data 189 data, ok := <-dataChan 190 if !ok { 191 return 192 } 193 // acquire semaphore 194 sema <- struct{}{} 195 go func(data []byte) { 196 defer func() { 197 // release semaphore 198 <-sema 199 }() 200 var returnCode int 201 defer func() { 202 resChan <- returnCode 203 }() 204 // assign data randomly 205 // 3 users, 2 teams, 3 channels 206 input := getActionData(data, 207 []string{u1.Id, u2.Id, u3.Id, ""}, 208 []string{t1.Id, t2.Id, ""}, 209 []string{ch1.Id, ch2.Id, ""}) 210 if input == nil { 211 returnCode = 0 212 return 213 } 214 // We get the input from the random data. 215 // Now we perform some actions based on that. 216 217 conn := registerDummyWebConn(th.App, s.Listener.Addr(), input.createUserID) 218 defer func() { 219 conn.Close() 220 // A sleep of 2 seconds to allow other connections 221 // from the same user to be created, before unregistering them. 222 // This hits some additional code paths. 223 go func() { 224 time.Sleep(2 * time.Second) 225 th.App.HubUnregister(conn) 226 }() 227 }() 228 229 msg := model.NewWebSocketEvent(input.event, 230 input.selectTeamID, 231 input.selectChannelID, 232 input.createUserID, nil) 233 for k, v := range input.attachment { 234 msg.Add(k, v) 235 } 236 th.App.Publish(msg) 237 238 th.App.InvalidateWebConnSessionCacheForUser(input.invalidateConnUserID) 239 240 sessions, err := th.App.GetSessions(input.updateConnUserID) 241 if err != nil { 242 panic(err) 243 } 244 if len(sessions) > 0 { 245 th.App.UpdateWebConnUserActivity(*sessions[0], model.GetMillis()) 246 } 247 returnCode = 1 248 }(data) 249 } 250 }() 251 }) 252 253 // send data to dataChan 254 dataChan <- data 255 256 // get data from res chan 257 result := <-resChan 258 return result 259 } 260 261 // generateInitialCorpus generates the corpus for go-fuzz. 262 // Place this function in any main.go file and run it. 263 // Use the generated directory as the corpus. 264 func generateInitialCorpus() error { 265 err := os.MkdirAll("workdir/corpus", 0755) 266 if err != nil { 267 return err 268 } 269 for i := 0; i < 100; i++ { 270 data := make([]byte, 25) 271 _, err = rand.Read(data) 272 if err != nil { 273 return err 274 } 275 err = ioutil.WriteFile("./workdir/corpus"+strconv.Itoa(i), data, 0644) 276 if err != nil { 277 return err 278 } 279 } 280 return nil 281 }