Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(19)

Unified Diff: tools/bug_chomper/src/server/server.go

Issue 274693002: BugChomper utility - rewrite in Go (Closed) Base URL: https://skia.googlesource.com/skia.git@master
Patch Set: Created 6 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: tools/bug_chomper/src/server/server.go
diff --git a/tools/bug_chomper/src/server/server.go b/tools/bug_chomper/src/server/server.go
new file mode 100644
index 0000000000000000000000000000000000000000..a8f6970a88a61af222471aa5cb951c690fc89c3c
--- /dev/null
+++ b/tools/bug_chomper/src/server/server.go
@@ -0,0 +1,352 @@
+// Copyright (c) 2014 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.
+
+/*
+ Serves a webpage for easy management of Skia bugs.
+
+ WARNING: This server is NOT secure and should not be made publicly
+ accessible.
+*/
+
+package main
+
+import (
+ "crypto/rand"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "html/template"
+ "issue_tracker"
+ "log"
+ "net/http"
+ "net/url"
+ "path"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+)
+
+const certFile = "certs/cert.pem"
+const keyFile = "certs/key.pem"
+const issueComment = "Edited by BugChomper"
+const oauthConfigFile = "oauth_client_secret.json"
+const defaultPort = 8000
+const maxSessionLen = 3600
+const priorityPrefix = "Priority-"
+const project = "skia"
+const scheme = "http"
+const sidBytes = 64
+const sidCookieName = "BugChomperSID"
+
+var contentTypesByExtension = map[string]string{
jcgregorio 2014/05/07 19:32:25 Is there a reason you didn't use http://golang.org
borenet 2014/05/07 22:27:37 Didn't know about it!
+ "css": "text/css",
+ "js": "application/javascript",
+ "ico": "image/x-icon",
+ "htm": "text/html",
+ "html": "text/html",
+}
+var staticPath, _ = filepath.Abs("static")
+var templatePath, _ = filepath.Abs("templates")
+var templates = template.Must(template.ParseFiles(
+ path.Join(templatePath, "bug_chomper.html"),
+ path.Join(templatePath, "submitted.html")))
+
+// SessionState contains data for a given session.
+type SessionState struct {
+ issueTracker *issue_tracker.IssueTracker
+ origRequestURL string
+ sessionStart time.Time
+}
+
+// sessionStates contains the state for all sessions. It's leaky because
jcgregorio 2014/05/07 19:32:25 It also goes away when the server restarts. This w
borenet 2014/05/07 22:27:37 So: - (de)serialize session states on startup/shut
jcgregorio 2014/05/08 15:11:56 Again, the choices depend on if this is going to r
+// nothing ever gets removed!
+var sessionStates = make(map[string]*SessionState)
+
+// makeSid returns a randomly-generated session identifier.
+func makeSid() string {
+ sid := make([]byte, sidBytes)
+ rand.Read(sid)
+ encodedSid := make([]byte, base64.StdEncoding.EncodedLen(len(sid)))
+ base64.StdEncoding.Encode(encodedSid, sid)
+ return string(encodedSid)
+}
+
+// getAbsoluteURL returns the absolute URL of the given Request.
+func getAbsoluteURL(req *http.Request) string {
+ return scheme + "://" + req.Host + req.URL.Path
+}
+
+// makeSession creates a new session for the Request.
+func makeSession(
+ resp http.ResponseWriter, req *http.Request) (*SessionState, error) {
+ sid := makeSid()
+ issueTracker, err := issue_tracker.MakeIssueTracker(oauthConfigFile)
+ if err != nil {
+ return nil, err
+ }
+ session := SessionState{
+ issueTracker: issueTracker,
+ origRequestURL: getAbsoluteURL(req),
+ sessionStart: time.Now(),
+ }
+ log.Println("Started session with SID: ", sid)
+ sessionStates[sid] = &session
+ cookie := makeCookieForSession(sid)
+ http.SetCookie(resp, cookie)
+ return &session, nil
+}
+
+// makeCookieForSession creates a Cookie containing the session ID for this
+// session.
+func makeCookieForSession(sid string) *http.Cookie {
+ expires := time.Now().Add(time.Duration(maxSessionLen * time.Second))
+ cookie := http.Cookie{
+ Name: sidCookieName,
+ Value: sid,
+ Path: "",
+ Domain: "",
+ Expires: expires,
+ RawExpires: expires.String(),
+ MaxAge: maxSessionLen,
+ Secure: false,
+ HttpOnly: true,
+ Raw: "",
+ Unparsed: nil,
+ }
+ return &cookie
+}
+
+// getSession retrieves the active SessionState or creates and returns a new
+// SessionState.
+func getSession(
+ resp http.ResponseWriter, req *http.Request) (*SessionState, error) {
+ cookie, err := req.Cookie(sidCookieName)
+ if err != nil {
+ // This is a new session. Create a SessionState.
+ return makeSession(resp, req)
+ }
+ sid := cookie.Value
+ session, ok := sessionStates[sid]
+ if !ok {
+ // Unknown session. Start a new one.
+ return makeSession(resp, req)
+ }
+ return session, nil
+}
+
+// makeBugChomperPage builds and serves the BugChomper page.
+func makeBugChomperPage(resp http.ResponseWriter, req *http.Request) error {
+ session, err := getSession(resp, req)
+ if err != nil {
+ return err
+ }
+ issueTracker := session.issueTracker
+ user := "borenet@google.com"
+ log.Println("Loading bugs for " + user)
+ bugList, err := issueTracker.GetBugs(project, user)
+ if err != nil {
+ return err
+ }
+ bugsById := make(map[string]*issue_tracker.Issue)
+ bugsByPriority := make(map[string][]*issue_tracker.Issue)
+ for _, bug := range bugList.Items {
+ bugsById[strconv.Itoa(bug.Id)] = bug
+ var bugPriority string
+ for _, label := range bug.Labels {
+ if strings.HasPrefix(label, priorityPrefix) {
+ bugPriority = label[len(priorityPrefix):]
+ }
+ }
+ if _, ok := bugsByPriority[bugPriority]; !ok {
+ bugsByPriority[bugPriority] = make(
+ []*issue_tracker.Issue, 0)
+ }
+ bugsByPriority[bugPriority] = append(
+ bugsByPriority[bugPriority], bug)
+ }
+ bugsJson, err := json.Marshal(bugsById)
+ if err != nil {
+ return err
+ }
+ pageData := struct {
+ Title string
+ User string
+ BugsJson template.JS
+ BugsByPriority *map[string][]*issue_tracker.Issue
+ Priorities []string
+ PriorityPrefix string
+ }{
+ Title: "BugChomper",
+ User: "borenet@google.com",
+ BugsJson: template.JS(string(bugsJson)),
+ BugsByPriority: &bugsByPriority,
+ Priorities: issue_tracker.BugPriorities,
+ PriorityPrefix: priorityPrefix,
+ }
+ err = templates.ExecuteTemplate(resp, "bug_chomper.html", pageData)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// authIfNeeded determines whether the current user is logged in. If not, it
+// redirects to a login page. If the user is just returning from the login,
+// it upgrades the single-use authorization code obtained from the login page
+// and redirects to the original request URL. Returns true if the user is
+// redirected and false otherwise.
+func authIfNeeded(resp http.ResponseWriter, req *http.Request) (bool, error) {
+ session, err := getSession(resp, req)
+ if err != nil {
+ return false, err
+ }
+ issueTracker := session.issueTracker
+ if !issueTracker.IsAuthenticated() {
+ params, err := url.ParseQuery(req.URL.RawQuery)
+ if err != nil {
+ return false, err
+ }
+ if code, ok := params["code"]; ok {
+ // We're returning from the login page.
+ log.Println("Upgrading auth token:", code[0])
+ err = issueTracker.UpgradeCode(code[0])
jcgregorio 2014/05/07 19:32:25 This shouldn't be part of a regular page, i.e. the
borenet 2014/05/07 22:27:37 Done.
+ if err != nil {
+ return false, err
+ }
+ http.Redirect(resp, req, session.origRequestURL,
+ http.StatusTemporaryRedirect)
+ return true, nil
+ } else {
+ loginURL := issueTracker.MakeAuthRequestURL(
+ session.origRequestURL)
+ log.Println("Redirecting for login:", loginURL)
+ http.Redirect(resp, req, loginURL,
+ http.StatusTemporaryRedirect)
+ return true, nil
+ }
+ }
+ return false, nil
+}
+
+// submitData attempts to submit data from a POST request to the IssueTracker.
+func submitData(resp http.ResponseWriter, req *http.Request) error {
+ session, err := getSession(resp, req)
+ if err != nil {
+ return err
+ }
+ issueTracker := session.issueTracker
+ edits := req.FormValue("all_edits")
+ var editsMap map[string]*issue_tracker.Issue
+ err = json.Unmarshal([]byte(edits), &editsMap)
+ if err != nil {
+ return errors.New(
+ "Could not parse edits from form response: " +
+ err.Error())
+ }
+ pageData := struct {
+ Title string
+ Message string
+ BackLink string
+ }{}
+ if len(editsMap) == 0 {
+ pageData.Title = "No Changes Submitted"
+ pageData.Message = "You didn't change anything!"
+ pageData.BackLink = ""
+ err = templates.ExecuteTemplate(
+ resp, "submitted.html", pageData)
+ if err != nil {
+ return err
+ }
+ return nil
+ }
+ errorList := make([]error, 0)
+ for issueId, newIssue := range editsMap {
+ log.Println("Editing issue " + issueId)
+ err = issueTracker.SubmitIssueChanges(newIssue, issueComment)
+ if err != nil {
+ errorList = append(errorList, err)
+ }
+ }
+ if len(errorList) > 0 {
+ errorStrings := ""
+ for _, err := range errorList {
+ errorStrings += err.Error() + "\n"
+ }
+ return errors.New(
+ "Not all changes could be submitted: \n" +
+ errorStrings)
+ }
+ pageData.Title = "Submitted Changes"
+ pageData.Message = "Your changes were submitted to the issue tracker."
+ pageData.BackLink = ""
+ err = templates.ExecuteTemplate(resp, "submitted.html", pageData)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// handleBugChomper handles HTTP requests for the bug_chomper page.
+func handleBugChomper(resp http.ResponseWriter, req *http.Request) error {
+ redirected, err := authIfNeeded(resp, req)
+ if err != nil {
+ return err
+ }
+ if redirected {
+ return nil
+ }
+ switch req.Method {
+ case "GET":
+ err = makeBugChomperPage(resp, req)
+ if err != nil {
+ return err
+ }
+ case "POST":
+ err = submitData(resp, req)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// handle is the top-level handler function for all HTTP requests coming to
+// this server.
+func handle(resp http.ResponseWriter, req *http.Request) {
jcgregorio 2014/05/07 19:32:25 Use w and r as the variable names here: func ha
borenet 2014/05/07 22:27:37 Done. Is this just convention? To me it seems ea
jcgregorio 2014/05/08 15:11:56 Yes, it's a convention. On 2014/05/07 22:27:37, b
+ log.Println("Fetching " + req.URL.Path)
+ if req.URL.Path == "/" || req.URL.Path == "/index.html" {
+ err := handleBugChomper(resp, req)
+ if err != nil {
+ http.Error(resp, err.Error(),
+ http.StatusInternalServerError)
+ log.Println(err.Error())
+ }
+ return
+ }
jcgregorio 2014/05/07 19:32:25 Just serve this off a subdir, i.e. do this in main
borenet 2014/05/07 22:27:37 Done.
+ for ext := range contentTypesByExtension {
+ if strings.HasSuffix(req.URL.Path, ext) {
+ http.ServeFile(
+ resp, req, path.Join(staticPath, req.URL.Path))
+ return
+ }
+ }
+ http.NotFound(resp, req)
+}
+
+// Run the BugChomper server.
+func main() {
+ http.HandleFunc("/", handle)
+ port := ":" + strconv.Itoa(defaultPort)
+ log.Println("Server is running at " + port)
+ var err error
+ if scheme == "https" {
+ err = http.ListenAndServeTLS(port, certFile, keyFile, nil)
+ } else {
+ err = http.ListenAndServe(port, nil)
+ }
+ if err != nil {
+ log.Println(err.Error())
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698