Parcourir la source

Add TLS to the NIO implementation (#406)

* Add SSL handlers, update EchoNIO

* Update Echo certificate and key

* Diagram of GRPCServer pipeline configuration stages

* Handle errors in the HTTPProtocolSwitcher

* Make TLS easier to configure for clients.
George Barnett il y a 6 ans
Parent
commit
5cdda76868

+ 9 - 0
Package.resolved

@@ -37,6 +37,15 @@
           "version": "1.0.0-convergence.1"
         }
       },
+      {
+        "package": "swift-nio-ssl",
+        "repositoryURL": "https://github.com/apple/swift-nio-ssl.git",
+        "state": {
+          "branch": null,
+          "revision": "5995ce5eac7b8e9fa65f4871c0173ffc54e26db7",
+          "version": "2.0.0-convergence.1"
+        }
+      },
       {
         "package": "swift-nio-zlib-support",
         "repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git",

+ 3 - 0
Package.swift

@@ -31,6 +31,8 @@ var packageDependencies: [Package.Dependency] = [
   .package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0-convergence.1"),
   // HTTP2 via SwiftNIO
   .package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.0.0-convergence.1"),
+  // TLS via SwiftNIO
+  .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.0.0-convergence.1"),
 ]
 
 var cGRPCDependencies: [Target.Dependency] = []
@@ -67,6 +69,7 @@ let package = Package(
               "NIOFoundationCompat",
               "NIOHTTP1",
               "NIOHTTP2",
+              "NIOSSL",
               "SwiftProtobuf"]),
     .target(name: "CgRPC",
             dependencies: cGRPCDependencies),

+ 15 - 15
Sources/Examples/Echo/ssl.crt

@@ -1,17 +1,17 @@
 -----BEGIN CERTIFICATE-----
-MIICqDCCAZACCQCqiXoqUJivWDANBgkqhkiG9w0BAQUFADAWMRQwEgYDVQQDEwtl
-eGFtcGxlLmNvbTAeFw0xNzA5MDIwMDI5NDVaFw0xODA5MDIwMDI5NDVaMBYxFDAS
-BgNVBAMTC2V4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
-AQEA1VsheX+pqTrOV23/BZlUselhrguBI1wt0IvY3hcEPddTzjzG33LDCw2DsY5m
-zfITDjar0DaOcqGNdgoMFOOnja47BZgqgZS5hdl5XzC1N7QVZ4KrdYeY6N8eaVI4
-sHreD0oUDcHtLjRY8dSI/UcyeWmJQFNrf/nQVYD1Z57WTJrTbk+L7svRAKK41+Gv
-h3Y5QPl07yBGLyMMTCO4mWeslFekSDnmknniUMq+7U8s7tCTi04U33h6UhBRm328
-RJLXiSbe6D5N2X5mf4MwXyqYKaYQRnloTJfRxWk33O/zhy688/cHxHU/H4YrjN2z
-IowoHMg52KUCuZgr7F48mq6F3QIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQB6GMt8
-31wrtZtFm9GSDmYKbTUTItzME42H67KnkcLoTiBDOEC1cKSIIlxZsVgTIVgF09Ko
-3HBbJ6JSDXbsgTZIzVcmHxEPsmxsNCEa2zmaSCZ57DE48iOekBi+Ts0oiSo2LjpB
-fWARUNXEDHCE4EKwVzDwO0/DujFFj7PeZSU1WWU0qQbTagglOGEYgLPJYfYNVw9F
-8CoZIdRJV3QH6XW21WS2/dRebEbTw3wDU3QJ4P7eRDmAoZGfR6Lvk0wKcZRdtwTf
-2HfM/m0AsUSB5bo9ywp2Tdhh3CGSNz//h19RrgzQpwaX2+ncSajkODHGzZAkGTRo
-mqj68/iiCGEfImUe
+MIICqDCCAZACCQDDdBhtO7mlFDANBgkqhkiG9w0BAQsFADAWMRQwEgYDVQQDDAtl
+eGFtcGxlLmNvbTAeFw0xOTAzMTkxNDI4MjlaFw0yMDAzMTgxNDI4MjlaMBYxFDAS
+BgNVBAMMC2V4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEArwgy42KIaHXf1lPLKrDbc/ih77gW55SkQtOibUAdeGyzLU331w1sAy40NJZV
+teIv9wBdFMnivXEpffw7NRvG0B8omma+I8tHynzllbp4tCYUZVMlV3Hq/Z4l4b67
+vTy3SGCx9I7W9sTEkE5D6jvrqdXTJY/HsNpol4X50WYlsoDZw/7vhDETaBFDa4TO
+MNtpBSechjhhkITVWGt0coIfyEE1CQWhZgHT4c3Ixum8Amu6CI+g90mzyrcciYV7
+wY5CwzLr5Z5HpQl5qmfwsmI5m+PCE6EJVPHb5MfSREZfWKq2dbfwndKjnhUl/jSF
+pfnQpqO738EhCaPCPO+3bKYsswIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBFH2rw
+J5oKCx8BfnYWER1UP2mPzjUFhvpB+hhN1ccAsXPPLl/ePRbobeO3qJhUZ/pjPJpy
+rGtXPc5zF6o3HqPhwQRWdB0Sow7tdiIJ+yTKo09080HdanFVZEULNhs6wZDKF0i4
+w5v0XOLPRiHgX2mGTECSa4V+mN3oZth9/TtRy5HW0TrFgeSoV/M9Lm23YlQFq94u
+ttdN613aJ9jCaYY8r6juuypOZUWReeJTPNAbZPyW+QL4CQTYb0AQIQnrPW4fvogk
+9jpHHeX/PoCXO/qxge0pTh8eu7mlR4gxlGiVzQvUF+rEbJ/7npWUeg7vrvxL4+EM
+OPVapoeGk6ZxnpVE
 -----END CERTIFICATE-----

