Skip to content

Commit cab0c87

Browse files
authored
Merge pull request #356 from yozachar/workshop
feat: adds `private` parameter to `ip_address`, `hostname` & `url`
2 parents a2b4fc0 + 9327008 commit cab0c87

File tree

7 files changed

+105
-11
lines changed

7 files changed

+105
-11
lines changed

CHANGES.md

+19
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,25 @@ Note to self: Breaking changes must increment either
99
1010
-->
1111

12+
## 0.27.0 (2024-04-03)
13+
14+
_**Breaking**_ ⚠️
15+
16+
- patch: moves `base58` and `base64` into `encoding` by @yozachar in [#354](https://github.com/python-validators/validators/pull/354)
17+
18+
_**Features**_
19+
20+
- feat: lays foundation for URI validation by @yozachar in [#353](https://github.com/python-validators/validators/pull/353)
21+
- feat: adds `private` parameter to `ip_address`, `hostname` & `url` by @yozachar in [#356](https://github.com/python-validators/validators/pull/356)
22+
23+
_**Maintenance**_
24+
25+
- patch: adds `encoding` tests and docs by @yozachar in [#355](https://github.com/python-validators/validators/pull/355)
26+
27+
**Full Changelog**: [`0.26.0...0.27.0`](https://github.com/python-validators/validators/compare/0.26.0...0.27.0)
28+
29+
---
30+
1231
## 0.26.0 (2024-04-02)
1332

1433
_**Breaking**_

SECURITY.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
| Version | Supported |
66
| ---------- | ------------------ |
7-
| `>=0.26.0` | :white_check_mark: |
7+
| `>=0.27.0` | :white_check_mark: |
88

99
## Reporting a Vulnerability
1010

src/validators/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,4 @@
8585
"validator",
8686
)
8787

88-
__version__ = "0.26.0"
88+
__version__ = "0.27.0"

src/validators/hostname.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# standard
44
from functools import lru_cache
55
import re
6+
from typing import Optional
67

78
from .domain import domain
89

@@ -54,6 +55,7 @@ def hostname(
5455
skip_ipv4_addr: bool = False,
5556
may_have_port: bool = True,
5657
maybe_simple: bool = True,
58+
private: Optional[bool] = None, # only for ip-addresses
5759
rfc_1034: bool = False,
5860
rfc_2782: bool = False,
5961
):
@@ -92,6 +94,8 @@ def hostname(
9294
Hostname string may contain port number.
9395
maybe_simple:
9496
Hostname string maybe only hyphens and alpha-numerals.
97+
private:
98+
Embedded IP address is public if `False`, private/local if `True`.
9599
rfc_1034:
96100
Allow trailing dot in domain/host name.
97101
Ref: [RFC 1034](https://www.rfc-editor.org/rfc/rfc1034).
@@ -110,13 +114,13 @@ def hostname(
110114
return (
111115
(_simple_hostname_regex().match(host_seg) if maybe_simple else False)
112116
or domain(host_seg, rfc_1034=rfc_1034, rfc_2782=rfc_2782)
113-
or (False if skip_ipv4_addr else ipv4(host_seg, cidr=False))
117+
or (False if skip_ipv4_addr else ipv4(host_seg, cidr=False, private=private))
114118
or (False if skip_ipv6_addr else ipv6(host_seg, cidr=False))
115119
)
116120

117121
return (
118122
(_simple_hostname_regex().match(value) if maybe_simple else False)
119123
or domain(value, rfc_1034=rfc_1034, rfc_2782=rfc_2782)
120-
or (False if skip_ipv4_addr else ipv4(value, cidr=False))
124+
or (False if skip_ipv4_addr else ipv4(value, cidr=False, private=private))
121125
or (False if skip_ipv6_addr else ipv6(value, cidr=False))
122126
)

src/validators/ip_address.py

+40-7
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,44 @@
99
IPv6Network,
1010
NetmaskValueError,
1111
)
12+
import re
13+
from typing import Optional
1214

1315
# local
1416
from .utils import validator
1517

1618

19+
def _check_private_ip(value: str, is_private: Optional[bool]):
20+
if is_private is None:
21+
return True
22+
if is_private and (
23+
any(
24+
value.startswith(l_bit)
25+
for l_bit in {
26+
"10.", # private
27+
"192.168.", # private
28+
"169.254.", # link-local
29+
"127.", # localhost
30+
"0.0.0.0", # loopback #nosec
31+
}
32+
)
33+
or re.match(r"^172\.(?:1[6-9]|2\d|3[0-1])\.", value) # private
34+
or re.match(r"^(?:22[4-9]|23[0-9]|24[0-9]|25[0-5])\.", value) # broadcast
35+
):
36+
return True
37+
return False
38+
39+
1740
@validator
18-
def ipv4(value: str, /, *, cidr: bool = True, strict: bool = False, host_bit: bool = True):
41+
def ipv4(
42+
value: str,
43+
/,
44+
*,
45+
cidr: bool = True,
46+
strict: bool = False,
47+
private: Optional[bool] = None,
48+
host_bit: bool = True,
49+
):
1950
"""Returns whether a given value is a valid IPv4 address.
2051
2152
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
3667
value:
3768
IP address string to validate.
3869
cidr:
39-
IP address string may contain CIDR notation
70+
IP address string may contain CIDR notation.
4071
strict:
41-
IP address string is strictly in CIDR notation
72+
IP address string is strictly in CIDR notation.
73+
private:
74+
IP address is public if `False`, private/local/loopback/broadcast if `True`.
4275
host_bit:
4376
If `False` and host bits (along with network bits) _are_ set in the supplied
4477
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
5487
if cidr:
5588
if strict and value.count("/") != 1:
5689
raise ValueError("IPv4 address was expected in CIDR notation")
57-
return IPv4Network(value, strict=not host_bit)
58-
return IPv4Address(value)
90+
return IPv4Network(value, strict=not host_bit) and _check_private_ip(value, private)
91+
return IPv4Address(value) and _check_private_ip(value, private)
5992
except (ValueError, AddressValueError, NetmaskValueError):
6093
return False
6194

@@ -81,9 +114,9 @@ def ipv6(value: str, /, *, cidr: bool = True, strict: bool = False, host_bit: bo
81114
value:
82115
IP address string to validate.
83116
cidr:
84-
IP address string may contain CIDR annotation
117+
IP address string may contain CIDR annotation.
85118
strict:
86-
IP address string is strictly in CIDR notation
119+
IP address string is strictly in CIDR notation.
87120
host_bit:
88121
If `False` and host bits (along with network bits) _are_ set in the supplied
89122
address, this function raises a validation error. ref [IPv6Network][2].

src/validators/url.py

+8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# standard
44
from functools import lru_cache
55
import re
6+
from typing import Optional
67
from urllib.parse import parse_qs, unquote, urlsplit
78

89
# local
@@ -80,6 +81,7 @@ def _validate_netloc(
8081
skip_ipv4_addr: bool,
8182
may_have_port: bool,
8283
simple_host: bool,
84+
private: Optional[bool],
8385
rfc_1034: bool,
8486
rfc_2782: bool,
8587
):
@@ -97,6 +99,7 @@ def _validate_netloc(
9799
skip_ipv4_addr=skip_ipv4_addr,
98100
may_have_port=may_have_port,
99101
maybe_simple=simple_host,
102+
private=private,
100103
rfc_1034=rfc_1034,
101104
rfc_2782=rfc_2782,
102105
)
@@ -111,6 +114,7 @@ def _validate_netloc(
111114
skip_ipv4_addr=skip_ipv4_addr,
112115
may_have_port=may_have_port,
113116
maybe_simple=simple_host,
117+
private=private,
114118
rfc_1034=rfc_1034,
115119
rfc_2782=rfc_2782,
116120
) and _validate_auth_segment(basic_auth)
@@ -151,6 +155,7 @@ def url(
151155
may_have_port: bool = True,
152156
simple_host: bool = False,
153157
strict_query: bool = True,
158+
private: Optional[bool] = None, # only for ip-addresses
154159
rfc_1034: bool = False,
155160
rfc_2782: bool = False,
156161
):
@@ -191,6 +196,8 @@ def url(
191196
URL string maybe only hyphens and alpha-numerals.
192197
strict_query:
193198
Fail validation on query string parsing error.
199+
private:
200+
Embedded IP address is public if `False`, private/local if `True`.
194201
rfc_1034:
195202
Allow trailing dot in domain/host name.
196203
Ref: [RFC 1034](https://www.rfc-editor.org/rfc/rfc1034).
@@ -220,6 +227,7 @@ def url(
220227
skip_ipv4_addr,
221228
may_have_port,
222229
simple_host,
230+
private,
223231
rfc_1034,
224232
rfc_2782,
225233
)

tests/test_url.py

+30
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
"""Test URL."""
22

3+
# standard
4+
from typing import Optional
5+
36
# external
47
import pytest
58

@@ -106,6 +109,19 @@ def test_returns_true_on_valid_url(value: str):
106109
assert url(value)
107110

108111

112+
@pytest.mark.parametrize(
113+
"value, private",
114+
[
115+
("http://username:[email protected]/", True),
116+
("http://username:[email protected]:4010/", True),
117+
("http://127.0.0.1", True),
118+
],
119+
)
120+
def test_returns_true_on_valid_private_url(value: str, private: Optional[bool]):
121+
"""Test returns true on valid private url."""
122+
assert url(value, private=private)
123+
124+
109125
@pytest.mark.parametrize(
110126
"value",
111127
[
@@ -188,3 +204,17 @@ def test_returns_true_on_valid_url(value: str):
188204
def test_returns_failed_validation_on_invalid_url(value: str):
189205
"""Test returns failed validation on invalid url."""
190206
assert isinstance(url(value), ValidationError)
207+
208+
209+
@pytest.mark.parametrize(
210+
"value, private",
211+
[
212+
("http://username:[email protected]:4010", False),
213+
("http://username:[email protected]:8080", False),
214+
("http://10.0.10.1", False),
215+
("http://255.255.255.255", False),
216+
],
217+
)
218+
def test_returns_failed_validation_on_invalid_private_url(value: str, private: Optional[bool]):
219+
"""Test returns failed validation on invalid private url."""
220+
assert isinstance(url(value, private=private), ValidationError)

0 commit comments

Comments
 (0)