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 |