package ldntlm import ( "crypto/x509" "fmt" "io" "net/http" "net/http/httptest" "regexp" "testing" "github.com/launchdarkly/go-test-helpers/v3/httphelpers" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/launchdarkly/go-server-sdk/v6/ldhttp" ) const ( username = "username" password = "password" domain = "domain" targetURL = "http://example.com/test" targetServer = "example.com:80" targetURLPath = "/test" responseBody = "hello" // The following base64 NTLM message strings/patterns should not be considered authoritative; the exact content will // vary depending on the time, the server implementation, etc. We're just verifying that the proxy logic is sending // well-formed messages in the order that we expect, and is able to decode a well-formed server response. proxyAuthStep1Expected = "NTLM TlRMTVNTUAABAAAAAZKIoAYABgAoAAAAAAAAAC4AAAAGAbEdAAAAD0RPTUFJTg==" proxyAuthStep2Challenge = "NTLM TlRMTVNTUAACAAAADAAMADAAAAA1gomgZ38cVXpe6WwAAAAAAAAAAEYARgA8AAAAVABFAFMAVABOAFQAAgAMAFQARQBTAFQATgBUAAEADABNAEUATQBCAEUAUgADAB4AbQBlAG0AYgBlAHIALgB0AGUAcwB0AC4AYwBvAG0AAAAAAA==" proxyAuthStep3ExpectedRegex = "NTLM TlRMTVNTUAADAAAAAAAAAEAAAAB2AHYAQAAAAAwADAC2AAAAEAAQAMIAAAAUABQA0gAAAAAAAAAAAAAANYK.*AAAAAAgAMAFQARQBTAFQATgBUAAEADABNAEUATQBCAEUAUgADAB4AbQBlAG0AYgBlAHIALgB0AGUAcwB0AC4AYwBvAG0AAAAAAAAAAABUAEUAUwBUAE4AVAB1AHMAZQByAG4AYQBtAGUAZwBvAC0AbgB0AGwAbQBzAHMAcAA=" ) func TestCanConnectToNTLMProxyServer(t *testing.T) { httphelpers.WithServer(makeFakeNTLMProxyHandler(), func(server *httptest.Server) { factory, err := NewNTLMProxyHTTPClientFactory(server.URL, username, password, domain) require.NoError(t, err) client := factory() resp, err := client.Get(targetURL) require.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) body, err := io.ReadAll(resp.Body) require.NoError(t, err) assert.Equal(t, responseBody, string(body)) }) } func TestCanConnectSecurelyToNTLMProxyServerWithSelfSignedCert(t *testing.T) { handler := makeFakeNTLMProxyHandler() httphelpers.WithSelfSignedServer(handler, func(server *httptest.Server, certData []byte, certs *x509.CertPool) { factory, err := NewNTLMProxyHTTPClientFactory(server.URL, username, password, domain, ldhttp.CACertOption(certData)) require.NoError(t, err) client := factory() resp, err := client.Get(targetURL) require.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) body, err := io.ReadAll(resp.Body) require.NoError(t, err) assert.Equal(t, responseBody, string(body)) }) } func TestInvalidParameters(t *testing.T) { _, err := NewNTLMProxyHTTPClientFactory("", "user", "pass", domain) assert.Error(t, err) _, err = NewNTLMProxyHTTPClientFactory("http://proxy", "", "pass", domain) assert.Error(t, err) _, err = NewNTLMProxyHTTPClientFactory("http://proxy", "user", "", domain) assert.Error(t, err) _, err = NewNTLMProxyHTTPClientFactory("://bad", "user", "pass", domain) assert.Error(t, err) _, err = NewNTLMProxyHTTPClientFactory("http://proxy", "user", "pass", "domain", ldhttp.CACertOption([]byte("not a valid cert"))) assert.Error(t, err) } func makeFakeNTLMProxyHandler() http.Handler { step := 0 // This is an extremely minimal simulation of an NTLM proxy exchange: // 1. Client sends CONNECT request, with Proxy-Authorization header containing "negotiate" message. // Server sends 407 response, with Proxy-Authenticate header containing "challenge" message. // 2. Client sends CONNECT request, with Proxy-Authorization header containing "authorization" message. // Server sends 200 response. // 3. Client sends GET request for target URL. return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { step = step + 1 expectedMethod := "CONNECT" expectedURL := targetServer if step == 3 { expectedMethod = "GET" expectedURL = targetURLPath } if step < 3 { if req.Method != expectedMethod { fmt.Printf("Expected %s, got %s for step %d\n", expectedMethod, req.Method, step) w.WriteHeader(405) return } if req.RequestURI != expectedURL { fmt.Printf("Expected %s, got %s for step %d\n", expectedURL, req.RequestURI, step) w.WriteHeader(404) return } } proxyAuth := req.Header.Get("Proxy-Authorization") badAuth := func() { fmt.Printf("Unexpected Proxy-Authorization value: %s\n", proxyAuth) w.WriteHeader(401) } switch step { case 1: if proxyAuth == proxyAuthStep1Expected { w.Header().Set("Proxy-Authenticate", proxyAuthStep2Challenge) w.WriteHeader(407) } else { badAuth() } case 2: if matched, _ := regexp.MatchString(proxyAuthStep3ExpectedRegex, proxyAuth); matched { w.WriteHeader(200) } else { badAuth() } case 3: w.WriteHeader(200) w.Write([]byte(responseBody)) } }) }