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
}