Fix bug 616264. r=ehsan,sdwilsh, a=drivers
authorDan Witte <dwitte@gmail.com>
Tue, 29 Mar 2011 15:19:19 -0400
changeset 27383 2b48d65b67c97682eec68b4f80e5ec78a5fe619a
parent 27382 fbb2dfda1784cbce2f2e0a078286f9869e94af5c
child 27384 4d8083b91ac8bf5e3dd39322a51177e4a148deba
push id2705
push usereakhgari@mozilla.com
push dateThu, 31 Mar 2011 22:34:09 +0000
reviewersehsan, sdwilsh, drivers
bugs616264
milestone1.9.1.19pre
Fix bug 616264. r=ehsan,sdwilsh, a=drivers
extensions/cookie/test/unit/test_bug526789.js
netwerk/cookie/src/nsCookieService.cpp
netwerk/cookie/src/nsCookieService.h
netwerk/dns/src/nsEffectiveTLDService.cpp
--- a/extensions/cookie/test/unit/test_bug526789.js
+++ b/extensions/cookie/test/unit/test_bug526789.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 
 function do_check_throws(f, result, stack)
 {
   if (!stack)
     stack = Components.stack.caller;
@@ -16,20 +19,36 @@ function do_check_throws(f, result, stac
   }
   do_throw("expected result " + result + ", none thrown", stack);
 }
 
 function run_test() {
   var cs = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService);
   var cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager2);
   var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+  var expiry = (Date.now() + 1000) * 1000;
 
   cm.removeAll();
 
-  // test that an empty or '.' http:// host results in a no-op
+  // Test that 'baz.com' and 'baz.com.' are treated differently
+  cm.add("baz.com", "/", "foo", "bar", false, false, true, expiry);
+  do_check_eq(cm.countCookiesFromHost("baz.com"), 1);
+  do_check_eq(cm.countCookiesFromHost("baz.com."), 0);
+  cm.remove("baz.com", "foo", "/", false);
+  do_check_eq(cm.countCookiesFromHost("baz.com"), 0);
+
+  cm.add("baz.com.", "/", "foo", "bar", false, false, true, expiry);
+  do_check_eq(cm.countCookiesFromHost("baz.com"), 0);
+  do_check_eq(cm.countCookiesFromHost("baz.com."), 1);
+  cm.remove("baz.com", "foo", "/", false);
+  do_check_eq(cm.countCookiesFromHost("baz.com."), 1);
+  cm.remove("baz.com.", "foo", "/", false);
+  do_check_eq(cm.countCookiesFromHost("baz.com."), 0);
+
+  // Test that setting an empty or '.' http:// host results in a no-op
   var uri = ios.newURI("http://baz.com/", null, null);
   var emptyuri = ios.newURI("http:///", null, null);
   var doturi = ios.newURI("http://./", null, null);
   do_check_eq(uri.asciiHost, "baz.com");
   do_check_eq(emptyuri.asciiHost, "");
   do_check_eq(doturi.asciiHost, ".");
   cs.setCookieString(emptyuri, null, "foo2=bar", null);
   do_check_eq(getCookieCount(), 0);
