add support for HTTPS proxy URL

This commit is contained in:
Eli Bishop
2019-07-02 13:23:03 -07:00
parent 93e5be72ed
commit 88448ca6e9
2 changed files with 136 additions and 90 deletions

View File

@@ -1,11 +1,33 @@
# go-ntlm-proxy-auth # go-ntlm-proxy-auth
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![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. 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 := 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 ```golang
// create a dialer // create a dialer
@@ -21,9 +43,7 @@ ntlmDialContext := ntlm.WrapDialContext(dialer.DialContext, "proxyAddr", "user",
client := &http.Client{ client := &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
Proxy: nil, // !!! IMPORTANT, do not set proxy here !!! Proxy: nil, // !!! IMPORTANT, do not set proxy here !!!
Dial: dialer.Dial,
DialContext: ntlmDialContext, DialContext: ntlmDialContext,
// TLSClientConfig: ...
}, },
} }
``` ```

198
ntlm.go
View File

@@ -3,6 +3,7 @@ package ntlm
import ( import (
"bufio" "bufio"
"context" "context"
"crypto/tls"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
@@ -23,93 +24,118 @@ import (
// dialContext := (&net.Dialer{KeepAlive: 30*time.Second, Timeout: 30*time.Second}).DialContext // dialContext := (&net.Dialer{KeepAlive: 30*time.Second, Timeout: 30*time.Second}).DialContext
type DialContext func(ctx context.Context, network, addr string) (net.Conn, error) type DialContext func(ctx context.Context, network, addr string) (net.Conn, error)
// WrapDialContext wraps a DialContext with an NTLM Authentication to a proxy. // NewNTLMProxyDialContext provides a DialContext function that includes transparent NTLM proxy authentication.
func WrapDialContext(dialContext DialContext, proxyAddress, proxyUsername, proxyPassword, proxyDomain string) DialContext { // 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) { return func(ctx context.Context, network, addr string) (net.Conn, error) {
conn, err := dialContext(ctx, network, proxyAddress) dialProxy := func() (net.Conn, error) {
if err != nil { debugf("ntlm> Will connect to proxy at " + proxyURL.Host)
debugf("ntlm> Could not call dial context with proxy: %s", err) if proxyURL.Scheme == "https" {
return conn, err return tls.DialWithDialer(dialer, "tcp", proxyURL.Host, tlsConfig)
}
return dialer.DialContext(ctx, network, proxyURL.Host)
} }
// NTLM Step 1: Send Negotiate Message return dialAndNegotiate(addr, proxyUsername, proxyPassword, proxyDomain, dialProxy)
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
} }
} }
// 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
}