Skip to content

Commit 73fcfb6

Browse files
committed
PYTHON-2795 Improve host parsing and error messages
1 parent 65a082d commit 73fcfb6

File tree

6 files changed

+94
-50
lines changed

6 files changed

+94
-50
lines changed

pymongo/_ipaddress.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Copyright 2021-present MongoDB, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Test if a string is an IP Address"""
16+
17+
import socket
18+
19+
from bson.py3compat import _unicode
20+
21+
try:
22+
from ipaddress import ip_address
23+
def is_ip_address(address):
24+
try:
25+
ip_address(_unicode(address))
26+
return True
27+
except (ValueError, UnicodeError):
28+
return False
29+
except ImportError:
30+
if hasattr(socket, 'inet_pton') and socket.has_ipv6:
31+
# Most *nix, Windows newer than XP
32+
def is_ip_address(address):
33+
try:
34+
# inet_pton rejects IPv4 literals with leading zeros
35+
# (e.g. 192.168.0.01), inet_aton does not, and we
36+
# can connect to them without issue. Use inet_aton.
37+
socket.inet_aton(address)
38+
return True
39+
except socket.error:
40+
try:
41+
socket.inet_pton(socket.AF_INET6, address)
42+
return True
43+
except socket.error:
44+
return False
45+
else:
46+
# No inet_pton
47+
def is_ip_address(address):
48+
try:
49+
socket.inet_aton(address)
50+
return True
51+
except socket.error:
52+
if ':' in address:
53+
# ':' is not a valid character for a hostname.
54+
return True
55+
return False
56+

pymongo/mongo_client.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -658,7 +658,10 @@ def __init__(
658658
opts = common._CaseInsensitiveDictionary()
659659
fqdn = None
660660
for entity in host:
661-
if "://" in entity:
661+
# A hostname can only include a-z, 0-9, '-' and '.'. If we find a '/'
662+
# it must be a URI,
663+
# https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
664+
if "/" in entity:
662665
# Determine connection timeout from kwargs.
663666
timeout = keyword_opts.get("connecttimeoutms")
664667
if timeout is not None:

pymongo/pool.py

Lines changed: 1 addition & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
OperationFailure,
5353
PyMongoError)
5454
from pymongo.hello import HelloCompat
55+
from pymongo._ipaddress import is_ip_address
5556
from pymongo.ismaster import IsMaster
5657
from pymongo.monotonic import time as _time
5758
from pymongo.monitoring import (ConnectionCheckOutFailedReason,
@@ -65,51 +66,6 @@
6566
# Always use our backport so we always have support for IP address matching
6667
from pymongo.ssl_match_hostname import match_hostname
6768

68-
# For SNI support. According to RFC6066, section 3, IPv4 and IPv6 literals are
69-
# not permitted for SNI hostname.
70-
try:
71-
from ipaddress import ip_address
72-
def is_ip_address(address):
73-
try:
74-
ip_address(_unicode(address))
75-
return True
76-
except (ValueError, UnicodeError):
77-
return False
78-
except ImportError:
79-
if hasattr(socket, 'inet_pton') and socket.has_ipv6:
80-
# Most *nix, recent Windows
81-
def is_ip_address(address):
82-
try:
83-
# inet_pton rejects IPv4 literals with leading zeros
84-
# (e.g. 192.168.0.01), inet_aton does not, and we
85-
# can connect to them without issue. Use inet_aton.
86-
socket.inet_aton(address)
87-
return True
88-
except socket.error:
89-
try:
90-
socket.inet_pton(socket.AF_INET6, address)
91-
return True
92-
except socket.error:
93-
return False
94-
else:
95-
# No inet_pton
96-
def is_ip_address(address):
97-
try:
98-
socket.inet_aton(address)
99-
return True
100-
except socket.error:
101-
if ':' in address:
102-
# ':' is not a valid character for a hostname. If we get
103-
# here a few things have to be true:
104-
# - We're on a recent version of python 2.7 (2.7.9+).
105-
# Older 2.7 versions don't support SNI.
106-
# - We're on Windows XP or some unusual Unix that doesn't
107-
# have inet_pton.
108-
# - The application is using IPv6 literals with TLS, which
109-
# is pretty unusual.
110-
return True
111-
return False
112-
11369
try:
11470
from fcntl import fcntl, F_GETFD, F_SETFD, FD_CLOEXEC
11571
def _set_non_inheritable_non_atomic(fd):

pymongo/srv_resolver.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
from pymongo.common import CONNECT_TIMEOUT
2626
from pymongo.errors import ConfigurationError
27+
from pymongo._ipaddress import is_ip_address
2728

2829

2930
if PY3:
@@ -46,20 +47,26 @@ def _resolve(*args, **kwargs):
4647
# dnspython 1.X
4748
return resolver.query(*args, **kwargs)
4849

50+
_INVALID_HOST_MSG = (
51+
"Invalid URI host: %s is not a valid hostname for 'mongodb+srv://'. "
52+
"Did you mean to use 'mongodb://'?")
4953

5054
class _SrvResolver(object):
5155
def __init__(self, fqdn, connect_timeout=None):
5256
self.__fqdn = fqdn
5357
self.__connect_timeout = connect_timeout or CONNECT_TIMEOUT
5458

5559
# Validate the fully qualified domain name.
60+
if is_ip_address(fqdn):
61+
raise ConfigurationError(_INVALID_HOST_MSG % ("an IP address",))
62+
5663
try:
5764
self.__plist = self.__fqdn.split(".")[1:]
5865
except Exception:
59-
raise ConfigurationError("Invalid URI host: %s" % (fqdn,))
66+
raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,))
6067
self.__slen = len(self.__plist)
6168
if self.__slen < 2:
62-
raise ConfigurationError("Invalid URI host: %s" % (fqdn,))
69+
raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,))
6370

6471
def get_options(self):
6572
try:

test/test_client.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,20 @@ def test_max_pool_size_zero(self):
169169
with self.assertRaises(ValueError):
170170
MongoClient(maxPoolSize=0)
171171

172+
def test_uri_detection(self):
173+
self.assertRaises(
174+
ConfigurationError,
175+
MongoClient,
176+
"/foo")
177+
self.assertRaises(
178+
ConfigurationError,
179+
MongoClient,
180+
"://")
181+
self.assertRaises(
182+
ConfigurationError,
183+
MongoClient,
184+
"foo/")
185+
172186
def test_get_db(self):
173187
def make_db(base, name):
174188
return base[name]

test/test_dns.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,20 @@ class TestParsingErrors(unittest.TestCase):
142142
def test_invalid_host(self):
143143
self.assertRaisesRegex(
144144
ConfigurationError,
145-
"Invalid URI host: mongodb",
145+
"Invalid URI host: mongodb is not",
146146
MongoClient, "mongodb+srv://mongodb")
147147
self.assertRaisesRegex(
148148
ConfigurationError,
149-
"Invalid URI host: mongodb.com",
149+
"Invalid URI host: mongodb.com is not",
150150
MongoClient, "mongodb+srv://mongodb.com")
151+
self.assertRaisesRegex(
152+
ConfigurationError,
153+
"Invalid URI host: an IP address is not",
154+
MongoClient, "mongodb+srv://127.0.0.1")
155+
self.assertRaisesRegex(
156+
ConfigurationError,
157+
"Invalid URI host: an IP address is not",
158+
MongoClient, "mongodb+srv://[::1]")
151159

152160

153161
if __name__ == '__main__':

0 commit comments

Comments
 (0)