@@ -39,78 +58,132 @@ function run_test() {
   do_check_eq(getCookieCount(), 1);
 
   do_check_eq(cs.getCookieString(uri, null), "foo=bar");
   do_check_eq(cs.getCookieString(emptyuri, null), null);
   do_check_eq(cs.getCookieString(doturi, null), null);
 
   do_check_eq(cm.countCookiesFromHost("baz.com"), 1);
   do_check_eq(cm.countCookiesFromHost(""), 0);
-  do_check_eq(cm.countCookiesFromHost("."), 0);
+  do_check_throws(function() {
+    cm.countCookiesFromHost(".");
+  }, Cr.NS_ERROR_ILLEGAL_VALUE);
 
   cm.removeAll();
 
-  // test that an empty file:// host works
-  var emptyuri = ios.newURI("file:///", null, null);
+  // Test that an empty file:// host works
+  emptyuri = ios.newURI("file:///", null, null);
   do_check_eq(emptyuri.asciiHost, "");
   do_check_eq(ios.newURI("file://./", null, null).asciiHost, "");
   do_check_eq(ios.newURI("file://foo.bar/", null, null).asciiHost, "");
   cs.setCookieString(emptyuri, null, "foo2=bar", null);
   do_check_eq(getCookieCount(), 1);
   cs.setCookieString(emptyuri, null, "foo3=bar; domain=", null);
   do_check_eq(getCookieCount(), 2);
   cs.setCookieString(emptyuri, null, "foo4=bar; domain=.", null);
   do_check_eq(getCookieCount(), 2);
   cs.setCookieString(emptyuri, null, "foo5=bar; domain=bar.com", null);
   do_check_eq(getCookieCount(), 2);
 
   do_check_eq(cs.getCookieString(emptyuri, null), "foo2=bar; foo3=bar");
 
   do_check_eq(cm.countCookiesFromHost("baz.com"), 0);
   do_check_eq(cm.countCookiesFromHost(""), 2);
-  do_check_eq(cm.countCookiesFromHost("."), 0);
 
   cm.removeAll();
 
-  // test that an empty host to add() or remove() works,
-  // but a host of '.' or ending with a '.' doesn't
-  var expiry = (Date.now() + 1000) * 1000;
+  // Test that an empty host to add() or remove() works,
+  // but a host of '.' doesn't
   cm.add("", "/", "foo2", "bar", false, false, true, expiry);
   do_check_eq(getCookieCount(), 1);
   do_check_throws(function() {
     cm.add(".", "/", "foo3", "bar", false, false, true, expiry);
   }, Cr.NS_ERROR_ILLEGAL_VALUE);
   do_check_eq(getCookieCount(), 1);
-  cm.add("test.com", "/", "foo", "bar", false, false, true, expiry);
-  do_check_eq(getCookieCount(), 2);
-  do_check_throws(function() {
-    cm.add("test.com.", "/", "foo4", "bar", false, false, true, expiry);
-  }, Cr.NS_ERROR_ILLEGAL_VALUE);
-  do_check_eq(getCookieCount(), 2);
 
   cm.remove("", "foo2", "/", false);
-  do_check_eq(getCookieCount(), 1);
+  do_check_eq(getCookieCount(), 0);
   do_check_throws(function() {
     cm.remove(".", "foo3", "/", false);
   }, Cr.NS_ERROR_ILLEGAL_VALUE);
-  do_check_eq(getCookieCount(), 1);
-  do_check_throws(function() {
-    cm.remove("test.com.", "foo4", "/", false);
-  }, Cr.NS_ERROR_ILLEGAL_VALUE);
-  do_check_eq(getCookieCount(), 1);
-  cm.remove("test.com", "foo", "/", false);
-  do_check_eq(getCookieCount(), 0);
+
+  // Test that the 'domain' attribute accepts a leading dot for IP addresses,
+  // aliases such as 'localhost', and eTLD's such as 'co.uk'; but that the
+  // resulting cookie is for the exact host only.
+  testDomainCookie("http://192.168.0.1/", "192.168.0.1");
+  testDomainCookie("http://localhost/", "localhost");
+  testDomainCookie("http://co.uk/", "co.uk");
+
+  // Test that trailing dots are treated differently for purposes of the
+  // 'domain' attribute when using setCookieString.
+  testTrailingDotCookie("http://192.168.0.1", "192.168.0.1");
+  testTrailingDotCookie("http://localhost", "localhost");
+  testTrailingDotCookie("http://foo.com", "foo.com");
 
   cm.removeAll();
 }
 
 function getCookieCount() {
   var count = 0;
   var cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager2);
   var enumerator = cm.enumerator;
   while (enumerator.hasMoreElements()) {
     if (!(enumerator.getNext() instanceof Ci.nsICookie2))
       throw new Error("not a cookie");
     ++count;
   }
   return count;
 }
 
