PYTHON-2795 Improve host parsing and error messages

This commit is contained in:
Bernie Hackett 2021-07-07 17:04:46 -07:00
parent 65a082d2b4
commit 73fcfb696e
6 changed files with 94 additions and 50 deletions

56
pymongo/_ipaddress.py Normal file
View File

@ -0,0 +1,56 @@
# Copyright 2021-present MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Test if a string is an IP Address"""
import socket
from bson.py3compat import _unicode
try:
from ipaddress import ip_address
def is_ip_address(address):
try:
ip_address(_unicode(address))
return True
except (ValueError, UnicodeError):
return False
except ImportError:
if hasattr(socket, 'inet_pton') and socket.has_ipv6:
# Most *nix, Windows newer than XP
def is_ip_address(address):
try:
# inet_pton rejects IPv4 literals with leading zeros
# (e.g. 192.168.0.01), inet_aton does not, and we
# can connect to them without issue. Use inet_aton.
socket.inet_aton(address)
return True
except socket.error:
try:
socket.inet_pton(socket.AF_INET6, address)
return True
except socket.error:
return False
else:
# No inet_pton
def is_ip_address(address):
try:
socket.inet_aton(address)
return True
except socket.error:
if ':' in address:
# ':' is not a valid character for a hostname.
return True
return False

View File

@ -658,7 +658,10 @@ class MongoClient(common.BaseObject):
opts = common._CaseInsensitiveDictionary()
fqdn = None
for entity in host:
if "://" in entity:
# A hostname can only include a-z, 0-9, '-' and '.'. If we find a '/'
# it must be a URI,
# https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
if "/" in entity:
# Determine connection timeout from kwargs.
timeout = keyword_opts.get("connecttimeoutms")
if timeout is not None:

View File

@ -52,6 +52,7 @@ from pymongo.errors import (AutoReconnect,
OperationFailure,
PyMongoError)
from pymongo.hello import HelloCompat
from pymongo._ipaddress import is_ip_address
from pymongo.ismaster import IsMaster
from pymongo.monotonic import time as _time
from pymongo.monitoring import (ConnectionCheckOutFailedReason,
@ -65,51 +66,6 @@ from pymongo.socket_checker import SocketChecker
# Always use our backport so we always have support for IP address matching
from pymongo.ssl_match_hostname import match_hostname
# For SNI support. According to RFC6066, section 3, IPv4 and IPv6 literals are
# not permitted for SNI hostname.
try:
from ipaddress import ip_address
def is_ip_address(address):
try:
ip_address(_unicode(address))
return True
except (ValueError, UnicodeError):
return False
except ImportError:
if hasattr(socket, 'inet_pton') and socket.has_ipv6:
# Most *nix, recent Windows
def is_ip_address(address):
try:
# inet_pton rejects IPv4 literals with leading zeros
# (e.g. 192.168.0.01), inet_aton does not, and we
# can connect to them without issue. Use inet_aton.
socket.inet_aton(address)
return True
except socket.error:
try:
socket.inet_pton(socket.AF_INET6, address)
return True
except socket.error:
return False
else:
# No inet_pton
def is_ip_address(address):
try:
socket.inet_aton(address)
return True
except socket.error:
if ':' in address:
# ':' is not a valid character for a hostname. If we get
# here a few things have to be true:
# - We're on a recent version of python 2.7 (2.7.9+).
# Older 2.7 versions don't support SNI.
# - We're on Windows XP or some unusual Unix that doesn't
# have inet_pton.
# - The application is using IPv6 literals with TLS, which
# is pretty unusual.
return True
return False
try:
from fcntl import fcntl, F_GETFD, F_SETFD, FD_CLOEXEC
def _set_non_inheritable_non_atomic(fd):

View File

@ -24,6 +24,7 @@ from bson.py3compat import PY3
from pymongo.common import CONNECT_TIMEOUT
from pymongo.errors import ConfigurationError
from pymongo._ipaddress import is_ip_address
if PY3:
@ -46,6 +47,9 @@ def _resolve(*args, **kwargs):
# dnspython 1.X
return resolver.query(*args, **kwargs)
_INVALID_HOST_MSG = (
"Invalid URI host: %s is not a valid hostname for 'mongodb+srv://'. "
"Did you mean to use 'mongodb://'?")
class _SrvResolver(object):
def __init__(self, fqdn, connect_timeout=None):
@ -53,13 +57,16 @@ class _SrvResolver(object):
self.__connect_timeout = connect_timeout or CONNECT_TIMEOUT
# Validate the fully qualified domain name.
if is_ip_address(fqdn):
raise ConfigurationError(_INVALID_HOST_MSG % ("an IP address",))
try:
self.__plist = self.__fqdn.split(".")[1:]
except Exception:
raise ConfigurationError("Invalid URI host: %s" % (fqdn,))
raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,))
self.__slen = len(self.__plist)
if self.__slen < 2:
raise ConfigurationError("Invalid URI host: %s" % (fqdn,))
raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,))
def get_options(self):
try:

View File

@ -169,6 +169,20 @@ class ClientUnitTest(unittest.TestCase):
with self.assertRaises(ValueError):
MongoClient(maxPoolSize=0)
def test_uri_detection(self):
self.assertRaises(
ConfigurationError,
MongoClient,
"/foo")
self.assertRaises(
ConfigurationError,
MongoClient,
"://")
self.assertRaises(
ConfigurationError,
MongoClient,
"foo/")
def test_get_db(self):
def make_db(base, name):
return base[name]

View File

@ -142,12 +142,20 @@ class TestParsingErrors(unittest.TestCase):
def test_invalid_host(self):
self.assertRaisesRegex(
ConfigurationError,
"Invalid URI host: mongodb",
"Invalid URI host: mongodb is not",
MongoClient, "mongodb+srv://mongodb")
self.assertRaisesRegex(
ConfigurationError,
"Invalid URI host: mongodb.com",
"Invalid URI host: mongodb.com is not",
MongoClient, "mongodb+srv://mongodb.com")
self.assertRaisesRegex(
ConfigurationError,
"Invalid URI host: an IP address is not",
MongoClient, "mongodb+srv://127.0.0.1")
self.assertRaisesRegex(
ConfigurationError,
"Invalid URI host: an IP address is not",
MongoClient, "mongodb+srv://[::1]")
if __name__ == '__main__':