Skip to content

Commit fe89a35

Browse files
committed
Subclass URLValidator for URIValidator
1 parent f780035 commit fe89a35

File tree

3 files changed

+28
-37
lines changed

3 files changed

+28
-37
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* **Compatibility**: Python 3.4 is the new minimum required version.
44
* **Compatibility**: Django 2.0 is the new minimum required version.
55
* **New feature**: Added TokenMatchesOASRequirements Permissions.
6+
* validators.URIValidator has been updated to match URLValidator behaviour more closely.
67

78

89
### 1.1.2 [2018-05-12]

oauth2_provider/validators.py

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,58 +2,39 @@
22
from urllib.parse import urlsplit, urlunsplit
33

44
from django.core.exceptions import ValidationError
5-
from django.core.validators import RegexValidator
5+
from django.core.validators import URLValidator
66
from django.utils.encoding import force_text
77
from django.utils.translation import ugettext_lazy as _
88

99
from .settings import oauth2_settings
1010

1111

12-
class URIValidator(RegexValidator):
13-
regex = re.compile(
14-
r"^(?:[a-z][a-z0-9\.\-\+]*)://" # scheme...
15-
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # domain...
16-
r"(?!-)[A-Z\d-]{1,63}(?<!-)|" # also cover non-dotted domain
17-
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|" # ...or ipv4
18-
r"\[?[A-F0-9]*:[A-F0-9:]+\]?)" # ...or ipv6
19-
r"(?::\d+)?" # optional port
20-
r"(?:/?|[/?]\S+)$", re.IGNORECASE)
21-
message = _("Enter a valid URL.")
12+
class URIValidator(URLValidator):
13+
scheme_re = r"^(?:[a-z][a-z0-9\.\-\+]*)://"
2214

23-
def __call__(self, value):
24-
try:
25-
super().__call__(value)
26-
except ValidationError as e:
27-
# Trivial case failed. Try for possible IDN domain
28-
if value:
29-
value = force_text(value)
30-
try:
31-
scheme, netloc, path, query, fragment = urlsplit(value)
32-
except ValueError as e:
33-
raise ValidationError("Cannot parse Redirect URI. Error: {}".format(e))
34-
try:
35-
netloc = netloc.encode("idna").decode("ascii") # IDN -> ACE
36-
except UnicodeError: # invalid domain part
37-
raise e
38-
url = urlunsplit((scheme, netloc, path, query, fragment))
39-
super().__call__(url)
40-
else:
41-
raise
42-
else:
43-
url = value
15+
dotless_domain_re = r"(?!-)[A-Z\d-]{1,63}(?<!-)"
16+
host_re = "|".join((
17+
r"(?:"+ URLValidator.host_re,
18+
URLValidator.ipv4_re,
19+
URLValidator.ipv6_re,
20+
dotless_domain_re + ")"
21+
))
22+
port_re = r"(?::\d{2,5})?"
23+
path_re = r"(?:[/?#][^\s]*)?"
24+
regex = re.compile(scheme_re + host_re + port_re + path_re, re.IGNORECASE)
4425

4526

4627
class RedirectURIValidator(URIValidator):
4728
def __init__(self, allowed_schemes):
48-
self.allowed_schemes = allowed_schemes
29+
super().__init__(schemes=allowed_schemes)
4930

5031
def __call__(self, value):
5132
super().__call__(value)
5233
value = force_text(value)
5334
if len(value.split("#")) > 1:
5435
raise ValidationError("Redirect URIs must not contain fragments")
5536
scheme, netloc, path, query, fragment = urlsplit(value)
56-
if scheme.lower() not in self.allowed_schemes:
37+
if scheme.lower() not in self.schemes:
5738
raise ValidationError("Redirect URI scheme is not allowed.")
5839

5940

tests/test_validators.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,23 @@ def test_validate_good_uris(self):
1313
"https://example.org/?key=val",
1414
"https://example",
1515
"https://localhost",
16+
"https://1.1.1.1",
17+
"https://127.0.0.1",
18+
"https://255.255.255.255",
1619
]
1720
for uri in good_uris:
1821
# Check ValidationError not thrown
1922
validator(uri)
2023

2124
def test_validate_custom_uri_scheme(self):
22-
validator = RedirectURIValidator(allowed_schemes=["my-scheme", "https"])
25+
validator = RedirectURIValidator(allowed_schemes=["my-scheme", "https", "git+ssh"])
2326
good_uris = [
2427
"my-scheme://example.com",
2528
"my-scheme://example",
2629
"my-scheme://localhost",
2730
"https://example.com",
2831
"HTTPS://example.com",
32+
"git+ssh://example.com",
2933
]
3034
for uri in good_uris:
3135
# Check ValidationError not thrown
@@ -39,15 +43,20 @@ def test_validate_whitespace_separators(self):
3943

4044
def test_validate_bad_uris(self):
4145
validator = RedirectURIValidator(allowed_schemes=["https"])
42-
oauth2_settings.ALLOWED_REDIRECT_URI_SCHEMES = ["https"]
46+
oauth2_settings.ALLOWED_REDIRECT_URI_SCHEMES = ["https", "good"]
4347
bad_uris = [
4448
"http:/example.com",
4549
"HTTP://localhost",
4650
"HTTP://example.com",
4751
"HTTP://example.com.",
4852
"http://example.com/#fragment",
49-
"my-scheme://example.com"
53+
"123://example.com",
54+
"http://fe80::1",
55+
"git+ssh://example.com",
56+
"my-scheme://example.com",
5057
"uri-without-a-scheme",
58+
"https://example.com/#fragment",
59+
"good://example.com/#fragment",
5160
" ",
5261
"",
5362
# Bad IPv6 URL, urlparse behaves differently for these

0 commit comments

Comments
 (0)