+function testDomainCookie(uriString, domain) {
+  var cs = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService);
+  var cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager2);
+  var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+  cm.removeAll();
+
+  var uri = ios.newURI(uriString, null, null);
+  cs.setCookieString(uri, null, "foo=bar; domain=" + domain, null);
+  do_check_true(cookieExistsForHost(domain));
+  do_check_false(cookieExistsForHost("." + domain));
+  cm.removeAll();
+
+  cs.setCookieString(uri, null, "foo=bar; domain=." + domain, null);
+  do_check_true(cookieExistsForHost(domain));
+  do_check_false(cookieExistsForHost("." + domain));
+  cm.removeAll();
+}
+
+function testTrailingDotCookie(uriString, domain) {
+  var cs = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService);
+  var cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager2);
+  var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+  cm.removeAll();
+
+  var uri = ios.newURI(uriString, null, null);
+  cs.setCookieString(uri, null, "foo=bar; domain=" + domain + ".", null);
+  do_check_eq(cm.countCookiesFromHost(domain), 0);
+  do_check_eq(cm.countCookiesFromHost(domain + "."), 0);
+  cm.removeAll();
+
+  uri = ios.newURI(uriString + ".", null, null);
+  cs.setCookieString(uri, null, "foo=bar; domain=" + domain, null);
+  do_check_eq(cm.countCookiesFromHost(domain), 0);
+  do_check_eq(cm.countCookiesFromHost(domain + "."), 0);
+  cm.removeAll();
+}
+
+// Get a single cookie with a host exactly equal to 'domain'.
+function cookieExistsForHost(domain) {
+  var result = false;
+  var cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager2);
+  var enumerator = cm.enumerator;
+  while (enumerator.hasMoreElements()) {
+    var cookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
+    if (cookie.host == domain) {
+      do_check_false(result);
+      result = true;
+    }
+  }
+  return result;
+}
+
--- a/netwerk/cookie/src/nsCookieService.cpp
+++ b/netwerk/cookie/src/nsCookieService.cpp
@@ -948,20 +948,20 @@ nsCookieService::Add(const nsACString &a
                      const nsACString &aPath,
                      const nsACString &aName,
                      const nsACString &aValue,
                      PRBool            aIsSecure,
                      PRBool            aIsHttpOnly,
                      PRBool            aIsSession,
                      PRInt64           aExpiry)
 {
-  // empty domains are acceptable (e.g. file:// URI's), but domains containing
-  // a trailing '.' will break our domainwalking code.
-  NS_ENSURE_TRUE(aDomain.IsEmpty() || aDomain.Last() != '.',
-                 NS_ERROR_INVALID_ARG);
+  // empty domains are acceptable (e.g. file:// URI's), but we reject the host
+  // '.'. 
+  NS_ENSURE_TRUE(aDomain.IsEmpty() ||
+    !(aDomain.Length() == 1 && aDomain.Last() == '.'), NS_ERROR_INVALID_ARG);
 
   PRInt64 currentTimeInUsec = PR_Now();
 
   nsRefPtr<nsCookie> cookie =
     nsCookie::Create(aName, aValue, aDomain, aPath,
                      aExpiry,
                      currentTimeInUsec,
                      currentTimeInUsec,
@@ -977,20 +977,20 @@ nsCookieService::Add(const nsACString &a
 }
 
 NS_IMETHODIMP
 nsCookieService::Remove(const nsACString &aHost,
                         const nsACString &aName,
                         const nsACString &aPath,
                         PRBool           aBlocked)
 {
-  // empty domains are acceptable (e.g. file:// URI's), but domains containing
-  // a trailing '.' will break our domainwalking code.
-  NS_ENSURE_TRUE(aHost.IsEmpty() || aHost.Last() != '.',
-                 NS_ERROR_INVALID_ARG);
+  // empty domains are acceptable (e.g. file:// URI's), but we reject the host
+  // '.'.
+  NS_ENSURE_TRUE(aHost.IsEmpty() ||
+    !(aHost.Length() == 1 && aHost.Last() == '.'), NS_ERROR_INVALID_ARG);
 
   nsListIter matchIter;
   if (FindCookie(PromiseFlatCString(aHost),
                  PromiseFlatCString(aName),
                  PromiseFlatCString(aPath),
                  matchIter,
                  PR_Now() / PR_USEC_PER_SEC)) {
     nsRefPtr<nsCookie> cookie = matchIter.current;
@@ -1260,29 +1260,31 @@ nsCookieService::GetCookieInternal(nsIUR
   // note: there was a "check if host has embedded whitespace" here.
   // it was removed since this check was added into the nsIURI impl (bug 146094).
   nsCAutoString hostFromURI, pathFromURI;
   if (NS_FAILED(aHostURI->GetAsciiHost(hostFromURI)) ||
       NS_FAILED(aHostURI->GetPath(pathFromURI))) {
     COOKIE_LOGFAILURE(GET_COOKIE, aHostURI, nsnull, "couldn't get host/path from URI");
     return;
   }
-  // trim trailing dots
-  hostFromURI.Trim(".");
 
   // block any URIs without a host that aren't file:// URIs
   if (hostFromURI.IsEmpty()) {
     PRBool isFileURI = PR_FALSE;
     aHostURI->SchemeIs("file", &isFileURI);
     if (!isFileURI) {
       COOKIE_LOGFAILURE(GET_COOKIE, aHostURI, nsnull, "host is empty");
       return;
     }
   }
 
+  // aHostURI may be the string '.'. If so, fail.
+  if (hostFromURI.Length() == 1 && hostFromURI.Last() == '.')
+    return;
+
   // insert a leading dot, so we begin the hash lookup with the
   // equivalent domain cookie host
   hostFromURI.Insert(NS_LITERAL_CSTRING("."), 0);
 
   // check if aHostURI is using an https secure protocol.
   // if it isn't, then we can't send a secure cookie over the connection.
   // if SchemeIs fails, assume an insecure connection, to be on the safe side
   PRBool isSecure;
@@ -1345,17 +1347,17 @@ nsCookieService::GetCookieInternal(nsIUR
       }
 
       // all checks passed - add to list and check if lastAccessed stamp needs updating
       foundCookieList.AppendElement(cookie);
       if (currentTimeInUsec - cookie->LastAccessed() > kCookieStaleThreshold)
         stale = PR_TRUE;
     }
 
-    if (!nextDot)
+    if (!nextDot || *(nextDot + 1) == '.')
       break;
 
     currentDot = nextDot;
     nextDot = *currentDot ? strchr(currentDot + 1, '.') : nsnull;
   } while (1);
 
   PRInt32 count = foundCookieList.Count();
   if (count == 0)
@@ -1859,19 +1861,16 @@ nsCookieService::IsForeign(nsIURI *aHost
 {
   // Get hosts
   nsCAutoString currentHost, firstHost;
   if (NS_FAILED(aHostURI->GetAsciiHost(currentHost)) ||
       NS_FAILED(aFirstURI->GetAsciiHost(firstHost))) {
     // assume foreign
     return PR_TRUE;
   }
-  // trim trailing dots
-  currentHost.Trim(".");
-  firstHost.Trim(".");
 
   // fast path: check if the two hosts are identical.
   // this also covers two special cases:
   // 1) if we're dealing with IP addresses, require an exact match. this
   // eliminates any chance of IP address funkiness (e.g. the alias 127.1
   // domain-matching 99.54.127.1). bug 105917 originally noted the requirement
   // to deal with IP addresses. note that GetBaseDomain() below will return an
   // error if the URI is an IP address.
@@ -1885,17 +1884,16 @@ nsCookieService::IsForeign(nsIURI *aHost
   // get the base domain for the originating URI.
   // e.g. for "images.bbc.co.uk", this would be "bbc.co.uk".
   nsCAutoString baseDomain;
   nsresult rv = mTLDService->GetBaseDomain(aFirstURI, 0, baseDomain);
   if (NS_FAILED(rv)) {
     // URI is an IP, eTLD, or something else went wrong - assume foreign
     return PR_TRUE;
   }  
-  baseDomain.Trim(".");
 
   // ensure the host domain is derived from the base domain.
   // we prepend dots before the comparison to ensure e.g.
   // "mybbc.co.uk" isn't matched as a superset of "bbc.co.uk".
   currentHost.Insert(NS_LITERAL_CSTRING("."), 0);
   baseDomain.Insert(NS_LITERAL_CSTRING("."), 0);
   return !StringEndsWith(currentHost, baseDomain);
 }
@@ -1966,46 +1964,53 @@ nsCookieService::CheckDomain(nsCookieAtt
 {
   nsresult rv;
 
   // get host from aHostURI
   nsCAutoString hostFromURI;
   if (NS_FAILED(aHostURI->GetAsciiHost(hostFromURI))) {
     return PR_FALSE;
   }
-  // trim trailing dots
-  hostFromURI.Trim(".");
 
   // block any URIs without a host that aren't file:// URIs
   if (hostFromURI.IsEmpty()) {
     PRBool isFileURI = PR_FALSE;
     aHostURI->SchemeIs("file", &isFileURI);
     if (!isFileURI)
       return PR_FALSE;
   }
 
+  // aHostURI may be the string '.'. If so, fail.
+  if (hostFromURI.Length() == 1 && hostFromURI.Last() == '.')
+    return PR_FALSE;
+
   // if a domain is given, check the host has permission
   if (!aCookieAttributes.host.IsEmpty()) {
-    aCookieAttributes.host.Trim(".");
+    // Tolerate leading '.' characters, but not if it's otherwise an empty host.
+    if (aCookieAttributes.host.Length() > 1 &&
+        aCookieAttributes.host.First() == '.') {
+      aCookieAttributes.host.Cut(0, 1);
+    }
+
     // switch to lowercase now, to avoid case-insensitive compares everywhere
     ToLowerCase(aCookieAttributes.host);
 
     // get the base domain for the host URI.
     // e.g. for "images.bbc.co.uk", this would be "bbc.co.uk", which
     // represents the lowest level domain a cookie can be set for.
     nsCAutoString baseDomain;
     rv = mTLDService->GetBaseDomain(aHostURI, 0, baseDomain);
-    baseDomain.Trim(".");
     if (NS_FAILED(rv)) {
       // check whether the host is an IP address, and leave the cookie as
       // a non-domain one. this will require an exact host match for the cookie,
       // so we eliminate any chance of IP address funkiness (e.g. the alias 127.1
       // domain-matching 99.54.127.1). bug 105917 originally noted the
       // requirement to deal with IP addresses.
-      if (rv == NS_ERROR_HOST_IS_IP_ADDRESS)
+      if (rv == NS_ERROR_HOST_IS_IP_ADDRESS ||
+          rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS)
         return hostFromURI.Equals(aCookieAttributes.host);
 
       return PR_FALSE;
     }
 
     // ensure the proposed domain is derived from the base domain; and also
     // that the host domain is derived from the proposed domain (per RFC2109).
     // we prepend a dot before the comparison to ensure e.g.
@@ -2196,16 +2201,21 @@ nsCookieService::CookieExists(nsICookie2
 }
 
 // count the number of cookies from a given host, and simultaneously find the
 // oldest cookie from the host.
 PRUint32
 nsCookieService::CountCookiesFromHostInternal(const nsACString  &aHost,
                                               nsEnumerationData &aData)
 {
+  // empty domains are acceptable (e.g. file:// URI's), but we reject the host
+  // '.'.
+  NS_ASSERTION(aHost.IsEmpty() ||
+    !(aHost.Length() == 1 && aHost.Last() == '.'), "invalid host");
+
   PRUint32 countFromHost = 0;
   nsCAutoString hostWithDot(NS_LITERAL_CSTRING(".") + aHost);
 
   const char *currentDot = hostWithDot.get();
   const char *nextDot = currentDot + 1;
   do {
     nsCookieEntry *entry = mDBState->hostTable.GetEntry(currentDot);
     for (nsListIter iter(entry); iter.current; ++iter) {
@@ -2216,32 +2226,37 @@ nsCookieService::CountCookiesFromHostInt
         // check if we've found the oldest cookie so far
         if (aData.oldestTime > iter.current->LastAccessed()) {
           aData.oldestTime = iter.current->LastAccessed();
           aData.iter = iter;
         }
       }
     }
 
-    if (!nextDot)
+    if (!nextDot || *(nextDot + 1) == '.')
       break;
 
     currentDot = nextDot;
     nextDot = *currentDot ? strchr(currentDot + 1, '.') : nsnull;
   } while (1);
 
   return countFromHost;
 }
 
 // count the number of cookies stored by a particular host. this is provided by the
 // nsICookieManager2 interface.
 NS_IMETHODIMP
 nsCookieService::CountCookiesFromHost(const nsACString &aHost,
                                       PRUint32         *aCountFromHost)
 {
+  // empty domains are acceptable (e.g. file:// URI's), but we reject the host
+  // '.'.
+  NS_ENSURE_TRUE(aHost.IsEmpty() ||
+    !(aHost.Length() == 1 && aHost.Last() == '.'), NS_ERROR_INVALID_ARG);
+
   // we don't care about finding the oldest cookie here, so disable the search
   nsEnumerationData data(PR_Now() / PR_USEC_PER_SEC, LL_MININT);
   *aCountFromHost = CountCookiesFromHostInternal(aHost, data);
   return NS_OK;
 }
 
 // find an exact cookie specified by host, name, and path that hasn't expired.
 PRBool
--- a/netwerk/cookie/src/nsCookieService.h
+++ b/netwerk/cookie/src/nsCookieService.h
@@ -187,16 +187,17 @@ class nsCookieService : public nsICookie
     nsresult                      SetCookieStringInternal(nsIURI *aHostURI, nsIPrompt *aPrompt, const char *aCookieHeader, const char *aServerTime, nsIChannel *aChannel, PRBool aFromHttp);
     PRBool                        SetCookieInternal(nsIURI *aHostURI, nsIChannel *aChannel, nsDependentCString &aCookieHeader, PRInt64 aServerTime, PRBool aFromHttp);
     void                          AddInternal(nsCookie *aCookie, PRInt64 aCurrentTime, nsIURI *aHostURI, const char *aCookieHeader, PRBool aFromHttp);
     void                          RemoveCookieFromList(nsListIter &aIter);
     PRBool                        AddCookieToList(nsCookie *aCookie, PRBool aWriteToDB = PR_TRUE);
     void                          UpdateCookieInList(nsCookie *aCookie, PRInt64 aLastAccessed);
     static PRBool                 GetTokenValue(nsASingleFragmentCString::const_char_iterator &aIter, nsASingleFragmentCString::const_char_iterator &aEndIter, nsDependentCSubstring &aTokenString, nsDependentCSubstring &aTokenValue, PRBool &aEqualsFound);
     static PRBool                 ParseAttributes(nsDependentCString &aCookieHeader, nsCookieAttributes &aCookie);
+    nsresult                      NormalizeHost(nsCString &aHost);
     PRBool                        IsForeign(nsIURI *aHostURI, nsIURI *aFirstURI);
     PRUint32                      CheckPrefs(nsIURI *aHostURI, nsIChannel *aChannel, const char *aCookieHeader);
     PRBool                        CheckDomain(nsCookieAttributes &aCookie, nsIURI *aHostURI);
     static PRBool                 CheckPath(nsCookieAttributes &aCookie, nsIURI *aHostURI);
     static PRBool                 GetExpiry(nsCookieAttributes &aCookie, PRInt64 aServerTime, PRInt64 aCurrentTime);
     void                          RemoveAllFromMemory();
     void                          RemoveExpiredCookies(PRInt64 aCurrentTime);
     PRBool                        FindCookie(const nsAFlatCString &aHost, const nsAFlatCString &aName, const nsAFlatCString &aPath, nsListIter &aIter, PRInt64 aCurrentTime);
--- a/netwerk/dns/src/nsEffectiveTLDService.cpp
+++ b/netwerk/dns/src/nsEffectiveTLDService.cpp
@@ -164,23 +164,28 @@ nsEffectiveTLDService::GetBaseDomainFrom
 // includes characters that are not valid in a URL. Normalization is performed
 // on the host string and the result will be in UTF8.
 nsresult
 nsEffectiveTLDService::GetBaseDomainInternal(nsCString  &aHostname,
                                              PRUint32    aAdditionalParts,
                                              nsACString &aBaseDomain)
 {
   if (aHostname.IsEmpty())
-    return NS_ERROR_INVALID_ARG;
+    return NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS;
 
   // chomp any trailing dot, and keep track of it for later
   PRBool trailingDot = aHostname.Last() == '.';
   if (trailingDot)
     aHostname.Truncate(aHostname.Length() - 1);
 
+  // check the edge cases of the host being '.' or having a second trailing '.',
+  // since subsequent checks won't catch it.
+  if (aHostname.IsEmpty() || aHostname.Last() == '.')
+    return NS_ERROR_INVALID_ARG;
+
   // Check if we're dealing with an IPv4/IPv6 hostname, and return
   PRNetAddr addr;
   PRStatus result = PR_StringToNetAddr(aHostname.get(), &addr);
   if (result == PR_SUCCESS)
     return NS_ERROR_HOST_IS_IP_ADDRESS;
 
   // Walk up the domain tree, most specific to least specific,
   // looking for matches at each level.  Note that a given level may