Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 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( | |
|
jcgregorio
2014/05/13 17:50:17
nit, keep this one line.
borenet
2014/05/13 17:57:08
Done here and elsewhere.
| |
| 93 w http.ResponseWriter, r *http.Request) (*SessionState, error) { | |
| 94 log.Println("Creating new session.") | |
| 95 // Create the session state. | |
| 96 issueTracker, err := issue_tracker.MakeIssueTracker( | |
| 97 oauthConfigFile, getOAuth2CallbackURL(r)) | |
| 98 if err != nil { | |
| 99 return nil, fmt.Errorf("unable to create IssueTracker for sessio n: %s", err) | |
| 100 } | |
| 101 session := SessionState{ | |
| 102 IssueTracker: issueTracker, | |
| 103 OrigRequestURL: getAbsoluteURL(r), | |
| 104 SessionStart: time.Now(), | |
| 105 } | |
| 106 | |
| 107 // Encode and store the session state. | |
| 108 if err := saveSession(&session, w, r); err != nil { | |
| 109 return nil, err | |
| 110 } | |
| 111 | |
| 112 return &session, nil | |
| 113 } | |
| 114 | |
| 115 // getSession retrieves the active SessionState or creates and returns a new | |
| 116 // SessionState. | |
| 117 func getSession( | |
|
jcgregorio
2014/05/13 17:50:17
nit, keep this one line.
| |
| 118 w http.ResponseWriter, r *http.Request) (*SessionState, error) { | |
| 119 cookie, err := r.Cookie(cookieName) | |
| 120 if err != nil { | |
| 121 log.Println("No cookie found! Starting new session.") | |
| 122 return makeSession(w, r) | |
| 123 } | |
| 124 var session SessionState | |
| 125 if err := secureCookie.Decode(cookieName, cookie.Value, &session); err ! = nil { | |
| 126 log.Printf("Invalid or corrupted session. Starting another: %s", err.Error()) | |
| 127 return makeSession(w, r) | |
| 128 } | |
| 129 | |
| 130 currentTime := time.Now() | |
| 131 if currentTime.Sub(session.SessionStart) > maxSessionLen { | |
| 132 log.Printf("Session starting at %s is expired. Starting another. ", | |
| 133 session.SessionStart.Format(time.RFC822)) | |
| 134 return makeSession(w, r) | |
| 135 } | |
| 136 saveSession(&session, w, r) | |
| 137 return &session, nil | |
| 138 } | |
| 139 | |
| 140 // reportError serves the error page with the given message. | |
| 141 func reportError(w http.ResponseWriter, msg string, code int) { | |
| 142 errData := struct { | |
| 143 Code int | |
| 144 CodeString string | |
| 145 Message string | |
| 146 }{ | |
| 147 Code: code, | |
| 148 CodeString: http.StatusText(code), | |
| 149 Message: msg, | |
| 150 } | |
| 151 w.WriteHeader(code) | |
| 152 err := templates.ExecuteTemplate(w, "error.html", errData) | |
| 153 if err != nil { | |
| 154 log.Println("Failed to display error.html!!") | |
| 155 } | |
| 156 } | |
| 157 | |
| 158 // makeBugChomperPage builds and serves the BugChomper page. | |
| 159 func makeBugChomperPage(w http.ResponseWriter, r *http.Request) { | |
| 160 session, err := getSession(w, r) | |
| 161 if err != nil { | |
| 162 reportError(w, err.Error(), http.StatusInternalServerError) | |
| 163 return | |
| 164 } | |
| 165 issueTracker := session.IssueTracker | |
| 166 user, err := issueTracker.GetLoggedInUser() | |
| 167 if err != nil { | |
| 168 reportError(w, err.Error(), http.StatusInternalServerError) | |
| 169 return | |
| 170 } | |
| 171 log.Println("Loading bugs for " + user) | |
| 172 bugList, err := issueTracker.GetBugs(project, user) | |
| 173 if err != nil { | |
| 174 reportError(w, err.Error(), http.StatusInternalServerError) | |
| 175 return | |
| 176 } | |
| 177 bugsById := make(map[string]*issue_tracker.Issue) | |
| 178 bugsByPriority := make(map[string][]*issue_tracker.Issue) | |
| 179 for _, bug := range bugList.Items { | |
| 180 bugsById[strconv.Itoa(bug.Id)] = bug | |
| 181 var bugPriority string | |
| 182 for _, label := range bug.Labels { | |
| 183 if strings.HasPrefix(label, priorityPrefix) { | |
| 184 bugPriority = label[len(priorityPrefix):] | |
| 185 } | |
| 186 } | |
| 187 if _, ok := bugsByPriority[bugPriority]; !ok { | |
| 188 bugsByPriority[bugPriority] = make( | |
| 189 []*issue_tracker.Issue, 0) | |
| 190 } | |
| 191 bugsByPriority[bugPriority] = append( | |
| 192 bugsByPriority[bugPriority], bug) | |
| 193 } | |
| 194 bugsJson, err := json.Marshal(bugsById) | |
| 195 if err != nil { | |
| 196 reportError(w, err.Error(), http.StatusInternalServerError) | |
| 197 return | |
| 198 } | |
| 199 data := struct { | |
| 200 Title string | |
| 201 User string | |
| 202 BugsJson template.JS | |
| 203 BugsByPriority *map[string][]*issue_tracker.Issue | |
| 204 Priorities []string | |
| 205 PriorityPrefix string | |
| 206 }{ | |
| 207 Title: "BugChomper", | |
| 208 User: user, | |
| 209 BugsJson: template.JS(string(bugsJson)), | |
| 210 BugsByPriority: &bugsByPriority, | |
| 211 Priorities: issue_tracker.BugPriorities, | |
| 212 PriorityPrefix: priorityPrefix, | |
| 213 } | |
| 214 | |
| 215 if err:= templates.ExecuteTemplate(w, "bug_chomper.html", data); err != nil { | |
| 216 reportError(w, err.Error(), http.StatusInternalServerError) | |
| 217 return | |
| 218 } | |
| 219 } | |
| 220 | |
| 221 // authIfNeeded determines whether the current user is logged in. If not, it | |
| 222 // redirects to a login page. Returns true if the user is redirected and false | |
| 223 // otherwise. | |
| 224 func authIfNeeded(w http.ResponseWriter, r *http.Request) bool { | |
| 225 session, err := getSession(w, r) | |
| 226 if err != nil { | |
| 227 reportError(w, err.Error(), http.StatusInternalServerError) | |
| 228 return false | |
| 229 } | |
| 230 issueTracker := session.IssueTracker | |
| 231 if !issueTracker.IsAuthenticated() { | |
| 232 loginURL := issueTracker.MakeAuthRequestURL() | |
| 233 log.Println("Redirecting for login:", loginURL) | |
| 234 http.Redirect(w, r, loginURL, http.StatusTemporaryRedirect) | |
| 235 return true | |
| 236 } | |
| 237 return false | |
| 238 } | |
| 239 | |
| 240 // submitData attempts to submit data from a POST request to the IssueTracker. | |
| 241 func submitData(w http.ResponseWriter, r *http.Request) { | |
| 242 session, err := getSession(w, r) | |
| 243 if err != nil { | |
| 244 reportError(w, err.Error(), http.StatusInternalServerError) | |
| 245 return | |
| 246 } | |
| 247 issueTracker := session.IssueTracker | |
| 248 edits := r.FormValue("all_edits") | |
| 249 var editsMap map[string]*issue_tracker.Issue | |
| 250 if err := json.Unmarshal([]byte(edits), &editsMap); err != nil { | |
| 251 errMsg := "Could not parse edits from form response: " + err.Err or() | |
| 252 reportError(w, errMsg, http.StatusInternalServerError) | |
| 253 return | |
| 254 } | |
| 255 data := struct { | |
| 256 Title string | |
| 257 Message string | |
| 258 BackLink string | |
| 259 }{} | |
| 260 if len(editsMap) == 0 { | |
| 261 data.Title = "No Changes Submitted" | |
| 262 data.Message = "You didn't change anything!" | |
| 263 data.BackLink = "" | |
| 264 if err := templates.ExecuteTemplate(w, "submitted.html", data); err != nil { | |
| 265 reportError(w, err.Error(), http.StatusInternalServerErr or) | |
| 266 return | |
| 267 } | |
| 268 return | |
| 269 } | |
| 270 errorList := make([]error, 0) | |
| 271 for issueId, newIssue := range editsMap { | |
| 272 log.Println("Editing issue " + issueId) | |
| 273 if err := issueTracker.SubmitIssueChanges(newIssue, issueComment ); err != nil { | |
| 274 errorList = append(errorList, err) | |
| 275 } | |
| 276 } | |
| 277 if len(errorList) > 0 { | |
| 278 errorStrings := "" | |
| 279 for _, err := range errorList { | |
| 280 errorStrings += err.Error() + "\n" | |
| 281 } | |
| 282 errMsg := "Not all changes could be submitted: \n" + errorString s | |
| 283 reportError(w, errMsg, http.StatusInternalServerError) | |
| 284 return | |
| 285 } | |
| 286 data.Title = "Submitted Changes" | |
| 287 data.Message = "Your changes were submitted to the issue tracker." | |
| 288 data.BackLink = "" | |
| 289 if err := templates.ExecuteTemplate(w, "submitted.html", data); err != n il { | |
| 290 reportError(w, err.Error(), http.StatusInternalServerError) | |
| 291 return | |
| 292 } | |
| 293 return | |
| 294 } | |
| 295 | |
| 296 // handleBugChomper handles HTTP requests for the bug_chomper page. | |
| 297 func handleBugChomper(w http.ResponseWriter, r *http.Request) { | |
| 298 if authIfNeeded(w, r) { | |
| 299 return | |
| 300 } | |
| 301 switch r.Method { | |
| 302 case "GET": | |
| 303 makeBugChomperPage(w, r) | |
| 304 case "POST": | |
| 305 submitData(w, r) | |
| 306 } | |
| 307 } | |
| 308 | |
| 309 // handleOAuth2Callback handles callbacks from the OAuth2 sign-in. | |
| 310 func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) { | |
| 311 session, err := getSession(w, r) | |
| 312 if err != nil { | |
| 313 reportError(w, err.Error(), http.StatusInternalServerError) | |
| 314 } | |
| 315 issueTracker := session.IssueTracker | |
| 316 invalidLogin := "Invalid login credentials" | |
| 317 params, err := url.ParseQuery(r.URL.RawQuery) | |
| 318 if err != nil { | |
| 319 reportError(w, invalidLogin + ": " + err.Error(), http.StatusFor bidden) | |
| 320 return | |
| 321 } | |
| 322 code, ok := params["code"] | |
| 323 if !ok { | |
| 324 reportError(w, invalidLogin + ": redirect did not include auth c ode.", | |
| 325 http.StatusForbidden) | |
| 326 return | |
| 327 } | |
| 328 log.Println("Upgrading auth token:", code[0]) | |
| 329 if err := issueTracker.UpgradeCode(code[0]); err != nil { | |
| 330 errMsg := "failed to upgrade token: " + err.Error() | |
| 331 reportError(w, errMsg, http.StatusForbidden) | |
| 332 return | |
| 333 } | |
| 334 if err := saveSession(session, w, r); err != nil { | |
| 335 reportError(w, "failed to save session: "+ err.Error(), | |
| 336 http.StatusInternalServerError) | |
| 337 return | |
| 338 } | |
| 339 http.Redirect(w, r, session.OrigRequestURL, http.StatusTemporaryRedirect ) | |
| 340 return | |
| 341 } | |
| 342 | |
| 343 // handleRoot is the handler function for all HTTP requests at the root level. | |
| 344 func handleRoot(w http.ResponseWriter, r *http.Request) { | |
| 345 log.Println("Fetching " + r.URL.Path) | |
| 346 if r.URL.Path == "/" || r.URL.Path == "/index.html" { | |
| 347 handleBugChomper(w, r) | |
| 348 return | |
| 349 } | |
| 350 http.NotFound(w, r) | |
| 351 } | |
| 352 | |
| 353 // Run the BugChomper server. | |
| 354 func main() { | |
| 355 var public bool | |
| 356 flag.BoolVar( | |
| 357 &public, "public", false, "Make this server publicly accessible. ") | |
| 358 flag.Parse() | |
| 359 | |
| 360 http.HandleFunc("/", handleRoot) | |
| 361 http.HandleFunc(oauthCallbackPath, handleOAuth2Callback) | |
| 362 http.Handle("/res/", http.FileServer(http.Dir(curdir))) | |
| 363 port := ":" + strconv.Itoa(defaultPort) | |
| 364 log.Println("Server is running at " + scheme + "://" + localHost + port) | |
| 365 var err error | |
| 366 if public { | |
| 367 log.Println("WARNING: This server is not secure and should not b e made " + | |
| 368 "publicly accessible.") | |
| 369 scheme = "https" | |
| 370 err = http.ListenAndServeTLS(port, certFile, keyFile, nil) | |
| 371 } else { | |
| 372 scheme = "http" | |
| 373 err = http.ListenAndServe(localHost+port, nil) | |
| 374 } | |
| 375 if err != nil { | |
| 376 log.Println(err.Error()) | |
| 377 } | |
| 378 } | |
| OLD | NEW |