| // Copyright 2011 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package main |
| |
| import ( |
| "context" |
| "crypto/hmac" |
| "crypto/md5" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "html" |
| "net/http" |
| "strconv" |
| "strings" |
| "time" |
| "unicode/utf8" |
| |
| "golang.org/x/build/app/key" |
| "google.golang.org/appengine" |
| "google.golang.org/appengine/datastore" |
| "google.golang.org/appengine/log" |
| "google.golang.org/appengine/memcache" |
| ) |
| |
| const ( |
| commitsPerPage = 30 |
| builderVersion = 1 // must match x/build/cmd/coordinator/dash.go's value |
| ) |
| |
| // buildingHandler records that a build is in progress. |
| // The data is only stored in memcache and with a timeout. It's assumed |
| // that the build system will periodically refresh this if the build |
| // is slow. |
| func buildingHandler(r *http.Request) (interface{}, error) { |
| if r.Method != "POST" { |
| return nil, errBadMethod(r.Method) |
| } |
| c := contextForRequest(r) |
| key := buildingKey(r.FormValue("hash"), r.FormValue("gohash"), r.FormValue("builder")) |
| err := memcache.Set(c, &memcache.Item{ |
| Key: key, |
| Value: []byte(r.FormValue("url")), |
| Expiration: 15 * time.Minute, |
| }) |
| if err != nil { |
| return nil, err |
| } |
| return map[string]interface{}{ |
| "key": key, |
| }, nil |
| } |
| |
| // resultHandler records a build result. |
| // It reads a JSON-encoded Result value from the request body, |
| // creates a new Result entity, and creates or updates the relevant Commit entity. |
| // If the Log field is not empty, resultHandler creates a new Log entity |
| // and updates the LogHash field before putting the Commit entity. |
| func resultHandler(r *http.Request) (interface{}, error) { |
| if r.Method != "POST" { |
| return nil, errBadMethod(r.Method) |
| } |
| |
| v, _ := strconv.Atoi(r.FormValue("version")) |
| if v != builderVersion { |
| return nil, fmt.Errorf("rejecting POST from builder; need version %v instead of %v", |
| builderVersion, v) |
| } |
| |
| c := contextForRequest(r) |
| res := new(Result) |
| defer r.Body.Close() |
| if err := json.NewDecoder(r.Body).Decode(res); err != nil { |
| return nil, fmt.Errorf("decoding Body: %v", err) |
| } |
| if err := res.Valid(); err != nil { |
| return nil, fmt.Errorf("validating Result: %v", err) |
| } |
| // store the Log text if supplied |
| if len(res.Log) > 0 { |
| hash, err := PutLog(c, res.Log) |
| if err != nil { |
| return nil, fmt.Errorf("putting Log: %v", err) |
| } |
| res.LogHash = hash |
| } |
| tx := func(c context.Context) error { |
| if _, err := getOrMakePackageInTx(c, res.PackagePath); err != nil { |
| return fmt.Errorf("GetPackage: %v", err) |
| } |
| // put Result |
| if _, err := datastore.Put(c, res.Key(c), res); err != nil { |
| return fmt.Errorf("putting Result: %v", err) |
| } |
| // add Result to Commit |
| com := &Commit{PackagePath: res.PackagePath, Hash: res.Hash} |
| if err := com.AddResult(c, res); err != nil { |
| return fmt.Errorf("AddResult: %v", err) |
| } |
| return nil |
| } |
| return nil, datastore.RunInTransaction(c, tx, nil) |
| } |
| |
| // logHandler displays log text for a given hash. |
| // It handles paths like "/log/hash". |
| func logHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-type", "text/plain; charset=utf-8") |
| c := contextForRequest(r) |
| hash := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:] |
| key := datastore.NewKey(c, "Log", hash, 0, nil) |
| l := new(Log) |
| if err := datastore.Get(c, key, l); err != nil { |
| if err == datastore.ErrNoSuchEntity { |
| // Fall back to default namespace; |
| // maybe this was on the old dashboard. |
| c := appengine.NewContext(r) |
| key := datastore.NewKey(c, "Log", hash, 0, nil) |
| err = datastore.Get(c, key, l) |
| } |
| if err != nil { |
| logErr(w, r, err) |
| return |
| } |
| } |
| b, err := l.Text() |
| if err != nil { |
| logErr(w, r, err) |
| return |
| } |
| w.Write(b) |
| } |
| |
| // clearResultsHandler purge a single build failure from the dashboard. |
| // It currently only supports the main Go repo. |
| func clearResultsHandler(r *http.Request) (interface{}, error) { |
| if r.Method != "POST" { |
| return nil, errBadMethod(r.Method) |
| } |
| builder := r.FormValue("builder") |
| hash := r.FormValue("hash") |
| if builder == "" { |
| return nil, errors.New("missing 'builder'") |
| } |
| if hash == "" { |
| return nil, errors.New("missing 'hash'") |
| } |
| |
| ctx := contextForRequest(r) |
| |
| err := datastore.RunInTransaction(ctx, func(ctx context.Context) error { |
| c := &Commit{ |
| PackagePath: "", // TODO(adg): support clearing sub-repos |
| Hash: hash, |
| } |
| err := datastore.Get(ctx, c.Key(ctx), c) |
| err = filterDatastoreError(err) |
| if err == datastore.ErrNoSuchEntity { |
| // Doesn't exist, so no build to clear. |
| return nil |
| } |
| if err != nil { |
| return err |
| } |
| |
| r := c.Result(builder, "") |
| if r == nil { |
| // No result, so nothing to clear. |
| return nil |
| } |
| c.RemoveResult(r) |
| _, err = datastore.Put(ctx, c.Key(ctx), c) |
| if err != nil { |
| return err |
| } |
| return datastore.Delete(ctx, r.Key(ctx)) |
| }, nil) |
| return nil, err |
| } |
| |
| type dashHandler func(*http.Request) (interface{}, error) |
| |
| type dashResponse struct { |
| Response interface{} |
| Error string |
| } |
| |
| // errBadMethod is returned by a dashHandler when |
| // the request has an unsuitable method. |
| type errBadMethod string |
| |
| func (e errBadMethod) Error() string { |
| return "bad method: " + string(e) |
| } |
| |
| func builderKeyRevoked(builder string) bool { |
| switch builder { |
| case "plan9-amd64-mischief": |
| // Broken and unmaintained for months. |
| // It's polluting the dashboard. |
| return true |
| case "linux-arm-onlinenet": |
| // Requested to be revoked by Dave Cheney. |
| // The machine is in a fail+report loop |
| // and can't be accessed. Revoke it for now. |
| return true |
| } |
| return false |
| } |
| |
| // AuthHandler wraps a http.HandlerFunc with a handler that validates the |
| // supplied key and builder query parameters. |
| func AuthHandler(h dashHandler) http.HandlerFunc { |
| return func(w http.ResponseWriter, r *http.Request) { |
| c := contextForRequest(r) |
| |
| // Put the URL Query values into r.Form to avoid parsing the |
| // request body when calling r.FormValue. |
| r.Form = r.URL.Query() |
| |
| var err error |
| var resp interface{} |
| |
| // Validate key query parameter for POST requests only. |
| key := r.FormValue("key") |
| builder := r.FormValue("builder") |
| if r.Method == "POST" && !validKey(c, key, builder) { |
| err = fmt.Errorf("invalid key %q for builder %q", key, builder) |
| } |
| |
| // Call the original HandlerFunc and return the response. |
| if err == nil { |
| resp, err = h(r) |
| } |
| |
| // Write JSON response. |
| dashResp := &dashResponse{Response: resp} |
| if err != nil { |
| log.Errorf(c, "%v", err) |
| dashResp.Error = err.Error() |
| } |
| w.Header().Set("Content-Type", "application/json") |
| if err = json.NewEncoder(w).Encode(dashResp); err != nil { |
| log.Criticalf(c, "encoding response: %v", err) |
| } |
| } |
| } |
| |
| // validHash reports whether hash looks like a valid git commit hash. |
| func validHash(hash string) bool { |
| // TODO: correctly validate a hash: check that it's exactly 40 |
| // lowercase hex digits. But this is what we historically did: |
| return hash != "" |
| } |
| |
| func validKey(c context.Context, key, builder string) bool { |
| if isMasterKey(c, key) { |
| return true |
| } |
| if builderKeyRevoked(builder) { |
| return false |
| } |
| return key == builderKey(c, builder) |
| } |
| |
| func isMasterKey(c context.Context, k string) bool { |
| return appengine.IsDevAppServer() || k == key.Secret(c) |
| } |
| |
| func builderKey(c context.Context, builder string) string { |
| h := hmac.New(md5.New, []byte(key.Secret(c))) |
| h.Write([]byte(builder)) |
| return fmt.Sprintf("%x", h.Sum(nil)) |
| } |
| |
| func logErr(w http.ResponseWriter, r *http.Request, err error) { |
| c := contextForRequest(r) |
| log.Errorf(c, "Error: %v", err) |
| w.WriteHeader(http.StatusInternalServerError) |
| fmt.Fprint(w, "Error: ", html.EscapeString(err.Error())) |
| } |
| |
| func contextForRequest(r *http.Request) context.Context { |
| return goDash.Context(appengine.NewContext(r)) |
| } |
| |
| // limitStringLength essentially does return s[:max], |
| // but it ensures that we dot not split UTF-8 rune in half. |
| // Otherwise appengine python scripts will break badly. |
| func limitStringLength(s string, max int) string { |
| if len(s) <= max { |
| return s |
| } |
| for { |
| s = s[:max] |
| r, size := utf8.DecodeLastRuneInString(s) |
| if r != utf8.RuneError || size != 1 { |
| return s |
| } |
| max-- |
| } |
| } |