question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

curl --digest not working

See original GitHub issue

Are you requesting support for a new curl flag? If so, what is the flag and the equivalent Go code?

curl --user "{PUBLIC-KEY}:{PRIVATE-KEY}" --digest \
 --header "Accept: application/json" \
 --include \
 --request GET "https://{OPSMANAGER-HOST}:{PORT}/api/public/v1.0/groups/{PROJECT-ID}/hosts?pretty=true"

import (
	"bytes"
	"crypto/md5"
	"crypto/rand"
	"crypto/sha256"
	"errors"
	"fmt"
	"hash"
	"io"
	"io/ioutil"
	"net/http"
	"strings"
)

const (
	MsgAuth   string = "auth"
	AlgMD5    string = "MD5"
	AlgSha256 string = "SHA-256"
)

var (
	ErrNilTransport      = errors.New("transport is nil")
	ErrBadChallenge      = errors.New("challenge is bad")
	ErrAlgNotImplemented = errors.New("alg not implemented")
)



func DoRequest() (err error) {
	body := bytes.NewBufferString(data)
	req, err := http.NewRequest("GET","https://{OPSMANAGER-HOST}:{PORT}/api/public/v1.0/groups/{PROJECT-ID}/hosts?pretty=true",nil)
	if err != nil {
		return err
	}
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	client, err := NewTransport("{PUBLIC-KEY}","{PRIVATE-KEY}").Client()
	if err != nil {
		return err
	}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	bytesResp, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	fmt.Println(string(bytesResp))
	return
}

// 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: AlgMD5,
	}
	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":
			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
	impl       hashingFunc
}

type hashingFunc func() hash.Hash

func h(data string, f hashingFunc) (string, error) {
	hf := f()
	if _, err := io.WriteString(hf, data); err != nil {
		return "", err
	}
	return fmt.Sprintf("%x", hf.Sum(nil)), nil
}

func kd(secret, data string, f hashingFunc) (string, error) {
	return h(fmt.Sprintf("%s:%s", secret, data), f)
}

func (c *credentials) ha1() (string, error) {
	return h(fmt.Sprintf("%s:%s:%s", c.Username, c.Realm, c.password), c.impl)
}

func (c *credentials) ha2() (string, error) {
	return h(fmt.Sprintf("%s:%s", c.method, c.DigestURI), c.impl)
}

func (c *credentials) resp(cnonce string) (resp string, err error) {
	var ha1 string
	var ha2 string
	c.NonceCount++
	if c.MessageQop == MsgAuth {
		if cnonce != "" {
			c.Cnonce = cnonce
		} else {
			b := make([]byte, 8)
			_, err = io.ReadFull(rand.Reader, b)
			if err != nil {
				return "", err
			}
			c.Cnonce = fmt.Sprintf("%x", b)[:16]
		}
		if ha1, err = c.ha1(); err != nil {
			return "", err
		}
		if ha2, err = c.ha2(); err != nil {
			return "", err
		}
		return kd(ha1, fmt.Sprintf("%s:%08x:%s:%s:%s", c.Nonce, c.NonceCount, c.Cnonce, c.MessageQop, ha2), c.impl)
	} else if c.MessageQop == "" {
		if ha1, err = c.ha1(); err != nil {
			return "", err
		}
		if ha2, err = c.ha2(); err != nil {
			return "", err
		}
		return kd(ha1, fmt.Sprintf("%s:%s", c.Nonce, ha2), c.impl)
	}
	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 != AlgMD5 && c.Algorithm != AlgSha256 {
		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 != MsgAuth && 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),
		fmt.Sprintf(`nonce="%s"`, c.Nonce),
		fmt.Sprintf(`uri="%s"`, c.DigestURI),
		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),
			fmt.Sprintf("nc=%08x", c.NonceCount),
			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, error) {
	cred := &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,
	}
	switch c.Algorithm {
	case AlgMD5:
		cred.impl = md5.New
	case AlgSha256:
		cred.impl = sha256.New
	default:
		return nil, ErrAlgNotImplemented
	}

	return cred, nil
}

