diff --git a/Makefile.PL b/Makefile.PL index 27f927f6..9f1e28db 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -37,6 +37,7 @@ requires 'List::Util'; requires 'MIME::Base64'; requires 'Moo' => '2'; requires 'Moo::Role'; +requires 'Net::DNS'; requires 'Safe::Isa'; requires 'Scalar::Util'; requires 'Socket'; diff --git a/devel/config/replicaset-any-27017.yml b/devel/config/replicaset-any-27017.yml index 57af9ffc..1b82cb06 100644 --- a/devel/config/replicaset-any-27017.yml +++ b/devel/config/replicaset-any-27017.yml @@ -1,13 +1,13 @@ --- type: replica -setName: foo -default_args: -v --noprealloc --nojournal --smallfiles --bind_ip 0.0.0.0 --nssize 6 --quiet +setName: repl0 +default_args: -v --bind_ip 0.0.0.0 --quiet mongod: - name: host1 port_override: 27017 - name: host2 + port_override: 27018 - name: host3 - rs_config: - arbiterOnly: true + port_override: 27019 # vim: ts=4 sts=4 sw=4 et: diff --git a/devel/lib/MongoDBTest/ReplicaSet.pm b/devel/lib/MongoDBTest/ReplicaSet.pm index 90ca1284..5cefffbd 100644 --- a/devel/lib/MongoDBTest/ReplicaSet.pm +++ b/devel/lib/MongoDBTest/ReplicaSet.pm @@ -43,7 +43,7 @@ has client => ( sub _build_client { my ($self) = @_; - return MongoDB::MongoClient->new( host => $self->as_uri, dt_type => undef ); + return $self->get_client; } has 'keyfile' => ( @@ -62,6 +62,7 @@ sub _build_keyfile { after 'start' => sub { my ($self) = @_; $self->rs_initiate; + $self->wait_for_primary; }; # override to only set up auth on the first server @@ -123,16 +124,13 @@ sub rs_initiate { # not $self->client because this needs to be a direct connection, i.e. one # seed and no replicaSet URI option - my $uri = $first->as_uri_with_auth; - my $client = MongoDB::MongoClient->new( host => $uri, dt_type => undef, ); + my $client = $first->get_direct_client; $client->get_database("admin")->run_command({ismaster => 1}); $client->get_database("admin")->run_command({replSetInitiate => $rs_config}); - $self->_logger->debug("waiting for primary"); - $self->wait_for_primary; return; @@ -142,7 +140,7 @@ sub wait_for_all_hosts { my ($self) = @_; my ($first) = $self->all_servers; retry { - my $client = MongoDB::MongoClient->new( host => $first->as_uri_with_auth, dt_type => undef ); + my $client = $first->get_direct_client; my $admin = $client->get_database("admin"); if ( my $status = eval { $admin->run_command({replSetGetStatus => 1}) } ) { my @member_states = map { $_->{state} } @{ $status->{members} }; @@ -164,12 +162,14 @@ sub wait_for_all_hosts { } sub wait_for_primary { - my ($self) = @_; + my ($self) = @_; my ($first) = $self->all_servers; + my $uri = $first->as_uri_with_auth; + $self->_logger->debug("Waiting from primary on URI: $uri"); retry { - my $client = MongoDB::MongoClient->new( host => $first->as_uri_with_auth, dt_type => undef ); - my $admin = $client->get_database("admin"); - if ( my $status = eval { $admin->run_command({replSetGetStatus => 1}) } ) { + my $client = $first->get_direct_client; + my $admin = $client->get_database("admin"); + if ( my $status = eval { $admin->run_command( { replSetGetStatus => 1 } ) } ) { my @member_states = map { $_->{state} } @{ $status->{members} }; $self->_logger->debug("host states: @member_states"); die "No PRIMARY\n" diff --git a/devel/lib/MongoDBTest/Role/Server.pm b/devel/lib/MongoDBTest/Role/Server.pm index c22db410..a9325372 100644 --- a/devel/lib/MongoDBTest/Role/Server.pm +++ b/devel/lib/MongoDBTest/Role/Server.pm @@ -21,6 +21,7 @@ package MongoDBTest::Role::Server; use MongoDB; use CPAN::Meta::Requirements; +use JSON; use Path::Tiny; use POSIX qw/SIGTERM SIGKILL/; use Proc::Guard; @@ -364,22 +365,10 @@ sub _local_restart { my ($self) = @_; my $port = $self->port; # must be localhost for shutdown command - my @args = ( - host => "mongodb://localhost:$port", - connect_type => 'direct', - dt_type => undef, - ); - if ( my $ssl = $self->ssl_config ) { - my $ssl_arg = {}; - $ssl_arg->{SSL_verifycn_scheme} = 'none'; - $ssl_arg->{SSL_ca_file} = $ssl->{certs}{ca} - if $ssl->{certs}{ca}; - $ssl_arg->{SSL_verifycn_name} = $ssl->{servercn} - if $ssl->{servercn}; - push @args, ssl => $ssl_arg; - } + my $uri = "mongodb://localhost:$port"; eval { - MongoDB::MongoClient->new( @args )->get_database("admin")->run_command( [ shutdown => 1 ] ); + my $client = $self->get_direct_client($uri, connect_type => 'direct'); + $client->get_database("admin")->run_command( [ shutdown => 1 ] ); }; my $err = $@; # 'shutdown' closes the socket so we don't get a network error instead @@ -414,6 +403,29 @@ sub stop { $self->_logger->debug("cleared guard and client for " . $self->name); } +sub get_direct_client { + my ($self, $uri, @args) = @_; + $uri //= $self->as_uri_with_auth; + my $config = { host => $uri, dt_type => undef, @args }; + if ( my $ssl = $self->ssl_config ) { + my $ssl_arg = {}; + $ssl_arg->{SSL_verifycn_scheme} = 'none'; + $ssl_arg->{SSL_ca_file} = $ssl->{certs}{ca} + if $ssl->{certs}{ca}; + $ssl_arg->{SSL_verifycn_name} = $ssl->{servercn} + if $ssl->{servercn}; + $ssl_arg->{SSL_hostname} = $ssl->{servercn} + if $ssl->{servercn}; + if ( $self->did_ssl_auth_setup ) { + $config->{username} = $ssl->{username}; + $config->{auth_mechanism} = 'MONGODB-X509'; + $ssl_arg->{SSL_cert_file} = $ssl->{certs}{client}; + } + $config->{ssl} = $ssl_arg; + } + return MongoDB::MongoClient->new($config); +} + sub is_alive { my ($self) = @_; return unless $self->has_guard; diff --git a/devel/lib/MongoDBTest/Role/ServerSet.pm b/devel/lib/MongoDBTest/Role/ServerSet.pm index 01a7fc4d..a805c090 100644 --- a/devel/lib/MongoDBTest/Role/ServerSet.pm +++ b/devel/lib/MongoDBTest/Role/ServerSet.pm @@ -21,6 +21,7 @@ package MongoDBTest::Role::ServerSet; use MongoDBTest::Mongod; use MongoDBTest::Mongos; +use JSON; use Moo::Role; use Types::Standard -types; use namespace::clean; @@ -113,6 +114,30 @@ sub get_server { return; } +sub get_client { + my ($self) = @_; + my $config = { host => $self->as_uri, dt_type => undef }; + if ( my $ssl = $self->ssl_config ) { + my $ssl_arg = {}; + $ssl_arg->{SSL_verifycn_scheme} = 'none'; + $ssl_arg->{SSL_ca_file} = $ssl->{certs}{ca} + if $ssl->{certs}{ca}; + $ssl_arg->{SSL_verifycn_name} = $ssl->{servercn} + if $ssl->{servercn}; + $ssl_arg->{SSL_hostname} = $ssl->{servercn} + if $ssl->{servercn}; + if ($ssl->{username}) { + $config->{username} = $ssl->{username}; + $config->{auth_mechanism} = 'MONGODB-X509'; + $ssl_arg->{SSL_cert_file} = $ssl->{certs}{client}; + } + $config->{ssl} = $ssl_arg; + } + $self->_logger->debug("connecting to server with: " . to_json($config)); + + return MongoDB::MongoClient->new($config); +} + sub start { my ($self) = @_; # XXX eventually factor out wait_port from server->start, start all servers diff --git a/devel/lib/MongoDBTest/ShardedCluster.pm b/devel/lib/MongoDBTest/ShardedCluster.pm index b524ee21..ee4023f7 100644 --- a/devel/lib/MongoDBTest/ShardedCluster.pm +++ b/devel/lib/MongoDBTest/ShardedCluster.pm @@ -23,6 +23,7 @@ use MongoDBTest::Deployment; use MongoDBTest::ServerSet; use MongoDBTest::Mongod; +use JSON; use Moo; use Try::Tiny::Retry qw/:all/; use Types::Standard -types; @@ -119,7 +120,12 @@ sub start { my $uri = $self->routers->as_uri; $self->_logger->debug("connecting to mongos at $uri"); my $client = - retry { MongoDB::MongoClient->new( host => $uri ) } + retry { + my $client = $self->get_client; + $client->db("admin")->run_command({ismaster => 1}); + $client + } + on_retry { $self->_logger->debug($_) } delay_exp { 15, 1e4 } catch { chomp; die "$_. Giving up!\n" }; @@ -173,6 +179,30 @@ sub get_server { return; } +sub get_client { + my ($self) = @_; + my $config = { host => $self->as_uri, dt_type => undef }; + if ( my $ssl = $self->ssl_config ) { + my $ssl_arg = {}; + $ssl_arg->{SSL_verifycn_scheme} = 'none'; + $ssl_arg->{SSL_ca_file} = $ssl->{certs}{ca} + if $ssl->{certs}{ca}; + $ssl_arg->{SSL_verifycn_name} = $ssl->{servercn} + if $ssl->{servercn}; + $ssl_arg->{SSL_hostname} = $ssl->{servercn} + if $ssl->{servercn}; + if ($ssl->{username}) { + $config->{username} = $ssl->{username}; + $config->{auth_mechanism} = 'MONGODB-X509'; + $ssl_arg->{SSL_cert_file} = $ssl->{certs}{client}; + } + $config->{ssl} = $ssl_arg; + } + $self->_logger->debug("connecting to server with: " . to_json($config)); + + return MongoDB::MongoClient->new($config); +} + with 'MooseX::Role::Logger', 'MongoDBTest::Role::Deployment'; 1; diff --git a/devel/t-dynamic/PERL-807-DNS-SRV-TXT.t b/devel/t-dynamic/PERL-807-DNS-SRV-TXT.t new file mode 100644 index 00000000..6b016eab --- /dev/null +++ b/devel/t-dynamic/PERL-807-DNS-SRV-TXT.t @@ -0,0 +1,131 @@ +# +# Copyright 2009-2013 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. +# + +use strict; +use warnings; +use JSON::MaybeXS; +use YAML::XS qw/LoadFile DumpFile/; +use Path::Tiny 0.054; # basename with suffix +use Test::More 0.88; +use Test::Fatal; +use Try::Tiny; +use Sys::Hostname; + +use lib "t/lib"; +use lib "devel/lib"; + +use if $ENV{MONGOVERBOSE}, qw/Log::Any::Adapter Stderr/; + +use MongoDBTest::Orchestrator; + +use MongoDBTest qw/build_client get_test_db clear_testdbs server_version + server_type skip_if_mongod skip_unless_mongod/; + +# This test starts servers on localhost ports 27017, 27018 and 27019. We skip if +# these aren't available. +for my $port ( 27017, 27018, 27019 ) { + local $ENV{MONGOD} = "mongodb://localhost:$port/"; + skip_if_mongod(); +} + +my $cert_dir = $ENV{MONGO_TEST_CERT_PATH} || 'devel/certs'; +my $cert_user = $ENV{MONGO_TEST_CERT_USER} + || "CN=client,OU=Drivers,O=MongoDB,L=New York,ST=New York,C=US"; +my $server_cn = "CN=localhost,OU=Server,O=MongoDB,L=New York,ST=New York,C=US"; + +my %certs = ( + client => "$cert_dir/client.pem", + ca => "$cert_dir/ca.pem", + server => "$cert_dir/server.pem", +); + +my $config = LoadFile("devel/config/replicaset-any-27017.yml"); +$config->{ssl_config} = { + # not require, as need to connect without SSL for one of the tests + mode => 'preferSSL', + servercn => $server_cn, + certs => { map { $_ => $certs{$_} } qw/server ca client/ }, +}; +my $config_path = Path::Tiny->tempfile; +DumpFile( "$config_path", $config ); + +my $orc = + MongoDBTest::Orchestrator->new( + config_file => "$config_path" ); +$orc->start; + +sub new_client { + my $host = shift; + return build_client( + host => $host, + dt_type => undef, + ssl => { + SSL_ca_file => $certs{ca}, + SSL_verifycn_scheme => 'none', + }, + ); +} + +sub run_test { + my $test = shift; + + if ( $test->{error} ) { + # This test should error the parsing step at some point + isnt( exception { new_client( $test->{uri} ) }, undef, + "invalid uri" ); + return; + } + + my $mongo; + try { + $mongo = new_client( $test->{uri} ); + }; + isa_ok( $mongo, 'MongoDB::MongoClient' ); + # drop out of test to save on undef errors - its already failed + return unless defined $mongo; + my $uri = $mongo->_uri; + + my $lc_opts = { map { lc $_ => $test->{options}->{$_} } keys %{ $test->{options} } }; + # force ssl JSON boolean to perlish + $lc_opts->{ssl} = $lc_opts->{ssl} ? 1 : 0; + is_deeply( $uri->options, $lc_opts, "options are correct" ); + is_deeply( [ sort @{ $uri->hostids } ], [ sort @{ $test->{seeds} } ], "seeds are correct" ); + my $topology = $mongo->topology_status( refresh => 1 ); + my @found_servers = map { $_->{address} } @{ $topology->{servers} }; + my $hostname = hostname(); + my @wanted_servers = map { ( my $h = $_ ) =~ s/localhost/$hostname/; $h } @{ $test->{hosts} }; + is_deeply( [ sort @found_servers ], [ sort @wanted_servers ], "hosts are correct" ); +} + +my $dir = path("t/data/initial_dns_seedlist_discovery"); +my $iterator = $dir->iterator; +while ( my $path = $iterator->() ) { + next unless $path =~ /\.json$/; + my $plan = eval { decode_json( $path->slurp_utf8 ) }; + if ($@) { + die "Error decoding $path: $@"; + } + subtest $path => sub { + my $description = $plan->{comment}; + subtest $description => sub { + run_test( $plan ); + } + }; +} + +clear_testdbs; + +done_testing; diff --git a/lib/MongoDB/MongoClient.pm b/lib/MongoDB/MongoClient.pm index eacdd360..46680813 100644 --- a/lib/MongoDB/MongoClient.pm +++ b/lib/MongoDB/MongoClient.pm @@ -784,11 +784,16 @@ has ssl => ( sub _build_ssl { my ($self) = @_; - return $self->__uri_or_else( + my $ssl = $self->__uri_or_else( u => 'ssl', e => 'ssl', d => 0, ); + # allow optional arguments to override as long as SSL is already enabled + if ( $ssl && exists $self->_deferred->{ssl} ) { + return $self->_deferred->{ssl}; + } + return $ssl; } =attr username @@ -1198,7 +1203,7 @@ has _uri => ( sub _build__uri { my ($self) = @_; - if ( $self->host =~ m{^\w+://} ) { + if ( $self->host =~ m{^[\w\+]+://} ) { return MongoDB::_URI->new( uri => $self->host ); } else { diff --git a/lib/MongoDB/_URI.pm b/lib/MongoDB/_URI.pm index 94e78189..bb251cde 100644 --- a/lib/MongoDB/_URI.pm +++ b/lib/MongoDB/_URI.pm @@ -33,7 +33,7 @@ use namespace::clean -except => 'meta'; my $uri_re = qr{ - mongodb:// + mongodb(?:\+srv|):// (?: ([^:]*) (?: : ([^@]*) )? @ )? # [username(:password)?@] ([^/?]*) # host1[:port1][,host2[:port2],...[,hostN[:portN]]] (?: @@ -120,6 +120,21 @@ sub _build_valid_options { }; } +has valid_srv_options => ( + is => 'ro', + isa => HashRef, + builder => '_build_valid_srv_options', +); + +sub _build_valid_srv_options { + return { + map { lc($_) => 1 } qw( + authSource + replicaSet + ) + }; +} + sub _unescape_all { my $str = shift; return '' unless defined $str; @@ -141,12 +156,177 @@ sub _parse_doc { return $set; } +sub _parse_options { + my ( $self, $valid, $result, $txt_record ) = @_; + + my %parsed; + for my $opt ( split '&', $result->{options} ) { + my @kv = split '=', $opt, -1; + MongoDB::UsageError->throw("expected key value pair") unless @kv == 2; + my ( $k, $v ) = map { _unescape_all($_) } @kv; + # connection string spec calls for case normalization + ( my $lc_k = $k ) =~ tr[A-Z][a-z]; + if ( !$valid->{$lc_k} ) { + if ( $txt_record ) { + MongoDB::Error->throw("Unsupported option '$k' in URI $self for TXT record $txt_record\n"); + } else { + warn "Unsupported option '$k' in URI $self\n"; + } + next; + } + if ( exists $parsed{$lc_k} && !exists $options_with_list_type{$lc_k} ) { + warn "Multiple options were found for the same value '$lc_k'. The first occurrence will be used\n"; + next; + } + if ( $lc_k eq 'authmechanismproperties' ) { + $parsed{$lc_k} = _parse_doc( $k, $v ); + } + elsif ( $lc_k eq 'authsource' ) { + $result->{db_name} = $v; + $parsed{$lc_k} = $v; + } + elsif ( $lc_k eq 'readpreferencetags' ) { + $parsed{$lc_k} ||= []; + push @{ $parsed{$lc_k} }, _parse_doc( $k, $v ); + } + elsif ( $lc_k eq 'ssl' || $lc_k eq 'journal' || $lc_k eq 'serverselectiontryonce' ) { + $parsed{$lc_k} = __str_to_bool( $k, $v ); + } + else { + $parsed{$lc_k} = $v; + } + } + return \%parsed; +} + +sub _fetch_dns_seedlist { + my ( $self, $host_name ) = @_; + + my @split_name = split( '\.', $host_name ); + MongoDB::Error->throw("URI '$self' must contain domain name and hostname") + unless scalar( @split_name ) > 2; + + require Net::DNS; + + my $res = Net::DNS::Resolver->new; + my $srv_data = $res->query( sprintf( '_mongodb._tcp.%s', $host_name ), 'SRV' ); + + my @hosts; + my $options = {}; + my $domain_name = join( '.', @split_name[1..$#split_name] ); + if ( $srv_data ) { + foreach my $rr ( $srv_data->answer ) { + next unless $rr->type eq 'SRV'; + my $target = $rr->target; + # search for dot before domain name for a valid hostname - can have sub-subdomain + unless ( $target =~ /\.\Q$domain_name\E$/ ) { + MongoDB::Error->throw( + "URI '$self' SRV record returns FQDN '$target'" + . " which does not match domain name '${$domain_name}'" + ); + } + push @hosts, { + target => $target, + port => $rr->port, + }; + } + my $txt_data = $res->query( $host_name, 'TXT' ); + if ( defined $txt_data ) { + my @txt_answers; + foreach my $rr ( $txt_data->answer ) { + next unless $rr->type eq 'TXT'; + push @txt_answers, $rr; + } + if ( scalar( @txt_answers ) > 1 ) { + MongoDB::Error->throw("URI '$self' returned more than one TXT result"); + } elsif ( scalar( @txt_answers ) == 1 ) { + my $txt_opt_string = join ( '', $txt_answers[0]->txtdata ); + $options = $self->_parse_options( $self->valid_srv_options, { options => $txt_opt_string }, $txt_opt_string ); + } + } + } else { + MongoDB::Error->throw("URI '$self' does not return any SRV results"); + } + + return ( \@hosts, $options ); +} + +sub _parse_srv_uri { + my ( $self, $uri ) = @_; + + my %result; + + $uri =~ m{^$uri_re$}; + + ( + $result{username}, $result{password}, $result{hostids}, + $result{db_name}, $result{options} + ) = ( $1, $2, $3, $4, $5 ); + + if ( defined $result{username} ) { + MongoDB::Error->throw("URI '$self' cannot have a username if using an SRV connection string"); + } + + if ( defined $result{password} ) { + MongoDB::Error->throw("URI '$self' cannot have a password if using an SRV connection string"); + } + + if ( defined $result{db_name} && length $result{db_name} ) { + MongoDB::Error->throw("URI '$self' cannot have a database name if using an SRV connection string"); + } + + $result{hostids} = lc _unescape_all( $result{hostids} ); + + if ( !defined $result{hostids} || !length $result{hostids} ) { + MongoDB::Error->throw("URI '$self' cannot be empty if using an SRV connection string"); + } + + if ( $result{hostids} =~ /,/ ) { + MongoDB::Error->throw("URI '$self' cannot contain a comma or multiple host names if using an SRV connection string"); + } + + if ( $result{hostids} =~ /:\d+$/ ) { + MongoDB::Error->throw("URI '$self' cannot contain port number if using an SRV connection string"); + } + + if ( defined $result{options} ) { + $result{options} = $self->_parse_options( $self->valid_options, \%result ); + } + + my ( $hosts, $options ) = $self->_fetch_dns_seedlist( $result{hostids} ); + + # Default to SSL on unless specified in conn string options + $options = { + ssl => 'true', + %$options, + %{ $result{options} || {} }, + }; + + # URI requires string based booleans for re-constructing the URI + if ( ! $options->{ssl} && $options->{ssl} == 0 ) { + $options->{ssl} = 'false'; + } + + my $new_uri = sprintf( + 'mongodb://%s/%s%s', + join( ',', map { sprintf( '%s:%s', $_->{target}, $_->{port} ) } @$hosts ), + scalar( keys %$options ) ? '?' : '', + join( '&', map { sprintf( '%s=%s', $_, __uri_escape( $options->{$_} ) ) } keys %$options ), + ); + + return $new_uri; +} + sub BUILD { my ($self) = @_; my $uri = $self->uri; my %result; + if ( $uri =~ m{^mongodb\+srv://} ) { + $uri = $self->_parse_srv_uri( $uri ); + } + # we throw Error instead of UsageError for errors, to avoid stacktrace revealing credentials if ( $uri !~ m{^$uri_re$} ) { MongoDB::Error->throw("URI '$self' could not be parsed"); @@ -203,40 +383,7 @@ sub BUILD { } if ( defined $result{options} ) { - my $valid = $self->valid_options; - my %parsed; - for my $opt ( split '&', $result{options} ) { - my @kv = split '=', $opt, -1; - MongoDB::UsageError->throw("expected key value pair") unless @kv == 2; - my ( $k, $v ) = map { _unescape_all($_) } @kv; - # connection string spec calls for case normalization - ( my $lc_k = $k ) =~ tr[A-Z][a-z]; - if ( !$valid->{$lc_k} ) { - warn "Unsupported option '$k' in URI $self\n"; - next; - } - if ( exists $parsed{$lc_k} && !exists $options_with_list_type{$lc_k} ) { - warn "Multiple options were found for the same value '$lc_k'\n"; - next; - } - if ( $lc_k eq 'authmechanismproperties' ) { - $parsed{$lc_k} = _parse_doc( $k, $v ); - } - elsif ( $lc_k eq 'authsource' ) { - $result{db_name} = $v; - } - elsif ( $lc_k eq 'readpreferencetags' ) { - $parsed{$lc_k} ||= []; - push @{ $parsed{$lc_k} }, _parse_doc( $k, $v ); - } - elsif ( $lc_k eq 'ssl' || $lc_k eq 'journal' || $lc_k eq 'serverselectiontryonce' ) { - $parsed{$lc_k} = __str_to_bool( $k, $v ); - } - else { - $parsed{$lc_k} = $v; - } - } - $result{options} = \%parsed; + $result{options} = $self->_parse_options( $self->valid_options, \%result ); } for my $attr (qw/username password db_name options hostids/) { @@ -256,6 +403,25 @@ sub __str_to_bool { MongoDB::UsageError->throw("expected boolean string 'true' or 'false' for key '$k' but instead received '$str'"); } +# uri_escape borrowed from HTTP::Tiny 0.070 +my %escapes = map { chr($_) => sprintf("%%%02X", $_) } 0..255; +$escapes{' '}="+"; +my $unsafe_char = qr/[^A-Za-z0-9\-\._~]/; + +sub __uri_escape { + my ($str) = @_; + if ( $] ge '5.008' ) { + utf8::encode($str); + } + else { + $str = pack("U*", unpack("C*", $str)) # UTF-8 encode a byte string + if ( length $str == do { use bytes; length $str } ); + $str = pack("C*", unpack("C*", $str)); # clear UTF-8 flag + } + $str =~ s/($unsafe_char)/$escapes{$1}/ge; + return $str; +} + # redact user credentials when stringifying use overload '""' => sub { diff --git a/t/data/initial_dns_seedlist_discovery/README.rst b/t/data/initial_dns_seedlist_discovery/README.rst new file mode 100644 index 00000000..5b093e19 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/README.rst @@ -0,0 +1,87 @@ +==================================== +Initial DNS Seedlist Discovery tests +==================================== + +This directory contains platform-independent tests that drivers can use +to prove their conformance to the Initial DNS Seedlist Discovery spec. + +Test Setup +---------- + +Start a three-node replica set on localhost, on ports 27017, 27018, and 27019, +with replica set name "repl0". The replica set MUST be started with SSL +enabled. + +To run the tests that accompany this spec, you need to configure the SRV and +TXT records with a real name server. The following records are required for +these tests:: + + Record TTL Class Address + localhost.test.build.10gen.cc. 86400 IN A 127.0.0.1 + localhost.sub.test.build.10gen.cc. 86400 IN A 127.0.0.1 + + Record TTL Class Port Target + _mongodb._tcp.test1.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. + _mongodb._tcp.test1.test.build.10gen.cc. 86400 IN SRV 27018 localhost.test.build.10gen.cc. + _mongodb._tcp.test2.test.build.10gen.cc. 86400 IN SRV 27018 localhost.test.build.10gen.cc. + _mongodb._tcp.test2.test.build.10gen.cc. 86400 IN SRV 27019 localhost.test.build.10gen.cc. + _mongodb._tcp.test3.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. + _mongodb._tcp.test5.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. + _mongodb._tcp.test6.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. + _mongodb._tcp.test7.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. + _mongodb._tcp.test8.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. + _mongodb._tcp.test10.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. + _mongodb._tcp.test11.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. + _mongodb._tcp.test12.test.build.10gen.cc. 86400 IN SRV 27017 localhost.build.10gen.cc. + _mongodb._tcp.test13.test.build.10gen.cc. 86400 IN SRV 27017 test.build.10gen.cc. + _mongodb._tcp.test14.test.build.10gen.cc. 86400 IN SRV 27017 localhost.not-test.build.10gen.cc. + _mongodb._tcp.test15.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.not-build.10gen.cc. + _mongodb._tcp.test16.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.not-10gen.cc. + _mongodb._tcp.test17.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.not-cc. + _mongodb._tcp.test18.test.build.10gen.cc. 86400 IN SRV 27017 localhost.sub.test.build.10gen.cc. + _mongodb._tcp.test19.test.build.10gen.cc. 86400 IN SRV 27017 localhost.evil.build.10gen.cc. + _mongodb._tcp.test19.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. + + Record TTL Class Text + test5.test.build.10gen.cc. 86400 IN TXT "replicaSet=repl0&authSource=thisDB" + test6.test.build.10gen.cc. 86400 IN TXT "replicaSet=repl0" + test6.test.build.10gen.cc. 86400 IN TXT "authSource=otherDB" + test7.test.build.10gen.cc. 86400 IN TXT "ssl=false" + test8.test.build.10gen.cc. 86400 IN TXT "authSource" + test10.test.build.10gen.cc. 86400 IN TXT "socketTimeoutMS=500" + test11.test.build.10gen.cc. 86400 IN TXT "replicaS" "et=rep" "l0" + +Note that ``test4`` is omitted deliberately to test what happens with no SRV +record. ``test9`` is missing because it was deleted during the development of +the tests. The missing ``test.`` sub-domain in the SRV record target for +``test12`` is deliberate. + +In our tests we have used ``localhost.test.build.10gen.cc`` as the domain, and +then configured ``localhost.test.build.10gen.cc`` to resolve to 127.0.0.1. + +You need to adapt the records shown above to replace ``test.build.10gen.cc`` +with your own domain name, and update the "uri" field in the YAML or JSON files +in this directory with the actual domain. + +Test Format and Use +------------------- + +These YAML and JSON files contain the following fields: + +- ``uri``: a mongodb+srv connection string +- ``seeds``: the expected set of initial seeds discovered from the SRV record +- ``hosts``: the discovered topology's list of hosts once SDAM completes a scan +- ``options``: the parsed connection string options as discovered from URI and + TXT records +- ``error``: indicates that the parsing of the URI, or the resolving or + contents of the SRV or TXT records included errors. +- ``comment``: a comment to indicate why a test would fail. + +For each file, create MongoClient initialized with the mongodb+srv connection +string. You SHOULD verify that the client's initial seed list matches the list of +seeds. You MUST verify that the set of ServerDescriptions in the client's +TopologyDescription eventually matches the list of hosts. You MUST verify that +each of the values of the Connection String Options under ``options`` match the +Client's parsed value for that option. There may be other options parsed by +the Client as well, which a test does not verify. You MUST verify that an +error has been thrown if ``error`` is present. diff --git a/t/data/initial_dns_seedlist_discovery/longer-parent-in-return.json b/t/data/initial_dns_seedlist_discovery/longer-parent-in-return.json new file mode 100644 index 00000000..9a8267ea --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/longer-parent-in-return.json @@ -0,0 +1,16 @@ +{ + "uri": "mongodb+srv://test18.test.build.10gen.cc/?replicaSet=repl0", + "seeds": [ + "localhost.sub.test.build.10gen.cc:27017" + ], + "hosts": [ + "localhost:27017", + "localhost:27018", + "localhost:27019" + ], + "options": { + "replicaSet": "repl0", + "ssl": true + }, + "comment": "Is correct, as returned host name shared the URI root \"test.build.10gen.cc\"." +} diff --git a/t/data/initial_dns_seedlist_discovery/longer-parent-in-return.yml b/t/data/initial_dns_seedlist_discovery/longer-parent-in-return.yml new file mode 100644 index 00000000..e77c4570 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/longer-parent-in-return.yml @@ -0,0 +1,11 @@ +uri: "mongodb+srv://test18.test.build.10gen.cc/?replicaSet=repl0" +seeds: + - localhost.sub.test.build.10gen.cc:27017 +hosts: + - localhost:27017 + - localhost:27018 + - localhost:27019 +options: + replicaSet: repl0 + ssl: true +comment: Is correct, as returned host name shared the URI root "test.build.10gen.cc". diff --git a/t/data/initial_dns_seedlist_discovery/misformatted-option.json b/t/data/initial_dns_seedlist_discovery/misformatted-option.json new file mode 100644 index 00000000..3c8c29ac --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/misformatted-option.json @@ -0,0 +1,7 @@ +{ + "uri": "mongodb+srv://test8.test.build.10gen.cc/", + "seeds": [], + "hosts": [], + "error": true, + "comment": "Should fail because the options in the TXT record are incorrectly formatted (misses value)." +} diff --git a/t/data/initial_dns_seedlist_discovery/misformatted-option.yml b/t/data/initial_dns_seedlist_discovery/misformatted-option.yml new file mode 100644 index 00000000..9669772c --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/misformatted-option.yml @@ -0,0 +1,5 @@ +uri: "mongodb+srv://test8.test.build.10gen.cc/" +seeds: [] +hosts: [] +error: true +comment: Should fail because the options in the TXT record are incorrectly formatted (misses value). diff --git a/t/data/initial_dns_seedlist_discovery/no-results.json b/t/data/initial_dns_seedlist_discovery/no-results.json new file mode 100644 index 00000000..c1dc02d2 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/no-results.json @@ -0,0 +1,7 @@ +{ + "uri": "mongodb+srv://test4.test.build.10gen.cc/", + "seeds": [], + "hosts": [], + "error": true, + "comment": "Should fail because no SRV records are present for this URI." +} diff --git a/t/data/initial_dns_seedlist_discovery/no-results.yml b/t/data/initial_dns_seedlist_discovery/no-results.yml new file mode 100644 index 00000000..e09bd060 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/no-results.yml @@ -0,0 +1,5 @@ +uri: "mongodb+srv://test4.test.build.10gen.cc/" +seeds: [] +hosts: [] +error: true +comment: Should fail because no SRV records are present for this URI. diff --git a/t/data/initial_dns_seedlist_discovery/not-enough-parts.json b/t/data/initial_dns_seedlist_discovery/not-enough-parts.json new file mode 100644 index 00000000..7cfce2ec --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/not-enough-parts.json @@ -0,0 +1,7 @@ +{ + "uri": "mongodb+srv://10gen.cc/", + "seeds": [], + "hosts": [], + "error": true, + "comment": "Should fail because host in URI does not have {hostname}, {domainname} and {tld}." +} diff --git a/t/data/initial_dns_seedlist_discovery/not-enough-parts.yml b/t/data/initial_dns_seedlist_discovery/not-enough-parts.yml new file mode 100644 index 00000000..b36fa4a5 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/not-enough-parts.yml @@ -0,0 +1,5 @@ +uri: "mongodb+srv://10gen.cc/" +seeds: [] +hosts: [] +error: true +comment: Should fail because host in URI does not have {hostname}, {domainname} and {tld}. diff --git a/t/data/initial_dns_seedlist_discovery/one-result-default-port.json b/t/data/initial_dns_seedlist_discovery/one-result-default-port.json new file mode 100644 index 00000000..cebb3b1e --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/one-result-default-port.json @@ -0,0 +1,15 @@ +{ + "uri": "mongodb+srv://test3.test.build.10gen.cc/?replicaSet=repl0", + "seeds": [ + "localhost.test.build.10gen.cc:27017" + ], + "hosts": [ + "localhost:27017", + "localhost:27018", + "localhost:27019" + ], + "options": { + "replicaSet": "repl0", + "ssl": true + } +} diff --git a/t/data/initial_dns_seedlist_discovery/one-result-default-port.yml b/t/data/initial_dns_seedlist_discovery/one-result-default-port.yml new file mode 100644 index 00000000..395bcdc9 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/one-result-default-port.yml @@ -0,0 +1,10 @@ +uri: "mongodb+srv://test3.test.build.10gen.cc/?replicaSet=repl0" +seeds: + - localhost.test.build.10gen.cc:27017 +hosts: + - localhost:27017 + - localhost:27018 + - localhost:27019 +options: + replicaSet: repl0 + ssl: true diff --git a/t/data/initial_dns_seedlist_discovery/one-txt-record-multiple-strings.json b/t/data/initial_dns_seedlist_discovery/one-txt-record-multiple-strings.json new file mode 100644 index 00000000..622668c3 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/one-txt-record-multiple-strings.json @@ -0,0 +1,15 @@ +{ + "uri": "mongodb+srv://test11.test.build.10gen.cc/", + "seeds": [ + "localhost.test.build.10gen.cc:27017" + ], + "hosts": [ + "localhost:27017", + "localhost:27018", + "localhost:27019" + ], + "options": { + "replicaSet": "repl0", + "ssl": true + } +} diff --git a/t/data/initial_dns_seedlist_discovery/one-txt-record-multiple-strings.yml b/t/data/initial_dns_seedlist_discovery/one-txt-record-multiple-strings.yml new file mode 100644 index 00000000..90a702cd --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/one-txt-record-multiple-strings.yml @@ -0,0 +1,10 @@ +uri: "mongodb+srv://test11.test.build.10gen.cc/" +seeds: + - localhost.test.build.10gen.cc:27017 +hosts: + - localhost:27017 + - localhost:27018 + - localhost:27019 +options: + replicaSet: repl0 + ssl: true diff --git a/t/data/initial_dns_seedlist_discovery/one-txt-record.json b/t/data/initial_dns_seedlist_discovery/one-txt-record.json new file mode 100644 index 00000000..2385021a --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/one-txt-record.json @@ -0,0 +1,16 @@ +{ + "uri": "mongodb+srv://test5.test.build.10gen.cc/", + "seeds": [ + "localhost.test.build.10gen.cc:27017" + ], + "hosts": [ + "localhost:27017", + "localhost:27018", + "localhost:27019" + ], + "options": { + "replicaSet": "repl0", + "authSource": "thisDB", + "ssl": true + } +} diff --git a/t/data/initial_dns_seedlist_discovery/one-txt-record.yml b/t/data/initial_dns_seedlist_discovery/one-txt-record.yml new file mode 100644 index 00000000..9356eaa2 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/one-txt-record.yml @@ -0,0 +1,11 @@ +uri: "mongodb+srv://test5.test.build.10gen.cc/" +seeds: + - localhost.test.build.10gen.cc:27017 +hosts: + - localhost:27017 + - localhost:27018 + - localhost:27019 +options: + replicaSet: repl0 + authSource: thisDB + ssl: true diff --git a/t/data/initial_dns_seedlist_discovery/parent-part-mismatch1.json b/t/data/initial_dns_seedlist_discovery/parent-part-mismatch1.json new file mode 100644 index 00000000..8d0147a4 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/parent-part-mismatch1.json @@ -0,0 +1,7 @@ +{ + "uri": "mongodb+srv://test14.test.build.10gen.cc/", + "seeds": [], + "hosts": [], + "error": true, + "comment": "Should fail because returned host name's part \"not-test\" mismatches URI parent part \"test\"." +} diff --git a/t/data/initial_dns_seedlist_discovery/parent-part-mismatch1.yml b/t/data/initial_dns_seedlist_discovery/parent-part-mismatch1.yml new file mode 100644 index 00000000..e35dfdf6 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/parent-part-mismatch1.yml @@ -0,0 +1,5 @@ +uri: "mongodb+srv://test14.test.build.10gen.cc/" +seeds: [] +hosts: [] +error: true +comment: Should fail because returned host name's part "not-test" mismatches URI parent part "test". diff --git a/t/data/initial_dns_seedlist_discovery/parent-part-mismatch2.json b/t/data/initial_dns_seedlist_discovery/parent-part-mismatch2.json new file mode 100644 index 00000000..996249eb --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/parent-part-mismatch2.json @@ -0,0 +1,7 @@ +{ + "uri": "mongodb+srv://test15.test.build.10gen.cc/", + "seeds": [], + "hosts": [], + "error": true, + "comment": "Should fail because returned host name's part \"not-build\" mismatches URI parent part \"build\"." +} diff --git a/t/data/initial_dns_seedlist_discovery/parent-part-mismatch2.yml b/t/data/initial_dns_seedlist_discovery/parent-part-mismatch2.yml new file mode 100644 index 00000000..595e5493 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/parent-part-mismatch2.yml @@ -0,0 +1,5 @@ +uri: "mongodb+srv://test15.test.build.10gen.cc/" +seeds: [] +hosts: [] +error: true +comment: Should fail because returned host name's part "not-build" mismatches URI parent part "build". diff --git a/t/data/initial_dns_seedlist_discovery/parent-part-mismatch3.json b/t/data/initial_dns_seedlist_discovery/parent-part-mismatch3.json new file mode 100644 index 00000000..69e724af --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/parent-part-mismatch3.json @@ -0,0 +1,7 @@ +{ + "uri": "mongodb+srv://test16.test.build.10gen.cc/", + "seeds": [], + "hosts": [], + "error": true, + "comment": "Should fail because returned host name's part \"not-10gen\" mismatches URI parent part \"10gen\"." +} diff --git a/t/data/initial_dns_seedlist_discovery/parent-part-mismatch3.yml b/t/data/initial_dns_seedlist_discovery/parent-part-mismatch3.yml new file mode 100644 index 00000000..64ca2e70 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/parent-part-mismatch3.yml @@ -0,0 +1,5 @@ +uri: "mongodb+srv://test16.test.build.10gen.cc/" +seeds: [] +hosts: [] +error: true +comment: Should fail because returned host name's part "not-10gen" mismatches URI parent part "10gen". diff --git a/t/data/initial_dns_seedlist_discovery/parent-part-mismatch4.json b/t/data/initial_dns_seedlist_discovery/parent-part-mismatch4.json new file mode 100644 index 00000000..254168e3 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/parent-part-mismatch4.json @@ -0,0 +1,7 @@ +{ + "uri": "mongodb+srv://test17.test.build.10gen.cc/", + "seeds": [], + "hosts": [], + "error": true, + "comment": "Should fail because returned host name's TLD \"not-cc\" mismatches URI TLD \"cc\"." +} diff --git a/t/data/initial_dns_seedlist_discovery/parent-part-mismatch4.yml b/t/data/initial_dns_seedlist_discovery/parent-part-mismatch4.yml new file mode 100644 index 00000000..226d6fa3 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/parent-part-mismatch4.yml @@ -0,0 +1,5 @@ +uri: "mongodb+srv://test17.test.build.10gen.cc/" +seeds: [] +hosts: [] +error: true +comment: Should fail because returned host name's TLD "not-cc" mismatches URI TLD "cc". diff --git a/t/data/initial_dns_seedlist_discovery/parent-part-mismatch5.json b/t/data/initial_dns_seedlist_discovery/parent-part-mismatch5.json new file mode 100644 index 00000000..92c024b4 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/parent-part-mismatch5.json @@ -0,0 +1,7 @@ +{ + "uri": "mongodb+srv://test19.test.build.10gen.cc/", + "seeds": [], + "hosts": [], + "error": true, + "comment": "Should fail because one of the returned host names' domain name parts \"evil\" mismatches \"test\"." +} diff --git a/t/data/initial_dns_seedlist_discovery/parent-part-mismatch5.yml b/t/data/initial_dns_seedlist_discovery/parent-part-mismatch5.yml new file mode 100644 index 00000000..1ed2bda4 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/parent-part-mismatch5.yml @@ -0,0 +1,5 @@ +uri: "mongodb+srv://test19.test.build.10gen.cc/" +seeds: [] +hosts: [] +error: true +comment: Should fail because one of the returned host names' domain name parts "evil" mismatches "test". diff --git a/t/data/initial_dns_seedlist_discovery/returned-parent-too-short.json b/t/data/initial_dns_seedlist_discovery/returned-parent-too-short.json new file mode 100644 index 00000000..676eb0c0 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/returned-parent-too-short.json @@ -0,0 +1,7 @@ +{ + "uri": "mongodb+srv://test13.test.build.10gen.cc/", + "seeds": [], + "hosts": [], + "error": true, + "comment": "Should fail because returned host name's parent (build.10gen.cc) misses \"test.\"" +} diff --git a/t/data/initial_dns_seedlist_discovery/returned-parent-too-short.yml b/t/data/initial_dns_seedlist_discovery/returned-parent-too-short.yml new file mode 100644 index 00000000..397aec89 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/returned-parent-too-short.yml @@ -0,0 +1,5 @@ +uri: "mongodb+srv://test13.test.build.10gen.cc/" +seeds: [] +hosts: [] +error: true +comment: Should fail because returned host name's parent (build.10gen.cc) misses "test." diff --git a/t/data/initial_dns_seedlist_discovery/returned-parent-wrong.json b/t/data/initial_dns_seedlist_discovery/returned-parent-wrong.json new file mode 100644 index 00000000..3aabfd81 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/returned-parent-wrong.json @@ -0,0 +1,7 @@ +{ + "uri": "mongodb+srv://test12.test.build.10gen.cc/", + "seeds": [], + "hosts": [], + "error": true, + "comment": "Should fail because returned host name is too short and mismatches a parent." +} diff --git a/t/data/initial_dns_seedlist_discovery/returned-parent-wrong.yml b/t/data/initial_dns_seedlist_discovery/returned-parent-wrong.yml new file mode 100644 index 00000000..1fc3867a --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/returned-parent-wrong.yml @@ -0,0 +1,5 @@ +uri: "mongodb+srv://test12.test.build.10gen.cc/" +seeds: [] +hosts: [] +error: true +comment: Should fail because returned host name is too short and mismatches a parent. diff --git a/t/data/initial_dns_seedlist_discovery/two-results-default-port.json b/t/data/initial_dns_seedlist_discovery/two-results-default-port.json new file mode 100644 index 00000000..66028310 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/two-results-default-port.json @@ -0,0 +1,16 @@ +{ + "uri": "mongodb+srv://test1.test.build.10gen.cc/?replicaSet=repl0", + "seeds": [ + "localhost.test.build.10gen.cc:27017", + "localhost.test.build.10gen.cc:27018" + ], + "hosts": [ + "localhost:27017", + "localhost:27018", + "localhost:27019" + ], + "options": { + "replicaSet": "repl0", + "ssl": true + } +} diff --git a/t/data/initial_dns_seedlist_discovery/two-results-default-port.yml b/t/data/initial_dns_seedlist_discovery/two-results-default-port.yml new file mode 100644 index 00000000..61d38b5e --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/two-results-default-port.yml @@ -0,0 +1,11 @@ +uri: "mongodb+srv://test1.test.build.10gen.cc/?replicaSet=repl0" +seeds: + - localhost.test.build.10gen.cc:27017 + - localhost.test.build.10gen.cc:27018 +hosts: + - localhost:27017 + - localhost:27018 + - localhost:27019 +options: + replicaSet: repl0 + ssl: true diff --git a/t/data/initial_dns_seedlist_discovery/two-results-nonstandard-port.json b/t/data/initial_dns_seedlist_discovery/two-results-nonstandard-port.json new file mode 100644 index 00000000..4900f7cf --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/two-results-nonstandard-port.json @@ -0,0 +1,16 @@ +{ + "uri": "mongodb+srv://test2.test.build.10gen.cc/?replicaSet=repl0", + "seeds": [ + "localhost.test.build.10gen.cc:27018", + "localhost.test.build.10gen.cc:27019" + ], + "hosts": [ + "localhost:27017", + "localhost:27018", + "localhost:27019" + ], + "options": { + "replicaSet": "repl0", + "ssl": true + } +} diff --git a/t/data/initial_dns_seedlist_discovery/two-results-nonstandard-port.yml b/t/data/initial_dns_seedlist_discovery/two-results-nonstandard-port.yml new file mode 100644 index 00000000..7185f52c --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/two-results-nonstandard-port.yml @@ -0,0 +1,11 @@ +uri: "mongodb+srv://test2.test.build.10gen.cc/?replicaSet=repl0" +seeds: + - localhost.test.build.10gen.cc:27018 + - localhost.test.build.10gen.cc:27019 +hosts: + - localhost:27017 + - localhost:27018 + - localhost:27019 +options: + replicaSet: repl0 + ssl: true diff --git a/t/data/initial_dns_seedlist_discovery/two-txt-records.json b/t/data/initial_dns_seedlist_discovery/two-txt-records.json new file mode 100644 index 00000000..f0654ef6 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/two-txt-records.json @@ -0,0 +1,7 @@ +{ + "uri": "mongodb+srv://test6.test.build.10gen.cc/", + "seeds": [], + "hosts": [], + "error": true, + "comment": "Should fail because there are two TXT records." +} diff --git a/t/data/initial_dns_seedlist_discovery/two-txt-records.yml b/t/data/initial_dns_seedlist_discovery/two-txt-records.yml new file mode 100644 index 00000000..c6093613 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/two-txt-records.yml @@ -0,0 +1,5 @@ +uri: "mongodb+srv://test6.test.build.10gen.cc/" +seeds: [] +hosts: [] +error: true +comment: Should fail because there are two TXT records. diff --git a/t/data/initial_dns_seedlist_discovery/txt-record-not-allowed-option.json b/t/data/initial_dns_seedlist_discovery/txt-record-not-allowed-option.json new file mode 100644 index 00000000..2a5cf2f0 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/txt-record-not-allowed-option.json @@ -0,0 +1,7 @@ +{ + "uri": "mongodb+srv://test10.test.build.10gen.cc/?replicaSet=repl0", + "seeds": [], + "hosts": [], + "error": true, + "comment": "Should fail because socketTimeoutMS is not an allowed option." +} diff --git a/t/data/initial_dns_seedlist_discovery/txt-record-not-allowed-option.yml b/t/data/initial_dns_seedlist_discovery/txt-record-not-allowed-option.yml new file mode 100644 index 00000000..f4ff1cfd --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/txt-record-not-allowed-option.yml @@ -0,0 +1,5 @@ +uri: "mongodb+srv://test10.test.build.10gen.cc/?replicaSet=repl0" +seeds: [] +hosts: [] +error: true +comment: Should fail because socketTimeoutMS is not an allowed option. diff --git a/t/data/initial_dns_seedlist_discovery/txt-record-with-overridden-ssl-option.json b/t/data/initial_dns_seedlist_discovery/txt-record-with-overridden-ssl-option.json new file mode 100644 index 00000000..0ebc737b --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/txt-record-with-overridden-ssl-option.json @@ -0,0 +1,16 @@ +{ + "uri": "mongodb+srv://test5.test.build.10gen.cc/?ssl=false", + "seeds": [ + "localhost.test.build.10gen.cc:27017" + ], + "hosts": [ + "localhost:27017", + "localhost:27018", + "localhost:27019" + ], + "options": { + "replicaSet": "repl0", + "authSource": "thisDB", + "ssl": false + } +} diff --git a/t/data/initial_dns_seedlist_discovery/txt-record-with-overridden-ssl-option.yml b/t/data/initial_dns_seedlist_discovery/txt-record-with-overridden-ssl-option.yml new file mode 100644 index 00000000..2a922aa2 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/txt-record-with-overridden-ssl-option.yml @@ -0,0 +1,11 @@ +uri: "mongodb+srv://test5.test.build.10gen.cc/?ssl=false" +seeds: + - localhost.test.build.10gen.cc:27017 +hosts: + - localhost:27017 + - localhost:27018 + - localhost:27019 +options: + replicaSet: repl0 + authSource: thisDB + ssl: false diff --git a/t/data/initial_dns_seedlist_discovery/txt-record-with-overridden-uri-option.json b/t/data/initial_dns_seedlist_discovery/txt-record-with-overridden-uri-option.json new file mode 100644 index 00000000..2626ba60 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/txt-record-with-overridden-uri-option.json @@ -0,0 +1,16 @@ +{ + "uri": "mongodb+srv://test5.test.build.10gen.cc/?authSource=otherDB", + "seeds": [ + "localhost.test.build.10gen.cc:27017" + ], + "hosts": [ + "localhost:27017", + "localhost:27018", + "localhost:27019" + ], + "options": { + "replicaSet": "repl0", + "authSource": "otherDB", + "ssl": true + } +} diff --git a/t/data/initial_dns_seedlist_discovery/txt-record-with-overridden-uri-option.yml b/t/data/initial_dns_seedlist_discovery/txt-record-with-overridden-uri-option.yml new file mode 100644 index 00000000..a9015599 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/txt-record-with-overridden-uri-option.yml @@ -0,0 +1,11 @@ +uri: "mongodb+srv://test5.test.build.10gen.cc/?authSource=otherDB" +seeds: + - localhost.test.build.10gen.cc:27017 +hosts: + - localhost:27017 + - localhost:27018 + - localhost:27019 +options: + replicaSet: repl0 + authSource: otherDB + ssl: true diff --git a/t/data/initial_dns_seedlist_discovery/txt-record-with-unallowed-option.json b/t/data/initial_dns_seedlist_discovery/txt-record-with-unallowed-option.json new file mode 100644 index 00000000..0d333a45 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/txt-record-with-unallowed-option.json @@ -0,0 +1,7 @@ +{ + "uri": "mongodb+srv://test7.test.build.10gen.cc/", + "seeds": [], + "hosts": [], + "error": true, + "comment": "Should fail because \"ssl\" is not an allowed option." +} diff --git a/t/data/initial_dns_seedlist_discovery/txt-record-with-unallowed-option.yml b/t/data/initial_dns_seedlist_discovery/txt-record-with-unallowed-option.yml new file mode 100644 index 00000000..ba3877ee --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/txt-record-with-unallowed-option.yml @@ -0,0 +1,5 @@ +uri: "mongodb+srv://test7.test.build.10gen.cc/" +seeds: [] +hosts: [] +error: true +comment: Should fail because "ssl" is not an allowed option. diff --git a/t/data/initial_dns_seedlist_discovery/uri-with-port.json b/t/data/initial_dns_seedlist_discovery/uri-with-port.json new file mode 100644 index 00000000..b981e2a1 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/uri-with-port.json @@ -0,0 +1,7 @@ +{ + "uri": "mongodb+srv://test5.test.build.10gen.cc:8123/?replicaSet=repl0", + "seeds": [], + "hosts": [], + "error": true, + "comment": "Should fail because the mongodb+srv URI includes a port." +} diff --git a/t/data/initial_dns_seedlist_discovery/uri-with-port.yml b/t/data/initial_dns_seedlist_discovery/uri-with-port.yml new file mode 100644 index 00000000..f1944dcd --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/uri-with-port.yml @@ -0,0 +1,5 @@ +uri: "mongodb+srv://test5.test.build.10gen.cc:8123/?replicaSet=repl0" +seeds: [] +hosts: [] +error: true +comment: Should fail because the mongodb+srv URI includes a port. diff --git a/t/data/initial_dns_seedlist_discovery/uri-with-two-hosts.json b/t/data/initial_dns_seedlist_discovery/uri-with-two-hosts.json new file mode 100644 index 00000000..5261a39c --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/uri-with-two-hosts.json @@ -0,0 +1,7 @@ +{ + "uri": "mongodb+srv://test5.test.build.10gen.cc,test6.test.build.10gen.cc/?replicaSet=repl0", + "seeds": [], + "hosts": [], + "error": true, + "comment": "Should fail because the mongodb+srv URI includes two host names." +} diff --git a/t/data/initial_dns_seedlist_discovery/uri-with-two-hosts.yml b/t/data/initial_dns_seedlist_discovery/uri-with-two-hosts.yml new file mode 100644 index 00000000..3b2189d4 --- /dev/null +++ b/t/data/initial_dns_seedlist_discovery/uri-with-two-hosts.yml @@ -0,0 +1,5 @@ +uri: "mongodb+srv://test5.test.build.10gen.cc,test6.test.build.10gen.cc/?replicaSet=repl0" +seeds: [] +hosts: [] +error: true +comment: Should fail because the mongodb+srv URI includes two host names. diff --git a/t/lib/MongoDBTest.pm b/t/lib/MongoDBTest.pm index 5816b598..528cd326 100644 --- a/t/lib/MongoDBTest.pm +++ b/t/lib/MongoDBTest.pm @@ -29,7 +29,7 @@ use version; our @EXPORT_OK = qw( build_client get_test_db server_version server_type clear_testdbs get_capped - skip_unless_mongod uri_escape get_unique_collection + skip_unless_mongod skip_if_mongod uri_escape get_unique_collection ); my @testdbs; @@ -123,6 +123,19 @@ sub skip_unless_mongod { } } +sub skip_if_mongod { + eval { + my $conn = build_client( server_selection_timeout_ms => 1000 ); + my $topo = $conn->_topology; + $topo->scan_all_servers; + # will throw if no servers available + $topo->get_readable_link(MongoDB::ReadPreference->new({mode => 'nearest'})); + }; + if ( ! $@ ) { + plan skip_all => "Test can't start with a running mongod"; + } +} + sub server_version { my $conn = shift;