github.com/psiphon-labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/upstreamproxy/transport_proxy_auth.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 "bytes" 24 "fmt" 25 "io/ioutil" 26 "net/http" 27 ) 28 29 // ProxyAuthTransport provides support for proxy authentication when doing plain HTTP 30 // by tapping into HTTP conversation and adding authentication headers to the requests 31 // when requested by server 32 // 33 // Limitation: in violation of https://golang.org/pkg/net/http/#RoundTripper, 34 // ProxyAuthTransport is _not_ safe for concurrent RoundTrip calls. This is acceptable 35 // for its use in Psiphon to provide upstream proxy support for meek, which makes only 36 // serial RoundTrip calls. Concurrent RoundTrip calls will result in data race conditions 37 // and undefined behavior during an authentication handshake. 38 type ProxyAuthTransport struct { 39 *http.Transport 40 username string 41 password string 42 authenticator HttpAuthenticator 43 customHeaders http.Header 44 clonedBodyBuffer bytes.Buffer 45 } 46 47 func NewProxyAuthTransport( 48 rawTransport *http.Transport, 49 customHeaders http.Header) (*ProxyAuthTransport, error) { 50 51 if rawTransport.Proxy == nil { 52 return nil, fmt.Errorf("rawTransport must have Proxy") 53 } 54 55 tr := &ProxyAuthTransport{ 56 Transport: rawTransport, 57 customHeaders: customHeaders, 58 } 59 60 proxyUrl, err := rawTransport.Proxy(nil) 61 if err != nil { 62 return nil, err 63 } 64 if proxyUrl.Scheme != "http" { 65 return nil, fmt.Errorf("%s unsupported", proxyUrl.Scheme) 66 } 67 if proxyUrl.User != nil { 68 tr.username = proxyUrl.User.Username() 69 tr.password, _ = proxyUrl.User.Password() 70 } 71 // strip username and password from the proxyURL because 72 // we do not want the wrapped transport to handle authentication 73 proxyUrl.User = nil 74 rawTransport.Proxy = http.ProxyURL(proxyUrl) 75 76 return tr, nil 77 } 78 79 func (tr *ProxyAuthTransport) RoundTrip(request *http.Request) (*http.Response, error) { 80 81 if request.URL.Scheme != "http" { 82 return nil, fmt.Errorf("%s unsupported", request.URL.Scheme) 83 } 84 85 // Notes: 86 // 87 // - The 407 authentication loop assumes no concurrent calls of RoundTrip 88 // and additionally assumes that serial RoundTrip calls will always 89 // resuse any existing HTTP persistent conn. The entire authentication 90 // handshake must occur on the same HTTP persistent conn. 91 // 92 // - Requests are cloned for the lifetime of the ProxyAuthTransport, 93 // since we don't know when the next initial RoundTrip may need to enter 94 // the 407 authentication loop, which requires the initial request to be 95 // cloned and replayable. Even if we hook into the Close call for any 96 // existing HTTP persistent conn, it could be that it closes only after 97 // RoundTrip is called. 98 // 99 // - Cloning reuses a buffer (clonedBodyBuffer) to store the request body 100 // to avoid excessive allocations. 101 102 var cachedRequestBody []byte 103 if request.Body != nil { 104 tr.clonedBodyBuffer.Reset() 105 tr.clonedBodyBuffer.ReadFrom(request.Body) 106 request.Body.Close() 107 cachedRequestBody = tr.clonedBodyBuffer.Bytes() 108 } 109 110 clonedRequest := cloneRequest( 111 request, tr.customHeaders, cachedRequestBody) 112 113 if tr.authenticator != nil { 114 115 // For some authentication schemes (e.g., non-connection-based), once 116 // an initial 407 has been handled, add necessary and sufficient 117 // authentication headers to every request. 118 119 err := tr.authenticator.PreAuthenticate(clonedRequest) 120 if err != nil { 121 return nil, err 122 } 123 } 124 125 response, err := tr.Transport.RoundTrip(clonedRequest) 126 if err != nil { 127 return response, proxyError(err) 128 } 129 130 if response.StatusCode == 407 { 131 132 authenticator, err := NewHttpAuthenticator( 133 response, tr.username, tr.password) 134 if err != nil { 135 response.Body.Close() 136 return nil, err 137 } 138 139 for { 140 clonedRequest = cloneRequest( 141 request, tr.customHeaders, cachedRequestBody) 142 143 err = authenticator.Authenticate(clonedRequest, response) 144 response.Body.Close() 145 if err != nil { 146 return nil, err 147 } 148 149 response, err = tr.Transport.RoundTrip(clonedRequest) 150 if err != nil { 151 return nil, proxyError(err) 152 } 153 154 if response.StatusCode != 407 { 155 156 // Save the authenticator result to use for PreAuthenticate. 157 158 tr.authenticator = authenticator 159 break 160 } 161 } 162 } 163 164 return response, nil 165 } 166 167 // Based on https://github.com/golang/oauth2/blob/master/transport.go 168 // Copyright 2014 The Go Authors. All rights reserved. 169 func cloneRequest(r *http.Request, ch http.Header, body []byte) *http.Request { 170 // shallow copy of the struct 171 r2 := new(http.Request) 172 *r2 = *r 173 // deep copy of the Header 174 r2.Header = make(http.Header) 175 for k, s := range r.Header { 176 r2.Header[k] = s 177 } 178 179 //Add custom headers to the cloned request 180 for k, s := range ch { 181 // handle special Host header case 182 if k == "Host" { 183 if len(s) > 0 { 184 // hack around special case when http proxy is used: 185 // https://golang.org/src/net/http/request.go#L474 186 // using URL.Opaque, see URL.RequestURI() https://golang.org/src/net/url/url.go#L915 187 if r2.URL.Opaque == "" { 188 r2.URL.Opaque = r2.URL.Scheme + "://" + r2.Host + r2.URL.RequestURI() 189 } 190 r2.Host = s[0] 191 } 192 } else { 193 r2.Header[k] = s 194 } 195 } 196 197 if body != nil { 198 r2.Body = ioutil.NopCloser(bytes.NewReader(body)) 199 } 200 201 // A replayed request inherits the original request's deadline (and interruptability). 202 r2 = r2.WithContext(r.Context()) 203 204 return r2 205 }