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. #