Repository: pydantic/pydantic

Issue:
Validation Error on HttpUrl and AnyHttpUrl on parsing valid url starting with 4 numbers
# Validation Error on HttpUrl and AnyHttpUrl on parsing valid url starting with 4 numbers

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.5.1
            pydantic compiled: True
                 install path: /opt/virtualenvs/python3/lib/python3.8/site-packages/pydantic
               python version: 3.8.3 (default, May 14 2020, 20:11:43)  [GCC 7.5.0]
                     platform: Linux-5.4.0-1009-gcp-x86_64-with-glibc2.27
     optional deps. installed: []
```

```py
from pydantic import BaseModel, HttpUrl, AnyHttpUrl

class MyModel(BaseModel):
    url: HttpUrl

class MyModel2(BaseModel):
    url: AnyHttpUrl

a = MyModel(url='http://abc.11.11.11.11.nip.io/action')
b = MyModel2(url='http://abc.11.11.11.11.nip.io/action')
c = MyModel(url='http://11.11.11.11.nip.io/action')
d = MyModel2(url='http://11.11.11.11.nip.io/action')
```

Output of `c` and `d`
```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "pydantic/main.py", line 338, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for MyModel2
url
  URL invalid, extra characters found after valid URL: '.nip.io/action' (type=value_error.url.extra; extra=.nip.io/action)
```

`a` and `b` can be validated

`c` and `d` raise ValidationError

However they are valid URL.



Comments:
Is there any reason that pydantic is not using the [python builtin url parser](https://docs.python.org/3/library/urllib.parse.html)?
`11.11.11.11` looks like an ip address which is what's causing the problem. Happy to accept a PR if you can find an easy way to fix this, but I think it's not trivial.

> Is there any reason that pydantic is not using the [python builtin url parser](https://docs.python.org/3/library/urllib.parse.html)?

Yes,

```py
 from urllib.parse import urlparse
urlparse('not valid')
#> ParseResult(scheme='', netloc='', path='not valid', params='', query='', fragment='')
```
all strings are parsed, it's not trivial to see how you'd use `urlparse` to check if a URL is valid, and then give a reasonable error when it's not.

I guess we could check `scheme` is ok, then check `netloc` is not empty and use regexes to see if it's an IP or a valid domain, and so on. I've no idea if this would be more performant than the current solution.

pydantic's URL parsing using regexes was originally taken from marshmallow, then altered a lot.

I suspect that `urlparse` is not a golden panacea or all other validation libraries would be using it, but maybe I'm wrong.

Patch:
diff --git a/pydantic/networks.py b/pydantic/networks.py
--- a/pydantic/networks.py
+++ b/pydantic/networks.py
@@ -69,8 +69,8 @@ def url_regex() -> Pattern[str]:
             r'(?:(?P<scheme>[a-z][a-z0-9+\-.]+)://)?'  # scheme https://tools.ietf.org/html/rfc3986#appendix-A
             r'(?:(?P<user>[^\s:/]*)(?::(?P<password>[^\s/]*))?@)?'  # user info
             r'(?:'
-            r'(?P<ipv4>(?:\d{1,3}\.){3}\d{1,3})|'  # ipv4
-            r'(?P<ipv6>\[[A-F0-9]*:[A-F0-9:]+\])|'  # ipv6
+            r'(?P<ipv4>(?:\d{1,3}\.){3}\d{1,3})(?=$|[/:#?])|'  # ipv4
+            r'(?P<ipv6>\[[A-F0-9]*:[A-F0-9:]+\])(?=$|[/:#?])|'  # ipv6
             r'(?P<domain>[^\s/:?#]+)'  # domain, validation occurs later
             r')?'
             r'(?::(?P<port>\d+))?'  # port


Test patch:
diff --git a/tests/test_networks.py b/tests/test_networks.py
--- a/tests/test_networks.py
+++ b/tests/test_networks.py
@@ -51,6 +51,8 @@
         AnyUrl('https://example.com', scheme='https', host='example.com'),
         'https://exam_ple.com/',
         'http://twitter.com/@handle/',
+        'http://11.11.11.11.example.com/action',
+        'http://abc.11.11.11.11.example.com/action',
     ],
 )
 def test_any_url_success(value):
@@ -159,6 +161,16 @@ def test_ipv4_port():
     assert url.password is None
 
 
+def test_ipv4_no_port():
+    url = validate_url('ftp://123.45.67.8')
+    assert url.scheme == 'ftp'
+    assert url.host == '123.45.67.8'
+    assert url.host_type == 'ipv4'
+    assert url.port is None
+    assert url.user is None
+    assert url.password is None
+
+
 def test_ipv6_port():
     url = validate_url('wss://[2001:db8::ff00:42]:8329')
     assert url.scheme == 'wss'