diff --git a/lib/cookie.js b/lib/cookie.js index 2ab6f092..c17471ea 100644 --- a/lib/cookie.js +++ b/lib/cookie.js @@ -31,7 +31,6 @@ 'use strict'; var urlParse = require('url').parse; var util = require('util'); -var ipRegex = require('ip-regex')({ exact: true }); var pubsuffix = require('./pubsuffix-psl'); var Store = require('./store').Store; var MemoryCookieStore = require('./memstore').MemoryCookieStore; @@ -78,6 +77,12 @@ var NUM_TO_DAY = [ var MAX_TIME = 2147483647000; // 31-bit max var MIN_TIME = 0; // 31-bit min +// Dumped from ip-regex@4.0.0, with the following changes: +// * all capturing groups converted to non-capturing -- "(?:)" +// * support for IPv6 Scoped Literal ("%eth1") removed +// * lowercase hexadecimal only +var IP_REGEX_LOWERCASE =/(?:^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$)|(?:^(?:(?:[a-f\d]{1,4}:){7}(?:[a-f\d]{1,4}|:)|(?:[a-f\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|:[a-f\d]{1,4}|:)|(?:[a-f\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-f\d]{1,4}){1,2}|:)|(?:[a-f\d]{1,4}:){4}(?:(?::[a-f\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-f\d]{1,4}){1,3}|:)|(?:[a-f\d]{1,4}:){3}(?:(?::[a-f\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-f\d]{1,4}){1,4}|:)|(?:[a-f\d]{1,4}:){2}(?:(?::[a-f\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-f\d]{1,4}){1,5}|:)|(?:[a-f\d]{1,4}:){1}(?:(?::[a-f\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-f\d]{1,4}){1,6}|:)|(?::(?:(?::[a-f\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-f\d]{1,4}){1,7}|:)))$)/; + /* * Parses a Natural number (i.e., non-negative integer) with either the * *DIGIT ( non-digit *OCTET ) @@ -309,7 +314,11 @@ function domainMatch(str, domStr, canonicalize) { } /* - * "The domain string and the string are identical. (Note that both the + * S5.1.3: + * "A string domain-matches a given domain string if at least one of the + * following conditions hold:" + * + * " o The domain string and the string are identical. (Note that both the * domain string and the string will have been canonicalized to lower case at * this point)" */ @@ -317,29 +326,31 @@ function domainMatch(str, domStr, canonicalize) { return true; } - /* "All of the following [three] conditions hold:" (order adjusted from the RFC) */ - - /* "* The string is a host name (i.e., not an IP address)." */ - if (ipRegex.test(str)) { - return false; - } + /* " o All of the following [three] conditions hold:" */ - /* "* The domain string is a suffix of the string" */ + /* " * The domain string is a suffix of the string" */ + // first, check if substring: var idx = str.indexOf(domStr); if (idx <= 0) { return false; // it's a non-match (-1) or prefix (0) } - // e.g "a.b.c".indexOf("b.c") === 2 + // next, check it's a proper suffix + // e.g., "a.b.c".indexOf("b.c") === 2 // 5 === 3+2 - if (str.length !== domStr.length + idx) { // it's not a suffix - return false; + if (str.length !== domStr.length + idx) { + return false; // it's not a suffix } - /* "* The last character of the string that is not included in the domain - * string is a %x2E (".") character." */ + /* " * The last character of the string that is not included in the + * domain string is a %x2E (".") character." */ if (str.substr(idx-1,1) !== '.') { - return false; + return false; // doesn't align on "." + } + + /* " * The string is a host name (i.e., not an IP address)." */ + if (IP_REGEX_LOWERCASE.test(str)) { + return false; // it's an IP address } return true; @@ -1015,7 +1026,7 @@ CookieJar.prototype.setCookie = function(cookie, url, options, cb) { } } else if (!(cookie instanceof Cookie)) { - // If you're seeing this error, and are passing in a Cookie object, + // If you're seeing this error, and are passing in a Cookie object, // it *might* be a Cookie object from another loaded version of tough-cookie. err = new Error("First argument to setCookie must be a Cookie object or string"); return cb(options.ignoreError ? null : err); diff --git a/package.json b/package.json index dcd32875..0ec11b3c 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,6 @@ "vows": "^0.8.2" }, "dependencies": { - "ip-regex": "^2.1.0", "psl": "^1.1.28", "punycode": "^2.1.1" } diff --git a/test/domain_and_path_test.js b/test/domain_and_path_test.js index 36b85b9b..d2103851 100644 --- a/test/domain_and_path_test.js +++ b/test/domain_and_path_test.js @@ -49,14 +49,17 @@ function matchVows(func, table) { return theVows; } -function defaultPathVows(table) { +function transformVows(fn, table) { var theVows = {}; table.forEach(function (item) { var str = item[0]; var expect = item[1]; var label = str + " gives " + expect; + if (item.length >= 3) { + label += " (" + item[2] + ")"; + } theVows[label] = function () { - assert.equal(tough.defaultPath(str), expect); + assert.equal(fn(str), expect); }; }); return theVows; @@ -65,56 +68,86 @@ function defaultPathVows(table) { vows .describe('Domain and Path') .addBatch({ - "domain normalization": { - "simple": function () { - var c = new Cookie(); - c.domain = "EXAMPLE.com"; - assert.equal(c.canonicalizedDomain(), "example.com"); - }, - "extra dots": function () { - var c = new Cookie(); - c.domain = ".EXAMPLE.com"; - assert.equal(c.cdomain(), "example.com"); - }, - "weird trailing dot": function () { - var c = new Cookie(); - c.domain = "EXAMPLE.ca."; - assert.equal(c.canonicalizedDomain(), "example.ca."); - }, - "weird internal dots": function () { - var c = new Cookie(); - c.domain = "EXAMPLE...ca."; - assert.equal(c.canonicalizedDomain(), "example...ca."); - }, - "IDN": function () { - var c = new Cookie(); - c.domain = "δοκιμή.δοκιμή"; // "test.test" in greek - assert.equal(c.canonicalizedDomain(), "xn--jxalpdlp.xn--jxalpdlp"); - } - } + "domain normalization": transformVows(tough.canonicalDomain, [ + ["example.com", "example.com", "already canonical"], + ["EXAMPLE.com", "example.com", "simple"], + [".EXAMPLE.com", "example.com", "leading dot stripped"], + ["EXAMPLE.com.", "example.com.", "trailing dot"], + [".EXAMPLE.com.", "example.com.", "leading and trailing dot"], + [".EXAMPLE...com.", "example...com.", "internal dots"], + ["δοκιμή.δοκιμή","xn--jxalpdlp.xn--jxalpdlp", "IDN: test.test in greek"], + ]) }) .addBatch({ "Domain Match": matchVows(tough.domainMatch, [ // str, dom, expect - ["example.com", "example.com", true], - ["eXaMpLe.cOm", "ExAmPlE.CoM", true], + ["example.com", "example.com", true], // identical + ["eXaMpLe.cOm", "ExAmPlE.CoM", true], // both canonicalized ["no.ca", "yes.ca", false], ["wwwexample.com", "example.com", false], - ["www.example.com", "example.com", true], - ["example.com", "www.example.com", false], ["www.subdom.example.com", "example.com", true], ["www.subdom.example.com", "subdom.example.com", true], ["example.com", "example.com.", false], // RFC6265 S4.1.2.3 - ["192.168.0.1", "168.0.1", false], // S5.1.3 "The string is a host name" + + // nulls and undefineds [null, "example.com", null], ["example.com", null, null], [null, null, null], [undefined, undefined, null], + + // suffix matching: + ["www.example.com", "example.com", true], // substr AND suffix + ["www.example.com.org", "example.com", false], // substr but not suffix + ["example.com", "www.example.com.org", false], // neither + ["example.com", "www.example.com", false], // super-str + ["aaa.com", "aaaa.com", false], // str can't be suffix of domain + ["aaaa.com", "aaa.com", false], // dom is suffix, but has to match on "." boundary! + ["www.aaaa.com", "aaa.com", false], + ["www.aaa.com", "aaa.com", true], + ["www.aexample.com", "example.com", false], // has to match on "." boundary + + // S5.1.3 "The string is a host name (i.e., not an IP address)" + ["192.168.0.1", "168.0.1", false], // because str is an IP (v4) + ["100.192.168.0.1", "168.0.1", true], // WEIRD: because str is not a valid IPv4 + ["100.192.168.0.1", "192.168.0.1", true], // WEIRD: because str is not a valid IPv4 + ["::ffff:192.168.0.1", "168.0.1", false], // because str is an IP (v6) + ["::ffff:192.168.0.1", "192.168.0.1", false], // because str is an IP (v6) + ["::FFFF:192.168.0.1", "192.168.0.1", false], // because str is an IP (v6) + ["::192.168.0.1", "192.168.0.1", false], // because str is an IP (yes, v6!) + [":192.168.0.1", "168.0.1", true], // WEIRD: because str is not valid IPv6 + [":ffff:100.192.168.0.1", "192.168.0.1", true], // WEIRD: because str is not valid IPv6 + [":ffff:192.168.0.1", "192.168.0.1", false], + [":ffff:192.168.0.1", "168.0.1", true], // WEIRD: because str is not valid IPv6 + ["::Fxxx:192.168.0.1", "168.0.1", true], // WEIRD: because str isnt IPv6 + ["192.168.0.1", "68.0.1", false], + ["192.168.0.1", "2.68.0.1", false], + ["192.168.0.1", "92.68.0.1", false], + ["10.1.2.3", "210.1.2.3", false], + ["2008::1", "::1", false], + ["::1", "2008::1", false], + ["::1", "::1", true], // "are identical" rule, despite IPv6 + ["::3xam:1e", "2008::3xam:1e", false], // malformed IPv6 + ["::3Xam:1e", "::3xaM:1e", true], // identical, even though malformed + ["3xam::1e", "3xam::1e", true], // identical + ["::3xam::1e", "3xam::1e", false], + ["3xam::1e", "::3xam:1e", false], + ["::f00f:10.0.0.1", "10.0.0.1", false], + ["10.0.0.1", "::f00f:10.0.0.1", false], + + // "IP like" hostnames: + ["1.example.com", "example.com", true], + ["11.example.com", "example.com", true], + ["192.168.0.1.example.com", "example.com", true], + + // exact length "TLD" tests: + ["com", "net", false], // same len, non-match + ["com", "com", true], // "are identical" rule + ["NOTATLD", "notaTLD", true], // "are identical" rule (after canonicalization) ]) }) .addBatch({ - "default-path": defaultPathVows([ + "default-path": transformVows(tough.defaultPath,[ [null, "/"], ["/", "/"], ["/file", "/"],