| 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..b4ea21035e1757967777f0a7c9bdd37d46d1e4f4
|
| --- /dev/null
|
| +++ b/tools/bug_chomper/src/server/server.go
|
| @@ -0,0 +1,363 @@
|
| +// 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"
|
| + "fmt"
|
| + "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 oauthCallbackPath = "/oauth2callback"
|
| +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 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")))
|
| +
|
| +// 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
|
| +// 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(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
|
| +}
|
| +
|
| +// makeSession creates a new session for the Request.
|
| +func makeSession(
|
| + w http.ResponseWriter, r *http.Request) (*SessionState, error) {
|
| + sid := makeSid()
|
| + issueTracker, err := issue_tracker.MakeIssueTracker(oauthConfigFile)
|
| + 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(),
|
| + }
|
| + log.Println("Started session with SID: ", sid)
|
| + sessionStates[sid] = &session
|
| + cookie := makeCookieForSession(sid)
|
| + http.SetCookie(w, 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(
|
| + w http.ResponseWriter, r *http.Request) (*SessionState, error) {
|
| + cookie, err := r.Cookie(sidCookieName)
|
| + if err != nil {
|
| + // This is a new session. Create a SessionState.
|
| + return makeSession(w, r)
|
| + }
|
| + sid := cookie.Value
|
| + session, ok := sessionStates[sid]
|
| + if !ok {
|
| + // Unknown session. Start a new one.
|
| + return makeSession(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
|
| + }
|
| + pageData := 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,
|
| + }
|
| + err = templates.ExecuteTemplate(w, "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. 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(
|
| + getOAuth2CallbackURL(r))
|
| + 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
|
| + 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(
|
| + w, "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(w, "submitted.html", pageData)
|
| + if 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":
|
| + err = makeBugChomperPage(w, r)
|
| + if err != nil {
|
| + return err
|
| + }
|
| + case "POST":
|
| + err = submitData(w, r)
|
| + if 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, http.StatusForbidden)
|
| + return
|
| + }
|
| + code, ok := params["code"]
|
| + if !ok {
|
| + http.Error(w, invalidLogin, http.StatusForbidden)
|
| + return
|
| + }
|
| + log.Println("Upgrading auth token:", code[0])
|
| + err = issueTracker.UpgradeCode(code[0])
|
| + if err != nil {
|
| + errMsg := "failed to upgrade token: " + err.Error()
|
| + http.Error(w, errMsg, http.StatusForbidden)
|
| + return
|
| + }
|
| + http.Redirect(w, r, session.origRequestURL,
|
| + http.StatusTemporaryRedirect)
|
| + return
|
| +}
|
| +
|
| +// handle is the top-level handler function for all HTTP requests coming to
|
| +// 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" {
|
| + err := handleBugChomper(w, r)
|
| + if err != nil {
|
| + http.Error(w, err.Error(),
|
| + http.StatusInternalServerError)
|
| + log.Println(err.Error())
|
| + }
|
| + return
|
| + }
|
| + http.NotFound(w, r)
|
| +}
|
| +
|
| +// Run the BugChomper server.
|
| +func main() {
|
| + 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 " + 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())
|
| + }
|
| +}
|
|
|