// 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()) } }