diff --git a/mod.ts b/mod.ts index 67d2cef3..d56089e7 100644 --- a/mod.ts +++ b/mod.ts @@ -77,6 +77,7 @@ import isSemVer from "./src/lib/isSemVer.ts"; import isSlug from "./src/lib/isSlug.ts"; import isSurrogatePair from "./src/lib/isSurrogatePair.ts"; import isUpperCase from "./src/lib/isUpperCase.ts"; +import isURL from "./src/lib/isURL.ts"; import isUUID from "./src/lib/isUUID.ts"; import isVariableWidth from "./src/lib/isVariableWidth.ts"; import isWhitelisted from "./src/lib/isWhitelisted.ts"; @@ -155,6 +156,7 @@ const validator: ValidatorMap = { isSlug, isSurrogatePair, isUpperCase, + isURL, isUUID, isVariableWidth, isWhitelisted, diff --git a/src/lib/isURL.ts b/src/lib/isURL.ts new file mode 100644 index 00000000..f4a47183 --- /dev/null +++ b/src/lib/isURL.ts @@ -0,0 +1,166 @@ +import isFQDN from "./isFQDN.ts"; +import isIP from "./isIP.ts"; +import merge from "./util/merge.ts"; + +/* +options for isURL method + +require_protocol - if set as true isURL will return false if protocol is not present in the URL +require_valid_protocol - isURL will check if the URL's protocol is present in the protocols option +protocols - valid protocols can be modified with this option +require_host - if set as false isURL will not check if host is present in the URL +allow_protocol_relative_urls - if set as true protocol relative URLs will be allowed +disallow_auth - if set true wont allow auth data in urls +host_whiitelist - list of url allowed (can include both string and RegExp) +host_balcklist - list of url not allowed (can include both string and RegExp) + +*/ + +interface isURLOptions { + protocols?: string[]; + require_tld?: boolean; + require_protocol?: boolean; + require_host?: boolean; + require_valid_protocol?: boolean; + allow_underscores?: boolean; + allow_trailing_dot?: boolean; + allow_protocol_relative_urls?: boolean; + host_whitelist?: string[]; + host_blacklist?: string[]; + disallow_auth?: boolean; +} + +const defaultURLOptions: isURLOptions = { + protocols: ["http", "https", "ftp"], + require_tld: true, + require_protocol: false, + require_host: true, + require_valid_protocol: true, + allow_underscores: false, + allow_trailing_dot: false, + allow_protocol_relative_urls: false, + host_whitelist: [], + host_blacklist: [], + disallow_auth: false, +}; + +const wrapped_ipv6: RegExp = /^\[([^\]]+)\](?::([0-9]+))?$/; + +function checkHost(host: string, matches: (string | RegExp)[]): boolean { + for (let i = 0; i < matches.length; i++) { + let match: string | RegExp = matches[i]; + if (host === match || ((match instanceof RegExp) && match.test(host))) { + return true; + } + } + return false; +} + +export default function isURL(url: string, options: isURLOptions): boolean { + if (!url || url.length >= 2083 || /[\s<>]/.test(url)) { + return false; + } + if (url.indexOf("mailto:") === 0) { + return false; + } + options = merge(options, defaultURLOptions); + let protocol: string, + auth: string, + host: string, + hostname: string, + port: number, + port_str: string | null, + split: string[], + ipv6: string | null; + + split = url.split("#"); + url = split.shift()!; + + split = url.split("?"); + url = split.shift()!; + + split = url.split("://"); + if (split.length > 1) { + protocol = split.shift()!.toLowerCase(); + if ( + options.require_valid_protocol && + options.protocols!.indexOf(protocol) === -1 + ) { + return false; + } + } else if (options.require_protocol) { + return false; + } else if (url.substr(0, 2) === "//") { + if (!options.allow_protocol_relative_urls) { + return false; + } + split[0] = url.substr(2); + } + url = split.join("://"); + + if (url === "") { + return false; + } + + split = url.split("/"); + url = split.shift()!; + + if (url === "" && !options.require_host) { + return true; + } + + split = url.split("@"); + if (split.length > 1) { + if (options.disallow_auth) { + return false; + } + auth = split.shift()!; + if (auth.indexOf(":") >= 0 && auth.split(":").length > 2) { + return false; + } + } + hostname = split.join("@"); + + port_str = null; + ipv6 = null; + const ipv6_match = hostname.match(wrapped_ipv6); + if (ipv6_match) { + host = ""; + ipv6 = ipv6_match[1]; + port_str = ipv6_match[2] || null; + } else { + split = hostname.split(":"); + host = split.shift()!; + if (split.length) { + port_str = split.join(":"); + } + } + + if (port_str !== null) { + port = parseInt(port_str, 10); + if (!/^[0-9]+$/.test(port_str) || port <= 0 || port > 65535) { + return false; + } + } + + if (!isIP(host) && !isFQDN(host, options) && (!ipv6 || !isIP(ipv6, 6))) { + return false; + } + + host = (host || ipv6)!; + + if ( + options.host_whitelist!.length > 0 && + !checkHost(host, options.host_whitelist!) + ) { + return false; + } + if ( + options.host_blacklist!.length > 0 && + checkHost(host, options.host_blacklist!) + ) { + return false; + } + + return true; +} diff --git a/test/test.ts b/test/test.ts index 360dc438..ceb420db 100644 --- a/test/test.ts +++ b/test/test.ts @@ -6929,6 +6929,308 @@ test({ "123abc", ], }); +test({ + validator: "isURL", + valid: [ + "foobar.com", + "www.foobar.com", + "foobar.com/", + "valid.au", + "http://www.foobar.com/", + "HTTP://WWW.FOOBAR.COM/", + "https://www.foobar.com/", + "HTTPS://WWW.FOOBAR.COM/", + "http://www.foobar.com:23/", + "http://www.foobar.com:65535/", + "http://www.foobar.com:5/", + "https://www.foobar.com/", + "ftp://www.foobar.com/", + "http://www.foobar.com/~foobar", + "http://user:pass@www.foobar.com/", + "http://user:@www.foobar.com/", + "http://127.0.0.1/", + "http://10.0.0.0/", + "http://189.123.14.13/", + "http://duckduckgo.com/?q=%2F", + "http://foobar.com/t$-_.+!*'(),", + "http://foobar.com/?foo=bar#baz=qux", + "http://foobar.com?foo=bar", + "http://foobar.com#baz=qux", + "http://www.xn--froschgrn-x9a.net/", + "http://xn--froschgrn-x9a.com/", + "http://foo--bar.com", + "http://høyfjellet.no", + "http://xn--j1aac5a4g.xn--j1amh", + "http://xn------eddceddeftq7bvv7c4ke4c.xn--p1ai", + "http://кулік.укр", + "test.com?ref=http://test2.com", + "http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html", + "http://[1080:0:0:0:8:800:200C:417A]/index.html", + "http://[3ffe:2a00:100:7031::1]", + "http://[1080::8:800:200C:417A]/foo", + "http://[::192.9.5.5]/ipng", + "http://[::FFFF:129.144.52.38]:80/index.html", + "http://[2010:836B:4179::836B:4179]", + "http://example.com/example.json#/foo/bar", + ], + invalid: [ + "http://localhost:3000/", + "//foobar.com", + "xyz://foobar.com", + "invalid/", + "invalid.x", + "invalid.", + ".com", + "http://com/", + "http://300.0.0.1/", + "mailto:foo@bar.com", + "rtmp://foobar.com", + "http://www.xn--.com/", + "http://xn--.com/", + "http://www.foobar.com:0/", + "http://www.foobar.com:70000/", + "http://www.foobar.com:99999/", + "http://www.-foobar.com/", + "http://www.foobar-.com/", + "http://foobar/# lol", + "http://foobar/? lol", + "http://foobar/ lol/", + "http://lol @foobar.com/", + "http://lol:lol @foobar.com/", + "http://lol:lol:lol@foobar.com/", + "http://lol: @foobar.com/", + "http://www.foo_bar.com/", + "http://www.foobar.com/\t", + "http://\n@www.foobar.com/", + "", + // `http://foobar.com/${new Array(2083).join('f')}`, + "http://*.foo.com", + "*.foo.com", + "!.foo.com", + "http://example.com.", + "http://localhost:61500this is an invalid url!!!!", + "////foobar.com", + "http:////foobar.com", + "https://example.com/foo//", + ], +}); +test({ + validator: "isURL", + args: [{ + protocols: ["rtmp"], + }], + valid: [ + "rtmp://foobar.com", + ], + invalid: [ + "http://foobar.com", + ], +}); + +test({ + validator: "isURL", + args: [{ + protocols: ["file"], + require_host: false, + require_tld: false, + }], + valid: [ + "file://localhost/foo.txt", + "file:///foo.txt", + "file:///", + ], + invalid: [ + "http://foobar.com", + "file://", + ], +}); + +test({ + validator: "isURL", + args: [{ + require_valid_protocol: false, + }], + valid: [ + "rtmp://foobar.com", + "http://foobar.com", + "test://foobar.com", + ], + invalid: [ + "mailto:test@example.com", + ], +}); + +test({ + validator: "isURL", + args: [{ + allow_underscores: true, + }], + valid: [ + "http://foo_bar.com", + "http://pr.example_com.294.example.com/", + "http://foo__bar.com", + ], + invalid: [], +}); + +test({ + validator: "isURL", + args: [{ + require_tld: false, + }], + valid: [ + "http://foobar.com/", + "http://foobar/", + "http://localhost/", + "foobar/", + "foobar", + ], + invalid: [], +}); + +test({ + validator: "isURL", + args: [{ + allow_trailing_dot: true, + require_tld: false, + }], + valid: [ + "http://example.com.", + "foobar.", + ], +}); + +test({ + validator: "isURL", + args: [{ + allow_protocol_relative_urls: true, + }], + valid: [ + "//foobar.com", + "http://foobar.com", + "foobar.com", + ], + invalid: [ + "://foobar.com", + "/foobar.com", + "////foobar.com", + "http:////foobar.com", + ], +}); + +test({ + validator: "isURL", + args: [{ + allow_protocol_relative_urls: true, + require_protocol: true, + }], + valid: [ + "http://foobar.com", + ], + invalid: [ + "//foobar.com", + "://foobar.com", + "/foobar.com", + "foobar.com", + ], +}); + +test({ + validator: "isURL", + args: [{ + require_protocol: true, + }], + valid: [ + "http://foobar.com/", + ], + invalid: [ + "http://localhost/", + "foobar.com", + "foobar", + ], +}); + +test({ + validator: "isURL", + args: [{ + host_whitelist: ["foo.com", "bar.com"], + }], + valid: [ + "http://bar.com/", + "http://foo.com/", + ], + invalid: [ + "http://foobar.com", + "http://foo.bar.com/", + "http://qux.com", + ], +}); + +test({ + validator: "isURL", + args: [{ + host_whitelist: ["bar.com", "foo.com", /\.foo\.com$/], + }], + valid: [ + "http://bar.com/", + "http://foo.com/", + "http://images.foo.com/", + "http://cdn.foo.com/", + "http://a.b.c.foo.com/", + ], + invalid: [ + "http://foobar.com", + "http://foo.bar.com/", + "http://qux.com", + ], +}); + +test({ + validator: "isURL", + args: [{ + host_blacklist: ["foo.com", "bar.com"], + }], + valid: [ + "http://foobar.com", + "http://foo.bar.com/", + "http://qux.com", + ], + invalid: [ + "http://bar.com/", + "http://foo.com/", + ], +}); + +test({ + validator: "isURL", + args: [{ + host_blacklist: ["bar.com", "foo.com", /\.foo\.com$/], + }], + valid: [ + "http://foobar.com", + "http://foo.bar.com/", + "http://qux.com", + ], + invalid: [ + "http://bar.com/", + "http://foo.com/", + "http://images.foo.com/", + "http://cdn.foo.com/", + "http://a.b.c.foo.com/", + ], +}); + +test({ + validator: "isURL", + args: [{ disallow_auth: true }], + valid: [ + "doe.com", + ], + invalid: [ + "john@doe.com", + "john:john@doe.com", + ], +}); test({ validator: "isUUID",