diff --git a/CHANGELOG.md b/CHANGELOG.md index a5474a18..5a5fcef0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## Recent change notes +### 0.4.1 + +* Secure Connection +* The connection string now accepts more than one server. + * Before: only mongodb://www.example.org/test. + * Now it can be: mongodb://www.example.org,www1.example.org,www2.example.org/test + * It is equivalent to: db.pool([mongodb://www.example.org/test, mongodb://www1.example.org/test, mongodb://www2.example.org/test]); +* Added an "uriList" getter in "Db" class. + ### 0.4.1-dev.2.2 * Lint clean-up diff --git a/README.md b/README.md index 0a8c11c5..0546981d 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Server-side driver library for MongoDb implemented in pure Dart. ```dart - Db db = new Db("mongodb://localhost:27017/mongo_dart-blog"); + var db = Db("mongodb://localhost:27017/mongo_dart-blog"); await db.open(); ``` @@ -91,13 +91,13 @@ Simple app on base of [JSON ZIPS dataset](http://media.mongodb.org/zips.json) ```dart import 'package:mongo_dart/mongo_dart.dart'; -main() async { +void main() async { void displayZip(Map zip) { print( 'state: ${zip["state"]}, city: ${zip["city"]}, zip: ${zip["id"]}, population: ${zip["pop"]}'); } - Db db = - new Db("mongodb://reader:vHm459fU@ds037468.mongolab.com:37468/samlple"); + var db = + Db("mongodb://reader:vHm459fU@ds037468.mongolab.com:37468/samlple"); var zips = db.collection('zip'); await db.open(); print(''' @@ -145,12 +145,66 @@ main() async { } ``` +### Secure connection + +You can connect using a secured tls/ssl connection in one of this two ways: + +* setting the secure connection parameter to true in db.open() + +```dart + await db.open(secure: true); +``` + +* adding a query parameter => "tls=true" (or "ssl=true"). + +```dart + var db = DB('mongodb://www.example.com:27017/test?tls=true&authSource=admin'); + or + var db = DB('mongodb://www.example.com:27017/test?ssl=true&authSource=admin'); +``` + +No certificates can be used. + +### Atlas (MongoDb cloud service) connection + +Atlas requires a tls connection, so now it is possible to connect to this cloud service. +When creating a cluster Atlas shows you three ways of connecting: +Mongo shell, driver and MongoDb Compass Application. +The connection string is in Seedlist Connection Format (starts with mongodb+srv://). +At present this driver does not support this connection string format. +You can do the following: +connect with the mongo shell to the address given by the site, for example: + +```bash +mongo "mongodb+srv://cluster0.xtest.mongodb.net/" --username +``` + +The shell will ask you the password, enter it. + +immediately after, the shell will show you the connection string in Standard Connection Format (starting with "mongodb://") in a line starting with "connecting to", for example. + +```code +connecting to: mongodb://cluster0-shard-00-00.xtest.mongodb.net:27017,cluster0-shard-00-02.xtest.mongodb.net:27017,cluster0-shard-00-01.xtest.mongodb.net:27017/?authSource=admin&compressors=disabled&gssapiServiceName=mongodb&replicaSet=atlas-stcn2i-shard-0&ssl=true +``` + +Copy that string and add your user and password immediately after the "mongodb://" schema in the format "username:password@", for example + +```code +mongodb://:@cluster0-shard-00-00.xtest.mongodb.net:27017,cluster0-shard-00-02.xtest.mongodb.net:27017,cluster0-shard-00-01.xtest.mongodb.net:27017/?authSource=admin&compressors=disabled&gssapiServiceName=mongodb&replicaSet=atlas-stcn2i-shard-0&ssl=true +``` + +Here we are, you can use the latter Connection String in the Db constructor + +```dart +var db = Db("mongodb://dbUser:password@cluster0-shard-00-00.xtest.mongodb.net:27017,cluster0-shard-00-02.xtest.mongodb.net:27017,cluster0-shard-00-01.xtest.mongodb.net:27017/test-db?authSource=admin&compressors=disabled&gssapiServiceName=mongodb&replicaSet=atlas-stcn2i-shard-0&ssl=true"); +``` + ### See also -- [API Doc](http://www.dartdocs.org/documentation/mongo_dart/latest) +* [API Doc](http://www.dartdocs.org/documentation/mongo_dart/latest) -- [Feature check list](https://github.com/vadimtsushko/mongo_dart/blob/master/doc/feature_checklist.md) +* [Feature check list](https://github.com/vadimtsushko/mongo_dart/blob/master/doc/feature_checklist.md) -- [Recent change notes](https://github.com/vadimtsushko/mongo_dart/blob/master/changelog.md) +* [Recent change notes](https://github.com/vadimtsushko/mongo_dart/blob/master/changelog.md) -- Additional [examples](https://github.com/vadimtsushko/mongo_dart/tree/master/example) and [tests](https://github.com/vadimtsushko/mongo_dart/tree/master/test) +* Additional [examples](https://github.com/vadimtsushko/mongo_dart/tree/master/example) and [tests](https://github.com/vadimtsushko/mongo_dart/tree/master/test) diff --git a/example/blog.dart b/example/blog.dart index 78fcd43f..48016121 100644 --- a/example/blog.dart +++ b/example/blog.dart @@ -6,6 +6,14 @@ String port = Platform.environment['MONGO_DART_DRIVER_PORT'] ?? '27017'; void main() async { var db = Db('mongodb://$host:$port/mongo_dart-blog'); + // Example url for Atlas connection + /* var db = Db('mongodb://:@' + 'cluster0-shard-00-02.xtest.mongodb.net:27017,' + 'cluster0-shard-00-01.xtest.mongodb.net:27017,' + 'cluster0-shard-00-00.xtest.mongodb.net:27017/' + 'mongo_dart-blog?authSource=admin&compressors=disabled' + '&gssapiServiceName=mongodb&replicaSet=atlas-stcn2i-shard-0' + '&ssl=true'); */ var authors = {}; var users = {}; await db.open(); diff --git a/lib/mongo_dart.dart b/lib/mongo_dart.dart index fa62913c..e2665a4b 100644 --- a/lib/mongo_dart.dart +++ b/lib/mongo_dart.dart @@ -7,7 +7,7 @@ library mongo_dart; import 'dart:async'; import 'dart:collection'; import 'dart:convert' show base64, utf8; -import 'dart:io' show File, FileMode, IOSink, Socket; +import 'dart:io' show File, FileMode, IOSink, SecureSocket, Socket; import 'dart:math'; import 'dart:typed_data'; import 'package:collection/collection.dart'; diff --git a/lib/src/database/db.dart b/lib/src/database/db.dart index 9967761c..5cdaee8d 100644 --- a/lib/src/database/db.dart +++ b/lib/src/database/db.dart @@ -113,10 +113,12 @@ class WriteConcern { class _UriParameters { static const authMechanism = 'authMechanism'; static const authSource = 'authSource'; + static const tls = 'tls'; + static const ssl = 'ssl'; } class Db { - final MONGO_DEFAULT_PORT = 27017; + static const mongoDefaultPort = 27017; final _log = Logger('Db'); final List _uriList = []; @@ -143,7 +145,11 @@ class Db { /// And that code direct to MongoLab server on 37637 port, database *testdb*, username *dart*, password *test* /// var db = new Db('mongodb://dart:test@ds037637-a.mongolab.com:37637/objectory_blog'); Db(String uriString, [this._debugInfo]) { - _uriList.add(uriString); + if (uriString.contains(',')) { + _uriList.addAll(_splitServers(uriString)); + } else { + _uriList.add(uriString); + } } Db.pool(List uriList, [this._debugInfo]) { @@ -156,19 +162,59 @@ class Db { _Connection get masterConnection => _connectionManager.masterConnection; - ServerConfig _parseUri(String uriString) { + List get uriList => _uriList.toList(); + + List _splitServers(String uriString) { + String prefix, suffix; + var startServersIndex, endServersIndex; + if (uriString.startsWith('mongodb://')) { + startServersIndex = 10; + } else { + throw MongoDartError('Unexpected scheme in url $uriString. ' + 'The url is expected to start with "mongodb://"'); + } + endServersIndex = uriString.indexOf('/', startServersIndex); + var serversString = uriString.substring(startServersIndex, endServersIndex); + var credentialsIndex = serversString.indexOf('@'); + if (credentialsIndex != -1) { + startServersIndex += credentialsIndex + 1; + serversString = uriString.substring(startServersIndex, endServersIndex); + } + prefix = uriString.substring(0, startServersIndex); + suffix = uriString.substring(endServersIndex); + var parts = serversString.split(','); + return [for (var server in parts) '$prefix${server.trim()}$suffix']; + } + + ServerConfig _parseUri(String uriString, {bool isSecure}) { + isSecure ??= false; var uri = Uri.parse(uriString); if (uri.scheme != 'mongodb') { throw MongoDartError('Invalid scheme in uri: $uriString ${uri.scheme}'); } - var serverConfig = ServerConfig(); - serverConfig.host = uri.host; - serverConfig.port = uri.port; + uri.queryParameters.forEach((String queryParam, String value) { + if (queryParam == _UriParameters.authMechanism) { + selectAuthenticationMechanism(value); + } + + if (queryParam == _UriParameters.authSource) { + authSourceDb = Db._authDb(value); + } - if (serverConfig.port == null || serverConfig.port == 0) { - serverConfig.port = MONGO_DEFAULT_PORT; + if ((queryParam == _UriParameters.tls || + queryParam == _UriParameters.ssl) && + value == 'true') { + isSecure = true; + } + }); + + var serverConfig = ServerConfig( + uri.host ?? '127.0.0.1', uri.port ?? mongoDefaultPort, isSecure); + + if (serverConfig.port == 0) { + serverConfig.port = mongoDefaultPort; } if (uri.userInfo.isNotEmpty) { @@ -186,16 +232,6 @@ class Db { databaseName = uri.path.replaceAll('/', ''); } - uri.queryParameters.forEach((String queryParam, String value) { - if (queryParam == _UriParameters.authMechanism) { - selectAuthenticationMechanism(value); - } - - if (queryParam == _UriParameters.authSource) { - authSourceDb = Db._authDb(value); - } - }); - return serverConfig; } @@ -255,7 +291,9 @@ class Db { return section.payload.content; } - Future open({WriteConcern writeConcern = WriteConcern.ACKNOWLEDGED}) { + Future open( + {WriteConcern writeConcern = WriteConcern.ACKNOWLEDGED, + bool secure = false}) { return Future.sync(() { if (state == State.OPENING) { throw MongoDartError('Attempt to open db in state $state'); @@ -266,7 +304,7 @@ class Db { _connectionManager = _ConnectionManager(this); _uriList.forEach((uri) { - _connectionManager.addConnection(_parseUri(uri)); + _connectionManager.addConnection(_parseUri(uri, isSecure: secure)); }); return _connectionManager.open(writeConcern); @@ -384,7 +422,7 @@ class Db { return ListCollectionsCursor(this, filter).stream; } else { // Using system collections (pre v3.0 API) - Map selector = {}; + var selector = {}; // If we are limiting the access to a specific collection name if (filter.containsKey('name')) { selector['name'] = "${databaseName}.${filter['name']}"; @@ -599,8 +637,7 @@ class Db { if (!_masterConnection.serverCapabilities.supportsOpMsg) { return {}; } - var operation = - ServerStatusOperation(this, options: options); + var operation = ServerStatusOperation(this, options: options); return operation.execute(); } diff --git a/lib/src/database/info/server_status.dart b/lib/src/database/info/server_status.dart index 5f382661..c079f1cb 100644 --- a/lib/src/database/info/server_status.dart +++ b/lib/src/database/info/server_status.dart @@ -24,7 +24,9 @@ class ServerStatus { storageEngineName = serverStatus[keyStorageEngine][keyName]; isPersistent = serverStatus[keyStorageEngine][keyPersistent] ?? true; if (storageEngineName == keyWiredTiger) { - if (serverStatus[keyWiredTiger][keyLog][keyMaximumLogFileSize] > 0) { + // Atlas service does not return the "wiredTiger" element + if (!serverStatus.containsKey(keyWiredTiger) || + serverStatus[keyWiredTiger][keyLog][keyMaximumLogFileSize] > 0) { isJournaled = true; } } diff --git a/lib/src/database/operation/create_index_operation.dart b/lib/src/database/operation/create_index_operation.dart index 39a75aba..c17f2a02 100644 --- a/lib/src/database/operation/create_index_operation.dart +++ b/lib/src/database/operation/create_index_operation.dart @@ -18,12 +18,11 @@ const Set keysToOmit = { }; class CreateIndexOperation extends CommandOperation { - DbCollection collection; Object fieldOrSpec; Map indexes; - CreateIndexOperation( - Db db, this.collection, this.fieldOrSpec, CreateIndexOptions indexOptions) + CreateIndexOperation(Db db, DbCollection collection, this.fieldOrSpec, + CreateIndexOptions indexOptions) : super(db, indexOptions.options, collection: collection, aspect: Aspect.writeOperation) { var indexParameters = parseIndexOptions(fieldOrSpec); diff --git a/lib/src/database/server_config.dart b/lib/src/database/server_config.dart index 12a06cd0..5333a8de 100644 --- a/lib/src/database/server_config.dart +++ b/lib/src/database/server_config.dart @@ -3,8 +3,12 @@ part of mongo_dart; class ServerConfig { String host; int port; + bool isSecure; String userName; String password; - ServerConfig([this.host = '127.0.0.1', this.port = 27017]); + ServerConfig( + [this.host = '127.0.0.1', + this.port = Db.mongoDefaultPort, + this.isSecure = false]); String get hostUrl => '$host:${port.toString()}'; } diff --git a/lib/src/network/connection.dart b/lib/src/network/connection.dart index eb4f9d33..9da54364 100644 --- a/lib/src/network/connection.dart +++ b/lib/src/network/connection.dart @@ -1,5 +1,12 @@ part of mongo_dart; +const noSecureRequestError = 'The socket connection has been reset by peer.' + '\nPossible causes:' + '\n- Trying to connect to an ssl/tls encrypted database without specifiyng' + '\n either the query parm tls=true ' + 'or the secure=true parameter in db.open()' + '\n- Others'; + class _ServerCapabilities { int maxWireVersion = 0; bool aggregationCursor = false; @@ -60,9 +67,15 @@ class _Connection { Future connect() async { Socket _socket; try { - _socket = await Socket.connect(serverConfig.host, serverConfig.port); + if (serverConfig.isSecure) { + _socket = + await SecureSocket.connect(serverConfig.host, serverConfig.port); + } else { + _socket = await Socket.connect(serverConfig.host, serverConfig.port); + } } catch (e, st) { - _log.severe('Socket error on connect(): ${e} ${st}'); + _log.severe( + 'Socket error on connect to ${serverConfig.hostUrl}: ${e} ${st}'); _closed = true; connected = false; var ex = const ConnectionException('Could not connect to the Data Base.'); @@ -74,19 +87,24 @@ class _Connection { socket = _socket; _repliesSubscription = socket - .transform( - MongoMessageHandler().transformer) + .transform(MongoMessageHandler().transformer) .listen(_receiveReply, onError: (e, st) { _log.severe('Socket error ${e} ${st}'); if (!_closed) { - _onSocketError(); + _closeSocketOnError(socketError: e); } }, cancelOnError: true, + // onDone is not called in any case after onData or OnError, + // it is called when the socket closes, i.e. it is an error. + // Possible causes: + // * Trying to connect to a tls encrypted Database + // without specifing tls=true in the query parms or setting + // the secure parameter to true in db.open() onDone: () { if (!_closed) { - _onSocketError(); + _closeSocketOnError(socketError: noSecureRequestError); } }); connected = true; @@ -99,7 +117,7 @@ class _Connection { return socket.close(); } - _sendBuffer() { + void _sendBuffer() { _log.fine(() => '_sendBuffer ${_sendQueue.isNotEmpty}'); var message = []; while (_sendQueue.isNotEmpty) { @@ -171,10 +189,11 @@ class _Connection { } } - void _onSocketError() { + void _closeSocketOnError({dynamic socketError}) { _closed = true; connected = false; - var ex = const ConnectionException('connection closed.'); + var ex = ConnectionException( + 'connection closed${socketError == null ? '.' : ': $socketError'}'); _pendingQueries.forEach((id) { Completer completer = _replyCompleters.remove(id); completer.completeError(ex); diff --git a/lib/src/network/connection_manager.dart b/lib/src/network/connection_manager.dart index 790e552b..902184ae 100644 --- a/lib/src/network/connection_manager.dart +++ b/lib/src/network/connection_manager.dart @@ -83,12 +83,12 @@ class _ConnectionManager { }); } - addConnection(ServerConfig serverConfig) { + void addConnection(ServerConfig serverConfig) { var connection = _Connection(this, serverConfig); _connectionPool[serverConfig.hostUrl] = connection; } - removeConnection(_Connection connection) { + _Connection removeConnection(_Connection connection) { connection.close(); if (connection.isMaster) { _masterConnection = null; diff --git a/lib/src/network/mongo_message_transformer.dart b/lib/src/network/mongo_message_transformer.dart index cced1377..0decf275 100644 --- a/lib/src/network/mongo_message_transformer.dart +++ b/lib/src/network/mongo_message_transformer.dart @@ -4,7 +4,9 @@ class MongoMessageHandler { final _log = Logger('MongoMessageTransformer'); final converter = PacketConverter(); - void handleData(List data, EventSink sink) { + void handleData( + /* List */ Uint8List data, + EventSink sink) { converter.addPacket(data); while (!converter.messages.isEmpty) { var buffer = BsonBinary.from(converter.messages.removeFirst()); diff --git a/pubspec.yaml b/pubspec.yaml index 2f47c3f7..68a41e20 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: mongo_dart -version: 0.4.1-dev.2.2 +version: 0.4.1 description: MongoDB driver for Dart, implemented in pure Dart homepage: https://github.com/mongo-dart/mongo_dart environment: diff --git a/test/all_tests.dart b/test/all_tests.dart index 122bd894..faab4123 100644 --- a/test/all_tests.dart +++ b/test/all_tests.dart @@ -5,6 +5,7 @@ import 'gridfs_test.dart' as gridfs; import 'packet_converter_test.dart' as converter; import 'mongo_dart_query_test.dart' as mongo_dart_query; import 'authentication_test.dart' as auth_tests; +import 'ssl_connection_test.dart' as ssl_tests; //import 'replica_tests.dart' as replica; void main() { @@ -13,5 +14,6 @@ void main() { gridfs.main(); mongo_dart_query.main(); auth_tests.main(); + ssl_tests.main(); //replica.main(); } diff --git a/test/database_test.dart b/test/database_test.dart index 55c04353..2553a823 100644 --- a/test/database_test.dart +++ b/test/database_test.dart @@ -21,6 +21,39 @@ String getRandomCollectionName() { return name; } +Future testDbConnectionString() async { + var db = Db('mongodb://www.example.com'); + expect(db.uriList.first, 'mongodb://www.example.com'); + db = Db('mongodb://www.example.com:27317'); + expect(db.uriList.first, 'mongodb://www.example.com:27317'); + db = Db.pool([ + 'mongodb://www.example.com:27017', + 'mongodb://www.example.com:27217', + 'mongodb://www.example.com:27317' + ]); + expect(db.uriList.first, 'mongodb://www.example.com:27017'); + expect(db.uriList[1], 'mongodb://www.example.com:27217'); + expect(db.uriList.last, 'mongodb://www.example.com:27317'); + db = Db.pool([ + 'mongodb://www.example.com:27017/test', + 'mongodb://www.example.com:27217/test', + 'mongodb://www.example.com:27317/test' + ]); + expect(db.uriList[1], 'mongodb://www.example.com:27217/test'); + db = Db('mongodb://www.example.com:27017,www.example.com:27217,' + 'www.example.com:27317/test'); + expect(db.uriList.first, 'mongodb://www.example.com:27017/test'); + expect(db.uriList[1], 'mongodb://www.example.com:27217/test'); + expect(db.uriList.last, 'mongodb://www.example.com:27317/test'); + // As a syntactic sugar we accept also blnak after comma, + // even if it should not be correct. + db = Db('mongodb://www.example.com:27017, www.example.com:27217, ' + 'www.example.com:27317/test'); + expect(db.uriList.first, 'mongodb://www.example.com:27017/test'); + expect(db.uriList[1], 'mongodb://www.example.com:27217/test'); + expect(db.uriList.last, 'mongodb://www.example.com:27317/test'); +} + Future testGetCollectionInfos() async { var collectionName = getRandomCollectionName(); var collection = db.collection(collectionName); @@ -389,8 +422,11 @@ db.runCommand( expect(p1['\u0024group'], isNotNull); expect(p1['\$group'], isNotNull); - var v = await collection.aggregate(pipeline); - final result = v['result'] as List; + /* var v = await collection.aggregate(pipeline); + final result = v['result'] as List; */ + var v = await collection.aggregate(pipeline, cursor: {}); + var cursor = v['cursor'] as Map; + var result = cursor['firstBatch'] as List; expect(result[0]['_id'], 'Age of Steam'); expect(result[0]['avgRating'], 3); } @@ -1311,6 +1347,9 @@ void main() async { await cleanupDatabase(); }); + group('Db creation tests:', () { + test('test connection string', testDbConnectionString); + }); group('DbCollection tests:', () { test('testAuthComponents', testAuthComponents); }); diff --git a/test/ssl_connection_test.dart b/test/ssl_connection_test.dart new file mode 100644 index 00000000..dfc8be2e --- /dev/null +++ b/test/ssl_connection_test.dart @@ -0,0 +1,61 @@ +import 'package:test/test.dart'; +import 'package:mongo_dart/mongo_dart.dart'; + +const sslDbConnectionString = + 'mongodb://cluster0-shard-00-00-smeth.gcp.mongodb.net:27017/' + 'test?authSource=admin,' + 'mongodb://cluster0-shard-00-01-smeth.gcp.mongodb.net:27017/' + 'test?authSource=admin,' + 'mongodb://cluster0-shard-00-02-smeth.gcp.mongodb.net:27017/' + 'test?authSource=admin'; +const sslDbUsername = 'mongo_dart_tester'; +const sslDbPassword = 'O8kipHnIyenpc9fV'; +const sslQueryParmConnectionString = + 'mongodb://cluster0-shard-00-00-smeth.gcp.mongodb.net:27017,' + 'cluster0-shard-00-01-smeth.gcp.mongodb.net:27017,' + 'cluster0-shard-00-02-smeth.gcp.mongodb.net:27017/' + 'test?authSource=admin&ssl=true'; +const tlsQueryParmConnectionString = 'mongodb://cluster0-shard-00-01-smeth' + '.gcp.mongodb.net:27017/test?tls=true&authSource=admin'; + +void main() { + test('Connect and authenticate to a database over SSL', () async { + var db = Db.pool(sslDbConnectionString.split(',')); + + await db.open(secure: true); + await db.authenticate(sslDbUsername, sslDbPassword); + await db.collection('test').find().toList(); + await db.close(); + }); + + test('Ssl as query parm', () async { + var db = Db(sslQueryParmConnectionString); + + await db.open(); + await db.authenticate(sslDbUsername, sslDbPassword); + await db.collection('test').find().toList(); + await db.close(); + }); + + test('Ssl wit no secure info => Error', () async { + var db = Db.pool(sslDbConnectionString.split(',')); + expect(() => db.open(), throwsA((ConnectionException e) => true)); + }); + + test('Tls as query parm', () async { + var db = Db(tlsQueryParmConnectionString); + + await db.open(); + await db.authenticate(sslDbUsername, sslDbPassword); + await db.collection('test').find().toList(); + await db.close(); + }); + test('Tls as query parm plus secure parameter', () async { + var db = Db(tlsQueryParmConnectionString); + + await db.open(secure: true); + await db.authenticate(sslDbUsername, sslDbPassword); + await db.collection('test').find().toList(); + await db.close(); + }); +}