| 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 |
| } |