Skip to content

proposal: net/http: support for digest authentication #29409

@Baozisoftware

Description

@Baozisoftware

I tried to add Digest access authentication support in http.Transport today. currently it is available for proxy servers. (compatible with basic auth,but not tested.)
I hope the official can integrate it. After all, this is a base library.
Reference: https://github.com/delphinus/go-digest-request

Mainly modified:
Transport.roundTrip
Transport.dialConn

package http

type Transport struct {
	//...
	// digest auth fields
	nonceCount nonceCount
	authParts  map[string]string
	needAuth   bool
	basicAuth  bool
}

const nonce = "nonce"
const qop = "qop"
const realm = "realm"
const proxyAuthenticate = "Proxy-Authenticate"
const proxyAuthorization = "Proxy-Authorization"

var digestAuthHeanderswanted = []string{nonce, qop, realm}

func getRandomString(l int) string {
	str := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
	bytes := []byte(str)
	var result []byte
	lstr := len(str) - 1
	for i := 0; i < l; i++ {
		n := getRandomInt(0, lstr)
		result = append(result, bytes[n])
	}
	return string(result)
}

var r = rand.New(rand.NewSource(time.Now().UnixNano()))

func getRandomInt(min, max int) int {
	sub := max - min + 1
	if sub <= 1 {
		return min
	}
	return min + r.Intn(sub)
}

func (t *Transport) makeAuthorization(proxy *url.URL, req *Request, parts map[string]string) string {
	username, password := "", ""
	if u := proxy.User; u != nil {
		username = u.Username()
		password, _ = u.Password()
	}
	ha1 := getMD5([]string{username, parts[realm], password})
	ha2 := getMD5([]string{req.Method, req.URL.String()})
	cnonce := getRandomString(16)
	nc := t.getNonceCount()
	response := getMD5([]string{
		ha1,
		parts[nonce],
		nc,
		cnonce,
		parts[qop],
		ha2,
	})
	return fmt.Sprintf(
		`Digest username="%s", realm="%s", nonce="%s", uri="%s", qop=%s, nc=%s, cnonce="%s", response="%s"`,
		username,
		parts[realm],
		parts[nonce],
		req.URL.String(),
		parts[qop],
		nc,
		cnonce,
		response,
	)
}

func makeParts(resp *Response) (map[string]string, error) {
	headers := strings.Split(resp.Header[proxyAuthenticate][0], ",")
	parts := make(map[string]string, len(digestAuthHeanderswanted))
	for _, r := range headers {
		for _, w := range digestAuthHeanderswanted {
			if strings.Contains(r, w) {
				parts[w] = strings.Split(r, `"`)[1]
			}
		}
	}

	if len(parts) != len(digestAuthHeanderswanted) {
		return nil, fmt.Errorf("header is invalid: %+v", parts)
	}

	return parts, nil
}

type nonceCount int

func (nc nonceCount) String() string {
	c := int(nc)
	return fmt.Sprintf("%08x", c)
}

func getMD5(texts []string) string {
	h := md5.New()
	_, _ = io.WriteString(h, strings.Join(texts, ":"))
	return hex.EncodeToString(h.Sum(nil))
}

func (t *Transport) getNonceCount() string {
	t.nonceCount++
	return t.nonceCount.String()
}

func (t *Transport) roundTrip(req *Request) (*Response, error) {
	//...
	isHTTP := scheme == "http" || scheme == "https"
	if isHTTP {
		if scheme == "http" && t.needAuth && !t.basicAuth {
			p, err := t.Proxy(req)
			if err == nil {
				auth := t.makeAuthorization(p, req, t.authParts)
				req.Header.Add(proxyAuthorization, auth)
			}
		}
		//...
	}
	//...
	for {
		//...
		if err == nil {
			if resp.StatusCode == 407 && req.URL.Scheme == "http" {
				if strings.HasPrefix(resp.Header[proxyAuthenticate][0], "Basic") {
					t.basicAuth = true
				} else {
					t.basicAuth = false
					t.authParts, err = makeParts(resp)
					if err != nil {
						return nil, err
					}
				}
				t.needAuth = true
				return t.roundTrip(req)
			}
			return resp, nil
		}
		//...
	}
}

func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (*persistConn, error) {
	// Proxy setup.
	switch {
	//...
	case cm.targetScheme == "http":
		pconn.isProxy = true
		if t.needAuth && t.basicAuth {
			if pa := cm.proxyAuth(); pa != "" {
				pconn.mutateHeaderFunc = func(h Header) {
					h.Set(proxyAuthorization, pa)
				}
			}
		}
	case cm.targetScheme == "https":
		conn := pconn.conn
		hdr := t.ProxyConnectHeader
		if hdr == nil {
			hdr = make(Header)
		}
		connectReq := &Request{
			Method: "CONNECT",
			URL:    &url.URL{Opaque: cm.targetAddr},
			Host:   cm.targetAddr,
			Header: hdr,
		}

		if t.needAuth {
			auth := ""
			if t.basicAuth {
				if pa := cm.proxyAuth(); pa != "" {
					auth = pa
				}
			} else {
				auth = t.makeAuthorization(cm.proxyURL, connectReq, t.authParts)
			}
			connectReq.Header.Add(proxyAuthorization, auth)
		}

		connectReq.Write(conn)

		br := bufio.NewReader(conn)
		resp, err := ReadResponse(br, connectReq)
		if err != nil {
			conn.Close()
			return nil, err
		}
		if resp.StatusCode != 200 {
			if resp.StatusCode == 407 {
				t.authParts, err = makeParts(resp)
				if err != nil {
					return nil, err
				}
				t.needAuth = true
				return t.dialConn(ctx, cm)
			}
			//...
		}
	}
	//...
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status

    Incoming

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions