From 552e1b761e6c6653f44f631e57166622d14fdd54 Mon Sep 17 00:00:00 2001 From: wanglong001 <406134592@qq.com> Date: Sun, 18 Jul 2021 01:19:01 +0800 Subject: [PATCH] feature: add digest --- resources/js/curl-to-go.js | 312 +++++++++++++++++++++++++++++++++++++ 1 file changed, 312 insertions(+) diff --git a/resources/js/curl-to-go.js b/resources/js/curl-to-go.js index 5acd900..5ad55bb 100644 --- a/resources/js/curl-to-go.js +++ b/resources/js/curl-to-go.js @@ -215,6 +215,14 @@ function curlToGo(curl) { clientName = "client"; } + // digest + // --digest + if (req.digest) { + go += addDigestHead(); + go += 'client, err := NewTransport("'+ req.basicauth.user +'","'+ req.basicauth.pass +'").Client()\n'+ err; + clientName = "client"; + } + // load body data // KNOWN ISSUE: -d and --data are treated like --data-binary in // that we don't strip out carriage returns and newlines. @@ -306,7 +314,306 @@ function curlToGo(curl) { go += "\nresp, err := "+clientName+".Do(req)\n"; go += err+deferClose; + if (req.digest) { + go += '}\n' + } + return go; + + function addDigestHead(){ + return ['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")', + ')', + '// 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', + '}', + 'func DoRequest() (err error) {', + ].join("\n"); + } } // extractRelevantPieces returns an object with relevant pieces @@ -383,6 +690,9 @@ function curlToGo(curl) { if (dataFiles.length > 0) relevant.data.files = dataFiles; + // set digest value + relevant.digest = cmd.digest + var basicAuthString = ""; if (cmd.user && cmd.user.length > 0) basicAuthString = cmd.user[cmd.user.length-1]; @@ -637,4 +947,6 @@ function parseCommand(input, options) { function whitespace(ch) { return ch == " " || ch == "\t" || ch == "\n" || ch == "\r"; } + + // }