Skip to content

Commit 8bc1856

Browse files
author
Andrew Theis
committed
Add support for SSL connections to PostgreSQL servers (no unit tests yet)
1 parent dfc03db commit 8bc1856

9 files changed

+86
-16
lines changed

Sources/PostgreSQL/Connection/PostgreSQLConnection.swift

+18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Crypto
22
import NIO
3+
import NIOOpenSSL
34

45
/// A PostgreSQL frontend client.
56
public final class PostgreSQLConnection: DatabaseConnection, BasicWorker {
@@ -127,6 +128,23 @@ public final class PostgreSQLConnection: DatabaseConnection, BasicWorker {
127128
/// return the newly enqueued work's future result
128129
return new
129130
}
131+
132+
/// Trys opening a SLL connection
133+
internal func attemptSSLConnection(using tlsConfiguration: TLSConfiguration) -> Future<Void> {
134+
return queue.enqueue([.sslSupportRequest(PostgreSQLSSLSupportRequest())]) { message in
135+
guard case .sslSupportResponse(let response) = message else {
136+
throw PostgreSQLError(identifier: "SSL support check", reason: "Unsupported message encountered during SSL support check: \(message).", source: .capture())
137+
}
138+
guard response == .supported else {
139+
throw PostgreSQLError(identifier: "SSL support check", reason: "tlsConfiguration given in PostgresSQLConfiguration, but SSL connection not supported by PostgreSQL server", source: .capture())
140+
}
141+
return true
142+
}.flatMap {
143+
let sslContext = try SSLContext(configuration: tlsConfiguration)
144+
let handler = try OpenSSLClientHandler(context: sslContext)
145+
return self.channel.pipeline.add(handler: handler, first: true)
146+
}
147+
}
130148

131149
/// Authenticates the `PostgreSQLClient` using a username with no password.
132150
public func authenticate(username: String, database: String? = nil, password: String? = nil) -> Future<Void> {

Sources/PostgreSQL/Database/PostgreSQLDatabase.swift

+5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ public final class PostgreSQLDatabase: Database, LogSupporting {
2121
return Future.flatMap(on: worker) {
2222
return try PostgreSQLConnection.connect(hostname: config.hostname, port: config.port, on: worker) { error in
2323
print("[PostgreSQL] \(error)")
24+
}.flatMap(to: PostgreSQLConnection.self) { client in
25+
if let tlsConfiguration = config.tlsConfiguration {
26+
return client.attemptSSLConnection(using: tlsConfiguration).transform(to: client)
27+
}
28+
return Future.map(on: worker) { client }
2429
}.flatMap(to: PostgreSQLConnection.self) { client in
2530
return client.authenticate(
2631
username: config.username,

Sources/PostgreSQL/Database/PostgreSQLDatabaseConfig.swift

+24-15
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import Foundation
2+
import NIOOpenSSL
3+
24
/// Config options for a `PostgreSQLConnection`
35
public struct PostgreSQLDatabaseConfig {
46
/// Creates a `PostgreSQLDatabaseConfig` with default settings.
@@ -22,38 +24,45 @@ public struct PostgreSQLDatabaseConfig {
2224
/// Optional password to use for authentication.
2325
public let password: String?
2426

27+
/// Optional TLSConfiguration. Set this if your PostgreSQL server requires an SSL configuration
28+
public let tlsConfiguration: TLSConfiguration?
29+
2530
/// Creates a new `PostgreSQLDatabaseConfig`.
26-
public init(hostname: String, port: Int = 5432, username: String, database: String? = nil, password: String? = nil) {
31+
public init(hostname: String, port: Int = 5432, username: String, database: String? = nil, password: String? = nil, tlsConfiguration: TLSConfiguration? = nil) {
2732
self.hostname = hostname
2833
self.port = port
2934
self.username = username
3035
self.database = database
3136
self.password = password
37+
self.tlsConfiguration = tlsConfiguration
3238
}
3339

3440
/// Creates a `PostgreSQLDatabaseConfig` frome a connection string.
35-
public init(url: String) throws {
36-
guard let urL = URL(string: url),
37-
let hostname = urL.host,
38-
let port = urL.port,
39-
let username = urL.user,
40-
let database = URL(string: url)?.path,
41-
database.count > 0
42-
else {
43-
throw PostgreSQLError(identifier: "Bad Connection String",
44-
reason: "Host could not be parsed",
45-
possibleCauses: ["Foundation URL is unable to parse the provided connection string"],
46-
suggestedFixes: ["Check the connection string being passed"],
47-
source: .capture())
41+
public init(url urlString: String, tlsConfiguration: TLSConfiguration? = nil) throws {
42+
guard let url = URL(string: urlString),
43+
let hostname = url.host,
44+
let port = url.port,
45+
let username = url.user,
46+
url.path.count > 0
47+
else {
48+
throw PostgreSQLError(
49+
identifier: "Bad Connection String",
50+
reason: "Host could not be parsed",
51+
possibleCauses: ["Foundation URL is unable to parse the provided connection string"],
52+
suggestedFixes: ["Check the connection string being passed"],
53+
source: .capture()
54+
)
4855
}
4956
self.hostname = hostname
5057
self.port = port
5158
self.username = username
59+
let database = url.path
5260
if database.hasPrefix("/") {
5361
self.database = database.dropFirst().description
5462
} else {
5563
self.database = database
5664
}
57-
self.password = urL.password
65+
self.password = url.password
66+
self.tlsConfiguration = tlsConfiguration
5867
}
5968
}

Sources/PostgreSQL/Message+Parse/PostgreSQLMessageDecoder.swift

+10-1
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,17 @@ final class PostgreSQLMessageDecoder: ByteToMessageDecoder {
2626
return .needMoreData
2727
}
2828

29-
//// peek at messageSize
29+
/// peek at messageSize
3030
guard let messageSize: Int32 = buffer.peekInteger(skipping: MemoryLayout<Byte>.size) else {
31+
// Response from a PostgreSQLSSLSupportRequest will only return a single byte, so we need to handle that case
32+
if (messageType == .S || messageType == .N), let data = buffer.readSlice(length: MemoryLayout<Byte>.size) {
33+
let decoder = _PostgreSQLMessageDecoder(data: data)
34+
let message = try PostgreSQLMessage.sslSupportResponse(decoder.decode())
35+
ctx.fireChannelRead(wrapInboundOut(message))
36+
VERBOSE(" [message=\(message)]")
37+
return .continue
38+
}
39+
3140
VERBOSE(" [needMoreData: messageSize=nil]")
3241
return .needMoreData
3342
}

Sources/PostgreSQL/Message+Serialize/PostgreSQLMessageEncoder.swift

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ final class PostgreSQLMessageEncoder: MessageToByteEncoder {
1919
let encoder = _PostgreSQLMessageEncoder()
2020
let identifier: Byte?
2121
switch message {
22+
case .sslSupportRequest(let request):
23+
identifier = nil
24+
try request.encode(to: encoder)
2225
case .startupMessage(let message):
2326
identifier = nil
2427
try message.encode(to: encoder)

Sources/PostgreSQL/Message/PostgreSQLMessage.swift

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import Bits
22

33
/// A frontend or backend PostgreSQL message.
44
enum PostgreSQLMessage {
5+
/// Asks the server if it supports SSL.
6+
case sslSupportRequest(PostgreSQLSSLSupportRequest)
7+
/// Response after sending an sslSupportRequest message.
8+
case sslSupportResponse(PostgreSQLSSLSupportResponse)
9+
/// Startup message
510
case startupMessage(PostgreSQLStartupMessage)
611
/// Identifies the message as an error.
712
case error(PostgreSQLDiagnosticResponse)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/// A message asking the PostgreSQL server if SSL is supported
2+
/// For more info, see https://www.postgresql.org/docs/10/static/protocol-flow.html#id-1.10.5.7.11
3+
struct PostgreSQLSSLSupportRequest: Encodable {
4+
/// The SSL request code. The value is chosen to contain 1234 in the most significant 16 bits,
5+
/// and 5679 in the least significant 16 bits.
6+
let code: Int32 = 80877103
7+
8+
/// See Encodable.encode
9+
func encode(to encoder: Encoder) throws {
10+
var single = encoder.singleValueContainer()
11+
try single.encode(code)
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Core
2+
3+
/// Response given after sending a PostgreSQLSSLSupportRequest
4+
/// For more info, see https://www.postgresql.org/docs/10/static/protocol-flow.html#id-1.10.5.7.11
5+
enum PostgreSQLSSLSupportResponse: UInt8, Decodable {
6+
case supported = 0x53
7+
case notSupported = 0x4E
8+
}

0 commit comments

Comments
 (0)