blob: 1488ef897cdd65ebaa772d979f49b00617cc4d03 [file] [log] [blame]
// A Go implementation of https://gerrit.googlesource.com/gcompute-tools/+/master/git-cookie-authdaemon
package gitauth
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"strings"
"time"
"go.skia.org/infra/go/exec"
"go.skia.org/infra/go/git"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
"golang.org/x/oauth2"
)
var (
ErrCookiesMissing = errors.New("Unable to find a valid .gitcookies file.")
// SCOPES are the list of valid scopes for git auth.
SCOPES = []string{
"https://www.googleapis.com/auth/gerritcodereview",
"https://www.googleapis.com/auth/source.full_control",
"https://www.googleapis.com/auth/source.read_write",
"https://www.googleapis.com/auth/source.read_only",
}
)
const (
REFRESH = time.Minute
RETRY_INTERVAL = 5 * time.Second
)
// GitAuth continuously updates the git cookie.
//
type GitAuth struct {
tokenSource oauth2.TokenSource
filename string
}
func (g *GitAuth) updateCookie() (time.Duration, error) {
token, err := g.tokenSource.Token()
if err != nil {
return RETRY_INTERVAL, fmt.Errorf("Failed to retrieve token: %s", err)
}
refresh_in := token.Expiry.Sub(time.Now())
refresh_in -= REFRESH
if refresh_in < 0 {
refresh_in = REFRESH
}
contents := []string{}
// As documented on a random website: https://xiix.wordpress.com/2006/03/23/mozillafirefox-cookie-format/
contents = append(contents, fmt.Sprintf("source.developers.google.com\tFALSE\t/\tTRUE\t%d\to\t%s\n", token.Expiry.Unix(), token.AccessToken))
contents = append(contents, fmt.Sprintf(".googlesource.com\tTRUE\t/\tTRUE\t%d\to\t%s\n", token.Expiry.Unix(), token.AccessToken))
err = util.WithWriteFile(g.filename, func(w io.Writer) error {
_, err := w.Write([]byte(strings.Join(contents, "")))
return err
})
if err != nil {
return RETRY_INTERVAL, fmt.Errorf("Failed to write new cookie file: %s", err)
}
sklog.Infof("Refreshing cookie in %v", refresh_in)
return refresh_in, nil
}
// New returns a new *GitAuth.
//
// tokenSource - An oauth2.TokenSource authorized to access the repository, with an appropriate scope set.
// filename - The name of the git cookie file, e.g. "~/.git-credential-cache/cookie".
// config - If true then set the http.cookiefile config globally for git and set the user name and email globally if 'email' is not the empty string.
// email - The email address of the authorized account. Used to set the git config user.name and user.email. Can be "", in which case user.name
// and user.email are not set.
//
// If config if false then Git must be told about the location of the Cookie file, for example:
//
// git config --global http.cookiefile ~/.git-credential-cache/cookie
//
func New(tokenSource oauth2.TokenSource, filename string, config bool, email string) (*GitAuth, error) {
if config {
gitExec, err := git.Executable(context.TODO())
if err != nil {
return nil, skerr.Wrap(err)
}
output := bytes.Buffer{}
err = exec.Run(context.Background(), &exec.Command{
Name: gitExec,
Args: []string{
"config",
"--global",
"http.cookiefile",
filename},
CombinedOutput: &output,
})
if err != nil {
return nil, fmt.Errorf("Failed to set cookie in git config %q: %s", output.String(), err)
}
if email != "" {
ctx := context.Background()
out, err := exec.RunSimple(ctx, fmt.Sprintf("git config --global user.email %s", email))
if err != nil {
return nil, fmt.Errorf("Failed to config: %s: %s", err, out)
}
name := strings.Split(email, "@")[0]
out, err = exec.RunSimple(ctx, fmt.Sprintf("git config --global user.name %s", name))
if err != nil {
return nil, fmt.Errorf("Failed to config: %s: %s", err, out)
}
}
}
g := &GitAuth{
tokenSource: tokenSource,
filename: filename,
}
refresh_in, err := g.updateCookie()
if err != nil {
return nil, fmt.Errorf("Failed to get initial git cookie: %s", err)
}
// Set the GIT_COOKIES_PATH environment variable for Depot Tools.
if err := os.Setenv("GIT_COOKIES_PATH", filename); err != nil {
return nil, err
}
go func() {
for {
time.Sleep(refresh_in)
refresh_in, err = g.updateCookie()
if err != nil {
sklog.Errorf("Failed to get initial git cookie: %s", err)
}
}
}()
return g, nil
}
// getCredentials returns the parsed contents of .gitCookies.
//
// This logic has been borrowed from
// https://cs.chromium.org/chromium/tools/depot_tools/gerrit_util.py?l=143
func getCredentials(gitCookiesPath string) (map[string]string, error) {
// Set empty cookies if no path was given and issue a warning.
if gitCookiesPath == "" {
return nil, ErrCookiesMissing
}
gitCookies := map[string]string{}
dat, err := ioutil.ReadFile(gitCookiesPath)
if err != nil {
return nil, err
}
contents := string(dat)
for _, line := range strings.Split(contents, "\n") {
if strings.HasPrefix(line, "#") || line == "" {
continue
}
tokens := strings.Split(line, "\t")
domain, xpath, key, value := tokens[0], tokens[2], tokens[5], tokens[6]
if xpath == "/" && key == "o" {
gitCookies[domain] = value
}
}
return gitCookies, nil
}
// AddAuthenticationCookie adds a git authentication cookie to the given request.
//
// TODO(borenet): It would be nice if this could be part of an http.Client, or
// use an http.CookieJar in combination with the above code so that we don't
// have to call AddAuthenticationCookie manually all the time.
func AddAuthenticationCookie(gitCookiesPath string, req *http.Request) error {
auth := ""
cookies, err := getCredentials(gitCookiesPath)
if err != nil {
return err
}
for d, a := range cookies {
if util.CookieDomainMatch(req.URL.Host, d) {
auth = a
cookie := http.Cookie{Name: "o", Value: a}
req.AddCookie(&cookie)
break
}
}
if auth == "" {
return ErrCookiesMissing
}
return nil
}