Skip to content

Commit b1b1a4a

Browse files
committed
Merge pull request #675 from estolfo/RUBY-951-conn-string
RUBY-951 Update YAML tests and refactor URI class
2 parents 89755b1 + 7ccc407 commit b1b1a4a

File tree

6 files changed

+176
-149
lines changed

6 files changed

+176
-149
lines changed

lib/mongo/uri.rb

Lines changed: 95 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,16 @@ class URI
4444
# @since 2.0.0
4545
attr_reader :servers
4646

47-
# Unsafe characters that must be URI-escaped.
47+
# Unsafe characters that must be urlencoded.
4848
#
4949
# @since 2.1.0
5050
UNSAFE = /[\:\/\+\@]/
5151

52+
# Unix socket suffix.
53+
#
54+
# @since 2.1.0
55+
UNIX_SOCKET = /.sock/
56+
5257
# The mongodb connection string scheme.
5358
#
5459
# @since 2.0.0
@@ -67,7 +72,7 @@ class URI
6772
# The character delimiting a database.
6873
#
6974
# @since 2.1.0
70-
DATABSE_DELIM = '/'.freeze
75+
DATABASE_DELIM = '/'.freeze
7176

7277
# The character delimiting options.
7378
#
@@ -105,30 +110,35 @@ class URI
105110
INVALID_OPTS_VALUE_DELIM = "Options and their values must be delimited" +
106111
" by '#{URI_OPTS_VALUE_DELIM}'".freeze
107112

108-
# Error details for an un-escaped user name or password.
113+
# Error details for an non-urlencoded user name or password.
109114
#
110115
# @since 2.1.0
111-
UNESCAPED_USER_PWD = "User name and password must be URI-escaped.".freeze
116+
UNESCAPED_USER_PWD = "User name and password must be urlencoded.".freeze
112117

113-
# Error details for a non-delimited database name.
118+
# Error details for a non-urlencoded unix socket path.
114119
#
115120
# @since 2.1.0
116-
INVALID_DB_DELIM = "Database must be delimited by a #{DATABSE_DELIM}.".freeze
121+
UNESCAPED_UNIX_SOCKET = "UNIX domain sockets must be urlencoded.".freeze
117122

118-
# Error details for a missing host.
123+
# Error details for a non-urlencoded auth databsae name.
119124
#
120125
# @since 2.1.0
121-
INVALID_HOST = "At least one host must be specified.".freeze
126+
UNESCAPED_DATABASE = "Auth database must be urlencoded.".freeze
122127

123-
# Error details for an invalid port.
128+
# Error details for providing options without a database delimiter.
124129
#
125130
# @since 2.1.0
126-
INVALID_PORT = "Invalid port. Port must be greater than 0 and less than 65536".freeze
131+
INVALID_OPTS_DELIM = "Database delimiter '#{DATABASE_DELIM}' must be present if options are specified.".freeze
127132

128-
# Error details for an invalid host:port format.
133+
# Error details for a missing host.
129134
#
130135
# @since 2.1.0
131-
INVALID_HOST_PORT= "Invalid host:port format.".freeze
136+
INVALID_HOST = "Missing host; at least one must be provided.".freeze
137+
138+
# Error details for an invalid port.
139+
#
140+
# @since 2.1.0
141+
INVALID_PORT = "Invalid port. Port must be an integer greater than 0 and less than 65536".freeze
132142

133143
# MongoDB URI format specification.
134144
#
@@ -161,6 +171,11 @@ class URI
161171
'GSSAPI' => :gssapi
162172
}.freeze
163173

174+
# Options that are allowed to appear more than once in the uri.
175+
#
176+
# @since 2.1.0
177+
REPEATABLE_OPTIONS = [ :tag_sets ]
178+
164179
# Create the new uri from the provided string.
165180
#
166181
# @example Create the new URI.
@@ -175,8 +190,8 @@ class URI
175190
def initialize(string, options = {})
176191
@string = string
177192
@options = options
178-
remaining = @string.split(SCHEME)[1]
179-
raise_invalid_error!(INVALID_SCHEME) unless remaining
193+
empty, scheme, remaining = @string.partition(SCHEME)
194+
raise_invalid_error!(INVALID_SCHEME) unless scheme == SCHEME
180195
setup!(remaining)
181196
end
182197

@@ -218,14 +233,48 @@ def credentials
218233
#
219234
# @since 2.0.0
220235
def database
221-
@database ? ::URI.decode(@database) : Database::ADMIN
236+
@database ? @database : Database::ADMIN
222237
end
223238

224239
private
225240

