diff options
author | wtc@chromium.org <wtc@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-03-11 22:21:29 +0000 |
---|---|---|
committer | wtc@chromium.org <wtc@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-03-11 22:21:29 +0000 |
commit | 385a467616a675d979514c64bda2fa5ddddb70c0 (patch) | |
tree | a50ec11679303302d305c5dffb700847ce6c93da /net | |
parent | 5203a4f3fc6bd8bcb2d521db09bc2625bf48c2cc (diff) | |
download | chromium_src-385a467616a675d979514c64bda2fa5ddddb70c0.zip chromium_src-385a467616a675d979514c64bda2fa5ddddb70c0.tar.gz chromium_src-385a467616a675d979514c64bda2fa5ddddb70c0.tar.bz2 |
Add unit tests for NTLM authentication. This requires
overriding the functions that generate random bytes or get
the local host name, so that the generated NTLM messages are
reproducible.
R=eroman
BUG=6567,6824
Review URL: http://codereview.chromium.org/42052
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@11488 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'net')
-rw-r--r-- | net/http/http_auth_handler_ntlm.cc | 39 | ||||
-rw-r--r-- | net/http/http_auth_handler_ntlm.h | 14 | ||||
-rw-r--r-- | net/http/http_network_transaction_unittest.cc | 262 |
3 files changed, 295 insertions, 20 deletions
diff --git a/net/http/http_auth_handler_ntlm.cc b/net/http/http_auth_handler_ntlm.cc index 5dbec24..533b484 100644 --- a/net/http/http_auth_handler_ntlm.cc +++ b/net/http/http_auth_handler_ntlm.cc @@ -434,6 +434,25 @@ static int ParseType2Msg(const void* in_buf, uint32 in_len, Type2Msg* msg) { return OK; } +static void GenerateRandom(uint8* output, size_t n) { + for (size_t i = 0; i < n; ++i) + output[i] = base::RandInt(0, 255); +} + +static void GetHostName(char* name, size_t namelen) { + if (gethostname(name, namelen) != 0) + name[0] = '\0'; +} + +// TODO(wtc): these two function pointers should become static members of +// HttpAuthHandlerNTLM. They are file-scope static variables now so that +// GenerateType3Msg can use them without being a friend function. We should +// have HttpAuthHandlerNTLM absorb NTLMAuthModule and pass the host name and +// random bytes as input arguments to GenerateType3Msg. +static HttpAuthHandlerNTLM::GenerateRandomProc generate_random_proc_ = + GenerateRandom; +static HttpAuthHandlerNTLM::HostNameProc get_host_name_proc_ = GetHostName; + // Returns OK or a network error code. static int GenerateType3Msg(const string16& domain, const string16& username, @@ -511,9 +530,10 @@ static int GenerateType3Msg(const string16& domain, // Get workstation name (use local machine's hostname). // char host_buf[256]; // Host names are limited to 255 bytes. - if (gethostname(host_buf, sizeof(host_buf)) != 0) - return ERR_UNEXPECTED; + get_host_name_proc_(host_buf, sizeof(host_buf)); host_len = strlen(host_buf); + if (host_len == 0) + return ERR_UNEXPECTED; if (unicode) { // hostname is ASCII, so we can do a simple zero-pad expansion: ucs_host_buf.assign(host_buf, host_buf + host_len); @@ -547,10 +567,7 @@ static int GenerateType3Msg(const string16& domain, MD5Digest session_hash; uint8 temp[16]; - // TODO(wtc): Add a function that generates random bytes so we can say: - // GenerateRandom(lm_resp, 8); - for (int i = 0; i < 8; ++i) - lm_resp[i] = base::RandInt(0, 255); + generate_random_proc_(lm_resp, 8); memset(lm_resp + 8, 0, LM_RESP_LEN - 8); memcpy(temp, msg.challenge, 8); @@ -757,6 +774,16 @@ std::string HttpAuthHandlerNTLM::GenerateCredentials( return std::string("NTLM ") + encode_output; } +// static +void HttpAuthHandlerNTLM::SetGenerateRandomProc(GenerateRandomProc proc) { + generate_random_proc_ = proc; +} + +// static +void HttpAuthHandlerNTLM::SetHostNameProc(HostNameProc proc) { + get_host_name_proc_ = proc; +} + // The NTLM challenge header looks like: // WWW-Authenticate: NTLM auth-data bool HttpAuthHandlerNTLM::ParseChallenge( diff --git a/net/http/http_auth_handler_ntlm.h b/net/http/http_auth_handler_ntlm.h index 05ce06b..00c246a 100644 --- a/net/http/http_auth_handler_ntlm.h +++ b/net/http/http_auth_handler_ntlm.h @@ -7,6 +7,7 @@ #include <string> +#include "base/basictypes.h" #include "base/scoped_ptr.h" #include "net/http/http_auth_handler.h" @@ -17,6 +18,15 @@ class NTLMAuthModule; // Code for handling HTTP NTLM authentication. class HttpAuthHandlerNTLM : public HttpAuthHandler { public: + // A function that generates n random bytes in the output buffer. + typedef void (*GenerateRandomProc)(uint8* output, size_t n); + + // A function that returns the local host name as a null-terminated string + // in the output buffer. Returns an empty string if the local host name is + // not available. + // TODO(wtc): return a std::string instead. + typedef void (*HostNameProc)(char* name, size_t namelen); + HttpAuthHandlerNTLM(); virtual ~HttpAuthHandlerNTLM(); @@ -28,6 +38,10 @@ class HttpAuthHandlerNTLM : public HttpAuthHandler { const HttpRequestInfo* request, const ProxyInfo* proxy); + // For unit tests to override the GenerateRandom and GetHostName functions. + static void SetGenerateRandomProc(GenerateRandomProc proc); + static void SetHostNameProc(HostNameProc proc); + protected: virtual bool Init(std::string::const_iterator challenge_begin, std::string::const_iterator challenge_end) { diff --git a/net/http/http_network_transaction_unittest.cc b/net/http/http_network_transaction_unittest.cc index 08eab15..337f6c7 100644 --- a/net/http/http_network_transaction_unittest.cc +++ b/net/http/http_network_transaction_unittest.cc @@ -8,6 +8,7 @@ #include "net/base/client_socket_factory.h" #include "net/base/test_completion_callback.h" #include "net/base/upload_data.h" +#include "net/http/http_auth_handler_ntlm.h" #include "net/http/http_network_session.h" #include "net/http/http_network_transaction.h" #include "net/http/http_transaction_unittest.h" @@ -295,6 +296,40 @@ void FillLargeHeadersString(std::string* str, int size) { str->append(row, sizeof_row); } +// Alternative functions that eliminate randomness and dependency on the local +// host name so that the generated NTLM messages are reproducible. +void MyGenerateRandom1(uint8* output, size_t n) { + static const uint8 bytes[] = { + 0x55, 0x29, 0x66, 0x26, 0x6b, 0x9c, 0x73, 0x54 + }; + static size_t current_byte = 0; + for (size_t i = 0; i < n; ++i) { + output[i] = bytes[current_byte++]; + current_byte %= arraysize(bytes); + } +} + +void MyGenerateRandom2(uint8* output, size_t n) { + static const uint8 bytes[] = { + 0x96, 0x79, 0x85, 0xe7, 0x49, 0x93, 0x70, 0xa1, + 0x4e, 0xe7, 0x87, 0x45, 0x31, 0x5b, 0xd3, 0x1f + }; + static size_t current_byte = 0; + for (size_t i = 0; i < n; ++i) { + output[i] = bytes[current_byte++]; + current_byte %= arraysize(bytes); + } +} + +void MyGetHostName(char* name, size_t namelen) { + static const char hostname[] = "WTC-WIN7"; + if (namelen >= arraysize(hostname)) { + memcpy(name, hostname, arraysize(hostname)); + } else { + name[0] = '\0'; + } +} + //----------------------------------------------------------------------------- TEST_F(HttpNetworkTransactionTest, Basic) { @@ -1535,10 +1570,15 @@ TEST_F(HttpNetworkTransactionTest, BasicAuthProxyThenServer) { EXPECT_EQ(100, response->headers->GetContentLength()); } -// Test NTLM authentication. -// TODO(wtc): This test doesn't work because we need to control the 8 random -// bytes and the "workstation name" for a deterministic expected result. -TEST_F(HttpNetworkTransactionTest, DISABLED_NTLMAuth) { +// The NTLM authentication unit tests were generated by capturing the HTTP +// requests and responses using Fiddler 2 and inspecting the generated random +// bytes in the debugger. + +// Enter the correct password and authenticate successfully. +TEST_F(HttpNetworkTransactionTest, NTLMAuth1) { + net::HttpAuthHandlerNTLM::SetGenerateRandomProc(MyGenerateRandom1); + net::HttpAuthHandlerNTLM::SetHostNameProc(MyGetHostName); + scoped_ptr<net::ProxyService> proxy_service(CreateNullProxyService()); scoped_ptr<net::HttpTransaction> trans(new net::HttpNetworkTransaction( CreateSession(proxy_service.get()), &mock_socket_factory)); @@ -1583,18 +1623,18 @@ TEST_F(HttpNetworkTransactionTest, DISABLED_NTLMAuth) { MockWrite("GET /kids/login.aspx HTTP/1.1\r\n" "Host: 172.22.68.17\r\n" "Connection: keep-alive\r\n" - "Authorization: NTLM TlRMTVNTUAADAAAAGAAYAHAAAAAYABgAiA" - "AAAAAAAABAAAAAGAAYAEAAAAAYABgAWAAAAAAAAAAAAAAABYIIAHQA" - "ZQBzAHQAaQBuAGcALQBuAHQAbABtAHcAdABjAGgAYQBuAGcALQBjAG" - "8AcgBwAMertjYHfqUhAAAAAAAAAAAAAAAAAAAAAEP3kddZKtMDMssm" - "KYA6SCllVGUeyoQppQ==\r\n\r\n"), + "Authorization: NTLM TlRMTVNTUAADAAAAGAAYAGgAAAAYABgAgA" + "AAAAAAAABAAAAAGAAYAEAAAAAQABAAWAAAAAAAAAAAAAAABYIIAHQA" + "ZQBzAHQAaQBuAGcALQBuAHQAbABtAFcAVABDAC0AVwBJAE4ANwBVKW" + "Yma5xzVAAAAAAAAAAAAAAAAAAAAACH+gWcm+YsP9Tqb9zCR3WAeZZX" + "ahlhx5I=\r\n\r\n"), }; MockRead data_reads2[] = { // The origin server responds with a Type 2 message. MockRead("HTTP/1.1 401 Access Denied\r\n"), MockRead("WWW-Authenticate: NTLM " - "TlRMTVNTUAACAAAADAAMADgAAAAFgokCTroKF1e/DRcAAAAAAAAAALo" + "TlRMTVNTUAACAAAADAAMADgAAAAFgokCjGpMpPGlYKkAAAAAAAAAALo" "AugBEAAAABQEoCgAAAA9HAE8ATwBHAEwARQACAAwARwBPAE8ARwBMAE" "UAAQAaAEEASwBFAEUAUwBBAFIAQQAtAEMATwBSAFAABAAeAGMAbwByA" "HAALgBnAG8AbwBnAGwAZQAuAGMAbwBtAAMAQABhAGsAZQBlAHMAYQBy" @@ -1643,10 +1683,6 @@ TEST_F(HttpNetworkTransactionTest, DISABLED_NTLMAuth) { EXPECT_EQ(L"", response->auth_challenge->realm); EXPECT_EQ(L"ntlm", response->auth_challenge->scheme); - // Pass a null identity to the first RestartWithAuth. - // TODO(wtc): In the future we may pass the actual identity to the first - // RestartWithAuth. - TestCompletionCallback callback2; rv = trans->RestartWithAuth(L"testing-ntlm", L"testing-ntlm", &callback2); @@ -1660,6 +1696,204 @@ TEST_F(HttpNetworkTransactionTest, DISABLED_NTLMAuth) { EXPECT_EQ(13, response->headers->GetContentLength()); } +// Enter a wrong password, and then the correct one. +TEST_F(HttpNetworkTransactionTest, NTLMAuth2) { + net::HttpAuthHandlerNTLM::SetGenerateRandomProc(MyGenerateRandom2); + net::HttpAuthHandlerNTLM::SetHostNameProc(MyGetHostName); + + scoped_ptr<net::ProxyService> proxy_service(CreateNullProxyService()); + scoped_ptr<net::HttpTransaction> trans(new net::HttpNetworkTransaction( + CreateSession(proxy_service.get()), &mock_socket_factory)); + + net::HttpRequestInfo request; + request.method = "GET"; + request.url = GURL("http://172.22.68.17/kids/login.aspx"); + request.load_flags = 0; + + MockWrite data_writes1[] = { + MockWrite("GET /kids/login.aspx HTTP/1.1\r\n" + "Host: 172.22.68.17\r\n" + "Connection: keep-alive\r\n\r\n"), + }; + + MockRead data_reads1[] = { + MockRead("HTTP/1.1 401 Access Denied\r\n"), + // Negotiate and NTLM are often requested together. We only support NTLM. + MockRead("WWW-Authenticate: Negotiate\r\n"), + MockRead("WWW-Authenticate: NTLM\r\n"), + MockRead("Connection: close\r\n"), + MockRead("Content-Length: 42\r\n"), + MockRead("Content-Type: text/html\r\n"), + MockRead("Proxy-Support: Session-Based-Authentication\r\n\r\n"), + // Missing content -- won't matter, as connection will be reset. + MockRead(false, net::ERR_UNEXPECTED), + }; + + MockWrite data_writes2[] = { + // After automatically restarting with a null identity, this is the + // request we should be issuing -- the final header line contains a Type + // 1 message. + MockWrite("GET /kids/login.aspx HTTP/1.1\r\n" + "Host: 172.22.68.17\r\n" + "Connection: keep-alive\r\n" + "Authorization: NTLM " + "TlRMTVNTUAABAAAAB4IIAAAAAAAAAAAAAAAAAAAAAAA=\r\n\r\n"), + + // After calling trans->RestartWithAuth(), we should send a Type 3 message + // (the credentials for the origin server). The second request continues + // on the same connection. + MockWrite("GET /kids/login.aspx HTTP/1.1\r\n" + "Host: 172.22.68.17\r\n" + "Connection: keep-alive\r\n" + "Authorization: NTLM TlRMTVNTUAADAAAAGAAYAGgAAAAYABgAgA" + "AAAAAAAABAAAAAGAAYAEAAAAAQABAAWAAAAAAAAAAAAAAABYIIAHQA" + "ZQBzAHQAaQBuAGcALQBuAHQAbABtAFcAVABDAC0AVwBJAE4ANwCWeY" + "XnSZNwoQAAAAAAAAAAAAAAAAAAAADLa34/phTTKzNTWdub+uyFleOj" + "4Ww7b7E=\r\n\r\n"), + }; + + MockRead data_reads2[] = { + // The origin server responds with a Type 2 message. + MockRead("HTTP/1.1 401 Access Denied\r\n"), + MockRead("WWW-Authenticate: NTLM " + "TlRMTVNTUAACAAAADAAMADgAAAAFgokCbVWUZezVGpAAAAAAAAAAALo" + "AugBEAAAABQEoCgAAAA9HAE8ATwBHAEwARQACAAwARwBPAE8ARwBMAE" + "UAAQAaAEEASwBFAEUAUwBBAFIAQQAtAEMATwBSAFAABAAeAGMAbwByA" + "HAALgBnAG8AbwBnAGwAZQAuAGMAbwBtAAMAQABhAGsAZQBlAHMAYQBy" + "AGEALQBjAG8AcgBwAC4AYQBkAC4AYwBvAHIAcAAuAGcAbwBvAGcAbAB" + "lAC4AYwBvAG0ABQAeAGMAbwByAHAALgBnAG8AbwBnAGwAZQAuAGMAbw" + "BtAAAAAAA=\r\n"), + MockRead("Content-Length: 42\r\n"), + MockRead("Content-Type: text/html\r\n"), + MockRead("Proxy-Support: Session-Based-Authentication\r\n\r\n"), + MockRead("You are not authorized to view this page\r\n"), + + // Wrong password. + MockRead("HTTP/1.1 401 Access Denied\r\n"), + MockRead("WWW-Authenticate: Negotiate\r\n"), + MockRead("WWW-Authenticate: NTLM\r\n"), + MockRead("Connection: close\r\n"), + MockRead("Content-Length: 42\r\n"), + MockRead("Content-Type: text/html\r\n"), + MockRead("Proxy-Support: Session-Based-Authentication\r\n\r\n"), + // Missing content -- won't matter, as connection will be reset. + MockRead(false, net::ERR_UNEXPECTED), + }; + + MockWrite data_writes3[] = { + // After automatically restarting with a null identity, this is the + // request we should be issuing -- the final header line contains a Type + // 1 message. + MockWrite("GET /kids/login.aspx HTTP/1.1\r\n" + "Host: 172.22.68.17\r\n" + "Connection: keep-alive\r\n" + "Authorization: NTLM " + "TlRMTVNTUAABAAAAB4IIAAAAAAAAAAAAAAAAAAAAAAA=\r\n\r\n"), + + // After calling trans->RestartWithAuth(), we should send a Type 3 message + // (the credentials for the origin server). The second request continues + // on the same connection. + MockWrite("GET /kids/login.aspx HTTP/1.1\r\n" + "Host: 172.22.68.17\r\n" + "Connection: keep-alive\r\n" + "Authorization: NTLM TlRMTVNTUAADAAAAGAAYAGgAAAAYABgAgA" + "AAAAAAAABAAAAAGAAYAEAAAAAQABAAWAAAAAAAAAAAAAAABYIIAHQA" + "ZQBzAHQAaQBuAGcALQBuAHQAbABtAFcAVABDAC0AVwBJAE4ANwBO54" + "dFMVvTHwAAAAAAAAAAAAAAAAAAAACS7sT6Uzw7L0L//WUqlIaVWpbI" + "+4MUm7c=\r\n\r\n"), + }; + + MockRead data_reads3[] = { + // The origin server responds with a Type 2 message. + MockRead("HTTP/1.1 401 Access Denied\r\n"), + MockRead("WWW-Authenticate: NTLM " + "TlRMTVNTUAACAAAADAAMADgAAAAFgokCL24VN8dgOR8AAAAAAAAAALo" + "AugBEAAAABQEoCgAAAA9HAE8ATwBHAEwARQACAAwARwBPAE8ARwBMAE" + "UAAQAaAEEASwBFAEUAUwBBAFIAQQAtAEMATwBSAFAABAAeAGMAbwByA" + "HAALgBnAG8AbwBnAGwAZQAuAGMAbwBtAAMAQABhAGsAZQBlAHMAYQBy" + "AGEALQBjAG8AcgBwAC4AYQBkAC4AYwBvAHIAcAAuAGcAbwBvAGcAbAB" + "lAC4AYwBvAG0ABQAeAGMAbwByAHAALgBnAG8AbwBnAGwAZQAuAGMAbw" + "BtAAAAAAA=\r\n"), + MockRead("Content-Length: 42\r\n"), + MockRead("Content-Type: text/html\r\n"), + MockRead("Proxy-Support: Session-Based-Authentication\r\n\r\n"), + MockRead("You are not authorized to view this page\r\n"), + + // Lastly we get the desired content. + MockRead("HTTP/1.1 200 OK\r\n"), + MockRead("Content-Type: text/html; charset=utf-8\r\n"), + MockRead("Content-Length: 13\r\n\r\n"), + MockRead("Please Login\r\n"), + MockRead(false, net::OK), + }; + + MockSocket data1; + data1.reads = data_reads1; + data1.writes = data_writes1; + MockSocket data2; + data2.reads = data_reads2; + data2.writes = data_writes2; + MockSocket data3; + data3.reads = data_reads3; + data3.writes = data_writes3; + mock_sockets[0] = &data1; + mock_sockets[1] = &data2; + mock_sockets[2] = &data3; + mock_sockets[3] = NULL; + + TestCompletionCallback callback1; + + int rv = trans->Start(&request, &callback1); + EXPECT_EQ(net::ERR_IO_PENDING, rv); + + rv = callback1.WaitForResult(); + EXPECT_EQ(net::OK, rv); + + const net::HttpResponseInfo* response = trans->GetResponseInfo(); + EXPECT_FALSE(response == NULL); + + // The password prompt info should have been set in response->auth_challenge. + EXPECT_FALSE(response->auth_challenge.get() == NULL); + + // TODO(eroman): this should really include the effective port (80) + EXPECT_EQ(L"172.22.68.17", response->auth_challenge->host); + EXPECT_EQ(L"", response->auth_challenge->realm); + EXPECT_EQ(L"ntlm", response->auth_challenge->scheme); + + TestCompletionCallback callback2; + + // Enter the wrong password. + rv = trans->RestartWithAuth(L"testing-ntlm", L"wrongpassword", &callback2); + EXPECT_EQ(net::ERR_IO_PENDING, rv); + + rv = callback2.WaitForResult(); + EXPECT_EQ(net::OK, rv); + + response = trans->GetResponseInfo(); + EXPECT_FALSE(response == NULL); + + // The password prompt info should have been set in response->auth_challenge. + EXPECT_FALSE(response->auth_challenge.get() == NULL); + + // TODO(eroman): this should really include the effective port (80) + EXPECT_EQ(L"172.22.68.17", response->auth_challenge->host); + EXPECT_EQ(L"", response->auth_challenge->realm); + EXPECT_EQ(L"ntlm", response->auth_challenge->scheme); + + TestCompletionCallback callback3; + + // Now enter the right password. + rv = trans->RestartWithAuth(L"testing-ntlm", L"testing-ntlm", &callback3); + EXPECT_EQ(net::ERR_IO_PENDING, rv); + + rv = callback3.WaitForResult(); + EXPECT_EQ(net::OK, rv); + + response = trans->GetResponseInfo(); + EXPECT_TRUE(response->auth_challenge.get() == NULL); + EXPECT_EQ(13, response->headers->GetContentLength()); +} + // Test reading a server response which has only headers, and no body. // After some maximum number of bytes is consumed, the transaction should // fail with ERR_RESPONSE_HEADERS_TOO_BIG. |