forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			462 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			462 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| // This file tests authentication prompt callbacks
 | |
| // TODO NIT use do_check_eq(expected, actual) consistently, not sometimes eq(actual, expected)
 | |
| 
 | |
| "use strict";
 | |
| 
 | |
| const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
 | |
| 
 | |
| // Turn off the authentication dialog blocking for this test.
 | |
| var prefs = Services.prefs;
 | |
| prefs.setIntPref("network.auth.subresource-http-auth-allow", 2);
 | |
| 
 | |
| function URL(domain, path = "") {
 | |
|   if (path.startsWith("/")) {
 | |
|     path = path.substring(1);
 | |
|   }
 | |
|   return `http://${domain}:${httpserv.identity.primaryPort}/${path}`;
 | |
| }
 | |
| 
 | |
| XPCOMUtils.defineLazyGetter(this, "PORT", function() {
 | |
|   return httpserv.identity.primaryPort;
 | |
| });
 | |
| 
 | |
| const FLAG_RETURN_FALSE = 1 << 0;
 | |
| const FLAG_WRONG_PASSWORD = 1 << 1;
 | |
| const FLAG_BOGUS_USER = 1 << 2;
 | |
| // const FLAG_PREVIOUS_FAILED = 1 << 3;
 | |
| const CROSS_ORIGIN = 1 << 4;
 | |
| // const FLAG_NO_REALM = 1 << 5;
 | |
| const FLAG_NON_ASCII_USER_PASSWORD = 1 << 6;
 | |
| 
 | |
| function AuthPrompt1(flags) {
 | |
|   this.flags = flags;
 | |
| }
 | |
| 
 | |
| AuthPrompt1.prototype = {
 | |
|   user: "guest",
 | |
|   pass: "guest",
 | |
| 
 | |
|   expectedRealm: "secret",
 | |
| 
 | |
|   QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt"]),
 | |
| 
 | |
|   prompt: function ap1_prompt(title, text, realm, save, defaultText, result) {
 | |
|     do_throw("unexpected prompt call");
 | |
|   },
 | |
| 
 | |
|   promptUsernameAndPassword: function ap1_promptUP(
 | |
|     title,
 | |
|     text,
 | |
|     realm,
 | |
|     savePW,
 | |
|     user,
 | |
|     pw
 | |
|   ) {
 | |
|     if (!(this.flags & CROSS_ORIGIN)) {
 | |
|       if (!text.includes(this.expectedRealm)) {
 | |
|         do_throw("Text must indicate the realm");
 | |
|       }
 | |
|     } else if (text.includes(this.expectedRealm)) {
 | |
|       do_throw("There should not be realm for cross origin");
 | |
|     }
 | |
|     if (!text.includes("localhost")) {
 | |
|       do_throw("Text must indicate the hostname");
 | |
|     }
 | |
|     if (!text.includes(String(PORT))) {
 | |
|       do_throw("Text must indicate the port");
 | |
|     }
 | |
|     if (text.includes("-1")) {
 | |
|       do_throw("Text must contain negative numbers");
 | |
|     }
 | |
| 
 | |
|     if (this.flags & FLAG_RETURN_FALSE) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     if (this.flags & FLAG_BOGUS_USER) {
 | |
|       this.user = "foo\nbar";
 | |
|     } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) {
 | |
|       this.user = "é";
 | |
|     }
 | |
| 
 | |
|     user.value = this.user;
 | |
|     if (this.flags & FLAG_WRONG_PASSWORD) {
 | |
|       pw.value = this.pass + ".wrong";
 | |
|       // Now clear the flag to avoid an infinite loop
 | |
|       this.flags &= ~FLAG_WRONG_PASSWORD;
 | |
|     } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) {
 | |
|       pw.value = "é";
 | |
|     } else {
 | |
|       pw.value = this.pass;
 | |
|     }
 | |
|     return true;
 | |
|   },
 | |
| 
 | |
|   promptPassword: function ap1_promptPW(title, text, realm, save, pwd) {
 | |
|     do_throw("unexpected promptPassword call");
 | |
|   },
 | |
| };
 | |
| 
 | |
| function AuthPrompt2(flags) {
 | |
|   this.flags = flags;
 | |
| }
 | |
| 
 | |
| AuthPrompt2.prototype = {
 | |
|   user: "guest",
 | |
|   pass: "guest",
 | |
| 
 | |
|   expectedRealm: "secret",
 | |
| 
 | |
|   QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]),
 | |
| 
 | |
