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 "crypto/rand" | |
| 16 "encoding/base64" | |
| 17 "encoding/json" | |
| 18 "errors" | |
| 19 "html/template" | |
| 20 "issue_tracker" | |
| 21 "log" | |
| 22 "net/http" | |
| 23 "net/url" | |
| 24 "path" | |
| 25 "path/filepath" | |
| 26 "strconv" | |
| 27 "strings" | |
| 28 "time" | |
| 29 ) | |
| 30 | |
| 31 const certFile = "certs/cert.pem" | |
| 32 const keyFile = "certs/key.pem" | |
| 33 const issueComment = "Edited by BugChomper" | |
| 34 const oauthConfigFile = "oauth_client_secret.json" | |
| 35 const defaultPort = 8000 | |
| 36 const maxSessionLen = 3600 | |
| 37 const priorityPrefix = "Priority-" | |
| 38 const project = "skia" | |
| 39 const scheme = "http" | |
| 40 const sidBytes = 64 | |
| 41 const sidCookieName = "BugChomperSID" | |
| 42 | |
| 43 var contentTypesByExtension = map[string]string{ | |
|
jcgregorio
2014/05/07 19:32:25
Is there a reason you didn't use http://golang.org
borenet
2014/05/07 22:27:37
Didn't know about it!
| |
| 44 "css": "text/css", | |
| 45 "js": "application/javascript", | |
| 46 "ico": "image/x-icon", | |
| 47 "htm": "text/html", | |
| 48 "html": "text/html", | |
| 49 } | |
| 50 var staticPath, _ = filepath.Abs("static") | |
| 51 var templatePath, _ = filepath.Abs("templates") | |
| 52 var templates = template.Must(template.ParseFiles( | |
| 53 path.Join(templatePath, "bug_chomper.html"), | |
| 54 path.Join(templatePath, "submitted.html"))) | |
| 55 | |
| 56 // SessionState contains data for a given session. | |
| 57 type SessionState struct { | |
| 58 issueTracker *issue_tracker.IssueTracker | |
| 59 origRequestURL string | |
| 60 sessionStart time.Time | |
| 61 } | |
| 62 | |
| 63 // sessionStates contains the state for all sessions. It's leaky because | |
|
jcgregorio
2014/05/07 19:32:25
It also goes away when the server restarts. This w
borenet
2014/05/07 22:27:37
So:
- (de)serialize session states on startup/shut
jcgregorio
2014/05/08 15:11:56
Again, the choices depend on if this is going to r
| |
| 64 // nothing ever gets removed! | |
| 65 var sessionStates = make(map[string]*SessionState) | |
| 66 | |
| 67 // makeSid returns a randomly-generated session identifier. | |
| 68 func makeSid() string { | |
| 69 sid := make([]byte, sidBytes) | |
| 70 rand.Read(sid) | |
| 71 encodedSid := make([]byte, base64.StdEncoding.EncodedLen(len(sid))) | |
| 72 base64.StdEncoding.Encode(encodedSid, sid) | |
| 73 return string(encodedSid) | |
| 74 } | |
| 75 | |
| 76 // getAbsoluteURL returns the absolute URL of the given Request. | |
| 77 func getAbsoluteURL(req *http.Request) string { | |
| 78 return scheme + "://" + req.Host + req.URL.Path | |
| 79 } | |
| 80 | |
| 81 // makeSession creates a new session for the Request. | |
| 82 func makeSession( | |
| 83 resp http.ResponseWriter, req *http.Request) (*SessionState, error) { | |
| 84 sid := makeSid() | |
| 85 issueTracker, err := issue_tracker.MakeIssueTracker(oauthConfigFile) | |
| 86 if err != nil { | |
| 87 return nil, err | |
| 88 } | |
| 89 session := SessionState{ | |
| 90 issueTracker: issueTracker, | |
| 91 origRequestURL: getAbsoluteURL(req), | |
| 92 sessionStart: time.Now(), | |
| 93 } | |
| 94 log.Println("Started session with SID: ", sid) | |
| 95 sessionStates[sid] = &session | |
| 96 cookie := makeCookieForSession(sid) | |
| 97 http.SetCookie(resp, cookie) | |
| 98 return &session, nil | |
| 99 } | |
| 100 | |
| 101 // makeCookieForSession creates a Cookie containing the session ID for this | |
| 102 // session. | |
| 103 func makeCookieForSession(sid string) *http.Cookie { | |
| 104 expires := time.Now().Add(time.Duration(maxSessionLen * time.Second)) | |
| 105 cookie := http.Cookie{ | |
| 106 Name: sidCookieName, | |
| 107 Value: sid, | |
| 108 Path: "", | |
| 109 Domain: "", | |
| 110 Expires: expires, | |
| 111 RawExpires: expires.String(), | |
| 112 MaxAge: maxSessionLen, | |
| 113 Secure: false, | |
| 114 HttpOnly: true, | |
| 115 Raw: "", | |
| 116 Unparsed: nil, | |
| 117 } | |
| 118 return &cookie | |
| 119 } | |
| 120 | |
| 121 // getSession retrieves the active SessionState or creates and returns a new | |
| 122 // SessionState. | |
| 123 func getSession( | |
| 124 resp http.ResponseWriter, req *http.Request) (*SessionState, error) { | |
| 125 cookie, err := req.Cookie(sidCookieName) | |
| 126 if err != nil { | |
| 127 // This is a new session. Create a SessionState. | |
| 128 return makeSession(resp, req) | |
| 129 } | |
| 130 sid := cookie.Value | |
| 131 session, ok := sessionStates[sid] | |
| 132 if !ok { | |
| 133 // Unknown session. Start a new one. | |
| 134 return makeSession(resp, req) | |
| 135 } | |
| 136 return session, nil | |
| 137 } | |
| 138 | |
| 139 // makeBugChomperPage builds and serves the BugChomper page. | |
| 140 func makeBugChomperPage(resp http.ResponseWriter, req *http.Request) error { | |
| 141 session, err := getSession(resp, req) | |
| 142 if err != nil { | |
| 143 return err | |
| 144 } | |
| 145 issueTracker := session.issueTracker | |
| 146 user := "borenet@google.com" | |
| 147 log.Println("Loading bugs for " + user) | |
| 148 bugList, err := issueTracker.GetBugs(project, user) | |
| 149 if err != nil { | |
| 150 return err | |
| 151 } | |
| 152 bugsById := make(map[string]*issue_tracker.Issue) | |
| 153 bugsByPriority := make(map[string][]*issue_tracker.Issue) | |
| 154 for _, bug := range bugList.Items { | |
| 155 bugsById[strconv.Itoa(bug.Id)] = bug | |
| 156 var bugPriority string | |
| 157 for _, label := range bug.Labels { | |
| 158 if strings.HasPrefix(label, priorityPrefix) { | |
| 159 bugPriority = label[len(priorityPrefix):] | |
| 160 } | |
| 161 } | |
| 162 if _, ok := bugsByPriority[bugPriority]; !ok { | |
| 163 bugsByPriority[bugPriority] = make( | |
| 164 []*issue_tracker.Issue, 0) | |
| 165 } | |
| 166 bugsByPriority[bugPriority] = append( | |
| 167 bugsByPriority[bugPriority], bug) | |
| 168 } | |
| 169 bugsJson, err := json.Marshal(bugsById) | |
| 170 if err != nil { | |
| 171 return err | |
| 172 } | |
| 173 pageData := struct { | |
| 174 Title string | |
| 175 User string | |
| 176 BugsJson template.JS | |
| 177 BugsByPriority *map[string][]*issue_tracker.Issue | |
| 178 Priorities []string | |
| 179 PriorityPrefix string | |
| 180 }{ | |
| 181 Title: "BugChomper", | |
| 182 User: "borenet@google.com", | |
| 183 BugsJson: template.JS(string(bugsJson)), | |
| 184 BugsByPriority: &bugsByPriority, | |
| 185 Priorities: issue_tracker.BugPriorities, | |
| 186 PriorityPrefix: priorityPrefix, | |
| 187 } | |
| 188 err = templates.ExecuteTemplate(resp, "bug_chomper.html", pageData) | |
| 189 if err != nil { | |
| 190 return err | |
| 191 } | |
| 192 return nil | |
| 193 } | |
| 194 | |
| 195 // authIfNeeded determines whether the current user is logged in. If not, it | |
| 196 // redirects to a login page. If the user is just returning from the login, | |
| 197 // it upgrades the single-use authorization code obtained from the login page | |
| 198 // and redirects to the original request URL. Returns true if the user is | |
| 199 // redirected and false otherwise. | |
| 200 func authIfNeeded(resp http.ResponseWriter, req *http.Request) (bool, error) { | |
| 201 session, err := getSession(resp, req) | |
| 202 if err != nil { | |
| 203 return false, err | |
| 204 } | |
| 205 issueTracker := session.issueTracker | |
| 206 if !issueTracker.IsAuthenticated() { | |
| 207 params, err := url.ParseQuery(req.URL.RawQuery) | |
| 208 if err != nil { | |
| 209 return false, err | |
| 210 } | |
| 211 if code, ok := params["code"]; ok { | |
| 212 // We're returning from the login page. | |
| 213 log.Println("Upgrading auth token:", code[0]) | |
| 214 err = issueTracker.UpgradeCode(code[0]) | |
|
jcgregorio
2014/05/07 19:32:25
This shouldn't be part of a regular page, i.e. the
borenet
2014/05/07 22:27:37
Done.
| |
| 215 if err != nil { | |
| 216 return false, err | |
| 217 } | |
| 218 http.Redirect(resp, req, session.origRequestURL, | |
| 219 http.StatusTemporaryRedirect) | |
| 220 return true, nil | |
| 221 } else { | |
| 222 loginURL := issueTracker.MakeAuthRequestURL( | |
| 223 session.origRequestURL) | |
| 224 log.Println("Redirecting for login:", loginURL) | |
| 225 http.Redirect(resp, req, loginURL, | |
| 226 http.StatusTemporaryRedirect) | |
| 227 return true, nil | |
| 228 } | |
| 229 } | |
| 230 return false, nil | |
| 231 } | |
| 232 | |
| 233 // submitData attempts to submit data from a POST request to the IssueTracker. | |
| 234 func submitData(resp http.ResponseWriter, req *http.Request) error { | |
| 235 session, err := getSession(resp, req) | |
| 236 if err != nil { | |
| 237 return err | |
| 238 } | |
| 239 issueTracker := session.issueTracker | |
| 240 edits := req.FormValue("all_edits") | |
| 241 var editsMap map[string]*issue_tracker.Issue | |
| 242 err = json.Unmarshal([]byte(edits), &editsMap) | |
| 243 if err != nil { | |
| 244 return errors.New( | |
| 245 "Could not parse edits from form response: " + | |
| 246 err.Error()) | |
| 247 } | |
| 248 pageData := struct { | |
| 249 Title string | |
| 250 Message string | |
| 251 BackLink string | |
| 252 }{} | |
| 253 if len(editsMap) == 0 { | |
| 254 pageData.Title = "No Changes Submitted" | |
| 255 pageData.Message = "You didn't change anything!" | |
| 256 pageData.BackLink = "" | |
| 257 err = templates.ExecuteTemplate( | |
| 258 resp, "submitted.html", pageData) | |
| 259 if err != nil { | |
| 260 return err | |
| 261 } | |
| 262 return nil | |
| 263 } | |
| 264 errorList := make([]error, 0) | |
| 265 for issueId, newIssue := range editsMap { | |
| 266 log.Println("Editing issue " + issueId) | |
| 267 err = issueTracker.SubmitIssueChanges(newIssue, issueComment) | |
| 268 if err != nil { | |
| 269 errorList = append(errorList, err) | |
| 270 } | |
| 271 } | |
| 272 if len(errorList) > 0 { | |
| 273 errorStrings := "" | |
| 274 for _, err := range errorList { | |
| 275 errorStrings += err.Error() + "\n" | |
| 276 } | |
| 277 return errors.New( | |
| 278 "Not all changes could be submitted: \n" + | |
| 279 errorStrings) | |
| 280 } | |
| 281 pageData.Title = "Submitted Changes" | |
| 282 pageData.Message = "Your changes were submitted to the issue tracker." | |
| 283 pageData.BackLink = "" | |
| 284 err = templates.ExecuteTemplate(resp, "submitted.html", pageData) | |
| 285 if err != nil { | |
| 286 return err | |
| 287 } | |
| 288 return nil | |
| 289 } | |
| 290 | |
| 291 // handleBugChomper handles HTTP requests for the bug_chomper page. | |
| 292 func handleBugChomper(resp http.ResponseWriter, req *http.Request) error { | |
| 293 redirected, err := authIfNeeded(resp, req) | |
| 294 if err != nil { | |
| 295 return err | |
| 296 } | |
| 297 if redirected { | |
| 298 return nil | |
| 299 } | |
| 300 switch req.Method { | |
| 301 case "GET": | |
| 302 err = makeBugChomperPage(resp, req) | |
| 303 if err != nil { | |
| 304 return err | |
| 305 } | |
| 306 case "POST": | |
| 307 err = submitData(resp, req) | |
| 308 if err != nil { | |
| 309 return err | |
| 310 } | |
| 311 } | |
| 312 return nil | |
| 313 } | |
| 314 | |
| 315 // handle is the top-level handler function for all HTTP requests coming to | |
| 316 // this server. | |
| 317 func handle(resp http.ResponseWriter, req *http.Request) { | |
|
jcgregorio
2014/05/07 19:32:25
Use w and r as the variable names here:
func ha
borenet
2014/05/07 22:27:37
Done. Is this just convention? To me it seems ea
jcgregorio
2014/05/08 15:11:56
Yes, it's a convention.
On 2014/05/07 22:27:37, b
| |
| 318 log.Println("Fetching " + req.URL.Path) | |
| 319 if req.URL.Path == "/" || req.URL.Path == "/index.html" { | |
| 320 err := handleBugChomper(resp, req) | |
| 321 if err != nil { | |
| 322 http.Error(resp, err.Error(), | |
| 323 http.StatusInternalServerError) | |
| 324 log.Println(err.Error()) | |
| 325 } | |
| 326 return | |
| 327 } | |
|
jcgregorio
2014/05/07 19:32:25
Just serve this off a subdir, i.e. do this in main
borenet
2014/05/07 22:27:37
Done.
| |
| 328 for ext := range contentTypesByExtension { | |
| 329 if strings.HasSuffix(req.URL.Path, ext) { | |
| 330 http.ServeFile( | |
| 331 resp, req, path.Join(staticPath, req.URL.Path)) | |
| 332 return | |
| 333 } | |
| 334 } | |
| 335 http.NotFound(resp, req) | |
| 336 } | |
| 337 | |
| 338 // Run the BugChomper server. | |
| 339 func main() { | |
| 340 http.HandleFunc("/", handle) | |
| 341 port := ":" + strconv.Itoa(defaultPort) | |
| 342 log.Println("Server is running at " + port) | |
| 343 var err error | |
| 344 if scheme == "https" { | |
| 345 err = http.ListenAndServeTLS(port, certFile, keyFile, nil) | |
| 346 } else { | |
| 347 err = http.ListenAndServe(port, nil) | |
| 348 } | |
| 349 if err != nil { | |
| 350 log.Println(err.Error()) | |
| 351 } | |
| 352 } | |
| OLD | NEW |