11import Foundation
2+ import SwiftBSON
23
34/// Represents a MongoDB connection string.
45/// - SeeAlso: https://docs.mongodb.com/manual/reference/connection-string/
@@ -8,6 +9,22 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
89 /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc3986#section-2.2
910 fileprivate static let forbiddenUserInfoCharacters = [ " : " , " / " , " ? " , " # " , " [ " , " ] " , " @ " ]
1011
12+ fileprivate enum OptionName : String {
13+ case authSource = " authsource "
14+ case authMechanism = " authmechanism "
15+ case authMechanismProperties = " authmechanismproperties "
16+ case ssl
17+ case tls
18+ case tlsAllowInvalidCertificates = " tlsallowinvalidcertificates "
19+ case tlsAllowInvalidHostnames = " tlsallowinvalidhostnames "
20+ case tlsCAFile = " tlscafile "
21+ case tlsCertificateKeyFile = " tlscertificatekeyfile "
22+ case tlsCertificateKeyFilePassword = " tlscertificatekeyfilepassword "
23+ case tlsDisableCertificateRevocationCheck = " tlsdisablecertificaterevocationcheck "
24+ case tlsDisableOCSPEndpointCheck = " tlsdisableocspendpointcheck "
25+ case tlsInsecure = " tlsinsecure "
26+ }
27+
1128 /// Represents a connection string scheme.
1229 public struct Scheme : LosslessStringConvertible , Equatable {
1330 /// Indicates that this connection string uses the scheme `mongodb`.
@@ -126,10 +143,23 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
126143 }
127144
128145 private struct Options {
146+ // Authentication options
129147 fileprivate var authSource : String ?
130148 fileprivate var authMechanism : MongoCredential . Mechanism ?
131149 fileprivate var authMechanismProperties : BSONDocument ?
132150
151+ // TLS options
152+ fileprivate var ssl : Bool ?
153+ fileprivate var tls : Bool ?
154+ fileprivate var tlsAllowInvalidCertificates : Bool ?
155+ fileprivate var tlsAllowInvalidHostnames : Bool ?
156+ fileprivate var tlsCAFile : URL ?
157+ fileprivate var tlsCertificateKeyFile : URL ?
158+ fileprivate var tlsCertificateKeyFilePassword : String ?
159+ fileprivate var tlsDisableCertificateRevocationCheck : Bool ?
160+ fileprivate var tlsDisableOCSPEndpointCheck : Bool ?
161+ fileprivate var tlsInsecure : Bool ?
162+
133163 fileprivate init ( _ uriOptions: String ) throws {
134164 let options = uriOptions. components ( separatedBy: " & " )
135165 for option in options {
@@ -140,23 +170,45 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
140170 + " equals signs "
141171 )
142172 }
143- let name = nameAndValue [ 0 ] . lowercased ( )
144- let value = try nameAndValue [ 1 ] . getPercentDecoded ( forKey: name)
173+ guard let name = OptionName ( rawValue: nameAndValue [ 0 ] . lowercased ( ) ) else {
174+ throw MongoError . InvalidArgumentError (
175+ message: " Connection string contains unsupported option: \( nameAndValue [ 0 ] ) "
176+ )
177+ }
178+ let value = try nameAndValue [ 1 ] . getPercentDecoded ( forKey: name. rawValue)
145179 switch name {
146- case " authsource " :
180+ case . authSource :
147181 if value. isEmpty {
148182 throw MongoError . InvalidArgumentError (
149- message: " Connection string authSource option must not be empty "
183+ message: " Connection string \( name . rawValue ) option must not be empty "
150184 )
151185 }
152186 self . authSource = value
153- case " authmechanism " :
187+ case . authMechanism :
154188 self . authMechanism = try MongoCredential . Mechanism ( value)
155- case " authmechanismproperties " :
189+ case . authMechanismProperties :
156190 self . authMechanismProperties = try self . parseAuthMechanismProperties ( properties: value)
157- default :
158- // TODO: SWIFT-1163: error on unknown options
159- break
191+ case . ssl:
192+ self . ssl = try value. getBool ( forKey: name. rawValue)
193+ case . tls:
194+ self . tls = try value. getBool ( forKey: name. rawValue)
195+ case . tlsAllowInvalidCertificates:
196+ self . tlsAllowInvalidCertificates = try value. getBool ( forKey: name. rawValue)
197+ case . tlsAllowInvalidHostnames:
198+ self . tlsAllowInvalidHostnames = try value. getBool ( forKey: name. rawValue)
199+ case . tlsCAFile:
200+ self . tlsCAFile = URL ( string: value)
201+ case . tlsCertificateKeyFile:
202+ self . tlsCertificateKeyFile = URL ( string: value)
203+ case . tlsCertificateKeyFilePassword:
204+ self . tlsCertificateKeyFilePassword = value
205+ case . tlsDisableCertificateRevocationCheck:
206+ self . tlsDisableCertificateRevocationCheck =
207+ try value. getBool ( forKey: name. rawValue)
208+ case . tlsDisableOCSPEndpointCheck:
209+ self . tlsDisableOCSPEndpointCheck = try value. getBool ( forKey: name. rawValue)
210+ case . tlsInsecure:
211+ self . tlsInsecure = try value. getBool ( forKey: name. rawValue)
160212 }
161213 }
162214 }
@@ -175,16 +227,7 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
175227 case " service_name " , " service_realm " :
176228 propertiesDoc [ kv [ 0 ] ] = . string( kv [ 1 ] )
177229 case " canonicalize_host_name " :
178- switch kv [ 1 ] {
179- case " true " :
180- propertiesDoc [ kv [ 0 ] ] = . bool( true )
181- case " false " :
182- propertiesDoc [ kv [ 0 ] ] = . bool( false )
183- default :
184- throw MongoError . InvalidArgumentError (
185- message: " Value for CANONICALIZE_HOST_NAME in authMechanismProperties must be true or false "
186- )
187- }
230+ propertiesDoc [ kv [ 0 ] ] = . bool( try kv [ 1 ] . getBool ( forKey: " CANONICALIZE_HOST_NAME " ) )
188231 case let other:
189232 throw MongoError . InvalidArgumentError ( message: " Unknown key for authMechanismProperties: \( other) " )
190233 }
@@ -282,6 +325,13 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
282325 let options = try Options ( authDatabaseAndOptions [ 1 ] )
283326
284327 // Parse authentication options into a MongoCredential
328+ try self . validateAndUpdateCredential ( options: options)
329+
330+ // Validate and set TLS options
331+ try self . validateAndSetTLSOptions ( options: options)
332+ }
333+
334+ private mutating func validateAndUpdateCredential( options: Options ) throws {
285335 if let mechanism = options. authMechanism {
286336 var credential = self . credential ?? MongoCredential ( )
287337 credential. source = options. authSource ?? mechanism. getDefaultSource ( defaultAuthDB: self . defaultAuthDB)
@@ -311,6 +361,57 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
311361 }
312362 }
313363
364+ private mutating func validateAndSetTLSOptions( options: Options ) throws {
365+ guard options. tlsInsecure == nil
366+ || ( options. tlsAllowInvalidCertificates == nil
367+ && options. tlsAllowInvalidHostnames == nil
368+ && options. tlsDisableCertificateRevocationCheck == nil
369+ && options. tlsDisableOCSPEndpointCheck == nil )
370+ else {
371+ throw MongoError . InvalidArgumentError (
372+ message: " tlsAllowInvalidCertificates, tlsAllowInvalidHostnames, tlsDisableCertificateRevocationCheck, "
373+ + " and tlsDisableOCSPEndpointCheck cannot be specified if tlsInsecure is specified in the "
374+ + " connection string "
375+ )
376+ }
377+ guard !( options. tlsAllowInvalidCertificates != nil && options. tlsDisableOCSPEndpointCheck != nil ) else {
378+ throw MongoError . InvalidArgumentError (
379+ message: " tlsAllowInvalidCertificates and tlsDisableOCSPEndpointCheck cannot both be specified in the "
380+ + " connection string "
381+ )
382+ }
383+ guard !( options. tlsAllowInvalidCertificates != nil && options. tlsDisableCertificateRevocationCheck != nil )
384+ else {
385+ throw MongoError . InvalidArgumentError (
386+ message: " tlsAllowInvalidCertificates and tlsDisableCertificateRevocationCheck cannot both be "
387+ + " specified in the connection string "
388+ )
389+ }
390+ guard !( options. tlsDisableOCSPEndpointCheck != nil
391+ && options. tlsDisableCertificateRevocationCheck != nil )
392+ else {
393+ throw MongoError . InvalidArgumentError (
394+ message: " tlsDisableOCSPEndpointCheck and tlsDisableCertificateRevocationCheck cannot both be "
395+ + " specified in the connection string "
396+ )
397+ }
398+ if let tls = options. tls, let ssl = options. ssl, tls != ssl {
399+ throw MongoError . InvalidArgumentError (
400+ message: " tls and ssl must have the same value if both are specified in the connection string "
401+ )
402+ }
403+ // if either tls or ssl is specified, the value should be stored in the tls field
404+ self . tls = options. tls ?? options. ssl
405+ self . tlsAllowInvalidCertificates = options. tlsAllowInvalidCertificates
406+ self . tlsAllowInvalidHostnames = options. tlsAllowInvalidHostnames
407+ self . tlsCAFile = options. tlsCAFile
408+ self . tlsCertificateKeyFile = options. tlsCertificateKeyFile
409+ self . tlsCertificateKeyFilePassword = options. tlsCertificateKeyFilePassword
410+ self . tlsDisableCertificateRevocationCheck = options. tlsDisableCertificateRevocationCheck
411+ self . tlsDisableOCSPEndpointCheck = options. tlsDisableOCSPEndpointCheck
412+ self . tlsInsecure = options. tlsInsecure
413+ }
414+
314415 /// `Codable` conformance
315416 public func encode( to encoder: Encoder ) throws {
316417 var container = encoder. singleValueContainer ( )
@@ -343,6 +444,50 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
343444 return des
344445 }
345446
447+ /// Returns a document containing all of the options provided after the ? of the URI.
448+ internal var options : BSONDocument {
449+ var options = BSONDocument ( )
450+
451+ if let source = self . credential? . source {
452+ options [ OptionName . authSource] = . string( source)
453+ }
454+ if let mechanism = self . credential? . mechanism {
455+ options [ OptionName . authMechanism] = . string( mechanism. description)
456+ }
457+ if let properties = self . credential? . mechanismProperties {
458+ options [ OptionName . authMechanismProperties] = . document( properties)
459+ }
460+ if let tls = self . tls {
461+ options [ OptionName . tls] = . bool( tls)
462+ }
463+ if let tlsAllowInvalidCertificates = self . tlsAllowInvalidCertificates {
464+ options [ OptionName . tlsAllowInvalidCertificates] = . bool( tlsAllowInvalidCertificates)
465+ }
466+ if let tlsAllowInvalidHostnames = self . tlsAllowInvalidHostnames {
467+ options [ OptionName . tlsAllowInvalidHostnames] = . bool( tlsAllowInvalidHostnames)
468+ }
469+ if let tlsCAFile = self . tlsCAFile {
470+ options [ OptionName . tlsCAFile] = . string( tlsCAFile. description)
471+ }
472+ if let tlsCertificateKeyFile = self . tlsCertificateKeyFile {
473+ options [ OptionName . tlsCertificateKeyFile] = . string( tlsCertificateKeyFile. description)
474+ }
475+ if let tlsCertificateKeyFilePassword = self . tlsCertificateKeyFilePassword {
476+ options [ OptionName . tlsCertificateKeyFilePassword] = . string( tlsCertificateKeyFilePassword)
477+ }
478+ if let tlsDisableCertificateRevocationCheck = self . tlsDisableCertificateRevocationCheck {
479+ options [ OptionName . tlsDisableCertificateRevocationCheck] = . bool( tlsDisableCertificateRevocationCheck)
480+ }
481+ if let tlsDisableOCSPEndpointCheck = self . tlsDisableOCSPEndpointCheck {
482+ options [ OptionName . tlsDisableOCSPEndpointCheck] = . bool( tlsDisableOCSPEndpointCheck)
483+ }
484+ if let tlsInsecure = self . tlsInsecure {
485+ options [ OptionName . tlsInsecure] = . bool( tlsInsecure)
486+ }
487+
488+ return options
489+ }
490+
346491 /// Specifies the format this connection string is in.
347492 public var scheme : Scheme
348493
@@ -355,6 +500,47 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
355500
356501 /// Specifies the authentication credentials.
357502 public var credential : MongoCredential ?
503+
504+ /// Specifies whether or not to require TLS for connections to the server. By default this is set to false.
505+ ///
506+ /// - Note: Specifying any other "tls"-prefixed option will require TLS for connections to the server.
507+ public var tls : Bool ?
508+
509+ /// Specifies whether to bypass validation of the certificate presented by the mongod/mongos instance. By default
510+ /// this is set to false.
511+ public var tlsAllowInvalidCertificates : Bool ?
512+
513+ /// Specifies whether to disable hostname validation for the certificate presented by the mongod/mongos instance.
514+ /// By default this is set to false.
515+ public var tlsAllowInvalidHostnames : Bool ?
516+
517+ /// Specifies the location of a local .pem file that contains the root certificate chain from the Certificate
518+ /// Authority. This file is used to validate the certificate presented by the mongod/mongos instance.
519+ public var tlsCAFile : URL ?
520+
521+ /// Specifies the location of a local .pem file that contains either the client's TLS certificate or the client's
522+ /// TLS certificate and key. The client presents this file to the mongod/mongos instance.
523+ public var tlsCertificateKeyFile : URL ?
524+
525+ /// Specifies the password to de-crypt the `tlsCertificateKeyFile`.
526+ public var tlsCertificateKeyFilePassword : String ?
527+
528+ /// Specifies whether revocation checking (CRL / OCSP) should be disabled.
529+ /// On macOS, this setting has no effect.
530+ /// By default this is set to false.
531+ /// It is an error to specify both this option and `tlsDisableOCSPEndpointCheck`.
532+ public var tlsDisableCertificateRevocationCheck : Bool ?
533+
534+ /// Indicates if OCSP responder endpoints should not be requested when an OCSP response is not stapled.
535+ /// On macOS, this setting has no effect.
536+ /// By default this is set to false.
537+ public var tlsDisableOCSPEndpointCheck : Bool ?
538+
539+ /// When specified, TLS constraints will be relaxed as much as possible. Currently, setting this option to `true`
540+ /// is equivalent to setting `tlsAllowInvalidCertificates`, `tlsAllowInvalidHostnames`, and
541+ /// `tlsDisableCertificateRevocationCheck` to `true`.
542+ /// It is an error to specify both this option and any of the options enabled by it.
543+ public var tlsInsecure : Bool ?
358544}
359545
360546extension StringProtocol {
@@ -377,4 +563,29 @@ extension StringProtocol {
377563 }
378564 return try self . getPercentDecoded ( forKey: key)
379565 }
566+
567+ fileprivate func getBool( forKey key: String ) throws -> Bool {
568+ switch self {
569+ case " true " :
570+ return true
571+ case " false " :
572+ return false
573+ default :
574+ throw MongoError . InvalidArgumentError (
575+ message: " Value for \( key) in connection string must be true or false "
576+ )
577+ }
578+ }
579+ }
580+
581+ /// Helper extension to set a document field with a `MongoConnectionString.Name`.
582+ extension BSONDocument {
583+ fileprivate subscript( name: MongoConnectionString . OptionName ) -> BSON ? {
584+ get {
585+ self [ name. rawValue]
586+ }
587+ set {
588+ self [ name. rawValue] = newValue
589+ }
590+ }
380591}
0 commit comments