|   promptAuth: function ap2_promptAuth(channel, level, authInfo) {
 | |
|     authInfo.username = this.user;
 | |
|     authInfo.password = this.pass;
 | |
|     return true;
 | |
|   },
 | |
| 
 | |
|   asyncPromptAuth: function ap2_async(chan, cb, ctx, lvl, info) {
 | |
|     throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
 | |
|   },
 | |
| };
 | |
| 
 | |
| function Requestor(flags, versions) {
 | |
|   this.flags = flags;
 | |
|   this.versions = versions;
 | |
| }
 | |
| 
 | |
| Requestor.prototype = {
 | |
|   QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
 | |
| 
 | |
|   getInterface: function requestor_gi(iid) {
 | |
|     if (this.versions & 1 && iid.equals(Ci.nsIAuthPrompt)) {
 | |
|       // Allow the prompt to store state by caching it here
 | |
|       if (!this.prompt1) {
 | |
|         this.prompt1 = new AuthPrompt1(this.flags);
 | |
|       }
 | |
|       return this.prompt1;
 | |
|     }
 | |
|     if (this.versions & 2 && iid.equals(Ci.nsIAuthPrompt2)) {
 | |
|       // Allow the prompt to store state by caching it here
 | |
|       if (!this.prompt2) {
 | |
|         this.prompt2 = new AuthPrompt2(this.flags);
 | |
|       }
 | |
|       return this.prompt2;
 | |
|     }
 | |
| 
 | |
|     throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
 | |
|   },
 | |
| 
 | |
|   prompt1: null,
 | |
|   prompt2: null,
 | |
| };
 | |
| 
 | |
| function RealmTestRequestor() {}
 | |
| 
 | |
| RealmTestRequestor.prototype = {
 | |
|   QueryInterface: ChromeUtils.generateQI([
 | |
|     "nsIInterfaceRequestor",
 | |
|     "nsIAuthPrompt2",
 | |
|   ]),
 | |
| 
 | |
|   getInterface: function realmtest_interface(iid) {
 | |
|     if (iid.equals(Ci.nsIAuthPrompt2)) {
 | |
|       return this;
 | |
|     }
 | |
| 
 | |
|     throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
 | |
|   },
 | |
| 
 | |
|   promptAuth: function realmtest_checkAuth(channel, level, authInfo) {
 | |
|     Assert.equal(authInfo.realm, '"foo_bar');
 | |
| 
 | |
|     return false;
 | |
|   },
 | |
| 
 | |
|   asyncPromptAuth: function realmtest_async(chan, cb, ctx, lvl, info) {
 | |
|     throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
 | |
|   },
 | |
| };
 | |
| 
 | |
| function makeChan(url) {
 | |
|   let loadingUrl = Services.io
 | |
|     .newURI(url)
 | |
|     .mutate()
 | |
|     .setPathQueryRef("")
 | |
|     .finalize();
 | |
|   var principal = Services.scriptSecurityManager.createContentPrincipal(
 | |
|     loadingUrl,
 | |
|     {}
 | |
|   );
 | |
|   return NetUtil.newChannel({
 | |
|     uri: url,
 | |
|     loadingPrincipal: principal,
 | |
|     securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
 | |
|     contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
 | |
|   });
 | |
| }
 | |
| 
 | |
