golang.org/x/build@v0.0.0-20240506185731-218518f32b70/gerrit/auth.go (about)

     1  // Copyright 2015 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package gerrit
     6  
     7  import (
     8  	"bytes"
     9  	"crypto/md5"
    10  	"encoding/hex"
    11  	"fmt"
    12  	"net/http"
    13  	"net/http/cookiejar"
    14  	"net/url"
    15  	"os"
    16  	"os/exec"
    17  	"path/filepath"
    18  	"runtime"
    19  	"strconv"
    20  	"strings"
    21  	"sync"
    22  	"time"
    23  
    24  	"golang.org/x/oauth2"
    25  )
    26  
    27  // Auth is a Gerrit authentication mode.
    28  // The most common ones are NoAuth or BasicAuth.
    29  type Auth interface {
    30  	setAuth(*Client, *http.Request) error
    31  }
    32  
    33  // BasicAuth sends a username and password.
    34  func BasicAuth(username, password string) Auth {
    35  	return basicAuth{username, password}
    36  }
    37  
    38  type basicAuth struct {
    39  	username, password string
    40  }
    41  
    42  func (ba basicAuth) setAuth(c *Client, r *http.Request) error {
    43  	r.SetBasicAuth(ba.username, ba.password)
    44  	return nil
    45  }
    46  
    47  // GitCookiesAuth derives the Gerrit authentication token from
    48  // gitcookies based on the URL of the Gerrit request.
    49  // The cookie file used is determined by running "git config
    50  // http.cookiefile" in the current directory.
    51  // To use a specific file, see GitCookieFileAuth.
    52  func GitCookiesAuth() Auth {
    53  	return gitCookiesAuth{}
    54  }
    55  
    56  // GitCookieFileAuth derives the Gerrit authentication token from the
    57  // provided gitcookies file. It is equivalent to GitCookiesAuth,
    58  // except that "git config http.cookiefile" is not used to find which
    59  // cookie file to use.
    60  func GitCookieFileAuth(file string) Auth {
    61  	return &gitCookieFileAuth{file: file}
    62  }
    63  
    64  func netrcPath() string {
    65  	if runtime.GOOS == "windows" {
    66  		return filepath.Join(os.Getenv("USERPROFILE"), "_netrc")
    67  	}
    68  	return filepath.Join(os.Getenv("HOME"), ".netrc")
    69  }
    70  
    71  type gitCookiesAuth struct{}
    72  
    73  func (gitCookiesAuth) setAuth(c *Client, r *http.Request) error {
    74  	// First look in Git's http.cookiefile, which is where Gerrit
    75  	// now tells users to store this information.
    76  	git := exec.Command("git", "config", "http.cookiefile")
    77  	git.Stderr = os.Stderr
    78  
    79  	// Ignore a failure here, git will exit(1) if no cookies are
    80  	// present and prevent the netrc from being read below.
    81  	gitOut, _ := git.Output()
    82  
    83  	cookieFile := strings.TrimSpace(string(gitOut))
    84  	if len(cookieFile) != 0 {
    85  		auth := &gitCookieFileAuth{file: cookieFile}
    86  		if err := auth.setAuth(c, r); err != nil {
    87  			return err
    88  		}
    89  		if len(r.Header["Cookie"]) > 0 {
    90  			return nil
    91  		}
    92  	}
    93  
    94  	url, err := url.Parse(c.url)
    95  	if err != nil {
    96  		return err
    97  	}
    98  
    99  	// If not there, then look in $HOME/.netrc, which is where Gerrit
   100  	// used to tell users to store the information, until the passwords
   101  	// got so long that old versions of curl couldn't handle them.
   102  	host := url.Host
   103  	netrc := netrcPath()
   104  	data, _ := os.ReadFile(netrc)
   105  	for _, line := range strings.Split(string(data), "\n") {
   106  		if i := strings.Index(line, "#"); i >= 0 {
   107  			line = line[:i]
   108  		}
   109  		f := strings.Fields(line)
   110  		if len(f) >= 6 && f[0] == "machine" && f[1] == host && f[2] == "login" && f[4] == "password" {
   111  			r.SetBasicAuth(f[3], f[5])
   112  			return nil
   113  		}
   114  	}
   115  	return fmt.Errorf("no authentication configured for Gerrit; tried both git config http.cookiefile and %s", netrc)
   116  }
   117  
   118  type gitCookieFileAuth struct {
   119  	file string
   120  
   121  	once sync.Once
   122  	jar  *cookiejar.Jar
   123  	err  error
   124  }
   125  
   126  func (a *gitCookieFileAuth) loadCookieFileOnce() {
   127  	data, err := os.ReadFile(a.file)
   128  	if err != nil {
   129  		a.err = fmt.Errorf("Error loading cookie file: %v", err)
   130  		return
   131  	}
   132  	a.jar = parseGitCookies(string(data))
   133  }
   134  
   135  func (a *gitCookieFileAuth) setAuth(c *Client, r *http.Request) error {
   136  	a.once.Do(a.loadCookieFileOnce)
   137  	if a.err != nil {
   138  		return a.err
   139  	}
   140  
   141  	url, err := url.Parse(c.url)
   142  	if err != nil {
   143  		return err
   144  	}
   145  
   146  	for _, cookie := range a.jar.Cookies(url) {
   147  		r.AddCookie(cookie)
   148  	}
   149  	return nil
   150  }
   151  
   152  func parseGitCookies(data string) *cookiejar.Jar {
   153  	jar, _ := cookiejar.New(nil)
   154  	for _, line := range strings.Split(data, "\n") {
   155  		f := strings.Split(line, "\t")
   156  		if len(f) < 7 {
   157  			continue
   158  		}
   159  		expires, err := strconv.ParseInt(f[4], 10, 64)
   160  		if err != nil {
   161  			continue
   162  		}
   163  		c := http.Cookie{
   164  			Domain:  f[0],
   165  			Path:    f[2],
   166  			Secure:  f[3] == "TRUE",
   167  			Expires: time.Unix(expires, 0),
   168  			Name:    f[5],
   169  			Value:   f[6],
   170  		}
   171  		// Construct a fake URL to add c to the jar.
   172  		url := url.URL{
   173  			Scheme: "http",
   174  			Host:   c.Domain,
   175  			Path:   c.Path,
   176  		}
   177  		jar.SetCookies(&url, []*http.Cookie{&c})
   178  	}
   179  	return jar
   180  }
   181  
   182  // Scopes to use when creating a TokenSource.
   183  var OAuth2Scopes = []string{
   184  	"https://www.googleapis.com/auth/cloud-platform",
   185  	"https://www.googleapis.com/auth/gerritcodereview",
   186  	"https://www.googleapis.com/auth/source.full_control",
   187  	"https://www.googleapis.com/auth/source.read_write",
   188  	"https://www.googleapis.com/auth/source.read_only",
   189  }
   190  
   191  // OAuth2Auth uses the given TokenSource to authenticate requests.
   192  func OAuth2Auth(src oauth2.TokenSource) Auth {
   193  	return oauth2Auth{src}
   194  }
   195  
   196  type oauth2Auth struct {
   197  	src oauth2.TokenSource
   198  }
   199  
   200  func (a oauth2Auth) setAuth(c *Client, r *http.Request) error {
   201  	token, err := a.src.Token()
   202  	if err != nil {
   203  		return err
   204  	}
   205  	token.SetAuthHeader(r)
   206  	return nil
   207  }
   208  
   209  // NoAuth makes requests unauthenticated.
   210  var NoAuth = noAuth{}
   211  
   212  type noAuth struct{}
   213  
   214  func (noAuth) setAuth(c *Client, r *http.Request) error {
   215  	return nil
   216  }
   217  
   218  type digestAuth struct {
   219  	Username, Password, Realm, NONCE, QOP, Opaque, Algorithm string
   220  }
   221  
   222  func getDigestAuth(username, password string, resp *http.Response) *digestAuth {
   223  	header := resp.Header.Get("www-authenticate")
   224  	parts := strings.SplitN(header, " ", 2)
   225  	parts = strings.Split(parts[1], ", ")
   226  	opts := make(map[string]string)
   227  
   228  	for _, part := range parts {
   229  		vals := strings.SplitN(part, "=", 2)
   230  		key := vals[0]
   231  		val := strings.Trim(vals[1], "\",")
   232  		opts[key] = val
   233  	}
   234  
   235  	auth := digestAuth{
   236  		username, password,
   237  		opts["realm"], opts["nonce"], opts["qop"], opts["opaque"], opts["algorithm"],
   238  	}
   239  	return &auth
   240  }
   241  
   242  func setDigestAuth(r *http.Request, username, password string, resp *http.Response, nc int) {
   243  	auth := getDigestAuth(username, password, resp)
   244  	authStr := getDigestAuthString(auth, r.URL, r.Method, nc)
   245  	r.Header.Add("Authorization", authStr)
   246  }
   247  
   248  func getDigestAuthString(auth *digestAuth, url *url.URL, method string, nc int) string {
   249  	var buf bytes.Buffer
   250  	h := md5.New()
   251  	fmt.Fprintf(&buf, "%s:%s:%s", auth.Username, auth.Realm, auth.Password)
   252  	buf.WriteTo(h)
   253  	ha1 := hex.EncodeToString(h.Sum(nil))
   254  
   255  	h = md5.New()
   256  	fmt.Fprintf(&buf, "%s:%s", method, url.Path)
   257  	buf.WriteTo(h)
   258  	ha2 := hex.EncodeToString(h.Sum(nil))
   259  
   260  	ncStr := fmt.Sprintf("%08x", nc)
   261  	hnc := "MTM3MDgw"
   262  
   263  	h = md5.New()
   264  	fmt.Fprintf(&buf, "%s:%s:%s:%s:%s:%s", ha1, auth.NONCE, ncStr, hnc, auth.QOP, ha2)
   265  	buf.WriteTo(h)
   266  	respdig := hex.EncodeToString(h.Sum(nil))
   267  
   268  	buf.Write([]byte("Digest "))
   269  	fmt.Fprintf(&buf,
   270  		`username="%s", realm="%s", nonce="%s", uri="%s", response="%s"`,
   271  		auth.Username, auth.Realm, auth.NONCE, url.Path, respdig,
   272  	)
   273  
   274  	if auth.Opaque != "" {
   275  		fmt.Fprintf(&buf, `, opaque="%s"`, auth.Opaque)
   276  	}
   277  	if auth.QOP != "" {
   278  		fmt.Fprintf(&buf, `, qop="%s", nc=%s, cnonce="%s"`, auth.QOP, ncStr, hnc)
   279  	}
   280  	if auth.Algorithm != "" {
   281  		fmt.Fprintf(&buf, `, algorithm="%s"`, auth.Algorithm)
   282  	}
   283  
   284  	return buf.String()
   285  }
   286  
   287  func (a digestAuth) setAuth(c *Client, r *http.Request) error {
   288  	resp, err := http.Get(r.URL.String())
   289  	if err != nil {
   290  		return err
   291  	}
   292  	setDigestAuth(r, a.Username, a.Password, resp, 1)
   293  	return nil
   294  }
   295  
   296  // DigestAuth returns an Auth implementation which sends
   297  // the provided username and password using HTTP Digest Authentication
   298  // (RFC 2617)
   299  func DigestAuth(username, password string) Auth {
   300  	return digestAuth{
   301  		Username: username,
   302  		Password: password,
   303  	}
   304  }