HTTPProtocolSwitcher.swift 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. import Foundation
  2. import NIO
  3. import NIOHTTP1
  4. import NIOHTTP2
  5. /// Channel handler that creates different processing pipelines depending on whether
  6. /// the incoming request is HTTP 1 or 2.
  7. public class HTTPProtocolSwitcher {
  8. private let handlersInitializer: ((Channel) -> EventLoopFuture<Void>)
  9. public init(handlersInitializer: (@escaping (Channel) -> EventLoopFuture<Void>)) {
  10. self.handlersInitializer = handlersInitializer
  11. }
  12. }
  13. extension HTTPProtocolSwitcher: ChannelInboundHandler {
  14. public typealias InboundIn = ByteBuffer
  15. public typealias InboundOut = ByteBuffer
  16. enum HTTPProtocolVersionError: Error {
  17. /// Raised when it wasn't possible to detect HTTP Protocol version.
  18. case invalidHTTPProtocolVersion
  19. var localizedDescription: String {
  20. switch self {
  21. case .invalidHTTPProtocolVersion:
  22. return "Could not identify HTTP Protocol Version"
  23. }
  24. }
  25. }
  26. /// HTTP Protocol Version type
  27. enum HTTPProtocolVersion {
  28. case http1
  29. case http2
  30. }
  31. public func channelRead(ctx: ChannelHandlerContext, data: NIOAny) {
  32. // Detect the HTTP protocol version for the incoming request, or error out if it
  33. // couldn't be detected.
  34. var inBuffer = unwrapInboundIn(data)
  35. guard let initialData = inBuffer.readString(length: inBuffer.readableBytes),
  36. let preamble = initialData.split(separator: "\r\n",
  37. maxSplits: 1,
  38. omittingEmptySubsequences: true).first,
  39. let version = protocolVersion(String(preamble)) else {
  40. ctx.fireErrorCaught(HTTPProtocolVersionError.invalidHTTPProtocolVersion)
  41. return
  42. }
  43. // Depending on whether it is HTTP1 or HTTP2, created different processing pipelines.
  44. // Inbound handlers in handlersInitializer should expect HTTPServerRequestPart objects
  45. // and outbound handlers should return HTTPServerResponsePart objects.
  46. switch version {
  47. case .http1:
  48. // Upgrade connections are not handled since gRPC connections already arrive in HTTP2,
  49. // while gRPC-Web does not support HTTP2 at all, so there are no compelling use cases
  50. // to support this.
  51. _ = ctx.pipeline.configureHTTPServerPipeline(withErrorHandling: true)
  52. .then { ctx.pipeline.add(handler: WebCORSHandler()) }
  53. .then { (Void) -> EventLoopFuture<Void> in self.handlersInitializer(ctx.channel) }
  54. case .http2:
  55. _ = ctx.pipeline.add(handler: HTTP2Parser(mode: .server))
  56. .then { () -> EventLoopFuture<Void> in
  57. let multiplexer = HTTP2StreamMultiplexer { (channel, streamID) -> EventLoopFuture<Void> in
  58. return channel.pipeline.add(handler: HTTP2ToHTTP1ServerCodec(streamID: streamID))
  59. .then { (Void) -> EventLoopFuture<Void> in self.handlersInitializer(channel) }
  60. }
  61. return ctx.pipeline.add(handler: multiplexer)
  62. }
  63. }
  64. ctx.fireChannelRead(data)
  65. _ = ctx.pipeline.remove(ctx: ctx)
  66. }
  67. /// Peek into the first line of the packet to check which HTTP version is being used.
  68. private func protocolVersion(_ preamble: String) -> HTTPProtocolVersion? {
  69. let range = NSRange(location: 0, length: preamble.utf16.count)
  70. let regex = try! NSRegularExpression(pattern: "^.*HTTP/(\\d)\\.\\d$")
  71. let result = regex.firstMatch(in: preamble, options: [], range: range)!
  72. let versionRange = result.range(at: 1)
  73. #if swift(>=5.0)
  74. let start = String.Index(utf16Offset: versionRange.location, in: preamble)
  75. let end = String.Index(utf16Offset: versionRange.location + versionRange.length, in: preamble)
  76. #else
  77. let start = String.UTF16View.Index(encodedOffset: versionRange.location)
  78. let end = String.UTF16View.Index(encodedOffset: versionRange.location + versionRange.length)
  79. #endif
  80. switch String(preamble.utf16[start..<end])! {
  81. case "1":
  82. return .http1
  83. case "2":
  84. return .http2
  85. default:
  86. return nil
  87. }
  88. }
  89. }