lib/mongo/uri.rb in mongo-2.1.0.beta vs lib/mongo/uri.rb in mongo-2.1.0.rc0
- old
+ new
@@ -27,63 +27,121 @@
#
# @since 2.0.0
class URI
include Loggable
- # Scheme Regex: non-capturing, matches scheme.
+ # The uri parser object options.
#
# @since 2.0.0
- SCHEME = %r{(?:mongodb://)}.freeze
+ attr_reader :options
- # User Regex: capturing, group 1, matches anything but ':'
+ # The options specified in the uri.
#
- # @since 2.0.0
- USER = /([^:]+)/.freeze
+ # @since 2.1.0
+ attr_reader :uri_options
- # Password Regex: capturing, group 2, matches anything but '@'
+ # The servers specified in the uri.
#
# @since 2.0.0
- PASSWORD = /([^@]+)/.freeze
+ attr_reader :servers
- # Credentials Regex: non capturing, matches 'user:password@'
+ # Unsafe characters that must be urlencoded.
#
- # @since 2.0.0
- CREDENTIALS = /(?:#{USER}:#{PASSWORD}?@)?/.freeze
+ # @since 2.1.0
+ UNSAFE = /[\:\/\+\@]/
- # Host and port server Regex: matches anything but a forward slash
+ # Unix socket suffix.
#
- # @since 2.0.0
- HOSTPORT = /[^\/]+/.freeze
+ # @since 2.1.0
+ UNIX_SOCKET = /.sock/
- # Unix socket server Regex: matches unix socket server
+ # The mongodb connection string scheme.
#
# @since 2.0.0
- UNIX = /\/.+.sock?/.freeze
+ SCHEME = 'mongodb://'.freeze
- # server Regex: capturing, matches host and port server or unix server
+ # The character delimiting hosts.
#
- # @since 2.0.0
- SERVERS = /((?:(?:#{HOSTPORT}|#{UNIX}),?)+)/.freeze
+ # @since 2.1.0
+ HOST_DELIM = ','.freeze
- # Database Regex: matches anything but the characters that cannot
- # be part of any MongoDB database name.
+ # The character separating a host and port.
#
- # @since 2.0.0
- DATABASE = %r{(?:/([^/\.\ "*<>:\|\?]*))?}.freeze
+ # @since 2.1.0
+ HOST_PORT_DELIM = ':'.freeze
- # Option Regex: only matches the ampersand separator and does
- # not allow for semicolon to be used to separate options.
+ # The character delimiting a database.
#
- # @since 2.0.0
- OPTIONS = /(?:\?(?:(.+=.+)&?)+)*/.freeze
+ # @since 2.1.0
+ DATABASE_DELIM = '/'.freeze
- # Complete URI Regex: matches all of the combined components
+ # The character delimiting options.
#
- # @since 2.0.0
- URI = /#{SCHEME}#{CREDENTIALS}#{SERVERS}#{DATABASE}#{OPTIONS}/.freeze
+ # @since 2.1.0
+ URI_OPTS_DELIM = '?'.freeze
+ # The character delimiting multiple options.
+ #
+ # @since 2.1.0
+ INDIV_URI_OPTS_DELIM = '&'.freeze
+ # The character delimiting an option and its value.
+ #
+ # @since 2.1.0
+ URI_OPTS_VALUE_DELIM = '='.freeze
+
+ # The character separating a username from the password.
+ #
+ # @since 2.1.0
+ AUTH_USER_PWD_DELIM = ':'.freeze
+
+ # The character delimiting auth credentials.
+ #
+ # @since 2.1.0
+ AUTH_DELIM = '@'.freeze
+
+ # Error details for an invalid scheme.
+ #
+ # @since 2.1.0
+ INVALID_SCHEME = "Invalid scheme. Scheme must be '#{SCHEME}'".freeze
+
+ # Error details for an invalid options format.
+ #
+ # @since 2.1.0
+ INVALID_OPTS_VALUE_DELIM = "Options and their values must be delimited" +
+ " by '#{URI_OPTS_VALUE_DELIM}'".freeze
+
+ # Error details for an non-urlencoded user name or password.
+ #
+ # @since 2.1.0
+ UNESCAPED_USER_PWD = "User name and password must be urlencoded.".freeze
+
+ # Error details for a non-urlencoded unix socket path.
+ #
+ # @since 2.1.0
+ UNESCAPED_UNIX_SOCKET = "UNIX domain sockets must be urlencoded.".freeze
+
+ # Error details for a non-urlencoded auth databsae name.
+ #
+ # @since 2.1.0
+ UNESCAPED_DATABASE = "Auth database must be urlencoded.".freeze
+
+ # Error details for providing options without a database delimiter.
+ #
+ # @since 2.1.0
+ INVALID_OPTS_DELIM = "Database delimiter '#{DATABASE_DELIM}' must be present if options are specified.".freeze
+
+ # Error details for a missing host.
+ #
+ # @since 2.1.0
+ INVALID_HOST = "Missing host; at least one must be provided.".freeze
+
+ # Error details for an invalid port.
+ #
+ # @since 2.1.0
+ INVALID_PORT = "Invalid port. Port must be an integer greater than 0 and less than 65536".freeze
+
# MongoDB URI format specification.
#
# @since 2.0.0
FORMAT = 'mongodb://[username:password@]host1[:port1][,host2[:port2]' +
',...[,hostN[:portN]]][/[database][?options]]'.freeze
@@ -111,38 +169,34 @@
'PLAIN' => :plain,
'MONGODB-CR' => :mongodb_cr,
'GSSAPI' => :gssapi
}.freeze
+ # Options that are allowed to appear more than once in the uri.
+ #
+ # @since 2.1.0
+ REPEATABLE_OPTIONS = [ :tag_sets ]
+
# Create the new uri from the provided string.
#
# @example Create the new URI.
# URI.new('mongodb://localhost:27017')
#
# @param [ String ] string The uri string.
+ # @param [ Hash ] options The options.
#
- # @raise [ BadURI ] If the uri does not match the spec.
+ # @raise [ Error::InvalidURI ] If the uri does not match the spec.
#
# @since 2.0.0
- def initialize(string)
+ def initialize(string, options = {})
@string = string
- @match = @string.match(URI)
- raise Error::InvalidURI.new(string) unless @match
+ @options = options
+ empty, scheme, remaining = @string.partition(SCHEME)
+ raise_invalid_error!(INVALID_SCHEME) unless scheme == SCHEME
+ setup!(remaining)
end
- # Get the servers provided in the URI.
- #
- # @example Get the servers.
- # uri.servers
- #
- # @return [ Array<String> ] The servers.
- #
- # @since 2.0.0
- def servers
- @match[3].split(',')
- end
-
# Gets the options hash that needs to be passed to a Mongo::Client on
# instantiation, so we don't have to merge the credentials and database in
# at that point - we only have a single point here.
#
# @example Get the client options.
@@ -150,12 +204,12 @@
#
# @return [ Hash ] The options passed to the Mongo::Client
#
# @since 2.0.0
def client_options
- opts = options.merge(:database => database)
- user ? opts.merge(credentials) : opts
+ opts = uri_options.merge(:database => database)
+ @user ? opts.merge(credentials) : opts
end
# Get the credentials provided in the URI.
#
# @example Get the credentials.
@@ -165,11 +219,11 @@
# * :user [ String ] The user.
# * :password [ String ] The provided password.
#
# @since 2.0.0
def credentials
- { :user => user, :password => password }
+ { :user => @user, :password => @password }
end
# Get the database provided in the URI.
#
# @example Get the database.
@@ -177,120 +231,166 @@
#
# @return [String] The database.
#
# @since 2.0.0
def database
- @match[4].nil? ? Database::ADMIN : @match[4]
+ @database ? @database : Database::ADMIN
end
- # Get the options provided in the URI.
- #
- # @example Get The options.
- # uri.options
- #
- # @return [Hash] The options.
- #
- # Generic Options
- # * :replica_set [String] replica set name
- # * :connect_timeout [Fixnum] connect timeout
- # * :socket_timeout [Fixnum] socket timeout
- # * :ssl [true, false] ssl enabled?
- #
- # Write Options (returned in a hash under the :write key)
- # * :w [String, Fixnum] write concern value
- # * :j [true, false] journal
- # * :fsync [true, false] fsync
- # * :timeout [Fixnum] timeout for write operation
- #
- # Read Options (returned in a hash under the :read key)
- # * :mode [Symbol] read mode
- # * :tag_sets [Array<Hash>] read tag sets
- #
- # @since 2.0.0
- def options
- parsed_options = @match[5]
- return {} unless parsed_options
- parsed_options.split('&').reduce({}) do |options, option|
- key, value = option.split('=')
- strategy = OPTION_MAP[key]
+ private
+
+ def setup!(remaining)
+ creds_hosts, db_opts = extract_db_opts!(remaining)
+ parse_creds_hosts!(creds_hosts)
+ parse_db_opts!(db_opts)
+ end
+
+ def extract_db_opts!(string)
+ db_opts, d, creds_hosts = string.reverse.partition(DATABASE_DELIM)
+ db_opts, creds_hosts = creds_hosts, db_opts if creds_hosts.empty?
+ if db_opts.empty? && creds_hosts.include?(URI_OPTS_DELIM)
+ raise_invalid_error!(INVALID_OPTS_DELIM)
+ end
+ [ creds_hosts, db_opts ].map { |s| s.reverse }
+ end
+
+ def parse_creds_hosts!(string)
+ hosts, creds = split_creds_hosts(string)
+ @servers = parse_servers!(hosts)
+ @user = parse_user!(creds)
+ @password = parse_password!(creds)
+ end
+
+ def split_creds_hosts(string)
+ hosts, d, creds = string.reverse.partition(AUTH_DELIM)
+ hosts, creds = creds, hosts if hosts.empty?
+ [ hosts, creds ].map { |s| s.reverse }
+ end
+
+ def parse_db_opts!(string)
+ auth_db, d, uri_opts = string.partition(URI_OPTS_DELIM)
+ @uri_options = parse_uri_options!(uri_opts)
+ @database = parse_database!(auth_db)
+ end
+
+ def parse_uri_options!(string)
+ return {} unless string
+ string.split(INDIV_URI_OPTS_DELIM).reduce({}) do |uri_options, opt|
+ raise_invalid_error!(INVALID_OPTS_VALUE_DELIM) unless opt.index(URI_OPTS_VALUE_DELIM)
+ key, value = opt.split(URI_OPTS_VALUE_DELIM)
+ strategy = URI_OPTION_MAP[key.downcase]
if strategy.nil?
- log_warn([
- "Unsupported URI option '#{key}' on URI '#{@string}'. It will be ignored."
- ])
+ log_warn("Unsupported URI option '#{key}' on URI '#{@string}'. It will be ignored.")
else
- add_option(strategy, value, options)
+ add_uri_option(strategy, value, uri_options)
end
- options
+ uri_options
end
end
- private
+ def parse_user!(string)
+ if (string && user = string.partition(AUTH_USER_PWD_DELIM)[0])
+ raise_invalid_error!(UNESCAPED_USER_PWD) if user =~ UNSAFE
+ decode(user) if user.length > 0
+ end
+ end
+ def parse_password!(string)
+ if (string && pwd = string.partition(AUTH_USER_PWD_DELIM)[2])
+ raise_invalid_error!(UNESCAPED_USER_PWD) if pwd =~ UNSAFE
+ decode(pwd) if pwd.length > 0
+ end
+ end
+
+ def parse_database!(string)
+ raise_invalid_error!(UNESCAPED_DATABASE) if string =~ UNSAFE
+ decode(string) if string.length > 0
+ end
+
+ def validate_port_string!(port)
+ 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, d, 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
+ end
+ servers << host
+ end
+ end
+
+ def raise_invalid_error!(details)
+ raise Error::InvalidURI.new(@string, details)
+ end
+
+ def decode(value)
+ ::URI.decode(value)
+ end
+
# Hash for storing map of URI option parameters to conversion strategies
- OPTION_MAP = {}
+ URI_OPTION_MAP = {}
- # Simple internal dsl to register a MongoDB URI option in the OPTION_MAP.
+ # Simple internal dsl to register a MongoDB URI option in the URI_OPTION_MAP.
#
# @param uri_key [String] The MongoDB URI option to register.
# @param name [Symbol] The name of the option in the driver.
# @param extra [Hash] Extra options.
# * :group [Symbol] Nested hash where option will go.
# * :type [Symbol] Name of function to transform value.
- def self.option(uri_key, name, extra = {})
- OPTION_MAP[uri_key] = { :name => name }.merge(extra)
+ def self.uri_option(uri_key, name, extra = {})
+ URI_OPTION_MAP[uri_key] = { :name => name }.merge(extra)
end
# Replica Set Options
- option 'replicaSet', :replica_set, :type => :replica_set
+ uri_option 'replicaset', :replica_set, :type => :replica_set
# Timeout Options
- option 'connectTimeoutMS', :connect_timeout, :type => :ms_convert
- option 'socketTimeoutMS', :socket_timeout, :type => :ms_convert
- option 'serverSelectionTimeoutMS', :server_selection_timeout, :type => :ms_convert
- option 'localThresholdMS', :local_threshold, :type => :ms_convert
+ uri_option 'connecttimeoutms', :connect_timeout, :type => :ms_convert
+ uri_option 'sockettimeoutms', :socket_timeout, :type => :ms_convert
+ uri_option 'serverselectiontimeoutms', :server_selection_timeout, :type => :ms_convert
+ uri_option 'localthresholdms', :local_threshold, :type => :ms_convert
# Write Options
- option 'w', :w, :group => :write
- option 'journal', :j, :group => :write
- option 'fsync', :fsync, :group => :write
- option 'wtimeoutMS', :timeout, :group => :write
+ uri_option 'w', :w, :group => :write
+ uri_option 'journal', :j, :group => :write
+ uri_option 'fsync', :fsync, :group => :write
+ uri_option 'wtimeoutms', :timeout, :group => :write
# Read Options
- option 'readPreference', :mode, :group => :read, :type => :read_mode
- option 'readPreferenceTags', :tag_sets, :group => :read, :type => :read_tags
+ uri_option 'readpreference', :mode, :group => :read, :type => :read_mode
+ uri_option 'readpreferencetags', :tag_sets, :group => :read, :type => :read_tags
# Pool options
- option 'minPoolSize', :min_pool_size
- option 'maxPoolSize', :max_pool_size
- option 'waitQueueTimeoutMS', :wait_queue_timeout, :type => :ms_convert
+ uri_option 'minpoolsize', :min_pool_size
+ uri_option 'maxpoolsize', :max_pool_size
+ uri_option 'waitqueuetimeoutms', :wait_queue_timeout, :type => :ms_convert
# Security Options
- option 'ssl', :ssl
+ uri_option 'ssl', :ssl
# Topology options
- option 'connect', :connect
+ uri_option 'connect', :connect
# Auth Options
- option 'authSource', :source, :group => :auth, :type => :auth_source
- option 'authMechanism', :mechanism, :group => :auth, :type => :auth_mech
- option 'authMechanismProperties', :auth_mech_properties, :group => :auth,
- :type => :auth_mech_props
+ uri_option 'authsource', :source, :group => :auth, :type => :auth_source
+ uri_option 'authmechanism', :auth_mech, :type => :auth_mech
+ uri_option 'authmechanismproperties', :auth_mech_properties, :group => :auth,
+ :type => :auth_mech_props
- # Gets the user provided in the URI
- #
- # @return [String] The user.
- def user
- @match[1]
- end
-
- # Gets the password provided in the URI
- #
- # @return [String] The password.
- def password
- @match[2]
- end
-
# Casts option values that do not have a specifically provided
# transofrmation to the appropriate type.
#
# @param value [String] The value to be cast.
#
@@ -301,11 +401,11 @@
elsif value == 'false'
false
elsif value =~ /[\d]/
value.to_i
else
- value.to_sym
+ decode(value).to_sym
end
end
# Applies URI value transformation by either using the default cast
# or a transformation appropriate for the given type.
@@ -320,19 +420,19 @@
end
end
# Selects the output destination for an option.
#
- # @param options [Hash] The base target.
- # @param group [Symbol] Group subtarget.
+ # @param [Hash] uri_options The base target.
+ # @param [Symbol] group Group subtarget.
#
# @return [Hash] The target for the option.
- def select_target(options, group = nil)
+ def select_target(uri_options, group = nil)
if group
- options[group] ||= {}
+ uri_options[group] ||= {}
else
- options
+ uri_options
end
end
# Merges a new option into the target.
#
@@ -343,50 +443,54 @@
# to the array of tag sets without overwriting the original.
#
# @param target [Hash] The destination.
# @param value [Object] The value to be merged.
# @param name [Symbol] The name of the option.
- def merge_option(target, value, name)
+ def merge_uri_option(target, value, name)
if target.key?(name)
- target[name] += value
+ if REPEATABLE_OPTIONS.include?(name)
+ target[name] += value
+ else
+ log_warn("Repeated option key: #{name}.")
+ end
else
target.merge!(name => value)
end
end
- # Adds an option to the options hash via the supplied strategy.
+ # Adds an option to the uri options hash via the supplied strategy.
#
# Acquires a target for the option based on group.
# Transforms the value.
# Merges the option into the target.
#
# @param strategy [Symbol] The strategy for this option.
# @param value [String] The value of the option.
- # @param options [Hash] The base option target.
- def add_option(strategy, value, options)
- target = select_target(options, strategy[:group])
+ # @param uri_options [Hash] The base option target.
+ def add_uri_option(strategy, value, uri_options)
+ target = select_target(uri_options, strategy[:group])
value = apply_transform(value, strategy[:type])
- merge_option(target, value, strategy[:name])
+ 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)
- 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 : value
+ value == '$external' ? :external : decode(value)
end
# Authentication mechanism transformation.
#
# @param value [String] The authentication mechanism.
@@ -456,10 +560,10 @@
#
# @return [ Hash ] The hash built from the string.
def hash_extractor(value)
value.split(',').reduce({}) do |set, tag|
k, v = tag.split(':')
- set.merge(k.downcase.to_sym => v)
+ set.merge(decode(k).downcase.to_sym => decode(v))
end
end
end
end