Bug 1268975 - Port Bug 1255570 to SeaMonkey r=IanN a=IanN comm-aurora CLOSED TREE
authorPhilip Chee <philip.chee@gmail.com>
Sat, 30 Apr 2016 23:34:33 +0800
changeset 22008 89f9c42ded4b203968195b43f3059757a7e6b6d9
parent 22007 c1bf457da82c5fad5936c007be9a73f94d461d18
child 22009 b120a5b361b84a369935dc9da94c2c1a2bd7d2b1
push id1745
push userclokep@gmail.com
push dateMon, 06 Jun 2016 19:53:03 +0000
treeherdercomm-aurora@3ad3af1478aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersIanN, IanN
bugs1268975, 1255570
Bug 1268975 - Port Bug 1255570 to SeaMonkey r=IanN a=IanN comm-aurora CLOSED TREE
suite/browser/navigator.js
suite/browser/test/browser/authenticate.sjs
suite/browser/test/browser/browser.ini
suite/browser/test/browser/browser_urlbarCopying.js
--- a/suite/browser/navigator.js
+++ b/suite/browser/navigator.js
@@ -2059,28 +2059,40 @@ function URLBarSetURI(aURI, aValid) {
   // become set because of oninput, so reset it to null.
   getBrowser().userTypedValue = null;
 
   SetPageProxyState((value && (!aURI || aValid)) ? "valid" : "invalid", uri);
 }
 
 function losslessDecodeURI(aURI) {
   var value = aURI.spec;
+  var scheme = aURI.scheme;
+
+  var decodeASCIIOnly = !["https", "http", "file", "ftp"].includes(scheme);
   // Try to decode as UTF-8 if there's no encoding sequence that we would break.
-  if (!/%25(?:3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/i.test(value))
-    try {
-      value = decodeURI(value)
-                // decodeURI decodes %25 to %, which creates unintended
-                // encoding sequences. Re-encode it, unless it's part of
-                // a sequence that survived decodeURI, i.e. one for:
-                // ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '#'
-                // (RFC 3987 section 3.2)
-                .replace(/%(?!3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/ig,
-                         encodeURIComponent);
-    } catch (e) {}
+  if (!/%25(?:3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/i.test(value)) {
+    if (decodeASCIIOnly) {
+      // This only decodes ascii characters (hex) 20-7e, except 25 (%).
+      // This avoids both cases stipulated below (%-related issues, and \r, \n
+      // and \t, which would be %0d, %0a and %09, respectively) as well as any
+      // non-US-ascii characters.
+      value = value.replace(/%(2[0-4]|2[6-9a-f]|[3-6][0-9a-f]|7[0-9a-e])/g, decodeURI);
+    } else {
+      try {
+        value = decodeURI(value)
+                  // decodeURI decodes %25 to %, which creates unintended
+                  // encoding sequences. Re-encode it, unless it's part of
+                  // a sequence that survived decodeURI, i.e. one for:
+                  // ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '#'
+                  // (RFC 3987 section 3.2)
+                  .replace(/%(?!3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/ig,
+                           encodeURIComponent);
+      } catch (e) {}
+    }
+  }
 
   // Encode invisible characters (soft hyphen, zero-width space, BOM,
   // line and paragraph separator, word joiner, invisible times,
   // invisible separator, object replacement character,
   // C0/C1 controls). (bug 452979, bug 909264)
   // Encode bidirectional formatting characters.
   // (RFC 3987 sections 3.2 and 4.1 paragraph 6)
   // Re-encode whitespace so that it doesn't get eaten away
new file mode 100644
--- /dev/null
+++ b/suite/browser/test/browser/authenticate.sjs
@@ -0,0 +1,220 @@
+function handleRequest(request, response)
+{
+  try {
+    reallyHandleRequest(request, response);
+  } catch (e) {
+    response.setStatusLine("1.0", 200, "AlmostOK");
+    response.write("Error handling request: " + e);
+  }
+}
+
+
+function reallyHandleRequest(request, response) {
+  var match;
+  var requestAuth = true, requestProxyAuth = true;
+
+  // Allow the caller to drive how authentication is processed via the query.
+  // Eg, http://localhost:8888/authenticate.sjs?user=foo&realm=bar
+  // The extra ? allows the user/pass/realm checks to succeed if the name is
+  // at the beginning of the query string.
+  var query = "?" + request.queryString;
+
+  var expected_user = "", expected_pass = "", realm = "mochitest";
+  var proxy_expected_user = "", proxy_expected_pass = "", proxy_realm = "mochi-proxy";
+  var huge = false, plugin = false, anonymous = false;
+  var authHeaderCount = 1;
+  // user=xxx
+  match = /[^_]user=([^&]*)/.exec(query);
+  if (match)
+    expected_user = match[1];
+
+  // pass=xxx
+  match = /[^_]pass=([^&]*)/.exec(query);
+  if (match)
+    expected_pass = match[1];
+
+  // realm=xxx
+  match = /[^_]realm=([^&]*)/.exec(query);
+  if (match)
+    realm = match[1];
+
+  // proxy_user=xxx
+  match = /proxy_user=([^&]*)/.exec(query);
+  if (match)
+    proxy_expected_user = match[1];
+
+  // proxy_pass=xxx
+  match = /proxy_pass=([^&]*)/.exec(query);
+  if (match)
+    proxy_expected_pass = match[1];
+
+  // proxy_realm=xxx
+  match = /proxy_realm=([^&]*)/.exec(query);
+  if (match)
+    proxy_realm = match[1];
+
+  // huge=1
+  match = /huge=1/.exec(query);
+  if (match)
+    huge = true;
+
+  // plugin=1
+  match = /plugin=1/.exec(query);
+  if (match)
+    plugin = true;
+
+  // multiple=1
+  match = /multiple=([^&]*)/.exec(query);
+  if (match)
+    authHeaderCount = match[1]+0;
+
+  // anonymous=1
+  match = /anonymous=1/.exec(query);
+  if (match)
+    anonymous = true;
+
+  // Look for an authentication header, if any, in the request.
+  //
+  // EG: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
+  // 
+  // This test only supports Basic auth. The value sent by the client is
+  // "username:password", obscured with base64 encoding.
+
+  var actual_user = "", actual_pass = "", authHeader, authPresent = false;
+  if (request.hasHeader("Authorization")) {
+    authPresent = true;
+    authHeader = request.getHeader("Authorization");
+    match = /Basic (.+)/.exec(authHeader);
+    if (match.length != 2)
+        throw "Couldn't parse auth header: " + authHeader;
+
+    var userpass = base64ToString(match[1]); // no atob() :-(
+    match = /(.*):(.*)/.exec(userpass);
+    if (match.length != 3)
+        throw "Couldn't decode auth header: " + userpass;
+    actual_user = match[1];
+    actual_pass = match[2];
+  } 
+
+  var proxy_actual_user = "", proxy_actual_pass = "";
+  if (request.hasHeader("Proxy-Authorization")) {
+    authHeader = request.getHeader("Proxy-Authorization");
+    match = /Basic (.+)/.exec(authHeader);
+    if (match.length != 2)
+        throw "Couldn't parse auth header: " + authHeader;
+
+    var userpass = base64ToString(match[1]); // no atob() :-(
+    match = /(.*):(.*)/.exec(userpass);
+    if (match.length != 3)
+        throw "Couldn't decode auth header: " + userpass;
+    proxy_actual_user = match[1];
+    proxy_actual_pass = match[2];
+  }
+
+  // Don't request authentication if the credentials we got were what we
+  // expected.
+  if (expected_user == actual_user &&
+    expected_pass == actual_pass) {
+    requestAuth = false;
+  }
+  if (proxy_expected_user == proxy_actual_user &&
+    proxy_expected_pass == proxy_actual_pass) {
+    requestProxyAuth = false;
+  }
+
+  if (anonymous) {
+    if (authPresent) {
+      response.setStatusLine("1.0", 400, "Unexpected authorization header found");
+    } else {
+      response.setStatusLine("1.0", 200, "Authorization header not found");
+    }
+  } else {
+    if (requestProxyAuth) {
+      response.setStatusLine("1.0", 407, "Proxy authentication required");
+      for (i = 0; i < authHeaderCount; ++i)
+        response.setHeader("Proxy-Authenticate", "basic realm=\"" + proxy_realm + "\"", true);
+    } else if (requestAuth) {
+      response.setStatusLine("1.0", 401, "Authentication required");
+      for (i = 0; i < authHeaderCount; ++i)
+        response.setHeader("WWW-Authenticate", "basic realm=\"" + realm + "\"", true);
+    } else {
+      response.setStatusLine("1.0", 200, "OK");
+    }
+  }
+
+  response.setHeader("Content-Type", "application/xhtml+xml", false);
+  response.write("<html xmlns='http://www.w3.org/1999/xhtml'>");
+  response.write("<p>Login: <span id='ok'>" + (requestAuth ? "FAIL" : "PASS") + "</span></p>\n");
+  response.write("<p>Proxy: <span id='proxy'>" + (requestProxyAuth ? "FAIL" : "PASS") + "</span></p>\n");
+  response.write("<p>Auth: <span id='auth'>" + authHeader + "</span></p>\n");
+  response.write("<p>User: <span id='user'>" + actual_user + "</span></p>\n");
+  response.write("<p>Pass: <span id='pass'>" + actual_pass + "</span></p>\n");
+
+  if (huge) {
+    response.write("<div style='display: none'>");
+    for (i = 0; i < 100000; i++) {
+      response.write("123456789\n");
+    }
+    response.write("</div>");
+    response.write("<span id='footnote'>This is a footnote after the huge content fill</span>");
+  }
+
+  if (plugin) {
+    response.write("<embed id='embedtest' style='width: 400px; height: 100px;' " + 
+           "type='application/x-test'></embed>\n");
+  }
+
+  response.write("</html>");
+}
+
+
+// base64 decoder
+//
+// Yoinked from extensions/xml-rpc/src/nsXmlRpcClient.js because btoa()
+// doesn't seem to exist. :-(
+/* Convert Base64 data to a string */
+const toBinaryTable = [
+    -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+    -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+    -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,
+    52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1,
+    -1, 0, 1, 2,  3, 4, 5, 6,  7, 8, 9,10, 11,12,13,14,
+    15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
+    -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
+    41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
+];
+const base64Pad = '=';
+
+function base64ToString(data) {
+
+    var result = '';
+    var leftbits = 0; // number of bits decoded, but yet to be appended
+    var leftdata = 0; // bits decoded, but yet to be appended
+
+    // Convert one by one.
+    for (var i = 0; i < data.length; i++) {
+        var c = toBinaryTable[data.charCodeAt(i) & 0x7f];
+        var padding = (data[i] == base64Pad);
+        // Skip illegal characters and whitespace
+        if (c == -1) continue;
+        
+        // Collect data into leftdata, update bitcount
+        leftdata = (leftdata << 6) | c;
+        leftbits += 6;
+
+        // If we have 8 or more bits, append 8 bits to the result
+        if (leftbits >= 8) {
+            leftbits -= 8;
+            // Append if not padding.
+            if (!padding)
+                result += String.fromCharCode((leftdata >> leftbits) & 0xff);
+            leftdata &= (1 << leftbits) - 1;
+        }
+    }
+
+    // If there are any bits left, the base64 string was corrupted
+    if (leftbits)
+        throw Components.Exception('Corrupted base64 string');
+
+    return result;
+}
--- a/suite/browser/test/browser/browser.ini
+++ b/suite/browser/test/browser/browser.ini
@@ -50,8 +50,11 @@ support-files =
   plugin_unknown.html
 [browser_pluginplaypreview.js]
 [browser_popupNotification.js]
 [browser_privatebrowsing_protocolhandler.js]
 support-files = browser_privatebrowsing_protocolhandler_page.html
 [browser_relatedTabs.js]
 [browser_scope.js]
 [browser_selectTabAtIndex.js]
+[browser_urlbarCopying.js]
+support-files =
+  authenticate.sjs
new file mode 100644
--- /dev/null
+++ b/suite/browser/test/browser/browser_urlbarCopying.js
@@ -0,0 +1,204 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const trimPref = "browser.urlbar.trimURLs";
+const phishyUserPassPref = "network.http.phishy-userpass-length";
+
+function test() {
+
+  let tab = gBrowser.selectedTab = gBrowser.addTab();
+
+  registerCleanupFunction(function () {
+    gBrowser.removeTab(tab);
+    Services.prefs.clearUserPref(trimPref);
+    Services.prefs.clearUserPref(phishyUserPassPref);
+    URLBarSetURI();
+  });
+
+  Services.prefs.setBoolPref(trimPref, true);
+  Services.prefs.setIntPref(phishyUserPassPref, 32); // avoid prompting about phishing
+
+  waitForExplicitFinish();
+
+  nextTest();
+}
+
+var tests = [
+  // pageproxystate="invalid"
+  {
+    setURL: "http://example.com/",
+    expectedURL: "example.com",
+    copyExpected: "example.com"
+  },
+  {
+    copyVal: "<e>xample.com",
+    copyExpected: "e"
+  },
+
+  // pageproxystate="valid" from this point on (due to the load)
+  {
+    loadURL: "http://example.com/",
+    expectedURL: "example.com",
+    copyExpected: "http://example.com/"
+  },
+  {
+    copyVal: "<example.co>m",
+    copyExpected: "example.co"
+  },
+  {
+    copyVal: "e<x>ample.com",
+    copyExpected: "x"
+  },
+  {
+    copyVal: "<e>xample.com",
+    copyExpected: "e"
+  },
+
+  {
+    loadURL: "http://example.com/foo",
+    expectedURL: "example.com/foo",
+    copyExpected: "http://example.com/foo"
+  },
+  {
+    copyVal: "<example.com>/foo",
+    copyExpected: "http://example.com"
+  },
+  {
+    copyVal: "<example>.com/foo",
+    copyExpected: "example"
+  },
+
+  // Test that userPass is stripped out
+  {
+    loadURL: "http://user:pass@mochi.test:8888/browser/browser/base/content/test/general/authenticate.sjs?user=user&pass=pass",
+    expectedURL: "mochi.test:8888/browser/browser/base/content/test/general/authenticate.sjs?user=user&pass=pass",
+    copyExpected: "http://mochi.test:8888/browser/browser/base/content/test/general/authenticate.sjs?user=user&pass=pass"
+  },
+
+  // Test escaping
+  {
+    loadURL: "http://example.com/()%28%29%C3%A9",
+    expectedURL: "example.com/()()\xe9",
+    copyExpected: "http://example.com/()%28%29%C3%A9"
+  },
+  {
+    copyVal: "<example.com/(>)()\xe9",
+    copyExpected: "http://example.com/("
+  },
+  {
+    copyVal: "e<xample.com/(>)()\xe9",
+    copyExpected: "xample.com/("
+  },
+
+  {
+    loadURL: "http://example.com/%C3%A9%C3%A9",
+    expectedURL: "example.com/\xe9\xe9",
+    copyExpected: "http://example.com/%C3%A9%C3%A9"
+  },
+  {
+    copyVal: "e<xample.com/\xe9>\xe9",
+    copyExpected: "xample.com/\xe9"
+  },
+  {
+    copyVal: "<example.com/\xe9>\xe9",
+    copyExpected: "http://example.com/\xe9"
+  },
+
+  {
+    loadURL: "http://example.com/?%C3%B7%C3%B7",
+    expectedURL: "example.com/?\xf7\xf7",
+    copyExpected: "http://example.com/?%C3%B7%C3%B7"
+  },
+  {
+    copyVal: "e<xample.com/?\xf7>\xf7",
+    copyExpected: "xample.com/?\xf7"
+  },
+  {
+    copyVal: "<example.com/?\xf7>\xf7",
+    copyExpected: "http://example.com/?\xf7"
+  },
+
+  // data: and javsacript: URIs shouldn't be encoded
+  {
+    loadURL: "javascript:('%C3%A9%20%25%50')",
+    expectedURL: "javascript:('%C3%A9 %25P')",
+    copyExpected: "javascript:('%C3%A9 %25P')"
+  },
+  {
+    copyVal: "<javascript:(>'%C3%A9 %25P')",
+    copyExpected: "javascript:("
+  },
+
+  {
+    loadURL: "data:text/html,(%C3%A9%20%25%50)",
+    expectedURL: "data:text/html,(%C3%A9 %25P)",
+    copyExpected: "data:text/html,(%C3%A9 %25P)",
+  },
+  {
+    copyVal: "<data:text/html,(>%C3%A9 %25P)",
+    copyExpected: "data:text/html,("
+  },
+  {
+    copyVal: "<data:text/html,(%C3%A9 %25P>)",
+    copyExpected: "data:text/html,(%C3%A9 %25P",
+  }
+];
+
+function nextTest() {
+  let test = tests.shift();
+  if (tests.length == 0)
+    runTest(test, finish);
+  else
+    runTest(test, nextTest);
+}
+
+function runTest(test, cb) {
+  function doCheck() {
+    if (test.setURL || test.loadURL) {
+      gURLBar.valueIsTyped = !!test.setURL;
+      is(gURLBar.textValue, test.expectedURL, "url bar value set");
+    }
+
+    testCopy(test.copyVal, test.copyExpected, cb);
+  }
+
+  if (test.loadURL) {
+    loadURL(test.loadURL, doCheck);
+  } else {
+    if (test.setURL)
+      gURLBar.value = test.setURL;
+    doCheck();
+  }
+}
+
+function testCopy(copyVal, targetValue, cb) {
+  info("Expecting copy of: " + targetValue);
+  waitForClipboard(targetValue, function () {
+    gURLBar.focus();
+    if (copyVal) {
+      let startBracket = copyVal.indexOf("<");
+      let endBracket = copyVal.indexOf(">");
+      if (startBracket == -1 || endBracket == -1 ||
+          startBracket > endBracket ||
+          copyVal.replace("<", "").replace(">", "") != gURLBar.textValue) {
+        ok(false, "invalid copyVal: " + copyVal);
+      }
+      gURLBar.selectionStart = startBracket;
+      gURLBar.selectionEnd = endBracket - 1;
+    } else {
+      gURLBar.select();
+    }
+
+    goDoCommand("cmd_copy");
+  }, cb, cb);
+}
+
+function loadURL(aURL, aCB) {
+  gBrowser.selectedBrowser.addEventListener("load", function () {
+    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+    is(gBrowser.currentURI.spec, aURL, "loaded expected URL");
+    aCB();
+  }, true);
+
+  gBrowser.loadURI(aURL);
+}