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