golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/coordinator/internal/legacydash/handler.go (about) 1 // Copyright 2011 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 //go:build linux || darwin 6 7 package legacydash 8 9 import ( 10 "crypto/hmac" 11 "crypto/md5" 12 "encoding/json" 13 "errors" 14 "fmt" 15 "log" 16 "net/http" 17 "strconv" 18 "strings" 19 "unicode/utf8" 20 21 "cloud.google.com/go/datastore" 22 ) 23 24 const ( 25 commitsPerPage = 30 26 builderVersion = 1 // must match x/build/cmd/coordinator/dash.go's value 27 ) 28 29 // resultHandler records a build result. 30 // It reads a JSON-encoded Result value from the request body, 31 // creates a new Result entity, and creates or updates the relevant Commit entity. 32 // If the Log field is not empty, resultHandler creates a new Log entity 33 // and updates the LogHash field before putting the Commit entity. 34 func (h handler) resultHandler(r *http.Request) (interface{}, error) { 35 if r.Method != "POST" { 36 return nil, errBadMethod(r.Method) 37 } 38 39 v, _ := strconv.Atoi(r.FormValue("version")) 40 if v != builderVersion { 41 return nil, fmt.Errorf("rejecting POST from builder; need version %v instead of %v", 42 builderVersion, v) 43 } 44 45 ctx := r.Context() 46 res := new(Result) 47 defer r.Body.Close() 48 if err := json.NewDecoder(r.Body).Decode(res); err != nil { 49 return nil, fmt.Errorf("decoding Body: %v", err) 50 } 51 if err := res.Valid(); err != nil { 52 return nil, fmt.Errorf("validating Result: %v", err) 53 } 54 // store the Log text if supplied 55 if len(res.Log) > 0 { 56 hash, err := h.putLog(ctx, res.Log) 57 if err != nil { 58 return nil, fmt.Errorf("putting Log: %v", err) 59 } 60 res.LogHash = hash 61 } 62 tx := func(tx *datastore.Transaction) error { 63 if _, err := getOrMakePackageInTx(ctx, tx, res.PackagePath); err != nil { 64 return fmt.Errorf("GetPackage: %v", err) 65 } 66 // put Result 67 if _, err := tx.Put(res.Key(), res); err != nil { 68 return fmt.Errorf("putting Result: %v", err) 69 } 70 // add Result to Commit 71 com := &Commit{PackagePath: res.PackagePath, Hash: res.Hash} 72 if err := com.AddResult(tx, res); err != nil { 73 return fmt.Errorf("AddResult: %v", err) 74 } 75 return nil 76 } 77 _, err := h.datastoreCl.RunInTransaction(ctx, tx) 78 return nil, err 79 } 80 81 // logHandler displays log text for a given hash. 82 // It handles paths like "/log/hash". 83 func (h handler) logHandler(w http.ResponseWriter, r *http.Request) { 84 if h.datastoreCl == nil { 85 http.Error(w, "no datastore client", http.StatusNotFound) 86 return 87 } 88 c := r.Context() 89 hash := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:] 90 key := dsKey("Log", hash, nil) 91 l := new(Log) 92 if err := h.datastoreCl.Get(c, key, l); err != nil { 93 if err == datastore.ErrNoSuchEntity { 94 // Fall back to default namespace; 95 // maybe this was on the old dashboard. 96 key.Namespace = "" 97 err = h.datastoreCl.Get(c, key, l) 98 } 99 if err != nil { 100 log.Printf("Error: %v", err) 101 http.Error(w, "Error: "+err.Error(), http.StatusInternalServerError) 102 return 103 } 104 } 105 b, err := l.Text() 106 if err != nil { 107 log.Printf("Error: %v", err) 108 http.Error(w, "Error: "+err.Error(), http.StatusInternalServerError) 109 return 110 } 111 w.Header().Set("Content-type", "text/plain; charset=utf-8") 112 w.Write(b) 113 } 114 115 // clearResultsHandler purge a single build failure from the dashboard. 116 // It currently only supports the main Go repo. 117 func (h handler) clearResultsHandler(r *http.Request) (interface{}, error) { 118 if r.Method != "POST" { 119 return nil, errBadMethod(r.Method) 120 } 121 builder := r.FormValue("builder") 122 hash := r.FormValue("hash") 123 if builder == "" { 124 return nil, errors.New("missing 'builder'") 125 } 126 if hash == "" { 127 return nil, errors.New("missing 'hash'") 128 } 129 130 ctx := r.Context() 131 132 _, err := h.datastoreCl.RunInTransaction(ctx, func(tx *datastore.Transaction) error { 133 c := &Commit{ 134 PackagePath: "", // TODO(adg): support clearing sub-repos 135 Hash: hash, 136 } 137 err := tx.Get(c.Key(), c) 138 err = filterDatastoreError(err) 139 if err == datastore.ErrNoSuchEntity { 140 // Doesn't exist, so no build to clear. 141 return nil 142 } 143 if err != nil { 144 return err 145 } 146 147 r := c.Result(builder, "") 148 if r == nil { 149 // No result, so nothing to clear. 150 return nil 151 } 152 c.RemoveResult(r) 153 _, err = tx.Put(c.Key(), c) 154 if err != nil { 155 return err 156 } 157 return tx.Delete(r.Key()) 158 }) 159 return nil, err 160 } 161 162 type dashHandler func(*http.Request) (interface{}, error) 163 164 type dashResponse struct { 165 Response interface{} 166 Error string 167 } 168 169 // errBadMethod is returned by a dashHandler when 170 // the request has an unsuitable method. 171 type errBadMethod string 172 173 func (e errBadMethod) Error() string { 174 return "bad method: " + string(e) 175 } 176 177 func builderKeyRevoked(builder string) bool { 178 switch builder { 179 case "plan9-amd64-mischief": 180 // Broken and unmaintained for months. 181 // It's polluting the dashboard. 182 return true 183 case "linux-arm-onlinenet": 184 // Requested to be revoked by Dave Cheney. 185 // The machine is in a fail+report loop 186 // and can't be accessed. Revoke it for now. 187 return true 188 } 189 return false 190 } 191 192 // authHandler wraps an http.HandlerFunc with a handler that validates the 193 // supplied key and builder query parameters with the provided key checker. 194 type authHandler struct { 195 kc keyCheck 196 h dashHandler 197 } 198 199 func (a authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 200 { // Block to improve diff readability. Can be unnested later. 201 // Put the URL Query values into r.Form to avoid parsing the 202 // request body when calling r.FormValue. 203 r.Form = r.URL.Query() 204 205 var err error 206 var resp interface{} 207 208 // Validate key query parameter for POST requests only. 209 key := r.FormValue("key") 210 builder := r.FormValue("builder") 211 if r.Method == "POST" && !a.kc.ValidKey(key, builder) { 212 err = fmt.Errorf("invalid key %q for builder %q", key, builder) 213 } 214 215 // Call the original HandlerFunc and return the response. 216 if err == nil { 217 resp, err = a.h(r) 218 } 219 220 // Write JSON response. 221 dashResp := &dashResponse{Response: resp} 222 if err != nil { 223 log.Printf("%v", err) 224 dashResp.Error = err.Error() 225 } 226 w.Header().Set("Content-Type", "application/json") 227 if err = json.NewEncoder(w).Encode(dashResp); err != nil { 228 log.Printf("encoding response: %v", err) 229 } 230 } 231 } 232 233 // validHash reports whether hash looks like a valid git commit hash. 234 func validHash(hash string) bool { 235 // TODO: correctly validate a hash: check that it's exactly 40 236 // lowercase hex digits. But this is what we historically did: 237 return hash != "" 238 } 239 240 type keyCheck struct { 241 // The builder master key. 242 masterKey string 243 } 244 245 func (kc keyCheck) ValidKey(key, builder string) bool { 246 if kc.isMasterKey(key) { 247 return true 248 } 249 if builderKeyRevoked(builder) { 250 return false 251 } 252 return key == kc.builderKey(builder) 253 } 254 255 func (kc keyCheck) isMasterKey(k string) bool { 256 return k == kc.masterKey 257 } 258 259 func (kc keyCheck) builderKey(builder string) string { 260 h := hmac.New(md5.New, []byte(kc.masterKey)) 261 h.Write([]byte(builder)) 262 return fmt.Sprintf("%x", h.Sum(nil)) 263 } 264 265 // limitStringLength essentially does return s[:max], 266 // but it ensures that we dot not split UTF-8 rune in half. 267 // Otherwise appengine python scripts will break badly. 268 func limitStringLength(s string, max int) string { 269 if len(s) <= max { 270 return s 271 } 272 for { 273 s = s[:max] 274 r, size := utf8.DecodeLastRuneInString(s) 275 if r != utf8.RuneError || size != 1 { 276 return s 277 } 278 max-- 279 } 280 }