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