Home | History | Annotate | Download | only in server
      1 // Copyright (c) 2014 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 /*
      6 	Serves a webpage for easy management of Skia bugs.
      7 
      8 	WARNING: This server is NOT secure and should not be made publicly
      9 	accessible.
     10 */
     11 
     12 package main
     13 
     14 import (
     15 	"encoding/json"
     16 	"flag"
     17 	"fmt"
     18 	"html/template"
     19 	"issue_tracker"
     20 	"log"
     21 	"net/http"
     22 	"net/url"
     23 	"path"
     24 	"path/filepath"
     25 	"strconv"
     26 	"strings"
     27 	"time"
     28 )
     29 
     30 import "github.com/gorilla/securecookie"
     31 
     32 const certFile = "certs/cert.pem"
     33 const keyFile = "certs/key.pem"
     34 const issueComment = "Edited by BugChomper"
     35 const oauthCallbackPath = "/oauth2callback"
     36 const oauthConfigFile = "oauth_client_secret.json"
     37 const defaultPort = 8000
     38 const localHost = "127.0.0.1"
     39 const maxSessionLen = time.Duration(3600 * time.Second)
     40 const priorityPrefix = "Priority-"
     41 const project = "skia"
     42 const cookieName = "BugChomperCookie"
     43 
     44 var scheme = "http"
     45 
     46 var curdir, _ = filepath.Abs(".")
     47 var templatePath, _ = filepath.Abs("templates")
     48 var templates = template.Must(template.ParseFiles(
     49 	path.Join(templatePath, "bug_chomper.html"),
     50 	path.Join(templatePath, "submitted.html"),
     51 	path.Join(templatePath, "error.html")))
     52 
     53 var hashKey = securecookie.GenerateRandomKey(32)
     54 var blockKey = securecookie.GenerateRandomKey(32)
     55 var secureCookie = securecookie.New(hashKey, blockKey)
     56 
     57 // SessionState contains data for a given session.
     58 type SessionState struct {
     59 	IssueTracker   *issue_tracker.IssueTracker
     60 	OrigRequestURL string
     61 	SessionStart   time.Time
     62 }
     63 
     64 // getAbsoluteURL returns the absolute URL of the given Request.
     65 func getAbsoluteURL(r *http.Request) string {
     66 	return scheme + "://" + r.Host + r.URL.Path
     67 }
     68 
     69 // getOAuth2CallbackURL returns a callback URL to be used by the OAuth2 login
     70 // page.
     71 func getOAuth2CallbackURL(r *http.Request) string {
     72 	return scheme + "://" + r.Host + oauthCallbackPath
     73 }
     74 
     75 func saveSession(session *SessionState, w http.ResponseWriter, r *http.Request) error {
     76 	encodedSession, err := secureCookie.Encode(cookieName, session)
     77 	if err != nil {
     78 		return fmt.Errorf("unable to encode session state: %s", err)
     79 	}
     80 	cookie := &http.Cookie{
     81 		Name:     cookieName,
     82 		Value:    encodedSession,
     83 		Domain:   strings.Split(r.Host, ":")[0],
     84 		Path:     "/",
     85 		HttpOnly: true,
     86 	}
     87 	http.SetCookie(w, cookie)
     88 	return nil
     89 }
     90 
     91 // makeSession creates a new session for the Request.
     92 func makeSession(w http.ResponseWriter, r *http.Request) (*SessionState, error) {
     93 	log.Println("Creating new session.")
     94 	// Create the session state.
     95 	issueTracker, err := issue_tracker.MakeIssueTracker(
     96 		oauthConfigFile, getOAuth2CallbackURL(r))
     97 	if err != nil {
     98 		return nil, fmt.Errorf("unable to create IssueTracker for session: %s", err)
     99 	}
    100 	session := SessionState{
    101 		IssueTracker:   issueTracker,
    102 		OrigRequestURL: getAbsoluteURL(r),
    103 		SessionStart:   time.Now(),
    104 	}
    105 
    106 	// Encode and store the session state.
    107 	if err := saveSession(&session, w, r); err != nil {
    108 		return nil, err
    109 	}
    110 
    111 	return &session, nil
    112 }
    113 
    114 // getSession retrieves the active SessionState or creates and returns a new
    115 // SessionState.
    116 func getSession(w http.ResponseWriter, r *http.Request) (*SessionState, error) {
    117 	cookie, err := r.Cookie(cookieName)
    118 	if err != nil {
    119 		log.Println("No cookie found! Starting new session.")
    120 		return makeSession(w, r)
    121 	}
    122 	var session SessionState
    123 	if err := secureCookie.Decode(cookieName, cookie.Value, &session); err != nil {
    124 		log.Printf("Invalid or corrupted session. Starting another: %s", err.Error())
    125 		return makeSession(w, r)
    126 	}
    127 
    128 	currentTime := time.Now()
    129 	if currentTime.Sub(session.SessionStart) > maxSessionLen {
    130 		log.Printf("Session starting at %s is expired. Starting another.",
    131 			session.SessionStart.Format(time.RFC822))
    132 		return makeSession(w, r)
    133 	}
    134 	saveSession(&session, w, r)
    135 	return &session, nil
    136 }
    137 
    138 // reportError serves the error page with the given message.
    139 func reportError(w http.ResponseWriter, msg string, code int) {
    140 	errData := struct {
    141 		Code       int
    142 		CodeString string
    143 		Message    string
    144 	}{
    145 		Code:       code,
    146 		CodeString: http.StatusText(code),
    147 		Message:    msg,
    148 	}
    149 	w.WriteHeader(code)
    150 	err := templates.ExecuteTemplate(w, "error.html", errData)
    151 	if err != nil {
    152 		log.Println("Failed to display error.html!!")
    153 	}
    154 }
    155 
    156 // makeBugChomperPage builds and serves the BugChomper page.
    157 func makeBugChomperPage(w http.ResponseWriter, r *http.Request) {
    158 	session, err := getSession(w, r)
    159 	if err != nil {
    160 		reportError(w, err.Error(), http.StatusInternalServerError)
    161 		return
    162 	}
    163 	issueTracker := session.IssueTracker
    164 	user, err := issueTracker.GetLoggedInUser()
    165 	if err != nil {
    166 		reportError(w, err.Error(), http.StatusInternalServerError)
    167 		return
    168 	}
    169 	log.Println("Loading bugs for " + user)
    170 	bugList, err := issueTracker.GetBugs(project, user)
    171 	if err != nil {
    172 		reportError(w, err.Error(), http.StatusInternalServerError)
    173 		return
    174 	}
    175 	bugsById := make(map[string]*issue_tracker.Issue)
    176 	bugsByPriority := make(map[string][]*issue_tracker.Issue)
    177 	for _, bug := range bugList.Items {
    178 		bugsById[strconv.Itoa(bug.Id)] = bug
    179 		var bugPriority string
    180 		for _, label := range bug.Labels {
    181 			if strings.HasPrefix(label, priorityPrefix) {
    182 				bugPriority = label[len(priorityPrefix):]
    183 			}
    184 		}
    185 		if _, ok := bugsByPriority[bugPriority]; !ok {
    186 			bugsByPriority[bugPriority] = make(
    187 				[]*issue_tracker.Issue, 0)
    188 		}
    189 		bugsByPriority[bugPriority] = append(
    190 			bugsByPriority[bugPriority], bug)
    191 	}
    192 	bugsJson, err := json.Marshal(bugsById)
    193 	if err != nil {
    194 		reportError(w, err.Error(), http.StatusInternalServerError)
    195 		return
    196 	}
    197 	data := struct {
    198 		Title          string
    199 		User           string
    200 		BugsJson       template.JS
    201 		BugsByPriority *map[string][]*issue_tracker.Issue
    202 		Priorities     []string
    203 		PriorityPrefix string
    204 	}{
    205 		Title:          "BugChomper",
    206 		User:           user,
    207 		BugsJson:       template.JS(string(bugsJson)),
    208 		BugsByPriority: &bugsByPriority,
    209 		Priorities:     issue_tracker.BugPriorities,
    210 		PriorityPrefix: priorityPrefix,
    211 	}
    212 
    213 	if err := templates.ExecuteTemplate(w, "bug_chomper.html", data); err != nil {
    214 		reportError(w, err.Error(), http.StatusInternalServerError)
    215 		return
    216 	}
    217 }
    218 
    219 // authIfNeeded determines whether the current user is logged in. If not, it
    220 // redirects to a login page. Returns true if the user is redirected and false
    221 // otherwise.
    222 func authIfNeeded(w http.ResponseWriter, r *http.Request) bool {
    223 	session, err := getSession(w, r)
    224 	if err != nil {
    225 		reportError(w, err.Error(), http.StatusInternalServerError)
    226 		return false
    227 	}
    228 	issueTracker := session.IssueTracker
    229 	if !issueTracker.IsAuthenticated() {
    230 		loginURL := issueTracker.MakeAuthRequestURL()
    231 		log.Println("Redirecting for login:", loginURL)
    232 		http.Redirect(w, r, loginURL, http.StatusTemporaryRedirect)
    233 		return true
    234 	}
    235 	return false
    236 }
    237 
    238 // submitData attempts to submit data from a POST request to the IssueTracker.
    239 func submitData(w http.ResponseWriter, r *http.Request) {
    240 	session, err := getSession(w, r)
    241 	if err != nil {
    242 		reportError(w, err.Error(), http.StatusInternalServerError)
    243 		return
    244 	}
    245 	issueTracker := session.IssueTracker
    246 	edits := r.FormValue("all_edits")
    247 	var editsMap map[string]*issue_tracker.Issue
    248 	if err := json.Unmarshal([]byte(edits), &editsMap); err != nil {
    249 		errMsg := "Could not parse edits from form response: " + err.Error()
    250 		reportError(w, errMsg, http.StatusInternalServerError)
    251 		return
    252 	}
    253 	data := struct {
    254 		Title    string
    255 		Message  string
    256 		BackLink string
    257 	}{}
    258 	if len(editsMap) == 0 {
    259 		data.Title = "No Changes Submitted"
    260 		data.Message = "You didn't change anything!"
    261 		data.BackLink = ""
    262 		if err := templates.ExecuteTemplate(w, "submitted.html", data); err != nil {
    263 			reportError(w, err.Error(), http.StatusInternalServerError)
    264 			return
    265 		}
    266 		return
    267 	}
    268 	errorList := make([]error, 0)
    269 	for issueId, newIssue := range editsMap {
    270 		log.Println("Editing issue " + issueId)
    271 		if err := issueTracker.SubmitIssueChanges(newIssue, issueComment); err != nil {
    272 			errorList = append(errorList, err)
    273 		}
    274 	}
    275 	if len(errorList) > 0 {
    276 		errorStrings := ""
    277 		for _, err := range errorList {
    278 			errorStrings += err.Error() + "\n"
    279 		}
    280 		errMsg := "Not all changes could be submitted: \n" + errorStrings
    281 		reportError(w, errMsg, http.StatusInternalServerError)
    282 		return
    283 	}
    284 	data.Title = "Submitted Changes"
    285 	data.Message = "Your changes were submitted to the issue tracker."
    286 	data.BackLink = ""
    287 	if err := templates.ExecuteTemplate(w, "submitted.html", data); err != nil {
    288 		reportError(w, err.Error(), http.StatusInternalServerError)
    289 		return
    290 	}
    291 	return
    292 }
    293 
    294 // handleBugChomper handles HTTP requests for the bug_chomper page.
    295 func handleBugChomper(w http.ResponseWriter, r *http.Request) {
    296 	if authIfNeeded(w, r) {
    297 		return
    298 	}
    299 	switch r.Method {
    300 	case "GET":
    301 		makeBugChomperPage(w, r)
    302 	case "POST":
    303 		submitData(w, r)
    304 	}
    305 }
    306 
    307 // handleOAuth2Callback handles callbacks from the OAuth2 sign-in.
    308 func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) {
    309 	session, err := getSession(w, r)
    310 	if err != nil {
    311 		reportError(w, err.Error(), http.StatusInternalServerError)
    312 	}
    313 	issueTracker := session.IssueTracker
    314 	invalidLogin := "Invalid login credentials"
    315 	params, err := url.ParseQuery(r.URL.RawQuery)
    316 	if err != nil {
    317 		reportError(w, invalidLogin+": "+err.Error(), http.StatusForbidden)
    318 		return
    319 	}
    320 	code, ok := params["code"]
    321 	if !ok {
    322 		reportError(w, invalidLogin+": redirect did not include auth code.",
    323 			http.StatusForbidden)
    324 		return
    325 	}
    326 	log.Println("Upgrading auth token:", code[0])
    327 	if err := issueTracker.UpgradeCode(code[0]); err != nil {
    328 		errMsg := "failed to upgrade token: " + err.Error()
    329 		reportError(w, errMsg, http.StatusForbidden)
    330 		return
    331 	}
    332 	if err := saveSession(session, w, r); err != nil {
    333 		reportError(w, "failed to save session: "+err.Error(),
    334 			http.StatusInternalServerError)
    335 		return
    336 	}
    337 	http.Redirect(w, r, session.OrigRequestURL, http.StatusTemporaryRedirect)
    338 	return
    339 }
    340 
    341 // handleRoot is the handler function for all HTTP requests at the root level.
    342 func handleRoot(w http.ResponseWriter, r *http.Request) {
    343 	log.Println("Fetching " + r.URL.Path)
    344 	if r.URL.Path == "/" || r.URL.Path == "/index.html" {
    345 		handleBugChomper(w, r)
    346 		return
    347 	}
    348 	http.NotFound(w, r)
    349 }
    350 
    351 // Run the BugChomper server.
    352 func main() {
    353 	var public bool
    354 	flag.BoolVar(
    355 		&public, "public", false, "Make this server publicly accessible.")
    356 	flag.Parse()
    357 
    358 	http.HandleFunc("/", handleRoot)
    359 	http.HandleFunc(oauthCallbackPath, handleOAuth2Callback)
    360 	http.Handle("/res/", http.FileServer(http.Dir(curdir)))
    361 	port := ":" + strconv.Itoa(defaultPort)
    362 	log.Println("Server is running at " + scheme + "://" + localHost + port)
    363 	var err error
    364 	if public {
    365 		log.Println("WARNING: This server is not secure and should not be made " +
    366 			"publicly accessible.")
    367 		scheme = "https"
    368 		err = http.ListenAndServeTLS(port, certFile, keyFile, nil)
    369 	} else {
    370 		scheme = "http"
    371 		err = http.ListenAndServe(localHost+port, nil)
    372 	}
    373 	if err != nil {
    374 		log.Println(err.Error())
    375 	}
    376 }
    377