diff --git a/README.md b/README.md index a775848..5493a63 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,33 @@ # go-ntlm-proxy-auth [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![GoDoc](https://godoc.org/github.com/Codehardt/go-ntlm-proxy-auth?status.svg)](https://godoc.org/github.com/Codehardt/go-ntlm-proxy-auth) +[![GoDoc](https://godoc.org/github.com/launchdarkly/go-ntlm-proxy-auth?status.svg)](https://godoc.org/github.com/launchdarkly/go-ntlm-proxy-auth) With this package, you can connect to http/https servers protected by an NTLM proxy in Golang. -## Example +This is a fork of https://github.com/Codehardt/go-ntlm-proxy-auth which adds support for HTTPS proxy URLs. + +## Example: NewNTLMProxyDialContext + +```golang +// create a dialer +dialer := &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, +} + +// wrap dial context with NTLM +ntlmDialContext := ntlm.NewNTLMProxyDialContext(dialer, proxyURL, "user", "password", "domain", nil) + +// create a http(s) client +client := &http.Client{ + Transport: &http.Transport{ + Proxy: nil, // !!! IMPORTANT, do not set proxy here !!! + DialContext: ntlmDialContext, + }, +} +``` +## Example: WrapDialContext (deprecated - does not support HTTPS proxy URL) ```golang // create a dialer @@ -21,9 +43,7 @@ ntlmDialContext := ntlm.WrapDialContext(dialer.DialContext, "proxyAddr", "user", client := &http.Client{ Transport: &http.Transport{ Proxy: nil, // !!! IMPORTANT, do not set proxy here !!! - Dial: dialer.Dial, DialContext: ntlmDialContext, - // TLSClientConfig: ... }, } ``` \ No newline at end of file diff --git a/ntlm.go b/ntlm.go index fc5f1d7..d9ecccc 100644 --- a/ntlm.go +++ b/ntlm.go @@ -3,6 +3,7 @@ package ntlm import ( "bufio" "context" + "crypto/tls" "encoding/base64" "errors" "fmt" @@ -23,93 +24,118 @@ import ( // dialContext := (&net.Dialer{KeepAlive: 30*time.Second, Timeout: 30*time.Second}).DialContext type DialContext func(ctx context.Context, network, addr string) (net.Conn, error) -// WrapDialContext wraps a DialContext with an NTLM Authentication to a proxy. -func WrapDialContext(dialContext DialContext, proxyAddress, proxyUsername, proxyPassword, proxyDomain string) DialContext { +// NewNTLMProxyDialContext provides a DialContext function that includes transparent NTLM proxy authentication. +// Unlike WrapDialContext, it describes the proxy location with a full URL, whose scheme can be HTTP or HTTPS. +func NewNTLMProxyDialContext(dialer *net.Dialer, proxyURL url.URL, proxyUsername, proxyPassword, proxyDomain string, tlsConfig *tls.Config) DialContext { + if dialer == nil { + dialer = &net.Dialer{} + } return func(ctx context.Context, network, addr string) (net.Conn, error) { - conn, err := dialContext(ctx, network, proxyAddress) - if err != nil { - debugf("ntlm> Could not call dial context with proxy: %s", err) - return conn, err + dialProxy := func() (net.Conn, error) { + debugf("ntlm> Will connect to proxy at " + proxyURL.Host) + if proxyURL.Scheme == "https" { + return tls.DialWithDialer(dialer, "tcp", proxyURL.Host, tlsConfig) + } + return dialer.DialContext(ctx, network, proxyURL.Host) } - // NTLM Step 1: Send Negotiate Message - negotiateMessage, err := ntlmssp.NewNegotiateMessage(proxyDomain, "") - if err != nil { - debugf("ntlm> Could not negotiate domain '%s': %s", proxyDomain, err) - return conn, err - } - debugf("ntlm> NTLM negotiate message: '%s'", base64.StdEncoding.EncodeToString(negotiateMessage)) - header := make(http.Header) - header.Set("Proxy-Authorization", fmt.Sprintf("NTLM %s", base64.StdEncoding.EncodeToString(negotiateMessage))) - header.Set("Proxy-Connection", "Keep-Alive") - connect := &http.Request{ - Method: "CONNECT", - URL: &url.URL{Opaque: addr}, - Host: addr, - Header: header, - } - if err := connect.Write(conn); err != nil { - debugf("ntlm> Could not write negotiate message to proxy: %s", err) - return conn, err - } - debugf("ntlm> Successfully sent negotiate message to proxy") - // NTLM Step 2: Receive Challenge Message - br := bufio.NewReader(conn) - resp, err := http.ReadResponse(br, connect) - if err != nil { - debugf("ntlm> Could not read response from proxy: %s", err) - return conn, err - } - _, err = ioutil.ReadAll(resp.Body) - if err != nil { - debugf("ntlm> Could not read response body from proxy: %s", err) - return conn, err - } - resp.Body.Close() - if resp.StatusCode != http.StatusProxyAuthRequired { - debugf("ntlm> Expected %d as return status, got: %d", http.StatusProxyAuthRequired, resp.StatusCode) - return conn, errors.New(http.StatusText(resp.StatusCode)) - } - challenge := strings.Split(resp.Header.Get("Proxy-Authenticate"), " ") - if len(challenge) < 2 { - debugf("ntlm> The proxy did not return an NTLM challenge, got: '%s'", resp.Header.Get("Proxy-Authenticate")) - return conn, errors.New("no NTLM challenge received") - } - debugf("ntlm> NTLM challenge: '%s'", challenge[1]) - challengeMessage, err := base64.StdEncoding.DecodeString(challenge[1]) - if err != nil { - debugf("ntlm> Could not base64 decode the NTLM challenge: %s", err) - return conn, err - } - // NTLM Step 3: Send Authorization Message - debugf("ntlm> Processing NTLM challenge with username '%s' and password with length %d", proxyUsername, len(proxyPassword)) - authenticateMessage, err := ntlmssp.ProcessChallenge(challengeMessage, proxyUsername, proxyPassword) - if err != nil { - debugf("ntlm> Could not process the NTLM challenge: %s", err) - return conn, err - } - debugf("ntlm> NTLM authorization: '%s'", base64.StdEncoding.EncodeToString(authenticateMessage)) - header.Set("Proxy-Authorization", fmt.Sprintf("NTLM %s", base64.StdEncoding.EncodeToString(authenticateMessage))) - connect = &http.Request{ - Method: "CONNECT", - URL: &url.URL{Opaque: addr}, - Host: addr, - Header: header, - } - if err := connect.Write(conn); err != nil { - debugf("ntlm> Could not write authorization to proxy: %s", err) - return conn, err - } - resp, err = http.ReadResponse(br, connect) - if err != nil { - debugf("ntlm> Could not read response from proxy: %s", err) - return conn, err - } - if resp.StatusCode != http.StatusOK { - debugf("ntlm> Expected %d as return status, got: %d", http.StatusOK, resp.StatusCode) - return conn, errors.New(http.StatusText(resp.StatusCode)) - } - // Succussfully authorized with NTLM - debugf("ntlm> Successfully injected NTLM to connection") - return conn, nil + return dialAndNegotiate(addr, proxyUsername, proxyPassword, proxyDomain, dialProxy) } } + +// WrapDialContext wraps a DialContext with an NTLM Authentication to a proxy. Note that this does not support +// using HTTPS to connect to the proxy; use NewNTLMProxyDialContext if that is required. +func WrapDialContext(dialContext DialContext, proxyAddress, proxyUsername, proxyPassword, proxyDomain string) DialContext { + return func(ctx context.Context, network, addr string) (net.Conn, error) { + return dialAndNegotiate(addr, proxyUsername, proxyPassword, proxyDomain, func() (net.Conn, error) { + return dialContext(ctx, network, proxyAddress) + }) + } +} + +func dialAndNegotiate(addr, proxyUsername, proxyPassword, proxyDomain string, baseDial func() (net.Conn, error)) (net.Conn, error) { + conn, err := baseDial() + if err != nil { + debugf("ntlm> Could not call dial context with proxy: %s", err) + return conn, err + } + // NTLM Step 1: Send Negotiate Message + negotiateMessage, err := ntlmssp.NewNegotiateMessage(proxyDomain, "") + if err != nil { + debugf("ntlm> Could not negotiate domain '%s': %s", proxyDomain, err) + return conn, err + } + debugf("ntlm> NTLM negotiate message: '%s'", base64.StdEncoding.EncodeToString(negotiateMessage)) + header := make(http.Header) + header.Set("Proxy-Authorization", fmt.Sprintf("NTLM %s", base64.StdEncoding.EncodeToString(negotiateMessage))) + header.Set("Proxy-Connection", "Keep-Alive") + connect := &http.Request{ + Method: "CONNECT", + URL: &url.URL{Opaque: addr}, + Host: addr, + Header: header, + } + if err := connect.Write(conn); err != nil { + debugf("ntlm> Could not write negotiate message to proxy: %s", err) + return conn, err + } + debugf("ntlm> Successfully sent negotiate message to proxy") + // NTLM Step 2: Receive Challenge Message + br := bufio.NewReader(conn) + resp, err := http.ReadResponse(br, connect) + if err != nil { + debugf("ntlm> Could not read response from proxy: %s", err) + return conn, err + } + _, err = ioutil.ReadAll(resp.Body) + if err != nil { + debugf("ntlm> Could not read response body from proxy: %s", err) + return conn, err + } + resp.Body.Close() + if resp.StatusCode != http.StatusProxyAuthRequired { + debugf("ntlm> Expected %d as return status, got: %d", http.StatusProxyAuthRequired, resp.StatusCode) + return conn, errors.New(http.StatusText(resp.StatusCode)) + } + challenge := strings.Split(resp.Header.Get("Proxy-Authenticate"), " ") + if len(challenge) < 2 { + debugf("ntlm> The proxy did not return an NTLM challenge, got: '%s'", resp.Header.Get("Proxy-Authenticate")) + return conn, errors.New("no NTLM challenge received") + } + debugf("ntlm> NTLM challenge: '%s'", challenge[1]) + challengeMessage, err := base64.StdEncoding.DecodeString(challenge[1]) + if err != nil { + debugf("ntlm> Could not base64 decode the NTLM challenge: %s", err) + return conn, err + } + // NTLM Step 3: Send Authorization Message + debugf("ntlm> Processing NTLM challenge with username '%s' and password with length %d", proxyUsername, len(proxyPassword)) + authenticateMessage, err := ntlmssp.ProcessChallenge(challengeMessage, proxyUsername, proxyPassword) + if err != nil { + debugf("ntlm> Could not process the NTLM challenge: %s", err) + return conn, err + } + debugf("ntlm> NTLM authorization: '%s'", base64.StdEncoding.EncodeToString(authenticateMessage)) + header.Set("Proxy-Authorization", fmt.Sprintf("NTLM %s", base64.StdEncoding.EncodeToString(authenticateMessage))) + connect = &http.Request{ + Method: "CONNECT", + URL: &url.URL{Opaque: addr}, + Host: addr, + Header: header, + } + if err := connect.Write(conn); err != nil { + debugf("ntlm> Could not write authorization to proxy: %s", err) + return conn, err + } + resp, err = http.ReadResponse(br, connect) + if err != nil { + debugf("ntlm> Could not read response from proxy: %s", err) + return conn, err + } + if resp.StatusCode != http.StatusOK { + debugf("ntlm> Expected %d as return status, got: %d", http.StatusOK, resp.StatusCode) + return conn, errors.New(http.StatusText(resp.StatusCode)) + } + // Succussfully authorized with NTLM + debugf("ntlm> Successfully injected NTLM to connection") + return conn, nil +}