lib/mongo/uri.rb in mongo-2.9.2 vs lib/mongo/uri.rb in mongo-2.10.0.rc0
- old
+ new
@@ -113,10 +113,11 @@
URI_OPTS_DELIM = '?'.freeze
# The character delimiting multiple options.
#
# @since 2.1.0
+ # @deprecated
INDIV_URI_OPTS_DELIM = '&'.freeze
# The character delimiting an option and its value.
#
# @since 2.1.0
@@ -186,25 +187,25 @@
# Map of URI authentication mechanisms to Ruby driver mechanisms
#
# @since 2.0.0
AUTH_MECH_MAP = {
- 'PLAIN' => :plain,
+ 'GSSAPI' => :gssapi,
# MONGODB-CR is deprecated and will be removed in driver version 3.0
'MONGODB-CR' => :mongodb_cr,
- 'GSSAPI' => :gssapi,
'MONGODB-X509' => :mongodb_x509,
+ 'PLAIN' => :plain,
'SCRAM-SHA-1' => :scram,
'SCRAM-SHA-256' => :scram256
}.freeze
# Options that are allowed to appear more than once in the uri.
#
- # In order to follow the URI options spec requirement that all instances of 'tls' and 'ssl' have
- # the same value, we need to keep track of all of the values passed in for those options.
- # Assuming they don't conflict, they will be condensed to a single value immediately after
- # parsing the URI.
+ # In order to follow the URI options spec requirement that all instances
+ # of 'tls' and 'ssl' have the same value, we need to keep track of all
+ # of the values passed in for those options. Assuming they don't conflict,
+ # they will be condensed to a single value immediately after parsing the URI.
#
# @since 2.1.0
REPEATABLE_OPTIONS = [ :tag_sets, :ssl ]
# Get either a URI object or a SRVProtocol URI object.
@@ -347,11 +348,23 @@
if hosts.index('@')
raise_invalid_error!("Unescaped @ in auth info")
end
end
- @servers = parse_servers!(hosts)
+ unless hosts.length > 0
+ raise_invalid_error!("Missing host; at least one must be provided")
+ end
+
+ @servers = hosts.split(',').map do |host|
+ if host.empty?
+ raise_invalid_error!('Empty host given in the host list')
+ end
+ decode(host).tap do |host|
+ validate_host!(host)
+ end
+ end
+
@user = parse_user!(creds)
@password = parse_password!(creds)
@uri_options = Options::Redacted.new(parse_uri_options!(options))
if db
@database = parse_database!(db)
@@ -366,24 +379,30 @@
end
[ creds_hosts, db_opts ].map { |s| s.reverse }
end
def parse_uri_options!(string)
- return {} unless string
- string.split(INDIV_URI_OPTS_DELIM).reduce({}) do |uri_options, opt|
- key, value = opt.split('=', 2)
+ uri_options = {}
+ unless string
+ return uri_options
+ end
+ string.split('&').each do |option_str|
+ if option_str.empty?
+ next
+ end
+ key, value = option_str.split('=', 2)
if value.nil?
raise_invalid_error!("Option #{key} has no value")
end
if value.index('=')
raise_invalid_error!("Value for option #{key} contains the key/value delimiter (=): #{value}")
end
- key = ::URI.decode(key)
- value = ::URI.decode(value)
+ key = decode(key)
+ value = decode(value)
add_uri_option(key, value, uri_options)
- uri_options
end
+ uri_options
end
def parse_user!(string)
if (string && user = string.partition(AUTH_USER_PWD_DELIM)[0])
if user.length > 0
@@ -419,27 +438,48 @@
unless port.nil? || (port.length > 0 && port.to_i > 0 && port.to_i <= 65535)
raise_invalid_error!(INVALID_PORT)
end
end
- def parse_servers!(string)
- raise_invalid_error!(INVALID_HOST) unless string.size > 0
- string.split(HOST_DELIM).reduce([]) do |servers, host|
- if host[0] == '['
- if host.index(']:')
- h, p = host.split(']:')
- validate_port_string!(p)
- end
- elsif host.index(HOST_PORT_DELIM)
- h, _, p = host.partition(HOST_PORT_DELIM)
- raise_invalid_error!(INVALID_HOST) unless h.size > 0
- validate_port_string!(p)
- elsif host =~ UNIX_SOCKET
- raise_invalid_error!(UNESCAPED_UNIX_SOCKET) if host =~ UNSAFE
- host = decode(host)
+ # Takes a host in ipv4/ipv6/hostname/socket path format and validates
+ # its format.
+ def validate_host!(host)
+ case host
+ when /\A\[[\d:]+\](?::(\d+))?\z/
+ # ipv6 with optional port
+ if port_str = $1
+ validate_port_string!(port_str)
end
- servers << host
+ when /\A\//, /\.sock\z/
+ # Unix socket path.
+ # Spec requires us to validate that the path has no unescaped
+ # slashes, but if this were to be the case, parsing would have
+ # already failed elsewhere because the URI would've been split in
+ # a weird place.
+ # The spec also allows relative socket paths and requires that
+ # socket paths end in ".sock". We accept all paths but special case
+ # the .sock extension to avoid relative paths falling into the
+ # host:port case below.
+ when /[\/\[\]]/
+ # Not a host:port nor an ipv4 address with optional port.
+ # Possibly botched ipv6 address with e.g. port delimiter present and
+ # port missing, or extra junk before or after.
+ raise_invalid_error!("Invalid hostname: #{host}")
+ when /:.*:/m
+ raise_invalid_error!("Multiple port delimiters are not allowed: #{host}")
+ else
+ # host:port or ipv4 address with optional port number
+ host, port = host.split(':')
+ if host.empty?
+ raise_invalid_error!("Host is empty: #{host}")
+ end
+
+ if port && port.empty?
+ raise_invalid_error!("Port is empty: #{port}")
+ end
+
+ validate_port_string!(port)
end
end
def raise_invalid_error!(details)
raise Error::InvalidURI.new(@string, details, FORMAT)
@@ -470,47 +510,47 @@
def self.uri_option(uri_key, name, extra = {})
URI_OPTION_MAP[uri_key] = { :name => name }.merge(extra)
end
# Replica Set Options
- uri_option 'replicaset', :replica_set, :type => :replica_set
+ uri_option 'replicaset', :replica_set
# Timeout Options
- uri_option 'connecttimeoutms', :connect_timeout, :type => :connect_timeout
- uri_option 'sockettimeoutms', :socket_timeout, :type => :socket_timeout
- uri_option 'serverselectiontimeoutms', :server_selection_timeout, :type => :server_selection_timeout
- uri_option 'localthresholdms', :local_threshold, :type => :local_threshold
- uri_option 'heartbeatfrequencyms', :heartbeat_frequency, :type => :heartbeat_frequency
- uri_option 'maxidletimems', :max_idle_time, :type => :max_idle_time
+ uri_option 'connecttimeoutms', :connect_timeout, :type => :ms
+ uri_option 'sockettimeoutms', :socket_timeout, :type => :ms
+ uri_option 'serverselectiontimeoutms', :server_selection_timeout, :type => :ms
+ uri_option 'localthresholdms', :local_threshold, :type => :ms
+ uri_option 'heartbeatfrequencyms', :heartbeat_frequency, :type => :ms
+ uri_option 'maxidletimems', :max_idle_time, :type => :ms
# Write Options
- uri_option 'w', :w, :group => :write, type: :w
- uri_option 'journal', :j, :group => :write, :type => :journal
- uri_option 'fsync', :fsync, :group => :write, type: :bool
- uri_option 'wtimeoutms', :wtimeout, :group => :write, :type => :wtimeout
+ uri_option 'w', :w, :group => :write_concern, type: :w
+ uri_option 'journal', :j, :group => :write_concern, :type => :bool
+ uri_option 'fsync', :fsync, :group => :write_concern, type: :bool
+ uri_option 'wtimeoutms', :wtimeout, :group => :write_concern, :type => :integer
# Read Options
uri_option 'readpreference', :mode, :group => :read, :type => :read_mode
uri_option 'readpreferencetags', :tag_sets, :group => :read, :type => :read_tags
uri_option 'maxstalenessseconds', :max_staleness, :group => :read, :type => :max_staleness
# Pool options
- uri_option 'minpoolsize', :min_pool_size, :type => :min_pool_size
- uri_option 'maxpoolsize', :max_pool_size, :type => :max_pool_size
- uri_option 'waitqueuetimeoutms', :wait_queue_timeout, :type => :wait_queue_timeout
+ uri_option 'minpoolsize', :min_pool_size, :type => :integer
+ uri_option 'maxpoolsize', :max_pool_size, :type => :integer
+ uri_option 'waitqueuetimeoutms', :wait_queue_timeout, :type => :ms
# Security Options
- uri_option 'ssl', :ssl, :type => :ssl
- uri_option 'tls', :ssl, :type => :tls
+ uri_option 'ssl', :ssl, :type => :repeated_bool
+ uri_option 'tls', :ssl, :type => :repeated_bool
uri_option 'tlsallowinvalidcertificates', :ssl_verify_certificate,
- :type => :ssl_verify_certificate
+ :type => :inverse_bool
uri_option 'tlsallowinvalidhostnames', :ssl_verify_hostname,
- :type => :ssl_verify_hostname
+ :type => :inverse_bool
uri_option 'tlscafile', :ssl_ca_cert
uri_option 'tlscertificatekeyfile', :ssl_cert
uri_option 'tlscertificatekeyfilepassword', :ssl_key_pass_phrase
- uri_option 'tlsinsecure', :ssl_verify, :type => :ssl_verify
+ uri_option 'tlsinsecure', :ssl_verify, :type => :inverse_bool
# Topology options
uri_option 'connect', :connect, type: :symbol
# Auth Options
@@ -519,13 +559,13 @@
uri_option 'authmechanismproperties', :auth_mech_properties, :type => :auth_mech_props
# Client Options
uri_option 'appname', :app_name
uri_option 'compressors', :compressors, :type => :array
- uri_option 'readconcernlevel', :level, group: :read_concern
- uri_option 'retryreads', :retry_reads, :type => :retry_reads
- uri_option 'retrywrites', :retry_writes, :type => :retry_writes
+ uri_option 'readconcernlevel', :level, group: :read_concern, type: :symbol
+ uri_option 'retryreads', :retry_reads, :type => :bool
+ uri_option 'retrywrites', :retry_writes, :type => :bool
uri_option 'zlibcompressionlevel', :zlib_compression_level, :type => :zlib_compression_level
# Applies URI value transformation by either using the default cast
# or a transformation appropriate for the given type.
#
@@ -600,47 +640,38 @@
target = select_target(uri_options, strategy[:group])
value = apply_transform(key, value, strategy[:type])
merge_uri_option(target, value, strategy[:name])
end
- # Replica set transformation, avoid converting to Symbol.
- #
- # @param value [String] Replica set name.
- #
- # @return [String] Same value to avoid cast to Symbol.
- def replica_set(value)
- decode(value)
- end
-
# Auth source transformation, either db string or :external.
#
# @param value [String] Authentication source.
#
# @return [String] If auth source is database name.
# @return [:external] If auth source is external authentication.
def auth_source(value)
- value == '$external' ? :external : decode(value)
+ value == '$external' ? :external : value
end
# Authentication mechanism transformation.
#
# @param value [String] The authentication mechanism.
#
# @return [Symbol] The transformed authentication mechanism.
def auth_mech(value)
- AUTH_MECH_MAP[value.upcase].tap do |mech|
+ (AUTH_MECH_MAP[value.upcase] || value).tap do |mech|
log_warn("#{value} is not a valid auth mechanism") unless mech
end
end
# Read preference mode transformation.
#
# @param value [String] The read mode string value.
#
# @return [Symbol] The read mode symbol.
def read_mode(value)
- READ_MODE_MAP[value.downcase]
+ READ_MODE_MAP[value.downcase] || value
end
# Read preference tags transformation.
#
# @param value [String] The string representing tag set.
@@ -666,11 +697,11 @@
# @return [ Hash ] The auth mechanism properties hash.
def auth_mech_props(value)
properties = hash_extractor('authMechanismProperties', value)
if properties[:canonicalize_host_name]
properties.merge!(canonicalize_host_name:
- %w(true TRUE).include?(properties[:canonicalize_host_name]))
+ properties[:canonicalize_host_name].downcase == 'true')
end
properties
end
# Parses the zlib compression level.
@@ -690,124 +721,21 @@
log_warn("#{value} is not a valid zlibCompressionLevel")
nil
end
- # Parses the max pool size.
+ # Converts the value into a boolean and returns it wrapped in an array.
#
- # @param value [ String ] The max pool size string.
+ # @param name [ String ] Name of the URI option being processed.
+ # @param value [ String ] URI option value.
#
- # @return [ Integer | nil ] The min pool size if it is valid, otherwise nil (and a warning will)
- # be logged.
- def max_pool_size(value)
- if /\A\d+\z/ =~ value
- return value.to_i
- end
-
- log_warn("#{value} is not a valid maxPoolSize")
- nil
+ # @return [ Array<true | false> ] The boolean value parsed and wraped
+ # in an array.
+ def convert_repeated_bool(name, value)
+ [convert_bool(name, value)]
end
-
- # Parses the min pool size.
- #
- # @param value [ String ] The min pool size string.
- #
- # @return [ Integer | nil ] The min pool size if it is valid, otherwise nil (and a warning will
- # be logged).
- def min_pool_size(value)
- if /\A\d+\z/ =~ value
- return value.to_i
- end
-
- log_warn("#{value} is not a valid minPoolSize")
- nil
- end
-
- # Parses the journal value.
- #
- # @param value [ String ] The journal value.
- #
- # @return [ true | false | nil ] The journal value parsed out, otherwise nil (and a warning
- # will be logged).
- def journal(value)
- convert_bool('journal', value)
- end
-
- # Parses the ssl value from the URI.
- #
- # @param value [ String ] The ssl value.
- #
- # @return [ Array<true | false> ] The ssl value parsed out (stored in an array to facilitate
- # keeping track of all values).
- def ssl(value)
- [convert_bool('ssl', value)]
- end
-
- # Parses the tls value from the URI.
- #
- # @param value [ String ] The tls value.
- #
- # @return [ Array<true | false> ] The tls value parsed out (stored in an array to facilitate
- # keeping track of all values).
- def tls(value)
- [convert_bool('tls', value)]
- end
-
- # Parses the ssl_verify value from the tlsInsecure URI value. Note that this will be the inverse
- # of the value of tlsInsecure (if present).
- #
- # @param value [ String ] The tlsInsecure value.
- #
- # @return [ true | false | nil ] The ssl_verify value parsed out, otherwise nil (and a warning
- # will be logged).
- def ssl_verify(value)
- inverse_bool('tlsAllowInvalidCertificates', value)
- end
-
- # Parses the ssl_verify_certificate value from the tlsAllowInvalidCertificates URI value. Note
- # that this will be the inverse of the value of tlsInsecure (if present).
- #
- # @param value [ String ] The tlsAllowInvalidCertificates value.
- #
- # @return [ true | false | nil ] The ssl_verify_certificate value parsed out, otherwise nil
- # (and a warning will be logged).
- def ssl_verify_certificate(value)
- inverse_bool('tlsAllowInvalidCertificates', value)
- end
-
- # Parses the ssl_verify_hostname value from the tlsAllowInvalidHostnames URI value. Note that
- # this will be the inverse of the value of tlsAllowInvalidHostnames (if present).
- #
- # @param value [ String ] The tlsAllowInvalidHostnames value.
- #
- # @return [ true | false | nil ] The ssl_verify_hostname value parsed out, otherwise nil
- # (and a warning will be logged).
- def ssl_verify_hostname(value)
- inverse_bool('tlsAllowInvalidHostnames', value)
- end
-
- # Parses the retryReads value.
- #
- # @param value [ String ] The retryReads value.
- #
- # @return [ true | false | nil ] The boolean value parsed out, otherwise nil (and a warning
- # will be logged).
- def retry_reads(value)
- convert_bool('retryReads', value)
- end
-
- # Parses the retryWrites value.
- #
- # @param value [ String ] The retryWrites value.
- #
- # @return [ true | false | nil ] The boolean value parsed out, otherwise nil (and a warning
- # will be logged).
- def retry_writes(value)
- convert_bool('retryWrites', value)
- end
-
# Converts +value+ into an integer.
#
# If the value is not a valid integer, warns and returns nil.
#
# @param name [ String ] Name of the URI option being processed.
@@ -878,11 +806,11 @@
#
# @param value [ String ] The URI option value.
#
# @return [ true | false | nil ] The inverse of the boolean value parsed out, otherwise nil
# (and a warning will be logged).
- def inverse_bool(name, value)
+ def convert_inverse_bool(name, value)
b = convert_bool(name, value)
if b.nil?
nil
else
@@ -895,119 +823,38 @@
# @param value [ String ] The max staleness string.
#
# @return [ Integer | nil ] The max staleness integer parsed out if it is valid, otherwise nil
# (and a warning will be logged).
def max_staleness(value)
- if /\A\d+\z/ =~ value
+ if /\A-?\d+\z/ =~ value
int = value.to_i
- if int >= 0 && int < 90
- log_warn("max staleness must be either 0 or greater than 90: #{value}")
+ if int == -1
+ int = nil
end
+ if int && (int >= 0 && int < 90 || int < 0)
+ log_warn("max staleness should be either 0 or greater than 90: #{value}")
+ end
+
return int
end
log_warn("Invalid max staleness value: #{value}")
nil
end
- # Parses the connectTimeoutMS value.
- #
- # @param value [ String ] The connectTimeoutMS value.
- #
- # @return [ Integer | nil ] The integer parsed out, otherwise nil (and a warning will be
- # logged).
- def connect_timeout(value)
- ms_convert('connectTimeoutMS', value)
- end
-
- # Parses the localThresholdMS value.
- #
- # @param value [ String ] The localThresholdMS value.
- #
- # @return [ Integer | nil ] The integer parsed out, otherwise nil (and a warning will be
- # logged).
- def local_threshold(value)
- ms_convert('localThresholdMS', value)
- end
-
- # Parses the heartbeatFrequencyMS value.
- #
- # @param value [ String ] The heartbeatFrequencyMS value.
- #
- # @return [ Integer | nil ] The integer parsed out, otherwise nil (and a warning will be
- # logged).
- def heartbeat_frequency(value)
- ms_convert('heartbeatFrequencyMS', value)
- end
-
- # Parses the maxIdleTimeMS value.
- #
- # @param value [ String ] The maxIdleTimeMS value.
- #
- # @return [ Integer | nil ] The integer parsed out, otherwise nil (and a warning will be
- # logged).
- def max_idle_time(value)
- ms_convert('maxIdleTimeMS', value)
- end
-
- # Parses the serverSelectionMS value.
- #
- # @param value [ String ] The serverSelectionMS value.
- #
- # @return [ Integer | nil ] The integer parsed out, otherwise nil (and a warning will be
- # logged).
- def server_selection_timeout(value)
- ms_convert('serverSelectionTimeoutMS', value)
- end
-
- # Parses the socketTimeoutMS value.
- #
- # @param value [ String ] The socketTimeoutMS value.
- #
- # @return [ Integer | nil ] The integer parsed out, otherwise nil (and a warning will be
- # logged).
- def socket_timeout(value)
- ms_convert('socketTimeoutMS', value)
- end
-
- # Parses the waitQueueTimeoutMS value.
- #
- # @param value [ String ] The waitQueueTimeoutMS value.
- #
- # @return [ Integer | nil ] The integer parsed out, otherwise nil (and a warning will be
- # logged).
- def wait_queue_timeout(value)
- ms_convert('MS', value)
- end
-
- # Parses the wtimeoutMS value.
- #
- # @param value [ String ] The wtimeoutMS value.
- #
- # @return [ Integer | nil ] The integer parsed out, otherwise nil (and a warning will be
- # logged).
- def wtimeout(value)
- unless /\A\d+\z/ =~ value
- log_warn("Invalid wtimeoutMS value: #{value}")
- return nil
- end
-
- value.to_i
- end
-
# Ruby's convention is to provide timeouts in seconds, not milliseconds and
# to use fractions where more precision is necessary. The connection string
# options are always in MS so we provide an easy conversion type.
#
# @param [ Integer ] value The millisecond value.
#
# @return [ Float ] The seconds value.
#
# @since 2.0.0
- def ms_convert(name, value)
+ def convert_ms(name, value)
unless /\A-?\d+(\.\d+)?\z/ =~ value
log_warn("Invalid ms value for #{name}: #{value}")
return nil
end
@@ -1030,10 +877,10 @@
if v.nil?
log_warn("Invalid hash value for #{name}: #{value}")
return nil
end
- set.merge(decode(k).downcase.to_sym => decode(v))
+ set.merge(k.downcase.to_sym => v)
end
end
# Extract values from the string and put them into an array.
#