Skip to content

Commit

Permalink
feat: adds private parameter to ip_address, hostname & url
Browse files Browse the repository at this point in the history
  • Loading branch information
yozachar committed Apr 3, 2024
1 parent a2b4fc0 commit 98664e3
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 9 deletions.
8 changes: 6 additions & 2 deletions src/validators/hostname.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# standard
from functools import lru_cache
import re
from typing import Optional

from .domain import domain

Expand Down Expand Up @@ -54,6 +55,7 @@ def hostname(
skip_ipv4_addr: bool = False,
may_have_port: bool = True,
maybe_simple: bool = True,
private: Optional[bool] = None, # only for ip-addresses
rfc_1034: bool = False,
rfc_2782: bool = False,
):
Expand Down Expand Up @@ -92,6 +94,8 @@ def hostname(
Hostname string may contain port number.
maybe_simple:
Hostname string maybe only hyphens and alpha-numerals.
private:
Embedded IP address is public if `False`, private/local if `True`.
rfc_1034:
Allow trailing dot in domain/host name.
Ref: [RFC 1034](https://www.rfc-editor.org/rfc/rfc1034).
Expand All @@ -110,13 +114,13 @@ def hostname(
return (
(_simple_hostname_regex().match(host_seg) if maybe_simple else False)
or domain(host_seg, rfc_1034=rfc_1034, rfc_2782=rfc_2782)
or (False if skip_ipv4_addr else ipv4(host_seg, cidr=False))
or (False if skip_ipv4_addr else ipv4(host_seg, cidr=False, private=private))
or (False if skip_ipv6_addr else ipv6(host_seg, cidr=False))
)

return (
(_simple_hostname_regex().match(value) if maybe_simple else False)
or domain(value, rfc_1034=rfc_1034, rfc_2782=rfc_2782)
or (False if skip_ipv4_addr else ipv4(value, cidr=False))
or (False if skip_ipv4_addr else ipv4(value, cidr=False, private=private))
or (False if skip_ipv6_addr else ipv6(value, cidr=False))
)
47 changes: 40 additions & 7 deletions src/validators/ip_address.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,44 @@
IPv6Network,
NetmaskValueError,
)
import re
from typing import Optional

# local
from .utils import validator


def _check_private_ip(value: str, is_private: Optional[bool]):
if is_private is None:
return True
if is_private and (
any(
value.startswith(l_bit)
for l_bit in {
"10.", # private
"192.168.", # private
"169.254.", # link-local
"127.", # localhost
"0.0.0.0", # loopback #nosec
}
)
or re.match(r"^172\.(?:1[6-9]|2\d|3[0-1])\.", value) # private
or re.match(r"^(?:22[4-9]|23[0-9]|24[0-9]|25[0-5])\.", value) # broadcast
):
return True
return False


@validator
def ipv4(value: str, /, *, cidr: bool = True, strict: bool = False, host_bit: bool = True):
def ipv4(
value: str,
/,
*,
cidr: bool = True,
strict: bool = False,
private: Optional[bool] = None,
host_bit: bool = True,
):
"""Returns whether a given value is a valid IPv4 address.
From Python version 3.9.5 leading zeros are no longer tolerated
Expand All @@ -36,9 +67,11 @@ def ipv4(value: str, /, *, cidr: bool = True, strict: bool = False, host_bit: bo
value:
IP address string to validate.
cidr:
IP address string may contain CIDR notation
IP address string may contain CIDR notation.
strict:
IP address string is strictly in CIDR notation
IP address string is strictly in CIDR notation.
private:
IP address is public if `False`, private/local/loopback/broadcast if `True`.
host_bit:
If `False` and host bits (along with network bits) _are_ set in the supplied
address, this function raises a validation error. ref [IPv4Network][2].
Expand All @@ -54,8 +87,8 @@ def ipv4(value: str, /, *, cidr: bool = True, strict: bool = False, host_bit: bo
if cidr:
if strict and value.count("/") != 1:
raise ValueError("IPv4 address was expected in CIDR notation")
return IPv4Network(value, strict=not host_bit)
return IPv4Address(value)
return IPv4Network(value, strict=not host_bit) and _check_private_ip(value, private)
return IPv4Address(value) and _check_private_ip(value, private)
except (ValueError, AddressValueError, NetmaskValueError):
return False

Expand All @@ -81,9 +114,9 @@ def ipv6(value: str, /, *, cidr: bool = True, strict: bool = False, host_bit: bo
value:
IP address string to validate.
cidr:
IP address string may contain CIDR annotation
IP address string may contain CIDR annotation.
strict:
IP address string is strictly in CIDR notation
IP address string is strictly in CIDR notation.
host_bit:
If `False` and host bits (along with network bits) _are_ set in the supplied
address, this function raises a validation error. ref [IPv6Network][2].
Expand Down
8 changes: 8 additions & 0 deletions src/validators/url.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# standard
from functools import lru_cache
import re
from typing import Optional
from urllib.parse import parse_qs, unquote, urlsplit

# local
Expand Down Expand Up @@ -80,6 +81,7 @@ def _validate_netloc(
skip_ipv4_addr: bool,
may_have_port: bool,
simple_host: bool,
private: Optional[bool],
rfc_1034: bool,
rfc_2782: bool,
):
Expand All @@ -97,6 +99,7 @@ def _validate_netloc(
skip_ipv4_addr=skip_ipv4_addr,
may_have_port=may_have_port,
maybe_simple=simple_host,
private=private,
rfc_1034=rfc_1034,
rfc_2782=rfc_2782,
)
Expand All @@ -111,6 +114,7 @@ def _validate_netloc(
skip_ipv4_addr=skip_ipv4_addr,
may_have_port=may_have_port,
maybe_simple=simple_host,
private=private,
rfc_1034=rfc_1034,
rfc_2782=rfc_2782,
) and _validate_auth_segment(basic_auth)
Expand Down Expand Up @@ -151,6 +155,7 @@ def url(
may_have_port: bool = True,
simple_host: bool = False,
strict_query: bool = True,
private: Optional[bool] = None, # only for ip-addresses
rfc_1034: bool = False,
rfc_2782: bool = False,
):
Expand Down Expand Up @@ -191,6 +196,8 @@ def url(
URL string maybe only hyphens and alpha-numerals.
strict_query:
Fail validation on query string parsing error.
private:
Embedded IP address is public if `False`, private/local if `True`.
rfc_1034:
Allow trailing dot in domain/host name.
Ref: [RFC 1034](https://www.rfc-editor.org/rfc/rfc1034).
Expand Down Expand Up @@ -220,6 +227,7 @@ def url(
skip_ipv4_addr,
may_have_port,
simple_host,
private,
rfc_1034,
rfc_2782,
)
Expand Down
27 changes: 27 additions & 0 deletions tests/test_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,19 @@ def test_returns_true_on_valid_url(value: str):
assert url(value)


@pytest.mark.parametrize(
"address, private",
[
("http://username:[email protected]/", True),
("http://username:[email protected]:4010/", True),
("http://127.0.0.1", True),
],
)
def test_returns_true_on_valid_private_url(address, private):
"""Test returns true on valid private url."""
assert url(address, private=private)


@pytest.mark.parametrize(
"value",
[
Expand Down Expand Up @@ -188,3 +201,17 @@ def test_returns_true_on_valid_url(value: str):
def test_returns_failed_validation_on_invalid_url(value: str):
"""Test returns failed validation on invalid url."""
assert isinstance(url(value), ValidationError)


@pytest.mark.parametrize(
"address, private",
[
("http://username:[email protected]:4010", False),
("http://username:[email protected]:8080", False),
("http://10.0.10.1", False),
("http://255.255.255.255", False),
],
)
def test_returns_failed_validation_on_invalid_private_url(address, private):
"""Test returns failed validation on invalid private url."""
assert isinstance(url(address, private=private), ValidationError)

0 comments on commit 98664e3

Please sign in to comment.