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