+ 25 - 25
Sources/Examples/Echo/ssl.key

@@ -1,27 +1,27 @@
 -----BEGIN RSA PRIVATE KEY-----
-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
+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
 -----END RSA PRIVATE KEY-----

+ 50 - 13
Sources/Examples/EchoNIO/main.swift

@@ -17,9 +17,11 @@ import Commander
 import Dispatch
 import Foundation
 import NIO
+import NIOSSL
 import SwiftGRPCNIO
 
 // Common flags and options
+let sslFlag = Flag("ssl", description: "if true, use SSL for connections")
 func addressOption(_ address: String) -> Option<String> {
   return Option("address", default: address, description: "address of server")
 }
@@ -29,11 +31,40 @@ let messageOption = Option("message",
                            default: "Testing 1 2 3",
                            description: "message to send")
 
+func makeClientTLSConfiguration() throws -> TLSConfiguration {
+  let certificate = try NIOSSLCertificate(file: "ssl.crt", format: .pem)
+  // The certificate common name is "example.com", so skip hostname verification.
+  return .forClient(certificateVerification: .noHostnameVerification,
+                    trustRoots: .certificates([certificate]))
+}
+
+func makeServerTLSConfiguration() throws -> TLSConfiguration {
+  let certificate = try NIOSSLCertificate(file: "ssl.crt", format: .pem)
+  let key = try NIOSSLPrivateKey(file: "ssl.key", format: .pem)
+  return .forServer(certificateChain: [.certificate(certificate)],
+                    privateKey: .privateKey(key),
+                    trustRoots: .certificates([certificate]))
+}
+
+func makeClientTLS(enabled: Bool) throws -> GRPCClient.TLSMode {
+  guard enabled else {
+    return .none
+  }
+  return .custom(try NIOSSLContext(configuration: try makeClientTLSConfiguration()))
+}
+
+func makeServerTLS(enabled: Bool) throws -> GRPCServer.TLSMode {
+  guard enabled else {
+    return .none
+  }
+  return .custom(try NIOSSLContext(configuration: try makeServerTLSConfiguration()))
+}
+
 /// Create en `EchoClient` and wait for it to initialize. Returns nil if initialisation fails.
