Golang程序  |  377行  |  10.69 KB

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