1// Copyright 2011 Google LLC. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package main
6
7import (
8	"context"
9	"encoding/gob"
10	"errors"
11	"flag"
12	"fmt"
13	"hash/fnv"
14	"io/ioutil"
15	"log"
16	"net/http"
17	"net/http/httptest"
18	"net/url"
19	"os"
20	"os/exec"
21	"path/filepath"
22	"runtime"
23	"strings"
24	"time"
25
26	"golang.org/x/oauth2"
27	"golang.org/x/oauth2/google"
28)
29
30// Flags
31var (
32	clientID     = flag.String("clientid", "", "OAuth 2.0 Client ID.  If non-empty, overrides --clientid_file")
33	clientIDFile = flag.String("clientid-file", "clientid.dat",
34		"Name of a file containing just the project's OAuth 2.0 Client ID from https://developers.google.com/console.")
35	secret     = flag.String("secret", "", "OAuth 2.0 Client Secret.  If non-empty, overrides --secret_file")
36	secretFile = flag.String("secret-file", "clientsecret.dat",
37		"Name of a file containing just the project's OAuth 2.0 Client Secret from https://developers.google.com/console.")
38	cacheToken = flag.Bool("cachetoken", true, "cache the OAuth 2.0 token")
39	debug      = flag.Bool("debug", false, "show HTTP traffic")
40)
41
42func usage() {
43	fmt.Fprintf(os.Stderr, "Usage: go-api-demo <api-demo-name> [api name args]\n\nPossible APIs:\n\n")
44	for n := range demoFunc {
45		fmt.Fprintf(os.Stderr, "  * %s\n", n)
46	}
47	os.Exit(2)
48}
49
50func main() {
51	flag.Parse()
52	if flag.NArg() == 0 {
53		usage()
54	}
55
56	name := flag.Arg(0)
57	demo, ok := demoFunc[name]
58	if !ok {
59		usage()
60	}
61
62	config := &oauth2.Config{
63		ClientID:     valueOrFileContents(*clientID, *clientIDFile),
64		ClientSecret: valueOrFileContents(*secret, *secretFile),
65		Endpoint:     google.Endpoint,
66		Scopes:       []string{demoScope[name]},
67	}
68
69	ctx := context.Background()
70	if *debug {
71		ctx = context.WithValue(ctx, oauth2.HTTPClient, &http.Client{
72			Transport: &logTransport{http.DefaultTransport},
73		})
74	}
75	c := newOAuthClient(ctx, config)
76	demo(c, flag.Args()[1:])
77}
78
79var (
80	demoFunc  = make(map[string]func(*http.Client, []string))
81	demoScope = make(map[string]string)
82)
83
84func registerDemo(name, scope string, main func(c *http.Client, argv []string)) {
85	if demoFunc[name] != nil {
86		panic(name + " already registered")
87	}
88	demoFunc[name] = main
89	demoScope[name] = scope
90}
91
92func osUserCacheDir() string {
93	switch runtime.GOOS {
94	case "darwin":
95		return filepath.Join(os.Getenv("HOME"), "Library", "Caches")
96	case "linux", "freebsd":
97		return filepath.Join(os.Getenv("HOME"), ".cache")
98	}
99	log.Printf("TODO: osUserCacheDir on GOOS %q", runtime.GOOS)
100	return "."
101}
102
103func tokenCacheFile(config *oauth2.Config) string {
104	hash := fnv.New32a()
105	hash.Write([]byte(config.ClientID))
106	hash.Write([]byte(config.ClientSecret))
107	hash.Write([]byte(strings.Join(config.Scopes, " ")))
108	fn := fmt.Sprintf("go-api-demo-tok%v", hash.Sum32())
109	return filepath.Join(osUserCacheDir(), url.QueryEscape(fn))
110}
111
112func tokenFromFile(file string) (*oauth2.Token, error) {
113	if !*cacheToken {
114		return nil, errors.New("--cachetoken is false")
115	}
116	f, err := os.Open(file)
117	if err != nil {
118		return nil, err
119	}
120	t := new(oauth2.Token)
121	err = gob.NewDecoder(f).Decode(t)
122	return t, err
123}
124
125func saveToken(file string, token *oauth2.Token) {
126	f, err := os.Create(file)
127	if err != nil {
128		log.Printf("Warning: failed to cache oauth token: %v", err)
129		return
130	}
131	defer f.Close()
132	gob.NewEncoder(f).Encode(token)
133}
134
135func newOAuthClient(ctx context.Context, config *oauth2.Config) *http.Client {
136	cacheFile := tokenCacheFile(config)
137	token, err := tokenFromFile(cacheFile)
138	if err != nil {
139		token = tokenFromWeb(ctx, config)
140		saveToken(cacheFile, token)
141	} else {
142		log.Printf("Using cached token %#v from %q", token, cacheFile)
143	}
144
145	return config.Client(ctx, token)
146}
147
148func tokenFromWeb(ctx context.Context, config *oauth2.Config) *oauth2.Token {
149	ch := make(chan string)
150	randState := fmt.Sprintf("st%d", time.Now().UnixNano())
151	ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
152		if req.URL.Path == "/favicon.ico" {
153			http.Error(rw, "", 404)
154			return
155		}
156		if req.FormValue("state") != randState {
157			log.Printf("State doesn't match: req = %#v", req)
158			http.Error(rw, "", 500)
159			return
160		}
161		if code := req.FormValue("code"); code != "" {
162			fmt.Fprintf(rw, "<h1>Success</h1>Authorized.")
163			rw.(http.Flusher).Flush()
164			ch <- code
165			return
166		}
167		log.Printf("no code")
168		http.Error(rw, "", 500)
169	}))
170	defer ts.Close()
171
172	config.RedirectURL = ts.URL
173	authURL := config.AuthCodeURL(randState)
174	go openURL(authURL)
175	log.Printf("Authorize this app at: %s", authURL)
176	code := <-ch
177	log.Printf("Got code: %s", code)
178
179	token, err := config.Exchange(ctx, code)
180	if err != nil {
181		log.Fatalf("Token exchange error: %v", err)
182	}
183	return token
184}
185
186func openURL(url string) {
187	try := []string{"xdg-open", "google-chrome", "open"}
188	for _, bin := range try {
189		err := exec.Command(bin, url).Run()
190		if err == nil {
191			return
192		}
193	}
194	log.Printf("Error opening URL in browser.")
195}
196
197func valueOrFileContents(value string, filename string) string {
198	if value != "" {
199		return value
200	}
201	slurp, err := ioutil.ReadFile(filename)
202	if err != nil {
203		log.Fatalf("Error reading %q: %v", filename, err)
204	}
205	return strings.TrimSpace(string(slurp))
206}
207