| function ntlm_auth(metadata, response) {
 | |
|   let challenge = metadata.getHeader("Authorization");
 | |
|   if (!challenge.startsWith("NTLM ")) {
 | |
|     response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   let decoded = atob(challenge.substring(5));
 | |
|   info(decoded);
 | |
| 
 | |
|   if (!decoded.startsWith("NTLMSSP\0")) {
 | |
|     response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   let isNegotiate = decoded.substring(8).startsWith("\x01\x00\x00\x00");
 | |
|   let isAuthenticate = decoded.substring(8).startsWith("\x03\x00\x00\x00");
 | |
| 
 | |
|   if (isNegotiate) {
 | |
|     response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
 | |
|     response.setHeader(
 | |
|       "WWW-Authenticate",
 | |
|       "NTLM TlRMTVNTUAACAAAAAAAAAAAoAAABggAAASNFZ4mrze8AAAAAAAAAAAAAAAAAAAAA",
 | |
|       false
 | |
|     );
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   if (isAuthenticate) {
 | |
|     let body = "OK";
 | |
|     response.bodyOutputStream.write(body, body.length);
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   // Something else went wrong.
 | |
|   response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
 | |
| }
 | |
| 
 | |
| function basic_auth(metadata, response) {
 | |
|   let challenge = metadata.getHeader("Authorization");
 | |
|   if (!challenge.startsWith("Basic ")) {
 | |
|     response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   if (challenge == "Basic Z3Vlc3Q6Z3Vlc3Q=") {
 | |
|     response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
 | |
|     response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
 | |
| 
 | |
|     let body = "success";
 | |
|     response.bodyOutputStream.write(body, body.length);
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
 | |
|   response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
 | |
| }
 | |
| 
 | |
| //
 | |
| // Digest functions
 | |
| //
 | |
| function bytesFromString(str) {
 | |
|   const encoder = new TextEncoder("utf-8");
 | |
|   return encoder.encode(str);
 | |
| }
 | |
| 
 | |
| // return the two-digit hexadecimal code for a byte
 | |
| function toHexString(charCode) {
 | |
|   return ("0" + charCode.toString(16)).slice(-2);
 | |
| }
 | |
| 
 | |
| function H(str) {
 | |
|   var data = bytesFromString(str);
 | |
|   var ch = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
 | |
|   ch.init(Ci.nsICryptoHash.MD5);
 | |
|   ch.update(data, data.length);
 | |
|   var hash = ch.finish(false);
 | |
|   return Array.from(hash, (c, i) => toHexString(hash.charCodeAt(i))).join("");
 | |
| }
 | |
| 
 | |
| const nonce = "6f93719059cf8d568005727f3250e798";
 | |
| const opaque = "1234opaque1234";
 | |
| const digestChallenge = `Digest realm="secret", domain="/",  qop=auth,algorithm=MD5, nonce="${nonce}" opaque="${opaque}"`;
 | |
| //
 | |
| // Digest handler
 | |
| //
 | |
| // /auth/digest
 | |
| function authDigest(metadata, response) {
 | |
|   var cnonceRE = /cnonce="(\w+)"/;
 | |
|   var responseRE = /response="(\w+)"/;
 | |
|   var usernameRE = /username="(\w+)"/;
 | |
|   var body = "";
 | |
|   // check creds if we have them
 | |
|   if (metadata.hasHeader("Authorization")) {
 | |
|     var auth = metadata.getHeader("Authorization");
 | |
|     var cnonce = auth.match(cnonceRE)[1];
 | |
|     var clientDigest = auth.match(responseRE)[1];
 | |
|     var username = auth.match(usernameRE)[1];
 | |
|     var nc = "00000001";
 | |
| 
 | |
|     if (username != "guest") {
 | |
|       response.setStatusLine(metadata.httpVersion, 400, "bad request");
 | |
|       body = "should never get here";
 | |
|     } else {
 | |
|       // see RFC2617 for the description of this calculation
 | |
|       var A1 = "guest:secret:guest";
 | |
|       var A2 = "GET:/path";
 | |
|       var noncebits = [nonce, nc, cnonce, "auth", H(A2)].join(":");
 | |
|       var digest = H([H(A1), noncebits].join(":"));
 | |
| 
 | |
|       if (clientDigest == digest) {
 | |
|         response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
 | |
|         body = "digest";
 | |
|       } else {
 | |
|         info(clientDigest);
 | |
|         info(digest);
 | |
|         handle_unauthorized(metadata, response);
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
|   } else {
 | |
|     // no header, send one
 | |
|     handle_unauthorized(metadata, response);
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   response.bodyOutputStream.write(body, body.length);
 | |
| }
 | |
| 
 | |
| let challenges = ["NTLM", `Basic realm="secret"`, digestChallenge];
 | |
| 
 | |
| function handle_unauthorized(metadata, response) {
 | |
|   response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
 | |
| 
 | |
|   for (let ch of challenges) {
 | |
|     response.setHeader("WWW-Authenticate", ch, true);
 | |
|   }
 | |
| }
 | |
| 
 | |
| // /path
 | |
| function auth_handler(metadata, response) {
 | |
|   if (!metadata.hasHeader("Authorization")) {
 | |
|     handle_unauthorized(metadata, response);
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   let challenge = metadata.getHeader("Authorization");
 | |
|   if (challenge.startsWith("NTLM ")) {
 | |
|     ntlm_auth(metadata, response);
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   if (challenge.startsWith("Basic ")) {
 | |
|     basic_auth(metadata, response);
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   if (challenge.startsWith("Digest ")) {
 | |
|     authDigest(metadata, response);
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   handle_unauthorized(metadata, response);
 | |
| }
 | |
| 
 | |
| let httpserv;
 | |
| add_setup(() => {
 | |
|   Services.prefs.setBoolPref("network.auth.force-generic-ntlm", true);
 | |
|   Services.prefs.setBoolPref("network.auth.force-generic-ntlm-v1", true);
 | |
|   Services.prefs.setBoolPref("network.dns.native-is-localhost", true);
 | |
|   Services.prefs.setBoolPref("network.http.sanitize-headers-in-logs", false);
 | |
| 
 | |
|   httpserv = new HttpServer();
 | |
|   httpserv.registerPathHandler("/path", auth_handler);
 | |
|   httpserv.start(-1);
 | |
| 
 | |
|   registerCleanupFunction(async () => {
 | |
|     Services.prefs.clearUserPref("network.auth.force-generic-ntlm");
 | |
|     Services.prefs.clearUserPref("network.auth.force-generic-ntlm-v1");
 | |
|     Services.prefs.clearUserPref("network.dns.native-is-localhost");
 | |
|     Services.prefs.clearUserPref("network.http.sanitize-headers-in-logs");
 | |
| 
 | |
|     await httpserv.stop();
 | |
|   });
 | |
| });
 | |
| 
 | |
| add_task(async function test_ntlm_first() {
 | |
|   Services.prefs.setBoolPref(
 | |
|     "network.auth.choose_most_secure_challenge",
 | |
|     false
 | |
|   );
 | |
|   challenges = ["NTLM", `Basic realm="secret"`, digestChallenge];
 | |
|   httpserv.identity.add("http", "ntlm.com", httpserv.identity.primaryPort);
 | |
|   let chan = makeChan(URL("ntlm.com", "/path"));
 | |
| 
 | |
|   chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
 | |
|   let [req, buf] = await new Promise(resolve => {
 | |
|     chan.asyncOpen(
 | |
|       new ChannelListener((req, buf) => resolve([req, buf]), null)
 | |
|     );
 | |
|   });
 | |
|   Assert.equal(buf, "OK");
 | |
|   Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
 | |
| });
 | |
| 
 | |
| add_task(async function test_basic_first() {
 | |
|   Services.prefs.setBoolPref(
 | |
|     "network.auth.choose_most_secure_challenge",
 | |
|     false
 | |
|   );
 | |
|   challenges = [`Basic realm="secret"`, "NTLM", digestChallenge];
 | |
|   httpserv.identity.add("http", "basic.com", httpserv.identity.primaryPort);
 | |
|   let chan = makeChan(URL("basic.com", "/path"));
 | |
| 
 | |
|   chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
 | |
|   let [req, buf] = await new Promise(resolve => {
 | |
|     chan.asyncOpen(
 | |
|       new ChannelListener((req, buf) => resolve([req, buf]), null)
 | |
|     );
 | |
|   });
 | |
|   Assert.equal(buf, "success");
 | |
|   Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
 | |
| });
 | |
| 
 | |
| add_task(async function test_digest_first() {
 | |
|   Services.prefs.setBoolPref(
 | |
|     "network.auth.choose_most_secure_challenge",
 | |
|     false
 | |
|   );
 | |
|   challenges = [digestChallenge, `Basic realm="secret"`, "NTLM"];
 | |
|   httpserv.identity.add("http", "digest.com", httpserv.identity.primaryPort);
 | |
|   let chan = makeChan(URL("digest.com", "/path"));
 | |
| 
 | |
|   chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
 | |
|   let [req, buf] = await new Promise(resolve => {
 | |
|     chan.asyncOpen(
 | |
|       new ChannelListener((req, buf) => resolve([req, buf]), null)
 | |
|     );
 | |
|   });
 | |
|   Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
 | |
|   Assert.equal(buf, "digest");
 | |
| });
 | |
| 
 | |
| add_task(async function test_choose_most_secure() {
 | |
|   // When the pref is true, we rank the challenges by how secure they are.
 | |
|   // In this case, NTLM should be the most secure.
 | |
|   Services.prefs.setBoolPref("network.auth.choose_most_secure_challenge", true);
 | |
|   challenges = [digestChallenge, `Basic realm="secret"`, "NTLM"];
 | |
|   httpserv.identity.add(
 | |
|     "http",
 | |
|     "ntlmstrong.com",
 | |
|     httpserv.identity.primaryPort
 | |
|   );
 | |
|   let chan = makeChan(URL("ntlmstrong.com", "/path"));
 | |
| 
 | |
|   chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
 | |
|   let [req, buf] = await new Promise(resolve => {
 | |
|     chan.asyncOpen(
 | |
|       new ChannelListener((req, buf) => resolve([req, buf]), null)
 | |
|     );
 | |
|   });
 | |
|   Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
 | |
|   Assert.equal(buf, "OK");
 | |
| });
 | 
