408 lines
13 KiB
JavaScript
408 lines
13 KiB
JavaScript
// Unit tests for a NTLM authenticated proxy
|
|
//
|
|
// Currently the tests do not determine whether the Authentication dialogs have
|
|
// been displayed.
|
|
//
|
|
|
|
"use strict";
|
|
|
|
const { HttpServer } = ChromeUtils.importESModule(
|
|
"resource://testing-common/httpd.sys.mjs"
|
|
);
|
|
|
|
ChromeUtils.defineLazyGetter(this, "URL", function () {
|
|
return "http://localhost:" + httpserver.identity.primaryPort;
|
|
});
|
|
|
|
function AuthPrompt() {}
|
|
|
|
AuthPrompt.prototype = {
|
|
user: "guest",
|
|
pass: "guest",
|
|
|
|
QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]),
|
|
|
|
promptAuth: function ap_promptAuth(channel, level, authInfo) {
|
|
authInfo.username = this.user;
|
|
authInfo.password = this.pass;
|
|
|
|
return true;
|
|
},
|
|
|
|
asyncPromptAuth: function ap_async() {
|
|
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
|
},
|
|
};
|
|
|
|
function Requestor() {}
|
|
|
|
Requestor.prototype = {
|
|
QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
|
|
|
|
getInterface: function requestor_gi(iid) {
|
|
if (iid.equals(Ci.nsIAuthPrompt2)) {
|
|
// Allow the prompt to store state by caching it here
|
|
if (!this.prompt) {
|
|
this.prompt = new AuthPrompt();
|
|
}
|
|
return this.prompt;
|
|
}
|
|
|
|
throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
|
|
},
|
|
|
|
prompt: null,
|
|
};
|
|
|
|
function makeChan(url, loadingUrl) {
|
|
var principal = Services.scriptSecurityManager.createContentPrincipal(
|
|
Services.io.newURI(loadingUrl),
|
|
{}
|
|
);
|
|
return NetUtil.newChannel({
|
|
uri: url,
|
|
loadingPrincipal: principal,
|
|
securityFlags: Ci.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT,
|
|
contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
|
|
});
|
|
}
|
|
|
|
function TestListener(resolve) {
|
|
this.resolve = resolve;
|
|
}
|
|
TestListener.prototype.onStartRequest = function (request) {
|
|
// Need to do the instanceof to allow request.responseStatus
|
|
// to be read.
|
|
if (!(request instanceof Ci.nsIHttpChannel)) {
|
|
dump("Expecting an HTTP channel");
|
|
}
|
|
|
|
Assert.equal(expectedResponse, request.responseStatus, "HTTP Status code");
|
|
};
|
|
TestListener.prototype.onStopRequest = function () {
|
|
Assert.equal(expectedRequests, requestsMade, "Number of requests made ");
|
|
Assert.equal(
|
|
exptTypeOneCount,
|
|
ntlmTypeOneCount,
|
|
"Number of type one messages received"
|
|
);
|
|
Assert.equal(
|
|
exptTypeTwoCount,
|
|
ntlmTypeTwoCount,
|
|
"Number of type two messages received"
|
|
);
|
|
|
|
this.resolve();
|
|
};
|
|
TestListener.prototype.onDataAvaiable = function (
|
|
request,
|
|
context,
|
|
stream,
|
|
offset,
|
|
count
|
|
) {
|
|
read_stream(stream, count);
|
|
};
|
|
|
|
// NTLM Messages, for the received type 1 and 3 messages only check that they
|
|
// are of the expected type.
|
|
const NTLM_TYPE1_PREFIX = "NTLM TlRMTVNTUAABAAAA";
|
|
const NTLM_TYPE2_PREFIX = "NTLM TlRMTVNTUAACAAAA";
|
|
const NTLM_TYPE3_PREFIX = "NTLM TlRMTVNTUAADAAAA";
|
|
const NTLM_PREFIX_LEN = 21;
|
|
|
|
const PROXY_CHALLENGE =
|
|
NTLM_TYPE2_PREFIX +
|
|
"DgAOADgAAAAFgooCqLNOPe2aZOAAAAAAAAAAAFAAUABGAAAA" +
|
|
"BgEAAAAAAA9HAFcATAAtAE0ATwBaAAIADgBHAFcATAAtAE0A" +
|
|
"TwBaAAEADgBHAFcATAAtAE0ATwBaAAQAAgAAAAMAEgBsAG8A" +
|
|
"YwBhAGwAaABvAHMAdAAHAAgAOKEwGEZL0gEAAAAA";
|
|
|
|
// Proxy responses for the happy path scenario.
|
|
// i.e. successful proxy auth
|
|
//
|
|
function successfulAuth(metadata, response) {
|
|
let authorization;
|
|
let authPrefix;
|
|
switch (requestsMade) {
|
|
case 0:
|
|
// Proxy - First request to the Proxy resppond with a 407 to start auth
|
|
response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
|
|
response.setHeader("Proxy-Authenticate", "NTLM", false);
|
|
break;
|
|
case 1:
|
|
// Proxy - Expecting a type 1 negotiate message from the client
|
|
authorization = metadata.getHeader("Proxy-Authorization");
|
|
authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
|
|
Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message");
|
|
response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
|
|
response.setHeader("Proxy-Authenticate", PROXY_CHALLENGE, false);
|
|
break;
|
|
case 2:
|
|
// Proxy - Expecting a type 3 Authenticate message from the client
|
|
// Will respond with a 401 to start web server auth sequence
|
|
authorization = metadata.getHeader("Proxy-Authorization");
|
|
authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
|
|
Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 3 message");
|
|
response.setStatusLine(metadata.httpVersion, 200, "Successful");
|
|
break;
|
|
default:
|
|
// We should be authenticated and further requests are permitted
|
|
authorization = metadata.getHeader("Proxy-Authorization");
|
|
Assert.isnull(authorization);
|
|
response.setStatusLine(metadata.httpVersion, 200, "Successful");
|
|
}
|
|
requestsMade++;
|
|
}
|
|
|
|
// Proxy responses simulating an invalid proxy password
|
|
// Note: that the connection should not be reused after the
|
|
// proxy auth fails.
|
|
//
|
|
function failedAuth(metadata, response) {
|
|
let authorization;
|
|
let authPrefix;
|
|
switch (requestsMade) {
|
|
case 0:
|
|
// Proxy - First request respond with a 407 to initiate auth sequence
|
|
response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
|
|
response.setHeader("Proxy-Authenticate", "NTLM", false);
|
|
break;
|
|
case 1:
|
|
// Proxy - Expecting a type 1 negotiate message from the client
|
|
authorization = metadata.getHeader("Proxy-Authorization");
|
|
authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
|
|
Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message");
|
|
response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
|
|
response.setHeader("Proxy-Authenticate", PROXY_CHALLENGE, false);
|
|
break;
|
|
case 2:
|
|
// Proxy - Expecting a type 3 Authenticate message from the client
|
|
// Respond with a 407 to indicate invalid credentials
|
|
authorization = metadata.getHeader("Proxy-Authorization");
|
|
authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
|
|
Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 3 message");
|
|
response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
|
|
response.setHeader("Proxy-Authenticate", "NTLM", false);
|
|
break;
|
|
default:
|
|
// Strictly speaking the connection should not be reused at this point
|
|
// commenting out for now.
|
|
dump("ERROR: NTLM Proxy Authentication, connection should not be reused");
|
|
// assert.fail();
|
|
response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
|
|
}
|
|
requestsMade++;
|
|
}
|
|
//
|
|
// Simulate a connection reset once the connection has been authenticated
|
|
// Detects bug 486508
|
|
//
|
|
function connectionReset(metadata, response) {
|
|
let authorization;
|
|
let authPrefix;
|
|
switch (requestsMade) {
|
|
case 0:
|
|
// Proxy - First request to the Proxy resppond with a 407 to start auth
|
|
response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
|
|
response.setHeader("Proxy-Authenticate", "NTLM", false);
|
|
break;
|
|
case 1:
|
|
// Proxy - Expecting a type 1 negotiate message from the client
|
|
authorization = metadata.getHeader("Proxy-Authorization");
|
|
authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
|
|
Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message");
|
|
ntlmTypeOneCount++;
|
|
response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
|
|
response.setHeader("Proxy-Authenticate", PROXY_CHALLENGE, false);
|
|
break;
|
|
case 2:
|
|
authorization = metadata.getHeader("Proxy-Authorization");
|
|
authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
|
|
Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 3 message");
|
|
ntlmTypeTwoCount++;
|
|
try {
|
|
response.seizePower();
|
|
response.finish();
|
|
} catch (e) {
|
|
Assert.ok(false, "unexpected exception" + e);
|
|
}
|
|
break;
|
|
default:
|
|
// Should not get any further requests on this channel
|
|
dump("ERROR: NTLM Proxy Authentication, connection should not be reused");
|
|
Assert.ok(false);
|
|
}
|
|
requestsMade++;
|
|
}
|
|
|
|
//
|
|
// Reset the connection after a negotiate message has been received
|
|
//
|
|
function connectionReset02(metadata, response) {
|
|
var connectionNumber = httpserver.connectionNumber;
|
|
switch (requestsMade) {
|
|
case 0:
|
|
// Proxy - First request to the Proxy respond with a 407 to start auth
|
|
response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
|
|
response.setHeader("Proxy-Authenticate", "NTLM", false);
|
|
Assert.equal(connectionNumber, httpserver.connectionNumber);
|
|
break;
|
|
case 1:
|
|
// eslint-disable-next-line no-fallthrough
|
|
default:
|
|
// Proxy - Expecting a type 1 negotiate message from the client
|
|
Assert.equal(connectionNumber, httpserver.connectionNumber);
|
|
var authorization = metadata.getHeader("Proxy-Authorization");
|
|
var authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
|
|
Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message");
|
|
ntlmTypeOneCount++;
|
|
try {
|
|
response.seizePower();
|
|
response.finish();
|
|
} catch (e) {
|
|
Assert.ok(false, "unexpected exception" + e);
|
|
}
|
|
}
|
|
requestsMade++;
|
|
}
|
|
|
|
var httpserver = null;
|
|
function setup() {
|
|
httpserver = new HttpServer();
|
|
httpserver.start(-1);
|
|
|
|
Services.prefs.setCharPref("network.proxy.http", "localhost");
|
|
Services.prefs.setIntPref(
|
|
"network.proxy.http_port",
|
|
httpserver.identity.primaryPort
|
|
);
|
|
Services.prefs.setCharPref("network.proxy.no_proxies_on", "");
|
|
Services.prefs.setIntPref("network.proxy.type", 1);
|
|
Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
|
|
|
|
registerCleanupFunction(async () => {
|
|
Services.prefs.clearUserPref("network.proxy.http");
|
|
Services.prefs.clearUserPref("network.proxy.http_port");
|
|
Services.prefs.clearUserPref("network.proxy.no_proxies_on");
|
|
Services.prefs.clearUserPref("network.proxy.type");
|
|
Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
|
|
|
|
await httpserver.stop();
|
|
});
|
|
}
|
|
setup();
|
|
|
|
var expectedRequests = 0; // Number of HTTP requests that are expected
|
|
var requestsMade = 0; // The number of requests that were made
|
|
var expectedResponse = 0; // The response code
|
|
// Note that any test failures in the HTTP handler
|
|
// will manifest as a 500 response code
|
|
|
|
// Common test setup
|
|
// Parameters:
|
|
// path - path component of the URL
|
|
// handler - http handler function for the httpserver
|
|
// requests - expected number oh http requests
|
|
// response - expected http response code
|
|
// clearCache - clear the authentication cache before running the test
|
|
function setupTest(path, handler, requests, response, clearCache) {
|
|
requestsMade = 0;
|
|
expectedRequests = requests;
|
|
expectedResponse = response;
|
|
|
|
// clear the auth cache if requested
|
|
if (clearCache) {
|
|
dump("Clearing auth cache");
|
|
Cc["@mozilla.org/network/http-auth-manager;1"]
|
|
.getService(Ci.nsIHttpAuthManager)
|
|
.clearAll();
|
|
}
|
|
|
|
return new Promise(resolve => {
|
|
var chan = makeChan(URL + path, URL);
|
|
httpserver.registerPathHandler(path, handler);
|
|
chan.notificationCallbacks = new Requestor();
|
|
chan.asyncOpen(new TestListener(resolve));
|
|
});
|
|
}
|
|
|
|
let ntlmTypeOneCount = 0; // The number of NTLM type one messages received
|
|
let exptTypeOneCount = 0; // The number of NTLM type one messages that should be received
|
|
let ntlmTypeTwoCount = 0; // The number of NTLM type two messages received
|
|
let exptTypeTwoCount = 0; // The number of NTLM type two messages that should received
|
|
|
|
// Happy code path
|
|
// Successful proxy auth.
|
|
async function test_happy_path() {
|
|
dump("RUNNING TEST: test_happy_path");
|
|
await setupTest("/auth", successfulAuth, 3, 200, 1);
|
|
}
|
|
|
|
// Failed proxy authentication
|
|
async function test_failed_auth() {
|
|
dump("RUNNING TEST:failed auth ");
|
|
await setupTest("/auth", failedAuth, 4, 407, 1);
|
|
}
|
|
|
|
// Test connection reset, after successful auth
|
|
async function test_connection_reset() {
|
|
dump("RUNNING TEST:connection reset ");
|
|
ntlmTypeOneCount = 0;
|
|
ntlmTypeTwoCount = 0;
|
|
exptTypeOneCount = 1;
|
|
exptTypeTwoCount = 1;
|
|
await setupTest("/auth", connectionReset, 3, 500, 1);
|
|
}
|
|
|
|
// Test connection reset after sending a negotiate.
|
|
// Client should retry request using the same connection
|
|
async function test_connection_reset02() {
|
|
dump("RUNNING TEST:connection reset ");
|
|
ntlmTypeOneCount = 0;
|
|
ntlmTypeTwoCount = 0;
|
|
let maxRetryAttempt = 5;
|
|
exptTypeOneCount = maxRetryAttempt;
|
|
exptTypeTwoCount = 0;
|
|
|
|
Services.prefs.setIntPref(
|
|
"network.http.request.max-attempts",
|
|
maxRetryAttempt
|
|
);
|
|
|
|
await setupTest("/auth", connectionReset02, maxRetryAttempt + 1, 500, 1);
|
|
}
|
|
|
|
add_task(
|
|
{ pref_set: [["network.auth.use_redirect_for_retries", false]] },
|
|
test_happy_path
|
|
);
|
|
add_task(
|
|
{ pref_set: [["network.auth.use_redirect_for_retries", false]] },
|
|
test_failed_auth
|
|
);
|
|
add_task(
|
|
{ pref_set: [["network.auth.use_redirect_for_retries", false]] },
|
|
test_connection_reset
|
|
);
|
|
add_task(
|
|
{ pref_set: [["network.auth.use_redirect_for_retries", false]] },
|
|
test_connection_reset02
|
|
);
|
|
|
|
add_task(
|
|
{ pref_set: [["network.auth.use_redirect_for_retries", true]] },
|
|
test_happy_path
|
|
);
|
|
add_task(
|
|
{ pref_set: [["network.auth.use_redirect_for_retries", true]] },
|
|
test_failed_auth
|
|
);
|
|
add_task(
|
|
{ pref_set: [["network.auth.use_redirect_for_retries", true]] },
|
|
test_connection_reset
|
|
);
|
|
add_task(
|
|
{ pref_set: [["network.auth.use_redirect_for_retries", true]] },
|
|
test_connection_reset02
|
|
);
|