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 "errors" | |
17 "flag" | |
18 "fmt" | |
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 import "github.com/gorilla/securecookie" | |
32 | |
33 const certFile = "certs/cert.pem" | |
34 const keyFile = "certs/key.pem" | |
35 const issueComment = "Edited by BugChomper" | |
36 const oauthCallbackPath = "/oauth2callback" | |
37 const oauthConfigFile = "oauth_client_secret.json" | |
38 const defaultPort = 8000 | |
39 const localHost = "127.0.0.1" | |
40 const maxSessionLen = time.Duration(3600 * time.Second) | |
41 const priorityPrefix = "Priority-" | |
42 const project = "skia" | |
43 const cookieName = "BugChomperCookie" | |
44 | |
45 var scheme = "http" | |
46 | |
47 var curdir, _ = filepath.Abs(".") | |
48 var templatePath, _ = filepath.Abs("templates") | |
49 var templates = template.Must(template.ParseFiles( | |
50 path.Join(templatePath, "bug_chomper.html"), | |
51 path.Join(templatePath, "submitted.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( | |
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( | |
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 // makeBugChomperPage builds and serves the BugChomper page. | |
141 func makeBugChomperPage(w http.ResponseWriter, r *http.Request) error { | |
142 session, err := getSession(w, r) | |
143 if err != nil { | |
144 return err | |
145 } | |
146 issueTracker := session.IssueTracker | |
147 user, err := issueTracker.GetLoggedInUser() | |
148 if err != nil { | |
149 return err | |
150 } | |
151 log.Println("Loading bugs for " + user) | |
152 bugList, err := issueTracker.GetBugs(project, user) | |
153 if err != nil { | |
154 return err | |
155 } | |
156 bugsById := make(map[string]*issue_tracker.Issue) | |
157 bugsByPriority := make(map[string][]*issue_tracker.Issue) | |
158 for _, bug := range bugList.Items { | |
159 bugsById[strconv.Itoa(bug.Id)] = bug | |
160 var bugPriority string | |
161 for _, label := range bug.Labels { | |
162 if strings.HasPrefix(label, priorityPrefix) { | |
163 bugPriority = label[len(priorityPrefix):] | |
164 } | |
165 } | |
166 if _, ok := bugsByPriority[bugPriority]; !ok { | |
167 bugsByPriority[bugPriority] = make( | |
168 []*issue_tracker.Issue, 0) | |
169 } | |
170 bugsByPriority[bugPriority] = append( | |
171 bugsByPriority[bugPriority], bug) | |
172 } | |
173 bugsJson, err := json.Marshal(bugsById) | |
174 if err != nil { | |
175 return err | |
176 } | |
177 data := struct { | |
178 Title string | |
179 User string | |
180 BugsJson template.JS | |
181 BugsByPriority *map[string][]*issue_tracker.Issue | |
182 Priorities []string | |
183 PriorityPrefix string | |
184 }{ | |
185 Title: "BugChomper", | |
186 User: user, | |
187 BugsJson: template.JS(string(bugsJson)), | |
188 BugsByPriority: &bugsByPriority, | |
189 Priorities: issue_tracker.BugPriorities, | |
190 PriorityPrefix: priorityPrefix, | |
191 } | |
192 | |
193 return templates.ExecuteTemplate(w, "bug_chomper.html", data) | |
194 } | |
195 | |
196 // authIfNeeded determines whether the current user is logged in. If not, it | |
197 // redirects to a login page. Returns true if the user is redirected and false | |
198 // otherwise. | |
199 func authIfNeeded(w http.ResponseWriter, r *http.Request) (bool, error) { | |
200 session, err := getSession(w, r) | |
201 if err != nil { | |
202 return false, err | |
203 } | |
204 issueTracker := session.IssueTracker | |
205 if !issueTracker.IsAuthenticated() { | |
206 loginURL := issueTracker.MakeAuthRequestURL() | |
207 log.Println("Redirecting for login:", loginURL) | |
208 http.Redirect(w, r, loginURL, http.StatusTemporaryRedirect) | |
209 return true, nil | |
210 } | |
211 return false, nil | |
212 } | |
213 | |
214 // submitData attempts to submit data from a POST request to the IssueTracker. | |
215 func submitData(w http.ResponseWriter, r *http.Request) error { | |
216 session, err := getSession(w, r) | |
217 if err != nil { | |
218 return err | |
219 } | |
220 issueTracker := session.IssueTracker | |
221 edits := r.FormValue("all_edits") | |
222 var editsMap map[string]*issue_tracker.Issue | |
223 if err := json.Unmarshal([]byte(edits), &editsMap); err != nil { | |
224 return errors.New( | |
225 "Could not parse edits from form response: " + | |
226 err.Error()) | |
227 } | |
228 data := struct { | |
229 Title string | |
230 Message string | |
231 BackLink string | |
232 }{} | |
233 if len(editsMap) == 0 { | |
234 data.Title = "No Changes Submitted" | |
235 data.Message = "You didn't change anything!" | |
236 data.BackLink = "" | |
237 if err := templates.ExecuteTemplate(w, "submitted.html", data); err != nil { | |
238 return err | |
239 } | |
240 return nil | |
241 } | |
242 errorList := make([]error, 0) | |
243 for issueId, newIssue := range editsMap { | |
244 log.Println("Editing issue " + issueId) | |
245 if err := issueTracker.SubmitIssueChanges(newIssue, issueComment ); err != nil { | |
246 errorList = append(errorList, err) | |
247 } | |
248 } | |
249 if len(errorList) > 0 { | |
250 errorStrings := "" | |
251 for _, err := range errorList { | |
252 errorStrings += err.Error() + "\n" | |
253 } | |
254 return errors.New( | |
255 "Not all changes could be submitted: \n" + | |
256 errorStrings) | |
257 } | |
258 data.Title = "Submitted Changes" | |
259 data.Message = "Your changes were submitted to the issue tracker." | |
260 data.BackLink = "" | |
261 if err := templates.ExecuteTemplate(w, "submitted.html", data); err != n il { | |
262 return err | |
263 } | |
264 return nil | |
265 } | |
266 | |
267 // handleBugChomper handles HTTP requests for the bug_chomper page. | |
268 func handleBugChomper(w http.ResponseWriter, r *http.Request) error { | |
269 redirected, err := authIfNeeded(w, r) | |
270 if err != nil { | |
271 return err | |
272 } | |
273 if redirected { | |
274 return nil | |
275 } | |
276 switch r.Method { | |
277 case "GET": | |
278 if err := makeBugChomperPage(w, r); err != nil { | |
jcgregorio
2014/05/13 16:28:12
I think the code would get simpler by not returnin
borenet
2014/05/13 17:43:20
Done.
| |
279 return err | |
280 } | |
281 case "POST": | |
282 if err := submitData(w, r); err != nil { | |
283 return err | |
284 } | |
285 } | |
286 return nil | |
287 } | |
288 | |
289 // handleOAuth2Callback handles callbacks from the OAuth2 sign-in. | |
290 func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) { | |
291 session, err := getSession(w, r) | |
292 if err != nil { | |
293 http.Error(w, err.Error(), http.StatusInternalServerError) | |
294 } | |
295 issueTracker := session.IssueTracker | |
296 invalidLogin := "Invalid login credentials" | |
297 params, err := url.ParseQuery(r.URL.RawQuery) | |
298 if err != nil { | |
299 http.Error(w, invalidLogin + ": " + err.Error(), http.StatusForb idden) | |
300 return | |
301 } | |
302 code, ok := params["code"] | |
303 if !ok { | |
304 http.Error(w, invalidLogin + ": redirect did not include auth co de.", | |
305 http.StatusForbidden) | |
306 return | |
307 } | |
308 log.Println("Upgrading auth token:", code[0]) | |
309 if err := issueTracker.UpgradeCode(code[0]); err != nil { | |
310 errMsg := "failed to upgrade token: " + err.Error() | |
311 http.Error(w, errMsg, http.StatusForbidden) | |
312 return | |
313 } | |
314 if err := saveSession(session, w, r); err != nil { | |
315 http.Error(w, "failed to save session: "+ err.Error(), | |
316 http.StatusInternalServerError) | |
317 } | |
318 http.Redirect(w, r, session.OrigRequestURL, http.StatusTemporaryRedirect ) | |
319 return | |
320 } | |
321 | |
322 // handle is the top-level handler function for all HTTP requests coming to | |
jcgregorio
2014/05/13 16:28:12
Comment no longer correct.
| |
323 // this server. | |
324 func handle(w http.ResponseWriter, r *http.Request) { | |
325 log.Println("Fetching " + r.URL.Path) | |
326 if r.URL.Path == "/" || r.URL.Path == "/index.html" { | |
327 if err := handleBugChomper(w, r); err != nil { | |
328 http.Error(w, err.Error(), | |
329 http.StatusInternalServerError) | |
330 log.Println(err.Error()) | |
331 } | |
332 return | |
333 } | |
334 http.NotFound(w, r) | |
335 } | |
336 | |
337 // Run the BugChomper server. | |
338 func main() { | |
339 var public bool | |
340 flag.BoolVar( | |
341 &public, "public", false, "Make this server publicly accessible. ") | |
342 flag.Parse() | |
343 | |
344 http.HandleFunc("/", handle) | |
345 http.HandleFunc(oauthCallbackPath, handleOAuth2Callback) | |
346 http.Handle("/res/", http.FileServer(http.Dir(curdir))) | |
347 port := ":" + strconv.Itoa(defaultPort) | |
348 log.Println("Server is running at " + scheme + "://" + localHost + port) | |
349 var err error | |
350 if public { | |
351 log.Println("WARNING: This server is not secure and should not b e made " + | |
352 "publicly accessible.") | |
353 scheme = "https" | |
354 err = http.ListenAndServeTLS(port, certFile, keyFile, nil) | |
355 } else { | |
356 scheme = "http" | |
357 err = http.ListenAndServe(localHost+port, nil) | |
358 } | |
359 if err != nil { | |
360 log.Println(err.Error()) | |
361 } | |
362 } | |
OLD | NEW |