/*
Timelinize
Copyright (c) 2013 Matthew Holt
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
package oauth2client
import (
"bytes"
"context"
"fmt"
"net"
"net/http"
"net/url"
"os/exec"
"runtime"
"strings"
"time"
)
// Browser gets an OAuth2 code via the web browser.
type Browser struct {
// RedirectURL is the URL to redirect the browser
// to after the code is obtained; it is usually a
// loopback address. If empty, DefaultRedirectURL
// will be used instead.
RedirectURL string
}
// Get opens a browser window to authCodeURL for the user to
// authorize the application, and it returns the resulting
// OAuth2 code. It rejects requests where the "state" param
// does not match expectedStateVal.
func (b Browser) Get(ctx context.Context, expectedStateVal, authCodeURL string) (string, error) {
redirURLStr := b.RedirectURL
if redirURLStr == "" {
redirURLStr = DefaultRedirectURL
}
redirURL, err := url.Parse(redirURLStr)
if err != nil {
return "", err
}
ln, err := new(net.ListenConfig).Listen(ctx, "tcp", redirURL.Host)
if err != nil {
return "", err
}
defer ln.Close()
ch := make(chan string)
errCh := make(chan error)
go func() {
handler := func(w http.ResponseWriter, r *http.Request) {
state := r.FormValue("state")
code := r.FormValue("code")
if r.Method != http.MethodGet || r.URL.Path != redirURL.Path || state == "" || code == "" {
http.Error(w, "This endpoint is for OAuth2 callbacks only", http.StatusNotFound)
return
}
if state != expectedStateVal {
http.Error(w, "invalid state", http.StatusUnauthorized)
errCh <- fmt.Errorf("invalid OAuth2 state; expected '%s' but got '%s'",
expectedStateVal, state)
return
}
fmt.Fprint(w, successBody)
ch <- code
}
// must disable keep-alives, otherwise repeated calls to
// this method can block indefinitely in some weird bug
srv := http.Server{Handler: http.HandlerFunc(handler), ReadHeaderTimeout: 30 * time.Second}
srv.SetKeepAlivesEnabled(false)
srv.Serve(ln) //nolint:errcheck
}()
err = openBrowser(ctx, authCodeURL)
if err != nil {
fmt.Printf("Can't open browser: %s.\nPlease follow this link: %s", err, authCodeURL)
}
select {
case <-ctx.Done():
return "", ctx.Err()
case code := <-ch:
return code, nil
case err := <-errCh:
return "", err
}
}
// openBrowser opens the browser to url.
func openBrowser(ctx context.Context, url string) error {
osCommand := map[string][]string{
"darwin": {"open"},
"freebsd": {"xdg-open"},
"linux": {"xdg-open"},
"netbsd": {"xdg-open"},
"openbsd": {"xdg-open"},
"windows": {"cmd", "/c", "start"},
}
if runtime.GOOS == "windows" {
// escape characters not allowed by cmd
url = strings.ReplaceAll(url, "&", `^&`)
}
all := osCommand[runtime.GOOS]
exe := all[0]
args := all[1:]
buf := new(bytes.Buffer)
cmd := exec.CommandContext(ctx, exe, append(args, url)...)
cmd.Stdout = buf
cmd.Stderr = buf
err := cmd.Run()
if err != nil {
return fmt.Errorf("%w: %s", err, buf.String())
}
return nil
}
const successBody = `
OAuth2 Success
Code obtained, thank you!
You may now close this page and return to the application.
`
var _ Getter = Browser{}