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