226-
def parse_uri_options!(part, remaining)
227-
return {} unless part
228-
part.split(INDIV_URI_OPTS_DELIM).reduce({}) do |uri_options, opt|
241+
def setup!(remaining)
242+
creds_hosts, db_opts = extract_db_opts!(remaining)
243+
parse_creds_hosts!(creds_hosts)
244+
parse_db_opts!(db_opts)
245+
end
246+
247+
def extract_db_opts!(string)
248+
db_opts, d, creds_hosts = string.reverse.partition(DATABASE_DELIM)
249+
db_opts, creds_hosts = creds_hosts, db_opts if creds_hosts.empty?
250+
if db_opts.empty? && creds_hosts.include?(URI_OPTS_DELIM)
251+
raise_invalid_error!(INVALID_OPTS_DELIM)
252+
end
253+
[ creds_hosts, db_opts ].map { |s| s.reverse }
254+
end
255+
256+
def parse_creds_hosts!(string)
257+
hosts, creds = split_creds_hosts(string)
258+
@servers = parse_servers!(hosts)
259+
@user = parse_user!(creds)
260+
@password = parse_password!(creds)
261+
end
262+
263+
def split_creds_hosts(string)
264+
hosts, d, creds = string.reverse.partition(AUTH_DELIM)
265+
hosts, creds = creds, hosts if hosts.empty?
266+
[ hosts, creds ].map { |s| s.reverse }
267+
end
268+
269+
def parse_db_opts!(string)
270+
auth_db, d, uri_opts = string.partition(URI_OPTS_DELIM)
271+
@uri_options = parse_uri_options!(uri_opts)
272+
@database = parse_database!(auth_db)
273+
end
274+
275+
def parse_uri_options!(string)
276+
return {} unless string
277+
string.split(INDIV_URI_OPTS_DELIM).reduce({}) do |uri_options, opt|
229278
raise_invalid_error!(INVALID_OPTS_VALUE_DELIM) unless opt.index(URI_OPTS_VALUE_DELIM)
230279
key, value = opt.split(URI_OPTS_VALUE_DELIM)
231280
strategy = URI_OPTION_MAP[key.downcase]
@@ -238,52 +287,23 @@ def parse_uri_options!(part, remaining)
238287
end
239288
end
240289

241-
def extract_uri_options!(remaining)
242-
if index = remaining.index(URI_OPTS_DELIM)
243-
part = remaining[index+1..-1]
244-
remaining = remaining[0...index]
245-
end
246-
[ parse_uri_options!(part, remaining), remaining ]
247-
end
248-
249-
def parse_user!(part)
250-
if (part && user = part.partition(AUTH_USER_PWD_DELIM)[0])
290+
def parse_user!(string)
291+
if (string && user = string.partition(AUTH_USER_PWD_DELIM)[0])
251292
raise_invalid_error!(UNESCAPED_USER_PWD) if user =~ UNSAFE
252-
::URI.decode(user)
293+
decode(user) if user.length > 0
253294
end
254295
end
255296

256-
def parse_password!(part)
257-
if (part && pwd = part.partition(AUTH_USER_PWD_DELIM)[2])
297+
def parse_password!(string)
298+
if (string && pwd = string.partition(AUTH_USER_PWD_DELIM)[2])
258299
raise_invalid_error!(UNESCAPED_USER_PWD) if pwd =~ UNSAFE
259-
::URI.decode(pwd) unless pwd.length == 0
300+
decode(pwd) if pwd.length > 0
260301
end
261302
end
262303

263-
def extract_auth!(remaining)
264-
if index = remaining.reverse.index(AUTH_DELIM)
265-
part = remaining[0...-(index+1)]
266-
remaining = remaining[part.size+1..-1]
267-
end
268-
[ parse_user!(part), parse_password!(part), remaining ]
269-
end
270-
271-
def extract_database!(remaining)
272-
if index = remaining.reverse.index(DATABSE_DELIM)
273-
if index == 0
274-
part = nil
275-
remaining = remaining[0...-1]
276-
else
277-
db = remaining[-index..-1]
278-
unless db.end_with?('.sock')
279-
part = db
280-
remaining = remaining[0..-(part.size+2)]
281-
end
282-
end
283-
elsif !@uri_options.empty?
284-
raise_invalid_error!(INVALID_DB_DELIM)
285-
end
286-
[ part, remaining ]
304+
def parse_database!(string)
305+
raise_invalid_error!(UNESCAPED_DATABASE) if string =~ UNSAFE
306+
decode(string) if string.length > 0
287307
end
288308

289309
def validate_port_string!(port)
@@ -292,37 +312,31 @@ def validate_port_string!(port)
292312
end
293313
end
294314

