github.com/psiphon-labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/upstreamproxy/auth_digest.go (about)

     1  /*
     2   * Copyright (c) 2015, Psiphon Inc.
     3   * All rights reserved.
     4   *
     5   * This program is free software: you can redistribute it and/or modify
     6   * it under the terms of the GNU General Public License as published by
     7   * the Free Software Foundation, either version 3 of the License, or
     8   * (at your option) any later version.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package upstreamproxy
    21  
    22  import (
    23  	"crypto/md5"
    24  	"crypto/rand"
    25  	"encoding/base64"
    26  	"fmt"
    27  	"net/http"
    28  	"strings"
    29  )
    30  
    31  type DigestHttpAuthState int
    32  
    33  const (
    34  	DIGEST_HTTP_AUTH_STATE_CHALLENGE_RECEIVED DigestHttpAuthState = iota
    35  	DIGEST_HTTP_AUTH_STATE_RESPONSE_GENERATED
    36  )
    37  
    38  type DigestHttpAuthenticator struct {
    39  	state         DigestHttpAuthState
    40  	username      string
    41  	password      string
    42  	digestHeaders *DigestHeaders
    43  }
    44  
    45  func newDigestAuthenticator(username, password string) *DigestHttpAuthenticator {
    46  	return &DigestHttpAuthenticator{
    47  		state:    DIGEST_HTTP_AUTH_STATE_CHALLENGE_RECEIVED,
    48  		username: username,
    49  		password: password,
    50  	}
    51  }
    52  
    53  /* Adapted from https://github.com/ryanjdew/http-digest-auth-client */
    54  
    55  type DigestHeaders struct {
    56  	Realm     string
    57  	Qop       string
    58  	Method    string
    59  	Nonce     string
    60  	Opaque    string
    61  	Algorithm string
    62  	HA1       string
    63  	HA2       string
    64  	Cnonce    string
    65  	Uri       string
    66  	Nc        int16
    67  	Username  string
    68  	Password  string
    69  }
    70  
    71  // ApplyAuth adds proper auth header to the passed request
    72  func (d *DigestHeaders) ApplyAuth(req *http.Request) {
    73  	d.Nc += 0x1
    74  	d.Method = req.Method
    75  	d.digestChecksum()
    76  	response := h(strings.Join([]string{d.HA1, d.Nonce, fmt.Sprintf("%08x", d.Nc),
    77  		d.Cnonce, d.Qop, d.HA2}, ":"))
    78  	AuthHeader := fmt.Sprintf(`Digest username="%s", realm="%s", nonce="%s", uri="%s", response="%s", qop=%s, nc=%08x, cnonce="%s", algorithm=%s`,
    79  		d.Username, d.Realm, d.Nonce, d.Uri, response, d.Qop, d.Nc, d.Cnonce, d.Algorithm)
    80  	if d.Opaque != "" {
    81  		AuthHeader = fmt.Sprintf(`%s, opaque="%s"`, AuthHeader, d.Opaque)
    82  	}
    83  	req.Header.Set("Proxy-Authorization", AuthHeader)
    84  }
    85  
    86  func (d *DigestHeaders) digestChecksum() {
    87  	var A1 string
    88  	switch d.Algorithm {
    89  	case "MD5":
    90  		// HA1=MD5(username:realm:password)
    91  		A1 = fmt.Sprintf("%s:%s:%s", d.Username, d.Realm, d.Password)
    92  
    93  	case "MD5-sess":
    94  		// HA1=MD5(MD5(username:realm:password):nonce:cnonce)
    95  		str := fmt.Sprintf("%s:%s:%s", d.Username, d.Realm, d.Password)
    96  		A1 = fmt.Sprintf("%s:%s:%s", h(str), d.Nonce, d.Cnonce)
    97  	default:
    98  		// Token
    99  	}
   100  	if A1 == "" {
   101  		return
   102  	}
   103  	// HA1
   104  	d.HA1 = h(A1)
   105  	// HA2
   106  	A2 := fmt.Sprintf("%s:%s", d.Method, d.Uri)
   107  	d.HA2 = h(A2)
   108  
   109  }
   110  
   111  func randomKey() string {
   112  	k := make([]byte, 12)
   113  	for bytes := 0; bytes < len(k); {
   114  		n, err := rand.Read(k[bytes:])
   115  		if err != nil {
   116  			panic("rand.Read() failed")
   117  		}
   118  		k[bytes] = byte(bytes)
   119  		bytes += n
   120  	}
   121  	return base64.StdEncoding.EncodeToString(k)
   122  }
   123  
   124  /*
   125  H function for MD5 algorithm (returns a lower-case hex MD5 digest)
   126  */
   127  func h(data string) string {
   128  	digest := md5.New()
   129  	digest.Write([]byte(data))
   130  	return fmt.Sprintf("%x", digest.Sum(nil))
   131  }
   132  
   133  func (a *DigestHttpAuthenticator) Authenticate(req *http.Request, resp *http.Response) error {
   134  	if a.state != DIGEST_HTTP_AUTH_STATE_CHALLENGE_RECEIVED {
   135  		return proxyError(fmt.Errorf("authorization is not accepted by the proxy server"))
   136  	}
   137  	challenges, err := parseAuthChallenge(resp)
   138  	if err != nil {
   139  		// Already wrapped in proxyError
   140  		return err
   141  	}
   142  	challenge := challenges["Digest"]
   143  	if len(challenge) == 0 {
   144  		return proxyError(fmt.Errorf("digest authentication challenge is empty"))
   145  	}
   146  	// Parse challenge
   147  	digestParams := map[string]string{}
   148  	for _, keyval := range strings.Split(challenge, ",") {
   149  		param := strings.SplitN(keyval, "=", 2)
   150  		if len(param) != 2 {
   151  			continue
   152  		}
   153  		digestParams[strings.Trim(param[0], "\" ")] = strings.Trim(param[1], "\" ")
   154  	}
   155  	if len(digestParams) == 0 {
   156  		return proxyError(fmt.Errorf("digest authentication challenge is malformed"))
   157  	}
   158  
   159  	algorithm := digestParams["algorithm"]
   160  
   161  	if stale, ok := digestParams["stale"]; ok && stale == "true" {
   162  		// Server indicated that the nonce is stale
   163  		// Reset auth cache and state
   164  		a.digestHeaders = nil
   165  		a.state = DIGEST_HTTP_AUTH_STATE_CHALLENGE_RECEIVED
   166  		return nil
   167  	}
   168  
   169  	if a.digestHeaders == nil {
   170  		d := &DigestHeaders{}
   171  		if req.Method == "CONNECT" {
   172  			d.Uri = req.URL.Host
   173  		} else {
   174  			d.Uri = req.URL.Scheme + "://" + req.URL.Host + req.URL.RequestURI()
   175  		}
   176  		d.Realm = digestParams["realm"]
   177  		d.Qop = digestParams["qop"]
   178  		d.Nonce = digestParams["nonce"]
   179  		d.Opaque = digestParams["opaque"]
   180  		if algorithm == "" {
   181  			d.Algorithm = "MD5"
   182  		} else {
   183  			d.Algorithm = digestParams["algorithm"]
   184  		}
   185  		d.Nc = 0x0
   186  		d.Cnonce = randomKey()
   187  		d.Username = a.username
   188  		d.Password = a.password
   189  		a.digestHeaders = d
   190  	}
   191  
   192  	a.digestHeaders.ApplyAuth(req)
   193  	a.state = DIGEST_HTTP_AUTH_STATE_RESPONSE_GENERATED
   194  	return nil
   195  }
   196  
   197  func (a *DigestHttpAuthenticator) IsConnectionBased() bool {
   198  	return false
   199  }
   200  
   201  func (a *DigestHttpAuthenticator) IsComplete() bool {
   202  	return a.state == DIGEST_HTTP_AUTH_STATE_RESPONSE_GENERATED
   203  }
   204  
   205  func (a *DigestHttpAuthenticator) Reset() {
   206  	a.state = DIGEST_HTTP_AUTH_STATE_CHALLENGE_RECEIVED
   207  }
   208  
   209  func (a *DigestHttpAuthenticator) PreAuthenticate(req *http.Request) error {
   210  	if a.digestHeaders != nil {
   211  		a.digestHeaders.ApplyAuth(req)
   212  	}
   213  	return nil
   214  }