package gerrit import ( "crypto/md5" // nolint: gosec "crypto/rand" "encoding/base64" "errors" "fmt" "io" "net/http" "strings" ) var ( // ErrWWWAuthenticateHeaderMissing is returned by digestAuthHeader when the WWW-Authenticate header is missing ErrWWWAuthenticateHeaderMissing = errors.New("WWW-Authenticate header is missing") // ErrWWWAuthenticateHeaderInvalid is returned by digestAuthHeader when the WWW-Authenticate invalid ErrWWWAuthenticateHeaderInvalid = errors.New("WWW-Authenticate header is invalid") // ErrWWWAuthenticateHeaderNotDigest is returned by digestAuthHeader when the WWW-Authenticate header is not 'Digest' ErrWWWAuthenticateHeaderNotDigest = errors.New("WWW-Authenticate header type is not Digest") ) const ( // HTTP Basic Authentication authTypeBasic = 1 // HTTP Digest Authentication authTypeDigest = 2 // HTTP Cookie Authentication authTypeCookie = 3 ) // AuthenticationService contains Authentication related functions. // // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api.html#authentication type AuthenticationService struct { client *Client // Storage for authentication // Username or name of cookie name string // Password or value of cookie secret string authType int } // SetBasicAuth sets basic parameters for HTTP Basic auth func (s *AuthenticationService) SetBasicAuth(username, password string) { s.name = username s.secret = password s.authType = authTypeBasic } // SetDigestAuth sets digest parameters for HTTP Digest auth. func (s *AuthenticationService) SetDigestAuth(username, password string) { s.name = username s.secret = password s.authType = authTypeDigest } // digestAuthHeader is called by gerrit.Client.Do in the event the server // returns 401 Unauthorized and authType was set to authTypeDigest. The // resulting string is used to set the Authorization header before retrying // the request. func (s *AuthenticationService) digestAuthHeader(response *http.Response) (string, error) { authenticateHeader := response.Header.Get("WWW-Authenticate") if authenticateHeader == "" { return "", ErrWWWAuthenticateHeaderMissing } split := strings.SplitN(authenticateHeader, " ", 2) if len(split) != 2 { return "", ErrWWWAuthenticateHeaderInvalid } if split[0] != "Digest" { return "", ErrWWWAuthenticateHeaderNotDigest } // Iterate over all the fields from the WWW-Authenticate header // and create a map of keys and values. authenticate := map[string]string{} for _, value := range strings.Split(split[1], ",") { kv := strings.SplitN(value, "=", 2) if len(kv) != 2 { continue } key := strings.Trim(strings.Trim(kv[0], " "), "\"") value := strings.Trim(strings.Trim(kv[1], " "), "\"") authenticate[key] = value } // Gerrit usually responds without providing the algorithm. According // to RFC2617 if no algorithm is provided then the default is to use // MD5. At the time this code was implemented Gerrit did not appear // to support other algorithms or provide a means of changing the // algorithm. if value, ok := authenticate["algorithm"]; ok { if value != "MD5" { return "", fmt.Errorf( "algorithm not implemented: %s", value) } } realmHeader := authenticate["realm"] qopHeader := authenticate["qop"] nonceHeader := authenticate["nonce"] // If the server does not inform us what the uri is supposed // to be then use the last requests's uri instead. if _, ok := authenticate["uri"]; !ok { authenticate["uri"] = response.Request.URL.Path } uriHeader := authenticate["uri"] // A1 h := md5.New() // nolint: gosec A1 := fmt.Sprintf("%s:%s:%s", s.name, realmHeader, s.secret) if _, err := io.WriteString(h, A1); err != nil { return "", err } HA1 := fmt.Sprintf("%x", h.Sum(nil)) // A2 h = md5.New() // nolint: gosec A2 := fmt.Sprintf("%s:%s", response.Request.Method, uriHeader) if _, err := io.WriteString(h, A2); err != nil { return "", err } HA2 := fmt.Sprintf("%x", h.Sum(nil)) k := make([]byte, 12) for bytes := 0; bytes < len(k); { n, err := rand.Read(k[bytes:]) if err != nil { return "", fmt.Errorf("cnonce generation failed: %s", err) } bytes += n } cnonce := base64.StdEncoding.EncodeToString(k) digest := md5.New() // nolint: gosec if _, err := digest.Write([]byte(strings.Join([]string{HA1, nonceHeader, "00000001", cnonce, qopHeader, HA2}, ":"))); err != nil { return "", err } responseField := fmt.Sprintf("%x", digest.Sum(nil)) return fmt.Sprintf( `Digest username="%s", realm="%s", nonce="%s", uri="%s", cnonce="%s", nc=00000001, qop=%s, response="%s"`, s.name, realmHeader, nonceHeader, uriHeader, cnonce, qopHeader, responseField), nil } // SetCookieAuth sets basic parameters for HTTP Cookie func (s *AuthenticationService) SetCookieAuth(name, value string) { s.name = name s.secret = value s.authType = authTypeCookie } // HasBasicAuth checks if the auth type is HTTP Basic auth func (s *AuthenticationService) HasBasicAuth() bool { return s.authType == authTypeBasic } // HasDigestAuth checks if the auth type is HTTP Digest based func (s *AuthenticationService) HasDigestAuth() bool { return s.authType == authTypeDigest } // HasCookieAuth checks if the auth type is HTTP Cookie based func (s *AuthenticationService) HasCookieAuth() bool { return s.authType == authTypeCookie } // HasAuth checks if an auth type is used func (s *AuthenticationService) HasAuth() bool { return s.authType > 0 } // ResetAuth resets all former authentification settings func (s *AuthenticationService) ResetAuth() { s.name = "" s.secret = "" s.authType = 0 }