2015-02-11 07:25:37 +03:00
|
|
|
// Copyright 2015 The Go Authors. All rights reserved.
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
package gerrit
|
|
|
|
|
2015-10-22 04:35:17 +03:00
|
|
|
import (
|
2016-10-19 16:34:28 +03:00
|
|
|
"bytes"
|
|
|
|
"crypto/md5"
|
|
|
|
"encoding/hex"
|
2016-02-09 01:14:13 +03:00
|
|
|
"fmt"
|
2015-10-22 04:35:17 +03:00
|
|
|
"net/http"
|
|
|
|
"net/http/cookiejar"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"path/filepath"
|
2018-08-03 20:51:18 +03:00
|
|
|
"runtime"
|
2015-10-22 04:35:17 +03:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2016-02-09 01:14:13 +03:00
|
|
|
"sync"
|
2015-10-22 04:35:17 +03:00
|
|
|
"time"
|
2022-07-12 20:41:14 +03:00
|
|
|
|
|
|
|
"golang.org/x/oauth2"
|
2015-10-22 04:35:17 +03:00
|
|
|
)
|
2015-02-11 07:25:37 +03:00
|
|
|
|
|
|
|
// Auth is a Gerrit authentication mode.
|
|
|
|
// The most common ones are NoAuth or BasicAuth.
|
|
|
|
type Auth interface {
|
2022-07-12 20:41:14 +03:00
|
|
|
setAuth(*Client, *http.Request) error
|
2015-02-11 07:25:37 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// BasicAuth sends a username and password.
|
|
|
|
func BasicAuth(username, password string) Auth {
|
|
|
|
return basicAuth{username, password}
|
|
|
|
}
|
|
|
|
|
|
|
|
type basicAuth struct {
|
|
|
|
username, password string
|
|
|
|
}
|
|
|
|
|
2022-07-12 20:41:14 +03:00
|
|
|
func (ba basicAuth) setAuth(c *Client, r *http.Request) error {
|
2015-02-11 07:25:37 +03:00
|
|
|
r.SetBasicAuth(ba.username, ba.password)
|
2022-07-12 20:41:14 +03:00
|
|
|
return nil
|
2015-02-11 07:25:37 +03:00
|
|
|
}
|
|
|
|
|
2015-10-22 04:35:17 +03:00
|
|
|
// GitCookiesAuth derives the Gerrit authentication token from
|
|
|
|
// gitcookies based on the URL of the Gerrit request.
|
2016-02-09 01:14:13 +03:00
|
|
|
// The cookie file used is determined by running "git config
|
|
|
|
// http.cookiefile" in the current directory.
|
|
|
|
// To use a specific file, see GitCookieFileAuth.
|
2015-10-22 04:35:17 +03:00
|
|
|
func GitCookiesAuth() Auth {
|
|
|
|
return gitCookiesAuth{}
|
|
|
|
}
|
|
|
|
|
2016-02-09 01:14:13 +03:00
|
|
|
// GitCookieFileAuth derives the Gerrit authentication token from the
|
|
|
|
// provided gitcookies file. It is equivalent to GitCookiesAuth,
|
|
|
|
// except that "git config http.cookiefile" is not used to find which
|
|
|
|
// cookie file to use.
|
|
|
|
func GitCookieFileAuth(file string) Auth {
|
|
|
|
return &gitCookieFileAuth{file: file}
|
|
|
|
}
|
|
|
|
|
2018-08-03 20:51:18 +03:00
|
|
|
func netrcPath() string {
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
return filepath.Join(os.Getenv("USERPROFILE"), "_netrc")
|
|
|
|
}
|
|
|
|
return filepath.Join(os.Getenv("HOME"), ".netrc")
|
|
|
|
}
|
|
|
|
|
2015-10-22 04:35:17 +03:00
|
|
|
type gitCookiesAuth struct{}
|
|
|
|
|
2022-07-12 20:41:14 +03:00
|
|
|
func (gitCookiesAuth) setAuth(c *Client, r *http.Request) error {
|
2015-10-22 04:35:17 +03:00
|
|
|
// First look in Git's http.cookiefile, which is where Gerrit
|
|
|
|
// now tells users to store this information.
|
|
|
|
git := exec.Command("git", "config", "http.cookiefile")
|
|
|
|
git.Stderr = os.Stderr
|
2018-08-03 20:51:18 +03:00
|
|
|
|
|
|
|
// Ignore a failure here, git will exit(1) if no cookies are
|
|
|
|
// present and prevent the netrc from being read below.
|
|
|
|
gitOut, _ := git.Output()
|
|
|
|
|
2015-10-22 04:35:17 +03:00
|
|
|
cookieFile := strings.TrimSpace(string(gitOut))
|
|
|
|
if len(cookieFile) != 0 {
|
2016-02-09 01:14:13 +03:00
|
|
|
auth := &gitCookieFileAuth{file: cookieFile}
|
2022-07-12 20:41:14 +03:00
|
|
|
if err := auth.setAuth(c, r); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2016-02-09 01:14:13 +03:00
|
|
|
if len(r.Header["Cookie"]) > 0 {
|
2022-07-12 20:41:14 +03:00
|
|
|
return nil
|
2015-10-22 04:35:17 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-02-09 01:14:13 +03:00
|
|
|
url, err := url.Parse(c.url)
|
|
|
|
if err != nil {
|
2022-07-12 20:41:14 +03:00
|
|
|
return err
|
2016-02-09 01:14:13 +03:00
|
|
|
}
|
|
|
|
|
2015-10-22 04:35:17 +03:00
|
|
|
// If not there, then look in $HOME/.netrc, which is where Gerrit
|
|
|
|
// used to tell users to store the information, until the passwords
|
|
|
|
// got so long that old versions of curl couldn't handle them.
|
2016-02-09 01:14:13 +03:00
|
|
|
host := url.Host
|
2018-08-03 20:51:18 +03:00
|
|
|
netrc := netrcPath()
|
2023-09-08 19:42:54 +03:00
|
|
|
data, _ := os.ReadFile(netrc)
|
2015-10-22 04:35:17 +03:00
|
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
|
|
if i := strings.Index(line, "#"); i >= 0 {
|
|
|
|
line = line[:i]
|
|
|
|
}
|
|
|
|
f := strings.Fields(line)
|
|
|
|
if len(f) >= 6 && f[0] == "machine" && f[1] == host && f[2] == "login" && f[4] == "password" {
|
|
|
|
r.SetBasicAuth(f[3], f[5])
|
2022-07-12 20:41:14 +03:00
|
|
|
return nil
|
2015-10-22 04:35:17 +03:00
|
|
|
}
|
|
|
|
}
|
2022-07-12 20:41:14 +03:00
|
|
|
return fmt.Errorf("no authentication configured for Gerrit; tried both git config http.cookiefile and %s", netrc)
|
2015-10-22 04:35:17 +03:00
|
|
|
}
|
|
|
|
|
2016-02-09 01:14:13 +03:00
|
|
|
type gitCookieFileAuth struct {
|
|
|
|
file string
|
|
|
|
|
|
|
|
once sync.Once
|
|
|
|
jar *cookiejar.Jar
|
|
|
|
err error
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *gitCookieFileAuth) loadCookieFileOnce() {
|
2023-09-08 19:42:54 +03:00
|
|
|
data, err := os.ReadFile(a.file)
|
2016-02-09 01:14:13 +03:00
|
|
|
if err != nil {
|
|
|
|
a.err = fmt.Errorf("Error loading cookie file: %v", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
a.jar = parseGitCookies(string(data))
|
|
|
|
}
|
|
|
|
|
2022-07-12 20:41:14 +03:00
|
|
|
func (a *gitCookieFileAuth) setAuth(c *Client, r *http.Request) error {
|
2016-02-09 01:14:13 +03:00
|
|
|
a.once.Do(a.loadCookieFileOnce)
|
|
|
|
if a.err != nil {
|
2022-07-12 20:41:14 +03:00
|
|
|
return a.err
|
2016-02-09 01:14:13 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
url, err := url.Parse(c.url)
|
|
|
|
if err != nil {
|
2022-07-12 20:41:14 +03:00
|
|
|
return err
|
2016-02-09 01:14:13 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, cookie := range a.jar.Cookies(url) {
|
|
|
|
r.AddCookie(cookie)
|
|
|
|
}
|
2022-07-12 20:41:14 +03:00
|
|
|
return nil
|
2016-02-09 01:14:13 +03:00
|
|
|
}
|
|
|
|
|
2015-10-22 04:35:17 +03:00
|
|
|
func parseGitCookies(data string) *cookiejar.Jar {
|
|
|
|
jar, _ := cookiejar.New(nil)
|
|
|
|
for _, line := range strings.Split(data, "\n") {
|
|
|
|
f := strings.Split(line, "\t")
|
|
|
|
if len(f) < 7 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
expires, err := strconv.ParseInt(f[4], 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
c := http.Cookie{
|
|
|
|
Domain: f[0],
|
|
|
|
Path: f[2],
|
|
|
|
Secure: f[3] == "TRUE",
|
|
|
|
Expires: time.Unix(expires, 0),
|
|
|
|
Name: f[5],
|
|
|
|
Value: f[6],
|
|
|
|
}
|
|
|
|
// Construct a fake URL to add c to the jar.
|
|
|
|
url := url.URL{
|
|
|
|
Scheme: "http",
|
|
|
|
Host: c.Domain,
|
|
|
|
Path: c.Path,
|
|
|
|
}
|
|
|
|
jar.SetCookies(&url, []*http.Cookie{&c})
|
|
|
|
}
|
|
|
|
return jar
|
|
|
|
}
|
|
|
|
|
2022-07-12 20:41:14 +03:00
|
|
|
// Scopes to use when creating a TokenSource.
|
|
|
|
var OAuth2Scopes = []string{
|
|
|
|
"https://www.googleapis.com/auth/cloud-platform",
|
|
|
|
"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",
|
|
|
|
}
|
|
|
|
|
|
|
|
// OAuth2Auth uses the given TokenSource to authenticate requests.
|
|
|
|
func OAuth2Auth(src oauth2.TokenSource) Auth {
|
|
|
|
return oauth2Auth{src}
|
|
|
|
}
|
|
|
|
|
|
|
|
type oauth2Auth struct {
|
|
|
|
src oauth2.TokenSource
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a oauth2Auth) setAuth(c *Client, r *http.Request) error {
|
|
|
|
token, err := a.src.Token()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
token.SetAuthHeader(r)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-02-11 07:25:37 +03:00
|
|
|
// NoAuth makes requests unauthenticated.
|
|
|
|
var NoAuth = noAuth{}
|
|
|
|
|
|
|
|
type noAuth struct{}
|
|
|
|
|
2022-07-12 20:41:14 +03:00
|
|
|
func (noAuth) setAuth(c *Client, r *http.Request) error {
|
|
|
|
return nil
|
|
|
|
}
|
2016-10-19 16:34:28 +03:00
|
|
|
|
|
|
|
type digestAuth struct {
|
|
|
|
Username, Password, Realm, NONCE, QOP, Opaque, Algorithm string
|
|
|
|
}
|
|
|
|
|
|
|
|
func getDigestAuth(username, password string, resp *http.Response) *digestAuth {
|
|
|
|
header := resp.Header.Get("www-authenticate")
|
|
|
|
parts := strings.SplitN(header, " ", 2)
|
|
|
|
parts = strings.Split(parts[1], ", ")
|
|
|
|
opts := make(map[string]string)
|
|
|
|
|
|
|
|
for _, part := range parts {
|
|
|
|
vals := strings.SplitN(part, "=", 2)
|
|
|
|
key := vals[0]
|
|
|
|
val := strings.Trim(vals[1], "\",")
|
|
|
|
opts[key] = val
|
|
|
|
}
|
|
|
|
|
|
|
|
auth := digestAuth{
|
|
|
|
username, password,
|
|
|
|
opts["realm"], opts["nonce"], opts["qop"], opts["opaque"], opts["algorithm"],
|
|
|
|
}
|
|
|
|
return &auth
|
|
|
|
}
|
|
|
|
|
|
|
|
func setDigestAuth(r *http.Request, username, password string, resp *http.Response, nc int) {
|
|
|
|
auth := getDigestAuth(username, password, resp)
|
|
|
|
authStr := getDigestAuthString(auth, r.URL, r.Method, nc)
|
|
|
|
r.Header.Add("Authorization", authStr)
|
|
|
|
}
|
|
|
|
|
|
|
|
func getDigestAuthString(auth *digestAuth, url *url.URL, method string, nc int) string {
|
|
|
|
var buf bytes.Buffer
|
|
|
|
h := md5.New()
|
|
|
|
fmt.Fprintf(&buf, "%s:%s:%s", auth.Username, auth.Realm, auth.Password)
|
|
|
|
buf.WriteTo(h)
|
|
|
|
ha1 := hex.EncodeToString(h.Sum(nil))
|
|
|
|
|
|
|
|
h = md5.New()
|
|
|
|
fmt.Fprintf(&buf, "%s:%s", method, url.Path)
|
|
|
|
buf.WriteTo(h)
|
|
|
|
ha2 := hex.EncodeToString(h.Sum(nil))
|
|
|
|
|
|
|
|
ncStr := fmt.Sprintf("%08x", nc)
|
|
|
|
hnc := "MTM3MDgw"
|
|
|
|
|
|
|
|
h = md5.New()
|
|
|
|
fmt.Fprintf(&buf, "%s:%s:%s:%s:%s:%s", ha1, auth.NONCE, ncStr, hnc, auth.QOP, ha2)
|
|
|
|
buf.WriteTo(h)
|
|
|
|
respdig := hex.EncodeToString(h.Sum(nil))
|
|
|
|
|
|
|
|
buf.Write([]byte("Digest "))
|
|
|
|
fmt.Fprintf(&buf,
|
|
|
|
`username="%s", realm="%s", nonce="%s", uri="%s", response="%s"`,
|
|
|
|
auth.Username, auth.Realm, auth.NONCE, url.Path, respdig,
|
|
|
|
)
|
|
|
|
|
|
|
|
if auth.Opaque != "" {
|
|
|
|
fmt.Fprintf(&buf, `, opaque="%s"`, auth.Opaque)
|
|
|
|
}
|
|
|
|
if auth.QOP != "" {
|
|
|
|
fmt.Fprintf(&buf, `, qop="%s", nc=%s, cnonce="%s"`, auth.QOP, ncStr, hnc)
|
|
|
|
}
|
|
|
|
if auth.Algorithm != "" {
|
|
|
|
fmt.Fprintf(&buf, `, algorithm="%s"`, auth.Algorithm)
|
|
|
|
}
|
|
|
|
|
|
|
|
return buf.String()
|
|
|
|
}
|
|
|
|
|
2022-07-12 20:41:14 +03:00
|
|
|
func (a digestAuth) setAuth(c *Client, r *http.Request) error {
|
2016-10-19 16:34:28 +03:00
|
|
|
resp, err := http.Get(r.URL.String())
|
|
|
|
if err != nil {
|
2022-07-12 20:41:14 +03:00
|
|
|
return err
|
2016-10-19 16:34:28 +03:00
|
|
|
}
|
|
|
|
setDigestAuth(r, a.Username, a.Password, resp, 1)
|
2022-07-12 20:41:14 +03:00
|
|
|
return nil
|
2016-10-19 16:34:28 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// DigestAuth returns an Auth implementation which sends
|
|
|
|
// the provided username and password using HTTP Digest Authentication
|
|
|
|
// (RFC 2617)
|
|
|
|
func DigestAuth(username, password string) Auth {
|
|
|
|
return digestAuth{
|
|
|
|
Username: username,
|
|
|
|
Password: password,
|
|
|
|
}
|
|
|
|
}
|