Skip to content
New issue

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

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

Already on GitHub? # to your account

curl --digest not working #45

Open
wanglong001 opened this issue Jun 29, 2021 · 7 comments
Open

curl --digest not working #45

wanglong001 opened this issue Jun 29, 2021 · 7 comments

Comments

@wanglong001
Copy link

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
}
@mholt
Copy link
Owner

mholt commented Jul 12, 2021

Thanks for the feature request and the Go code -- I'm a bit busy lately but if you want to submit a PR, that would be welcomed. 👍

@wanglong001
Copy link
Author

Thanks for the feature request and the Go code -- I'm a bit busy lately but if you want to submit a PR, that would be welcomed. 👍

Hi,
I am honored to contribute code,and
find a time to submit a PR

@mholt
Copy link
Owner

mholt commented Jul 19, 2021

Just to clarify, you are the original author of the above Go code, correct?

@wanglong001
Copy link
Author

Just to clarify, you are the original author of the above Go code, correct?

Not all, but refer to https://github.com/mongodb-forks/digest/blob/master/digest.go

@mholt
Copy link
Owner

mholt commented 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?

@wanglong001
Copy link
Author

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

@mholt
Copy link
Owner

mholt commented Jul 23, 2021

Thanks for understanding; and it can be simple as needed, that's probably best.

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants