Test round 3.

Change-Id: I78933fe390f1e5516eb3882de9d2231bafa773ae
Reviewed-on: https://chromium-review.googlesource.com/679998
Reviewed-by: Chan Li <chanli@chromium.org>
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..aa4fe9b
--- /dev/null
+++ b/main.go
@@ -0,0 +1,441 @@
+// Copyright 2016 The Chromium 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 som implements HTTP server that handles requests to default module.
+package som
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"html/template"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"strings"
+	"time"
+
+	"golang.org/x/net/context"
+
+	"github.com/luci/gae/service/datastore"
+	"github.com/luci/gae/service/info"
+	"github.com/luci/gae/service/memcache"
+	"github.com/luci/luci-go/appengine/gaeauth/server"
+	"github.com/luci/luci-go/appengine/gaeauth/server/gaesigner"
+	"github.com/luci/luci-go/appengine/gaemiddleware"
+	"github.com/luci/luci-go/common/logging"
+	"github.com/luci/luci-go/common/tsmon/metric"
+	"github.com/luci/luci-go/server/auth"
+	"github.com/luci/luci-go/server/auth/identity"
+	"github.com/luci/luci-go/server/auth/xsrf"
+	"github.com/luci/luci-go/server/router"
+	"github.com/luci/luci-go/server/settings"
+)
+
+const (
+	authGroup           = "sheriff-o-matic-access"
+	annotationsCacheKey = "annotation-metadata"
+	bugQueueCacheFormat = "bugqueue-%s"
+	settingsKey         = "tree"
+	// annotations will expire after this amount of time
+	annotationExpiration  = time.Hour * 24 * 10
+	productionAnalyticsID = "UA-55762617-1"
+	stagingAnalyticsID    = "UA-55762617-22"
+)
+
+var (
+	mainPage         = template.Must(template.ParseFiles("./index.html"))
+	accessDeniedPage = template.Must(template.ParseFiles("./access-denied.html"))
+	monorailEndpoint = "https://monorail-prod.appspot.com/_ah/api/monorail/v1/"
+	jsErrors         = metric.NewCounter("frontend/js_errors",
+		"Number of uncaught javascript errors.", nil)
+)
+
+var errStatus = func(c context.Context, w http.ResponseWriter, status int, msg string) {
+	logging.Errorf(c, "Status %d msg %s", status, msg)
+	w.WriteHeader(status)
+	w.Write([]byte(msg))
+}
+
+func indexPage(ctx *router.Context) {
+	c, w, r, p := ctx.Context, ctx.Writer, ctx.Request, ctx.Params
+	if p.ByName("path") == "" {
+		http.Redirect(w, r, "/chromium", http.StatusFound)
+		return
+	}
+
+	user := auth.CurrentIdentity(c)
+
+	if user.Kind() == identity.Anonymous {
+		url, err := auth.LoginURL(c, p.ByName("path"))
+		if err != nil {
+			errStatus(c, w, http.StatusInternalServerError, fmt.Sprintf(
+				"You must login. Additionally, an error was encountered while serving this request: %s", err.Error()))
+		} else {
+			http.Redirect(w, r, url, http.StatusFound)
+		}
+
+		return
+	}
+
+	isGoogler, err := auth.IsMember(c, authGroup)
+
+	if err != nil {
+		errStatus(c, w, http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	logoutURL, err := auth.LogoutURL(c, "/")
+
+	if err != nil {
+		errStatus(c, w, http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	if !isGoogler {
+		err = accessDeniedPage.Execute(w, map[string]interface{}{
+			"Group":     authGroup,
+			"LogoutURL": logoutURL,
+		})
+		if err != nil {
+			logging.Errorf(c, "while rendering index: %s", err)
+		}
+		return
+	}
+
+	tok, err := xsrf.Token(c)
+	if err != nil {
+		logging.Errorf(c, "while getting xrsf token: %s", err)
+	}
+
+	AnalyticsID := stagingAnalyticsID
+	if !strings.HasSuffix(info.AppID(c), "-staging") {
+		logging.Debugf(c, "Using production GA ID for app %s", info.AppID(c))
+		AnalyticsID = productionAnalyticsID
+	}
+
+	data := map[string]interface{}{
+		"User":           user.Email(),
+		"LogoutUrl":      logoutURL,
+		"IsDevAppServer": info.IsDevAppServer(c),
+		"XsrfToken":      tok,
+		"AnalyticsID":    AnalyticsID,
+	}
+
+	err = mainPage.Execute(w, data)
+	if err != nil {
+		logging.Errorf(c, "while rendering index: %s", err)
+	}
+}
+
+func getTreesHandler(ctx *router.Context) {
+	c, w := ctx.Context, ctx.Writer
+
+	q := datastore.NewQuery("Tree")
+	results := []*Tree{}
+	err := datastore.GetAll(c, q, &results)
+	if err != nil {
+		errStatus(c, w, http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	txt, err := json.Marshal(results)
+	if err != nil {
+		errStatus(c, w, http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	w.Write(txt)
+}
+
+// Switches chromium.org emails for google.com emails and vice versa.
+// Note that chromium.org emails may be different from google.com emails.
+func getAlternateEmail(email string) string {
+	s := strings.Split(email, "@")
+	if len(s) != 2 {
+		return email
+	}
+
+	user, domain := s[0], s[1]
+	if domain == "chromium.org" {
+		return fmt.Sprintf("%s@google.com", user)
+	}
+	return fmt.Sprintf("%s@chromium.org", user)
+}
+
+func getCrRevJSON(c context.Context, pos string) (map[string]string, error) {
+	itm := memcache.NewItem(c, fmt.Sprintf("crrev:%s", pos))
+	err := memcache.Get(c, itm)
+
+	if err == memcache.ErrCacheMiss {
+		hc, err := getOAuthClient(c)
+		if err != nil {
+			return nil, err
+		}
+
+		resp, err := hc.Get(fmt.Sprintf("https://cr-rev.appspot.com/_ah/api/crrev/v1/redirect/%s", pos))
+		if err != nil {
+			return nil, err
+		}
+
+		defer resp.Body.Close()
+		body, err := ioutil.ReadAll(resp.Body)
+		if err != nil {
+			return nil, err
+		}
+		itm.SetValue(body)
+		if err = memcache.Set(c, itm); err != nil {
+			return nil, fmt.Errorf("while setting memcache: %s", err)
+		}
+	} else if err != nil {
+		return nil, fmt.Errorf("while getting from memcache: %s", err)
+	}
+
+	m := map[string]string{}
+	err = json.Unmarshal(itm.Value(), &m)
+	if err != nil {
+		return nil, err
+	}
+
+	return m, nil
+}
+
+func getRevRangeHandler(ctx *router.Context) {
+	c, w, r, p := ctx.Context, ctx.Writer, ctx.Request, ctx.Params
+
+	start := p.ByName("start")
+	end := p.ByName("end")
+	if start == "" || end == "" {
+		errStatus(c, w, http.StatusBadRequest, "Start and end parameters must be set.")
+		return
+	}
+
+	itm := memcache.NewItem(c, fmt.Sprintf("revrange:%s..%s", start, end))
+	err := memcache.Get(c, itm)
+
+	if err == memcache.ErrCacheMiss {
+		startRev, err := getCrRevJSON(c, start)
+		if err != nil {
+			errStatus(c, w, http.StatusInternalServerError, err.Error())
+			return
+		}
+
+		endRev, err := getCrRevJSON(c, end)
+		if err != nil {
+			errStatus(c, w, http.StatusInternalServerError, err.Error())
+			return
+		}
+
+		// TODO(seanmccullough): some sanity checking of the rev json (same repo etc)
+
+		gitilesURL := fmt.Sprintf("https://chromium.googlesource.com/chromium/src/+log/%s^..%s?format=JSON",
+			startRev["git_sha"], endRev["git_sha"])
+
+		itm.SetValue([]byte(gitilesURL))
+		if err = memcache.Set(c, itm); err != nil {
+			errStatus(c, w, http.StatusInternalServerError, fmt.Sprintf("while setting memcache: %s", err))
+			return
+		}
+	} else if err != nil {
+		errStatus(c, w, http.StatusInternalServerError, fmt.Sprintf("while getting memcache: %s", err))
+		return
+	}
+
+	http.Redirect(w, r, string(itm.Value()), 301)
+}
+
+type eCatcherReq struct {
+	Errors    map[string]int64 `json:"errors"`
+	XSRFToken string           `json:"xsrf_token"`
+}
+
+func postClientMonHandler(ctx *router.Context) {
+	c, w, r := ctx.Context, ctx.Writer, ctx.Request
+
+	req := &eCatcherReq{}
+	if err := json.NewDecoder(r.Body).Decode(req); err != nil {
+		errStatus(c, w, http.StatusBadRequest, err.Error())
+		return
+	}
+
+	if err := xsrf.Check(c, req.XSRFToken); err != nil {
+		errStatus(c, w, http.StatusForbidden, err.Error())
+		return
+	}
+
+	for _, errCount := range req.Errors {
+		jsErrors.Add(c, errCount)
+	}
+	logging.Errorf(c, "clientmon report: %v", req.Errors)
+}
+func getTreeLogoHandler(ctx *router.Context) {
+	c, w := ctx.Context, ctx.Writer
+
+	sa, err := info.ServiceAccount(c)
+	if err != nil {
+		logging.Errorf(c, "failed to get service account: %v", err)
+		errStatus(c, w, http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	getTreeLogo(ctx, sa, gaesigner.Signer{})
+}
+
+type signer interface {
+	SignBytes(c context.Context, b []byte) (string, []byte, error)
+}
+
+func getTreeLogo(ctx *router.Context, sa string, sign signer) {
+	c, w, r, p := ctx.Context, ctx.Writer, ctx.Request, ctx.Params
+	tree := p.ByName("tree")
+	resource := fmt.Sprintf("/%s.appspot.com/logos/%s.png", info.AppID(c), tree)
+	expStr := fmt.Sprintf("%d", time.Now().Add(10*time.Minute).Unix())
+	sl := []string{
+		"GET",
+		"", // Optional MD5, which we don't have.
+		"", // Content type, ommitted because it breaks the signature.
+		expStr,
+		resource,
+	}
+	unsigned := strings.Join(sl, "\n")
+	_, b, err := sign.SignBytes(c, []byte(unsigned))
+	if err != nil {
+		logging.Errorf(c, "failed to sign bytes: %v", err)
+		errStatus(c, w, http.StatusInternalServerError, err.Error())
+		return
+	}
+	sig := base64.StdEncoding.EncodeToString(b)
+	params := url.Values{
+		"GoogleAccessId": {sa},
+		"Expires":        {expStr},
+		"Signature":      {sig},
+	}
+
+	signedURL := fmt.Sprintf("https://storage.googleapis.com%s?%s", resource, params.Encode())
+	http.Redirect(w, r, signedURL, http.StatusFound)
+}
+
+// getOAuthClient returns a client capable of making HTTP requests authenticated
+// with OAuth access token for userinfo.email scope.
+var getOAuthClient = func(c context.Context) (*http.Client, error) {
+	// Note: "https://www.googleapis.com/auth/userinfo.email" is the default
+	// scope used by GetRPCTransport(AsSelf). Use auth.WithScopes(...) option to
+	// override.
+	t, err := auth.GetRPCTransport(c, auth.AsSelf)
+	if err != nil {
+		return nil, err
+	}
+	return &http.Client{Transport: t}, nil
+}
+
+// base is the root of the middleware chain.
+func base(includeCookie bool) router.MiddlewareChain {
+	methods := []auth.Method{
+		&server.OAuth2Method{Scopes: []string{server.EmailScope}},
+		&server.InboundAppIDAuthMethod{},
+	}
+	if includeCookie {
+		methods = append(methods, server.CookieAuth)
+	}
+
+	return gaemiddleware.BaseProd().Extend(
+		auth.Use(methods),
+		auth.Authenticate,
+	)
+}
+
+func requireGoogler(c *router.Context, next router.Handler) {
+	isGoogler, err := auth.IsMember(c.Context, authGroup)
+	switch {
+	case err != nil:
+		errStatus(c.Context, c.Writer, http.StatusInternalServerError, err.Error())
+	case !isGoogler:
+		errStatus(c.Context, c.Writer, http.StatusForbidden, "Access denied")
+	default:
+		next(c)
+	}
+}
+
+func noopHandler(ctx *router.Context) {
+	return
+}
+
+func getXSRFToken(ctx *router.Context) {
+	c, w := ctx.Context, ctx.Writer
+
+	tok, err := xsrf.Token(c)
+	if err != nil {
+		logging.Errorf(c, "while getting xrsf token: %s", err)
+	}
+
+	data := map[string]string{
+		"token": tok,
+	}
+	txt, err := json.Marshal(data)
+	if err != nil {
+		errStatus(c, w, http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	w.Write(txt)
+}
+
+//// Routes.
+func init() {
+
+	settings.RegisterUIPage(settingsKey, settingsUIPage{})
+
+	r := router.New()
+	basemw := base(true)
+
+	protected := basemw.Extend(requireGoogler)
+
+	gaemiddleware.InstallHandlers(r, gaemiddleware.BaseProd())
+	r.GET("/api/v1/trees/", protected, getTreesHandler)
+	r.GET("/api/v1/alerts/:tree", protected, getAlertsHandler)
+	r.GET("/api/v1/pubsubalerts/:tree", protected, getPubSubAlertsHandler)
+	r.GET("/api/v1/restarts/:tree", protected, getRestartingMastersHandler)
+	r.GET("/api/v1/xsrf_token", protected, getXSRFToken)
+
+	// Disallow cookies because this handler should not be accessible by regular
+	// users.
+	r.POST("/api/v1/alerts/:tree", basemw, postAlertsHandler)
+	r.GET("/api/v1/annotations/", protected, getAnnotationsHandler)
+	r.POST("/api/v1/annotations/:annKey/:action", protected, postAnnotationsHandler)
+	r.GET("/api/v1/bugqueue/:label", protected, getBugQueueHandler)
+	r.GET("/api/v1/bugqueue/:label/uncached/", protected, getUncachedBugsHandler)
+	r.GET("/api/v1/revrange/:start/:end", basemw, getRevRangeHandler)
+	r.GET("/logos/:tree", protected, getTreeLogoHandler)
+	r.GET("/alertdiff/:tree", protected, getMiloDiffHandler)
+
+	// Non-public endpoints.
+	r.GET("/_cron/refresh/bugqueue/:label", basemw, refreshBugQueueHandler)
+	r.GET("/_cron/annotations/flush_old/", basemw, flushOldAnnotationsHandler)
+	r.GET("/_cron/annotations/refresh/", basemw, refreshAnnotationsHandler)
+	r.GET("/_cron/analyze/:tree", basemw, getAnalyzeHandler)
+	r.POST("/_/clientmon", basemw, postClientMonHandler)
+	r.POST("/_ah/push-handlers/milo", basemw, postMiloPubSubHandler)
+
+	// Ingore reqeuests from builder-alerts rather than 404.
+	r.GET("/alerts", gaemiddleware.BaseProd(), noopHandler)
+	r.POST("/alerts", gaemiddleware.BaseProd(), noopHandler)
+
+	rootRouter := router.New()
+	rootRouter.GET("/*path", basemw, indexPage)
+
+	http.DefaultServeMux.Handle("/_cron/", r)
+	http.DefaultServeMux.Handle("/api/", r)
+	http.DefaultServeMux.Handle("/admin/", r)
+	http.DefaultServeMux.Handle("/auth/", r)
+	http.DefaultServeMux.Handle("/_ah/", r)
+	http.DefaultServeMux.Handle("/internal/", r)
+	http.DefaultServeMux.Handle("/_/", r)
+	http.DefaultServeMux.Handle("/logos/", r)
+	http.DefaultServeMux.Handle("/alerts", r)
+	http.DefaultServeMux.Handle("/alertdiff/", r)
+
+	http.DefaultServeMux.Handle("/", rootRouter)
+}