diff --git a/CHANGES.md b/CHANGES.md index e2eb3017..1913b961 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,25 @@ Note to self: Breaking changes must increment either --> +## 0.27.0 (2024-04-03) + +_**Breaking**_ ⚠️ + +- patch: moves `base58` and `base64` into `encoding` by @yozachar in [#354](https://github.com/python-validators/validators/pull/354) + +_**Features**_ + +- feat: lays foundation for URI validation by @yozachar in [#353](https://github.com/python-validators/validators/pull/353) +- feat: adds `private` parameter to `ip_address`, `hostname` & `url` by @yozachar in [#356](https://github.com/python-validators/validators/pull/356) + +_**Maintenance**_ + +- patch: adds `encoding` tests and docs by @yozachar in [#355](https://github.com/python-validators/validators/pull/355) + +**Full Changelog**: [`0.26.0...0.27.0`](https://github.com/python-validators/validators/compare/0.26.0...0.27.0) + +--- + ## 0.26.0 (2024-04-02) _**Breaking**_ diff --git a/SECURITY.md b/SECURITY.md index e2bc917d..e0b76b7c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,7 @@ | Version | Supported | | ---------- | ------------------ | -| `>=0.26.0` | :white_check_mark: | +| `>=0.27.0` | :white_check_mark: | ## Reporting a Vulnerability diff --git a/src/validators/__init__.py b/src/validators/__init__.py index dc8513e0..a64fb0e0 100644 --- a/src/validators/__init__.py +++ b/src/validators/__init__.py @@ -85,4 +85,4 @@ "validator", ) -__version__ = "0.26.0" +__version__ = "0.27.0" diff --git a/src/validators/hostname.py b/src/validators/hostname.py index 55b30647..10450585 100644 --- a/src/validators/hostname.py +++ b/src/validators/hostname.py @@ -3,6 +3,7 @@ # standard from functools import lru_cache import re +from typing import Optional from .domain import domain @@ -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, ): @@ -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). @@ -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)) ) diff --git a/src/validators/ip_address.py b/src/validators/ip_address.py index 64bf15f4..66ca2e99 100644 --- a/src/validators/ip_address.py +++ b/src/validators/ip_address.py @@ -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 @@ -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]. @@ -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 @@ -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]. diff --git a/src/validators/url.py b/src/validators/url.py index 57c3a6bd..38d898ec 100644 --- a/src/validators/url.py +++ b/src/validators/url.py @@ -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 @@ -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, ): @@ -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, ) @@ -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) @@ -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, ): @@ -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). @@ -220,6 +227,7 @@ def url( skip_ipv4_addr, may_have_port, simple_host, + private, rfc_1034, rfc_2782, ) diff --git a/tests/test_url.py b/tests/test_url.py index 079e62e2..6b365833 100644 --- a/tests/test_url.py +++ b/tests/test_url.py @@ -1,5 +1,8 @@ """Test URL.""" +# standard +from typing import Optional + # external import pytest @@ -106,6 +109,19 @@ def test_returns_true_on_valid_url(value: str): assert url(value) +@pytest.mark.parametrize( + "value, private", + [ + ("http://username:password@10.0.10.1/", True), + ("http://username:password@192.168.10.10:4010/", True), + ("http://127.0.0.1", True), + ], +) +def test_returns_true_on_valid_private_url(value: str, private: Optional[bool]): + """Test returns true on valid private url.""" + assert url(value, private=private) + + @pytest.mark.parametrize( "value", [ @@ -188,3 +204,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( + "value, private", + [ + ("http://username:password@192.168.10.10:4010", False), + ("http://username:password@127.0.0.1:8080", False), + ("http://10.0.10.1", False), + ("http://255.255.255.255", False), + ], +) +def test_returns_failed_validation_on_invalid_private_url(value: str, private: Optional[bool]): + """Test returns failed validation on invalid private url.""" + assert isinstance(url(value, private=private), ValidationError)