From d0f0a2f4c97c8cca17891672b09a187b0ca72f84 Mon Sep 17 00:00:00 2001 From: durck Date: Thu, 9 Jul 2026 14:02:05 +0300 Subject: [PATCH] fix(client): handle Negotiate proxy auth --- internal/client/client.go | 138 ++++++++++++++--------------- internal/client/proxy_ntlm.go | 18 +++- internal/client/proxy_ntlm_test.go | 58 ++++++++++++ 3 files changed, 142 insertions(+), 72 deletions(-) diff --git a/internal/client/client.go b/internal/client/client.go index 641404b..24ad93f 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -154,92 +154,90 @@ func Connect(addr, proxy string, timeout time.Duration, hostKerberos bool, stati // If we get a 407 Proxy Authentication Required if bytes.Contains(bytes.ToLower(responseStatus), []byte("407")) { - // Check if NTLM is supported - if bytes.Contains(bytes.ToLower(responseStatus), []byte(AskingForNTLMProxy)) { - //if we have specific credentials supplied by our user, attempt to use those for our NTLM negotiation with the proxy + schemes := ClassifyProxyAuth(responseStatus) + log.Printf("Proxy returned 407; advertised schemes: NTLM=%v Negotiate=%v", schemes.NTLM, schemes.Negotiate) - if staticNTLMCreds != nil { - - // Start NTLM negotiation - ntlmHeader, err := getNTLMAuthHeader(staticNTLMCreds, nil) - if err != nil { - return nil, fmt.Errorf("NTLM negotiation failed: %v", err) - } - - // Send Type 1 message - req = []string{ - fmt.Sprintf("CONNECT %s HTTP/1.1", addr), - fmt.Sprintf("Host: %s", addr), - fmt.Sprintf("Proxy-Authorization: %s", ntlmHeader), - } + switch { + case schemes.NTLM && staticNTLMCreds != nil: + // Start NTLM negotiation + ntlmHeader, err := getNTLMAuthHeader(staticNTLMCreds, nil) + if err != nil { + return nil, fmt.Errorf("NTLM negotiation failed: %v", err) + } - err = WriteHTTPReq(req, proxyCon) - if err != nil { - return nil, fmt.Errorf("unable to send NTLM negotiate message: %s", err) - } + // Send Type 1 message + req = []string{ + fmt.Sprintf("CONNECT %s HTTP/1.1", addr), + fmt.Sprintf("Host: %s", addr), + fmt.Sprintf("Proxy-Authorization: %s", ntlmHeader), + } - // Read challenge response + err = WriteHTTPReq(req, proxyCon) + if err != nil { + return nil, fmt.Errorf("unable to send NTLM negotiate message: %s", err) + } - challengeResponseOk := scanner.Scan() - if !challengeResponseOk { - return conn, fmt.Errorf("reading NTLM challenge failed") - } + // Read challenge response - challengeResponse := scanner.Text() + challengeResponseOk := scanner.Scan() + if !challengeResponseOk { + return conn, fmt.Errorf("reading NTLM challenge failed") + } - // Extract Type 2 message - ntlmParts := strings.SplitN(challengeResponse, NTLM, 2) - if len(ntlmParts) != 2 { - return nil, fmt.Errorf("no NTLM challenge received") - } + challengeResponse := scanner.Text() - challengeStr := strings.SplitN(ntlmParts[1], "\r\n", 2)[0] - challenge, err := base64.StdEncoding.DecodeString(challengeStr) - if err != nil { - return nil, fmt.Errorf("invalid NTLM challenge: %v", err) - } + // Extract Type 2 message + ntlmParts := strings.SplitN(challengeResponse, NTLM, 2) + if len(ntlmParts) != 2 { + return nil, fmt.Errorf("no NTLM challenge received") + } - // Generate Type 3 message - ntlmHeader, err = getNTLMAuthHeader(staticNTLMCreds, challenge) - if err != nil { - return nil, fmt.Errorf("NTLM authentication failed: %v", err) - } + challengeStr := strings.SplitN(ntlmParts[1], "\r\n", 2)[0] + challenge, err := base64.StdEncoding.DecodeString(challengeStr) + if err != nil { + return nil, fmt.Errorf("invalid NTLM challenge: %v", err) + } - // Send Type 3 message - req = []string{ - fmt.Sprintf("CONNECT %s HTTP/1.1", addr), - fmt.Sprintf("Host: %s", addr), - fmt.Sprintf("Proxy-Authorization: %s", ntlmHeader), - } + // Generate Type 3 message + ntlmHeader, err = getNTLMAuthHeader(staticNTLMCreds, challenge) + if err != nil { + return nil, fmt.Errorf("NTLM authentication failed: %v", err) + } - err = WriteHTTPReq(req, proxyCon) - if err != nil { - return nil, fmt.Errorf("unable to send NTLM authenticate message: %v", err) - } + // Send Type 3 message + req = []string{ + fmt.Sprintf("CONNECT %s HTTP/1.1", addr), + fmt.Sprintf("Host: %s", addr), + fmt.Sprintf("Proxy-Authorization: %s", ntlmHeader), + } - // Read final response - finalResponseOk := scanner.Scan() - if !finalResponseOk { - return conn, fmt.Errorf("failed to read final NTLM response") - } + err = WriteHTTPReq(req, proxyCon) + if err != nil { + return nil, fmt.Errorf("unable to send NTLM authenticate message: %v", err) + } - responseStatus = scanner.Bytes() + // Read final response + finalResponseOk := scanner.Scan() + if !finalResponseOk { + return conn, fmt.Errorf("failed to read final NTLM response") + } - } else if hostKerberos { - // otherwise, (if the user has allowed us to) use the host/user kerberos to auth with the proxy - req = addHostKerberosHeaders(proxy, req) - err = WriteHTTPReq(req, proxyCon) - if err != nil { - return nil, fmt.Errorf("unable to connect proxy %s", proxy) - } + responseStatus = scanner.Bytes() - proxyResponseOK := scanner.Scan() - if !proxyResponseOK { - return conn, fmt.Errorf("failed reading proxy after offering it host kerberos") - } + case (schemes.Negotiate || schemes.NTLM) && hostKerberos: + // otherwise, (if the user has allowed us to) use the host/user kerberos to auth with the proxy + req = addHostKerberosHeaders(proxy, req) + err = WriteHTTPReq(req, proxyCon) + if err != nil { + return nil, fmt.Errorf("unable to connect proxy %s", proxy) + } - responseStatus = scanner.Bytes() + proxyResponseOK := scanner.Scan() + if !proxyResponseOK { + return conn, fmt.Errorf("failed reading proxy after offering it host kerberos") } + + responseStatus = scanner.Bytes() } } diff --git a/internal/client/proxy_ntlm.go b/internal/client/proxy_ntlm.go index 2463776..f334217 100644 --- a/internal/client/proxy_ntlm.go +++ b/internal/client/proxy_ntlm.go @@ -9,10 +9,24 @@ import ( ) const ( - NTLM = "NTLM " - AskingForNTLMProxy = "proxy-authenticate: ntlm" + NTLM = "NTLM " + AskingForNTLMProxy = "proxy-authenticate: ntlm" + AskingForNegotiateProxy = "proxy-authenticate: negotiate" ) +type ProxyAuthSchemes struct { + NTLM bool + Negotiate bool +} + +func ClassifyProxyAuth(response []byte) ProxyAuthSchemes { + lower := strings.ToLower(string(response)) + return ProxyAuthSchemes{ + NTLM: strings.Contains(lower, AskingForNTLMProxy), + Negotiate: strings.Contains(lower, AskingForNegotiateProxy), + } +} + func parseNTLMCreds(creds string) (domain, user, pass string, err error) { if creds == "" { return "", "", "", fmt.Errorf("NTLM credentials not provided. Use --ntlm-proxy-creds in format DOMAIN\\USER:PASS") diff --git a/internal/client/proxy_ntlm_test.go b/internal/client/proxy_ntlm_test.go index db91807..9f077ff 100644 --- a/internal/client/proxy_ntlm_test.go +++ b/internal/client/proxy_ntlm_test.go @@ -74,3 +74,61 @@ func TestParseNTLMCreds(t *testing.T) { }) } } + +func TestClassifyProxyAuth(t *testing.T) { + tests := []struct { + name string + response string + wantNTLM bool + wantNegotiate bool + }{ + { + name: "Negotiate only", + response: "HTTP/1.1 407 Proxy Authentication Required\r\n" + + "Proxy-Authenticate: Negotiate\r\n", + wantNegotiate: true, + }, + { + name: "NTLM only", + response: "HTTP/1.1 407 Proxy Authentication Required\r\n" + + "Proxy-Authenticate: NTLM\r\n", + wantNTLM: true, + }, + { + name: "Negotiate and NTLM", + response: "HTTP/1.1 407 Proxy Authentication Required\r\n" + + "Proxy-Authenticate: Negotiate\r\n" + + "Proxy-Authenticate: NTLM\r\n" + + "Proxy-Authenticate: Basic realm=\"corp\"\r\n", + wantNTLM: true, + wantNegotiate: true, + }, + { + name: "lowercase header", + response: "HTTP/1.1 407 Proxy Authentication Required\r\n" + + "proxy-authenticate: Negotiate\r\n", + wantNegotiate: true, + }, + { + name: "Basic only", + response: "HTTP/1.1 407 Proxy Authentication Required\r\n" + + "Proxy-Authenticate: Basic realm=\"corp\"\r\n", + }, + { + name: "empty response", + response: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ClassifyProxyAuth([]byte(tt.response)) + if got.NTLM != tt.wantNTLM { + t.Errorf("NTLM = %v, want %v", got.NTLM, tt.wantNTLM) + } + if got.Negotiate != tt.wantNegotiate { + t.Errorf("Negotiate = %v, want %v", got.Negotiate, tt.wantNegotiate) + } + }) + } +}