blob: e60c16bafe2aae3bcd9d631b57bd769890e4f724 [file] [log] [blame]
// Copyright 2011 Google LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"context"
"encoding/gob"
"errors"
"flag"
"fmt"
"hash/fnv"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
// Flags
var (
clientID = flag.String("clientid", "", "OAuth 2.0 Client ID. If non-empty, overrides --clientid_file")
clientIDFile = flag.String("clientid-file", "clientid.dat",
"Name of a file containing just the project's OAuth 2.0 Client ID from https://developers.google.com/console.")
secret = flag.String("secret", "", "OAuth 2.0 Client Secret. If non-empty, overrides --secret_file")
secretFile = flag.String("secret-file", "clientsecret.dat",
"Name of a file containing just the project's OAuth 2.0 Client Secret from https://developers.google.com/console.")
cacheToken = flag.Bool("cachetoken", true, "cache the OAuth 2.0 token")
debug = flag.Bool("debug", false, "show HTTP traffic")
)
func usage() {
fmt.Fprintf(os.Stderr, "Usage: go-api-demo <api-demo-name> [api name args]\n\nPossible APIs:\n\n")
for n := range demoFunc {
fmt.Fprintf(os.Stderr, " * %s\n", n)
}
os.Exit(2)
}
func main() {
flag.Parse()
if flag.NArg() == 0 {
usage()
}
name := flag.Arg(0)
demo, ok := demoFunc[name]
if !ok {
usage()
}
config := &oauth2.Config{
ClientID: valueOrFileContents(*clientID, *clientIDFile),
ClientSecret: valueOrFileContents(*secret, *secretFile),
Endpoint: google.Endpoint,
Scopes: []string{demoScope[name]},
}
ctx := context.Background()
if *debug {
ctx = context.WithValue(ctx, oauth2.HTTPClient, &http.Client{
Transport: &logTransport{http.DefaultTransport},
})
}
c := newOAuthClient(ctx, config)
demo(c, flag.Args()[1:])
}
var (
demoFunc = make(map[string]func(*http.Client, []string))
demoScope = make(map[string]string)
)
func registerDemo(name, scope string, main func(c *http.Client, argv []string)) {
if demoFunc[name] != nil {
panic(name + " already registered")
}
demoFunc[name] = main
demoScope[name] = scope
}
func osUserCacheDir() string {
switch runtime.GOOS {
case "darwin":
return filepath.Join(os.Getenv("HOME"), "Library", "Caches")
case "linux", "freebsd":
return filepath.Join(os.Getenv("HOME"), ".cache")
}
log.Printf("TODO: osUserCacheDir on GOOS %q", runtime.GOOS)
return "."
}
func tokenCacheFile(config *oauth2.Config) string {
hash := fnv.New32a()
hash.Write([]byte(config.ClientID))
hash.Write([]byte(config.ClientSecret))
hash.Write([]byte(strings.Join(config.Scopes, " ")))
fn := fmt.Sprintf("go-api-demo-tok%v", hash.Sum32())
return filepath.Join(osUserCacheDir(), url.QueryEscape(fn))
}
func tokenFromFile(file string) (*oauth2.Token, error) {
if !*cacheToken {
return nil, errors.New("--cachetoken is false")
}
f, err := os.Open(file)
if err != nil {
return nil, err
}
t := new(oauth2.Token)
err = gob.NewDecoder(f).Decode(t)
return t, err
}
func saveToken(file string, token *oauth2.Token) {
f, err := os.Create(file)
if err != nil {
log.Printf("Warning: failed to cache oauth token: %v", err)
return
}
defer f.Close()
gob.NewEncoder(f).Encode(token)
}
func newOAuthClient(ctx context.Context, config *oauth2.Config) *http.Client {
cacheFile := tokenCacheFile(config)
token, err := tokenFromFile(cacheFile)
if err != nil {
token = tokenFromWeb(ctx, config)
saveToken(cacheFile, token)
} else {
log.Printf("Using cached token %#v from %q", token, cacheFile)
}
return config.Client(ctx, token)
}
func tokenFromWeb(ctx context.Context, config *oauth2.Config) *oauth2.Token {
ch := make(chan string)
randState := fmt.Sprintf("st%d", time.Now().UnixNano())
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/favicon.ico" {
http.Error(rw, "", 404)
return
}
if req.FormValue("state") != randState {
log.Printf("State doesn't match: req = %#v", req)
http.Error(rw, "", 500)
return
}
if code := req.FormValue("code"); code != "" {
fmt.Fprintf(rw, "<h1>Success</h1>Authorized.")
rw.(http.Flusher).Flush()
ch <- code
return
}
log.Printf("no code")
http.Error(rw, "", 500)
}))
defer ts.Close()
config.RedirectURL = ts.URL
authURL := config.AuthCodeURL(randState)
go openURL(authURL)
log.Printf("Authorize this app at: %s", authURL)
code := <-ch
log.Printf("Got code: %s", code)
token, err := config.Exchange(ctx, code)
if err != nil {
log.Fatalf("Token exchange error: %v", err)
}
return token
}
func openURL(url string) {
try := []string{"xdg-open", "google-chrome", "open"}
for _, bin := range try {
err := exec.Command(bin, url).Run()
if err == nil {
return
}
}
log.Printf("Error opening URL in browser.")
}
func valueOrFileContents(value string, filename string) string {
if value != "" {
return value
}
slurp, err := ioutil.ReadFile(filename)
if err != nil {
log.Fatalf("Error reading %q: %v", filename, err)
}
return strings.TrimSpace(string(slurp))
}