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 }