github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/dashboard/app/handler.go (about)

     1  // Copyright 2017 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  	"context"
     9  	"encoding/base64"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"net/http"
    14  	"sort"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/google/syzkaller/pkg/html"
    19  	"google.golang.org/appengine/v2"
    20  	"google.golang.org/appengine/v2/log"
    21  	"google.golang.org/appengine/v2/user"
    22  )
    23  
    24  // This file contains common middleware for UI handlers (auth, html templates, etc).
    25  
    26  type contextHandler func(c context.Context, w http.ResponseWriter, r *http.Request) error
    27  
    28  func handlerWrapper(fn contextHandler) http.Handler {
    29  	return handleContext(handleAuth(fn))
    30  }
    31  
    32  func handleContext(fn contextHandler) http.Handler {
    33  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    34  		c := appengine.NewContext(r)
    35  		c = context.WithValue(c, &currentURLKey, r.URL.RequestURI())
    36  		if !throttleRequest(c, w, r) {
    37  			return
    38  		}
    39  		defer backpressureRobots(c, r)()
    40  		if err := fn(c, w, r); err != nil {
    41  			hdr := commonHeaderRaw(c, r)
    42  			data := &struct {
    43  				Header *uiHeader
    44  				Error  string
    45  			}{
    46  				Header: hdr,
    47  				Error:  err.Error(),
    48  			}
    49  			if err == ErrAccess {
    50  				if hdr.LoginLink != "" {
    51  					http.Redirect(w, r, hdr.LoginLink, http.StatusTemporaryRedirect)
    52  					return
    53  				}
    54  				http.Error(w, "403 Forbidden", http.StatusForbidden)
    55  				return
    56  			}
    57  			var redir *ErrRedirect
    58  			if errors.As(err, &redir) {
    59  				http.Redirect(w, r, redir.Error(), http.StatusFound)
    60  				return
    61  			}
    62  
    63  			status := http.StatusInternalServerError
    64  			logf := log.Errorf
    65  			var clientError *ErrClient
    66  			if errors.As(err, &clientError) {
    67  				// We don't log these as errors because they can be provoked
    68  				// by invalid user requests, so we don't wan't to pollute error log.
    69  				logf = log.Warningf
    70  				status = clientError.HTTPStatus()
    71  			}
    72  			logf(c, "%v", err)
    73  			w.WriteHeader(status)
    74  			if err1 := templates.ExecuteTemplate(w, "error.html", data); err1 != nil {
    75  				combinedError := fmt.Sprintf("got err \"%v\" processing ExecuteTemplate() for err \"%v\"", err1, err)
    76  				http.Error(w, combinedError, http.StatusInternalServerError)
    77  			}
    78  		}
    79  	})
    80  }
    81  
    82  func isRobot(r *http.Request) bool {
    83  	userAgent := strings.ToLower(strings.Join(r.Header["User-Agent"], " "))
    84  	if strings.HasPrefix(userAgent, "curl") ||
    85  		strings.HasPrefix(userAgent, "wget") {
    86  		return true
    87  	}
    88  	return false
    89  }
    90  
    91  // We don't count the request round trip time here.
    92  // Actual delay will be the minDelay + requestRoundTripTime.
    93  func backpressureRobots(c context.Context, r *http.Request) func() {
    94  	if !isRobot(r) {
    95  		return func() {}
    96  	}
    97  	cfg := getConfig(c).Throttle
    98  	if cfg.Empty() {
    99  		return func() {}
   100  	}
   101  	minDelay := cfg.Window / time.Duration(cfg.Limit)
   102  	delayUntil := time.Now().Add(minDelay)
   103  	return func() {
   104  		select {
   105  		case <-c.Done():
   106  		case <-time.After(time.Until(delayUntil)):
   107  		}
   108  	}
   109  }
   110  
   111  func throttleRequest(c context.Context, w http.ResponseWriter, r *http.Request) bool {
   112  	// AppEngine removes all App Engine-specific headers, which include
   113  	// X-Appengine-User-IP and X-Forwarded-For.
   114  	// https://cloud.google.com/appengine/docs/standard/reference/request-headers?tab=python#removed_headers
   115  	ip := r.Header.Get("X-Appengine-User-IP")
   116  	if ip == "" {
   117  		ip = r.Header.Get("X-Forwarded-For")
   118  		ip, _, _ = strings.Cut(ip, ",") // X-Forwarded-For is a comma-delimited list.
   119  		ip = strings.TrimSpace(ip)
   120  	}
   121  	cron := r.Header.Get("X-Appengine-Cron") != ""
   122  	if ip == "" || cron {
   123  		log.Infof(c, "cannot throttle request from %q, cron %t", ip, cron)
   124  		return true
   125  	}
   126  	accept, err := ThrottleRequest(c, ip)
   127  	if errors.Is(err, ErrThrottleTooManyRetries) {
   128  		// We get these at peak QPS anyway, it's not an error.
   129  		log.Warningf(c, "failed to throttle: %v", err)
   130  	} else if err != nil {
   131  		log.Errorf(c, "failed to throttle: %v", err)
   132  	}
   133  	log.Infof(c, "throttling for %q: %t", ip, accept)
   134  	if !accept {
   135  		http.Error(w, throttlingErrorMessage(c), http.StatusTooManyRequests)
   136  		return false
   137  	}
   138  	return true
   139  }
   140  
   141  func throttlingErrorMessage(c context.Context) string {
   142  	ret := fmt.Sprintf("429 Too Many Requests\nAllowed rate is %d requests per %d seconds.",
   143  		getConfig(c).Throttle.Limit, int(getConfig(c).Throttle.Window.Seconds()))
   144  	email := getConfig(c).ContactEmail
   145  	if email == "" {
   146  		return ret
   147  	}
   148  	return fmt.Sprintf("%s\nPlease contact us at %s if you need access to our data.", ret, email)
   149  }
   150  
   151  var currentURLKey = "the URL of the HTTP request in context"
   152  
   153  func getCurrentURL(c context.Context) string {
   154  	val, ok := c.Value(&currentURLKey).(string)
   155  	if ok {
   156  		return val
   157  	}
   158  	return ""
   159  }
   160  
   161  type (
   162  	ErrClient   struct{ error }
   163  	ErrRedirect struct{ error }
   164  )
   165  
   166  var ErrClientNotFound = &ErrClient{errors.New("resource not found")}
   167  var ErrClientBadRequest = &ErrClient{errors.New("bad request")}
   168  
   169  func (ce *ErrClient) HTTPStatus() int {
   170  	switch ce {
   171  	case ErrClientNotFound:
   172  		return http.StatusNotFound
   173  	case ErrClientBadRequest:
   174  		return http.StatusBadRequest
   175  	}
   176  	return http.StatusInternalServerError
   177  }
   178  
   179  func handleAuth(fn contextHandler) contextHandler {
   180  	return func(c context.Context, w http.ResponseWriter, r *http.Request) error {
   181  		if err := checkAccessLevel(c, r, getConfig(c).AccessLevel); err != nil {
   182  			return err
   183  		}
   184  		return fn(c, w, r)
   185  	}
   186  }
   187  
   188  func serveTemplate(w http.ResponseWriter, name string, data interface{}) error {
   189  	buf := new(bytes.Buffer)
   190  	if err := templates.ExecuteTemplate(buf, name, data); err != nil {
   191  		return err
   192  	}
   193  	w.Write(buf.Bytes())
   194  	return nil
   195  }
   196  
   197  type uiHeader struct {
   198  	Admin               bool
   199  	URLPath             string
   200  	LoginLink           string
   201  	AnalyticsTrackingID string
   202  	Subpage             string
   203  	Namespace           string
   204  	ContactEmail        string
   205  	BugCounts           *CachedBugStats
   206  	MissingBackports    int
   207  	Namespaces          []uiNamespace
   208  	ShowSubsystems      bool
   209  }
   210  
   211  type uiNamespace struct {
   212  	Name    string
   213  	Caption string
   214  }
   215  
   216  type cookieData struct {
   217  	Namespace string `json:"namespace"`
   218  }
   219  
   220  func commonHeaderRaw(c context.Context, r *http.Request) *uiHeader {
   221  	h := &uiHeader{
   222  		Admin:               accessLevel(c, r) == AccessAdmin,
   223  		URLPath:             r.URL.Path,
   224  		AnalyticsTrackingID: getConfig(c).AnalyticsTrackingID,
   225  		ContactEmail:        getConfig(c).ContactEmail,
   226  	}
   227  	if user.Current(c) == nil {
   228  		h.LoginLink, _ = user.LoginURL(c, r.URL.String())
   229  	}
   230  	return h
   231  }
   232  
   233  func commonHeader(c context.Context, r *http.Request, w http.ResponseWriter, ns string) (*uiHeader, error) {
   234  	accessLevel := accessLevel(c, r)
   235  	if ns == "" {
   236  		ns = strings.ToLower(r.URL.Path)
   237  		if ns != "" && ns[0] == '/' {
   238  			ns = ns[1:]
   239  		}
   240  		if pos := strings.IndexByte(ns, '/'); pos != -1 {
   241  			ns = ns[:pos]
   242  		}
   243  	}
   244  	h := commonHeaderRaw(c, r)
   245  	const adminPage = "admin"
   246  	isAdminPage := r.URL.Path == "/"+adminPage
   247  	found := false
   248  	for ns1, cfg := range getConfig(c).Namespaces {
   249  		if accessLevel < cfg.AccessLevel {
   250  			if ns1 == ns {
   251  				return nil, ErrAccess
   252  			}
   253  			continue
   254  		}
   255  		if ns1 == ns {
   256  			found = true
   257  		}
   258  		if getNsConfig(c, ns1).Decommissioned {
   259  			continue
   260  		}
   261  		h.Namespaces = append(h.Namespaces, uiNamespace{
   262  			Name:    ns1,
   263  			Caption: cfg.DisplayTitle,
   264  		})
   265  	}
   266  	sort.Slice(h.Namespaces, func(i, j int) bool {
   267  		return h.Namespaces[i].Caption < h.Namespaces[j].Caption
   268  	})
   269  	cookie := decodeCookie(r)
   270  	if !found {
   271  		ns = getConfig(c).DefaultNamespace
   272  		if cfg := getNsConfig(c, cookie.Namespace); cfg != nil && cfg.AccessLevel <= accessLevel {
   273  			ns = cookie.Namespace
   274  		}
   275  		if accessLevel == AccessAdmin {
   276  			ns = adminPage
   277  		}
   278  		if ns != adminPage || !isAdminPage {
   279  			return nil, &ErrRedirect{fmt.Errorf("/%v", ns)}
   280  		}
   281  	}
   282  	if ns != adminPage {
   283  		h.Namespace = ns
   284  		h.ShowSubsystems = getNsConfig(c, ns).Subsystems.Service != nil
   285  		cookie.Namespace = ns
   286  		encodeCookie(w, cookie)
   287  		cached, err := CacheGet(c, r, ns)
   288  		if err != nil {
   289  			return nil, err
   290  		}
   291  		h.BugCounts = &cached.Total
   292  		h.MissingBackports = cached.MissingBackports
   293  	}
   294  	return h, nil
   295  }
   296  
   297  const cookieName = "syzkaller"
   298  
   299  func decodeCookie(r *http.Request) *cookieData {
   300  	cd := new(cookieData)
   301  	cookie, err := r.Cookie(cookieName)
   302  	if err != nil {
   303  		return cd
   304  	}
   305  	decoded, err := base64.StdEncoding.DecodeString(cookie.Value)
   306  	if err != nil {
   307  		return cd
   308  	}
   309  	json.Unmarshal(decoded, cd)
   310  	return cd
   311  }
   312  
   313  func encodeCookie(w http.ResponseWriter, cd *cookieData) {
   314  	data, err := json.Marshal(cd)
   315  	if err != nil {
   316  		return
   317  	}
   318  	cookie := &http.Cookie{
   319  		Name:    cookieName,
   320  		Value:   base64.StdEncoding.EncodeToString(data),
   321  		Expires: time.Now().Add(time.Hour * 24 * 365),
   322  	}
   323  	http.SetCookie(w, cookie)
   324  }
   325  
   326  var templates = html.CreateGlob("*.html")