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  }