// Copyright 2013 M-Lab // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // The digest package provides an implementation of http.RoundTripper that takes // care of HTTP Digest Authentication (http://www.ietf.org/rfc/rfc2617.txt). // This only implements the MD5 and "auth" portions of the RFC, but that covers // the majority of avalible server side implementations including apache web // server. // // Example usage: // // t := NewTransport("myUserName", "myP@55w0rd") // req, err := http.NewRequest("GET", "http://notreal.com/path?arg=1", nil) // if err != nil { // return err // } // resp, err := t.RoundTrip(req) // if err != nil { // return err // } // // OR it can be used as a client: // // c, err := t.Client() // if err != nil { // return err // } // resp, err := c.Get("http://notreal.com/path?arg=1") // if err != nil { // return err // } package digest import ( "bytes" "crypto/md5" "crypto/rand" "errors" "fmt" "io" "io/ioutil" "net/http" "strings" ) var ( ErrNilTransport = errors.New("Transport is nil") ErrBadChallenge = errors.New("Challenge is bad") ErrAlgNotImplemented = errors.New("Alg not implemented") ) // Transport is an implementation of http.RoundTripper that takes care of http // digest authentication. type Transport struct { Username string Password string Transport http.RoundTripper } // NewTransport creates a new digest transport using the http.DefaultTransport. func NewTransport(username, password string) *Transport { t := &Transport{ Username: username, Password: password, } t.Transport = http.DefaultTransport return t } type challenge struct { Realm string Domain string Nonce string Opaque string Stale string Algorithm string Qop string } func parseChallenge(input string) (*challenge, error) { const ws = " \n\r\t" const qs = `"` s := strings.Trim(input, ws) if !strings.HasPrefix(s, "Digest ") { return nil, ErrBadChallenge } s = strings.Trim(s[7:], ws) sl := strings.Split(s, ", ") c := &challenge{ Algorithm: "MD5", } var r []string for i := range sl { r = strings.SplitN(sl[i], "=", 2) switch r[0] { case "realm": c.Realm = strings.Trim(r[1], qs) case "domain": c.Domain = strings.Trim(r[1], qs) case "nonce": c.Nonce = strings.Trim(r[1], qs) case "opaque": c.Opaque = strings.Trim(r[1], qs) case "stale": c.Stale = strings.Trim(r[1], qs) case "algorithm": c.Algorithm = strings.Trim(r[1], qs) case "qop": //TODO(gavaletz) should be an array of strings? c.Qop = strings.Trim(r[1], qs) default: return nil, ErrBadChallenge } } return c, nil } type credentials struct { Username string Realm string Nonce string DigestURI string Algorithm string Cnonce string Opaque string MessageQop string NonceCount int method string password string } func h(data string) string { hf := md5.New() io.WriteString(hf, data) return fmt.Sprintf("%x", hf.Sum(nil)) } func kd(secret, data string) string { return h(fmt.Sprintf("%s:%s", secret, data)) } func (c *credentials) ha1() string { return h(fmt.Sprintf("%s:%s:%s", c.Username, c.Realm, c.password)) } func (c *credentials) ha2() string { return h(fmt.Sprintf("%s:%s", c.method, c.DigestURI)) } func (c *credentials) resp(cnonce string) (string, error) { c.NonceCount++ if c.MessageQop == "auth" { if cnonce != "" { c.Cnonce = cnonce } else { b := make([]byte, 8) io.ReadFull(rand.Reader, b) c.Cnonce = fmt.Sprintf("%x", b)[:16] } return kd(c.ha1(), fmt.Sprintf("%s:%08x:%s:%s:%s", c.Nonce, c.NonceCount, c.Cnonce, c.MessageQop, c.ha2())), nil } else if c.MessageQop == "" { return kd(c.ha1(), fmt.Sprintf("%s:%s", c.Nonce, c.ha2())), nil } return "", ErrAlgNotImplemented } func (c *credentials) authorize() (string, error) { // Note that this is only implemented for MD5 and NOT MD5-sess. // MD5-sess is rarely supported and those that do are a big mess. if c.Algorithm != "MD5" { return "", ErrAlgNotImplemented } // Note that this is NOT implemented for "qop=auth-int". Similarly the // auth-int server side implementations that do exist are a mess. if c.MessageQop != "auth" && c.MessageQop != "" { return "", ErrAlgNotImplemented } resp, err := c.resp("") if err != nil { return "", ErrAlgNotImplemented } sl := []string{fmt.Sprintf(`username="%s"`, c.Username)} sl = append(sl, fmt.Sprintf(`realm="%s"`, c.Realm)) sl = append(sl, fmt.Sprintf(`nonce="%s"`, c.Nonce)) sl = append(sl, fmt.Sprintf(`uri="%s"`, c.DigestURI)) sl = append(sl, fmt.Sprintf(`response="%s"`, resp)) if c.Algorithm != "" { sl = append(sl, fmt.Sprintf(`algorithm="%s"`, c.Algorithm)) } if c.Opaque != "" { sl = append(sl, fmt.Sprintf(`opaque="%s"`, c.Opaque)) } if c.MessageQop != "" { sl = append(sl, fmt.Sprintf("qop=%s", c.MessageQop)) sl = append(sl, fmt.Sprintf("nc=%08x", c.NonceCount)) sl = append(sl, fmt.Sprintf(`cnonce="%s"`, c.Cnonce)) } return fmt.Sprintf("Digest %s", strings.Join(sl, ", ")), nil } func (t *Transport) newCredentials(req *http.Request, c *challenge) *credentials { return &credentials{ Username: t.Username, Realm: c.Realm, Nonce: c.Nonce, DigestURI: req.URL.RequestURI(), Algorithm: c.Algorithm, Opaque: c.Opaque, MessageQop: c.Qop, // "auth" must be a single value NonceCount: 0, method: req.Method, password: t.Password, } } // RoundTrip makes a request expecting a 401 response that will require digest // authentication. It creates the credentials it needs and makes a follow-up // request. func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { if t.Transport == nil { return nil, ErrNilTransport } // Copy the request so we don't modify the input. req2 := new(http.Request) *req2 = *req req2.Header = make(http.Header) for k, s := range req.Header { req2.Header[k] = s } // We need two reader for the body. if req.Body != nil { tmp, err := ioutil.ReadAll(req.Body) if err != nil { return nil, err } reqBody01 := ioutil.NopCloser(bytes.NewBuffer(tmp)) reqBody02 := ioutil.NopCloser(bytes.NewBuffer(tmp)) req.Body = reqBody01 req2.Body = reqBody02 } // Make a request to get the 401 that contains the challenge. resp, err := t.Transport.RoundTrip(req) if err != nil || resp.StatusCode != 401 { return resp, err } chal := resp.Header.Get("WWW-Authenticate") c, err := parseChallenge(chal) if err != nil { return resp, err } // Form credentials based on the challenge. cr := t.newCredentials(req2, c) auth, err := cr.authorize() if err != nil { return resp, err } // We'll no longer use the initial response, so close it resp.Body.Close() // Make authenticated request. req2.Header.Set("Authorization", auth) return t.Transport.RoundTrip(req2) } // Client returns an HTTP client that uses the digest transport. func (t *Transport) Client() (*http.Client, error) { if t.Transport == nil { return nil, ErrNilTransport } return &http.Client{Transport: t}, nil }