295-
def parse_servers!(remaining)
296-
raise_invalid_error!(INVALID_HOST) unless remaining.size > 0
297-
remaining.split(HOST_DELIM).reduce([]) do |servers, host|
315+
def parse_servers!(string)
316+
raise_invalid_error!(INVALID_HOST) unless string.size > 0
317+
string.split(HOST_DELIM).reduce([]) do |servers, host|
298318
if host[0] == '['
299319
if host.index(']:')
300320
h, p = host.split(']:')
301321
validate_port_string!(p)
302322
end
303323
elsif host.index(HOST_PORT_DELIM)
304-
raise_invalid_error!(INVALID_HOST_PORT) unless host.count(HOST_PORT_DELIM) == 1
305-
h, p = host.split(HOST_PORT_DELIM)
306-
raise_invalid_error!(INVALID_HOST) unless h
324+
h, d, p = host.partition(HOST_PORT_DELIM)
325+
raise_invalid_error!(INVALID_HOST) unless h.size > 0
307326
validate_port_string!(p)
327+
elsif host =~ UNIX_SOCKET
328+
raise_invalid_error!(UNESCAPED_UNIX_SOCKET) if host =~ UNSAFE
308329
end
309330
servers << host
310331
end
311332
end
312333

313-
def extract_servers!(remaining)
314-
[ parse_servers!(remaining), remaining ]
315-
end
316-
317334
def raise_invalid_error!(details)
318335
raise Error::InvalidURI.new(@string, details)
319336
end
320337

321-
def setup!(remaining)
322-
@uri_options, remaining = extract_uri_options!(remaining)
323-
@user, @password, remaining = extract_auth!(remaining) if remaining
324-
@database, remaining = extract_database!(remaining) if remaining
325-
@servers, remaining = extract_servers!(remaining) if remaining
338+
def decode(value)
339+
::URI.decode(value)
326340
end
327341

328342
# Hash for storing map of URI option parameters to conversion strategies
@@ -373,7 +387,7 @@ def self.uri_option(uri_key, name, extra = {})
373387
uri_option 'authsource', :source, :group => :auth, :type => :auth_source
374388
uri_option 'authmechanism', :auth_mech, :type => :auth_mech
375389
uri_option 'authmechanismproperties', :auth_mech_properties, :group => :auth,
376-
:type => :auth_mech_props
390+
:type => :auth_mech_props
377391

378392
# Casts option values that do not have a specifically provided
379393
# transofrmation to the appropriate type.
@@ -389,7 +403,7 @@ def cast(value)
389403
elsif value =~ /[\d]/
390404
value.to_i
391405
else
392-
value.to_sym
406+
decode(value).to_sym
393407
end
394408
end
395409

@@ -433,7 +447,11 @@ def select_target(uri_options, group = nil)
433447
# @param name [Symbol] The name of the option.
434448
def merge_uri_option(target, value, name)
435449
if target.key?(name)
436-
target[name] += value
450+
if REPEATABLE_OPTIONS.include?(name)
451+
target[name] += value
452+
else
453+
log_warn("Repeated option key: #{name}.")
454+
end
437455
else
438456
target.merge!(name => value)
439457
end
@@ -460,7 +478,7 @@ def add_uri_option(strategy, value, uri_options)
460478
#
461479
# @return [String] Same value to avoid cast to Symbol.
462480
def replica_set(value)
463-
::URI.decode(value)
481+
decode(value)
464482
end
465483

466484
# Auth source transformation, either db string or :external.
@@ -470,7 +488,7 @@ def replica_set(value)
470488
# @return [String] If auth source is database name.
471489
# @return [:external] If auth source is external authentication.
472490
def auth_source(value)
473-
value == '$external' ? :external : value
491+
value == '$external' ? :external : decode(value)
474492
end
475493

476494
# Authentication mechanism transformation.
@@ -544,7 +562,7 @@ def ms_convert(value)
544562
def hash_extractor(value)
545563
value.split(',').reduce({}) do |set, tag|
546564
k, v = tag.split(':')
547-
set.merge(::URI.decode(k).downcase.to_sym => ::URI.decode(v))
565+
set.merge(decode(k).downcase.to_sym => decode(v))
548566
end
549567
end
550568
end

spec/mongo/uri_spec.rb

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,6 @@
114114
end
115115
end
116116

117-
context 'mongodb://localhost:65536/' do
118-
119-
let(:string) { 'mongodb://localhost:65536/' }
120-
121-
it 'raises an error' do
122-
expect { uri }.to raise_error(Mongo::Error::InvalidURI)
123-
end
124-
end
125-
126117
context 'mongodb://localhost:foo' do
127118

128119
let(:string) { 'mongodb://localhost:foo' }
@@ -269,7 +260,7 @@
269260
end
270261

271262
context 'unix socket server' do
272-
let(:servers) { '/tmp/mongodb-27017.sock' }
263+
let(:servers) { '%2Ftmp%2Fmongodb-27017.sock' }
273264

274265
it 'returns an array with the parsed server' do
275266
expect(uri.servers).to eq([servers])

spec/support/connection_string_tests/invalid-uris.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,11 @@ tests:
183183
hosts: ~
184184
auth: ~
185185
options: ~
186+
-
187+
description: "Host with unescaped slash"
188+
uri: "mongodb:///tmp/mongodb-27017.sock/"
189+
valid: false
190+
warning: ~
191+
hosts: ~
192+
auth: ~
193+
options: ~

0 commit comments

Comments
 (0)