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

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: Use securecookie, add --public flag 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 "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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698