-func makeEchoClient(address: String, port: Int) -> Echo_EchoService_NIOClient? {
+func makeEchoClient(address: String, port: Int, ssl: Bool) -> Echo_EchoService_NIOClient? {
   let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
   do {
-    return try GRPCClient.start(host: address, port: port, eventLoopGroup: eventLoopGroup)
+    return try GRPCClient.start(host: address, port: port, eventLoopGroup: eventLoopGroup, tls: try makeClientTLS(enabled: ssl))
       .map { client in Echo_EchoService_NIOClient(client: client) }
       .wait()
   } catch {
@@ -44,17 +75,19 @@ func makeEchoClient(address: String, port: Int) -> Echo_EchoService_NIOClient? {
 
 Group {
   $0.command("serve",
+             sslFlag,
              addressOption("localhost"),
              portOption,
-             description: "Run an echo server.") { address, port in
+             description: "Run an echo server.") { ssl, address, port in
     let sem = DispatchSemaphore(value: 0)
     let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
 
-    print("starting insecure server")
+    print(ssl ? "starting secure server" : "starting insecure server")
     _ = try! GRPCServer.start(hostname: address,
                               port: port,
                               eventLoopGroup: eventLoopGroup,
-                              serviceProviders: [EchoProviderNIO()])
+                              serviceProviders: [EchoProviderNIO()],
+                              tls: makeServerTLS(enabled: ssl))
       .wait()
 
     // This blocks to keep the main thread from finishing while the server runs,
@@ -64,13 +97,14 @@ Group {
 
   $0.command(
     "get",
+    sslFlag,
     addressOption("localhost"),
     portOption,
     messageOption,
     description: "Perform a unary get()."
-  ) { address, port, message in
+  ) { ssl, address, port, message in
     print("calling get")
-    guard let echo = makeEchoClient(address: address, port: port) else { return }
+    guard let echo = makeEchoClient(address: address, port: port, ssl: ssl) else { return }
 
     var requestMessage = Echo_EchoRequest()
     requestMessage.text = message
@@ -96,13 +130,14 @@ Group {
 
   $0.command(
     "expand",
+    sslFlag,
     addressOption("localhost"),
     portOption,
     messageOption,
     description: "Perform a server-streaming expand()."
-  ) { address, port, message in
+  ) { ssl, address, port, message in
     print("calling expand")
-    guard let echo = makeEchoClient(address: address, port: port) else { return }
+    guard let echo = makeEchoClient(address: address, port: port, ssl: ssl) else { return }
 
     let requestMessage = Echo_EchoRequest.with { $0.text = message }
 
@@ -122,13 +157,14 @@ Group {
 
   $0.command(
     "collect",
+    sslFlag,
     addressOption("localhost"),
     portOption,
     messageOption,
     description: "Perform a client-streaming collect()."
-  ) { address, port, message in
+  ) { ssl, address, port, message in
     print("calling collect")
-    guard let echo = makeEchoClient(address: address, port: port) else { return }
+    guard let echo = makeEchoClient(address: address, port: port, ssl: ssl) else { return }
 
     let collect = echo.collect()
 
@@ -160,13 +196,14 @@ Group {
 
   $0.command(
     "update",
+    sslFlag,
     addressOption("localhost"),
     portOption,
     messageOption,
     description: "Perform a bidirectional-streaming update()."
-  ) { address, port, message in
+  ) { ssl, address, port, message in
     print("calling update")
-    guard let echo = makeEchoClient(address: address, port: port) else { return }
+    guard let echo = makeEchoClient(address: address, port: port, ssl: ssl) else { return }
 
     let update = echo.update { response in
       print("update received: \(response.text)")

+ 1 - 0
Sources/Examples/EchoNIO/ssl.crt

@@ -0,0 +1 @@
+../Echo/ssl.crt

+ 1 - 0
Sources/Examples/EchoNIO/ssl.key

@@ -0,0 +1 @@
+../Echo/ssl.key

+ 1 - 1
Sources/SwiftGRPCNIO/ClientCalls/BaseClientCall.swift

@@ -105,7 +105,7 @@ extension BaseClientCall {
   private func createStreamChannel() {
     self.client.channel.eventLoop.execute {
       self.client.multiplexer.createStreamChannel(promise: self.streamPromise) { (subchannel, streamID) -> EventLoopFuture<Void> in
-        subchannel.pipeline.addHandlers(HTTP2ToHTTP1ClientCodec(streamID: streamID, httpProtocol: .http),
+        subchannel.pipeline.addHandlers(HTTP2ToHTTP1ClientCodec(streamID: streamID, httpProtocol: self.client.httpProtocol),
                                         HTTP1ToRawGRPCClientCodec(),
                                         GRPCClientCodec<RequestMessage, ResponseMessage>(),
                                         self.clientChannelHandler)

+ 71 - 5
Sources/SwiftGRPCNIO/GRPCClient.swift

@@ -16,6 +16,7 @@
 import Foundation
 import NIO
 import NIOHTTP2
+import NIOSSL
 
 /// Underlying channel and HTTP/2 stream multiplexer.
 ///
@@ -24,34 +25,64 @@ open class GRPCClient {
   public static func start(
     host: String,
     port: Int,
-    eventLoopGroup: EventLoopGroup
-  ) -> EventLoopFuture<GRPCClient> {
+    eventLoopGroup: EventLoopGroup,
+    tls tlsMode: TLSMode = .none
+  ) throws -> EventLoopFuture<GRPCClient> {
+    // We need to capture the multiplexer from the channel initializer to store it after connection.
     let multiplexerPromise: EventLoopPromise<HTTP2StreamMultiplexer> = eventLoopGroup.next().makePromise()
 
     let bootstrap = ClientBootstrap(group: eventLoopGroup)
       // Enable SO_REUSEADDR.
       .channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
       .channelInitializer { channel in
-        let multiplexer = channel.configureHTTP2Pipeline(mode: .client)
+        let multiplexer = configureTLS(mode: tlsMode, channel: channel, host: host).flatMap {
+          channel.configureHTTP2Pipeline(mode: .client)
+        }
+
         multiplexer.cascade(to: multiplexerPromise)
         return multiplexer.map { _ in }
       }
 
     return bootstrap.connect(host: host, port: port)
       .and(multiplexerPromise.futureResult)
-      .map { channel, multiplexer in GRPCClient(channel: channel, multiplexer: multiplexer, host: host) }
+      .map { channel, multiplexer in GRPCClient(channel: channel, multiplexer: multiplexer, host: host, httpProtocol: tlsMode.httpProtocol) }
+  }
+
+  /// Configure an SSL handler on the channel, if one is required.
+  ///
+  /// - Parameters:
+  ///   - mode: TLS mode to use when creating the new handler.
+  ///   - channel: The channel on which to add the SSL handler.
+  ///   - host: The hostname of the server we're connecting to.
+  /// - Returns: A future which will be succeeded when the pipeline has been configured.
+  private static func configureTLS(mode tls: TLSMode, channel: Channel, host: String) -> EventLoopFuture<Void> {
+    let handlerAddedPromise: EventLoopPromise<Void> = channel.eventLoop.makePromise()
+
+    do {
+      guard let sslContext = try tls.makeSSLContext() else {
+        handlerAddedPromise.succeed(())
+        return handlerAddedPromise.futureResult
+      }
+      channel.pipeline.addHandler(try NIOSSLClientHandler(context: sslContext, serverHostname: host)).cascade(to: handlerAddedPromise)
+    } catch {
+      handlerAddedPromise.fail(error)
+    }
+
+    return handlerAddedPromise.futureResult
   }
 
   public let channel: Channel
   public let multiplexer: HTTP2StreamMultiplexer
   public let host: String
   public var defaultCallOptions: CallOptions
+  public let httpProtocol: HTTP2ToHTTP1ClientCodec.HTTPProtocol
 
-  init(channel: Channel, multiplexer: HTTP2StreamMultiplexer, host: String, defaultCallOptions: CallOptions = CallOptions()) {
+  init(channel: Channel, multiplexer: HTTP2StreamMultiplexer, host: String, httpProtocol: HTTP2ToHTTP1ClientCodec.HTTPProtocol, defaultCallOptions: CallOptions = CallOptions()) {
     self.channel = channel
     self.multiplexer = multiplexer
     self.host = host
     self.defaultCallOptions = defaultCallOptions
+    self.httpProtocol = httpProtocol
   }
 
   /// Fired when the client shuts down.
@@ -84,6 +115,41 @@ public protocol GRPCServiceClient {
   func path(forMethod method: String) -> String
 }
 
+extension GRPCClient {
+  public enum TLSMode {
+    case none
+    case anonymous
+    case custom(NIOSSLContext)
+
+    /// Returns an SSL context for the TLS mode.
+    ///
+    /// - Returns: An SSL context for the TLS mode, or `nil` if TLS is not being used.
+    public func makeSSLContext() throws -> NIOSSLContext? {
+      switch self {
+      case .none:
+        return nil
+
+      case .anonymous:
+        return try NIOSSLContext(configuration: .forClient())
+
+      case .custom(let context):
+        return context
+      }
+    }
+
+    /// Rethrns the HTTP protocol for the TLS mode.
+    public var httpProtocol: HTTP2ToHTTP1ClientCodec.HTTPProtocol {
+      switch self {
+      case .none:
+        return .http
+
+      case .anonymous, .custom:
+        return .https
+      }
+    }
+  }
+}
+
 extension GRPCServiceClient {
   public func path(forMethod method: String) -> String {
     return "/\(service)/\(method)"

+ 119 - 4
Sources/SwiftGRPCNIO/GRPCServer.swift

@@ -2,8 +2,79 @@ import Foundation
 import NIO
 import NIOHTTP1
 import NIOHTTP2
+import NIOSSL
 
 /// Wrapper object to manage the lifecycle of a gRPC server.
+///
+/// The pipeline is configured in three stages detailed below. Note: handlers marked with
+/// a '*' are responsible for handling errors.
+///
+/// 1. Initial stage, prior to HTTP protocol detection.
+///
+///                           ┌───────────────────────────┐
+///                           │   HTTPProtocolSwitcher*   │
+///                           └─▲───────────────────────┬─┘
+///                   ByteBuffer│                       │ByteBuffer
+///                           ┌─┴───────────────────────▼─┐
+///                           │       NIOSSLHandler       │
+///                           └─▲───────────────────────┬─┘
+///                   ByteBuffer│                       │ByteBuffer
+///                             │                       ▼
+///
+///    The NIOSSLHandler is optional and depends on how the framework user has configured
+///    their server. The HTTPProtocolSwitched detects which HTTP version is being used and
+///    configures the pipeline accordingly.
+///
+/// 2. HTTP version detected. "HTTP Handlers" depends on the HTTP version determined by
+///    HTTPProtocolSwitcher. All of these handlers are provided by NIO except for the
+///    WebCORSHandler which is used for HTTP/1.
+///
+///                           ┌───────────────────────────┐
+///                           │    GRPCChannelHandler*    │
+///                           └─▲───────────────────────┬─┘
+///     RawGRPCServerRequestPart│                       │RawGRPCServerResponsePart
+///                           ┌─┴───────────────────────▼─┐
+///                           │ HTTP1ToRawGRPCServerCodec │
+///                           └─▲───────────────────────┬─┘
+///        HTTPServerRequestPart│                       │HTTPServerResponsePart
+///                           ┌─┴───────────────────────▼─┐
+///                           │       HTTP Handlers       │
+///                           └─▲───────────────────────┬─┘
+///                   ByteBuffer│                       │ByteBuffer
+///                           ┌─┴───────────────────────▼─┐
+///                           │       NIOSSLHandler       │
+///                           └─▲───────────────────────┬─┘
+///                   ByteBuffer│                       │ByteBuffer
+///                             │                       ▼
+///
+///    The GPRCChannelHandler resolves the request head and configures the rest of the pipeline
+///    based on the RPC call being made.
+///
+/// 3. The call has been resolved and is a function that this server can handle. Responses are
+///    written into `BaseCallHandler` by a user-implemented `CallHandlerProvider`.
+///
+///                           ┌───────────────────────────┐
+///                           │     BaseCallHandler*      │
+///                           └─▲───────────────────────┬─┘
+///    GRPCServerRequestPart<T1>│                       │GRPCServerResponsePart<T2>
+///                           ┌─┴───────────────────────▼─┐
+///                           │      GRPCServerCodec      │
+///                           └─▲───────────────────────┬─┘
+///     RawGRPCServerRequestPart│                       │RawGRPCServerResponsePart
+///                           ┌─┴───────────────────────▼─┐
+///                           │ HTTP1ToRawGRPCServerCodec │
+///                           └─▲───────────────────────┬─┘
+///        HTTPServerRequestPart│                       │HTTPServerResponsePart
+///                           ┌─┴───────────────────────▼─┐
+///                           │       HTTP Handlers       │
+///                           └─▲───────────────────────┬─┘
+///                   ByteBuffer│                       │ByteBuffer
+///                           ┌─┴───────────────────────▼─┐
+///                           │       NIOSSLHandler       │
+///                           └─▲───────────────────────┬─┘
+///                   ByteBuffer│                       │ByteBuffer
+///                             │                       ▼
+///
 public final class GRPCServer {
   /// Starts up a server that serves the given providers.
   ///
@@ -13,8 +84,9 @@ public final class GRPCServer {
     port: Int,
     eventLoopGroup: EventLoopGroup,
     serviceProviders: [CallHandlerProvider],
-    errorDelegate: ServerErrorDelegate? = LoggingServerErrorDelegate()
-  ) -> EventLoopFuture<GRPCServer> {
+    errorDelegate: ServerErrorDelegate? = LoggingServerErrorDelegate(),
+    tls tlsMode: TLSMode = .none
+  ) throws -> EventLoopFuture<GRPCServer> {
     let servicesByName = Dictionary(uniqueKeysWithValues: serviceProviders.map { ($0.serviceName, $0) })
     let bootstrap = ServerBootstrap(group: eventLoopGroup)
       // Specify a backlog to avoid overloading the server.
@@ -23,10 +95,14 @@ public final class GRPCServer {
       .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
       // Set the handlers that are applied to the accepted Channels
       .childChannelInitializer { channel in
-        channel.pipeline.addHandler(HTTPProtocolSwitcher { channel in
+        let protocolSwitcherHandler = HTTPProtocolSwitcher(errorDelegate: errorDelegate) { channel -> EventLoopFuture<Void> in
           channel.pipeline.addHandlers(HTTP1ToRawGRPCServerCodec(),
                                        GRPCChannelHandler(servicesByName: servicesByName, errorDelegate: errorDelegate))
-        })
+        }
+
+        return configureTLS(mode: tlsMode, channel: channel).flatMap {
+          channel.pipeline.addHandler(protocolSwitcherHandler)
+        }
       }
 
       // Enable TCP_NODELAY and SO_REUSEADDR for the accepted Channels
@@ -37,6 +113,28 @@ public final class GRPCServer {
       .map { GRPCServer(channel: $0, errorDelegate: errorDelegate) }
   }
 
+  /// Configure an SSL handler on the channel, if one is provided.
+  ///
+  /// - Parameters:
+  ///   - mode: TLS mode to run the server in.
+  ///   - channel: The channel on which to add the SSL handler.
+  /// - Returns: A future which will be succeeded when the pipeline has been configured.
+  private static func configureTLS(mode: TLSMode, channel: Channel) -> EventLoopFuture<Void> {
+    guard let sslContext = mode.sslContext else {
+      return channel.eventLoop.makeSucceededFuture(())
+    }
+
+    let handlerAddedPromise: EventLoopPromise<Void> = channel.eventLoop.makePromise()
+
+    do {
+      channel.pipeline.addHandler(try NIOSSLServerHandler(context: sslContext)).cascade(to: handlerAddedPromise)
+    } catch {
+      handlerAddedPromise.fail(error)
+    }
+
+    return handlerAddedPromise.futureResult
+  }
+
   private let channel: Channel
   private var errorDelegate: ServerErrorDelegate?
 
@@ -62,3 +160,20 @@ public final class GRPCServer {
     return channel.close(mode: .all)
   }
 }
+
+extension GRPCServer {
+  public enum TLSMode {
+    case none
+    case custom(NIOSSLContext)
+
+    var sslContext: NIOSSLContext? {
+      switch self {
+      case .none:
+        return nil
+
+      case .custom(let context):
+        return context
+      }
+    }
+  }
+}

+ 26 - 6
Sources/SwiftGRPCNIO/HTTPProtocolSwitcher.swift

@@ -7,6 +7,7 @@ import NIOHTTP2
 /// the incoming request is HTTP 1 or 2.
 public class HTTPProtocolSwitcher {
   private let handlersInitializer: ((Channel) -> EventLoopFuture<Void>)
+  private let errorDelegate: ServerErrorDelegate?
 
   // We could receive additional data after the initial data and before configuring
   // the pipeline; buffer it and fire it down the pipeline once it is configured.
@@ -19,7 +20,8 @@ public class HTTPProtocolSwitcher {
   private var state: State = .notConfigured
   private var bufferedData: [NIOAny] = []
 
-  public init(handlersInitializer: (@escaping (Channel) -> EventLoopFuture<Void>)) {
+  public init(errorDelegate: ServerErrorDelegate?, handlersInitializer: (@escaping (Channel) -> EventLoopFuture<Void>)) {
+    self.errorDelegate = errorDelegate
     self.handlersInitializer = handlersInitializer
   }
 }
@@ -28,7 +30,6 @@ extension HTTPProtocolSwitcher: ChannelInboundHandler, RemovableChannelHandler {
   public typealias InboundIn = ByteBuffer
   public typealias InboundOut = ByteBuffer
 
-
   enum HTTPProtocolVersionError: Error {
     /// Raised when it wasn't possible to detect HTTP Protocol version.
     case invalidHTTPProtocolVersion
@@ -65,11 +66,18 @@ extension HTTPProtocolSwitcher: ChannelInboundHandler, RemovableChannelHandler {
           return
       }
 
-      // Once configured remove ourself from the pipeline.
+      // Once configured remove ourself from the pipeline, or handle the error.
       let pipelineConfigured: EventLoopPromise<Void> = context.eventLoop.makePromise()
-      pipelineConfigured.futureResult.whenSuccess {
-        self.state = .configured
-        context.pipeline.removeHandler(context: context, promise: nil)
+      pipelineConfigured.futureResult.whenComplete { result in
+        switch result {
+        case .success:
+          self.state = .configuring
+          context.pipeline.removeHandler(context: context, promise: nil)
+
+        case .failure(let error):
+          self.state = .notConfigured
+          self.errorCaught(context: context, error: error)
+        }
       }
 
       // Depending on whether it is HTTP1 or HTTP2, create different processing pipelines.
@@ -110,6 +118,18 @@ extension HTTPProtocolSwitcher: ChannelInboundHandler, RemovableChannelHandler {
     context.leavePipeline(removalToken: removalToken)
   }
 
+  public func errorCaught(context: ChannelHandlerContext, error: Error) {
+    switch self.state {
+    case .notConfigured, .configuring:
+      errorDelegate?.observe(error)
+      context.close(mode: .all, promise: nil)
+
+    case .configured:
+      // If we're configured we will rely on a handler further down the pipeline.
+      context.fireErrorCaught(error)
+    }
+  }
+
   /// Peek into the first line of the packet to check which HTTP version is being used.
   private func protocolVersion(_ preamble: String) -> HTTPProtocolVersion? {
     let range = NSRange(location: 0, length: preamble.utf16.count)