diff --git a/doc/changelog.rst b/doc/changelog.rst index 10904b4e30..1e782cdd70 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -16,6 +16,31 @@ Changes in Version 3.8.0 - :class:`~bson.objectid.ObjectId` now implements the `ObjectID specification version 0.2 `_. + +- Version 3.8.0 implements the `URI options specification`_ in the + :meth:`~pymongo.mongo_client.MongoClient` constructor. Consequently, there are + a number of changes in connection options: + + - The ``tlsInsecure`` option has been added. + - The ``tls`` option has been added. The older ``ssl`` option has been retained + as an alias to the new ``tls`` option. + - ``wTimeout`` has been deprecated in favor of ``wTimeoutMS``. + - ``wTimeoutMS`` now overrides ``wTimeout`` if the user provides both. + - ``j`` has been deprecated in favor of ``journal``. + - ``journal`` now overrides ``j`` if the user provides both. + - ``ssl_cert_reqs`` has been deprecated in favor of ``tlsAllowInvalidCertificates``. + Instead of ``ssl.CERT_NONE``, ``ssl.CERT_OPTIONAL`` and ``ssl.CERT_REQUIRED``, the + new option expects a boolean value - ``True`` is equivalent to ``ssl.CERT_NONE``, + while ``False`` is equivalent to ``ssl.CERT_REQUIRED``. + - ``ssl_match_hostname`` has been deprecated in favor of ``tlsAllowInvalidHostnames``. + - ``ssl_ca_certs`` has been deprecated in favor of ``tlsCAFile``. + - ``ssl_certfile`` has been deprecated in favor of ``tlsCertificateKeyFile``. + - ``ssl_pem_passphrase`` has been deprecated in favor of ``tlsCertificateKeyFilePassword``. + + +.. _URI options specification: https://github.com/mongodb/specifications/blob/master/source/uri-options/uri-options.rst` + + Issues Resolved ............... diff --git a/pymongo/client_options.py b/pymongo/client_options.py index 3c3865b32b..14541ae7dd 100644 --- a/pymongo/client_options.py +++ b/pymongo/client_options.py @@ -55,8 +55,8 @@ def _parse_read_preference(options): def _parse_write_concern(options): """Parse write concern options.""" concern = options.get('w') - wtimeout = options.get('wtimeout', options.get('wtimeoutms')) - j = options.get('j', options.get('journal')) + wtimeout = options.get('wtimeoutms') + j = options.get('journal') fsync = options.get('fsync') return WriteConcern(concern, wtimeout, j, fsync) diff --git a/pymongo/common.py b/pymongo/common.py index 91a8c2d928..8cbe5be8d7 100644 --- a/pymongo/common.py +++ b/pymongo/common.py @@ -32,7 +32,8 @@ from pymongo.monitoring import _validate_event_listeners from pymongo.read_concern import ReadConcern from pymongo.read_preferences import _MONGOS_MODES, _ServerMode -from pymongo.ssl_support import validate_cert_reqs +from pymongo.ssl_support import (validate_cert_reqs, + validate_allow_invalid_certs) from pymongo.write_concern import DEFAULT_WRITE_CONCERN, WriteConcern try: @@ -524,56 +525,79 @@ def validate_tzinfo(dummy, value): return value -# journal is an alias for j, -# wtimeoutms is an alias for wtimeout, -URI_VALIDATORS = { - 'replicaset': validate_string_or_none, - 'w': validate_non_negative_int_or_basestring, - 'wtimeout': validate_non_negative_integer, - 'wtimeoutms': validate_non_negative_integer, - 'fsync': validate_boolean_or_string, - 'j': validate_boolean_or_string, +# Dictionary where keys are the names of public URI options, and values +# are lists of aliases for that option. Aliases of option names are assumed +# to have been deprecated. +URI_OPTIONS_ALIAS_MAP = { + 'journal': ['j'], + 'wtimeoutms': ['wtimeout'], + 'tls': ['ssl'], + 'tlsallowinvalidcertificates': ['ssl_cert_reqs'], + 'tlsallowinvalidhostnames': ['ssl_match_hostname'], + 'tlscrlfile': ['ssl_crlfile'], + 'tlscafile': ['ssl_ca_certs'], + 'tlscertificatekeyfile': ['ssl_certfile'], + 'tlscertificatekeyfilepassword': ['ssl_pem_passphrase'], +} + +# Dictionary where keys are the names of URI options, and values +# are functions that validate user-input values for that option. If an option +# alias uses a different validator than its public counterpart, it should be +# included here as a key, value pair. +URI_OPTIONS_VALIDATOR_MAP = { + 'appname': validate_appname_or_none, + 'authmechanism': validate_auth_mechanism, + 'authmechanismproperties': validate_auth_mechanism_properties, + 'authsource': validate_string, + 'compressors': validate_compressors, + 'connecttimeoutms': validate_timeout_or_none, + 'heartbeatfrequencyms': validate_timeout_or_none, 'journal': validate_boolean_or_string, + 'localthresholdms': validate_positive_float_or_zero, + 'maxidletimems': validate_timeout_or_none, 'maxpoolsize': validate_positive_integer_or_none, - 'socketkeepalive': validate_boolean_or_string, - 'waitqueuemultiple': validate_non_negative_integer_or_none, - 'ssl': validate_boolean_or_string, - 'ssl_keyfile': validate_readable, - 'ssl_certfile': validate_readable, - 'ssl_pem_passphrase': validate_string_or_none, - 'ssl_cert_reqs': validate_cert_reqs, - 'ssl_ca_certs': validate_readable, - 'ssl_match_hostname': validate_boolean_or_string, - 'ssl_crlfile': validate_readable, + 'maxstalenessseconds': validate_max_staleness, 'readconcernlevel': validate_string_or_none, 'readpreference': validate_read_preference_mode, 'readpreferencetags': validate_read_preference_tags, - 'localthresholdms': validate_positive_float_or_zero, - 'authmechanism': validate_auth_mechanism, - 'authsource': validate_string, - 'authmechanismproperties': validate_auth_mechanism_properties, - 'tz_aware': validate_boolean_or_string, - 'uuidrepresentation': validate_uuid_representation, - 'connect': validate_boolean_or_string, - 'minpoolsize': validate_non_negative_integer, - 'appname': validate_appname_or_none, - 'driver': validate_driver_or_none, - 'unicode_decode_error_handler': validate_unicode_decode_error_handler, + 'replicaset': validate_string_or_none, 'retrywrites': validate_boolean_or_string, - 'compressors': validate_compressors, + 'serverselectiontimeoutms': validate_timeout_or_zero, + 'sockettimeoutms': validate_timeout_or_none, + 'ssl_keyfile': validate_readable, + 'tls': validate_boolean_or_string, + 'tlsallowinvalidcertificates': validate_allow_invalid_certs, + 'ssl_cert_reqs': validate_cert_reqs, + 'tlsallowinvalidhostnames': lambda *x: not validate_boolean_or_string(*x), + 'ssl_match_hostname': validate_boolean_or_string, + 'tlscafile': validate_readable, + 'tlscertificatekeyfile': validate_readable, + 'tlscertificatekeyfilepassword': validate_string_or_none, + 'tlsinsecure': validate_boolean_or_string, + 'w': validate_non_negative_int_or_basestring, + 'wtimeoutms': validate_non_negative_integer, 'zlibcompressionlevel': validate_zlib_compression_level, } -TIMEOUT_VALIDATORS = { - 'connecttimeoutms': validate_timeout_or_none, - 'sockettimeoutms': validate_timeout_or_none, +# Dictionary where keys are the names of URI options specific to pymongo, +# and values are functions that validate user-input values for those options. +NONSPEC_OPTIONS_VALIDATOR_MAP = { + 'connect': validate_boolean_or_string, + 'driver': validate_driver_or_none, + 'fsync': validate_boolean_or_string, + 'minpoolsize': validate_non_negative_integer, + 'socketkeepalive': validate_boolean_or_string, + 'tlscrlfile': validate_readable, + 'tz_aware': validate_boolean_or_string, + 'unicode_decode_error_handler': validate_unicode_decode_error_handler, + 'uuidrepresentation': validate_uuid_representation, + 'waitqueuemultiple': validate_non_negative_integer_or_none, 'waitqueuetimeoutms': validate_timeout_or_none, - 'serverselectiontimeoutms': validate_timeout_or_zero, - 'heartbeatfrequencyms': validate_timeout_or_none, - 'maxidletimems': validate_timeout_or_none, - 'maxstalenessseconds': validate_max_staleness, } +# Dictionary where keys are the names of keyword-only options for the +# MongoClient constructor, and values are functions that validate user-input +# values for those options. KW_VALIDATORS = { 'document_class': validate_document_class, 'read_preference': validate_read_preference, @@ -584,10 +608,57 @@ def validate_tzinfo(dummy, value): 'server_selector': validate_is_callable_or_none, } -URI_VALIDATORS.update(TIMEOUT_VALIDATORS) -VALIDATORS = URI_VALIDATORS.copy() +# Dictionary where keys are any URI option name, and values are the +# internally-used names of that URI option. Options with only one name +# variant need not be included here. Options whose public and internal +# names are the same need not be included here. +INTERNAL_URI_OPTION_NAME_MAP = { + 'j': 'journal', + 'wtimeout': 'wtimeoutms', + 'tls': 'ssl', + 'tlsallowinvalidcertificates': 'ssl_cert_reqs', + 'tlsallowinvalidhostnames': 'ssl_match_hostname', + 'tlscrlfile': 'ssl_crlfile', + 'tlscafile': 'ssl_ca_certs', + 'tlscertificatekeyfile': 'ssl_certfile', + 'tlscertificatekeyfilepassword': 'ssl_pem_passphrase', +} + +# Map from deprecated URI option names to the updated option names. +# Case is preserved for updated option names as they are part of user warnings. +URI_OPTIONS_DEPRECATION_MAP = { + 'j': 'journal', + 'wtimeout': 'wTimeoutMS', + 'ssl_cert_reqs': 'tlsAllowInvalidCertificates', + 'ssl_match_hostname': 'tlsAllowInvalidHostnames', + 'ssl_crlfile': 'tlsCRLFile', + 'ssl_ca_certs': 'tlsCAFile', + 'ssl_pem_passphrase': 'tlsCertificateKeyFilePassword', +} + +# Augment the option validator map with pymongo-specific option information. +URI_OPTIONS_VALIDATOR_MAP.update(NONSPEC_OPTIONS_VALIDATOR_MAP) +for optname, aliases in iteritems(URI_OPTIONS_ALIAS_MAP): + for alias in aliases: + if alias not in URI_OPTIONS_VALIDATOR_MAP: + URI_OPTIONS_VALIDATOR_MAP[alias] = ( + URI_OPTIONS_VALIDATOR_MAP[optname]) + +# Map containing all URI option and keyword argument validators. +VALIDATORS = URI_OPTIONS_VALIDATOR_MAP.copy() VALIDATORS.update(KW_VALIDATORS) +# List of timeout-related options. +TIMEOUT_OPTIONS = [ + 'connecttimeoutms', + 'heartbeatfrequencyms', + 'maxidletimems', + 'maxstalenessseconds', + 'serverselectiontimeoutms', + 'sockettimeoutms', + 'waitqueuetimeoutms', +] + _AUTH_OPTIONS = frozenset(['authmechanismproperties']) @@ -613,15 +684,22 @@ def validate(option, value): def get_validated_options(options, warn=True): """Validate each entry in options and raise a warning if it is not valid. - Returns a copy of options with invalid entries removed + Returns a copy of options with invalid entries removed. + + :Parameters: + - `opts`: A dict of MongoDB URI options. + - `warn` (optional): If ``True`` then warnings will be logged and + invalid options will be ignored. Otherwise, invalid options will + cause errors. """ validated_options = {} for opt, value in iteritems(options): lower = opt.lower() try: - validator = URI_VALIDATORS.get(lower, raise_config_error) + validator = URI_OPTIONS_VALIDATOR_MAP.get( + lower, raise_config_error) value = validator(opt, value) - except (ValueError, ConfigurationError) as exc: + except (ValueError, TypeError, ConfigurationError) as exc: if warn: warnings.warn(str(exc)) else: @@ -631,6 +709,7 @@ def get_validated_options(options, warn=True): return validated_options +# List of write-concern-related options. WRITE_CONCERN_OPTIONS = frozenset([ 'w', 'wtimeout', diff --git a/pymongo/compression_support.py b/pymongo/compression_support.py index a4987df088..f52d5e0d98 100644 --- a/pymongo/compression_support.py +++ b/pymongo/compression_support.py @@ -36,7 +36,13 @@ def validate_compressors(dummy, value): - compressors = value.split(",") + try: + # `value` is string. + compressors = value.split(",") + except AttributeError: + # `value` is an iterable. + compressors = list(value) + for compressor in compressors[:]: if compressor not in _SUPPORTED_COMPRESSORS: compressors.remove(compressor) diff --git a/pymongo/mongo_client.py b/pymongo/mongo_client.py index 6895df5f80..889b61048c 100644 --- a/pymongo/mongo_client.py +++ b/pymongo/mongo_client.py @@ -71,6 +71,9 @@ from pymongo.topology import Topology from pymongo.topology_description import TOPOLOGY_TYPE from pymongo.settings import TopologySettings +from pymongo.uri_parser import (_CaseInsensitiveDictionary, + _handle_option_deprecations, + _normalize_options) from pymongo.write_concern import DEFAULT_WRITE_CONCERN @@ -151,7 +154,7 @@ def __init__( `_ for more details. Note that the use of SRV URIs implicitly enables - TLS support. Pass ssl=false in the URI to override. + TLS support. Pass tls=false in the URI to override. .. note:: MongoClient creation will block waiting for answers from DNS when mongodb+srv:// URIs are used. @@ -297,16 +300,17 @@ def __init__( primary (e.g. w=3 means write to the primary and wait until replicated to **two** secondaries). Passing w=0 **disables write acknowledgement** and all other write concern options. - - `wtimeout`: (integer) Used in conjunction with `w`. Specify a value + - `wTimeoutMS`: (integer) Used in conjunction with `w`. Specify a value in milliseconds to control how long to wait for write propagation to complete. If replication does not complete in the given - timeframe, a timeout exception is raised. - - `j`: If ``True`` block until write operations have been committed - to the journal. Cannot be used in combination with `fsync`. Prior - to MongoDB 2.6 this option was ignored if the server was running - without journaling. Starting with MongoDB 2.6 write operations will - fail with an exception if this option is used when the server is - running without journaling. + timeframe, a timeout exception is raised. Passing wTimeoutMS=0 + will cause **write operations to wait indefinitely**. + - `journal`: If ``True`` block until write operations have been + committed to the journal. Cannot be used in combination with + `fsync`. Prior to MongoDB 2.6 this option was ignored if the server + was running without journaling. Starting with MongoDB 2.6 write + operations will fail with an exception if this option is used when + the server is running without journaling. - `fsync`: If ``True`` and the server is running without journaling, blocks until the server has synced all data files to disk. If the server is running with journaling, this acts the same as the `j` @@ -366,47 +370,56 @@ def __init__( .. seealso:: :doc:`/examples/authentication` - | **SSL configuration:** - - - `ssl`: If ``True``, create the connection to the server using SSL. - Defaults to ``False``. + | **TLS/SSL configuration:** + + - `tls`: (boolean) If ``True``, create the connection to the server + using transport layer security. Defaults to ``False``. + - `tlsInsecure`: (boolean) Specify whether TLS constraints should be + relaxed as much as possible. Setting ``tlsInsecure=True`` implies + ``tlsAllowInvalidCertificates=True`` and + ``tlsAllowInvalidHostnames=True``. Defaults to ``False``. Think + very carefully before setting this to ``True`` as it dramatically + reduces the security of TLS. + - `tlsAllowInvalidCertificates`: (boolean) If ``True``, continues + the TLS handshake regardless of the outcome of the certificate + verification process. If this is ``False``, and a value is not + provided for ``tlsCAFile``, PyMongo will attempt to load system + provided CA certificates. If the python version in use does not + support loading system CA certificates then the ``tlsCAFile`` + parameter must point to a file of CA certificates. + ``tlsAllowInvalidCertificates=False`` implies ``tls=True``. + Defaults to ``False``. Think very carefully before setting this + to ``True`` as that could make your application vulnerable to + man-in-the-middle attacks. + - `tlsAllowInvalidHostnames`: (boolean) If ``True``, disables TLS + hostname verification. ``tlsAllowInvalidHostnames=False`` implies + ``tls=True``. Defaults to ``False``. Think very carefully before + setting this to ``True`` as that could make your application + vulnerable to man-in-the-middle attacks. + - `tlsCAFile`: A file containing a single or a bundle of + "certification authority" certificates, which are used to validate + certificates passed from the other end of the connection. + Implies ``tls=True``. Defaults to ``None``. + - `tlsCertificateKeyFile`: A file containing the client certificate + and private key. If you want to pass the certificate and private + key as separate files, use the ``ssl_certfile`` and ``ssl_keyfile`` + options instead. Implies ``tls=True``. Defaults to ``None``. + - `tlsCRLFile`: A file containing a PEM or DER formatted + certificate revocation list. Only supported by python 2.7.9+ + (pypy 2.5.1+). Implies ``tls=True``. Defaults to ``None``. + - `tlsCertificateKeyFilePassword`: The password or passphrase for + decrypting the private key in ``tlsCertificateKeyFile`` or + ``ssl_keyfile``. Only necessary if the private key is encrypted. + Only supported by python 2.7.9+ (pypy 2.5.1+) and 3.3+. Defaults + to ``None``. + - `ssl`: (boolean) Alias for ``tls``. - `ssl_certfile`: The certificate file used to identify the local - connection against mongod. Implies ``ssl=True``. Defaults to + connection against mongod. Implies ``tls=True``. Defaults to ``None``. - `ssl_keyfile`: The private keyfile used to identify the local - connection against mongod. If included with the ``certfile`` then - only the ``ssl_certfile`` is needed. Implies ``ssl=True``. + connection against mongod. Can be omitted if the keyfile is + included with the ``tlsCertificateKeyFile``. Implies ``tls=True``. Defaults to ``None``. - - `ssl_pem_passphrase`: The password or passphrase for decrypting - the private key in ``ssl_certfile`` or ``ssl_keyfile``. Only - necessary if the private key is encrypted. Only supported by python - 2.7.9+ (pypy 2.5.1+) and 3.3+. Defaults to ``None``. - - `ssl_cert_reqs`: Specifies whether a certificate is required from - the other side of the connection, and whether it will be validated - if provided. It must be one of the three values ``ssl.CERT_NONE`` - (certificates ignored), ``ssl.CERT_REQUIRED`` (certificates - required and validated), or ``ssl.CERT_OPTIONAL`` (the same as - CERT_REQUIRED, unless the server was configured to use anonymous - ciphers). If the value of this parameter is not ``ssl.CERT_NONE`` - and a value is not provided for ``ssl_ca_certs`` PyMongo will - attempt to load system provided CA certificates. If the python - version in use does not support loading system CA certificates - then the ``ssl_ca_certs`` parameter must point to a file of CA - certificates. Implies ``ssl=True``. Defaults to - ``ssl.CERT_REQUIRED`` if not provided and ``ssl=True``. - - `ssl_ca_certs`: The ca_certs file contains a set of concatenated - "certification authority" certificates, which are used to validate - certificates passed from the other end of the connection. - Implies ``ssl=True``. Defaults to ``None``. - - `ssl_crlfile`: The path to a PEM or DER formatted certificate - revocation list. Only supported by python 2.7.9+ (pypy 2.5.1+) - and 3.4+. Defaults to ``None``. - - `ssl_match_hostname`: If ``True`` (the default), and - `ssl_cert_reqs` is not ``ssl.CERT_NONE``, enables hostname - verification using the :func:`~ssl.match_hostname` function from - python's :mod:`~ssl` module. Think very carefully before setting - this to ``False`` as that could make your application vulnerable to - man-in-the-middle attacks. | **Read Concern options:** | (If not set explicitly, this will use the server default) @@ -419,6 +432,23 @@ def __init__( .. mongodoc:: connections + .. versionchanged:: 4.0 + Added the ``tlsInsecure`` keyword argument and URI option. + The following keyword arguments and URI options were deprecated: + + - ``wTimeout`` was deprecated in favor of ``wTimeoutMS``. + - ``j`` was deprecated in favor of ``journal``. + - ``ssl_cert_reqs`` was deprecated in favor of + ``tlsAllowInvalidCertificates``. + - ``ssl_match_hostname`` was deprecated in favor of + ``tlsAllowInvalidHostnames``. + - ``ssl_ca_certs`` was deprecated in favor of ``tlsCAFile``. + - ``ssl_certfile`` was deprecated in favor of + ``tlsCertificateKeyFile``. + - ``ssl_crlfile`` was deprecated in favor of ``tlsCRLFile``. + - ``ssl_pem_passphrase`` was deprecated in favor of + ``tlsCertificateKeyFilePassword``. + .. versionchanged:: 3.8 Added the ``server_selector`` keyword argument. @@ -433,8 +463,8 @@ def __init__( Add ``username`` and ``password`` options. Document the ``authSource``, ``authMechanism``, and ``authMechanismProperties `` options. - Deprecated the `socketKeepAlive` keyword argument and URI option. - `socketKeepAlive` now defaults to ``True``. + Deprecated the ``socketKeepAlive`` keyword argument and URI option. + ``socketKeepAlive`` now defaults to ``True``. .. versionchanged:: 3.0 :class:`~pymongo.mongo_client.MongoClient` is now the one and only @@ -511,7 +541,8 @@ def __init__( opts = {} for entity in host: if "://" in entity: - res = uri_parser.parse_uri(entity, port, warn=True) + res = uri_parser.parse_uri( + entity, port, validate=True, warn=True) seeds.update(res["nodelist"]) username = res["username"] or username password = res["password"] or password @@ -536,9 +567,15 @@ def __init__( connect = opts.get('connect', True) keyword_opts['tz_aware'] = tz_aware keyword_opts['connect'] = connect - # Validate all keyword options. - keyword_opts = dict(common.validate(k, v) - for k, v in keyword_opts.items()) + + # Validate kwargs options. + keyword_opts = _CaseInsensitiveDictionary( + dict(common.validate(k, v) for k, v in keyword_opts.items())) + # Handle deprecated options in kwarg list. + keyword_opts = _handle_option_deprecations(keyword_opts) + # Change kwarg option names to those used internally. + keyword_opts = _normalize_options(keyword_opts) + # Augment URI options with kwarg options, overriding the former. opts.update(keyword_opts) # Username and password passed as kwargs override user info in URI. username = opts.get("username", username) @@ -1316,7 +1353,7 @@ def option_repr(option, value): else: return 'document_class=%s.%s' % (value.__module__, value.__name__) - if option in common.TIMEOUT_VALIDATORS and value is not None: + if option in common.TIMEOUT_OPTIONS and value is not None: return "%s=%s" % (option, int(value * 1000)) return '%s=%r' % (option, value) diff --git a/pymongo/ssl_support.py b/pymongo/ssl_support.py index 9eeeb4f7a5..ba156553b5 100644 --- a/pymongo/ssl_support.py +++ b/pymongo/ssl_support.py @@ -69,6 +69,15 @@ def validate_cert_reqs(option, value): "`ssl.CERT_NONE`, `ssl.CERT_OPTIONAL` or " "`ssl.CERT_REQUIRED`" % (option,)) + def validate_allow_invalid_certs(option, value): + """Validate the option to allow invalid certificates is valid.""" + # Avoid circular import. + from pymongo.common import validate_boolean_or_string + boolean_cert_reqs = validate_boolean_or_string(option, value) + if boolean_cert_reqs: + return ssl.CERT_NONE + return ssl.CERT_REQUIRED + def _load_wincerts(): """Set _WINCERTS to an instance of wincertstore.Certfile.""" global _WINCERTS @@ -184,6 +193,10 @@ def validate_cert_reqs(option, dummy): "validated. The ssl module is not available" % (option,)) + def validate_allow_invalid_certs(option, dummy): + """No ssl module, raise ConfigurationError.""" + return validate_cert_reqs(option, dummy) + def get_ssl_context(*dummy): """No ssl module, raise ConfigurationError.""" raise ConfigurationError("The ssl module is not available.") diff --git a/pymongo/uri_parser.py b/pymongo/uri_parser.py index 8ee5fbb11a..447936b114 100644 --- a/pymongo/uri_parser.py +++ b/pymongo/uri_parser.py @@ -23,14 +23,15 @@ except ImportError: _HAVE_DNSPYTHON = False -from bson.py3compat import PY3, string_type +from bson.py3compat import abc, iteritems, string_type, PY3 if PY3: from urllib.parse import unquote_plus else: from urllib import unquote_plus -from pymongo.common import get_validated_options +from pymongo.common import ( + get_validated_options, URI_OPTIONS_DEPRECATION_MAP, INTERNAL_URI_OPTION_NAME_MAP) from pymongo.errors import ConfigurationError, InvalidURI @@ -41,6 +42,80 @@ DEFAULT_PORT = 27017 +class _CaseInsensitiveDictionary(abc.MutableMapping): + def __init__(self, *args, **kwargs): + self.__casedkeys = {} + self.__data = {} + self.update(dict(*args, **kwargs)) + + def __contains__(self, key): + return key.lower() in self.__data + + def __len__(self): + return len(self.__data) + + def __iter__(self): + return (self.__casedkeys[key] for key in self.__casedkeys) + + def __repr__(self): + return str(self.__data) + + def __setitem__(self, key, value): + lc_key = key.lower() + self.__casedkeys[lc_key] = key + self.__data[lc_key] = value + + def __getitem__(self, key): + return self.__data[key.lower()] + + def __delitem__(self, key): + lc_key = key.lower() + del self.__casedkeys[lc_key] + del self.__data[lc_key] + + def get(self, key, default=None): + lc_key = key.lower() + if lc_key in self: + return self.__data[lc_key] + return default + + def pop(self, key, *args, **kwargs): + lc_key = key.lower() + self.__casedkeys.pop(lc_key, None) + return self.__data.pop(lc_key, *args, **kwargs) + + def popitem(self): + lc_key, cased_key = self.__casedkeys.popitem() + value = self.__data.pop(lc_key) + return cased_key, value + + def clear(self): + self.__casedkeys.clear() + self.__data.clear() + + def setdefault(self, key, default=None): + lc_key = key.lower() + if key in self: + return self.__data[lc_key] + else: + self.__casedkeys[lc_key] = key + self.__data[lc_key] = default + return default + + def update(self, other): + for key in other: + self[key] = other[key] + + def cased_key(self, key): + return self.__casedkeys[key.lower()] + + def as_dict(self): + lc_data = {} + for lc_key in self.__data: + lc_data[lc_key] = self.__data[lc_key] + return lc_data + + def parse_userinfo(userinfo): """Validates the format of user information in a MongoDB URI. Reserved characters like ':', '/', '+' and '@' must be escaped @@ -129,6 +204,66 @@ def parse_host(entity, default_port=DEFAULT_PORT): return host.lower(), port +_IMPLICIT_TLSINSECURE_OPTS = {"tlsallowinvalidcertificates", + "tlsallowinvalidhostnames"} + + +def _parse_options(opts, delim): + """Helper method for split_options which creates the options dict. + Also handles the creation of a list for the URI tag_sets/ + readpreferencetags portion and the use of the tlsInsecure option.""" + options = _CaseInsensitiveDictionary() + for uriopt in opts.split(delim): + key, value = uriopt.split("=") + if key.lower() == 'readpreferencetags': + options.setdefault(key, []).append(value) + else: + if key in options: + warnings.warn("Duplicate URI option '%s'." % (key,)) + options[key] = unquote_plus(value) + + if 'tlsInsecure' in options: + for implicit_option in _IMPLICIT_TLSINSECURE_OPTS: + if implicit_option in options: + warn_msg = "URI option '%s' overrides value implied by '%s'." + warnings.warn(warn_msg % (options.cased_key(implicit_option), + options.cased_key('tlsInsecure'))) + continue + options[implicit_option] = options['tlsInsecure'] + + return options + + +def _handle_option_deprecations(options): + """Issue appropriate warnings when deprecated options are present in the + options dictionary. Removes deprecated option key, value pairs if the + options dictionary is found to also have the renamed option.""" + undeprecated_options = _CaseInsensitiveDictionary() + for key, value in iteritems(options): + optname = str(key).lower() + if optname in URI_OPTIONS_DEPRECATION_MAP: + renamed_key = URI_OPTIONS_DEPRECATION_MAP[optname] + if renamed_key.lower() in options: + warnings.warn("Deprecated option '%s' ignored in favor of " + "'%s'." % (str(key), renamed_key)) + continue + warnings.warn("Option '%s' is deprecated, use '%s' instead." % ( + str(key), renamed_key)) + undeprecated_options[str(key)] = value + return undeprecated_options + + +def _normalize_options(options): + """Renames keys in the options dictionary to their internally-used + names.""" + normalized_options = {} + for key, value in iteritems(options): + optname = str(key).lower() + intname = INTERNAL_URI_OPTION_NAME_MAP.get(optname, key) + normalized_options[intname] = options[key] + return normalized_options + + def validate_options(opts, warn=False): """Validates and normalizes options passed in a MongoDB URI. @@ -138,41 +273,14 @@ def validate_options(opts, warn=False): :Parameters: - `opts`: A dict of MongoDB URI options. - - `warn` (optional): If ``True`` then warnigns will be logged and + - `warn` (optional): If ``True`` then warnings will be logged and invalid options will be ignored. Otherwise invalid options will cause errors. """ return get_validated_options(opts, warn) -def _parse_options(opts, delim): - """Helper method for split_options which creates the options dict. - Also handles the creation of a list for the URI tag_sets/ - readpreferencetags portion.""" - options = {} - for opt in opts.split(delim): - key, val = opt.split("=") - if key.lower() == 'readpreferencetags': - options.setdefault('readpreferencetags', []).append(val) - else: - # str(option) to ensure that a unicode URI results in plain 'str' - # option names. 'normalized' is then suitable to be passed as - # kwargs in all Python versions. - if str(key) in options: - warnings.warn("Duplicate URI option %s" % (str(key),)) - options[str(key)] = unquote_plus(val) - - # Special case for deprecated options - if "wtimeout" in options: - if "wtimeoutMS" in options: - options.pop("wtimeout") - warnings.warn("Option wtimeout is deprecated, use 'wtimeoutMS'" - " instead") - - return options - - -def split_options(opts, validate=True, warn=False): +def split_options(opts, validate=True, warn=False, normalize=True): """Takes the options portion of a MongoDB URI, validates each option and returns the options in a dictionary. @@ -180,6 +288,10 @@ def split_options(opts, validate=True, warn=False): - `opt`: A string representing MongoDB URI options. - `validate`: If ``True`` (the default), validate and normalize all options. + - `warn`: If ``False`` (the default), suppress all warnings raised + during validation of options. + - `normalize`: If ``True`` (the default), renames all options to their + internally-used names. """ and_idx = opts.find("&") semi_idx = opts.find(";") @@ -197,8 +309,14 @@ def split_options(opts, validate=True, warn=False): except ValueError: raise InvalidURI("MongoDB URI options are key=value pairs.") + options = _handle_option_deprecations(options) + if validate: - return validate_options(options, warn) + options = validate_options(options, warn) + + if normalize: + options = _normalize_options(options) + return options @@ -426,4 +544,4 @@ def parse_uri(uri, default_port=DEFAULT_PORT, validate=True, warn=False): pprint.pprint(parse_uri(sys.argv[1])) except InvalidURI as exc: print(exc) - sys.exit(0) + sys.exit(0) \ No newline at end of file diff --git a/test/__init__.py b/test/__init__.py index 2a5985570b..9f23101267 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -36,7 +36,6 @@ import pymongo.errors from bson.son import SON -from bson.py3compat import _unicode from pymongo import common, message from pymongo.common import partition_node from pymongo.ssl_support import HAVE_SSL, validate_cert_reqs @@ -749,3 +748,11 @@ def test_cases(suite): # unittest.TestSuite for case in test_cases(suite_or_case): yield case + + +# Helper method to workaround https://bugs.python.org/issue21724 +def clear_warning_registry(): + """Clear the __warningregistry__ for all modules.""" + for name, module in list(sys.modules.items()): + if hasattr(module, "__warningregistry__"): + setattr(module, "__warningregistry__", {}) diff --git a/test/test_client.py b/test/test_client.py index 60deae7b4e..6bc0526ab8 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -882,8 +882,8 @@ def test_socketKeepAlive(self): with warnings.catch_warnings(record=True) as ctx: warnings.simplefilter("always") client = rs_or_single_client(socketKeepAlive=socketKeepAlive) - self.assertIn("The socketKeepAlive option is deprecated", - str(ctx[0])) + self.assertTrue(any("The socketKeepAlive option is deprecated" + in str(k) for k in ctx)) pool = get_pool(client) self.assertEqual(socketKeepAlive, pool.opts.socket_keepalive) diff --git a/test/test_uri_parser.py b/test/test_uri_parser.py index 4211ba4395..49fd91b10d 100644 --- a/test/test_uri_parser.py +++ b/test/test_uri_parser.py @@ -24,11 +24,13 @@ split_hosts, split_options, parse_uri) +from pymongo.common import get_validated_options from pymongo.errors import ConfigurationError, InvalidURI +from pymongo.ssl_support import ssl from pymongo import ReadPreference from bson.binary import JAVA_LEGACY from bson.py3compat import string_type, _unicode -from test import unittest +from test import clear_warning_registry, unittest class TestURI(unittest.TestCase): @@ -474,6 +476,51 @@ def test_parse_ssl_paths(self): 'mongodb://jesse:foo%2Fbar@%2FMongoDB.sock/?ssl_certfile=a/b', validate=False)) + def test_parse_tls_insecure_options(self): + # tlsInsecure is expanded correctly. + uri = "mongodb://example.com/?tlsInsecure=true" + res = get_validated_options( + {"ssl_match_hostname": False, "ssl_cert_reqs": ssl.CERT_NONE, + "tlsinsecure": True}, warn=False) + self.assertEqual(res, parse_uri(uri)["options"]) + + # tlsAllow* specified AFTER tlsInsecure. + # tlsAllow* options warns and overrides values implied by tlsInsecure. + uri = ("mongodb://example.com/?tlsInsecure=true" + "&tlsAllowInvalidCertificates=false" + "&tlsAllowInvalidHostnames=false") + res = get_validated_options( + {"ssl_match_hostname": True, "ssl_cert_reqs": ssl.CERT_REQUIRED, + "tlsinsecure": True}, warn=False) + with warnings.catch_warnings(record=True) as ctx: + warnings.simplefilter('always') + self.assertEqual(res, parse_uri(uri)["options"]) + for warning in ctx: + self.assertRegexpMatches( + warning.message.args[0], + ".*tlsAllowInvalid.*overrides.*tlsInsecure.*") + clear_warning_registry() + + # tlsAllow* specified BEFORE tlsInsecure. + # tlsAllow* options warns and overrides values implied by tlsInsecure. + uri = ("mongodb://example.com/" + "?tlsAllowInvalidCertificates=false" + "&tlsAllowInvalidHostnames=false" + "&tlsInsecure=true") + res = get_validated_options( + {"ssl_match_hostname": True, "ssl_cert_reqs": ssl.CERT_REQUIRED, + "tlsinsecure": True}, warn=False) + with warnings.catch_warnings(record=True) as ctx: + warnings.simplefilter('always') + self.assertEqual(res, parse_uri(uri)["options"]) + for warning in ctx: + self.assertRegexpMatches( + warning.message.args[0], + ".*tlsAllowInvalid.*overrides.*tlsInsecure.*") + + + + if __name__ == "__main__": unittest.main() diff --git a/test/test_uri_spec.py b/test/test_uri_spec.py index f9ff7ab003..f55987c280 100644 --- a/test/test_uri_spec.py +++ b/test/test_uri_spec.py @@ -12,7 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Test the pymongo uri_parser module is up to spec.""" +"""Test that the pymongo.uri_parser module is compliant with the connection +string and uri options specifications.""" + import json import os import sys @@ -20,108 +22,151 @@ sys.path[0:0] = [""] +from pymongo.common import INTERNAL_URI_OPTION_NAME_MAP, validate from pymongo.uri_parser import parse_uri -from test import unittest +from test import clear_warning_registry, unittest + -# Location of JSON test specifications. -_TEST_PATH = os.path.join( +CONN_STRING_TEST_PATH = os.path.join( os.path.dirname(os.path.realpath(__file__)), os.path.join('connection_string', 'test')) +URI_OPTIONS_TEST_PATH = os.path.join( + os.path.dirname(os.path.realpath(__file__)), 'uri_options') + +TEST_DESC_SKIP_LIST = [ + "Valid options specific to single-threaded drivers are parsed correctly", + "Invalid serverSelectionTryOnce causes a warning"] + class TestAllScenarios(unittest.TestCase): - pass + def setUp(self): + clear_warning_registry() + + +def get_error_message_template(expected, artefact): + return "%s %s for test '%s'" % ( + "Expected" if expected else "Unexpected", artefact, "%s") + +def run_scenario_in_dir(target_workdir): + def workdir_context_decorator(func): + def modified_test_scenario(*args, **kwargs): + original_workdir = os.getcwd() + os.chdir(target_workdir) + func(*args, **kwargs) + os.chdir(original_workdir) + return modified_test_scenario + return workdir_context_decorator -def create_test(scenario_def): + +def create_test(test, test_workdir): def run_scenario(self): - self.assertTrue(scenario_def['tests'], "tests cannot be empty") - for test in scenario_def['tests']: - dsc = test['description'] - - warned = False - error = False - - with warnings.catch_warnings(): - warnings.filterwarnings('error') - try: - options = parse_uri(test['uri'], warn=True) - except Warning: - warned = True - except Exception: - error = True - - self.assertEqual(not error, test['valid'], - "Test failure '%s'" % dsc) - - if test.get("warning", False): - self.assertTrue(warned, - "Expected warning for test '%s'" - % (dsc,)) - - # Redo in the case there were warnings that were not expected. - if warned: - options = parse_uri(test['uri'], warn=True) + valid = True + warning = False - # Compare hosts and port. - if test['hosts'] is not None: - self.assertEqual( - len(test['hosts']), len(options['nodelist']), - "Incorrect number of hosts parsed from URI") - - for exp, actual in zip(test['hosts'], - options['nodelist']): - self.assertEqual(exp['host'], actual[0], - "Expected host %s but got %s" - % (exp['host'], actual[0])) - if exp['port'] is not None: - self.assertEqual(exp['port'], actual[1], - "Expected port %s but got %s" - % (exp['port'], actual)) - - # Compare auth options. - auth = test['auth'] - if auth is not None: - auth['database'] = auth.pop('db') # db == database - # Special case for PyMongo's collection parsing. - if options.get('collection') is not None: - options['database'] += "." + options['collection'] - for elm in auth: - if auth[elm] is not None: - self.assertEqual(auth[elm], options[elm], - "Expected %s but got %s" - % (auth[elm], options[elm])) - - # Compare URI options. - if test['options'] is not None: - for opt in test['options']: - if options.get(opt) is not None: - self.assertEqual( - options[opt], test['options'][opt], - "For option %s expected %s but got %s" - % (opt, options[opt], - test['options'][opt])) - - return run_scenario - - -def create_tests(): - for dirpath, _, filenames in os.walk(_TEST_PATH): + with warnings.catch_warnings(record=True) as ctx: + warnings.simplefilter('always') + try: + options = parse_uri(test['uri'], warn=True) + except Exception: + valid = False + else: + warning = len(ctx) > 0 + + expected_valid = test.get('valid', True) + self.assertEqual( + valid, expected_valid, get_error_message_template( + not expected_valid, "error") % test['description']) + + if expected_valid: + expected_warning = test.get('warning', False) + self.assertEqual( + warning, expected_warning, get_error_message_template( + expected_warning, "warning") % test['description']) + + # Compare hosts and port. + if test['hosts'] is not None: + self.assertEqual( + len(test['hosts']), len(options['nodelist']), + "Incorrect number of hosts parsed from URI") + + for exp, actual in zip(test['hosts'], + options['nodelist']): + self.assertEqual(exp['host'], actual[0], + "Expected host %s but got %s" + % (exp['host'], actual[0])) + if exp['port'] is not None: + self.assertEqual(exp['port'], actual[1], + "Expected port %s but got %s" + % (exp['port'], actual)) + + # Compare auth options. + auth = test['auth'] + if auth is not None: + auth['database'] = auth.pop('db') # db == database + # Special case for PyMongo's collection parsing. + if options.get('collection') is not None: + options['database'] += "." + options['collection'] + for elm in auth: + if auth[elm] is not None: + self.assertEqual(auth[elm], options[elm], + "Expected %s but got %s" + % (auth[elm], options[elm])) + + # Compare URI options. + err_msg = "For option %s expected %s but got %s" + if test['options'] is not None: + opts = options['options'] + for opt in test['options']: + lopt = opt.lower() + optname = INTERNAL_URI_OPTION_NAME_MAP.get(lopt, lopt) + if opts.get(optname) is not None: + if opts[optname] == test['options'][opt]: + expected_value = test['options'][opt] + else: + expected_value = validate( + lopt, test['options'][opt])[1] + self.assertEqual( + opts[optname], expected_value, + err_msg % (opt, expected_value, opts[optname],)) + else: + self.fail( + "Missing expected option %s" % (opt,)) + + return run_scenario_in_dir(test_workdir)(run_scenario) + + +def create_tests(test_path): + for dirpath, _, filenames in os.walk(test_path): dirname = os.path.split(dirpath) dirname = os.path.split(dirname[-2])[-1] + '_' + dirname[-1] for filename in filenames: + if not filename.endswith('.json'): + # skip everything that is not a test specification + continue with open(os.path.join(dirpath, filename)) as scenario_stream: scenario_def = json.load(scenario_stream) - # Construct test from scenario. - new_test = create_test(scenario_def) - test_name = 'test_%s_%s' % ( - dirname, os.path.splitext(filename)[0]) - new_test.__name__ = test_name - setattr(TestAllScenarios, new_test.__name__, new_test) + for testcase in scenario_def['tests']: + dsc = testcase['description'] + + if dsc in TEST_DESC_SKIP_LIST: + print("Skipping test '%s'" % dsc) + continue + + testmethod = create_test(testcase, dirpath) + testname = 'test_%s_%s_%s' % ( + dirname, os.path.splitext(filename)[0], + str(dsc).replace(' ', '_')) + testmethod.__name__ = testname + setattr(TestAllScenarios, testmethod.__name__, testmethod) + + +for test_path in [CONN_STRING_TEST_PATH, URI_OPTIONS_TEST_PATH]: + create_tests(test_path) -create_tests() if __name__ == "__main__": unittest.main() diff --git a/test/uri_options/auth-options.json b/test/uri_options/auth-options.json new file mode 100644 index 0000000000..65a168b334 --- /dev/null +++ b/test/uri_options/auth-options.json @@ -0,0 +1,20 @@ +{ + "tests": [ + { + "description": "Valid auth options are parsed correctly", + "uri": "mongodb://foo:bar@example.com/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:true&authSource=$external", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": { + "authMechanism": "GSSAPI", + "authMechanismProperties": { + "SERVICE_NAME": "other", + "CANONICALIZE_HOST_NAME": true + }, + "authSource": "$external" + } + } + ] +} diff --git a/test/uri_options/ca.pem b/test/uri_options/ca.pem new file mode 100644 index 0000000000..b4bdaefa85 --- /dev/null +++ b/test/uri_options/ca.pem @@ -0,0 +1 @@ +# This file exists solely for the purpose of facilitating drivers which check for the existence of files specified in the URI options at parse time. diff --git a/test/uri_options/cert.pem b/test/uri_options/cert.pem new file mode 100644 index 0000000000..b4bdaefa85 --- /dev/null +++ b/test/uri_options/cert.pem @@ -0,0 +1 @@ +# This file exists solely for the purpose of facilitating drivers which check for the existence of files specified in the URI options at parse time. diff --git a/test/uri_options/client.pem b/test/uri_options/client.pem new file mode 100644 index 0000000000..b4bdaefa85 --- /dev/null +++ b/test/uri_options/client.pem @@ -0,0 +1 @@ +# This file exists solely for the purpose of facilitating drivers which check for the existence of files specified in the URI options at parse time. diff --git a/test/uri_options/compression-options.json b/test/uri_options/compression-options.json new file mode 100644 index 0000000000..c3297b254b --- /dev/null +++ b/test/uri_options/compression-options.json @@ -0,0 +1,59 @@ +{ + "tests": [ + { + "description": "Valid compression options are parsed correctly", + "uri": "mongodb://example.com/?compressors=zlib&zlibCompressionLevel=9", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": { + "compressors": [ + "zlib" + ], + "zlibCompressionLevel": 9 + } + }, + { + "description": "Multiple compressors are parsed correctly", + "uri": "mongodb://example.com/?compressors=snappy,zlib", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": { + "compressors": [ + "snappy", + "zlib" + ] + } + }, + { + "description": "Non-numeric zlibCompressionLevel causes a warning", + "uri": "mongodb://example.com/?compressors=zlib&zlibCompressionLevel=invalid", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "Too low zlibCompressionLevel causes a warning", + "uri": "mongodb://example.com/?compressors=zlib&zlibCompressionLevel=-2", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "Too high zlibCompressionLevel causes a warning", + "uri": "mongodb://example.com/?compressors=zlib&zlibCompressionLevel=10", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + } + ] +} diff --git a/test/uri_options/concern-options.json b/test/uri_options/concern-options.json new file mode 100644 index 0000000000..2b3783746c --- /dev/null +++ b/test/uri_options/concern-options.json @@ -0,0 +1,76 @@ +{ + "tests": [ + { + "description": "Valid read and write concern are parsed correctly", + "uri": "mongodb://example.com/?readConcernLevel=majority&w=5&wTimeoutMS=30000&journal=false", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": { + "readConcernLevel": "majority", + "w": 5, + "wTimeoutMS": 30000, + "journal": false + } + }, + { + "description": "Arbitrary string readConcernLevel does not cause a warning", + "uri": "mongodb://example.com/?readConcernLevel=arbitraryButStillValid", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": { + "readConcernLevel": "arbitraryButStillValid" + } + }, + { + "description": "Arbitrary string w doesn't cause a warning", + "uri": "mongodb://example.com/?w=arbitraryButStillValid", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": { + "w": "arbitraryButStillValid" + } + }, + { + "description": "Too low w causes a warning", + "uri": "mongodb://example.com/?w=-2", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "Non-numeric wTimeoutMS causes a warning", + "uri": "mongodb://example.com/?wTimeoutMS=invalid", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "Too low wTimeoutMS causes a warning", + "uri": "mongodb://example.com/?wTimeoutMS=-2", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "Invalid journal causes a warning", + "uri": "mongodb://example.com/?journal=invalid", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + } + ] +} diff --git a/test/uri_options/connection-options.json b/test/uri_options/connection-options.json new file mode 100644 index 0000000000..1e2dccd6e2 --- /dev/null +++ b/test/uri_options/connection-options.json @@ -0,0 +1,122 @@ +{ + "tests": [ + { + "description": "Valid connection and timeout options are parsed correctly", + "uri": "mongodb://example.com/?appname=URI-OPTIONS-SPEC-TEST&connectTimeoutMS=20000&heartbeatFrequencyMS=5000&localThresholdMS=3000&maxIdleTimeMS=50000&replicaSet=uri-options-spec&retryWrites=true&serverSelectionTimeoutMS=15000&socketTimeoutMS=7500", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": { + "appname": "URI-OPTIONS-SPEC-TEST", + "connectTimeoutMS": 20000, + "heartbeatFrequencyMS": 5000, + "localThresholdMS": 3000, + "maxIdleTimeMS": 50000, + "replicaSet": "uri-options-spec", + "retryWrites": true, + "serverSelectionTimeoutMS": 15000, + "socketTimeoutMS": 7500 + } + }, + { + "description": "Non-numeric connectTimeoutMS causes a warning", + "uri": "mongodb://example.com/?connectTimeoutMS=invalid", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "Too low connectTimeoutMS causes a warning", + "uri": "mongodb://example.com/?connectTimeoutMS=-2", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "Non-numeric heartbeatFrequencyMS causes a warning", + "uri": "mongodb://example.com/?heartbeatFrequencyMS=invalid", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "Too low heartbeatFrequencyMS causes a warning", + "uri": "mongodb://example.com/?heartbeatFrequencyMS=-2", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "Non-numeric localThresholdMS causes a warning", + "uri": "mongodb://example.com/?localThresholdMS=invalid", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "Too low localThresholdMS causes a warning", + "uri": "mongodb://example.com/?localThresholdMS=-2", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "Invalid retryWrites causes a warning", + "uri": "mongodb://example.com/?retryWrites=invalid", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "Non-numeric serverSelectionTimeoutMS causes a warning", + "uri": "mongodb://example.com/?serverSelectionTimeoutMS=invalid", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "Too low serverSelectionTimeoutMS causes a warning", + "uri": "mongodb://example.com/?serverSelectionTimeoutMS=-2", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "Non-numeric socketTimeoutMS causes a warning", + "uri": "mongodb://example.com/?socketTimeoutMS=invalid", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "Too low socketTimeoutMS causes a warning", + "uri": "mongodb://example.com/?socketTimeoutMS=-2", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + } + ] +} diff --git a/test/uri_options/connection-pool-options.json b/test/uri_options/connection-pool-options.json new file mode 100644 index 0000000000..be401f55d5 --- /dev/null +++ b/test/uri_options/connection-pool-options.json @@ -0,0 +1,33 @@ +{ + "tests": [ + { + "description": "Valid connection pool options are parsed correctly", + "uri": "mongodb://example.com/?maxIdleTimeMS=50000", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": { + "maxIdleTimeMS": 50000 + } + }, + { + "description": "Non-numeric maxIdleTimeMS causes a warning", + "uri": "mongodb://example.com/?maxIdleTimeMS=invalid", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "Too low maxIdleTimeMS causes a warning", + "uri": "mongodb://example.com/?maxIdleTimeMS=-2", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + } + ] +} diff --git a/test/uri_options/read-preference-options.json b/test/uri_options/read-preference-options.json new file mode 100644 index 0000000000..e62ce4fa75 --- /dev/null +++ b/test/uri_options/read-preference-options.json @@ -0,0 +1,52 @@ +{ + "tests": [ + { + "description": "Valid read preference options are parsed correctly", + "uri": "mongodb://example.com/?readPreference=primaryPreferred&readPreferenceTags=dc:ny,rack:1&maxStalenessSeconds=120&readPreferenceTags=dc:ny", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": { + "readPreference": "primaryPreferred", + "readPreferenceTags": [ + { + "dc": "ny", + "rack": "1" + }, + { + "dc": "ny" + } + ], + "maxStalenessSeconds": 120 + } + }, + { + "description": "Invalid readPreferenceTags causes a warning", + "uri": "mongodb://example.com/?readPreferenceTags=invalid", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "Non-numeric maxStalenessSeconds causes a warning", + "uri": "mongodb://example.com/?maxStalenessSeconds=invalid", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "Too low maxStalenessSeconds causes a warning", + "uri": "mongodb://example.com/?maxStalenessSeconds=-2", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + } + ] +} diff --git a/test/uri_options/single-threaded-options.json b/test/uri_options/single-threaded-options.json new file mode 100644 index 0000000000..fcd24fb880 --- /dev/null +++ b/test/uri_options/single-threaded-options.json @@ -0,0 +1,24 @@ +{ + "tests": [ + { + "description": "Valid options specific to single-threaded drivers are parsed correctly", + "uri": "mongodb://example.com/?serverSelectionTryOnce=false", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": { + "serverSelectionTryOnce": false + } + }, + { + "description": "Invalid serverSelectionTryOnce causes a warning", + "uri": "mongodb://example.com/?serverSelectionTryOnce=invalid", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + } + ] +} diff --git a/test/uri_options/tls-options.json b/test/uri_options/tls-options.json new file mode 100644 index 0000000000..d535a23f56 --- /dev/null +++ b/test/uri_options/tls-options.json @@ -0,0 +1,105 @@ +{ + "tests": [ + { + "description": "Valid required tls options are parsed correctly", + "uri": "mongodb://example.com/?tls=true&tlsCAFile=ca.pem&tlsCertificateKeyFile=cert.pem&tlsCertificateKeyFilePassword=hunter2", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": { + "tls": true, + "tlsCAFile": "ca.pem", + "tlsCertificateKeyFile": "cert.pem", + "tlsCertificateKeyFilePassword": "hunter2" + } + }, + { + "description": "Invalid tlsAllowInvalidCertificates causes a warning", + "uri": "mongodb://example.com/?tlsAllowInvalidCertificates=invalid", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "tlsAllowInvalidCertificates is parsed correctly", + "uri": "mongodb://example.com/?tlsAllowInvalidCertificates=true", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": { + "tlsAllowInvalidCertificates": true + } + }, + { + "description": "Invalid tlsAllowInvalidCertificates causes a warning", + "uri": "mongodb://example.com/?tlsAllowInvalidCertificates=invalid", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "tlsAllowInvalidHostnames is parsed correctly", + "uri": "mongodb://example.com/?tlsAllowInvalidHostnames=true", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": { + "tlsAllowInvalidHostnames": true + } + }, + { + "description": "Invalid tlsAllowInvalidHostnames causes a warning", + "uri": "mongodb://example.com/?tlsAllowInvalidHostnames=invalid", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "tlsInsecure is parsed correctly", + "uri": "mongodb://example.com/?tlsInsecure=true", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": { + "tlsInsecure": true + } + }, + { + "description": "Invalid tlsAllowInsecure causes a warning", + "uri": "mongodb://example.com/?tlsAllowInsecure=invalid", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "tlsInsecure=true and tlsAllowInvalidCertificates=false warns", + "uri": "mongodb://example.com/?tlsInsecure=true&tlsAllowInvalidCertificates=false", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "tlsInsecure=true and tlsAllowInvalidHostnames=false warns", + "uri": "mongodb://example.com/?tlsInsecure=true&tlsAllowInvalidHostnames=false", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + } + ] +}