github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/tools/syz-testbed/html.go (about) 1 // Copyright 2021 syzkaller project authors. All rights reserved. 2 // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. 3 4 package main 5 6 import ( 7 "bytes" 8 "embed" 9 "fmt" 10 "html/template" 11 "log" 12 "net" 13 "net/http" 14 "net/url" 15 "os" 16 "time" 17 18 "github.com/google/syzkaller/pkg/html/pages" 19 "github.com/google/syzkaller/pkg/osutil" 20 "github.com/gorilla/handlers" 21 ) 22 23 func (ctx *TestbedContext) setupHTTPServer() { 24 mux := http.NewServeMux() 25 26 mux.HandleFunc("/", ctx.httpMain) 27 mux.HandleFunc("/graph", ctx.httpGraph) 28 mux.HandleFunc("/favicon.ico", ctx.httpFavicon) 29 30 listener, err := net.Listen("tcp", ctx.Config.HTTP) 31 if err != nil { 32 log.Fatalf("failed to listen on %s", ctx.Config.HTTP) 33 } 34 35 log.Printf("handling HTTP on %s", listener.Addr()) 36 go func() { 37 err := http.Serve(listener, handlers.CompressHandler(mux)) 38 if err != nil { 39 log.Fatalf("failed to listen on %v: %v", ctx.Config.HTTP, err) 40 } 41 }() 42 } 43 44 func (ctx *TestbedContext) httpFavicon(w http.ResponseWriter, r *http.Request) { 45 http.Error(w, "Not Found", http.StatusNotFound) 46 } 47 48 func (ctx *TestbedContext) getCurrentStatView(r *http.Request) (*StatView, error) { 49 views, err := ctx.GetStatViews() 50 if err != nil { 51 return nil, err 52 } 53 if len(views) == 0 { 54 return nil, fmt.Errorf("no stat views available") 55 } 56 viewName := r.FormValue("view") 57 if viewName != "" { 58 var targetView *StatView 59 for _, view := range views { 60 if view.Name == viewName { 61 targetView = &view 62 break 63 } 64 } 65 if targetView == nil { 66 return nil, fmt.Errorf("the requested view is not found") 67 } 68 return targetView, nil 69 } 70 // No specific view is requested. 71 // First try to find the first non-empty one. 72 for _, view := range views { 73 if !view.IsEmpty() { 74 return &view, nil 75 } 76 } 77 return &views[0], nil 78 } 79 80 func (ctx *TestbedContext) httpGraph(w http.ResponseWriter, r *http.Request) { 81 over := r.FormValue("over") 82 83 if ctx.Config.BenchCmp == "" { 84 http.Error(w, "the path to the benchcmp tool is not specified", http.StatusInternalServerError) 85 return 86 } 87 88 targetView, err := ctx.getCurrentStatView(r) 89 if err != nil { 90 http.Error(w, fmt.Sprintf("%s", err), http.StatusInternalServerError) 91 return 92 } 93 94 // TODO: move syz-benchcmp functionality to pkg/ and just import it? 95 dir, err := os.MkdirTemp("", "") 96 if err != nil { 97 http.Error(w, "failed to create temp folder", http.StatusInternalServerError) 98 return 99 } 100 defer os.RemoveAll(dir) 101 102 file, err := osutil.TempFile("") 103 if err != nil { 104 http.Error(w, "failed to create temp file", http.StatusInternalServerError) 105 return 106 } 107 defer os.Remove(file) 108 109 benches, err := targetView.SaveAvgBenches(dir) 110 if err != nil { 111 http.Error(w, "failed to save avg benches", http.StatusInternalServerError) 112 return 113 } 114 115 args := append([]string{"-all", "-over", over, "-out", file}, benches...) 116 if out, err := osutil.RunCmd(time.Hour, "", ctx.Config.BenchCmp, args...); err != nil { 117 http.Error(w, "syz-benchcmp failed\n"+string(out), http.StatusInternalServerError) 118 return 119 } 120 121 data, err := os.ReadFile(file) 122 if err != nil { 123 http.Error(w, "failed to read the temporary file", http.StatusInternalServerError) 124 return 125 } 126 w.Write(data) 127 } 128 129 type uiTable struct { 130 Table *Table 131 ColumnURL func(string) string 132 RowURL func(string) string 133 Extra bool 134 HasFooter bool 135 AlignedBy string 136 } 137 138 const ( 139 HTMLStatsTable = "stats" 140 HTMLBugsTable = "bugs" 141 HTMLBugCountsTable = "bug_counts" 142 HTMLReprosTable = "repros" 143 HTMLCReprosTable = "crepros" 144 HTMLReproAttemptsTable = "repro_attempts" 145 HTMLReproDurationTable = "repro_duration" 146 ) 147 148 type uiTableGenerator = func(urlPrefix string, view StatView, r *http.Request) (*uiTable, error) 149 150 type uiTableType struct { 151 Key string 152 Title string 153 Generator uiTableGenerator 154 } 155 156 type uiStatView struct { 157 Name string 158 TableTypes map[string]uiTableType 159 ActiveTableType string 160 ActiveTable *uiTable 161 GenTableURL func(uiTableType) string 162 } 163 164 type uiMainPage struct { 165 Name string 166 Summary uiTable 167 Views []StatView 168 ActiveView uiStatView 169 } 170 171 func (ctx *TestbedContext) getTableTypes() []uiTableType { 172 allTypeList := []uiTableType{ 173 {HTMLStatsTable, "Statistics", ctx.httpMainStatsTable}, 174 {HTMLBugsTable, "Bugs", ctx.genSimpleTableController((StatView).GenerateBugTable, true)}, 175 {HTMLBugCountsTable, "Bug Counts", ctx.genSimpleTableController((StatView).GenerateBugCountsTable, false)}, 176 {HTMLReprosTable, "Repros", ctx.genSimpleTableController((StatView).GenerateReproSuccessTable, true)}, 177 {HTMLCReprosTable, "C Repros", ctx.genSimpleTableController((StatView).GenerateCReproSuccessTable, true)}, 178 {HTMLReproAttemptsTable, "All Repros", ctx.genSimpleTableController((StatView).GenerateReproAttemptsTable, false)}, 179 {HTMLReproDurationTable, "Duration", ctx.genSimpleTableController((StatView).GenerateReproDurationTable, true)}, 180 } 181 typeList := []uiTableType{} 182 for _, t := range allTypeList { 183 if ctx.Target.SupportsHTMLView(t.Key) { 184 typeList = append(typeList, t) 185 } 186 } 187 return typeList 188 } 189 190 func (ctx *TestbedContext) genSimpleTableController(method func(view StatView) (*Table, error), 191 hasFooter bool) uiTableGenerator { 192 return func(urlPrefix string, view StatView, r *http.Request) (*uiTable, error) { 193 table, err := method(view) 194 if err != nil { 195 return nil, fmt.Errorf("table generation failed: %w", err) 196 } 197 return &uiTable{ 198 Table: table, 199 HasFooter: hasFooter, 200 }, nil 201 } 202 } 203 204 func (ctx *TestbedContext) httpMainStatsTable(urlPrefix string, view StatView, r *http.Request) (*uiTable, error) { 205 alignBy := r.FormValue("align") 206 if alignBy == "" { 207 alignBy = "fuzzing" 208 } 209 table, err := view.AlignedStatsTable(alignBy) 210 if err != nil { 211 return nil, fmt.Errorf("stat table generation failed: %w", err) 212 } 213 baseColumn := r.FormValue("base_column") 214 if baseColumn != "" { 215 err := table.SetRelativeValues(baseColumn) 216 if err != nil { 217 log.Printf("failed to execute SetRelativeValues: %s", err) 218 } 219 } 220 221 return &uiTable{ 222 Table: table, 223 Extra: baseColumn != "", 224 ColumnURL: func(column string) string { 225 if column == baseColumn { 226 return "" 227 } 228 v := url.Values{} 229 v.Set("base_column", column) 230 v.Set("align", alignBy) 231 return urlPrefix + v.Encode() 232 }, 233 RowURL: func(row string) string { 234 if row == alignBy { 235 return "" 236 } 237 v := url.Values{} 238 v.Set("base_column", baseColumn) 239 v.Set("align", row) 240 return urlPrefix + v.Encode() 241 }, 242 AlignedBy: alignBy, 243 }, nil 244 } 245 246 func (ctx *TestbedContext) httpMain(w http.ResponseWriter, r *http.Request) { 247 activeView, err := ctx.getCurrentStatView(r) 248 if err != nil { 249 http.Error(w, fmt.Sprintf("%s", err), http.StatusInternalServerError) 250 return 251 } 252 views, err := ctx.GetStatViews() 253 if err != nil { 254 http.Error(w, fmt.Sprintf("%s", err), http.StatusInternalServerError) 255 return 256 } 257 uiView := uiStatView{Name: activeView.Name} 258 tableTypes := ctx.getTableTypes() 259 if len(tableTypes) == 0 { 260 http.Error(w, "No tables are available", http.StatusInternalServerError) 261 return 262 } 263 uiView.TableTypes = map[string]uiTableType{} 264 for _, table := range tableTypes { 265 uiView.TableTypes[table.Key] = table 266 } 267 uiView.ActiveTableType = r.FormValue("table") 268 if uiView.ActiveTableType == "" { 269 uiView.ActiveTableType = tableTypes[0].Key 270 } 271 tableType, found := uiView.TableTypes[uiView.ActiveTableType] 272 if !found { 273 http.Error(w, fmt.Sprintf("%s", err), http.StatusInternalServerError) 274 return 275 } 276 uiView.GenTableURL = func(t uiTableType) string { 277 v := url.Values{} 278 v.Set("view", activeView.Name) 279 v.Set("table", t.Key) 280 return "/?" + v.Encode() 281 } 282 uiView.ActiveTable, err = tableType.Generator(uiView.GenTableURL(tableType)+"&", *activeView, r) 283 if err != nil { 284 http.Error(w, fmt.Sprintf("%s", err), http.StatusInternalServerError) 285 return 286 } 287 data := &uiMainPage{ 288 Name: ctx.Config.Name, 289 Summary: uiTable{Table: ctx.TestbedStatsTable()}, 290 Views: views, 291 ActiveView: uiView, 292 } 293 294 executeTemplate(w, mainTemplate, "testbed.html", data) 295 } 296 297 func executeTemplate(w http.ResponseWriter, templ *template.Template, name string, data interface{}) { 298 buf := new(bytes.Buffer) 299 if err := templ.ExecuteTemplate(buf, name, data); err != nil { 300 log.Printf("failed to execute template: %v", err) 301 http.Error(w, fmt.Sprintf("failed to execute template: %v", err), http.StatusInternalServerError) 302 return 303 } 304 w.Write(buf.Bytes()) 305 } 306 307 //go:embed templates 308 var testbedTemplates embed.FS 309 var mainTemplate = pages.CreateFromFS(testbedTemplates, "templates/*.html")