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, ¤tURLKey, 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(¤tURLKey).(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")