auth_digest.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  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. package upstreamproxy
  20. import (
  21. "crypto/md5"
  22. "crypto/rand"
  23. "encoding/base64"
  24. "fmt"
  25. "net/http"
  26. "strings"
  27. )
  28. type DigestHttpAuthState int
  29. const (
  30. DIGEST_HTTP_AUTH_STATE_CHALLENGE_RECEIVED DigestHttpAuthState = iota
  31. DIGEST_HTTP_AUTH_STATE_RESPONSE_GENERATED
  32. )
  33. type DigestHttpAuthenticator struct {
  34. state DigestHttpAuthState
  35. }
  36. func newDigestAuthenticator() *DigestHttpAuthenticator {
  37. return &DigestHttpAuthenticator{state: DIGEST_HTTP_AUTH_STATE_CHALLENGE_RECEIVED}
  38. }
  39. /* Adapted from https://github.com/ryanjdew/http-digest-auth-client */
  40. type DigestHeaders struct {
  41. Realm string
  42. Qop string
  43. Method string
  44. Nonce string
  45. Opaque string
  46. Algorithm string
  47. HA1 string
  48. HA2 string
  49. Cnonce string
  50. Uri string
  51. Nc int16
  52. Username string
  53. Password string
  54. }
  55. // ApplyAuth adds proper auth header to the passed request
  56. func (d *DigestHeaders) ApplyAuth(req *http.Request) {
  57. d.Nc += 0x1
  58. d.Cnonce = randomKey()
  59. d.Method = req.Method
  60. d.digestChecksum()
  61. response := h(strings.Join([]string{d.HA1, d.Nonce, fmt.Sprintf("%08x", d.Nc),
  62. d.Cnonce, d.Qop, d.HA2}, ":"))
  63. AuthHeader := fmt.Sprintf(`Digest username="%s", realm="%s", nonce="%s", uri="%s", response="%s", qop=%s, nc=%08x, cnonce="%s", algorithm=%s`,
  64. d.Username, d.Realm, d.Nonce, d.Uri, response, d.Qop, d.Nc, d.Cnonce, d.Algorithm)
  65. if d.Opaque != "" {
  66. AuthHeader = fmt.Sprintf(`%s, opaque="%s"`, AuthHeader, d.Opaque)
  67. }
  68. req.Header.Set("Proxy-Authorization", AuthHeader)
  69. }
  70. func (d *DigestHeaders) digestChecksum() {
  71. var A1 string
  72. switch d.Algorithm {
  73. case "MD5":
  74. //HA1=MD5(username:realm:password)
  75. A1 = fmt.Sprintf("%s:%s:%s", d.Username, d.Realm, d.Password)
  76. case "MD5-sess":
  77. // HA1=MD5(MD5(username:realm:password):nonce:cnonce)
  78. str := fmt.Sprintf("%s:%s:%s", d.Username, d.Realm, d.Password)
  79. A1 = fmt.Sprintf("%s:%s:%s", h(str), d.Nonce, d.Cnonce)
  80. default:
  81. //token
  82. }
  83. if A1 == "" {
  84. return
  85. }
  86. //HA1
  87. d.HA1 = h(A1)
  88. // HA2
  89. A2 := fmt.Sprintf("%s:%s", d.Method, d.Uri)
  90. d.HA2 = h(A2)
  91. }
  92. func randomKey() string {
  93. k := make([]byte, 12)
  94. for bytes := 0; bytes < len(k); {
  95. n, err := rand.Read(k[bytes:])
  96. if err != nil {
  97. panic("rand.Read() failed")
  98. }
  99. k[bytes] = byte(bytes)
  100. bytes += n
  101. }
  102. return base64.StdEncoding.EncodeToString(k)
  103. }
  104. /*
  105. H function for MD5 algorithm (returns a lower-case hex MD5 digest)
  106. */
  107. func h(data string) string {
  108. digest := md5.New()
  109. digest.Write([]byte(data))
  110. return fmt.Sprintf("%x", digest.Sum(nil))
  111. }
  112. func (a *DigestHttpAuthenticator) Authenticate(req *http.Request, resp *http.Response, username, password string) error {
  113. if a.state != DIGEST_HTTP_AUTH_STATE_CHALLENGE_RECEIVED {
  114. return proxyError(fmt.Errorf("Authorization is not accepted by the proxy server"))
  115. }
  116. challenges, err := parseAuthChallenge(resp)
  117. if err != nil {
  118. //already wrapped in proxyError
  119. return err
  120. }
  121. challenge := challenges["Digest"]
  122. if len(challenge) == 0 {
  123. return proxyError(fmt.Errorf("Digest authentication challenge is empty"))
  124. }
  125. //parse challenge
  126. digestParams := map[string]string{}
  127. for _, keyval := range strings.Split(challenge, ",") {
  128. param := strings.SplitN(keyval, "=", 2)
  129. if len(param) != 2 {
  130. continue
  131. }
  132. digestParams[strings.Trim(param[0], "\" ")] = strings.Trim(param[1], "\" ")
  133. }
  134. if len(digestParams) == 0 {
  135. return proxyError(fmt.Errorf("Digest authentication challenge is malformed"))
  136. }
  137. algorithm := digestParams["algorithm"]
  138. d := &DigestHeaders{}
  139. if req.Method == "CONNECT" {
  140. d.Uri = req.URL.Host
  141. } else {
  142. d.Uri = req.URL.Scheme + "://" + req.URL.Host + req.URL.RequestURI()
  143. }
  144. d.Realm = digestParams["realm"]
  145. d.Qop = digestParams["qop"]
  146. d.Nonce = digestParams["nonce"]
  147. d.Opaque = digestParams["opaque"]
  148. if algorithm == "" {
  149. d.Algorithm = "MD5"
  150. } else {
  151. d.Algorithm = digestParams["algorithm"]
  152. }
  153. d.Nc = 0x0
  154. d.Username = username
  155. d.Password = password
  156. d.ApplyAuth(req)
  157. a.state = DIGEST_HTTP_AUTH_STATE_RESPONSE_GENERATED
  158. return nil
  159. }