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")