diff --git a/docs/index.md b/docs/index.md index 4ed81bac..c76eb5a8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -24,7 +24,7 @@ ::: validators.slug - +::: validators.url ::: validators.uuid diff --git a/tests/test_url.py b/tests/test_url.py index 2252f24d..15d9d471 100644 --- a/tests/test_url.py +++ b/tests/test_url.py @@ -1,153 +1,175 @@ +"""Test URL.""" # -*- coding: utf-8 -*- -import pytest - -from validators import url, ValidationFailure - -@pytest.mark.parametrize('address', [ - u'http://foobar.dk', - u'http://foobar.museum/foobar', - u'http://fo.com', - u'http://FOO.com', - u'http://foo.com/blah_blah', - u'http://foo.com/blah_blah/', - u'http://foo.com/blah_blah_(wikipedia)', - u'http://foo.com/blah_blah_(wikipedia)_(again)', - u'http://www.example.com/wpstyle/?p=364', - u'https://www.example.com/foo/?bar=baz&inga=42&quux', - u'https://www.example.com?bar=baz', - u'http://✪df.ws/123', - u'http://userid:password@example.com:8080', - u'http://userid:password@example.com:8080/', - u'http://userid@example.com', - u'http://userid@example.com/', - u'http://userid@example.com:8080', - u'http://userid@example.com:8080/', - u'http://userid:password@example.com', - u'http://userid:password@example.com/', - u'http://142.42.1.1/', - u'http://142.42.1.1:8080/', - u'http://➡.ws/䨹', - u'http://⌘.ws', - u'http://⌘.ws/', - u'http://foo.com/blah_(wikipedia)#cite-1', - u'http://foo.com/blah_(wikipedia)_blah#cite-1', - u'http://foo.com/unicode_(✪)_in_parens', - u'http://foo.com/(something)?after=parens', - u'http://☺.damowmow.com/', - u'http://code.google.com/events/#&product=browser', - u'http://j.mp', - u'ftp://foo.bar/baz', - u'http://foo.bar/?q=Test%20URL-encoded%20stuff', - u'http://مثال.إختبار', - u'http://例子.测试', - u'http://उदाहरण.परीक्षा', - u'http://www.😉.com', - u'http://😉.com/😁', - u'http://উদাহরণ.বাংলা', - u'http://xn--d5b6ci4b4b3a.xn--54b7fta0cc', - u'http://дом-м.рф/1/asdf', - u'http://xn----gtbybh.xn--p1ai/1/asdf', - u'http://-.~_!$&\'()*+,;=:%40:80%2f::::::@example.com', - u'http://1337.net', - u'http://a.b-c.de', - u'http://223.255.255.254', - u'http://10.1.1.0', - u'http://10.1.1.1', - u'http://10.1.1.254', - u'http://10.1.1.255', - u'http://127.0.0.1:8080', - u'http://127.0.10.150', - u'http://localhost', - u'http://localhost:8000', - u'http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html', - u'http://[1080:0:0:0:8:800:200C:417A]/index.html', - u'http://[3ffe:2a00:100:7031::1]', - u'http://[1080::8:800:200C:417A]/foo', - u'http://[::192.9.5.5]/ipng', - u'http://[::FFFF:129.144.52.38]:80/index.html', - u'http://[2010:836B:4179::836B:4179]', -]) -def test_returns_true_on_valid_url(address): - assert url(address) - - -@pytest.mark.parametrize('address, public', [ - (u'http://foo.bar', True), - (u'http://username:password@example.com:4010/', False), - (u'http://username:password@112.168.10.10:4010/', True), - (u'http://username:password@192.168.10.10:4010/', False), - (u'http://10.0.10.1', False), - (u'http://127.0.0.1', False), -]) -def test_returns_true_on_valid_public_url(address, public): - assert url(address, public=public) +# external +import pytest +# local +from validators import ValidationFailure, url -@pytest.mark.parametrize('address', [ - 'http://foobar', - 'foobar.dk', - 'http://127.0.0/asdf', - 'http://foobar.d', - 'http://foobar.12', - 'http://foobar', - 'htp://foobar.com', - 'http://foobar..com', - 'http://fo..com', - 'http://', - 'http://.', - 'http://..', - 'http://../', - 'http://?', - 'http://??', - 'http://??/', - 'http://#', - 'http://##', - 'http://##/', - 'http://foo.bar?q=Spaces should be encoded', - '//', - '//a', - '///a', - '///', - 'http:///a', - 'foo.com', - 'rdar://1234', - 'h://test', - 'http:// shouldfail.com', - ':// should fail', - 'http://foo.bar/foo(bar)baz quux', - 'ftps://foo.bar/', - 'http://-error-.invalid/', - 'http://a.b--c.de/', - 'http://-a.b.co', - 'http://a.b-.co', - 'http://0.0.0.0', - 'http://224.1.1.1', - 'http://1.1.1.1.1', - 'http://123.123.123', - 'http://3628126748', - 'http://.www.foo.bar/', - 'http://www.foo.bar./', - 'http://.www.foo.bar./', - 'http://127.12.0.260', - 'http://example.com/">user@example.com', - 'http://[2010:836B:4179::836B:4179', - 'http://2010:836B:4179::836B:4179', - 'http://2010:836B:4179::836B:4179:80/index.html', -]) -def test_returns_failed_validation_on_invalid_url(address): - assert isinstance(url(address), ValidationFailure) +@pytest.mark.parametrize( + "value", + [ + "http://foobar.dk", + "http://foobar.museum/foobar", + "http://fo.com", + "http://FOO.com", + "http://foo.com/blah_blah", + "http://foo.com/blah_blah/", + "http://foo.com/blah_blah_(wikipedia)", + "http://foo.com/blah_blah_(wikipedia)_(again)", + "http://www.example.com/wpstyle/?p=364", + "https://www.example.com/foo/?bar=baz&inga=42&quux", + "https://www.example.com?bar=baz", + "http://✪df.ws/123", + "http://userid:password@example.com:8080", + "http://userid:password@example.com:8080/", + "http://userid@example.com", + "http://userid@example.com/", + "http://userid@example.com:8080", + "http://userid@example.com:8080/", + "http://userid:password@example.com", + "http://userid:password@example.com/", + "http://142.42.1.1/", + "http://142.42.1.1:8080/", + "http://➡.ws/䨹", + "http://⌘.ws", + "http://⌘.ws/", + "http://foo.com/blah_(wikipedia)#cite-1", + "http://foo.com/blah_(wikipedia)_blah#cite-1", + "http://foo.com/unicode_(✪)_in_parens", + "http://foo.com/(something)?after=parens", + "http://☺.damowmow.com/", + "http://code.google.com/events/#&product=browser", + "http://j.mp", + "ftp://foo.bar/baz", + "http://foo.bar/?q=Test%20URL-encoded%20stuff", + "http://مثال.إختبار", + "http://例子.测试", + "http://उदाहरण.परीक्षा", + "http://www.😉.com", + "http://😉.com/😁", + "http://উদাহরণ.বাংলা", + "http://xn--d5b6ci4b4b3a.xn--54b7fta0cc", + "http://дом-м.рф/1/asdf", + "http://xn----gtbybh.xn--p1ai/1/asdf", + "http://1337.net", + "http://a.b-c.de", + "http://a.b--c.de/", + "http://0.0.0.0", + "http://224.1.1.1", + "http://223.255.255.254", + "http://10.1.1.0", + "http://10.1.1.1", + "http://10.1.1.254", + "http://10.1.1.255", + "http://127.0.0.1:8080", + "http://127.0.10.150", + "http://47.96.118.255:2333/", + "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://foo.bar", + "http://google.com:9/test", + "http://5.196.190.0/", + "http://username:password@example.com:4010/", + "http://username:password@112.168.10.10:4010/", + "http://base-test-site.local", + "http://президент.рф/", + "http://10.24.90.255:83/", + "https://travel-usa.com/wisconsin/旅行/", + # when simple_host=True + # "http://localhost", + # "http://localhost:8000", + # "http://pc:8081/", + # "http://3628126748", + # "http://foobar", + ], +) +def test_returns_true_on_valid_url(value: str): + """Test returns true on valid url.""" + assert url(value) -@pytest.mark.parametrize('address, public', [ - (u'http://username:password@192.168.10.10:4010/', True), - (u'http://10.0.10.1', True), - (u'http://127.0.0.1', True), - (u'foo://127.0.0.1', True), - (u'http://username:password@127.0.0.1:8080', True), - (u'http://localhost', True), - (u'http://localhost:8000', True), -]) -def test_returns_failed_validation_on_invalid_public_url(address, public): - assert isinstance(url(address, public=public), ValidationFailure) +@pytest.mark.parametrize( + "value", + [ + "foobar.dk", + "http://127.0.0/asdf", + "http://foobar.d", + "http://foobar.12", + "htp://foobar.com", + "http://foobar..com", + "http://fo..com", + "http://", + "http://.", + "http://..", + "http://../", + "http://?", + "http://??", + "http://??/", + "http://#", + "http://##", + "http://##/", + "http://foo.bar?q=Spaces should be encoded", + "//", + "//a", + "///a", + "///", + "http:///a", + "foo.com", + "rdar://1234", + "h://test", + "http:// shouldfail.com", + ":// should fail", + "http://foo.bar/foo(bar)baz quux", + "http://-error-.invalid/", + "http://www.\uFFFD.ch", + "http://-a.b.co", + "http://a.b-.co", + "http://1.1.1.1.1", + "http://123.123.123", + "http://.www.foo.bar/", + "http://www.foo.bar./", + "http://.www.foo.bar./", + "http://127.12.0.260", + 'http://example.com/">user@example.com', + "http://[2010:836B:4179::836B:4179", + "http://2010:836B:4179::836B:4179", + "http://2010:836B:4179::836B:4179:80/index.html", + "http://0.00.00.00.00.00.00.00.00.00.00.00.00.00.00." + + "00.00.00.00.00.00.00.00.00.00.00.00.00.00.00.00." + + "00.00.00.00.00.00.00.00.00.00.00.00.00.00.00.00." + + "00.00.00.00.00.00.00.00.00.00.00.00.00.", # ReDoS + "http://172.20.201.135-10.10.10.1656172.20.11.80-10." + + "10.10.1746172.16.9.13-192.168.17.68610.10.10.226-192." + + "168.17.64610.10.10.226-192.168.17.63610.10.10.226-192." + + "168.17.62610.10.10.226-192.168.17.61610.10.10.226-192." + + "168.17.60610.10.10.226-192.168.17.59610.10.10.226-192." + + "168.17.58610.10.10.226-192.168.17.57610.10.10.226-192." + + "168.17.56610.10.10.226-192.168.17.55610.10.10.226-192." + + "168.17.54610.10.10.226-192.168.17.53610.10.10.226-192." + + "168.17.52610.10.10.226-192.168.17.51610.10.10.195-10." + + "10.10.2610.10.10.194-192.168.17.685172.20.11.52-10.10." + + "10.195510.10.10.226-192.168.17.50510.10.10.186-172.20." + + "11.1510.10.10.165-198.41.0.54192.168.84.1-192.168.17." + + "684192.168.222.1-192.168.17.684172.20.11.52-10.10.10." + + "174410.10.10.232-172.20.201.198410.10.10.228-172.20.201." + + "1983192.168.17.135-10.10.10.1423192.168.17.135-10.10.10." + + "122310.10.10.224-172.20.201.198310.10.10.195-172.20.11." + + "1310.10.10.160-172.20.201.198310.10.10.142-192.168.17." + + "1352192.168.22.207-10.10.10.2242192.168.17.66-10.10.10." + + "1122192.168.17.135-10.10.10.1122192.168.17.129-10.10.10." + + "1122172.20.201.198-10.10.10.2282172.20.201.198-10.10.10." + + "2242172.20.201.1-10.10.10.1652172.20.11.2-10.10.10.1412172." + + "16.8.229-12.162.170.196210.10.10.212-192.168.22.133", # ReDoS + ], +) +def test_returns_failed_validation_on_invalid_url(value: str): + """Test returns failed validation on invalid url.""" + assert isinstance(url(value), ValidationFailure) diff --git a/validators/url.py b/validators/url.py index 37d946cb..37616372 100644 --- a/validators/url.py +++ b/validators/url.py @@ -1,154 +1,208 @@ +"""URL.""" +# -*- coding: utf-8 -*- + +# standard +from urllib.parse import urlsplit +from functools import lru_cache import re +# local +from .hostname import hostname from .utils import validator -ip_middle_octet = r"(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5]))" -ip_last_octet = r"(?:\.(?:0|[1-9]\d?|1\d\d|2[0-4]\d|25[0-5]))" - -regex = re.compile( # noqa: W605 - r"^" - # protocol identifier - r"(?:(?:https?|ftp)://)" - # user:pass authentication - r"(?:[-a-z\u00a1-\uffff0-9._~%!$&'()*+,;=:]+" - r"(?::[-a-z0-9._~%!$&'()*+,;=:]*)?@)?" - r"(?:" - r"(?P" - # IP address exclusion - # private & local networks - r"(?:(?:10|127)" + ip_middle_octet + r"{2}" + ip_last_octet + r")|" - r"(?:(?:169\.254|192\.168)" + ip_middle_octet + ip_last_octet + r")|" - r"(?:172\.(?:1[6-9]|2\d|3[0-1])" + ip_middle_octet + ip_last_octet + r"))" - r"|" - # private & local hosts - r"(?P" - r"(?:localhost))" - r"|" - # IP address dotted notation octets - # excludes loopback network 0.0.0.0 - # excludes reserved space >= 224.0.0.0 - # excludes network & broadcast addresses - # (first & last IP address of each class) - r"(?P" - r"(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])" - r"" + ip_middle_octet + r"{2}" - r"" + ip_last_octet + r")" - r"|" - # IPv6 RegEx from https://stackoverflow.com/a/17871737 - r"\[(" - # 1:2:3:4:5:6:7:8 - r"([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|" - # 1:: 1:2:3:4:5:6:7:: - r"([0-9a-fA-F]{1,4}:){1,7}:|" - # 1::8 1:2:3:4:5:6::8 1:2:3:4:5:6::8 - r"([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|" - # 1::7:8 1:2:3:4:5::7:8 1:2:3:4:5::8 - r"([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|" - # 1::6:7:8 1:2:3:4::6:7:8 1:2:3:4::8 - r"([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|" - # 1::5:6:7:8 1:2:3::5:6:7:8 1:2:3::8 - r"([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|" - # 1::4:5:6:7:8 1:2::4:5:6:7:8 1:2::8 - r"([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|" - # 1::3:4:5:6:7:8 1::3:4:5:6:7:8 1::8 - r"[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|" - # ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 :: - r":((:[0-9a-fA-F]{1,4}){1,7}|:)|" - # fe80::7:8%eth0 fe80::7:8%1 - # (link-local IPv6 addresses with zone index) - r"fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|" - r"::(ffff(:0{1,4}){0,1}:){0,1}" - r"((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}" - # ::255.255.255.255 ::ffff:255.255.255.255 ::ffff:0:255.255.255.255 - # (IPv4-mapped IPv6 addresses and IPv4-translated addresses) - r"(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|" - r"([0-9a-fA-F]{1,4}:){1,4}:" - r"((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}" - # 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 - # (IPv4-Embedded IPv6 Address) - r"(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])" - r")\]|" - # host name - r"(?:(?:(?:xn--[-]{0,2})|[a-z\u00a1-\uffff\U00010000-\U0010ffff0-9]-?)*" - r"[a-z\u00a1-\uffff\U00010000-\U0010ffff0-9]+)" - # domain name - r"(?:\.(?:(?:xn--[-]{0,2})|[a-z\u00a1-\uffff\U00010000-\U0010ffff0-9]-?)*" - r"[a-z\u00a1-\uffff\U00010000-\U0010ffff0-9]+)*" - # TLD identifier - r"(?:\.(?:(?:xn--[-]{0,2}[a-z\u00a1-\uffff\U00010000-\U0010ffff0-9]{2,})|" - r"[a-z\u00a1-\uffff\U00010000-\U0010ffff]{2,}))" - r")" - # port number - r"(?::\d{2,5})?" - # resource path - r"(?:/[-a-z\u00a1-\uffff\U00010000-\U0010ffff0-9._~%!$&'()*+,;=:@/]*)?" - # query string - r"(?:\?\S*)?" - # fragment - r"(?:#\S*)?" - r"$", - re.UNICODE | re.IGNORECASE -) - -pattern = re.compile(regex) - - -@validator -def url(value, public=False): - """ - Return whether or not given value is a valid URL. - - If the value is valid URL this function returns ``True``, otherwise - :class:`~validators.utils.ValidationFailure`. - - This validator is based on the wonderful `URL validator of dperini`_. - - .. _URL validator of dperini: - https://gist.github.com/dperini/729294 - - Examples:: - >>> url('http://foobar.dk') - True +@lru_cache +def _username_regex(): + return re.compile( + # dot-atom + r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*$" + # non-quoted-string + + r"|^([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*$)", + re.IGNORECASE, + ) - >>> url('ftp://foobar.dk') - True - >>> url('http://10.0.0.1') - True +@lru_cache +def _path_regex(): + return re.compile(r"^[\/a-zA-Z0-9\-\.\_\~\!\$\&\'\(\)\*\+\,\;\=\:\@\%]+$", re.IGNORECASE) - >>> url('http://foobar.d') - ValidationFailure(func=url, ...) - >>> url('http://10.0.0.1', public=True) - ValidationFailure(func=url, ...) +@lru_cache +def _query_regex(): + return re.compile(r"&?(\w+=?[^\s&]*)", re.IGNORECASE) - .. versionadded:: 0.2 - .. versionchanged:: 0.10.2 +def _validate_scheme(value: str): + """Validate scheme.""" + # More schemes will be considered later. + return ( + value in {"ftp", "ftps", "git", "http", "https", "rtsp", "sftp", "ssh", "telnet"} + if value + else False + ) - Added support for various exotic URLs and fixed various false - positives. - .. versionchanged:: 0.10.3 +def _confirm_ipv6_skip(value: str, skip_ipv6_addr: bool): + """Confirm skip IPv6 check.""" + return skip_ipv6_addr or value.count(":") < 2 or not value.startswith("[") - Added ``public`` parameter. - .. versionchanged:: 0.11.0 +def _validate_auth_segment(value: str): + """Validate authentication segment.""" + if not value: + return True + if (colon_count := value.count(":")) > 1: + return False + if colon_count < 1: + return _username_regex().match(value) + username, password = value.rsplit(":", 1) + return _username_regex().match(username) and all( + char_to_avoid not in password for char_to_avoid in {"/", "?", "#", "@"} + ) - Made the regular expression this function uses case insensitive. - .. versionchanged:: 0.11.3 +def _validate_netloc( + value: str, + skip_ipv6_addr: bool, + skip_ipv4_addr: bool, + may_have_port: bool, + simple_host: bool, + rfc_1034: bool, + rfc_2782: bool, +): + """Validate netloc.""" + if not value or value.count("@") > 1: + return False + if value.count("@") < 1: + return hostname( + value + if _confirm_ipv6_skip(value, skip_ipv6_addr) or "]:" in value + else value.lstrip("[").replace("]", "", 1), + skip_ipv6_addr=_confirm_ipv6_skip(value, skip_ipv6_addr), + skip_ipv4_addr=skip_ipv4_addr, + may_have_port=may_have_port, + maybe_simple=simple_host, + rfc_1034=rfc_1034, + rfc_2782=rfc_2782, + ) + basic_auth, host = value.rsplit("@", 1) + return hostname( + host + if _confirm_ipv6_skip(host, skip_ipv6_addr) or "]:" in value + else host.lstrip("[").replace("]", "", 1), + skip_ipv6_addr=_confirm_ipv6_skip(host, skip_ipv6_addr), + skip_ipv4_addr=skip_ipv4_addr, + may_have_port=may_have_port, + maybe_simple=simple_host, + rfc_1034=rfc_1034, + rfc_2782=rfc_2782, + ) and _validate_auth_segment(basic_auth) + + +def _validate_optionals(path: str, query: str, fragment: str): + """Validate path query and fragments.""" + optional_segments = True + if path: + optional_segments &= bool(_path_regex().match(path.encode("idna").decode("utf-8"))) + if query: + optional_segments &= bool(_query_regex().match(query.encode("idna").decode("utf-8"))) + if fragment: + optional_segments &= all(char_to_avoid not in fragment for char_to_avoid in {"/", "?"}) + return optional_segments - Added support for URLs containing localhost - :param value: URL address string to validate - :param public: (default=False) Set True to only allow a public IP address +@validator +def url( + value: str, + /, + *, + skip_ipv6_addr: bool = False, + skip_ipv4_addr: bool = False, + may_have_port: bool = True, + simple_host: bool = False, + rfc_1034: bool = False, + rfc_2782: bool = False, +): + r"""Return whether or not given value is a valid URL. + + This validator was inspired from [URL validator of dperini][1]. + The following diagram is from [urlly][2]. + + foo://admin:hunter1@example.com:8042/over/there?name=ferret#nose + \_/ \___/ \_____/ \_________/ \__/\_________/ \_________/ \__/ + | | | | | | | | + scheme username password hostname port path query fragment + + [1]: https://gist.github.com/dperini/729294 + [2]: https://github.com/treeform/urlly + + Examples: + >>> url('http://duck.com') + # Output: True + >>> url('ftp://foobar.dk') + # Output: True + >>> url('http://10.0.0.1') + # Output: True + >>> url('http://example.com/">user@example.com') + # Output: ValidationFailure(func=url, ...) + + Args: + value: + URL string to validate. + skip_ipv6_addr: + When URL string cannot contain an IPv6 address. + skip_ipv4_addr: + When URL string cannot contain an IPv4 address. + may_have_port: + URL string may contain port number. + maybe_simple: + URL string maybe only hyphens and alpha-numerals. + rfc_1034: + Allow trailing dot in domain/host name. + Ref: [RFC 1034](https://www.rfc-editor.org/rfc/rfc1034). + rfc_2782: + Domain/Host name is of type service record. + Ref: [RFC 2782](https://www.rfc-editor.org/rfc/rfc2782). + + Returns: + (Literal[True]): + If `value` is a valid slug. + (ValidationFailure): + If `value` is an invalid slug. + + Note: + - *In version 0.11.3*: + - Added support for URLs containing localhost. + - *In version 0.11.0*: + - Made the regular expression case insensitive. + - *In version 0.10.3*: + - Added a `public` parameter. + - *In version 0.10.2*: + - Added support for various exotic URLs. + - Fixed various false positives. + + > *New in version 0.2.0*. """ - result = pattern.match(value) - if not public: - return result - - return result and not any( - (result.groupdict().get(key) for key in ('private_ip', 'private_host')) + if not value or re.search(r"\s", value): + # url must not contain any white + # spaces, they must be encoded + return False + + try: + scheme, netloc, path, query, fragment = urlsplit(value) + except ValueError: + return False + + return ( + _validate_scheme(scheme) + and _validate_netloc( + netloc, + skip_ipv6_addr, + skip_ipv4_addr, + may_have_port, + simple_host, + rfc_1034, + rfc_2782, + ) + and _validate_optionals(path, query, fragment) )