New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

net/http: add digest access authentication to Transport #29409

Open
Baozisoftware opened this Issue Dec 24, 2018 · 3 comments

Comments

Projects
None yet
5 participants
@Baozisoftware
Copy link

Baozisoftware commented Dec 24, 2018

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)
			}
			//...
		}
	}
	//...
}

@Baozisoftware Baozisoftware changed the title Tay add Digest access authentication to http.Transport Try add Digest access authentication to http.Transport Dec 24, 2018

@Baozisoftware Baozisoftware changed the title Try add Digest access authentication to http.Transport Try add digest access authentication to http.Transport Dec 24, 2018

@odeke-em odeke-em changed the title Try add digest access authentication to http.Transport proposal: net/http: add digest access authentication to Transport Dec 31, 2018

@gopherbot gopherbot added this to the Proposal milestone Dec 31, 2018

@gopherbot gopherbot added the Proposal label Dec 31, 2018

@odeke-em

This comment has been minimized.

Copy link
Member

odeke-em commented Dec 31, 2018

Thank you @Baozisoftware for filing this request and welcome to the Go project!

I'll page some experts @bradfitz @FiloSottile @agl.

@bradfitz

This comment has been minimized.

Copy link
Member

bradfitz commented Jan 2, 2019

I'm not opposed. I would use this functionality myself. (I have code in a number of places to do this by hand, which gets tedious.)

Please start by proposing a concrete API. Once we like the API we can then move on to reviewing code.

@bradfitz bradfitz changed the title proposal: net/http: add digest access authentication to Transport net/http: add digest access authentication to Transport Jan 2, 2019

@bradfitz bradfitz modified the milestones: Proposal, Unplanned Jan 2, 2019

@icholy

This comment has been minimized.

Copy link

icholy commented Jan 8, 2019

The best third party library I've found is https://github.com/bobziuchkovski/digest

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment