Skip to content

Commit 2cdfd8e

Browse files
committed
Add validation to eliminate URIReference.is_valid
Even though URIReference.is_valid does the same thing (roughly), this adds to our existing Validator framework the ability to check that a URI is in fact compliant with the specification.
1 parent a3bca13 commit 2cdfd8e

File tree

3 files changed

+146
-0
lines changed

3 files changed

+146
-0
lines changed

src/rfc3986/exceptions.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,22 @@ def __init__(self, uri):
9090
)
9191
)
9292
self.uri = uri
93+
94+
95+
class InvalidComponentsError(ValidationError):
96+
"""Exception raised when one or more components are invalid."""
97+
98+
def __init__(self, uri, *component_names):
99+
"""Initialize the error with the invalid component name(s)."""
100+
verb = 'was'
101+
if len(component_names) > 1:
102+
verb = 'were'
103+
104+
self.uri = uri
105+
self.components = sorted(component_names)
106+
components = ', '.join(self.components)
107+
super(InvalidComponentsError, self).__init__(
108+
"{} {} found to be invalid".format(components, verb),
109+
uri,
110+
self.components,
111+
)

src/rfc3986/validators.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def __init__(self):
7070
'query': False,
7171
'fragment': False,
7272
}
73+
self.validated_components = self.required_components.copy()
7374

7475
def allow_schemes(self, *schemes):
7576
"""Require the scheme to be one of the provided schemes.
@@ -147,9 +148,36 @@ def forbid_use_of_password(self):
147148
self.allow_password = False
148149
return self
149150

151+
def check_validity_of(self, *components):
152+
"""Check the validity of the components provided.
153+
154+
This can be specified repeatedly.
155+
156+
.. versionadded:: 1.1
157+
158+
:param components:
159+
Names of components from :attr:`Validator.COMPONENT_NAMES`.
160+
:returns:
161+
The validator instance.
162+
:rtype:
163+
Validator
164+
"""
165+
components = [c.lower() for c in components]
166+
for component in components:
167+
if component not in self.COMPONENT_NAMES:
168+
raise ValueError(
169+
'"{}" is not a valid component'.format(component)
170+
)
171+
self.validated_components.update({
172+
component: True for component in components
173+
})
174+
return self
175+
150176
def require_presence_of(self, *components):
151177
"""Require the components provided.
152178
179+
This can be specified repeatedly.
180+
153181
.. versionadded:: 1.0
154182
155183
:param components:
@@ -186,6 +214,8 @@ def validate(self, uri):
186214
:raises PasswordForbidden:
187215
When a password is present in the userinfo component but is
188216
not permitted by configuration.
217+
:raises InvalidComponentsError:
218+
When a component was found to be invalid.
189219
"""
190220
if not self.allow_password:
191221
check_password(uri)
@@ -195,8 +225,15 @@ def validate(self, uri):
195225
for component, required in self.required_components.items()
196226
if required
197227
]
228+
validated_components = [
229+
component
230+
for component, required in self.validated_components.items()
231+
if required
232+
]
198233
if required_components:
199234
ensure_required_components_exist(uri, required_components)
235+
if validated_components:
236+
ensure_components_are_valid(uri, validated_components)
200237

201238
ensure_one_of(self.allowed_schemes, uri, 'scheme')
202239
ensure_one_of(self.allowed_hosts, uri, 'host')
@@ -337,3 +374,52 @@ def valid_ipv4_host_address(host):
337374
# If the host exists, and it might be IPv4, check each byte in the
338375
# address.
339376
return all([0 <= int(byte, base=10) <= 255 for byte in host.split('.')])
377+
378+
379+
_COMPONENT_VALIDATORS = {
380+
'scheme': scheme_is_valid,
381+
'path': path_is_valid,
382+
'query': query_is_valid,
383+
'fragment': fragment_is_valid,
384+
}
385+
386+
_SUBAUTHORITY_VALIDATORS = set(['userinfo', 'host', 'port'])
387+
388+
389+
def subauthority_component_is_valid(uri, component):
390+
"""Determine if the userinfo, host, and port are valid."""
391+
try:
392+
subauthority_dict = uri.authority_info()
393+
except exceptions.InvalidAuthority:
394+
return False
395+
396+
# If we can parse the authority into sub-components and we're not
397+
# validating the port, we can assume it's valid.
398+
if component != 'port':
399+
return True
400+
401+
try:
402+
port = int(subauthority_dict['port'])
403+
except TypeError:
404+
# If the port wasn't provided it'll be None and int(None) raises a
405+
# TypeError
406+
return True
407+
408+
return (0 <= port <= 65535)
409+
410+
411+
def ensure_components_are_valid(uri, validated_components):
412+
"""Assert that all components are valid in the URI."""
413+
invalid_components = set([])
414+
for component in validated_components:
415+
if component in _SUBAUTHORITY_VALIDATORS:
416+
if not subauthority_component_is_valid(uri, component):
417+
invalid_components.add(component)
418+
continue
419+
420+
validator = _COMPONENT_VALIDATORS[component]
421+
if not validator(getattr(uri, component)):
422+
invalid_components.add(component)
423+
424+
if invalid_components:
425+
raise exceptions.InvalidComponentsError(uri, *invalid_components)

tests/test_validators.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ def test_requiring_invalid_component():
5252
validators.Validator().require_presence_of('frob')
5353

5454

55+
def test_checking_validity_of_component():
56+
"""Verify that we validate components we're validating."""
57+
with pytest.raises(ValueError):
58+
validators.Validator().check_validity_of('frob')
59+
60+
5561
def test_use_of_password():
5662
"""Verify the behaviour of {forbid,allow}_use_of_password."""
5763
validator = validators.Validator()
@@ -182,6 +188,22 @@ def test_allowed_hosts_and_schemes(uri, failed_component):
182188
rfc3986.uri_reference('ssh://git.openstack.org/sigmavirus24'),
183189
rfc3986.uri_reference('ssh://[email protected]:22/sigmavirus24'),
184190
rfc3986.uri_reference('https://git.openstack.org:443/sigmavirus24'),
191+
rfc3986.uri_reference(
192+
'ssh://[email protected]:22/sigmavirus24?foo=bar#fragment'
193+
),
194+
rfc3986.uri_reference(
195+
'ssh://git.openstack.org:22/sigmavirus24?foo=bar#fragment'
196+
),
197+
rfc3986.uri_reference('ssh://git.openstack.org:22/?foo=bar#fragment'),
198+
rfc3986.uri_reference('ssh://git.openstack.org:22/sigmavirus24#fragment'),
199+
rfc3986.uri_reference('ssh://git.openstack.org:22/#fragment'),
200+
rfc3986.uri_reference('ssh://git.openstack.org:22/'),
201+
rfc3986.uri_reference('ssh://[email protected]:22/?foo=bar#fragment'),
202+
rfc3986.uri_reference(
203+
'ssh://[email protected]:22/sigmavirus24#fragment'
204+
),
205+
rfc3986.uri_reference('ssh://[email protected]:22/#fragment'),
206+
rfc3986.uri_reference('ssh://[email protected]:22/'),
185207
])
186208
def test_successful_complex_validation(uri):
187209
"""Verify we do not raise ValidationErrors for good URIs."""
@@ -193,4 +215,23 @@ def test_successful_complex_validation(uri):
193215
'22', '443',
194216
).require_presence_of(
195217
'scheme', 'host', 'path',
218+
).check_validity_of(
219+
'scheme', 'userinfo', 'host', 'port', 'path', 'query', 'fragment',
196220
).validate(uri)
221+
222+
223+
def test_invalid_uri_generates_error(invalid_uri):
224+
"""Verify we catch invalid URIs."""
225+
uri = rfc3986.uri_reference(invalid_uri)
226+
with pytest.raises(exceptions.InvalidComponentsError):
227+
validators.Validator().check_validity_of('host').validate(uri)
228+
229+
230+
def test_invalid_uri_with_invalid_path(invalid_uri):
231+
"""Verify we catch multiple invalid components."""
232+
uri = rfc3986.uri_reference(invalid_uri)
233+
uri = uri.copy_with(path='#foobar')
234+
with pytest.raises(exceptions.InvalidComponentsError):
235+
validators.Validator().check_validity_of(
236+
'host', 'path',
237+
).validate(uri)

0 commit comments

Comments
 (0)