Suharsh Sivakumar | d308c7e | 2014-10-03 12:46:50 -0700 | [diff] [blame^] | 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "crypto/rand" |
| 5 | "encoding/base64" |
| 6 | "fmt" |
| 7 | "html/template" |
| 8 | "net" |
| 9 | "net/http" |
| 10 | "net/url" |
| 11 | "os" |
| 12 | "os/exec" |
| 13 | "strings" |
| 14 | |
| 15 | "veyron.io/veyron/veyron/services/identity/googleoauth" |
| 16 | "veyron.io/veyron/veyron2/vlog" |
| 17 | ) |
| 18 | |
| 19 | func getMacaroonForBlessRPC(blessServerURL string, blessedChan <-chan string) (<-chan string, error) { |
| 20 | // Setup a HTTP server to recieve a blessing macaroon from the identity server. |
| 21 | // Steps: |
| 22 | // 1. Generate a state token to be included in the HTTP request |
| 23 | // (though, arguably, the random port assigment for the HTTP server is enough |
| 24 | // for XSRF protection) |
| 25 | // 2. Setup a HTTP server which will receive the final blessing macaroon from the id server. |
| 26 | // 3. Print out the link (to start the auth flow) for the user to click. |
| 27 | // 4. Return the macaroon and the rpc object name(where to make the MacaroonBlesser.Bless RPC call) |
| 28 | // in the "result" channel. |
| 29 | var stateBuf [32]byte |
| 30 | if _, err := rand.Read(stateBuf[:]); err != nil { |
| 31 | return nil, fmt.Errorf("failed to generate state token for OAuth: %v", err) |
| 32 | } |
| 33 | state := base64.URLEncoding.EncodeToString(stateBuf[:]) |
| 34 | |
| 35 | ln, err := net.Listen("tcp", "127.0.0.1:0") |
| 36 | if err != nil { |
| 37 | return nil, fmt.Errorf("failed to setup authorization code interception server: %v", err) |
| 38 | } |
| 39 | result := make(chan string) |
| 40 | |
| 41 | redirectURL := fmt.Sprintf("http://%s/macaroon", ln.Addr()) |
| 42 | http.HandleFunc("/macaroon", func(w http.ResponseWriter, r *http.Request) { |
| 43 | w.Header().Set("Content-Type", "text/html") |
| 44 | tmplArgs := struct { |
| 45 | Blessing, ErrShort, ErrLong string |
| 46 | }{} |
| 47 | defer func() { |
| 48 | if len(tmplArgs.ErrShort) > 0 { |
| 49 | w.WriteHeader(http.StatusBadRequest) |
| 50 | } |
| 51 | if err := tmpl.Execute(w, tmplArgs); err != nil { |
| 52 | vlog.Info("Failed to render template:", err) |
| 53 | } |
| 54 | }() |
| 55 | |
| 56 | toolState := r.FormValue("state") |
| 57 | if toolState != state { |
| 58 | tmplArgs.ErrShort = "Unexpected request" |
| 59 | tmplArgs.ErrLong = "Mismatched state parameter. Possible cross-site-request-forging?" |
| 60 | return |
| 61 | } |
| 62 | result <- r.FormValue("macaroon") |
| 63 | result <- r.FormValue("object_name") |
| 64 | defer close(result) |
| 65 | blessed, ok := <-blessedChan |
| 66 | if !ok { |
| 67 | tmplArgs.ErrShort = "No blessing received" |
| 68 | tmplArgs.ErrLong = "Unable to obtain blessing from the Veyron service" |
| 69 | return |
| 70 | } |
| 71 | tmplArgs.Blessing = blessed |
| 72 | ln.Close() |
| 73 | }) |
| 74 | go http.Serve(ln, nil) |
| 75 | |
| 76 | // Print the link to start the flow. |
| 77 | url, err := seekBlessingURL(blessServerURL, redirectURL, state) |
| 78 | if err != nil { |
| 79 | return nil, fmt.Errorf("failed to create seekBlessingURL: %s", err) |
| 80 | } |
| 81 | fmt.Fprintln(os.Stderr, "Please visit the following URL to complete the blessing creation:") |
| 82 | fmt.Fprintln(os.Stderr, url) |
| 83 | // Make an attempt to start the browser as a convenience. |
| 84 | // If it fails, doesn't matter - the client can see the URL printed above. |
| 85 | // Use exec.Command().Start instead of exec.Command().Run since there is no |
| 86 | // need to wait for the command to return (and indeed on some window managers, |
| 87 | // the command will not exit until the browser is closed). |
| 88 | exec.Command(openCommand, url).Start() |
| 89 | return result, nil |
| 90 | } |
| 91 | |
| 92 | func seekBlessingURL(blessServerURL, redirectURL, state string) (string, error) { |
| 93 | baseURL, err := url.Parse(joinURL(blessServerURL, googleoauth.SeekBlessingsRoute)) |
| 94 | if err != nil { |
| 95 | return "", fmt.Errorf("failed to parse url: %v", err) |
| 96 | } |
| 97 | params := url.Values{} |
| 98 | params.Add("redirect_url", redirectURL) |
| 99 | params.Add("state", state) |
| 100 | baseURL.RawQuery = params.Encode() |
| 101 | return baseURL.String(), nil |
| 102 | } |
| 103 | |
| 104 | func joinURL(baseURL, suffix string) string { |
| 105 | if !strings.HasSuffix(baseURL, "/") { |
| 106 | baseURL += "/" |
| 107 | } |
| 108 | return baseURL + suffix |
| 109 | } |
| 110 | |
| 111 | var tmpl = template.Must(template.New("name").Parse(`<!doctype html> |
| 112 | <html> |
| 113 | <head> |
| 114 | <meta charset="UTF-8"> |
| 115 | <title>Veyron Identity: Google</title> |
| 116 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 117 | <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"> |
| 118 | {{if .Blessing}} |
| 119 | <!--Attempt to close the window. Though this script does not work on many browser configurations--> |
| 120 | <script type="text/javascript">window.close();</script> |
| 121 | {{end}} |
| 122 | </head> |
| 123 | <body> |
| 124 | <div class="container"> |
| 125 | {{if .ErrShort}} |
| 126 | <h1><span class="label label-danger">error</span>{{.ErrShort}}</h1> |
| 127 | <div class="well">{{.ErrLong}}</div> |
| 128 | {{else}} |
| 129 | <h3>Received blessing: <tt>{{.Blessing}}</tt></h3> |
| 130 | <div class="well">If the name is prefixed with "unknown/", ignore that. You can close this window, the command line tool has retrieved the blessing</div> |
| 131 | {{end}} |
| 132 | </div> |
| 133 | </body> |
| 134 | </html>`)) |