test_ntlm_proxy_auth.js (13466B)
1 // Unit tests for a NTLM authenticated proxy 2 // 3 // Currently the tests do not determine whether the Authentication dialogs have 4 // been displayed. 5 // 6 7 "use strict"; 8 9 const { HttpServer } = ChromeUtils.importESModule( 10 "resource://testing-common/httpd.sys.mjs" 11 ); 12 13 ChromeUtils.defineLazyGetter(this, "URL", function () { 14 return "http://localhost:" + httpserver.identity.primaryPort; 15 }); 16 17 function AuthPrompt() {} 18 19 AuthPrompt.prototype = { 20 user: "guest", 21 pass: "guest", 22 23 QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]), 24 25 promptAuth: function ap_promptAuth(channel, level, authInfo) { 26 authInfo.username = this.user; 27 authInfo.password = this.pass; 28 29 return true; 30 }, 31 32 asyncPromptAuth: function ap_async() { 33 throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); 34 }, 35 }; 36 37 function Requestor() {} 38 39 Requestor.prototype = { 40 QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]), 41 42 getInterface: function requestor_gi(iid) { 43 if (iid.equals(Ci.nsIAuthPrompt2)) { 44 // Allow the prompt to store state by caching it here 45 if (!this.prompt) { 46 this.prompt = new AuthPrompt(); 47 } 48 return this.prompt; 49 } 50 51 throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); 52 }, 53 54 prompt: null, 55 }; 56 57 function makeChan(url, loadingUrl) { 58 var principal = Services.scriptSecurityManager.createContentPrincipal( 59 Services.io.newURI(loadingUrl), 60 {} 61 ); 62 return NetUtil.newChannel({ 63 uri: url, 64 loadingPrincipal: principal, 65 securityFlags: Ci.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT, 66 contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, 67 }); 68 } 69 70 function TestListener(resolve) { 71 this.resolve = resolve; 72 } 73 TestListener.prototype.onStartRequest = function (request) { 74 // Need to do the instanceof to allow request.responseStatus 75 // to be read. 76 if (!(request instanceof Ci.nsIHttpChannel)) { 77 dump("Expecting an HTTP channel"); 78 } 79 80 Assert.equal(expectedResponse, request.responseStatus, "HTTP Status code"); 81 }; 82 TestListener.prototype.onStopRequest = function () { 83 Assert.equal(expectedRequests, requestsMade, "Number of requests made "); 84 Assert.equal( 85 exptTypeOneCount, 86 ntlmTypeOneCount, 87 "Number of type one messages received" 88 ); 89 Assert.equal( 90 exptTypeTwoCount, 91 ntlmTypeTwoCount, 92 "Number of type two messages received" 93 ); 94 95 this.resolve(); 96 }; 97 TestListener.prototype.onDataAvaiable = function ( 98 request, 99 context, 100 stream, 101 offset, 102 count 103 ) { 104 read_stream(stream, count); 105 }; 106 107 // NTLM Messages, for the received type 1 and 3 messages only check that they 108 // are of the expected type. 109 const NTLM_TYPE1_PREFIX = "NTLM TlRMTVNTUAABAAAA"; 110 const NTLM_TYPE2_PREFIX = "NTLM TlRMTVNTUAACAAAA"; 111 const NTLM_TYPE3_PREFIX = "NTLM TlRMTVNTUAADAAAA"; 112 const NTLM_PREFIX_LEN = 21; 113 114 const PROXY_CHALLENGE = 115 NTLM_TYPE2_PREFIX + 116 "DgAOADgAAAAFgooCqLNOPe2aZOAAAAAAAAAAAFAAUABGAAAA" + 117 "BgEAAAAAAA9HAFcATAAtAE0ATwBaAAIADgBHAFcATAAtAE0A" + 118 "TwBaAAEADgBHAFcATAAtAE0ATwBaAAQAAgAAAAMAEgBsAG8A" + 119 "YwBhAGwAaABvAHMAdAAHAAgAOKEwGEZL0gEAAAAA"; 120 121 // Proxy responses for the happy path scenario. 122 // i.e. successful proxy auth 123 // 124 function successfulAuth(metadata, response) { 125 let authorization; 126 let authPrefix; 127 switch (requestsMade) { 128 case 0: 129 // Proxy - First request to the Proxy resppond with a 407 to start auth 130 response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); 131 response.setHeader("Proxy-Authenticate", "NTLM", false); 132 break; 133 case 1: 134 // Proxy - Expecting a type 1 negotiate message from the client 135 authorization = metadata.getHeader("Proxy-Authorization"); 136 authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); 137 Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message"); 138 response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); 139 response.setHeader("Proxy-Authenticate", PROXY_CHALLENGE, false); 140 break; 141 case 2: 142 // Proxy - Expecting a type 3 Authenticate message from the client 143 // Will respond with a 401 to start web server auth sequence 144 authorization = metadata.getHeader("Proxy-Authorization"); 145 authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); 146 Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 3 message"); 147 response.setStatusLine(metadata.httpVersion, 200, "Successful"); 148 break; 149 default: 150 // We should be authenticated and further requests are permitted 151 authorization = metadata.getHeader("Proxy-Authorization"); 152 Assert.isnull(authorization); 153 response.setStatusLine(metadata.httpVersion, 200, "Successful"); 154 } 155 requestsMade++; 156 } 157 158 // Proxy responses simulating an invalid proxy password 159 // Note: that the connection should not be reused after the 160 // proxy auth fails. 161 // 162 function failedAuth(metadata, response) { 163 let authorization; 164 let authPrefix; 165 switch (requestsMade) { 166 case 0: 167 // Proxy - First request respond with a 407 to initiate auth sequence 168 response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); 169 response.setHeader("Proxy-Authenticate", "NTLM", false); 170 break; 171 case 1: 172 // Proxy - Expecting a type 1 negotiate message from the client 173 authorization = metadata.getHeader("Proxy-Authorization"); 174 authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); 175 Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message"); 176 response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); 177 response.setHeader("Proxy-Authenticate", PROXY_CHALLENGE, false); 178 break; 179 case 2: 180 // Proxy - Expecting a type 3 Authenticate message from the client 181 // Respond with a 407 to indicate invalid credentials 182 authorization = metadata.getHeader("Proxy-Authorization"); 183 authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); 184 Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 3 message"); 185 response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); 186 response.setHeader("Proxy-Authenticate", "NTLM", false); 187 break; 188 default: 189 // Strictly speaking the connection should not be reused at this point 190 // commenting out for now. 191 dump("ERROR: NTLM Proxy Authentication, connection should not be reused"); 192 // assert.fail(); 193 response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); 194 } 195 requestsMade++; 196 } 197 // 198 // Simulate a connection reset once the connection has been authenticated 199 // Detects bug 486508 200 // 201 function connectionReset(metadata, response) { 202 let authorization; 203 let authPrefix; 204 switch (requestsMade) { 205 case 0: 206 // Proxy - First request to the Proxy resppond with a 407 to start auth 207 response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); 208 response.setHeader("Proxy-Authenticate", "NTLM", false); 209 break; 210 case 1: 211 // Proxy - Expecting a type 1 negotiate message from the client 212 authorization = metadata.getHeader("Proxy-Authorization"); 213 authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); 214 Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message"); 215 ntlmTypeOneCount++; 216 response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); 217 response.setHeader("Proxy-Authenticate", PROXY_CHALLENGE, false); 218 break; 219 case 2: 220 authorization = metadata.getHeader("Proxy-Authorization"); 221 authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); 222 Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 3 message"); 223 ntlmTypeTwoCount++; 224 try { 225 response.seizePower(); 226 response.finish(); 227 } catch (e) { 228 Assert.ok(false, "unexpected exception" + e); 229 } 230 break; 231 default: 232 // Should not get any further requests on this channel 233 dump("ERROR: NTLM Proxy Authentication, connection should not be reused"); 234 Assert.ok(false); 235 } 236 requestsMade++; 237 } 238 239 // 240 // Reset the connection after a negotiate message has been received 241 // 242 function connectionReset02(metadata, response) { 243 var connectionNumber = httpserver.connectionNumber; 244 switch (requestsMade) { 245 case 0: 246 // Proxy - First request to the Proxy respond with a 407 to start auth 247 response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); 248 response.setHeader("Proxy-Authenticate", "NTLM", false); 249 Assert.equal(connectionNumber, httpserver.connectionNumber); 250 break; 251 case 1: 252 // eslint-disable-next-line no-fallthrough 253 default: 254 // Proxy - Expecting a type 1 negotiate message from the client 255 Assert.equal(connectionNumber, httpserver.connectionNumber); 256 var authorization = metadata.getHeader("Proxy-Authorization"); 257 var authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); 258 Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message"); 259 ntlmTypeOneCount++; 260 try { 261 response.seizePower(); 262 response.finish(); 263 } catch (e) { 264 Assert.ok(false, "unexpected exception" + e); 265 } 266 } 267 requestsMade++; 268 } 269 270 var httpserver = null; 271 function setup() { 272 httpserver = new HttpServer(); 273 httpserver.start(-1); 274 275 Services.prefs.setCharPref("network.proxy.http", "localhost"); 276 Services.prefs.setIntPref( 277 "network.proxy.http_port", 278 httpserver.identity.primaryPort 279 ); 280 Services.prefs.setCharPref("network.proxy.no_proxies_on", ""); 281 Services.prefs.setIntPref("network.proxy.type", 1); 282 Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); 283 284 registerCleanupFunction(async () => { 285 Services.prefs.clearUserPref("network.proxy.http"); 286 Services.prefs.clearUserPref("network.proxy.http_port"); 287 Services.prefs.clearUserPref("network.proxy.no_proxies_on"); 288 Services.prefs.clearUserPref("network.proxy.type"); 289 Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); 290 291 await httpserver.stop(); 292 }); 293 } 294 setup(); 295 296 var expectedRequests = 0; // Number of HTTP requests that are expected 297 var requestsMade = 0; // The number of requests that were made 298 var expectedResponse = 0; // The response code 299 // Note that any test failures in the HTTP handler 300 // will manifest as a 500 response code 301 302 // Common test setup 303 // Parameters: 304 // path - path component of the URL 305 // handler - http handler function for the httpserver 306 // requests - expected number oh http requests 307 // response - expected http response code 308 // clearCache - clear the authentication cache before running the test 309 function setupTest(path, handler, requests, response, clearCache) { 310 requestsMade = 0; 311 expectedRequests = requests; 312 expectedResponse = response; 313 314 // clear the auth cache if requested 315 if (clearCache) { 316 dump("Clearing auth cache"); 317 Cc["@mozilla.org/network/http-auth-manager;1"] 318 .getService(Ci.nsIHttpAuthManager) 319 .clearAll(); 320 } 321 322 return new Promise(resolve => { 323 var chan = makeChan(URL + path, URL); 324 httpserver.registerPathHandler(path, handler); 325 chan.notificationCallbacks = new Requestor(); 326 chan.asyncOpen(new TestListener(resolve)); 327 }); 328 } 329 330 let ntlmTypeOneCount = 0; // The number of NTLM type one messages received 331 let exptTypeOneCount = 0; // The number of NTLM type one messages that should be received 332 let ntlmTypeTwoCount = 0; // The number of NTLM type two messages received 333 let exptTypeTwoCount = 0; // The number of NTLM type two messages that should received 334 335 // Happy code path 336 // Successful proxy auth. 337 async function test_happy_path() { 338 dump("RUNNING TEST: test_happy_path"); 339 await setupTest("/auth", successfulAuth, 3, 200, 1); 340 } 341 342 // Failed proxy authentication 343 async function test_failed_auth() { 344 dump("RUNNING TEST:failed auth "); 345 await setupTest("/auth", failedAuth, 4, 407, 1); 346 } 347 348 // Test connection reset, after successful auth 349 async function test_connection_reset() { 350 dump("RUNNING TEST:connection reset "); 351 ntlmTypeOneCount = 0; 352 ntlmTypeTwoCount = 0; 353 exptTypeOneCount = 1; 354 exptTypeTwoCount = 1; 355 await setupTest("/auth", connectionReset, 3, 500, 1); 356 } 357 358 // Test connection reset after sending a negotiate. 359 // Client should retry request using the same connection 360 async function test_connection_reset02() { 361 dump("RUNNING TEST:connection reset "); 362 ntlmTypeOneCount = 0; 363 ntlmTypeTwoCount = 0; 364 let maxRetryAttempt = 5; 365 exptTypeOneCount = maxRetryAttempt; 366 exptTypeTwoCount = 0; 367 368 Services.prefs.setIntPref( 369 "network.http.request.max-attempts", 370 maxRetryAttempt 371 ); 372 373 await setupTest("/auth", connectionReset02, maxRetryAttempt + 1, 500, 1); 374 } 375 376 add_task( 377 { pref_set: [["network.auth.use_redirect_for_retries", false]] }, 378 test_happy_path 379 ); 380 add_task( 381 { pref_set: [["network.auth.use_redirect_for_retries", false]] }, 382 test_failed_auth 383 ); 384 add_task( 385 { pref_set: [["network.auth.use_redirect_for_retries", false]] }, 386 test_connection_reset 387 ); 388 add_task( 389 { pref_set: [["network.auth.use_redirect_for_retries", false]] }, 390 test_connection_reset02 391 ); 392 393 add_task( 394 { pref_set: [["network.auth.use_redirect_for_retries", true]] }, 395 test_happy_path 396 ); 397 add_task( 398 { pref_set: [["network.auth.use_redirect_for_retries", true]] }, 399 test_failed_auth 400 ); 401 add_task( 402 { pref_set: [["network.auth.use_redirect_for_retries", true]] }, 403 test_connection_reset 404 ); 405 add_task( 406 { pref_set: [["network.auth.use_redirect_for_retries", true]] }, 407 test_connection_reset02 408 );