| // A Go implementation of https://gerrit.googlesource.com/gcompute-tools/+show/master/git-cookie-authdaemon |
| package gitauth |
| |
| import ( |
| "bytes" |
| "context" |
| "fmt" |
| "io" |
| "os" |
| "strings" |
| "time" |
| |
| "go.skia.org/infra/go/exec" |
| "go.skia.org/infra/go/git" |
| "go.skia.org/infra/go/now" |
| "go.skia.org/infra/go/skerr" |
| "go.skia.org/infra/go/sklog" |
| "go.skia.org/infra/go/util" |
| "golang.org/x/oauth2" |
| ) |
| |
| 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(ctx context.Context) (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(now.Now(ctx)) |
| 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 is false then Git must be told about the location of the Cookie file, for example: |
| // |
| // git config --global http.cookiefile ~/.git-credential-cache/cookie |
| // |
| // A goroutine will be started to refresh the token. It will stop when the passed-in context is cancelled. |
| func New(ctx context.Context, tokenSource oauth2.TokenSource, filename string, config bool, email string) (*GitAuth, error) { |
| if config { |
| gitExec, err := git.Executable(ctx) |
| if err != nil { |
| return nil, skerr.Wrap(err) |
| } |
| output := bytes.Buffer{} |
| err = exec.Run(ctx, &exec.Command{ |
| Name: gitExec, |
| Args: []string{ |
| "config", |
| "--global", |
| "http.cookiefile", |
| filename}, |
| CombinedOutput: &output, |
| }) |
| if err != nil { |
| return nil, skerr.Wrapf(err, "Failed to set cookie in git config %q", output.String()) |
| } |
| if email != "" { |
| out, err := exec.RunSimple(ctx, fmt.Sprintf("%s config --global user.email %s", gitExec, email)) |
| if err != nil { |
| return nil, skerr.Wrapf(err, "Failed to config user.email: %s", out) |
| } |
| name := strings.Split(email, "@")[0] |
| out, err = exec.RunSimple(ctx, fmt.Sprintf("%s config --global user.name %s", gitExec, name)) |
| if err != nil { |
| return nil, skerr.Wrapf(err, "Failed to config user.name: %s", out) |
| } |
| } |
| // Read back gitconfig. |
| out, err := exec.RunSimple(ctx, fmt.Sprintf("%s config --list --show-origin", gitExec)) |
| if err != nil { |
| return nil, skerr.Wrapf(err, "Failed to read git config: %s", out) |
| } |
| sklog.Infof("Created git configuration:\n%s", out) |
| } |
| g := &GitAuth{ |
| tokenSource: tokenSource, |
| filename: filename, |
| } |
| refresh_in, err := g.updateCookie(ctx) |
| if err != nil { |
| return nil, skerr.Wrapf(err, "Failed to get initial git cookie") |
| } |
| // Set the GIT_COOKIES_PATH environment variable for Depot Tools. |
| if err := os.Setenv("GIT_COOKIES_PATH", filename); err != nil { |
| return nil, skerr.Wrap(err) |
| } |
| |
| go func() { |
| for { |
| if err := ctx.Err(); err != nil { |
| sklog.Errorf("git update cookie goroutine exited because context error %s", err) |
| return |
| } |
| time.Sleep(refresh_in) |
| refresh_in, err = g.updateCookie(ctx) |
| if err != nil { |
| sklog.Errorf("Failed to update git cookie: %s", err) |
| } |
| } |
| }() |
| return g, nil |
| } |