Skip to content

Commit 22cdb55

Browse files
authored
Merge pull request #1 from launchdarkly/eb/allow-https-proxy
add support for HTTPS proxy URL
2 parents 93e5be7 + fdffc76 commit 22cdb55

File tree

2 files changed

+136
-90
lines changed

2 files changed

+136
-90
lines changed

README.md

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
11
# go-ntlm-proxy-auth
22

33
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4-
[![GoDoc](https://godoc.org/github.com/Codehardt/go-ntlm-proxy-auth?status.svg)](https://godoc.org/github.com/Codehardt/go-ntlm-proxy-auth)
4+
[![GoDoc](https://godoc.org/github.com/launchdarkly/go-ntlm-proxy-auth?status.svg)](https://godoc.org/github.com/launchdarkly/go-ntlm-proxy-auth)
55

66
With this package, you can connect to http/https servers protected by an NTLM proxy in Golang.
77

8-
## Example
8+
This is a fork of https://github.com/Codehardt/go-ntlm-proxy-auth which adds support for HTTPS proxy URLs.
9+
10+
## Example: NewNTLMProxyDialContext
11+
12+
```golang
13+
// create a dialer
14+
dialer := &net.Dialer{
15+
Timeout: 30 * time.Second,
16+
KeepAlive: 30 * time.Second,
17+
}
18+
19+
// wrap dial context with NTLM
20+
ntlmDialContext := ntlm.NewNTLMProxyDialContext(dialer, proxyURL, "user", "password", "domain", nil)
21+
22+
// create a http(s) client
23+
client := &http.Client{
24+
Transport: &http.Transport{
25+
Proxy: nil, // !!! IMPORTANT, do not set proxy here !!!
26+
DialContext: ntlmDialContext,
27+
},
28+
}
29+
```
30+
## Example: WrapDialContext (deprecated - does not support HTTPS proxy URL)
931

1032
```golang
1133
// create a dialer
@@ -21,9 +43,7 @@ ntlmDialContext := ntlm.WrapDialContext(dialer.DialContext, "proxyAddr", "user",
2143
client := &http.Client{
2244
Transport: &http.Transport{
2345
Proxy: nil, // !!! IMPORTANT, do not set proxy here !!!
24-
Dial: dialer.Dial,
2546
DialContext: ntlmDialContext,
26-
// TLSClientConfig: ...
2747
},
2848
}
2949
```

ntlm.go

Lines changed: 112 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ntlm
33
import (
44
"bufio"
55
"context"
6+
"crypto/tls"
67
"encoding/base64"
78
"errors"
89
"fmt"
@@ -23,93 +24,118 @@ import (
2324
// dialContext := (&net.Dialer{KeepAlive: 30*time.Second, Timeout: 30*time.Second}).DialContext
2425
type DialContext func(ctx context.Context, network, addr string) (net.Conn, error)
2526

26-
// WrapDialContext wraps a DialContext with an NTLM Authentication to a proxy.
27-
func WrapDialContext(dialContext DialContext, proxyAddress, proxyUsername, proxyPassword, proxyDomain string) DialContext {
27+
// NewNTLMProxyDialContext provides a DialContext function that includes transparent NTLM proxy authentication.
28+
// Unlike WrapDialContext, it describes the proxy location with a full URL, whose scheme can be HTTP or HTTPS.
29+
func NewNTLMProxyDialContext(dialer *net.Dialer, proxyURL url.URL, proxyUsername, proxyPassword, proxyDomain string, tlsConfig *tls.Config) DialContext {
30+
if dialer == nil {
31+
dialer = &net.Dialer{}
32+
}
2833
return func(ctx context.Context, network, addr string) (net.Conn, error) {
29-
conn, err := dialContext(ctx, network, proxyAddress)
30-
if err != nil {
31-
debugf("ntlm> Could not call dial context with proxy: %s", err)
32-
return conn, err
33-
}
34-
// NTLM Step 1: Send Negotiate Message
35-
negotiateMessage, err := ntlmssp.NewNegotiateMessage(proxyDomain, "")
36-
if err != nil {
37-
debugf("ntlm> Could not negotiate domain '%s': %s", proxyDomain, err)
38-
return conn, err
39-
}
40-
debugf("ntlm> NTLM negotiate message: '%s'", base64.StdEncoding.EncodeToString(negotiateMessage))
41-
header := make(http.Header)
42-
header.Set("Proxy-Authorization", fmt.Sprintf("NTLM %s", base64.StdEncoding.EncodeToString(negotiateMessage)))
43-
header.Set("Proxy-Connection", "Keep-Alive")
44-
connect := &http.Request{
45-
Method: "CONNECT",
46-
URL: &url.URL{Opaque: addr},
47-
Host: addr,
48-
Header: header,
49-
}
50-
if err := connect.Write(conn); err != nil {
51-
debugf("ntlm> Could not write negotiate message to proxy: %s", err)
52-
return conn, err
53-
}
54-
debugf("ntlm> Successfully sent negotiate message to proxy")
55-
// NTLM Step 2: Receive Challenge Message
56-
br := bufio.NewReader(conn)
57-
resp, err := http.ReadResponse(br, connect)
58-
if err != nil {
59-
debugf("ntlm> Could not read response from proxy: %s", err)
60-
return conn, err
61-
}
62-
_, err = ioutil.ReadAll(resp.Body)
63-
if err != nil {
64-
debugf("ntlm> Could not read response body from proxy: %s", err)
65-
return conn, err
66-
}
67-
resp.Body.Close()
68-
if resp.StatusCode != http.StatusProxyAuthRequired {
69-
debugf("ntlm> Expected %d as return status, got: %d", http.StatusProxyAuthRequired, resp.StatusCode)
70-
return conn, errors.New(http.StatusText(resp.StatusCode))
71-
}
72-
challenge := strings.Split(resp.Header.Get("Proxy-Authenticate"), " ")
73-
if len(challenge) < 2 {
74-
debugf("ntlm> The proxy did not return an NTLM challenge, got: '%s'", resp.Header.Get("Proxy-Authenticate"))
75-
return conn, errors.New("no NTLM challenge received")
34+
dialProxy := func() (net.Conn, error) {
35+
debugf("ntlm> Will connect to proxy at " + proxyURL.Host)
36+
if proxyURL.Scheme == "https" {
37+
return tls.DialWithDialer(dialer, "tcp", proxyURL.Host, tlsConfig)
38+
}
39+
return dialer.DialContext(ctx, network, proxyURL.Host)
7640
}
77-
debugf("ntlm> NTLM challenge: '%s'", challenge[1])
78-
challengeMessage, err := base64.StdEncoding.DecodeString(challenge[1])
79-
if err != nil {
80-
debugf("ntlm> Could not base64 decode the NTLM challenge: %s", err)
81-
return conn, err
82-
}
83-
// NTLM Step 3: Send Authorization Message
84-
debugf("ntlm> Processing NTLM challenge with username '%s' and password with length %d", proxyUsername, len(proxyPassword))
85-
authenticateMessage, err := ntlmssp.ProcessChallenge(challengeMessage, proxyUsername, proxyPassword)
86-
if err != nil {
87-
debugf("ntlm> Could not process the NTLM challenge: %s", err)
88-
return conn, err
89-
}
90-
debugf("ntlm> NTLM authorization: '%s'", base64.StdEncoding.EncodeToString(authenticateMessage))
91-
header.Set("Proxy-Authorization", fmt.Sprintf("NTLM %s", base64.StdEncoding.EncodeToString(authenticateMessage)))
92-
connect = &http.Request{
93-
Method: "CONNECT",
94-
URL: &url.URL{Opaque: addr},
95-
Host: addr,
96-
Header: header,
97-
}
98-
if err := connect.Write(conn); err != nil {
99-
debugf("ntlm> Could not write authorization to proxy: %s", err)
100-
return conn, err
101-
}
102-
resp, err = http.ReadResponse(br, connect)
103-
if err != nil {
104-
debugf("ntlm> Could not read response from proxy: %s", err)
105-
return conn, err
106-
}
107-
if resp.StatusCode != http.StatusOK {
108-
debugf("ntlm> Expected %d as return status, got: %d", http.StatusOK, resp.StatusCode)
109-
return conn, errors.New(http.StatusText(resp.StatusCode))
110-
}
111-
// Succussfully authorized with NTLM
112-
debugf("ntlm> Successfully injected NTLM to connection")
113-
return conn, nil
41+
return dialAndNegotiate(addr, proxyUsername, proxyPassword, proxyDomain, dialProxy)
42+
}
43+
}
44+
45+
// WrapDialContext wraps a DialContext with an NTLM Authentication to a proxy. Note that this does not support
46+
// using HTTPS to connect to the proxy; use NewNTLMProxyDialContext if that is required.
47+
func WrapDialContext(dialContext DialContext, proxyAddress, proxyUsername, proxyPassword, proxyDomain string) DialContext {
48+
return func(ctx context.Context, network, addr string) (net.Conn, error) {
49+
return dialAndNegotiate(addr, proxyUsername, proxyPassword, proxyDomain, func() (net.Conn, error) {
50+
return dialContext(ctx, network, proxyAddress)
51+
})
52+
}
53+
}
54+
55+
func dialAndNegotiate(addr, proxyUsername, proxyPassword, proxyDomain string, baseDial func() (net.Conn, error)) (net.Conn, error) {
56+
conn, err := baseDial()
57+
if err != nil {
58+
debugf("ntlm> Could not call dial context with proxy: %s", err)
59+
return conn, err
60+
}
61+
// NTLM Step 1: Send Negotiate Message
62+
negotiateMessage, err := ntlmssp.NewNegotiateMessage(proxyDomain, "")
63+
if err != nil {
64+
debugf("ntlm> Could not negotiate domain '%s': %s", proxyDomain, err)
65+
return conn, err
66+
}
67+
debugf("ntlm> NTLM negotiate message: '%s'", base64.StdEncoding.EncodeToString(negotiateMessage))
68+
header := make(http.Header)
69+
header.Set("Proxy-Authorization", fmt.Sprintf("NTLM %s", base64.StdEncoding.EncodeToString(negotiateMessage)))
70+
header.Set("Proxy-Connection", "Keep-Alive")
71+
connect := &http.Request{
72+
Method: "CONNECT",
73+
URL: &url.URL{Opaque: addr},
74+
Host: addr,
75+
Header: header,
76+
}
77+
if err := connect.Write(conn); err != nil {
78+
debugf("ntlm> Could not write negotiate message to proxy: %s", err)
79+
return conn, err
80+
}
81+
debugf("ntlm> Successfully sent negotiate message to proxy")
82+
// NTLM Step 2: Receive Challenge Message
83+
br := bufio.NewReader(conn)
84+
resp, err := http.ReadResponse(br, connect)
85+
if err != nil {
86+
debugf("ntlm> Could not read response from proxy: %s", err)
87+
return conn, err
88+
}
89+
_, err = ioutil.ReadAll(resp.Body)
90+
if err != nil {
91+
debugf("ntlm> Could not read response body from proxy: %s", err)
92+
return conn, err
93+
}
94+
resp.Body.Close()
95+
if resp.StatusCode != http.StatusProxyAuthRequired {
96+
debugf("ntlm> Expected %d as return status, got: %d", http.StatusProxyAuthRequired, resp.StatusCode)
97+
return conn, errors.New(http.StatusText(resp.StatusCode))
98+
}
99+
challenge := strings.Split(resp.Header.Get("Proxy-Authenticate"), " ")
100+
if len(challenge) < 2 {
101+
debugf("ntlm> The proxy did not return an NTLM challenge, got: '%s'", resp.Header.Get("Proxy-Authenticate"))
102+
return conn, errors.New("no NTLM challenge received")
103+
}
104+
debugf("ntlm> NTLM challenge: '%s'", challenge[1])
105+
challengeMessage, err := base64.StdEncoding.DecodeString(challenge[1])
106+
if err != nil {
107+
debugf("ntlm> Could not base64 decode the NTLM challenge: %s", err)
108+
return conn, err
109+
}
110+
// NTLM Step 3: Send Authorization Message
111+
debugf("ntlm> Processing NTLM challenge with username '%s' and password with length %d", proxyUsername, len(proxyPassword))
112+
authenticateMessage, err := ntlmssp.ProcessChallenge(challengeMessage, proxyUsername, proxyPassword)
113+
if err != nil {
114+
debugf("ntlm> Could not process the NTLM challenge: %s", err)
115+
return conn, err
116+
}
117+
debugf("ntlm> NTLM authorization: '%s'", base64.StdEncoding.EncodeToString(authenticateMessage))
118+
header.Set("Proxy-Authorization", fmt.Sprintf("NTLM %s", base64.StdEncoding.EncodeToString(authenticateMessage)))
119+
connect = &http.Request{
120+
Method: "CONNECT",
121+
URL: &url.URL{Opaque: addr},
122+
Host: addr,
123+
Header: header,
124+
}
125+
if err := connect.Write(conn); err != nil {
126+
debugf("ntlm> Could not write authorization to proxy: %s", err)
127+
return conn, err
128+
}
129+
resp, err = http.ReadResponse(br, connect)
130+
if err != nil {
131+
debugf("ntlm> Could not read response from proxy: %s", err)
132+
return conn, err
133+
}
134+
if resp.StatusCode != http.StatusOK {
135+
debugf("ntlm> Expected %d as return status, got: %d", http.StatusOK, resp.StatusCode)
136+
return conn, errors.New(http.StatusText(resp.StatusCode))
114137
}
138+
// Succussfully authorized with NTLM
139+
debugf("ntlm> Successfully injected NTLM to connection")
140+
return conn, nil
115141
}

0 commit comments

Comments
 (0)