File: uri.rb

package info (click to toggle)
ruby-mongo 2.21.3-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 14,764 kB
  • sloc: ruby: 108,806; makefile: 5; sh: 2
file content (590 lines) | stat: -rw-r--r-- 17,982 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
# frozen_string_literal: true
# rubocop:todo all

# Copyright (C) 2014-2020 MongoDB Inc.
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

module Mongo

  # The URI class provides a way for users to parse the MongoDB uri as
  # defined in the connection string format spec.
  #
  # https://www.mongodb.com/docs/manual/reference/connection-string/
  #
  # @example Use the uri string to make a client connection.
  #   uri = Mongo::URI.new('mongodb://localhost:27017')
  #   client = Mongo::Client.new(uri.servers, uri.options)
  #   client.login(uri.credentials)
  #   client[uri.database]
  #
  # @since 2.0.0
  class URI
    include Loggable
    include Address::Validator

    # The uri parser object options.
    #
    # @since 2.0.0
    attr_reader :options

    # Mongo::Options::Redacted of the options specified in the uri.
    #
    # @since 2.1.0
    attr_reader :uri_options

    # The servers specified in the uri.
    #
    # @since 2.0.0
    attr_reader :servers

    # The mongodb connection string scheme.
    #
    # @deprecated Will be removed in 3.0.
    #
    # @since 2.0.0
    SCHEME = 'mongodb://'.freeze

    # The mongodb connection string scheme root.
    #
    # @since 2.5.0
    MONGODB_SCHEME = 'mongodb'.freeze

    # The mongodb srv protocol connection string scheme root.
    #
    # @since 2.5.0
    MONGODB_SRV_SCHEME = 'mongodb+srv'.freeze

    # Error details for an invalid scheme.
    #
    # @since 2.1.0
    # @deprecated
    INVALID_SCHEME = "Invalid scheme. Scheme must be '#{MONGODB_SCHEME}' or '#{MONGODB_SRV_SCHEME}'".freeze

    # MongoDB URI format specification.
    #
    # @since 2.0.0
    FORMAT = 'mongodb://[username:password@]host1[:port1][,host2[:port2]' +
        ',...[,hostN[:portN]]][/[database][?options]]'.freeze

    # MongoDB URI (connection string) documentation url
    #
    # @since 2.0.0
    HELP = 'https://www.mongodb.com/docs/manual/reference/connection-string/'.freeze

    # Unsafe characters that must be urlencoded.
    #
    # @since 2.1.0
    UNSAFE = /[\:\/\@]/

    # Percent sign that must be encoded in user creds.
    #
    # @since 2.5.1
    PERCENT_CHAR = /\%/

    # Unix socket suffix.
    #
    # @since 2.1.0
    UNIX_SOCKET = /.sock/

    # The character delimiting hosts.
    #
    # @since 2.1.0
    HOST_DELIM = ','.freeze

    # The character separating a host and port.
    #
    # @since 2.1.0
    HOST_PORT_DELIM = ':'.freeze

    # The character delimiting a database.
    #
    # @since 2.1.0
    DATABASE_DELIM = '/'.freeze

    # The character delimiting options.
    #
    # @since 2.1.0
    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
    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

    # Scheme delimiter.
    #
    # @since 2.5.0
    SCHEME_DELIM = '://'.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 database 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

    # Map of URI read preference modes to Ruby driver read preference modes
    #
    # @since 2.0.0
    READ_MODE_MAP = {
      'primary'            => :primary,
      'primarypreferred'   => :primary_preferred,
      'secondary'          => :secondary,
      'secondarypreferred' => :secondary_preferred,
      'nearest'            => :nearest
    }.freeze

    # Map of URI authentication mechanisms to Ruby driver mechanisms
    #
    # @since 2.0.0
    AUTH_MECH_MAP = {
      'GSSAPI'       => :gssapi,
      'MONGODB-AWS'  => :aws,
      # MONGODB-CR is deprecated and will be removed in driver version 3.0
      'MONGODB-CR'   => :mongodb_cr,
      '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.
    #
    # @since 2.1.0
    REPEATABLE_OPTIONS = [ :tag_sets, :ssl ]

    # Get either a URI object or a SRVProtocol URI object.
    #
    # @example Get the uri object.
    #   URI.get(string)
    #
    # @param [ String ] string The URI to parse.
    # @param [ Hash ] opts The options.
    #
    # @option options [ Logger ] :logger A custom logger to use.
    #
    # @return [URI, URI::SRVProtocol] The uri object.
    #
    # @since 2.5.0
    def self.get(string, opts = {})
      unless string
        raise Error::InvalidURI.new(string, 'URI must be a string, not nil.')
      end
      if string.empty?
        raise Error::InvalidURI.new(string, 'Cannot parse an empty URI.')
      end

      scheme, _, _ = string.partition(SCHEME_DELIM)
      case scheme
        when MONGODB_SCHEME
          URI.new(string, opts)
        when MONGODB_SRV_SCHEME
          SRVProtocol.new(string, opts)
        else
          raise Error::InvalidURI.new(string, "Invalid scheme '#{scheme}'. Scheme must be '#{MONGODB_SCHEME}' or '#{MONGODB_SRV_SCHEME}'")
      end
    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.
    #   uri.client_options
    #
    # @return [ Mongo::Options::Redacted ] The options passed to the Mongo::Client
    #
    # @since 2.0.0
    def client_options
      opts = uri_options.tap do |opts|
        opts[:database] = @database if @database
      end

      @user ? opts.merge(credentials) : opts
    end

    def srv_records
      nil
    end

    # Create the new uri from the provided string.
    #
    # @example Create the new URI.
    #   URI.new('mongodb://localhost:27017')
    #
    # @param [ String ] string The URI to parse.
    # @param [ Hash ] options The options.
    #
    # @option options [ Logger ] :logger A custom logger to use.
    #
    # @raise [ Error::InvalidURI ] If the uri does not match the spec.
    #
    # @since 2.0.0
    def initialize(string, options = {})
      unless string
        raise Error::InvalidURI.new(string, 'URI must be a string, not nil.')
      end
      if string.empty?
        raise Error::InvalidURI.new(string, 'Cannot parse an empty URI.')
      end

      @string = string
      @options = options
      parsed_scheme, _, remaining = string.partition(SCHEME_DELIM)
      unless parsed_scheme == scheme
        raise_invalid_error!("Invalid scheme '#{parsed_scheme}'. Scheme must be '#{MONGODB_SCHEME}'. Use URI#get to parse SRV URIs.")
      end
      if remaining.empty?
        raise_invalid_error!('No hosts in the URI')
      end
      parse!(remaining)
      validate_uri_options!
    end

    # Get the credentials provided in the URI.
    #
    # @example Get the credentials.
    #   uri.credentials
    #
    # @return [ Hash ] The credentials.
    #   * :user [ String ] The user.
    #   * :password [ String ] The provided password.
    #
    # @since 2.0.0
    def credentials
      { :user => @user, :password => @password }
    end

    # Get the database provided in the URI.
    #
    # @example Get the database.
    #   uri.database
    #
    # @return [String] The database.
    #
    # @since 2.0.0
    def database
      @database ? @database : Database::ADMIN
    end

    # Get the uri as a string.
    #
    # @example Get the uri as a string.
    #   uri.to_s
    #
    # @return [ String ] The uri string.
    def to_s
      reconstruct_uri
    end

    private

    # Reconstruct the URI from its parts. Invalid options are dropped and options
    # are converted to camelCase.
    #
    # @return [ String ] the uri.
    def reconstruct_uri
      servers = @servers.join(',')
      options = options_mapper.ruby_to_string(@uri_options).map do |k, vs|
        unless vs.nil?
          if vs.is_a?(Array)
            vs.map { |v| "#{k}=#{v}" }.join('&')
          else
            "#{k}=#{vs}"
          end
        end
      end.compact.join('&')

      uri = "#{scheme}#{SCHEME_DELIM}"
      uri += @user.to_s if @user
      uri += "#{AUTH_USER_PWD_DELIM}#{@password}" if @password
      uri += "@" if @user || @password
      uri += @query_hostname || servers
      uri += "/" if @database || !options.empty?
      uri += @database.to_s if @database
      uri += "?#{options}" unless options.empty?
      uri
    end

    def scheme
      MONGODB_SCHEME
    end

    def parse!(remaining)
      hosts_and_db, options = remaining.split('?', 2)
      if options && options.index('?')
        raise_invalid_error!("Options contain an unescaped question mark (?), or the database name contains a question mark and was not escaped")
      end

      hosts, db = hosts_and_db.split('/', 2)
      if db && db.index('/')
        raise_invalid_error!("Database name contains an unescaped slash (/): #{db}")
      end

      if hosts.index('@')
        creds, hosts = hosts.split('@', 2)
        if hosts.empty?
          raise_invalid_error!("Empty hosts list")
        end
        if hosts.index('@')
          raise_invalid_error!("Unescaped @ in auth info")
        end
      end

      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_address_str!(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)
      end
    rescue Error::InvalidAddress => e
      raise_invalid_error!(e.message)
    end

    def options_mapper
      @options_mapper ||= OptionsMapper.new(
        logger: @options[:logger],
      )
    end

    def parse_uri_options!(string)
      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
        key = decode(key)
        value = decode(value)
        options_mapper.add_uri_option(key, value, uri_options)
      end
      uri_options
    end

    def parse_user!(string)
      if (string && user = string.partition(AUTH_USER_PWD_DELIM)[0])
        raise_invalid_error!(UNESCAPED_USER_PWD) if user =~ UNSAFE
        user_decoded = decode(user)
        if user_decoded =~ PERCENT_CHAR && encode(user_decoded) != user
          raise_invalid_error!(UNESCAPED_USER_PWD)
        end
        user_decoded
      end
    end

    def parse_password!(string)
      if (string && pwd = string.partition(AUTH_USER_PWD_DELIM)[2])
        if pwd.length > 0
          raise_invalid_error!(UNESCAPED_USER_PWD) if pwd =~ UNSAFE
          pwd_decoded = decode(pwd)
          if pwd_decoded =~ PERCENT_CHAR && encode(pwd_decoded) != pwd
            raise_invalid_error!(UNESCAPED_USER_PWD)
          end
          pwd_decoded
        end
      end
    end

    def parse_database!(string)
      raise_invalid_error!(UNESCAPED_DATABASE) if string =~ UNSAFE
      decode(string) if string.length > 0
    end

    def raise_invalid_error!(details)
      raise Error::InvalidURI.new(@string, details, FORMAT)
    end

    def raise_invalid_error_no_fmt!(details)
      raise Error::InvalidURI.new(@string, details)
    end

    def decode(value)
      ::URI::DEFAULT_PARSER.unescape(value)
    end

    def encode(value)
      CGI.escape(value).gsub('+', '%20')
    end

    def validate_uri_options!
      # The URI options spec requires that we raise an error if there are conflicting values of
      # 'tls' and 'ssl'. In order to fulfill this, we parse the values of each instance into an
      # array; assuming all values in the array are the same, we replace the array with that value.
      unless uri_options[:ssl].nil? || uri_options[:ssl].empty?
        unless uri_options[:ssl].uniq.length == 1
          raise_invalid_error_no_fmt!("all instances of 'tls' and 'ssl' must have the same value")
        end

        uri_options[:ssl] = uri_options[:ssl].first
      end

      # Check for conflicting TLS insecure options.
      unless uri_options[:ssl_verify].nil?
        unless uri_options[:ssl_verify_certificate].nil?
          raise_invalid_error_no_fmt!("'tlsInsecure' and 'tlsAllowInvalidCertificates' cannot both be specified")
        end

        unless uri_options[:ssl_verify_hostname].nil?
          raise_invalid_error_no_fmt!("tlsInsecure' and 'tlsAllowInvalidHostnames' cannot both be specified")
        end

        unless uri_options[:ssl_verify_ocsp_endpoint].nil?
          raise_invalid_error_no_fmt!("tlsInsecure' and 'tlsDisableOCSPEndpointCheck' cannot both be specified")
        end
      end

      unless uri_options[:ssl_verify_certificate].nil?
        unless uri_options[:ssl_verify_ocsp_endpoint].nil?
          raise_invalid_error_no_fmt!("tlsAllowInvalidCertificates' and 'tlsDisableOCSPEndpointCheck' cannot both be specified")
        end
      end

      # Since we know that the only URI option that sets :ssl_cert is
      # "tlsCertificateKeyFile", any value set for :ssl_cert must also be set
      # for :ssl_key.
      if uri_options[:ssl_cert]
        uri_options[:ssl_key] = uri_options[:ssl_cert]
      end

      if uri_options[:write_concern] && !uri_options[:write_concern].empty?
        begin
          WriteConcern.get(uri_options[:write_concern])
        rescue Error::InvalidWriteConcern => e
          raise_invalid_error_no_fmt!("#{e.class}: #{e}")
        end
      end

      if uri_options[:direct_connection]
        if uri_options[:connect] && uri_options[:connect].to_s != 'direct'
          raise_invalid_error_no_fmt!("directConnection=true cannot be used with connect=#{uri_options[:connect]}")
        end
        if servers.length > 1
          raise_invalid_error_no_fmt!("directConnection=true cannot be used with multiple seeds")
        end
      elsif uri_options[:direct_connection] == false && uri_options[:connect].to_s == 'direct'
        raise_invalid_error_no_fmt!("directConnection=false cannot be used with connect=direct")
      end

      if uri_options[:load_balanced]
        if servers.length > 1
          raise_invalid_error_no_fmt!("loadBalanced=true cannot be used with multiple seeds")
        end

        if uri_options[:direct_connection]
          raise_invalid_error_no_fmt!("directConnection=true cannot be used with loadBalanced=true")
        end

        if uri_options[:connect] && uri_options[:connect].to_sym == :direct
          raise_invalid_error_no_fmt!("connect=direct cannot be used with loadBalanced=true")
        end

        if uri_options[:replica_set]
          raise_invalid_error_no_fmt!("loadBalanced=true cannot be used with replicaSet option")
        end
      end

      unless self.is_a?(URI::SRVProtocol)
        if uri_options[:srv_max_hosts]
          raise_invalid_error_no_fmt!("srvMaxHosts cannot be used on non-SRV URI")
        end

        if uri_options[:srv_service_name]
          raise_invalid_error_no_fmt!("srvServiceName cannot be used on non-SRV URI")
        end
      end

      if uri_options[:srv_max_hosts] && uri_options[:srv_max_hosts] > 0
        if uri_options[:replica_set]
          raise_invalid_error_no_fmt!("srvMaxHosts > 0 cannot be used with replicaSet option")
        end

        if options[:load_balanced]
          raise_invalid_error_no_fmt!("srvMaxHosts > 0 cannot be used with loadBalanced=true")
        end
      end
    end
  end
end

require 'mongo/uri/options_mapper'
require 'mongo/uri/srv_protocol'