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..9fb21ed594d4cf7ae6f431a557ef64f09b5098f3 |
--- /dev/null |
+++ b/tools/bug_chomper/src/server/server.go |
@@ -0,0 +1,376 @@ |
+// 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 ( |
+ "encoding/json" |
+ "flag" |
+ "fmt" |
+ "html/template" |
+ "issue_tracker" |
+ "log" |
+ "net/http" |
+ "net/url" |
+ "path" |
+ "path/filepath" |
+ "strconv" |
+ "strings" |
+ "time" |
+) |
+ |
+import "github.com/gorilla/securecookie" |
+ |
+const certFile = "certs/cert.pem" |
+const keyFile = "certs/key.pem" |
+const issueComment = "Edited by BugChomper" |
+const oauthCallbackPath = "/oauth2callback" |
+const oauthConfigFile = "oauth_client_secret.json" |
+const defaultPort = 8000 |
+const localHost = "127.0.0.1" |
+const maxSessionLen = time.Duration(3600 * time.Second) |
+const priorityPrefix = "Priority-" |
+const project = "skia" |
+const cookieName = "BugChomperCookie" |
+ |
+var scheme = "http" |
+ |
+var curdir, _ = filepath.Abs(".") |
+var templatePath, _ = filepath.Abs("templates") |
+var templates = template.Must(template.ParseFiles( |
+ path.Join(templatePath, "bug_chomper.html"), |
+ path.Join(templatePath, "submitted.html"), |
+ path.Join(templatePath, "error.html"))) |
+ |
+var hashKey = securecookie.GenerateRandomKey(32) |
+var blockKey = securecookie.GenerateRandomKey(32) |
+var secureCookie = securecookie.New(hashKey, blockKey) |
+ |
+// SessionState contains data for a given session. |
+type SessionState struct { |
+ IssueTracker *issue_tracker.IssueTracker |
+ OrigRequestURL string |
+ SessionStart time.Time |
+} |
+ |
+// getAbsoluteURL returns the absolute URL of the given Request. |
+func getAbsoluteURL(r *http.Request) string { |
+ return scheme + "://" + r.Host + r.URL.Path |
+} |
+ |
+// getOAuth2CallbackURL returns a callback URL to be used by the OAuth2 login |
+// page. |
+func getOAuth2CallbackURL(r *http.Request) string { |
+ return scheme + "://" + r.Host + oauthCallbackPath |
+} |
+ |
+func saveSession(session *SessionState, w http.ResponseWriter, r *http.Request) error { |
+ encodedSession, err := secureCookie.Encode(cookieName, session) |
+ if err != nil { |
+ return fmt.Errorf("unable to encode session state: %s", err) |
+ } |
+ cookie := &http.Cookie{ |
+ Name: cookieName, |
+ Value: encodedSession, |
+ Domain: strings.Split(r.Host, ":")[0], |
+ Path: "/", |
+ HttpOnly: true, |
+ } |
+ http.SetCookie(w, cookie) |
+ return nil |
+} |
+ |
+// makeSession creates a new session for the Request. |
+func makeSession(w http.ResponseWriter, r *http.Request) (*SessionState, error) { |
+ log.Println("Creating new session.") |
+ // Create the session state. |
+ issueTracker, err := issue_tracker.MakeIssueTracker( |
+ oauthConfigFile, getOAuth2CallbackURL(r)) |
+ if err != nil { |
+ return nil, fmt.Errorf("unable to create IssueTracker for session: %s", err) |
+ } |
+ session := SessionState{ |
+ IssueTracker: issueTracker, |
+ OrigRequestURL: getAbsoluteURL(r), |
+ SessionStart: time.Now(), |
+ } |
+ |
+ // Encode and store the session state. |
+ if err := saveSession(&session, w, r); err != nil { |
+ return nil, err |
+ } |
+ |
+ return &session, nil |
+} |
+ |
+// getSession retrieves the active SessionState or creates and returns a new |
+// SessionState. |
+func getSession(w http.ResponseWriter, r *http.Request) (*SessionState, error) { |
+ cookie, err := r.Cookie(cookieName) |
+ if err != nil { |
+ log.Println("No cookie found! Starting new session.") |
+ return makeSession(w, r) |
+ } |
+ var session SessionState |
+ if err := secureCookie.Decode(cookieName, cookie.Value, &session); err != nil { |
+ log.Printf("Invalid or corrupted session. Starting another: %s", err.Error()) |
+ return makeSession(w, r) |
+ } |
+ |
+ currentTime := time.Now() |
+ if currentTime.Sub(session.SessionStart) > maxSessionLen { |
+ log.Printf("Session starting at %s is expired. Starting another.", |
+ session.SessionStart.Format(time.RFC822)) |
+ return makeSession(w, r) |
+ } |
+ saveSession(&session, w, r) |
+ return &session, nil |
+} |
+ |
+// reportError serves the error page with the given message. |
+func reportError(w http.ResponseWriter, msg string, code int) { |
+ errData := struct { |
+ Code int |
+ CodeString string |
+ Message string |
+ }{ |
+ Code: code, |
+ CodeString: http.StatusText(code), |
+ Message: msg, |
+ } |
+ w.WriteHeader(code) |
+ err := templates.ExecuteTemplate(w, "error.html", errData) |
+ if err != nil { |
+ log.Println("Failed to display error.html!!") |
+ } |
+} |
+ |
+// makeBugChomperPage builds and serves the BugChomper page. |
+func makeBugChomperPage(w http.ResponseWriter, r *http.Request) { |
+ session, err := getSession(w, r) |
+ if err != nil { |
+ reportError(w, err.Error(), http.StatusInternalServerError) |
+ return |
+ } |
+ issueTracker := session.IssueTracker |
+ user, err := issueTracker.GetLoggedInUser() |
+ if err != nil { |
+ reportError(w, err.Error(), http.StatusInternalServerError) |
+ return |
+ } |
+ log.Println("Loading bugs for " + user) |
+ bugList, err := issueTracker.GetBugs(project, user) |
+ if err != nil { |
+ reportError(w, err.Error(), http.StatusInternalServerError) |
+ return |
+ } |
+ 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 { |
+ reportError(w, err.Error(), http.StatusInternalServerError) |
+ return |
+ } |
+ data := struct { |
+ Title string |
+ User string |
+ BugsJson template.JS |
+ BugsByPriority *map[string][]*issue_tracker.Issue |
+ Priorities []string |
+ PriorityPrefix string |
+ }{ |
+ Title: "BugChomper", |
+ User: user, |
+ BugsJson: template.JS(string(bugsJson)), |
+ BugsByPriority: &bugsByPriority, |
+ Priorities: issue_tracker.BugPriorities, |
+ PriorityPrefix: priorityPrefix, |
+ } |
+ |
+ if err := templates.ExecuteTemplate(w, "bug_chomper.html", data); err != nil { |
+ reportError(w, err.Error(), http.StatusInternalServerError) |
+ return |
+ } |
+} |
+ |
+// authIfNeeded determines whether the current user is logged in. If not, it |
+// redirects to a login page. Returns true if the user is redirected and false |
+// otherwise. |
+func authIfNeeded(w http.ResponseWriter, r *http.Request) bool { |
+ session, err := getSession(w, r) |
+ if err != nil { |
+ reportError(w, err.Error(), http.StatusInternalServerError) |
+ return false |
+ } |
+ issueTracker := session.IssueTracker |
+ if !issueTracker.IsAuthenticated() { |
+ loginURL := issueTracker.MakeAuthRequestURL() |
+ log.Println("Redirecting for login:", loginURL) |
+ http.Redirect(w, r, loginURL, http.StatusTemporaryRedirect) |
+ return true |
+ } |
+ return false |
+} |
+ |
+// submitData attempts to submit data from a POST request to the IssueTracker. |
+func submitData(w http.ResponseWriter, r *http.Request) { |
+ session, err := getSession(w, r) |
+ if err != nil { |
+ reportError(w, err.Error(), http.StatusInternalServerError) |
+ return |
+ } |
+ issueTracker := session.IssueTracker |
+ edits := r.FormValue("all_edits") |
+ var editsMap map[string]*issue_tracker.Issue |
+ if err := json.Unmarshal([]byte(edits), &editsMap); err != nil { |
+ errMsg := "Could not parse edits from form response: " + err.Error() |
+ reportError(w, errMsg, http.StatusInternalServerError) |
+ return |
+ } |
+ data := struct { |
+ Title string |
+ Message string |
+ BackLink string |
+ }{} |
+ if len(editsMap) == 0 { |
+ data.Title = "No Changes Submitted" |
+ data.Message = "You didn't change anything!" |
+ data.BackLink = "" |
+ if err := templates.ExecuteTemplate(w, "submitted.html", data); err != nil { |
+ reportError(w, err.Error(), http.StatusInternalServerError) |
+ return |
+ } |
+ return |
+ } |
+ errorList := make([]error, 0) |
+ for issueId, newIssue := range editsMap { |
+ log.Println("Editing issue " + issueId) |
+ if err := issueTracker.SubmitIssueChanges(newIssue, issueComment); err != nil { |
+ errorList = append(errorList, err) |
+ } |
+ } |
+ if len(errorList) > 0 { |
+ errorStrings := "" |
+ for _, err := range errorList { |
+ errorStrings += err.Error() + "\n" |
+ } |
+ errMsg := "Not all changes could be submitted: \n" + errorStrings |
+ reportError(w, errMsg, http.StatusInternalServerError) |
+ return |
+ } |
+ data.Title = "Submitted Changes" |
+ data.Message = "Your changes were submitted to the issue tracker." |
+ data.BackLink = "" |
+ if err := templates.ExecuteTemplate(w, "submitted.html", data); err != nil { |
+ reportError(w, err.Error(), http.StatusInternalServerError) |
+ return |
+ } |
+ return |
+} |
+ |
+// handleBugChomper handles HTTP requests for the bug_chomper page. |
+func handleBugChomper(w http.ResponseWriter, r *http.Request) { |
+ if authIfNeeded(w, r) { |
+ return |
+ } |
+ switch r.Method { |
+ case "GET": |
+ makeBugChomperPage(w, r) |
+ case "POST": |
+ submitData(w, r) |
+ } |
+} |
+ |
+// handleOAuth2Callback handles callbacks from the OAuth2 sign-in. |
+func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) { |
+ session, err := getSession(w, r) |
+ if err != nil { |
+ reportError(w, err.Error(), http.StatusInternalServerError) |
+ } |
+ issueTracker := session.IssueTracker |
+ invalidLogin := "Invalid login credentials" |
+ params, err := url.ParseQuery(r.URL.RawQuery) |
+ if err != nil { |
+ reportError(w, invalidLogin+": "+err.Error(), http.StatusForbidden) |
+ return |
+ } |
+ code, ok := params["code"] |
+ if !ok { |
+ reportError(w, invalidLogin+": redirect did not include auth code.", |
+ http.StatusForbidden) |
+ return |
+ } |
+ log.Println("Upgrading auth token:", code[0]) |
+ if err := issueTracker.UpgradeCode(code[0]); err != nil { |
+ errMsg := "failed to upgrade token: " + err.Error() |
+ reportError(w, errMsg, http.StatusForbidden) |
+ return |
+ } |
+ if err := saveSession(session, w, r); err != nil { |
+ reportError(w, "failed to save session: "+err.Error(), |
+ http.StatusInternalServerError) |
+ return |
+ } |
+ http.Redirect(w, r, session.OrigRequestURL, http.StatusTemporaryRedirect) |
+ return |
+} |
+ |
+// handleRoot is the handler function for all HTTP requests at the root level. |
+func handleRoot(w http.ResponseWriter, r *http.Request) { |
+ log.Println("Fetching " + r.URL.Path) |
+ if r.URL.Path == "/" || r.URL.Path == "/index.html" { |
+ handleBugChomper(w, r) |
+ return |
+ } |
+ http.NotFound(w, r) |
+} |
+ |
+// Run the BugChomper server. |
+func main() { |
+ var public bool |
+ flag.BoolVar( |
+ &public, "public", false, "Make this server publicly accessible.") |
+ flag.Parse() |
+ |
+ http.HandleFunc("/", handleRoot) |
+ http.HandleFunc(oauthCallbackPath, handleOAuth2Callback) |
+ http.Handle("/res/", http.FileServer(http.Dir(curdir))) |
+ port := ":" + strconv.Itoa(defaultPort) |
+ log.Println("Server is running at " + scheme + "://" + localHost + port) |
+ var err error |
+ if public { |
+ log.Println("WARNING: This server is not secure and should not be made " + |
+ "publicly accessible.") |
+ scheme = "https" |
+ err = http.ListenAndServeTLS(port, certFile, keyFile, nil) |
+ } else { |
+ scheme = "http" |
+ err = http.ListenAndServe(localHost+port, nil) |
+ } |
+ if err != nil { |
+ log.Println(err.Error()) |
+ } |
+} |