PYTHON-2795 Improve host parsing and error messages
This commit is contained in:
parent
65a082d2b4
commit
73fcfb696e
56
pymongo/_ipaddress.py
Normal file
56
pymongo/_ipaddress.py
Normal 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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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__':
|
||||
|
||||
Loading…
Reference in New Issue
Block a user