// 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.
	origReq := new(http.Request)
	*origReq = *req
	origReq.Header = make(http.Header, len(req.Header))
	for k, s := range req.Header {
		origReq.Header[k] = s
	}

	// We'll need the request body twice. In some cases we can use GetBody
	// to obtain a fresh reader for the second request, which we do right
	// before the RoundTrip(origReq) call. If GetBody is unavailable, read
	// the body into a memory buffer and use it for both requests.
	if req.Body != nil && req.GetBody == nil {
		body, err := ioutil.ReadAll(req.Body)
		if err != nil {
			return nil, err
		}
		req.Body = ioutil.NopCloser(bytes.NewBuffer(body))
		origReq.Body = ioutil.NopCloser(bytes.NewBuffer(body))
	}
	// Make a request to get the 401 that contains the challenge.
	challenge, resp, err := t.fetchChallenge(req)
	if challenge == "" || err != nil {
		return resp, err
	}

	c, err := parseChallenge(challenge)
	if err != nil {
		return nil, err
	}

	// Form credentials based on the challenge.
	cr, err := t.newCredentials(origReq, c)
	if err != nil {
		return nil, err
	}
	auth, err := cr.authorize()
	if err != nil {
		return nil, err
	}

	// Obtain a fresh body.
	if req.Body != nil && req.GetBody != nil {
		origReq.Body, err = req.GetBody()
		if err != nil {
			return nil, err
		}
	}

	// Make authenticated request.
	origReq.Header.Set("Authorization", auth)
	return t.Transport.RoundTrip(origReq)
}

func (t *Transport) fetchChallenge(req *http.Request) (string, *http.Response, error) {
	resp, err := t.Transport.RoundTrip(req)
	if err != nil {
		return "", resp, err
	}
	if resp.StatusCode != http.StatusUnauthorized {
		return "", resp, nil
	}

	// We'll no longer use the initial response, so close it
	defer func() {
		// Ensure the response body is fully read and closed
		// before we reconnect, so that we reuse the same TCP connection.
		// Close the previous response's body. But read at least some of
		// the body so if it's small the underlying TCP connection will be
		// re-used. No need to check for errors: if it fails, the Transport
		// won't reuse it anyway.
		const maxBodySlurpSize = 2 << 10
		if resp.ContentLength == -1 || resp.ContentLength <= maxBodySlurpSize {
			_, _ = io.CopyN(ioutil.Discard, resp.Body, maxBodySlurpSize)
		}

		resp.Body.Close()
	}()
	return resp.Header.Get("WWW-Authenticate"), resp, nil
}

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


Issue Analytics

  • State:open
  • Created 2 years ago
  • Comments:7 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
wanglong001commented, Jul 23, 2021

You are right, I am very supportive of originality, we will write one when we have time

1reaction
mholtcommented, Jul 20, 2021

Ok, thanks for the link. If any of the code was borrowed, we need to conform to the license restrictions. Maybe it’s a little tricky since we’re doing code generation (see the whole GitHub Copilot fiasco, though technically that’s very different, in practice it’s a very similar task)… it’s weird because the code we generate we don’t really care about license or have one, it’s basically public domain. So it’ll be difficult to use unoriginal code that has a license attached to that part of it. And part of conforming to the restrictions of the license is crediting the authors and putting in the copyright. But only that part of the code would be “copyrighted,” I guess.

See what I mean? Is there any way we can do this without licensing someone else’s code?

Read more comments on GitHub >

github_iconTop Results From Across the Web

DIGEST authentication not working with curl #397 - GitHub
I'm not able to get a successful response with the following command: curl -D - --digest -u "user:passwd" ...
Read more >
Curl not working with Digest Authentication - OpenWrt Forum
I'm using curl on a OpenWrt device to fetch images from a network camera. The problem is that the camera supports only Digest...
Read more >
How do I send a digest auth request using curl? - Super User
CURL takes care of computing the client response for you. This is exactly what "supporting of digest authentication" means for any client.
Read more >
How do I send a digest auth request using curl? - Stack Overflow
The only thing you do have to is username/password pair. CURL takes care of computing the client response for you. This is exactly...
Read more >
Digest authentification in Geoserver not working
I am trying to use Digest authentification in Geoserver. But when I send a Curl request I get the 401 error.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found