Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(507)

Side by Side Diff: tools/bug_chomper/src/server/server.go

Issue 274693002: BugChomper utility - rewrite in Go (Closed) Base URL: https://skia.googlesource.com/skia.git@master
Patch Set: Created 6 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698