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

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: Address comments 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 "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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698