curl-to-go
curl-to-go copied to clipboard
curl --digest not working
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
}
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. :+1:
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
Just to clarify, you are the original author of the above Go code, correct?
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
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?
You are right, I am very supportive of originality, we will write one when we have time
Thanks for understanding; and it can be simple as needed, that's probably best.