Просмотр исходного кода

Stateful HTTP authentication, http.Transport with stateful auth pending

Eugene Fryntov 10 лет назад
Родитель
Сommit
ea7baf63a5

+ 25 - 4
psiphon/upstreamproxy/auth_basic.go

@@ -2,11 +2,32 @@ package upstreamproxy
 
 import (
 	"encoding/base64"
+	"errors"
 	"net/http"
 )
 
-func basicAuthenticate(req *http.Request, challenge, username, password string) error {
-	auth := username + ":" + password
-	req.Header.Set("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
-	return nil
+type BasicHttpAuthState int
+
+const (
+	BASIC_HTTP_AUTH_STATE_CHALLENGE_RECEIVED BasicHttpAuthState = iota
+	BASIC_HTTP_AUTH_STATE_RESPONSE_GENERATED
+)
+
+type BasicHttpAuthenticator struct {
+	state BasicHttpAuthState
+}
+
+func newBasicAuthenticator() *BasicHttpAuthenticator {
+	return &BasicHttpAuthenticator{state: BASIC_HTTP_AUTH_STATE_CHALLENGE_RECEIVED}
+}
+
+func (a *BasicHttpAuthenticator) authenticate(req *http.Request, resp *http.Response, username, password string) error {
+	if a.state == BASIC_HTTP_AUTH_STATE_CHALLENGE_RECEIVED {
+		auth := username + ":" + password
+		req.Header.Set("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
+		a.state = BASIC_HTTP_AUTH_STATE_RESPONSE_GENERATED
+		return nil
+	} else {
+		return errors.New("upstreamproxy: Authorization is not accepted by the proxy server")
+	}
 }

+ 27 - 3
psiphon/upstreamproxy/auth_digest.go

@@ -10,6 +10,21 @@ import (
 	"strings"
 )
 
+type DigestHttpAuthState int
+
+const (
+	DIGEST_HTTP_AUTH_STATE_CHALLENGE_RECEIVED DigestHttpAuthState = iota
+	DIGEST_HTTP_AUTH_STATE_RESPONSE_GENERATED
+)
+
+type DigestHttpAuthenticator struct {
+	state DigestHttpAuthState
+}
+
+func newDigestAuthenticator() *DigestHttpAuthenticator {
+	return &DigestHttpAuthenticator{state: DIGEST_HTTP_AUTH_STATE_CHALLENGE_RECEIVED}
+}
+
 /* Adapted from https://github.com/ryanjdew/http-digest-auth-client */
 
 type DigestHeaders struct {
@@ -91,9 +106,17 @@ func h(data string) string {
 	return fmt.Sprintf("%x", digest.Sum(nil))
 }
 
-func digestAuthenticate(req *http.Request, challenge, username, password string) error {
+func (a *DigestHttpAuthenticator) authenticate(req *http.Request, resp *http.Response, username, password string) error {
+	if a.state != DIGEST_HTTP_AUTH_STATE_CHALLENGE_RECEIVED {
+		return errors.New("upstreamproxy: Authorization is not accepted by the proxy server")
+	}
+	challenges, err := parseAuthChallenge(resp)
+	if err != nil {
+		return err
+	}
+	challenge := challenges["Digest"]
 	if len(challenge) == 0 {
-		return errors.New("Digest authentication challenge is empty")
+		return errors.New("upstreamproxy: Digest authentication challenge is empty")
 	}
 	//parse challenge
 	digestParams := map[string]string{}
@@ -105,7 +128,7 @@ func digestAuthenticate(req *http.Request, challenge, username, password string)
 		digestParams[strings.Trim(param[0], "\" ")] = strings.Trim(param[1], "\" ")
 	}
 	if len(digestParams) == 0 {
-		return errors.New("Digest authentication challenge is malformed")
+		return errors.New("upstreamproxy: Digest authentication challenge is malformed")
 	}
 
 	algorithm := digestParams["algorithm"]
@@ -129,5 +152,6 @@ func digestAuthenticate(req *http.Request, challenge, username, password string)
 	d.Username = username
 	d.Password = password
 	d.ApplyAuth(req)
+	a.state = DIGEST_HTTP_AUTH_STATE_RESPONSE_GENERATED
 	return nil
 }

+ 34 - 8
psiphon/upstreamproxy/auth_ntlm.go

@@ -8,23 +8,47 @@ import (
 	"strings"
 )
 
-func ntlmAuthenticate(req *http.Request, challenge, username, password string) error {
-	err := errors.New("NTLM authentication unknown error")
+type NTLMHttpAuthState int
+
+const (
+	NTLM_HTTP_AUTH_STATE_CHALLENGE_RECEIVED NTLMHttpAuthState = iota
+	NTLM_HTTP_AUTH_STATE_RESPONSE_TYPE1_GENERATED
+	NTLM_HTTP_AUTH_STATE_RESPONSE_TYPE3_GENERATED
+)
+
+type NTLMHttpAuthenticator struct {
+	state NTLMHttpAuthState
+}
+
+func newNTLMAuthenticator() *NTLMHttpAuthenticator {
+	return &NTLMHttpAuthenticator{state: NTLM_HTTP_AUTH_STATE_CHALLENGE_RECEIVED}
+}
+
+func (a *NTLMHttpAuthenticator) authenticate(req *http.Request, resp *http.Response, username, password string) error {
+	challenges, err := parseAuthChallenge(resp)
+
+	challenge, ok := challenges["NTLM"]
+	if !ok {
+		return errors.New("upstreamproxy: Bad proxy response, no NTLM challenge for NTLMHttpAuthenticator")
+	}
+
 	var ntlmMsg []byte
 
 	session, err := ntlm.CreateClientSession(ntlm.Version2, ntlm.ConnectionOrientedMode)
 	if err != nil {
 		return err
 	}
-	if challenge == "" {
+	if a.state == NTLM_HTTP_AUTH_STATE_CHALLENGE_RECEIVED {
 		//generate TYPE 1 message
 		negotiate, err := session.GenerateNegotiateMessage()
 		if err != nil {
 			return err
 		}
 		ntlmMsg = negotiate.Bytes()
-		err = nil
-	} else {
+		a.state = NTLM_HTTP_AUTH_STATE_RESPONSE_TYPE1_GENERATED
+		req.Header.Set("Proxy-Authorization", "NTLM "+base64.StdEncoding.EncodeToString(ntlmMsg))
+		return nil
+	} else if a.state == NTLM_HTTP_AUTH_STATE_RESPONSE_TYPE1_GENERATED {
 		// Parse username for domain in form DOMAIN\username
 		var NTDomain, NTUser string
 		parts := strings.SplitN(username, "\\", 2)
@@ -50,8 +74,10 @@ func ntlmAuthenticate(req *http.Request, challenge, username, password string) e
 			return err
 		}
 		ntlmMsg = authenticate.Bytes()
-		err = nil
+		a.state = NTLM_HTTP_AUTH_STATE_RESPONSE_TYPE3_GENERATED
+		req.Header.Set("Proxy-Authorization", "NTLM "+base64.StdEncoding.EncodeToString(ntlmMsg))
+		return nil
 	}
-	req.Header.Set("Proxy-Authorization", "NTLM "+base64.StdEncoding.EncodeToString(ntlmMsg))
-	return err
+
+	return errors.New("upstreamproxy: Authorization is not accepted by the proxy server")
 }

+ 1 - 6
psiphon/upstreamproxy/go-ntlm/ntlm/ntlmv2.go

@@ -299,12 +299,7 @@ func (n *V2ClientSession) GenerateNegotiateMessage() (nm *NegotiateMessage, err
 		NEGOTIATE_FLAG_REQUEST_56BIT_ENCRYPTION |
 		NEGOTIATE_FLAG_REQUEST_UNICODE_ENCODING
 
-	nm.Version = &VersionStruct{ProductMajorVersion: 0x05,
-		ProductMinorVersion: 0x01,
-		ProductBuild:        0x280a,
-		Reserved:            []byte{0x00, 0x00, 0x00},
-		NTLMRevisionCurrent: 0x0f,
-	}
+	nm.Version = &VersionStruct{ProductMajorVersion: uint8(5), ProductMinorVersion: uint8(1), ProductBuild: uint16(2600), NTLMRevisionCurrent: uint8(15)}
 	return nm, nil
 }
 

+ 63 - 0
psiphon/upstreamproxy/http_authenticator.go

@@ -0,0 +1,63 @@
+package upstreamproxy
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+)
+
+type HttpAuthState int
+
+const (
+	HTTP_AUTH_STATE_UNCHALLENGED HttpAuthState = iota
+	HTTP_AUTH_STATE_CHALLENGED
+	HTTP_AUTH_STATE_FAILURE
+	HTTP_AUTH_STATE_SUCCESS
+)
+
+type HttpAuthenticator interface {
+	authenticate(req *http.Request, resp *http.Response, username, pasword string) error
+}
+
+func parseAuthChallenge(resp *http.Response) (map[string]string, error) {
+	challenges := make(map[string]string)
+	headers := resp.Header[http.CanonicalHeaderKey("proxy-authenticate")]
+
+	for _, val := range headers {
+		s := strings.SplitN(val, " ", 2)
+		if len(s) == 2 {
+			challenges[s[0]] = s[1]
+		}
+		if len(s) == 1 && s[0] != "" {
+			challenges[s[0]] = ""
+		}
+	}
+	if len(challenges) == 0 {
+		return nil, fmt.Errorf("upstreamproxy: No valid challenges in the Proxy-Authenticate header")
+	}
+	return challenges, nil
+}
+
+func newHttpAuthenticator(resp *http.Response) (HttpAuthenticator, error) {
+
+	challenges, err := parseAuthChallenge(resp)
+	if err != nil {
+		return nil, err
+	}
+
+	// NTLM > Digest > Basic
+	if _, ok := challenges["NTLM"]; ok {
+		return newNTLMAuthenticator(), nil
+	} else if _, ok := challenges["Digest"]; ok {
+		return newDigestAuthenticator(), nil
+	} else if _, ok := challenges["Basic"]; ok {
+		return newBasicAuthenticator(), nil
+	}
+
+	//Unsupported scheme
+	schemes := make([]string, 0, len(challenges))
+	for scheme := range challenges {
+		schemes = append(schemes, scheme)
+	}
+	return nil, fmt.Errorf("Unsupported proxy authentication scheme in %v", schemes)
+}

+ 15 - 48
psiphon/upstreamproxy/proxy_http.go

@@ -55,52 +55,9 @@ import (
 	"net/http"
 	"net/http/httputil"
 	"net/url"
-	"strings"
 	"time"
 )
 
-type HttpAuthState int
-
-const (
-	HTTP_AUTH_STATE_UNCHALLENGED HttpAuthState = iota
-	HTTP_AUTH_STATE_CHALLENGED
-	HTTP_AUTH_STATE_FAILURE
-	HTTP_AUTH_STATE_SUCCESS
-)
-
-func authenticateRequest(req *http.Request, resp *http.Response, username, pasword string) error {
-	challenges := make(map[string]string)
-	headers := resp.Header[http.CanonicalHeaderKey("proxy-authenticate")]
-
-	for _, val := range headers {
-		s := strings.SplitN(val, " ", 2)
-		if len(s) == 2 {
-			challenges[s[0]] = s[1]
-		}
-		if len(s) == 1 && s[0] != "" {
-			challenges[s[0]] = ""
-		}
-	}
-	if len(challenges) == 0 {
-		return fmt.Errorf("No valid challenges in the Proxy-Authenticate header")
-	}
-	// NTLM > Digest > Basic
-	if challenge, ok := challenges["NTLM"]; ok {
-		return ntlmAuthenticate(req, challenge, username, pasword)
-	} else if challenge, ok := challenges["Digest"]; ok {
-		return digestAuthenticate(req, challenge, username, pasword)
-	} else if challenge, ok := challenges["Basic"]; ok {
-		return basicAuthenticate(req, challenge, username, pasword)
-	}
-
-	//Unsupported scheme
-	schemes := make([]string, 0, len(challenges))
-	for scheme := range challenges {
-		schemes = append(schemes, scheme)
-	}
-	return fmt.Errorf("Unsupported proxy authentication scheme in %v", schemes)
-}
-
 // httpProxy is a HTTP connect proxy.
 type httpProxy struct {
 	hostPort string
@@ -127,10 +84,9 @@ func (hp *httpProxy) Dial(network, addr string) (net.Conn, error) {
 	pc := &proxyConn{authState: HTTP_AUTH_STATE_UNCHALLENGED}
 	err := pc.makeNewClientConn(hp.forward, hp.hostPort)
 	if err != nil {
-		return nil, fmt.Errorf("makeNewClientConn error: %v", err)
+		return nil, fmt.Errorf("upstreamproxy: makeNewClientConn error: %v", err)
 	}
 
-	//TODO: count handshake attempts
 	for {
 		err := pc.handshake(addr, hp.username, hp.password)
 		switch pc.authState {
@@ -146,7 +102,7 @@ func (hp *httpProxy) Dial(network, addr string) (net.Conn, error) {
 			if err == httputil.ErrPersistEOF {
 				err = pc.makeNewClientConn(hp.forward, hp.hostPort)
 				if err != nil {
-					return nil, fmt.Errorf("makeNewClientConn error: %v", err)
+					return nil, fmt.Errorf("upstreamproxy: makeNewClientConn error: %v", err)
 				}
 			}
 			continue
@@ -164,6 +120,7 @@ type proxyConn struct {
 	staleReader    *bufio.Reader
 	authResponse   *http.Response
 	authState      HttpAuthState
+	authenticator  HttpAuthenticator
 }
 
 func (pc *proxyConn) handshake(addr, username, password string) error {
@@ -186,7 +143,7 @@ func (pc *proxyConn) handshake(addr, username, password string) error {
 	req.Header.Set("User-Agent", "")
 
 	if pc.authState == HTTP_AUTH_STATE_CHALLENGED {
-		err := authenticateRequest(req, pc.authResponse, username, password)
+		err := pc.authenticator.authenticate(req, pc.authResponse, username, password)
 		if err != nil {
 			pc.authState = HTTP_AUTH_STATE_FAILURE
 			return err
@@ -207,12 +164,22 @@ func (pc *proxyConn) handshake(addr, username, password string) error {
 	}
 
 	if resp.StatusCode == 407 {
+		if pc.authState == HTTP_AUTH_STATE_UNCHALLENGED {
+			var auth_err error = nil
+			pc.authenticator, auth_err = newHttpAuthenticator(resp)
+			if auth_err != nil {
+				pc.httpClientConn.Close()
+				pc.authState = HTTP_AUTH_STATE_FAILURE
+				return auth_err
+			}
+		}
+
 		pc.authState = HTTP_AUTH_STATE_CHALLENGED
 		pc.authResponse = resp
 		if username == "" {
 			pc.httpClientConn.Close()
 			pc.authState = HTTP_AUTH_STATE_FAILURE
-			return errors.New("No credentials provided for proxy auth")
+			return errors.New("upstreamproxy: No credentials provided for proxy auth")
 		}
 		return err
 	}

+ 3 - 1
psiphon/upstreamproxy/transport_proxy_auth.go

@@ -19,7 +19,8 @@ func NewTransport(username, password string, dialFn DialFunc) *Transport {
 	return t
 }
 
-func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
+/*
+   func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
 
 	// TODO: Check if we cached auth header for the transport ProxyURL
 	resp, err := t.transport.RoundTrip(req)
@@ -35,6 +36,7 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
 	}
 	return resp, err
 }
+*/
 
 func cloneRequest(r *http.Request) *http.Request {
 	// shallow copy of the struct