Chromium Code Reviews| 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..a37bace940a2648fb7952636570c034c8841ddb5 |
| --- /dev/null |
| +++ b/tools/bug_chomper/src/server/server.go |
| @@ -0,0 +1,362 @@ |
| +// 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" |
| + "errors" |
| + "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"))) |
| + |
| +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 |
| +} |
| + |
| +// makeBugChomperPage builds and serves the BugChomper page. |
| +func makeBugChomperPage(w http.ResponseWriter, r *http.Request) error { |
| + session, err := getSession(w, r) |
| + if err != nil { |
| + return err |
| + } |
| + issueTracker := session.IssueTracker |
| + user, err := issueTracker.GetLoggedInUser() |
| + if err != nil { |
| + return err |
| + } |
| + 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 |
| + } |
| + 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, |
| + } |
| + |
| + return templates.ExecuteTemplate(w, "bug_chomper.html", data) |
| +} |
| + |
| +// 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, error) { |
| + session, err := getSession(w, r) |
| + if err != nil { |
| + return false, err |
| + } |
| + issueTracker := session.IssueTracker |
| + if !issueTracker.IsAuthenticated() { |
| + loginURL := issueTracker.MakeAuthRequestURL() |
| + log.Println("Redirecting for login:", loginURL) |
| + http.Redirect(w, r, loginURL, http.StatusTemporaryRedirect) |
| + return true, nil |
| + } |
| + return false, nil |
| +} |
| + |
| +// submitData attempts to submit data from a POST request to the IssueTracker. |
| +func submitData(w http.ResponseWriter, r *http.Request) error { |
| + session, err := getSession(w, r) |
| + if err != nil { |
| + return err |
| + } |
| + issueTracker := session.IssueTracker |
| + edits := r.FormValue("all_edits") |
| + var editsMap map[string]*issue_tracker.Issue |
| + if err := json.Unmarshal([]byte(edits), &editsMap); err != nil { |
| + return errors.New( |
| + "Could not parse edits from form response: " + |
| + err.Error()) |
| + } |
| + 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 { |
| + return err |
| + } |
| + return nil |
| + } |
| + 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" |
| + } |
| + return errors.New( |
| + "Not all changes could be submitted: \n" + |
| + errorStrings) |
| + } |
| + 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 { |
| + return err |
| + } |
| + return nil |
| +} |
| + |
| +// handleBugChomper handles HTTP requests for the bug_chomper page. |
| +func handleBugChomper(w http.ResponseWriter, r *http.Request) error { |
| + redirected, err := authIfNeeded(w, r) |
| + if err != nil { |
| + return err |
| + } |
| + if redirected { |
| + return nil |
| + } |
| + switch r.Method { |
| + case "GET": |
| + if err := makeBugChomperPage(w, r); err != nil { |
|
jcgregorio
2014/05/13 16:28:12
I think the code would get simpler by not returnin
borenet
2014/05/13 17:43:20
Done.
|
| + return err |
| + } |
| + case "POST": |
| + if err := submitData(w, r); err != nil { |
| + return err |
| + } |
| + } |
| + return nil |
| +} |
| + |
| +// handleOAuth2Callback handles callbacks from the OAuth2 sign-in. |
| +func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) { |
| + session, err := getSession(w, r) |
| + if err != nil { |
| + http.Error(w, err.Error(), http.StatusInternalServerError) |
| + } |
| + issueTracker := session.IssueTracker |
| + invalidLogin := "Invalid login credentials" |
| + params, err := url.ParseQuery(r.URL.RawQuery) |
| + if err != nil { |
| + http.Error(w, invalidLogin + ": " + err.Error(), http.StatusForbidden) |
| + return |
| + } |
| + code, ok := params["code"] |
| + if !ok { |
| + http.Error(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() |
| + http.Error(w, errMsg, http.StatusForbidden) |
| + return |
| + } |
| + if err := saveSession(session, w, r); err != nil { |
| + http.Error(w, "failed to save session: "+ err.Error(), |
| + http.StatusInternalServerError) |
| + } |
| + http.Redirect(w, r, session.OrigRequestURL, http.StatusTemporaryRedirect) |
| + return |
| +} |
| + |
| +// handle is the top-level handler function for all HTTP requests coming to |
|
jcgregorio
2014/05/13 16:28:12
Comment no longer correct.
|
| +// this server. |
| +func handle(w http.ResponseWriter, r *http.Request) { |
| + log.Println("Fetching " + r.URL.Path) |
| + if r.URL.Path == "/" || r.URL.Path == "/index.html" { |
| + if err := handleBugChomper(w, r); err != nil { |
| + http.Error(w, err.Error(), |
| + http.StatusInternalServerError) |
| + log.Println(err.Error()) |
| + } |
| + 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("/", handle) |
| + 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()) |
| + } |
| +} |